Skip to content

feat(spec-vendor): build-time REQUIREMENTS from vendored frontmatter#29

Merged
brettdavies merged 8 commits intodevfrom
feat/spec-vendor-build-time-requirements
Apr 27, 2026
Merged

feat(spec-vendor): build-time REQUIREMENTS from vendored frontmatter#29
brettdavies merged 8 commits intodevfrom
feat/spec-vendor-build-time-requirements

Conversation

@brettdavies
Copy link
Copy Markdown
Owner

Summary

Vendors agentnative-spec at the v0.2.0 tag (commit 83bf0fd) under src/principles/spec/ and replaces the
hand-maintained 327-line REQUIREMENTS slice with a build-time generator that parses the vendored frontmatter. Adds
spec_version to the scorecard JSON so consumers know which spec contract a scorecard's IDs map to. Resets the
scorecard schema_version from 1.20.3 to match anc's pre-launch posture (no public consumers; will lock at
1.0 on first release).

Implements docs/plans/2026-04-23-001-feat-spec-vendor-plan.md in
full. Resolves Open Question (a) in the spec-side plan 001 (vendoring pattern: commit-a-copy chosen).

Changelog

Added

  • Vendored agentnative-spec snapshot under src/principles/spec/ with scripts/sync-spec.sh for pinned-tag resync
    (extracts via git show <ref> so the spec checkout's working tree is not perturbed).
  • spec_version field in anc check --output json scorecard, sourced at build time from vendored
    src/principles/spec/VERSION. Pin against this to know which spec contract a scorecard's requirement IDs reference.

Changed

  • REQUIREMENTS is now generated at build time from vendored frontmatter; no hand-maintained duplicate. No scoring
    behavior change — pre/post diff verified byte-identical across all 33 check results, summaries, and coverage totals.
  • Scorecard schema_version reset 1.20.3. Pre-launch correction; the schema is at 0.x while anc is
    pre-launch and will lock at 1.0 on first public release. No public consumers exist today.

Documentation

  • AGENTS.md "Spec source" section rewritten to describe build-time vendoring and resync cadence (SPEC_REF env var
    bumps the vendored tag).

Type of Change

  • feat: New feature (non-breaking change which adds functionality)

Related Issues/Stories

  • Plan: docs/plans/2026-04-23-001-feat-spec-vendor-plan.md
  • Parent roadmap (spec repo): agentnative:docs/plans/2026-04-22-002-post-frontmatter-roadmap.md item 5
  • Sibling plan (site repo): agentnative-site:docs/plans/2026-04-23-001-feat-sync-spec-plan.md
  • Resolves: spec plan 001 Open Question (a) — vendoring pattern (commit-a-copy chosen)

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing completed
  • All tests passing

Test Summary:

  • Build-parser fixtures (NEW): 12 tests covering 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.
  • Drift-check tests (NEW, in src/principles/matrix.rs): R4 (every Check::covers() ID resolves), R5 (every MUST is
    covered or explicitly allowlisted), R5-hygiene (allowlist references real MUST ids).
  • Scorecard tests (UPDATED): spec_version presence asserted; integration test EXPECTED key set extended.
  • Full sweep clean: 51 integration + ≥401 unit tests pass; cargo clippy --all-targets --tests -D warnings clean;
    cargo fmt --check clean; cargo deny check clean; 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 — driver
  • build_support/parser.rs — testable parser shared with tests/build_parser.rs via #[path] include
  • scripts/sync-spec.sh — resync script
  • src/principles/spec/{VERSION,CHANGELOG.md,README.md,principles/p1-p7.md} — vendored snapshot
  • tests/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 TODO
    capturing the U5 placement deviation (binary-only crate → integration tests can't reach internal API)

Modified:

  • src/principles/registry.rs — 327-line REQUIREMENTS slice removed; replaced with include!() of build.rs output
  • src/principles/matrix.rs — adds R4/R5/hygiene drift tests inline (deviation from plan U5's
    tests/requirements_drift.rs placement; documented in commit e9860f1 and todo 016)
  • src/scorecard/mod.rs — adds spec_version field; resets SCHEMA_VERSION to 0.3; refactors comment narrative away
    from vN.M references that implied a v1 stability promise
  • tests/integration.rsspec_version added to EXPECTED key set; schema assertions updated to 0.3
  • Cargo.toml / Cargo.lockserde_yaml = "=0.9.34" added to [build-dependencies] and [dev-dependencies]
  • AGENTS.md — JSON-surface section + spec-source section refreshed for vendoring + resync cadence

Key Features

  • Single source of truth — IDs in vendored spec frontmatter ARE the contract; no two-copy drift risk.
  • Loud build-time errors when vendored frontmatter is malformed (every diagnostic cites file + id + field).
  • Drift tests fail loudly when a check's covers() references a removed requirement, or when a new spec MUST has no
    covering check and isn't allowlisted.
  • Resync is a single command (scripts/sync-spec.sh with optional SPEC_REF env var) that doesn't perturb the spec
    checkout's working tree.

Benefits

  • Eliminates a 327-line hand-maintained duplicate of the spec contract.
  • Future spec bumps require running one script + reviewing the diff — not editing two files in sync.
  • Scorecard spec_version lets the site, leaderboard, and any external consumer pin against the exact spec build the
    CLI ships against.

Breaking Changes

  • No 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_version field; nothing renamed or removed).

Deployment Notes

  • No special deployment steps required

The build script reads vendored files at compile time. End users running prebuilt binaries see no change beyond the new
spec_version field in JSON output.

Checklist

  • Code follows project conventions and style guidelines
  • Commit messages follow Conventional Commits
  • Self-review of code completed
  • Tests added/updated and passing
  • No new warnings or errors introduced
  • Changes are backward compatible (or breaking changes documented)

Additional Context

Plan deviations (documented in commits and the P1 TODO):

  1. R6 schema_version — plan called for 1.2; landed at 0.3 instead. Adjustment surfaced during PR conversation
    (2026-04-27): 1.x implied a stability promise the project hadn't earned. Schema is at 0.x while pre-launch; locks
    at 1.0 on first public release. Commit 242c57a carries the change.

  2. U5 placement — plan called for tests/requirements_drift.rs; tests landed in src/principles/matrix.rs instead.
    The crate has no [lib] target (binary-only), so external integration tests cannot reach REQUIREMENTS or
    all_checks_catalog without 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 their
    planned 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 in
UNVERIFIED_MUSTS. These are real MUSTs the team hasn't gotten to, not requirements deemed unenforceable — they are
roadmap items for the launch coverage push.

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 brettdavies merged commit 9a264f9 into dev Apr 27, 2026
6 checks passed
@brettdavies brettdavies deleted the feat/spec-vendor-build-time-requirements branch April 27, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant