@tekyzinc/gsd-t 4.6.10 → 4.7.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 CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [4.7.10] - 2026-06-22 (M90 — The Unproven-Assumption Doctrine — minor)
6
+
7
+ ### Added — a self-governing doctrine that stops the system building on unproven assumptions
8
+
9
+ M90 makes "don't act on belief; if it's not grounded in knowledge or research, research first" an enforced framework capability rather than advisory prose. The core principle (sourced at discuss: self-introspection is unreliable, *worse* on RLHF models — arxiv 2306.13063, 2310.01798) is **externalize + force, never introspect** — deterministic triggers + external checks, fail-closed on uncertainty.
10
+
11
+ Three deterministic mechanisms, wired fail-closed into the debug/execute/quick/phase/verify workflows (§4 R-FAIL-1/2/3):
12
+
13
+ - **Factual classifier** (`bin/gsd-t-research-gate.cjs`, absorbed from M89): routes a knowledge-gap to grep (internal) vs. research+cite (external); the INTERNAL decision enumerates no open category (never guess-internal). Premise-corrected during build (the partition's "vendor list causes a silent-miss" was false on disk — the list is kept as an `external→web` upgrade). New time-anchored override (R-FACT-3): a fast-moving lib/API/version or "current best practice" gap is researched regardless of confidence.
14
+ - **Loop-ledger non-convergence halt** (`bin/gsd-t-loop-ledger.cjs`): a debug loop on the SAME symptom (keyed on the symptom, not the changing file — so variant-spawning whack-a-mole is caught, R-LOOP-1) HARD-HALTS with a premise-re-examination directive instead of patching further. Detection ≠ resolution (the halt persists until a genuine re-examination clears it), per-milestone scoped (one milestone's halt can't brick another's verify), full-reset on re-examination, plain-object-validated, atomic write.
15
+ - **Architectural-assumption trigger** (`bin/gsd-t-architectural-trigger.cjs`): a divergence-sampling + extend-existing-code trigger that flags building on an unproven approach. Shipped EXPERIMENTAL+MEASURED; the spike/adversary response-mode enforcement (R-FAIL-2) is honestly DECLARED interface-only this milestone (no live producer yet — backlog #42 wires it).
16
+
17
+ Doctrine contract `unproven-assumption-doctrine-contract.md` v1.0.0 STABLE (absorbs `auto-research-contract.md` v1.3.3) + §6 guard map + self-obedience lints. CLAUDE-global Research Policy upgraded from advisory prose to the doctrine.
18
+
19
+ ### Process note
20
+
21
+ The milestone obeyed its own doctrine: plan-hardening caught M90 about to build a domain on an unproven premise (re-verified on disk, re-scoped), and a 9-round verify sequence hardened M90's OWN gate-wiring lifecycle to ground (hollow-gate → fail-open → no-producer → lifecycle flaws → vacuous-gate → array-type-confusion → cross-milestone-brick → symptom-keying). Suite 1998 / 1994 pass / 0 fail / 4 skip. Deferred to backlog: #42 (live spike-feasibility producer + sink fail-closed), #43 (AWS/S3 classifier edge), 3 documented code-review nits.
22
+
23
+ ## [4.6.11] - 2026-06-16 (Output Style — six named conciseness tics + backlog #33 — patch)
24
+
25
+ ### Changed — tightened the CONCISE Output Style rule with six named anti-patterns
26
+
27
+ User feedback that replies stayed wordy despite the existing CONCISE rule. Extracted six specific tics — three from live examples, three from the user's own wordy→concise rewrites (binvoice `Wordy Example 1/2.txt`, a labeled before/after set where ~70 wordy lines collapsed to ~25 with identical information) — and added a litmus test. Synced the template to the live global CLAUDE.md (blocks kept identical — ripple invariant). Also added backlog #33 from the binvoice FB-modal debug-loop retrospective.
28
+
29
+ - `templates/CLAUDE-global.md`: Output Style block gains six rules — no process narration, no answer sandwich, no affirmation throat-clearing, no honesty theater, a table replaces its prose (never repeats it), ask once — plus a litmus test ("delete any sentence that survives deletion without info loss").
30
+ - `.gsd-t/backlog.md`: #33 firing debug-cycle circuit-breaker + repro-fixture-on-regression + anchor-last scraping stack rule (completes #31/TD-294 from the loop-governance side).
31
+
32
+ Behavioral/doc-only — no test changes. Suite: 1603/1607 pass, 0 fail.
33
+
5
34
  ## [4.6.10] - 2026-06-15 (Installer wiring for status line + low-context cue — minor)
6
35
 
7
36
  ### Added — installer now copies and wires the status line + ctx-cue Stop hook
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v4.5.10** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
3
+ **v4.7.10** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
4
4
 
5
5
  **Eliminates context rot** — task-level fresh dispatch (one subagent per task, ~10-20% context each) means compaction never triggers.
6
6
  **Compaction-proof debug loops** — `gsd-t headless --debug-loop` runs test-fix-retest cycles as separate `claude -p` sessions. A JSONL debug ledger persists all hypothesis/fix/learning history across fresh sessions. Anti-repetition preamble injection prevents retrying failed hypotheses. Escalation tiers (sonnet → opus → human) and a hard iteration ceiling enforced externally.
@@ -0,0 +1,523 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * gsd-t-architectural-trigger.cjs — M90 D1 (v1.0.0)
5
+ *
6
+ * Deterministic architectural-assumption trigger module.
7
+ *
8
+ * TWO fire paths:
9
+ *
10
+ * R-ARCH-1 — DIVERGENCE SAMPLING (experimental + measured)
11
+ * Given N≥2 fresh-context answers to the SAME approach question, computes a
12
+ * deterministic divergence measure (normalized type-token ratio variance across
13
+ * answer n-gram fingerprints). When divergence exceeds DIVERGENCE_THRESHOLD, fires
14
+ * with reason "divergence-sampling:high-variance". Sourced from ClarifyGPT
15
+ * consistency-sampling (https://arxiv.org/pdf/2310.10996): divergence across
16
+ * fresh-context samples ⇒ a load-bearing unproven assumption exists.
17
+ * NOTE: This path is EXPERIMENTAL+MEASURED (competition-arm only). Self-MoA
18
+ * (one model, temperature-varied) may not diverge like the fresh-context saga
19
+ * cases the threshold was tuned on. That mismatch is RECORDED by the
20
+ * instrumentation sink; the path never claims it works.
21
+ *
22
+ * R-ARCH-2 — PROTOCOL CLASS (unconditional on extend-existing-code signal)
23
+ * Extending existing code is itself an approach decision — the trigger fires
24
+ * unconditionally on an extend signal (no confidence/threshold gate). Signal
25
+ * format: { type: "extend-existing-code", context, basis }.
26
+ * Signal is COMPUTED at runtime from task/scope inputs that already exist
27
+ * (a task whose **Touches** lists an EXISTING file ⇒ extend-class). The
28
+ * D-CONTRACT domain (D4-T4) computes this from brief/task inputs.
29
+ * Reason literal: "protocol-class:extend-existing" (distinguishable from R-ARCH-1).
30
+ *
31
+ * RESPONSE INTERFACE (R-ARCH-3..6) — INTERFACE ONLY; agent() wiring is D-CONTRACT.
32
+ *
33
+ * mode "spike" — PREFERRED response; attempt an executable spike to prove feasibility.
34
+ * mode "adversary-only" — fallback when spike is infeasible or fails.
35
+ * R-ARCH-4: spike-fail → STOP directive in envelope; agent cannot proceed.
36
+ * R-ARCH-5: spike-infeasible → logged skip + adversary MANDATORY.
37
+ * R-ARCH-6: proven-by-adversary-only → flag surfaced for verify gate.
38
+ *
39
+ * INSTRUMENTATION (no-claim invariant):
40
+ * Every fire emits a JSONL record to the measurement sink. The module NEVER asserts
41
+ * it works — it emits data only (fire-rate, catch-quality fields). No self-efficacy
42
+ * claim in any envelope field.
43
+ *
44
+ * HOUSE STYLE:
45
+ * { ok: true, ... } on success
46
+ * { ok: false, error } on bad input
47
+ * Deterministic: identical input → byte-identical envelope.
48
+ * Bad input → { ok:false, error } + non-zero CLI exit.
49
+ * Node built-ins only; zero new runtime deps; sync APIs.
50
+ *
51
+ * CONTRACT: .gsd-t/contracts/m90-doctrine-mechanisms-contract.md §2 v1.0.0
52
+ */
53
+
54
+ const fs = require("fs");
55
+ const path = require("path");
56
+ const os = require("os");
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Constants (DECLARED — not back-fit per fixture)
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /**
63
+ * Divergence threshold for the shared-core-term fraction metric.
64
+ * DECLARED CONSTANT — the divergence measure is a fixed formula and this threshold
65
+ * is a declared constant, not back-fit to any fixture.
66
+ *
67
+ * Formula: score = 1 - (sharedCoreTerms / allUniqueTerms)
68
+ * - sharedCoreTerms = non-stopword tokens (≥4 chars) appearing in ≥50% of N answers
69
+ * - allUniqueTerms = total unique non-stopword tokens across all answers
70
+ *
71
+ * Basis: ClarifyGPT (arXiv:2310.10996) consistency-sampling — across N independent
72
+ * fresh-context samples, low shared-core-term fraction (high unique-term proportion)
73
+ * indicates each answer introduced different technical mechanisms, the proxy for
74
+ * "an unproven architectural premise exists." A HIGH shared-core fraction means
75
+ * all answers converged on the same technical strategy.
76
+ *
77
+ * Threshold 0.80 was chosen as the midpoint of the confirmed gap in the TUNED corpus:
78
+ * divergent rows all score ≥0.89; convergent rows all score ≤0.70.
79
+ * Gap = 0.19; midpoint ≈ 0.79. Set to 0.80 (conservative toward precision over recall).
80
+ * NOT tuned to held-out rows.
81
+ */
82
+ const DIVERGENCE_THRESHOLD = 0.80;
83
+
84
+ /**
85
+ * Minimum N for divergence sampling. N<2 cannot produce a meaningful comparison.
86
+ */
87
+ const MIN_ANSWERS = 2;
88
+
89
+ /**
90
+ * Stopwords excluded from token analysis.
91
+ * Common English function words plus broad technical/generic terms that appear in
92
+ * BOTH divergent and convergent contexts equally (non-discriminative).
93
+ */
94
+ const STOPWORDS = new Set([
95
+ "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
96
+ "have", "has", "had", "do", "does", "did", "will", "would", "could",
97
+ "should", "may", "might", "must", "shall", "to", "of", "in", "on",
98
+ "at", "by", "for", "with", "from", "as", "or", "and", "but", "not",
99
+ "it", "its", "this", "that", "these", "those", "we", "our", "you",
100
+ "your", "they", "them", "i", "me", "my", "he", "she", "him", "her",
101
+ "which", "what", "how", "when", "where", "if", "so", "also", "just",
102
+ "then", "than", "there", "here", "same", "all", "any", "each", "can",
103
+ "into", "via", "no", "only", "need", "using", "use", "call", "make",
104
+ "ensure", "before", "after", "pass", "get", "set", "both", "up",
105
+ "out", "above", "below", "end", "result", "object", "string",
106
+ "function", "value", "input", "output", "code", "signal",
107
+ ]);
108
+
109
+ /**
110
+ * Measurement sink file location. Relative to the project root (cwd at runtime).
111
+ * Kept in .gsd-t/metrics/ alongside other measurement sinks.
112
+ */
113
+ const SINK_RELATIVE_PATH = path.join(".gsd-t", "metrics", "arch-trigger-events.jsonl");
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Divergence measure (R-ARCH-1)
117
+ // ---------------------------------------------------------------------------
118
+
119
+ /**
120
+ * Tokenize an answer string into a SET of unique lowercase key terms.
121
+ * Filters out stopwords and short tokens (< 4 chars) to focus on
122
+ * substantive technical vocabulary.
123
+ *
124
+ * Deterministic: same string → same token set.
125
+ *
126
+ * @param {string} text
127
+ * @returns {Set<string>}
128
+ */
129
+ function tokenize(text) {
130
+ const tokens = (text.match(/[a-z][a-z0-9_.-]*/gi) || [])
131
+ .map((t) => t.toLowerCase())
132
+ .filter((t) => t.length >= 4 && !STOPWORDS.has(t));
133
+ return new Set(tokens);
134
+ }
135
+
136
+ /**
137
+ * Given N fresh-context answers to the same approach question, compute a
138
+ * deterministic divergence score using the shared-core-term fraction metric.
139
+ *
140
+ * THE FIXED, DOCUMENTED FORMULA:
141
+ * 1. Tokenize each answer into a set of unique non-stopword terms (≥4 chars).
142
+ * 2. For each unique term across all answers, count how many answers contain it.
143
+ * 3. sharedCoreTerms = count of terms appearing in ≥ ceil(N * 0.5) answers.
144
+ * (i.e., appearing in at least half of all N answers)
145
+ * 4. allUniqueTerms = total count of distinct terms across all N answers.
146
+ * 5. score = 1 - (sharedCoreTerms / allUniqueTerms)
147
+ *
148
+ * Interpretation:
149
+ * - score ≈ 0: most terms are shared → answers agree on same strategy → CONVERGENT
150
+ * - score ≈ 1: few terms are shared → each answer introduced different mechanisms → DIVERGENT
151
+ *
152
+ * Basis: divergent answers each name a different technical mechanism (different
153
+ * key tokens). Convergent answers share the same core technical vocabulary because
154
+ * they all describe the same strategy. The shared-term fraction measures how much
155
+ * vocabulary each answer contributes that was NOT in the others.
156
+ *
157
+ * Threshold DIVERGENCE_THRESHOLD = 0.80. Gap between corpus classes:
158
+ * divergent ≥ 0.89, convergent ≤ 0.70, gap = 0.19.
159
+ *
160
+ * Deterministic: identical inputs (same strings in same order) → byte-identical score.
161
+ * allUniqueTerms = 0 → returns 0 (degenerate all-empty input; no fire).
162
+ *
163
+ * @param {string[]} answers - N≥2 fresh-context answers to the same question.
164
+ * @returns {number} Divergence score in [0, 1].
165
+ */
166
+ function computeDivergenceScore(answers) {
167
+ const tokenSets = answers.map((a) => tokenize(a));
168
+ const termCounts = new Map();
169
+
170
+ for (const set of tokenSets) {
171
+ for (const term of set) {
172
+ termCounts.set(term, (termCounts.get(term) || 0) + 1);
173
+ }
174
+ }
175
+
176
+ if (termCounts.size === 0) return 0;
177
+
178
+ const N = answers.length;
179
+ const sharedThreshold = Math.max(2, Math.ceil(N * 0.5));
180
+
181
+ let sharedCount = 0;
182
+ for (const count of termCounts.values()) {
183
+ if (count >= sharedThreshold) sharedCount++;
184
+ }
185
+
186
+ return 1 - sharedCount / termCounts.size;
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Instrumentation sink (no-claim invariant)
191
+ // ---------------------------------------------------------------------------
192
+
193
+ /**
194
+ * Emit a structured instrumentation record to the measurement sink.
195
+ *
196
+ * NEVER adds a self-efficacy claim. Records facts ONLY:
197
+ * - fired (boolean)
198
+ * - basis (string | null)
199
+ * - mode (string — the response mode selected)
200
+ * - divergenceScore (number | null — for R-ARCH-1 path)
201
+ * - firePath ("divergence-sampling" | "protocol-class" | null)
202
+ *
203
+ * Wall-clock timestamp is NOT in the asserted envelope shape (the trigger envelope
204
+ * is deterministic). The sink record carries an ISO timestamp for observability but
205
+ * it is NOT in the trigger envelope returned to callers.
206
+ *
207
+ * Emission is best-effort — if the sink directory does not exist or the write fails,
208
+ * the error is SWALLOWED (the trigger result is never blocked by instrumentation).
209
+ *
210
+ * @param {object} record
211
+ */
212
+ function emitInstrumentationRecord(record) {
213
+ try {
214
+ const sinkPath = path.resolve(process.cwd(), SINK_RELATIVE_PATH);
215
+ const dir = path.dirname(sinkPath);
216
+ if (!fs.existsSync(dir)) {
217
+ fs.mkdirSync(dir, { recursive: true });
218
+ }
219
+ // ISO timestamp for observability (not in the asserted envelope)
220
+ const entry = Object.assign({}, record, { _emittedAt: new Date().toISOString() });
221
+ fs.appendFileSync(sinkPath, JSON.stringify(entry) + os.EOL, "utf8");
222
+ } catch (_) {
223
+ // Instrumentation failure NEVER blocks the trigger result.
224
+ }
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Response mode resolver (R-ARCH-3..6)
229
+ // ---------------------------------------------------------------------------
230
+
231
+ /**
232
+ * Resolve the response mode flags given the spike feasibility / spike result inputs.
233
+ *
234
+ * @param {object} opts
235
+ * @param {boolean} [opts.spikeFeasible=true] - Can an executable spike be run?
236
+ * @param {boolean} [opts.spikePassed] - Did the spike pass? (ignored if spikeFeasible=false)
237
+ * @returns {object} Response mode envelope fragment.
238
+ */
239
+ function resolveResponseMode({ spikeFeasible = true, spikePassed } = {}) {
240
+ if (!spikeFeasible) {
241
+ // R-ARCH-5: spike infeasible → adversary-only + adversaryMandatory + logged skip
242
+ return {
243
+ mode: "adversary-only",
244
+ adversaryMandatory: true,
245
+ spikeSkipReason: "spike-infeasible: executable spike cannot be constructed for this premise",
246
+ provenByAdversaryOnly: true,
247
+ stopDirective: false,
248
+ };
249
+ }
250
+
251
+ if (spikePassed === false) {
252
+ // R-ARCH-4: spike ran and FAILED → STOP; agent cannot proceed
253
+ return {
254
+ mode: "adversary-only",
255
+ adversaryMandatory: true,
256
+ provenByAdversaryOnly: false,
257
+ stopDirective: true,
258
+ stopReason: "spike-fail: the executable spike failed — the premise is unproven; STOP and re-examine",
259
+ };
260
+ }
261
+
262
+ if (spikePassed === true) {
263
+ // Spike passed → prefer spike, adversary still RECOMMENDED but not mandatory
264
+ return {
265
+ mode: "spike",
266
+ adversaryMandatory: false,
267
+ provenByAdversaryOnly: false,
268
+ stopDirective: false,
269
+ };
270
+ }
271
+
272
+ // Default: spike PREFERRED (no spike result provided yet)
273
+ return {
274
+ mode: "spike",
275
+ adversaryMandatory: false,
276
+ provenByAdversaryOnly: false,
277
+ stopDirective: false,
278
+ };
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Main trigger function
283
+ // ---------------------------------------------------------------------------
284
+
285
+ /**
286
+ * Validate shared bad-input conditions.
287
+ * Returns { ok:false, error } if invalid, else null.
288
+ *
289
+ * @param {*} input
290
+ * @returns {{ ok:false, error:string }|null}
291
+ */
292
+ function validateInput(input) {
293
+ if (input === null || input === undefined) {
294
+ return { ok: false, error: "input must be a non-null object" };
295
+ }
296
+ if (typeof input !== "object" || Array.isArray(input)) {
297
+ return { ok: false, error: "input must be a plain object" };
298
+ }
299
+ return null;
300
+ }
301
+
302
+ /**
303
+ * R-ARCH-1 divergence-sampling trigger.
304
+ *
305
+ * @param {object} input
306
+ * @param {string[]} input.answers - N≥2 fresh-context answers to the same question.
307
+ * @param {string} input.basis - The approach/premise being evaluated.
308
+ * @param {object} [input.responseOpts] - Optional response mode options (see resolveResponseMode).
309
+ * @returns {object} House-style envelope.
310
+ */
311
+ function triggerDivergenceSampling(input) {
312
+ const badInput = validateInput(input);
313
+ if (badInput) return badInput;
314
+
315
+ const { answers, basis, responseOpts = {} } = input;
316
+
317
+ if (!Array.isArray(answers)) {
318
+ return { ok: false, error: "input.answers must be an array of strings" };
319
+ }
320
+ if (answers.length < MIN_ANSWERS) {
321
+ return {
322
+ ok: false,
323
+ error: `input.answers must contain at least ${MIN_ANSWERS} elements; got ${answers.length}`,
324
+ };
325
+ }
326
+ for (let i = 0; i < answers.length; i++) {
327
+ if (typeof answers[i] !== "string") {
328
+ return { ok: false, error: `input.answers[${i}] must be a string` };
329
+ }
330
+ }
331
+ if (!basis || typeof basis !== "string" || !basis.trim()) {
332
+ return { ok: false, error: "input.basis must be a non-empty string" };
333
+ }
334
+
335
+ const trimmedBasis = basis.trim();
336
+ const score = computeDivergenceScore(answers);
337
+ const fired = score >= DIVERGENCE_THRESHOLD;
338
+
339
+ const responseMode = resolveResponseMode(responseOpts);
340
+
341
+ const envelope = {
342
+ ok: true,
343
+ firePath: "divergence-sampling",
344
+ fired,
345
+ basis: trimmedBasis,
346
+ reason: fired
347
+ ? "divergence-sampling:high-variance"
348
+ : "divergence-sampling:low-variance",
349
+ divergenceScore: score,
350
+ divergenceThreshold: DIVERGENCE_THRESHOLD,
351
+ experimental: true, // NEVER omit: this path is experimental+measured, never proven
352
+ ...(fired ? responseMode : {}),
353
+ };
354
+
355
+ // Emit instrumentation (no-claim invariant: emitting a record, not asserting reliability).
356
+ // provenByAdversaryOnly + mode are PERSISTED so the verify R-FAIL-2 gate (§4) — which scans this
357
+ // sink for provenByAdversaryOnly===true — has the field it checks; without it the gate is vacuous
358
+ // (count always 0, can never fire), the exact hollow-gate failure M90 exists to prevent.
359
+ emitInstrumentationRecord({
360
+ firePath: "divergence-sampling",
361
+ fired,
362
+ basis: trimmedBasis,
363
+ mode: fired ? responseMode.mode : null,
364
+ provenByAdversaryOnly: fired ? !!responseMode.provenByAdversaryOnly : false,
365
+ divergenceScore: score,
366
+ });
367
+
368
+ return envelope;
369
+ }
370
+
371
+ /**
372
+ * R-ARCH-2 protocol-class trigger on extend-existing-code signal.
373
+ *
374
+ * @param {object} input
375
+ * @param {string} input.context - Description of the existing code being extended.
376
+ * @param {string} input.basis - The approach/premise being evaluated.
377
+ * @param {object} [input.responseOpts] - Optional response mode options.
378
+ * @returns {object} House-style envelope.
379
+ */
380
+ function triggerExtendExistingCode(input) {
381
+ const badInput = validateInput(input);
382
+ if (badInput) return badInput;
383
+
384
+ const { context, basis, responseOpts = {} } = input;
385
+
386
+ if (!context || typeof context !== "string" || !context.trim()) {
387
+ return { ok: false, error: "input.context must be a non-empty string" };
388
+ }
389
+ if (!basis || typeof basis !== "string" || !basis.trim()) {
390
+ return { ok: false, error: "input.basis must be a non-empty string" };
391
+ }
392
+
393
+ const trimmedBasis = basis.trim();
394
+ const responseMode = resolveResponseMode(responseOpts);
395
+
396
+ const envelope = {
397
+ ok: true,
398
+ firePath: "protocol-class",
399
+ fired: true, // ALWAYS fires — unconditional on this path (R-ARCH-2)
400
+ basis: trimmedBasis,
401
+ reason: "protocol-class:extend-existing", // DISTINCT from divergence path reason
402
+ context: context.trim(),
403
+ ...responseMode,
404
+ };
405
+
406
+ // Emit instrumentation. provenByAdversaryOnly + mode PERSISTED for the verify R-FAIL-2 gate (§4).
407
+ emitInstrumentationRecord({
408
+ firePath: "protocol-class",
409
+ fired: true,
410
+ basis: trimmedBasis,
411
+ mode: responseMode.mode,
412
+ provenByAdversaryOnly: !!responseMode.provenByAdversaryOnly,
413
+ divergenceScore: null,
414
+ });
415
+
416
+ return envelope;
417
+ }
418
+
419
+ /**
420
+ * Top-level resolver: dispatch to the appropriate fire path.
421
+ *
422
+ * The `type` field in input selects the path:
423
+ * "divergence-sampling" → R-ARCH-1 (input.answers[] required)
424
+ * "extend-existing-code" → R-ARCH-2 (input.context required)
425
+ *
426
+ * Bad input (missing type, unknown type, empty/whitespace/non-string fields) →
427
+ * { ok:false, error } + non-zero CLI exit.
428
+ *
429
+ * @param {object} input
430
+ * @returns {object} House-style envelope.
431
+ */
432
+ function resolve(input) {
433
+ const badInput = validateInput(input);
434
+ if (badInput) return badInput;
435
+
436
+ const { type } = input;
437
+
438
+ if (!type || typeof type !== "string" || !type.trim()) {
439
+ return { ok: false, error: 'input.type must be "divergence-sampling" or "extend-existing-code"' };
440
+ }
441
+
442
+ switch (type.trim()) {
443
+ case "divergence-sampling":
444
+ return triggerDivergenceSampling(input);
445
+ case "extend-existing-code":
446
+ return triggerExtendExistingCode(input);
447
+ default:
448
+ return {
449
+ ok: false,
450
+ error: `Unknown type "${type}". Expected "divergence-sampling" or "extend-existing-code"`,
451
+ };
452
+ }
453
+ }
454
+
455
+ // ---------------------------------------------------------------------------
456
+ // CLI entry point
457
+ // ---------------------------------------------------------------------------
458
+ //
459
+ // Usage:
460
+ // gsd-t-architectural-trigger trigger '{"type":"divergence-sampling","answers":[...],"basis":"..."}'
461
+ // gsd-t-architectural-trigger trigger '{"type":"extend-existing-code","context":"...","basis":"..."}'
462
+ //
463
+ // Bad input → { ok:false, error } + non-zero exit.
464
+
465
+ if (require.main === module) {
466
+ const args = process.argv.slice(2);
467
+
468
+ function emit(obj) {
469
+ process.stdout.write(JSON.stringify(obj) + "\n");
470
+ }
471
+ function emitError(msg, exitCode) {
472
+ emit({ ok: false, error: msg });
473
+ process.exit(exitCode);
474
+ }
475
+
476
+ const subcommand = args[0];
477
+ if (!subcommand || subcommand.trim() === "") {
478
+ emitError(
479
+ 'Missing subcommand. Usage: gsd-t-architectural-trigger trigger \'<JSON input>\'',
480
+ 64
481
+ );
482
+ }
483
+
484
+ if (subcommand !== "trigger") {
485
+ emitError(
486
+ `Unknown subcommand "${subcommand}". Usage: gsd-t-architectural-trigger trigger '<JSON input>'`,
487
+ 64
488
+ );
489
+ }
490
+
491
+ const rawInput = args[1];
492
+ if (rawInput === undefined || rawInput === null || rawInput.trim() === "") {
493
+ emitError(
494
+ "Missing JSON input. Usage: gsd-t-architectural-trigger trigger '<JSON input>'",
495
+ 64
496
+ );
497
+ }
498
+
499
+ let parsed;
500
+ try {
501
+ parsed = JSON.parse(rawInput);
502
+ } catch (e) {
503
+ emitError(`Malformed JSON input: ${e.message}`, 64);
504
+ }
505
+
506
+ const result = resolve(parsed);
507
+ emit(result);
508
+ if (!result.ok) process.exit(1);
509
+ // Success → exit 0
510
+ }
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // Module exports (frozen §2 signature — DO NOT CHANGE after Wave 1 closes)
514
+ // ---------------------------------------------------------------------------
515
+
516
+ module.exports = {
517
+ resolve,
518
+ triggerDivergenceSampling,
519
+ triggerExtendExistingCode,
520
+ resolveResponseMode,
521
+ computeDivergenceScore,
522
+ DIVERGENCE_THRESHOLD,
523
+ };