@tekyzinc/gsd-t 4.9.13 → 4.10.10
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 +24 -0
- package/README.md +1 -1
- package/bin/gsd-t-competition-judge.cjs +7 -1
- package/bin/gsd-t-context-brief-kinds/impact.cjs +91 -4
- package/bin/gsd-t-context-brief-kinds/partition.cjs +95 -0
- package/bin/gsd-t-context-brief-kinds/plan.cjs +110 -4
- package/bin/gsd-t-file-disjointness.cjs +319 -7
- package/bin/gsd-t-graph-anti-grep-lint.cjs +515 -0
- package/bin/gsd-t-graph-edge-extract.cjs +612 -0
- package/bin/gsd-t-graph-freshness.cjs +506 -0
- package/bin/gsd-t-graph-index.cjs +540 -0
- package/bin/gsd-t-graph-k1-sqlite-stream.cjs +251 -0
- package/bin/gsd-t-graph-query-cli.cjs +1182 -0
- package/bin/gsd-t-graph-scip-upgrade.cjs +440 -0
- package/bin/gsd-t-graph-store-bakeoff.cjs +889 -0
- package/bin/gsd-t-graph-synthetic-gen.cjs +304 -0
- package/bin/gsd-t-graph-ts-throughput.cjs +587 -0
- package/bin/gsd-t-scip-reader.cjs +167 -0
- package/bin/gsd-t.js +166 -48
- package/commands/gsd-t-debug.md +10 -0
- package/commands/gsd-t-design-build.md +10 -0
- package/commands/gsd-t-execute.md +15 -0
- package/commands/gsd-t-feature.md +15 -7
- package/commands/gsd-t-gap-analysis.md +17 -7
- package/commands/gsd-t-impact.md +25 -0
- package/commands/gsd-t-integrate.md +25 -0
- package/commands/gsd-t-partition.md +18 -0
- package/commands/gsd-t-plan.md +16 -0
- package/commands/gsd-t-populate.md +16 -5
- package/commands/gsd-t-prd.md +18 -0
- package/commands/gsd-t-project.md +19 -0
- package/commands/gsd-t-promote-debt.md +16 -5
- package/commands/gsd-t-qa.md +20 -8
- package/commands/gsd-t-quick.md +10 -0
- package/commands/gsd-t-scan.md +21 -3
- package/commands/gsd-t-test-sync.md +10 -0
- package/commands/gsd-t-verify.md +25 -0
- package/commands/gsd-t-wave.md +8 -0
- package/package.json +10 -2
- package/templates/workflows/gsd-t-debug.workflow.js +81 -0
- package/templates/workflows/gsd-t-integrate.workflow.js +47 -1
- package/templates/workflows/gsd-t-phase.workflow.js +99 -1
- package/templates/workflows/gsd-t-quick.workflow.js +64 -0
- package/templates/workflows/gsd-t-scan.workflow.js +200 -9
- package/templates/workflows/gsd-t-verify.workflow.js +50 -1
- package/bin/graph-cgc.js +0 -510
- package/bin/graph-indexer.js +0 -147
- package/bin/graph-overlay.js +0 -195
- package/bin/graph-parsers.js +0 -327
- package/bin/graph-query.js +0 -453
- package/bin/graph-store.js +0 -154
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [4.10.10] - 2026-06-26
|
|
6
|
+
|
|
7
|
+
### Added — M94 persistent code graph + M95 real SCIP call-graph resolution; graph runtime now propagates into projects
|
|
8
|
+
|
|
9
|
+
The code graph is no longer grep-dependent in principle: M94 shipped a persistent, all-local on-disk index (files / functions / imports / call graph) with a deterministic no-grep-fallback query CLI, and M95 made the precise tier REAL — it reads scip-typescript's `index.scip` and resolves cross-file call edges instead of relabeling unresolved tree-sitter edges "compiler-accurate." Verified on the real Atos repo: the test→impl verb returns 164 resolved cross-file edges (was 0). The graph runtime is now on the project-propagation list, so `update-all` copies the query CLI into every project's `bin/` — the wired consumers (execute/wave disjointness, debug, quick, impact, plan, scan) can finally read a project's graph instead of falling back to grep.
|
|
10
|
+
|
|
11
|
+
- `bin/gsd-t-scip-reader.cjs`: NEW — decodes `index.scip` into symbol→funcId + per-file reference maps (via scip-typescript's bundled protobuf decoder, located across install layouts, fail-loud if absent).
|
|
12
|
+
- `bin/gsd-t-graph-scip-upgrade.cjs`: `buildScipResolver` runs scip-typescript ONCE per repo (was once-per-file with the output discarded) and resolves `UNRESOLVED#<name>` call edges; tier honesty — `compiler-accurate` only when SCIP actually resolved an edge.
|
|
13
|
+
- `bin/gsd-t-graph-index.cjs`: auto-builds the resolver and threads it through `parse_and_put`.
|
|
14
|
+
- `bin/gsd-t.js`: `gsd-t install` auto-installs scip-typescript + scip-python; `gsd-t doctor` reports their presence; graph runtime added to `PROJECT_BIN_TOOLS`.
|
|
15
|
+
- `bin/gsd-t-graph-freshness.cjs` + `bin/gsd-t-graph-query-cli.cjs`: OOM fix (`runFreshnessCheck` walked `/` → 17 GB) + `openDb` canonical store-path fix + parse_and_put signature fix.
|
|
16
|
+
- `test/m95-scip-call-resolution.test.js`: NEW — resolution, tier honesty, fail-loud, passthrough (5/5).
|
|
17
|
+
|
|
18
|
+
Decision: GSD-T's zero-dependency installer rule is reframed as a GUIDING PRINCIPLE, not a hard rule — SCIP indexers are now an install requirement because they materially improve the product. Suite: 2502/2502 pass (4 pre-existing skips). Both heavy real-Atos tests pass.
|
|
19
|
+
|
|
20
|
+
## [4.9.14] - 2026-06-25 (workflow runCli retry — reliability patch)
|
|
21
|
+
|
|
22
|
+
### Fixed — transient helper-call flakes no longer false-block workflows
|
|
23
|
+
|
|
24
|
+
`runCli` (the inline helper every `*.workflow.js` uses to run a GSD-T CLI command) delegates the call to a haiku helper agent, not a subprocess — so a return can transiently come back missing its parsed result, surfacing later as a cryptic `classify failed: no envelope` or a fail-closed gate block even though the CLI itself succeeded. Caught live during the M94 plan phase: the traceability gate passed when run directly (exit 0) but the workflow's confirm-call came back unparsed and failed closed.
|
|
25
|
+
|
|
26
|
+
- `runCli` now **retries once** when JSON was expected but no parsed result returned (covers both the throw path and a malformed return the loose schema let through). A real CLI failure that returned valid JSON (`ok:false` with a present envelope) is **not** retried — a true result, not a transient miss.
|
|
27
|
+
- Hardens every phase/wave/integrate/debug workflow's CLI calls (preflight, verify-gate, traceability, brief, build-coverage, test-data).
|
|
28
|
+
|
|
5
29
|
## [4.9.13] - 2026-06-25 (M93 — Reader Contract every turn, retire the selective gate — patch)
|
|
6
30
|
|
|
7
31
|
### Changed — concise output is now enforced on EVERY reply, not by pattern-matching
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v4.
|
|
3
|
+
**v4.10.10** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
|
|
4
4
|
|
|
5
5
|
**Eliminates context rot** — task-level fresh dispatch (one subagent per task, ~10-20% context each) means compaction never triggers.
|
|
6
6
|
**Compaction-proof debug loops** — `gsd-t headless --debug-loop` runs test-fix-retest cycles as separate `claude -p` sessions. A JSONL debug ledger persists all hypothesis/fix/learning history across fresh sessions. Anti-repetition preamble injection prevents retrying failed hypotheses. Escalation tiers (sonnet → opus → human) and a hard iteration ceiling enforced externally.
|
|
@@ -123,8 +123,14 @@ function scorePartition(candidate, projectDir) {
|
|
|
123
123
|
|
|
124
124
|
// Run the real oracle when available; otherwise fall back to a self-contained
|
|
125
125
|
// overlap check so the judge still works if the lib isn't co-located.
|
|
126
|
+
// fallbackToTouchesOnly: the judge SCORES partition shapes — it is not gating a
|
|
127
|
+
// live parallel fan-out — so when the graph index is absent (M94: the query CLI
|
|
128
|
+
// exists but no index is built, e.g. on a tiny synthetic candidate), it must
|
|
129
|
+
// degrade to the literal-Touches disjointness count, NOT return empty groups
|
|
130
|
+
// (which would zero parallelGroups and break scoring). The FAIL-LOUD-HALT path
|
|
131
|
+
// is reserved for execute's real fan-out gate, which requires the graph.
|
|
126
132
|
const res = proveDisjointness
|
|
127
|
-
? proveDisjointness({ tasks, projectDir })
|
|
133
|
+
? proveDisjointness({ tasks, projectDir, disjointnessFallback: "touches-only" })
|
|
128
134
|
: _localDisjoint(tasks);
|
|
129
135
|
|
|
130
136
|
const parallelGroups = (res.parallel || []).length;
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* summary so an impact-analysis worker can understand the blast radius.
|
|
7
7
|
*
|
|
8
8
|
* Fail-open: missing optional source → empty field, brief still written.
|
|
9
|
+
*
|
|
10
|
+
* M94-D8-T4 [RULE] brief-resolves-active-milestone-never-completed:
|
|
11
|
+
* Cross-checks the resolved milestonePrefix against the Completed-Milestones
|
|
12
|
+
* table in progress.md. A prefix appearing there is COMPLETED and is rejected
|
|
13
|
+
* (staleness flag set), never returned as the active milestone prefix.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
const fs = require('fs');
|
|
@@ -19,10 +24,90 @@ function _readMaybe(file) {
|
|
|
19
24
|
try { return fs.readFileSync(file, 'utf8'); } catch (_) { return null; }
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Build the set of milestone prefixes (lowercase) that are COMPLETED.
|
|
29
|
+
* Sources:
|
|
30
|
+
* 1. The main Milestones table (## Milestones) — rows where Status column is COMPLETED/COMPLETE.
|
|
31
|
+
* 2. The Completed Milestones table (## Completed Milestones) — all data rows.
|
|
32
|
+
*
|
|
33
|
+
* [RULE] brief-resolves-active-milestone-never-completed
|
|
34
|
+
*
|
|
35
|
+
* @param {string} progressText
|
|
36
|
+
* @returns {Set<string>}
|
|
37
|
+
*/
|
|
38
|
+
function _completedPrefixes(progressText) {
|
|
39
|
+
const completed = new Set();
|
|
40
|
+
if (!progressText) return completed;
|
|
41
|
+
|
|
42
|
+
const lines = progressText.split(/\r?\n/);
|
|
43
|
+
let inCompletedMilestonesTable = false;
|
|
44
|
+
let inMilestonesTable = false;
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
if (/^##\s+Completed\s+Milestones/i.test(line)) {
|
|
48
|
+
inCompletedMilestonesTable = true;
|
|
49
|
+
inMilestonesTable = false;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (/^##\s+Milestones\s*$/i.test(line)) {
|
|
53
|
+
inMilestonesTable = true;
|
|
54
|
+
inCompletedMilestonesTable = false;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (/^##\s+/.test(line)) {
|
|
58
|
+
inCompletedMilestonesTable = false;
|
|
59
|
+
inMilestonesTable = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (inCompletedMilestonesTable) {
|
|
64
|
+
const match = line.match(/^\|\s*(M\d+)/i);
|
|
65
|
+
if (match) {
|
|
66
|
+
completed.add(match[1].toLowerCase());
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (inMilestonesTable) {
|
|
71
|
+
if (/^\|/.test(line)) {
|
|
72
|
+
const cols = line.split('|').slice(1, -1).map((c) => c.trim());
|
|
73
|
+
if (cols.length >= 3) {
|
|
74
|
+
const milestoneCol = cols[0];
|
|
75
|
+
const statusCol = cols[2];
|
|
76
|
+
const milestoneMatch = milestoneCol.match(/^(M\d+)/i);
|
|
77
|
+
if (milestoneMatch && /^COMPLET/i.test(statusCol)) {
|
|
78
|
+
completed.add(milestoneMatch[1].toLowerCase());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return completed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the ACTIVE milestone prefix (not completed).
|
|
90
|
+
*
|
|
91
|
+
* [RULE] brief-resolves-active-milestone-never-completed
|
|
92
|
+
*
|
|
93
|
+
* @param {string|null} progressText
|
|
94
|
+
* @returns {{ prefix: string|null, staleness: string|null }}
|
|
95
|
+
*/
|
|
22
96
|
function _currentMilestonePrefix(progressText) {
|
|
23
|
-
if (!progressText) return null;
|
|
24
|
-
|
|
25
|
-
|
|
97
|
+
if (!progressText) return { prefix: null, staleness: null };
|
|
98
|
+
|
|
99
|
+
const completed = _completedPrefixes(progressText);
|
|
100
|
+
|
|
101
|
+
const lines = progressText.split(/\r?\n/);
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
const m = line.match(/^\|\s*(M\d+)\s*\|.*(DEFINED|PARTITIONED|PLANNED|EXECUTING|EXECUTED|VERIFY)/i);
|
|
104
|
+
if (!m) continue;
|
|
105
|
+
const prefix = m[1].toLowerCase();
|
|
106
|
+
if (completed.has(prefix)) continue;
|
|
107
|
+
return { prefix, staleness: null };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { prefix: null, staleness: 'no-active-milestone-row' };
|
|
26
111
|
}
|
|
27
112
|
|
|
28
113
|
function _integrationPointsExcerpt(projectDir, prefix, recordSource) {
|
|
@@ -75,7 +160,7 @@ function collect(ctx) {
|
|
|
75
160
|
const progressText = _readMaybe(path.join(projectDir, PROGRESS_PATH));
|
|
76
161
|
if (progressText) recordSource(PROGRESS_PATH);
|
|
77
162
|
|
|
78
|
-
const prefix = _currentMilestonePrefix(progressText);
|
|
163
|
+
const { prefix, staleness } = _currentMilestonePrefix(progressText);
|
|
79
164
|
const integration = _integrationPointsExcerpt(projectDir, prefix, recordSource);
|
|
80
165
|
|
|
81
166
|
return {
|
|
@@ -87,6 +172,7 @@ function collect(ctx) {
|
|
|
87
172
|
integrationPointsExcerpt: integration,
|
|
88
173
|
gitDiffSummary: _gitDiffSummary(projectDir),
|
|
89
174
|
changedFiles: _gitDiffNames(projectDir),
|
|
175
|
+
...(staleness ? { staleness } : {}),
|
|
90
176
|
},
|
|
91
177
|
};
|
|
92
178
|
}
|
|
@@ -97,4 +183,5 @@ module.exports = {
|
|
|
97
183
|
collect,
|
|
98
184
|
_currentMilestonePrefix,
|
|
99
185
|
_integrationPointsExcerpt,
|
|
186
|
+
_completedPrefixes,
|
|
100
187
|
};
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* domain table for partition-phase workers.
|
|
7
7
|
*
|
|
8
8
|
* Fail-open: missing optional source → empty field, brief still written.
|
|
9
|
+
*
|
|
10
|
+
* M94-D8-T4 [RULE] brief-resolves-active-milestone-never-completed:
|
|
11
|
+
* Cross-checks the resolved milestonePrefix against the Completed-Milestones
|
|
12
|
+
* table in progress.md. A prefix appearing there is COMPLETED and is rejected
|
|
13
|
+
* (staleness flag set), never returned as the active milestone prefix.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
const fs = require('fs');
|
|
@@ -40,6 +45,92 @@ function _existingDomains(projectDir) {
|
|
|
40
45
|
return entries.filter((e) => /^[a-z0-9_-]+$/i.test(e)).sort();
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Build the set of milestone prefixes (lowercase) that are COMPLETED.
|
|
50
|
+
* Sources:
|
|
51
|
+
* 1. The main Milestones table (## Milestones) — rows where Status column is COMPLETED/COMPLETE.
|
|
52
|
+
* 2. The Completed Milestones table (## Completed Milestones) — all data rows.
|
|
53
|
+
*
|
|
54
|
+
* [RULE] brief-resolves-active-milestone-never-completed
|
|
55
|
+
*
|
|
56
|
+
* @param {string} progressText
|
|
57
|
+
* @returns {Set<string>}
|
|
58
|
+
*/
|
|
59
|
+
function _completedPrefixes(progressText) {
|
|
60
|
+
const completed = new Set();
|
|
61
|
+
if (!progressText) return completed;
|
|
62
|
+
|
|
63
|
+
const lines = progressText.split(/\r?\n/);
|
|
64
|
+
let inCompletedMilestonesTable = false;
|
|
65
|
+
let inMilestonesTable = false;
|
|
66
|
+
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
if (/^##\s+Completed\s+Milestones/i.test(line)) {
|
|
69
|
+
inCompletedMilestonesTable = true;
|
|
70
|
+
inMilestonesTable = false;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (/^##\s+Milestones\s*$/i.test(line)) {
|
|
74
|
+
inMilestonesTable = true;
|
|
75
|
+
inCompletedMilestonesTable = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (/^##\s+/.test(line)) {
|
|
79
|
+
inCompletedMilestonesTable = false;
|
|
80
|
+
inMilestonesTable = false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (inCompletedMilestonesTable) {
|
|
85
|
+
const match = line.match(/^\|\s*(M\d+)/i);
|
|
86
|
+
if (match) {
|
|
87
|
+
completed.add(match[1].toLowerCase());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (inMilestonesTable) {
|
|
92
|
+
if (/^\|/.test(line)) {
|
|
93
|
+
const cols = line.split('|').slice(1, -1).map((c) => c.trim());
|
|
94
|
+
if (cols.length >= 3) {
|
|
95
|
+
const milestoneCol = cols[0];
|
|
96
|
+
const statusCol = cols[2];
|
|
97
|
+
const milestoneMatch = milestoneCol.match(/^(M\d+)/i);
|
|
98
|
+
if (milestoneMatch && /^COMPLET/i.test(statusCol)) {
|
|
99
|
+
completed.add(milestoneMatch[1].toLowerCase());
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return completed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the ACTIVE milestone prefix (not completed).
|
|
111
|
+
*
|
|
112
|
+
* [RULE] brief-resolves-active-milestone-never-completed
|
|
113
|
+
*
|
|
114
|
+
* @param {string|null} progressText
|
|
115
|
+
* @returns {{ prefix: string|null, staleness: string|null }}
|
|
116
|
+
*/
|
|
117
|
+
function _currentMilestonePrefix(progressText) {
|
|
118
|
+
if (!progressText) return { prefix: null, staleness: null };
|
|
119
|
+
|
|
120
|
+
const completed = _completedPrefixes(progressText);
|
|
121
|
+
|
|
122
|
+
const lines = progressText.split(/\r?\n/);
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const m = line.match(/^\|\s*(M\d+)\s*\|.*(DEFINED|PARTITIONED|PLANNED|EXECUTING|EXECUTED|VERIFY)/i);
|
|
125
|
+
if (!m) continue;
|
|
126
|
+
const prefix = m[1].toLowerCase();
|
|
127
|
+
if (completed.has(prefix)) continue;
|
|
128
|
+
return { prefix, staleness: null };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { prefix: null, staleness: 'no-active-milestone-row' };
|
|
132
|
+
}
|
|
133
|
+
|
|
43
134
|
function collect(ctx) {
|
|
44
135
|
const { projectDir, recordSource } = ctx;
|
|
45
136
|
|
|
@@ -50,6 +141,7 @@ function collect(ctx) {
|
|
|
50
141
|
if (disjointRules) recordSource(DISJOINT_RULES_PATH);
|
|
51
142
|
|
|
52
143
|
const milestoneRow = _currentMilestoneRow(progressText);
|
|
144
|
+
const { prefix, staleness } = _currentMilestonePrefix(progressText);
|
|
53
145
|
const domains = _existingDomains(projectDir);
|
|
54
146
|
|
|
55
147
|
// Trim disjoint-rules excerpt to ~1,200 chars to honor cap.
|
|
@@ -68,6 +160,7 @@ function collect(ctx) {
|
|
|
68
160
|
currentMilestoneRow: milestoneRow,
|
|
69
161
|
existingDomains: domains,
|
|
70
162
|
disjointnessRulesExcerpt: rulesExcerpt,
|
|
163
|
+
...(staleness ? { staleness } : {}),
|
|
71
164
|
},
|
|
72
165
|
};
|
|
73
166
|
}
|
|
@@ -77,5 +170,7 @@ module.exports = {
|
|
|
77
170
|
requiresSources: [],
|
|
78
171
|
collect,
|
|
79
172
|
_currentMilestoneRow,
|
|
173
|
+
_currentMilestonePrefix,
|
|
80
174
|
_existingDomains,
|
|
175
|
+
_completedPrefixes,
|
|
81
176
|
};
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* partitioned domain names with their scope.md "Files Owned" first-N entries.
|
|
6
6
|
*
|
|
7
7
|
* Fail-open: missing optional source → empty field, brief still written.
|
|
8
|
+
*
|
|
9
|
+
* M94-D8-T4 [RULE] brief-resolves-active-milestone-never-completed:
|
|
10
|
+
* The resolved milestonePrefix MUST belong to an ACTIVE (not COMPLETED) milestone.
|
|
11
|
+
* Cross-check against the Completed-Milestones table in progress.md before returning.
|
|
12
|
+
* If the resolved prefix appears in the Completed-Milestones table (or the main Milestones
|
|
13
|
+
* table with a COMPLETED status), it is rejected and ancillary.staleness is set.
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
const fs = require('fs');
|
|
@@ -28,6 +34,76 @@ function _currentMilestoneRow(progressText) {
|
|
|
28
34
|
return null;
|
|
29
35
|
}
|
|
30
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Build the set of milestone prefixes (lowercase) that are COMPLETED.
|
|
39
|
+
* Sources:
|
|
40
|
+
* 1. The main Milestones table (## Milestones) — rows where the Status column is COMPLETED/COMPLETE.
|
|
41
|
+
* 2. The Completed Milestones table (## Completed Milestones) — all data rows.
|
|
42
|
+
*
|
|
43
|
+
* Returns a Set<string> of lowercase prefix strings like 'm65', 'm93', etc.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} progressText
|
|
46
|
+
* @returns {Set<string>}
|
|
47
|
+
*/
|
|
48
|
+
function _completedPrefixes(progressText) {
|
|
49
|
+
const completed = new Set();
|
|
50
|
+
if (!progressText) return completed;
|
|
51
|
+
|
|
52
|
+
const lines = progressText.split(/\r?\n/);
|
|
53
|
+
let inCompletedMilestonesTable = false;
|
|
54
|
+
let inMilestonesTable = false;
|
|
55
|
+
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
// Detect the "## Completed Milestones" section header.
|
|
58
|
+
if (/^##\s+Completed\s+Milestones/i.test(line)) {
|
|
59
|
+
inCompletedMilestonesTable = true;
|
|
60
|
+
inMilestonesTable = false;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// Detect the "## Milestones" section header (not "## Completed Milestones").
|
|
64
|
+
if (/^##\s+Milestones\s*$/i.test(line)) {
|
|
65
|
+
inMilestonesTable = true;
|
|
66
|
+
inCompletedMilestonesTable = false;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Stop tracking on any other heading.
|
|
70
|
+
if (/^##\s+/.test(line)) {
|
|
71
|
+
inCompletedMilestonesTable = false;
|
|
72
|
+
inMilestonesTable = false;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (inCompletedMilestonesTable) {
|
|
77
|
+
// All data rows in the Completed Milestones table are completed milestones.
|
|
78
|
+
// Row format: | M93 Brevity Guard | 4.9.11 | ...
|
|
79
|
+
// or: | M93 | ... | COMPLETED | ...
|
|
80
|
+
const match = line.match(/^\|\s*(M\d+)/i);
|
|
81
|
+
if (match) {
|
|
82
|
+
completed.add(match[1].toLowerCase());
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (inMilestonesTable) {
|
|
87
|
+
// Rows in the main Milestones table with COMPLETED or COMPLETE status.
|
|
88
|
+
// Row format: | M65 | Orchestration-Shell Retirement | COMPLETED | ...
|
|
89
|
+
// The status is the 3rd pipe-column.
|
|
90
|
+
if (/^\|/.test(line)) {
|
|
91
|
+
const cols = line.split('|').slice(1, -1).map((c) => c.trim());
|
|
92
|
+
if (cols.length >= 3) {
|
|
93
|
+
const milestoneCol = cols[0]; // e.g. "M65"
|
|
94
|
+
const statusCol = cols[2]; // e.g. "COMPLETED"
|
|
95
|
+
const milestoneMatch = milestoneCol.match(/^(M\d+)/i);
|
|
96
|
+
if (milestoneMatch && /^COMPLET/i.test(statusCol)) {
|
|
97
|
+
completed.add(milestoneMatch[1].toLowerCase());
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return completed;
|
|
105
|
+
}
|
|
106
|
+
|
|
31
107
|
function _domainSummaries(projectDir, currentMilestonePrefix, recordSource) {
|
|
32
108
|
const dir = path.join(projectDir, '.gsd-t', 'domains');
|
|
33
109
|
let entries;
|
|
@@ -57,10 +133,38 @@ function _domainSummaries(projectDir, currentMilestonePrefix, recordSource) {
|
|
|
57
133
|
return out;
|
|
58
134
|
}
|
|
59
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the ACTIVE milestone prefix from the progress text.
|
|
138
|
+
* Returns the prefix string (e.g. 'm94') if found and NOT completed, or null.
|
|
139
|
+
* Also returns a staleness flag if a resolved prefix is completed.
|
|
140
|
+
*
|
|
141
|
+
* [RULE] brief-resolves-active-milestone-never-completed
|
|
142
|
+
*
|
|
143
|
+
* @param {string|null} progressText
|
|
144
|
+
* @returns {{ prefix: string|null, staleness: string|null }}
|
|
145
|
+
*/
|
|
60
146
|
function _currentMilestonePrefix(progressText) {
|
|
61
|
-
if (!progressText) return null;
|
|
62
|
-
|
|
63
|
-
|
|
147
|
+
if (!progressText) return { prefix: null, staleness: null };
|
|
148
|
+
|
|
149
|
+
// Build the completed set FIRST for cross-checking.
|
|
150
|
+
const completed = _completedPrefixes(progressText);
|
|
151
|
+
|
|
152
|
+
// Walk all matching rows and return the first one that is NOT completed.
|
|
153
|
+
const lines = progressText.split(/\r?\n/);
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
const m = line.match(/^\|\s*(M\d+)\s*\|.*(DEFINED|PARTITIONED|PLANNED|EXECUTING|EXECUTED|VERIFY)/i);
|
|
156
|
+
if (!m) continue;
|
|
157
|
+
const prefix = m[1].toLowerCase();
|
|
158
|
+
if (completed.has(prefix)) {
|
|
159
|
+
// This row's milestone is COMPLETED — skip it, continue scanning.
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Found an active (non-completed) milestone row.
|
|
163
|
+
return { prefix, staleness: null };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// No active milestone row found (or all candidates are completed).
|
|
167
|
+
return { prefix: null, staleness: 'no-active-milestone-row' };
|
|
64
168
|
}
|
|
65
169
|
|
|
66
170
|
function collect(ctx) {
|
|
@@ -70,7 +174,7 @@ function collect(ctx) {
|
|
|
70
174
|
if (progressText) recordSource(PROGRESS_PATH);
|
|
71
175
|
|
|
72
176
|
const milestoneRow = _currentMilestoneRow(progressText);
|
|
73
|
-
const prefix = _currentMilestonePrefix(progressText);
|
|
177
|
+
const { prefix, staleness } = _currentMilestonePrefix(progressText);
|
|
74
178
|
const domains = _domainSummaries(projectDir, prefix, recordSource);
|
|
75
179
|
|
|
76
180
|
return {
|
|
@@ -81,6 +185,7 @@ function collect(ctx) {
|
|
|
81
185
|
currentMilestoneRow: milestoneRow,
|
|
82
186
|
milestonePrefix: prefix,
|
|
83
187
|
partitionedDomains: domains,
|
|
188
|
+
...(staleness ? { staleness } : {}),
|
|
84
189
|
},
|
|
85
190
|
};
|
|
86
191
|
}
|
|
@@ -92,4 +197,5 @@ module.exports = {
|
|
|
92
197
|
_currentMilestoneRow,
|
|
93
198
|
_currentMilestonePrefix,
|
|
94
199
|
_domainSummaries,
|
|
200
|
+
_completedPrefixes,
|
|
95
201
|
};
|