Add render reference asset manifest contract

This commit is contained in:
2026-05-15 14:08:26 +02:00
parent 03e5ae00cc
commit 305d75177a
14 changed files with 1405 additions and 20 deletions

View File

@@ -13,9 +13,13 @@ Generated from `markitect_tool.__all__`.
- `LOCAL_INDEX_SCHEMA_VERSION` - object. str(object='') -> str - `LOCAL_INDEX_SCHEMA_VERSION` - object. str(object='') -> str
- `MAX_FUNCTION_PIPELINE_DEPTH` - object. int([x]) -> integer - `MAX_FUNCTION_PIPELINE_DEPTH` - object. int([x]) -> integer
- `NORMALIZED_SOURCE_SCHEMA_VERSION` - object. str(object='') -> str - `NORMALIZED_SOURCE_SCHEMA_VERSION` - object. str(object='') -> str
- `RENDER_ASSET_COPY_POLICIES` - object. set() -> new empty set object
- `RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP` - object. str(object='') -> str - `RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP` - object. str(object='') -> str
- `RENDER_EXPORT_ADAPTER_KIND` - object. str(object='') -> str - `RENDER_EXPORT_ADAPTER_KIND` - object. str(object='') -> str
- `RENDER_EXPORT_SCHEMA_VERSION` - object. str(object='') -> str - `RENDER_EXPORT_SCHEMA_VERSION` - object. str(object='') -> str
- `RENDER_REFERENCE_MANIFEST_KIND` - object. str(object='') -> str
- `RENDER_REFERENCE_SCHEMA_VERSION` - object. str(object='') -> str
- `RENDER_UNIT_KINDS` - object. set() -> new empty set object
- `SOURCE_ADAPTER_ENTRY_POINT_GROUP` - object. str(object='') -> str - `SOURCE_ADAPTER_ENTRY_POINT_GROUP` - object. str(object='') -> str
## `markitect_tool.backend.engine` ## `markitect_tool.backend.engine`
@@ -317,17 +321,31 @@ Generated from `markitect_tool.__all__`.
- `FakeRenderExportAdapter() -> 'None'` - class. Deterministic no-op renderer used for contract tests. - `FakeRenderExportAdapter() -> 'None'` - class. Deterministic no-op renderer used for contract tests.
- `RenderArtifact(artifact_id: 'str', role: 'str', media_type: 'str', content: 'str | None' = None, path: 'str | None' = None, uri: 'str | None' = None, digest: 'str | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Metadata for a rendered or exported artifact. - `RenderArtifact(artifact_id: 'str', role: 'str', media_type: 'str', content: 'str | None' = None, path: 'str | None' = None, uri: 'str | None' = None, digest: 'str | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Metadata for a rendered or exported artifact.
- `RenderAsset(asset_id: 'str' = '', source_uri: 'str | None' = None, source_path: 'str | None' = None, name: 'str | None' = None, media_type: 'str | None' = None, extension: 'str | None' = None, digest: 'str | None' = None, role: 'str' = 'asset', copy_policy: 'str' = 'copy', output_reference: 'str | None' = None, provenance: 'list[RenderAssetProvenance]' = <factory>, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Static asset descriptor with declared renderer copy behavior.
- `RenderAssetManifest(manifest_id: 'str' = '', assets: 'list[RenderAsset]' = <factory>, source_path: 'str | None' = None, source_digest: 'str | None' = None, schema_version: 'str' = 'markitect.render.reference.v1', metadata: 'dict[str, Any]' = <factory>) -> None` - class. Deterministic list of static assets requested by renderer source.
- `RenderAssetProvenance(source_uri: 'str | None' = None, source_path: 'str | None' = None, source_href: 'str | None' = None, package_path: 'str | None' = None, attachment_id: 'str | None' = None, source_adapter_id: 'str | None' = None, source_span: 'RenderSourceSpan | None' = None, digest: 'str | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Provenance for an asset manifest entry.
- `RenderCrossReference(target_unit_id: 'str', reference_id: 'str' = '', source_unit_id: 'str | None' = None, source_span: 'RenderSourceSpan | None' = None, label: 'str | None' = None, requested_style: 'str' = 'numbered', fallback_text: 'str | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Requested cross-reference link before renderer-specific numbering exists.
- `RenderExportAdapter(*args, **kwargs)` - class. Render/export adapter protocol. - `RenderExportAdapter(*args, **kwargs)` - class. Render/export adapter protocol.
- `RenderExportAdapterDescriptor(id: 'str', version: 'str', name: 'str', operations: 'list[str]', input_contracts: 'list[str]', output_profiles: 'list[str]', artifact_media_types: 'list[str]', factory: 'RenderExportAdapterFactory', summary: 'str | None' = None, option_schema: 'dict[str, Any]' = <factory>, optional_dependencies: 'list[OptionalDependency]' = <factory>, safety: 'dict[str, Any]' = <factory>, quality_profile: 'dict[str, Any]' = <factory>, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Inspectable descriptor for one render/export adapter. - `RenderExportAdapterDescriptor(id: 'str', version: 'str', name: 'str', operations: 'list[str]', input_contracts: 'list[str]', output_profiles: 'list[str]', artifact_media_types: 'list[str]', factory: 'RenderExportAdapterFactory', summary: 'str | None' = None, option_schema: 'dict[str, Any]' = <factory>, optional_dependencies: 'list[OptionalDependency]' = <factory>, safety: 'dict[str, Any]' = <factory>, quality_profile: 'dict[str, Any]' = <factory>, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Inspectable descriptor for one render/export adapter.
- `RenderExportAdapterError` - class. Raised when render/export adapter contracts are invalid. - `RenderExportAdapterError` - class. Raised when render/export adapter contracts are invalid.
- `RenderExportAdapterRegistry(descriptors: 'Iterable[RenderExportAdapterDescriptor] | None' = None) -> 'None'` - class. Registry of render/export adapter descriptors. - `RenderExportAdapterRegistry(descriptors: 'Iterable[RenderExportAdapterDescriptor] | None' = None) -> 'None'` - class. Registry of render/export adapter descriptors.
- `RenderExportRequest(source: 'str', operation: 'str' = 'render-artifact', profile: 'str' = 'plain', source_path: 'str | None' = None, options: 'dict[str, Any]' = <factory>, policy: 'dict[str, Any]' = <factory>, schema_version: 'str' = 'markitect.render.export.v1', metadata: 'dict[str, Any]' = <factory>) -> None` - class. Service-free render/export request. - `RenderExportRequest(source: 'str', operation: 'str' = 'render-artifact', profile: 'str' = 'plain', source_path: 'str | None' = None, options: 'dict[str, Any]' = <factory>, policy: 'dict[str, Any]' = <factory>, render_manifest: 'RenderReferenceManifest | dict[str, Any] | None' = None, schema_version: 'str' = 'markitect.render.export.v1', metadata: 'dict[str, Any]' = <factory>) -> None` - class. Service-free render/export request.
- `RenderExportResult(adapter: 'dict[str, Any]', operation: 'str', profile: 'str', artifacts: 'list[RenderArtifact]' = <factory>, exported_source: 'str | None' = None, diagnostics: 'list[Diagnostic]' = <factory>, provenance: 'list[RenderProvenance]' = <factory>, schema_version: 'str' = 'markitect.render.export.v1', metadata: 'dict[str, Any]' = <factory>) -> None` - class. Result of a render/export adapter operation. - `RenderExportResult(adapter: 'dict[str, Any]', operation: 'str', profile: 'str', artifacts: 'list[RenderArtifact]' = <factory>, exported_source: 'str | None' = None, diagnostics: 'list[Diagnostic]' = <factory>, provenance: 'list[RenderProvenance]' = <factory>, schema_version: 'str' = 'markitect.render.export.v1', metadata: 'dict[str, Any]' = <factory>) -> None` - class. Result of a render/export adapter operation.
- `RenderProvenance(operation: 'str', adapter_id: 'str', profile: 'str', source_path: 'str | None' = None, source_digest: 'str | None' = None, artifact_id: 'str | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Source-to-render provenance envelope. - `RenderProvenance(operation: 'str', adapter_id: 'str', profile: 'str', source_path: 'str | None' = None, source_digest: 'str | None' = None, artifact_id: 'str | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Source-to-render provenance envelope.
- `RenderReferenceError` - class. Raised when render reference or asset manifest contracts are invalid.
- `RenderReferenceManifest(manifest_id: 'str' = '', units: 'list[RenderUnitReference]' = <factory>, cross_references: 'list[RenderCrossReference]' = <factory>, toc: 'list[RenderTocEntry]' = <factory>, asset_manifest: 'RenderAssetManifest' = <factory>, source_maps: 'list[RenderSourceMap]' = <factory>, source_path: 'str | None' = None, source_digest: 'str | None' = None, schema_version: 'str' = 'markitect.render.reference.v1', metadata: 'dict[str, Any]' = <factory>) -> None` - class. Passive render structure, cross-reference, asset, and provenance manifest.
- `RenderSourceMap(map_id: 'str' = '', source: 'RenderSourceSpan | None' = None, source_unit_id: 'str | None' = None, generated_by: 'str | None' = None, function_run_id: 'str | None' = None, render_unit_id: 'str | None' = None, artifact_id: 'str | None' = None, artifact_ref: 'str | None' = None, role: 'str | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Trace from Markitect source/generated output to renderer units or artifacts.
- `RenderSourceSpan(source_path: 'str | None' = None, line_start: 'int | None' = None, line_end: 'int | None' = None, selector: 'str | None' = None, source_unit_id: 'str | None' = None, content_hash: 'str | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Source location for a render unit, asset, or source-map edge.
- `RenderTocEntry(unit_id: 'str', title: 'str', level: 'int', entry_id: 'str' = '', parent_id: 'str | None' = None, order: 'int | None' = None, source_span: 'RenderSourceSpan | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Table-of-contents planning entry before final renderer links exist.
- `RenderUnitReference(unit_id: 'str' = '', kind: 'str' = 'custom', label: 'str | None' = None, title: 'str | None' = None, caption: 'str | None' = None, source_path: 'str | None' = None, anchor: 'str | None' = None, source_span: 'RenderSourceSpan | None' = None, content_hash: 'str | None' = None, ordinal_hint: 'int | None' = None, numbering: 'dict[str, Any]' = <factory>, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Stable identity for one renderable unit before final renderer numbering.
- `default_render_export_adapter_registry() -> 'RenderExportAdapterRegistry'` - function. Return the built-in render/export adapter registry. - `default_render_export_adapter_registry() -> 'RenderExportAdapterRegistry'` - function. Return the built-in render/export adapter registry.
- `discover_render_export_adapters() -> 'list[RenderExportAdapterDescriptor]'` - function. Discover package-provided render/export adapter descriptors. - `discover_render_export_adapters() -> 'list[RenderExportAdapterDescriptor]'` - function. Discover package-provided render/export adapter descriptors.
- `render_asset_id(source: 'str', *, digest: 'str | None' = None, role: 'str | None' = None, output_reference: 'str | None' = None) -> 'str'` - function. Return a deterministic asset id without copying or reading the asset.
- `render_capability_diagnostics(descriptor: 'RenderExportAdapterDescriptor', request: 'RenderExportRequest') -> 'list[Diagnostic]'` - function. Return diagnostics for capabilities blocked by request policy. - `render_capability_diagnostics(descriptor: 'RenderExportAdapterDescriptor', request: 'RenderExportRequest') -> 'list[Diagnostic]'` - function. Return diagnostics for capabilities blocked by request policy.
- `render_export_registry_descriptor() -> 'ExtensionDescriptor'` - function. Descriptor for the render/export adapter registry itself. - `render_export_registry_descriptor() -> 'ExtensionDescriptor'` - function. Descriptor for the render/export adapter registry itself.
- `render_manifest_id(manifest: 'RenderReferenceManifest | dict[str, Any]') -> 'str'` - function. Return the deterministic id for a render reference manifest-like object.
- `render_reference_manifest_descriptor() -> 'ExtensionDescriptor'` - function. Descriptor for passive render reference and asset manifest contracts.
- `render_unit_id(kind: 'str', *, source_path: 'str | None' = None, anchor: 'str | None' = None, content_hash: 'str | None' = None, ordinal_hint: 'int | None' = None, title: 'str | None' = None) -> 'str'` - function. Return a deterministic render unit id for passive reference manifests.
- `render_with_adapter(request: 'RenderExportRequest', *, registry: 'RenderExportAdapterRegistry | None' = None, adapter_id: 'str' = 'render.fake') -> 'RenderExportResult'` - function. Render/export through a registered adapter. - `render_with_adapter(request: 'RenderExportRequest', *, registry: 'RenderExportAdapterRegistry | None' = None, adapter_id: 'str' = 'render.fake') -> 'RenderExportResult'` - function. Render/export through a registered adapter.
## `markitect_tool.runtime.assessment` ## `markitect_tool.runtime.assessment`

View File

@@ -42,6 +42,7 @@ This index maps example files to practical usecases and useful commands.
| Files | Usecase | Try | | Files | Usecase | Try |
| --- | --- | --- | | --- | --- | --- |
| `examples/render/fake-render-request.yaml` | Deterministic render/export contract fixture | Use with the `render.fake` API adapter; no external renderer required | | `examples/render/fake-render-request.yaml` | Deterministic render/export contract fixture | Use with the `render.fake` API adapter; no external renderer required |
| `examples/render/render-reference-manifest.yaml` | Render unit, cross-reference, TOC, asset, and source-map manifest fixture | Load with `RenderReferenceManifest.from_dict`; no renderer or asset copying required |
## Cache, Backend, Policy, And Context ## Cache, Backend, Policy, And Context

View File

@@ -42,6 +42,7 @@ framework organizes how Markitect itself exposes and composes capabilities.
| `source-adapter` | EPUB3/PDF/DOCX adapters in external packages | source asset in, normalized Markdown out | | `source-adapter` | EPUB3/PDF/DOCX adapters in external packages | source asset in, normalized Markdown out |
| `cli-group` | cache, backend, ref, class | command descriptors or registration hook | | `cli-group` | cache, backend, ref, class | command descriptors or registration hook |
| `render-export` | Quarkdown/export adapters | Markdown source in, rendered/exported artifact descriptor out | | `render-export` | Quarkdown/export adapters | Markdown source in, rendered/exported artifact descriptor out |
| `render-reference-contract` | render units, cross-references, TOC, asset manifests | passive manifest in, renderer-planning metadata out |
| `document-function` | future function layer | function call in, typed document value out | | `document-function` | future function layer | function call in, typed document value out |
## Canonical Lifecycle ## Canonical Lifecycle
@@ -172,8 +173,10 @@ Each module exposes one or more descriptors plus a registration function. The
root registry can be assembled explicitly at import time or by a small internal root registry can be assembled explicitly at import time or by a small internal
discovery list. Source adapters are the first external package-discovery slice discovery list. Source adapters are the first external package-discovery slice
and use the `markitect_tool.source_adapters` entry point group defined in and use the `markitect_tool.source_adapters` entry point group defined in
`docs/source-adapter-contract.md`; other extension kinds can adopt package `docs/source-adapter-contract.md`. Render/export adapters use
entry points later if they become a real requirement. `markitect_tool.render_export_adapters` and keep concrete renderer execution in
external packages. Render reference and asset manifests remain built-in passive
contracts; they do not need adapter discovery.
See `docs/extension-authoring.md` for the extension authoring checklist and See `docs/extension-authoring.md` for the extension authoring checklist and
descriptor template. descriptor template.

View File

@@ -56,6 +56,12 @@ identity, options, and local policy flags.
- source-to-render provenance - source-to-render provenance
- metadata such as whether an external renderer was invoked - metadata such as whether an external renderer was invoked
`RenderExportRequest` may also carry a passive render reference manifest. That
manifest is defined in `docs/render-reference-asset-manifest.md` and lets core
Markitect pass unit identities, cross-reference intent, TOC planning, source
maps, and asset descriptors to a renderer without assigning final numbering or
copying assets.
Artifacts are descriptors, not durable storage records. Real renderer packages Artifacts are descriptors, not durable storage records. Real renderer packages
may write files, but core Markitect only models the result. may write files, but core Markitect only models the result.

View File

@@ -0,0 +1,96 @@
# Render Reference And Asset Manifest Contract
Markitect models render-aware structure before an optional renderer runs. The
contract is passive: it records units, requested references, static assets, and
source maps, but it does not assign final numbering, rewrite links, copy
assets, or validate renderer output directories.
## Contract Version
- `markitect.render.reference.v1`
The manifest kind is:
```text
render-reference-manifest
```
## Render Units
`RenderUnitReference` gives a stable identity to renderable content:
| Unit kind | Typical use |
| --- | --- |
| `section` | Headings and TOC entries. |
| `figure` | Images, charts, diagrams, and captions. |
| `table` | Markdown or generated tables. |
| `equation` | Math blocks or equation-like units. |
| `code-block` | Numbered or referenced code listings. |
| `custom` | Project-defined numbered units. |
The unit model may carry labels, captions, anchors, source spans, content
hashes, and numbering intent such as scope or sequence. It intentionally does
not carry final numbers.
## Cross-References And TOC Planning
`RenderCrossReference` represents a requested link from one source location to
a target render unit. It records the target unit and requested style, while the
renderer decides final label text, numbering, and URL rewriting.
`RenderTocEntry` records planned table-of-contents entries by unit id, title,
level, parent id, and order. The renderer owns final page numbers and links.
## Static Assets
`RenderAssetManifest` is a deterministic list of static assets referenced by
renderer source. Each `RenderAsset` records:
- source URI or source path
- name, media type, and extension
- digest when available
- logical role
- copy policy declaration
- renderer output reference placeholder
- provenance back to source spans or source adapter attachments
Supported copy policies are `copy`, `embed`, `link`, `preserve`, and `skip`.
They are declarations for downstream renderers. Core Markitect never copies the
asset.
`markitect-filter` may provide read-side attachment metadata, such as source
URI, path, media type, extension, digest, and attachment provenance. Markitect
can transform that metadata into a render asset entry without making
`markitect-filter` responsible for rendering.
## Source Maps
`RenderSourceMap` connects Markitect source spans or generated function outputs
to renderer units and artifact references. This is the bridge from deterministic
Markdown processing to renderer-owned artifacts:
```text
source span / function output -> render unit -> renderer source or artifact
```
The map can reference a Markitect source unit, a document function run, a
render unit id, an artifact id, or a renderer-owned artifact reference.
## Fake Renderer Integration
`RenderExportRequest` accepts an optional `render_manifest`. The built-in
`render.fake` adapter echoes the manifest id, asset manifest id, unit counts,
cross-reference count, TOC count, source-map count, and asset count into result
and artifact metadata. It still performs no external rendering and has no
filesystem or network side effects.
See `examples/render/render-reference-manifest.yaml` for a complete manifest
fixture.
## Repository Boundary
- `markitect-tool` owns these passive contracts and fake-renderer metadata
integration.
- `markitect-filter` owns read-side source asset and attachment metadata.
- `markitect-quarkdown` owns concrete Quarkdown execution, final numbering,
output paths, link rewriting, asset copying, and artifact validation.

View File

@@ -47,7 +47,7 @@ and descriptions mirror the operational view.
| `MKTT-WP-0015` | complete | done | `MKTT-WP-0010`, `MKTT-WP-0011`, `MKTT-WP-0012` | Document function value contracts are complete: typed values, deterministic Markdown/JSON mapping, descriptor output validation, API exports, docs, examples, and tests. | | `MKTT-WP-0015` | complete | done | `MKTT-WP-0010`, `MKTT-WP-0011`, `MKTT-WP-0012` | Document function value contracts are complete: typed values, deterministic Markdown/JSON mapping, descriptor output validation, API exports, docs, examples, and tests. |
| `MKTT-WP-0016` | complete | done | `MKTT-WP-0008`, `MKTT-WP-0007`, `MKTT-WP-0009`, `MKTT-WP-0013` | Memory graph profile contracts are complete: graph/profile/event models, validation, context-package compilation, CLI, fixture breadth, invalid fixtures, and runtime adapter handoff descriptors. | | `MKTT-WP-0016` | complete | done | `MKTT-WP-0008`, `MKTT-WP-0007`, `MKTT-WP-0009`, `MKTT-WP-0013` | Memory graph profile contracts are complete: graph/profile/event models, validation, context-package compilation, CLI, fixture breadth, invalid fixtures, and runtime adapter handoff descriptors. |
| `MKTT-WP-0020` | complete | done | `MKTT-WP-0013`, `MKTT-WP-0015` | Render/export adapter contract is complete: descriptors, registry/discovery, request/result/artifact/provenance envelopes, fake deterministic renderer, capability diagnostics, extension descriptors, docs, examples, and tests. | | `MKTT-WP-0020` | complete | done | `MKTT-WP-0013`, `MKTT-WP-0015` | Render/export adapter contract is complete: descriptors, registry/discovery, request/result/artifact/provenance envelopes, fake deterministic renderer, capability diagnostics, extension descriptors, docs, examples, and tests. |
| `MKTT-WP-0021` | P2 | todo | `MKTT-WP-0010`, `MKTT-WP-0015`, `MKTT-WP-0020` | Render reference and asset manifest contract remains open: passive references, numbered unit metadata, static asset manifests, and source-to-render provenance maps. | | `MKTT-WP-0021` | complete | done | `MKTT-WP-0010`, `MKTT-WP-0015`, `MKTT-WP-0020` | Render reference and asset manifest contract is complete: passive render unit identities, cross-reference/TOC planning, deterministic asset manifests, source maps, fake-renderer metadata integration, docs, examples, and tests. |
## Dependency Notes ## Dependency Notes

View File

@@ -0,0 +1,83 @@
schema_version: markitect.render.reference.v1
kind: render-reference-manifest
manifest_id: render-manifest:example-report
source_path: examples/render/report.md
source_digest: sha256:example-report
units:
- unit_id: section:results
kind: section
title: Results
source_path: examples/render/report.md
anchor: results
source_span:
source_path: examples/render/report.md
line_start: 8
line_end: 12
ordinal_hint: 1
- unit_id: figure:revenue
kind: figure
label: Figure
caption: Quarterly revenue
source_path: examples/render/report.md
anchor: fig:revenue
source_span:
source_path: examples/render/report.md
line_start: 14
line_end: 18
selector: "#fig-revenue"
content_hash: sha256:figure-revenue
ordinal_hint: 2
numbering:
scope: document
sequence: figures
cross_references:
- reference_id: xref:revenue-from-results
source_unit_id: section:results
target_unit_id: figure:revenue
label: Figure
requested_style: numbered
fallback_text: Quarterly revenue
toc:
- entry_id: toc:results
unit_id: section:results
title: Results
level: 1
order: 1
asset_manifest:
schema_version: markitect.render.reference.v1
manifest_id: asset-manifest:example-report
source_path: examples/render/report.md
source_digest: sha256:example-report
assets:
- asset_id: asset:revenue-chart
source_uri: images/revenue-chart.png
source_path: examples/render/images/revenue-chart.png
name: revenue-chart.png
media_type: image/png
extension: .png
digest: sha256:revenue-chart
role: figure-image
copy_policy: copy
output_reference: renderer://asset/revenue-chart
provenance:
- source_uri: images/revenue-chart.png
source_path: examples/render/report.md
attachment_id: revenue-chart.png
source_adapter_id: markitect-filter
digest: sha256:revenue-chart
source_maps:
- map_id: source-map:revenue-figure
source:
source_path: examples/render/report.md
line_start: 14
line_end: 18
selector: "#fig-revenue"
source_unit_id: figure:revenue
generated_by: markitect.render.reference-manifest
render_unit_id: figure:revenue
artifact_ref: renderer-source
role: figure
metadata:
concrete_renderer_required: false
final_numbering_assigned_by_core: false
asset_copying_performed_by_core: false

View File

@@ -263,8 +263,16 @@ from markitect_tool.render import (
RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP, RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP,
RENDER_EXPORT_ADAPTER_KIND, RENDER_EXPORT_ADAPTER_KIND,
RENDER_EXPORT_SCHEMA_VERSION, RENDER_EXPORT_SCHEMA_VERSION,
RENDER_ASSET_COPY_POLICIES,
RENDER_REFERENCE_MANIFEST_KIND,
RENDER_REFERENCE_SCHEMA_VERSION,
RENDER_UNIT_KINDS,
FakeRenderExportAdapter, FakeRenderExportAdapter,
RenderArtifact, RenderArtifact,
RenderAsset,
RenderAssetManifest,
RenderAssetProvenance,
RenderCrossReference,
RenderExportAdapter, RenderExportAdapter,
RenderExportAdapterDescriptor, RenderExportAdapterDescriptor,
RenderExportAdapterError, RenderExportAdapterError,
@@ -272,10 +280,20 @@ from markitect_tool.render import (
RenderExportRequest, RenderExportRequest,
RenderExportResult, RenderExportResult,
RenderProvenance, RenderProvenance,
RenderReferenceError,
RenderReferenceManifest,
RenderSourceMap,
RenderSourceSpan,
RenderTocEntry,
RenderUnitReference,
default_render_export_adapter_registry, default_render_export_adapter_registry,
discover_render_export_adapters, discover_render_export_adapters,
render_capability_diagnostics, render_capability_diagnostics,
render_asset_id,
render_export_registry_descriptor, render_export_registry_descriptor,
render_manifest_id,
render_reference_manifest_descriptor,
render_unit_id,
render_with_adapter, render_with_adapter,
) )
from markitect_tool.schema import ( from markitect_tool.schema import (
@@ -592,8 +610,16 @@ __all__ = [
"RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP", "RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP",
"RENDER_EXPORT_ADAPTER_KIND", "RENDER_EXPORT_ADAPTER_KIND",
"RENDER_EXPORT_SCHEMA_VERSION", "RENDER_EXPORT_SCHEMA_VERSION",
"RENDER_ASSET_COPY_POLICIES",
"RENDER_REFERENCE_MANIFEST_KIND",
"RENDER_REFERENCE_SCHEMA_VERSION",
"RENDER_UNIT_KINDS",
"FakeRenderExportAdapter", "FakeRenderExportAdapter",
"RenderArtifact", "RenderArtifact",
"RenderAsset",
"RenderAssetManifest",
"RenderAssetProvenance",
"RenderCrossReference",
"RenderExportAdapter", "RenderExportAdapter",
"RenderExportAdapterDescriptor", "RenderExportAdapterDescriptor",
"RenderExportAdapterError", "RenderExportAdapterError",
@@ -601,10 +627,20 @@ __all__ = [
"RenderExportRequest", "RenderExportRequest",
"RenderExportResult", "RenderExportResult",
"RenderProvenance", "RenderProvenance",
"RenderReferenceError",
"RenderReferenceManifest",
"RenderSourceMap",
"RenderSourceSpan",
"RenderTocEntry",
"RenderUnitReference",
"default_render_export_adapter_registry", "default_render_export_adapter_registry",
"discover_render_export_adapters", "discover_render_export_adapters",
"render_capability_diagnostics", "render_capability_diagnostics",
"render_asset_id",
"render_export_registry_descriptor", "render_export_registry_descriptor",
"render_manifest_id",
"render_reference_manifest_descriptor",
"render_unit_id",
"render_with_adapter", "render_with_adapter",
"MissingTemplateVariable", "MissingTemplateVariable",
"TemplateAnalysis", "TemplateAnalysis",

View File

@@ -8,6 +8,7 @@ from markitect_tool.query import default_query_engine_registry
from markitect_tool.render import ( from markitect_tool.render import (
default_render_export_adapter_registry, default_render_export_adapter_registry,
render_export_registry_descriptor, render_export_registry_descriptor,
render_reference_manifest_descriptor,
) )
from markitect_tool.source import ( from markitect_tool.source import (
default_source_adapter_registry, default_source_adapter_registry,
@@ -33,6 +34,7 @@ def builtin_extension_registry() -> ExtensionRegistry:
_memory_runtime_adapter_descriptor(), _memory_runtime_adapter_descriptor(),
_agent_memory_descriptor(), _agent_memory_descriptor(),
render_export_registry_descriptor(), render_export_registry_descriptor(),
render_reference_manifest_descriptor(),
source_adapter_registry_descriptor(), source_adapter_registry_descriptor(),
]: ]:
registry.register(descriptor) registry.register(descriptor)

View File

@@ -4,8 +4,16 @@ from markitect_tool.render.engine import (
RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP, RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP,
RENDER_EXPORT_ADAPTER_KIND, RENDER_EXPORT_ADAPTER_KIND,
RENDER_EXPORT_SCHEMA_VERSION, RENDER_EXPORT_SCHEMA_VERSION,
RENDER_ASSET_COPY_POLICIES,
RENDER_REFERENCE_MANIFEST_KIND,
RENDER_REFERENCE_SCHEMA_VERSION,
RENDER_UNIT_KINDS,
FakeRenderExportAdapter, FakeRenderExportAdapter,
RenderArtifact, RenderArtifact,
RenderAsset,
RenderAssetManifest,
RenderAssetProvenance,
RenderCrossReference,
RenderExportAdapter, RenderExportAdapter,
RenderExportAdapterDescriptor, RenderExportAdapterDescriptor,
RenderExportAdapterError, RenderExportAdapterError,
@@ -13,10 +21,20 @@ from markitect_tool.render.engine import (
RenderExportRequest, RenderExportRequest,
RenderExportResult, RenderExportResult,
RenderProvenance, RenderProvenance,
RenderReferenceError,
RenderReferenceManifest,
RenderSourceMap,
RenderSourceSpan,
RenderTocEntry,
RenderUnitReference,
default_render_export_adapter_registry, default_render_export_adapter_registry,
discover_render_export_adapters, discover_render_export_adapters,
render_capability_diagnostics, render_capability_diagnostics,
render_asset_id,
render_export_registry_descriptor, render_export_registry_descriptor,
render_manifest_id,
render_reference_manifest_descriptor,
render_unit_id,
render_with_adapter, render_with_adapter,
) )
@@ -24,8 +42,16 @@ __all__ = [
"RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP", "RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP",
"RENDER_EXPORT_ADAPTER_KIND", "RENDER_EXPORT_ADAPTER_KIND",
"RENDER_EXPORT_SCHEMA_VERSION", "RENDER_EXPORT_SCHEMA_VERSION",
"RENDER_ASSET_COPY_POLICIES",
"RENDER_REFERENCE_MANIFEST_KIND",
"RENDER_REFERENCE_SCHEMA_VERSION",
"RENDER_UNIT_KINDS",
"FakeRenderExportAdapter", "FakeRenderExportAdapter",
"RenderArtifact", "RenderArtifact",
"RenderAsset",
"RenderAssetManifest",
"RenderAssetProvenance",
"RenderCrossReference",
"RenderExportAdapter", "RenderExportAdapter",
"RenderExportAdapterDescriptor", "RenderExportAdapterDescriptor",
"RenderExportAdapterError", "RenderExportAdapterError",
@@ -33,9 +59,19 @@ __all__ = [
"RenderExportRequest", "RenderExportRequest",
"RenderExportResult", "RenderExportResult",
"RenderProvenance", "RenderProvenance",
"RenderReferenceError",
"RenderReferenceManifest",
"RenderSourceMap",
"RenderSourceSpan",
"RenderTocEntry",
"RenderUnitReference",
"default_render_export_adapter_registry", "default_render_export_adapter_registry",
"discover_render_export_adapters", "discover_render_export_adapters",
"render_capability_diagnostics", "render_capability_diagnostics",
"render_asset_id",
"render_export_registry_descriptor", "render_export_registry_descriptor",
"render_manifest_id",
"render_reference_manifest_descriptor",
"render_unit_id",
"render_with_adapter", "render_with_adapter",
] ]

View File

@@ -37,6 +37,26 @@ RENDER_PROFILES = {
"pdf", "pdf",
} }
RENDER_REFERENCE_SCHEMA_VERSION = "markitect.render.reference.v1"
RENDER_REFERENCE_MANIFEST_KIND = "render-reference-manifest"
RENDER_UNIT_KINDS = {
"figure",
"table",
"equation",
"code-block",
"section",
"custom",
}
RENDER_ASSET_COPY_POLICIES = {
"copy",
"embed",
"link",
"preserve",
"skip",
}
_SAFETY_POLICY_FLAGS = { _SAFETY_POLICY_FLAGS = {
"filesystem_read": "filesystem_read", "filesystem_read": "filesystem_read",
"filesystem_write": "filesystem_write", "filesystem_write": "filesystem_write",
@@ -52,6 +72,657 @@ class RenderExportAdapterError(ValueError):
"""Raised when render/export adapter contracts are invalid.""" """Raised when render/export adapter contracts are invalid."""
class RenderReferenceError(ValueError):
"""Raised when render reference or asset manifest contracts are invalid."""
@dataclass(frozen=True)
class RenderSourceSpan:
"""Source location for a render unit, asset, or source-map edge."""
source_path: str | None = None
line_start: int | None = None
line_end: int | None = None
selector: str | None = None
source_unit_id: str | None = None
content_hash: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if (
self.line_start is not None
and self.line_end is not None
and self.line_start > self.line_end
):
raise RenderReferenceError("Render source span line_start cannot be after line_end")
def to_dict(self) -> dict[str, Any]:
return _drop_empty(asdict(self))
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "RenderSourceSpan | None":
if not data:
return None
return cls(
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
line_start=int(data["line_start"]) if data.get("line_start") is not None else None,
line_end=int(data["line_end"]) if data.get("line_end") is not None else None,
selector=str(data["selector"]) if data.get("selector") is not None else None,
source_unit_id=str(data["source_unit_id"]) if data.get("source_unit_id") is not None else None,
content_hash=str(data["content_hash"]) if data.get("content_hash") is not None else None,
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderUnitReference:
"""Stable identity for one renderable unit before final renderer numbering."""
unit_id: str = ""
kind: str = "custom"
label: str | None = None
title: str | None = None
caption: str | None = None
source_path: str | None = None
anchor: str | None = None
source_span: RenderSourceSpan | None = None
content_hash: str | None = None
ordinal_hint: int | None = None
numbering: dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
kind = self.kind.strip()
if kind not in RENDER_UNIT_KINDS:
raise RenderReferenceError(f"Unsupported render unit kind `{self.kind}`")
if self.ordinal_hint is not None and self.ordinal_hint < 0:
raise RenderReferenceError("Render unit ordinal_hint cannot be negative")
if not self.unit_id:
object.__setattr__(
self,
"unit_id",
render_unit_id(
kind,
source_path=self.source_path,
anchor=self.anchor,
content_hash=self.content_hash,
ordinal_hint=self.ordinal_hint,
title=self.title or self.caption,
),
)
if self.source_span and self.source_span.source_path and not self.source_path:
object.__setattr__(self, "source_path", self.source_span.source_path)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"unit_id": self.unit_id,
"kind": self.kind,
"label": self.label,
"title": self.title,
"caption": self.caption,
"source_path": self.source_path,
"anchor": self.anchor,
"source_span": self.source_span.to_dict() if self.source_span else None,
"content_hash": self.content_hash,
"ordinal_hint": self.ordinal_hint,
"numbering": self.numbering,
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderUnitReference":
return cls(
unit_id=str(data.get("unit_id", "")),
kind=str(data.get("kind", "custom")),
label=str(data["label"]) if data.get("label") is not None else None,
title=str(data["title"]) if data.get("title") is not None else None,
caption=str(data["caption"]) if data.get("caption") is not None else None,
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
anchor=str(data["anchor"]) if data.get("anchor") is not None else None,
source_span=RenderSourceSpan.from_dict(data.get("source_span")),
content_hash=str(data["content_hash"]) if data.get("content_hash") is not None else None,
ordinal_hint=int(data["ordinal_hint"]) if data.get("ordinal_hint") is not None else None,
numbering=dict(data.get("numbering", {})),
metadata=dict(data.get("metadata", {})),
)
@classmethod
def from_content_unit(
cls,
unit: Any,
*,
kind: str | None = None,
label: str | None = None,
) -> "RenderUnitReference":
data = _mapping_from(unit)
span_data = data.get("span") if isinstance(data.get("span"), dict) else {}
source_path = str(data["source_path"]) if data.get("source_path") is not None else None
return cls(
unit_id=str(data.get("unit_id", "")),
kind=kind or _render_kind_from_content_unit(str(data.get("kind", "custom"))),
label=label,
title=str(data["name"]) if data.get("name") is not None else None,
source_path=source_path,
source_span=RenderSourceSpan(
source_path=source_path,
line_start=int(span_data["line_start"]) if span_data.get("line_start") is not None else None,
line_end=int(span_data["line_end"]) if span_data.get("line_end") is not None else None,
source_unit_id=str(data.get("unit_id", "")) or None,
content_hash=str(data["content_hash"]) if data.get("content_hash") is not None else None,
)
if source_path or span_data
else None,
content_hash=str(data["content_hash"]) if data.get("content_hash") is not None else None,
metadata={"content_unit_kind": data.get("kind")} if data.get("kind") else {},
)
@dataclass(frozen=True)
class RenderCrossReference:
"""Requested cross-reference link before renderer-specific numbering exists."""
target_unit_id: str
reference_id: str = ""
source_unit_id: str | None = None
source_span: RenderSourceSpan | None = None
label: str | None = None
requested_style: str = "numbered"
fallback_text: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not self.target_unit_id.strip():
raise RenderReferenceError("Cross-reference target_unit_id cannot be empty")
if not self.reference_id:
object.__setattr__(
self,
"reference_id",
_stable_id(
"xref",
self.source_unit_id,
self.target_unit_id,
self.source_span.to_dict() if self.source_span else None,
self.requested_style,
),
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"reference_id": self.reference_id,
"source_unit_id": self.source_unit_id,
"target_unit_id": self.target_unit_id,
"source_span": self.source_span.to_dict() if self.source_span else None,
"label": self.label,
"requested_style": self.requested_style,
"fallback_text": self.fallback_text,
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderCrossReference":
return cls(
target_unit_id=str(data["target_unit_id"]),
reference_id=str(data.get("reference_id", "")),
source_unit_id=str(data["source_unit_id"]) if data.get("source_unit_id") is not None else None,
source_span=RenderSourceSpan.from_dict(data.get("source_span")),
label=str(data["label"]) if data.get("label") is not None else None,
requested_style=str(data.get("requested_style", "numbered")),
fallback_text=str(data["fallback_text"]) if data.get("fallback_text") is not None else None,
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderTocEntry:
"""Table-of-contents planning entry before final renderer links exist."""
unit_id: str
title: str
level: int
entry_id: str = ""
parent_id: str | None = None
order: int | None = None
source_span: RenderSourceSpan | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not self.unit_id.strip():
raise RenderReferenceError("TOC unit_id cannot be empty")
if not self.title.strip():
raise RenderReferenceError("TOC title cannot be empty")
if self.level < 1:
raise RenderReferenceError("TOC level must be greater than zero")
if self.order is not None and self.order < 0:
raise RenderReferenceError("TOC order cannot be negative")
if not self.entry_id:
object.__setattr__(self, "entry_id", _stable_id("toc", self.unit_id, self.title, self.level))
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"entry_id": self.entry_id,
"unit_id": self.unit_id,
"title": self.title,
"level": self.level,
"parent_id": self.parent_id,
"order": self.order,
"source_span": self.source_span.to_dict() if self.source_span else None,
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderTocEntry":
return cls(
unit_id=str(data["unit_id"]),
title=str(data["title"]),
level=int(data["level"]),
entry_id=str(data.get("entry_id", "")),
parent_id=str(data["parent_id"]) if data.get("parent_id") is not None else None,
order=int(data["order"]) if data.get("order") is not None else None,
source_span=RenderSourceSpan.from_dict(data.get("source_span")),
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderAssetProvenance:
"""Provenance for an asset manifest entry."""
source_uri: str | None = None
source_path: str | None = None
source_href: str | None = None
package_path: str | None = None
attachment_id: str | None = None
source_adapter_id: str | None = None
source_span: RenderSourceSpan | None = None
digest: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"source_uri": self.source_uri,
"source_path": self.source_path,
"source_href": self.source_href,
"package_path": self.package_path,
"attachment_id": self.attachment_id,
"source_adapter_id": self.source_adapter_id,
"source_span": self.source_span.to_dict() if self.source_span else None,
"digest": self.digest,
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderAssetProvenance":
return cls(
source_uri=str(data["source_uri"]) if data.get("source_uri") is not None else None,
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
source_href=str(data["source_href"]) if data.get("source_href") is not None else None,
package_path=str(data["package_path"]) if data.get("package_path") is not None else None,
attachment_id=str(data["attachment_id"]) if data.get("attachment_id") is not None else None,
source_adapter_id=str(data["source_adapter_id"]) if data.get("source_adapter_id") is not None else None,
source_span=RenderSourceSpan.from_dict(data.get("source_span")),
digest=str(data["digest"]) if data.get("digest") is not None else None,
metadata=dict(data.get("metadata", {})),
)
@classmethod
def from_source_provenance(
cls,
provenance: Any,
*,
attachment_id: str | None = None,
source_adapter_id: str | None = None,
) -> "RenderAssetProvenance":
data = _mapping_from(provenance)
return cls(
source_uri=str(data["source_uri"]) if data.get("source_uri") is not None else None,
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
source_href=str(data["source_href"]) if data.get("source_href") is not None else None,
package_path=str(data["package_path"]) if data.get("package_path") is not None else None,
attachment_id=attachment_id,
source_adapter_id=source_adapter_id,
source_span=RenderSourceSpan(
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
selector=str(data["anchor"]) if data.get("anchor") is not None else None,
metadata={
key: str(data[key])
for key in ("page", "section", "start_offset", "end_offset")
if data.get(key) is not None
},
),
digest=str(data["digest"]) if data.get("digest") is not None else None,
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderAsset:
"""Static asset descriptor with declared renderer copy behavior."""
asset_id: str = ""
source_uri: str | None = None
source_path: str | None = None
name: str | None = None
media_type: str | None = None
extension: str | None = None
digest: str | None = None
role: str = "asset"
copy_policy: str = "copy"
output_reference: str | None = None
provenance: list[RenderAssetProvenance] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not (self.source_uri or self.source_path):
raise RenderReferenceError("Render asset needs source_uri or source_path")
if not self.role.strip():
raise RenderReferenceError("Render asset role cannot be empty")
if self.copy_policy not in RENDER_ASSET_COPY_POLICIES:
raise RenderReferenceError(f"Unsupported render asset copy_policy `{self.copy_policy}`")
if not self.asset_id:
object.__setattr__(
self,
"asset_id",
render_asset_id(
self.source_uri or self.source_path or "",
digest=self.digest,
role=self.role,
output_reference=self.output_reference,
),
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"asset_id": self.asset_id,
"source_uri": self.source_uri,
"source_path": self.source_path,
"name": self.name,
"media_type": self.media_type,
"extension": self.extension,
"digest": self.digest,
"role": self.role,
"copy_policy": self.copy_policy,
"output_reference": self.output_reference,
"provenance": [event.to_dict() for event in self.provenance],
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderAsset":
return cls(
asset_id=str(data.get("asset_id", "")),
source_uri=str(data["source_uri"]) if data.get("source_uri") is not None else None,
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
name=str(data["name"]) if data.get("name") is not None else None,
media_type=str(data["media_type"]) if data.get("media_type") is not None else None,
extension=str(data["extension"]) if data.get("extension") is not None else None,
digest=str(data["digest"]) if data.get("digest") is not None else None,
role=str(data.get("role", "asset")),
copy_policy=str(data.get("copy_policy", "copy")),
output_reference=str(data["output_reference"]) if data.get("output_reference") is not None else None,
provenance=[RenderAssetProvenance.from_dict(event) for event in data.get("provenance", [])],
metadata=dict(data.get("metadata", {})),
)
@classmethod
def from_source_asset(
cls,
asset: Any,
*,
role: str = "attachment",
copy_policy: str = "copy",
output_reference: str | None = None,
provenance: list[RenderAssetProvenance] | None = None,
) -> "RenderAsset":
data = _mapping_from(asset)
source_uri = str(data.get("uri") or data.get("source_uri") or "")
source_path = str(data["path"]) if data.get("path") is not None else None
digest = str(data["digest"]) if data.get("digest") is not None else None
resolved_provenance = provenance
if resolved_provenance is None:
resolved_provenance = [
RenderAssetProvenance(
source_uri=source_uri or None,
source_path=source_path,
attachment_id=str(data["name"]) if data.get("name") is not None else None,
digest=digest,
metadata={"source_asset": True},
)
]
return cls(
source_uri=source_uri or None,
source_path=source_path,
name=str(data["name"]) if data.get("name") is not None else None,
media_type=str(data["media_type"]) if data.get("media_type") is not None else None,
extension=str(data["extension"]) if data.get("extension") is not None else None,
digest=digest,
role=role,
copy_policy=copy_policy,
output_reference=output_reference,
provenance=resolved_provenance,
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderAssetManifest:
"""Deterministic list of static assets requested by renderer source."""
manifest_id: str = ""
assets: list[RenderAsset] = field(default_factory=list)
source_path: str | None = None
source_digest: str | None = None
schema_version: str = RENDER_REFERENCE_SCHEMA_VERSION
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if self.schema_version != RENDER_REFERENCE_SCHEMA_VERSION:
raise RenderReferenceError(f"Expected schema_version `{RENDER_REFERENCE_SCHEMA_VERSION}`")
assets = sorted(self.assets, key=lambda asset: asset.asset_id)
_check_duplicates([asset.asset_id for asset in assets], "asset")
object.__setattr__(self, "assets", assets)
if not self.manifest_id:
object.__setattr__(
self,
"manifest_id",
_stable_id(
"asset-manifest",
self.source_path,
self.source_digest,
[asset.asset_id for asset in assets],
),
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"schema_version": self.schema_version,
"manifest_id": self.manifest_id,
"source_path": self.source_path,
"source_digest": self.source_digest,
"assets": [asset.to_dict() for asset in self.assets],
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "RenderAssetManifest":
if not data:
return cls()
return cls(
manifest_id=str(data.get("manifest_id", "")),
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
source_digest=str(data["source_digest"]) if data.get("source_digest") is not None else None,
assets=[RenderAsset.from_dict(asset) for asset in data.get("assets", [])],
schema_version=str(data.get("schema_version", RENDER_REFERENCE_SCHEMA_VERSION)),
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderSourceMap:
"""Trace from Markitect source/generated output to renderer units or artifacts."""
map_id: str = ""
source: RenderSourceSpan | None = None
source_unit_id: str | None = None
generated_by: str | None = None
function_run_id: str | None = None
render_unit_id: str | None = None
artifact_id: str | None = None
artifact_ref: str | None = None
role: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not (self.render_unit_id or self.artifact_id or self.artifact_ref):
raise RenderReferenceError(
"Render source map needs render_unit_id, artifact_id, or artifact_ref"
)
if not self.map_id:
object.__setattr__(
self,
"map_id",
_stable_id(
"source-map",
self.source.to_dict() if self.source else None,
self.source_unit_id,
self.generated_by,
self.function_run_id,
self.render_unit_id,
self.artifact_id,
self.artifact_ref,
self.role,
),
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"map_id": self.map_id,
"source": self.source.to_dict() if self.source else None,
"source_unit_id": self.source_unit_id,
"generated_by": self.generated_by,
"function_run_id": self.function_run_id,
"render_unit_id": self.render_unit_id,
"artifact_id": self.artifact_id,
"artifact_ref": self.artifact_ref,
"role": self.role,
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderSourceMap":
return cls(
map_id=str(data.get("map_id", "")),
source=RenderSourceSpan.from_dict(data.get("source")),
source_unit_id=str(data["source_unit_id"]) if data.get("source_unit_id") is not None else None,
generated_by=str(data["generated_by"]) if data.get("generated_by") is not None else None,
function_run_id=str(data["function_run_id"]) if data.get("function_run_id") is not None else None,
render_unit_id=str(data["render_unit_id"]) if data.get("render_unit_id") is not None else None,
artifact_id=str(data["artifact_id"]) if data.get("artifact_id") is not None else None,
artifact_ref=str(data["artifact_ref"]) if data.get("artifact_ref") is not None else None,
role=str(data["role"]) if data.get("role") is not None else None,
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderReferenceManifest:
"""Passive render structure, cross-reference, asset, and provenance manifest."""
manifest_id: str = ""
units: list[RenderUnitReference] = field(default_factory=list)
cross_references: list[RenderCrossReference] = field(default_factory=list)
toc: list[RenderTocEntry] = field(default_factory=list)
asset_manifest: RenderAssetManifest = field(default_factory=RenderAssetManifest)
source_maps: list[RenderSourceMap] = field(default_factory=list)
source_path: str | None = None
source_digest: str | None = None
schema_version: str = RENDER_REFERENCE_SCHEMA_VERSION
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if self.schema_version != RENDER_REFERENCE_SCHEMA_VERSION:
raise RenderReferenceError(f"Expected schema_version `{RENDER_REFERENCE_SCHEMA_VERSION}`")
_check_duplicates([unit.unit_id for unit in self.units], "render unit")
_check_duplicates([entry.entry_id for entry in self.toc], "TOC entry")
_check_duplicates([event.map_id for event in self.source_maps], "source map")
unit_ids = {unit.unit_id for unit in self.units}
if unit_ids:
for cross_reference in self.cross_references:
if cross_reference.target_unit_id not in unit_ids:
raise RenderReferenceError(
f"Cross-reference `{cross_reference.reference_id}` targets unknown unit "
f"`{cross_reference.target_unit_id}`"
)
for entry in self.toc:
if entry.unit_id not in unit_ids:
raise RenderReferenceError(
f"TOC entry `{entry.entry_id}` targets unknown unit `{entry.unit_id}`"
)
if not self.manifest_id:
object.__setattr__(
self,
"manifest_id",
_stable_id(
"render-manifest",
self.source_path,
self.source_digest,
[unit.unit_id for unit in self.units],
[reference.reference_id for reference in self.cross_references],
[entry.entry_id for entry in self.toc],
self.asset_manifest.manifest_id,
[event.map_id for event in self.source_maps],
),
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"schema_version": self.schema_version,
"kind": RENDER_REFERENCE_MANIFEST_KIND,
"manifest_id": self.manifest_id,
"source_path": self.source_path,
"source_digest": self.source_digest,
"units": [unit.to_dict() for unit in self.units],
"cross_references": [reference.to_dict() for reference in self.cross_references],
"toc": [entry.to_dict() for entry in self.toc],
"asset_manifest": self.asset_manifest.to_dict(),
"source_maps": [event.to_dict() for event in self.source_maps],
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderReferenceManifest":
return cls(
manifest_id=str(data.get("manifest_id", "")),
units=[RenderUnitReference.from_dict(unit) for unit in data.get("units", [])],
cross_references=[
RenderCrossReference.from_dict(reference)
for reference in data.get("cross_references", [])
],
toc=[RenderTocEntry.from_dict(entry) for entry in data.get("toc", [])],
asset_manifest=RenderAssetManifest.from_dict(data.get("asset_manifest")),
source_maps=[RenderSourceMap.from_dict(event) for event in data.get("source_maps", [])],
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
source_digest=str(data["source_digest"]) if data.get("source_digest") is not None else None,
schema_version=str(data.get("schema_version", RENDER_REFERENCE_SCHEMA_VERSION)),
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True) @dataclass(frozen=True)
class RenderArtifact: class RenderArtifact:
"""Metadata for a rendered or exported artifact.""" """Metadata for a rendered or exported artifact."""
@@ -115,6 +786,7 @@ class RenderExportRequest:
source_path: str | None = None source_path: str | None = None
options: dict[str, Any] = field(default_factory=dict) options: dict[str, Any] = field(default_factory=dict)
policy: dict[str, Any] = field(default_factory=dict) policy: dict[str, Any] = field(default_factory=dict)
render_manifest: RenderReferenceManifest | dict[str, Any] | None = None
schema_version: str = RENDER_EXPORT_SCHEMA_VERSION schema_version: str = RENDER_EXPORT_SCHEMA_VERSION
metadata: dict[str, Any] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict)
@@ -128,6 +800,7 @@ class RenderExportRequest:
"source_path": self.source_path, "source_path": self.source_path,
"options": self.options, "options": self.options,
"policy": self.policy, "policy": self.policy,
"render_manifest": _render_manifest_dict(self.render_manifest),
"metadata": self.metadata, "metadata": self.metadata,
} }
) )
@@ -384,18 +1057,20 @@ class FakeRenderExportAdapter:
operation=request.operation, operation=request.operation,
profile=request.profile, profile=request.profile,
provenance=provenance, provenance=provenance,
metadata={ metadata=_fake_result_metadata(
"profile": request.profile, request,
"supported_operations": self.descriptor.operations, {
"external_renderer_invoked": False, "profile": request.profile,
}, "supported_operations": self.descriptor.operations,
},
),
) )
if request.operation == "export-source": if request.operation == "export-source":
artifact = RenderArtifact.from_content( artifact = RenderArtifact.from_content(
exported, exported,
role="renderer-source", role="renderer-source",
media_type="text/markdown", media_type="text/markdown",
metadata={"profile": request.profile}, metadata=_fake_artifact_metadata(request, {"profile": request.profile}),
) )
provenance = [ provenance = [
_event_with_artifact(event, artifact.artifact_id) _event_with_artifact(event, artifact.artifact_id)
@@ -408,14 +1083,14 @@ class FakeRenderExportAdapter:
artifacts=[artifact], artifacts=[artifact],
exported_source=exported, exported_source=exported,
provenance=provenance, provenance=provenance,
metadata={"external_renderer_invoked": False}, metadata=_fake_result_metadata(request),
) )
rendered = f"FAKE RENDER ARTIFACT\nprofile: {request.profile}\n\n{exported}" rendered = f"FAKE RENDER ARTIFACT\nprofile: {request.profile}\n\n{exported}"
artifact = RenderArtifact.from_content( artifact = RenderArtifact.from_content(
rendered, rendered,
role="rendered-artifact", role="rendered-artifact",
media_type=_fake_media_type(request.profile), media_type=_fake_media_type(request.profile),
metadata={"profile": request.profile, "fake_renderer": True}, metadata=_fake_artifact_metadata(request, {"profile": request.profile, "fake_renderer": True}),
) )
provenance = [_event_with_artifact(event, artifact.artifact_id) for event in provenance] provenance = [_event_with_artifact(event, artifact.artifact_id) for event in provenance]
return RenderExportResult( return RenderExportResult(
@@ -425,7 +1100,7 @@ class FakeRenderExportAdapter:
artifacts=[artifact], artifacts=[artifact],
exported_source=exported, exported_source=exported,
provenance=provenance, provenance=provenance,
metadata={"external_renderer_invoked": False}, metadata=_fake_result_metadata(request),
) )
@@ -527,6 +1202,45 @@ def render_export_registry_descriptor() -> ExtensionDescriptor:
) )
def render_reference_manifest_descriptor() -> ExtensionDescriptor:
"""Descriptor for passive render reference and asset manifest contracts."""
return ExtensionDescriptor(
id="render.reference-manifest",
kind="render-reference-contract",
summary="Passive render unit, cross-reference, TOC, asset manifest, and source-map contracts.",
capabilities=[
ProcessingCapability(id="render_references", kind="model"),
ProcessingCapability(id="asset_manifest", kind="model"),
ProcessingCapability(id="source_maps", kind="model"),
ProcessingCapability(id="provenance", kind="emit"),
],
safety={
"filesystem_read": False,
"filesystem_write": False,
"external_process": False,
"network": False,
"asset_copying": False,
"final_numbering": False,
},
input_contract="Markdown units | normalized source attachments | renderer source planning metadata",
output_contract="RenderReferenceManifest",
diagnostics_namespace="render.reference",
provenance_prefix="render.reference_manifest",
docs=["docs/render-reference-asset-manifest.md"],
examples=["examples/render/render-reference-manifest.yaml"],
metadata={
"schema_version": RENDER_REFERENCE_SCHEMA_VERSION,
"kind": RENDER_REFERENCE_MANIFEST_KIND,
"unit_kinds": sorted(RENDER_UNIT_KINDS),
"copy_policies": sorted(RENDER_ASSET_COPY_POLICIES),
"core_performs_asset_copying": False,
"core_assigns_final_numbering": False,
"markitect_filter_boundary": "read-side source asset and attachment metadata only",
},
)
def fake_render_export_adapter_descriptor() -> RenderExportAdapterDescriptor: def fake_render_export_adapter_descriptor() -> RenderExportAdapterDescriptor:
"""Descriptor for the built-in fake render/export adapter.""" """Descriptor for the built-in fake render/export adapter."""
@@ -603,6 +1317,45 @@ def _render_export_capabilities() -> list[ProcessingCapability]:
] ]
def render_unit_id(
kind: str,
*,
source_path: str | None = None,
anchor: str | None = None,
content_hash: str | None = None,
ordinal_hint: int | None = None,
title: str | None = None,
) -> str:
"""Return a deterministic render unit id for passive reference manifests."""
if kind not in RENDER_UNIT_KINDS:
raise RenderReferenceError(f"Unsupported render unit kind `{kind}`")
return _stable_id("render-unit", kind, source_path, anchor, content_hash, ordinal_hint, title)
def render_asset_id(
source: str,
*,
digest: str | None = None,
role: str | None = None,
output_reference: str | None = None,
) -> str:
"""Return a deterministic asset id without copying or reading the asset."""
if not source.strip():
raise RenderReferenceError("Render asset id source cannot be empty")
return _stable_id("asset", source, digest, role, output_reference)
def render_manifest_id(manifest: RenderReferenceManifest | dict[str, Any]) -> str:
"""Return the deterministic id for a render reference manifest-like object."""
data = _render_manifest_dict(manifest)
if data and data.get("manifest_id"):
return str(data["manifest_id"])
return _stable_id("render-manifest", data)
def _event_with_artifact(event: RenderProvenance, artifact_id: str) -> RenderProvenance: def _event_with_artifact(event: RenderProvenance, artifact_id: str) -> RenderProvenance:
return RenderProvenance( return RenderProvenance(
operation=event.operation, operation=event.operation,
@@ -615,6 +1368,57 @@ def _event_with_artifact(event: RenderProvenance, artifact_id: str) -> RenderPro
) )
def _fake_result_metadata(
request: RenderExportRequest,
base: dict[str, Any] | None = None,
) -> dict[str, Any]:
metadata = {"external_renderer_invoked": False}
metadata.update(base or {})
metadata.update(_manifest_summary_metadata(request.render_manifest))
return metadata
def _fake_artifact_metadata(
request: RenderExportRequest,
base: dict[str, Any] | None = None,
) -> dict[str, Any]:
metadata = dict(base or {})
metadata.update(_manifest_summary_metadata(request.render_manifest))
return metadata
def _manifest_summary_metadata(manifest: RenderReferenceManifest | dict[str, Any] | None) -> dict[str, Any]:
data = _render_manifest_dict(manifest)
if not data:
return {}
asset_manifest = data.get("asset_manifest", {})
assets = asset_manifest.get("assets", []) if isinstance(asset_manifest, dict) else []
return _drop_empty(
{
"render_reference_manifest_id": data.get("manifest_id"),
"render_reference_schema_version": data.get("schema_version"),
"render_units": len(data.get("units", [])),
"render_cross_references": len(data.get("cross_references", [])),
"render_toc_entries": len(data.get("toc", [])),
"render_source_maps": len(data.get("source_maps", [])),
"asset_manifest_id": asset_manifest.get("manifest_id")
if isinstance(asset_manifest, dict)
else None,
"render_assets": len(assets),
}
)
def _render_manifest_dict(manifest: RenderReferenceManifest | dict[str, Any] | None) -> dict[str, Any]:
if manifest is None:
return {}
if isinstance(manifest, RenderReferenceManifest):
return manifest.to_dict()
if isinstance(manifest, dict):
return dict(manifest)
raise RenderReferenceError("render_manifest must be a RenderReferenceManifest or mapping")
def _fake_exported_source(request: RenderExportRequest) -> str: def _fake_exported_source(request: RenderExportRequest) -> str:
return ( return (
f"<!-- render.fake profile={request.profile} operation={request.operation} -->\n\n" f"<!-- render.fake profile={request.profile} operation={request.operation} -->\n\n"
@@ -630,6 +1434,44 @@ def _fake_media_type(profile: str) -> str:
return "text/plain" return "text/plain"
def _stable_id(prefix: str, *parts: Any) -> str:
payload = json.dumps(parts, sort_keys=True, separators=(",", ":"), default=str)
digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
return f"{prefix}:{digest}"
def _check_duplicates(values: list[str], label: str) -> None:
seen: set[str] = set()
duplicates: set[str] = set()
for value in values:
if value in seen:
duplicates.add(value)
seen.add(value)
if duplicates:
raise RenderReferenceError(
f"Duplicate {label} id(s): " + ", ".join(sorted(duplicates))
)
def _mapping_from(value: Any) -> dict[str, Any]:
if isinstance(value, dict):
return dict(value)
to_dict = getattr(value, "to_dict", None)
if callable(to_dict):
return dict(to_dict())
raise RenderReferenceError("Expected a mapping or object with to_dict()")
def _render_kind_from_content_unit(kind: str) -> str:
if kind == "heading":
return "section"
if kind == "code":
return "code-block"
if kind in RENDER_UNIT_KINDS:
return kind
return "custom"
def _digest_text(value: str) -> str: def _digest_text(value: str) -> str:
return "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest() return "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest()

View File

@@ -25,6 +25,7 @@ def test_builtin_extension_registry_lists_query_processors_and_backend():
assert "memory.runtime-adapter-boundary" in ids assert "memory.runtime-adapter-boundary" in ids
assert "memory.context-package" in ids assert "memory.context-package" in ids
assert "render.export-registry" in ids assert "render.export-registry" in ids
assert "render.reference-manifest" in ids
assert "render.fake" in ids assert "render.fake" in ids
assert "source.adapter-registry" in ids assert "source.adapter-registry" in ids
@@ -190,11 +191,16 @@ def test_builtin_render_export_descriptors_expose_contract_boundary():
registry = builtin_extension_registry() registry = builtin_extension_registry()
registry_descriptor = registry.get("render.export-registry") registry_descriptor = registry.get("render.export-registry")
reference_manifest = registry.get("render.reference-manifest")
fake = registry.get("render.fake") fake = registry.get("render.fake")
assert registry_descriptor.kind == "render-export-registry" assert registry_descriptor.kind == "render-export-registry"
assert registry_descriptor.safety["external_process"] is False assert registry_descriptor.safety["external_process"] is False
assert registry_descriptor.metadata["concrete_renderer_execution_required"] is False assert registry_descriptor.metadata["concrete_renderer_execution_required"] is False
assert reference_manifest.kind == "render-reference-contract"
assert reference_manifest.safety["asset_copying"] is False
assert reference_manifest.safety["final_numbering"] is False
assert reference_manifest.metadata["core_performs_asset_copying"] is False
assert fake.kind == "render-export" assert fake.kind == "render-export"
assert fake.safety["external_process"] is False assert fake.safety["external_process"] is False
assert fake.safety["filesystem_write"] is False assert fake.safety["filesystem_write"] is False

View File

@@ -0,0 +1,236 @@
import yaml
from markitect_tool.render import (
RENDER_REFERENCE_MANIFEST_KIND,
RENDER_REFERENCE_SCHEMA_VERSION,
RenderAsset,
RenderAssetManifest,
RenderCrossReference,
RenderExportRequest,
RenderReferenceError,
RenderReferenceManifest,
RenderSourceMap,
RenderSourceSpan,
RenderTocEntry,
RenderUnitReference,
render_asset_id,
render_manifest_id,
render_unit_id,
render_with_adapter,
)
from markitect_tool.source import SourceAsset
def test_render_unit_reference_serializes_with_stable_id_and_no_final_numbering():
span = RenderSourceSpan(
source_path="docs/report.md",
line_start=12,
line_end=16,
selector="#fig-revenue",
)
first = RenderUnitReference(
kind="figure",
label="Figure",
caption="Quarterly revenue",
source_path="docs/report.md",
anchor="fig:revenue",
source_span=span,
content_hash="sha256:figure-content",
ordinal_hint=1,
numbering={"scope": "document", "sequence": "figures"},
)
second = RenderUnitReference(
kind="figure",
label="Figure",
caption="Quarterly revenue",
source_path="docs/report.md",
anchor="fig:revenue",
source_span=span,
content_hash="sha256:figure-content",
ordinal_hint=1,
numbering={"scope": "document", "sequence": "figures"},
)
assert first.unit_id == second.unit_id
assert first.unit_id == render_unit_id(
"figure",
source_path="docs/report.md",
anchor="fig:revenue",
content_hash="sha256:figure-content",
ordinal_hint=1,
title="Quarterly revenue",
)
assert "final_number" not in first.to_dict()
assert RenderUnitReference.from_dict(first.to_dict()).to_dict() == first.to_dict()
def test_render_manifest_models_cross_references_and_toc_before_layout():
section = RenderUnitReference(
kind="section",
title="Results",
anchor="results",
source_path="docs/report.md",
ordinal_hint=1,
)
table = RenderUnitReference(
kind="table",
label="Table",
caption="Quarterly totals",
anchor="tbl:totals",
source_path="docs/report.md",
ordinal_hint=2,
numbering={"scope": "section", "sequence": "tables"},
)
cross_reference = RenderCrossReference(
source_unit_id=section.unit_id,
target_unit_id=table.unit_id,
label="Table",
requested_style="numbered",
fallback_text="Quarterly totals",
)
toc_entry = RenderTocEntry(
unit_id=section.unit_id,
title="Results",
level=1,
order=1,
)
manifest = RenderReferenceManifest(
source_path="docs/report.md",
source_digest="sha256:report",
units=[section, table],
cross_references=[cross_reference],
toc=[toc_entry],
)
data = manifest.to_dict()
assert data["schema_version"] == RENDER_REFERENCE_SCHEMA_VERSION
assert data["kind"] == RENDER_REFERENCE_MANIFEST_KIND
assert data["cross_references"][0]["target_unit_id"] == table.unit_id
assert "final_number" not in data["cross_references"][0]
assert "href" not in data["toc"][0]
assert RenderReferenceManifest.from_dict(data).to_dict() == data
assert render_manifest_id(manifest) == manifest.manifest_id
def test_render_asset_manifest_preserves_source_adapter_attachment_metadata():
source_asset = SourceAsset(
uri="attachments/chart.png",
path="attachments/chart.png",
name="chart.png",
media_type="image/png",
extension=".png",
digest="sha256:chart",
metadata={"source_adapter": "markitect-filter"},
)
render_asset = RenderAsset.from_source_asset(
source_asset,
role="figure-image",
copy_policy="copy",
output_reference="renderer://asset/chart",
)
skipped_asset = RenderAsset(
source_uri="https://example.test/logo.svg",
media_type="image/svg+xml",
extension=".svg",
digest="sha256:logo",
role="brand-logo",
copy_policy="link",
output_reference="renderer://asset/logo",
)
manifest = RenderAssetManifest(
assets=[skipped_asset, render_asset],
source_path="docs/report.md",
source_digest="sha256:report",
)
data = manifest.to_dict()
assert [asset["asset_id"] for asset in data["assets"]] == sorted(
[render_asset.asset_id, skipped_asset.asset_id]
)
assert render_asset.asset_id == render_asset_id(
"attachments/chart.png",
digest="sha256:chart",
role="figure-image",
output_reference="renderer://asset/chart",
)
chart_entry = next(asset for asset in data["assets"] if asset["asset_id"] == render_asset.asset_id)
assert chart_entry["provenance"][0]["digest"].startswith("sha256:")
assert chart_entry["copy_policy"] == "copy"
assert RenderAssetManifest.from_dict(data).to_dict() == data
def test_render_source_maps_are_echoed_by_fake_renderer_metadata():
section = RenderUnitReference(
kind="section",
title="Demo",
anchor="demo",
source_path="docs/demo.md",
)
source_map = RenderSourceMap(
source=RenderSourceSpan(source_path="docs/demo.md", line_start=1, line_end=3),
source_unit_id=section.unit_id,
generated_by="document.function.echo",
function_run_id="run:echo",
render_unit_id=section.unit_id,
artifact_ref="renderer-source",
)
asset = RenderAsset(
source_uri="images/demo.png",
source_path="images/demo.png",
media_type="image/png",
extension=".png",
digest="sha256:demo-image",
role="figure-image",
copy_policy="copy",
output_reference="renderer://asset/demo-image",
)
manifest = RenderReferenceManifest(
units=[section],
source_maps=[source_map],
asset_manifest=RenderAssetManifest(assets=[asset]),
source_path="docs/demo.md",
)
result = render_with_adapter(
RenderExportRequest(
source="# Demo\n\n![Demo](images/demo.png)",
operation="render-artifact",
profile="docs",
source_path="docs/demo.md",
render_manifest=manifest,
)
)
assert result.valid
assert result.metadata["external_renderer_invoked"] is False
assert result.metadata["render_reference_manifest_id"] == manifest.manifest_id
assert result.metadata["render_source_maps"] == 1
assert result.artifacts[0].metadata["asset_manifest_id"] == manifest.asset_manifest.manifest_id
def test_render_reference_manifest_rejects_unknown_cross_reference_targets():
unit = RenderUnitReference(kind="section", title="Known", anchor="known")
try:
RenderReferenceManifest(
units=[unit],
cross_references=[RenderCrossReference(target_unit_id="missing")],
)
except RenderReferenceError as exc:
assert "targets unknown unit" in str(exc)
else:
raise AssertionError("expected unknown cross-reference target to fail")
def test_render_reference_manifest_example_loads_and_roundtrips():
with open("examples/render/render-reference-manifest.yaml", encoding="utf-8") as handle:
data = yaml.safe_load(handle)
manifest = RenderReferenceManifest.from_dict(data)
assert manifest.schema_version == RENDER_REFERENCE_SCHEMA_VERSION
assert manifest.asset_manifest.assets
assert manifest.to_dict()["kind"] == RENDER_REFERENCE_MANIFEST_KIND

View File

@@ -3,10 +3,10 @@ id: MKTT-WP-0021
type: workplan type: workplan
title: "Render Reference And Asset Manifest Contract" title: "Render Reference And Asset Manifest Contract"
domain: markitect domain: markitect
status: todo status: done
owner: markitect-tool owner: markitect-tool
topic_slug: markitect topic_slug: markitect
planning_priority: P2 planning_priority: complete
planning_order: 155 planning_order: 155
depends_on_workplans: depends_on_workplans:
- MKTT-WP-0010 - MKTT-WP-0010
@@ -55,11 +55,31 @@ Renderer packages own:
`markitect-filter` owns only read-side source asset and attachment metadata `markitect-filter` owns only read-side source asset and attachment metadata
needed by normalized Markdown inputs. needed by normalized Markdown inputs.
## Implementation Summary
Completed in `markitect-tool` as a passive render reference and asset manifest
contract:
- `RenderUnitReference`, `RenderCrossReference`, and `RenderTocEntry` model
renderable units, requested cross-references, and TOC planning without final
numbering.
- `RenderAsset`, `RenderAssetManifest`, and `RenderAssetProvenance` model
static assets, copy-policy declarations, output placeholders, and source
adapter attachment provenance without asset copying.
- `RenderSourceMap` and `RenderReferenceManifest` tie Markitect source spans,
generated function outputs, render units, assets, and artifact references
together under `markitect.render.reference.v1`.
- `RenderExportRequest` can carry a passive render manifest; `render.fake`
echoes manifest and asset counts into result/artifact metadata without
invoking a renderer.
- Docs, examples, extension catalog metadata, generated API reference, and
tests were added.
## P21.1 - Define render unit references ## P21.1 - Define render unit references
```task ```task
id: MKTT-WP-0021-T001 id: MKTT-WP-0021-T001
status: todo status: done
priority: high priority: high
state_hub_task_id: "3d33d387-633e-4ffb-962e-1a5061d3db01" state_hub_task_id: "3d33d387-633e-4ffb-962e-1a5061d3db01"
``` ```
@@ -79,7 +99,7 @@ Output: render reference model, serialization tests, and examples.
```task ```task
id: MKTT-WP-0021-T002 id: MKTT-WP-0021-T002
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "4a96e27b-9165-450c-899c-f7af484d9438" state_hub_task_id: "4a96e27b-9165-450c-899c-f7af484d9438"
``` ```
@@ -93,7 +113,7 @@ Output: manifest model and tests that keep final numbering outside core.
```task ```task
id: MKTT-WP-0021-T003 id: MKTT-WP-0021-T003
status: todo status: done
priority: high priority: high
state_hub_task_id: "ba917e45-1912-4bdb-bf3f-5946c20957b2" state_hub_task_id: "ba917e45-1912-4bdb-bf3f-5946c20957b2"
``` ```
@@ -114,7 +134,7 @@ Output: model, examples, and compatibility note for `MKTF-WP-0003`.
```task ```task
id: MKTT-WP-0021-T004 id: MKTT-WP-0021-T004
status: todo status: done
priority: high priority: high
state_hub_task_id: "dd9f1128-af1e-44a8-961b-3aba6104ec9a" state_hub_task_id: "dd9f1128-af1e-44a8-961b-3aba6104ec9a"
``` ```
@@ -128,7 +148,7 @@ Output: source map model, fake-renderer fixture integration, and tests.
```task ```task
id: MKTT-WP-0021-T005 id: MKTT-WP-0021-T005
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "1d472c44-d970-4403-9f0b-18e6192da737" state_hub_task_id: "1d472c44-d970-4403-9f0b-18e6192da737"
``` ```