diff --git a/pyproject.toml b/pyproject.toml index 473f7bb..aacd381 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ packages = [{ include = "spec_artifacts_process" }] include = [ { path = "spec_artifacts_process/manifest.yaml", format = ["sdist", "wheel"] }, { path = "spec_artifacts_process/schemas/**/*.json", format = ["sdist", "wheel"] }, + { path = "spec_artifacts_process/skeletons/**/*.md", format = ["sdist", "wheel"] }, ] [tool.poetry.dependencies] diff --git a/spec_artifacts_process/examples/spec-review.md b/spec_artifacts_process/examples/spec-review.md new file mode 100644 index 0000000..1fbf642 --- /dev/null +++ b/spec_artifacts_process/examples/spec-review.md @@ -0,0 +1,21 @@ +--- +id: SR-001 +title: "Failure-domain review of the quoin spec" +type: SpecReview +analysis: failure-domain +scope: spec/spec.md +review_set: subset +--- + +## Summary + +One review document per analysis skill. The `## Findings` table is validated +by the SpecReview archetype: exact columns, an `FND-NNN` id per row, and a +`Severity` constrained to `low`/`medium`/`high` (quire CR-010 `column_choices`). + +## Findings + +| ID | Severity | Summary | Refs | +| --- | --- | --- | --- | +| FND-001 | medium | ix-flow spawn failure behavior is undefined | FR-021 | +| FND-002 | low | cycle termination unstated for the graph walk | FR-024 | diff --git a/spec_artifacts_process/manifest.yaml b/spec_artifacts_process/manifest.yaml index 83c65cb..97bb913 100644 --- a/spec_artifacts_process/manifest.yaml +++ b/spec_artifacts_process/manifest.yaml @@ -30,6 +30,14 @@ archetypes: nav: top_level: true order: 12 +- kind: spec-review + name: SpecReview + doc_backed: true + supports_membership: false + composition: + nav: + top_level: true + order: 13 - kind: test-matrix name: Test Matrix doc_backed: true @@ -46,6 +54,7 @@ grammars: - adr - plan - review + - spec-review - test-matrix - standard - finding @@ -94,6 +103,35 @@ artifact_types: - references examples: [] lint_rules_ref: [] +- name: SpecReview + grammar_ref: process-artifacts + frontmatter_schema_ref: schemas/spec-review-frontmatter.schema.json + defaults: + id_pattern: SR-{next:03d} + allowed_links: + - reviews + - references + examples: [] + lint_rules_ref: [] + body_extraction: + yield_pattern: + match: + summary: + from: section_body + after_heading: Summary + required: true + findings: + from: table_row + under_section: Findings + required: true + multiple: true + assert: + columns: [ID, Severity, Summary, Refs] + min_rows: 1 + id_column: ID + id_pattern: '^FND-\d+$' + column_choices: + Severity: [low, medium, high] - name: Finding grammar_ref: process-artifacts frontmatter_schema_ref: schemas/finding-frontmatter.schema.json @@ -128,6 +166,7 @@ doc_kinds: - adr - plan - review +- spec-review - test-matrix - standard - finding diff --git a/spec_artifacts_process/schemas/spec-review-frontmatter.schema.json b/spec_artifacts_process/schemas/spec-review-frontmatter.schema.json new file mode 100644 index 0000000..f4bcf47 --- /dev/null +++ b/spec_artifacts_process/schemas/spec-review-frontmatter.schema.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": [ + "id", + "title", + "type" + ], + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "pattern": "^[A-Z]{2,4}-[0-9]+$" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "type": { + "const": "SpecReview" + }, + "analysis": { + "type": "string", + "enum": [ + "base", + "failure-domain", + "integrity", + "dependency", + "evidence", + "risk-complexity", + "scope-boundary" + ] + }, + "scope": { + "type": "string" + }, + "review_set": { + "type": "string", + "enum": [ + "base", + "all", + "subset" + ] + }, + "object": { + "type": "string" + }, + "relationships": { + "type": "array", + "items": { + "type": "object", + "required": [ + "target", + "type" + ], + "additionalProperties": false, + "properties": { + "target": { + "type": "string", + "pattern": "^ix://" + }, + "type": { + "type": "string" + }, + "cardinality": { + "type": "string" + } + } + } + } + } +} diff --git a/spec_artifacts_process/skeletons/SpecReview.md b/spec_artifacts_process/skeletons/SpecReview.md new file mode 100644 index 0000000..ede40a5 --- /dev/null +++ b/spec_artifacts_process/skeletons/SpecReview.md @@ -0,0 +1,33 @@ +--- +id: SR-001 +title: " review of " +type: SpecReview +analysis: failure-domain +scope: "spec/spec.md" +review_set: subset +--- + + +## Summary + + + +## Findings + +| ID | Severity | Summary | Refs | +| ------- | -------- | -------------------------------- | ------ | +| FND-001 | medium | | FR-001 | diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 3ba4491..eb29034 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -25,6 +25,49 @@ def test_manifest_loads() -> None: assert manifest["version"] +def test_spec_review_archetype_registered_with_findings_validation() -> None: + """SpecReview is the per-analysis review archetype: a Summary section plus a + Findings table whose Severity column is constrained to low/medium/high + (quire CR-010 `column_choices`).""" + manifest = yaml.safe_load(MANIFEST_PATH.read_text()) + + names = {a["name"] for a in manifest["archetypes"]} + assert "SpecReview" in names, "SpecReview archetype must be registered" + + sr = next(t for t in manifest["artifact_types"] if t["name"] == "SpecReview") + assert sr["frontmatter_schema_ref"] == "schemas/spec-review-frontmatter.schema.json" + + findings = sr["body_extraction"]["yield_pattern"]["match"]["findings"] + assert findings["from"] == "table_row" + assert findings["under_section"] == "Findings" + assert findings["assert"]["columns"] == ["ID", "Severity", "Summary", "Refs"] + assert findings["assert"]["column_choices"]["Severity"] == [ + "low", + "medium", + "high", + ] + assert findings["assert"]["id_pattern"] == r"^FND-\d+$" + + schema_path = pack.PACK_ROOT / "schemas" / "spec-review-frontmatter.schema.json" + assert schema_path.is_file() + schema = json.loads(schema_path.read_text()) + assert schema["properties"]["type"]["const"] == "SpecReview" + + # An authoring skeleton must exist so `quoin write --types SpecReview` + # emits the authoritative template (catalog resolves skeletons/.md). + skeleton = pack.PACK_ROOT / "skeletons" / "SpecReview.md" + assert skeleton.is_file() + body = skeleton.read_text() + assert "type: SpecReview" in body + header = next(line for line in body.splitlines() if line.strip().startswith("| ID")) + assert [c.strip() for c in header.strip().strip("|").split("|")] == [ + "ID", + "Severity", + "Summary", + "Refs", + ] + + def test_manifest_validates_against_fr035_schema() -> None: """Skip if jsonschema lacks draft 2020-12 (use CI check-jsonschema instead).""" try: