@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.
@@ -0,0 +1,439 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * gsd-t-research-gate.cjs — M90 D3 (v1.4.0 — time-anchored override + premise-corrected vendor list)
5
+ *
6
+ * Deterministic internal-vs-external-vs-AMBIGUOUS gap classifier. Given a GUESSED
7
+ * CLAIM (a claim the agent tagged GUESSED in its Stated-Claims list per §6.5),
8
+ * returns the house-style JSON envelope:
9
+ *
10
+ * { ok:true, gap, class:"internal"|"external"|"ambiguous", route:"grep"|"web"|"judge", reason }
11
+ *
12
+ * On bad input:
13
+ * { ok:false, error:"<reason>" }
14
+ *
15
+ * --- THE DOCTRINE APPLIED TO THE CLASSIFIER (v1.3.0 premise correction, M90 additions) ---
16
+ *
17
+ * M89's own rule is: never act on belief; if a claim is not grounded in definitive
18
+ * knowledge or research evidence, RESEARCH it. The previous classifier (~745 LOC of
19
+ * hand-fit regexes) violated that rule against itself: it tried to SEMANTICALLY decide
20
+ * "is this an external claim?" via pattern-guessing. So this classifier is a MECHANICAL
21
+ * STRING-FACT FILTER, NOT a semantic oracle.
22
+ *
23
+ * M90 premise correction (D3-T0 baseline, 2026-06-22):
24
+ * The partition asserted the vendor list caused "silent-miss" routing (an absent vendor →
25
+ * silently-internal). Verified FALSE on disk: an absent vendor with no internal signal falls
26
+ * through to `ambiguous→judge` ("never guess-internal"). The vendor list ONLY UPGRADES a
27
+ * vendor+API match to high-confidence `external→web`; its absence never routes internal.
28
+ * M89 already removed ALL "wins-outright→internal" overrides (auto-research-contract v1.3.3).
29
+ * Therefore: the vendor list is KEPT (deleting it would DOWNGRADE Stripe/Chrome/Plaid/Twilio
30
+ * et al. from `external→web` to `ambiguous→judge` — a pure regression). Changed or tightened
31
+ * ONLY if a concrete misroute defect is proven by the T0 baseline; never deleted on the
32
+ * falsified silent-miss premise. (Empirical: 10/10 unseen never-seen vendors → `ambiguous→judge`;
33
+ * 0/10 silently-internal — see D3-T0 known-answer section in the corpus test.)
34
+ *
35
+ * --- M90 D3 ADDITION: R-FACT-3 TIME-ANCHORED PROTOCOL OVERRIDE ---
36
+ *
37
+ * A confidence gate cannot catch a stale-but-confident fact (intrinsic self-knowledge signals
38
+ * collapse to chance on the temporal axis — CoVe, arXiv:2309.11495; Self-RAG, arXiv:2310.11511;
39
+ * temporal-collapse survey, arXiv:2510.19172v1). Therefore:
40
+ *
41
+ * A claim that is FAST-MOVING (a versioned library, API, protocol spec, or "current/latest
42
+ * best practice") → route to research REGARDLESS of any confidence signal.
43
+ *
44
+ * This is a deterministic string-fact check: a small set of temporal-signal phrases (e.g.
45
+ * "current best practice", "latest version", "current version", "stable release" — CLOSED
46
+ * PHRASES, not an open semantic category) fires BEFORE the vendor/internal signal tests and
47
+ * short-circuits to `class:"external", route:"web"`. The rationale: these phrases are string
48
+ * facts that the claim is fast-moving; the correct behavior under M89's own doctrine is to
49
+ * always verify, never trust a cached belief about what's "latest" or "current".
50
+ *
51
+ * --- THE THREE RESULT CLASSES ---
52
+ *
53
+ * - TIME-ANCHORED (checked FIRST, deterministic): a fast-moving-signal phrase → external/web
54
+ * always. Bypasses the vendor/internal test (temporal collapse makes confidence
55
+ * unreliable).
56
+ * - `internal` ONLY on a STRING-FACT internal signal (a concrete repo path/file
57
+ * shape, a real gsd-t-* tool name, or an explicit this-repo anchor
58
+ * phrase). These are string facts about THIS repo, not beliefs.
59
+ * - `external` ONLY on an UNAMBIGUOUS string-fact external signal: a recognized
60
+ * vendor/product proper noun (long, multi-char, non-homograph) that
61
+ * CO-OCCURS with an API/HTTP/protocol term. Kept SHORT and unambiguous;
62
+ * when in doubt a token is left OUT (→ ambiguous, which is safe).
63
+ * - `ambiguous` EVERYTHING ELSE. If placing a claim requires JUDGMENT rather than a
64
+ * string fact, it is NOT regex's call — it goes to the LLM judge, and if
65
+ * the LLM is not confident it is RESEARCHED (uncertain→verify). The
66
+ * ambiguous→LLM→uncertain→research routing lives in the WIRING (the 4
67
+ * workflows), not here — D1 stays a pure calculator.
68
+ *
69
+ * Hard rules:
70
+ * - Deterministic: identical claim text → byte-identical envelope
71
+ * - Zero new runtime deps (Node built-ins only), sync APIs
72
+ * - Never throws on string input (returns {ok:false,error} instead)
73
+ * - route is derived from class: internal→grep, external→web, ambiguous→judge
74
+ * - Bad input (empty/whitespace/non-string) → {ok:false,error} + non-zero CLI exit
75
+ *
76
+ * Contract: .gsd-t/contracts/auto-research-contract.md §1/§1.1 v1.3.3 STABLE
77
+ * .gsd-t/contracts/m90-doctrine-mechanisms-contract.md §1 v1.0.0
78
+ */
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // R-FACT-3 — Time-anchored protocol override (M90 D3 addition)
82
+ // ---------------------------------------------------------------------------
83
+ //
84
+ // A CLOSED set of temporal-signal phrases that indicate a claim is fast-moving
85
+ // (a versioned library, API spec, protocol, or "current/latest best practice").
86
+ // When matched, the claim bypasses ALL confidence gates and routes to research
87
+ // IMMEDIATELY (`class:"external", route:"web"`). These are STRING FACTS that
88
+ // the claim is temporal, not semantic judgments. The set is intentionally short
89
+ // and conservative — false positives (non-temporal claims matching) are safe
90
+ // (they get researched rather than cached); false negatives reach the ambiguous
91
+ // path and are judged. Phrases are checked BEFORE vendor/internal signals.
92
+ //
93
+ // Source: CoVe (arXiv:2309.11495) — intrinsic self-knowledge collapses on the
94
+ // temporal axis; a confidence gate cannot catch a stale-but-confident fact.
95
+ // Self-RAG (arXiv:2310.11511) — temporal freshness is a distinct retrieval axis.
96
+ // Temporal-collapse survey (arXiv:2510.19172v1).
97
+ //
98
+ // SC-NO-FINITE-LIST compliance: the INTERNAL decision still enumerates no OPEN
99
+ // category (internal requires a positive own-repo signal, never the mere absence
100
+ // of a temporal phrase). The temporal phrases are a closed set for the EXTERNAL
101
+ // decision (routing to web/research), not for the internal decision.
102
+
103
+ /** Closed set of temporal-signal phrases → always route to research (R-FACT-3).
104
+ *
105
+ * Design constraint: phrases must be SPECIFIC enough to avoid false-positives on
106
+ * internal-convention questions (e.g. bare "best practice" alone would fire on
107
+ * "what is our best practice for naming domains?" — an internal doc question).
108
+ * Each phrase here is qualified by a temporal/freshness adjective ("current",
109
+ * "latest", "recommended", "stable", "deprecated") that makes the fast-moving
110
+ * nature explicit. Bare "best practice" or "version" alone are NOT in this list.
111
+ */
112
+ const TEMPORAL_SIGNAL_PHRASES = [
113
+ // "current/latest" best-practice or standard (qualified with temporal adjective)
114
+ "current best practice", "current best-practice",
115
+ "latest best practice", "latest best-practice",
116
+ // "current" / "latest" version signals
117
+ "current version", "latest version", "latest stable",
118
+ "current release", "latest release", "stable release",
119
+ "newest version", "most recent version",
120
+ // upgrade / deprecation / migration signals (fast-moving)
121
+ "upgrade to", "migrate to", "migration guide",
122
+ "deprecated in", "is deprecated", "was deprecated",
123
+ // "recommended" signals (recommendations change — qualified to avoid false positives)
124
+ "recommended approach", "recommended way", "recommended practice",
125
+ "recommended version", "currently recommended",
126
+ ];
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Word-boundary token match (kills the substring-match anti-pattern)
130
+ // ---------------------------------------------------------------------------
131
+ //
132
+ // `text.includes(token)` matches INSIDE words ("our" hits "four", "rest" hits "the
133
+ // rest of"). The correct test is a WORD-BOUNDARY match. A token may contain spaces,
134
+ // dots, slashes and hyphens (e.g. "rest api", "node.js", "/v1/"); we anchor on \b only
135
+ // where the token edge is a word char and fall back to a plain substring test for edges
136
+ // that are non-word (a \b before "/" is meaningless).
137
+
138
+ /** Escape a string for use as a literal inside a RegExp. */
139
+ function escapeRegExp(s) {
140
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
141
+ }
142
+
143
+ /**
144
+ * Word-boundary-aware token match against an already-lowercased haystack.
145
+ * @param {string} lowerText - the lowercased claim text
146
+ * @param {string} token - a lowercased token (may contain spaces/dots/slashes/hyphens)
147
+ * @returns {boolean}
148
+ */
149
+ function boundaryMatch(lowerText, token) {
150
+ const t = token.trim();
151
+ if (t.length === 0) return false;
152
+ const left = /\w/.test(t[0]) ? "\\b" : "";
153
+ const right = /\w/.test(t[t.length - 1]) ? "\\b" : "";
154
+ return new RegExp(left + escapeRegExp(t) + right).test(lowerText);
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // INTERNAL string-facts (this-repo anchors + repo path/file/tool shapes)
159
+ // ---------------------------------------------------------------------------
160
+ //
161
+ // These are STRING FACTS about this repo, not semantic guesses. An explicit repo
162
+ // anchor phrase, or a concrete repo-relative path / file-naming shape / real gsd-t-*
163
+ // tool name, is a verifiable string-level signal that the claim is about THIS repo's
164
+ // own code. Anything subtler (a bare camelCase symbol that COULD be external, a
165
+ // question phrasing) is NOT a string fact → it falls through to ambiguous.
166
+
167
+ /**
168
+ * This-repo anchor phrases. An anchor signals internal ONLY when NO strong external signal
169
+ * (a real vendor proper-noun + API term) co-occurs (see the final decision rule in
170
+ * `classify()`). An anchor NEVER overrides a strong external signal — a generic English
171
+ * phrase like "exit code" / "who owns" / "this file" can appear in a sentence whose real
172
+ * subject is an external vendor ("what exit code does the Stripe API return?"), so it is not
173
+ * a string fact that the claim is internal. (There is no "decisive" vs "broad" split: since
174
+ * NO anchor overrides a strong external signal, the distinction is behaviorally dead — one
175
+ * flat list. Interrogative words are deliberately ABSENT — neutral, the natural way to phrase
176
+ * an external research question.)
177
+ */
178
+ const INTERNAL_ANCHORS = [
179
+ "this repo", "this repo's", "repo's",
180
+ "our repo", "the repo",
181
+ "this codebase", "our codebase",
182
+ "in this project", "our project",
183
+ "this module", "our module",
184
+ "this file", "our file",
185
+ "the existing", "our existing",
186
+ "in the codebase",
187
+ "our", "our internal",
188
+ // ownership / file-system phrasings about this repo's structure
189
+ "which domain owns", "which file owns", "who owns",
190
+ // exit code / return value of a module
191
+ "exit code",
192
+ ];
193
+
194
+ /**
195
+ * Internal file / path / tool-name SHAPE patterns. STRUCTURAL string facts (file
196
+ * extensions, repo-relative path separators, GSD-T's naming convention) — NOT an
197
+ * enumerated list of specific held-out symbols (a guard test forbids re-hardcoding
198
+ * those). A claim referencing one of these references this repo's own files.
199
+ */
200
+ const INTERNAL_FILE_PATTERNS = [
201
+ // GSD-T module / file naming conventions (shape, not a specific filename)
202
+ "gsd-t-", ".workflow.js", ".test.js", "gsd-t.js",
203
+ // contract / domain / scope file shapes
204
+ "-contract.md", "scope.md", "tasks.md", "constraints.md",
205
+ "progress.md", "architecture.md", "workflows.md",
206
+ // repo-relative path separators
207
+ "bin/", "test/", "templates/", "commands/", ".gsd-t/",
208
+ ];
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // EXTERNAL string-facts (unambiguous vendor proper-noun + API/protocol term)
212
+ // ---------------------------------------------------------------------------
213
+ //
214
+ // SHORT and unambiguous by design (the directive). Only LONG, multi-char, NON-homograph
215
+ // vendor/product proper nouns — never single-word English homographs ("go"/"square"/
216
+ // "edge"/"rest"/"swift"/"amazon"…), which are left OUT so a benign internal sentence is
217
+ // never misrouted external. `external` fires ONLY when such a proper noun CO-OCCURS with
218
+ // an API/HTTP/protocol term: that conjunction is an unambiguous string fact that the
219
+ // claim is about an external system's contract. A proper noun ALONE, or a term alone, is
220
+ // NOT enough — it falls through to ambiguous (the LLM's call), which is safe.
221
+
222
+ /** Unambiguous third-party vendor / product proper nouns (string facts, no homographs). */
223
+ const EXTERNAL_VENDOR_NOUNS = [
224
+ // payment processors
225
+ "paypal", "stripe", "braintree", "adyen", "klarna", "plaid",
226
+ // cloud platforms / CDN / infra
227
+ "azure", "google cloud", "gcp", "cloudflare", "cloudfront",
228
+ "fastly", "akamai", "vercel", "netlify",
229
+ // auth providers
230
+ "auth0", "okta", "cognito",
231
+ // databases / data
232
+ "mongodb", "postgresql", "elasticsearch", "dynamodb",
233
+ "firestore", "bigquery", "snowflake",
234
+ // messaging / 3rd-party services
235
+ "rabbitmq", "twilio", "sendgrid", "salesforce", "hubspot", "intercom",
236
+ // browsers (multi-char, non-homograph)
237
+ "chrome", "chromium", "firefox", "safari", "webkit",
238
+ // UI frameworks (multi-char names)
239
+ "react", "angular", "svelte",
240
+ ];
241
+
242
+ /** API / HTTP / protocol / auth-flow terms (the co-signal that makes a vendor external). */
243
+ const EXTERNAL_API_TERMS = [
244
+ "api", "endpoint", "webhook", "oauth", "openid", "saml", "jwt",
245
+ "rest api", "graphql", "grpc", "openapi", "swagger",
246
+ "rate limit", "rate-limit",
247
+ "access token", "refresh token", "bearer token",
248
+ "authorization header", "redirect uri", "redirect url", "callback url",
249
+ "/v1/", "/v2/", "/v3/",
250
+ "http", "https", "header", "request body", "response body",
251
+ ];
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // §7 fail-closed cite gate marker format (R-FACT-4, preserved from M89)
255
+ // ---------------------------------------------------------------------------
256
+ //
257
+ // When an external claim is classified, the wiring writes a machine-readable marker
258
+ // into the artifact at classify time:
259
+ // <!-- auto-research-claim: class=external key=<claim-key> status=uncited -->
260
+ // The verify gate (gsd-t-verify.workflow.js §7) FAILs if this marker is still
261
+ // status=uncited after the research stage. The marker is flipped to status=cited
262
+ // when the Verified-Facts block lands. This module's role: produce the `class`,
263
+ // `route`, and `reason` fields that trigger marker creation in the wiring. The
264
+ // marker write is the WIRING's responsibility; this classifier is a pure calculator.
265
+ // R-FACT-4: this marker mechanism is PRESERVED unchanged from the M89 baseline.
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Classification — a mechanical string-fact filter (no semantic judgment)
269
+ // ---------------------------------------------------------------------------
270
+
271
+ /**
272
+ * Classify a guessed claim as internal / external / ambiguous by STRING FACTS only.
273
+ *
274
+ * Decision order (deterministic, checked in sequence):
275
+ * 1. Bad-input guard (SC1): empty/whitespace/non-string → {ok:false}, never silent.
276
+ * 2. TIME-ANCHORED OVERRIDE (R-FACT-3): a fast-moving signal phrase (current/latest
277
+ * best practice, version signals, deprecation, upgrade) → external/web ALWAYS,
278
+ * bypassing all other tests. Temporal-collapse makes confidence unreliable on this
279
+ * class of claim; the safe rule is always-research.
280
+ * 3. STRONG EXTERNAL: an unambiguous vendor proper-noun AND an API/protocol term.
281
+ * + any path/anchor → ambiguous; else → external/web.
282
+ * 4. INTERNAL: concrete repo path/file shape or this-repo anchor, with ZERO strong
283
+ * external signal → internal/grep.
284
+ * 5. AMBIGUOUS: everything else → judge.
285
+ *
286
+ * @param {string} gap - The claim text (a GUESSED claim per §6.5).
287
+ * @returns {{ ok:true, gap:string, class:"internal"|"external"|"ambiguous",
288
+ * route:"grep"|"web"|"judge", reason:string }
289
+ * |{ ok:false, error:string }}
290
+ */
291
+ function classify(gap) {
292
+ // ── 1. Bad-input guard (SC1) ───────────────────────────────────────────────
293
+ // empty/whitespace/non-string → error envelope, never silent.
294
+ if (typeof gap !== "string") {
295
+ return { ok: false, error: "gap must be a string" };
296
+ }
297
+ const trimmed = gap.trim();
298
+ if (trimmed.length === 0) {
299
+ return { ok: false, error: "gap must not be empty or whitespace-only" };
300
+ }
301
+
302
+ const lower = trimmed.toLowerCase();
303
+
304
+ // ── 2. TIME-ANCHORED OVERRIDE (R-FACT-3) ─────────────────────────────────
305
+ //
306
+ // A fast-moving claim (best-practice, version, deprecation, recommended approach)
307
+ // ALWAYS routes to research regardless of any confidence signal. Temporal-collapse
308
+ // (CoVe/Self-RAG) makes a cached "latest" or "best practice" belief unreliable —
309
+ // the correct behavior is always-verify. This is checked BEFORE vendor/internal
310
+ // signals so that e.g. "the current best practice for React hooks api" routes to
311
+ // web even though it also has a vendor (react) + an api term.
312
+ //
313
+ // SC-NO-FINITE-LIST: this is a CLOSED set for the external/temporal decision. The
314
+ // INTERNAL decision still requires a positive own-repo signal (a path or anchor) —
315
+ // the mere ABSENCE of a temporal phrase never routes internal. A claim with a
316
+ // temporal phrase and also an internal path (e.g. "upgrade our gsd-t-phase.workflow.js
317
+ // to the current best practice") still routes temporal→web (the time-anchored check
318
+ // fires first and the recommendation itself is external regardless of which file
319
+ // will be changed).
320
+ const matchedTemporal = TEMPORAL_SIGNAL_PHRASES.find((p) => boundaryMatch(lower, p));
321
+ if (matchedTemporal) {
322
+ return {
323
+ ok: true, gap: trimmed, class: "external", route: "web",
324
+ reason: `Time-anchored override (R-FACT-3): temporal-signal phrase "${matchedTemporal}" indicates a fast-moving claim (best practice / version / deprecation). Temporal-collapse makes confidence unreliable on this class — always route to research regardless of any other signal (CoVe: arXiv:2309.11495).`,
325
+ };
326
+ }
327
+
328
+ // ── 3. String-fact signals ────────────────────────────────────────────────
329
+ const matchedPath = INTERNAL_FILE_PATTERNS.find((p) => boundaryMatch(lower, p));
330
+ const matchedAnchor = INTERNAL_ANCHORS.find((p) => boundaryMatch(lower, p));
331
+
332
+ // STRONG external string fact: an unambiguous vendor proper-noun AND an API/protocol term.
333
+ // BOTH are required — a bare homograph or a bare URL-looking token cannot make a claim
334
+ // strong-external (the vendor list is multi-char proper nouns only, no homographs).
335
+ //
336
+ // PREMISE-CORRECTED ROLE OF THE VENDOR LIST (M90-D3-T0 baseline):
337
+ // The partition asserted the list caused silent-miss routing. Verified FALSE: an absent
338
+ // vendor with no internal signal falls through to ambiguous→judge (never internal). The
339
+ // list's ACTUAL role: UPGRADE a vendor+API match to high-confidence `external→web`,
340
+ // SKIPPING the judge for known vendors. Deleting it would downgrade Stripe/Chrome/Plaid/
341
+ // Twilio et al. to `ambiguous→judge` — a regression, not a fix. KEPT unchanged.
342
+ const matchedVendor = EXTERNAL_VENDOR_NOUNS.find((v) => boundaryMatch(lower, v));
343
+ const matchedApiTerm = EXTERNAL_API_TERMS.find((t) => boundaryMatch(lower, t));
344
+ const hasStrongExternal = !!matchedVendor && !!matchedApiTerm;
345
+
346
+ // ── FINAL DECISION RULE (the structural resolution of the 7-cycle silent-miss class) ──
347
+ //
348
+ // `internal` is returned ONLY when there is ZERO strong-external signal. The mechanical
349
+ // classifier NEVER overrides an external signal with anything — not a path, not "this
350
+ // repo", not anything. There is no string-fact that is exclusively internal (a single
351
+ // claim can carry BOTH a real repo path AND a real external-API subject at once — e.g.
352
+ // "wire the Stripe OAuth refresh into gsd-t-execute.workflow.js"), so every "wins
353
+ // outright → internal" override re-opens the silent miss. Removing ALL override rules
354
+ // closes the class by construction. The cost is more ambiguous→judge calls — the safe
355
+ // direction (the judge researches when unsure; it never silently guesses internal).
356
+ //
357
+ // hasStrongExternal + (any path/anchor) → ambiguous
358
+ // hasStrongExternal + nothing else → external
359
+ // NO strong external + (path OR anchor) → internal
360
+ // else → ambiguous
361
+
362
+ if (hasStrongExternal) {
363
+ if (matchedPath || matchedAnchor) {
364
+ const co = matchedPath ? `repo path "${matchedPath}"` : `anchor "${matchedAnchor}"`;
365
+ return {
366
+ ok: true, gap: trimmed, class: "ambiguous", route: "judge",
367
+ reason: `Ambiguous: a strong external signal (vendor "${matchedVendor}" + API term "${matchedApiTerm}") co-occurs with an internal ${co} — the mechanical classifier NEVER overrides an external signal (a claim can be about BOTH at once); the LLM judge decides (uncertain → research)`,
368
+ };
369
+ }
370
+ return {
371
+ ok: true, gap: trimmed, class: "external", route: "web",
372
+ reason: `External string-fact: vendor proper noun "${matchedVendor}" + API/protocol term "${matchedApiTerm}" (no internal path/anchor) — concerns an external system's contract`,
373
+ };
374
+ }
375
+
376
+ // No strong external signal at all → a concrete repo path or this-repo anchor is decisive
377
+ // internal (there is nothing external to override).
378
+ if (matchedPath) {
379
+ return {
380
+ ok: true, gap: trimmed, class: "internal", route: "grep",
381
+ reason: `Internal string-fact: repo path/file/tool shape "${matchedPath}" and NO strong external signal — concerns this repo's own files`,
382
+ };
383
+ }
384
+ if (matchedAnchor) {
385
+ return {
386
+ ok: true, gap: trimmed, class: "internal", route: "grep",
387
+ reason: `Internal string-fact: this-repo anchor "${matchedAnchor}" and NO strong external signal — concerns this repo's own code/structure`,
388
+ };
389
+ }
390
+
391
+ // EVERYTHING ELSE is AMBIGUOUS — no string fact at all (semantic placement is the LLM's
392
+ // call, not regex's). The wiring routes ambiguous → LLM judge; if the LLM is not
393
+ // confident, the claim is RESEARCHED (uncertain → verify, never guess).
394
+ return {
395
+ ok: true, gap: trimmed, class: "ambiguous", route: "judge",
396
+ reason: "Ambiguous — no strong external signal, no concrete repo path, no this-repo anchor. Semantic placement requires judgment, so the LLM judge decides; if not confident, the claim is researched (uncertain → verify, never guess-internal)",
397
+ };
398
+ }
399
+
400
+ // ---------------------------------------------------------------------------
401
+ // CLI entry point
402
+ // ---------------------------------------------------------------------------
403
+
404
+ if (require.main === module) {
405
+ // Usage: gsd-t-research-gate classify "<gap>" [--json]
406
+ const args = process.argv.slice(2);
407
+ const filteredArgs = args.filter((a) => a !== "--json");
408
+
409
+ function emit(obj) {
410
+ process.stdout.write(JSON.stringify(obj) + "\n");
411
+ }
412
+ function emitError(msg, exitCode) {
413
+ emit({ ok: false, error: msg });
414
+ process.exit(exitCode);
415
+ }
416
+
417
+ if (filteredArgs[0] !== "classify") {
418
+ emitError(
419
+ `Unknown subcommand "${filteredArgs[0] || ""}". Usage: gsd-t-research-gate classify "<gap>" [--json]`,
420
+ 64
421
+ );
422
+ }
423
+
424
+ const gap = filteredArgs[1];
425
+ if (gap === undefined || gap === null) {
426
+ emitError("Missing <gap> argument. Usage: gsd-t-research-gate classify \"<gap>\" [--json]", 64);
427
+ }
428
+
429
+ const result = classify(gap);
430
+ emit(result);
431
+ if (!result.ok) process.exit(1);
432
+ // Success → exit 0
433
+ }
434
+
435
+ // ---------------------------------------------------------------------------
436
+ // Module exports
437
+ // ---------------------------------------------------------------------------
438
+
439
+ module.exports = { classify };
package/bin/gsd-t.js CHANGED
@@ -1262,6 +1262,12 @@ const GLOBAL_BIN_TOOLS = [
1262
1262
  "gsd-t-model-tier-policy.cjs",
1263
1263
  // M86 — Model-profile config + resolver CLI (standard/pro/premium tier-spend switch).
1264
1264
  "gsd-t-model-profile.cjs",
1265
+ // M89 — Auto-research gate classifier (internal vs external claim routing; no LLM call).
1266
+ "gsd-t-research-gate.cjs",
1267
+ // M90 D1 — Architectural-assumption trigger (divergence-sampling + extend-signal; §2).
1268
+ "gsd-t-architectural-trigger.cjs",
1269
+ // M90 D2 — Loop ledger (non-convergence detection + halt directive; §3).
1270
+ "gsd-t-loop-ledger.cjs",
1265
1271
  ];
1266
1272
 
1267
1273
  function installGlobalBinTools() {
@@ -2563,6 +2569,16 @@ const PROJECT_BIN_TOOLS = [
2563
2569
  "gsd-t-model-tier-policy.cjs",
2564
2570
  // M86 — Model-profile config + resolver CLI (standard/pro/premium tier-spend switch).
2565
2571
  "gsd-t-model-profile.cjs",
2572
+ // M89 — Auto-research gate classifier (classify a guessed claim as internal vs external;
2573
+ // propagated to each registered project's bin/ so the workflow runCli fallback resolves
2574
+ // downstream — per [[project_global_bin_propagation_gap]]).
2575
+ "gsd-t-research-gate.cjs",
2576
+ // M90 D1 — Architectural-assumption trigger (divergence-sampling + extend-signal; §2).
2577
+ // Propagated so project-local runCli helpers can invoke it without the global binary.
2578
+ "gsd-t-architectural-trigger.cjs",
2579
+ // M90 D2 — Loop ledger (non-convergence detection + halt directive; §3).
2580
+ // Propagated so project-local runCli helpers (gsd-t-debug.workflow.js) can invoke it.
2581
+ "gsd-t-loop-ledger.cjs",
2566
2582
  ];
2567
2583
 
2568
2584
  // Files that older versions of this installer copied into project bin/ but
@@ -4339,7 +4355,10 @@ function showHelp() {
4339
4355
  log(` ${CYAN}graph${RESET} Code graph operations (index, status, query)`);
4340
4356
  log(` ${CYAN}headless${RESET} Non-interactive execution via claude -p + fast state queries`);
4341
4357
  log(` ${CYAN}design-build${RESET} Deterministic design→code pipeline (elements → widgets → pages)`);
4342
- log(` ${CYAN}help${RESET} Show this help\n`);
4358
+ log(` ${CYAN}research-gate${RESET} Classify a guessed claim as internal (grep) or external (web-research)`);
4359
+ log(` ${CYAN}architectural-trigger${RESET} Fire the arch-assumption trigger (divergence-sampling | extend-signal)`);
4360
+ log(` ${CYAN}loop-ledger${RESET} Record a debug cycle, read exit-state, or clear re-examination flag`);
4361
+ log(` ${CYAN}help${RESET} Show this help\n`);
4343
4362
  log(`${BOLD}Examples:${RESET}`);
4344
4363
  log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t install`);
4345
4364
  log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t init my-saas-app`);
@@ -4692,6 +4711,49 @@ if (require.main === module) {
4692
4711
  });
4693
4712
  process.exit(res.status == null ? 1 : res.status);
4694
4713
  }
4714
+ case "research-gate": {
4715
+ // M89 — `gsd-t research-gate classify "<claim>"` thin dispatcher to the
4716
+ // auto-research gate classifier (internal vs external routing; no LLM call).
4717
+ const { spawnSync } = require("child_process");
4718
+ const js = path.join(__dirname, "gsd-t-research-gate.cjs");
4719
+ if (!require("node:fs").existsSync(js)) {
4720
+ error(`gsd-t-research-gate.cjs not found at ${js} — install or build M89-D1 first`);
4721
+ process.exit(1);
4722
+ }
4723
+ const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
4724
+ stdio: "inherit",
4725
+ });
4726
+ process.exit(res.status == null ? 1 : res.status);
4727
+ }
4728
+ case "architectural-trigger": {
4729
+ // M90 D1 — `gsd-t architectural-trigger <subcommand>` thin dispatcher to
4730
+ // the architectural-assumption trigger (divergence-sampling + extend-signal; §2).
4731
+ const { spawnSync } = require("child_process");
4732
+ const js = path.join(__dirname, "gsd-t-architectural-trigger.cjs");
4733
+ if (!require("node:fs").existsSync(js)) {
4734
+ error(`gsd-t-architectural-trigger.cjs not found at ${js} — install or build M90-D1 first`);
4735
+ process.exit(1);
4736
+ }
4737
+ const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
4738
+ stdio: "inherit",
4739
+ });
4740
+ process.exit(res.status == null ? 1 : res.status);
4741
+ }
4742
+ case "loop-ledger": {
4743
+ // M90 D2 — `gsd-t loop-ledger <subcommand>` thin dispatcher to the loop-ledger
4744
+ // (non-convergence detection + halt; §3). Subcommands: append-cycle, read-exit-state,
4745
+ // record-re-examination.
4746
+ const { spawnSync } = require("child_process");
4747
+ const js = path.join(__dirname, "gsd-t-loop-ledger.cjs");
4748
+ if (!require("node:fs").existsSync(js)) {
4749
+ error(`gsd-t-loop-ledger.cjs not found at ${js} — install or build M90-D2 first`);
4750
+ process.exit(1);
4751
+ }
4752
+ const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
4753
+ stdio: "inherit",
4754
+ });
4755
+ process.exit(res.status == null ? 1 : res.status);
4756
+ }
4695
4757
  case "metrics":
