@ktpartners/dgs-platform 3.0.4 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/README.md +8 -1
- package/agents/dgs-executor.md +124 -3
- package/agents/dgs-idea-researcher.md +447 -0
- package/agents/dgs-plan-checker.md +32 -0
- package/agents/dgs-planner.md +41 -8
- package/bin/install.js +44 -0
- package/commands/dgs/audit-milestone.md +2 -1
- package/commands/dgs/diff-report.md +124 -0
- package/commands/dgs/new-project.md +8 -21
- package/commands/dgs/package-scan.md +43 -0
- package/commands/dgs/research-idea.md +1 -0
- package/commands/dgs/switch-project.md +13 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +120 -5
- package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
- package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
- package/deliver-great-systems/bin/lib/commands.cjs +311 -16
- package/deliver-great-systems/bin/lib/commands.test.cjs +115 -0
- package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
- package/deliver-great-systems/bin/lib/config.cjs +41 -0
- package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
- package/deliver-great-systems/bin/lib/core.cjs +7 -3
- package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
- package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
- package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
- package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
- package/deliver-great-systems/bin/lib/governance.cjs +211 -0
- package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
- package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
- package/deliver-great-systems/bin/lib/init.cjs +56 -27
- package/deliver-great-systems/bin/lib/init.test.cjs +212 -5
- package/deliver-great-systems/bin/lib/jobs.cjs +7 -4
- package/deliver-great-systems/bin/lib/milestone.cjs +101 -3
- package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
- package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
- package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
- package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
- package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
- package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
- package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
- package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
- package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
- package/deliver-great-systems/bin/lib/phase.cjs +18 -1
- package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
- package/deliver-great-systems/bin/lib/projects.cjs +38 -3
- package/deliver-great-systems/bin/lib/projects.test.cjs +112 -2
- package/deliver-great-systems/bin/lib/quick.cjs +178 -23
- package/deliver-great-systems/bin/lib/quick.test.cjs +138 -4
- package/deliver-great-systems/bin/lib/repos.cjs +12 -12
- package/deliver-great-systems/bin/lib/review.cjs +1821 -0
- package/deliver-great-systems/bin/lib/state.cjs +7 -3
- package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
- package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
- package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
- package/deliver-great-systems/bin/lib/verify.cjs +118 -6
- package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
- package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
- package/deliver-great-systems/bin/lib/worktrees.cjs +27 -1
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +76 -0
- package/deliver-great-systems/references/agent-step-reliability.md +60 -0
- package/deliver-great-systems/references/conflict-resolution.md +4 -0
- package/deliver-great-systems/references/context-tiers.md +4 -0
- package/deliver-great-systems/references/package-scan-config.md +151 -0
- package/deliver-great-systems/references/questioning.md +0 -30
- package/deliver-great-systems/references/spec-review-loop.md +1 -2
- package/deliver-great-systems/references/workflow-conventions.md +29 -0
- package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
- package/deliver-great-systems/templates/REVIEW.md +35 -0
- package/deliver-great-systems/templates/VALIDATION.md +1 -1
- package/deliver-great-systems/templates/claude-md.md +11 -0
- package/deliver-great-systems/templates/package-scan-report.md +108 -0
- package/deliver-great-systems/templates/project.md +6 -170
- package/deliver-great-systems/templates/summary.md +3 -1
- package/deliver-great-systems/workflows/add-phase.md +5 -0
- package/deliver-great-systems/workflows/audit-milestone.md +66 -10
- package/deliver-great-systems/workflows/cancel-job.md +1 -1
- package/deliver-great-systems/workflows/codereview.md +103 -9
- package/deliver-great-systems/workflows/complete-milestone.md +26 -7
- package/deliver-great-systems/workflows/complete-quick.md +40 -2
- package/deliver-great-systems/workflows/discuss-phase.md +3 -2
- package/deliver-great-systems/workflows/execute-phase.md +89 -2
- package/deliver-great-systems/workflows/execute-plan.md +10 -1
- package/deliver-great-systems/workflows/help.md +51 -18
- package/deliver-great-systems/workflows/import-spec.md +65 -7
- package/deliver-great-systems/workflows/init-product.md +46 -152
- package/deliver-great-systems/workflows/new-milestone.md +115 -14
- package/deliver-great-systems/workflows/new-project.md +60 -331
- package/deliver-great-systems/workflows/package-scan.md +59 -0
- package/deliver-great-systems/workflows/plan-phase.md +79 -1
- package/deliver-great-systems/workflows/quick-complete.md +40 -2
- package/deliver-great-systems/workflows/quick.md +183 -10
- package/deliver-great-systems/workflows/research-idea.md +80 -142
- package/deliver-great-systems/workflows/run-job.md +21 -35
- package/deliver-great-systems/workflows/settings.md +13 -77
- package/deliver-great-systems/workflows/write-spec.md +9 -11
- package/hooks/dist/dgs-enforce-discipline.js +196 -0
- package/package.json +1 -1
- package/scripts/build-hooks.js +1 -0
|
@@ -678,7 +678,10 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
678
678
|
normalizedStatus = 'discussing';
|
|
679
679
|
} else if (statusLower.includes('verif')) {
|
|
680
680
|
normalizedStatus = 'verifying';
|
|
681
|
-
} else if (statusLower
|
|
681
|
+
} else if (statusLower === 'completed' || statusLower === 'done' || statusLower === 'project completed') {
|
|
682
|
+
// Only exact matches trigger 'completed' — "Phase X execution complete" or
|
|
683
|
+
// "Milestone shipped" should NOT mark the project as completed.
|
|
684
|
+
// Project completion is a manual action via /dgs:complete-project.
|
|
682
685
|
normalizedStatus = 'completed';
|
|
683
686
|
} else if (statusLower.includes('ready to execute')) {
|
|
684
687
|
normalizedStatus = 'executing';
|
|
@@ -919,8 +922,9 @@ function markMilestoneComplete(cwd) {
|
|
|
919
922
|
const today = new Date().toISOString().split('T')[0];
|
|
920
923
|
const now = new Date().toISOString();
|
|
921
924
|
|
|
922
|
-
// Update frontmatter fields
|
|
923
|
-
|
|
925
|
+
// Update frontmatter fields — milestone complete, NOT project complete
|
|
926
|
+
// Project completion is a separate manual action via /dgs:complete-project
|
|
927
|
+
fm.status = 'milestone_shipped';
|
|
924
928
|
if (!fm.progress) fm.progress = {};
|
|
925
929
|
fm.progress.percent = 100;
|
|
926
930
|
fm.last_updated = now;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// deliver-great-systems/bin/lib/summary-frontmatter.cjs
|
|
2
|
+
// REL-07: canonical SUMMARY-frontmatter writer.
|
|
3
|
+
// Reads PLAN.md frontmatter `requirements:` field and writes a SUMMARY.md
|
|
4
|
+
// frontmatter that ALWAYS populates the canonical YAML key (CANONICAL_KEY)
|
|
5
|
+
// with those exact values.
|
|
6
|
+
//
|
|
7
|
+
// Canonical key: requirements_completed (underscore — pinned by 157-Q2-FINDINGS.md).
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
11
|
+
|
|
12
|
+
const CANONICAL_KEY = 'requirements_completed';
|
|
13
|
+
|
|
14
|
+
function writeSummaryFrontmatter({ planPath, summaryPath, extra = {} }) {
|
|
15
|
+
if (!fs.existsSync(planPath)) {
|
|
16
|
+
throw new Error(`writeSummaryFrontmatter: plan not found at ${planPath}`);
|
|
17
|
+
}
|
|
18
|
+
const planContent = fs.readFileSync(planPath, 'utf-8');
|
|
19
|
+
const planFm = extractFrontmatter(planContent);
|
|
20
|
+
const requirements = Array.isArray(planFm.requirements) ? planFm.requirements : [];
|
|
21
|
+
|
|
22
|
+
// Build SUMMARY frontmatter with the canonical key populated verbatim.
|
|
23
|
+
const fm = {
|
|
24
|
+
phase: extra.phase || planFm.phase || 'unknown',
|
|
25
|
+
plan: extra.plan || planFm.plan || '01',
|
|
26
|
+
subsystem: extra.subsystem || 'unspecified',
|
|
27
|
+
tags: extra.tags || [],
|
|
28
|
+
duration: extra.duration || '0min',
|
|
29
|
+
completed: extra.completed || new Date().toISOString().slice(0, 10),
|
|
30
|
+
[CANONICAL_KEY]: requirements,
|
|
31
|
+
...(extra.executed_by ? { executed_by: extra.executed_by } : {}),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Serialise to YAML frontmatter. Minimal stable serialiser to avoid
|
|
35
|
+
// introducing a new dependency.
|
|
36
|
+
const lines = ['---'];
|
|
37
|
+
for (const [key, value] of Object.entries(fm)) {
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
if (value.length === 0) {
|
|
40
|
+
lines.push(`${key}: []`);
|
|
41
|
+
} else {
|
|
42
|
+
lines.push(`${key}: [${value.join(', ')}]`);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
lines.push(`${key}: ${value}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
lines.push('---', '', `# Phase ${fm.phase} Plan ${fm.plan}: Summary`, '');
|
|
49
|
+
|
|
50
|
+
fs.writeFileSync(summaryPath, lines.join('\n'));
|
|
51
|
+
return { summaryPath, canonicalKey: CANONICAL_KEY, requirementsCopied: requirements.length };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { writeSummaryFrontmatter, CANONICAL_KEY };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// deliver-great-systems/bin/lib/summary-frontmatter.test.cjs
|
|
2
|
+
// REL-07 regression test scaffold — initially RED. Turns GREEN after plan 02
|
|
3
|
+
// pins the canonical YAML key and updates writer/template/agent/workflow/reader.
|
|
4
|
+
|
|
5
|
+
const test = require('node:test');
|
|
6
|
+
const assert = require('node:assert');
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const os = require('node:os');
|
|
10
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
11
|
+
|
|
12
|
+
// CANONICAL_KEY: pinned by 157-Q2-FINDINGS.md (underscore variant).
|
|
13
|
+
// Plan 01 leaves the placeholder; plan 02 confirms the writer/template/etc. agree.
|
|
14
|
+
const CANONICAL_KEY = 'requirements_completed';
|
|
15
|
+
|
|
16
|
+
test('REL-07: SUMMARY writer copies PLAN requirements verbatim into canonical key', () => {
|
|
17
|
+
// RED: this assertion fails because no canonical writer module exists yet.
|
|
18
|
+
let writer;
|
|
19
|
+
try {
|
|
20
|
+
writer = require('./summary-frontmatter.cjs');
|
|
21
|
+
} catch (err) {
|
|
22
|
+
assert.fail('REL-07 writer module not yet implemented: bin/lib/summary-frontmatter.cjs (plan 02 task)');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rel07-'));
|
|
26
|
+
const planPath = path.join(tmpDir, 'test-PLAN.md');
|
|
27
|
+
const summaryPath = path.join(tmpDir, 'test-SUMMARY.md');
|
|
28
|
+
fs.writeFileSync(planPath, '---\nphase: test\nplan: 01\nrequirements:\n - TEST-01\n - TEST-02\n---\n');
|
|
29
|
+
|
|
30
|
+
writer.writeSummaryFrontmatter({ planPath, summaryPath });
|
|
31
|
+
|
|
32
|
+
const summary = fs.readFileSync(summaryPath, 'utf-8');
|
|
33
|
+
const fm = extractFrontmatter(summary);
|
|
34
|
+
assert.deepStrictEqual(fm[CANONICAL_KEY], ['TEST-01', 'TEST-02']);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('REL-07: SUMMARY writer leaves canonical key empty when PLAN requirements is empty', () => {
|
|
38
|
+
let writer;
|
|
39
|
+
try {
|
|
40
|
+
writer = require('./summary-frontmatter.cjs');
|
|
41
|
+
} catch (err) {
|
|
42
|
+
assert.fail('REL-07 writer module not yet implemented: bin/lib/summary-frontmatter.cjs (plan 02 task)');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rel07-empty-'));
|
|
46
|
+
const planPath = path.join(tmpDir, 'test-PLAN.md');
|
|
47
|
+
const summaryPath = path.join(tmpDir, 'test-SUMMARY.md');
|
|
48
|
+
fs.writeFileSync(planPath, '---\nphase: test\nplan: 01\nrequirements: []\n---\n');
|
|
49
|
+
|
|
50
|
+
writer.writeSummaryFrontmatter({ planPath, summaryPath });
|
|
51
|
+
|
|
52
|
+
const summary = fs.readFileSync(summaryPath, 'utf-8');
|
|
53
|
+
const fm = extractFrontmatter(summary);
|
|
54
|
+
assert.deepStrictEqual(fm[CANONICAL_KEY] || [], []);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('REL-07: canonical YAML key is consistent across template, agent prompt, workflow, reader, schema', () => {
|
|
58
|
+
// Convergence assertion. RED until plan 02 lands all five edits.
|
|
59
|
+
// __dirname = .../deliver-great-systems/bin/lib
|
|
60
|
+
// bin/lib/.. = .../deliver-great-systems/bin
|
|
61
|
+
// bin/.. = .../deliver-great-systems
|
|
62
|
+
// deliver-great-systems/.. = repo root (which contains the top-level agents/ and the nested deliver-great-systems/)
|
|
63
|
+
const repoRoot = path.resolve(__dirname, '../../..');
|
|
64
|
+
const sources = {
|
|
65
|
+
template: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/templates/summary.md'), 'utf-8'),
|
|
66
|
+
agent: fs.readFileSync(path.join(repoRoot, 'agents/dgs-executor.md'), 'utf-8'),
|
|
67
|
+
workflow: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/workflows/execute-plan.md'), 'utf-8'),
|
|
68
|
+
reader: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/bin/lib/commands.cjs'), 'utf-8'),
|
|
69
|
+
schema: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/bin/lib/frontmatter.cjs'), 'utf-8'),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Each file must reference the CANONICAL_KEY at least once.
|
|
73
|
+
for (const [name, content] of Object.entries(sources)) {
|
|
74
|
+
assert.ok(content.includes(CANONICAL_KEY), `${name} must reference canonical key ${CANONICAL_KEY}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// REL-07 sentinel — flag this file as a Wave-0 RED scaffold for plan 02.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RED test scaffold for REL-02 (Phase 156 plan 03).
|
|
3
|
+
*
|
|
4
|
+
* The `computePhaseSweep` helper is implemented in plan 03; running this
|
|
5
|
+
* file before plan 03 lands MUST produce 6 failed tests with
|
|
6
|
+
* 'not yet implemented — REL-02' style messages.
|
|
7
|
+
*
|
|
8
|
+
* Behaviour under test:
|
|
9
|
+
* - union: commit list = git-discovered phase-dir paths UNION
|
|
10
|
+
* executor-reported modified_files (deduplicated)
|
|
11
|
+
* - underreport defence: empty modified_files still commits the
|
|
12
|
+
* git-discovered phase-dir paths
|
|
13
|
+
* - scope filter — sibling phase: dirty file in a different phase
|
|
14
|
+
* dir is NOT included
|
|
15
|
+
* - scope filter — out-of-scope dirs: dirty files in ideas/, specs/,
|
|
16
|
+
* and project root are NOT included
|
|
17
|
+
* - reported-path escape: a modified_files entry pointing outside the
|
|
18
|
+
* phase dir is filtered out (or surfaced via dropped_out_of_scope)
|
|
19
|
+
* - idempotency: running the helper twice on the same fixture returns
|
|
20
|
+
* identical results
|
|
21
|
+
*
|
|
22
|
+
* Conventions:
|
|
23
|
+
* - Uses node:test runner + node:assert (matches state-transition-gate.test.cjs)
|
|
24
|
+
* - Each test creates and tears down its own temp git repo via os.tmpdir()
|
|
25
|
+
* - Until plan 03 lands, every test fails with 'not yet implemented'
|
|
26
|
+
* so the file is RED in a controlled way (no parse errors).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const test = require('node:test');
|
|
30
|
+
const assert = require('node:assert');
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const os = require('os');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
const { execSync } = require('child_process');
|
|
35
|
+
|
|
36
|
+
const NOT_IMPL = 'computePhaseSweep not yet implemented — REL-02';
|
|
37
|
+
|
|
38
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function tryRequireCommands() {
|
|
41
|
+
try {
|
|
42
|
+
// Reset the planning-root cache between tests so each
|
|
43
|
+
// makeTempPlanningRoot() sees a fresh root; getPlanningRoot caches
|
|
44
|
+
// per-process and would otherwise pin the first temp dir for the
|
|
45
|
+
// whole test run.
|
|
46
|
+
try { require('./paths.cjs').resetPaths(); } catch { /* ignore */ }
|
|
47
|
+
return require('./commands.cjs');
|
|
48
|
+
} catch (err) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeTempPlanningRoot() {
|
|
54
|
+
// Resolve real path to handle symlink wrap on macOS (/tmp -> /private/tmp).
|
|
55
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'sweep-scope-test-')));
|
|
56
|
+
execSync('git init --quiet', { cwd: dir });
|
|
57
|
+
execSync('git config user.email test@example.com', { cwd: dir });
|
|
58
|
+
execSync('git config user.name "Test User"', { cwd: dir });
|
|
59
|
+
// Seed an initial commit so HEAD exists
|
|
60
|
+
fs.writeFileSync(path.join(dir, 'README.md'), '# seed\n');
|
|
61
|
+
execSync('git add README.md', { cwd: dir });
|
|
62
|
+
execSync('git commit --quiet -m "seed"', { cwd: dir });
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cleanupRoot(dir) {
|
|
67
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeFile(root, rel, content) {
|
|
71
|
+
const abs = path.join(root, rel);
|
|
72
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
73
|
+
fs.writeFileSync(abs, content);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Test 1: union ────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
test('REL-02 union: commit list = git-discovered phase-dir paths UNION executor-reported modified_files', () => {
|
|
79
|
+
const cmds = tryRequireCommands();
|
|
80
|
+
if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
|
|
81
|
+
assert.fail(NOT_IMPL);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const root = makeTempPlanningRoot();
|
|
85
|
+
try {
|
|
86
|
+
const phasesDir = 'phases';
|
|
87
|
+
const phaseDir = '156-test';
|
|
88
|
+
// git-dirty: A and B exist in the phase dir, untracked
|
|
89
|
+
writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
|
|
90
|
+
writeFile(root, `${phasesDir}/${phaseDir}/B.md`, 'b\n');
|
|
91
|
+
// C is reported by the executor but does not exist on disk yet
|
|
92
|
+
writeFile(root, `${phasesDir}/${phaseDir}/C.md`, 'c\n');
|
|
93
|
+
const result = cmds.computePhaseSweep(root, {
|
|
94
|
+
phasesDir,
|
|
95
|
+
phaseDir,
|
|
96
|
+
modifiedFiles: [
|
|
97
|
+
`${phasesDir}/${phaseDir}/B.md`,
|
|
98
|
+
`${phasesDir}/${phaseDir}/C.md`,
|
|
99
|
+
],
|
|
100
|
+
}, true);
|
|
101
|
+
const swept = (result && result.swept) || [];
|
|
102
|
+
const expected = [
|
|
103
|
+
`${phasesDir}/${phaseDir}/A.md`,
|
|
104
|
+
`${phasesDir}/${phaseDir}/B.md`,
|
|
105
|
+
`${phasesDir}/${phaseDir}/C.md`,
|
|
106
|
+
].sort();
|
|
107
|
+
assert.deepStrictEqual(swept.slice().sort(), expected, 'union must equal A,B,C');
|
|
108
|
+
} finally {
|
|
109
|
+
cleanupRoot(root);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ─── Test 2: underreport defence ──────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
test('REL-02 underreport: executor returns empty modified_files but git-discovered phase-dir paths still committed', () => {
|
|
116
|
+
const cmds = tryRequireCommands();
|
|
117
|
+
if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
|
|
118
|
+
assert.fail(NOT_IMPL);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const root = makeTempPlanningRoot();
|
|
122
|
+
try {
|
|
123
|
+
const phasesDir = 'phases';
|
|
124
|
+
const phaseDir = '156-test';
|
|
125
|
+
writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
|
|
126
|
+
writeFile(root, `${phasesDir}/${phaseDir}/B.md`, 'b\n');
|
|
127
|
+
const result = cmds.computePhaseSweep(root, {
|
|
128
|
+
phasesDir,
|
|
129
|
+
phaseDir,
|
|
130
|
+
modifiedFiles: [],
|
|
131
|
+
}, true);
|
|
132
|
+
const swept = (result && result.swept) || [];
|
|
133
|
+
assert.strictEqual(swept.length, 2, 'expected 2 git-dirty files');
|
|
134
|
+
assert.ok(swept.includes(`${phasesDir}/${phaseDir}/A.md`));
|
|
135
|
+
assert.ok(swept.includes(`${phasesDir}/${phaseDir}/B.md`));
|
|
136
|
+
} finally {
|
|
137
|
+
cleanupRoot(root);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ─── Test 3: scope filter — sibling phase ─────────────────────────────────
|
|
142
|
+
|
|
143
|
+
test('REL-02 scope filter: dirty file in sibling phase dir is NOT included', () => {
|
|
144
|
+
const cmds = tryRequireCommands();
|
|
145
|
+
if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
|
|
146
|
+
assert.fail(NOT_IMPL);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const root = makeTempPlanningRoot();
|
|
150
|
+
try {
|
|
151
|
+
const phasesDir = 'phases';
|
|
152
|
+
const phaseDir = '156-test';
|
|
153
|
+
const sibling = '155-other';
|
|
154
|
+
writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
|
|
155
|
+
writeFile(root, `${phasesDir}/${sibling}/X.md`, 'x\n');
|
|
156
|
+
const result = cmds.computePhaseSweep(root, {
|
|
157
|
+
phasesDir,
|
|
158
|
+
phaseDir,
|
|
159
|
+
modifiedFiles: [],
|
|
160
|
+
}, true);
|
|
161
|
+
const swept = (result && result.swept) || [];
|
|
162
|
+
assert.strictEqual(swept.length, 1);
|
|
163
|
+
assert.ok(swept.includes(`${phasesDir}/${phaseDir}/A.md`));
|
|
164
|
+
assert.ok(!swept.some(p => p.includes(sibling)),
|
|
165
|
+
'sibling phase paths must NOT appear in swept');
|
|
166
|
+
} finally {
|
|
167
|
+
cleanupRoot(root);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ─── Test 4: scope filter — ideas / specs / project root ──────────────────
|
|
172
|
+
|
|
173
|
+
test('REL-02 scope filter: dirty file in ideas/, specs/, project root is NOT included', () => {
|
|
174
|
+
const cmds = tryRequireCommands();
|
|
175
|
+
if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
|
|
176
|
+
assert.fail(NOT_IMPL);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const root = makeTempPlanningRoot();
|
|
180
|
+
try {
|
|
181
|
+
const phasesDir = 'phases';
|
|
182
|
+
const phaseDir = '156-test';
|
|
183
|
+
writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
|
|
184
|
+
writeFile(root, 'ideas/foo.md', 'idea\n');
|
|
185
|
+
writeFile(root, 'specs/bar.md', 'spec\n');
|
|
186
|
+
writeFile(root, 'NOTES.md', 'top-level\n');
|
|
187
|
+
const result = cmds.computePhaseSweep(root, {
|
|
188
|
+
phasesDir,
|
|
189
|
+
phaseDir,
|
|
190
|
+
modifiedFiles: [],
|
|
191
|
+
}, true);
|
|
192
|
+
const swept = (result && result.swept) || [];
|
|
193
|
+
assert.strictEqual(swept.length, 1, 'only the in-scope phase file should be swept');
|
|
194
|
+
assert.ok(swept.includes(`${phasesDir}/${phaseDir}/A.md`));
|
|
195
|
+
for (const out of ['ideas/foo.md', 'specs/bar.md', 'NOTES.md']) {
|
|
196
|
+
assert.ok(!swept.includes(out), `${out} must NOT be swept`);
|
|
197
|
+
}
|
|
198
|
+
} finally {
|
|
199
|
+
cleanupRoot(root);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ─── Test 5: reported-path escape ─────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
test('REL-02 reported-path escape: a modified_files entry that points outside the phase dir is filtered out', () => {
|
|
206
|
+
const cmds = tryRequireCommands();
|
|
207
|
+
if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
|
|
208
|
+
assert.fail(NOT_IMPL);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const root = makeTempPlanningRoot();
|
|
212
|
+
try {
|
|
213
|
+
const phasesDir = 'phases';
|
|
214
|
+
const phaseDir = '156-test';
|
|
215
|
+
const sibling = '155-other';
|
|
216
|
+
writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
|
|
217
|
+
writeFile(root, `${phasesDir}/${sibling}/X.md`, 'x\n');
|
|
218
|
+
const result = cmds.computePhaseSweep(root, {
|
|
219
|
+
phasesDir,
|
|
220
|
+
phaseDir,
|
|
221
|
+
modifiedFiles: [
|
|
222
|
+
`${phasesDir}/${phaseDir}/A.md`,
|
|
223
|
+
`${phasesDir}/${sibling}/X.md`, // escape attempt
|
|
224
|
+
],
|
|
225
|
+
}, true);
|
|
226
|
+
const swept = (result && result.swept) || [];
|
|
227
|
+
assert.strictEqual(swept.length, 1, 'only the in-scope path should be swept');
|
|
228
|
+
assert.ok(!swept.includes(`${phasesDir}/${sibling}/X.md`),
|
|
229
|
+
'sibling phase path must be filtered out of swept');
|
|
230
|
+
// Implementations may or may not surface a dropped_out_of_scope array;
|
|
231
|
+
// the contract is only that the dropped path is NOT in `swept`.
|
|
232
|
+
} finally {
|
|
233
|
+
cleanupRoot(root);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ─── Test 6: idempotency ──────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
test('REL-02 idempotency: running the sweep twice on the same fixture returns the same set', () => {
|
|
240
|
+
const cmds = tryRequireCommands();
|
|
241
|
+
if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
|
|
242
|
+
assert.fail(NOT_IMPL);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const root = makeTempPlanningRoot();
|
|
246
|
+
try {
|
|
247
|
+
const phasesDir = 'phases';
|
|
248
|
+
const phaseDir = '156-test';
|
|
249
|
+
writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
|
|
250
|
+
writeFile(root, `${phasesDir}/${phaseDir}/B.md`, 'b\n');
|
|
251
|
+
const opts = {
|
|
252
|
+
phasesDir,
|
|
253
|
+
phaseDir,
|
|
254
|
+
modifiedFiles: [`${phasesDir}/${phaseDir}/B.md`],
|
|
255
|
+
};
|
|
256
|
+
const r1 = cmds.computePhaseSweep(root, opts, true);
|
|
257
|
+
const r2 = cmds.computePhaseSweep(root, opts, true);
|
|
258
|
+
assert.deepStrictEqual((r1 && r1.swept) || [], (r2 && r2.swept) || [],
|
|
259
|
+
'two identical calls must return identical swept sets');
|
|
260
|
+
} finally {
|
|
261
|
+
cleanupRoot(root);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
@@ -550,16 +550,13 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
550
550
|
return;
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
-
// ─── Check 2: PROJECT.md exists and has
|
|
553
|
+
// ─── Check 2: PROJECT.md exists and has a heading ────────────────────────
|
|
554
554
|
if (!fs.existsSync(projectPath)) {
|
|
555
555
|
addIssue('error', 'E002', 'PROJECT.md not found', 'Run /dgs:new-project to create');
|
|
556
556
|
} else {
|
|
557
557
|
const content = fs.readFileSync(projectPath, 'utf-8');
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (!content.includes(section)) {
|
|
561
|
-
addIssue('warning', 'W001', `PROJECT.md missing section: ${section}`, 'Add section manually');
|
|
562
|
-
}
|
|
558
|
+
if (!/^#\s+\S/m.test(content)) {
|
|
559
|
+
addIssue('warning', 'W001', 'PROJECT.md missing top-level heading', 'Add a # Title line');
|
|
563
560
|
}
|
|
564
561
|
}
|
|
565
562
|
|
|
@@ -788,6 +785,121 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
788
785
|
}
|
|
789
786
|
} catch { /* non-git or git error — skip silently */ }
|
|
790
787
|
|
|
788
|
+
// ─── Check 11: Untracked scaffolding (.gitkeep) — REL-12 ────────────────
|
|
789
|
+
// Walks planning root for `.gitkeep` files in standard scaffold locations
|
|
790
|
+
// (specs/, docs/product/, quick/, projects/*/{phases,quick,debug,research}/)
|
|
791
|
+
// and warns if any exist on disk but aren't tracked in git. User-facing analog
|
|
792
|
+
// of REL-09 (init-product .gitkeep commit) — REL-09 prevented the leak,
|
|
793
|
+
// REL-12 catches any reintroduction immediately on the next health-check run.
|
|
794
|
+
try {
|
|
795
|
+
const { execGit } = require('./core.cjs');
|
|
796
|
+
const isGitWT = (() => {
|
|
797
|
+
try {
|
|
798
|
+
const res = execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
799
|
+
return res && res.exitCode === 0 && (res.stdout || '').trim() === 'true';
|
|
800
|
+
} catch { return false; }
|
|
801
|
+
})();
|
|
802
|
+
|
|
803
|
+
if (isGitWT) {
|
|
804
|
+
const candidates = [];
|
|
805
|
+
|
|
806
|
+
// Direct scaffold locations under planning root
|
|
807
|
+
const directDirs = ['specs', 'docs/product', 'quick'];
|
|
808
|
+
for (const d of directDirs) {
|
|
809
|
+
const p = path.join(planningDir, d, '.gitkeep');
|
|
810
|
+
if (fs.existsSync(p)) candidates.push(path.join(d, '.gitkeep'));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Per-project scaffold locations: projects/*/{phases,quick,debug,research}/.gitkeep
|
|
814
|
+
const projectsDir = path.join(planningDir, 'projects');
|
|
815
|
+
if (fs.existsSync(projectsDir)) {
|
|
816
|
+
try {
|
|
817
|
+
const projects = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
818
|
+
for (const proj of projects) {
|
|
819
|
+
if (!proj.isDirectory()) continue;
|
|
820
|
+
for (const sub of ['phases', 'quick', 'debug', 'research']) {
|
|
821
|
+
const p = path.join(planningDir, 'projects', proj.name, sub, '.gitkeep');
|
|
822
|
+
if (fs.existsSync(p)) {
|
|
823
|
+
candidates.push(path.join('projects', proj.name, sub, '.gitkeep'));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
} catch { /* ignore */ }
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// For each candidate, check whether it's tracked in git via
|
|
831
|
+
// `git ls-files <rel>` — empty stdout means untracked.
|
|
832
|
+
const untracked = [];
|
|
833
|
+
for (const rel of candidates) {
|
|
834
|
+
const lsResult = execGit(cwd, ['ls-files', '--', rel]);
|
|
835
|
+
const tracked = lsResult && lsResult.exitCode === 0 && (lsResult.stdout || '').trim().length > 0;
|
|
836
|
+
if (!tracked) untracked.push(rel);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (untracked.length > 0) {
|
|
840
|
+
addIssue(
|
|
841
|
+
'warning',
|
|
842
|
+
'untracked-scaffolding',
|
|
843
|
+
`Found ${untracked.length} untracked .gitkeep file(s) in standard scaffold locations: ${untracked.join(', ')}`,
|
|
844
|
+
`Run 'git add ${untracked.join(' ')} && git commit -m "docs: track scaffolding files"' to commit them; closes idea #29 (init-product .gitkeep leak) on the next health-check run.`,
|
|
845
|
+
false
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
} catch { /* non-git or git error — skip silently per existing convention */ }
|
|
850
|
+
|
|
851
|
+
// ─── Check 12: untracked-phase-artifacts (REL-11, Phase 156) ───────────────
|
|
852
|
+
// Walk every phase directory under projects/<project>/phases/ (or
|
|
853
|
+
// top-level phases/ in v1) and report any uncommitted PLAN.md /
|
|
854
|
+
// CONTEXT.md / RESEARCH.md / UAT.md / VERIFICATION.md files. This is
|
|
855
|
+
// the loud-fail safety net for REL-01 / REL-02 — if either of those
|
|
856
|
+
// commit-step contracts ever regresses, this health check surfaces
|
|
857
|
+
// the dangling artifact on the next /dgs:health run.
|
|
858
|
+
try {
|
|
859
|
+
const untracked = [];
|
|
860
|
+
if (fs.existsSync(phasesDir)) {
|
|
861
|
+
const ARTIFACT_RE = /^[0-9].*-(PLAN|CONTEXT|RESEARCH|UAT|VERIFICATION)\.md$/;
|
|
862
|
+
const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
863
|
+
for (const entry of phaseEntries) {
|
|
864
|
+
if (!entry.isDirectory()) continue;
|
|
865
|
+
const phaseDirAbs = path.join(phasesDir, entry.name);
|
|
866
|
+
let files;
|
|
867
|
+
try { files = fs.readdirSync(phaseDirAbs); } catch { continue; }
|
|
868
|
+
for (const f of files) {
|
|
869
|
+
if (!ARTIFACT_RE.test(f)) continue;
|
|
870
|
+
const absPath = path.join(phaseDirAbs, f);
|
|
871
|
+
const relPath = path.relative(planningDir, absPath);
|
|
872
|
+
// `git ls-files -- <relPath>` returns the path if tracked,
|
|
873
|
+
// empty string if untracked. Run with cwd = planningDir so
|
|
874
|
+
// the relative path resolves correctly.
|
|
875
|
+
const lsResult = execGit(planningDir, ['ls-files', '--', relPath]);
|
|
876
|
+
const tracked = lsResult.exitCode === 0 && (lsResult.stdout || '').trim().length > 0;
|
|
877
|
+
if (!tracked) untracked.push(relPath);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (untracked.length > 0) {
|
|
882
|
+
const remediation =
|
|
883
|
+
`Run 'node $TOOLS commit "docs: catch up untracked phase artifacts" --files ${untracked.join(' ')}' ` +
|
|
884
|
+
`or run /dgs:health --repair`;
|
|
885
|
+
addIssue(
|
|
886
|
+
'warning',
|
|
887
|
+
'W010',
|
|
888
|
+
`untracked-phase-artifacts: ${untracked.length} uncommitted phase artifact(s) — ${untracked.join(', ')}`,
|
|
889
|
+
remediation,
|
|
890
|
+
false
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
} catch (err) {
|
|
894
|
+
addIssue(
|
|
895
|
+
'warning',
|
|
896
|
+
'W011',
|
|
897
|
+
`untracked-phase-artifacts check failed: ${err.message}`,
|
|
898
|
+
'Investigate verify.cjs walker',
|
|
899
|
+
false
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
791
903
|
// ─── Perform repairs if requested ─────────────────────────────────────────
|
|
792
904
|
const repairActions = [];
|
|
793
905
|
if (options.repair && repairs.length > 0) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// deliver-great-systems/bin/lib/verify.test.cjs
|
|
2
|
+
// REL-12 untracked-scaffolding health check — initially RED. Turns GREEN after plan 04
|
|
3
|
+
// adds Check 11 to bin/lib/verify.cjs::cmdValidateHealth.
|
|
4
|
+
|
|
5
|
+
const test = require('node:test');
|
|
6
|
+
const assert = require('node:assert');
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const os = require('node:os');
|
|
10
|
+
const { execSync } = require('node:child_process');
|
|
11
|
+
|
|
12
|
+
function setupREL12Fixture({ trackedKeeps, untrackedKeeps }) {
|
|
13
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rel12-'));
|
|
14
|
+
execSync('git init -q', { cwd: root });
|
|
15
|
+
execSync('git config user.email test@test', { cwd: root });
|
|
16
|
+
execSync('git config user.name test', { cwd: root });
|
|
17
|
+
fs.writeFileSync(path.join(root, 'PROJECT.md'),
|
|
18
|
+
'# Test\n## What This Is\n## Core Value\n## Requirements\n');
|
|
19
|
+
fs.writeFileSync(path.join(root, 'ROADMAP.md'), '# Roadmap\n');
|
|
20
|
+
fs.writeFileSync(path.join(root, 'STATE.md'), '# State\n');
|
|
21
|
+
fs.mkdirSync(path.join(root, 'phases'), { recursive: true });
|
|
22
|
+
// Create + track requested .gitkeep paths
|
|
23
|
+
for (const p of trackedKeeps) {
|
|
24
|
+
fs.mkdirSync(path.join(root, path.dirname(p)), { recursive: true });
|
|
25
|
+
fs.writeFileSync(path.join(root, p), '');
|
|
26
|
+
execSync(`git add "${p}"`, { cwd: root });
|
|
27
|
+
}
|
|
28
|
+
// Always commit at least PROJECT.md so HEAD exists even when no keeps are tracked
|
|
29
|
+
execSync('git add PROJECT.md ROADMAP.md STATE.md', { cwd: root });
|
|
30
|
+
execSync('git commit -q -m "init"', { cwd: root });
|
|
31
|
+
// Create untracked .gitkeep paths (not added to git)
|
|
32
|
+
for (const p of untrackedKeeps) {
|
|
33
|
+
fs.mkdirSync(path.join(root, path.dirname(p)), { recursive: true });
|
|
34
|
+
fs.writeFileSync(path.join(root, p), '');
|
|
35
|
+
}
|
|
36
|
+
return root;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function captureHealthOutput(root) {
|
|
40
|
+
// cmdValidateHealth calls output() which process.exit(0)s — run in a child
|
|
41
|
+
// process and capture stdout JSON. Pattern matches health-untracked-phase.test.cjs.
|
|
42
|
+
const verifyPath = path.resolve(__dirname, 'verify.cjs');
|
|
43
|
+
const shim = [
|
|
44
|
+
`const v = require(${JSON.stringify(verifyPath)});`,
|
|
45
|
+
`v.cmdValidateHealth(${JSON.stringify(root)}, { raw: true }, true);`,
|
|
46
|
+
].join('\n');
|
|
47
|
+
const shimPath = path.join(os.tmpdir(), `rel12-shim-${process.pid}-${Date.now()}.cjs`);
|
|
48
|
+
fs.writeFileSync(shimPath, shim);
|
|
49
|
+
try {
|
|
50
|
+
const stdout = execSync(`node ${JSON.stringify(shimPath)}`, { encoding: 'utf-8' });
|
|
51
|
+
try { return JSON.parse(stdout); } catch { return { stdout, warnings: [] }; }
|
|
52
|
+
} catch (err) {
|
|
53
|
+
const out = err.stdout && err.stdout.toString();
|
|
54
|
+
try { return JSON.parse(out); } catch { return { stdout: out, warnings: [] }; }
|
|
55
|
+
} finally {
|
|
56
|
+
try { fs.unlinkSync(shimPath); } catch { /* ignore */ }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test('REL-12: cmdValidateHealth flags untracked .gitkeep in standard scaffold locations (untracked-scaffolding)', () => {
|
|
61
|
+
const root = setupREL12Fixture({
|
|
62
|
+
trackedKeeps: ['specs/.gitkeep'],
|
|
63
|
+
untrackedKeeps: ['docs/product/.gitkeep', 'quick/.gitkeep'],
|
|
64
|
+
});
|
|
65
|
+
const result = captureHealthOutput(root);
|
|
66
|
+
const scaffoldingWarnings = (result.warnings || []).filter(w => w.code === 'untracked-scaffolding');
|
|
67
|
+
assert.strictEqual(scaffoldingWarnings.length, 1, 'exactly one untracked-scaffolding warning expected');
|
|
68
|
+
assert.match(scaffoldingWarnings[0].message, /docs\/product\/\.gitkeep/);
|
|
69
|
+
assert.match(scaffoldingWarnings[0].message, /quick\/\.gitkeep/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('REL-12: cmdValidateHealth does NOT flag tracked .gitkeep files', () => {
|
|
73
|
+
const root = setupREL12Fixture({
|
|
74
|
+
trackedKeeps: ['specs/.gitkeep', 'docs/product/.gitkeep', 'quick/.gitkeep'],
|
|
75
|
+
untrackedKeeps: [],
|
|
76
|
+
});
|
|
77
|
+
const result = captureHealthOutput(root);
|
|
78
|
+
const scaffoldingWarnings = (result.warnings || []).filter(w => w.code === 'untracked-scaffolding');
|
|
79
|
+
assert.strictEqual(scaffoldingWarnings.length, 0, 'no untracked-scaffolding warning when all keeps tracked');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// REL-12 sentinel — flag this block as a Wave-0 RED scaffold for plan 04.
|