@sdd-method/sdd-cli 1.2.0 → 1.3.0

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.
Files changed (40) hide show
  1. package/dist/lib/catalogue/build.d.ts.map +1 -1
  2. package/dist/lib/catalogue/build.js +14 -1
  3. package/dist/lib/catalogue/build.js.map +1 -1
  4. package/dist/lib/catalogue/builders/capability-edges.d.ts.map +1 -1
  5. package/dist/lib/catalogue/builders/capability-edges.js +10 -1
  6. package/dist/lib/catalogue/builders/capability-edges.js.map +1 -1
  7. package/dist/lib/catalogue/builders/index.d.ts +2 -0
  8. package/dist/lib/catalogue/builders/index.d.ts.map +1 -1
  9. package/dist/lib/catalogue/builders/index.js +1 -0
  10. package/dist/lib/catalogue/builders/index.js.map +1 -1
  11. package/dist/lib/catalogue/builders/quality-scores.d.ts +14 -0
  12. package/dist/lib/catalogue/builders/quality-scores.d.ts.map +1 -0
  13. package/dist/lib/catalogue/builders/quality-scores.js +321 -0
  14. package/dist/lib/catalogue/builders/quality-scores.js.map +1 -0
  15. package/dist/lib/catalogue/builders/services.d.ts.map +1 -1
  16. package/dist/lib/catalogue/builders/services.js +5 -1
  17. package/dist/lib/catalogue/builders/services.js.map +1 -1
  18. package/dist/lib/catalogue/canonical-schema.yaml +199 -1
  19. package/dist/lib/catalogue/coverage-render.d.ts +15 -0
  20. package/dist/lib/catalogue/coverage-render.d.ts.map +1 -1
  21. package/dist/lib/catalogue/coverage-render.js +85 -0
  22. package/dist/lib/catalogue/coverage-render.js.map +1 -1
  23. package/dist/lib/catalogue/coverage-run.d.ts.map +1 -1
  24. package/dist/lib/catalogue/coverage-run.js +7 -1
  25. package/dist/lib/catalogue/coverage-run.js.map +1 -1
  26. package/dist/lib/catalogue/scores-coverage.d.ts +138 -0
  27. package/dist/lib/catalogue/scores-coverage.d.ts.map +1 -0
  28. package/dist/lib/catalogue/scores-coverage.js +346 -0
  29. package/dist/lib/catalogue/scores-coverage.js.map +1 -0
  30. package/dist/lib/mcp/parse/gate-registry.d.ts +63 -4
  31. package/dist/lib/mcp/parse/gate-registry.d.ts.map +1 -1
  32. package/dist/lib/mcp/parse/gate-registry.js +127 -31
  33. package/dist/lib/mcp/parse/gate-registry.js.map +1 -1
  34. package/dist/lib/mcp/resources/gates.d.ts.map +1 -1
  35. package/dist/lib/mcp/resources/gates.js +13 -27
  36. package/dist/lib/mcp/resources/gates.js.map +1 -1
  37. package/dist/lib/mcp/tools/query-gate-status.d.ts.map +1 -1
  38. package/dist/lib/mcp/tools/query-gate-status.js +13 -27
  39. package/dist/lib/mcp/tools/query-gate-status.js.map +1 -1
  40. package/package.json +1 -1
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Scores coverage — M-ADR 0205 §4.5's coverage instrument, the fourteenth
3
+ * engine check (Phase B, B2). SQL on the §4.1 engine, like every other
4
+ * catalogue-reading predicate; `check-repo-config-coverage.sh` remains the
5
+ * only shell survivor and is not the model for this.
6
+ *
7
+ * The claim: *every eligible artefact carries a current score*. Eligibility is
8
+ * computed per rubric against the catalogue that holds its targets, and the
9
+ * predicate reports BOTH directions, because the two say different things:
10
+ *
11
+ * - an eligible target with no score is a GAP — nobody has judged it;
12
+ * - a score whose target does not resolve is an ORPHANED SCORE — the
13
+ * assessment corpus has decayed away from the model it describes.
14
+ *
15
+ * The second direction is not decoration. Measured on the forward estate at
16
+ * introduction: 85 of 99 ADR scores point at ids that no longer exist, because
17
+ * a rename moved the ADR canon and the registry keys were never migrated. A
18
+ * one-directional predicate would have reported that estate as having 99
19
+ * scored ADRs while none of them resolved.
20
+ *
21
+ * THE JOIN VOCABULARY IS THE WHOLE DIFFICULTY, and it is declared rather than
22
+ * guessed. A registry's entry keys are not the catalogue's ids, and the
23
+ * mismatch differs per rubric — measured, not assumed:
24
+ *
25
+ * - contract-quality keys are SPEC PATHS (`contracts-avro/.../X.avsc`),
26
+ * joining `contracts.spec_path`; against `contracts.id` they score 0 of 55.
27
+ * - adr-quality keys carry a tier prefix (`M-ADR-0001`) the catalogue strips
28
+ * (`ADR-0001`); the raw join scores 0 of 99.
29
+ * - domain-architecture keys are plain domain slugs and join `domains.id`
30
+ * directly, 13 of 13.
31
+ *
32
+ * So each rubric declares its catalogue, its join column and its reference
33
+ * transform, and the predicate REPORTS the resolution rate it achieved. A join
34
+ * that silently resolves nothing looks exactly like an estate that has scored
35
+ * nothing — the confident-false-zero shape this whole phase exists to remove.
36
+ *
37
+ * Rubrics whose targets have NO catalogue (c4-diagram, strategic-doc,
38
+ * user-story, work-order) have no eligibility denominator to compute. They are
39
+ * reported as such by name and their scores are still resolution-checked as
40
+ * far as they can be — never silently omitted, because a rubric missing from
41
+ * the output reads as a rubric with nothing to say.
42
+ *
43
+ * Reporting-only at both tiers, per §4.5. Exemption vocabulary starts EMPTY
44
+ * (M-ADR 0199): there is no way to mark a target exempt yet, deliberately, so
45
+ * the first figure is the honest total.
46
+ *
47
+ * ADOPTION GATING — a rubric is measured only where its REGISTRY EXISTS. An SDD
48
+ * that carries no `<rubric>-scores.yaml` has not adopted that rubric, so it has
49
+ * no eligible population for it, and reporting its whole ADR/contract/capability
50
+ * corpus as unscored would be true-but-useless noise. This is the same n/a rule
51
+ * the repo-config predicate applies and the same rule the quality-scores builder
52
+ * applies to an absent registry directory: absence of the whole surface is not a
53
+ * gap in it. Measured before adopting it: without this rule ONE reverse-estate
54
+ * SDD emits 630 gap lines for rubrics it has never used, which across that
55
+ * estate would swamp the baseline re-take and bury the real signal.
56
+ *
57
+ * The distinction that must NOT be lost, and is not: a registry that EXISTS but
58
+ * is EMPTY reports its full population as gaps. That is the method repo's own
59
+ * state — fifteen deliberately-empty containers — and it is exactly the silence
60
+ * §4.5 exists to make loud. Absent surface and empty surface are different
61
+ * claims, and conflating them is the defect this whole phase is about.
62
+ *
63
+ * The n/a count is always printed. A rubric skipped silently reads as a rubric
64
+ * with nothing to report.
65
+ */
66
+ import { existsSync } from "node:fs";
67
+ import { join } from "node:path";
68
+ /**
69
+ * The declared mapping. Three entries are EXERCISED against real estate data
70
+ * (adr-quality, contract-quality, domain-architecture); the rest are declared
71
+ * from the registry and rubric shapes and have no populated corpus to verify
72
+ * against yet — a distinction the metric line states rather than blurs.
73
+ */
74
+ export const RUBRIC_ELIGIBILITY = [
75
+ {
76
+ rubricId: "adr-quality",
77
+ targetKind: "adr",
78
+ catalogue: "adrs",
79
+ joinColumn: "id",
80
+ // The catalogue strips the tier prefix (M-ADR 0186 §4.4); registries keep it.
81
+ refTransform: (r) => `regexp_replace(${r}, '^[A-Z]+-ADR-', 'ADR-')`,
82
+ eligibilityFilter: "status = 'Accepted'",
83
+ eligibilityNote: "Accepted only. A Proposed ADR is mid-decision and a Superseded one is history; requiring a current score on either would manufacture a gap nobody should close.",
84
+ },
85
+ {
86
+ rubricId: "contract-quality",
87
+ targetKind: "contract",
88
+ catalogue: "contracts",
89
+ joinColumn: "spec_path",
90
+ refTransform: null,
91
+ eligibilityFilter: "status <> 'deprecated'",
92
+ eligibilityNote: "Every non-deprecated contract. Deprecated contracts are excluded for the same reason the contract-coverage predicate excludes them — they are on the way out, and scoring them is work with no consumer.",
93
+ },
94
+ {
95
+ rubricId: "domain-architecture",
96
+ targetKind: "domain",
97
+ catalogue: "domains",
98
+ joinColumn: "id",
99
+ refTransform: null,
100
+ eligibilityFilter: null,
101
+ eligibilityNote: "Every domain on disk. A domain that exists is a domain that should be assessed.",
102
+ },
103
+ {
104
+ rubricId: "intent-quality",
105
+ targetKind: "domain",
106
+ catalogue: "domains",
107
+ joinColumn: "id",
108
+ refTransform: null,
109
+ eligibilityFilter: null,
110
+ eligibilityNote: "Every domain, same population as domain-architecture — the two rubrics judge different things about the same unit.",
111
+ },
112
+ {
113
+ rubricId: "feature-grounding",
114
+ targetKind: "capability",
115
+ catalogue: "capabilities",
116
+ joinColumn: "id",
117
+ refTransform: null,
118
+ eligibilityFilter: null,
119
+ eligibilityNote: "Every capability.",
120
+ },
121
+ {
122
+ rubricId: "traceability-manifest",
123
+ targetKind: "capability",
124
+ catalogue: "capabilities",
125
+ joinColumn: "id",
126
+ refTransform: null,
127
+ eligibilityFilter: null,
128
+ eligibilityNote: "Every capability.",
129
+ },
130
+ {
131
+ rubricId: "glossary",
132
+ targetKind: "glossary",
133
+ catalogue: "domains",
134
+ joinColumn: "id",
135
+ refTransform: null,
136
+ eligibilityFilter: null,
137
+ eligibilityNote: "One glossary per domain — the registry keys by domain slug, so the eligible population is domains, not glossary_items (a glossary is scored whole, not term by term).",
138
+ },
139
+ {
140
+ rubricId: "persona",
141
+ targetKind: "persona",
142
+ catalogue: "personas",
143
+ joinColumn: "id",
144
+ refTransform: null,
145
+ eligibilityFilter: null,
146
+ eligibilityNote: "Every persona.",
147
+ },
148
+ {
149
+ rubricId: "product-definition",
150
+ targetKind: "product",
151
+ catalogue: "products",
152
+ joinColumn: "id",
153
+ refTransform: null,
154
+ eligibilityFilter: null,
155
+ eligibilityNote: "Every product.",
156
+ },
157
+ {
158
+ rubricId: "screen-spec",
159
+ targetKind: "screen",
160
+ catalogue: "screens",
161
+ joinColumn: "id",
162
+ refTransform: null,
163
+ eligibilityFilter: null,
164
+ eligibilityNote: "Every screen.",
165
+ },
166
+ {
167
+ rubricId: "integration-sdd-quality",
168
+ targetKind: "integration",
169
+ catalogue: "integrations",
170
+ joinColumn: "id",
171
+ refTransform: null,
172
+ eligibilityFilter: null,
173
+ eligibilityNote: "Every integration.",
174
+ },
175
+ // ── No catalogued eligibility population ────────────────────────────────
176
+ // These rubrics score artefacts the catalogue model does not hold, so there
177
+ // is no denominator to compute. Declared explicitly so they appear in the
178
+ // output as "no denominator" rather than being absent — an absent rubric
179
+ // reads as a rubric with nothing to report.
180
+ {
181
+ rubricId: "c4-diagram",
182
+ targetKind: "c4_diagram",
183
+ catalogue: null,
184
+ joinColumn: "id",
185
+ refTransform: null,
186
+ eligibilityFilter: null,
187
+ eligibilityNote: "Diagrams are files, not catalogue rows.",
188
+ },
189
+ {
190
+ rubricId: "strategic-doc",
191
+ targetKind: "strategic_doc",
192
+ catalogue: null,
193
+ joinColumn: "id",
194
+ refTransform: null,
195
+ eligibilityFilter: null,
196
+ eligibilityNote: "Strategic documents are not catalogued.",
197
+ },
198
+ {
199
+ rubricId: "user-story",
200
+ targetKind: "user_story",
201
+ catalogue: null,
202
+ joinColumn: "id",
203
+ refTransform: null,
204
+ eligibilityFilter: null,
205
+ eligibilityNote: "User stories are not catalogued.",
206
+ },
207
+ {
208
+ rubricId: "work-order",
209
+ targetKind: "work_order",
210
+ catalogue: null,
211
+ joinColumn: "id",
212
+ refTransform: null,
213
+ eligibilityFilter: null,
214
+ eligibilityNote: "Work orders are not catalogued.",
215
+ },
216
+ ];
217
+ const sqlStr = (s) => `'${s.replace(/'/g, "''")}'`;
218
+ async function rows(session, sql) {
219
+ const reader = await session.conn.runAndReadAll(sql);
220
+ return reader.getRows();
221
+ }
222
+ /**
223
+ * Runs the predicate. `available` names the catalogues actually present, so a
224
+ * rubric whose population file is absent is skipped the way every other
225
+ * predicate skips an absent population — silently, because absence of the
226
+ * whole surface is not a gap in it — while a rubric whose population is
227
+ * PRESENT but empty still reports its zero.
228
+ */
229
+ export async function runScoresCoverage(session, options) {
230
+ const findings = [];
231
+ const metrics = [];
232
+ if (!options.available.has("quality_scores")) {
233
+ return {
234
+ findings: [],
235
+ metrics: [],
236
+ undeclaredRubrics: [],
237
+ notAdopted: [],
238
+ stalenessBasis: { corpusRef: 0, scoredAt: 0, none: 0 },
239
+ totalScores: 0,
240
+ };
241
+ }
242
+ const totalScores = Number((await rows(session, `SELECT COUNT(*) FROM "quality_scores";`))[0][0]);
243
+ const basis = await rows(session, `SELECT staleness_basis, COUNT(*) FROM "quality_scores" GROUP BY 1 ORDER BY 1;`);
244
+ const stalenessBasis = { corpusRef: 0, scoredAt: 0, none: 0 };
245
+ for (const [b, n] of basis) {
246
+ const c = Number(n);
247
+ if (b === "corpus_ref")
248
+ stalenessBasis.corpusRef = c;
249
+ else if (b === "scored_at")
250
+ stalenessBasis.scoredAt = c;
251
+ else
252
+ stalenessBasis.none = c;
253
+ }
254
+ // A row that can support no staleness verdict at all is its own finding.
255
+ // Without it the predicate would silently treat "unknown" as "fresh", which
256
+ // is the basis error this column exists to prevent.
257
+ if (stalenessBasis.none > 0) {
258
+ findings.push({
259
+ kind: "unresolvable-staleness",
260
+ rubricId: "(all)",
261
+ subject: `${stalenessBasis.none} score(s) carry neither corpus_ref nor a resolvable target_path — their currency is UNKNOWN, not fresh`,
262
+ });
263
+ }
264
+ const declared = new Set(RUBRIC_ELIGIBILITY.map((r) => r.rubricId));
265
+ const present = await rows(session, `SELECT DISTINCT rubric_id FROM "quality_scores" ORDER BY 1;`);
266
+ const undeclaredRubrics = present
267
+ .map((r) => String(r[0]))
268
+ .filter((r) => !declared.has(r));
269
+ const adopted = (rubricId) => existsSync(join(options.sddRoot, "orchestration", "gate-registry", `${rubricId}-scores.yaml`));
270
+ const notAdopted = [];
271
+ for (const spec of RUBRIC_ELIGIBILITY) {
272
+ if (!adopted(spec.rubricId)) {
273
+ notAdopted.push(spec.rubricId);
274
+ continue;
275
+ }
276
+ const scored = Number((await rows(session, `SELECT COUNT(*) FROM "quality_scores" WHERE rubric_id = ${sqlStr(spec.rubricId)};`))[0][0]);
277
+ // No catalogued population: report the scores, state that there is no
278
+ // denominator, and do not invent one.
279
+ if (spec.catalogue === null) {
280
+ if (scored > 0) {
281
+ metrics.push({
282
+ rubricId: spec.rubricId,
283
+ targetKind: spec.targetKind,
284
+ eligible: null,
285
+ covered: scored,
286
+ scored,
287
+ orphaned: 0,
288
+ });
289
+ }
290
+ continue;
291
+ }
292
+ if (!options.available.has(spec.catalogue))
293
+ continue;
294
+ if (scored === 0) {
295
+ // The population exists and nothing is scored — the loudest case, and
296
+ // exactly the silence M-ADR 0205 §4.5 names. Every eligible row is a gap.
297
+ const eligibleRows = await rows(session, `SELECT id FROM "${spec.catalogue}" WHERE (${spec.eligibilityFilter ?? "TRUE"}) ORDER BY id;`);
298
+ for (const [id] of eligibleRows) {
299
+ findings.push({ kind: "uncovered", rubricId: spec.rubricId, subject: String(id) });
300
+ }
301
+ metrics.push({
302
+ rubricId: spec.rubricId,
303
+ targetKind: spec.targetKind,
304
+ eligible: eligibleRows.length,
305
+ covered: 0,
306
+ scored: 0,
307
+ orphaned: 0,
308
+ });
309
+ continue;
310
+ }
311
+ const ref = spec.refTransform ? spec.refTransform("s.target_ref") : "s.target_ref";
312
+ // Eligibility is always a conjunct, so it composes with the not-exists
313
+ // clause without any string surgery on an assembled WHERE — `TRUE` for the
314
+ // unfiltered case keeps one query shape instead of two.
315
+ const eligibleWhere = spec.eligibilityFilter ?? "TRUE";
316
+ const scoreSet = `SELECT * FROM "quality_scores" WHERE rubric_id = ${sqlStr(spec.rubricId)}`;
317
+ // Direction 1 — eligible targets with no score.
318
+ const uncovered = await rows(session, `SELECT c.id FROM "${spec.catalogue}" c
319
+ WHERE (${eligibleWhere})
320
+ AND NOT EXISTS (SELECT 1 FROM (${scoreSet}) s WHERE ${ref} = c."${spec.joinColumn}")
321
+ ORDER BY c.id;`);
322
+ for (const [id] of uncovered) {
323
+ findings.push({ kind: "uncovered", rubricId: spec.rubricId, subject: String(id) });
324
+ }
325
+ // Direction 2 — scores whose target does not resolve.
326
+ const orphaned = await rows(session, `SELECT s.target_ref FROM (${scoreSet}) s
327
+ WHERE NOT EXISTS (
328
+ SELECT 1 FROM "${spec.catalogue}" c WHERE c."${spec.joinColumn}" = ${ref}
329
+ )
330
+ ORDER BY s.target_ref;`);
331
+ for (const [r] of orphaned) {
332
+ findings.push({ kind: "orphaned-score", rubricId: spec.rubricId, subject: String(r) });
333
+ }
334
+ const eligible = Number((await rows(session, `SELECT COUNT(*) FROM "${spec.catalogue}" WHERE (${eligibleWhere});`))[0][0]);
335
+ metrics.push({
336
+ rubricId: spec.rubricId,
337
+ targetKind: spec.targetKind,
338
+ eligible,
339
+ covered: eligible - uncovered.length,
340
+ scored,
341
+ orphaned: orphaned.length,
342
+ });
343
+ }
344
+ return { findings, metrics, undeclaredRubrics, notAdopted, stalenessBasis, totalScores };
345
+ }
346
+ //# sourceMappingURL=scores-coverage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scores-coverage.js","sourceRoot":"","sources":["../../../src/lib/catalogue/scores-coverage.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,30 @@
1
1
  /**
2
2
  * YAML reader for orchestration/gate-registry/<gate>-scores.yaml.
3
- * Schema is conventional (schema_version, description, pass_threshold,
4
- * plus per-entry records keyed by artefact ID). We treat the file as a
5
- * generic YAML document and extract the artefact entries after
6
- * excluding the known header fields.
3
+ *
4
+ * Registry shape: header fields (schema_version, description, pass_threshold,
5
+ * and the reverse family's registry_type / assessed_at / assessed_by) plus ONE
6
+ * container key holding the per-artefact entries. The container key is not a
7
+ * stable name — it drifts between the method template and live estate data,
8
+ * and is not unique across rubrics — so it is unwrapped rather than trusted.
9
+ *
10
+ * Three defects were found here by the M-ADR 0205 Phase-B survey (2026-08-06)
11
+ * and fixed together, since all three are the same mistake in different
12
+ * places: assuming the registry population looks like the reader's model of it.
13
+ *
14
+ * - `listGates` hard-coded ten names, so gate discovery was silently blind
15
+ * to nine of the nineteen rubric families now in existence (five newer
16
+ * method-side registries and four reverse-only ones). An unlisted gate
17
+ * read as absent. It now enumerates the directory.
18
+ * - `entries` returned the CONTAINER, not the artefacts, so the summary in
19
+ * `resources/gates.ts` reported `total: 1` for a registry holding 99
20
+ * scored ADRs — while its own description advertised "per-artefact
21
+ * entries". It now unwraps the single container.
22
+ * - `parseYaml` ran unguarded, so a malformed registry threw a raw
23
+ * YAMLParseError through two surfaces that otherwise honour the
24
+ * McpEnvelopeError contract. Measured: three of one estate's ten
25
+ * registries are unparseable. It now reports `parseError` and returns an
26
+ * empty-but-well-formed registry, leaving each caller to decide how to
27
+ * surface it — the reader's job is to report, not to throw.
7
28
  */
8
29
  export interface GateRegistry {
9
30
  readonly gate: string;
@@ -11,9 +32,47 @@ export interface GateRegistry {
11
32
  readonly schemaVersion: string | null;
12
33
  readonly description: string | null;
13
34
  readonly passThreshold: unknown;
35
+ /** Per-artefact entries, unwrapped from the container key. */
14
36
  readonly entries: Record<string, unknown>;
37
+ /** The container key the entries came from, when there was exactly one. */
38
+ readonly entryContainer: string | null;
39
+ /**
40
+ * Parser message when the registry is malformed. Non-null means `entries` is
41
+ * empty because the file could not be read — NOT because nothing is scored.
42
+ * A caller that conflates the two reports an empty gate as a clean one.
43
+ */
44
+ readonly parseError: string | null;
15
45
  }
16
46
  export declare function gateRegistryPath(sddRoot: string, gate: string): string;
47
+ /**
48
+ * Every gate this SDD actually carries, by enumeration — never a fixed list.
49
+ * A registry is any `<gate>-scores.yaml` in the gate-registry directory, so a
50
+ * newly-shipped rubric is discoverable the moment its registry exists.
51
+ */
17
52
  export declare function listGates(sddRoot: string): string[];
18
53
  export declare function readGateRegistry(sddRoot: string, gate: string): GateRegistry | undefined;
54
+ export interface GateSummary {
55
+ readonly total: number;
56
+ readonly pass: number;
57
+ readonly conditional: number;
58
+ readonly fail: number;
59
+ readonly unscored: number;
60
+ }
61
+ /**
62
+ * Verdict tally over unwrapped entries. ONE implementation, deliberately: this
63
+ * existed identically in both MCP consumers, and a duplicated counter is a
64
+ * drift surface that only shows up as two surfaces disagreeing about the same
65
+ * registry.
66
+ *
67
+ * Reads BOTH authored dialects. The gate sits at `entry.current.gate` in the
68
+ * nested shape all fifteen method templates specify, and at `entry.gate` in
69
+ * the flat shape the live estate data overwhelmingly uses. Reading only the
70
+ * flat one reports every nested entry as `unscored` — a confident false zero
71
+ * of exactly the kind this arc keeps paying for.
72
+ *
73
+ * An out-of-vocabulary verdict counts as `unscored` rather than being dropped,
74
+ * so `pass + conditional + fail + unscored === total` always holds and the
75
+ * tally can never quietly lose a row.
76
+ */
77
+ export declare function summariseGateEntries(entries: Record<string, unknown>): GateSummary;
19
78
  //# sourceMappingURL=gate-registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gate-registry.d.ts","sourceRoot":"","sources":["../../../../src/lib/mcp/parse/gate-registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAcnD;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,YAAY,GAAG,SAAS,CA2B1B"}
1
+ {"version":3,"file":"gate-registry.d.ts","sourceRoot":"","sources":["../../../../src/lib/mcp/parse/gate-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAiBH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAanD;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,YAAY,GAAG,SAAS,CAmD1B;AAMD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,CAyBlF"}
@@ -1,56 +1,104 @@
1
1
  /**
2
2
  * YAML reader for orchestration/gate-registry/<gate>-scores.yaml.
3
- * Schema is conventional (schema_version, description, pass_threshold,
4
- * plus per-entry records keyed by artefact ID). We treat the file as a
5
- * generic YAML document and extract the artefact entries after
6
- * excluding the known header fields.
3
+ *
4
+ * Registry shape: header fields (schema_version, description, pass_threshold,
5
+ * and the reverse family's registry_type / assessed_at / assessed_by) plus ONE
6
+ * container key holding the per-artefact entries. The container key is not a
7
+ * stable name — it drifts between the method template and live estate data,
8
+ * and is not unique across rubrics — so it is unwrapped rather than trusted.
9
+ *
10
+ * Three defects were found here by the M-ADR 0205 Phase-B survey (2026-08-06)
11
+ * and fixed together, since all three are the same mistake in different
12
+ * places: assuming the registry population looks like the reader's model of it.
13
+ *
14
+ * - `listGates` hard-coded ten names, so gate discovery was silently blind
15
+ * to nine of the nineteen rubric families now in existence (five newer
16
+ * method-side registries and four reverse-only ones). An unlisted gate
17
+ * read as absent. It now enumerates the directory.
18
+ * - `entries` returned the CONTAINER, not the artefacts, so the summary in
19
+ * `resources/gates.ts` reported `total: 1` for a registry holding 99
20
+ * scored ADRs — while its own description advertised "per-artefact
21
+ * entries". It now unwraps the single container.
22
+ * - `parseYaml` ran unguarded, so a malformed registry threw a raw
23
+ * YAMLParseError through two surfaces that otherwise honour the
24
+ * McpEnvelopeError contract. Measured: three of one estate's ten
25
+ * registries are unparseable. It now reports `parseError` and returns an
26
+ * empty-but-well-formed registry, leaving each caller to decide how to
27
+ * surface it — the reader's job is to report, not to throw.
7
28
  */
8
- import { readFileSync } from "node:fs";
29
+ import { readFileSync, readdirSync } from "node:fs";
9
30
  import { join } from "node:path";
10
31
  import { parse as parseYaml } from "yaml";
11
32
  import { safeIsFile } from "./paths.js";
33
+ /** File-level keys that are registry metadata rather than the entry container. */
12
34
  const HEADER_KEYS = new Set([
13
35
  "schema_version",
14
36
  "description",
15
37
  "pass_threshold",
38
+ "registry_type",
39
+ "assessed_at",
40
+ "assessed_by",
16
41
  ]);
17
42
  export function gateRegistryPath(sddRoot, gate) {
18
43
  return join(sddRoot, "orchestration", "gate-registry", `${gate}-scores.yaml`);
19
44
  }
45
+ /**
46
+ * Every gate this SDD actually carries, by enumeration — never a fixed list.
47
+ * A registry is any `<gate>-scores.yaml` in the gate-registry directory, so a
48
+ * newly-shipped rubric is discoverable the moment its registry exists.
49
+ */
20
50
  export function listGates(sddRoot) {
21
- const candidates = [
22
- "adr-quality",
23
- "c4-diagram",
24
- "contract-quality",
25
- "domain-architecture",
26
- "feature-grounding",
27
- "intent-quality",
28
- "strategic-doc",
29
- "traceability-manifest",
30
- "user-story",
31
- "work-order",
32
- ];
33
- return candidates.filter((g) => safeIsFile(gateRegistryPath(sddRoot, g)));
51
+ const dir = join(sddRoot, "orchestration", "gate-registry");
52
+ let names;
53
+ try {
54
+ names = readdirSync(dir);
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ return names
60
+ .filter((n) => n.endsWith("-scores.yaml"))
61
+ .map((n) => n.slice(0, -".yaml".length).replace(/-scores$/, ""))
62
+ .filter((g) => safeIsFile(gateRegistryPath(sddRoot, g)))
63
+ .sort();
34
64
  }
35
65
  export function readGateRegistry(sddRoot, gate) {
36
66
  const path = gateRegistryPath(sddRoot, gate);
37
67
  if (!safeIsFile(path))
38
68
  return undefined;
39
69
  const raw = readFileSync(path, "utf8");
40
- const parsed = parseYaml(raw);
41
- if (!parsed || typeof parsed !== "object") {
42
- return {
43
- gate,
44
- path,
45
- schemaVersion: null,
46
- description: null,
47
- passThreshold: null,
48
- entries: {},
49
- };
70
+ const empty = (parseError) => ({
71
+ gate,
72
+ path,
73
+ schemaVersion: null,
74
+ description: null,
75
+ passThreshold: null,
76
+ entries: {},
77
+ entryContainer: null,
78
+ parseError,
79
+ });
80
+ let parsed;
81
+ try {
82
+ parsed = parseYaml(raw);
50
83
  }
51
- const entries = {};
52
- for (const [k, v] of Object.entries(parsed)) {
53
- if (!HEADER_KEYS.has(k))
84
+ catch (e) {
85
+ return empty(String(e.message).split("\n")[0] ?? "unparseable YAML");
86
+ }
87
+ if (!parsed || typeof parsed !== "object")
88
+ return empty(null);
89
+ // Exactly one non-header mapping key is the entry container. Zero means a
90
+ // headers-only registry (a deliberate, common state); more than one is
91
+ // ambiguous, and the whole document is returned rather than a guessed pick.
92
+ const containers = Object.entries(parsed).filter(([k, v]) => !HEADER_KEYS.has(k) && typeof v === "object" && v !== null && !Array.isArray(v));
93
+ let entries = {};
94
+ let entryContainer = null;
95
+ if (containers.length === 1) {
96
+ const [key, value] = containers[0];
97
+ entryContainer = key;
98
+ entries = value;
99
+ }
100
+ else if (containers.length > 1) {
101
+ for (const [k, v] of containers)
54
102
  entries[k] = v;
55
103
  }
56
104
  return {
@@ -60,9 +108,57 @@ export function readGateRegistry(sddRoot, gate) {
60
108
  description: typeOrNull(parsed["description"]),
61
109
  passThreshold: parsed["pass_threshold"] ?? null,
62
110
  entries,
111
+ entryContainer,
112
+ parseError: null,
63
113
  };
64
114
  }
65
115
  function typeOrNull(v) {
66
116
  return typeof v === "string" ? v : null;
67
117
  }
118
+ /**
119
+ * Verdict tally over unwrapped entries. ONE implementation, deliberately: this
120
+ * existed identically in both MCP consumers, and a duplicated counter is a
121
+ * drift surface that only shows up as two surfaces disagreeing about the same
122
+ * registry.
123
+ *
124
+ * Reads BOTH authored dialects. The gate sits at `entry.current.gate` in the
125
+ * nested shape all fifteen method templates specify, and at `entry.gate` in
126
+ * the flat shape the live estate data overwhelmingly uses. Reading only the
127
+ * flat one reports every nested entry as `unscored` — a confident false zero
128
+ * of exactly the kind this arc keeps paying for.
129
+ *
130
+ * An out-of-vocabulary verdict counts as `unscored` rather than being dropped,
131
+ * so `pass + conditional + fail + unscored === total` always holds and the
132
+ * tally can never quietly lose a row.
133
+ */
134
+ export function summariseGateEntries(entries) {
135
+ let total = 0;
136
+ let pass = 0;
137
+ let conditional = 0;
138
+ let fail = 0;
139
+ let unscored = 0;
140
+ for (const value of Object.values(entries)) {
141
+ total++;
142
+ if (value && typeof value === "object" && !Array.isArray(value)) {
143
+ const entry = value;
144
+ const nested = entry["current"];
145
+ const source = nested && typeof nested === "object" && !Array.isArray(nested)
146
+ ? nested
147
+ : entry;
148
+ const gateVal = source["gate"];
149
+ if (gateVal === "pass")
150
+ pass++;
151
+ else if (gateVal === "conditional")
152
+ conditional++;
153
+ else if (gateVal === "fail")
154
+ fail++;
155
+ else
156
+ unscored++;
157
+ }
158
+ else {
159
+ unscored++;
160
+ }
161
+ }
162
+ return { total, pass, conditional, fail, unscored };
163
+ }
68
164
  //# sourceMappingURL=gate-registry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gate-registry.js","sourceRoot":"","sources":["../../../../src/lib/mcp/parse/gate-registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC;IAC1B,gBAAgB;IAChB,aAAa;IACb,gBAAgB;CACjB,CAAC,CAAC;AAWH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,IAAY;IAC5D,OAAO,IAAI,CAAC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,GAAG,IAAI,cAAc,CAAC,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,OAAe;IACvC,MAAM,UAAU,GAAG;QACjB,aAAa;QACb,YAAY;QACZ,kBAAkB;QAClB,qBAAqB;QACrB,mBAAmB;QACnB,gBAAgB;QAChB,eAAe;QACf,uBAAuB;QACvB,YAAY;QACZ,YAAY;KACb,CAAC;IACF,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,OAAe,EACf,IAAY;IAEZ,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC7C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAmC,CAAC;IAChE,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC1C,OAAO;YACL,IAAI;YACJ,IAAI;YACJ,aAAa,EAAE,IAAI;YACnB,WAAW,EAAE,IAAI;YACjB,aAAa,EAAE,IAAI;YACnB,OAAO,EAAE,EAAE;SACZ,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAA4B,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO;QACL,IAAI;QACJ,IAAI;QACJ,aAAa,EAAE,UAAU,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;QACnD,WAAW,EAAE,UAAU,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QAC9C,aAAa,EAAE,MAAM,CAAC,gBAAgB,CAAC,IAAI,IAAI;QAC/C,OAAO;KACR,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,CAAU;IAC5B,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1C,CAAC"}
1
+ {"version":3,"file":"gate-registry.js","sourceRoot":"","sources":["../../../../src/lib/mcp/parse/gate-registry.ts"],"names":[],"mappings":"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"}