@tekyzinc/gsd-t 5.11.31 → 5.11.32
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 +56 -0
- package/README.md +1 -1
- package/bin/gsd-t-graph-use-gate.cjs +239 -0
- package/bin/gsd-t-verify-gate.cjs +2 -0
- package/bin/gsd-t.js +4 -0
- package/commands/gsd-t-debug.md +1 -1
- package/commands/gsd-t-design-build.md +1 -1
- package/commands/gsd-t-quick.md +1 -1
- package/commands/gsd-t-test-sync.md +1 -1
- package/package.json +2 -2
- package/templates/workflows/gsd-t-integrate.workflow.js +46 -15
- package/templates/workflows/gsd-t-scan.workflow.js +32 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [5.11.32] - 2026-08-14
|
|
6
|
+
|
|
7
|
+
### Fixed — a missing graph is now BUILT, and a "wired" claim has to prove itself
|
|
8
|
+
|
|
9
|
+
A TimeTracking scan finished, reported success, and had done its whole deep
|
|
10
|
+
analysis with grep. The graph was healthy, current, and the run even logged
|
|
11
|
+
`graphWiringMode: WIRED` — but of 666 ledger events, every single one carried
|
|
12
|
+
`consumer: "cli"`. Not one came from an analyst agent. The reports say it
|
|
13
|
+
plainly in their own words: *"a repo-wide grep shows no caller anywhere."*
|
|
14
|
+
Nothing failed, because nothing was watching for this.
|
|
15
|
+
|
|
16
|
+
Three defects, one theme — **a check that cannot fail is not a check**:
|
|
17
|
+
|
|
18
|
+
- **An absent graph left consumers blind instead of building it.** Every
|
|
19
|
+
FAIL-LOUD clause said: don't fall back to grep, then *proceed without the
|
|
20
|
+
structural slice*. That is a third bad outcome sitting between the two the
|
|
21
|
+
doctrine names — the agent answers "what calls this?" with no structural
|
|
22
|
+
knowledge at all. The clauses in `/quick`, `/debug`, `/design-build` and
|
|
23
|
+
`/test-sync` now BUILD the index and re-run the query; only a failed build
|
|
24
|
+
halts. This was already the contract's own rule (§FAIL-LOUD line 64,
|
|
25
|
+
"REPAIR IT, do not merely halt") — the command docs had been contradicting
|
|
26
|
+
it. `[RULE] graph-absent-builds-not-degrades`
|
|
27
|
+
- **`/integrate` had a licensed exemption.** It printed `⚠ graph ABSENT —
|
|
28
|
+
structural wiring-check skipped (announced carve-out)`. A fresh worktree
|
|
29
|
+
carries no graph (it is gitignored), so the check did nothing on exactly the
|
|
30
|
+
runs it was written for. Announcing a skip does not stop it being a skip. It
|
|
31
|
+
now builds. The verify/integrate exception covers the HARD-FAIL only; it
|
|
32
|
+
never covered the build.
|
|
33
|
+
- **The anti-grep lint aimed at the wrong target.** Being static, its only
|
|
34
|
+
failure mode is a code shape (`try graph → catch → grep`). A consumer that
|
|
35
|
+
never calls the graph has no catch block, so the lint reports clean while the
|
|
36
|
+
entire run uses text search.
|
|
37
|
+
|
|
38
|
+
### Added — `gsd-t graph-use-gate` (runtime, `[RULE] wired-claim-requires-query-evidence`)
|
|
39
|
+
|
|
40
|
+
`bin/gsd-t-graph-use-gate.cjs` reads the append-only ledger and fails any
|
|
41
|
+
consumer that logged `WIRED` with zero `kind:"query"` events. It detects the
|
|
42
|
+
**absence of use**, not the presence of a fallback — the thing a static lint
|
|
43
|
+
structurally cannot see. `kind:"read"` (the Read-intercept hook) is never
|
|
44
|
+
accepted as evidence, or it would mask the very failure being detected.
|
|
45
|
+
|
|
46
|
+
Run against the two projects that prompted this, it flags both — confirming the
|
|
47
|
+
defect is systemic, not a one-off. Wired into `gsd-t-verify-gate.cjs` as the
|
|
48
|
+
FAIL-CLOSED `graph-use` check, propagated via `PROJECT_BIN_TOOLS` (it reads each
|
|
49
|
+
project's own ledger), and covered by `test/m113-graph-use-gate.test.js` — whose
|
|
50
|
+
leading test is the negative one, because a gate nobody has watched fail is a
|
|
51
|
+
gate nobody knows works. Exit `0` clean · `4` violations · `64` bad input; a
|
|
52
|
+
missing ledger is a *documented* no-op PASS under `--verify-mode`, tagged
|
|
53
|
+
`noOpPass:true` so it stays distinguishable from wired-and-clean.
|
|
54
|
+
|
|
55
|
+
**Still open:** none of this covers ordinary conversational code-reading, which
|
|
56
|
+
is where the original complaint arose — an agent asked "what calls this?" in a
|
|
57
|
+
worktree with no graph and grepped, with nothing objecting. That needs a
|
|
58
|
+
PreToolUse hook on structural Grep/Read, and is deliberately left for its own
|
|
59
|
+
milestone rather than bolted on here.
|
|
60
|
+
|
|
5
61
|
## [5.11.31] - 2026-08-11
|
|
6
62
|
|
|
7
63
|
### Added — the scanner now ranks its own findings before it numbers them
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v5.11.
|
|
3
|
+
**v5.11.32** - 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.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gsd-t-graph-use-gate.cjs
|
|
4
|
+
*
|
|
5
|
+
* Graph USE gate — proves a consumer that declared itself WIRED actually
|
|
6
|
+
* QUERIED the graph.
|
|
7
|
+
*
|
|
8
|
+
* [RULE] wired-claim-requires-query-evidence
|
|
9
|
+
* [RULE] use-gate-detects-absence-of-use-not-presence-of-fallback
|
|
10
|
+
*
|
|
11
|
+
* WHY THIS EXISTS (the gap it closes):
|
|
12
|
+
* `gsd-t-graph-anti-grep-lint.cjs` is STATIC. It reads source files and looks
|
|
13
|
+
* for a `try graph-query -> catch -> structural grep` fallback. That shape is
|
|
14
|
+
* the only way it can fail.
|
|
15
|
+
*
|
|
16
|
+
* A real scan failed a different way. Every analyst agent simply NEVER CALLED
|
|
17
|
+
* the graph — so there was no catch block, no fallback branch, nothing for a
|
|
18
|
+
* static scan to find. The lint passed, the run logged
|
|
19
|
+
* `{"kind":"wiring","graphWiringMode":"WIRED"}`, and the whole scan ran on
|
|
20
|
+
* text search anyway. A check whose only failure mode is a code shape cannot
|
|
21
|
+
* see a consumer that never tried.
|
|
22
|
+
*
|
|
23
|
+
* This gate reads the RUNTIME ledger instead. A consumer that stamped WIRED
|
|
24
|
+
* and issued zero graph queries in that run is a violation, because "wired"
|
|
25
|
+
* is a claim about behaviour and the ledger is the only record of behaviour.
|
|
26
|
+
*
|
|
27
|
+
* INPUT : .gsd-t/graphDB/logs/graph-events-*.jsonl (append-only, JSONL)
|
|
28
|
+
* OUTPUT : {ok, violations:[{consumer, wiringMode, queryCount, firstWiringTs}], ...}
|
|
29
|
+
* EXIT : 0 clean | 4 violations | 64 bad input (no ledger / unreadable)
|
|
30
|
+
*
|
|
31
|
+
* Unreadable input THROWS (LedgerUnavailable) rather than returning a value —
|
|
32
|
+
* a gate that hands back a result when it could not read its own evidence is
|
|
33
|
+
* the exact failure mode this file exists to remove.
|
|
34
|
+
*
|
|
35
|
+
* Node built-ins only (zero-dep invariant).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
'use strict';
|
|
39
|
+
|
|
40
|
+
const fs = require('fs');
|
|
41
|
+
const path = require('path');
|
|
42
|
+
|
|
43
|
+
// A `kind:"read"` event is the Read-intercept hook, NOT a structural query — it
|
|
44
|
+
// fires on ordinary file reads and would mask the exact failure this gate exists
|
|
45
|
+
// to catch. Only `kind:"query"` counts as evidence that the graph was consulted.
|
|
46
|
+
const EVIDENCE_KIND = 'query';
|
|
47
|
+
const WIRING_KIND = 'wiring';
|
|
48
|
+
|
|
49
|
+
// `cli` is the operator at a terminal, not a workflow consumer; it has no WIRED
|
|
50
|
+
// claim to honour. Consumers are exempt ONLY by explicit name here.
|
|
51
|
+
const NON_CONSUMER_IDS = new Set(['cli']);
|
|
52
|
+
|
|
53
|
+
/** Thrown when the ledger cannot be located or read. Never swallowed internally. */
|
|
54
|
+
class LedgerUnavailable extends Error {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'LedgerUnavailable';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ledgerDir(projectDir) {
|
|
62
|
+
return path.join(projectDir, '.gsd-t', 'graphDB', 'logs');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Resolve every graph-events-*.jsonl file, oldest first. Throws if none. */
|
|
66
|
+
function ledgerFiles(projectDir) {
|
|
67
|
+
const dir = ledgerDir(projectDir);
|
|
68
|
+
let names;
|
|
69
|
+
try {
|
|
70
|
+
names = fs.readdirSync(dir);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
throw new LedgerUnavailable(`graph event ledger not readable: ${dir} (${err.code || err.message})`);
|
|
73
|
+
}
|
|
74
|
+
const files = names
|
|
75
|
+
.filter((n) => /^graph-events-\d+\.jsonl$/.test(n))
|
|
76
|
+
.sort()
|
|
77
|
+
.map((n) => path.join(dir, n));
|
|
78
|
+
if (files.length === 0) {
|
|
79
|
+
throw new LedgerUnavailable(`no graph-events-*.jsonl in ${dir}`);
|
|
80
|
+
}
|
|
81
|
+
return files;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse the ledger into per-consumer counters.
|
|
86
|
+
* A malformed line is SKIPPED and COUNTED — it is never treated as a query,
|
|
87
|
+
* because counting an unparseable line as evidence would let a corrupt ledger
|
|
88
|
+
* satisfy the gate.
|
|
89
|
+
*/
|
|
90
|
+
function readLedger(files, sinceTs) {
|
|
91
|
+
const consumers = Object.create(null); // prototype-less: consumer ids come from a file
|
|
92
|
+
let malformed = 0;
|
|
93
|
+
let total = 0;
|
|
94
|
+
|
|
95
|
+
const ensure = (id) => {
|
|
96
|
+
if (!consumers[id]) {
|
|
97
|
+
consumers[id] = { consumer: id, queryCount: 0, wiringModes: [], firstWiringTs: null };
|
|
98
|
+
}
|
|
99
|
+
return consumers[id];
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
let text;
|
|
104
|
+
try {
|
|
105
|
+
text = fs.readFileSync(file, 'utf8');
|
|
106
|
+
} catch (err) {
|
|
107
|
+
throw new LedgerUnavailable(`cannot read ${file}: ${err.code || err.message}`);
|
|
108
|
+
}
|
|
109
|
+
for (const line of text.split('\n')) {
|
|
110
|
+
if (!line.trim()) continue;
|
|
111
|
+
total++;
|
|
112
|
+
let ev;
|
|
113
|
+
try {
|
|
114
|
+
ev = JSON.parse(line);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
malformed++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (sinceTs && typeof ev.ts === 'string' && ev.ts < sinceTs) continue;
|
|
120
|
+
|
|
121
|
+
const id = typeof ev.consumer === 'string' && ev.consumer ? ev.consumer : 'unknown';
|
|
122
|
+
const kind = String(ev.kind || '');
|
|
123
|
+
|
|
124
|
+
if (kind === EVIDENCE_KIND) {
|
|
125
|
+
ensure(id).queryCount++;
|
|
126
|
+
} else if (kind === WIRING_KIND) {
|
|
127
|
+
const c = ensure(id);
|
|
128
|
+
c.wiringModes.push(String(ev.graphWiringMode || ''));
|
|
129
|
+
if (!c.firstWiringTs && typeof ev.ts === 'string') c.firstWiringTs = ev.ts;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { consumers, malformed, total };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A consumer violates the gate when it CLAIMED wired and issued ZERO queries.
|
|
138
|
+
* Claiming `fallback-announced` or `disabled` is not a violation here — those
|
|
139
|
+
* are honest declarations, and the anti-grep lint governs whether the fallback
|
|
140
|
+
* itself is permitted.
|
|
141
|
+
*/
|
|
142
|
+
function evaluate(consumers) {
|
|
143
|
+
const violations = [];
|
|
144
|
+
const checked = [];
|
|
145
|
+
|
|
146
|
+
for (const id of Object.keys(consumers)) {
|
|
147
|
+
if (NON_CONSUMER_IDS.has(id)) continue;
|
|
148
|
+
const c = consumers[id];
|
|
149
|
+
if (c.wiringModes.length === 0) continue; // never declared; nothing claimed, nothing to prove
|
|
150
|
+
|
|
151
|
+
const claimedWired = c.wiringModes.some((m) => m.toLowerCase() === 'wired');
|
|
152
|
+
checked.push({ consumer: id, wiringModes: c.wiringModes, queryCount: c.queryCount });
|
|
153
|
+
|
|
154
|
+
if (claimedWired && c.queryCount === 0) {
|
|
155
|
+
violations.push({
|
|
156
|
+
consumer: id,
|
|
157
|
+
wiringMode: 'WIRED',
|
|
158
|
+
queryCount: 0,
|
|
159
|
+
firstWiringTs: c.firstWiringTs,
|
|
160
|
+
evidence:
|
|
161
|
+
`consumer "${id}" logged graphWiringMode=WIRED but issued 0 graph queries ` +
|
|
162
|
+
`(kind:"query") in this window — a WIRED claim with no query evidence means ` +
|
|
163
|
+
`the structural question was answered some other way.`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { violations, checked };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Throws LedgerUnavailable when the evidence cannot be read. */
|
|
171
|
+
function runGate(opts = {}) {
|
|
172
|
+
const projectDir = opts.projectDir || process.cwd();
|
|
173
|
+
|
|
174
|
+
// Test override: callers may inject parsed consumers directly.
|
|
175
|
+
if (opts.consumers) {
|
|
176
|
+
const { violations, checked } = evaluate(opts.consumers);
|
|
177
|
+
return { ok: violations.length === 0, violations, checked, malformed: 0, ledgerFiles: [] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const files = ledgerFiles(projectDir);
|
|
181
|
+
const { consumers, malformed } = readLedger(files, opts.since || null);
|
|
182
|
+
const { violations, checked } = evaluate(consumers);
|
|
183
|
+
return { ok: violations.length === 0, violations, checked, malformed, ledgerFiles: files };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── CLI entry point ─────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
if (require.main === module) {
|
|
189
|
+
const argv = process.argv.slice(2);
|
|
190
|
+
const opts = { projectDir: process.cwd() };
|
|
191
|
+
for (let i = 0; i < argv.length; i++) {
|
|
192
|
+
if (argv[i] === '--project' || argv[i] === '--repo') opts.projectDir = argv[++i];
|
|
193
|
+
else if (argv[i] === '--since') opts.since = argv[++i];
|
|
194
|
+
else if (argv[i] === '--verify-mode') opts.verifyMode = true;
|
|
195
|
+
else if (!argv[i].startsWith('-')) opts.projectDir = argv[i];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let result;
|
|
199
|
+
try {
|
|
200
|
+
result = runGate(opts);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (err instanceof LedgerUnavailable) {
|
|
203
|
+
// No ledger means NO CONSUMER HAS RUN YET — nothing has been claimed, so
|
|
204
|
+
// there is nothing to disprove. That is a documented no-op PASS in
|
|
205
|
+
// --verify-mode, distinguishable in the JSON (`noOpPass:true`) from a
|
|
206
|
+
// wired-but-broken run. Invoked directly it is BAD INPUT (exit 64): an
|
|
207
|
+
// operator asking about evidence that does not exist deserves to be told,
|
|
208
|
+
// not handed a clean bill.
|
|
209
|
+
if (opts.verifyMode) {
|
|
210
|
+
process.stdout.write(JSON.stringify(
|
|
211
|
+
{ ok: true, noOpPass: true, reason: 'no-graph-event-ledger', violations: [], checked: [] }, null, 2) + '\n');
|
|
212
|
+
process.stderr.write('[graph-use-gate] PASS (no-op): no graph event ledger — no consumer has claimed WIRED yet.\n');
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
process.stderr.write(`[graph-use-gate] ERROR: ${err.message}\n`);
|
|
216
|
+
process.stdout.write(JSON.stringify({ ok: false, violations: [], error: err.message }, null, 2) + '\n');
|
|
217
|
+
process.exit(64);
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
223
|
+
|
|
224
|
+
if (!result.ok) {
|
|
225
|
+
process.stderr.write(
|
|
226
|
+
`[graph-use-gate] FAIL: ${result.violations.length} consumer(s) claimed WIRED with zero graph queries.\n`
|
|
227
|
+
);
|
|
228
|
+
for (const v of result.violations) {
|
|
229
|
+
process.stderr.write(` ${v.consumer}: ${v.evidence}\n`);
|
|
230
|
+
}
|
|
231
|
+
process.exit(4);
|
|
232
|
+
}
|
|
233
|
+
process.stderr.write(
|
|
234
|
+
`[graph-use-gate] PASS: ${result.checked.length} declaring consumer(s), all WIRED claims backed by queries.\n`
|
|
235
|
+
);
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = { runGate, readLedger, evaluate, ledgerFiles, LedgerUnavailable, EVIDENCE_KIND, NON_CONSUMER_IDS };
|
|
@@ -313,6 +313,8 @@ function _detectDefaultTrack2(projectDir, notes) {
|
|
|
313
313
|
|
|
314
314
|
plan.push({ id: 'fallbacks', cmd: 'node', args: [path.join(__dirname, 'gsd-t-fallback-detect.cjs'), '--scan', '--project', projectDir, '--json'], timeoutMs: 120000 }); // M106: no unapproved continue-after-failure branch, FAIL-CLOSED (pre-existing ones excluded via .gsd-t/fallbacks-baseline.json)
|
|
315
315
|
|
|
316
|
+
plan.push({ id: 'graph-use', cmd: 'node', args: [path.join(__dirname, 'gsd-t-graph-use-gate.cjs'), '--project', projectDir, '--verify-mode'], timeoutMs: 30000 }); // M113: a consumer that logged graphWiringMode=WIRED must have issued >=1 graph query, FAIL-CLOSED (documented no-op PASS when no ledger exists yet). Catches what the STATIC anti-grep lint structurally cannot: a consumer that never queried at all.
|
|
317
|
+
|
|
316
318
|
// secrets — gitleaks (PATH detection deferred to runtime)
|
|
317
319
|
if (_hasOnPath('gitleaks')) {
|
|
318
320
|
plan.push({
|
package/bin/gsd-t.js
CHANGED
|
@@ -3487,6 +3487,10 @@ const PROJECT_BIN_TOOLS = [
|
|
|
3487
3487
|
// every Write/Edit is DENIED (the guard halts rather than allowing unchecked),
|
|
3488
3488
|
// so this entry is load-bearing — [[project_global_bin_propagation_gap]].
|
|
3489
3489
|
"gsd-t-fallback-detect.cjs",
|
|
3490
|
+
// M113 — Graph USE gate. The verify gate dispatches to it by absolute path,
|
|
3491
|
+
// and it reads the project's OWN .gsd-t/graphDB/logs ledger, so it must live
|
|
3492
|
+
// in the project — [[project_global_bin_propagation_gap]].
|
|
3493
|
+
"gsd-t-graph-use-gate.cjs",
|
|
3490
3494
|
// M107 — Concise rewriter, invoked by the Stop hook.
|
|
3491
3495
|
// M108 — Install self-check. Every project carries its own copy so it can
|
|
3492
3496
|
// verify and repair itself even when the global install is what broke.
|
package/commands/gsd-t-debug.md
CHANGED
|
@@ -98,7 +98,7 @@ Debug applies the **WRITER pattern** from `graph-consumer-wiring-contract.md`:
|
|
|
98
98
|
|
|
99
99
|
**WRITER half (re-index after fix):** After the fix lands, the workflow triggers a re-index of the edited files so downstream graph queries see fresh edges (`graph-freshness-contract.md` D4 surface — `freshness_check_on_query` over the touched set). `[RULE] debug-reader-and-writer-both`.
|
|
100
100
|
|
|
101
|
-
**
|
|
101
|
+
**BUILD-THEN-QUERY on graph-absent:** On `{ok:false, reason:"graph-unavailable"}` the debug workflow **builds the index right then** (`gsd-t graph index`) and re-runs the localization query. It does NOT fall back to grep for the structural question, and it does NOT skip the query. Only a **failed build** halts the graph-query step, with the build's own stderr surfaced. The existing debug-loop logic (2-cycle cap, loop-ledger halt) is NOT disrupted — the graph query is additive, injected before the fix agent receives context. `[RULE] graph-absent-builds-not-degrades`.
|
|
102
102
|
|
|
103
103
|
## Contract-Boundary Debugging
|
|
104
104
|
|
|
@@ -10,7 +10,7 @@ Design-build applies the **WRITER pattern** from `graph-consumer-wiring-contract
|
|
|
10
10
|
|
|
11
11
|
**WRITER half:** After each tier (elements → widgets → pages) generates files, the pipeline triggers a re-index of the generated files (`freshness_check_on_query` from `graph-freshness-contract.md` D4 surface) so the next tier's `who-imports` / `cluster` query sees the new edges from the generated components. `[RULE] design-build-writer-pattern`.
|
|
12
12
|
|
|
13
|
-
**
|
|
13
|
+
**BUILD-THEN-QUERY on graph-absent:** On `{ok:false, reason:"graph-unavailable"}` the pipeline **builds the index right then** (`gsd-t graph index`) and re-runs the structural query. It does NOT fall back to grep, and it does NOT proceed without the structural answer. Only a **failed build** halts, with the build's own stderr surfaced. `[RULE] consumer-structural-grep-removed`, `[RULE] graph-absent-builds-not-degrades`.
|
|
14
14
|
|
|
15
15
|
## Step 1: Launch the Orchestrator
|
|
16
16
|
|
package/commands/gsd-t-quick.md
CHANGED
|
@@ -182,7 +182,7 @@ Quick applies the **WRITER pattern** from `graph-consumer-wiring-contract.md`:
|
|
|
182
182
|
|
|
183
183
|
**WRITER half:** After edits land, the workflow triggers a re-index of the touched files (`freshness_check_on_query` from `graph-freshness-contract.md` D4 surface) so downstream graph queries see fresh edges. `[RULE] quick-writer-pattern`.
|
|
184
184
|
|
|
185
|
-
**
|
|
185
|
+
**BUILD-THEN-QUERY on graph-absent:** On `{ok:false, reason:"graph-unavailable"}` the workflow **builds the index right then** (`gsd-t graph index`) and re-runs the query. It does NOT fall back to grep, and it does NOT proceed without the structural slice — proceeding blind is a third failure mode, not a safe default. Only a **failed build** halts, with the build's own stderr surfaced. `[RULE] consumer-structural-grep-removed`, `[RULE] graph-absent-builds-not-degrades`.
|
|
186
186
|
|
|
187
187
|
## Step 1.5: Graph-Enhanced Scope Check
|
|
188
188
|
|
|
@@ -40,7 +40,7 @@ Test-sync applies the **WRITER pattern** from `graph-consumer-wiring-contract.md
|
|
|
40
40
|
|
|
41
41
|
**WRITER half:** After writing or updating tests, trigger a re-index of the edited test files (`freshness_check_on_query` from `graph-freshness-contract.md` D4 surface) so the next `test-impl` query sees the updated call-site edges from the new test code. `[RULE] test-sync-uses-test-impl-verb`.
|
|
42
42
|
|
|
43
|
-
**
|
|
43
|
+
**BUILD-THEN-QUERY on graph-absent:** On `{ok:false, reason:"graph-unavailable"}` the workflow **builds the index right then** (`gsd-t graph index`) and re-runs the `test-impl` / `untested-impl` queries. Filesystem discovery is NOT an acceptable substitute — announcing a fallback does not stop it being one, and "which test exercises which impl" answered by filename convention is exactly the wrong-answer class the graph exists to remove. Only a **failed build** halts, with the build's own stderr surfaced. `[RULE] consumer-structural-grep-removed`, `[RULE] graph-absent-builds-not-degrades`.
|
|
44
44
|
|
|
45
45
|
## Step 1.5: Graph-Enhanced Test Discovery
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "5.11.
|
|
4
|
-
"description": "GSD-T: Contract-Driven Development for Claude Code
|
|
3
|
+
"version": "5.11.32",
|
|
4
|
+
"description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -79,6 +79,15 @@ async function classifyGraphFailure(projectDir, reason, detail, phaseName) {
|
|
|
79
79
|
if (env && (env.state === "ABSENT" || env.state === "BROKEN")) return env.state;
|
|
80
80
|
return "BROKEN";
|
|
81
81
|
}
|
|
82
|
+
// [RULE] absent-graph-auto-builds-once — build the index once via the existing
|
|
83
|
+
// `gsd-t graph index` path (local bin: gsd-t-graph-index.cjs build --repo <dir>).
|
|
84
|
+
async function buildGraphIndex(projectDir, phaseName) {
|
|
85
|
+
const r = await runCli(
|
|
86
|
+
projectDir, "graph index", ["build", "--repo", projectDir],
|
|
87
|
+
"gsd-t-graph-index.cjs", "graph-index", false, phaseName
|
|
88
|
+
).catch(() => null);
|
|
89
|
+
return !!(r && r.ok);
|
|
90
|
+
}
|
|
82
91
|
async function runVerifyGate(projectDir, label = "verify-gate", phaseName) { return runCli(projectDir, "verify-gate", ["--json"], "gsd-t-verify-gate.cjs", label, true, phaseName); }
|
|
83
92
|
async function generateBrief(projectDir, { kind = "execute", milestone, domain, id, label = "brief", phaseName } = {}) {
|
|
84
93
|
const argv = ["--kind", kind, "--spawn-id", id, "--out", `${projectDir}/.gsd-t/briefs/${id}.json`];
|
|
@@ -133,32 +142,54 @@ await persistWiringMode("Preflight");
|
|
|
133
142
|
|
|
134
143
|
// M94-D10-T6: Graph Structural Slice — who-imports + blast-radius (ADDITIVE, announced-degradation)
|
|
135
144
|
// [RULE] integrate-uses-graph-for-wiring-verification
|
|
136
|
-
// [RULE] verify-integrate-graph-additive-announced-not-hard-fail —
|
|
137
|
-
//
|
|
145
|
+
// [RULE] verify-integrate-graph-additive-announced-not-hard-fail — integrate does NOT
|
|
146
|
+
// hard-fail on a graph it cannot use. It DOES build an ABSENT index first
|
|
147
|
+
// ([RULE] graph-absent-builds-not-degrades); only a BROKEN graph, or a build that
|
|
148
|
+
// itself fails, degrades announced.
|
|
138
149
|
let _graphWhoImportsSlice = null;
|
|
139
150
|
let _graphBlastRadiusSlice = null;
|
|
140
151
|
let _graphIntegrateWarning = null;
|
|
141
152
|
|
|
142
153
|
{
|
|
143
|
-
|
|
154
|
+
// [RULE] graph-absent-builds-not-degrades — an ABSENT graph is BUILT here, not
|
|
155
|
+
// skipped. The former "announced carve-out" skipped the structural wiring-check
|
|
156
|
+
// whenever the index had never been built — which is exactly when integrate most
|
|
157
|
+
// needs it, because a fresh worktree carries no graph (it is gitignored). The
|
|
158
|
+
// check therefore did nothing on the very runs it was written for. Announcing a
|
|
159
|
+
// skip does not stop it being a skip.
|
|
160
|
+
let wiEnv = (await runCli(
|
|
144
161
|
projectDir, "graph who-imports", [], "gsd-t-graph-query-cli.cjs",
|
|
145
162
|
"graph:who-imports", true, "Integrate"
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
if (wiEnv.ok ===
|
|
149
|
-
|
|
150
|
-
log(`M94 graph who-imports: ${(wiEnv.results || []).length} result(s) (tier: ${wiEnv.tier || "?"})`);
|
|
151
|
-
} else if (wiEnv.ok === false) {
|
|
152
|
-
// [RULE] one-availability-classifier — distinguish ABSENT (announced skip) from BROKEN (LOUD).
|
|
163
|
+
)).envelope || {};
|
|
164
|
+
|
|
165
|
+
if (wiEnv.ok === false) {
|
|
166
|
+
// [RULE] one-availability-classifier — ONE classify call, ONE state, no re-derivation.
|
|
153
167
|
const _state = await classifyGraphFailure(projectDir, wiEnv.reason, wiEnv.detail, "Integrate");
|
|
154
|
-
if (_state === "
|
|
155
|
-
|
|
168
|
+
if (_state === "ABSENT") {
|
|
169
|
+
log("M94 graph who-imports: index ABSENT (never indexed) — building it now (gsd-t graph index)...");
|
|
170
|
+
if (await buildGraphIndex(projectDir, "Integrate")) {
|
|
171
|
+
wiEnv = (await runCli(
|
|
172
|
+
projectDir, "graph who-imports", [], "gsd-t-graph-query-cli.cjs",
|
|
173
|
+
"graph:who-imports", true, "Integrate"
|
|
174
|
+
)).envelope || {};
|
|
175
|
+
if (wiEnv.ok !== true) {
|
|
176
|
+
_graphIntegrateWarning = `⚠ graph built but who-imports still failed (reason=${wiEnv.reason || "?"}) — structural wiring-check skipped. FIX it (gsd-t graph status).`;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
// A failed BUILD is loud, and distinct from a merely-missing index.
|
|
180
|
+
_graphIntegrateWarning = "⚠ graph ABSENT and `gsd-t graph index` FAILED — structural wiring-check cannot run. FIX it (gsd-t graph status).";
|
|
181
|
+
}
|
|
156
182
|
} else {
|
|
157
|
-
_graphIntegrateWarning =
|
|
183
|
+
_graphIntegrateWarning = `⚠ graph BROKEN (reason=${wiEnv.reason || "?"}) — structural wiring-check skipped. This is NOT merely un-indexed; FIX it (gsd-t graph status).`;
|
|
158
184
|
}
|
|
159
|
-
|
|
160
|
-
} else {
|
|
185
|
+
} else if (wiEnv.ok !== true) {
|
|
161
186
|
_graphIntegrateWarning = `⚠ graph who-imports query unexpected envelope (reason: ${wiEnv.reason || "?"}); structural wiring-check skipped`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (wiEnv.ok === true) {
|
|
190
|
+
_graphWhoImportsSlice = wiEnv;
|
|
191
|
+
log(`M94 graph who-imports: ${(wiEnv.results || []).length} result(s) (tier: ${wiEnv.tier || "?"})`);
|
|
192
|
+
} else {
|
|
162
193
|
log(`M94 graph who-imports: ${_graphIntegrateWarning}`);
|
|
163
194
|
}
|
|
164
195
|
|
|
@@ -1309,12 +1309,16 @@ const ARCHITECT_SCHEMA = {
|
|
|
1309
1309
|
additionalProperties: true,
|
|
1310
1310
|
properties: {
|
|
1311
1311
|
key: { type: "string", description: "short id, e.g. R1" },
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1312
|
+
// Short by contract, not by request. The first architect run produced
|
|
1313
|
+
// headings like "Doors that check who you are but never which flight
|
|
1314
|
+
// school you belong to" over 250-word explanations; a reader scanning
|
|
1315
|
+
// 28 roots for the one to fix next cannot skim that.
|
|
1316
|
+
name: { type: "string", maxLength: 40, description: "2-5 word noun phrase, e.g. 'Missing tenant scoping'. NOT a sentence." },
|
|
1317
|
+
why: { type: "string", maxLength: 320, description: "ONE sentence naming the shared mistake, plus the file:line citations that prove it. Keep every citation; compress the prose around them." },
|
|
1318
|
+
fix: { type: "string", maxLength: 200, description: "ONE sentence: the single change that closes them all." },
|
|
1315
1319
|
tier: { type: "string", enum: ["EXTREME", "CRITICAL", "HIGH", "MEDIUM", "LOW", "extreme", "critical", "high", "medium", "low"] },
|
|
1316
1320
|
rank: { type: "integer", description: "rank among roots of the same tier, 1 = worst" },
|
|
1317
|
-
rankReason:{ type: "string" },
|
|
1321
|
+
rankReason:{ type: "string", maxLength: 220, description: "ONE sentence: why this root outranks the ones below it in its tier." },
|
|
1318
1322
|
},
|
|
1319
1323
|
},
|
|
1320
1324
|
},
|
|
@@ -1413,7 +1417,8 @@ async function architectPass(findings) {
|
|
|
1413
1417
|
`FINDINGS:`,
|
|
1414
1418
|
listing,
|
|
1415
1419
|
``,
|
|
1416
|
-
`GROUP BY ROOT CAUSE. A root is one underlying cause where ONE fix closes several findings — e.g. many routes missing the same tenant check.
|
|
1420
|
+
`GROUP BY ROOT CAUSE. A root is one underlying cause where ONE fix closes several findings — e.g. many routes missing the same tenant check. A root with ONE member is not a group; leave it standalone. A root's tier is the tier of its WORST member.`,
|
|
1421
|
+
`WRITE IT SHORT. The name is a 2-5 word noun phrase ("Missing tenant scoping", "Silent write failures"), never a sentence. \`why\` and \`fix\` and \`rankReason\` are ONE sentence each. Someone scanning 28 roots to pick what to fix next cannot read a paragraph per root — but KEEP every file:line citation inside \`why\`, because those are what make the grouping checkable. Compress the prose, never the evidence.`,
|
|
1417
1422
|
`Rank roots within their tier by worst consequence, and members within a root by consequence. RISK order, never the order the findings arrived.`,
|
|
1418
1423
|
``,
|
|
1419
1424
|
`Return JSON per the schema. Every one of the ${chunk.length} findings must appear exactly once in \`placements\` — a finding you drop is a defect nobody will see again.`,
|
|
@@ -1506,6 +1511,8 @@ if (architect) {
|
|
|
1506
1511
|
f._rootKey = p.rootKey;
|
|
1507
1512
|
f._rootName = (root && root.name) || p.rootKey;
|
|
1508
1513
|
f._rootFix = root && root.fix;
|
|
1514
|
+
f._rootWhy = root && root.why;
|
|
1515
|
+
f._rootRankReason = root && root.rankReason;
|
|
1509
1516
|
f._rootRank = (root && Number.isInteger(root.rank)) ? root.rank : 99;
|
|
1510
1517
|
f._rankInRoot = Number.isInteger(p.rankInRoot) ? p.rankInRoot : 99;
|
|
1511
1518
|
}
|
|
@@ -1610,17 +1617,34 @@ function fmtChunks(today) {
|
|
|
1610
1617
|
|
|
1611
1618
|
const CHUNK_MAX = 30000;
|
|
1612
1619
|
const chunks = [head.join("\n")];
|
|
1613
|
-
let buf = "", n = tdStart, lastSev = null, lastType = null;
|
|
1620
|
+
let buf = "", n = tdStart, lastSev = null, lastType = null, lastRoot = null;
|
|
1614
1621
|
const flush = () => { if (buf) { chunks.push(buf); buf = ""; } };
|
|
1615
1622
|
// Consume the shared `orderedFindings` (severity → type → original-index) so the
|
|
1616
1623
|
// TD numbers assigned here are IDENTICAL to those the consolidation stage references.
|
|
1617
1624
|
for (const { f, t } of orderedFindings) {
|
|
1618
1625
|
let piece = "";
|
|
1619
|
-
if (f.severity !== lastSev) { piece += `\n## ${sevHead[f.severity] || f.severity} Priority\n\n`; lastSev = f.severity; lastType = null; }
|
|
1626
|
+
if (f.severity !== lastSev) { piece += `\n## ${sevHead[f.severity] || f.severity} Priority\n\n`; lastSev = f.severity; lastType = null; lastRoot = null; }
|
|
1620
1627
|
// Type sub-heading uses a bold marker line (NOT `###`) so it never collides with
|
|
1621
1628
|
// the `### TD-N` item headings that downstream tools grep for. ASCII hyphens only
|
|
1622
1629
|
// (M76: no em/en-dashes in fmtChunks literals).
|
|
1623
|
-
|
|
1630
|
+
// Root grouping is the fix unit. When the architect grouped the findings, the
|
|
1631
|
+
// register shows the root and its one-line cause/fix/rank above its members,
|
|
1632
|
+
// because scheduling the members separately produces separate half-fixes of
|
|
1633
|
+
// one defect. Bullets sit in <sub> so the eye lands on the finding titles.
|
|
1634
|
+
// Without an architect result there are no roots, and the old type marker
|
|
1635
|
+
// still separates the sections.
|
|
1636
|
+
if (f._rootKey && f._rootKey !== lastRoot) {
|
|
1637
|
+
piece += `\n### ${f._rootKey} - ${ascii(f._rootName) || f._rootKey}\n\n<sub>\n\n`;
|
|
1638
|
+
if (f._rootWhy) piece += `- **Cause:** ${ascii(f._rootWhy)}\n`;
|
|
1639
|
+
if (f._rootFix) piece += `- **Fix:** ${ascii(f._rootFix)}\n`;
|
|
1640
|
+
if (f._rootRankReason) piece += `- **Rank:** ${ascii(f._rootRankReason)}\n`;
|
|
1641
|
+
piece += `\n</sub>\n\n`;
|
|
1642
|
+
lastRoot = f._rootKey; lastType = null;
|
|
1643
|
+
} else if (!f._rootKey && lastRoot) {
|
|
1644
|
+
piece += `\n### Standalone\n\n`;
|
|
1645
|
+
lastRoot = null; lastType = null;
|
|
1646
|
+
}
|
|
1647
|
+
if (!f._rootKey && t !== lastType) { piece += `**-- ${t} --**\n\n`; lastType = t; }
|
|
1624
1648
|
piece += itemMd(f, n++);
|
|
1625
1649
|
if (buf.length + piece.length > CHUNK_MAX) flush();
|
|
1626
1650
|
buf += piece;
|