@sdd-method/author-cli 0.0.0-stage → 0.1.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 (203) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +17 -0
  3. package/README.md +6 -2
  4. package/dist/author-bin.js +8 -0
  5. package/dist/author-program.js +99 -0
  6. package/dist/lib/adr-origin.js +59 -0
  7. package/dist/lib/agent-harness.js +37 -0
  8. package/dist/lib/api-surface/__fixtures__/serverless-repo/serverless/serverless.yml +18 -0
  9. package/dist/lib/api-surface/analyzers/library.js +95 -0
  10. package/dist/lib/api-surface/analyzers/serverless.js +336 -0
  11. package/dist/lib/api-surface/analyzers/spring.js +237 -0
  12. package/dist/lib/api-surface/analyzers/terraform.js +109 -0
  13. package/dist/lib/api-surface/detect.js +25 -0
  14. package/dist/lib/api-surface/emit.js +109 -0
  15. package/dist/lib/api-surface/file-set.js +43 -0
  16. package/dist/lib/api-surface/index.js +134 -0
  17. package/dist/lib/api-surface/types.js +17 -0
  18. package/dist/lib/briefing/index.js +13 -0
  19. package/dist/lib/briefing/render.js +132 -0
  20. package/dist/lib/briefing/variables.js +92 -0
  21. package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
  22. package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
  23. package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
  24. package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
  25. package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
  26. package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
  27. package/dist/lib/catalogue/archetype/resolver.js +103 -0
  28. package/dist/lib/catalogue/archetype/wireframe.js +418 -0
  29. package/dist/lib/catalogue/baseline-check.js +151 -0
  30. package/dist/lib/catalogue/build.js +1502 -0
  31. package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
  32. package/dist/lib/catalogue/builders/adrs.js +222 -0
  33. package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
  34. package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
  35. package/dist/lib/catalogue/builders/c4-components.js +229 -0
  36. package/dist/lib/catalogue/builders/c4-containers.js +240 -0
  37. package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
  38. package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
  39. package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
  40. package/dist/lib/catalogue/builders/c4-systems.js +98 -0
  41. package/dist/lib/catalogue/builders/capabilities.js +488 -0
  42. package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
  43. package/dist/lib/catalogue/builders/capability-edges.js +256 -0
  44. package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
  45. package/dist/lib/catalogue/builders/capability-spec.js +348 -0
  46. package/dist/lib/catalogue/builders/container-dependency.js +183 -0
  47. package/dist/lib/catalogue/builders/contracts.js +402 -0
  48. package/dist/lib/catalogue/builders/data-items.js +344 -0
  49. package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
  50. package/dist/lib/catalogue/builders/domain-models.js +175 -0
  51. package/dist/lib/catalogue/builders/domains.js +119 -0
  52. package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
  53. package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
  54. package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
  55. package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
  56. package/dist/lib/catalogue/builders/entity-slug.js +47 -0
  57. package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
  58. package/dist/lib/catalogue/builders/event-edges.js +327 -0
  59. package/dist/lib/catalogue/builders/events.js +168 -0
  60. package/dist/lib/catalogue/builders/evidence-class.js +43 -0
  61. package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
  62. package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
  63. package/dist/lib/catalogue/builders/feature-contract.js +77 -0
  64. package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
  65. package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
  66. package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
  67. package/dist/lib/catalogue/builders/feature-groups.js +63 -0
  68. package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
  69. package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
  70. package/dist/lib/catalogue/builders/features.js +300 -0
  71. package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
  72. package/dist/lib/catalogue/builders/glossary.js +204 -0
  73. package/dist/lib/catalogue/builders/implementations.js +214 -0
  74. package/dist/lib/catalogue/builders/index.js +64 -0
  75. package/dist/lib/catalogue/builders/integration-components.js +344 -0
  76. package/dist/lib/catalogue/builders/integrations.js +181 -0
  77. package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
  78. package/dist/lib/catalogue/builders/persona-container.js +133 -0
  79. package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
  80. package/dist/lib/catalogue/builders/personas.js +152 -0
  81. package/dist/lib/catalogue/builders/product-edges.js +106 -0
  82. package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
  83. package/dist/lib/catalogue/builders/products.js +162 -0
  84. package/dist/lib/catalogue/builders/quality-scores.js +635 -0
  85. package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
  86. package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
  87. package/dist/lib/catalogue/builders/repo-edges.js +340 -0
  88. package/dist/lib/catalogue/builders/repositories.js +458 -0
  89. package/dist/lib/catalogue/builders/scaffolds.js +197 -0
  90. package/dist/lib/catalogue/builders/screen-tree.js +222 -0
  91. package/dist/lib/catalogue/builders/screens.js +396 -0
  92. package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
  93. package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
  94. package/dist/lib/catalogue/builders/service-contract.js +74 -0
  95. package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
  96. package/dist/lib/catalogue/builders/services.js +232 -0
  97. package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
  98. package/dist/lib/catalogue/builders/user-journeys.js +253 -0
  99. package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
  100. package/dist/lib/catalogue/cli-version.js +10 -0
  101. package/dist/lib/catalogue/contract-id.js +49 -0
  102. package/dist/lib/catalogue/coverage-predicates.js +603 -0
  103. package/dist/lib/catalogue/coverage-render.js +552 -0
  104. package/dist/lib/catalogue/coverage-run.js +115 -0
  105. package/dist/lib/catalogue/csv-reader.js +97 -0
  106. package/dist/lib/catalogue/csv-writer.js +96 -0
  107. package/dist/lib/catalogue/data-plane-coherence.js +77 -0
  108. package/dist/lib/catalogue/ddl-generator.js +233 -0
  109. package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
  110. package/dist/lib/catalogue/engine-validator.js +239 -0
  111. package/dist/lib/catalogue/ext-id.js +76 -0
  112. package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
  113. package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
  114. package/dist/lib/catalogue/finding-identity.js +183 -0
  115. package/dist/lib/catalogue/findings.js +46 -0
  116. package/dist/lib/catalogue/fk-validator.js +136 -0
  117. package/dist/lib/catalogue/freshness.js +70 -0
  118. package/dist/lib/catalogue/gate-checks.js +338 -0
  119. package/dist/lib/catalogue/graph-projection.js +346 -0
  120. package/dist/lib/catalogue/impact-run.js +228 -0
  121. package/dist/lib/catalogue/manifest-loader.js +435 -0
  122. package/dist/lib/catalogue/platform-scope.js +7 -0
  123. package/dist/lib/catalogue/population-checks.js +87 -0
  124. package/dist/lib/catalogue/relationship-semantics.js +66 -0
  125. package/dist/lib/catalogue/reverse-product-quality.js +99 -0
  126. package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
  127. package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
  128. package/dist/lib/catalogue/scaffold-delta.js +261 -0
  129. package/dist/lib/catalogue/schema-compat.js +52 -0
  130. package/dist/lib/catalogue/schema-loader.js +87 -0
  131. package/dist/lib/catalogue/scores-coverage.js +573 -0
  132. package/dist/lib/catalogue/screen-validation-loader.js +89 -0
  133. package/dist/lib/catalogue/service-architecture-parser.js +193 -0
  134. package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
  135. package/dist/lib/catalogue/staleness-verdicts.js +345 -0
  136. package/dist/lib/catalogue/traceability-loader.js +350 -0
  137. package/dist/lib/catalogue/validate.js +200 -0
  138. package/dist/lib/catalogue/version.js +18 -0
  139. package/dist/lib/conformance/binding.js +70 -0
  140. package/dist/lib/conformance/finding.js +69 -0
  141. package/dist/lib/conformance/index.js +578 -0
  142. package/dist/lib/context-pull/index.js +191 -0
  143. package/dist/lib/context-pull/resolve.js +70 -0
  144. package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
  145. package/dist/lib/contracts-availability/index.js +267 -0
  146. package/dist/lib/generate-briefing/index.js +77 -0
  147. package/dist/lib/list/discover.js +100 -0
  148. package/dist/lib/list/format.js +84 -0
  149. package/dist/lib/list/index.js +16 -0
  150. package/dist/lib/lock-file.js +437 -0
  151. package/dist/lib/pack/log.js +18 -0
  152. package/dist/lib/pack/schema.js +121 -0
  153. package/dist/lib/pack/seed.js +182 -0
  154. package/dist/lib/pack/validate.js +49 -0
  155. package/dist/lib/package-version.js +92 -0
  156. package/dist/lib/plan/index.js +69 -0
  157. package/dist/lib/plan/read-sdd.js +102 -0
  158. package/dist/lib/plan/render-template.js +84 -0
  159. package/dist/lib/repo-kind.js +129 -0
  160. package/dist/lib/scaffold/domain.js +234 -0
  161. package/dist/lib/scaffold/registry.js +136 -0
  162. package/dist/lib/scripts.js +7 -0
  163. package/dist/lib/sdd-index/repo-config.js +245 -0
  164. package/dist/lib/sdd-reader.js +91 -0
  165. package/dist/lib/snapshot/advance.js +454 -0
  166. package/dist/lib/snapshot/diff.js +190 -0
  167. package/dist/lib/snapshot/fold.js +208 -0
  168. package/dist/lib/snapshot/land.js +178 -0
  169. package/dist/lib/snapshot/manifest.js +206 -0
  170. package/dist/lib/snapshot/targets.js +169 -0
  171. package/dist/lib/snapshot/verify.js +91 -0
  172. package/dist/lib/sync/briefing-merge.js +106 -0
  173. package/dist/lib/sync/bundle-cache.js +109 -0
  174. package/dist/lib/sync/yaml-block-merge.js +120 -0
  175. package/dist/lib/validate/aggregate.js +51 -0
  176. package/dist/lib/validate/bundle-coherence.js +74 -0
  177. package/dist/lib/validate/dispatch.js +243 -0
  178. package/dist/lib/validate/index.js +110 -0
  179. package/dist/lib/validate/plan.js +54 -0
  180. package/dist/lib/validate/runner.js +85 -0
  181. package/dist/lib/validate/yaml-parse.js +112 -0
  182. package/dist/maintain-version.json +4 -0
  183. package/dist/verbs/api-surface.js +71 -0
  184. package/dist/verbs/catalogue-check.js +28 -0
  185. package/dist/verbs/catalogue-verify.js +114 -0
  186. package/dist/verbs/conformance.js +34 -0
  187. package/dist/verbs/context.js +39 -0
  188. package/dist/verbs/contracts.js +34 -0
  189. package/dist/verbs/generate-briefing.js +29 -0
  190. package/dist/verbs/list.js +45 -0
  191. package/dist/verbs/pack-validate.js +17 -0
  192. package/dist/verbs/path-option.js +25 -0
  193. package/dist/verbs/plan.js +42 -0
  194. package/dist/verbs/render.js +87 -0
  195. package/dist/verbs/scaffold.js +83 -0
  196. package/dist/verbs/snapshot.js +134 -0
  197. package/dist/verbs/validate.js +48 -0
  198. package/dist/verify-program.js +122 -0
  199. package/dist/verify-version.json +4 -0
  200. package/package.json +32 -4
  201. package/scripts/validate-app-dependencies.sh +664 -0
  202. package/scripts/validate-app-docs.sh +199 -0
  203. package/scripts/validate-integration-docs.sh +505 -0
