@tekyzinc/gsd-t 4.6.11 → 4.7.11

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,14 @@ 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",
1271
+ // Backlog #40 — deterministic archive+sweep of a completed milestone's domain dirs.
1272
+ "gsd-t-archive-domains.cjs",
1265
1273
  ];
1266
1274
 
1267
1275
  function installGlobalBinTools() {
@@ -2563,6 +2571,19 @@ const PROJECT_BIN_TOOLS = [
2563
2571
  "gsd-t-model-tier-policy.cjs",
2564
2572
  // M86 — Model-profile config + resolver CLI (standard/pro/premium tier-spend switch).
2565
2573
  "gsd-t-model-profile.cjs",
2574
+ // M89 — Auto-research gate classifier (classify a guessed claim as internal vs external;
2575
+ // propagated to each registered project's bin/ so the workflow runCli fallback resolves
2576
+ // downstream — per [[project_global_bin_propagation_gap]]).
2577
+ "gsd-t-research-gate.cjs",
2578
+ // M90 D1 — Architectural-assumption trigger (divergence-sampling + extend-signal; §2).
2579
+ // Propagated so project-local runCli helpers can invoke it without the global binary.
2580
+ "gsd-t-architectural-trigger.cjs",
2581
+ // M90 D2 — Loop ledger (non-convergence detection + halt directive; §3).
2582
+ // Propagated so project-local runCli helpers (gsd-t-debug.workflow.js) can invoke it.
2583
+ "gsd-t-loop-ledger.cjs",
2584
+ // Backlog #40 — deterministic archive+sweep of a completed milestone's domain dirs
2585
+ // (complete-milestone Step 7). Propagated so complete-milestone can invoke it project-local.
2586
+ "gsd-t-archive-domains.cjs",
2566
2587
  ];
2567
2588
 
2568
2589
  // Files that older versions of this installer copied into project bin/ but
@@ -4339,7 +4360,10 @@ function showHelp() {
4339
4360
  log(` ${CYAN}graph${RESET} Code graph operations (index, status, query)`);
4340
4361
  log(` ${CYAN}headless${RESET} Non-interactive execution via claude -p + fast state queries`);
4341
4362
  log(` ${CYAN}design-build${RESET} Deterministic design→code pipeline (elements → widgets → pages)`);
4342
- log(` ${CYAN}help${RESET} Show this help\n`);
4363
+ log(` ${CYAN}research-gate${RESET} Classify a guessed claim as internal (grep) or external (web-research)`);
4364
+ log(` ${CYAN}architectural-trigger${RESET} Fire the arch-assumption trigger (divergence-sampling | extend-signal)`);
4365
+ log(` ${CYAN}loop-ledger${RESET} Record a debug cycle, read exit-state, or clear re-examination flag`);
4366
+ log(` ${CYAN}help${RESET} Show this help\n`);
4343
4367
  log(`${BOLD}Examples:${RESET}`);
4344
4368
  log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t install`);
4345
4369
  log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t init my-saas-app`);
@@ -4692,6 +4716,63 @@ if (require.main === module) {
4692
4716
  });
4693
4717
  process.exit(res.status == null ? 1 : res.status);
4694
4718
  }
