@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,418 @@
1
+ // Deterministic structural-wireframe renderer (ADR 0169 §4.4).
2
+ //
3
+ // Lowers a screen's openui-lang block to an SVG wireframe: parse the block into
4
+ // a composition tree, resolve each node to a visual archetype, and lay it out
5
+ // driven by the archetype's render hints — orientation, shape (bar / chips /
6
+ // grid / card / floating / paged / leaf shapes), slots, overlay, active_first,
7
+ // header_row. Each node is drawn as its archetype's visual SHAPE (an app-bar is
8
+ // a band, a tab-strip is a row of chips, a paged-panes shows the active pane and
9
+ // collapses the rest, a table has a header row, a modal floats), labelled with
10
+ // the component name + resolved archetype and shaded by resolution provenance so
11
+ // low-confidence inferences stay visible. A structural wireframe, not a pixel
12
+ // design; parameterised entirely by the archetype map (adopter-agnostic — no
13
+ // component vocabulary, no adopter knowledge).
14
+ //
15
+ // Deterministic companion to the LLM-driven render-screen-wireframe skill.
16
+ import { buildCompositionTree, stripBlockComments } from "../builders/screen-tree.js";
17
+ import { archetypeInfo, isKnownPlatform, mapStripPrefixes, resolveArchetype, } from "./resolver.js";
18
+ // --- geometry constants ----------------------------------------------------
19
+ const PAD = 8;
20
+ const GAP = 6;
21
+ const LABEL_H = 16; // container label strip
22
+ const TITLE_H = 26;
23
+ const TABLE_HDR_H = 20;
24
+ const COLLAPSE_H = 22; // collapsed (inactive) paged pane row
25
+ const SHADOW = 4; // floating/card drop-shadow offset
26
+ const GRID_COLS = 2;
27
+ /** Render an openui-lang block to a structural wireframe SVG string. */
28
+ export function renderWireframe(input) {
29
+ const tree = buildCompositionTree(stripBlockComments(input.block));
30
+ if (tree === null) {
31
+ return emptySvg(input.title, "empty composition — no statements");
32
+ }
33
+ const platform = isKnownPlatform(input.platform) ? input.platform : undefined;
34
+ const prefixes = [...(input.overlay?.strip_prefixes ?? []), ...mapStripPrefixes(platform)];
35
+ const root = measure(shape(tree, platform, input.overlay, prefixes));
36
+ const titleOffset = input.title ? TITLE_H : 0;
37
+ const width = root.w + PAD * 2;
38
+ const height = root.h + PAD * 2 + titleOffset;
39
+ const body = [];
40
+ if (input.title)
41
+ body.push(text(PAD, 17, escapeXml(input.title), "title"));
42
+ emit(root, PAD, PAD + titleOffset, body);
43
+ return svg(width, height, body.join("\n"));
44
+ }
45
+ // --- shape: resolve archetype + classify a render mode per node ------------
46
+ const LEAF_SHAPES = new Set(["button", "chip", "text", "image", "circle", "row"]);
47
+ function shape(node, platform, overlay, prefixes) {
48
+ const children = node.children.map((c) => shape(c, platform, overlay, prefixes));
49
+ const isContainer = children.length > 0;
50
+ linkPagedTabs(children, prefixes);
51
+ if (node.component === "" || platform === undefined) {
52
+ return base("", "none", isContainer ? "stack" : "leaf", "box");
53
+ }
54
+ const { archetype, provenance } = resolveArchetype(node.component, platform, overlay);
55
+ const r = archetypeInfo(archetype).render;
56
+ const sh = r.shape ?? "";
57
+ if (!isContainer) {
58
+ const leafShape = LEAF_SHAPES.has(sh) ? sh : "box";
59
+ return base(archetype, provenance, "leaf", leafShape);
60
+ }
61
+ let mode;
62
+ if (sh === "bar")
63
+ mode = "bar";
64
+ else if (sh === "chips")
65
+ mode = "chips";
66
+ else if (sh === "grid")
67
+ mode = r.header_row ? "table" : "grid";
68
+ else if (sh === "paged")
69
+ mode = "paged";
70
+ else if (sh === "card")
71
+ mode = "card";
72
+ else if (sh === "floating")
73
+ mode = "floating";
74
+ else
75
+ mode = r.orientation === "horizontal" ? "row" : "stack";
76
+ return base(archetype, provenance, mode, "box");
77
+ function base(archetype, provenance, mode, leafShape) {
78
+ return { node, archetype, provenance, mode, leafShape, children, stripPrefixes: prefixes, derivedLabel: "", w: 0, h: 0 };
79
+ }
80
+ }
81
+ /**
82
+ * Derive tab labels from windowed content (deterministic, spec-faithful): when a
83
+ * sibling group holds a tab-strip and a paged-panes with the SAME pane count, and
84
+ * the tabs carry no authored label, label each tab from its corresponding pane's
85
+ * content component name. Reads only names already in the spec — no fabrication.
86
+ */
87
+ function linkPagedTabs(siblings, prefixes) {
88
+ const strip = siblings.find((s) => s.archetype === "tab-strip");
89
+ const paged = siblings.find((s) => s.archetype === "paged-panes");
90
+ if (!strip || !paged)
91
+ return;
92
+ if (strip.children.length === 0 || strip.children.length !== paged.children.length)
93
+ return;
94
+ strip.children.forEach((tab, i) => {
95
+ if (tab.node.args.length > 0 || tab.derivedLabel)
96
+ return; // authored label wins
97
+ const pane = paged.children[i];
98
+ const contentName = pane.children[0]?.node.component || pane.node.component;
99
+ if (contentName)
100
+ tab.derivedLabel = humaniseName(contentName, prefixes);
101
+ });
102
+ }
103
+ // --- measure: size every node bottom-up ------------------------------------
104
+ function leafSize(shapeName, lbl) {
105
+ switch (shapeName) {
106
+ case "button": return { w: Math.max(84, lbl.length * 6 + 16), h: 26 };
107
+ case "chip": return { w: Math.max(56, lbl.length * 6 + 14), h: 22 };
108
+ case "text": return { w: Math.max(120, lbl.length * 6 + 12), h: 28 };
109
+ case "image": return { w: 72, h: 54 };
110
+ case "circle": return { w: 44, h: 44 };
111
+ case "row": return { w: Math.max(200, lbl.length * 6 + 24), h: 26 };
112
+ default: return { w: Math.max(120, lbl.length * 6.2 + 12), h: 40 };
113
+ }
114
+ }
115
+ function measure(s) {
116
+ if (s.mode === "leaf") {
117
+ const { w, h } = leafSize(s.leafShape, labelPlain(s));
118
+ s.w = w;
119
+ s.h = h;
120
+ return s;
121
+ }
122
+ s.children.forEach(measure);
123
+ const cs = s.children;
124
+ let innerW;
125
+ let innerH;
126
+ switch (s.mode) {
127
+ case "row":
128
+ case "bar":
129
+ case "chips": {
130
+ innerW = cs.reduce((a, c) => a + c.w, 0) + GAP * (cs.length - 1);
131
+ innerH = Math.max(...cs.map((c) => c.h));
132
+ break;
133
+ }
134
+ case "grid": {
135
+ const colW = Math.max(...cs.map((c) => c.w));
136
+ const rowH = Math.max(...cs.map((c) => c.h));
137
+ const rows = Math.ceil(cs.length / GRID_COLS);
138
+ const cols = Math.min(GRID_COLS, cs.length);
139
+ innerW = cols * colW + GAP * (cols - 1);
140
+ innerH = rows * rowH + GAP * (rows - 1);
141
+ break;
142
+ }
143
+ case "table": {
144
+ innerW = Math.max(180, ...cs.map((c) => c.w));
145
+ innerH = TABLE_HDR_H + GAP + cs.reduce((a, c) => a + c.h, 0) + GAP * (cs.length - 1);
146
+ break;
147
+ }
148
+ case "paged": {
149
+ const active = cs[0];
150
+ const restN = cs.length - 1;
151
+ innerW = Math.max(160, active.w);
152
+ innerH = active.h + (restN > 0 ? GAP + restN * COLLAPSE_H + GAP * (restN - 1) : 0);
153
+ break;
154
+ }
155
+ default: {
156
+ // stack / card / floating
157
+ innerW = Math.max(...cs.map((c) => c.w));
158
+ innerH = cs.reduce((a, c) => a + c.h, 0) + GAP * (cs.length - 1);
159
+ }
160
+ }
161
+ s.w = innerW + PAD * 2;
162
+ s.h = LABEL_H + innerH + PAD * 2;
163
+ return s;
164
+ }
165
+ // --- emit: place + draw top-down -------------------------------------------
166
+ function emit(s, x, y, out) {
167
+ if (s.mode === "leaf") {
168
+ drawLeaf(s, x, y, out);
169
+ return;
170
+ }
171
+ drawContainer(s, x, y, out);
172
+ const cx0 = x + PAD;
173
+ const cy0 = y + LABEL_H + PAD;
174
+ const cs = s.children;
175
+ switch (s.mode) {
176
+ case "row":
177
+ case "bar":
178
+ case "chips": {
179
+ const rowH = Math.max(...cs.map((c) => c.h));
180
+ let cx = cx0;
181
+ for (const c of cs) {
182
+ emit(c, cx, cy0 + (rowH - c.h) / 2, out);
183
+ cx += c.w + GAP;
184
+ }
185
+ break;
186
+ }
187
+ case "grid": {
188
+ const colW = Math.max(...cs.map((c) => c.w));
189
+ const rowH = Math.max(...cs.map((c) => c.h));
190
+ cs.forEach((c, i) => {
191
+ const col = i % GRID_COLS;
192
+ const row = Math.floor(i / GRID_COLS);
193
+ emit(c, cx0 + col * (colW + GAP), cy0 + row * (rowH + GAP), out);
194
+ });
195
+ break;
196
+ }
197
+ case "table": {
198
+ const innerW = s.w - PAD * 2;
199
+ out.push(rect(cx0, cy0, innerW, TABLE_HDR_H, "none", true, "#e2e8f0"));
200
+ for (let i = 1; i < 4; i++) {
201
+ const lx = cx0 + (innerW * i) / 4;
202
+ out.push(line(lx, cy0, lx, cy0 + TABLE_HDR_H));
203
+ }
204
+ let ry = cy0 + TABLE_HDR_H + GAP;
205
+ for (const c of cs) {
206
+ emit(c, cx0, ry, out);
207
+ ry += c.h + GAP;
208
+ }
209
+ break;
210
+ }
211
+ case "paged": {
212
+ const active = cs[0];
213
+ emit(active, cx0, cy0, out);
214
+ let ry = cy0 + active.h + GAP;
215
+ for (const c of cs.slice(1)) {
216
+ out.push(rect(cx0, ry, s.w - PAD * 2, COLLAPSE_H, c.provenance, false, "#f1f5f9"));
217
+ out.push(text(cx0 + 6, ry + 15, "▸ " + collapsedPaneLabel(c), "dim"));
218
+ ry += COLLAPSE_H + GAP;
219
+ }
220
+ break;
221
+ }
222
+ default: {
223
+ let cy = cy0;
224
+ for (const c of cs) {
225
+ emit(c, cx0, cy, out);
226
+ cy += c.h + GAP;
227
+ }
228
+ }
229
+ }
230
+ }
231
+ // --- drawing primitives ----------------------------------------------------
232
+ const DARK_BARS = new Set(["app-bar", "nav-bar"]);
233
+ function drawContainer(s, x, y, out) {
234
+ const dark = DARK_BARS.has(s.archetype);
235
+ let fill = "#f8fafc";
236
+ if (s.archetype === "banner")
237
+ fill = "#fef3c7";
238
+ else if (s.archetype === "tab-bar")
239
+ fill = "#eef2ff";
240
+ else if (s.mode === "card" || s.mode === "floating")
241
+ fill = "#ffffff";
242
+ else if (dark)
243
+ fill = "#1f2937";
244
+ if (s.mode === "floating" || s.mode === "card") {
245
+ out.push(`<rect x="${x + SHADOW}" y="${y + SHADOW}" width="${s.w}" height="${s.h}" rx="4" fill="#0f172a" opacity="0.12"/>`);
246
+ }
247
+ out.push(rect(x, y, s.w, s.h, s.provenance, true, fill));
248
+ out.push(text(x + PAD, y + 12, labelMarkup(s), dark ? "lblw" : "lbl"));
249
+ }
250
+ function drawLeaf(s, x, y, out) {
251
+ const lbl = labelMarkup(s);
252
+ switch (s.leafShape) {
253
+ case "button":
254
+ out.push(`<rect x="${x}" y="${y}" width="${s.w}" height="${s.h}" rx="13" fill="#dbeafe" stroke="#3b82f6"${dash(s.provenance)}/>`);
255
+ out.push(text(x + 10, y + s.h / 2 + 4, lbl, "btn"));
256
+ break;
257
+ case "chip":
258
+ out.push(`<rect x="${x}" y="${y}" width="${s.w}" height="${s.h}" rx="11" fill="#e2e8f0" stroke="#94a3b8"${dash(s.provenance)}/>`);
259
+ out.push(text(x + 9, y + s.h / 2 + 4, lbl, "lbl"));
260
+ break;
261
+ case "circle": {
262
+ const r = s.w / 2;
263
+ out.push(`<circle cx="${x + r}" cy="${y + r}" r="${r}" fill="#dbeafe" stroke="#3b82f6"${dash(s.provenance)}/>`);
264
+ out.push(text(x + 6, y + r + 4, lbl, "dim"));
265
+ break;
266
+ }
267
+ case "image":
268
+ out.push(rect(x, y, s.w, s.h, s.provenance, false, "#f1f5f9"));
269
+ out.push(line(x, y, x + s.w, y + s.h));
270
+ out.push(line(x + s.w, y, x, y + s.h));
271
+ out.push(text(x + 4, y + s.h - 4, lbl, "dim"));
272
+ break;
273
+ case "text":
274
+ out.push(text(x, y + 12, lbl, "lbl"));
275
+ out.push(`<rect x="${x}" y="${y + 16}" width="${s.w}" height="5" rx="2" fill="#cbd5e1"/>`);
276
+ out.push(`<rect x="${x}" y="${y + 24}" width="${s.w * 0.66}" height="5" rx="2" fill="#e2e8f0"/>`);
277
+ break;
278
+ case "row":
279
+ out.push(rect(x, y, s.w, s.h, s.provenance, false, "#ffffff"));
280
+ out.push(`<circle cx="${x + 11}" cy="${y + s.h / 2}" r="5" fill="#cbd5e1"/>`);
281
+ out.push(text(x + 24, y + s.h / 2 + 4, lbl, "lbl"));
282
+ break;
283
+ default:
284
+ out.push(rect(x, y, s.w, s.h, s.provenance, false, "#fdfdfe"));
285
+ out.push(text(x + 6, y + s.h / 2 + 4, lbl, "lbl"));
286
+ }
287
+ }
288
+ // --- label derivation ------------------------------------------------------
289
+ /** The primary human label for a node, derived only from tokens in the spec. */
290
+ function primaryLabel(s) {
291
+ if (s.node.args.length > 0)
292
+ return s.node.args[0]; // authored positional label
293
+ if (s.mode === "leaf" && s.node.actions.length > 0 && (s.archetype === "button" || s.archetype === "fab"))
294
+ return humaniseAction(s.node.actions[0]); // CTA from action=
295
+ if (s.derivedLabel)
296
+ return s.derivedLabel; // windowed-pane tab label
297
+ const comp = s.node.component;
298
+ if (!comp)
299
+ return s.node.id;
300
+ // A `default` resolution is a recognised standard widget — its short vendor name
301
+ // reads fine as-is; bespoke names (override / heuristic / fallback) humanise better.
302
+ return s.provenance === "default" || s.provenance === "none" ? comp : humaniseName(comp, s.stripPrefixes);
303
+ }
304
+ function archMark(prov) {
305
+ return prov === "fallback" ? "?" : prov === "heuristic" ? "~" : "";
306
+ }
307
+ /** Plain text label (for width measurement): primary + archetype annotation. */
308
+ function labelPlain(s) {
309
+ const primary = primaryLabel(s);
310
+ if (s.archetype === "" || s.provenance === "none")
311
+ return primary;
312
+ return `${primary} :${s.archetype}${archMark(s.provenance)}`;
313
+ }
314
+ /**
315
+ * Collapsed paged-pane label: a wrapper pane often has a generic name (its
316
+ * content carries the identity), so prefer the content component's label when
317
+ * the pane itself has no authored label of its own. Spec-faithful — the content
318
+ * name is already in the block.
319
+ */
320
+ function collapsedPaneLabel(s) {
321
+ const content = s.children[0];
322
+ if (s.node.args.length === 0 && !s.derivedLabel && content && content.node.component) {
323
+ const lbl = escapeXml(primaryLabel(content));
324
+ return s.archetype === "" ? lbl : `${lbl} <tspan class="arch">:${escapeXml(s.archetype)}</tspan>`;
325
+ }
326
+ return labelMarkup(s);
327
+ }
328
+ /** SVG markup label (for emission): primary text + a small dim `:archetype` tspan. */
329
+ function labelMarkup(s) {
330
+ const primary = escapeXml(primaryLabel(s));
331
+ if (s.archetype === "" || s.provenance === "none")
332
+ return primary;
333
+ return `${primary} <tspan class="arch">:${escapeXml(s.archetype)}${archMark(s.provenance)}</tspan>`;
334
+ }
335
+ // --- humanisation (deterministic, adopter-agnostic) ------------------------
336
+ const HUMANISE_SUFFIXES = [
337
+ "Component", "Container", "Controller", "Activity", "Fragment", "Section",
338
+ "Layout", "Panel", "Popup", "Dialog", "Modal", "Widget", "View", "Screen",
339
+ "Page", "Table", "List", "Card", "Item",
340
+ ];
341
+ /** Strip a vendor prefix + a trailing archetype-ish suffix, split camelCase, Title Case. */
342
+ function humaniseName(name, prefixes) {
343
+ let s = name;
344
+ for (const p of prefixes) {
345
+ // Strip only at a PascalCase boundary (next char uppercase) so a short prefix
346
+ // like "St" peels "StLayout" → "Layout" but never "Status" → "atus".
347
+ if (s.length > p.length && s.startsWith(p) && /[A-Z]/.test(s.charAt(p.length))) {
348
+ s = s.slice(p.length);
349
+ break;
350
+ }
351
+ }
352
+ for (const suf of HUMANISE_SUFFIXES) {
353
+ if (s.length > suf.length && s.endsWith(suf)) {
354
+ s = s.slice(0, -suf.length);
355
+ break;
356
+ }
357
+ }
358
+ const words = splitWords(s);
359
+ if (words.length === 0)
360
+ return titleCaseWords(splitWords(name)); // prefix/suffix ate the whole name
361
+ return titleCaseWords(words);
362
+ }
363
+ /** Humanise an action token, prefixing create/add/new verbs with "+ ". */
364
+ function humaniseAction(action) {
365
+ const human = titleCaseWords(splitWords(action));
366
+ return /^(new|add|create)([^a-z]|$)/i.test(action) ? `+ ${human}` : human;
367
+ }
368
+ function splitWords(s) {
369
+ return s
370
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
371
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
372
+ .replace(/[_\-.]+/g, " ")
373
+ .trim()
374
+ .split(/\s+/)
375
+ .filter(Boolean);
376
+ }
377
+ function titleCaseWords(words) {
378
+ return words.map((w) => (w === w.toUpperCase() ? w : w.charAt(0).toUpperCase() + w.slice(1))).join(" ");
379
+ }
380
+ function dash(prov) {
381
+ return prov === "heuristic" ? ' stroke-dasharray="5 3"' : prov === "fallback" ? ' stroke-dasharray="1 4"' : "";
382
+ }
383
+ function rect(x, y, w, h, prov, container, fill) {
384
+ const f = fill ?? (container ? "#f4f6f8" : "#fdfdfe");
385
+ return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="3" fill="${f}" stroke="#64748b" stroke-width="1"${dash(prov)}/>`;
386
+ }
387
+ function line(x1, y1, x2, y2) {
388
+ return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#cbd5e1" stroke-width="1"/>`;
389
+ }
390
+ function text(x, y, content, cls) {
391
+ return `<text x="${x}" y="${y}" class="${cls}">${content}</text>`;
392
+ }
393
+ function svg(width, height, body) {
394
+ return [
395
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
396
+ `<style>text{font-family:ui-monospace,monospace;font-size:11px;fill:#334155}.title{font-size:13px;font-weight:600;fill:#0f172a}.lbl{fill:#334155}.lblw{fill:#f8fafc}.dim{fill:#94a3b8}.btn{fill:#1e40af;font-weight:600}.arch{font-size:9px;fill:#a8b3c2}</style>`,
397
+ `<rect width="${width}" height="${height}" fill="#fff"/>`,
398
+ body,
399
+ `</svg>`,
400
+ ].join("\n");
401
+ }
402
+ function emptySvg(title, message) {
403
+ const body = [
404
+ title ? text(PAD, 16, escapeXml(title), "title") : "",
405
+ text(PAD, title ? 36 : 16, escapeXml(message), "lbl"),
406
+ ]
407
+ .filter(Boolean)
408
+ .join("\n");
409
+ return svg(240, title ? 48 : 28, body);
410
+ }
411
+ function escapeXml(s) {
412
+ return s
413
+ .replace(/&/g, "&amp;")
414
+ .replace(/</g, "&lt;")
415
+ .replace(/>/g, "&gt;")
416
+ .replace(/"/g, "&quot;");
417
+ }
418
+ //# sourceMappingURL=wireframe.js.map
@@ -0,0 +1,151 @@
1
+ /**
2
+ * `catalogue baseline-check` — compares emitted findings against a repo's
3
+ * findings baseline BY IDENTITY (M-ADR 0132 §9), and reports the four outcomes
4
+ * a re-take needs to be reviewable: unbaselined, reworded, closed, unchanged.
5
+ *
6
+ * WHY THIS IS A COMMAND AND NOT A DISCIPLINE. The comparison was previously
7
+ * described in prose and performed per repo, which is why the rewording class
8
+ * (§9.1) went undetected twice on the same finding family: every repo
9
+ * implemented "is this line in the file" independently and correctly, and the
10
+ * defect was in the question, not the implementations.
11
+ *
12
+ * It reads findings rather than producing them, so it composes with whatever
13
+ * produced them and cannot disagree with a separate run of the builder:
14
+ *
15
+ * sdd-cli catalogue validate --coverage > findings.txt
16
+ * sdd-cli catalogue baseline-check --findings findings.txt
17
+ *
18
+ * ONLY GATE-ACTING CATEGORIES ARE COMPARED (`orphan`, `divergence` — M-ADR 0132
19
+ * §4.3). Filtering happens here, visibly, and the count of what was filtered is
20
+ * printed: a comparator that silently dropped categories would make an empty
21
+ * result ambiguous between "nothing gate-acting" and "nothing at all".
22
+ */
23
+ import { readFile } from "node:fs/promises";
24
+ import { join } from "node:path";
25
+ import { compareToBaseline } from "./finding-identity.js";
26
+ /** The categories a baseline governs, per M-ADR 0132 §4.3. */
27
+ const GATE_ACTING = new Set(["orphan", "divergence"]);
28
+ export const DEFAULT_BASELINE_REL = join("orchestration", "catalogue-findings-baseline.txt");
29
+ const FINDING_LINE = /^\[finding\] (\w+)(?: \[error\])?[ (:]/;
30
+ /**
31
+ * Findings from a stream of mixed output. Non-finding lines (metrics, headers,
32
+ * blank lines, a baseline file's `#` provenance) are not findings and are
33
+ * dropped without comment; a `[finding]` line of a non-gate-acting category is
34
+ * dropped and COUNTED, because that number is the difference between "nothing
35
+ * to compare" and "nothing was comparable".
36
+ */
37
+ export function extractGateActing(text) {
38
+ const lines = [];
39
+ let nonGateActing = 0;
40
+ for (const raw of text.split("\n")) {
41
+ const line = raw.trimEnd();
42
+ const m = FINDING_LINE.exec(line);
43
+ if (!m)
44
+ continue;
45
+ if (GATE_ACTING.has(m[1]))
46
+ lines.push(line);
47
+ else
48
+ nonGateActing++;
49
+ }
50
+ return { lines, nonGateActing };
51
+ }
52
+ export async function runBaselineCheck(options, write) {
53
+ const root = options.sddPath ?? process.cwd();
54
+ const baselinePath = options.baseline ?? join(root, DEFAULT_BASELINE_REL);
55
+ let findingsText;
56
+ if (options.findings !== undefined) {
57
+ findingsText = await readFile(options.findings, "utf-8");
58
+ }
59
+ else if (options.stdin) {
60
+ findingsText = await options.stdin();
61
+ }
62
+ else {
63
+ write("[error] baseline-check: no findings source (--findings <path>, or pipe them in)\n");
64
+ return { exitCode: 2 };
65
+ }
66
+ // An input with no lines at all is not "zero findings": every real findings
67
+ // stream (build, coverage, validate output) carries at least its own metric
68
+ // and verdict lines. The default source is stdin, so an empty pipe arrives
69
+ // here as nothing — refused, never compared (empty-set sweep, R2).
70
+ if (findingsText.trim() === "") {
71
+ write(`[error] baseline-check: the findings stream is EMPTY — no lines of any kind. ` +
72
+ `This is not zero findings; it is no measurement (an empty pipe, or the wrong ` +
73
+ `file). Pass --findings <path> to the captured stream\n`);
74
+ return { exitCode: 2 };
75
+ }
76
+ let baselineText;
77
+ try {
78
+ baselineText = await readFile(baselinePath, "utf-8");
79
+ }
80
+ catch (err) {
81
+ if (err.code !== "ENOENT")
82
+ throw err;
83
+ // A MISSING baseline is not an empty one. Under §9.3 the canonical zero is
84
+ // an empty-but-present file, precisely so that "measured, and there were
85
+ // none" is distinguishable from "never measured" — so this reports rather
86
+ // than treating every live finding as unbaselined.
87
+ write(`[error] baseline-check: no baseline at ${baselinePath} — a missing file is not a ` +
88
+ `recorded zero (M-ADR 0132 §9.3); create it empty, with its provenance header, ` +
89
+ `to record that findings were measured and there were none\n`);
90
+ return { exitCode: 2 };
91
+ }
92
+ const live = extractGateActing(findingsText);
93
+ const base = extractGateActing(baselineText);
94
+ const cmp = compareToBaseline(live.lines, base.lines);
95
+ write(`[metric] baseline_check: ${live.lines.length} gate-acting finding(s) against ` +
96
+ `${base.lines.length} baseline entr(ies) — ${cmp.unchanged} unchanged, ` +
97
+ `${cmp.reworded.length} reworded, ${cmp.unbaselined.length} unbaselined, ` +
98
+ `${cmp.stale.length} closed; ${live.nonGateActing} non-gate-acting finding(s) not compared\n`);
99
+ // ⚠ THE WHOLE-BASELINE-CLOSED GUARD. An all-closed result against a non-empty
100
+ // baseline is far more likely to mean the comparison read the WRONG STREAM than
101
+ // that an estate fixed everything at once, and until now it rendered as a clean
102
+ // green number: "0 gate-acting findings, 215 closed" reads as success and is
103
+ // indistinguishable from a gate comparing against nothing.
104
+ //
105
+ // Raised by an adopter estate that hit it on a 215-entry baseline: its
106
+ // baseline had been captured from one stream/tier and was being compared
107
+ // against another, so every entry reported closed and the regression gate
108
+ // passed unconditionally. The `--findings` default is stdin, so an empty pipe
109
+ // reaches here as legitimately-zero findings — the same estate had already
110
+ // been caught once by a bare invocation reporting 832 closed.
111
+ //
112
+ // The condition is deliberately narrow: live gate-acting findings ZERO, baseline
113
+ // non-empty, and EVERY entry closed. That triple cannot arise from ordinary
114
+ // remediation without also being worth a human look.
115
+ const allClosed = live.lines.length === 0 && base.lines.length > 0 && cmp.stale.length === base.lines.length;
116
+ if (allClosed) {
117
+ write(`[finding] divergence: baseline_check compared ZERO live gate-acting findings ` +
118
+ `against ${base.lines.length} baseline entr(ies) and reported ALL of them closed. ` +
119
+ `This is reported as a DEFECT rather than a pass: an all-closed baseline is far ` +
120
+ `more likely to mean the findings stream was empty or was the wrong one than that ` +
121
+ `every finding was fixed at once. Check that --findings received the stream the ` +
122
+ `baseline was CAPTURED from (note the default is stdin, so an empty pipe arrives ` +
123
+ `here as zero findings), and that --tier matches. Re-taking the baseline against ` +
124
+ `this comparison would bake the empty stream in as the new truth and destroy the ` +
125
+ `${base.lines.length}-entry record.\n`);
126
+ }
127
+ // Reported even at zero. While it is above zero those entries are matched on
128
+ // whole text and remain exposed to the rewording class; silence here would
129
+ // imply a protection the repo has not got.
130
+ write(`[metric] baseline_check.legacy_keyed: ${cmp.legacyKeyed} baseline entr(ies) carry no ` +
131
+ `identity and are matched on their whole line — re-take them as their emission ` +
132
+ `sites migrate (M-ADR 0132 §9.2)\n`);
133
+ for (const r of cmp.reworded) {
134
+ // Named, not counted only: a re-take must be able to show which line
135
+ // replaced which, and "N reworded" is not reviewable evidence.
136
+ write(`[reworded] ${r.baseline}\n[reworded] -> ${r.live}\n`);
137
+ }
138
+ for (const s of cmp.stale)
139
+ write(`[closed] ${s}\n`);
140
+ for (const u of cmp.unbaselined)
141
+ write(`[unbaselined] ${u}\n`);
142
+ // Only genuinely unbaselined findings fail. A reworded finding is the case
143
+ // this whole mechanism exists to stop failing, and a closed one is good news
144
+ // that a re-take tidies — neither is a regression.
145
+ // The all-closed guard FAILS the gate (empty-set sweep, R2, ruled 2026-09-29):
146
+ // it used to print "a DEFECT rather than a pass" and exit 0, so the regression
147
+ // gate still passed on exactly the empty/wrong stream it was written to catch.
148
+ // A genuinely all-fixed estate re-takes its baseline once, deliberately.
149
+ return { exitCode: cmp.unbaselined.length > 0 || allClosed ? 1 : 0 };
150
+ }
151
+ //# sourceMappingURL=baseline-check.js.map