@@ -0,0 +1,573 @@
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
+ import { readLockFile } from "../lock-file.js";
69
+ /**
70
+ * The declared mapping. Three entries are EXERCISED against real estate data
71
+ * (adr-quality, contract-quality, domain-architecture); the rest are declared
72
+ * from the registry and rubric shapes and have no populated corpus to verify
73
+ * against yet — a distinction the metric line states rather than blurs.
74
+ */
75
+ export const RUBRIC_ELIGIBILITY = [
76
+ // ── The banded reverse family (schema 3.3.0, Phase B4) ──────────────────
77
+ //
78
+ // EVERY join below was MEASURED against a real estate before being declared,
79
+ // and the three that do not resolve are declared unmeasurable rather than
80
+ // given a plausible transform. That discipline is not decorative: an earlier
81
+ // ADR mapping in this arc was "verified" by checking that its targets EXISTED
82
+ // in a dense id range, which is nearly free, and it turned out to point 75 of
83
+ // 99 scores at the wrong ADR. Existence is not correctness.
84
+ {
85
+ rubricId: "reverse-repo-quality.reverseability",
86
+ registryFile: "reverse-repo-quality-scores.yaml",
87
+ targetKind: "repository",
88
+ catalogue: "repositories",
89
+ joinColumn: "id",
90
+ refTransform: null,
91
+ eligibilityFilter: null,
92
+ eligibilityNote: "Every catalogued repository. Measured 123/123 resolving against 370 repositories — a real 33% coverage figure, not a join defect.",
93
+ },
94
+ {
95
+ rubricId: "reverse-repo-quality.repo_doc_quality",
96
+ registryFile: "reverse-repo-quality-scores.yaml",
97
+ targetKind: "repository",
98
+ catalogue: "repositories",
99
+ joinColumn: "id",
100
+ refTransform: null,
101
+ eligibilityFilter: null,
102
+ eligibilityNote: "Same population as reverseability — the two rubrics judge different things about the same repository. Measured 123/123.",
103
+ },
104
+ {
105
+ rubricId: "reverse-product-quality.product_synthesis_quality",
106
+ registryFile: "reverse-product-quality-scores.yaml",
107
+ targetKind: "product",
108
+ catalogue: "domains",
109
+ joinColumn: "id",
110
+ refTransform: null,
111
+ eligibilityFilter: null,
112
+ eligibilityNote: "Joins DOMAINS, not products, and that is measured rather than inferred from the name: 9/9 against domains, 0/9 against products. The reverse method's 'product' units are per-domain product areas; the target_kind records what the rubric calls them, the join records where they resolve.",
113
+ },
114
+ {
115
+ rubricId: "reverse-platform-evidence-quality.reverse_platform_evidence_quality",
116
+ registryFile: "reverse-platform-evidence-quality-scores.yaml",
117
+ targetKind: "platform_evidence",
118
+ catalogue: "domains",
119
+ joinColumn: "id",
120
+ refTransform: null,
121
+ eligibilityFilter: null,
122
+ eligibilityNote: "Platform evidence is assessed per domain. Measured 9/9.",
123
+ },
124
+ {
125
+ rubricId: "reverse-data-model-quality",
126
+ registryFile: "domain-artifact-scores.yaml",
127
+ targetKind: "data-model",
128
+ catalogue: "domain_models",
129
+ joinColumn: "id",
130
+ refTransform: null,
131
+ eligibilityFilter: null,
132
+ eligibilityNote: "Every domain model. Measured 9/9 against 12.",
133
+ },
134
+ {
135
+ rubricId: "reverse-glossary-quality",
136
+ registryFile: "domain-artifact-scores.yaml",
137
+ targetKind: "glossary",
138
+ catalogue: "domains",
139
+ joinColumn: "id",
140
+ refTransform: (r) => `regexp_replace(${r}, '-glossary$', '')`,
141
+ eligibilityFilter: null,
142
+ eligibilityNote: "One glossary per domain; refs are '<domain>-glossary'. Measured 9/9 after the suffix strip, 0/9 without it.",
143
+ },
144
+ {
145
+ rubricId: "reverse-c4-quality",
146
+ registryFile: "domain-artifact-scores.yaml",
147
+ targetKind: "c4-l2",
148
+ catalogue: "domains",
149
+ joinColumn: "id",
150
+ refTransform: (r) => `regexp_replace(${r}, '-c4-l2$', '')`,
151
+ eligibilityFilter: null,
152
+ eligibilityNote: "One L2 diagram per domain; refs are '<domain>-c4-l2'. Measured 8/8 after the strip, 0/8 without it.",
153
+ },
154
+ {
155
+ rubricId: "reverse-capability-spec-quality",
156
+ registryFile: "domain-artifact-scores.yaml",
157
+ targetKind: "capability-spec",
158
+ catalogue: "capabilities",
159
+ joinColumn: "id",
160
+ // Refs are '<domain>-capability-spec-<capability>'; the capability is the tail.
161
+ refTransform: (r) => `regexp_replace(${r}, '^.*-capability-spec-', '')`,
162
+ eligibilityFilter: "status <> 'deprecated'",
163
+ eligibilityNote: "Measured 19/19 on the tail. The domain prefix is NOT part of capabilities.id here, so the whole ref resolves to nothing (0/19) — the transform is required, not cosmetic. Excludes deprecated capabilities on the same footing as feature-grounding and traceability-manifest: three rubrics judging one catalogue must agree about who is in the population, and a rubric that disagreed would reintroduce as a metric discrepancy exactly the retirement the other two just retired. Missed on the first pass because the control hardcoded two rubric names instead of deriving them; it now derives them.",
164
+ },
165
+ // ── Banded rubrics with NO catalogued eligibility population ─────────────
166
+ {
167
+ rubricId: "reverse-feature-grounding-quality",
168
+ registryFile: "domain-artifact-scores.yaml",
169
+ targetKind: "feature-grounding",
170
+ catalogue: null,
171
+ joinColumn: "id",
172
+ refTransform: null,
173
+ eligibilityFilter: null,
174
+ eligibilityNote: "NO local population, measured rather than assumed. Refs are '<domain>-feature-grounding-<slug>' and the tails are SIBLING SDD SLUGS (app-shell, cold-chain, connect-rapide), not rows in this SDD: 0/15 against products, applications, features and repositories, and 1/15 against capabilities — a coincidence, not a join. A cross-SDD reference the member tier cannot resolve, so its coverage is declared unmeasurable rather than given a transform that would produce plausible wrong numbers.",
175
+ },
176
+ {
177
+ rubricId: "reverse-contract-quality",
178
+ registryFile: "domain-artifact-scores.yaml",
179
+ targetKind: "contract",
180
+ catalogue: null,
181
+ joinColumn: "id",
182
+ refTransform: null,
183
+ eligibilityFilter: null,
184
+ eligibilityNote: "HETEROGENEOUS targets in one rubric: '<domain>-event-catalog' (resolves to domains, 2/2) and '<domain>-openapi-<service>' (0/4 against contracts — the tails are service names, not contract ids). A rubric gets one eligibility entry, and half a join is worse than none, so the whole rubric is declared unmeasurable until the reverse method splits it.",
185
+ },
186
+ // ── The forward house form ───────────────────────────────────────────────
187
+ {
188
+ rubricId: "adr-quality",
189
+ targetKind: "adr",
190
+ catalogue: "adrs",
191
+ joinColumn: "id",
192
+ // The catalogue strips the tier prefix (M-ADR 0186 §4.4); registries keep it.
193
+ refTransform: (r) => `regexp_replace(${r}, '^[A-Z]+-ADR-', 'ADR-')`,
194
+ eligibilityFilter: "status = 'Accepted'",
195
+ managedPathColumn: "doc_path",
196
+ canonicalPathColumn: "doc_path",
197
+ eligibilityNote: "Accepted only, and adopter-owned 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. The managed-path rule adds the second half of the same argument, ruled 2026-08-07: an adopter cannot remediate an ADR that sync overwrites, so a method-managed ADR is not in the adopter's denominator — its quality belongs to the producer's own registry, and an adopter's assessment of it is feedback to raise rather than a row to maintain. Measured at both estates 2026-08-08 (committed catalogues, member tier): forward 203 Accepted -> 83 adopter-owned (120 excluded); reverse 182 Accepted -> 62 adopter-owned (120 excluded). Zero rows under a producer path escaped the lock at either estate, so the lock and a path heuristic agree on today's data — the lock is used anyway, because agreement today is not the same as being the right rule.",
198
+ },
199
+ {
200
+ rubricId: "contract-quality",
201
+ targetKind: "contract",
202
+ catalogue: "contracts",
203
+ joinColumn: "spec_path",
204
+ refTransform: null,
205
+ eligibilityFilter: "status <> 'deprecated'",
206
+ canonicalPathColumn: "spec_path",
207
+ 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.",
208
+ },
209
+ {
210
+ rubricId: "domain-architecture",
211
+ targetKind: "domain",
212
+ catalogue: "domains",
213
+ joinColumn: "id",
214
+ refTransform: null,
215
+ eligibilityFilter: null,
216
+ eligibilityNote: "Every domain on disk. A domain that exists is a domain that should be assessed.",
217
+ },
218
+ {
219
+ rubricId: "intent-quality",
220
+ targetKind: "domain",
221
+ catalogue: "domains",
222
+ joinColumn: "id",
223
+ refTransform: null,
224
+ eligibilityFilter: null,
225
+ eligibilityNote: "Every domain, same population as domain-architecture — the two rubrics judge different things about the same unit.",
226
+ },
227
+ {
228
+ rubricId: "feature-grounding",
229
+ targetKind: "capability",
230
+ catalogue: "capabilities",
231
+ joinColumn: "id",
232
+ refTransform: null,
233
+ eligibilityFilter: "status <> 'deprecated'",
234
+ eligibilityNote: "Every capability. EXCLUDES DEPRECATED CAPABILITIES — the SAME PATTERN contract-quality already applies to the contracts catalogue (identical operator and value, and its note gives the same reason: they are on the way out, and scoring them is work with no consumer), and the same argument adr-quality makes for a Superseded ADR. This applies an existing pattern to a third catalogue rather than setting a new one. This is the mechanism that retires a withdrawn capability's permanently-unanswerable findings, replacing the M-ADR 0199 exemption vocabulary that was investigated and NOT built: across both estates the authored-intent evidence base was zero, and a retirement expressed in the catalogue's OWN documented vocabulary needs no annotation surface. It resolves only where the retirement is DECLARED — a capability whose traceability manifest was deleted has no authored status and falls to the builder's hardcoded default, so declaring the state is the estate-side half.",
235
+ },
236
+ {
237
+ rubricId: "traceability-manifest",
238
+ targetKind: "capability",
239
+ catalogue: "capabilities",
240
+ joinColumn: "id",
241
+ refTransform: null,
242
+ eligibilityFilter: "status <> 'deprecated'",
243
+ eligibilityNote: "Every capability. Excludes deprecated capabilities per feature-grounding's note — the rubrics judging this catalogue must agree about who is in the population.",
244
+ },
245
+ {
246
+ rubricId: "glossary",
247
+ targetKind: "glossary",
248
+ catalogue: "domains",
249
+ joinColumn: "id",
250
+ refTransform: null,
251
+ eligibilityFilter: null,
252
+ 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).",
253
+ },
254
+ {
255
+ rubricId: "persona",
256
+ targetKind: "persona",
257
+ catalogue: "personas",
258
+ joinColumn: "id",
259
+ refTransform: null,
260
+ eligibilityFilter: null,
261
+ eligibilityNote: "Every persona.",
262
+ },
263
+ {
264
+ rubricId: "product-definition",
265
+ targetKind: "product",
266
+ catalogue: "products",
267
+ joinColumn: "id",
268
+ refTransform: null,
269
+ eligibilityFilter: null,
270
+ eligibilityNote: "Every product.",
271
+ },
272
+ {
273
+ rubricId: "screen-spec",
274
+ targetKind: "screen",
275
+ catalogue: "screens",
276
+ joinColumn: "id",
277
+ refTransform: null,
278
+ eligibilityFilter: null,
279
+ eligibilityNote: "Every screen.",
280
+ },
281
+ {
282
+ rubricId: "integration-sdd-quality",
283
+ targetKind: "integration",
284
+ catalogue: "integrations",
285
+ joinColumn: "id",
286
+ refTransform: null,
287
+ eligibilityFilter: null,
288
+ eligibilityNote: "Every integration.",
289
+ },
290
+ // ── No catalogued eligibility population ────────────────────────────────
291
+ // These rubrics score artefacts the catalogue model does not hold, so there
292
+ // is no denominator to compute. Declared explicitly so they appear in the
293
+ // output as "no denominator" rather than being absent — an absent rubric
294
+ // reads as a rubric with nothing to report.
295
+ {
296
+ rubricId: "c4-diagram",
297
+ targetKind: "c4_diagram",
298
+ catalogue: null,
299
+ joinColumn: "id",
300
+ refTransform: null,
301
+ eligibilityFilter: null,
302
+ eligibilityNote: "Diagrams are files, not catalogue rows.",
303
+ },
304
+ {
305
+ rubricId: "strategic-doc",
306
+ targetKind: "strategic_doc",
307
+ catalogue: null,
308
+ joinColumn: "id",
309
+ refTransform: null,
310
+ eligibilityFilter: null,
311
+ eligibilityNote: "Strategic documents are not catalogued.",
312
+ },
313
+ {
314
+ rubricId: "user-story",
315
+ targetKind: "user_story",
316
+ catalogue: null,
317
+ joinColumn: "id",
318
+ refTransform: null,
319
+ eligibilityFilter: null,
320
+ eligibilityNote: "User stories are not catalogued.",
321
+ },
322
+ {
323
+ rubricId: "work-order",
324
+ targetKind: "work_order",
325
+ catalogue: null,
326
+ joinColumn: "id",
327
+ refTransform: null,
328
+ eligibilityFilter: null,
329
+ eligibilityNote: "Work orders are not catalogued.",
330
+ },
331
+ ];
332
+ const sqlStr = (s) => `'${s.replace(/'/g, "''")}'`;
333
+ async function rows(session, sql) {
334
+ const reader = await session.conn.runAndReadAll(sql);
335
+ return reader.getRows();
336
+ }
337
+ /**
338
+ * Every path claimed by a synced bundle in this repo's lock, mapped to the
339
+ * bundle claiming it.
340
+ *
341
+ * Empty at the producer — it carries no `method-baseline.lock`, so nothing is
342
+ * written from elsewhere and nothing is excluded. That is the degenerate case
343
+ * working by construction rather than by a repo-kind check: `canonical-method`
344
+ * is not even a member of the CLI's `RepoKind` union, so a check on it would
345
+ * have been a special case resting on a marker the type system does not model.
346
+ */
347
+ async function managedPathOwners(sddRoot) {
348
+ const lock = await readLockFile(sddRoot);
349
+ const owners = new Map();
350
+ if (!lock)
351
+ return owners;
352
+ for (const bundle of lock.bundles) {
353
+ for (const p of bundle.managedPaths) {
354
+ // First claim wins, matching `pathToBundleMap`: a path claimed twice is a
355
+ // sync-side concern, and this predicate is not the place to adjudicate it.
356
+ if (!owners.has(p))
357
+ owners.set(p, bundle.name);
358
+ }
359
+ }
360
+ return owners;
361
+ }
362
+ /**
363
+ * Runs the predicate. `available` names the catalogues actually present, so a
364
+ * rubric whose population file is absent is skipped the way every other
365
+ * predicate skips an absent population — silently, because absence of the
366
+ * whole surface is not a gap in it — while a rubric whose population is
367
+ * PRESENT but empty still reports its zero.
368
+ */
369
+ export async function runScoresCoverage(session, options) {
370
+ const findings = [];
371
+ const metrics = [];
372
+ if (!options.available.has("quality_scores")) {
373
+ return {
374
+ findings: [],
375
+ metrics: [],
376
+ undeclaredRubrics: [],
377
+ notAdopted: [],
378
+ stalenessBasis: { corpusRef: 0, scoredAt: 0, none: 0 },
379
+ totalScores: 0,
380
+ transformBridgedRefs: 0,
381
+ };
382
+ }
383
+ const totalScores = Number((await rows(session, `SELECT COUNT(*) FROM "quality_scores";`))[0][0]);
384
+ const transformBridgedRefs = Number((await rows(session, `SELECT COUNT(*) FROM "quality_scores"
385
+ WHERE rubric_id = 'adr-quality' AND regexp_matches(target_ref, '^[A-Z]+-ADR-');`))[0][0]);
386
+ const basis = await rows(session, `SELECT staleness_basis, COUNT(*) FROM "quality_scores" GROUP BY 1 ORDER BY 1;`);
387
+ const stalenessBasis = { corpusRef: 0, scoredAt: 0, none: 0 };
388
+ for (const [b, n] of basis) {
389
+ const c = Number(n);
390
+ if (b === "corpus_ref")
391
+ stalenessBasis.corpusRef = c;
392
+ else if (b === "scored_at")
393
+ stalenessBasis.scoredAt = c;
394
+ else
395
+ stalenessBasis.none = c;
396
+ }
397
+ // A row that can support no staleness verdict at all is its own finding.
398
+ // Without it the predicate would silently treat "unknown" as "fresh", which
399
+ // is the basis error this column exists to prevent.
400
+ if (stalenessBasis.none > 0) {
401
+ findings.push({
402
+ kind: "unresolvable-staleness",
403
+ rubricId: "(all)",
404
+ subject: `${stalenessBasis.none} score(s) carry neither corpus_ref nor a resolvable target_path — their currency is UNKNOWN, not fresh`,
405
+ });
406
+ }
407
+ const managedOwners = await managedPathOwners(options.sddRoot);
408
+ const declared = new Set(RUBRIC_ELIGIBILITY.map((r) => r.rubricId));
409
+ const present = await rows(session, `SELECT DISTINCT rubric_id FROM "quality_scores" ORDER BY 1;`);
410
+ const undeclaredRubrics = present
411
+ .map((r) => String(r[0]))
412
+ .filter((r) => !declared.has(r));
413
+ const adopted = (spec) => existsSync(join(options.sddRoot, "orchestration", "gate-registry", spec.registryFile ?? `${spec.rubricId}-scores.yaml`));
414
+ const notAdopted = [];
415
+ for (const spec of RUBRIC_ELIGIBILITY) {
416
+ if (!adopted(spec)) {
417
+ notAdopted.push(spec.rubricId);
418
+ continue;
419
+ }
420
+ const scored = Number((await rows(session, `SELECT COUNT(*) FROM "quality_scores" WHERE rubric_id = ${sqlStr(spec.rubricId)};`))[0][0]);
421
+ // No catalogued population: report the scores, state that there is no
422
+ // denominator, and do not invent one.
423
+ if (spec.catalogue === null) {
424
+ if (scored > 0) {
425
+ metrics.push({
426
+ rubricId: spec.rubricId,
427
+ targetKind: spec.targetKind,
428
+ eligible: null,
429
+ covered: scored,
430
+ scored,
431
+ orphaned: 0,
432
+ managedExcluded: 0,
433
+ managedExcludedByBundle: {},
434
+ recordedPath: null,
435
+ });
436
+ }
437
+ continue;
438
+ }
439
+ if (!options.available.has(spec.catalogue))
440
+ continue;
441
+ // The method-managed exclusion, resolved BEFORE any count is taken so that
442
+ // every figure below shares one denominator. Computed here rather than
443
+ // inlined at each query because `eligibleWhere` reaches four call sites,
444
+ // and a rule applied to three of them is worse than a rule applied to none.
445
+ const excludedRows = spec.managedPathColumn && managedOwners.size > 0
446
+ ? await rows(session, `SELECT "${spec.managedPathColumn}" FROM "${spec.catalogue}"
447
+ WHERE (${spec.eligibilityFilter ?? "TRUE"})
448
+ AND "${spec.managedPathColumn}" IN (${[...managedOwners.keys()].map(sqlStr).join(", ")});`)
449
+ : [];
450
+ const managedExcludedByBundle = {};
451
+ for (const [p] of excludedRows) {
452
+ const bundle = managedOwners.get(String(p)) ?? "(unknown bundle)";
453
+ managedExcludedByBundle[bundle] = (managedExcludedByBundle[bundle] ?? 0) + 1;
454
+ }
455
+ const managedExcluded = excludedRows.length;
456
+ const eligibleWhere = managedExcluded > 0
457
+ ? `(${spec.eligibilityFilter ?? "TRUE"}) AND "${spec.managedPathColumn}" NOT IN (${[
458
+ ...managedOwners.keys(),
459
+ ]
460
+ .map(sqlStr)
461
+ .join(", ")})`
462
+ : (spec.eligibilityFilter ?? "TRUE");
463
+ if (scored === 0) {
464
+ // The population exists and nothing is scored — the loudest case, and
465
+ // exactly the silence M-ADR 0205 §4.5 names. Every eligible row is a gap.
466
+ const eligibleRows = await rows(session, `SELECT id FROM "${spec.catalogue}" WHERE (${eligibleWhere}) ORDER BY id;`);
467
+ for (const [id] of eligibleRows) {
468
+ findings.push({ kind: "uncovered", rubricId: spec.rubricId, subject: String(id) });
469
+ }
470
+ metrics.push({
471
+ rubricId: spec.rubricId,
472
+ targetKind: spec.targetKind,
473
+ eligible: eligibleRows.length,
474
+ covered: 0,
475
+ scored: 0,
476
+ orphaned: 0,
477
+ managedExcluded,
478
+ managedExcludedByBundle,
479
+ recordedPath: spec.canonicalPathColumn !== undefined ? { agree: 0, mismatch: 0, noRecordedPath: 0 } : null,
480
+ });
481
+ continue;
482
+ }
483
+ const ref = spec.refTransform ? spec.refTransform("s.target_ref") : "s.target_ref";
484
+ // Eligibility is always a conjunct, so it composes with the not-exists
485
+ // clause without any string surgery on an assembled WHERE — `TRUE` for the
486
+ // unfiltered case keeps one query shape instead of two.
487
+ const scoreSet = `SELECT * FROM "quality_scores" WHERE rubric_id = ${sqlStr(spec.rubricId)}`;
488
+ // Direction 1 — eligible targets with no score.
489
+ const uncovered = await rows(session, `SELECT c.id FROM "${spec.catalogue}" c
490
+ WHERE (${eligibleWhere})
491
+ AND NOT EXISTS (SELECT 1 FROM (${scoreSet}) s WHERE ${ref} = c."${spec.joinColumn}")
492
+ ORDER BY c.id;`);
493
+ for (const [id] of uncovered) {
494
+ findings.push({ kind: "uncovered", rubricId: spec.rubricId, subject: String(id) });
495
+ }
496
+ // Direction 2 — scores whose target does not resolve.
497
+ const orphaned = await rows(session, `SELECT s.target_ref FROM (${scoreSet}) s
498
+ WHERE NOT EXISTS (
499
+ SELECT 1 FROM "${spec.catalogue}" c WHERE c."${spec.joinColumn}" = ${ref}
500
+ )
501
+ ORDER BY s.target_ref;`);
502
+ for (const [r] of orphaned) {
503
+ findings.push({ kind: "orphaned-score", rubricId: spec.rubricId, subject: String(r) });
504
+ }
505
+ // Direction 3 — scores whose target RESOLVES but sits outside this repo's
506
+ // scope. Only reachable where the managed rule actually excluded something.
507
+ if (managedExcluded > 0) {
508
+ const outOfScope = await rows(session, `SELECT s.target_ref FROM (${scoreSet}) s
509
+ WHERE EXISTS (
510
+ SELECT 1 FROM "${spec.catalogue}" c
511
+ WHERE c."${spec.joinColumn}" = ${ref}
512
+ AND c."${spec.managedPathColumn}" IN (${[...managedOwners.keys()]
513
+ .map(sqlStr)
514
+ .join(", ")})
515
+ )
516
+ ORDER BY s.target_ref;`);
517
+ for (const [r] of outOfScope) {
518
+ findings.push({
519
+ kind: "out-of-scope-score",
520
+ rubricId: spec.rubricId,
521
+ subject: String(r),
522
+ });
523
+ }
524
+ }
525
+ // Direction 4 — a resolved score whose RECORDED path is not the target's
526
+ // canonical path (decision #3, report-only). Paths are normalised on both
527
+ // sides (leading `./` stripped, backslashes to `/`) before comparison. A
528
+ // path the builder filled in from the catalogue agrees by construction —
529
+ // the metric says "agree", not "verified".
530
+ let recordedPath = null;
531
+ if (spec.canonicalPathColumn !== undefined) {
532
+ const pairs = await rows(session, `SELECT s.target_ref, s.target_path, c."${spec.canonicalPathColumn}"
533
+ FROM (${scoreSet}) s
534
+ JOIN "${spec.catalogue}" c ON c."${spec.joinColumn}" = ${ref}
535
+ ORDER BY s.target_ref;`);
536
+ let agree = 0;
537
+ let mismatch = 0;
538
+ let noRecordedPath = 0;
539
+ const n = (v) => String(v ?? "").replace(/\\/g, "/").replace(/^(\.\/)+/, "");
540
+ for (const [targetRef, recorded, canonical] of pairs) {
541
+ const r = n(recorded);
542
+ const c = n(canonical);
543
+ if (r === "")
544
+ noRecordedPath++;
545
+ else if (c === "" || r === c)
546
+ agree++;
547
+ else {
548
+ mismatch++;
549
+ findings.push({
550
+ kind: "recorded-path-mismatch",
551
+ rubricId: spec.rubricId,
552
+ subject: `${String(targetRef)} — recorded "${r}", catalogue "${c}"`,
553
+ });
554
+ }
555
+ }
556
+ recordedPath = { agree, mismatch, noRecordedPath };
557
+ }
558
+ const eligible = Number((await rows(session, `SELECT COUNT(*) FROM "${spec.catalogue}" WHERE (${eligibleWhere});`))[0][0]);
559
+ metrics.push({
560
+ rubricId: spec.rubricId,
561
+ targetKind: spec.targetKind,
562
+ eligible,
563
+ covered: eligible - uncovered.length,
564
+ scored,
565
+ orphaned: orphaned.length,
566
+ managedExcluded,
567
+ managedExcludedByBundle,
568
+ recordedPath,
569
+ });
570
+ }
571
+ return { findings, metrics, undeclaredRubrics, notAdopted, stalenessBasis, totalScores, transformBridgedRefs };
572
+ }
573
+ //# sourceMappingURL=scores-coverage.js.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Loader for the optional `.sdd/screen-validation.yaml` config — the
3
+ * adopter-tunable parameters for screen-spec composition-depth validation
4
+ * (ADR 0168 §4.5) and the archetype overlay (ADR 0169 §4.3).
5
+ *
6
+ * Every field is optional and carries a method default; an absent file means
7
+ * "method defaults only" (the vocabulary-free checks all still run). Follows
8
+ * the same shape convention as the `.sdd/*-aliases.yaml` loaders.
9
+ *
10
+ * File shape:
11
+ *
12
+ * flat_threshold: 6 # screen-flat-composition N (default 6)
13
+ * decorative_primitives: # unlocks screen-styling-primitive-as-structure
14
+ * - Spacer
15
+ * - Divider
16
+ * archetype_overlays: # per-platform overlay (ADR 0169 §4.3)
17
+ * web-generic:
18
+ * strip_prefixes: [Mdc, Mde] # render-only: stripped when humanising labels
19
+ * overrides:
20
+ * VBespokeGrid: grid
21
+ * swiftui:
22
+ * overrides:
23
+ * BrandCard: card
24
+ *
25
+ * The decorative set + the overlays are adopter-owned (vendor-standard defaults
26
+ * ship in the method; bespoke names live here). The checks themselves stay
27
+ * vocabulary-free — they consume these parameters, they don't hardcode them.
28
+ */
29
+ import { readFile } from "node:fs/promises";
30
+ import { join } from "node:path";
31
+ import { parse as parseYaml } from "yaml";
32
+ export const SCREEN_VALIDATION_REL_PATH = join(".sdd", "screen-validation.yaml");
33
+ /**
34
+ * Load the optional screen-validation config. Returns `null` when the file is
35
+ * absent — callers apply method defaults. Throws on malformed YAML.
36
+ */
37
+ export async function loadScreenValidationConfig(sddPath) {
38
+ const full = join(sddPath, SCREEN_VALIDATION_REL_PATH);
39
+ let text;
40
+ try {
41
+ text = await readFile(full, "utf-8");
42
+ }
43
+ catch (err) {
44
+ if (err.code === "ENOENT")
45
+ return null;
46
+ throw err;
47
+ }
48
+ const raw = parseYaml(text);
49
+ if (raw === null || typeof raw !== "object") {
50
+ throw new Error(`${SCREEN_VALIDATION_REL_PATH}: expected a mapping at the document root`);
51
+ }
52
+ const flatThreshold = typeof raw.flat_threshold === "number" && Number.isFinite(raw.flat_threshold) && raw.flat_threshold > 0
53
+ ? raw.flat_threshold
54
+ : undefined;
55
+ const decorativePrimitives = new Set();
56
+ if (Array.isArray(raw.decorative_primitives)) {
57
+ for (const v of raw.decorative_primitives) {
58
+ if (typeof v === "string" && v !== "")
59
+ decorativePrimitives.add(v);
60
+ }
61
+ }
62
+ const archetypeOverlays = new Map();
63
+ if (raw.archetype_overlays !== null && typeof raw.archetype_overlays === "object" && !Array.isArray(raw.archetype_overlays)) {
64
+ for (const [platform, body] of Object.entries(raw.archetype_overlays)) {
65
+ const overrides = toStringMap(body?.overrides);
66
+ const stripPrefixes = toStringList(body?.strip_prefixes);
67
+ if (Object.keys(overrides).length > 0 || stripPrefixes.length > 0) {
68
+ archetypeOverlays.set(platform, { overrides, strip_prefixes: stripPrefixes });
69
+ }
70
+ }
71
+ }
72
+ return { flatThreshold, decorativePrimitives, archetypeOverlays, relPath: SCREEN_VALIDATION_REL_PATH };
73
+ }
74
+ function toStringMap(raw) {
75
+ const out = {};
76
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
77
+ return out;
78
+ for (const [k, v] of Object.entries(raw)) {
79
+ if (typeof v === "string" && v !== "")
80
+ out[k] = v;
81
+ }
82
+ return out;
83
+ }
84
+ function toStringList(raw) {
85
+ if (!Array.isArray(raw))
86
+ return [];
87
+ return raw.filter((v) => typeof v === "string" && v !== "");
88
+ }
89
+ //# sourceMappingURL=screen-validation-loader.js.map