feat(standards): Add an advisory tier and converge presentation
The estate had one severity, so every human-facing guideline had to either block a merge or be deleted. That fork is what produced allow_failure: true on the vale job: a red job nobody reads, which is worse than either option, because it trains the reader to ignore the colour.
The tier
contract <name> advisory marks a standard whose tripped assertions print ADVISE and leave the exit code at zero. Severity is declared per standard, not per assertion, so a check.sh cannot quietly downgrade the one assertion it keeps tripping over, and moving a standard between tiers is a one-word reviewed diff. A typo in the severity argument exits 2 rather than silently defaulting to blocking.
No new primitive. Every primitive already routed its negative result through _fail, so the tier is a change to that one function and the closed set of ten is untouched.
This behaviour was documented and unimplemented. README.md stated that a standard at introduced "ships its check but never blocks merge", and nothing reads the status: frontmatter field: the only blocking-suppression path is check-with-deviations.sh, which requires a registered deviation per FAIL. So the standards at introduced have been blocking all along, contrary to their own documentation. Tier (mechanical, declared in check.sh) and status (lifecycle position, documentation only) are now separate axes and both say what they are. This MR does not flip any existing standard's tier.
The convergence
repository + naming + posture become presentation: 27 assertions to 14, blocking to advisory.
Advisory is safe because no control in compliance/unified.yaml names this standard or named any of its three predecessors. All three sat in the set of standards no control references, so the tier change breaks no regulatory binding. There is no framework obligation behind a badge row, and blocking a merge on one spends maintainer goodwill on taste.
What went, and why each was not load-bearing:
- Three bare
check_file_existscalls on the brand SVGs.check_xml_wellformedfails a missing file, so they asserted nothing the well-formedness check did not already cover. That also takes the primitive from one call site to three, which is what it needed to justify its place in the closed set. - Three of four independently-asserted README badges, keeping the identity badge. Four ways to trip on one badge row was the punitive shape; the full row stays in
templates/README.md. - A footer-attribution assertion duplicated verbatim across
repositoryandnaming. Now stated once. - Two of three
check_file_lackscalls for deprecated wordmark forms, now one alternation with identical coverage.
One property is lost, and says so
The rule that a package description must not lead with the umbrella name is now documented under "Stated, not checked" in standards/presentation/SKILL.md and explicitly marked unenforced.
POSIX ERE has no negative lookahead, and check_json_key / check_toml_key compare a value against an ERE. So naming emulated the lookahead as a character-by-character alternation, roughly 1500 characters per assertion, three times over, for a rule a reader states in eight words.
The clean fix is a primitive asserting a structured-config value does NOT match, the negative companion to check_json_key in the same way check_file_lacks companions check_file_contains. It would close a class and would have three call sites immediately, so it may earn its place later. It was deliberately not added in a change whose purpose is reduction.
llms-txt asserts both forms
The single check_grep 'llms*' '.' assertion passed on any file whose basename began with llms, so a site shipping only /llms.txt and a site shipping only /llms-full.txt were indistinguishable from a conforming one. Now two assertions: the short form (a curated index per llmstxt.org) and the long form (the site's prose concatenated for one-fetch loading). Each also accepts a build-time generator source, since an Astro integration emits both files and leaves no static source to read.
The single-page carve-out is gone. It made both shipped starters non-conformant the moment the check could tell the forms apart, so templates/lab/ and templates/docs/ now ship llms-full.txt. "Emit both" is a simpler rule to hold than "emit both unless".
The proxy is still source-side and the SKILL now says where the limit is: shape cannot be asserted here, because requiring an H1 in llms.txt would fail every generator-based site. Verifying the deployed pair needs a check that runs after the site builds. The catalog's site-check component is that seam and currently has no consumers.
Verification
Run locally before pushing:
just validate— all gates green: 31 controls, 20 standards, binding intact.just guard— 20 files, 0 violations. The whitelist matches on the leading token, socontract presentation advisoryneeded no guard change.just check— 20 standards passed, 0 failed. 88 assertions become 61.
The tier was proven both directions before anything was built on it. Against a deliberately non-conforming fixture, presentation reports 11 ADVISE and exits 0; the identical assertions with the severity argument removed report 11 FAIL and exit 1. A scaffolded lab project passes llms-txt, and a site carrying only the short form now fails the long-form assertion where the old glob passed it.
Two loose threads, neither introduced here
validate-binding reports clone-upstream as having one consumer. A comment-aware scan of every .gitlab-ci.yml on the default branch of all 57 projects under gitlab-com/public-sector/ finds none. Either the consumer sits outside the group or the note is stale.
The standards currently at introduced are blocking despite their documentation, as described above. Whether they should be advisory is a separate decision and a separate MR.