4696
4758
  doMetrics(args.slice(1));
4697
4759
  break;
@@ -73,6 +73,9 @@ HEADLESS (CI/CD) CLI
73
73
  headless query Read project state without LLM (<100ms)
74
74
  headless --debug-loop Compaction-proof test-fix-retest loop (fresh sessions)
75
75
  parallel Task-level parallel dispatch with mode-aware gating (M44)
76
+ research-gate Classify a guessed claim: internal (grep) / external (web) / ambiguous (LLM judge) [M89]
77
+ architectural-trigger Fire the architectural-assumption trigger (divergence-sampling or extend-existing-code) [M90]
78
+ loop-ledger Record a debug cycle, read exit state, detect non-convergence (premise-re-examination) [M90]
76
79
 
77
80
  BACKLOG Manual
78
81
  ───────────────────────────────────────────────────────────────────────────────
@@ -516,6 +519,28 @@ Use these when user asks for help on a specific command:
516
519
  - **Out of scope**: Session default model (`/model`) — profiles govern WORKFLOW STAGES only.
517
520
  - **Contract**: `.gsd-t/contracts/model-profile-config-contract.md` v1.0.0 STABLE.
518
521
 
522
+ ### research-gate (M89)
523
+ - **Summary**: Deterministic CLASSIFY step of the M89 auto-research pipeline. Takes a load-bearing claim the agent tagged `[GUESSED:*]` in its Stated Claims section. The classifier is a MECHANICAL STRING-FACT FILTER (NOT a semantic oracle) returning one of THREE classes: `internal` (grep/Read, route `grep`), `external` (web-research, route `web`), or `ambiguous` (route `judge`). No LLM call in the classifier itself — semantic judgment is deferred to the wiring's LLM judge. An `ambiguous` claim is routed to a small LLM `classify-judge` stage (model: "fable") that decides internal/external/uncertain; **uncertain → research** (never guess-internal). The research `agent()` stage (model: "fable") runs for external guesses. An ENFORCE marker (`<!-- auto-research-claim: ... status=uncited -->`) is written into the artifact (a deterministic `.gsd-t/research/...` fallback if the stage reports no artifact path — fail-closed); the verify gate FAILs if it stays uncited.
524
+ - **Files**: `bin/gsd-t-research-gate.cjs` (D1). Contract: `.gsd-t/contracts/auto-research-contract.md` v1.3.2 STABLE. Stage prompt: `templates/prompts/research-subagent.md`. DETECT snippet: `templates/prompts/stated-claims-snippet.md`. Cite-format test: `test/m89-research-stage-cite-format.test.js`.
525
+ - **Use when**: A workflow stage needs to classify a guessed claim before deciding whether to grep, run a web-research sub-agent, or route to the LLM judge. Called by D3 (upper phases: plan, pre-mortem, partition, discuss, milestone) and D4 (worker phases: execute, debug, quick). NOT called manually — the wiring invokes it per `[GUESSED:*]` entry in the `## Stated Claims` section.
526
+ - **CLI**: `gsd-t research-gate classify "<guessed claim text>"`. Emits `{ok, gap, class, route, reason}` where `class` ∈ {internal, external, ambiguous} and `route` ∈ {grep, web, judge}. Exit 0 on classify · 1 on bad input.
527
+ - **Classification rules (string facts only — the v1.3.2 4-step priority)**: (1) a CONCRETE REPO PATH (`bin/x.cjs` / `*.workflow.js` / `gsd-t-*` / `templates/…`) → `internal` (a path is decisive, beats everything). (2) a STRONG EXTERNAL signal (unambiguous vendor proper-noun + API/protocol term) → at least ambiguous: with ANY anchor phrase present → `ambiguous`; with no anchor → `external`. NO anchor phrase ("this repo" / "exit code" / "who owns") may override a strong external signal — only a concrete path can. (3) an anchor with NO strong external → `internal`. (4) else → `ambiguous`. An `ambiguous` claim → LLM judge → uncertain→research (§1.1/§5.1, owned by D3/D4). The regex never guesses a paraphrase's semantic class.
528
+ - **Contract**: `.gsd-t/contracts/auto-research-contract.md` v1.3.2 STABLE.
529
+
530
+ ### architectural-trigger (M90)
531
+ - **Summary**: Deterministic architectural-assumption trigger (M90 §2). Two fire paths: **R-ARCH-1** (divergence-sampling — N fresh-context producer answers → divergence score; competition-arm only, EXPERIMENTAL+MEASURED) and **R-ARCH-2** (protocol-class — any task whose `**Touches**` lists an EXISTING file triggers unconditionally; everywhere feed). Fires produce an instrumentation record in `.gsd-t/metrics/arch-trigger-events.jsonl`; a `provenByAdversaryOnly=true` flag from either path surfaces at the §4 fail-closed verify gate (R-FAIL-2). The module NEVER asserts it works — it emits measurement data only.
532
+ - **Files**: `bin/gsd-t-architectural-trigger.cjs`. Contract: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` §2. Blind-adversary prompt: `templates/prompts/blind-adversary-subagent.md`. Model: `fable` (M85 RULE-ARCH-TIER).
533
+ - **Use when**: Workflows detect that a task extends existing code (R-ARCH-2 everywhere feed) or that competition producers diverged (R-ARCH-1 competition-arm only). Called automatically by `gsd-t-execute`, `gsd-t-quick`, and `gsd-t-phase` competition arm via inline runCli helper (M81 runtime-native).
534
+ - **CLI**: `gsd-t architectural-trigger trigger '<JSON>'`. JSON shapes: `{"type":"extend-existing-code","context":"...","basis":"..."}` or `{"type":"divergence-sampling","answers":[...],"basis":"..."}`. Emits `{ok, firePath, fired, basis, reason, ...}`. Exit 0 on success · 1 on bad input.
535
+ - **Contract**: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` §2 v1.0.0 STABLE.
536
+
537
+ ### loop-ledger (M90)
538
+ - **Summary**: Cross-process non-convergence detector (M90 §3). Records each debug cycle (symptom-signature + surface + fileClass), computes a stable signature, and detects when the SAME computed signature appears across cycles (non-convergence). After all cycles, `read-exit-state` returns `haltedButNoReExamination=true` when non-convergence is proven — the debug workflow exits with a PREMISE_RE_EXAMINATION directive (option b, re-anchored to the cycle-2 boundary) instead of the generic `needs-human`. A `haltedButNoReExamination=true` state that reaches verify triggers R-FAIL-3 (fail-closed).
539
+ - **Files**: `bin/gsd-t-loop-ledger.cjs`. Contract: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` §3. Ledger stored at `.gsd-t/ledgers/loop-ledger.jsonl` (per project).
540
+ - **Use when**: Debug workflow calls `append-cycle` after each iteration and `read-exit-state` after the loop. NOT called manually — the debug workflow wires it via inline runCli helper.
541
+ - **CLI**: `gsd-t loop-ledger append-cycle --assertion "<symptom>" --surface "<file>" --fileClass unit --projectDir <dir>`. `gsd-t loop-ledger read-exit-state --projectDir <dir>`. Exit 0 on success.
542
+ - **Contract**: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` §3 v1.0.0 STABLE.
543
+
519
544
  ## Unknown Command
520
545
 
521
546
  If user asks for help on unrecognized command:
@@ -71,6 +71,7 @@
71
71
  | REQ-060 | Quality North Star Persona — project CLAUDE.md can define a `## Quality North Star` section (1-3 sentences) with a project quality identity. gsd-t-init auto-detects preset (library/web-app/cli) or prompts user. gsd-t-setup offers persona config for existing projects. Persona is injected at subagent spawn time; skips silently if section absent (backward compatible). | P2 | complete | M32: templates/CLAUDE-project.md, gsd-t-init.md, gsd-t-setup.md |
72
72
  | REQ-061 | Design Brief Generation — during partition, if UI/frontend signals detected (React/Vue/Svelte/Flutter, CSS/SCSS, component files, or Tailwind config), generate `.gsd-t/contracts/design-brief.md` with color palette, typography, spacing, component patterns, layout principles, interaction patterns, and tone/voice. Skip for non-UI projects. Do not overwrite existing briefs. Referenced in plan for UI task descriptions. | P2 | complete | M32: gsd-t-partition.md, gsd-t-plan.md, gsd-t-setup.md |
73
73
  | REQ-062 | Exploratory Testing Blocks — after scripted tests pass, if Playwright MCP is registered, QA agents get 3 minutes and Red Team gets 5 minutes of interactive exploration using Playwright MCP. All findings tagged [EXPLORATORY] in qa-issues.md and red-team-report.md. Feeds into M31 QA calibration as separate category. Silent skip when Playwright MCP absent. Injected into execute, quick, integrate, debug. | P2 | complete | M32: gsd-t-execute.md, gsd-t-quick.md, gsd-t-integrate.md, gsd-t-debug.md |
74
+ | REQ-094 | Auto-Research Gate (M89) — deterministic CLASSIFY + cite-or-fail ENFORCE wrapped around an LLM-prompted DETECT step. For each load-bearing claim, the agent tags it KNOWN vs GUESSED (unknown / assumed / stale) in a `## Stated Claims` section (§6.5 DETECT seam). A GUESSED claim is classified by `gsd-t research-gate classify "<claim>"` (D1, `bin/gsd-t-research-gate.cjs`): external → a Fable-tier web-research stage writes a `## Verified Facts (auto-research)` block (URL + fetch date) into the artifact; internal → grep/Read only. A classify-time ENFORCE marker (`<!-- auto-research-claim: ... status=uncited -->`) is written for every external guess; the verify gate FAILs if any marker stays `status=uncited` (no silent guessing). Idempotency: "covers" is exact normalized-gap-key match, NOT fuzzy/substring. SC6 conversation-scope directive: when answering the user about an external/time-varying fact, verify-or-flag before asserting. Three guess-types: unknown / assumed / stale; staleness defaults to fail-toward-verify. | P1 | complete | M89: bin/gsd-t-research-gate.cjs (D1), auto-research-contract.md v1.2.0 (D2), templates/prompts/research-subagent.md (D2), templates/prompts/stated-claims-snippet.md (D2), test/m89-research-stage-cite-format.test.js (D2); wiring: D3 upper phases, D4 worker phases |
74
75
 
