@tekyzinc/gsd-t 4.7.11 → 4.8.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 +20 -0
- package/README.md +1 -1
- package/bin/gsd-t-divergence-grammar.cjs +213 -0
- package/bin/gsd-t-guard-map-derive.cjs +210 -0
- package/bin/gsd-t-guard-map.cjs +279 -0
- package/bin/gsd-t-milestone-state.cjs +236 -0
- package/bin/gsd-t-rule-consume.cjs +141 -0
- package/bin/gsd-t-traceability-gate.cjs +321 -24
- package/bin/gsd-t.js +8 -0
- package/commands/gsd-t-doc-ripple.md +1 -1
- package/commands/gsd-t-milestone.md +33 -1
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +2 -0
- package/templates/PseudoCode-spec.md +194 -0
- package/templates/prompts/keep-or-supersede-subagent.md +61 -0
- package/templates/prompts/qa-subagent.md +10 -0
- package/templates/prompts/red-team-subagent.md +12 -0
- package/templates/workflows/gsd-t-phase.workflow.js +22 -2
- package/templates/workflows/gsd-t-verify.workflow.js +101 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [4.8.10] - 2026-06-22 (M91 — PseudoCode Source-of-Truth, merged M87+M88 — minor)
|
|
6
|
+
|
|
7
|
+
### Added — the intention-first PseudoCode behavior map becomes the milestone source-of-truth
|
|
8
|
+
|
|
9
|
+
A `PseudoCode-[Title].md` authored BEFORE the build (a two-altitude behavior map: high-level approach → detailed per-step intention+mechanism) is now the milestone source-of-truth, enforced by deterministic gates. Eight file-disjoint domains across three waves, single-session build; full suite 2131 / 2127 pass / 0 fail / 4 skip.
|
|
10
|
+
|
|
11
|
+
**M87 core (Waves 1–2):**
|
|
12
|
+
- **Guard-map verify gate** (`bin/gsd-t-guard-map.cjs`) — parses the `[RULE …]` guard map (3 grammar forms, side-agnostic, derives `R-<SLUG>-<NN>` ids), gates a build→rule map (exit 0 / 4-names-the-rule / 64); wired FAIL-blocking into `gsd-t-verify.workflow.js` before the triad, with §7 doc+map discovery and distinct logged skips (`no-build-map` / `no-pseudocode-docs`).
|
|
13
|
+
- **Section-citation traceability** (`bin/gsd-t-traceability-gate.cjs` extended) — tasks cite `**PseudoCode-Section**: <Title>#<anchor>`; a section with no citing task is a structural coverage gap; slug-as-slug resolution, never substring. Plus a milestone→domains scoping fix (`--domains`; zero-prefix-match no longer falls back to all — exit 64 `milestone-scope-unresolved`) and a path-containment guard (`domain-path-escape`).
|
|
14
|
+
- **Two-altitude milestone flow** (`commands/gsd-t-milestone.md` + `gsd-t-phase.workflow.js`) — high-level approach sign-off precedes the detailed doc; competition solution-space probe shifts up to the approach altitude. Keep-or-supersede prompt protocol writes a `⚠ Divergence` flag on supersede.
|
|
15
|
+
- **Template + doc-ripple** — `templates/PseudoCode-spec.md` mold; `PseudoCode-[Title].md` joins the Living Documents ripple set (region-scoped A4 drift lint).
|
|
16
|
+
|
|
17
|
+
**M88 deterministic gates (Wave 3):**
|
|
18
|
+
- **Sign-off state** (`bin/gsd-t-milestone-state.cjs`) — `<!-- signed-off: … -->` marker + `isDefined(docs)` predicate; unsigned ≠ DEFINED; skip is a logged decision, never silent.
|
|
19
|
+
- **Build→map derivation** (`bin/gsd-t-guard-map-derive.cjs`) — mechanical evidence→map seam (imports the guard-map parser), end-to-end derive→gate test.
|
|
20
|
+
- **Triad-consumption seam** (`bin/gsd-t-rule-consume.cjs` + ingest directives in qa/red-team prompts) — deterministic seam-check, no live triad.
|
|
21
|
+
- **Divergence grammar** (`bin/gsd-t-divergence-grammar.cjs`) — `parseDivergence`/`formatDivergence` byte-stable round-trip + `countDivergences`.
|
|
22
|
+
|
|
23
|
+
Every gate meets the deterministic-gate bar: deterministic code, zero LLM judgment, structural-not-substring, killing test vs byte-verbatim fixtures, fail-closed. Verify: QA PASS (mutation-tested), Red Team GRUDGING-PASS (one path-traversal MEDIUM fixed in-verify; fence-awareness MEDIUM/LOW deferred to backlog #45, all fail-closed). Contract: `pseudocode-source-of-truth-contract.md` v1.1.5 STABLE.
|
|
24
|
+
|
|
5
25
|
## [4.7.11] - 2026-06-22 (backlog #40 — deterministic domain archive+sweep — patch)
|
|
6
26
|
|
|
7
27
|
### Fixed — complete-milestone now deterministically archives + sweeps a milestone's domains
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v4.
|
|
3
|
+
**v4.8.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,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* gsd-t-divergence-grammar — M88 G4 (M91 Wave 3)
|
|
5
|
+
*
|
|
6
|
+
* The deterministic round-trip over the `⚠ Divergence` flag grammar.
|
|
7
|
+
*
|
|
8
|
+
* Contract: .gsd-t/contracts/pseudocode-source-of-truth-contract.md §4 — the
|
|
9
|
+
* SINGLE source of truth for the grammar. This module IMPLEMENTS that spec; it
|
|
10
|
+
* does NOT redefine it. The canonical §4 form is, EXACTLY:
|
|
11
|
+
*
|
|
12
|
+
* ⚠ Divergence: <RULE-ID or section> — supersedes shipped <what>. Reason: <user intention>.
|
|
13
|
+
*
|
|
14
|
+
* ORIGIN: M87's D3 (`keep-or-supersede-subagent.md`) ships the keep-or-supersede
|
|
15
|
+
* ASK and the prose WRITING of a `⚠ Divergence` flag on every supersede. That
|
|
16
|
+
* makes the flag a string IN the doc — but not yet a code-checkable artifact.
|
|
17
|
+
* This module closes that gap: a `⚠ Divergence` line round-trips
|
|
18
|
+
* format→parse→format BYTE-STABLE, a malformed flag FAILS (named, no throw), and
|
|
19
|
+
* the divergence COUNT over a doc is emitted as a checkable JSON integer that can
|
|
20
|
+
* feed D1's guard-map rule set.
|
|
21
|
+
*
|
|
22
|
+
* Hard rules (contract §4 + domain constraints):
|
|
23
|
+
* - Zero external runtime deps.
|
|
24
|
+
* - Never throws. A parse failure is a RETURNED, NAMED error, not an exception
|
|
25
|
+
* (fail-closed).
|
|
26
|
+
* - Pure. parseDivergence / formatDivergence / countDivergences have no I/O and
|
|
27
|
+
* no side effects (the CLI wrapper at the bottom is the only I/O).
|
|
28
|
+
* - Round-trip is byte-stable: formatDivergence(parseDivergence(line).value)
|
|
29
|
+
* === line for every valid flag.
|
|
30
|
+
* - Structural marker parse, never a prose substring scan
|
|
31
|
+
* (feedback_coverage_check_structural_not_substring): countDivergences only
|
|
32
|
+
* counts lines that PARSE as a well-formed §4 flag, not lines that merely
|
|
33
|
+
* CONTAIN the substring "⚠ Divergence".
|
|
34
|
+
*
|
|
35
|
+
* CLI:
|
|
36
|
+
* --parse "<line>" --json → { ok, value|error }
|
|
37
|
+
* --count <docPath> --json → { ok, count } (count is the checkable artifact)
|
|
38
|
+
*
|
|
39
|
+
* Exit: 0 ok · 4 parse failure / count error · 64 bad input.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// --- §4 grammar literals (the EXACT canonical form — single source is the contract) ---
|
|
43
|
+
const PREFIX = "⚠ Divergence: "; // "⚠ Divergence: "
|
|
44
|
+
const SEP_SUPERSEDES = " — supersedes shipped "; // " — supersedes shipped " (em-dash U+2014)
|
|
45
|
+
const SEP_REASON = ". Reason: ";
|
|
46
|
+
const SUFFIX = ".";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse a single line as a §4 `⚠ Divergence` flag.
|
|
50
|
+
*
|
|
51
|
+
* Structural, anchored parse — NOT a substring scan. The line MUST be EXACTLY
|
|
52
|
+
* `⚠ Divergence: <ref> — supersedes shipped <what>. Reason: <reason>.` with no
|
|
53
|
+
* leading/trailing slop. Each field MUST be non-empty.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} line
|
|
56
|
+
* @returns {{ok: true, value: {ref: string, supersedes: string, reason: string}}
|
|
57
|
+
* | {ok: false, error: string}} never throws.
|
|
58
|
+
*/
|
|
59
|
+
function parseDivergence(line) {
|
|
60
|
+
if (typeof line !== "string") {
|
|
61
|
+
return { ok: false, error: "not-a-string" };
|
|
62
|
+
}
|
|
63
|
+
// Anchored: must START with the prefix (no leading slop — structural, not substring).
|
|
64
|
+
if (!line.startsWith(PREFIX)) {
|
|
65
|
+
return { ok: false, error: "missing-prefix" };
|
|
66
|
+
}
|
|
67
|
+
// Must END with the trailing period.
|
|
68
|
+
if (!line.endsWith(SUFFIX)) {
|
|
69
|
+
return { ok: false, error: "missing-trailing-period" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ref ... " — supersedes shipped " ... <what>
|
|
73
|
+
const afterPrefix = line.slice(PREFIX.length);
|
|
74
|
+
const supIdx = afterPrefix.indexOf(SEP_SUPERSEDES);
|
|
75
|
+
if (supIdx === -1) {
|
|
76
|
+
return { ok: false, error: "missing-supersedes-clause" };
|
|
77
|
+
}
|
|
78
|
+
const ref = afterPrefix.slice(0, supIdx);
|
|
79
|
+
const afterSup = afterPrefix.slice(supIdx + SEP_SUPERSEDES.length);
|
|
80
|
+
|
|
81
|
+
// <what> ". Reason: " <reason> "."
|
|
82
|
+
// Use the LAST ". Reason: " as the boundary so a <what> that itself contains
|
|
83
|
+
// ". Reason: " can never steal the reason field (round-trip stays exact).
|
|
84
|
+
const reasonIdx = afterSup.lastIndexOf(SEP_REASON);
|
|
85
|
+
if (reasonIdx === -1) {
|
|
86
|
+
return { ok: false, error: "missing-reason-clause" };
|
|
87
|
+
}
|
|
88
|
+
const supersedes = afterSup.slice(0, reasonIdx);
|
|
89
|
+
// reason is everything between ". Reason: " and the final ".", which we have
|
|
90
|
+
// already confirmed via endsWith(SUFFIX).
|
|
91
|
+
const reasonWithDot = afterSup.slice(reasonIdx + SEP_REASON.length);
|
|
92
|
+
const reason = reasonWithDot.slice(0, reasonWithDot.length - SUFFIX.length);
|
|
93
|
+
|
|
94
|
+
// Every field MUST be non-empty (an empty field is a malformed flag).
|
|
95
|
+
if (ref.length === 0) return { ok: false, error: "empty-ref" };
|
|
96
|
+
if (supersedes.length === 0) return { ok: false, error: "empty-supersedes" };
|
|
97
|
+
if (reason.length === 0) return { ok: false, error: "empty-reason" };
|
|
98
|
+
|
|
99
|
+
return { ok: true, value: { ref, supersedes, reason } };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Format a {ref, supersedes, reason} triple back into the canonical §4 line.
|
|
104
|
+
*
|
|
105
|
+
* @param {{ref: string, supersedes: string, reason: string}} obj
|
|
106
|
+
* @returns {{ok: true, value: string} | {ok: false, error: string}} never throws.
|
|
107
|
+
*/
|
|
108
|
+
function formatDivergence(obj) {
|
|
109
|
+
if (obj === null || typeof obj !== "object") {
|
|
110
|
+
return { ok: false, error: "not-an-object" };
|
|
111
|
+
}
|
|
112
|
+
const { ref, supersedes, reason } = obj;
|
|
113
|
+
if (typeof ref !== "string" || ref.length === 0) {
|
|
114
|
+
return { ok: false, error: "empty-ref" };
|
|
115
|
+
}
|
|
116
|
+
if (typeof supersedes !== "string" || supersedes.length === 0) {
|
|
117
|
+
return { ok: false, error: "empty-supersedes" };
|
|
118
|
+
}
|
|
119
|
+
if (typeof reason !== "string" || reason.length === 0) {
|
|
120
|
+
return { ok: false, error: "empty-reason" };
|
|
121
|
+
}
|
|
122
|
+
const value = PREFIX + ref + SEP_SUPERSEDES + supersedes + SEP_REASON + reason + SUFFIX;
|
|
123
|
+
return { ok: true, value };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Count the VALID `⚠ Divergence` flags in a document.
|
|
128
|
+
*
|
|
129
|
+
* Structural: a line is counted ONLY if it PARSES as a well-formed §4 flag — a
|
|
130
|
+
* line that merely contains the substring "⚠ Divergence" but is malformed is NOT
|
|
131
|
+
* counted (feedback_coverage_check_structural_not_substring). The returned count
|
|
132
|
+
* is a deterministic integer (the checkable artifact that can feed D1's rule map).
|
|
133
|
+
*
|
|
134
|
+
* @param {string} docText
|
|
135
|
+
* @returns {number} count of valid flags (0 on non-string input — never throws).
|
|
136
|
+
*/
|
|
137
|
+
function countDivergences(docText) {
|
|
138
|
+
if (typeof docText !== "string") return 0;
|
|
139
|
+
// Split on any newline style; trailing/leading whitespace on a line is slop,
|
|
140
|
+
// so a flag line with surrounding whitespace will fail the anchored parse and
|
|
141
|
+
// not be counted (matches the byte-stable round-trip invariant).
|
|
142
|
+
const lines = docText.split(/\r\n|\r|\n/);
|
|
143
|
+
let count = 0;
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
if (parseDivergence(line).ok) count++;
|
|
146
|
+
}
|
|
147
|
+
return count;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --------------------------------------------------------------------------
|
|
151
|
+
// CLI wrapper (the only I/O; the three functions above are pure).
|
|
152
|
+
// --------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
const HELP = [
|
|
155
|
+
"gsd-t-divergence-grammar — §4 ⚠ Divergence flag round-trip + count",
|
|
156
|
+
"",
|
|
157
|
+
"Usage:",
|
|
158
|
+
' gsd-t-divergence-grammar --parse "<line>" --json',
|
|
159
|
+
" gsd-t-divergence-grammar --count <docPath> --json",
|
|
160
|
+
"",
|
|
161
|
+
"Exit: 0 ok · 4 parse failure / count error · 64 bad input.",
|
|
162
|
+
].join("\n");
|
|
163
|
+
|
|
164
|
+
function parseArgs(argv) {
|
|
165
|
+
const o = { json: false, help: false, parse: null, count: null };
|
|
166
|
+
for (let i = 0; i < argv.length; i++) {
|
|
167
|
+
const a = argv[i];
|
|
168
|
+
if (a === "--help" || a === "-h") o.help = true;
|
|
169
|
+
else if (a === "--json") o.json = true;
|
|
170
|
+
else if (a === "--parse") o.parse = argv[++i];
|
|
171
|
+
else if (a === "--count") o.count = argv[++i];
|
|
172
|
+
}
|
|
173
|
+
return o;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function main() {
|
|
177
|
+
// Lazy require of fs ONLY inside the CLI path — the module export surface
|
|
178
|
+
// stays pure/dep-free.
|
|
179
|
+
const o = parseArgs(process.argv.slice(2));
|
|
180
|
+
if (o.help) {
|
|
181
|
+
process.stdout.write(HELP + "\n");
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (o.parse != null) {
|
|
186
|
+
const res = parseDivergence(o.parse);
|
|
187
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
188
|
+
process.exit(res.ok ? 0 : 4);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (o.count != null) {
|
|
192
|
+
let text;
|
|
193
|
+
try {
|
|
194
|
+
text = require("fs").readFileSync(o.count, "utf8");
|
|
195
|
+
} catch (e) {
|
|
196
|
+
process.stdout.write(
|
|
197
|
+
JSON.stringify({ ok: false, error: "read-failed", path: o.count }, null, 2) + "\n"
|
|
198
|
+
);
|
|
199
|
+
process.exit(4);
|
|
200
|
+
}
|
|
201
|
+
const count = countDivergences(text);
|
|
202
|
+
// The count is a CHECKABLE JSON integer artifact, not prose.
|
|
203
|
+
process.stdout.write(JSON.stringify({ ok: true, count }, null, 2) + "\n");
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
process.stdout.write(JSON.stringify({ ok: false, error: "no-mode" }, null, 2) + "\n");
|
|
208
|
+
process.exit(64);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (require.main === module) main();
|
|
212
|
+
|
|
213
|
+
module.exports = { parseDivergence, formatDivergence, countDivergences };
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* gsd-t-guard-map-derive — M88 G2 (map-derivation-seam)
|
|
5
|
+
*
|
|
6
|
+
* The mechanical build→rule-map DERIVATION seam. Where M87 D1
|
|
7
|
+
* (`bin/gsd-t-guard-map.cjs`) GATES a build→rule map, this module DERIVES that
|
|
8
|
+
* map from two inputs — instead of hand-authoring it:
|
|
9
|
+
*
|
|
10
|
+
* 1. a PseudoCode doc → the doc's derived RULE-ID set (via D1's parser,
|
|
11
|
+
* IMPORTED, never re-implemented — single source of truth for the grammar).
|
|
12
|
+
* 2. a build-evidence manifest → which test assertions / files back (or
|
|
13
|
+
* contradict) which RULE-ID(s).
|
|
14
|
+
*
|
|
15
|
+
* Output: `{ rules: { <id>: { backedBy:[<refs>], contradicted:bool } } }`
|
|
16
|
+
* keyed EXACTLY to the doc's derived RULE-ID set — EVERY doc rule present.
|
|
17
|
+
* A rule with no evidence → `backedBy:[]` (PRESENT, unbacked) — NOT an omitted
|
|
18
|
+
* key. Omitting it would re-open the map-side vacuous pass D1's gate closed
|
|
19
|
+
* (the gate iterates the DOC's id set; an absent key is treated as unbacked, but
|
|
20
|
+
* a faithful derivation must still emit every doc rule so the map is a complete,
|
|
21
|
+
* gate-ready artifact).
|
|
22
|
+
*
|
|
23
|
+
* Evidence manifest shape (the convention this seam defines):
|
|
24
|
+
* {
|
|
25
|
+
* "evidence": [
|
|
26
|
+
* { "ref": "test/foo.test.js:42", "backs": ["R-PAYPAL-03", "R-PAYPAL-05"] },
|
|
27
|
+
* { "ref": "test/bar.test.js:7", "contradicts": ["R-PAYPAL-07"] }
|
|
28
|
+
* ]
|
|
29
|
+
* }
|
|
30
|
+
* - `ref` (string, required): a stable evidence reference (file:line, file::name, …).
|
|
31
|
+
* - `backs` (string[], optional): RULE-IDs this evidence BACKS.
|
|
32
|
+
* - `contradicts` (string[], optional): RULE-IDs this evidence CONTRADICTS
|
|
33
|
+
* (build/test asserts the negation of the invariant). Any contradiction on a
|
|
34
|
+
* rule sets its `contradicted:true` in the derived map.
|
|
35
|
+
* An evidence entry that references a RULE-ID NOT in the doc's derived set is
|
|
36
|
+
* IGNORED (the doc is the source of truth; phantom refs never invent map keys).
|
|
37
|
+
*
|
|
38
|
+
* Hard engineering bar (mirror D1 + M83): zero deps (Node built-ins only), never
|
|
39
|
+
* throws (bad input → exitCode 64, never an uncaught throw), pure/read-only
|
|
40
|
+
* (writes nothing). Deterministic — same doc bytes + same manifest → same map.
|
|
41
|
+
*
|
|
42
|
+
* Input: --doc <PseudoCode-[Title].md> --evidence <manifest.json> [--json]
|
|
43
|
+
* Output: the derived map JSON ({ rules: {...} }) on stdout.
|
|
44
|
+
* Exit: 0 derivation succeeded · 64 bad input (unreadable doc, bad manifest,
|
|
45
|
+
* doc parse failure).
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
const fs = require("node:fs");
|
|
49
|
+
|
|
50
|
+
// IMPORT D1's parser + RULE-ID derivation — the single source of truth for the
|
|
51
|
+
// `[RULE]` grammar. We consume `parseRules(md, docPath)`; we do NOT re-implement
|
|
52
|
+
// the grammar (a re-impl would drift from the gate that reads our output).
|
|
53
|
+
const { parseRules } = require("./gsd-t-guard-map.cjs");
|
|
54
|
+
|
|
55
|
+
// ─── derivation (pure) ─────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Derive the build→rule map from a doc's derived RULE-ID set + an evidence
|
|
59
|
+
* manifest. Doc-keyed: EVERY doc rule is emitted; an unbacked rule is
|
|
60
|
+
* `backedBy:[]` present. Phantom evidence refs (RULE-IDs not in the doc) are
|
|
61
|
+
* ignored. Pure — no I/O, never throws.
|
|
62
|
+
*
|
|
63
|
+
* @param {Array<{id:string}>} rules - parseRules().rules (doc-derived id set)
|
|
64
|
+
* @param {{evidence?: Array<{ref?:string, backs?:string[], contradicts?:string[]}>}} manifest
|
|
65
|
+
* @returns {{ rules: Object<string, {backedBy:string[], contradicted:boolean}> }}
|
|
66
|
+
*/
|
|
67
|
+
function deriveMap(rules, manifest) {
|
|
68
|
+
// Seed every doc-derived id with an empty, unbacked, not-contradicted entry.
|
|
69
|
+
// Iterating the DOC's id set (not the manifest) is what preserves doc-keyed
|
|
70
|
+
// non-vacuity: a rule with zero evidence is PRESENT as backedBy:[].
|
|
71
|
+
const out = { rules: {} };
|
|
72
|
+
const docIds = new Set();
|
|
73
|
+
for (const r of Array.isArray(rules) ? rules : []) {
|
|
74
|
+
if (r && typeof r.id === "string") {
|
|
75
|
+
docIds.add(r.id);
|
|
76
|
+
if (!out.rules[r.id]) out.rules[r.id] = { backedBy: [], contradicted: false };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const evidence = (manifest && Array.isArray(manifest.evidence)) ? manifest.evidence : [];
|
|
81
|
+
for (const item of evidence) {
|
|
82
|
+
if (!item || typeof item !== "object") continue;
|
|
83
|
+
const ref = typeof item.ref === "string" ? item.ref : null;
|
|
84
|
+
const backs = Array.isArray(item.backs) ? item.backs : [];
|
|
85
|
+
const contradicts = Array.isArray(item.contradicts) ? item.contradicts : [];
|
|
86
|
+
|
|
87
|
+
for (const id of backs) {
|
|
88
|
+
// Only attach to a DOC-derived id (doc is the source of truth; phantom
|
|
89
|
+
// ids never invent a key). A backing without a `ref` is meaningless — skip.
|
|
90
|
+
if (typeof id === "string" && docIds.has(id) && ref) {
|
|
91
|
+
const entry = out.rules[id];
|
|
92
|
+
if (!entry.backedBy.includes(ref)) entry.backedBy.push(ref);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const id of contradicts) {
|
|
96
|
+
if (typeof id === "string" && docIds.has(id)) {
|
|
97
|
+
out.rules[id].contradicted = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── driver ────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Run the derivation over a doc + evidence manifest. Never throws — bad input
|
|
109
|
+
* returns an envelope with exitCode 64. On success, `map` is the derived
|
|
110
|
+
* build→rule map (gate-ready, consumable by gsd-t-guard-map.cjs unchanged).
|
|
111
|
+
*
|
|
112
|
+
* @param {{doc?:string, evidence?:string}} o
|
|
113
|
+
* @returns {{ ok:boolean, exitCode:number, map?:object, derivedIds?:string[], ... }}
|
|
114
|
+
*/
|
|
115
|
+
function runDerive(o) {
|
|
116
|
+
const { doc, evidence } = (o && typeof o === "object") ? o : {};
|
|
117
|
+
if (!doc || !evidence || typeof doc !== "string" || typeof evidence !== "string") {
|
|
118
|
+
return { ok: false, exitCode: 64, reason: "missing --doc and/or --evidence", doc: doc || null, evidence: evidence || null };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let md;
|
|
122
|
+
try {
|
|
123
|
+
md = fs.readFileSync(doc, "utf8");
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return { ok: false, exitCode: 64, reason: `cannot read doc: ${e && e.message}`, doc };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let manifest;
|
|
129
|
+
try {
|
|
130
|
+
manifest = JSON.parse(fs.readFileSync(evidence, "utf8"));
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return { ok: false, exitCode: 64, reason: `cannot read/parse evidence JSON: ${e && e.message}`, evidence };
|
|
133
|
+
}
|
|
134
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
135
|
+
return { ok: false, exitCode: 64, reason: "evidence JSON must be an object with an `evidence` array", evidence };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { rules, parseErrors } = parseRules(md, doc);
|
|
139
|
+
|
|
140
|
+
// A doc parse failure (empty-invariant rule) is bad input — the same severity
|
|
141
|
+
// D1's gate assigns it. We cannot derive a faithful map from a malformed doc.
|
|
142
|
+
if (parseErrors.length > 0) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
exitCode: 64,
|
|
146
|
+
reason: `doc parse failure: ${parseErrors.length} rule line(s) yielded an empty invariant`,
|
|
147
|
+
parseErrors,
|
|
148
|
+
doc,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const map = deriveMap(rules, manifest);
|
|
153
|
+
const derivedIds = rules.map((r) => r.id);
|
|
154
|
+
|
|
155
|
+
return { ok: true, exitCode: 0, doc, evidence, derivedIds, ruleCount: derivedIds.length, map };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
function parseArgs(argv) {
|
|
161
|
+
const o = { doc: null, evidence: null, help: false };
|
|
162
|
+
for (let i = 0; i < argv.length; i++) {
|
|
163
|
+
const a = argv[i];
|
|
164
|
+
if (a === "--help" || a === "-h") o.help = true;
|
|
165
|
+
else if (a === "--doc") o.doc = argv[++i];
|
|
166
|
+
else if (a === "--evidence") o.evidence = argv[++i];
|
|
167
|
+
else if (a === "--json") {/* default output is JSON */}
|
|
168
|
+
}
|
|
169
|
+
return o;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const HELP = `Usage: gsd-t guard-map-derive --doc <PseudoCode-[Title].md> --evidence <manifest.json> [--json]
|
|
173
|
+
|
|
174
|
+
The M88 build→rule-map DERIVATION seam. Parses every [RULE] from a PseudoCode
|
|
175
|
+
doc (via the M87 guard-map parser — single source of truth), then DERIVES the
|
|
176
|
+
build→rule map gsd-t-guard-map.cjs gates, attaching each evidence ref to the
|
|
177
|
+
RULE-ID(s) it backs/contradicts. The map is keyed EXACTLY to the doc's derived
|
|
178
|
+
id set — every doc rule present; an unbacked rule is backedBy:[] (never omitted).
|
|
179
|
+
The derived map prints to stdout and is valid --map input to the gate unchanged.
|
|
180
|
+
|
|
181
|
+
--doc PATH the PseudoCode-[Title].md guard-map doc.
|
|
182
|
+
--evidence PATH the build-evidence manifest:
|
|
183
|
+
{ "evidence": [ { "ref": "test/x.test.js:42",
|
|
184
|
+
"backs": ["R-FOO-01"],
|
|
185
|
+
"contradicts": ["R-FOO-02"] } ] }
|
|
186
|
+
|
|
187
|
+
Exit: 0 derivation succeeded (map on stdout) · 64 bad input (unreadable doc,
|
|
188
|
+
bad manifest, or doc parse failure).`;
|
|
189
|
+
|
|
190
|
+
function main() {
|
|
191
|
+
const o = parseArgs(process.argv.slice(2));
|
|
192
|
+
if (o.help) { process.stdout.write(HELP + "\n"); process.exit(0); }
|
|
193
|
+
let res;
|
|
194
|
+
try {
|
|
195
|
+
res = runDerive(o);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// Defense in depth — runDerive is written never to throw, but the contract
|
|
198
|
+
// mandates the module never throws, so bad input maps to 64 even here.
|
|
199
|
+
res = { ok: false, exitCode: 64, reason: `derive-error: ${e && e.message}` };
|
|
200
|
+
}
|
|
201
|
+
// On success print the derived MAP (the artifact); on failure print the
|
|
202
|
+
// diagnostic envelope. Either way, valid JSON to stdout.
|
|
203
|
+
const payload = res.ok ? res.map : res;
|
|
204
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
205
|
+
process.exit(res.exitCode);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (require.main === module) main();
|
|
209
|
+
|
|
210
|
+
module.exports = { runDerive, deriveMap };
|