Files
msd-core/scripts/docs-guard-registry.cjs
Tom Boucher ddde001af6 enhance(#3873): the STATE.md schema — one owner, generated artifacts (#3880)
* test(#3873): failing-first locale parity, plus tripwires for what must not move

Pins ADR-3473 §8.8 at the artifact a reader actually sees. The English STATE.md
reference carries a Status lifecycle section that is missing from all four
translations — the section documenting the status enum whose clobbering is
#3853. The test derives the heading set rather than hard-coding the missing
one, and names the locale and the heading when it fails.

Two tripwires that must pass today and after. The field-drift guard still
catches a re-derived fallback ladder: §8.8 instructs deleting that script, and
that instruction rests on a wrong premise about what it guards, so the test
stops a future reader from deleting it on the ADR's word. And last_activity's
label resolution is pinned to what ships today, because it is declared in one
of the two tables this phase consolidates and not the other — the
consolidation must not silently pick a side.

The locale test buckets under docs rather than state, which is what it tests;
that bucket is allowlisted with justification rather than folded into an
unrelated docs suite. It reads only markdown, so it carries no allow-test-rule
marker — a marker there would suppress nothing and would grow the unverified
pool against its ceiling.

Refs #3873

* feat(#3873): one schema owns the STATE.md key set, three tables become projections

ADR-3473 §8.8. The key set was declared in four places that had to agree by
hand and already did not: FIELD_CLASSIFICATION, FRONTMATTER_BODY_SOURCE,
FRONTMATTER_KEY_TO_BODY_LABEL and buildStateFrontmatter's emit behavior. One
frozen null-prototype schema now declares each key's type, enum, cardinality,
source, preservation, body source, body label, accepted parse shapes and
whether it is emitted unconditionally; the three tables are derived from it at
module load.

The projections are byte-identical to the literals they replace, key order
included, and the parity tests compare against verbatim copies of today's
tables rather than re-deriving both sides from the schema — a parity test fed
from one source proves nothing, which is how a consolidation ships a changed
policy under a green test.

last_activity was the live disagreement: present in one table, absent from the
other. The schema declares what ships today rather than the tidier answer, and
a test pins it.

The schema is a leaf module and owns the four field-policy types, re-exported
from state-transition so existing importers are untouched — the same split
health-diagnostic-types made to break a CJS require cycle.

Refs #3873

* feat(#3873): generate the schema-derived regions, parity-check the prose tables

ADR-3473 §8.8's generator half. gen-state-md-docs.cjs owns marked regions in
the shipped template and all five reference docs, follows gen-features.cjs's
fail-closed contract, and is wired into regen:derived and lint:generated-sync.

The Status lifecycle section was missing from all four translations — the
section documenting the status enum behind #3853 — and is now generated into
every locale. Field cardinality is a new generated table: pure schema data,
no prose, so nothing to lose.

The Field-reference and Status-values tables are parity-CHECKED rather than
generated. Their Purpose, When-populated and Matched-text columns are
genuinely hand-translated per locale, and §8.8 itself says prose stays
hand-translated; generating them from an English registry would overwrite four
locales' translations on every write. The row set is checked against the schema
instead, so a key added to one and not the other fails, which is what field
drift actually means. Building that check found last_activity_desc
undocumented in all five tables.

Three keys the docs describe are absent from the schema — active_phase,
next_action, next_phases. They are grandfathered by name, not by wildcard, so a
fourth fails: a declared gap with a forcing function rather than a silent one.

Refs #3873

* fix(#3873): declare what the parsers do, and close the shape-parity gap

Two declarations in the new schema described intended behavior rather than
actual — the defect class this epic exists to end, committed inside the epic.
Both were caught by executing the parsers instead of reading their docstrings.

current_plan.acceptedShapes claimed ['N', 'N of M']. Standalone, the hybrid
shape errors; the path that looks like support is parseInt truncating '2 of 5'
to 2 and discarding the rest. Narrowed to ['N']. The parser is deliberately NOT
fixed here: that is #3784 and PR #3791 is already doing it. When #3791 lands
this row must widen, and the shape test will go red until it does — the schema
and the parser cannot drift apart quietly, which is what §8.8's checked-not-
generated rule is for.

STATUS_LIFECYCLE_ENUM claimed to be the closed set status can hold.
normalizeStateStatus passes unrecognized prose through unchanged, so it is not
closed at runtime. The seven members are the canonical values it maps onto; the
docstring now says that and the test asserts the real lenient contract.

Closes the acceptance item that a test asserts the parsers accept exactly the
declared shapes: the check is table-driven over every row carrying
acceptedShapes, guarded against passing vacuously on an empty set, and fails
loudly if a future row has no registered driver. Adds the unwired-label throw
and the fast-check property that every projection agrees with its schema row.

Refs #3873

* fix(#3873): keep the shipped template's frontmatter first, and make row 27 able to fail

The remote matrix caught 12 failures with one cause. Making the template's
frontmatter a generated region wrapped it in its own yaml fence ahead of the
markdown fence, so extractFileTemplate and readShippedStateTemplateBody — which
both match the single markdown block — found the heading first, not the
frontmatter. That breaks the contract every new project's STATE.md is created
from: bug #21 and epic #1969 B8 pin that the File Template block starts with
frontmatter and carries gsd_state_version.

The markers now sit inside the single markdown fence, so the fence opens before
the frontmatter and the region still ends ahead of the heading. Same layout as
before this phase, with markers embedded rather than a second fence.

Row 27 existed to catch exactly this and did not, because it was writer-seeded:
it asserted against the generator's own output shape, so it passed on the broken
template. It now parses the fence the way production does and was verified to
fail against the broken shape before being trusted against the fixed one. A test
that would not have caught the bug it exists to prevent is worse than no test.

The emitted-attribution failure was separate and the fragment was the wrong
remedy: gsd-core/templates/state.md self-attributes under a verbatim-copy
identity rule, so a diff touching it needs no acknowledgment. Fragment deleted
rather than left explaining nothing.

Refs #3873

* docs(#3873): how to change the STATE.md schema

The phase gate was right and my docs artifact was wrong. I listed
lint:generated-sync as the second enablement step, which is a verification
command dressed as one, and then claimed a one-step sequence owed no how-to.

The real sequence is build:lib then regen:derived, and the ordering is a trap:
the generator reads the COMPILED schema, so regenerating before building
regenerates against the previous schema and commits artifacts that look
plausible while disagreeing with the code just written. A reference table
cannot carry an ordering dependency; that is what the how-to test is for.

The page covers adding, changing and removing a key, every reason code the
check emits and what to do about each, what is generated versus hand-translated
and why the two prose-bearing tables are parity-checked instead of generated,
adding a language, and the three grandfathered keys. Indexed from docs/README.md.

Refs #3873

* chore(#3873): backfill changeset PR number

---------

Co-authored-by: sim <sim@local>
2026-08-26 01:57:47 -04:00

368 lines
17 KiB
JavaScript

#!/usr/bin/env node
'use strict';
/**
* docs-guard-registry.cjs — the sole source of truth for which doc-reading
* test files the docs-guard lane must run.
*
* ## Why this is its own module, not a `scripts/ci-test-scope.cjs` RULE
*
* A prior version of this PR added a `docs guards` RULE to `RULES` in
* scripts/ci-test-scope.cjs, on the theory that classify()'s `!codeChanged`
* normalization (which zeroes fullMatrix/targeted_tests/windows_tests for
* docs-only diffs) made the RULE inert to classify()'s scope decision.
*
* That is true for docs-ONLY diffs and FALSE for MIXED docs+code diffs:
* `codeChanged` is true whenever ANY changed file is product/pipeline code,
* so the normalization never runs, and every one of this registry's test
* files joined `targeted_tests` for every mixed PR. Probed on that version:
* `node scripts/ci-test-scope.cjs --files "docs/a.md src/semver.cts"`
* returned 25 targeted_tests, vs. 3 on `origin/next` — an unstated blowup
* of the scoped/targeted lane on every mixed docs+code PR.
*
* Root cause: `RULES` answers "given these changed files, what should
* test.yml run" for the MAIN scoped-lane pipeline. The docs-guard registry
* is a LANE MANIFEST for a completely different consumer (the `docs-lint`
* job in .github/workflows/docs-required.yml, gated on a `docs_changed`
* step output). Putting a lane manifest inside a scope-classification rule
* set was the defect; extracting it here removes any path by which it can
* influence classify() at all.
*
* ## Who reads this
*
* - .github/workflows/docs-required.yml (`docs-lint` job) — derives the
* PER-PR-SELECTED subset of this registry from scripts/select-docs-guards.cjs,
* which is itself driven by this module's DOCS_GUARD_TESTS map. This job has
* no `paths:` filter, so it always reports a status and can supply the
* already-required `docs-lint` context; a dedicated `paths:`-filtered
* workflow cannot be made required without hanging non-docs PRs forever.
* - scripts/lint-docs-guard-registration.cjs — derives its registration
* lint's comparison set from this module (so a docs-reading test file
* that is neither registered here nor exempted fails the lint).
* - scripts/select-docs-guards.cjs — the pure selector that maps a PR's
* changed docs/ paths to the subset of this registry that actually needs
* to run (#3753 follow-up: a flat "run everything" list is disproportionate
* for a single-file docs typo fix).
*
* ## Registry shape (#3753 follow-up)
*
* `DOCS_GUARD_TESTS` is a MAP from test file path to the array of docs/
* paths it actually reads, so a changed-docs-file can be resolved to the
* narrow subset of guards that read it, instead of always running the
* entire registry. Each value is a non-empty array of PATTERNS:
*
* - a plain path (e.g. `'docs/AGENTS.md'`) matches that exact file only;
* - a trailing-slash path (e.g. `'docs/adr/'`) matches any changed path
* under that directory prefix — use this for a test that walks or
* `readdirSync`s a whole docs/ subdirectory;
* - the sentinel `'*'` means "run on ANY docs/ change" — reserved for a
* test that cannot be resolved to a narrower set of paths (the path is
* computed, looped over an unresolvable variable, or the test walks
* docs/ generally). Conservative fallback: when in doubt, use `'*'`,
* never a guessed-narrow path — a false negative here (a guard that
* silently stops running) is exactly the #3753 defect class this
* registry exists to prevent.
*
* `DOCS_GUARD_TEST_FILES` (derived: `Object.keys(DOCS_GUARD_TESTS)`) is kept
* as a flat array export so the registration lint and its parity test
* continue to consume a plain file list without needing to know about the
* map shape.
*
* ## Adding a docs guard
*
* Add the test file's path (relative to the repo root, `tests/<file>`) as a
* key in DOCS_GUARD_TESTS below, with the docs/ paths it reads as the value
* (or `['*']` if that cannot be resolved narrowly). Do not duplicate this
* list anywhere else — a second, independently maintained list is exactly
* the #3753 defect class (10 registered-but-not-run guards, silently
* drifted) this registry exists to prevent from recurring.
*
* Sorted alphabetically by basename for unambiguous diffs.
*/
/**
* Duplicate of scripts/run-tests.cjs:50's `SUITES` array. That array is not
* exported by run-tests.cjs (module.exports there is deliberately narrow;
* run-tests.cjs's own behavior is intentional and out of scope for this
* registry to alter), so it cannot be imported directly without changing a
* shared, behavior-locked file.
*
* Why this must exist at all: `run-tests.cjs`'s `selectExplicitFiles`
* (scripts/run-tests.cjs:651) treats any registry entry that EQUALS a
* SUITES member (e.g. `all`, `unit`) as a suite selector, not a filename —
* so a typo in DOCS_GUARD_TESTS matching a suite name would silently run
* the ENTIRE suite inside the required `docs-lint` job instead of erroring.
* `assertNoSuiteCollision` below rejects that at the registry boundary
* instead.
*
* Divergence risk: if run-tests.cjs's real SUITES list ever changes and this
* copy is not updated, this check could under- or over-reject. That risk is
* covered by a parity test (tests/ci-docs-guard-registry.test.cjs) that
* drives run-tests.cjs's own exported `selectExplicitFiles` behaviorally —
* for every token here it asserts run-tests.cjs treats it as a suite
* selector (never "file not found"), and for a control non-member it
* asserts the opposite — so a real divergence fails that test rather than
* silently drifting.
*/
const RUN_TESTS_SUITES = ['all', 'unit', 'integration', 'install', 'security', 'slow', 'qa'];
/**
* Normalize a registry entry EXACTLY the way scripts/run-tests.cjs's
* `splitFileList` (:617-625) normalizes a requested token before its SUITES
* check (:651): strip a leading `tests/` and normalize `\`->`/`. Without this,
* comparing RAW registry keys (which all carry the `tests/` prefix by
* convention) against RUN_TESTS_SUITES misses the realistic typo `'tests/all'`
* entirely — proven by probe: `assertNoSuiteCollision(['tests/all'])` did not
* throw, and `selectExplicitFiles(allFiles, 'tests/all')` selected all 824
* files (the whole suite) inside the required docs-lint job.
*
* @param {string} t
* @returns {string}
*/
function normalizeForSuiteCheck(t) {
return t.replace(/\\/g, '/').replace(/^tests\//, '');
}
/**
* Reject any docs-guard registry entry that collides with a run-tests.cjs
* suite token (see RUN_TESTS_SUITES doc comment above). Throws with every
* offending entry named, rather than failing on only the first. Compares
* entries AFTER normalizeForSuiteCheck, mirroring run-tests.cjs's own
* splitFileList normalization, so a `tests/`-prefixed or backslash-spelled
* entry that would collide post-normalization is caught here too.
*
* @param {string[]} tests
*/
function assertNoSuiteCollision(tests) {
const collisions = tests.filter((t) => RUN_TESTS_SUITES.includes(normalizeForSuiteCheck(t)));
if (collisions.length > 0) {
throw new Error(
'docs-guard-registry: DOCS_GUARD_TESTS entry collides with a run-tests.cjs SUITES token ' +
`(${collisions.join(', ')}) — scripts/run-tests.cjs:651's selectExplicitFiles treats a ` +
'registry entry that equals a suite name as a suite selector, not a filename, so this ' +
'would silently run the entire suite instead of the intended file(s). Fix the registry entry.',
);
}
}
/**
* Map from docs-guard test file to the docs/ path patterns it reads. See
* this module's header comment for pattern semantics (exact / trailing-slash
* dir-prefix / `'*'` sentinel). Every value here was derived by reading the
* test file's actual read call(s) — not guessed — per #3753's own lesson: an
* unresolvable read is recorded as `'*'`, never a narrowed guess.
*/
const DOCS_GUARD_TESTS = {
'tests/adr-15-progress-converge.test.cjs': [
'docs/COMMANDS.md',
'docs/how-to/run-phases-autonomously.md',
],
// Walks docs/adr/ as a directory (builds/reads docs/adr/README.md and
// fixture ADRs throughout) and separately reads docs/contributor-standards.md.
'tests/adr-index-gate.test.cjs': ['docs/adr/', 'docs/contributor-standards.md'],
// Walks docs/features/ as a directory (the fragment corpus) and asserts the
// committed docs/FEATURES.md equals the generated projection of it (#3840).
'tests/features-index-gate.test.cjs': ['docs/features/', 'docs/FEATURES.md'],
'tests/agent-classification-parity.test.cjs': ['docs/AGENTS.md', 'docs/INVENTORY.md'],
// #3683: pins the learnings feature section's agreement with the canonical
// artifact registry (reads docs/FEATURES.md around the extract-learnings
// entry).
'tests/learnings.test.cjs': ['docs/FEATURES.md'],
'tests/analyze-dependencies.test.cjs': ['docs/COMMANDS.md'],
'tests/autonomous-converge.test.cjs': [
'docs/COMMANDS.md',
'docs/how-to/run-phases-autonomously.md',
],
'tests/capability-matrix-sync.test.cjs': ['docs/reference/capability-matrix.md'],
'tests/capability-registry.test.cjs': [
'docs/tutorials/build-your-first-capability.md',
'docs/tutorials/install-your-first-capability.md',
'docs/reference/capability-manifest.md',
],
'tests/claude-md.test.cjs': ['docs/COMMANDS.md'],
'tests/claude-orchestration.test.cjs': ['docs/explanation/claude-orchestration-capability.md'],
'tests/command-contract.test.cjs': [
'docs/INVENTORY.md',
'docs/ja-JP/INVENTORY.md',
'docs/ko-KR/INVENTORY.md',
'docs/zh-CN/INVENTORY.md',
'docs/pt-BR/INVENTORY.md',
],
// uncoveredFiles(...) scans 'docs' as a generic coverage-scan root
// (commit-files-pathspec.test.cjs:1618) — cannot be resolved to specific
// files without re-deriving the scan's own file-discovery logic.
'tests/commit-files-pathspec.test.cjs': ['*'],
'tests/config-field-docs.test.cjs': ['docs/CONFIGURATION.md'],
'tests/config.test.cjs': ['docs/CONFIGURATION.md'],
'tests/context-index-sync.test.cjs': ['docs/CONTEXT-INDEX.json'],
'tests/context-predicates-query.test.cjs': ['docs/contributor-standards.md'],
// SCAN_DIRS includes 'docs' and recursively walks every .md file under it
// (context7-tool-name-parity.test.cjs:31-38) — a generic tree walk, not a
// fixed file set.
'tests/context7-tool-name-parity.test.cjs': ['*'],
'tests/contributor-standards.test.cjs': ['docs/contributor-standards.md'],
'tests/cursor-reviewer.test.cjs': [
'docs/COMMANDS.md',
'docs/FEATURES.md',
'docs/ja-JP/COMMANDS.md',
'docs/ja-JP/FEATURES.md',
'docs/ko-KR/COMMANDS.md',
'docs/ko-KR/FEATURES.md',
],
'tests/discuss-all-flag.test.cjs': ['docs/COMMANDS.md'],
'tests/discuss-mode.test.cjs': ['docs/workflow-discuss-mode.md'],
// Walks docs/*.md and every docs/<locale>/*.md dir dynamically
// (docs-parity-live-registry.test.cjs:42, 428) — deliberately generic.
'tests/docs-parity-live-registry.test.cjs': ['*'],
'tests/docs-state-md-locale-parity.test.cjs': [
'docs/reference/state-md.md',
'docs/ja-JP/reference/state-md.md',
'docs/zh-CN/reference/state-md.md',
'docs/ko-KR/reference/state-md.md',
'docs/pt-BR/reference/state-md.md',
],
'tests/drift-detection.test.cjs': ['docs/CONFIGURATION.md', 'docs/AGENTS.md'],
'tests/edge-probe-docs-fixtures.test.cjs': ['docs/adr/550-spec-phase-probe-contract.md'],
'tests/edit-phase.test.cjs': [
'docs/INVENTORY.md',
'docs/INVENTORY-MANIFEST.json',
'docs/COMMANDS.md',
],
'tests/effort-surface-axis.test.cjs': ['docs/reference/host-integration-capability-matrix.md'],
'tests/execute-phase-active-flags.test.cjs': [
'docs/reference/host-integration-capability-matrix.md',
],
'tests/execute-phase-wave.test.cjs': ['docs/COMMANDS.md'],
'tests/external-job-waiting.test.cjs': ['docs/reference/planning-artifacts.md'],
// Rows 8/9 (negative controls) read real docs/registries/eos.json and
// docs/adr/0001-dispatch-policy-module.md and assert on their EXACT
// committed content (an entry's name field, ADR-0001's H1 title) as a
// sanity check before mutating an overlay fixture — a content edit to
// either file changes the string this test asserts on.
'tests/fragment-single-edit-propagation.install.test.cjs': [
'docs/registries/eos.json',
'docs/adr/0001-dispatch-policy-module.md',
],
// Seeds a temp fixture copy of these five files (never mutates the real
// tree) to exercise scripts/gen-state-md-docs.cjs's marked-region splicing
// against them — #3873 (ADR-3473 §8.8), rows 10-22/27. Read for fixture
// seeding, so a content edit to any of them (e.g. renaming a landmark
// heading/string a hostile-input test targets) can change this test's
// fixture assumptions.
'tests/gen-state-md-docs.test.cjs': [
'docs/reference/state-md.md',
'docs/ja-JP/reference/state-md.md',
'docs/zh-CN/reference/state-md.md',
'docs/ko-KR/reference/state-md.md',
'docs/pt-BR/reference/state-md.md',
],
'tests/gsd-write-guard.test.cjs': ['docs/USER-GUIDE.md'],
'tests/host-integration-descriptors.test.cjs': [
'docs/reference/host-integration-capability-matrix.md',
],
'tests/install.test.cjs': ['docs/AGENTS.md', 'docs/INVENTORY.md', 'docs/INVENTORY-MANIFEST.json'],
// SOURCE_DIRS includes 'docs' and walks it recursively for every .md file
// (intel.test.cjs:1223-1228) — a generic tree walk.
'tests/intel.test.cjs': ['*'],
'tests/inventory-headings-countfree.test.cjs': ['docs/INVENTORY.md'],
'tests/inventory-manifest-sync.test.cjs': ['docs/INVENTORY.md', 'docs/INVENTORY-MANIFEST.json'],
'tests/kilo-upgrades.test.cjs': ['docs/how-to/connect-gsd-mcp-server.md'],
'tests/live-config-guard.test.cjs': ['docs/TESTING-SUITES.md'],
'tests/model-catalog-runtime-defaults.test.cjs': ['docs/CONFIGURATION.md'],
// Scans every git-tracked file in the whole repo via `git ls-files`
// (no-pending-3212-markers.test.cjs:37-46), which includes all of docs/ —
// cannot be resolved to a fixed docs/ path set.
'tests/no-pending-3212-markers.test.cjs': ['*'],
'tests/phase6-capability-docs.test.cjs': ['docs/how-to/develop-a-capability.md', 'docs/README.md'],
'tests/phase6-review-capabilities.test.cjs': [
'docs/reference/review-verification-capabilities.md',
'docs/how-to/develop-a-capability.md',
'docs/README.md',
],
'tests/plan-checker-coupling.test.cjs': ['docs/AGENTS.md'],
'tests/plan-phase-drift-guard.test.cjs': [
'docs/CONFIGURATION.md',
'docs/COMMANDS.md',
'docs/USER-GUIDE.md',
'docs/ARCHITECTURE.md',
'docs/INVENTORY.md',
'docs/INVENTORY-MANIFEST.json',
],
'tests/plan-phase-stall-detection.test.cjs': ['docs/CONFIGURATION.md'],
'tests/plan-review-convergence.test.cjs': [
'docs/CONFIGURATION.md',
'docs/USER-GUIDE.md',
'docs/ARCHITECTURE.md',
],
'tests/planner-estimate-emission.test.cjs': ['docs/reference/plan-md.md', 'docs/CONFIGURATION.md'],
'tests/precondition-element.test.cjs': ['docs/reference/plan-md.md'],
'tests/product-name-purity.test.cjs': [
'docs/README.md',
'docs/zh-CN/README.md',
'docs/ko-KR/README.md',
'docs/ja-JP/README.md',
'docs/pt-BR/README.md',
],
'tests/progress-forensic.test.cjs': ['docs/COMMANDS.md'],
'tests/repo-layout.test.cjs': ['docs/contributing/bootstrap.md'],
'tests/reversibility-tagging.test.cjs': ['docs/reference/plan-md.md'],
'tests/reviewer-docs-parity.test.cjs': [
'docs/COMMANDS.md',
'docs/ja-JP/COMMANDS.md',
'docs/ko-KR/COMMANDS.md',
'docs/pt-BR/COMMANDS.md',
'docs/zh-CN/COMMANDS.md',
'docs/FEATURES.md',
'docs/ja-JP/FEATURES.md',
'docs/ko-KR/FEATURES.md',
'docs/pt-BR/FEATURES.md',
'docs/zh-CN/FEATURES.md',
],
'tests/runtime-converters.test.cjs': ['docs/CONFIGURATION.md'],
'tests/secure-phase.test.cjs': ['docs/CONFIGURATION.md'],
'tests/security-dead-exports.regression.test.cjs': ['docs/FEATURES.md'],
'tests/security.test.cjs': ['docs/INVENTORY-MANIFEST.json'],
// Walks docs/ recursively (todos-done-rename-guard.test.cjs:19-22
// SCAN_DIRS) looking for stale references — a generic tree walk.
'tests/todos-done-rename-guard.test.cjs': ['*'],
'tests/tracer-bullet.test.cjs': [
'docs/COMMANDS.md',
'docs/reference/plan-md.md',
'docs/how-to/plan-a-phase.md',
'docs/AGENTS.md',
],
'tests/ui-spec-inventory-provenance.test.cjs': [
'docs/FEATURES.md',
'docs/how-to/design-a-ui-phase.md',
'docs/ja-JP/FEATURES.md',
'docs/ja-JP/how-to/design-a-ui-phase.md',
'docs/zh-CN/FEATURES.md',
'docs/zh-CN/how-to/design-a-ui-phase.md',
'docs/ko-KR/FEATURES.md',
'docs/ko-KR/how-to/design-a-ui-phase.md',
'docs/pt-BR/how-to/design-a-ui-phase.md',
],
'tests/verifier-behavior-unverified.test.cjs': ['docs/reference/planning-artifacts.md'],
'tests/verifier-coincidental-reliance.test.cjs': ['docs/AGENTS.md'],
'tests/verify.test.cjs': ['docs/reference/plan-md.md'],
'tests/workflow-fragments.test.cjs': ['docs/reference/workflow-fragments.md'],
};
/**
* Flat file-list view of DOCS_GUARD_TESTS, kept for consumers (the
* registration lint and its parity test) that only need "which test files
* are registered", not their per-file docs path patterns.
*/
const DOCS_GUARD_TEST_FILES = Object.keys(DOCS_GUARD_TESTS);
assertNoSuiteCollision(DOCS_GUARD_TEST_FILES);
module.exports = {
DOCS_GUARD_TESTS,
DOCS_GUARD_TEST_FILES,
RUN_TESTS_SUITES,
assertNoSuiteCollision,
normalizeForSuiteCheck,
};