4719
+ case "research-gate": {
4720
+ // M89 — `gsd-t research-gate classify "<claim>"` thin dispatcher to the
4721
+ // auto-research gate classifier (internal vs external routing; no LLM call).
4722
+ const { spawnSync } = require("child_process");
4723
+ const js = path.join(__dirname, "gsd-t-research-gate.cjs");
4724
+ if (!require("node:fs").existsSync(js)) {
4725
+ error(`gsd-t-research-gate.cjs not found at ${js} — install or build M89-D1 first`);
4726
+ process.exit(1);
4727
+ }
4728
+ const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
4729
+ stdio: "inherit",
4730
+ });
4731
+ process.exit(res.status == null ? 1 : res.status);
4732
+ }
4733
+ case "architectural-trigger": {
4734
+ // M90 D1 — `gsd-t architectural-trigger <subcommand>` thin dispatcher to
4735
+ // the architectural-assumption trigger (divergence-sampling + extend-signal; §2).
4736
+ const { spawnSync } = require("child_process");
4737
+ const js = path.join(__dirname, "gsd-t-architectural-trigger.cjs");
4738
+ if (!require("node:fs").existsSync(js)) {
4739
+ error(`gsd-t-architectural-trigger.cjs not found at ${js} — install or build M90-D1 first`);
4740
+ process.exit(1);
4741
+ }
4742
+ const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
4743
+ stdio: "inherit",
4744
+ });
4745
+ process.exit(res.status == null ? 1 : res.status);
4746
+ }
4747
+ case "loop-ledger": {
4748
+ // M90 D2 — `gsd-t loop-ledger <subcommand>` thin dispatcher to the loop-ledger
4749
+ // (non-convergence detection + halt; §3). Subcommands: append-cycle, read-exit-state,
4750
+ // record-re-examination.
4751
+ const { spawnSync } = require("child_process");
4752
+ const js = path.join(__dirname, "gsd-t-loop-ledger.cjs");
4753
+ if (!require("node:fs").existsSync(js)) {
4754
+ error(`gsd-t-loop-ledger.cjs not found at ${js} — install or build M90-D2 first`);
4755
+ process.exit(1);
4756
+ }
4757
+ const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
4758
+ stdio: "inherit",
4759
+ });
4760
+ process.exit(res.status == null ? 1 : res.status);
4761
+ }
4762
+ case "archive-domains": {
4763
+ // Backlog #40 — `gsd-t archive-domains --domains a,b --archive <dir>` deterministic
4764
+ // archive+sweep of a completed milestone's domain dirs (complete-milestone Step 7).
4765
+ const { spawnSync } = require("child_process");
4766
+ const js = path.join(__dirname, "gsd-t-archive-domains.cjs");
4767
+ if (!require("node:fs").existsSync(js)) {
4768
+ error(`gsd-t-archive-domains.cjs not found at ${js} — install or build backlog #40 first`);
4769
+ process.exit(1);
4770
+ }
4771
+ const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
4772
+ stdio: "inherit",
4773
+ });
4774
+ process.exit(res.status == null ? 1 : res.status);
4775
+ }
4695
4776
  case "metrics":
4696
4777
  doMetrics(args.slice(1));
4697
4778
  break;
@@ -401,8 +401,21 @@ node scripts/gsd-t-watch-state.js advance --agent-id "$GSD_T_AGENT_ID" --parent-
401
401
 
402
402
  Reset `.gsd-t/` for next milestone:
403
403
 
404
- 1. Archive current domains `.gsd-t/milestones/{name}/domains/`
405
- 2. Clear `.gsd-t/domains/` (empty, ready for next partition)
404
+ 1. **Archive + sweep the completed milestone's domains DETERMINISTICALLY (backlog #40, NOT prose).**
405
+ This step was prose-only for ~30 milestones and got skipped, accumulating 77 stale domain dirs
406
+ that polluted the file-disjointness oracle. Run the helper with the EXPLICIT set of THIS
407
+ milestone's domain dirs (the partition's domain set — NOT a blanket wipe; a later still-active
408
+ milestone may legitimately have live domains):
409
+ ```bash
410
+ gsd-t archive-domains --domains "{comma-separated domain dir names for THIS milestone}" \
411
+ --archive ".gsd-t/milestones/{name}-{date}" --projectDir .
412
+ ```
413
+ (prefer the project-local `bin/gsd-t-archive-domains.cjs` when present). It (a) copies each named
414
+ domain → `<archive>/domains/<name>/`, then (b) removes it from `.gsd-t/domains/`. Idempotent
415
+ (re-running is a no-op), containment-guarded (refuses any name with path separators / dot-segments
416
+ or resolving outside `.gsd-t/domains/`), and fail-closed (a bad name aborts the whole batch — no
417
+ partial sweep). Domains belonging to other (still-active) milestones are left untouched.
418
+ 2. (Step 1 already cleared this milestone's domains from `.gsd-t/domains/` — do NOT blanket-wipe the dir.)
406
419
  3. Archive current reports → milestone folder
407
420
  4. Clear `.gsd-t/impact-report.md`, `.gsd-t/test-coverage.md`
408
421
  5. Update `.gsd-t/progress.md`:
@@ -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: