@tekyzinc/gsd-t 5.22.10 → 5.23.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 +17 -0
- package/README.md +3 -2
- package/bin/gsd-t-estimate-sheet.cjs +206 -28
- package/bin/gsd-t-graph-edge-extract.cjs +113 -2
- package/bin/gsd-t-graph-exclude.cjs +81 -0
- package/bin/gsd-t-graph-freshness.cjs +5 -1
- package/bin/gsd-t-graph-index.cjs +42 -7
- package/bin/gsd-t-graph-query-cli.cjs +210 -20
- package/bin/gsd-t-graph-scip-upgrade.cjs +92 -12
- package/bin/gsd-t-scip-reader.cjs +29 -2
- package/bin/gsd-t.js +26 -3
- package/commands/gsd-t-estimate-rescale.md +64 -0
- package/commands/gsd-t-estimate.md +2 -2
- package/commands/gsd-t-help.md +6 -0
- package/package.json +1 -1
- package/scripts/gsd-t-graph-search-guard.js +52 -2
- package/scripts/statusline-command.sh +11 -2
- package/templates/CLAUDE-global.md +1 -1
- package/templates/estimate-config.json +2 -2
- package/templates/estimate-sheet-spec.md +27 -7
- package/templates/playbooks/tekyz-estimation-and-prd-playbook.md +2 -2
|
@@ -134,6 +134,14 @@ function _resetScipCache(override) {
|
|
|
134
134
|
* is confirmed present and invocable. Phase-2 will replace the edges with
|
|
135
135
|
* SCIP-derived ones. This is the documented UPGRADE FLOOR behavior.
|
|
136
136
|
*/
|
|
137
|
+
const SCIP_MAX_FILE_BYTES = '64mb';
|
|
138
|
+
const SCIP_HEAP_MB = 8192;
|
|
139
|
+
const SCIP_MISSING_TIER = 'tree-sitter-floor-SCIP-MISSING';
|
|
140
|
+
// A file SCIP indexed but where fewer than this share of its repo-resolvable calls
|
|
141
|
+
// resolved. [RULE] scip-tier-proportional
|
|
142
|
+
const COMPILER_PARTIAL_TIER = 'compiler-partial';
|
|
143
|
+
const COMPILER_ACCURATE_MIN_FRACTION = 0.9;
|
|
144
|
+
|
|
137
145
|
function runScipTypescript(projectRoot, outPath) {
|
|
138
146
|
// M95: emit to a REAL file (outPath) so the index can be READ and call edges
|
|
139
147
|
// resolved — not /dev/null. The old /dev/null path only proved invocability.
|
|
@@ -145,10 +153,22 @@ function runScipTypescript(projectRoot, outPath) {
|
|
|
145
153
|
// --infer-tsconfig: resolve a tsconfig even when one isn't at the repo root
|
|
146
154
|
// (monorepos / nested layouts — e.g. web/tsconfig.json). Without it, projects
|
|
147
155
|
// whose tsconfig lives in a subdir got 0 resolved call edges. [RULE] scip-infer-nested-tsconfig
|
|
148
|
-
|
|
156
|
+
// --max-file-byte-size: scip-typescript SKIPS every file over 1mb by default,
|
|
157
|
+
// silently — no warning, no document in the index. A 50k-line route file
|
|
158
|
+
// (hilo-figma-atos routes-locations.ts, 1.9MB) was never resolved, so every
|
|
159
|
+
// call in it stayed unresolved and who-calls answered []. Raise the cap well
|
|
160
|
+
// past any hand-written source file. [RULE] scip-no-silent-large-file-skip
|
|
161
|
+
// A larger heap keeps a big project from dying mid-run once those files are in.
|
|
162
|
+
const nodeOpts = process.env.NODE_OPTIONS || '';
|
|
163
|
+
const env = /max-old-space-size/.test(nodeOpts)
|
|
164
|
+
? process.env
|
|
165
|
+
: { ...process.env, NODE_OPTIONS: `${nodeOpts} --max-old-space-size=${SCIP_HEAP_MB}`.trim() };
|
|
166
|
+
const scip = spawnSync('scip-typescript',
|
|
167
|
+
['index', '--infer-tsconfig', '--max-file-byte-size', SCIP_MAX_FILE_BYTES, '--output', out, '.'], {
|
|
149
168
|
cwd: projectRoot,
|
|
150
169
|
encoding: 'utf8',
|
|
151
170
|
timeout: 180_000,
|
|
171
|
+
env,
|
|
152
172
|
});
|
|
153
173
|
if (scip.status === 0) return { ok: true, scipPath: out };
|
|
154
174
|
return { ok: false, error: scip.stderr || `exit code ${scip.status}` };
|
|
@@ -314,7 +334,7 @@ function findTsProjectDirs(repoRoot) {
|
|
|
314
334
|
* resolveFileEdges: (relPath, edges) => { edges, resolved: number } }}
|
|
315
335
|
*/
|
|
316
336
|
function buildScipResolver(repoRoot, opts = {}) {
|
|
317
|
-
const { readScipIndex } = require('./gsd-t-scip-reader.cjs');
|
|
337
|
+
const { readScipIndex, scipPositionKey } = require('./gsd-t-scip-reader.cjs');
|
|
318
338
|
const avail = detectScip();
|
|
319
339
|
|
|
320
340
|
// Detect which languages have source present (so we only run the relevant
|
|
@@ -325,10 +345,21 @@ function buildScipResolver(repoRoot, opts = {}) {
|
|
|
325
345
|
// is language-agnostic (Python symbols use the same `name().` descriptor form),
|
|
326
346
|
// so TS and Python refs merge into one fileRefs map keyed by repo-relative path.
|
|
327
347
|
const fileRefs = new Map(); // relPath → [{symbol, funcId, line}]
|
|
348
|
+
const scipDocs = new Set(); // every file any indexer produced a document for
|
|
349
|
+
const defNames = new Set(); // every name SCIP saw DEFINED in the repo — what a call could resolve to
|
|
350
|
+
const occurrencePositions = new Map(); // relPath → Set<positionKey> of every SCIP occurrence
|
|
328
351
|
const ranIndexers = [];
|
|
329
352
|
|
|
330
353
|
function mergeRead(read) {
|
|
331
354
|
if (!read || !read.ok) return;
|
|
355
|
+
if (read.docPaths) for (const d of read.docPaths) scipDocs.add(d);
|
|
356
|
+
if (read.symbolToDef) for (const fid of read.symbolToDef.values()) defNames.add(fid.split('#').pop());
|
|
357
|
+
if (read.occurrencePositions) {
|
|
358
|
+
for (const [f, pos] of read.occurrencePositions) {
|
|
359
|
+
if (occurrencePositions.has(f)) for (const k of pos) occurrencePositions.get(f).add(k);
|
|
360
|
+
else occurrencePositions.set(f, pos);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
332
363
|
for (const [file, refs] of read.fileRefs) {
|
|
333
364
|
if (fileRefs.has(file)) fileRefs.get(file).push(...refs);
|
|
334
365
|
else fileRefs.set(file, refs.slice());
|
|
@@ -383,8 +414,7 @@ function buildScipResolver(repoRoot, opts = {}) {
|
|
|
383
414
|
* resolves to a real funcId, rewrite dst to that funcId.
|
|
384
415
|
*/
|
|
385
416
|
function resolveFileEdges(relPath, edges) {
|
|
386
|
-
const refs = fileRefs.get(relPath);
|
|
387
|
-
if (!refs || !refs.length) return { edges, resolved: 0 };
|
|
417
|
+
const refs = fileRefs.get(relPath) || [];
|
|
388
418
|
|
|
389
419
|
// name → resolved funcId (last writer wins; SCIP refs in this file)
|
|
390
420
|
const nameToFuncId = new Map();
|
|
@@ -394,6 +424,12 @@ function buildScipResolver(repoRoot, opts = {}) {
|
|
|
394
424
|
}
|
|
395
425
|
|
|
396
426
|
let resolved = 0;
|
|
427
|
+
// missed = a call to a name the repo defines, at a position where SCIP put NO
|
|
428
|
+
// symbol — the compiler never looked at it. A call SCIP resolved to a library
|
|
429
|
+
// (drizzle's `text()`) or a local (`const [x, setX] = useState()`) is not a
|
|
430
|
+
// miss: SCIP answered, the answer just is not a repo function.
|
|
431
|
+
const positions = occurrencePositions.get(relPath);
|
|
432
|
+
let missed = 0;
|
|
397
433
|
const out = edges.map((edge) => {
|
|
398
434
|
const dst = edge.target || edge.dst || '';
|
|
399
435
|
const kind = edge.kind;
|
|
@@ -401,12 +437,18 @@ function buildScipResolver(repoRoot, opts = {}) {
|
|
|
401
437
|
if (!isCall || !dst.startsWith('UNRESOLVED#')) return edge;
|
|
402
438
|
const calleeName = dst.slice('UNRESOLVED#'.length);
|
|
403
439
|
const funcId = nameToFuncId.get(calleeName);
|
|
404
|
-
if (!funcId)
|
|
440
|
+
if (!funcId) { // still unresolved → stays floor
|
|
441
|
+
const seen = positions && Number.isInteger(edge.col) && positions.has(scipPositionKey(edge.line - 1, edge.col));
|
|
442
|
+
if (defNames.has(calleeName) && !seen) missed++;
|
|
443
|
+
return edge;
|
|
444
|
+
}
|
|
405
445
|
resolved++;
|
|
406
446
|
// rewrite dst to the resolved funcId, mark scip-derived
|
|
407
447
|
return { ...edge, target: funcId, dst: funcId, scipResolved: true };
|
|
408
448
|
});
|
|
409
|
-
|
|
449
|
+
// resolvable = resolved + missed. A call to a library (`c.json`) or a local
|
|
450
|
+
// never counts against the file. [RULE] scip-tier-proportional
|
|
451
|
+
return { edges: out, resolved, resolvable: resolved + missed };
|
|
410
452
|
}
|
|
411
453
|
|
|
412
454
|
// tsProjects reports which tsconfig projects actually contributed refs. A
|
|
@@ -419,6 +461,10 @@ function buildScipResolver(repoRoot, opts = {}) {
|
|
|
419
461
|
indexedFiles: fileRefs.size,
|
|
420
462
|
scipPath: resolveScipPath('index.scip', repoRoot),
|
|
421
463
|
resolveFileEdges,
|
|
464
|
+
// [RULE] scip-missing-file-detected-never-silent — lets the upgrader tell a
|
|
465
|
+
// file the indexer never produced a document for from one it indexed.
|
|
466
|
+
coversLanguage: (lang) => ranIndexers.includes(lang),
|
|
467
|
+
hasScipDoc: (relPath) => scipDocs.has(relPath),
|
|
422
468
|
};
|
|
423
469
|
}
|
|
424
470
|
|
|
@@ -492,8 +538,17 @@ function tryScipUpgrade(absPath, relPath, entities, edges, options) {
|
|
|
492
538
|
return { upgraded: false, tier, entities, edges };
|
|
493
539
|
}
|
|
494
540
|
|
|
541
|
+
// The indexer ran for this language but produced no document for this file
|
|
542
|
+
// (skipped for size, outside every tsconfig, or dropped mid-run). Its call
|
|
543
|
+
// targets are unknown — say so in the tier, never label it plain floor or,
|
|
544
|
+
// worse, compiler-accurate. [RULE] scip-missing-file-detected-never-silent
|
|
545
|
+
if (typeof resolver.hasScipDoc === 'function' && typeof resolver.coversLanguage === 'function' &&
|
|
546
|
+
resolver.coversLanguage(lang) && !resolver.hasScipDoc(relPath)) {
|
|
547
|
+
return { upgraded: false, tier: SCIP_MISSING_TIER, entities, edges };
|
|
548
|
+
}
|
|
549
|
+
|
|
495
550
|
// Resolve this file's UNRESOLVED# call edges against the SCIP index.
|
|
496
|
-
const { edges: resolvedEdges, resolved } = resolver.resolveFileEdges(relPath, edges);
|
|
551
|
+
const { edges: resolvedEdges, resolved, resolvable = 0 } = resolver.resolveFileEdges(relPath, edges);
|
|
497
552
|
|
|
498
553
|
// Rust cross-crate edges stay flagged partial.
|
|
499
554
|
// [RULE] rust-cross-crate-flagged-partial
|
|
@@ -505,15 +560,36 @@ function tryScipUpgrade(absPath, relPath, entities, edges, options) {
|
|
|
505
560
|
});
|
|
506
561
|
|
|
507
562
|
// [RULE] scip-tier-honest: label compiler-accurate ONLY when SCIP actually
|
|
508
|
-
// resolved
|
|
509
|
-
// pure-definition file SCIP indexed cleanly). A file whose calls all stayed
|
|
563
|
+
// resolved enough edges (proportionally). A file whose calls all stayed
|
|
510
564
|
// UNRESOLVED is NOT compiler-accurate — it's floor.
|
|
565
|
+
// [RULE] scip-tier-proportional: the fraction of resolvable calls that actually
|
|
566
|
+
// resolved determines the tier. If ≥COMPILER_ACCURATE_MIN_FRACTION resolved,
|
|
567
|
+
// it's accurate; otherwise partial (still better than floor). A file with no
|
|
568
|
+
// resolvable edges (only locals/library calls) that SCIP indexed cleanly
|
|
569
|
+
// is compiler-accurate. [ISSUE] user reported: routes-locations.ts 80% UNRESOLVED
|
|
570
|
+
// yet labeled compiler-accurate because ONE edge resolved.
|
|
511
571
|
const hadCallEdges = edges.some(e => (e.kind === 'call-site' || e.kind === 'CALL'));
|
|
512
|
-
|
|
513
|
-
|
|
572
|
+
let tier = 'tree-sitter-floor';
|
|
573
|
+
if (!hadCallEdges) {
|
|
574
|
+
// Pure-definition file SCIP indexed cleanly → compiler-accurate
|
|
575
|
+
tier = 'compiler-accurate';
|
|
576
|
+
} else if (resolvable > 0) {
|
|
577
|
+
// File has resolvable calls — judge by proportion resolved
|
|
578
|
+
if (resolved / resolvable >= COMPILER_ACCURATE_MIN_FRACTION) {
|
|
579
|
+
// Enough resolvable calls resolved → accurate
|
|
580
|
+
tier = 'compiler-accurate';
|
|
581
|
+
} else if (resolved > 0) {
|
|
582
|
+
// Some but not enough resolvable calls resolved → partial
|
|
583
|
+
tier = COMPILER_PARTIAL_TIER;
|
|
584
|
+
}
|
|
585
|
+
// else: no edges resolved (0/resolvable) → tree-sitter-floor (default)
|
|
586
|
+
}
|
|
587
|
+
// else: no resolvable calls (all external/local) — stay tree-sitter-floor (default)
|
|
588
|
+
|
|
589
|
+
const upgraded = tier === 'compiler-accurate';
|
|
514
590
|
|
|
515
591
|
return {
|
|
516
|
-
upgraded
|
|
592
|
+
upgraded,
|
|
517
593
|
tier,
|
|
518
594
|
entities,
|
|
519
595
|
edges: finalEdges,
|
|
@@ -584,4 +660,8 @@ module.exports = {
|
|
|
584
660
|
_resetScipCache,
|
|
585
661
|
isRustCrossCrateEdge,
|
|
586
662
|
EXT_TO_LANG,
|
|
663
|
+
SCIP_MISSING_TIER,
|
|
664
|
+
SCIP_MAX_FILE_BYTES,
|
|
665
|
+
COMPILER_PARTIAL_TIER,
|
|
666
|
+
COMPILER_ACCURATE_MIN_FRACTION,
|
|
587
667
|
};
|
|
@@ -151,7 +151,8 @@ function isBuildOutputPath(relPath) {
|
|
|
151
151
|
* @param {string} [pathPrefix] repo-relative dir the index was produced from
|
|
152
152
|
* (e.g. "server"); "" or "." for the repo root
|
|
153
153
|
* @returns {{ ok: true, symbolToDef: Map<string,string>,
|
|
154
|
-
* fileRefs: Map<string, Array<{symbol:string, funcId:string, line:number}
|
|
154
|
+
* fileRefs: Map<string, Array<{symbol:string, funcId:string, line:number}>>,
|
|
155
|
+
* docPaths: Set<string> }
|
|
155
156
|
* | { ok: false, reason: string }}
|
|
156
157
|
*/
|
|
157
158
|
function readScipIndex(scipPath, pathPrefix) {
|
|
@@ -177,6 +178,18 @@ function readScipIndex(scipPath, pathPrefix) {
|
|
|
177
178
|
|
|
178
179
|
const symbolToDef = new Map(); // scipSymbol → funcId (relPath#name)
|
|
179
180
|
const fileRefs = new Map(); // relPath → [{symbol, line}]
|
|
181
|
+
// Every file the indexer produced a document for — including files with no
|
|
182
|
+
// resolvable reference, which never appear in fileRefs. Without this set a
|
|
183
|
+
// file the indexer SKIPPED (scip-typescript drops files over its byte-size cap)
|
|
184
|
+
// is indistinguishable from one it indexed and found nothing in.
|
|
185
|
+
// [RULE] scip-missing-file-detected-never-silent
|
|
186
|
+
const docPaths = new Set();
|
|
187
|
+
// Every position (line, column) SCIP put ANY symbol at, per file — external,
|
|
188
|
+
// local, or repo. A call site whose callee sits at one of these was SEEN by the
|
|
189
|
+
// compiler (resolved to something, even if not a repo function); one that
|
|
190
|
+
// does not was never looked at. Numbers, not strings: a large repo has
|
|
191
|
+
// millions of occurrences. [RULE] scip-tier-proportional
|
|
192
|
+
const occurrencePositions = new Map();
|
|
180
193
|
|
|
181
194
|
const docs = obj.documents || [];
|
|
182
195
|
|
|
@@ -185,6 +198,7 @@ function readScipIndex(scipPath, pathPrefix) {
|
|
|
185
198
|
const rawPath = doc.relative_path;
|
|
186
199
|
if (!rawPath || isBuildOutputPath(rawPath)) continue;
|
|
187
200
|
const relPath = reroot(rawPath);
|
|
201
|
+
docPaths.add(relPath);
|
|
188
202
|
for (const occ of doc.occurrences || []) {
|
|
189
203
|
const isDef = (occ.symbol_roles & SYMBOL_ROLE_DEFINITION) !== 0;
|
|
190
204
|
if (!isDef) continue;
|
|
@@ -196,12 +210,19 @@ function readScipIndex(scipPath, pathPrefix) {
|
|
|
196
210
|
}
|
|
197
211
|
|
|
198
212
|
// Second pass: collect every REFERENCE occurrence per file, resolved to the def.
|
|
213
|
+
// A reference is kept whatever scope encloses it — named function, anonymous
|
|
214
|
+
// route handler, or top-level callback. The CALLER identity comes from the
|
|
215
|
+
// tree-sitter floor (which synthesizes one for anonymous scopes); SCIP only
|
|
216
|
+
// resolves the TARGET. [RULE] anonymous-caller-synthesized-never-dropped
|
|
199
217
|
for (const doc of docs) {
|
|
200
218
|
const rawPath = doc.relative_path;
|
|
201
219
|
if (!rawPath || isBuildOutputPath(rawPath)) continue;
|
|
202
220
|
const relPath = reroot(rawPath);
|
|
203
221
|
const refs = [];
|
|
222
|
+
const positions = new Set();
|
|
223
|
+
occurrencePositions.set(relPath, positions);
|
|
204
224
|
for (const occ of doc.occurrences || []) {
|
|
225
|
+
if (Array.isArray(occ.range) && occ.range.length >= 2) positions.add(scipPositionKey(occ.range[0], occ.range[1]));
|
|
205
226
|
const isDef = (occ.symbol_roles & SYMBOL_ROLE_DEFINITION) !== 0;
|
|
206
227
|
if (isDef) continue; // refs only
|
|
207
228
|
const name = funcNameFromSymbol(occ.symbol);
|
|
@@ -214,11 +235,17 @@ function readScipIndex(scipPath, pathPrefix) {
|
|
|
214
235
|
if (refs.length) fileRefs.set(relPath, refs);
|
|
215
236
|
}
|
|
216
237
|
|
|
217
|
-
return { ok: true, symbolToDef, fileRefs };
|
|
238
|
+
return { ok: true, symbolToDef, fileRefs, docPaths, occurrencePositions };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 0-based line + column → one number (columns never reach 1e6). */
|
|
242
|
+
function scipPositionKey(line0, col) {
|
|
243
|
+
return line0 * 1e6 + col;
|
|
218
244
|
}
|
|
219
245
|
|
|
220
246
|
module.exports = {
|
|
221
247
|
loadScipProto,
|
|
222
248
|
funcNameFromSymbol,
|
|
223
249
|
readScipIndex,
|
|
250
|
+
scipPositionKey,
|
|
224
251
|
};
|
package/bin/gsd-t.js
CHANGED
|
@@ -3708,6 +3708,8 @@ const PROJECT_BIN_TOOLS = [
|
|
|
3708
3708
|
// graph dead → grep fallback (found by the Binvoice architect run 2026-07-12).
|
|
3709
3709
|
// Same class as [[project_global_bin_propagation_gap]] / [[project_m96_graph_runs_in_projects]].
|
|
3710
3710
|
"gsd-t-graph-store-resolver.cjs",
|
|
3711
|
+
// Required by the indexer + freshness walker (project graph exclude list).
|
|
3712
|
+
"gsd-t-graph-exclude.cjs",
|
|
3711
3713
|
// M96 — multi-location resolver for the store engine (better-sqlite3), so a
|
|
3712
3714
|
// copied tool finds the engine from the GSD-T global package, not the project's
|
|
3713
3715
|
// own (usually absent) node_modules. Fail-loud with remediation if all miss.
|
|
@@ -4724,14 +4726,22 @@ function doGraphIndex() {
|
|
|
4724
4726
|
heading("GSD-T Graph — Index");
|
|
4725
4727
|
const { spawnSync } = require("child_process");
|
|
4726
4728
|
const idxPath = require("path").join(__dirname, "gsd-t-graph-index.cjs");
|
|
4729
|
+
// A large repo's SCIP run legitimately takes many minutes (hilo-figma-atos: >5).
|
|
4730
|
+
// The old 5-minute cap killed the indexer mid-build and — because a killed child
|
|
4731
|
+
// has status null — reported nothing and exited 0, leaving a half-built graph
|
|
4732
|
+
// that looked complete. A kill or spawn error is a failure, said out loud.
|
|
4727
4733
|
const result = spawnSync(process.execPath, [idxPath, "build", "--repo", process.cwd()], {
|
|
4728
4734
|
encoding: "utf8",
|
|
4729
4735
|
cwd: process.cwd(),
|
|
4730
4736
|
stdio: ["ignore", "inherit", "inherit"],
|
|
4731
|
-
timeout:
|
|
4737
|
+
timeout: 30 * 60 * 1000,
|
|
4732
4738
|
});
|
|
4733
|
-
if (result.
|
|
4734
|
-
|
|
4739
|
+
if (result.error || result.signal || result.status !== 0) {
|
|
4740
|
+
const why = result.error ? result.error.message
|
|
4741
|
+
: result.signal ? `killed by ${result.signal} (timeout 30 min?) — the graph is INCOMPLETE`
|
|
4742
|
+
: `exit ${result.status}`;
|
|
4743
|
+
error(`Graph index build failed: ${why}`);
|
|
4744
|
+
process.exitCode = 1;
|
|
4735
4745
|
}
|
|
4736
4746
|
}
|
|
4737
4747
|
|
|
@@ -4757,6 +4767,19 @@ function doGraphStatus() {
|
|
|
4757
4767
|
}
|
|
4758
4768
|
success(`Graph index: ${envelope.fileCount || 0} files`);
|
|
4759
4769
|
if (envelope.tier) info(`Tier: ${envelope.tier}`);
|
|
4770
|
+
// [RULE] scip-missing-file-detected-never-silent — files the SCIP indexer never
|
|
4771
|
+
// produced a document for: their call edges stay unresolved, so who-calls is blind there.
|
|
4772
|
+
const miss = envelope.scipMissing;
|
|
4773
|
+
if (miss && miss.count > 0) {
|
|
4774
|
+
warn(`${miss.count} file(s) not in SCIP (call edges unresolved): ${miss.files.join(", ")}${miss.count > miss.files.length ? ", …" : ""}`);
|
|
4775
|
+
info("Re-run: gsd-t graph index (then check again)");
|
|
4776
|
+
}
|
|
4777
|
+
const ex = envelope.excludes;
|
|
4778
|
+
if (ex && ex.patterns && ex.patterns.length) {
|
|
4779
|
+
info(`Excluded by ${ex.source}: ${ex.patterns.join(", ")}`);
|
|
4780
|
+
} else {
|
|
4781
|
+
info("Excludes: none (add folders to .gsd-t/graph-exclude.json — { \"exclude\": [\"design/\"] })");
|
|
4782
|
+
}
|
|
4760
4783
|
if (envelope.storeSize !== undefined) info(`Store size: ${envelope.storeSize} bytes`);
|
|
4761
4784
|
if (envelope.detail) log(JSON.stringify(envelope, null, 2));
|
|
4762
4785
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# GSD-T: Estimate Rescale — Re-price an Existing Estimate on the AI-Assisted Scale
|
|
2
|
+
|
|
3
|
+
You are re-estimating an **existing** Tekyz estimate sheet with the AI-assisted sizing model, **without touching the original**. The tool copies the sheet's `T-Shirt Size Estimate` and `Team Mix` tabs into two new tabs — **`T-Shirt Size Estimate (Rescale)`** and **`Team Mix (Rescale)`** — and re-sizes the copies. The original tabs, the Overview tab and the Hilo Estimates Summaries index (which reads the Overview) are never written. `$ARGUMENTS` carries `--sheet <url>` and optionally the project type (`--project <type>`).
|
|
4
|
+
|
|
5
|
+
**THE SHEET IS WRITTEN BY A TOOL, NOT BY HAND.** `gsd-t estimate-sheet rescale` (`bin/gsd-t-estimate-sheet.cjs`, project-local `bin/` first, else the global `gsd-t`) lists the rows, validates your plan, makes the copies, writes the sizes, rebuilds the Team Mix and audits the copies by reading back — halting on any violation. **You never PUT a cell yourself.** Your output is judgment only: a size per row. Spec: `~/.claude/templates/estimate-sheet-spec.md` §1.4 (the sizing model) and §7 (rescale).
|
|
6
|
+
|
|
7
|
+
> **Client-billed work.** Dollar figures here are client deliverables, not GSD-T build cost.
|
|
8
|
+
|
|
9
|
+
## Human-in-the-Loop (SUPERVISED)
|
|
10
|
+
|
|
11
|
+
**Step 2 (sizing) PAUSES for review** before anything is written. Steps 1, 3 and 4 flow but show their result.
|
|
12
|
+
|
|
13
|
+
## Step 1: Inputs + the rows to re-size (MECHANICAL · show result)
|
|
14
|
+
|
|
15
|
+
1. Resolve the sheet from `--sheet <url>`; otherwise ask for the URL. A `403` means the sheet is not shared with the service account (`gsd-t-sheets-writer@ai-estimator-415612.iam.gserviceaccount.com`) — ask the operator to share it as Editor, then re-run.
|
|
16
|
+
2. **Confirm the project type** (spec §1.4) — it sets every row's multiplier:
|
|
17
|
+
|
|
18
|
+
| Project | `--project` | Multiplier |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Greenfield, solo | `greenfield-solo` | × 1 |
|
|
21
|
+
| Greenfield, team | `greenfield-team` | × 3 |
|
|
22
|
+
| Yellow-field (existing app), solo | `yellowfield-solo` | × 2 |
|
|
23
|
+
| Yellow-field, team — isolated change | `yellowfield-team-isolated` | × 5 |
|
|
24
|
+
| Yellow-field, team — big blast radius | `yellowfield-team-wide` | × 7 |
|
|
25
|
+
|
|
26
|
+
A yellow-field team estimate chooses isolated vs wide **per row**, from the code graph of the app being changed (`gsd-t graph blast-radius <file-or-symbol>` in that repo) — not a guess. No graph for the app → say so and ask the operator which rows are wide.
|
|
27
|
+
3. List the rows: `gsd-t estimate-sheet rescale --sheet <url> --list`. It prints every sized item row (row number, module, functionality, requirement, phase, current sizes), the size-column labels, the current legend and the sheet's overhead factor. Show the operator the count and the current Low hours.
|
|
28
|
+
|
|
29
|
+
## Step 2: Re-size every row — JUDGMENT · PAUSE FOR REVIEW
|
|
30
|
+
|
|
31
|
+
Nobody hand-writes code. For each row and each size column:
|
|
32
|
+
|
|
33
|
+
1. Estimate the **solo AI-assisted minutes** — one person directing Claude, greenfield, counting the person's time AND Claude's time. Runway rate (26 h = 13 human + 13 Claude): ~10 min for a trivial change, ~40 min for a typical screen element or endpoint, 2–4½ hrs for the heaviest pieces. Read the Functionality and Low Level Requirements; do not scale the old size mechanically — the old sizes assumed hand-coding.
|
|
34
|
+
2. Run `gsd-t estimate-sheet size --solo-min <n> --project <type> --xxs` — it multiplies, adds task switching after the multiplier, and prints the size. `--xxs` is always on here: the (Rescale) tab carries **XXS (0.5 hr)** so sub-hour work does not round up to XS. Count switching once per row: pass `--switch-min 0` for the row's smaller column.
|
|
35
|
+
3. Build the plan — one entry per listed row, `functionality` copied **exactly** from the list (the tool matches row AND text, and halts on a row that moved):
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{ "items": [ { "row": 15, "functionality": "Route the existing permission resolver into every unguarded surface", "sizes": ["S", "M"] } ] }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`sizes` follows the size-column order the list printed; `""` for a column with no work. Every sized row must be in the plan — a missing row halts, because its copy would silently re-price on the new scale.
|
|
42
|
+
4. Write it to `.gsd-t/estimate-rescale-plan.json` and preview: `gsd-t estimate-sheet rescale --sheet <url> --plan .gsd-t/estimate-rescale-plan.json --dry-run` (old Low hours → new Low hours; nothing written).
|
|
43
|
+
5. **PAUSE:** show the operator the per-row table (row · functionality · solo min · multiplier · size) and the before → after Low hours. Wait for `continue` or corrections.
|
|
44
|
+
|
|
45
|
+
## Step 3: Write the (Rescale) tabs (MECHANICAL · show result)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
gsd-t estimate-sheet rescale --sheet <url> --plan .gsd-t/estimate-rescale-plan.json # add --replace to rebuild existing (Rescale) tabs
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It copies the two tabs next to their originals, puts the AI-assisted scale in the copy's legend (XXS 0.0625 · XS 0.1 · S 0.25 · M 0.5 · L 1 · XL 2 · XXL 4 days; XXS in the row under XXL), switches the copy's Days formulas to an exact size lookup (the template's two-letter prefix would read `XX*` as XXS + XXL), writes the sizes, rebuilds `Team Mix (Rescale)` with the original tab's roster staffed from the copy's phase rollups, and audits the copies. `--suffix "<name>"` writes a separately named pair (`… (<name>)`) instead of `(Rescale)`. Existing (Rescale) tabs halt the run unless `--replace` — and `--replace` deletes only the two (Rescale) tabs. **Exit 4 = a ✗ — fix and re-run. Exit 64 = auth/API/input halt.** Show the tool's output verbatim.
|
|
52
|
+
|
|
53
|
+
## Step 4: Report
|
|
54
|
+
|
|
55
|
+
Sheet URL · project type · rows re-sized · Low hours before → after · the Team Mix (Rescale) roster and months · the audit result. State plainly that the original tabs and the Summary index still show the old figures.
|
|
56
|
+
|
|
57
|
+
## Document Ripple
|
|
58
|
+
|
|
59
|
+
- The Google Sheet (external) — only the two `(Rescale)` tabs are created or rebuilt.
|
|
60
|
+
- `.gsd-t/estimate-rescale-plan.json` — the sizing judgment, kept so the re-estimate is reproducible.
|
|
61
|
+
|
|
62
|
+
## ▶ Next Up
|
|
63
|
+
|
|
64
|
+
Standalone command — no auto-successor.
|
|
@@ -26,7 +26,7 @@ Read from `$ARGUMENTS` or `.gsd-t/estimate-config.json` if present; otherwise us
|
|
|
26
26
|
| `rate` | `$50/hr` | Blended hourly rate for the LOW figure. |
|
|
27
27
|
| `hoursPerDay` | `8` | Hours per person-day. |
|
|
28
28
|
| `sizeScale` | `XS 0.1 · S 0.25 · M 0.5 · L 1 · XL 2 · XXL 4` | AI-assisted T-shirt → person-days (spec §1.4). `write` puts it in the sheet legend. |
|
|
29
|
-
| `projectMultiplier` | greenfield solo ×1 · team ×
|
|
29
|
+
| `projectMultiplier` | greenfield solo ×1 · team ×3 · yellow-field solo ×2 · team ×5 isolated / ×7 wide | Solo AI minutes × this. Includes team overhead. Internal to the estimator — never on the sheet. |
|
|
30
30
|
| `totalMF` | `0.7` | Overhead multiplier. **The sheet's own MF list (`E4:F9`) wins when a sheet exists** — read it, never overwrite it. Hilo sheets run `0.9` (QA .3 · PM .1 · Analysis .1 · Deployment .05 · StdUps/Mtgs .15 · Buffer .2). |
|
|
31
31
|
| `highFactor` | `1.25` | HIGH = LOW × this (the sheet's `G4` wins when a sheet exists). |
|
|
32
32
|
| `sheetTemplateId` | (blank) | Optional template to clone; normally blank — the operator supplies the target sheet. |
|
|
@@ -58,7 +58,7 @@ Client-facing line-items carry **sequential, rational numbering starting at 1**.
|
|
|
58
58
|
|
|
59
59
|
For each in-scope item build a row per spec §1.2 — `A` Module · `B` User Type · `C` Functionality (**with the item id**) · `D` Low-Level Requirement · `E` Phase · `F` Web Portal size · `G` Backend/API size. `H:L` are formulas, never values.
|
|
60
60
|
|
|
61
|
-
- **Size in solo AI minutes, then let the tool pick the size** (spec §1.4). For each column (FE, BE) estimate the SOLO AI-assisted minutes — one person directing Claude — then run `gsd-t estimate-sheet size --solo-min <n> --project <type>`: it multiplies by the project type (greenfield solo ×1 · team ×
|
|
61
|
+
- **Size in solo AI minutes, then let the tool pick the size** (spec §1.4). For each column (FE, BE) estimate the SOLO AI-assisted minutes — one person directing Claude, counting the person's time AND Claude's time (Runway: ~10 min trivial · ~40 min typical · 2–4½ hr heaviest) — then run `gsd-t estimate-sheet size --solo-min <n> --project <type>`: it multiplies by the project type (greenfield solo ×1 · team ×3 · yellow-field solo ×2 · team ×5 isolated / ×7 big blast radius), adds task switching after the multiplier, and prints the size. Blast radius is measured with `gsd-t graph blast-radius`, not guessed. Count switching once per item (pass `--switch-min 0` for the smaller column).
|
|
62
62
|
- **Bare codes in `F:G`** — `XS` `S` `M` `L` `XL` `XXL`. Never the legend text (`"XS - Extra Small"`). Scale: **XS 0.1 · S 0.25 · M 0.5 · L 1 · XL 2 · XXL 4** person-days.
|
|
63
63
|
- The sheet computes: `Days = F+G` → `MFactor Days = Days × Total MF` → `Total Days` → `LOW $ = Total × 8 × rate` → `HIGH $ = LOW × high factor`. The overhead factors and high factor are per-project settings the operator adjusts by hand.
|
|
64
64
|
- **Cluster by fix-shape to size fast**: "add existing guard to N routes" (XS–S, repeated) vs "new backend surface" (M, +FE) vs "config / single route" (XS). Size the cluster once, apply to members.
|
package/commands/gsd-t-help.md
CHANGED
|
@@ -383,6 +383,12 @@ Use these when user asks for help on a specific command:
|
|
|
383
383
|
- **Updates**: the Tekyz estimate Google Sheet (three tabs, written and read-back-audited by `gsd-t estimate-sheet` from `.gsd-t/estimate-plan.json`) + optional `share/<Repo>-estimate-redteam-notes.md` (and, if renumbered, the source doc/docs/scan files)
|
|
384
384
|
- **Use when**: You need a client-facing paid estimate (T-shirt sizing, dollar range, staffed team by month) from a scan, a gap analysis, or a requirements/feature/app spec. **SUPERVISED** — judgment phases (sizing, adjustments, Team Mix, Red Team) pause for your review; **you are the final arbiter** of an Estimate Red Team that challenges the numbers. Accepts `--sheet <url>`. Sizes are AI-assisted (solo AI minutes × project multiplier + task switching → `gsd-t estimate-sheet size`; spec §1.4). Rate + factors are parameterized (default Tekyz; the sheet's own MF list wins). Playbook: `~/.claude/playbooks/tekyz-estimation-and-prd-playbook.md`
|
|
385
385
|
|
|
386
|
+
### estimate-rescale
|
|
387
|
+
- **Summary**: Re-price an EXISTING Tekyz estimate sheet on the AI-assisted scale without touching it — copies the T-Shirt and Team Mix tabs into `T-Shirt Size Estimate (Rescale)` / `Team Mix (Rescale)`, re-sizes every row (solo AI minutes × project multiplier + task switching, with an XXS 0.5 hr size), rebuilds the Team Mix and audits the copies
|
|
388
|
+
- **Auto-invoked**: No
|
|
389
|
+
- **Updates**: the two `(Rescale)` tabs on the sheet (original tabs, Overview and the estimates index are never written) + `.gsd-t/estimate-rescale-plan.json`
|
|
390
|
+
- **Use when**: An estimate was sized on the old hand-coding day scale and you want the AI-assisted figure beside it. **SUPERVISED** — the per-row sizing pauses for your review. Accepts `--sheet <url>` and `--project <greenfield-solo|greenfield-team|yellowfield-solo|yellowfield-team-isolated|yellowfield-team-wide>`. Spec: `~/.claude/templates/estimate-sheet-spec.md` §1.4 and §7
|
|
391
|
+
|
|
386
392
|
### stories
|
|
387
393
|
- **Summary**: Generate a dev-team handoff document in the Tekyz user-stories format — discrete user stories with workflows, grouped acceptance criteria, per-story flow diagrams (Mermaid rendered to embedded images), and mapped test-case tables — from any source (scan register, requirements doc, design contract, or a reverse-engineered codebase)
|
|
388
394
|
- **Auto-invoked**: No
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.23.10",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code \u2014 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",
|
|
@@ -326,6 +326,56 @@ function buildUnclearReason(found, cls) {
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
|
|
329
|
+
// --- A path forward after an incomplete, empty graph answer ----------------
|
|
330
|
+
//
|
|
331
|
+
// When the graph just answered [] and said its answer was incomplete, blocking
|
|
332
|
+
// the grep with "ask the graph" sends the caller back to the answer that failed
|
|
333
|
+
// them - a dead end. The query CLI records that answer; this names what IS
|
|
334
|
+
// allowed next: open the files holding the unresolved call sites (Read is not
|
|
335
|
+
// blocked), and re-index so SCIP can resolve them. This is a message, not a
|
|
336
|
+
// bypass - the search itself stays blocked.
|
|
337
|
+
// [RULE] incomplete-empty-answer-names-a-path-forward
|
|
338
|
+
|
|
339
|
+
const INCOMPLETE_MARKER_MAX_AGE_MS = 30 * 60 * 1000;
|
|
340
|
+
|
|
341
|
+
/** Throws when the marker exists but cannot be read - the caller denies with that. */
|
|
342
|
+
function incompleteAnswerNote(projectDir) {
|
|
343
|
+
const file = path.join(projectDir, ".gsd-t", "graphDB", "last-incomplete-answer.json");
|
|
344
|
+
if (!fs.existsSync(file)) return "";
|
|
345
|
+
const m = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
346
|
+
if (!m || Date.now() - Date.parse(m.ts) > INCOMPLETE_MARKER_MAX_AGE_MS) return "";
|
|
347
|
+
const lines = [
|
|
348
|
+
"",
|
|
349
|
+
"",
|
|
350
|
+
"The graph's last answer (" + m.verb + " " + m.target + ") was EMPTY and marked incomplete:",
|
|
351
|
+
" " + (m.note || "some call edges are unresolved"),
|
|
352
|
+
"",
|
|
353
|
+
"Allowed path forward:",
|
|
354
|
+
];
|
|
355
|
+
const sites = m.unresolvedCallSites;
|
|
356
|
+
if (sites && Array.isArray(sites.files) && sites.files.length) {
|
|
357
|
+
lines.push(" 1. Open these files with the Read tool - they hold " + sites.count +
|
|
358
|
+
" unresolved call site(s) naming the target:");
|
|
359
|
+
for (const f of sites.files.slice(0, 10)) lines.push(" " + f);
|
|
360
|
+
if (sites.files.length > 10) lines.push(" ... (" + sites.files.length + " files; full list in the query's coverage.unresolvedCallSites)");
|
|
361
|
+
} else {
|
|
362
|
+
lines.push(" 1. gsd-t graph body <symbol> - read the definition, then Read the files that import its module");
|
|
363
|
+
lines.push(" (gsd-t graph who-imports <file>)");
|
|
364
|
+
}
|
|
365
|
+
lines.push(" 2. gsd-t graph status - lists files missing from SCIP; gsd-t graph index re-resolves them");
|
|
366
|
+
return lines.join("\n");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The note, or a deny naming why the marker could not be read. */
|
|
370
|
+
function pathForwardOrDeny(projectDir) {
|
|
371
|
+
try {
|
|
372
|
+
return incompleteAnswerNote(projectDir);
|
|
373
|
+
} catch (e) {
|
|
374
|
+
deny("The graph's last incomplete-answer record (.gsd-t/graphDB/last-incomplete-answer.json) " +
|
|
375
|
+
"could not be read: " + e.message + "\n\nDelete it and re-run the graph query.");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
329
379
|
// --- Recording the decision ------------------------------------------------
|
|
330
380
|
//
|
|
331
381
|
// One line per block, into the same ledger the graph's own tooling writes. The
|
|
@@ -473,13 +523,13 @@ function decide(raw) {
|
|
|
473
523
|
|
|
474
524
|
if (cls.verdict === "structural") {
|
|
475
525
|
recordBlock(projectDir, f.program, f.pattern, "structural");
|
|
476
|
-
if (hasGraph) deny(buildStructuralReason(f, cls));
|
|
526
|
+
if (hasGraph) deny(buildStructuralReason(f, cls) + pathForwardOrDeny(projectDir));
|
|
477
527
|
else deny(buildNoGraphReason(f, cls));
|
|
478
528
|
return;
|
|
479
529
|
}
|
|
480
530
|
if (cls.verdict === "unclear") {
|
|
481
531
|
recordBlock(projectDir, f.program, f.pattern, "unclear");
|
|
482
|
-
deny(buildUnclearReason(f, cls));
|
|
532
|
+
deny(buildUnclearReason(f, cls) + (hasGraph ? pathForwardOrDeny(projectDir) : ""));
|
|
483
533
|
return;
|
|
484
534
|
}
|
|
485
535
|
}
|
|
@@ -66,11 +66,20 @@ model=$(printf '%s' "$input" | jq -r '.model.id // ""')
|
|
|
66
66
|
# Computed as input_tokens + cache_creation_input_tokens +
|
|
67
67
|
# cache_read_input_tokens to capture the full window pressure. ---
|
|
68
68
|
ctx_left=""
|
|
69
|
-
|
|
69
|
+
# Claude Code passes this session's transcript path directly. Prefer it: the
|
|
70
|
+
# cwd-derived slug below misses whenever the session's working folder is a
|
|
71
|
+
# subfolder (or contains spaces), which silently dropped the ctx field.
|
|
72
|
+
transcript=$(printf '%s' "$input" | jq -r '.transcript_path // ""')
|
|
73
|
+
if [ -n "$transcript" ] && [ -f "$transcript" ]; then
|
|
74
|
+
sess_dir=$(dirname "$transcript")
|
|
75
|
+
elif [ -n "$cwd" ]; then
|
|
70
76
|
proj_slug=$(printf '%s' "$cwd" | sed 's:/:-:g')
|
|
71
77
|
sess_dir="$HOME/.claude/projects/$proj_slug"
|
|
78
|
+
fi
|
|
79
|
+
if [ -n "$sess_dir" ]; then
|
|
72
80
|
if [ -d "$sess_dir" ]; then
|
|
73
|
-
latest_jsonl=$
|
|
81
|
+
latest_jsonl=$transcript
|
|
82
|
+
[ -f "$latest_jsonl" ] || latest_jsonl=$(ls -t "$sess_dir"/*.jsonl 2>/dev/null | head -1)
|
|
74
83
|
if [ -n "$latest_jsonl" ]; then
|
|
75
84
|
# Window size by model family. Haiku = 200k; everything else = 1M.
|
|
76
85
|
case "$model" in
|
|
@@ -625,7 +625,7 @@ Add `**Also available:**` with `- /gsd-t-{alt} — {desc}` lines if alternatives
|
|
|
625
625
|
| `setup` | `status` | |
|
|
626
626
|
| `design-decompose` | `design-build` | `partition` (if domains needed first) |
|
|
627
627
|
|
|
628
|
-
Commands with no successor (standalone): `quick`, `debug`, `brainstorm`, `status`, `help`, `resume`, `prompt`, `log`, `health`, `pause`, `estimate`, `stories`, backlog commands.
|
|
628
|
+
Commands with no successor (standalone): `quick`, `debug`, `brainstorm`, `status`, `help`, `resume`, `prompt`, `log`, `health`, `pause`, `estimate`, `estimate-rescale`, `stories`, backlog commands.
|
|
629
629
|
|
|
630
630
|
Skip the hint if auto-advancing (Level 3 mid-wave) — only show when the user needs to manually invoke the next step.
|
|
631
631
|
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"sizeScale": { "XS": 0.1, "S": 0.25, "M": 0.5, "L": 1, "XL": 2, "XXL": 4 },
|
|
11
11
|
"_sizeScale": "AI-assisted T-shirt scale, person-days (written into the sheet legend by `gsd-t estimate-sheet write`). A size = solo AI minutes x project multiplier + task switching; see estimate-sheet-spec.md section 1.4.",
|
|
12
12
|
|
|
13
|
-
"projectMultiplier": { "greenfield-solo": 1, "greenfield-team":
|
|
14
|
-
"_projectMultiplier": "Solo AI minutes are multiplied by this, by project type (isolated vs wide = code-graph blast radius). Internal to the estimator; never shown on the sheet.",
|
|
13
|
+
"projectMultiplier": { "greenfield-solo": 1, "greenfield-team": 3, "yellowfield-solo": 2, "yellowfield-team-isolated": 5, "yellowfield-team-wide": 7 },
|
|
14
|
+
"_projectMultiplier": "Solo AI minutes are multiplied by this, by project type (isolated vs wide = code-graph blast radius). Internal to the estimator; never shown on the sheet. Includes team overhead — set the sheet overhead factor accordingly (David, 2026-09-28).",
|
|
15
15
|
|
|
16
16
|
"totalMF": 0.7,
|
|
17
17
|
"_totalMF": "Overhead multiplier applied to raw Days. Tekyz default 0.7 = QA 0.3 + PM 0.1 + Analysis 0.05 + Deployment 0.05 + Buffer 0.2. Raise Buffer/QA when confidence is low.",
|
|
@@ -80,20 +80,20 @@ Column widths: `[150, 120, 300, 430, 122, 90, 90, 61, 53, 76, 81, 81]`.
|
|
|
80
80
|
|
|
81
81
|
#### 1.4 How a size is chosen — the AI-assisted model (David, 2026-09-27)
|
|
82
82
|
|
|
83
|
-
Nobody hand-writes code. A size is the **team hours** a task takes with AI-assisted development, and the multipliers that produce it live in the estimator — never on the sheet. Calibration: the Hilo Delivery Runway build (45 tasks, ~26 solo hours: 13 David + 13 Claude).
|
|
83
|
+
Nobody hand-writes code. A size is the **team hours** a task takes with AI-assisted development, and the multipliers that produce it live in the estimator — never on the sheet. Calibration: the Hilo Delivery Runway build (45 tasks, ~26 solo hours: 13 David + 13 Claude — solo minutes count both).
|
|
84
84
|
|
|
85
|
-
1. **Estimate the task in SOLO AI-assisted minutes** — one person directing Claude, greenfield. Runway rate: roughly
|
|
85
|
+
1. **Estimate the task in SOLO AI-assisted minutes** — one person directing Claude, greenfield, counting **the person's time AND Claude's time** (David, 2026-09-29). Runway rate: roughly 10 min for a trivial change, 40 min for a typical screen element or endpoint, 2–4½ hrs for the heaviest pieces.
|
|
86
86
|
2. **Multiply by the project type:**
|
|
87
87
|
|
|
88
88
|
| Project | Multiplier |
|
|
89
89
|
|---|---|
|
|
90
90
|
| Greenfield, solo | × 1 |
|
|
91
|
-
| Greenfield, team | ×
|
|
91
|
+
| Greenfield, team | × 3 |
|
|
92
92
|
| Yellow-field (existing app), solo | × 2 |
|
|
93
|
-
| Yellow-field, team — isolated change | ×
|
|
94
|
-
| Yellow-field, team — big blast radius | ×
|
|
93
|
+
| Yellow-field, team — isolated change | × 5 |
|
|
94
|
+
| Yellow-field, team — big blast radius | × 7 |
|
|
95
95
|
|
|
96
|
-
Blast radius comes from the code graph (`gsd-t graph blast-radius`), not a guess.
|
|
96
|
+
The multipliers include team overhead (reviews, QA, coordination) — do not also charge it through the overhead factors (David, 2026-09-28). Blast radius comes from the code graph (`gsd-t graph blast-radius`), not a guess.
|
|
97
97
|
3. **Add task switching AFTER the multiplier** — it is one person's pickup time and does not grow with team size: 5–10 min for small tasks (less when related tasks run back-to-back), ~15 min medium, up to 30 min large.
|
|
98
98
|
4. **Pick the nearest size** on the §1.1 legend. `gsd-t estimate-sheet size --solo-min <n> --project <type> [--switch-min <n>]` does steps 2–4 and prints the size.
|
|
99
99
|
|
|
@@ -273,7 +273,8 @@ Everything in §1–§5 is executed by `bin/gsd-t-estimate-sheet.cjs`, not re-de
|
|
|
273
273
|
|
|
274
274
|
```
|
|
275
275
|
gsd-t estimate-sheet plan-schema # the plan shape
|
|
276
|
-
gsd-t estimate-sheet size --solo-min <n> --project <type> [--switch-min <n>] # §1.4: solo minutes → team hours → size (no sheet needed)
|
|
276
|
+
gsd-t estimate-sheet size --solo-min <n> --project <type> [--switch-min <n>] [--xxs] # §1.4: solo minutes → team hours → size (no sheet needed); --xxs for (Rescale) tabs
|
|
277
|
+
gsd-t estimate-sheet rescale --sheet <id|url> (--list | --plan <p.json> [--dry-run] [--replace] [--suffix <name>]) # §7: re-price into (Rescale) copies; originals untouched
|
|
277
278
|
gsd-t estimate-sheet read --sheet <id|url> [--tab <name>] # read-before-write dump
|
|
278
279
|
gsd-t estimate-sheet plan-check --sheet <id|url> --plan plan.json # validate + the roster it WOULD write (the Step 4 pause)
|
|
279
280
|
gsd-t estimate-sheet write --sheet <id|url> --plan plan.json [--replace] # T-Shirt + Team Mix + Tech Stack, then audit
|
|
@@ -309,3 +310,22 @@ The plan (judgment only):
|
|
|
309
310
|
- `tshirt.mode` `items` writes whole rows below the header (halts if rows exist unless `--replace`); `sizes` fills `E:L` on rows that already exist (a gap-analysis sheet), matched by the `(id)` suffix in column C — never by position.
|
|
310
311
|
- `teamMix.fte` is per-discipline FTE (`backend` `frontend` `qa` `pm` `ba` `devops` `techlead` `design` `mobile`). The tool splits it into people (saturate then spill), computes months and the column count, ramps by discipline, writes the remainder formula, and refuses a roster that leaves a weighted MF factor unstaffed. It writes **one grid per phase with hours** (§2.6); `teamMix.phases: { "Phase 1": { "fte": {…} } }` overrides the mix for one phase.
|
|
311
312
|
- The MF list, rate and high factor are READ from the sheet; the plan never carries them. The legend VALUES are written by the tool (the AI-assisted scale, §1.4) — in `sizes` mode it HALTS if a sized row on the tab is missing from the plan, because moving the legend would silently re-price that row.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## 7. Rescale — re-price an existing estimate into copied tabs (`/gsd-t-estimate-rescale`)
|
|
317
|
+
|
|
318
|
+
An estimate written on the old day scale is re-priced **without touching it**. `gsd-t estimate-sheet rescale` copies `T-Shirt Size Estimate` → **`T-Shirt Size Estimate (Rescale)`** and `Team Mix` → **`Team Mix (Rescale)`** (each placed next to its original) and re-sizes the copies only. The original tabs, the Overview tab and the estimates index are never written.
|
|
319
|
+
|
|
320
|
+
| Step | Rule |
|
|
321
|
+
|---|---|
|
|
322
|
+
| List | `rescale --list` prints every sized item row (row, text, phase, sizes), the size-column labels, legend and MF. |
|
|
323
|
+
| Plan | `{ "items": [ { "row", "functionality", "sizes": [...] } ] }` — every sized row exactly once, matched by row AND exact Functionality text (a moved row HALTS); sizes in size-column order. A missing row HALTS — its copy would silently re-price. |
|
|
324
|
+
| Legend | The copy gets the AI scale plus **XXS 0.0625 d (0.5 hr)** in the row directly under XXL (HALTS if that row is not empty). XXS exists on (Rescale) tabs only. |
|
|
325
|
+
| Formulas | With XXS present the copy's Days column uses an **exact** lookup — `SUMIF(legend, F{r}&" -*", values)` — because the template's `LEFT(F{r},2)&"*"` reads `XX*` as XXS + XXL. The audit expects the exact form whenever the legend carries XXS. |
|
|
326
|
+
| Team Mix | `Team Mix (Rescale)` keeps the original tab's roster (derived as `teammix` does) and staffs the copy's phase rollups (midpoint of Low/High, §2.2). |
|
|
327
|
+
| Formatting | The copy is re-wrapped (C/D) and top-aligned so it passes §5 on its own. |
|
|
328
|
+
| Overhead | The COPY's overhead factors are zeroed by default (the multipliers already include team overhead); `"noOverhead": false` in the plan keeps them. The original tab's factors are never written. |
|
|
329
|
+
| Re-run | Existing (Rescale) tabs HALT the run; `--replace` rebuilds them **in place** — the T-Shirt copy is cleared and re-copied from the original (values, formulas, formats, validation, widths) and the Team Mix copy is cleared and rewritten, so both tabs keep their identity. They are never deleted and re-created: other sheets import them by name (IMPORTRANGE), and a view that refreshes while a tab is missing caches `#REF!`. `--dry-run` prints old → new Low hours and writes nothing. |
|
|
330
|
+
| Audit | §5 T-Shirt + Team Mix checks run on the (Rescale) tabs by read-back; exit 4 on any ✗. |
|
|
331
|
+
| Second copy | `--suffix "AI v2"` writes `T-Shirt Size Estimate (AI v2)` + `Team Mix (AI v2)` instead, so a second re-estimate never overwrites the `(Rescale)` tabs other sheets import by name. Plain text only (no parentheses, quotes or `!`). |
|
|
@@ -51,8 +51,8 @@ For each finding, write a row (cols A–G; leave H–L formulas alone):
|
|
|
51
51
|
`D` Low-Level Requirement · `E` Phase (MVP) · `F` Web Portal size · `G` Backend/API size.
|
|
52
52
|
|
|
53
53
|
- Size **each column independently** (FE and BE), **AI-assisted** — nobody hand-writes code.
|
|
54
|
-
Estimate SOLO AI minutes, then `gsd-t estimate-sheet size --solo-min <n> --project <type>`
|
|
55
|
-
(greenfield solo ×1 · team ×
|
|
54
|
+
Estimate SOLO AI minutes (human + Claude time; ~10 / 40 min / 2–4½ hr), then `gsd-t estimate-sheet size --solo-min <n> --project <type>`
|
|
55
|
+
(greenfield solo ×1 · team ×3 · yellow-field solo ×2 · team ×5 isolated / ×7 wide, + task
|
|
56
56
|
switching after the multiplier) picks the size. Sizes: XS .1, S .25, M .5, L 1, XL 2, XXL 4
|
|
57
57
|
(estimate-sheet-spec.md §1.4).
|
|
58
58
|
- Sheet computes: `Days = F+G`, `MFactor = Days×MF`, `Total = Days+MFactor`,
|