@tekyzinc/gsd-t 4.9.13 → 4.10.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/bin/gsd-t-competition-judge.cjs +7 -1
- package/bin/gsd-t-context-brief-kinds/impact.cjs +91 -4
- package/bin/gsd-t-context-brief-kinds/partition.cjs +95 -0
- package/bin/gsd-t-context-brief-kinds/plan.cjs +110 -4
- package/bin/gsd-t-file-disjointness.cjs +319 -7
- package/bin/gsd-t-graph-anti-grep-lint.cjs +515 -0
- package/bin/gsd-t-graph-edge-extract.cjs +612 -0
- package/bin/gsd-t-graph-freshness.cjs +506 -0
- package/bin/gsd-t-graph-index.cjs +540 -0
- package/bin/gsd-t-graph-k1-sqlite-stream.cjs +251 -0
- package/bin/gsd-t-graph-query-cli.cjs +1182 -0
- package/bin/gsd-t-graph-scip-upgrade.cjs +440 -0
- package/bin/gsd-t-graph-store-bakeoff.cjs +889 -0
- package/bin/gsd-t-graph-synthetic-gen.cjs +304 -0
- package/bin/gsd-t-graph-ts-throughput.cjs +587 -0
- package/bin/gsd-t-scip-reader.cjs +167 -0
- package/bin/gsd-t.js +166 -48
- package/commands/gsd-t-debug.md +10 -0
- package/commands/gsd-t-design-build.md +10 -0
- package/commands/gsd-t-execute.md +15 -0
- package/commands/gsd-t-feature.md +15 -7
- package/commands/gsd-t-gap-analysis.md +17 -7
- package/commands/gsd-t-impact.md +25 -0
- package/commands/gsd-t-integrate.md +25 -0
- package/commands/gsd-t-partition.md +18 -0
- package/commands/gsd-t-plan.md +16 -0
- package/commands/gsd-t-populate.md +16 -5
- package/commands/gsd-t-prd.md +18 -0
- package/commands/gsd-t-project.md +19 -0
- package/commands/gsd-t-promote-debt.md +16 -5
- package/commands/gsd-t-qa.md +20 -8
- package/commands/gsd-t-quick.md +10 -0
- package/commands/gsd-t-scan.md +21 -3
- package/commands/gsd-t-test-sync.md +10 -0
- package/commands/gsd-t-verify.md +25 -0
- package/commands/gsd-t-wave.md +8 -0
- package/package.json +10 -2
- package/templates/workflows/gsd-t-debug.workflow.js +81 -0
- package/templates/workflows/gsd-t-integrate.workflow.js +47 -1
- package/templates/workflows/gsd-t-phase.workflow.js +99 -1
- package/templates/workflows/gsd-t-quick.workflow.js +64 -0
- package/templates/workflows/gsd-t-scan.workflow.js +200 -9
- package/templates/workflows/gsd-t-verify.workflow.js +50 -1
- package/bin/graph-cgc.js +0 -510
- package/bin/graph-indexer.js +0 -147
- package/bin/graph-overlay.js +0 -195
- package/bin/graph-parsers.js +0 -327
- package/bin/graph-query.js +0 -453
- package/bin/graph-store.js +0 -154
|
@@ -102,7 +102,21 @@ async function runCli(projectDir, subcmd, argv, localBin, label, parseJson = tru
|
|
|
102
102
|
].join("\n");
|
|
103
103
|
const opts = { label, schema: _CLI_ENVELOPE_SCHEMA, model: "haiku" };
|
|
104
104
|
if (phaseNameOpt) opts.phase = phaseNameOpt;
|
|
105
|
-
|
|
105
|
+
// The CLI is run by a haiku helper agent, not a subprocess, so a return can transiently
|
|
106
|
+
// come back missing its parsed result. Retry ONCE on a missing/unparsed result before
|
|
107
|
+
// giving up — a genuine CLI failure fails both attempts (real error survives the retry),
|
|
108
|
+
// while a transient helper miss is recovered. Only retry when JSON was expected (parseJson)
|
|
109
|
+
// and the parsed result is absent; never retry on a clean exit that simply returned no JSON.
|
|
110
|
+
const runOnce = () => agent(prompt, opts).catch((e) => ({ ok: false, exitCode: -1, envelope: null, stderr: String(e && e.message), via: "error" }));
|
|
111
|
+
let r = await runOnce();
|
|
112
|
+
// Retry once when JSON was expected but no parsed result came back — covers both the
|
|
113
|
+
// throw path (via="error") and a malformed return the loose schema let through (ok=false
|
|
114
|
+
// with the result absent). A real CLI failure that returned valid JSON (envelope present,
|
|
115
|
+
// ok=false) is NOT retried — that is a true result, not a transient miss.
|
|
116
|
+
const missingResult = (x) => !x || (parseJson && (x.envelope === undefined || x.envelope === null) && x.ok !== true);
|
|
117
|
+
if (missingResult(r)) {
|
|
118
|
+
r = await runOnce();
|
|
119
|
+
}
|
|
106
120
|
return r || { ok: false, exitCode: -1, envelope: null, via: "error" };
|
|
107
121
|
}
|
|
108
122
|
async function runPreflight(projectDir, label = "preflight", phaseNameOpt) { return runCli(projectDir, "preflight", ["--json"], "cli-preflight.cjs", label, true, phaseNameOpt); }
|
|
@@ -122,6 +136,76 @@ async function generateBrief(projectDir, { kind = "execute", milestone, domain,
|
|
|
122
136
|
return { ok: r.ok, briefPath: `${projectDir}/.gsd-t/briefs/${id}.json`, via: r.via };
|
|
123
137
|
}
|
|
124
138
|
|
|
139
|
+
// M94-D10-T0: Structural-slice injection seam.
|
|
140
|
+
// Queries the graph CLI for the structural question appropriate to this phase and
|
|
141
|
+
// returns the slice as a JSON-stringified string to inject into worker agent context.
|
|
142
|
+
// On graph-unavailable returns a LOUD surfaced message (no silent grep).
|
|
143
|
+
// Invariant: [RULE] phase-workflow-injects-structural-slice
|
|
144
|
+
// Invariant: [RULE] phase-workflow-fail-loud-no-grep
|
|
145
|
+
//
|
|
146
|
+
// verb-map per phase (based on the d8 consumer manifest):
|
|
147
|
+
// impact → blast-radius (downstream impact set from target)
|
|
148
|
+
// plan → who-imports (touched-file dependents for sequencing)
|
|
149
|
+
// partition→ cluster (tightly-coupled file groups for domain-cut)
|
|
150
|
+
// feature → blast-radius (new-feature impact on existing code)
|
|
151
|
+
// gap-analysis → dead-code (requirements-vs-code coverage gaps)
|
|
152
|
+
// project → cluster (structure-aligned milestone decomposition)
|
|
153
|
+
// populate → who-imports (derive doc structure from real edges)
|
|
154
|
+
// promote-debt → blast-radius (scope a debt item's reach)
|
|
155
|
+
// prd → cluster (structure-aware decomposition)
|
|
156
|
+
// milestone/discuss/design-decompose/doc-ripple → no structural verb (no-op)
|
|
157
|
+
const PHASE_GRAPH_VERB_MAP = {
|
|
158
|
+
impact: "blast-radius",
|
|
159
|
+
plan: "who-imports",
|
|
160
|
+
partition: "cluster",
|
|
161
|
+
feature: "blast-radius",
|
|
162
|
+
"gap-analysis": "dead-code",
|
|
163
|
+
project: "cluster",
|
|
164
|
+
populate: "who-imports",
|
|
165
|
+
"promote-debt": "blast-radius",
|
|
166
|
+
prd: "cluster",
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* queryStructuralSlice — calls `gsd-t graph <verb>` for the phase's structural question.
|
|
171
|
+
* Returns { ok, verb, slice, graphUnavailable, loudMessage } where:
|
|
172
|
+
* ok=true → slice is a JSON-ready object (the graph result)
|
|
173
|
+
* ok=false, graphUnavailable=true → LOUD message (no grep fallback)
|
|
174
|
+
* ok=false, graphUnavailable=false → graph returned an error / unexpected envelope
|
|
175
|
+
*
|
|
176
|
+
* [RULE] phase-workflow-fail-loud-no-grep — on graph-unavailable, surface the loud
|
|
177
|
+
* message and NEVER fall back to grep for the structural question.
|
|
178
|
+
*/
|
|
179
|
+
async function queryStructuralSlice(projectDir, phaseName, phaseNameOpt) {
|
|
180
|
+
const verb = PHASE_GRAPH_VERB_MAP[phaseName];
|
|
181
|
+
if (!verb) {
|
|
182
|
+
// Phase has no mapped structural verb (milestone/discuss/design-decompose/doc-ripple) — no-op.
|
|
183
|
+
return { ok: true, verb: null, slice: null, graphUnavailable: false, loudMessage: null };
|
|
184
|
+
}
|
|
185
|
+
// The graph query uses gsd-t-graph-query-cli.cjs (local) or `gsd-t graph <verb>` (global).
|
|
186
|
+
// argv: only the verb (no target) for phase-level queries that return a global set
|
|
187
|
+
// (cluster/dead-code/who-imports/blast-radius without a target returns the full graph slice).
|
|
188
|
+
const r = await runCli(
|
|
189
|
+
projectDir, `graph ${verb}`, [], "gsd-t-graph-query-cli.cjs",
|
|
190
|
+
`graph:${verb}`, true, phaseNameOpt
|
|
191
|
+
);
|
|
192
|
+
const env = r.envelope || {};
|
|
193
|
+
if (env.ok === false && env.reason === "graph-unavailable") {
|
|
194
|
+
// [RULE] phase-workflow-fail-loud-no-grep: surface LOUD, no grep fallback.
|
|
195
|
+
const loudMessage = `⚠ graph unavailable — structural ${verb} slice NOT injected (fix it: gsd-t graph status). NO grep fallback — structural question unanswered.`;
|
|
196
|
+
log(`M94 graph-slice: ${loudMessage}`);
|
|
197
|
+
return { ok: false, verb, slice: null, graphUnavailable: true, loudMessage };
|
|
198
|
+
}
|
|
199
|
+
if (env.ok === true) {
|
|
200
|
+
log(`M94 graph-slice: ${phaseName} → ${verb} → ${(env.results || []).length} result(s) (tier: ${env.tier || "?"})`);
|
|
201
|
+
return { ok: true, verb, slice: env, graphUnavailable: false, loudMessage: null };
|
|
202
|
+
}
|
|
203
|
+
// Unexpected envelope (graph returned ok:false for a non-unavailable reason).
|
|
204
|
+
const loudMessage = `⚠ graph ${verb} query returned unexpected envelope (ok=${env.ok}, reason=${env.reason || "?"}). Structural slice NOT injected.`;
|
|
205
|
+
log(`M94 graph-slice: ${loudMessage}`);
|
|
206
|
+
return { ok: false, verb, slice: null, graphUnavailable: false, loudMessage };
|
|
207
|
+
}
|
|
208
|
+
|
|
125
209
|
// M82: run the deterministic selection oracle over a candidate-set spec. The spec
|
|
126
210
|
// is written to a file via the agent's Bash (no fs in this sandbox), then judged by
|
|
127
211
|
// `gsd-t competition-judge --in <file>`. The agent MUST copy the judge's rich output
|
|
@@ -749,6 +833,17 @@ ${STATED_CLAIMS_INSTRUCTION}`,
|
|
|
749
833
|
const baseObjective = promptByPhase[phaseName];
|
|
750
834
|
const briefLine = `**Brief (REQUIRED):** ${brief.briefPath || "(no brief — re-walk repo)"}`;
|
|
751
835
|
|
|
836
|
+
// M94-D10-T0: Query the structural graph slice for this phase.
|
|
837
|
+
// The result is threaded into every worker-agent prompt so the agent receives
|
|
838
|
+
// the pre-computed structural answer and has no reason to grep for structure.
|
|
839
|
+
// [RULE] phase-workflow-injects-structural-slice
|
|
840
|
+
const _graphSliceResult = await queryStructuralSlice(projectDir, phaseName, "Phase");
|
|
841
|
+
const _graphSliceLine = _graphSliceResult.ok && _graphSliceResult.slice
|
|
842
|
+
? `\n**Structural Graph Slice (${_graphSliceResult.verb}):** ${JSON.stringify(_graphSliceResult.slice)}\nUse this pre-computed graph slice to answer structural questions — do NOT grep for ${_graphSliceResult.verb} answers.`
|
|
843
|
+
: _graphSliceResult.loudMessage
|
|
844
|
+
? `\n**Structural Graph:** ${_graphSliceResult.loudMessage}`
|
|
845
|
+
: "";
|
|
846
|
+
|
|
752
847
|
let result;
|
|
753
848
|
// M90 §2 D4-T4: holds the divergence-path result from the competition arm (R-ARCH-1).
|
|
754
849
|
// Set inside the competition arm (before Finalize), attached to result after.
|
|
@@ -761,6 +856,7 @@ if (!competitionOn) {
|
|
|
761
856
|
`You are the ${phaseName} phase agent.`,
|
|
762
857
|
milestone ? `Milestone: ${milestone}` : "",
|
|
763
858
|
briefLine,
|
|
859
|
+
_graphSliceLine,
|
|
764
860
|
userInput ? `\nUser input:\n${userInput}` : "",
|
|
765
861
|
``,
|
|
766
862
|
`Objective: ${baseObjective}`,
|
|
@@ -849,6 +945,7 @@ if (!competitionOn) {
|
|
|
849
945
|
`You are candidate ${ids[i]} — one of ${competitionN} INDEPENDENT ${phaseName} proposals competing on quality.`,
|
|
850
946
|
milestone ? `Milestone: ${milestone}` : "",
|
|
851
947
|
briefLine,
|
|
948
|
+
_graphSliceLine,
|
|
852
949
|
userInput ? `\nUser input:\n${userInput}` : "",
|
|
853
950
|
``,
|
|
854
951
|
`Objective: ${baseObjective}`,
|
|
@@ -1030,6 +1127,7 @@ if (!competitionOn) {
|
|
|
1030
1127
|
`You are the ${phaseName} finalizer. A competition selected a WINNING proposal; implement it for real.`,
|
|
1031
1128
|
milestone ? `Milestone: ${milestone}` : "",
|
|
1032
1129
|
briefLine,
|
|
1130
|
+
_graphSliceLine,
|
|
1033
1131
|
``,
|
|
1034
1132
|
`Objective: ${baseObjective}`,
|
|
1035
1133
|
``,
|
|
@@ -166,6 +166,59 @@ const RESEARCH_RESULT_SCHEMA = {
|
|
|
166
166
|
properties: { ok: { type: "boolean" }, gapKey: { type: "string" }, citedBlock: { type: "string" }, sourceUrls: { type: "array", items: { type: "string" } }, fetchDates: { type: "array", items: { type: "string" } }, reason: { type: "string" } },
|
|
167
167
|
};
|
|
168
168
|
|
|
169
|
+
// M94-D11 §READER: Query blast-radius / who-imports for structural impact before editing.
|
|
170
|
+
// Returns a short text snippet injected into the Execute phase agent prompt.
|
|
171
|
+
// On graph-unavailable: logs the fail-loud message and returns null (no grep fallback).
|
|
172
|
+
// [RULE] quick-writer-pattern
|
|
173
|
+
async function queryGraphForQuick(projectDir, task, phaseName) {
|
|
174
|
+
// Extract a plausible target hint from the task description (first word-token that
|
|
175
|
+
// looks like a file or function name). Heuristic — the agent uses the full slice.
|
|
176
|
+
const words = (task || "").split(/\s+/).filter(Boolean);
|
|
177
|
+
const targetHint = words.find((w) => w.includes("/") || w.includes(".") || w.includes("#")) || words[0] || "";
|
|
178
|
+
if (!targetHint || targetHint.length < 2) return null;
|
|
179
|
+
|
|
180
|
+
const r = await runCli(
|
|
181
|
+
projectDir,
|
|
182
|
+
"graph",
|
|
183
|
+
["blast-radius", targetHint],
|
|
184
|
+
"gsd-t-graph-query-cli.cjs",
|
|
185
|
+
"graph-blast-radius",
|
|
186
|
+
true,
|
|
187
|
+
phaseName
|
|
188
|
+
).catch(() => ({ ok: false, exitCode: -1, envelope: null }));
|
|
189
|
+
|
|
190
|
+
const env = r && r.envelope;
|
|
191
|
+
if (!env) return null;
|
|
192
|
+
if (!env.ok && env.reason === "graph-unavailable") {
|
|
193
|
+
log(`M94-D11 READER: graph unavailable — fix it (gsd-t graph status). Quick proceeds without structural slice.`);
|
|
194
|
+
return null; // Fail-loud logged; no grep fallback [RULE consumer-structural-grep-removed]
|
|
195
|
+
}
|
|
196
|
+
if (env.ok && Array.isArray(env.results)) {
|
|
197
|
+
return `## Graph structural slice (blast-radius for "${targetHint}"):\n${JSON.stringify(env.results.slice(0, 20), null, 2)}`;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// M94-D11 §WRITER: Trigger a freshness pass over the touched set after edits.
|
|
203
|
+
// who-imports triggers freshness_check_on_query inline (D4 surface) — re-indexes any
|
|
204
|
+
// content-hash-dirty file before answering. Best-effort (non-blocking).
|
|
205
|
+
// [RULE] quick-writer-pattern
|
|
206
|
+
async function reindexTouchedFilesQuick(projectDir, filesEdited, phaseName) {
|
|
207
|
+
if (!Array.isArray(filesEdited) || filesEdited.length === 0) return;
|
|
208
|
+
for (const f of filesEdited.slice(0, 10)) {
|
|
209
|
+
await runCli(
|
|
210
|
+
projectDir,
|
|
211
|
+
"graph",
|
|
212
|
+
["who-imports", f],
|
|
213
|
+
"gsd-t-graph-query-cli.cjs",
|
|
214
|
+
`graph-reindex-touched:${f.slice(-30)}`,
|
|
215
|
+
true,
|
|
216
|
+
phaseName
|
|
217
|
+
).catch(() => {});
|
|
218
|
+
}
|
|
219
|
+
log(`M94-D11 WRITER: re-indexed ${Math.min(filesEdited.length, 10)} touched file(s) after quick task`);
|
|
220
|
+
}
|
|
221
|
+
|
|
169
222
|
if (!task) {
|
|
170
223
|
log("quick: args.task required");
|
|
171
224
|
return { status: "failed", reason: "no-task" };
|
|
@@ -176,11 +229,16 @@ const pre = await runPreflight(projectDir);
|
|
|
176
229
|
if (!pre.ok) return { status: "failed", reason: "preflight-failed", preflight: pre.envelope };
|
|
177
230
|
const brief = await generateBrief(projectDir, { kind: "execute", id: "quick-brief" });
|
|
178
231
|
|
|
232
|
+
// M94-D11 §READER: query graph for structural impact before the Execute agent reasons
|
|
233
|
+
// [RULE] quick-writer-pattern
|
|
234
|
+
const _graphSliceLine = await queryGraphForQuick(projectDir, task, "Preflight") || "";
|
|
235
|
+
|
|
179
236
|
phase("Execute");
|
|
180
237
|
const result = await agent(
|
|
181
238
|
[
|
|
182
239
|
`Quick task: ${task}`,
|
|
183
240
|
`**Brief:** ${brief.briefPath || "(no brief)"}`,
|
|
241
|
+
_graphSliceLine ? `\n**Graph structural slice (M94-D11 READER — blast-radius impact):**\n${_graphSliceLine}\nUse this slice for structural impact assessment — do NOT grep for import/dependency relationships.` : "",
|
|
184
242
|
``,
|
|
185
243
|
`**CRUX-FIRST — START HERE (smallest change is the default, ceremony is opt-in):**`,
|
|
186
244
|
`1. CRUX: state the crux of this ask in ONE line — the single thing that must become true.`,
|
|
@@ -213,6 +271,12 @@ if (result.status === "failed" || result.status === "blocked") {
|
|
|
213
271
|
return { status: result.status, result };
|
|
214
272
|
}
|
|
215
273
|
|
|
274
|
+
// M94-D11 §WRITER: re-index touched files after edits land (freshness trigger).
|
|
275
|
+
// [RULE] quick-writer-pattern
|
|
276
|
+
if (Array.isArray(result.filesEdited) && result.filesEdited.length > 0) {
|
|
277
|
+
await reindexTouchedFilesQuick(projectDir, result.filesEdited, "Execute");
|
|
278
|
+
}
|
|
279
|
+
|
|
216
280
|
// ── M89 Research Phase — Stated-Claims→classify→cite (§6.5/§1/§2/§3/§4/§7) ──
|
|
217
281
|
phase("Research");
|
|
218
282
|
const guessedClaims = [];
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
// agents: living docs + 5 dimension files + plain-english) → render(agent: HTML).
|
|
20
20
|
//
|
|
21
21
|
// args shape:
|
|
22
|
-
// { projectDir: ".", scanNumber?: 13, verify?: "single"|"none" }
|
|
22
|
+
// { projectDir: ".", scanNumber?: 13, verify?: "single"|"none", graphMode?: "wired"|"disabled" }
|
|
23
|
+
// graphMode:
|
|
24
|
+
// "wired" (default) — build index if absent, query structural slice, inject into scanSlice agents
|
|
25
|
+
// "disabled" — skip all graph calls (the no-graph baseline for AC-4 INSIGHT-delta comparison)
|
|
23
26
|
// (no slice cap — the probe's cohesive-sub-domain decomposition is the slice set.)
|
|
24
27
|
|
|
25
28
|
export const meta = {
|
|
@@ -27,10 +30,11 @@ export const meta = {
|
|
|
27
30
|
description:
|
|
28
31
|
"GSD-T scan (runtime-native): preflight → volume-probe → pipeline(deep-finder per slice → single verify) → synthesis(archive+register) → document(living docs + 5 dimension files + plain-english) → render. Orchestrator does NO fs/require; all I/O is inside subagents. Fans out by codebase volume.",
|
|
29
32
|
phases: [
|
|
30
|
-
{ title: "Preflight",
|
|
31
|
-
{ title: "Probe",
|
|
32
|
-
{ title: "
|
|
33
|
-
{ title: "
|
|
33
|
+
{ title: "Preflight", detail: "branch + prior-register check (agent via Bash)" },
|
|
34
|
+
{ title: "Probe", detail: "volume probe → per-area slice list", model: "sonnet" },
|
|
35
|
+
{ title: "Graph-Wiring", detail: "M94-D6: build index if absent + query structural slice (dead-code/dangling/cluster) → inject into finders ADDITIVELY; fallback announced on graph-unavailable", model: "haiku" },
|
|
36
|
+
{ title: "Deep Scan", detail: "pipeline: per-slice deep finder (graph-augmented when wired) → single verify" },
|
|
37
|
+
{ title: "Synthesis", detail: "archive prior + write fresh register + git", model: "opus" },
|
|
34
38
|
{ title: "Document", detail: "living docs + 5 dimension files (per-doc fan-out)" },
|
|
35
39
|
{ title: "Plain-English", detail: "non-technical companion: batched gen + severity-grouped chunked write" },
|
|
36
40
|
],
|
|
@@ -48,6 +52,9 @@ const projectDir = _args.projectDir || ".";
|
|
|
48
52
|
const scanNumber = _args.scanNumber || null;
|
|
49
53
|
const verifyMode = _args.verify || "single"; // "single" | "none"
|
|
50
54
|
const maxSlicesOverride = _args.maxSlicesHint || null; // optional power-user ceiling override
|
|
55
|
+
// M94-D6: graph wiring — "wired" (default) or "disabled" (no-graph baseline for AC-4).
|
|
56
|
+
// [RULE] scan-injects-structural-slice / [RULE] no-graph-baseline-proven-graph-free
|
|
57
|
+
const graphMode = (_args.graphMode === "disabled") ? "disabled" : "wired";
|
|
51
58
|
|
|
52
59
|
// VOLUME-DERIVED CAP — a RUNAWAY BACKSTOP, not the target count. The probe decides
|
|
53
60
|
// the actual slice count by cohesive sub-domain WITHIN this cap; the cap only fires
|
|
@@ -181,8 +188,81 @@ const RENDER_SCHEMA = {
|
|
|
181
188
|
},
|
|
182
189
|
};
|
|
183
190
|
|
|
191
|
+
// M94-D6: Graph-wiring schemas (additive — consumed only when graphMode === "wired").
|
|
192
|
+
// The structural slice the D5 CLI returns; injected into scanSlice finder agents.
|
|
193
|
+
// [RULE] scan-injects-structural-slice / [RULE] scan-slice-consumed
|
|
194
|
+
const GRAPH_STATUS_SCHEMA = {
|
|
195
|
+
type: "object",
|
|
196
|
+
required: ["ok"],
|
|
197
|
+
additionalProperties: true,
|
|
198
|
+
properties: {
|
|
199
|
+
ok: { type: "boolean" },
|
|
200
|
+
reason: { type: "string" },
|
|
201
|
+
// ok=true fields (from graph-query-cli-contract §status verb):
|
|
202
|
+
storeExists: { type: "boolean" },
|
|
203
|
+
notes: { type: "string" },
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const GRAPH_BUILD_SCHEMA = {
|
|
208
|
+
type: "object",
|
|
209
|
+
required: ["ok"],
|
|
210
|
+
additionalProperties: true,
|
|
211
|
+
properties: {
|
|
212
|
+
ok: { type: "boolean" },
|
|
213
|
+
notes: { type: "string" },
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const GRAPH_SLICE_SCHEMA = {
|
|
218
|
+
type: "object",
|
|
219
|
+
required: ["ok"],
|
|
220
|
+
additionalProperties: true,
|
|
221
|
+
properties: {
|
|
222
|
+
ok: { type: "boolean" },
|
|
223
|
+
reason: { type: "string" },
|
|
224
|
+
deadCode: { type: "array", items: { type: "object" }, description: "dead-code verb results" },
|
|
225
|
+
dangling: { type: "array", items: { type: "object" }, description: "dangling verb results" },
|
|
226
|
+
clusters: { type: "array", items: { type: "object" }, description: "cluster verb results" },
|
|
227
|
+
coverage: { type: "object", description: "coverage envelope from the query" },
|
|
228
|
+
notes: { type: "string" },
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
184
232
|
// ───── Script body — orchestration only, ZERO file I/O here ─────────────────
|
|
185
233
|
|
|
234
|
+
// M94-D6: structuralSlice — the pre-computed structural findings from the D5 CLI.
|
|
235
|
+
// Populated by the Graph-Wiring phase (after Probe, before Deep Scan) when graphMode==="wired".
|
|
236
|
+
// Null when graphMode==="disabled" (no-graph baseline) or graph-unavailable (fallback).
|
|
237
|
+
// [RULE] scan-injects-structural-slice / [RULE] no-graph-baseline-proven-graph-free
|
|
238
|
+
let structuralSlice = null; // { deadCode, dangling, clusters, coverage, tier } | null
|
|
239
|
+
let graphWiringMode = "pending"; // "wired" | "fallback-announced" | "disabled"
|
|
240
|
+
|
|
241
|
+
// M94-D6: runCli — inline async helper that delegates CLI calls to an agent() Bash.
|
|
242
|
+
// M81 invariant: NO require/fs/child_process in the orchestrator body.
|
|
243
|
+
// This is the ONLY way to invoke the D5 query CLI from the sandbox.
|
|
244
|
+
// [RULE] no-graph-baseline-proven-graph-free — this helper is called ONLY when graphMode==="wired".
|
|
245
|
+
async function runCli(verb, target, label) {
|
|
246
|
+
const targetArg = target ? ` ${JSON.stringify(target)}` : "";
|
|
247
|
+
const cmd = `node bin/gsd-t-graph-query-cli.cjs ${verb}${targetArg} 2>/dev/null || echo '{"ok":false,"reason":"graph-unavailable"}'`;
|
|
248
|
+
const result = await agent(
|
|
249
|
+
[
|
|
250
|
+
`Run the following command in \`${projectDir}\` via Bash and return ONLY the raw JSON line it prints (no commentary):`,
|
|
251
|
+
`\`\`\`bash`,
|
|
252
|
+
`cd ${JSON.stringify(projectDir)} && ${cmd}`,
|
|
253
|
+
`\`\`\``,
|
|
254
|
+
`If the command fails or prints no JSON, return: {"ok":false,"reason":"graph-unavailable"}`,
|
|
255
|
+
`Return ONLY the JSON, nothing else.`,
|
|
256
|
+
].join("\n"),
|
|
257
|
+
{ label: `graph:${label || verb}`, phase: "Graph-Wiring", model: "haiku" }
|
|
258
|
+
).catch(() => null);
|
|
259
|
+
try {
|
|
260
|
+
return (typeof result === "string") ? JSON.parse(result.trim()) : (result || { ok: false, reason: "graph-unavailable" });
|
|
261
|
+
} catch (_) {
|
|
262
|
+
return { ok: false, reason: "graph-unavailable" };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
186
266
|
// Preflight: an agent checks branch + whether a prior register exists, via Bash.
|
|
187
267
|
// (No fs in the body — that was the bug.)
|
|
188
268
|
phase("Preflight");
|
|
@@ -242,6 +322,62 @@ if (rawSlices.length > sliceCap) {
|
|
|
242
322
|
}
|
|
243
323
|
log(`probe derived ${rawSlices.length} slice(s); backstop cap=${computedCap}; running ${slices.length} deep-finder(s); totals=${JSON.stringify(probe.totals)}`);
|
|
244
324
|
|
|
325
|
+
// M94-D6: Graph-Wiring phase — ADDITIVE injection of the pre-computed structural slice.
|
|
326
|
+
// Current scan architecture is KEPT FULLY INTACT (Destructive Action Guard).
|
|
327
|
+
// [RULE] scan-injects-structural-slice / [RULE] no-graph-baseline-proven-graph-free
|
|
328
|
+
phase("Graph-Wiring");
|
|
329
|
+
if (graphMode === "disabled") {
|
|
330
|
+
// No-graph baseline mode (AC-4 INSIGHT-delta measurement).
|
|
331
|
+
// ZERO graph calls in this path — [RULE] no-graph-baseline-proven-graph-free.
|
|
332
|
+
graphWiringMode = "disabled";
|
|
333
|
+
log("graph-wiring: DISABLED (no-graph baseline mode — graph-query call-count == 0; AC-4 baseline)");
|
|
334
|
+
} else {
|
|
335
|
+
// graphMode === "wired": build index if absent, then query structural slice.
|
|
336
|
+
// Step 1: check if index exists and is queryable.
|
|
337
|
+
const statusResult = await runCli("status", null, "status");
|
|
338
|
+
if (!statusResult || !statusResult.ok) {
|
|
339
|
+
// Graph unavailable — announce fallback, continue with intact grep-mode scan.
|
|
340
|
+
// [RULE] parser-fail-disables-loud-never-silent (from graph-query-cli-contract)
|
|
341
|
+
graphWiringMode = "fallback-announced";
|
|
342
|
+
log(`⚠ GRAPH-FALLBACK (ANNOUNCED): graph index not available (${(statusResult && statusResult.reason) || "graph-unavailable"}) — scan continues in full grep-mode (today's architecture, intact). Structural findings from LLM reconstruction only. Build the index (gsd-t graph build) to enable graph-wired accuracy.`);
|
|
343
|
+
} else {
|
|
344
|
+
// Step 2: query the structural slice (dead-code + dangling + clusters).
|
|
345
|
+
// These are the findings the deep-finders currently reconstruct by reading files (error-prone);
|
|
346
|
+
// the graph hands them over pre-computed and ACCURATE.
|
|
347
|
+
const [deadCodeResult, danglingResult, clusterResult] = await parallel([
|
|
348
|
+
() => runCli("dead-code", null, "dead-code"),
|
|
349
|
+
() => runCli("dangling", null, "dangling"),
|
|
350
|
+
() => runCli("cluster", null, "cluster"),
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
const allOk = (deadCodeResult && deadCodeResult.ok) ||
|
|
354
|
+
(danglingResult && danglingResult.ok) ||
|
|
355
|
+
(clusterResult && clusterResult.ok);
|
|
356
|
+
|
|
357
|
+
if (!allOk) {
|
|
358
|
+
// All three verbs returned graph-unavailable — announce fallback.
|
|
359
|
+
graphWiringMode = "fallback-announced";
|
|
360
|
+
log(`⚠ GRAPH-FALLBACK (ANNOUNCED): all structural-slice queries returned graph-unavailable — scan continues in full grep-mode. Dead-code / dangling / cluster findings from LLM reconstruction only.`);
|
|
361
|
+
} else {
|
|
362
|
+
// Structural slice assembled — will be injected into scanSlice finder agents.
|
|
363
|
+
structuralSlice = {
|
|
364
|
+
deadCode: (deadCodeResult && deadCodeResult.ok && deadCodeResult.results) || [],
|
|
365
|
+
dangling: (danglingResult && danglingResult.ok && danglingResult.results) || [],
|
|
366
|
+
clusters: (clusterResult && clusterResult.ok && clusterResult.results) || [],
|
|
367
|
+
// Coverage envelopes: any incomplete coverage is surfaced to finders.
|
|
368
|
+
coverage: {
|
|
369
|
+
deadCode: (deadCodeResult && deadCodeResult.coverage) || null,
|
|
370
|
+
dangling: (danglingResult && danglingResult.coverage) || null,
|
|
371
|
+
cluster: (clusterResult && clusterResult.coverage) || null,
|
|
372
|
+
},
|
|
373
|
+
tier: (deadCodeResult && deadCodeResult.tier) || (danglingResult && danglingResult.tier) || "unknown",
|
|
374
|
+
};
|
|
375
|
+
graphWiringMode = "wired";
|
|
376
|
+
log(`graph-wiring: WIRED — structural slice ready (dead-code: ${structuralSlice.deadCode.length} candidates, dangling: ${structuralSlice.dangling.length} edges, clusters: ${structuralSlice.clusters.length} groups, tier: ${structuralSlice.tier}). Slice will be INJECTED ADDITIVELY into scanSlice deep-finders. [RULE] scan-injects-structural-slice`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
245
381
|
const deep = budget && budget.total && budget.total > 300000 ? "MAXIMUM" : "thorough";
|
|
246
382
|
|
|
247
383
|
// Deep scan — pipeline: per-slice deep finder → single verify (no barrier).
|
|
@@ -252,13 +388,54 @@ phase("Deep Scan");
|
|
|
252
388
|
// coverage as complete (7/19 slices dropped this way on the Hilo run). So: (1) the
|
|
253
389
|
// finder call is RETRIED once on a null/invalid result; (2) a slice that still fails
|
|
254
390
|
// is flagged `failed:true` and tracked — never conflated with a genuinely-empty slice.
|
|
255
|
-
|
|
391
|
+
//
|
|
392
|
+
// M94-D6: finderPrompt accepts an optional graphSliceContext string — the pre-computed
|
|
393
|
+
// structural slice from the D5 CLI, INJECTED ADDITIVELY so the finder reasons over
|
|
394
|
+
// accurate, deterministic structure rather than LLM-reconstructing relationships by reading.
|
|
395
|
+
// [RULE] scan-injects-structural-slice / [RULE] scan-slice-consumed
|
|
396
|
+
function finderPrompt(slice, graphSliceContext) {
|
|
256
397
|
return [
|
|
257
398
|
`⛔ Scan ONLY files under the absolute project path \`${projectDir}\`. \`cd ${projectDir}\` first; never read outside this tree.`,
|
|
258
399
|
`You are a DEEP tech-debt finder for ONE slice of a scan of \`${projectDir}\`: \`${slice.key}\` (dimension: ${slice.dimension}).`,
|
|
259
400
|
`Owned paths (relative to \`${projectDir}\`): ${JSON.stringify(slice.paths)}.`,
|
|
260
401
|
slice.why ? `Why this slice matters: ${slice.why}` : ``,
|
|
261
402
|
``,
|
|
403
|
+
// M94-D6: graph structural slice — ADDITIVE injection block.
|
|
404
|
+
// Present only when graphMode==="wired" AND the slice was fetched.
|
|
405
|
+
// The finder MUST use this data to answer structural questions — do NOT re-read
|
|
406
|
+
// the graph from files (that is error-prone reconstruction; this is accurate).
|
|
407
|
+
// [RULE] scan-injects-structural-slice / [RULE] scan-slice-consumed
|
|
408
|
+
graphSliceContext ? [
|
|
409
|
+
`## Pre-computed Structural Slice (GRAPH-WIRED — use this for structural findings)`,
|
|
410
|
+
``,
|
|
411
|
+
`The following structural data has been PRE-COMPUTED from the deterministic dependency graph`,
|
|
412
|
+
`(D5 query CLI — ${graphSliceContext.tier || "unknown"} tier). Use this data DIRECTLY for dead-code,`,
|
|
413
|
+
`cycle, dangling-reference, and coupling findings — do NOT reconstruct these from file reads`,
|
|
414
|
+
`(the graph is more accurate). This is ADDITIVE: you still read files for in-file logic defects.`,
|
|
415
|
+
``,
|
|
416
|
+
`**Dead-code candidates** (functions/files with no inbound edges from the graph):`,
|
|
417
|
+
graphSliceContext.deadCode && graphSliceContext.deadCode.length > 0
|
|
418
|
+
? "```json\n" + JSON.stringify(graphSliceContext.deadCode.slice(0, 50), null, 1) + "\n```"
|
|
419
|
+
: "(none — all indexed symbols have inbound edges, OR the index has incomplete coverage)",
|
|
420
|
+
graphSliceContext.coverage && graphSliceContext.coverage.deadCode && graphSliceContext.coverage.deadCode.complete === false
|
|
421
|
+
? `⚠ Dead-code coverage incomplete (${graphSliceContext.coverage.deadCode.unparsedContributors || "?"} file(s) unparsed — result may be partial).`
|
|
422
|
+
: "",
|
|
423
|
+
``,
|
|
424
|
+
`**Dangling references** (call/import edges to missing nodes — delete/rename residue):`,
|
|
425
|
+
graphSliceContext.dangling && graphSliceContext.dangling.length > 0
|
|
426
|
+
? "```json\n" + JSON.stringify(graphSliceContext.dangling.slice(0, 30), null, 1) + "\n```"
|
|
427
|
+
: "(none found)",
|
|
428
|
+
``,
|
|
429
|
+
`**Tightly-coupled file clusters** (may indicate coupling/cycle debt):`,
|
|
430
|
+
graphSliceContext.clusters && graphSliceContext.clusters.length > 0
|
|
431
|
+
? "```json\n" + JSON.stringify(graphSliceContext.clusters.slice(0, 20), null, 1) + "\n```"
|
|
432
|
+
: "(none found — no file groups above the coupling threshold)",
|
|
433
|
+
``,
|
|
434
|
+
`When you report a dead-code / dangling / coupling finding, CITE the graph data above`,
|
|
435
|
+
`(e.g. "funcId: X has no inbound call edges per the graph") so the finding is traceable`,
|
|
436
|
+
`to the pre-computed query result. [RULE] scan-insight-delta-graph-attributed`,
|
|
437
|
+
].filter(Boolean).join("\n") : "",
|
|
438
|
+
``,
|
|
262
439
|
`MANDATE: ENUMERATE, do NOT sample. Read EVERY file under your owned paths (use Read/Grep). You own only this slice, so go to the bottom of it.`,
|
|
263
440
|
`Depth = ${deep}. "thorough" = every file, every non-trivial real defect (high+medium confidence). "MAXIMUM" = also lower-confidence/speculative items worth review.`,
|
|
264
441
|
`Surface: bugs, security holes, missing validation, broken invariants, race conditions, dead/duplicated code, N+1s, untested critical paths, contract drift, domain-specific correctness (money math, state-machine gaps, timezone bugs, idempotency holes).`,
|
|
@@ -351,11 +528,12 @@ async function gatedAgent(prompt, opts) {
|
|
|
351
528
|
// but setTimeout is not). Used only for rate-limit backoff between retries.
|
|
352
529
|
function sleep(ms) { return new Promise((res) => setTimeout(res, ms)); }
|
|
353
530
|
|
|
354
|
-
async function runFinder(slice) {
|
|
531
|
+
async function runFinder(slice, graphSliceContext) {
|
|
355
532
|
// up to 2 attempts; a null/invalid (non-array findings) result counts as a drop.
|
|
533
|
+
// M94-D6: graphSliceContext passed through to finderPrompt for ADDITIVE injection.
|
|
356
534
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
357
535
|
try {
|
|
358
|
-
const r = await gatedAgent(finderPrompt(slice), {
|
|
536
|
+
const r = await gatedAgent(finderPrompt(slice, graphSliceContext), {
|
|
359
537
|
label: attempt === 1 ? `find:${slice.key}` : `find:${slice.key} (retry)`,
|
|
360
538
|
phase: "Deep Scan", schema: FINDER_SCHEMA, model: "sonnet",
|
|
361
539
|
});
|
|
@@ -370,7 +548,11 @@ async function runFinder(slice) {
|
|
|
370
548
|
|
|
371
549
|
async function scanSlice(slice) {
|
|
372
550
|
const sliceKey = slice.key || "unknown-slice";
|
|
373
|
-
|
|
551
|
+
// M94-D6: inject the structural slice context ADDITIVELY — only when graph is wired.
|
|
552
|
+
// When graphMode==="disabled" OR graph-unavailable, graphSliceContext is null (no injection).
|
|
553
|
+
// [RULE] scan-injects-structural-slice / [RULE] no-graph-baseline-proven-graph-free
|
|
554
|
+
const graphSliceContext = (graphWiringMode === "wired" && structuralSlice) ? structuralSlice : null;
|
|
555
|
+
const finderResult = await runFinder(slice, graphSliceContext);
|
|
374
556
|
// M72: distinguish a FAILED finder (null after retries) from a genuinely-clean slice.
|
|
375
557
|
if (!finderResult || !Array.isArray(finderResult.findings)) {
|
|
376
558
|
return { slice: sliceKey, findings: [], failed: true };
|
|
@@ -824,4 +1006,13 @@ return {
|
|
|
824
1006
|
plainEnglishComplete: peComplete,
|
|
825
1007
|
htmlReport: null, // render stage removed (M71)
|
|
826
1008
|
probeTotals: probe.totals,
|
|
1009
|
+
// M94-D6: graph wiring status (surfaced for AC-4 INSIGHT-delta comparison).
|
|
1010
|
+
// [RULE] scan-injects-structural-slice / [RULE] no-graph-baseline-proven-graph-free
|
|
1011
|
+
graphWiring: {
|
|
1012
|
+
mode: graphWiringMode, // "wired" | "fallback-announced" | "disabled"
|
|
1013
|
+
structuralSlicePresent: structuralSlice !== null,
|
|
1014
|
+
deadCodeCount: (structuralSlice && structuralSlice.deadCode) ? structuralSlice.deadCode.length : 0,
|
|
1015
|
+
danglingCount: (structuralSlice && structuralSlice.dangling) ? structuralSlice.dangling.length : 0,
|
|
1016
|
+
clusterCount: (structuralSlice && structuralSlice.clusters) ? structuralSlice.clusters.length : 0,
|
|
1017
|
+
},
|
|
827
1018
|
};
|
|
@@ -257,6 +257,49 @@ if (!vg.ok) {
|
|
|
257
257
|
}
|
|
258
258
|
log(`verify-gate green`);
|
|
259
259
|
|
|
260
|
+
// ─── M94-D10-T5: Graph Structural Slice — dead-code + dangling (ADDITIVE, announced-degradation) ──
|
|
261
|
+
// [RULE] qa-verify-use-orphan-dangling-verbs — query dead-code + dangling structurally.
|
|
262
|
+
// [RULE] verify-integrate-graph-additive-announced-not-hard-fail — PRE-MORTEM Finding 3
|
|
263
|
+
// bootstrap carve-out: verify degrades ANNOUNCED on graph-unavailable, does NOT hard-fail.
|
|
264
|
+
// The graph query ENRICHES the triad; it must not brick verify itself.
|
|
265
|
+
// Runtime-native (M81): delegate to an agent's Bash via runCli helper.
|
|
266
|
+
let _graphDeadCodeSlice = null;
|
|
267
|
+
let _graphDanglingSlice = null;
|
|
268
|
+
let _graphVerifyWarning = null;
|
|
269
|
+
|
|
270
|
+
{
|
|
271
|
+
const dcResult = await runCli(
|
|
272
|
+
projectDir, "graph dead-code", [], "gsd-t-graph-query-cli.cjs",
|
|
273
|
+
"graph:dead-code", true, "Verify-Gate"
|
|
274
|
+
);
|
|
275
|
+
const dcEnv = dcResult.envelope || {};
|
|
276
|
+
if (dcEnv.ok === true) {
|
|
277
|
+
_graphDeadCodeSlice = dcEnv;
|
|
278
|
+
log(`M94 graph dead-code: ${(dcEnv.results || []).length} dead-code result(s) (tier: ${dcEnv.tier || "?"})`);
|
|
279
|
+
} else if (dcEnv.reason === "graph-unavailable") {
|
|
280
|
+
_graphVerifyWarning = "⚠ graph unavailable — structural gate skipped, fix it (gsd-t graph status)";
|
|
281
|
+
log(`M94 graph dead-code: ${_graphVerifyWarning}`);
|
|
282
|
+
} else {
|
|
283
|
+
_graphVerifyWarning = `⚠ graph dead-code query unexpected envelope (reason: ${dcEnv.reason || "?"}); structural gate skipped`;
|
|
284
|
+
log(`M94 graph dead-code: ${_graphVerifyWarning}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!_graphVerifyWarning) {
|
|
288
|
+
// Only query dangling if dead-code succeeded (same graph availability)
|
|
289
|
+
const dangResult = await runCli(
|
|
290
|
+
projectDir, "graph dangling", [], "gsd-t-graph-query-cli.cjs",
|
|
291
|
+
"graph:dangling", true, "Verify-Gate"
|
|
292
|
+
);
|
|
293
|
+
const dangEnv = dangResult.envelope || {};
|
|
294
|
+
if (dangEnv.ok === true) {
|
|
295
|
+
_graphDanglingSlice = dangEnv;
|
|
296
|
+
log(`M94 graph dangling: ${(dangEnv.results || []).length} dangling result(s)`);
|
|
297
|
+
} else {
|
|
298
|
+
log(`M94 graph dangling: query failed (reason: ${dangEnv.reason || "?"}) — no dangling slice`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
260
303
|
// ─── M89 §7 ENFORCE Gate (FAIL-blocking — A4 no-silent-guess) ─────────────
|
|
261
304
|
// Scans all milestone artifacts for STRUCTURAL auto-research-claim markers and FAILs if:
|
|
262
305
|
// (a) ANY marker is status=uncited (an external guessed claim was never cited), OR
|
|
@@ -698,6 +741,12 @@ const stages = [
|
|
|
698
741
|
`You are the QA validator for milestone \`${milestone}\`.`,
|
|
699
742
|
``,
|
|
700
743
|
`**Brief (REQUIRED):** ${briefRef}`,
|
|
744
|
+
// M94-D10-T5: inject dead-code/dangling graph slice (ADDITIVE — enriches QA, announced-degradation on unavailable)
|
|
745
|
+
_graphVerifyWarning
|
|
746
|
+
? `\n**Graph Structural Gate:** ${_graphVerifyWarning} (WARNING only — QA continues other checks)`
|
|
747
|
+
: (_graphDeadCodeSlice
|
|
748
|
+
? `\n**Graph Structural Slice (dead-code):** ${JSON.stringify(_graphDeadCodeSlice)}\n**Graph Structural Slice (dangling):** ${_graphDanglingSlice ? JSON.stringify(_graphDanglingSlice) : "N/A"}\nInclude these structural findings in your QA analysis — dead-code and dangling-ref entries are structural quality signals. Do NOT grep for dead-code; the pre-computed slice above is authoritative.`
|
|
749
|
+
: ""),
|
|
701
750
|
``,
|
|
702
751
|
`Per .gsd-t/contracts/orthogonal-validation-contract.md, your scope is`,
|
|
703
752
|
`**test mechanics + contract compliance**. Run the test suite. Report pass/fail/skip`,
|
|
@@ -707,7 +756,7 @@ const stages = [
|
|
|
707
756
|
`Run the QA protocol. ${qaProtocolInstruction}`,
|
|
708
757
|
``,
|
|
709
758
|
`Return JSON per the schema.`,
|
|
710
|
-
].join("\n"),
|
|
759
|
+
].filter(Boolean).join("\n"),
|
|
711
760
|
{ label: "qa", phase: "Orthogonal Triad", schema: QA_SCHEMA, model: "sonnet" }
|
|
712
761
|
),
|
|
713
762
|
].filter(Boolean);
|