generated from coulomb/repo-seed
Add Quarkdown render adapter boundary
This commit is contained in:
45
README.md
45
README.md
@@ -1,3 +1,44 @@
|
|||||||
# repo-seed
|
# markitect-quarkdown
|
||||||
|
|
||||||
A git repository template to bootstrap coulomb projects from.
|
`markitect-quarkdown` provides the concrete Quarkdown render/export adapter for
|
||||||
|
Markitect. It keeps Quarkdown runtime assumptions out of `markitect-tool` while
|
||||||
|
still satisfying the Markitect render adapter contract.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This repository owns:
|
||||||
|
|
||||||
|
- `render.quarkdown` adapter descriptor and entry point
|
||||||
|
- Markitect profile to Quarkdown profile mapping
|
||||||
|
- controlled Quarkdown CLI execution plans
|
||||||
|
- Quarkdown permission and runtime dependency mapping
|
||||||
|
- generated artifact validation and checksums
|
||||||
|
- open-reuse integration metadata
|
||||||
|
|
||||||
|
It does not reimplement Quarkdown, fork Quarkdown, or make core Markitect
|
||||||
|
responsible for renderer behavior.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Run tests from this checkout:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src:/home/worsch/markitect-tool/src python3 -m pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
The suite uses fake execution for adapter-boundary tests. A real Quarkdown
|
||||||
|
runtime smoke check skips when `quarkdown` is not installed.
|
||||||
|
|
||||||
|
## Entry Point
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project.entry-points."markitect_tool.render_export_adapters"]
|
||||||
|
quarkdown = "markitect_quarkdown.adapter:quarkdown_adapter_descriptor"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- `docs/adapter-boundary.md`
|
||||||
|
- `docs/profile-matrix.md`
|
||||||
|
- `docs/permissions-and-runtime.md`
|
||||||
|
- `integration/quarkdown.integration.yaml`
|
||||||
|
|||||||
66
docs/adapter-boundary.md
Normal file
66
docs/adapter-boundary.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Markitect Quarkdown Adapter Boundary
|
||||||
|
|
||||||
|
`markitect-quarkdown` provides the concrete `render.quarkdown` adapter for the
|
||||||
|
Markitect render/export contract. Core `markitect-tool` owns the passive
|
||||||
|
request/result/artifact/provenance contracts; this repository owns Quarkdown
|
||||||
|
runtime behavior.
|
||||||
|
|
||||||
|
## Adapter Contract
|
||||||
|
|
||||||
|
The adapter supports:
|
||||||
|
|
||||||
|
- `inspect-profile`
|
||||||
|
- `export-source`
|
||||||
|
- `render-artifact`
|
||||||
|
|
||||||
|
`inspect-profile` and `export-source` do not invoke Quarkdown. `render-artifact`
|
||||||
|
requires a Quarkdown CLI plus runtime dependencies.
|
||||||
|
|
||||||
|
The adapter descriptor declares filesystem writes, external process execution,
|
||||||
|
native renderer dependency, and permission-controlled network behavior. Callers
|
||||||
|
can block render execution through Markitect request policy:
|
||||||
|
|
||||||
|
```python
|
||||||
|
RenderExportRequest(
|
||||||
|
source="# Demo",
|
||||||
|
operation="render-artifact",
|
||||||
|
profile="pdf",
|
||||||
|
policy={"external_process": False},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Execution Plan
|
||||||
|
|
||||||
|
`build_quarkdown_execution_plan` produces an inspectable command plan with:
|
||||||
|
|
||||||
|
- Quarkdown command path
|
||||||
|
- source file path
|
||||||
|
- output directory
|
||||||
|
- expected artifact path
|
||||||
|
- Markitect profile
|
||||||
|
- Quarkdown document type
|
||||||
|
- output format
|
||||||
|
- permission flags
|
||||||
|
- runtime dependency notes
|
||||||
|
|
||||||
|
The default output directory is `quarkdown-output`. The default permission
|
||||||
|
allow-list is `project-read`, and network access is denied by default.
|
||||||
|
|
||||||
|
## Structured Failure
|
||||||
|
|
||||||
|
The adapter returns `RenderExportResult` diagnostics instead of raising for
|
||||||
|
expected render failures:
|
||||||
|
|
||||||
|
- `render.quarkdown.runtime_missing`
|
||||||
|
- `render.quarkdown.execution_failed`
|
||||||
|
- `render.quarkdown.artifact_missing`
|
||||||
|
- `render.quarkdown.artifact_extension`
|
||||||
|
- `render.quarkdown.artifact_empty`
|
||||||
|
- `render.quarkdown.capability_blocked`
|
||||||
|
|
||||||
|
## Artifact Validation
|
||||||
|
|
||||||
|
`validate_quarkdown_artifact` validates that the expected artifact exists, has
|
||||||
|
the expected extension, is non-empty, and receives a sha256 digest. The result
|
||||||
|
is a Markitect `RenderArtifact` with source-to-artifact provenance generated by
|
||||||
|
the adapter.
|
||||||
45
docs/permissions-and-runtime.md
Normal file
45
docs/permissions-and-runtime.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Permissions And Runtime
|
||||||
|
|
||||||
|
Quarkdown 2.x introduced compile-time permissions. The Markitect adapter treats
|
||||||
|
those as renderer-local execution controls, not as enterprise authorization.
|
||||||
|
|
||||||
|
## Permission Mapping
|
||||||
|
|
||||||
|
The adapter recognizes Quarkdown permission names as CLI flags:
|
||||||
|
|
||||||
|
- `project-read`
|
||||||
|
- `global-read`
|
||||||
|
- `network`
|
||||||
|
- `native-content`
|
||||||
|
- `all`
|
||||||
|
|
||||||
|
The default allow-list is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
project-read
|
||||||
|
```
|
||||||
|
|
||||||
|
The adapter denies `network` by default unless callers explicitly opt out with
|
||||||
|
`deny_network: false`.
|
||||||
|
|
||||||
|
## Runtime Dependencies
|
||||||
|
|
||||||
|
The descriptor declares these optional runtime assumptions:
|
||||||
|
|
||||||
|
- Quarkdown 2.x CLI
|
||||||
|
- Java 17+
|
||||||
|
- Node.js, npm, and Puppeteer for PDF export
|
||||||
|
|
||||||
|
The test suite does not require those tools. It uses a fake runner for command
|
||||||
|
boundary tests and skips the real-runtime smoke check when `quarkdown` is not
|
||||||
|
available on `PATH`.
|
||||||
|
|
||||||
|
## Output Conventions
|
||||||
|
|
||||||
|
The default output directory is `quarkdown-output`. Expected artifact names are
|
||||||
|
derived from `artifact_stem` plus the profile extension, for example
|
||||||
|
`demo.pdf` for `profile=pdf`.
|
||||||
|
|
||||||
|
This repository owns those conventions and compatibility monitoring. Core
|
||||||
|
`markitect-tool` only sees `RenderExportResult`, `RenderArtifact`, diagnostics,
|
||||||
|
and provenance.
|
||||||
32
docs/profile-matrix.md
Normal file
32
docs/profile-matrix.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Quarkdown Profile Matrix
|
||||||
|
|
||||||
|
The adapter maps Markitect render profiles onto Quarkdown document/output
|
||||||
|
intent. The CLI command is still configurable because Quarkdown distribution
|
||||||
|
and command spelling may change across 2.x releases.
|
||||||
|
|
||||||
|
| Markitect profile | Quarkdown document type | Output format | Artifact media type | Extension |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `plain` | `plain` | `txt` | `text/plain` | `.txt` |
|
||||||
|
| `docs` | `docs` | `html` | `text/html` | `.html` |
|
||||||
|
| `slides` | `slides` | `html` | `text/html` | `.html` |
|
||||||
|
| `paged` | `paged` | `pdf` | `application/pdf` | `.pdf` |
|
||||||
|
| `static-site` | `docs` | `html` | `text/html` | `.html` |
|
||||||
|
| `pdf` | `paged` | `pdf` | `application/pdf` | `.pdf` |
|
||||||
|
|
||||||
|
PDF-oriented profiles require Quarkdown's PDF runtime path, including Java and
|
||||||
|
Node.js/npm/Puppeteer assumptions.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
The adapter accepts these stable Markitect-facing options:
|
||||||
|
|
||||||
|
- `command`: Quarkdown command name or path, default `quarkdown`
|
||||||
|
- `workspace`: temporary working directory override
|
||||||
|
- `output_dir`: output directory, default `quarkdown-output`
|
||||||
|
- `artifact_stem`: expected artifact basename, default from source stem
|
||||||
|
- `permissions`: Quarkdown permission allow-list, default `project-read`
|
||||||
|
- `deny_network`: append a network deny flag, default `true`
|
||||||
|
- `dry_run`: return an execution-plan artifact without invoking Quarkdown
|
||||||
|
|
||||||
|
Renderer packages and automation may add extra options, but they should remain
|
||||||
|
under this adapter boundary rather than leaking into `markitect-tool`.
|
||||||
17
examples/quarkdown-render-request.yaml
Normal file
17
examples/quarkdown-render-request.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version: markitect.render.export.v1
|
||||||
|
operation: render-artifact
|
||||||
|
profile: pdf
|
||||||
|
source_path: docs/demo.md
|
||||||
|
source: |
|
||||||
|
# Demo
|
||||||
|
|
||||||
|
This is a Markitect-compatible source document handed to Quarkdown.
|
||||||
|
options:
|
||||||
|
artifact_stem: demo
|
||||||
|
output_dir: quarkdown-output
|
||||||
|
permissions:
|
||||||
|
- project-read
|
||||||
|
deny_network: true
|
||||||
|
metadata:
|
||||||
|
adapter: render.quarkdown
|
||||||
|
real_runtime_required: true
|
||||||
66
integration/quarkdown.integration.yaml
Normal file
66
integration/quarkdown.integration.yaml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
schema_version: open-reuse.integration.v1
|
||||||
|
id: markitect-quarkdown
|
||||||
|
name: Markitect Quarkdown Render Adapter
|
||||||
|
upstream:
|
||||||
|
name: Quarkdown
|
||||||
|
project_url: https://github.com/iamgio/quarkdown
|
||||||
|
homepage: https://quarkdown.com/
|
||||||
|
version_policy: quarkdown-2.x
|
||||||
|
reuse:
|
||||||
|
primary_reuse_mode: adapter
|
||||||
|
secondary_reuse_modes:
|
||||||
|
- dependency-reuse
|
||||||
|
- cli-boundary
|
||||||
|
boundary:
|
||||||
|
local_adapter: markitect_quarkdown.adapter.MarkitectQuarkdownAdapter
|
||||||
|
markitect_adapter_id: render.quarkdown
|
||||||
|
entry_point_group: markitect_tool.render_export_adapters
|
||||||
|
core_contracts:
|
||||||
|
- markitect.render.export.v1
|
||||||
|
- markitect.render.reference.v1
|
||||||
|
runtime:
|
||||||
|
required_for_real_render:
|
||||||
|
- Quarkdown 2.x CLI
|
||||||
|
- Java 17+
|
||||||
|
required_for_pdf:
|
||||||
|
- Node.js
|
||||||
|
- npm
|
||||||
|
- Puppeteer
|
||||||
|
permissions:
|
||||||
|
known_flags:
|
||||||
|
- project-read
|
||||||
|
- global-read
|
||||||
|
- network
|
||||||
|
- native-content
|
||||||
|
- all
|
||||||
|
default_allow:
|
||||||
|
- project-read
|
||||||
|
default_deny:
|
||||||
|
- network
|
||||||
|
validation:
|
||||||
|
harness: python3 -m pytest
|
||||||
|
skip_without_runtime: true
|
||||||
|
checks:
|
||||||
|
- descriptor compatibility
|
||||||
|
- profile matrix
|
||||||
|
- command-plan construction
|
||||||
|
- fake-runner artifact validation
|
||||||
|
- missing runtime diagnostic
|
||||||
|
- real Quarkdown smoke test when CLI is installed
|
||||||
|
risks:
|
||||||
|
sensitivity:
|
||||||
|
- CLI command changes
|
||||||
|
- permission model changes
|
||||||
|
- output directory changes
|
||||||
|
- artifact naming changes
|
||||||
|
- PDF export dependency changes
|
||||||
|
- Java runtime changes
|
||||||
|
- Node/npm/Puppeteer changes
|
||||||
|
- license or security advisories
|
||||||
|
maintenance:
|
||||||
|
escalation_conditions:
|
||||||
|
- upstream Quarkdown release
|
||||||
|
- validation failure
|
||||||
|
- dependency requirement change
|
||||||
|
- permission behavior change
|
||||||
|
- generated artifact convention change
|
||||||
29
pyproject.toml
Normal file
29
pyproject.toml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=69"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "markitect-quarkdown"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Quarkdown render/export adapter for Markitect"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
dependencies = [
|
||||||
|
"markitect-tool @ file:///home/worsch/markitect-tool",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.entry-points."markitect_tool.render_export_adapters"]
|
||||||
|
quarkdown = "markitect_quarkdown.adapter:quarkdown_adapter_descriptor"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["src", "../markitect-tool/src"]
|
||||||
29
src/markitect_quarkdown/__init__.py
Normal file
29
src/markitect_quarkdown/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"""Quarkdown render/export adapter for Markitect."""
|
||||||
|
|
||||||
|
from markitect_quarkdown.adapter import (
|
||||||
|
MARKITECT_QUARKDOWN_VERSION_POLICY,
|
||||||
|
QUARKDOWN_PROFILE_MATRIX,
|
||||||
|
MarkitectQuarkdownAdapter,
|
||||||
|
QuarkdownExecutionPlan,
|
||||||
|
QuarkdownProcessResult,
|
||||||
|
QuarkdownProfileMapping,
|
||||||
|
build_quarkdown_execution_plan,
|
||||||
|
export_quarkdown_source,
|
||||||
|
quarkdown_adapter_descriptor,
|
||||||
|
quarkdown_profile_for,
|
||||||
|
validate_quarkdown_artifact,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"MARKITECT_QUARKDOWN_VERSION_POLICY",
|
||||||
|
"QUARKDOWN_PROFILE_MATRIX",
|
||||||
|
"MarkitectQuarkdownAdapter",
|
||||||
|
"QuarkdownExecutionPlan",
|
||||||
|
"QuarkdownProcessResult",
|
||||||
|
"QuarkdownProfileMapping",
|
||||||
|
"build_quarkdown_execution_plan",
|
||||||
|
"export_quarkdown_source",
|
||||||
|
"quarkdown_adapter_descriptor",
|
||||||
|
"quarkdown_profile_for",
|
||||||
|
"validate_quarkdown_artifact",
|
||||||
|
]
|
||||||
631
src/markitect_quarkdown/adapter.py
Normal file
631
src/markitect_quarkdown/adapter.py
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
"""Concrete Quarkdown render/export adapter boundary."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from markitect_tool.diagnostics import Diagnostic, SourceLocation, has_error
|
||||||
|
from markitect_tool.extension import OptionalDependency
|
||||||
|
from markitect_tool.render import (
|
||||||
|
RenderArtifact,
|
||||||
|
RenderExportAdapterDescriptor,
|
||||||
|
RenderExportRequest,
|
||||||
|
RenderExportResult,
|
||||||
|
RenderProvenance,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
MARKITECT_QUARKDOWN_VERSION_POLICY = "quarkdown-2.x"
|
||||||
|
QUARKDOWN_ADAPTER_ID = "render.quarkdown"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class QuarkdownProfileMapping:
|
||||||
|
"""Mapping from a Markitect render profile to Quarkdown invocation intent."""
|
||||||
|
|
||||||
|
markitect_profile: str
|
||||||
|
quarkdown_document_type: str
|
||||||
|
output_format: str
|
||||||
|
artifact_media_type: str
|
||||||
|
artifact_extension: str
|
||||||
|
pdf_export: bool = False
|
||||||
|
notes: str | None = None
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return _drop_empty(asdict(self))
|
||||||
|
|
||||||
|
|
||||||
|
QUARKDOWN_PROFILE_MATRIX: dict[str, QuarkdownProfileMapping] = {
|
||||||
|
"plain": QuarkdownProfileMapping(
|
||||||
|
markitect_profile="plain",
|
||||||
|
quarkdown_document_type="plain",
|
||||||
|
output_format="txt",
|
||||||
|
artifact_media_type="text/plain",
|
||||||
|
artifact_extension=".txt",
|
||||||
|
),
|
||||||
|
"docs": QuarkdownProfileMapping(
|
||||||
|
markitect_profile="docs",
|
||||||
|
quarkdown_document_type="docs",
|
||||||
|
output_format="html",
|
||||||
|
artifact_media_type="text/html",
|
||||||
|
artifact_extension=".html",
|
||||||
|
),
|
||||||
|
"slides": QuarkdownProfileMapping(
|
||||||
|
markitect_profile="slides",
|
||||||
|
quarkdown_document_type="slides",
|
||||||
|
output_format="html",
|
||||||
|
artifact_media_type="text/html",
|
||||||
|
artifact_extension=".html",
|
||||||
|
),
|
||||||
|
"paged": QuarkdownProfileMapping(
|
||||||
|
markitect_profile="paged",
|
||||||
|
quarkdown_document_type="paged",
|
||||||
|
output_format="pdf",
|
||||||
|
artifact_media_type="application/pdf",
|
||||||
|
artifact_extension=".pdf",
|
||||||
|
pdf_export=True,
|
||||||
|
),
|
||||||
|
"static-site": QuarkdownProfileMapping(
|
||||||
|
markitect_profile="static-site",
|
||||||
|
quarkdown_document_type="docs",
|
||||||
|
output_format="html",
|
||||||
|
artifact_media_type="text/html",
|
||||||
|
artifact_extension=".html",
|
||||||
|
notes="Quarkdown docs output plus static/public asset handling.",
|
||||||
|
),
|
||||||
|
"pdf": QuarkdownProfileMapping(
|
||||||
|
markitect_profile="pdf",
|
||||||
|
quarkdown_document_type="paged",
|
||||||
|
output_format="pdf",
|
||||||
|
artifact_media_type="application/pdf",
|
||||||
|
artifact_extension=".pdf",
|
||||||
|
pdf_export=True,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class QuarkdownExecutionPlan:
|
||||||
|
"""Concrete, inspectable Quarkdown CLI execution plan."""
|
||||||
|
|
||||||
|
command: list[str]
|
||||||
|
source_path: str
|
||||||
|
output_dir: str
|
||||||
|
expected_artifact_path: str
|
||||||
|
profile: str
|
||||||
|
quarkdown_document_type: str
|
||||||
|
output_format: str
|
||||||
|
permissions: list[str] = field(default_factory=list)
|
||||||
|
environment: dict[str, str] = field(default_factory=dict)
|
||||||
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return _drop_empty(asdict(self))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class QuarkdownProcessResult:
|
||||||
|
"""Result returned by a Quarkdown command runner."""
|
||||||
|
|
||||||
|
returncode: int
|
||||||
|
stdout: str = ""
|
||||||
|
stderr: str = ""
|
||||||
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def successful(self) -> bool:
|
||||||
|
return self.returncode == 0
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return _drop_empty(asdict(self))
|
||||||
|
|
||||||
|
|
||||||
|
QuarkdownCommandResolver = Callable[[str], str | None]
|
||||||
|
QuarkdownCommandRunner = Callable[[QuarkdownExecutionPlan], QuarkdownProcessResult]
|
||||||
|
|
||||||
|
|
||||||
|
class MarkitectQuarkdownAdapter:
|
||||||
|
"""Render/export adapter that invokes Quarkdown through a controlled boundary."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
command: str = "quarkdown",
|
||||||
|
command_resolver: QuarkdownCommandResolver | None = None,
|
||||||
|
runner: QuarkdownCommandRunner | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.command = command
|
||||||
|
self.command_resolver = command_resolver or shutil.which
|
||||||
|
self.runner = runner or _subprocess_runner
|
||||||
|
self.descriptor = quarkdown_adapter_descriptor()
|
||||||
|
|
||||||
|
def render(self, request: RenderExportRequest) -> RenderExportResult:
|
||||||
|
adapter_info = _adapter_info()
|
||||||
|
diagnostics = _validate_request(request)
|
||||||
|
if diagnostics:
|
||||||
|
return RenderExportResult(
|
||||||
|
adapter=adapter_info,
|
||||||
|
operation=request.operation,
|
||||||
|
profile=request.profile,
|
||||||
|
diagnostics=diagnostics,
|
||||||
|
)
|
||||||
|
|
||||||
|
mapping = quarkdown_profile_for(request.profile)
|
||||||
|
exported_source = export_quarkdown_source(request, mapping=mapping)
|
||||||
|
source_artifact = RenderArtifact.from_content(
|
||||||
|
exported_source,
|
||||||
|
role="renderer-source",
|
||||||
|
media_type="text/markdown",
|
||||||
|
metadata={
|
||||||
|
"quarkdown_document_type": mapping.quarkdown_document_type,
|
||||||
|
"profile": request.profile,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
provenance = [
|
||||||
|
RenderProvenance(
|
||||||
|
operation=request.operation,
|
||||||
|
adapter_id=QUARKDOWN_ADAPTER_ID,
|
||||||
|
profile=request.profile,
|
||||||
|
source_path=request.source_path,
|
||||||
|
source_digest=_digest_text(request.source),
|
||||||
|
artifact_id=source_artifact.artifact_id,
|
||||||
|
metadata={"version_policy": MARKITECT_QUARKDOWN_VERSION_POLICY},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
if request.operation == "inspect-profile":
|
||||||
|
return RenderExportResult(
|
||||||
|
adapter=adapter_info,
|
||||||
|
operation=request.operation,
|
||||||
|
profile=request.profile,
|
||||||
|
provenance=provenance,
|
||||||
|
metadata={
|
||||||
|
"profile": mapping.to_dict(),
|
||||||
|
"option_schema": _option_schema(),
|
||||||
|
"version_policy": MARKITECT_QUARKDOWN_VERSION_POLICY,
|
||||||
|
"external_renderer_invoked": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.operation == "export-source":
|
||||||
|
return RenderExportResult(
|
||||||
|
adapter=adapter_info,
|
||||||
|
operation=request.operation,
|
||||||
|
profile=request.profile,
|
||||||
|
artifacts=[source_artifact],
|
||||||
|
exported_source=exported_source,
|
||||||
|
provenance=provenance,
|
||||||
|
metadata={
|
||||||
|
"external_renderer_invoked": False,
|
||||||
|
"quarkdown_document_type": mapping.quarkdown_document_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
command_path = self.command_resolver(str(request.options.get("command", self.command)))
|
||||||
|
if not command_path:
|
||||||
|
return RenderExportResult(
|
||||||
|
adapter=adapter_info,
|
||||||
|
operation=request.operation,
|
||||||
|
profile=request.profile,
|
||||||
|
artifacts=[source_artifact],
|
||||||
|
exported_source=exported_source,
|
||||||
|
provenance=provenance,
|
||||||
|
diagnostics=[
|
||||||
|
_diagnostic(
|
||||||
|
"render.quarkdown.runtime_missing",
|
||||||
|
"Quarkdown CLI is not available on PATH.",
|
||||||
|
details={
|
||||||
|
"command": request.options.get("command", self.command),
|
||||||
|
"runtime_dependencies": ["Quarkdown CLI", "Java 17+", "Node.js/npm/Puppeteer for PDF"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
metadata={"external_renderer_invoked": False},
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="markitect-quarkdown-") as temp_dir:
|
||||||
|
workspace = Path(request.options.get("workspace") or temp_dir)
|
||||||
|
source_path = _write_renderer_source(workspace, exported_source)
|
||||||
|
output_dir = Path(request.options.get("output_dir") or workspace / "quarkdown-output")
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
plan = build_quarkdown_execution_plan(
|
||||||
|
request,
|
||||||
|
source_path=source_path,
|
||||||
|
output_dir=output_dir,
|
||||||
|
command=command_path,
|
||||||
|
mapping=mapping,
|
||||||
|
)
|
||||||
|
if request.options.get("dry_run"):
|
||||||
|
plan_artifact = RenderArtifact.from_content(
|
||||||
|
json.dumps(plan.to_dict(), indent=2, sort_keys=True),
|
||||||
|
role="execution-plan",
|
||||||
|
media_type="application/json",
|
||||||
|
metadata={"external_renderer_invoked": False},
|
||||||
|
)
|
||||||
|
return RenderExportResult(
|
||||||
|
adapter=adapter_info,
|
||||||
|
operation=request.operation,
|
||||||
|
profile=request.profile,
|
||||||
|
artifacts=[source_artifact, plan_artifact],
|
||||||
|
exported_source=exported_source,
|
||||||
|
provenance=provenance,
|
||||||
|
metadata={"plan": plan.to_dict(), "external_renderer_invoked": False},
|
||||||
|
)
|
||||||
|
|
||||||
|
process_result = self.runner(plan)
|
||||||
|
if not process_result.successful:
|
||||||
|
return RenderExportResult(
|
||||||
|
adapter=adapter_info,
|
||||||
|
operation=request.operation,
|
||||||
|
profile=request.profile,
|
||||||
|
artifacts=[source_artifact],
|
||||||
|
exported_source=exported_source,
|
||||||
|
provenance=provenance,
|
||||||
|
diagnostics=[
|
||||||
|
_diagnostic(
|
||||||
|
"render.quarkdown.execution_failed",
|
||||||
|
"Quarkdown CLI execution failed.",
|
||||||
|
details=process_result.to_dict() | {"plan": plan.to_dict()},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
metadata={"external_renderer_invoked": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
artifact, artifact_diagnostics = validate_quarkdown_artifact(plan)
|
||||||
|
if artifact:
|
||||||
|
provenance = [
|
||||||
|
RenderProvenance(
|
||||||
|
operation=request.operation,
|
||||||
|
adapter_id=QUARKDOWN_ADAPTER_ID,
|
||||||
|
profile=request.profile,
|
||||||
|
source_path=request.source_path,
|
||||||
|
source_digest=_digest_text(request.source),
|
||||||
|
artifact_id=artifact.artifact_id,
|
||||||
|
metadata={
|
||||||
|
"version_policy": MARKITECT_QUARKDOWN_VERSION_POLICY,
|
||||||
|
"plan": plan.to_dict(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return RenderExportResult(
|
||||||
|
adapter=adapter_info,
|
||||||
|
operation=request.operation,
|
||||||
|
profile=request.profile,
|
||||||
|
artifacts=[source_artifact, *([artifact] if artifact else [])],
|
||||||
|
exported_source=exported_source,
|
||||||
|
diagnostics=artifact_diagnostics,
|
||||||
|
provenance=provenance,
|
||||||
|
metadata={
|
||||||
|
"external_renderer_invoked": True,
|
||||||
|
"process": process_result.to_dict(),
|
||||||
|
"plan": plan.to_dict(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def quarkdown_adapter_descriptor() -> RenderExportAdapterDescriptor:
|
||||||
|
"""Return the Quarkdown render/export adapter descriptor."""
|
||||||
|
|
||||||
|
return RenderExportAdapterDescriptor(
|
||||||
|
id=QUARKDOWN_ADAPTER_ID,
|
||||||
|
version="1",
|
||||||
|
name="Quarkdown Render Adapter",
|
||||||
|
summary="Concrete Quarkdown CLI render/export adapter boundary for Markitect.",
|
||||||
|
operations=["inspect-profile", "export-source", "render-artifact"],
|
||||||
|
input_contracts=["Markdown", "RenderReferenceManifest", "DocumentFunctionEvaluationResult"],
|
||||||
|
output_profiles=list(QUARKDOWN_PROFILE_MATRIX),
|
||||||
|
artifact_media_types=sorted({mapping.artifact_media_type for mapping in QUARKDOWN_PROFILE_MATRIX.values()}),
|
||||||
|
factory=MarkitectQuarkdownAdapter,
|
||||||
|
optional_dependencies=[
|
||||||
|
OptionalDependency(
|
||||||
|
name="quarkdown-cli",
|
||||||
|
package="Quarkdown 2.x",
|
||||||
|
required=False,
|
||||||
|
purpose="Compile Quarkdown source to HTML/PDF/slides/docs artifacts.",
|
||||||
|
),
|
||||||
|
OptionalDependency(
|
||||||
|
name="java",
|
||||||
|
package="Java 17+",
|
||||||
|
required=False,
|
||||||
|
purpose="Quarkdown runtime.",
|
||||||
|
),
|
||||||
|
OptionalDependency(
|
||||||
|
name="node",
|
||||||
|
package="Node.js/npm/Puppeteer",
|
||||||
|
required=False,
|
||||||
|
purpose="PDF export through Quarkdown's HTML rendering pipeline.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
option_schema=_option_schema(),
|
||||||
|
safety={
|
||||||
|
"filesystem_read": True,
|
||||||
|
"filesystem_write": True,
|
||||||
|
"external_process": True,
|
||||||
|
"network": "permission-controlled",
|
||||||
|
"native_renderer_dependency": True,
|
||||||
|
"assisted_generation": False,
|
||||||
|
"publication_side_effect": False,
|
||||||
|
},
|
||||||
|
quality_profile={
|
||||||
|
"renderer": "quarkdown-2.x",
|
||||||
|
"pdf_export": "requires-node-npm-puppeteer",
|
||||||
|
"runtime_dependencies_optional": True,
|
||||||
|
"tests_skip_without_runtime": True,
|
||||||
|
},
|
||||||
|
metadata={
|
||||||
|
"version_policy": MARKITECT_QUARKDOWN_VERSION_POLICY,
|
||||||
|
"profile_matrix": [mapping.to_dict() for mapping in QUARKDOWN_PROFILE_MATRIX.values()],
|
||||||
|
"permission_flags": ["project-read", "global-read", "network", "native-content", "all"],
|
||||||
|
"default_output_dir": "quarkdown-output",
|
||||||
|
"open_reuse_integration": "integration/quarkdown.integration.yaml",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def quarkdown_profile_for(profile: str) -> QuarkdownProfileMapping:
|
||||||
|
"""Return Quarkdown mapping for a Markitect render profile."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return QUARKDOWN_PROFILE_MATRIX[profile]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise ValueError(f"Unsupported Markitect render profile `{profile}`") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def export_quarkdown_source(
|
||||||
|
request: RenderExportRequest,
|
||||||
|
*,
|
||||||
|
mapping: QuarkdownProfileMapping | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Return Quarkdown source exported from a Markitect render request."""
|
||||||
|
|
||||||
|
mapping = mapping or quarkdown_profile_for(request.profile)
|
||||||
|
header = [
|
||||||
|
"<!-- generated-by: markitect-quarkdown -->",
|
||||||
|
f"<!-- markitect-profile: {request.profile} -->",
|
||||||
|
f"<!-- quarkdown-document-type: {mapping.quarkdown_document_type} -->",
|
||||||
|
]
|
||||||
|
manifest_id = _manifest_id(request.render_manifest)
|
||||||
|
if manifest_id:
|
||||||
|
header.append(f"<!-- render-reference-manifest: {manifest_id} -->")
|
||||||
|
return "\n".join(header) + "\n\n" + request.source
|
||||||
|
|
||||||
|
|
||||||
|
def build_quarkdown_execution_plan(
|
||||||
|
request: RenderExportRequest,
|
||||||
|
*,
|
||||||
|
source_path: str | Path,
|
||||||
|
output_dir: str | Path,
|
||||||
|
command: str = "quarkdown",
|
||||||
|
mapping: QuarkdownProfileMapping | None = None,
|
||||||
|
) -> QuarkdownExecutionPlan:
|
||||||
|
"""Build an inspectable Quarkdown CLI command plan."""
|
||||||
|
|
||||||
|
mapping = mapping or quarkdown_profile_for(request.profile)
|
||||||
|
source_file = Path(source_path)
|
||||||
|
target_dir = Path(output_dir)
|
||||||
|
artifact_stem = str(request.options.get("artifact_stem") or source_file.stem or "document")
|
||||||
|
expected_artifact = target_dir / f"{artifact_stem}{mapping.artifact_extension}"
|
||||||
|
permissions = _permissions_for(request)
|
||||||
|
command_args = [
|
||||||
|
command,
|
||||||
|
"compile",
|
||||||
|
str(source_file),
|
||||||
|
"--output",
|
||||||
|
str(target_dir),
|
||||||
|
"--format",
|
||||||
|
mapping.output_format,
|
||||||
|
"--type",
|
||||||
|
mapping.quarkdown_document_type,
|
||||||
|
]
|
||||||
|
for permission in permissions:
|
||||||
|
command_args.extend(["--allow", permission])
|
||||||
|
if request.options.get("deny_network", True):
|
||||||
|
command_args.extend(["--deny", "network"])
|
||||||
|
return QuarkdownExecutionPlan(
|
||||||
|
command=command_args,
|
||||||
|
source_path=str(source_file),
|
||||||
|
output_dir=str(target_dir),
|
||||||
|
expected_artifact_path=str(expected_artifact),
|
||||||
|
profile=request.profile,
|
||||||
|
quarkdown_document_type=mapping.quarkdown_document_type,
|
||||||
|
output_format=mapping.output_format,
|
||||||
|
permissions=permissions,
|
||||||
|
environment={
|
||||||
|
"requires_java": "17+",
|
||||||
|
"requires_node_for_pdf": str(mapping.pdf_export).lower(),
|
||||||
|
},
|
||||||
|
metadata={
|
||||||
|
"pdf_export": mapping.pdf_export,
|
||||||
|
"version_policy": MARKITECT_QUARKDOWN_VERSION_POLICY,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_quarkdown_artifact(
|
||||||
|
plan: QuarkdownExecutionPlan,
|
||||||
|
) -> tuple[RenderArtifact | None, list[Diagnostic]]:
|
||||||
|
"""Validate the expected Quarkdown artifact and return Markitect metadata."""
|
||||||
|
|
||||||
|
path = Path(plan.expected_artifact_path)
|
||||||
|
mapping = quarkdown_profile_for(plan.profile)
|
||||||
|
diagnostics: list[Diagnostic] = []
|
||||||
|
if not path.exists() or not path.is_file():
|
||||||
|
diagnostics.append(
|
||||||
|
_diagnostic(
|
||||||
|
"render.quarkdown.artifact_missing",
|
||||||
|
"Expected Quarkdown artifact was not produced.",
|
||||||
|
details={"path": str(path), "plan": plan.to_dict()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None, diagnostics
|
||||||
|
if path.suffix.lower() != mapping.artifact_extension:
|
||||||
|
diagnostics.append(
|
||||||
|
_diagnostic(
|
||||||
|
"render.quarkdown.artifact_extension",
|
||||||
|
"Quarkdown artifact extension does not match the requested profile.",
|
||||||
|
details={
|
||||||
|
"path": str(path),
|
||||||
|
"expected_extension": mapping.artifact_extension,
|
||||||
|
"actual_extension": path.suffix.lower(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if path.stat().st_size <= 0:
|
||||||
|
diagnostics.append(
|
||||||
|
_diagnostic(
|
||||||
|
"render.quarkdown.artifact_empty",
|
||||||
|
"Quarkdown artifact is empty.",
|
||||||
|
details={"path": str(path)},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
artifact = RenderArtifact(
|
||||||
|
artifact_id=f"artifact:{_file_digest(path).removeprefix('sha256:')[:16]}",
|
||||||
|
role="rendered-artifact",
|
||||||
|
media_type=mapping.artifact_media_type,
|
||||||
|
path=str(path),
|
||||||
|
digest=_file_digest(path),
|
||||||
|
metadata={
|
||||||
|
"quarkdown_document_type": plan.quarkdown_document_type,
|
||||||
|
"output_format": plan.output_format,
|
||||||
|
"version_policy": MARKITECT_QUARKDOWN_VERSION_POLICY,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return artifact, diagnostics
|
||||||
|
|
||||||
|
|
||||||
|
def _option_schema() -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {"type": "string", "default": "quarkdown"},
|
||||||
|
"workspace": {"type": "string"},
|
||||||
|
"output_dir": {"type": "string", "default": "quarkdown-output"},
|
||||||
|
"artifact_stem": {"type": "string", "default": "document"},
|
||||||
|
"permissions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"default": ["project-read"],
|
||||||
|
},
|
||||||
|
"deny_network": {"type": "boolean", "default": True},
|
||||||
|
"dry_run": {"type": "boolean", "default": False},
|
||||||
|
},
|
||||||
|
"additionalProperties": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _permissions_for(request: RenderExportRequest) -> list[str]:
|
||||||
|
permissions = request.options.get("permissions", ["project-read"])
|
||||||
|
if isinstance(permissions, str):
|
||||||
|
permissions = [permissions]
|
||||||
|
return [str(permission) for permission in permissions]
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_request(request: RenderExportRequest) -> list[Diagnostic]:
|
||||||
|
diagnostics: list[Diagnostic] = []
|
||||||
|
if request.profile not in QUARKDOWN_PROFILE_MATRIX:
|
||||||
|
diagnostics.append(
|
||||||
|
_diagnostic(
|
||||||
|
"render.quarkdown.profile_unsupported",
|
||||||
|
f"Quarkdown adapter does not support profile `{request.profile}`.",
|
||||||
|
details={"profile": request.profile},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if request.operation not in {"inspect-profile", "export-source", "render-artifact"}:
|
||||||
|
diagnostics.append(
|
||||||
|
_diagnostic(
|
||||||
|
"render.quarkdown.operation_unsupported",
|
||||||
|
f"Quarkdown adapter does not support operation `{request.operation}`.",
|
||||||
|
details={"operation": request.operation},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not request.source and request.operation != "inspect-profile":
|
||||||
|
diagnostics.append(
|
||||||
|
_diagnostic("render.quarkdown.source_missing", "Quarkdown render source cannot be empty.")
|
||||||
|
)
|
||||||
|
if request.operation == "render-artifact":
|
||||||
|
blocked = []
|
||||||
|
policy = request.policy or {}
|
||||||
|
for capability in ("filesystem_write", "external_process", "native_renderer_dependency"):
|
||||||
|
if policy.get(capability) is False:
|
||||||
|
blocked.append(capability)
|
||||||
|
if blocked:
|
||||||
|
diagnostics.append(
|
||||||
|
_diagnostic(
|
||||||
|
"render.quarkdown.capability_blocked",
|
||||||
|
"Quarkdown render execution requires blocked capabilities.",
|
||||||
|
details={"capabilities": blocked},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return diagnostics
|
||||||
|
|
||||||
|
|
||||||
|
def _write_renderer_source(workspace: Path, source: str) -> Path:
|
||||||
|
workspace.mkdir(parents=True, exist_ok=True)
|
||||||
|
source_path = workspace / "markitect-source.qd"
|
||||||
|
source_path.write_text(source, encoding="utf-8")
|
||||||
|
return source_path
|
||||||
|
|
||||||
|
|
||||||
|
def _subprocess_runner(plan: QuarkdownExecutionPlan) -> QuarkdownProcessResult:
|
||||||
|
completed = subprocess.run(
|
||||||
|
plan.command,
|
||||||
|
cwd=Path(plan.output_dir).parent,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return QuarkdownProcessResult(
|
||||||
|
returncode=completed.returncode,
|
||||||
|
stdout=completed.stdout,
|
||||||
|
stderr=completed.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _adapter_info() -> dict[str, str]:
|
||||||
|
return {"id": QUARKDOWN_ADAPTER_ID, "version": "1", "name": "Quarkdown Render Adapter"}
|
||||||
|
|
||||||
|
|
||||||
|
def _manifest_id(manifest: Any) -> str | None:
|
||||||
|
if manifest is None:
|
||||||
|
return None
|
||||||
|
if hasattr(manifest, "to_dict"):
|
||||||
|
data = manifest.to_dict()
|
||||||
|
elif isinstance(manifest, dict):
|
||||||
|
data = manifest
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
value = data.get("manifest_id")
|
||||||
|
return str(value) if value else None
|
||||||
|
|
||||||
|
|
||||||
|
def _diagnostic(code: str, message: str, *, details: dict[str, Any] | None = None) -> Diagnostic:
|
||||||
|
return Diagnostic(
|
||||||
|
severity="error",
|
||||||
|
code=code,
|
||||||
|
message=message,
|
||||||
|
source=SourceLocation(path="<quarkdown>"),
|
||||||
|
details=details or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _digest_text(value: str) -> str:
|
||||||
|
return "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _file_digest(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return "sha256:" + digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_empty(data: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return {key: value for key, value in data.items() if value not in (None, [], {}, "")}
|
||||||
157
tests/test_quarkdown_adapter.py
Normal file
157
tests/test_quarkdown_adapter.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from markitect_tool.render import RenderExportRequest
|
||||||
|
from markitect_quarkdown import (
|
||||||
|
MARKITECT_QUARKDOWN_VERSION_POLICY,
|
||||||
|
MarkitectQuarkdownAdapter,
|
||||||
|
QuarkdownProcessResult,
|
||||||
|
build_quarkdown_execution_plan,
|
||||||
|
export_quarkdown_source,
|
||||||
|
quarkdown_adapter_descriptor,
|
||||||
|
quarkdown_profile_for,
|
||||||
|
validate_quarkdown_artifact,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_quarkdown_descriptor_matches_markitect_render_contract():
|
||||||
|
descriptor = quarkdown_adapter_descriptor()
|
||||||
|
data = descriptor.to_dict()
|
||||||
|
|
||||||
|
assert descriptor.id == "render.quarkdown"
|
||||||
|
assert data["kind"] == "render-export"
|
||||||
|
assert descriptor.operations == ["inspect-profile", "export-source", "render-artifact"]
|
||||||
|
assert "pdf" in descriptor.output_profiles
|
||||||
|
assert "application/pdf" in descriptor.artifact_media_types
|
||||||
|
assert descriptor.safety["external_process"] is True
|
||||||
|
assert descriptor.metadata["version_policy"] == MARKITECT_QUARKDOWN_VERSION_POLICY
|
||||||
|
assert descriptor.metadata["open_reuse_integration"] == "integration/quarkdown.integration.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_matrix_maps_markitect_profiles_to_quarkdown_modes():
|
||||||
|
pdf = quarkdown_profile_for("pdf")
|
||||||
|
docs = quarkdown_profile_for("docs")
|
||||||
|
static_site = quarkdown_profile_for("static-site")
|
||||||
|
|
||||||
|
assert pdf.quarkdown_document_type == "paged"
|
||||||
|
assert pdf.output_format == "pdf"
|
||||||
|
assert pdf.artifact_extension == ".pdf"
|
||||||
|
assert docs.quarkdown_document_type == "docs"
|
||||||
|
assert static_site.output_format == "html"
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_source_preserves_render_manifest_identifier():
|
||||||
|
request = RenderExportRequest(
|
||||||
|
source="# Demo",
|
||||||
|
operation="export-source",
|
||||||
|
profile="docs",
|
||||||
|
render_manifest={"manifest_id": "render-manifest:test"},
|
||||||
|
)
|
||||||
|
|
||||||
|
exported = export_quarkdown_source(request)
|
||||||
|
|
||||||
|
assert "<!-- generated-by: markitect-quarkdown -->" in exported
|
||||||
|
assert "<!-- markitect-profile: docs -->" in exported
|
||||||
|
assert "<!-- render-reference-manifest: render-manifest:test -->" in exported
|
||||||
|
assert exported.endswith("# Demo")
|
||||||
|
|
||||||
|
|
||||||
|
def test_adapter_exports_quarkdown_source_without_invoking_runtime():
|
||||||
|
adapter = MarkitectQuarkdownAdapter(command_resolver=lambda _command: None)
|
||||||
|
request = RenderExportRequest(
|
||||||
|
source="# Demo",
|
||||||
|
operation="export-source",
|
||||||
|
profile="docs",
|
||||||
|
source_path="docs/demo.md",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = adapter.render(request)
|
||||||
|
|
||||||
|
assert result.valid
|
||||||
|
assert result.exported_source is not None
|
||||||
|
assert result.artifacts[0].role == "renderer-source"
|
||||||
|
assert result.metadata["external_renderer_invoked"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_artifact_reports_missing_runtime_as_structured_diagnostic():
|
||||||
|
adapter = MarkitectQuarkdownAdapter(command_resolver=lambda _command: None)
|
||||||
|
request = RenderExportRequest(source="# Demo", operation="render-artifact", profile="pdf")
|
||||||
|
|
||||||
|
result = adapter.render(request)
|
||||||
|
|
||||||
|
assert not result.valid
|
||||||
|
assert result.diagnostics[0].code == "render.quarkdown.runtime_missing"
|
||||||
|
assert result.metadata["external_renderer_invoked"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_execution_plan_maps_permissions_and_output_conventions(tmp_path: Path):
|
||||||
|
request = RenderExportRequest(
|
||||||
|
source="# Demo",
|
||||||
|
operation="render-artifact",
|
||||||
|
profile="pdf",
|
||||||
|
options={"artifact_stem": "demo", "permissions": ["project-read"], "deny_network": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
plan = build_quarkdown_execution_plan(
|
||||||
|
request,
|
||||||
|
source_path=tmp_path / "source.qd",
|
||||||
|
output_dir=tmp_path / "out",
|
||||||
|
command="/usr/bin/quarkdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert plan.expected_artifact_path.endswith("demo.pdf")
|
||||||
|
assert "--allow" in plan.command
|
||||||
|
assert "project-read" in plan.command
|
||||||
|
assert "--deny" in plan.command
|
||||||
|
assert "network" in plan.command
|
||||||
|
assert plan.environment["requires_java"] == "17+"
|
||||||
|
assert plan.environment["requires_node_for_pdf"] == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def test_adapter_validates_fake_runner_artifact(tmp_path: Path):
|
||||||
|
def fake_runner(plan):
|
||||||
|
Path(plan.expected_artifact_path).write_text("<html>rendered</html>", encoding="utf-8")
|
||||||
|
return QuarkdownProcessResult(returncode=0, stdout="ok")
|
||||||
|
|
||||||
|
adapter = MarkitectQuarkdownAdapter(
|
||||||
|
command_resolver=lambda _command: "/usr/bin/quarkdown",
|
||||||
|
runner=fake_runner,
|
||||||
|
)
|
||||||
|
request = RenderExportRequest(
|
||||||
|
source="# Demo",
|
||||||
|
operation="render-artifact",
|
||||||
|
profile="docs",
|
||||||
|
source_path="docs/demo.md",
|
||||||
|
options={"workspace": str(tmp_path), "output_dir": str(tmp_path / "out"), "artifact_stem": "demo"},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = adapter.render(request)
|
||||||
|
|
||||||
|
assert result.valid
|
||||||
|
assert result.metadata["external_renderer_invoked"] is True
|
||||||
|
assert result.artifacts[1].role == "rendered-artifact"
|
||||||
|
assert result.artifacts[1].media_type == "text/html"
|
||||||
|
assert result.artifacts[1].path.endswith("demo.html")
|
||||||
|
assert result.artifacts[1].digest.startswith("sha256:")
|
||||||
|
assert result.provenance[0].artifact_id == result.artifacts[1].artifact_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_artifact_validation_reports_missing_artifact(tmp_path: Path):
|
||||||
|
request = RenderExportRequest(source="# Demo", operation="render-artifact", profile="pdf")
|
||||||
|
plan = build_quarkdown_execution_plan(
|
||||||
|
request,
|
||||||
|
source_path=tmp_path / "source.qd",
|
||||||
|
output_dir=tmp_path / "out",
|
||||||
|
)
|
||||||
|
|
||||||
|
artifact, diagnostics = validate_quarkdown_artifact(plan)
|
||||||
|
|
||||||
|
assert artifact is None
|
||||||
|
assert diagnostics[0].code == "render.quarkdown.artifact_missing"
|
||||||
|
|
||||||
|
|
||||||
|
def test_real_quarkdown_runtime_smoke_skips_when_unavailable():
|
||||||
|
if shutil.which("quarkdown") is None:
|
||||||
|
pytest.skip("Quarkdown CLI is not installed in this environment.")
|
||||||
@@ -3,10 +3,10 @@ id: MQD-WP-0001
|
|||||||
type: workplan
|
type: workplan
|
||||||
title: "Quarkdown Render Adapter Boundary"
|
title: "Quarkdown Render Adapter Boundary"
|
||||||
domain: markitect
|
domain: markitect
|
||||||
status: todo
|
status: done
|
||||||
owner: markitect-quarkdown
|
owner: markitect-quarkdown
|
||||||
topic_slug: markitect
|
topic_slug: markitect
|
||||||
planning_priority: P2
|
planning_priority: complete
|
||||||
planning_order: 10
|
planning_order: 10
|
||||||
related_workplans:
|
related_workplans:
|
||||||
- MKTT-WP-0020
|
- MKTT-WP-0020
|
||||||
@@ -50,11 +50,27 @@ integration maintenance.
|
|||||||
- enterprise authorization services
|
- enterprise authorization services
|
||||||
- a long-lived divergent Quarkdown fork
|
- a long-lived divergent Quarkdown fork
|
||||||
|
|
||||||
|
## Implementation Summary
|
||||||
|
|
||||||
|
Completed as the initial native Quarkdown render adapter boundary:
|
||||||
|
|
||||||
|
- Added a Python package with a `render.quarkdown` descriptor registered under
|
||||||
|
`markitect_tool.render_export_adapters`.
|
||||||
|
- Implemented `MarkitectQuarkdownAdapter` for `inspect-profile`,
|
||||||
|
`export-source`, and controlled `render-artifact` execution.
|
||||||
|
- Added Markitect profile to Quarkdown document/output mapping, option schema,
|
||||||
|
permission mapping, default output conventions, dry-run plan artifacts, and
|
||||||
|
structured runtime/execution/artifact diagnostics.
|
||||||
|
- Added artifact validation with extension, non-empty, digest, media type, and
|
||||||
|
source-to-artifact provenance.
|
||||||
|
- Added docs, an example render request, open-reuse integration metadata, and
|
||||||
|
tests that use fake execution plus a real-runtime skip hook.
|
||||||
|
|
||||||
## P1.1 - Map Markitect render contract to Quarkdown invocation
|
## P1.1 - Map Markitect render contract to Quarkdown invocation
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: MQD-WP-0001-T001
|
id: MQD-WP-0001-T001
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "cb8014d5-9384-4736-8be4-a1f336a416c8"
|
state_hub_task_id: "cb8014d5-9384-4736-8be4-a1f336a416c8"
|
||||||
```
|
```
|
||||||
@@ -76,7 +92,7 @@ Output: adapter boundary note and compatibility tests with contract fixtures.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: MQD-WP-0001-T002
|
id: MQD-WP-0001-T002
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "57a11359-4888-48e9-978b-bf30ba86c961"
|
state_hub_task_id: "57a11359-4888-48e9-978b-bf30ba86c961"
|
||||||
```
|
```
|
||||||
@@ -97,7 +113,7 @@ Output: profile matrix, option schema, and examples.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: MQD-WP-0001-T003
|
id: MQD-WP-0001-T003
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "e0aa1eb2-39e2-40c6-bd03-c44910303ce9"
|
state_hub_task_id: "e0aa1eb2-39e2-40c6-bd03-c44910303ce9"
|
||||||
```
|
```
|
||||||
@@ -113,7 +129,7 @@ cleanly when Quarkdown runtime dependencies are unavailable.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: MQD-WP-0001-T004
|
id: MQD-WP-0001-T004
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "c0d73105-ae4f-407e-9137-fe4216abd9c8"
|
state_hub_task_id: "c0d73105-ae4f-407e-9137-fe4216abd9c8"
|
||||||
```
|
```
|
||||||
@@ -128,7 +144,7 @@ notes.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: MQD-WP-0001-T005
|
id: MQD-WP-0001-T005
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "4719b31f-eec9-4e1b-a2e5-82a261ed8139"
|
state_hub_task_id: "4719b31f-eec9-4e1b-a2e5-82a261ed8139"
|
||||||
```
|
```
|
||||||
@@ -150,7 +166,7 @@ Output: artifact validation helpers, fixtures, and tests.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: MQD-WP-0001-T006
|
id: MQD-WP-0001-T006
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "f8c2c874-8fee-45b3-86fd-2fe7fc563ee8"
|
state_hub_task_id: "f8c2c874-8fee-45b3-86fd-2fe7fc563ee8"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user