75
76
  ## Technical Requirements
76
77
 
@@ -984,6 +985,22 @@ Supporting contracts (to be written during D1):
984
985
  | REQ-M86-05 | Active profile surfaced NAMED at every surface (SC f): `[GSD-T PROFILE]` banner token (GSD-T projects only), statusline segment, `gsd-t status` line, CLI envelopes — global default named when config absent; hook/statusline resilient to resolver absence/errors (exit 0, `[GSD-T NOW]` intact). | m86-d4 | m86-surfacing | complete (M86) |
985
986
  | REQ-M86-06 | Installer self-protection: `copyBinToolsToProject` skips the GSD-T source repo (copy loop AND stray sweep) — propagation must never revert in-flight source work; CLI carries a version-skew guard (structured error on pre-M86 sibling module). | m86-d1 (verify fix) | r2 killing tests | complete (M86) |
986
987
 
988
+ ## M90 The Unproven-Assumption Doctrine (complete - v4.7.10)
989
+
990
+ Externalize + force, never introspect: a deterministic TRIGGER + EXTERNAL response (blind-adversary / executable spike), protocol-forced research where self-detection is unreliable, fail-closed on uncertainty. The milestone obeys its own doctrine. SC criteria authored at plan time (M83 pre-mortem #5) so the traceability gate checks against real requirements; D4-T6 only ripples surrounding prose + version.
991
+
992
+ Contract: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` v1.0.0 STABLE
993
+
994
+ | ID | Requirement | Domain | Task | Status |
995
+ |----|-------------|--------|------|--------|
996
+ | REQ-M90-01 (SC-NO-FINITE-LIST) | The classifier's INTERNAL decision enumerates NO open category — `internal` is asserted ONLY on a concrete own-repo path/anchor, never a vendor's absence (already true on disk; M90 asserts + tests it). The vendor list is KEPT as an `external→web` upgrade (premise-corrected: deleting it is a regression, not a silent-miss fix). ≥10 unseen vendors route judge/research, none silently-internal. | m90-d-factual-redesign | D3-T0/T1/T5 | complete (M90) |
997
+ | REQ-M90-02 (SC-FACTUAL-PRESERVED) | M89's factual auto-research (classifier + research + cite + §7 fail-closed gate) stays green; suite ≥ the ed03a8d baseline (1824/0); held-out rows HO-E1/E2/E5 still classify `external→web`. | m90-d-factual-redesign | D3-T4/T5 | complete (M90) |
998
+ | REQ-M90-03 (SC-ARCH-TRIGGER) | An architectural-assumption trigger fires on a divergence signal (N fresh-context answers, phase/competition-only) OR an extend-existing-code signal (everywhere), deterministically, EXPERIMENTAL+MEASURED (instrumented fire-rate, never a silent claim it works). Prove-or-kill: a falsifiable held-out divergence split with a NAMED on-disk source, else R1 re-scope DOWN to factual-only. | m90-d-arch-trigger-response | D1-T1..T6 | complete (M90) |
999
+ | REQ-M90-04 (SC-LOOP-HOOK-FIRES) | On the 3rd same-computed-signature debug cycle the loop-ledger HARD-HALTS the patch path and emits a premise-re-examination directive — a returned ledger fact (cross-process persisted), NOT narration. Wired into the debug workflow via option (b): re-anchor the halt to the cycle-2 boundary. | m90-d-loop-ledger-halt + m90-d-contract-doctrine-integrate | D2-T3/T6, D4-T3 | complete (M90) |
1000
+ | REQ-M90-05 (SC-FAIL-CLOSED) | Verify FAILS (never warns-and-proceeds) on an uncited external claim, an unresolved proven-by-adversary-only premise, or a halted-but-no-re-examination ledger state. When a flag-producing mechanism is R1-de-scoped, the read is a documented no-op-PASS distinguishable from a wired-but-broken vacuous pass. | m90-d-contract-doctrine-integrate | D4-T5 | complete (M90) |
1001
+ | REQ-M90-06 (SC-SELF-OBEDIENCE) | M90's own artifacts show the doctrine applied to itself: discuss produced a sourced approach, pseudocode signed off before code, premises re-verified on disk, and any ≥3-cycle same-signature non-convergence triggered a recorded premise re-examination (not a variant patch). Every §6 [RULE] traces to an enforcement point (orphan rule FAILS the guard-map test). | m90-d-contract-doctrine-integrate | D4-T1/T7 | complete (M90) |
1002
+ | REQ-M90-07 (SC-RESEARCH-GATE) | Plan is mechanically blocked until discuss emits a sourced approach with ≥1 external citation per detection mechanism. | m90-d-contract-doctrine-integrate (doctrine contract) | D4-T1 | satisfied (discuss `.gsd-t/discuss/M90-approach-sourced.md`) |
1003
+
987
1004
  ## Updated Functional Requirements (scan findings - v4.0.27)
988
1005
 
989
1006
  The deep scan identified functional deficiencies not captured in previous requirements. These are recorded here for tracking:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "4.6.11",
3
+ "version": "4.7.10",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",