enhance(#4139): Phase 6 — the lazily-read remainder and the artifact templates (#4540)

* enhance(#4406): the lazily-read remainder and the artifact templates

ADR-4139 Decision 3, Phase 6 of the #4139 Compact Content epic. Covers stream 1b
(gsd-core/workflows/<name>/{modes,steps,templates}/*.md) and stream 4
(gsd-core/templates/**) with a variant-swap mechanism, confirmed with the user:
two independent, complete files per covered path (canonical + .compact.md
sibling), with the gate picking which one gets Read at the call site. This is
a different shape from Phase 5's spine+detail partition, and is safe here
specifically because these files are already reached only by a runtime Read —
a missed Read already means zero overlay content today, with or without
workflow.compact_content, so selecting between two independently-complete
files introduces no new failure mode (documented in
gsd-core/references/compact-content-gate.md's new "Streams 1b and 4" section).

Disposition, after inspecting every candidate rather than trusting a byte-size
threshold (same rigor Phase 5 applied to review.md):

- Stream 1b: 1 of 78 files compacted (help/modes/full.md, a user-facing
  reference doc emitted verbatim, not orchestrator instruction). The other 9
  size-threshold candidates are dominated by fail-closed guards, exact CLI
  invocations, or output-format contracts (AskUserQuestion blocks) — recorded
  not-worth-compacting, same reasoning as Phase 5's review.md.
- Stream 4: a ground-truth reachability audit replaced the initial size-only
  candidate list. Two files (summary.md, user-setup.md) got compact variants;
  a third (spec.md) was drafted, then dropped after discovering its only two
  call sites are eager @-includes, not a runtime Read — stream-1 material
  hiding under gsd-core/templates/, not stream-4's actual mechanism. summary.md
  itself has 3 eager call sites and only 1 genuine runtime-Read call site
  (execute-plan.md); only that one was wired, so the compact variant's savings
  apply to the sequential single-plan execution path only.
- Discovered while auditing reachability: 12 gsd-core/templates/** files with
  zero references anywhere in workflow/agent/command prose, compiled source,
  or tests — dead scaffolding predating this phase. Deleted in this same PR
  per this repo's no-defer policy, after re-verifying against a computed
  path.join(...) pattern (not just a plain-string search) that nearly caused
  two genuinely load-bearing templates (user-profile.md, dev-preferences.md)
  to be misclassified as dead.

New checker (tests/helpers/compact-content-variant.cjs): registration,
reachability, protected-content-preserved, size-smaller — replacing Phase
3/5's disjointness/completeness checks, which assume a partition rather than
two deliberately-overlapping documents. The reachability check's own
"unprefixed match" guard had a real bug (rejected the repo's own
`~/.claude/gsd-core/...` convention), caught by running it against the
already-wired help/modes/full.compact.md pair rather than only synthetic
fixtures — fixed to anchor on the nearest `gsd-core` path segment instead.

Template consumer parity (tests/compact-content-template-variant-parity.test.cjs):
proves each compact variant's `## File Template` fenced block — the actual
output-format contract a generated SUMMARY.md/USER-SETUP.md is parsed
against — is byte-identical to the canonical file, then runs the one real
deterministic consumer (gsd-core/bin/lib/coverage.cjs's classifyContent,
backing `gsd-tools uat classify-coverage`) against content built from that
shared contract.

Added a sibling benchmark script (scripts/benchmark-compact-content-variants.cjs)
rather than extending the existing spine/detail one — different data shape,
and the existing script's own contract deliberately isolates it from a
test-only helper's shape changing.

Emitted-drift acknowledgement: not needed. Every changed/added path in this
diff is hand-authored and present in the diff itself, so diffEmitted's
attribution loop resolves `via` to the path's own source before reaching the
ack-lookup branch (same reasoning Phase 5 verified for its own diff).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* enhance(#4406): address code-review findings on the variant-swap gate

- docs/CONFIGURATION.md and gsd-core/references/planning-config.md's
  workflow.compact_content entries described only the spine+detail mechanism
  (Phase 5) and were missing this phase's variant-swap mechanism and its
  benchmark:compact-content-variants script entirely — required since this
  PR's changeset is type Added (CLAUDE.md's "Missing Docs for Changesets"
  rule). Both now describe both mechanisms and which call sites are wired.
- Added the missing RED^-1/no-op fixture for checkProtectedContentPreserved:
  a canonical file with zero <!-- gsd:protected --> blocks must be a
  no-op, not a violation — the only branch of that function the existing
  fixtures didn't exercise.
- Collapsed findCompactFiles/findMarkdownFiles in
  tests/helpers/compact-content-variant.cjs into one findFilesWithSuffix
  helper — the two were identical recursive walks differing only in the
  extension predicate (minor Duplicated-Code finding).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#4406): restore copilot-instructions.md, a false-positive dead-template classification

gsd-test caught this, not static analysis: 10 real failures in
tests/copilot-install.test.cjs, tests/installer-migration-install.integration.test.cjs,
and tests/repo-layout.test.cjs — all downstream of bin/install.js's Copilot install
path, which does
fs.readFileSync(path.join(targetDir, 'gsd-core', 'templates', 'copilot-instructions.md'))
after copying gsd-core/templates/** into the target project, then merges it into both
.github/copilot-instructions.md and (local installs) AGENTS.md. The reachability audit
that flagged this file as dead checked src/*.cts and gsd-core/bin/*.cjs but never the
repo-root bin/install.js — a separately maintained installer bundle outside the
src/-to-gsd-core/bin/lib/ compiled-output convention. The fs.existsSync guard around
that read degrades to a silent skip rather than a crash when the template is missing,
which is why this surfaced only once the real E2E install test ran, not from any
static check.

Re-verified the remaining 11 deleted filenames against bin/install.js specifically
(plain substring and quoted-filename search) before trusting that list — all 11 have
zero hits there, confirmed dead by the same standard this one file failed.

Regenerated the installer emitted-tree goldens (tests/fixtures/install-tree/*.json) to
reflect the restored file, and corrected the "Removed" changeset (jolly-lynx-sprint.md)
and the phase design doc from 12 to 11 deleted files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Emitted-Drift-Ack-Growth: execute-plan.md — call-site wiring for the summary.md and user-setup.md .compact.md variants
Emitted-Drift-Ack-Growth: help.md — call-site wiring for full.compact.md, same variant-resolution rule

* docs(#4406): backfill changeset PR numbers

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#4406): resolve removed-but-needed lint findings on the dead-template deletion

CI's own full-test matrix (not gsd-test's matrix, which does not run this
check) caught 4 more false-positive dead-template classifications via
tests/removed-but-needed-lint.test.cjs / scripts/lint-removed-but-needed.cjs
— a literal, word-boundary basename check across .github/workflows/,
gsd-core/, and docs/ (excluding docs/adr/** and docs/research/**) for every
file a PR deletes. It has no semantic awareness, so a deleted template's
basename colliding with something else entirely still fires:

- claude-md.md: gsd-core/templates/README.md had a stale table row claiming
  /gsd-profile reads this template to generate CLAUDE.md. Verified false (no
  code reads it anywhere, same search that already covered bin/install.js) —
  fixed the row to *(inline)*, matching every other command-generated
  artifact in that table. File stays deleted.
- codebase/testing.md: collided with docs/guides/testing.md, an illustrative
  example row in docs-update.md's sample output table (an unrelated real
  generated-docs path). Swapped the example topic to "contributing" — the
  row is illustrative, any topic works. File stays deleted.
- codebase/architecture.md, codebase/stack.md: collided with docs/reference/
  planning-artifacts.md's directory listing of a user's own generated
  .planning/codebase/architecture.md and stack.md output — the same
  semantic mismatch already investigated and dismissed as unrelated earlier
  in this phase's audit, now caught by a gate instead of judgment. That
  listing repeats across 5 locale copies of the doc.
- continue-here.md: collided with the real .continue-here.md pause-work
  artifact, referenced across 15+ locale and workflow files.

For the last two, the lint's own error message offers "restore the file or
update every consumer in the same commit." Rewording 15+ files across
languages I cannot verify translation quality for, to shave 2 already-tiny
templates that were merely presumed dead, is disproportionate to this PR's
actual scope — restored codebase/architecture.md, codebase/stack.md, and
continue-here.md instead, and corrected docs/ARCHITECTURE.md's Templates
section accordingly.

Final confirmed-dead set: claude-md.md, codebase/concerns.md,
codebase/conventions.md, codebase/integrations.md, codebase/structure.md,
codebase/testing.md, debug-subagent-prompt.md, discovery.md — 8 files, down
from the original 12. Verified locally: GSD_REMOVED_BUT_NEEDED_BASE=next
node scripts/lint-removed-but-needed.cjs now passes clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Emitted-Drift-Ack-Growth: docs-update.md — swapped an illustrative example-table topic (testing -> contributing) to avoid a removed-but-needed basename collision with the deleted codebase/testing.md template; net +10 bytes

* fix(#4406): split codex-config.test.cjs to fix a genuine Windows CI timeout

Root cause of the `full test (windows-latest, 24, shard 2/3)` failure the
user asked to be actually fixed, not just re-run past: PR #4497 (landed
2026-09-07, one day before this PR's CI run) isolated
tests/codex-config.test.cjs into its own dedicated chunk because its
measured weight (17.87, ~45% of the post-cut Windows budget) made it unsafe
to share a chunk with any other file. That isolation was necessary but not
sufficient — even alone, with zero companion-file contention, the file's
real Windows execution time sits right at the 600s per-chunk ceiling. Two
independent CI runs on two unrelated PRs (this one and #4154) were both
killed within ~1.4s of the identical 600000ms mark — not random contention,
a deterministic near-miss the isolation fix couldn't address because it
never reduced the file's own cost, only removed the risk of a companion
file's cost stacking on top of it (which the PR #4497 comment explicitly
anticipated: "if a future profiling pass genuinely speeds up
codex-config.test.cjs itself, this isolation can be revisited").

The file itself explains why it's this heavy: 11,262 lines / 433 tests / 79
describe blocks, accumulated over dozens of bug-fix PRs (#2695, #2760,
#3245, #3285, #3346, #3426, #3427, #3562, #3566, #3582, #3808, and more),
several of which are explicitly documented as "folded" in from separate
files that were never actually split back out ("Verified non-duplicate
against both the pre-existing target and the other three folded sources").

Split into 4 files by top-level AST statement boundaries (never a naive
column-0 regex — an early attempt at that overcounted 79 apparent
"describe(" matches when only 21 are genuinely top-level; the rest are
nested inside a handful of large folded-in blocks, which a regex can't tell
apart from real top-level statements). Verified lossless twice: the split
script asserts byte-for-byte reconstruction of every source character, and
independently, total test()/describe() call counts match exactly between
the original file and the sum across all 4 new files (433/79 both sides).
Each new file carries the complete original shared header (imports/helpers)
for safety; per-file unused-import warnings from that duplication are
resolved via ESLint-precise alias renames (`{ foo: _foo }`, the standard
form for an intentionally-unused destructured binding — never a bare `{
_foo }`, which would destructure a different, nonexistent property).

No change needed to scripts/run-tests.cjs's ISOLATED_HEAVY_FILES or its
pinned test in tests/run-tests-harness.test.cjs: the file that keeps the
original name (tests/codex-config.test.cjs) is now only ~28% of the
original's size and safely isolated in its own chunk as before; the other
three new files re-enter normal weight-balanced packing, none individually
close to disproportionate. Confirmed no other file hardcodes the hardcoded
filename anywhere that would silently stop these tests from running (the
CI test-selection scripts determine scope algorithmically, not by literal
filename).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-09-08 14:31:04 -04:00
committed by GitHub
parent 66dbb104a0
commit a27cb6b2fa
52 changed files with 10865 additions and 10511 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,142 @@
'use strict';
/**
* tests/compact-content-template-variant-parity.test.cjs — ADR-4139, epic #4139, Phase 6 (#4406).
*
* Check 5 of `.gsd/phase/enhance-4406-lazy-remainder/40-design.md`'s variant-pair checklist:
* template consumer parity. Only two `gsd-core/templates/**` files got a `.compact.md` variant
* wired to a genuine runtime `Read` this phase — `summary.md` and `user-setup.md` (see
* `40-design.md`'s "a size-only candidate list was also the wrong test here" for why `spec.md`
* was dropped and why `summary.md`'s wiring is scoped to one call site only).
*
* The actual guarantee that makes compacting these templates safe: downstream consumers parse
* the GENERATED artifact (a real SUMMARY.md / USER-SETUP.md an agent wrote), never the template
* file itself. So the only way a compact variant could silently break a real consumer is if it
* changed the artifact's OUTPUT-FORMAT CONTRACT — the `## File Template` fenced block — relative
* to the canonical file. Both compact variants were authored to leave that block byte-identical
* and compact only the surrounding illustrative material (examples, guidelines prose). This test
* proves that invariant directly, rather than assuming it from the authoring process, and then
* proves the shared contract really is what the one real deterministic consumer
* (`gsd-core/bin/lib/coverage.cjs`'s `classifyContent`, backing `gsd-tools uat classify-coverage`)
* accepts and classifies correctly.
*
* `user-setup.md` has no deterministic content parser anywhere in this repo (confirmed by
* repo-wide search — the only "consumption" beyond an agent `Read` is a human `grep -r
* "USER-SETUP" .planning/` search over filenames, insensitive to internal structure). Its parity
* check is therefore limited to the File Template identity assertion; there is no real parser
* round trip to run against it.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const { classifyContent } = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'coverage.cjs'));
/**
* Extract the fenced code block immediately following a `## File Template` heading.
* Returns the block's inner content (without the opening/closing fences), or null if
* the heading or a following fence isn't found.
* @param {string} content
* @returns {string | null}
*/
function extractFileTemplateBlock(content) {
const headingIdx = content.indexOf('## File Template');
if (headingIdx === -1) return null;
const fenceStart = content.indexOf('```', headingIdx);
if (fenceStart === -1) return null;
const afterOpenFence = content.indexOf('\n', fenceStart) + 1;
const fenceEnd = content.indexOf('\n```', afterOpenFence);
if (fenceEnd === -1) return null;
return content.slice(afterOpenFence, fenceEnd);
}
describe('template consumer parity — File Template contract identity', () => {
test('summary.md and summary.compact.md share a byte-identical File Template block', () => {
const canonical = fs.readFileSync(path.join(ROOT, 'gsd-core', 'templates', 'summary.md'), 'utf8');
const compact = fs.readFileSync(path.join(ROOT, 'gsd-core', 'templates', 'summary.compact.md'), 'utf8');
const canonicalBlock = extractFileTemplateBlock(canonical);
const compactBlock = extractFileTemplateBlock(compact);
assert.ok(canonicalBlock, 'canonical summary.md must have an extractable File Template block');
assert.ok(compactBlock, 'summary.compact.md must have an extractable File Template block');
assert.strictEqual(
compactBlock,
canonicalBlock,
'summary.compact.md must not alter the output-format contract any downstream consumer parses',
);
});
test('user-setup.md and user-setup.compact.md share a byte-identical File Template block', () => {
const canonical = fs.readFileSync(path.join(ROOT, 'gsd-core', 'templates', 'user-setup.md'), 'utf8');
const compact = fs.readFileSync(path.join(ROOT, 'gsd-core', 'templates', 'user-setup.compact.md'), 'utf8');
const canonicalBlock = extractFileTemplateBlock(canonical);
const compactBlock = extractFileTemplateBlock(compact);
assert.ok(canonicalBlock, 'canonical user-setup.md must have an extractable File Template block');
assert.ok(compactBlock, 'user-setup.compact.md must have an extractable File Template block');
assert.strictEqual(
compactBlock,
canonicalBlock,
'user-setup.compact.md must not alter the output-format contract',
);
});
});
describe('template consumer parity — real classify-coverage round trip (summary.md)', () => {
const REALISTIC_SUMMARY = `---
phase: 07-example
plan: 01
subsystem: testing
tags: [example]
requires: []
provides: []
affects: []
coverage:
- id: D1
description: "Deterministic deliverable"
verification:
- kind: unit
ref: "tests/example.test.cjs#does the thing"
status: pass
human_judgment: false
- id: D2
description: "Deliverable needing a human"
verification: []
human_judgment: true
rationale: "UI screenshot review"
duration: 5min
completed: 2026-01-01
status: complete
---
# Phase 7: Example Summary
**A deterministic deliverable and a human-judgment deliverable.**
`;
test('a SUMMARY.md built from the shared File Template coverage schema classifies correctly', () => {
const result = classifyContent(REALISTIC_SUMMARY, '07-example-01-SUMMARY.md');
assert.strictEqual(result.mode, 'coverage');
assert.strictEqual(result.errors.length, 0, `expected no validation errors: ${JSON.stringify(result.errors)}`);
assert.strictEqual(result.auto_passed.length, 1, 'D1 (human_judgment:false, all verification pass) must auto-pass');
assert.strictEqual(result.present.length, 1, 'D2 (human_judgment:true) must route to a human');
assert.strictEqual(result.all_auto_covered, false);
});
test('boundary: a legacy SUMMARY.md with no coverage block still classifies (byte-identical fallback)', () => {
const legacy = REALISTIC_SUMMARY.replace(/coverage:[\s\S]*?rationale: "UI screenshot review"\n/, '');
const result = classifyContent(legacy, '07-example-01-SUMMARY.md');
assert.strictEqual(result.mode, 'legacy');
});
test('negative: a coverage block that fails validation is presented to a human, never silently auto-passed', () => {
const malformed = REALISTIC_SUMMARY.replace('human_judgment: false', '');
const result = classifyContent(malformed, '07-example-01-SUMMARY.md');
assert.strictEqual(result.mode, 'coverage');
assert.ok(
result.present.some((p) => p.id === 'D1'),
'D1 missing human_judgment must route to present, never auto-pass',
);
});
});

View File

@@ -0,0 +1,293 @@
'use strict';
/**
* tests/compact-content-variant-guard.test.cjs — ADR-4139, epic #4139, Phase 6 (#4406).
*
* Implements the four mechanical checks `.gsd/phase/enhance-4406-lazy-remainder/40-design.md`
* describes for the variant-swap shape (two independent, complete files; the gate picks which
* one gets `Read`) covering `gsd-core/workflows/<name>/{modes,steps,templates}/*.compact.md`
* and `gsd-core/templates/**\/*.compact.md`. This is a DIFFERENT shape from
* `tests/compact-content-partition-guard.test.cjs` (stream 1's spine+detail partition) — see
* `tests/helpers/compact-content-variant.cjs` for why disjointness/completeness do not apply
* here. Template consumer parity (the fifth check) lives in its own file,
* `tests/compact-content-template-variant-parity.test.cjs`, because it needs a real
* artifact-generation + real-parser round trip per template rather than a generic file-shape
* check.
*
* Each check gets a RED (deliberately broken) and GREEN (fixed) fixture pair, built against
* synthetic temp files — never against this repo's own real variants — per this repo's rule
* that a guard nobody has seen go red is not yet a guard.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { cleanup } = require('./helpers.cjs');
const {
discoverRegisteredVariants,
checkRegistration,
checkReachability,
checkProtectedContentPreserved,
checkSizeSmaller,
} = require('./helpers/compact-content-variant.cjs');
describe('compact-content variant guard — real repo state (ADR-4139, Phase 6 #4406)', () => {
test('check 1 (registration): every .compact.md file has a canonical sibling', () => {
const pairs = discoverRegisteredVariants();
const violations = checkRegistration(pairs);
assert.deepStrictEqual(violations, [], `registration violations: ${JSON.stringify(violations, null, 2)}`);
});
test('check 2 (reachability): every registered compact variant is named by at least one spine', () => {
const pairs = discoverRegisteredVariants();
const violations = checkReachability(pairs);
assert.deepStrictEqual(violations, [], `reachability violations: ${JSON.stringify(violations, null, 2)}`);
});
test('check 3 (protected content preserved): every protected block in a canonical file survives in its compact sibling', () => {
const pairs = discoverRegisteredVariants();
const violations = checkProtectedContentPreserved(pairs);
assert.deepStrictEqual(violations, [], `protected-content violations: ${JSON.stringify(violations, null, 2)}`);
});
test('check 4 (size smaller): every compact file is strictly smaller than its canonical sibling', () => {
const pairs = discoverRegisteredVariants();
const violations = checkSizeSmaller(pairs);
assert.deepStrictEqual(violations, [], `size violations: ${JSON.stringify(violations, null, 2)}`);
});
test('non-vacuity: this repo actually has registered variant pairs to check', () => {
const pairs = discoverRegisteredVariants();
assert.ok(pairs.length > 0, 'expected at least one registered .compact.md pair — an empty result proves nothing');
});
});
describe('failing-first fixture: check 1 (registration)', () => {
test('RED — a .compact.md file exists with no canonical sibling (orphan)', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-variant-orphan-'));
try {
const workflowsDir = path.join(tmpRoot, 'gsd-core', 'workflows', 'foo', 'steps');
fs.mkdirSync(workflowsDir, { recursive: true });
fs.writeFileSync(path.join(workflowsDir, 'bar.compact.md'), 'Compact content with no canonical pair.\n');
const pairs = discoverRegisteredVariants([path.join(tmpRoot, 'gsd-core', 'workflows')]);
const violations = checkRegistration(pairs);
assert.ok(violations.length > 0, 'expected at least one registration violation');
assert.ok(
violations.some((v) => v.kind === 'orphan_compact_file' && v.compactPath.endsWith('bar.compact.md')),
`expected the orphan file to be named: ${JSON.stringify(violations)}`,
);
} finally {
cleanup(tmpRoot);
}
});
test('GREEN — the canonical sibling is added', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-variant-orphan-ok-'));
try {
const workflowsDir = path.join(tmpRoot, 'gsd-core', 'workflows', 'foo', 'steps');
fs.mkdirSync(workflowsDir, { recursive: true });
fs.writeFileSync(path.join(workflowsDir, 'bar.compact.md'), 'Terser.\n');
fs.writeFileSync(path.join(workflowsDir, 'bar.md'), 'Canonical, longer content.\n');
const pairs = discoverRegisteredVariants([path.join(tmpRoot, 'gsd-core', 'workflows')]);
const violations = checkRegistration(pairs);
assert.deepStrictEqual(violations, []);
} finally {
cleanup(tmpRoot);
}
});
});
describe('failing-first fixture: check 2 (reachability)', () => {
test('RED — a registered pair whose compact path is never named by any spine', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-variant-unreached-'));
try {
const workflowsRoot = path.join(tmpRoot, 'gsd-core', 'workflows');
const stepsDir = path.join(workflowsRoot, 'foo', 'steps');
fs.mkdirSync(stepsDir, { recursive: true });
fs.writeFileSync(path.join(stepsDir, 'bar.md'), 'Canonical, longer content.\n');
fs.writeFileSync(path.join(stepsDir, 'bar.compact.md'), 'Terser.\n');
fs.writeFileSync(path.join(workflowsRoot, 'foo.md'), 'Spine with entirely unrelated content and no path reference of any kind.\n');
const pairs = discoverRegisteredVariants([workflowsRoot]);
const violations = checkReachability(pairs, [workflowsRoot]);
assert.ok(violations.length > 0, 'expected at least one reachability violation');
assert.ok(
violations.some((v) => v.kind === 'unreachable_compact_file' && v.compactPath.endsWith('bar.compact.md')),
`expected the unreached file to be named: ${JSON.stringify(violations)}`,
);
} finally {
cleanup(tmpRoot);
}
});
test('GREEN — the spine names the compact path via the variant-resolution rule', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-variant-unreached-ok-'));
try {
const workflowsRoot = path.join(tmpRoot, 'gsd-core', 'workflows');
const stepsDir = path.join(workflowsRoot, 'foo', 'steps');
fs.mkdirSync(stepsDir, { recursive: true });
fs.writeFileSync(path.join(stepsDir, 'bar.md'), 'Canonical, longer content.\n');
fs.writeFileSync(path.join(stepsDir, 'bar.compact.md'), 'Terser.\n');
fs.writeFileSync(
path.join(workflowsRoot, 'foo.md'),
'Read and execute `gsd-core/workflows/foo/steps/bar.md` (or its `gsd-core/workflows/foo/steps/bar.compact.md` variant per the shared gate).\n',
);
const pairs = discoverRegisteredVariants([workflowsRoot]);
const violations = checkReachability(pairs, [workflowsRoot]);
assert.deepStrictEqual(violations, []);
} finally {
cleanup(tmpRoot);
}
});
test('a vendored path ending in the same filename does not grant false reachability', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-variant-unreached-prefix-'));
try {
const workflowsRoot = path.join(tmpRoot, 'gsd-core', 'workflows');
const stepsDir = path.join(workflowsRoot, 'foo', 'steps');
fs.mkdirSync(stepsDir, { recursive: true });
fs.writeFileSync(path.join(stepsDir, 'bar.md'), 'Canonical, longer content.\n');
fs.writeFileSync(path.join(stepsDir, 'bar.compact.md'), 'Terser.\n');
fs.writeFileSync(
path.join(workflowsRoot, 'foo.md'),
'This mentions vendor/foo/steps/bar.compact.md, a different tree entirely.\n',
);
const pairs = discoverRegisteredVariants([workflowsRoot]);
const violations = checkReachability(pairs, [workflowsRoot]);
assert.ok(
violations.some((v) => v.kind === 'unreachable_compact_file'),
'a prefixed match must not count as reachability',
);
} finally {
cleanup(tmpRoot);
}
});
});
describe('failing-first fixture: check 3 (protected content preserved)', () => {
test('RED — a protected block in the canonical file is absent from the compact sibling', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-variant-protected-'));
try {
const workflowsRoot = path.join(tmpRoot, 'gsd-core', 'workflows', 'foo', 'steps');
fs.mkdirSync(workflowsRoot, { recursive: true });
fs.writeFileSync(
path.join(workflowsRoot, 'bar.md'),
'Intro.\n\n<!-- gsd:protected -->\nNever weaken this exact guardrail sentence.\n\nMore filler text that is safe to shorten elsewhere in this file to pad it out longer than the compact sibling.\n',
);
fs.writeFileSync(path.join(workflowsRoot, 'bar.compact.md'), 'Terser intro with the guardrail dropped.\n');
const pairs = discoverRegisteredVariants([path.join(tmpRoot, 'gsd-core', 'workflows')]);
const violations = checkProtectedContentPreserved(pairs);
assert.ok(violations.length > 0, 'expected at least one protected-content violation');
assert.ok(
violations.some((v) => v.missing.includes('Never weaken this exact guardrail sentence.')),
`expected the dropped sentence to be named: ${JSON.stringify(violations)}`,
);
} finally {
cleanup(tmpRoot);
}
});
test('GREEN — the guardrail sentence is preserved verbatim in the compact sibling', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-variant-protected-ok-'));
try {
const workflowsRoot = path.join(tmpRoot, 'gsd-core', 'workflows', 'foo', 'steps');
fs.mkdirSync(workflowsRoot, { recursive: true });
fs.writeFileSync(
path.join(workflowsRoot, 'bar.md'),
'Intro.\n\n<!-- gsd:protected -->\nNever weaken this exact guardrail sentence.\n\nMore filler text that is safe to shorten elsewhere in this file to pad it out longer than the compact sibling.\n',
);
fs.writeFileSync(
path.join(workflowsRoot, 'bar.compact.md'),
'Terser intro.\n\nNever weaken this exact guardrail sentence.\n',
);
const pairs = discoverRegisteredVariants([path.join(tmpRoot, 'gsd-core', 'workflows')]);
const violations = checkProtectedContentPreserved(pairs);
assert.deepStrictEqual(violations, []);
} finally {
cleanup(tmpRoot);
}
});
test('a canonical file with no protected blocks is a correct no-op (nothing to preserve)', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-variant-protected-none-'));
try {
const workflowsRoot = path.join(tmpRoot, 'gsd-core', 'workflows', 'foo', 'steps');
fs.mkdirSync(workflowsRoot, { recursive: true });
fs.writeFileSync(path.join(workflowsRoot, 'bar.md'), 'Plain canonical content with no protected sentinel at all.\n');
fs.writeFileSync(path.join(workflowsRoot, 'bar.compact.md'), 'Terser.\n');
const pairs = discoverRegisteredVariants([path.join(tmpRoot, 'gsd-core', 'workflows')]);
const violations = checkProtectedContentPreserved(pairs);
assert.deepStrictEqual(
violations,
[],
'a canonical file with zero <!-- gsd:protected --> blocks has nothing to check — never a violation',
);
} finally {
cleanup(tmpRoot);
}
});
});
describe('failing-first fixture: check 4 (size smaller)', () => {
test('RED — a "compact" file the same size as its canonical sibling', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-variant-size-'));
try {
const workflowsRoot = path.join(tmpRoot, 'gsd-core', 'workflows', 'foo', 'steps');
fs.mkdirSync(workflowsRoot, { recursive: true });
fs.writeFileSync(path.join(workflowsRoot, 'bar.md'), 'x'.repeat(100));
fs.writeFileSync(path.join(workflowsRoot, 'bar.compact.md'), 'y'.repeat(100));
const pairs = discoverRegisteredVariants([path.join(tmpRoot, 'gsd-core', 'workflows')]);
const violations = checkSizeSmaller(pairs);
assert.ok(violations.length > 0, 'expected at least one size violation');
assert.ok(
violations.some((v) => v.kind === 'compact_not_smaller' && v.canonicalSize === 100 && v.compactSize === 100),
`expected the equal-size pair to be named: ${JSON.stringify(violations)}`,
);
} finally {
cleanup(tmpRoot);
}
});
test('RED — a "compact" file one byte LARGER than its canonical sibling (boundary point)', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-variant-size-over-'));
try {
const workflowsRoot = path.join(tmpRoot, 'gsd-core', 'workflows', 'foo', 'steps');
fs.mkdirSync(workflowsRoot, { recursive: true });
fs.writeFileSync(path.join(workflowsRoot, 'bar.md'), 'x'.repeat(100));
fs.writeFileSync(path.join(workflowsRoot, 'bar.compact.md'), 'y'.repeat(101));
const pairs = discoverRegisteredVariants([path.join(tmpRoot, 'gsd-core', 'workflows')]);
const violations = checkSizeSmaller(pairs);
assert.ok(violations.length > 0, 'expected at least one size violation');
} finally {
cleanup(tmpRoot);
}
});
test('GREEN — a compact file exactly one byte smaller (boundary point)', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-variant-size-ok-'));
try {
const workflowsRoot = path.join(tmpRoot, 'gsd-core', 'workflows', 'foo', 'steps');
fs.mkdirSync(workflowsRoot, { recursive: true });
fs.writeFileSync(path.join(workflowsRoot, 'bar.md'), 'x'.repeat(100));
fs.writeFileSync(path.join(workflowsRoot, 'bar.compact.md'), 'y'.repeat(99));
const pairs = discoverRegisteredVariants([path.join(tmpRoot, 'gsd-core', 'workflows')]);
const violations = checkSizeSmaller(pairs);
assert.deepStrictEqual(violations, []);
} finally {
cleanup(tmpRoot);
}
});
});

View File

@@ -13,8 +13,8 @@
"reductionPct": 36.67
},
"docs-update": {
"offTokens": 14231,
"onTokens": 11881,
"offTokens": 14232,
"onTokens": 11882,
"reductionPct": 16.51
},
"execute-phase": {
@@ -39,8 +39,8 @@
}
},
"aggregate": {
"offTokens": 106922,
"onTokens": 90274,
"offTokens": 106923,
"onTokens": 90275,
"reductionPct": 15.57
}
}

View File

@@ -0,0 +1,31 @@
{
"schema_version": 1,
"generated_by": "scripts/benchmark-compact-content-variants.cjs",
"tokenizer": {
"name": "gpt-tokenizer",
"version": "4.0.0"
},
"label": "PROXY-TOKENIZER DELTA — gpt-tokenizer is a stand-in; Anthropic publishes no tokenizer for Claude 3+. The on/off COMPARISON is exact under this pinned tokenizer; absolute counts are not Claude's real token counts.",
"pairs": {
"gsd-core/templates/summary.md": {
"offTokens": 2938,
"onTokens": 2299,
"reductionPct": 21.75
},
"gsd-core/templates/user-setup.md": {
"offTokens": 2061,
"onTokens": 1363,
"reductionPct": 33.87
},
"gsd-core/workflows/help/modes/full.md": {
"offTokens": 9950,
"onTokens": 6579,
"reductionPct": 33.88
}
},
"aggregate": {
"offTokens": 14949,
"onTokens": 10241,
"reductionPct": 31.49
}
}

View File

@@ -296,21 +296,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -331,8 +323,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -412,6 +406,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -368,21 +368,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -403,8 +395,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -484,6 +478,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -261,21 +261,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -296,8 +288,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -377,6 +371,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -296,21 +296,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -331,8 +323,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -412,6 +406,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -298,21 +298,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -333,8 +325,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -414,6 +408,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -368,21 +368,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -403,8 +395,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -484,6 +478,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -332,21 +332,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -367,8 +359,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -448,6 +442,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -297,21 +297,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -332,8 +324,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -413,6 +407,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -296,21 +296,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -331,8 +323,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -412,6 +406,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -296,21 +296,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -331,8 +323,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -412,6 +406,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -368,21 +368,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -403,8 +395,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -484,6 +478,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -297,21 +297,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -332,8 +324,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -413,6 +407,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -333,21 +333,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -368,8 +360,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -449,6 +443,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -368,21 +368,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -403,8 +395,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -484,6 +478,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -156,21 +156,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -191,8 +183,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -272,6 +266,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -296,21 +296,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -331,8 +323,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -412,6 +406,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -296,21 +296,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -331,8 +323,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -412,6 +406,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -224,21 +224,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -259,8 +251,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -340,6 +334,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -368,21 +368,13 @@
"gsd-core/templates/UAT.md",
"gsd-core/templates/UI-SPEC.md",
"gsd-core/templates/VALIDATION.md",
"gsd-core/templates/claude-md.md",
"gsd-core/templates/codebase/architecture.md",
"gsd-core/templates/codebase/concerns.md",
"gsd-core/templates/codebase/conventions.md",
"gsd-core/templates/codebase/integrations.md",
"gsd-core/templates/codebase/stack.md",
"gsd-core/templates/codebase/structure.md",
"gsd-core/templates/codebase/testing.md",
"gsd-core/templates/config.json",
"gsd-core/templates/context.md",
"gsd-core/templates/continue-here.md",
"gsd-core/templates/copilot-instructions.md",
"gsd-core/templates/debug-subagent-prompt.md",
"gsd-core/templates/dev-preferences.md",
"gsd-core/templates/discovery.md",
"gsd-core/templates/discussion-log.md",
"gsd-core/templates/milestone-archive.md",
"gsd-core/templates/milestone.md",
@@ -403,8 +395,10 @@
"gsd-core/templates/summary-complex.md",
"gsd-core/templates/summary-minimal.md",
"gsd-core/templates/summary-standard.md",
"gsd-core/templates/summary.compact.md",
"gsd-core/templates/summary.md",
"gsd-core/templates/user-profile.md",
"gsd-core/templates/user-setup.compact.md",
"gsd-core/templates/user-setup.md",
"gsd-core/templates/verification-report.md",
"gsd-core/workflows/_runtime-launcher.snippet.sh",
@@ -484,6 +478,7 @@
"gsd-core/workflows/help.md",
"gsd-core/workflows/help/modes/brief.md",
"gsd-core/workflows/help/modes/default.md",
"gsd-core/workflows/help/modes/full.compact.md",
"gsd-core/workflows/help/modes/full.md",
"gsd-core/workflows/help/modes/topic.md",
"gsd-core/workflows/import.md",

View File

@@ -0,0 +1,293 @@
'use strict';
/**
* Shared library for the compact-content VARIANT guard (ADR-4139, epic #4139,
* Phase 6 #4406). See `gsd-core/references/compact-content-gate.md` §"Streams
* 1b and 4 — variant resolution" for the operational rule this module checks;
* this file is the mechanics, not the source of truth for behavior.
*
* This is a DIFFERENT shape from `compact-content-split.cjs` (Phase 3, stream
* 1's spine+detail partition). A partition is one document split into two
* halves that must never overlap (disjointness) and whose union must equal
* the original (completeness). A variant pair is two INDEPENDENT, complete
* documents that are EXPECTED to overlap heavily — the compact file is a
* hand-terser rewrite of the same content, not an extracted remainder. So
* this module has no disjointness check and no completeness-at-split-time
* check; it has the five checks `40-design.md` (Phase 6) describes instead:
*
* 1. Registration — `discoverRegisteredVariants` (a `.compact.md`
* file with no canonical sibling is not a registered pair; the guard
* test reports it as an orphan).
* 2. Reachability — `checkReachability` (a registered pair whose
* compact path is never named by any spine's "Read ... variant
* resolution" call site is unwired dead weight).
* 3. Protected content preserved — `checkProtectedContentPreserved` (a
* `<!-- gsd:protected -->` block's lines must appear verbatim in BOTH
* files, since nothing is "moved" in a variant pair — it is duplicated).
* 4. Size smaller — `checkSizeSmaller`.
* 5. Template consumer parity — NOT implemented here; it needs a real
* artifact-generation + real-parser round trip per template, which is
* the domain of `tests/compact-content-template-variant-parity.test.cjs`
* directly, not a generic file-shape check.
*
* This module only reads (filesystem + a search of markdown source for
* literal path substrings). No writes, no network, no git.
*/
const fs = require('node:fs');
const path = require('node:path');
const { extractProtectedBlocks, normalizeNonTrivialLines } = require('./compact-content-split.cjs');
/** Default scan roots: everywhere a `.compact.md` sibling can legally live. */
const DEFAULT_VARIANT_ROOTS = [
path.join(__dirname, '..', '..', 'gsd-core', 'workflows'),
path.join(__dirname, '..', '..', 'gsd-core', 'templates'),
];
/** Every markdown-source root a spine/fragment might name a variant path from. */
const DEFAULT_SEARCH_ROOTS = [
path.join(__dirname, '..', '..', 'gsd-core', 'workflows'),
];
const COMPACT_SUFFIX = '.compact.md';
/**
* Recursively list every file under `dir` whose name ends with `suffix`.
* Shared by both file-discovery needs this module has — `.compact.md` files
* (`findCompactFiles`) and general `.md` files to search for reachability
* (`findMarkdownFiles`) — which otherwise duplicated the same walk with only
* the extension predicate differing.
* @param {string} dir
* @param {string} suffix
* @returns {string[]} absolute paths
*/
function findFilesWithSuffix(dir, suffix) {
const results = [];
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return results;
}
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...findFilesWithSuffix(full, suffix));
} else if (entry.isFile() && entry.name.endsWith(suffix)) {
results.push(full);
}
}
return results;
}
/**
* Recursively list every `*.compact.md` file under `dir`.
* @param {string} dir
* @returns {string[]} absolute paths
*/
function findCompactFiles(dir) {
return findFilesWithSuffix(dir, COMPACT_SUFFIX);
}
/**
* Discover every registered compact/canonical variant pair under `roots`.
*
* A pair is registered by a `<dir>/<stem>.compact.md` file existing on disk —
* there is no separate registry. Its canonical sibling is `<dir>/<stem>.md`
* in the SAME directory. A `.compact.md` file with no canonical sibling is
* still returned (with `canonicalExists: false`) so the registration check
* can report it as an orphan by name, rather than silently skipping it.
*
* @param {string[]} roots
* @returns {{compactPath: string, canonicalPath: string, canonicalExists: boolean}[]}
*/
function discoverRegisteredVariants(roots = DEFAULT_VARIANT_ROOTS) {
const pairs = [];
for (const root of roots) {
for (const compactPath of findCompactFiles(root)) {
const dir = path.dirname(compactPath);
const stem = path.basename(compactPath, COMPACT_SUFFIX);
const canonicalPath = path.join(dir, `${stem}.md`);
pairs.push({
compactPath,
canonicalPath,
canonicalExists: fs.existsSync(canonicalPath),
});
}
}
return pairs.sort((a, b) => a.compactPath.localeCompare(b.compactPath));
}
/**
* Check 1 — registration. A `.compact.md` file must have a canonical sibling.
* @param {ReturnType<typeof discoverRegisteredVariants>} pairs
*/
function checkRegistration(pairs) {
const violations = [];
for (const pair of pairs) {
if (!pair.canonicalExists) {
violations.push({ kind: 'orphan_compact_file', compactPath: pair.compactPath });
}
}
return violations;
}
/**
* Check 2 — reachability. A registered pair's compact path must be named by
* at least one markdown file under `searchRoots` (a spine's "Read ... variant
* resolution" call site). Three needle forms, matched differently, because
* this corpus has two live conventions for naming these paths (found by
* walking up from the compact file itself to its nearest `gsd-core` ancestor,
* so this works the same way against the real repo and against a fixture
* that builds its own `<tmp>/gsd-core/...` tree):
*
* - The `gsd-core/<rest>` form (e.g. `gsd-core/workflows/autonomous/steps/
* converge-fail-fast.md`'s own convention) is unambiguous on its own — a
* different, longer path coincidentally ending in this exact multi-segment
* suffix is not a realistic false positive, so a plain substring match is
* sufficient without the "unprefixed" guard below.
* - The `<rest>` form without the leading `gsd-core/` (e.g. `workflows/help/
* modes/full.compact.md`, `help.md`'s own dispatch-table convention) is
* equally unambiguous for the same reason.
* - The bare `<stem>.compact.md` form has no such guarantee — a same-named
* file under an unrelated nested directory (the exact class of bug already
* hit once this phase: `discuss-phase/templates/context.md` vs. the root
* `templates/context.md`) could grant it a false reachability. This form
* keeps the `isUnprefixedMatch` guard from `namesFragmentAsEntryPoint`
* (`scripts/lint-response-language-coverage.cjs`): a path character
* immediately before the match means this is the tail of some longer,
* different path, not the fragment itself.
*
* @param {ReturnType<typeof discoverRegisteredVariants>} pairs
* @param {string[]} searchRoots
*/
function checkReachability(pairs, searchRoots = DEFAULT_SEARCH_ROOTS) {
const violations = [];
const haystacks = [];
for (const root of searchRoots) {
for (const file of findMarkdownFiles(root)) {
haystacks.push(fs.readFileSync(file, 'utf8'));
}
}
for (const pair of pairs) {
if (!pair.canonicalExists) continue; // already reported by checkRegistration
const gsdCoreRelative = relativeToNearestGsdCore(pair.compactPath);
const stem = path.basename(pair.compactPath, COMPACT_SUFFIX);
const bareNeedle = `${stem}${COMPACT_SUFFIX}`;
const reached = haystacks.some((text) => {
if (gsdCoreRelative && (text.includes(`gsd-core/${gsdCoreRelative}`) || text.includes(gsdCoreRelative))) {
return true;
}
return isUnprefixedMatch(text, bareNeedle);
});
if (!reached) {
violations.push({ kind: 'unreachable_compact_file', compactPath: pair.compactPath });
}
}
return violations;
}
/**
* Walk up from `filePath` to the nearest ancestor directory literally named
* `gsd-core`, and return the path from there to `filePath` (POSIX-separated).
* Returns `null` if no such ancestor exists. Anchoring on the literal
* `gsd-core` segment — rather than a hardcoded repo-root constant — is what
* lets this match both the real repo and a fixture built under its own
* `<tmp>/gsd-core/...` tree the same way.
* @param {string} filePath
* @returns {string | null}
*/
function relativeToNearestGsdCore(filePath) {
const segments = filePath.split(path.sep);
const idx = segments.lastIndexOf('gsd-core');
if (idx === -1) return null;
return segments.slice(idx + 1).join('/');
}
/** Is `needle` present in `text` with no path character immediately before it (any line)? */
function isUnprefixedMatch(text, needle) {
return text.split(/\r?\n/).some((line) => {
const at = line.indexOf(needle);
if (at === -1) return false;
const before = at > 0 ? line[at - 1] : '';
return !/[A-Za-z0-9_\-./]/.test(before);
});
}
function findMarkdownFiles(dir) {
return findFilesWithSuffix(dir, '.md');
}
/**
* Check 3 — protected content preserved. Every protected block's non-trivial
* lines in the canonical file must also appear (verbatim, after the same
* normalization the partition guard uses) somewhere in the compact sibling.
* Unlike the partition guard, this is NOT a sentinel-presence check on the
* compact file itself — the compact file need not carry `<!-- gsd:protected -->`
* markers of its own, since it is not itself audited for content it might
* shed later; it only must not have DROPPED the protected wording.
*
* @param {ReturnType<typeof discoverRegisteredVariants>} pairs
*/
function checkProtectedContentPreserved(pairs) {
const violations = [];
for (const pair of pairs) {
if (!pair.canonicalExists) continue;
const canonical = fs.readFileSync(pair.canonicalPath, 'utf8');
const compact = fs.readFileSync(pair.compactPath, 'utf8');
const { blocks } = extractProtectedBlocks(canonical);
if (blocks.length === 0) continue;
const compactLines = new Set(normalizeNonTrivialLines(compact));
for (const block of blocks) {
const missing = block.lines
.map((l) => l.trim())
.filter((l) => l.length > 0)
.filter((l) => !compactLines.has(l));
if (missing.length > 0) {
violations.push({
kind: 'protected_content_dropped',
canonicalPath: pair.canonicalPath,
compactPath: pair.compactPath,
missing,
});
}
}
}
return violations;
}
/**
* Check 4 — size smaller. The compact file must be strictly smaller than its
* canonical sibling; a same-size-or-larger "compact" file is not one.
* @param {ReturnType<typeof discoverRegisteredVariants>} pairs
*/
function checkSizeSmaller(pairs) {
const violations = [];
for (const pair of pairs) {
if (!pair.canonicalExists) continue;
const canonicalSize = fs.statSync(pair.canonicalPath).size;
const compactSize = fs.statSync(pair.compactPath).size;
if (!(compactSize < canonicalSize)) {
violations.push({
kind: 'compact_not_smaller',
canonicalPath: pair.canonicalPath,
compactPath: pair.compactPath,
canonicalSize,
compactSize,
});
}
}
return violations;
}
module.exports = {
DEFAULT_VARIANT_ROOTS,
DEFAULT_SEARCH_ROOTS,
COMPACT_SUFFIX,
discoverRegisteredVariants,
checkRegistration,
checkReachability,
checkProtectedContentPreserved,
checkSizeSmaller,
};