@tekyzinc/gsd-t 4.6.11 → 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 +18 -0
- package/README.md +1 -1
- package/bin/gsd-t-architectural-trigger.cjs +523 -0
- package/bin/gsd-t-loop-ledger.cjs +519 -0
- package/bin/gsd-t-research-gate.cjs +439 -0
- package/bin/gsd-t.js +63 -1
- package/commands/gsd-t-help.md +25 -0
- package/docs/requirements.md +17 -0
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +35 -16
- package/templates/prompts/blind-adversary-subagent.md +101 -0
- package/templates/prompts/research-subagent.md +127 -0
- package/templates/prompts/stated-claims-snippet.md +89 -0
- package/templates/test-helpers/launch-extension.ts +81 -0
- package/templates/workflows/gsd-t-debug.workflow.js +312 -0
- package/templates/workflows/gsd-t-execute.workflow.js +366 -1
- package/templates/workflows/gsd-t-phase.workflow.js +482 -5
- package/templates/workflows/gsd-t-quick.workflow.js +272 -2
- package/templates/workflows/gsd-t-verify.workflow.js +261 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
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
|
+
|
|
5
23
|
## [4.6.11] - 2026-06-16 (Output Style — six named conciseness tics + backlog #33 — patch)
|
|
6
24
|
|
|
7
25
|
### Changed — tightened the CONCISE Output Style rule with six named anti-patterns
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v4.
|
|
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
|
+
};
|