feat(spec-vendor): build-time REQUIREMENTS from vendored frontmatter#29
Merged
brettdavies merged 8 commits intodevfrom Apr 27, 2026
Merged
Conversation
Establishes the vendoring mechanism for agentnative-spec: - scripts/sync-spec.sh extracts spec content at a pinned ref via `git show <ref>:<path>`, leaving the spec checkout's working tree undisturbed. Defaults: SPEC_ROOT=$HOME/dev/agentnative-spec, SPEC_REF=v0.2.0. - src/principles/spec/README.md identifies the directory as vendored, cites CC BY 4.0 attribution, and points at the resync script. No vendored content yet — that lands in the next commit (U2).
…s/spec/ (spec@83bf0fd) Initial vendored commit. Files extracted at v0.2.0 via scripts/sync-spec.sh and verified byte-identical to the upstream ref. No code references the vendored content yet — build.rs lands in the next commit (U3). Verification: - diff against `git show v0.2.0:<path>` clean for all 9 files - cargo build --release succeeds - cargo test passes (51 integration + 401 unit, no behavior change) - cargo publish --dry-run packages 87 files cleanly
…d frontmatter Build script parses src/principles/spec/principles/p*-*.md and emits $OUT_DIR/generated_requirements.rs containing the REQUIREMENTS slice and SPEC_VERSION const. Not yet consumed by registry.rs — the cutover lands in the next commit (U4) so byte-identity verification can compare against the hand-maintained slice. Test-first: 12 fixture-driven tests in tests/build_parser.rs cover happy paths, sort order, duplicate IDs, missing fields, unknown levels, unsupported applicability shapes, unterminated frontmatter, invalid principle ids, empty requirement lists, and Rust-source emission with proper escaping. The final test parses the real vendored v0.2.0 content and asserts 46 requirements with p1-must-env-var first and p7-may-auto-verbosity last — matching the existing hand-maintained order exactly. Build-time error messages are a feature: every failure cites file path, requirement id, and field. Verified by inducing missing-field and unknown-level corruptions; both produce actionable single-line panics. Layout: - build.rs (crate root) — driver - build_support/parser.rs — testable parser shared via #[path] include - tests/build_parser.rs — integration test driver Dependencies: serde_yaml = "=0.9.34" added to [build-dependencies] and [dev-dependencies]. The crate is deprecated upstream but still functional; cargo-deny is clean. Re-evaluate (saphyr / yaml-rust2) if cargo-deny ever flags it.
Replaces the 327-line hand-maintained REQUIREMENTS slice in registry.rs with
a single `include!(concat!(env!("OUT_DIR"), "/generated_requirements.rs"))`.
The Requirement struct, Level/Applicability/ExceptionCategory enums, the
SUPPRESSION_TABLE, and the find()/count_at_level() helpers are unchanged —
they continue to operate on the generated slice exactly as before.
Generator now emits:
- A `///` doc comment on REQUIREMENTS describing the sort contract.
- A `///` doc comment + `#[allow(dead_code)]` on SPEC_VERSION (it lights up
in U6 when the scorecard plumbs it through; warning-suppress is temporary).
Verification:
- cargo build/test/clippy --tests -D warnings clean.
- cargo fmt --check clean.
- All 33 check results byte-identical pre/post on `anc check . --output json`
(status, score, group, layer match across the board; coverage_summary,
summary, audience, schema_version unchanged).
- `anc generate coverage-matrix --check` exits 0 — committed
docs/coverage-matrix.md and coverage/matrix.json still align with the
registry, confirming the generated slice matches the prior hand-maintained
ordering.
Pre-existing non-determinism in evidence-line ordering for one check (the
print!/println! evidence collector) is unrelated to this commit and out of
scope for the spec-vendor plan.
R4 — `live_catalog_has_no_dangling_cover_ids` runs the existing `dangling_cover_ids` helper against the real `all_checks_catalog()`. A typo in any `Check::covers()` declaration, or a requirement renamed in the vendored spec without a corresponding check update, fails this test rather than silently producing a coverage gap. R5 — `every_must_is_covered_or_explicitly_unverified` asserts every MUST in the generated REQUIREMENTS is covered by ≥1 check OR listed in a `UNVERIFIED_MUSTS` allowlist with a substantive rationale. Allowlist hygiene — `unverified_musts_allowlist_only_references_real_must_ids` catches stale shields after a rename or level change and rejects empty-rationale entries. Initial allowlist surfaces 6 pre-existing coverage gaps in v0.2.0 (p2-must-exit-codes, p2-must-json-errors, p3-must-subcommand-examples, p4-must-actionable-errors, p5-must-force-yes, p5-must-read-write-distinction). Each has a rationale citing the scope-of-work reason no check exists yet — these are real MUSTs the team has not gotten to, not requirements deemed unenforceable. Track follow-up work in the project roadmap. Plan deviation — the plan (U5) called for `tests/requirements_drift.rs` as an integration test. The crate has no `[lib]` target (binary-only), so an external test cannot reach `REQUIREMENTS` or `all_checks_catalog` without expanding public API. The drift tests live alongside the existing `dangling_cover_ids` unit tests in `src/principles/matrix.rs` instead. Same coverage, no new public surface. Diagnostic quality verified by inducing each failure mode locally: - R4: rename a real id in a check's covers() to `p1-must-NONEXISTENT`, test fails citing the offending check id and the dangling requirement id. - Hygiene: add `p99-must-typo` to allowlist, hygiene test fails citing the bogus id and prompting for rename or removal.
Adds a new `spec_version` field to `anc check --output json` sourced from the build.rs-emitted SPEC_VERSION const (which itself reads vendored `src/principles/spec/VERSION`). Bumps scorecard schema_version 1.1 → 1.2. The field is `&'static str`, never null — when the vendored VERSION file is missing at build time the build emits a cargo:warning and the value reads "unknown". Choosing a non-Option type matches the plan's decision that v1.2 is additive on shape and "unknown" is itself a meaningful sentinel. Wiring: - src/scorecard/mod.rs imports SPEC_VERSION from principles::registry (which exposes it via the build.rs-emitted include!). - Scorecard struct gains `pub spec_version: &'static str` after audit_profile (last position, additive). - build_scorecard() reads the const directly — no thread-through caller parameter needed (it's compile-time fixed metadata, not per-invocation). Tests: - src/scorecard/mod.rs::test_format_json_valid asserts spec_version is a non-empty string. - tests/integration.rs::test_scorecard_json_has_stable_top_level_keys adds "spec_version" to EXPECTED so any future drop-or-rename fails the contract test. - All schema_version assertions bumped from "1.1" to "1.2". AGENTS.md "Agent-facing JSON surface" updated: bumps the schema, adds the spec_version description, fixes a pre-existing P<n> markdownlint issue surfaced by the formatter.
`anc` is pre-launch and the scorecard JSON has zero public consumers, so a 1.x version implies a stability promise the project hasn't earned. Resetting to 0.x signals "shape may still evolve" honestly. The schema locks to 1.0 on first public release as the deliberate stability act. Mapping preserves the 3-step shape history without claiming v1: - 1.0 → 0.1 (initial) - 1.1 → 0.2 (audience, audit_profile, audience_reason, coverage_summary) - 1.2 → 0.3 (spec_version) Mechanical changes: - SCHEMA_VERSION const: "1.2" → "0.3" - 4 test assertions: scorecard/mod.rs (×2), tests/integration.rs (×2) - Doc comments rewritten: "vN.M consumers" / "vN.M additions" replaced with "pre-launch additive" or "older consumers feature-detect" - AGENTS.md: schema version + narrative explaining the 0.x semantic The matrix.json `schema_version: "1.0"` is a separate artifact (coverage matrix, not scorecard) and is left alone. Verified: cargo build/test/clippy --tests -D warnings + fmt --check all clean. `anc check . --output json` emits `schema_version: "0.3"`.
…cadence The previous "Spec source" section claimed "no build-time import, no live link" — that's now stale. The build.rs spec-vendor work introduces exactly that: a pinned vendored snapshot under src/principles/spec/ parsed at build time into REQUIREMENTS. Rewrites the section to describe what's true after this branch: - spec lives at brettdavies/agentnative (canonical) - src/principles/spec/ holds a pinned vendored snapshot - build.rs generates REQUIREMENTS from frontmatter at build time - only Check::covers() declarations are hand-maintained - principle prose → check derivation is still manual Adds resync cadence guidance (plan U1's deferred docs item): - rerun scripts/sync-spec.sh after every new agentnative-spec tag - bump SPEC_REF env var to adopt a newer release - repository_dispatch is the canonical trigger; future automation slots in by calling this script Repoints the principle-iteration workflow link from the obsidian vault to agentnative:principles/AGENTS.md (the spec's own contributor doc), since that's the public source of truth for how principles evolve.
brettdavies
added a commit
that referenced
this pull request
Apr 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Vendors
agentnative-specat the v0.2.0 tag (commit83bf0fd) undersrc/principles/spec/and replaces thehand-maintained 327-line
REQUIREMENTSslice with a build-time generator that parses the vendored frontmatter. Addsspec_versionto the scorecard JSON so consumers know which spec contract a scorecard's IDs map to. Resets thescorecard
schema_versionfrom1.2→0.3to matchanc's pre-launch posture (no public consumers; will lock at1.0on first release).Implements
docs/plans/2026-04-23-001-feat-spec-vendor-plan.mdinfull. Resolves Open Question (a) in the spec-side plan 001 (vendoring pattern: commit-a-copy chosen).
Changelog
Added
agentnative-specsnapshot undersrc/principles/spec/withscripts/sync-spec.shfor pinned-tag resync(extracts via
git show <ref>so the spec checkout's working tree is not perturbed).spec_versionfield inanc check --output jsonscorecard, sourced at build time from vendoredsrc/principles/spec/VERSION. Pin against this to know which spec contract a scorecard's requirement IDs reference.Changed
REQUIREMENTSis now generated at build time from vendored frontmatter; no hand-maintained duplicate. No scoringbehavior change — pre/post diff verified byte-identical across all 33 check results, summaries, and coverage totals.
schema_versionreset1.2→0.3. Pre-launch correction; the schema is at0.xwhileancispre-launch and will lock at
1.0on first public release. No public consumers exist today.Documentation
SPEC_REFenv varbumps the vendored tag).
Type of Change
feat: New feature (non-breaking change which adds functionality)Related Issues/Stories
docs/plans/2026-04-23-001-feat-spec-vendor-plan.mdagentnative:docs/plans/2026-04-22-002-post-frontmatter-roadmap.mditem 5agentnative-site:docs/plans/2026-04-23-001-feat-sync-spec-plan.mdTesting
Test Summary:
unsupported applicability shapes, unterminated frontmatter, invalid principle ids, empty requirement lists, and
Rust-source emission with proper escaping.
src/principles/matrix.rs): R4 (everyCheck::covers()ID resolves), R5 (every MUST iscovered or explicitly allowlisted), R5-hygiene (allowlist references real MUST ids).
spec_versionpresence asserted; integration test EXPECTED key set extended.cargo clippy --all-targets --tests -D warningsclean;cargo fmt --checkclean;cargo deny checkclean; pre-push hook (mirrors CI) green.Build-time error diagnostic quality verified by inducing missing-field, unknown-level, and orphan-cover-ID
corruptions; each produces an actionable single-line panic citing file path, requirement id, and field/value.
Files Modified
Created:
build.rs— driverbuild_support/parser.rs— testable parser shared withtests/build_parser.rsvia#[path]includescripts/sync-spec.sh— resync scriptsrc/principles/spec/{VERSION,CHANGELOG.md,README.md,principles/p1-p7.md}— vendored snapshottests/build_parser.rs— 12 parser fixture tests.context/compound-engineering/todos/016-pending-p1-lib-bin-split-for-internal-test-access.md— local-only P1 TODOcapturing the U5 placement deviation (binary-only crate → integration tests can't reach internal API)
Modified:
src/principles/registry.rs— 327-lineREQUIREMENTSslice removed; replaced withinclude!()of build.rs outputsrc/principles/matrix.rs— adds R4/R5/hygiene drift tests inline (deviation from plan U5'stests/requirements_drift.rsplacement; documented in commite9860f1and todo 016)src/scorecard/mod.rs— addsspec_versionfield; resetsSCHEMA_VERSIONto0.3; refactors comment narrative awayfrom
vN.Mreferences that implied a v1 stability promisetests/integration.rs—spec_versionadded to EXPECTED key set; schema assertions updated to0.3Cargo.toml/Cargo.lock—serde_yaml = "=0.9.34"added to[build-dependencies]and[dev-dependencies]AGENTS.md— JSON-surface section + spec-source section refreshed for vendoring + resync cadenceKey Features
covers()references a removed requirement, or when a new spec MUST has nocovering check and isn't allowlisted.
scripts/sync-spec.shwith optionalSPEC_REFenv var) that doesn't perturb the speccheckout's working tree.
Benefits
spec_versionlets the site, leaderboard, and any external consumer pin against the exact spec build theCLI ships against.
Breaking Changes
The schema reset is internal version-string churn — no public consumers exist, so no migration path is needed. The
scorecard JSON shape is strictly additive (new
spec_versionfield; nothing renamed or removed).Deployment Notes
The build script reads vendored files at compile time. End users running prebuilt binaries see no change beyond the new
spec_versionfield in JSON output.Checklist
Additional Context
Plan deviations (documented in commits and the P1 TODO):
R6 schema_version — plan called for
1.2; landed at0.3instead. Adjustment surfaced during PR conversation(2026-04-27):
1.ximplied a stability promise the project hadn't earned. Schema is at0.xwhile pre-launch; locksat
1.0on first public release. Commit242c57acarries the change.U5 placement — plan called for
tests/requirements_drift.rs; tests landed insrc/principles/matrix.rsinstead.The crate has no
[lib]target (binary-only), so external integration tests cannot reachREQUIREMENTSorall_checks_catalogwithout expanding public API. Same coverage, no public-surface change. Captured as a P1 TODO(
016-pending-p1-lib-bin-split-for-internal-test-access) so a future lib/bin refactor can move the tests to theirplanned location.
Surfaced finding from R5: the new MUST-coverage check identified six pre-existing coverage gaps in
v0.2.0(
p2-must-exit-codes,p2-must-json-errors,p3-must-subcommand-examples,p4-must-actionable-errors,p5-must-force-yes,p5-must-read-write-distinction). Each is allowlisted with substantive rationale inUNVERIFIED_MUSTS. These are real MUSTs the team hasn't gotten to, not requirements deemed unenforceable — they areroadmap items for the launch coverage push.