@tekyzinc/gsd-t 5.18.10 → 5.19.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.
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gsd-t-graph-use-report.js
4
+ *
5
+ * M117 - A Stop hook. At the end of a turn it compares the structural work the
6
+ * session did against the graph queries it issued, and says so when the two do
7
+ * not match.
8
+ *
9
+ * [RULE] use-report-measures-behaviour-not-code-shape
10
+ * [RULE] use-report-never-blocks-it-reports
11
+ *
12
+ * WHY A SECOND MECHANISM
13
+ * The PreToolUse guard (gsd-t-graph-search-guard.js) blocks structural
14
+ * SEARCHES. It governs tool calls, which is all a pattern-matcher can see.
15
+ * It cannot see a session that read six files end to end and worked out the
16
+ * call graph by eye - there is no grep to catch, and no code shape a static
17
+ * scan could find. That is the same blind spot gsd-t-graph-use-gate.cjs was
18
+ * built for, and for the same reason: only the runtime ledger records what a
19
+ * session actually did.
20
+ *
21
+ * This is the conversational counterpart of that gate. The gate runs inside
22
+ * `gsd-t verify`, so it only ever sees workflow runs; most work is a plain
23
+ * conversation, which nothing was measuring.
24
+ *
25
+ * WHAT IT DOES NOT DO
26
+ * It does not block, and it is not a gate. A Stop hook fires after the work
27
+ * is finished, so blocking there would punish work already done rather than
28
+ * redirect it. It reports - into the ledger always, and to the session when
29
+ * the mismatch is worth saying out loud. Prevention is the guard's job.
30
+ *
31
+ * --- Stdin (Claude Code Stop payload) --------------------------------------
32
+ * { "cwd": "...", "session_id": "...", ... }
33
+ *
34
+ * --- Output ----------------------------------------------------------------
35
+ * Exit 0 always. When there is something to say, one line on stdout.
36
+ *
37
+ * Zero dependencies.
38
+ */
39
+
40
+ "use strict";
41
+
42
+ const fs = require("fs");
43
+ const path = require("path");
44
+
45
+ // How far back a "this turn" window reaches. A turn is minutes, not hours.
46
+ const WINDOW_MS = 30 * 60 * 1000;
47
+
48
+ function quiet() { process.exit(0); }
49
+
50
+ function say(message) {
51
+ process.stdout.write(message + "\n");
52
+ process.exit(0);
53
+ }
54
+
55
+ function ledgerPath(projectDir) {
56
+ const dir = path.join(projectDir, ".gsd-t", "graphDB", "logs");
57
+ if (!fs.existsSync(dir)) return null;
58
+ let names;
59
+ try {
60
+ names = fs.readdirSync(dir).filter((n) => n.startsWith("graph-events-") && n.endsWith(".jsonl"));
61
+ } catch {
62
+ return null;
63
+ }
64
+ if (names.length === 0) return null;
65
+ names.sort();
66
+ return path.join(dir, names[names.length - 1]);
67
+ }
68
+
69
+ /**
70
+ * Read the tail of a file without loading all of it. The graph ledger reaches
71
+ * tens of megabytes, and a Stop hook must not stall a turn.
72
+ */
73
+ function tailLines(file, maxBytes) {
74
+ let fd;
75
+ try {
76
+ fd = fs.openSync(file, "r");
77
+ } catch {
78
+ return null;
79
+ }
80
+ try {
81
+ const size = fs.fstatSync(fd).size;
82
+ const start = size > maxBytes ? size - maxBytes : 0;
83
+ const len = size - start;
84
+ const buf = Buffer.alloc(len);
85
+ fs.readSync(fd, buf, 0, len, start);
86
+ const text = buf.toString("utf8");
87
+ const lines = text.split("\n");
88
+ if (start > 0) lines.shift(); // the first line is probably cut in half
89
+ return lines;
90
+ } catch {
91
+ return null;
92
+ } finally {
93
+ fs.closeSync(fd);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Count what happened inside the window: graph queries issued, and structural
99
+ * searches the guard blocked.
100
+ */
101
+ function readRecentActivity(file, sinceMs) {
102
+ const lines = tailLines(file, 2 * 1024 * 1024);
103
+ if (lines === null) return null;
104
+
105
+ let queries = 0;
106
+ let blockedStructural = 0;
107
+
108
+ for (const line of lines) {
109
+ const trimmed = line.trim();
110
+ if (!trimmed) continue;
111
+ let ev;
112
+ try {
113
+ ev = JSON.parse(trimmed);
114
+ } catch {
115
+ continue; // a half-written line at the tail; the next run reads it whole
116
+ }
117
+ const ts = Date.parse(ev.ts);
118
+ if (!Number.isFinite(ts)) continue;
119
+ if (ts < sinceMs) continue;
120
+
121
+ if (ev.kind === "query") queries++;
122
+ if (ev.kind === "search-blocked") blockedStructural++;
123
+ }
124
+
125
+ return { queries, blockedStructural };
126
+ }
127
+
128
+ function main() {
129
+ let input = "";
130
+ let done = false;
131
+
132
+ process.stdin.setEncoding("utf8");
133
+ process.stdin.on("data", (c) => { input += c; });
134
+ process.stdin.on("end", () => {
135
+ if (done) return;
136
+ done = true;
137
+ report(input);
138
+ });
139
+
140
+ setTimeout(() => {
141
+ if (done) return;
142
+ done = true;
143
+ report(input);
144
+ }, 3000);
145
+ }
146
+
147
+ function report(raw) {
148
+ let payload;
149
+ try {
150
+ payload = JSON.parse(raw);
151
+ } catch {
152
+ quiet();
153
+ return;
154
+ }
155
+
156
+ const projectDir = typeof payload.cwd === "string" ? payload.cwd : process.cwd();
157
+ if (!fs.existsSync(path.join(projectDir, ".gsd-t"))) { quiet(); return; }
158
+
159
+ const file = ledgerPath(projectDir);
160
+ if (file === null) { quiet(); return; }
161
+
162
+ const activity = readRecentActivity(file, Date.now() - WINDOW_MS);
163
+ if (activity === null) { quiet(); return; }
164
+
165
+ // The guard stopped structural searches this turn and the graph was never
166
+ // asked. Something structural was wanted and answered another way.
167
+ if (activity.blockedStructural > 0 && activity.queries === 0) {
168
+ say(
169
+ "[GSD-T GRAPH] " + activity.blockedStructural + " structural search(es) were blocked this " +
170
+ "turn and the graph was never queried. A structural question answered by reading files " +
171
+ "is the blind spot the guard cannot see - ask the graph: gsd-t graph who-calls <symbol>."
172
+ );
173
+ return;
174
+ }
175
+
176
+ quiet();
177
+ }
178
+
179
+ main();
@@ -154,7 +154,7 @@ WHEN creating a worktree directly (git worktree add, isolation: "worktree", etc.
154
154
 
155
155
  **A worktree is NOT usable until it is provisioned (M112).** `git worktree add` brings only what git TRACKS, so every ignored file stays behind — `.env` and its secrets, local settings, and the installed dependencies. The new folder looks complete and is not: tests fail on a missing module and the app cannot reach its database, both reading as broken code rather than a setup gap. `bin/gsd-t-worktree-provision.cjs` runs automatically inside `gsd-t-pick-worktree` and treats the three kinds of ignored file differently — **CARRY** local config and secrets (`.env*`, `.npmrc`, credentials; permissions preserved, so a 600 secret does not become world-readable), **SKIP** per-session `.gsd-t` state, build output and OS junk (copying another session's briefs/heartbeats is how two sessions come to believe they own the same work), and **INSTALL** dependencies from the worktree's own lockfile rather than copying or symlinking (a symlink makes a lockfile change in one tree silently alter the other). Anything it cannot read or copy is REPORTED, and a failed install HALTS — a worktree that came up short says so instead of looking ready. Creating a worktree by hand skips all of this, so prefer `gsd-t-pick-worktree --name <branch>`.
156
156
 
157
- **Naming an existing worktree WALKS YOU INTO IT (M113).** `gsd-t pick-worktree --name <branch>` used to refuse whenever the folder was already there, which refused the ordinary case — your own worktree, from yesterday, nobody in it — and left no way back except quitting the session and starting one by hand. It now ENTERS a worktree git confirms as this repo's, on that branch, with no interactive session in it. Two cases still STOP, because each is a way of landing on somebody's uncommitted work: a directory git does not know as that branch's worktree (a stray folder, or another branch's), and a worktree an interactive session already occupies (the M105 collision). `gsd-t pick-worktree --list` prints one line per worktree as `free|busy<TAB><path>`, so the launcher can show what exists before asking for a name.
157
+ **Naming an existing worktree WALKS YOU INTO IT (M113).** `gsd-t pick-worktree --name <branch>` used to refuse whenever the folder was already there, which refused the ordinary case — your own worktree, from yesterday, nobody in it — and left no way back except quitting the session and starting one by hand. It now ENTERS a worktree git confirms as this repo's, on that branch, with no interactive session in it. Two cases still STOP, because each is a way of landing on somebody's uncommitted work: a directory git does not know as that branch's worktree (a stray folder, or another branch's), and a worktree an interactive session already occupies (the M105 collision). `gsd-t pick-worktree --list` prints one line per worktree as `free|busy<TAB><path>`, so the launcher can show what exists before asking for a name. **Typing the DEFAULT branch (`main`) means the main checkout FOLDER, even when that checkout sits on a feature branch** — the picker asks git for the remote-declared default (`origin/HEAD`) as well as the current branch, and stays put on either; an existing branch checked out nowhere is checked out into a worktree, never re-created.
158
158
 
159
159
 
160
160
  **The expected-branch rule governs the MAIN checkout only (M112).** A worktree exists precisely to be on its own branch, so `branch-guard` passes there and names the skip; a **detached HEAD in a worktree FAILS**, because commits made with no branch attached are easily lost. The rule is read from the project CLAUDE.md in either shape — a sentence (`Expected branch: main`) or a table row (`| Expected branch | main |`) — and when no rule is declared the check says `NOT CHECKED` rather than returning a bare pass.
@@ -331,6 +331,8 @@ NEED A STRUCTURAL ANSWER? (what imports this, who calls this, what breaks if I c
331
331
 
332
332
  **This governs plain conversational work, not just `/gsd-t-*` commands.** The failure that produced this rule (binvoice, 2026-08-11) was an ordinary session: it reached for the graph, found none, grepped an 827-file project, and nothing objected. Checking the graph's existence before reasoning about code is the first move, not a fallback.
333
333
 
334
+ **ENFORCED, not advised (M117).** A PreToolUse guard on Bash and Grep BLOCKS a search that asks a structural question about code and names the graph query to run instead. Three outcomes, no fourth: structural → blocked; a search over content the graph does not index (`.md`, `.json`, `.sql`, config, prose) → runs, because the graph holds no answer to route to; a search the guard cannot classify → blocked, because guessing "probably text" is how this rule stopped having teeth. **A missing or unbuilt graph BLOCKS too**, with `gsd-t graph index` — allowing grep because the graph is down is the exact fallback that let a project grep its way through 827 files. There is no bypass flag; a project may switch it off only by writing `.gsd-t/graph-search-gate.json` `{"enabled": false}`, which is a recorded decision rather than an absence. A Stop-time report covers what no pattern-matcher can see: a turn that answered a structural question by reading files end to end. Why this was needed: the rule already existed and had three enforcement points, and all three watched paths the work does not take — the Grep-tool and Read-tool hooks never fire in bypass mode, and the runtime use-gate only runs inside verify. The ledger showed two grep events in three months against 34,418 tool-issued queries.
335
+
334
336
  **Absence is a repairable condition, not a stop sign.** When this was checked across the machine, 20 of 27 registered projects had no usable graph — 2 never built, 18 holding a real index at a path the tooling stopped reading after the store moved. Every one of those sessions had been grepping. `gsd-t update-all` now repairs both automatically and reports any it could not.
335
337
 
336
338
  ## Prime Rule