From b635e733407a87b3c54e18861e801d18981b9eee Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Tue, 4 Aug 2026 11:35:22 -0400 Subject: [PATCH 1/3] fix: make the five docs/adr ADRs conform to the adr genre and route them to the structured-madr gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo's own ADRs (docs/adr/0001-0005) carried type: semantic and no status:, so the ADR carve-out never fired: audit-v2 recorded them as mutable and the structured-madr oracle routing never applied (#203). - Rewrite the five ADR frontmatters to the genre's structured-MADR form (type: adr, conceptType: semantic, lifecycle status, category, created/updated dates, author, project), keeping the MIF L3 fields the smadr schema carries (temporal, provenance, citations, relationships, summary) and moving non-schema keys to x- extensions. Bodies untouched. - corpus.mjs: content-based ADR carve-out — listL3Docs excludes type: adr docs (predicate shared via mif-genre-signal.mjs), new listAdrDocs returns them; fail-closed checks still run pre-filter. - ci.yml: adr-smadr job now validates docs/adr in both smadr (strict) and mif (level 3) modes; stale corpus comment corrected. - tests: regression tests pinning that every docs/adr doc is the carve-out with a lifecycle status, that listL3Docs excludes exactly the ADRs, and that a non-adr doc under docs/adr stays gated. --- .github/workflows/ci.yml | 24 +++++++- CHANGELOG.md | 19 ++++-- ...0001-align-adr-genre-to-structured-madr.md | 49 ++++++++------- ...0002-host-ontologies-in-a-separate-repo.md | 53 ++++++++-------- .../0003-attested-delivery-release-pattern.md | 61 +++++++++---------- ...e-engine-authoritative-with-parity-gate.md | 52 +++++++++------- ...rovenance-consent-in-settings-hierarchy.md | 52 +++++++++------- scripts/lib/corpus.mjs | 36 ++++++++++- tests/corpus.test.mjs | 60 ++++++++++++++++++ 9 files changed, 271 insertions(+), 135 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index addbcd0..5492929 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,8 +87,10 @@ jobs: - name: MIF-validate the project's own docs (the suite documents itself) run: | # The suite documents itself: every doc under docs/ is a full MIF - # artifact (the project ADRs are L3 MIF, not structured-MADR). Gate each - # at the level it claims so its provenance/citations cannot rot. + # artifact. The project ADRs (docs/adr/, type: adr) are structured-MADR + # per ADR-0001 and issue #203 -- corpus.mjs carves them out here and the + # adr-smadr job below gates them instead. Gate everything else at the + # level it claims so its provenance/citations cannot rot. # list-gated-docs.mjs fails closed (missing directory, empty result) # so a drifted or renamed tree can't silently skip this gate; see the # plain-redirect note in the template-validation step above. @@ -147,3 +149,21 @@ jobs: mif-level: '3' path: skills/adr/templates pattern: good.md + # The project's own ADRs are type: adr per ADR-0001 (issue #203) and are + # carved out of the mif-validate corpus (scripts/lib/corpus.mjs); this + # Action is their gate, in the same two modes as the template above. + - name: Validate project ADRs — structured-MADR mode (strict) + uses: modeled-information-format/structured-madr@325eafe9a17a23e716daffddac5db10ee178c08e # main: internal actions SHA-pinned (org policy; v1.2.0 left setup-node on a tag) + with: + mode: smadr + path: docs/adr + pattern: '*.md' + strict: 'true' + fail-on-error: 'true' + - name: Validate project ADRs — MIF conformance mode (level 3) + uses: modeled-information-format/structured-madr@325eafe9a17a23e716daffddac5db10ee178c08e # main: internal actions SHA-pinned (org policy; v1.2.0 left setup-node on a tag) + with: + mode: mif + mif-level: '3' + path: docs/adr + pattern: '*.md' diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d04dab..f0ce07e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ id: changelog-mif-docs type: episodic created: '2026-06-30T00:00:00Z' -modified: '2026-08-04T15:06:35.000Z' +modified: '2026-08-04T15:34:08.079Z' namespace: changelog/mif-docs title: Changelog tags: @@ -22,17 +22,17 @@ provenance: '@type': Provenance sourceType: agent_inferred trustLevel: user_stated - agent: claude-code/claude-sonnet-5 + agent: claude-code/claude-fable-5 wasAttributedTo: '@id': https://github.com/modeled-information-format '@type': prov:Agent wasGeneratedBy: - '@id': urn:mif:activity:claude-code-session:b5bba701-d09b-493c-98c1-85bd98cd9eec + '@id': urn:mif:activity:claude-code-session:fa69eb8c-0e0f-4e98-847d-112c92f6177c '@type': prov:Activity wasDerivedFrom: - '@id': urn:mif:release:mif-docs-v0.1.0 '@type': prov:Entity - agentVersion: 2.1.220 + agentVersion: 2.1.221 citations: - '@type': Citation citationType: specification @@ -62,6 +62,17 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed + +- Made the repo's own five ADRs (`docs/adr/0001`-`0005`) conform to the + `adr` genre skill they ship with (#203): each now carries `type: adr`, + a lifecycle `status:`, and the full structured-MADR frontmatter, so + audit-v2's immutable-ADR policy and the `structured-madr` oracle + routing engage on them. `scripts/lib/corpus.mjs` gained a content-based + ADR carve-out (`listAdrDocs`) that keeps `type: adr` documents out of + the `mif-validate` corpus, and the `adr-smadr` CI job now validates + `docs/adr/` in both `smadr` (strict) and `mif` (level 3) modes. + ## [0.9.3] - 2026-08-04 ### Added diff --git a/docs/adr/0001-align-adr-genre-to-structured-madr.md b/docs/adr/0001-align-adr-genre-to-structured-madr.md index 0599294..45176f5 100644 --- a/docs/adr/0001-align-adr-genre-to-structured-madr.md +++ b/docs/adr/0001-align-adr-genre-to-structured-madr.md @@ -1,25 +1,28 @@ --- -id: adr-0001-align-adr-to-smadr -type: semantic -created: '2026-06-30T10:00:00Z' -modified: '2026-06-30T10:00:00Z' -namespace: adr/mif-docs -title: 'ADR-0001: Align the adr Genre Fully to Structured MADR' +title: Align the adr Genre Fully to Structured MADR description: >- Align the plugin's flagship adr genre fully to the org's canonical, Action-validated Structured MADR format instead of decoupling with an optional check, so ADRs never diverge from the ecosystem. +type: adr +conceptType: semantic +id: adr-0001-align-adr-to-smadr +namespace: adr/mif-docs +x-ontology: + id: mif-docs + version: '1.0.0' + uri: https://mif-spec.dev/ontologies/mif-docs + entity_type: decision-record +category: documentation-format tags: - adr - structured-madr - validation -aliases: - - ADR-0001 -ontology: - '@type': OntologyReference - id: mif-docs - version: 1.0.0 - uri: https://mif-spec.dev/ontologies/mif-docs +status: accepted +created: 2026-06-30 +updated: 2026-06-30 +author: modeled-information-format +project: mif-docs temporal: '@type': TemporalMetadata validFrom: '2026-06-30T00:00:00Z' @@ -66,23 +69,19 @@ relationships: target: urn:mif:adr-0002-ontologies-separate-repo - type: relates-to target: urn:mif:adr-0003-attested-delivery -entity: - name: Align the adr Genre Fully to Structured MADR - entity_type: decision-record summary: >- The flagship adr genre aligns fully to Structured MADR; the structured-madr Action is the authority for ADR validation in both smadr (strict) and mif (conformance) modes, so ADRs authored by the plugin never diverge from the org standard. -extensions: - x-adr-status: accepted - x-adr-category: documentation-format - x-superseded-from-smadr: true - x-decision-drivers: - - flagship-genre - - ecosystem-alignment - - action-validation - x-genre: adr +x-aliases: + - ADR-0001 +x-superseded-from-smadr: true +x-decision-drivers: + - flagship-genre + - ecosystem-alignment + - action-validation +x-genre: adr --- # ADR-0001: Align the adr Genre Fully to Structured MADR diff --git a/docs/adr/0002-host-ontologies-in-a-separate-repo.md b/docs/adr/0002-host-ontologies-in-a-separate-repo.md index 1c14e28..0bbb780 100644 --- a/docs/adr/0002-host-ontologies-in-a-separate-repo.md +++ b/docs/adr/0002-host-ontologies-in-a-separate-repo.md @@ -1,25 +1,28 @@ --- -id: adr-0002-ontologies-separate-repo -type: semantic -created: '2026-06-30T10:00:00Z' -modified: '2026-06-30T10:00:00Z' -namespace: adr/mif-docs -title: 'ADR-0002: Host Ontologies in a Separate Repository' +title: Host Ontologies in a Separate Repository description: >- Host the MIF ontology in the org-shared ontologies repo and have the plugin hydrate it at dev and vendor it into the release artifact, rather than committing a copy here that would drift. +type: adr +conceptType: semantic +id: adr-0002-ontologies-separate-repo +namespace: adr/mif-docs +x-ontology: + id: mif-docs + version: '1.0.0' + uri: https://mif-spec.dev/ontologies/mif-docs + entity_type: decision-record +category: architecture tags: - adr - ontology - vendoring -aliases: - - ADR-0002 -ontology: - '@type': OntologyReference - id: mif-docs - version: 1.0.0 - uri: https://mif-spec.dev/ontologies/mif-docs +status: accepted +created: 2026-06-30 +updated: 2026-06-30 +author: modeled-information-format +project: mif-docs temporal: '@type': TemporalMetadata validFrom: '2026-06-30T00:00:00Z' @@ -63,25 +66,21 @@ relationships: target: urn:mif:adr-0001-align-adr-to-smadr - type: relates-to target: urn:mif:adr-0003-attested-delivery -entity: - name: Host Ontologies in a Separate Repository - entity_type: decision-record summary: >- The MIF ontology stays in the org-shared ontologies repo; the plugin hydrates it from a raw URL at dev time and vendors a fixed copy into the release artifact, so there is one authoritative ontology with no committed duplicate to drift. -extensions: - x-adr-status: accepted - x-adr-category: architecture - x-superseded-from-smadr: true - x-decision-drivers: - - shared-corpus - - drift-avoidance - - vendor-at-release - x-hydration: - - dev-sibling - - release-vendored +x-aliases: + - ADR-0002 +x-superseded-from-smadr: true +x-decision-drivers: + - shared-corpus + - drift-avoidance + - vendor-at-release +x-hydration: + - dev-sibling + - release-vendored --- # ADR-0002: Host Ontologies in a Separate Repository diff --git a/docs/adr/0003-attested-delivery-release-pattern.md b/docs/adr/0003-attested-delivery-release-pattern.md index 9c7d310..eaeff0c 100644 --- a/docs/adr/0003-attested-delivery-release-pattern.md +++ b/docs/adr/0003-attested-delivery-release-pattern.md @@ -1,26 +1,33 @@ --- -id: adr-0003-attested-delivery -type: semantic -created: '2026-06-30T10:00:00Z' -modified: '2026-06-30T10:00:00Z' -namespace: adr/mif-docs -title: 'ADR-0003: Adopt the Attested-Delivery Release Pattern' +title: Adopt the Attested-Delivery Release Pattern description: >- Release the plugin with the self-contained attested-delivery pattern — a reproducible git-archive tarball, SLSA build provenance, and fail-closed verification before upload — so every release is independently verifiable. +type: adr +conceptType: semantic +id: adr-0003-attested-delivery +namespace: adr/mif-docs +x-ontology: + id: mif-docs + version: '1.0.0' + uri: https://mif-spec.dev/ontologies/mif-docs + entity_type: decision-record +category: supply-chain-security tags: - adr - release - attestation - supply-chain -aliases: - - ADR-0003 -ontology: - '@type': OntologyReference - id: mif-docs - version: 1.0.0 - uri: https://mif-spec.dev/ontologies/mif-docs +status: accepted +created: 2026-06-30 +updated: 2026-06-30 +author: modeled-information-format +project: mif-docs +technologies: + - slsa + - github-actions + - sigstore temporal: '@type': TemporalMetadata validFrom: '2026-06-30T00:00:00Z' @@ -73,28 +80,20 @@ relationships: target: urn:mif:adr-0002-ontologies-separate-repo - type: realized-by target: urn:mif:runbook-cut-attested-release -entity: - name: Adopt the Attested-Delivery Release Pattern - entity_type: decision-record summary: >- From v0.1.0, releases are reproducible git-archive tarballs carrying SLSA build provenance, fail-closed verified before upload; modeled on research-harness-template, every action SHA-pinned via the central pin-check. -extensions: - x-adr-status: accepted - x-adr-category: supply-chain-security - x-superseded-from-smadr: true - x-decision-drivers: - - supply-chain-integrity - - independent-verifiability - - fail-closed-publication - x-slsa-build-level: 3 - x-signer-workflow: .github/workflows/release.yml - x-verify-command: gh attestation verify --signer-workflow - x-technologies: - - slsa - - github-actions - - sigstore +x-aliases: + - ADR-0003 +x-superseded-from-smadr: true +x-decision-drivers: + - supply-chain-integrity + - independent-verifiability + - fail-closed-publication +x-slsa-build-level: 3 +x-signer-workflow: .github/workflows/release.yml +x-verify-command: gh attestation verify --signer-workflow --- # ADR-0003: Adopt the Attested-Delivery Release Pattern diff --git a/docs/adr/0004-node-engine-authoritative-with-parity-gate.md b/docs/adr/0004-node-engine-authoritative-with-parity-gate.md index 6dacdf2..94d3972 100644 --- a/docs/adr/0004-node-engine-authoritative-with-parity-gate.md +++ b/docs/adr/0004-node-engine-authoritative-with-parity-gate.md @@ -1,18 +1,29 @@ --- +title: Node Engine Stays Authoritative, Convergence Proven by a Parity Gate +description: >- + The plugin's node validation engine remains the authoritative MIF conformance + gate; the mif-rs Rust engine is compared against it by a non-required nightly + parity job with an explicit expected-disagreement ledger. +type: adr +conceptType: semantic id: adr-0004-node-engine-authoritative -type: semantic -created: '2026-07-05T12:00:00Z' -modified: '2026-07-28T22:28:30.021Z' namespace: adr/mif-docs -title: 'ADR-0004: Node Engine Stays Authoritative, Convergence Proven by a Parity Gate' -summary: The plugin's node validation engine remains the authoritative MIF conformance gate; the mif-rs Rust engine is compared against it by a non-required nightly parity job with an explicit expected-disagreement ledger, and no engine substitution happens until that ledger is empty and the upstream capability gaps are closed. +x-ontology: + id: mif-docs + version: '1.0.0' + uri: https://mif-spec.dev/ontologies/mif-docs + entity_type: decision-record +category: architecture tags: - adr - validation - parity - mif-rs -aliases: - - ADR-0004 +status: accepted +created: 2026-07-05 +updated: 2026-07-28 +author: modeled-information-format +project: mif-docs temporal: '@type': TemporalMetadata validFrom: '2026-07-05T00:00:00Z' @@ -55,21 +66,18 @@ citations: title: MIF — Modeled Information Format Specification url: https://mif-spec.dev/ accessed: '2026-07-05' -ontology: - '@type': OntologyReference - id: mif-docs - version: 1.0.0 - uri: https://mif-spec.dev/ontologies/mif-docs -entity: - name: Node Engine Authoritative with Parity Gate - entity_type: decision-record -extensions: - x-adr-status: accepted - x-adr-category: architecture - x-decision-drivers: - - single-authoritative-verdict - - fail-closed-determinism - - convergence-without-blocking +summary: >- + The plugin's node validation engine remains the authoritative MIF conformance + gate; the mif-rs Rust engine is compared against it by a non-required nightly + parity job with an explicit expected-disagreement ledger, and no engine + substitution happens until that ledger is empty and the upstream capability + gaps are closed. +x-aliases: + - ADR-0004 +x-decision-drivers: + - single-authoritative-verdict + - fail-closed-determinism + - convergence-without-blocking --- # ADR-0004: Node Engine Stays Authoritative, Convergence Proven by a Parity Gate diff --git a/docs/adr/0005-provenance-consent-in-settings-hierarchy.md b/docs/adr/0005-provenance-consent-in-settings-hierarchy.md index 2a28c4a..09abfc0 100644 --- a/docs/adr/0005-provenance-consent-in-settings-hierarchy.md +++ b/docs/adr/0005-provenance-consent-in-settings-hierarchy.md @@ -1,18 +1,29 @@ --- +title: Provenance Consent Rides the Settings Hierarchy, and Refusal Wins +description: >- + The mif-provenance consent surface is one namespaced key (mifProvenance) in + Claude Code's settings hierarchy; precedence orders only non-refusal values, + refusal at any scope wins, and configuration errors fail closed to disabled. +type: adr +conceptType: semantic id: adr-0005-provenance-consent -type: semantic -created: '2026-07-11T12:00:00Z' -modified: '2026-07-28T22:28:45.539Z' namespace: adr/mif-docs -title: 'ADR-0005: Provenance Consent Rides the Settings Hierarchy, and Refusal Wins' -summary: The mif-provenance helper's consent surface is one namespaced key (mifProvenance) in Claude Code's own settings hierarchy, with the plugin-local settings file pattern as the documented fallback carrier; precedence orders only non-refusal values, an explicit disable at any scope defeats enablement at every other scope, and configuration errors fail closed to disabled. +x-ontology: + id: mif-docs + version: '1.0.0' + uri: https://mif-spec.dev/ontologies/mif-docs + entity_type: decision-record +category: architecture tags: - adr - provenance - consent - configuration -aliases: - - ADR-0005 +status: accepted +created: 2026-07-11 +updated: 2026-07-28 +author: modeled-information-format +project: mif-docs temporal: '@type': TemporalMetadata validFrom: '2026-07-11T00:00:00Z' @@ -58,21 +69,18 @@ citations: citationRole: source title: 'mif-docs-plugin#63 — the mif-provenance Epic this decision anchors' url: https://github.com/modeled-information-format/mif-docs-plugin/issues/63 -ontology: - '@type': OntologyReference - id: mif-docs - version: 1.0.0 - uri: https://mif-spec.dev/ontologies/mif-docs -entity: - name: Provenance Consent in the Settings Hierarchy with Refusal-Wins - entity_type: decision-record -extensions: - x-adr-status: accepted - x-adr-category: architecture - x-decision-drivers: - - consent-before-observation - - fail-closed-misconfiguration - - no-novel-config-surface +summary: >- + The mif-provenance helper's consent surface is one namespaced key + (mifProvenance) in Claude Code's own settings hierarchy, with the plugin-local + settings file pattern as the documented fallback carrier; precedence orders + only non-refusal values, an explicit disable at any scope defeats enablement at + every other scope, and configuration errors fail closed to disabled. +x-aliases: + - ADR-0005 +x-decision-drivers: + - consent-before-observation + - fail-closed-misconfiguration + - no-novel-config-surface --- # ADR-0005: Provenance Consent Rides the Settings Hierarchy, and Refusal Wins diff --git a/scripts/lib/corpus.mjs b/scripts/lib/corpus.mjs index 991262f..9ad8aae 100644 --- a/scripts/lib/corpus.mjs +++ b/scripts/lib/corpus.mjs @@ -6,7 +6,8 @@ // until mif-docs-plugin#32 fixed it there alone, leaving engine-parity.mjs's // copy to drift again on the next nested subdirectory. One list, three // consumers, no hand-sync (mif-docs-plugin#34). -import { statSync, globSync } from "node:fs"; +import { statSync, globSync, readFileSync } from "node:fs"; +import { splitFrontmatter, isAdrCarveout } from "./mif-genre-signal.mjs"; function isDirectory(path) { try { @@ -16,6 +17,23 @@ function isDirectory(path) { } } +// Content-based ADR carve-out (issue #203): a `type: adr` document anywhere in +// the gated trees is owned by the structured-madr Action (the adr-smadr CI +// job), not by mif-validate, which keys on conceptType — the same predicate +// the guard and the audit runner already share via mif-genre-signal.mjs. +// Unreadable files stay IN the corpus so mif-validate reports them instead of +// this filter silently swallowing them. +function isAdrDoc(path) { + let text; + try { + text = readFileSync(path, "utf8"); + } catch { + return false; + } + const split = splitFrontmatter(text); + return split ? isAdrCarveout(split.fmText) : false; +} + export const TEMPLATE_GLOB = "skills/*/templates/good.md"; // The adr genre is fully aligned to structured-MADR (type: adr) and is // validated by the structured-madr Action, not by mif-validate (which keys @@ -54,7 +72,21 @@ export function listL3Docs() { if (files.length === 0) { throw new Error(`no L3 doc files found under ${L3_DIRS.join(", ")} -- check paths`); } - return files; + // Fail-closed check runs on the PRE-filter list: an L3 tree holding only + // ADRs (docs/adr) is present-and-owned-elsewhere, not silently empty. + return files.filter((f) => !isAdrDoc(f)); +} + +// The `type: adr` documents under the L3 trees — the complement of +// listL3Docs()'s filter, gated by the adr-smadr CI job (structured-madr +// Action in smadr strict + mif conformance modes), never by mif-validate. +export function listAdrDocs() { + for (const d of L3_DIRS) { + if (!isDirectory(d)) throw new Error(`L3 doc directory missing: ${d}`); + } + return L3_DIRS.flatMap((d) => globSync(`${d}/**/*.md`)) + .sort() + .filter((f) => isAdrDoc(f)); } export function listL2Docs() { diff --git a/tests/corpus.test.mjs b/tests/corpus.test.mjs index ff237d4..5408c56 100644 --- a/tests/corpus.test.mjs +++ b/tests/corpus.test.mjs @@ -12,14 +12,17 @@ import assert from 'node:assert/strict'; import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; +import { readFileSync, globSync } from 'node:fs'; import { listTemplates, listL3Docs, listL2Docs, listGatedDocs, + listAdrDocs, ADR_TEMPLATE_CARVEOUT, L3_DIRS, } from '../scripts/lib/corpus.mjs'; +import { splitFrontmatter, isAdrCarveout } from '../scripts/lib/mif-genre-signal.mjs'; test('listTemplates excludes the ADR carve-out', () => { const templates = listTemplates(); @@ -31,6 +34,9 @@ test('listL3Docs covers every configured L3 tree', () => { const files = listL3Docs(); assert.ok(files.length > 0, 'expected at least one L3 doc'); for (const dir of L3_DIRS) { + // docs/adr holds only type: adr documents, which the content-based ADR + // carve-out (issue #203) routes to the adr-smadr job instead. + if (dir === 'docs/adr') continue; assert.ok(files.some((f) => f.startsWith(`${dir}/`)), `expected at least one file under ${dir}`); } }); @@ -100,6 +106,60 @@ test('listL3Docs fails closed when an L3 tree is a file, not a directory', () => } }); +// Issue #203 regression: the repo's own ADRs carried `type: semantic` and no +// `status:`, so the ADR carve-out (and with it audit-v2's immutable-ADR +// policy and the structured-madr oracle routing) never engaged on them. +test('every docs/adr document is the type: adr carve-out with a lifecycle status (#203)', () => { + const SMADR_STATUSES = new Set(['proposed', 'accepted', 'deprecated', 'superseded']); + const adrs = globSync('docs/adr/*.md').sort(); + assert.ok(adrs.length > 0, 'expected at least one ADR under docs/adr'); + for (const f of adrs) { + const split = splitFrontmatter(readFileSync(f, 'utf8')); + assert.ok(split, `${f} must have frontmatter`); + assert.ok(isAdrCarveout(split.fmText), `${f} must carry type: adr (the ADR carve-out)`); + const status = split.fmText.match(/(^|\n)status[ \t]*:[ \t]*(\S+)/)?.[2]; + assert.ok( + status && SMADR_STATUSES.has(status), + `${f} must declare a structured-MADR lifecycle status, got ${status ?? '(none)'}`, + ); + } +}); + +test('listL3Docs excludes type: adr docs; listAdrDocs returns exactly them (#203)', () => { + const l3 = listL3Docs(); + const adrDocs = listAdrDocs(); + const repoAdrs = globSync('docs/adr/*.md').sort(); + assert.deepEqual(adrDocs, repoAdrs, 'listAdrDocs must return the docs/adr ADRs'); + for (const f of adrDocs) { + assert.ok(!l3.includes(f), `${f} must be carved out of the mif-validate corpus`); + } +}); + +test('a non-adr doc under an L3 tree stays gated despite the ADR carve-out (#203)', () => { + // The carve-out is content-based (type: adr), not directory-based: a plain + // semantic doc dropped into docs/adr must remain in the mif-validate corpus. + const scratch = mkdtempSync(join(tmpdir(), 'mif-corpus-test-')); + for (const dir of L3_DIRS) { + mkdirSync(join(scratch, dir), { recursive: true }); + writeFileSync(join(scratch, dir, 'x.md'), '---\ntype: semantic\n---\n\n# x\n'); + } + writeFileSync( + join(scratch, 'docs/adr', 'adr-1.md'), + '---\ntype: adr\nstatus: accepted\n---\n\n# ADR-0001: x\n', + ); + const originalCwd = process.cwd(); + process.chdir(scratch); + try { + const l3 = listL3Docs(); + assert.ok(l3.includes('docs/adr/x.md'), 'a non-adr doc under docs/adr must stay gated'); + assert.ok(!l3.includes('docs/adr/adr-1.md'), 'the type: adr doc must be carved out'); + assert.deepEqual(listAdrDocs(), ['docs/adr/adr-1.md']); + } finally { + process.chdir(originalCwd); + rmSync(scratch, { recursive: true, force: true }); + } +}); + test('listTemplates fails closed when the template glob resolves to nothing', () => { const scratch = mkdtempSync(join(tmpdir(), 'mif-corpus-test-')); const originalCwd = process.cwd(); From d2da5f7aec493c2ff6e54b736081befa950bd1fa Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Tue, 4 Aug 2026 11:50:50 -0400 Subject: [PATCH 2/3] fix: unrot the parity ledger and the corpus docs the ADR carve-out invalidated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups to the docs/adr genre-conformance change in this PR. Carving the five ADRs out of listL3Docs() removed them from listGatedDocs(), which is exactly the corpus engine-parity.mjs walks. Its expected-disagreements ledger still named docs/adr/0001-0003, so every one of them became an ORPHANED-EXPECTATION and the harness exits 1 ("RESULT: PARITY FAILED"). The engine-parity workflow is nightly and deliberately never runs on pull requests, so PR CI could not surface this: the break would have shown up at 05:17 UTC, detached from the change that caused it. - expected-disagreements.json: drop the three docs/adr entries. The description-key drift they tracked is still covered by the committed tests/fixtures/engine-parity/drift-description.md reproduction under the same mif-rs#38 upstream, so nothing stops being tracked. The ledger comment now also states the orphan failure mode and the corpus invariant every entry must satisfy. - tests/corpus.test.mjs: pin that invariant in a PR-gated test — every ledger entry must resolve into listGatedDocs() + the parity fixtures. Fails on the pre-fix ledger (3 orphans), passes after. This moves the whole rot class out of nightly-only visibility. Also folds the duplicate node:fs import into the existing one. Separately, three documents asserted the repo's ADRs carry MIF type: semantic, which this PR makes false: - skills/mif-corpus/SKILL.md instructed agents to identify the ingest skip set "never by a `type: adr` frontmatter value" — precisely backwards after this change, in a shipped skill. - docs/reference/skills/mif-corpus.md and docs/reference/corpus-layer.md repeated the same type: semantic claim. All three now say type: adr with conceptType: semantic, and name type as a valid skip-set discriminator alongside the description: key and the path. The conceptType columns in genre-and-cli-catalog.md and skills/adr.md were already correct and are untouched. Gates: test:hook 273 pass, validate-plugin 47 skills / 0 errors, lint:md 0 issues, check-doc-links 74 files clean, mif-validate L3 on both edited reference docs. --- docs/reference/corpus-layer.md | 12 ++++++---- docs/reference/skills/mif-corpus.md | 15 ++++++------ skills/mif-corpus/SKILL.md | 11 +++++---- tests/corpus.test.mjs | 23 +++++++++++++++++-- .../engine-parity/expected-disagreements.json | 17 +------------- 5 files changed, 43 insertions(+), 35 deletions(-) diff --git a/docs/reference/corpus-layer.md b/docs/reference/corpus-layer.md index b75bd3b..cf04811 100644 --- a/docs/reference/corpus-layer.md +++ b/docs/reference/corpus-layer.md @@ -2,7 +2,7 @@ id: reference-corpus-layer type: semantic created: '2026-07-05T12:00:00Z' -modified: '2026-07-05T12:00:00Z' +modified: '2026-08-04T15:48:55.755Z' namespace: reference/corpus title: The Semantic Corpus Layer tags: @@ -28,17 +28,18 @@ ontology: provenance: '@type': Provenance sourceType: agent_inferred - trustLevel: high_confidence - agent: anthropic/claude-code + trustLevel: user_stated + agent: claude-code/claude-fable-5 wasAttributedTo: '@id': https://github.com/modeled-information-format '@type': prov:Agent wasGeneratedBy: - '@id': urn:mif:activity:mif-docs-self-documentation + '@id': urn:mif:activity:claude-code-session:fa69eb8c-0e0f-4e98-847d-112c92f6177c '@type': prov:Activity wasDerivedFrom: - '@id': https://github.com/modeled-information-format/mif-rs '@type': prov:Entity + agentVersion: 2.1.221 citations: - '@type': Citation citationType: repository @@ -99,7 +100,8 @@ about `0.55` to `0.78`. frontmatter key currently fail the Rust round-trip inside ingest (the key is dropped on re-serialization; tracked in the engine-convergence epic). In this corpus those are the ADR documents under `docs/adr/` (whose MIF `type` - is `semantic`); bulk ingests skip them by key or path and say so. + is `adr`, with `conceptType: semantic`); bulk ingests skip them by key, + path, or type and say so. - **Errors** — failures render as RFC 9457 `application/problem+json` envelopes carrying `suggested_fix` and `code_actions[]` with applicability markers; only `machine_applicable` fixes are safe to apply unreviewed. diff --git a/docs/reference/skills/mif-corpus.md b/docs/reference/skills/mif-corpus.md index 72d8afa..753c307 100644 --- a/docs/reference/skills/mif-corpus.md +++ b/docs/reference/skills/mif-corpus.md @@ -2,7 +2,7 @@ id: reference-skill-mif-corpus type: semantic created: '2026-07-05T12:00:00Z' -modified: '2026-07-05T12:00:00Z' +modified: '2026-08-04T15:49:20.909Z' namespace: reference/skills title: 'Skill reference: mif-corpus' tags: @@ -19,19 +19,20 @@ temporal: provenance: '@type': Provenance sourceType: agent_inferred - trustLevel: high_confidence - agent: anthropic/claude-code + trustLevel: user_stated + agent: claude-code/claude-fable-5 wasAttributedTo: '@id': https://github.com/modeled-information-format '@type': prov:Agent wasGeneratedBy: - '@id': urn:mif:activity:mif-docs-self-documentation + '@id': urn:mif:activity:claude-code-session:fa69eb8c-0e0f-4e98-847d-112c92f6177c '@type': prov:Activity wasDerivedFrom: - '@id': https://github.com/modeled-information-format/mif-docs-plugin '@type': prov:Entity - '@id': urn:mif:skill:mif-corpus '@type': prov:Entity + agentVersion: 2.1.221 citations: - '@type': Citation citationType: repository @@ -115,9 +116,9 @@ the suite depends on this skill's availability. problem envelope naming the cause. - **ADR exclusion** — documents carrying a top-level `description:` key currently fail the Rust round-trip inside ingest; in this repo those are - the ADR documents under `docs/adr/` (MIF `type: semantic`), skipped in bulk - ingests by key or path with the skip stated (tracked in the - engine-convergence epic). + the ADR documents under `docs/adr/` (MIF `type: adr`, with + `conceptType: semantic`), skipped in bulk ingests by key, path, or type + with the skip stated (tracked in the engine-convergence epic). - **First-run cost** — the first ingest or search downloads the embedding model once; subsequent runs are local. - **Consumed by the planner** — `doc-set-planner`'s corpus-aware update diff --git a/skills/mif-corpus/SKILL.md b/skills/mif-corpus/SKILL.md index 68f2a80..4d8f7d6 100644 --- a/skills/mif-corpus/SKILL.md +++ b/skills/mif-corpus/SKILL.md @@ -91,11 +91,12 @@ Documents carrying a top-level `description:` frontmatter key currently fail the Rust round-trip check inside ingest (the key is dropped by the canonical re-serialization; tracked upstream and in this repo's engine-convergence epic). In this suite's corpus those are exactly the ADR documents under -`docs/adr/` — note they carry MIF `type: semantic`, so identify the skip set -by the `description:` key or the `docs/adr/` path, never by a `type: adr` -frontmatter value. Skip them in bulk ingests and say that they were skipped -and why. This sits alongside the suite's existing rule that the adr *genre* -is validated by the structured-madr Action, not `mif-validate`. +`docs/adr/`, which carry MIF `type: adr` (`conceptType: semantic`), so the +skip set is identifiable by the `description:` key, the `docs/adr/` path, or +the `type: adr` frontmatter value alike. Skip them in bulk ingests and say +that they were skipped and why. This sits alongside the suite's existing rule +that the adr *genre* is validated by the structured-madr Action, not +`mif-validate`. ## First-run cost diff --git a/tests/corpus.test.mjs b/tests/corpus.test.mjs index 5408c56..99fdd38 100644 --- a/tests/corpus.test.mjs +++ b/tests/corpus.test.mjs @@ -9,10 +9,9 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs'; +import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, globSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; -import { readFileSync, globSync } from 'node:fs'; import { listTemplates, listL3Docs, @@ -160,6 +159,26 @@ test('a non-adr doc under an L3 tree stays gated despite the ADR carve-out (#203 } }); +// engine-parity.mjs runs NIGHTLY and never on pull requests, so a ledger entry +// naming a file that has left the gated corpus (ORPHANED-EXPECTATION, exit 1) +// is invisible to PR CI until the next scheduled run. The ADR carve-out above +// is exactly that kind of corpus departure, so pin the ledger's referential +// integrity here, in a PR-gated test, instead of discovering the rot at 05:17. +test('every expected-disagreements entry resolves into the parity corpus (#203)', () => { + const LEDGER = 'tests/fixtures/engine-parity/expected-disagreements.json'; + // The exact corpus engine-parity.mjs builds: the gated docs plus this + // suite's own committed parity fixtures. + const corpus = new Set([...listGatedDocs(), ...globSync('tests/fixtures/engine-parity/*.md')]); + const { disagreements } = JSON.parse(readFileSync(LEDGER, 'utf8')); + assert.ok(disagreements.length > 0, `${LEDGER} must list at least one tracked disagreement`); + const orphans = disagreements.map((d) => d.file).filter((f) => !corpus.has(f)); + assert.deepEqual( + orphans, + [], + `${LEDGER} names files no corpus glob matches (engine-parity.mjs fails on these): ${orphans.join(', ')}`, + ); +}); + test('listTemplates fails closed when the template glob resolves to nothing', () => { const scratch = mkdtempSync(join(tmpdir(), 'mif-corpus-test-')); const originalCwd = process.cwd(); diff --git a/tests/fixtures/engine-parity/expected-disagreements.json b/tests/fixtures/engine-parity/expected-disagreements.json index 48461f0..eeb0ca3 100644 --- a/tests/fixtures/engine-parity/expected-disagreements.json +++ b/tests/fixtures/engine-parity/expected-disagreements.json @@ -1,21 +1,6 @@ { - "comment": "Documents where the node and mif-rs engines are KNOWN to disagree, each tied to the upstream issue that will close the gap. engine-parity.mjs fails on any disagreement not listed here AND on any listed file whose engines now agree (a stale expectation: the upstream fix landed, prune the entry).", + "comment": "Documents where the node and mif-rs engines are KNOWN to disagree, each tied to the upstream issue that will close the gap. engine-parity.mjs fails on any disagreement not listed here, on any listed file whose engines now agree (a stale expectation: the upstream fix landed, prune the entry), AND on any listed file the corpus never visits (an orphan: the file moved, was deleted, or left the gated corpus -- e.g. the docs/adr ADRs, carved out to the structured-madr gate in #203). Every entry must resolve into listGatedDocs() + tests/fixtures/engine-parity/*.md; tests/corpus.test.mjs pins that.", "disagreements": [ - { - "file": "docs/adr/0001-align-adr-genre-to-structured-madr.md", - "reason": "description frontmatter key dropped by mif-frontmatter jsonld_to_md; node round-trips it losslessly", - "upstream": "modeled-information-format/mif-rs#38" - }, - { - "file": "docs/adr/0002-host-ontologies-in-a-separate-repo.md", - "reason": "description frontmatter key dropped by mif-frontmatter jsonld_to_md; node round-trips it losslessly", - "upstream": "modeled-information-format/mif-rs#38" - }, - { - "file": "docs/adr/0003-attested-delivery-release-pattern.md", - "reason": "description frontmatter key dropped by mif-frontmatter jsonld_to_md; node round-trips it losslessly", - "upstream": "modeled-information-format/mif-rs#38" - }, { "file": "tests/fixtures/engine-parity/drift-description.md", "reason": "minimal committed reproduction of the description-key drift", From e7bcd44abfe955c33fe77f9525802857b838309e Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Tue, 4 Aug 2026 12:03:18 -0400 Subject: [PATCH 3/3] fix: keep the adr-smadr-owned docs in provenance coverage via listAllGatedDocs The #203 carve-out removed type: adr docs from listGatedDocs(), which silently shrank provenance-corpus-check.mjs's default corpus even though the ADRs (and the adr template) are still gated by the adr-smadr job. Add listAllGatedDocs() -- the mif-validate corpus plus the adr-smadr-owned docs -- switch provenance coverage to it, clarify listGatedDocs()'s doc-comment, and pin the union with a regression test. --- scripts/lib/corpus.mjs | 17 +++++++++++++++-- scripts/provenance-corpus-check.mjs | 13 +++++++------ tests/corpus.test.mjs | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/scripts/lib/corpus.mjs b/scripts/lib/corpus.mjs index 9ad8aae..b823d8f 100644 --- a/scripts/lib/corpus.mjs +++ b/scripts/lib/corpus.mjs @@ -93,8 +93,21 @@ export function listL2Docs() { return L2_GLOBS.flatMap((g) => globSync(g)).sort(); } -// The full corpus this suite gates with mif-validate at any level -- what -// engine-parity.mjs asserts node/mif-rs agreement over. +// The mif-validate corpus at any level -- what engine-parity.mjs asserts +// node/mif-rs agreement over. NOT every doc CI gates: the `type: adr` +// documents (listAdrDocs) and the adr template (ADR_TEMPLATE_CARVEOUT) are +// gated by the adr-smadr job instead; cross-cutting consumers that mean +// "every gated doc" want listAllGatedDocs(). export function listGatedDocs() { return [...listTemplates(), ...listL3Docs(), ...listL2Docs()].sort(); } + +// Every document ANY CI job gates, regardless of which gate owns it: the +// mif-validate corpus plus the adr-smadr-owned docs (the `type: adr` +// documents and the adr template). Cross-cutting reports like +// provenance-corpus-check.mjs use this so the ADR carve-out (#203) cannot +// silently shrink their coverage -- an ADR's provenance matters just as much +// as any other gated doc's. +export function listAllGatedDocs() { + return [...listGatedDocs(), ...listAdrDocs(), ADR_TEMPLATE_CARVEOUT].sort(); +} diff --git a/scripts/provenance-corpus-check.mjs b/scripts/provenance-corpus-check.mjs index bba86b6..aa8e64f 100644 --- a/scripts/provenance-corpus-check.mjs +++ b/scripts/provenance-corpus-check.mjs @@ -4,10 +4,11 @@ // // provenance-corpus-check [--dir ] [--ledger ] // -// Default corpus: the suite's own gated docs (scripts/lib/corpus.mjs — the -// same one definition ci.yml, release.yml and engine-parity.mjs consume), so -// this report can never disagree with the validation gates about what "the -// corpus" is. --dir switches to every .md under one tree instead. +// Default corpus: every doc the suite gates (scripts/lib/corpus.mjs's +// listAllGatedDocs — the mif-validate corpus PLUS the adr-smadr-owned ADRs +// and adr template, so the #203 carve-out can't shrink provenance coverage), +// built on the same one definition ci.yml, release.yml and engine-parity.mjs +// consume. --dir switches to every .md under one tree instead. // // Classification per document: // witnessed — the provenance block carries the stamp marker (a @@ -25,7 +26,7 @@ import { globSync } from "node:fs"; -import { listGatedDocs } from "./lib/corpus.mjs"; +import { listAllGatedDocs } from "./lib/corpus.mjs"; import { readLedger } from "./lib/provenance-ledger.mjs"; import { classifyProvenance, modelOf } from "./lib/provenance-classify.mjs"; @@ -42,7 +43,7 @@ for (let i = 0; i < args.length; i++) { } } -const files = (dir ? globSync(`${dir}/**/*.md`) : listGatedDocs()).sort(); +const files = (dir ? globSync(`${dir}/**/*.md`) : listAllGatedDocs()).sort(); if (files.length === 0) { console.error(`provenance-corpus-check: no .md files found${dir ? ` under ${dir}` : ""}`); process.exit(1); diff --git a/tests/corpus.test.mjs b/tests/corpus.test.mjs index 99fdd38..c464594 100644 --- a/tests/corpus.test.mjs +++ b/tests/corpus.test.mjs @@ -17,6 +17,7 @@ import { listL3Docs, listL2Docs, listGatedDocs, + listAllGatedDocs, listAdrDocs, ADR_TEMPLATE_CARVEOUT, L3_DIRS, @@ -59,6 +60,21 @@ test('listGatedDocs is exactly the union of templates + L3 + L2', () => { for (const f of union) assert.ok(gated.has(f), `${f} missing from listGatedDocs()`); }); +test('listAllGatedDocs unions the adr-smadr-owned docs back in (#203 coverage regression)', () => { + // The ADR carve-out removes `type: adr` docs and the adr template from the + // mif-validate corpus, but they are still gated (by the adr-smadr job) -- + // cross-cutting consumers like provenance-corpus-check.mjs must keep seeing + // them, or coverage silently shrinks. + const all = new Set(listAllGatedDocs()); + const expected = new Set([...listGatedDocs(), ...listAdrDocs(), ADR_TEMPLATE_CARVEOUT]); + assert.equal(all.size, expected.size); + for (const f of expected) assert.ok(all.has(f), `${f} missing from listAllGatedDocs()`); + for (const f of globSync('docs/adr/*.md')) { + assert.ok(all.has(f), `${f} must stay in the all-gated corpus despite the carve-out`); + } + assert.ok(all.has(ADR_TEMPLATE_CARVEOUT), 'the adr template is gated (by adr-smadr), so it belongs here'); +}); + test('listL3Docs fails closed when an L3 directory is missing', () => { // Exercise the guard in isolation, in a scratch cwd with only 4 of the 5 // configured L3 trees present, so a renamed/deleted tree can never