@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.
@@ -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
- const scip = spawnSync('scip-typescript', ['index', '--infer-tsconfig', '--output', out, '.'], {
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) return edge; // still unresolved → stays floor
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
- return { edges: out, resolved };
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 ≥1 edge in this file OR the file has no call edges to resolve (a
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
- const isAccurate = !hadCallEdges || resolved > 0;
513
- const tier = isAccurate ? 'compiler-accurate' : 'tree-sitter-floor';
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: isAccurate,
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: 300000,
4737
+ timeout: 30 * 60 * 1000,
4732
4738
  });
4733
- if (result.status !== 0 && result.status !== null) {
4734
- error("Graph index build failed — see output above");
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 ×5 · yellow-field solo ×2 · team ×8 isolated / ×12 wide | Solo AI minutes × this. Internal to the estimator — never on the sheet. |
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 ×5 · yellow-field solo ×2 · team ×8 isolated / ×12 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).
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.
@@ -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.22.10",
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
- if [ -n "$cwd" ]; then
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=$(ls -t "$sess_dir"/*.jsonl 2>/dev/null | head -1)
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": 5, "yellowfield-solo": 2, "yellowfield-team-isolated": 8, "yellowfield-team-wide": 12 },
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 5 min for a trivial change, 20 min for a typical screen element or endpoint, 1–2¼ hrs for the heaviest pieces.
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 | × 5 |
91
+ | Greenfield, team | × 3 |
92
92
  | Yellow-field (existing app), solo | × 2 |
93
- | Yellow-field, team — isolated change | × 8 |
94
- | Yellow-field, team — big blast radius | × 12 |
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 ×5 · yellow-field solo ×2 · team ×8 isolated / ×12 wide, + task
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`,