@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,222 @@
1
+ // Screen composition-tree analysis for openui-lang blocks (per ADR 0168).
2
+ //
3
+ // The screens builder (screens.ts) lowers a ```openui-lang block into catalogue
4
+ // rows by flat token-scanning. ADR 0168 adds a *fidelity floor*: the block must
5
+ // be a faithful tree, not a flat / empty / mis-rooted "widget bag". That needs
6
+ // the structural shape the flat scan throws away — so this module parses the
7
+ // block into a tree and computes the metrics the depth findings reason about.
8
+ //
9
+ // Pure functions, no I/O. The metrics reason about TREE SHAPE only — never about
10
+ // any component vocabulary (adopter-agnostic by construction, ADR 0168 §4.5).
11
+ //
12
+ // Grammar recap (ADR 0166 / openui-lang-grammar-reference.md): one
13
+ // `identifier = Expression` statement per line; the first statement is `root`;
14
+ // composition is by identifier reference inside list literals. A component is a
15
+ // Capitalised call head (`Name(`); local references are lowercase ids that are
16
+ // themselves defined as statements.
17
+ /** First Capitalised call head in an expression (the component invoked). */
18
+ const CALL_HEAD_RE = /\b([A-Z][A-Za-z0-9]*)\s*\(/;
19
+ /** All Capitalised call heads (for componentCount). */
20
+ const CALL_HEAD_RE_G = /\b([A-Z][A-Za-z0-9]*)\s*\(/g;
21
+ /**
22
+ * All *lowercase* call heads — `name(` where `name` starts lowercase. These are
23
+ * malformed: the grammar invokes a component only via a Capitalised head, and a
24
+ * lowercase identifier is a pure reference that never takes arguments. Such a
25
+ * token is silently uncounted by componentCount, so it composes child-less in
26
+ * the tree and the catalogue — caught here as a convention finding.
27
+ */
28
+ const LOWER_CALL_HEAD_RE_G = /\b([a-z][A-Za-z0-9_-]*)\s*\(/g;
29
+ /** `identifier = Expression`, one per line. */
30
+ const STATEMENT_RE = /^\s*([A-Za-z_][\w-]*)\s*=\s*(.+?)\s*$/;
31
+ /** Quoted string literals (single or double) — masked before call-head / ref detection. */
32
+ const STRING_RE = /"[^"]*"|'[^']*'/g;
33
+ /**
34
+ * Mask string-literal interiors with spaces of equal length (offset- and
35
+ * line-structure-preserving) so call-head lexing can never fire on quoted
36
+ * content — `Table("Odometer (km)")` must not mint a phantom `Odometer`
37
+ * component, and `"total (sum)"` must not false-fire the lowercase-call-head
38
+ * finding (tomfosteruk/sdd-cli#1). Quote characters are kept; newlines inside
39
+ * a span survive. The grammar defines no escape sequences (a quote always
40
+ * closes the literal), matching STRING_RE and stripLineComment.
41
+ *
42
+ * Scope: call-head / identifier-reference scanning ONLY. Positional-arg and
43
+ * `action=` extraction read string contents, so they must always run on the
44
+ * real (unmasked) text.
45
+ */
46
+ export function maskStringLiterals(text) {
47
+ return text.replace(STRING_RE, (lit) => lit[0] + lit.slice(1, -1).replace(/[^\r\n]/g, " ") + lit[lit.length - 1]);
48
+ }
49
+ /** Any identifier token. */
50
+ const IDENT_RE = /\b([A-Za-z_][\w-]*)\b/g;
51
+ /** `action="..."` attribute values on a statement (node-scoped CTA/messaging contract). */
52
+ const ACTION_ATTR_RE_G = /\baction\s*=\s*"([^"]*)"/g;
53
+ /** Any `key="..."` / `key='...'` named attribute — removed so its value isn't read as a positional arg. */
54
+ const NAMED_ATTR_RE_G = /\b[\w-]+\s*=\s*("[^"]*"|'[^']*')/g;
55
+ /**
56
+ * Split a call's RHS into its positional string args and its `action=` values.
57
+ * Render-only metadata (the deterministic wireframe renderer reads it to label
58
+ * nodes) — never feeds catalogue rows or the depth metrics. A `key="..."` named
59
+ * attribute's value is NOT a positional arg, so named attrs are removed first.
60
+ */
61
+ function parseArgsActions(rhs) {
62
+ const actions = [];
63
+ for (const m of rhs.matchAll(ACTION_ATTR_RE_G))
64
+ actions.push(m[1]);
65
+ const positional = rhs.replace(NAMED_ATTR_RE_G, "");
66
+ const args = [];
67
+ for (const m of positional.matchAll(STRING_RE)) {
68
+ const inner = m[0].slice(1, -1);
69
+ if (inner.length > 0)
70
+ args.push(inner);
71
+ }
72
+ return { args, actions };
73
+ }
74
+ /**
75
+ * Strip openui-lang inline comments: from the first *unquoted* `#` to
76
+ * end-of-line, per line (ADR 0168 §4.4, defect g). A `Capitalised(` token inside
77
+ * a comment must not be lexed as a component. `#` inside a string literal is
78
+ * preserved.
79
+ */
80
+ export function stripBlockComments(block) {
81
+ return block
82
+ .split(/\r?\n/)
83
+ .map(stripLineComment)
84
+ .join("\n");
85
+ }
86
+ function stripLineComment(line) {
87
+ let quote = null;
88
+ for (let i = 0; i < line.length; i++) {
89
+ const c = line[i];
90
+ if (quote) {
91
+ if (c === quote)
92
+ quote = null;
93
+ }
94
+ else if (c === '"' || c === "'") {
95
+ quote = c;
96
+ }
97
+ else if (c === "#") {
98
+ return line.slice(0, i);
99
+ }
100
+ }
101
+ return line;
102
+ }
103
+ // Parse a (comment-stripped) block into statements with resolved child
104
+ // references. Shared by analyzeComposition (metrics) and buildCompositionTree.
105
+ function parseStatements(cleanBlock) {
106
+ const statements = new Map();
107
+ const order = [];
108
+ for (const raw of cleanBlock.split(/\r?\n/)) {
109
+ const m = STATEMENT_RE.exec(raw);
110
+ if (!m)
111
+ continue;
112
+ const id = m[1];
113
+ const rhs = m[2];
114
+ // A component statement's RHS is a call (`Text("Q4")`) or a reference — never a
115
+ // bare string literal. A line like `action="user-details-changed"` (the block-level
116
+ // screen_action messaging contract, ADR 0166) matches the statement shape with
117
+ // id=`action`; skip it so it isn't lexed as a phantom component (and so a node
118
+ // carrying an `action=` attr doesn't pick it up as a spurious child).
119
+ if (/^["']/.test(rhs))
120
+ continue;
121
+ if (!statements.has(id))
122
+ order.push(id);
123
+ // Last assignment wins (re-declaration is unusual; keep it deterministic).
124
+ // Head detection runs on the string-masked rhs so a `Capitalised(` inside a
125
+ // quoted arg can't be read as the call head; args/actions read the real rhs.
126
+ const { args, actions } = parseArgsActions(rhs);
127
+ statements.set(id, { id, rhs, head: CALL_HEAD_RE.exec(maskStringLiterals(rhs))?.[1] ?? "", args, actions, childIds: [] });
128
+ }
129
+ const definedIds = new Set(statements.keys());
130
+ // Resolve child references: a defined id (other than self) appearing in the
131
+ // expression, after masking string literals so quoted text can't match
132
+ // (masking, not stripping, so removal can't splice adjacent tokens together).
133
+ for (const st of statements.values()) {
134
+ const noStrings = maskStringLiterals(st.rhs);
135
+ const refs = new Set();
136
+ for (const tok of noStrings.matchAll(IDENT_RE)) {
137
+ const name = tok[1];
138
+ if (name !== st.id && definedIds.has(name))
139
+ refs.add(name);
140
+ }
141
+ st.childIds = [...refs];
142
+ }
143
+ return { statements, order };
144
+ }
145
+ /**
146
+ * Build the composition tree from a (comment-stripped) block, rooted at the
147
+ * first statement. Cycle-safe (a back-edge becomes a childless leaf). Returns
148
+ * null when the block declares no statements.
149
+ */
150
+ export function buildCompositionTree(cleanBlock) {
151
+ const { statements, order } = parseStatements(cleanBlock);
152
+ const rootId = order[0];
153
+ if (rootId === undefined)
154
+ return null;
155
+ const build = (id, seen) => {
156
+ const st = statements.get(id);
157
+ if (st === undefined)
158
+ return { id, component: "", args: [], actions: [], children: [] }; // undefined ref → leaf
159
+ if (seen.has(id))
160
+ return { id, component: st.head, args: st.args, actions: st.actions, children: [] }; // cycle guard
161
+ const next = new Set(seen).add(id);
162
+ return { id, component: st.head, args: st.args, actions: st.actions, children: st.childIds.map((c) => build(c, next)) };
163
+ };
164
+ return build(rootId, new Set());
165
+ }
166
+ /**
167
+ * Parse a (comment-stripped) openui-lang block into a composition tree and
168
+ * compute its structural metrics. Cycle-safe.
169
+ */
170
+ export function analyzeComposition(cleanBlock) {
171
+ const { statements, order } = parseStatements(cleanBlock);
172
+ const containers = new Set();
173
+ for (const st of statements.values()) {
174
+ if (st.head && st.childIds.length > 0)
175
+ containers.add(st.head);
176
+ }
177
+ // componentCount: distinct Capitalised call heads across the block (string
178
+ // literals masked so a `Foo(` inside a quoted label is not counted — #1).
179
+ // lowercaseCallHeads: distinct lowercase call heads (malformed invocations)
180
+ // over the same masked block, so `"total (sum)"` can't false-fire either.
181
+ const noStrings = maskStringLiterals(cleanBlock);
182
+ const heads = new Set();
183
+ for (const tok of noStrings.matchAll(CALL_HEAD_RE_G)) {
184
+ heads.add(tok[1]);
185
+ }
186
+ const componentCount = heads.size;
187
+ const lowerHeads = new Set();
188
+ for (const tok of noStrings.matchAll(LOWER_CALL_HEAD_RE_G)) {
189
+ lowerHeads.add(tok[1]);
190
+ }
191
+ const lowercaseCallHeads = [...lowerHeads].sort();
192
+ const rootId = order[0];
193
+ if (rootId === undefined) {
194
+ return { maxDepth: 0, maxBreadth: 0, rootChildCount: 0, componentCount, rootComponent: "", containers, lowercaseCallHeads };
195
+ }
196
+ const rootStmt = statements.get(rootId);
197
+ let maxDepth = 0;
198
+ let maxBreadth = 0;
199
+ const walk = (id, depth, seen) => {
200
+ const st = statements.get(id);
201
+ maxDepth = Math.max(maxDepth, depth);
202
+ if (st === undefined)
203
+ return; // reference to an undefined id → leaf
204
+ maxBreadth = Math.max(maxBreadth, st.childIds.length);
205
+ if (seen.has(id))
206
+ return; // cycle guard
207
+ const next = new Set(seen).add(id);
208
+ for (const child of st.childIds)
209
+ walk(child, depth + 1, next);
210
+ };
211
+ walk(rootId, 0, new Set());
212
+ return {
213
+ maxDepth,
214
+ maxBreadth,
215
+ rootChildCount: rootStmt.childIds.length,
216
+ componentCount,
217
+ rootComponent: rootStmt.head,
218
+ containers,
219
+ lowercaseCallHeads,
220
+ };
221
+ }
222
+ //# sourceMappingURL=screen-tree.js.map
@@ -0,0 +1,396 @@
1
+ // Screens builder — emits the UI screens catalogue family (per ADR 0166):
2
+ // screens, ui_components, screen_component, screen_feature,
3
+ // persona_screen, screen_action.
4
+ //
5
+ // Source convention (per docs/method/methods/templates/screen-spec-template.md):
6
+ // screens/<slug>.md
7
+ // ---
8
+ // screen: <slug> # optional; defaults to filename stem
9
+ // route: /path # optional
10
+ // status: working # optional (ADR 0165 maturity vocabulary)
11
+ // persona_access: [<persona-slug>, ...] # -> persona_screen
12
+ // realises: [<feature-slug>, ...] # -> screen_feature
13
+ // ---
14
+ // # Screen: {Name}
15
+ // ```openui-lang
16
+ // root = Stack([header, refresh])
17
+ // header = TextContent("Q4 Dashboard", "large-heavy")
18
+ // refresh = Button("Refresh", action="refresh-dashboard")
19
+ // ```
20
+ //
21
+ // The fenced ```openui-lang block is the normative structural core: component
22
+ // invocations (Capitalised call heads) -> ui_components + screen_component;
23
+ // `action="..."` declarations -> screen_action. The frontmatter supplies the
24
+ // catalogue keys + FK lists. This is the forward / authored path; the reverse
25
+ // toolkit projects an app's router + component tree into the same shape (the
26
+ // upstream step that produces these artefacts — see
27
+ // research/adr-0166-prototypes/ for the Phase Pre validation).
28
+ //
29
+ // Per ADR 0135 the rows are identical regardless of authoring direction; only
30
+ // the `source` discriminator differs (screen-spec vs screen-extraction).
31
+ //
32
+ // Application-product profile. Like personas, the walker reads `screens/` at
33
+ // the SDD root regardless of profile and emits empty when the directory is
34
+ // absent (non-app profiles simply have none).
35
+ import { readdir, readFile } from "node:fs/promises";
36
+ import { basename, join } from "node:path";
37
+ import { parse as parseYaml } from "yaml";
38
+ import { analyzeComposition, maskStringLiterals, stripBlockComments } from "./screen-tree.js";
39
+ import { isContainerArchetype } from "../archetype/resolver.js";
40
+ /** Method default for the flat-composition threshold (ADR 0168 §4.2). */
41
+ const DEFAULT_FLAT_THRESHOLD = 6;
42
+ /** The only maturity value that asserts a faithful, complete composition (ADR 0168 §4.3). */
43
+ const COMPLETENESS_STATUS = "working";
44
+ const SCREENS_DIR_REL = "screens";
45
+ /** Subdir of screens/ holding rendered wireframe artifacts — never screen specs. */
46
+ const WIREFRAMES_DIR = "wireframes";
47
+ /** Reserved documentation files under screens/ that are not screen specs (e.g. a cross-reference index). */
48
+ const isReservedDoc = (name) => /^(INDEX|README)\.md$/i.test(name);
49
+ /**
50
+ * Collect screen-spec `*.md` files under `screens/`, recursively. Adopters with a
51
+ * single platform keep specs flat (`screens/<slug>.md`); multi-platform adopters
52
+ * may nest them (`screens/<platform>/<slug>.md`) — both are found. The screen id
53
+ * comes from frontmatter (`screen:`), not the path, so nesting is purely
54
+ * organisational. The `wireframes/` subtree (rendered SVGs) is skipped. Returns
55
+ * paths relative to `screens/`, sorted. Throws ENOENT if `screens/` is absent.
56
+ */
57
+ async function collectScreenSpecFiles(dir) {
58
+ const out = [];
59
+ const walk = async (rel) => {
60
+ const entries = await readdir(rel ? join(dir, rel) : dir, { withFileTypes: true });
61
+ for (const e of entries) {
62
+ const r = rel ? `${rel}/${e.name}` : e.name;
63
+ if (e.isDirectory()) {
64
+ if (e.name === WIREFRAMES_DIR)
65
+ continue;
66
+ await walk(r);
67
+ }
68
+ else if (e.isFile() && e.name.endsWith(".md") && !isTemplate(e.name) && !isReservedDoc(e.name)) {
69
+ out.push(r);
70
+ }
71
+ }
72
+ };
73
+ await walk("");
74
+ return out.sort();
75
+ }
76
+ // Recognised standard maturity vocabulary (ADR 0165); other values pass through.
77
+ const STANDARD_MATURITY = new Set(["working", "provisional", "unknown", "absent"]);
78
+ export async function buildScreens(inputs) {
79
+ const screens = [];
80
+ const uiById = new Map();
81
+ const screenComponent = new Map();
82
+ const screenFeature = new Map();
83
+ const personaScreen = new Map();
84
+ const screenAction = new Map();
85
+ const flatThreshold = inputs.flatThreshold ?? DEFAULT_FLAT_THRESHOLD;
86
+ const dir = join(inputs.sddPath, SCREENS_DIR_REL);
87
+ let files;
88
+ try {
89
+ files = await collectScreenSpecFiles(dir);
90
+ }
91
+ catch (err) {
92
+ if (err.code !== "ENOENT")
93
+ throw err;
94
+ inputs.findings.add("coverage", `${SCREENS_DIR_REL}/ not found — emitting empty screens family`);
95
+ return emptyResult();
96
+ }
97
+ for (const filename of files) {
98
+ const stem = basename(filename).replace(/\.md$/i, "");
99
+ const text = await readFile(join(dir, filename), "utf-8");
100
+ const fm = parseFrontmatter(text, inputs.findings, filename);
101
+ const slug = screenSlug(typeof fm.screen === "string" ? fm.screen : stem);
102
+ const screenId = `${inputs.productId}-${slug}`;
103
+ if (screens.some((s) => s["id"] === screenId)) {
104
+ inputs.findings.add("divergence", `screens: duplicate screen id ${screenId} (${filename}) — keeping first`);
105
+ continue;
106
+ }
107
+ const name = extractH1(text) || (typeof fm.name === "string" ? fm.name : stem);
108
+ const status = typeof fm.status === "string" && fm.status !== "" ? maturity(fm.status, inputs.findings, screenId) : "";
109
+ // Direction discriminator (ADR 0135): forward authoring is `screen-spec`,
110
+ // reverse extraction `screen-extraction`. Read from frontmatter, default
111
+ // forward; only these two are recognised (others pass through, flagged).
112
+ const source = screenSource(fm.source, inputs.findings, screenId);
113
+ screens.push({
114
+ id: screenId,
115
+ name,
116
+ product_id: inputs.productId,
117
+ route: typeof fm.route === "string" ? fm.route : "",
118
+ status,
119
+ source,
120
+ domain: typeof fm.domain === "string" ? fm.domain : "",
121
+ group: typeof fm.group === "string" ? fm.group : "",
122
+ context: typeof fm.context === "string" ? fm.context : "",
123
+ // Evidence class (M-ADR 0175, schema 2.42.0). Orthogonal to `source`
124
+ // above: `source` records the authoring DIRECTION, this records the
125
+ // claim's STRENGTH. A packaged-SaaS SDD carries the vendor's standard
126
+ // screens as `vendor-doc` (documentation-reversed from vendor
127
+ // publications) and its own customisations as `code-reverse`; a
128
+ // `screen-extraction` screen may be either class, since a documented
129
+ // vendor UI is extracted but not code-verified. The hyphen alias matches
130
+ // the events and data-model derivation sources.
131
+ evidence_class: evidenceClass(fm),
132
+ source_sdd_id: "",
133
+ });
134
+ // Evidence integrity (ADR 0168 §4.2, defect f): a reverse-extracted screen
135
+ // must cite its route + component evidence with a `[^…]` footnote. Forward-
136
+ // authored screens make no such claim, so the check is reverse-scoped.
137
+ if (source === "screen-extraction" && !/\[\^[^\]]+\]/.test(text)) {
138
+ inputs.findings.add("coverage", `screen ${screenId}: reverse-extracted but cites no evidence footnote (screen-evidence-missing)`);
139
+ }
140
+ const block = extractOpenUiLangBlock(text);
141
+ if (!block) {
142
+ inputs.findings.add("coverage", `screens: ${filename} has no \`\`\`openui-lang block — screen emitted, no components`);
143
+ }
144
+ else {
145
+ // Strip inline `#` comments before any scan so a `Capitalised(` token in a
146
+ // comment can't emit a phantom component (ADR 0168 §4.4, defect g).
147
+ const clean = stripBlockComments(block);
148
+ // Component invocations: Capitalised call heads (e.g. Stack(, StatCard().
149
+ // Local id references (lowercase, no parens) and list literals are skipped.
150
+ // String literals are masked first so a quoted label like "Odometer (km)"
151
+ // can't mint a phantom component (#1); the action= scan below reads the
152
+ // real (unmasked) block because it extracts string contents.
153
+ const componentNames = new Set(matchAll(maskStringLiterals(clean), /\b([A-Z][A-Za-z0-9]*)\s*\(/g));
154
+ for (const comp of componentNames) {
155
+ const cid = `${inputs.productId}-${screenSlug(comp)}`;
156
+ if (!uiById.has(cid)) {
157
+ uiById.set(cid, {
158
+ id: cid, name: comp, product_id: inputs.productId,
159
+ kind: "", source, source_sdd_id: "",
160
+ });
161
+ }
162
+ const scKey = `${screenId}__${cid}`;
163
+ if (!screenComponent.has(scKey)) {
164
+ screenComponent.set(scKey, { id: scKey, screen_id: screenId, component_id: cid, source_sdd_id: "" });
165
+ }
166
+ }
167
+ // Action declarations: action="..." (single or double quoted).
168
+ for (const action of new Set(matchAll(clean, /\baction\s*=\s*["']([^"']+)["']/g))) {
169
+ const aKey = `${screenId}__${screenSlug(action)}`;
170
+ if (!screenAction.has(aKey)) {
171
+ // event_id left empty: external-capable, resolved against the event
172
+ // catalogue when an explicit mapping exists (best-effort per ADR 0166).
173
+ screenAction.set(aKey, { id: aKey, screen_id: screenId, action, event_id: "", source_sdd_id: "" });
174
+ }
175
+ }
176
+ // Composition-depth validation (ADR 0168): structural fidelity floor +
177
+ // status gate + the archetype-aware root check (ADR 0169). The vocabulary-
178
+ // free checks reason about tree shape + status only; root-is-leaf consults
179
+ // the archetype map when the screen declares a `platform:`.
180
+ const platform = typeof fm.platform === "string" ? fm.platform : undefined;
181
+ const overlay = platform !== undefined ? inputs.archetypeOverlays?.get(platform) : undefined;
182
+ emitDepthFindings(inputs.findings, screenId, status, analyzeComposition(clean), {
183
+ flatThreshold,
184
+ platform,
185
+ overlay,
186
+ componentNames,
187
+ decorativePrimitives: inputs.decorativePrimitives,
188
+ });
189
+ }
190
+ // realises: -> screen_feature
191
+ for (const fref of toList(fm.realises)) {
192
+ const fid = inputs.featureIdBySlug.get(screenSlug(fref)) ?? inputs.featureIdByPath.get(fref);
193
+ if (!fid) {
194
+ // App/integration screens can realise a platform-resident feature
195
+ // (MFE-over-platform: the platform owns the capability tree and its
196
+ // features — ADR 0177 single-slug). Emit a deferred cross-SDD edge
197
+ // carrying the bare realises slug; the aggregator resolves it to the
198
+ // platform feature id, and the FK validator logs it `[finding] external`
199
+ // for this build (screen_feature.feature_id is external_for_profiles).
200
+ // For platform-profile a miss is a genuine local orphan — keep dropping.
201
+ if (inputs.profile === "application-product" ||
202
+ inputs.profile === "integration-product") {
203
+ const slug = screenSlug(fref);
204
+ const key = `${screenId}__${slug}`;
205
+ if (!screenFeature.has(key)) {
206
+ screenFeature.set(key, {
207
+ id: key,
208
+ screen_id: screenId,
209
+ feature_id: slug,
210
+ source_sdd_id: "",
211
+ });
212
+ }
213
+ }
214
+ else {
215
+ inputs.findings.add("orphan", `screen_feature: ${screenId} realises "${fref}" — no matching feature`);
216
+ }
217
+ continue;
218
+ }
219
+ const key = `${screenId}__${fid}`;
220
+ if (!screenFeature.has(key))
221
+ screenFeature.set(key, { id: key, screen_id: screenId, feature_id: fid, source_sdd_id: "" });
222
+ }
223
+ // persona_access: -> persona_screen
224
+ for (const pref of toList(fm.persona_access)) {
225
+ const pid = inputs.personaIdBySlug.get(screenSlug(pref)) ?? inputs.personaIdByName.get(normalise(pref));
226
+ if (!pid) {
227
+ inputs.findings.add("orphan", `persona_screen: ${screenId} persona_access "${pref}" — no matching persona`);
228
+ continue;
229
+ }
230
+ const key = `${pid}__${screenId}`;
231
+ if (!personaScreen.has(key))
232
+ personaScreen.set(key, { id: key, persona_id: pid, screen_id: screenId, source_sdd_id: "" });
233
+ }
234
+ }
235
+ return {
236
+ screens,
237
+ uiComponents: [...uiById.values()],
238
+ screenComponent: [...screenComponent.values()],
239
+ screenFeature: [...screenFeature.values()],
240
+ personaScreen: [...personaScreen.values()],
241
+ screenAction: [...screenAction.values()],
242
+ };
243
+ }
244
+ function emitDepthFindings(findings, screenId, status, m, ctx) {
245
+ const { flatThreshold, platform, overlay, componentNames, decorativePrimitives } = ctx;
246
+ const empty = m.componentCount <= 1;
247
+ const flat = !empty && m.maxDepth <= 1 && m.rootChildCount >= flatThreshold;
248
+ // Malformed invocation (vocabulary-free): a lowercase call head `name(`. The
249
+ // grammar invokes a component only via a Capitalised head; a lowercase id is a
250
+ // pure reference and never takes arguments. Such a token composes child-less and
251
+ // is uncounted by componentCount — so it can both *cause* a misleading
252
+ // `screen-empty-composition` and let a block render/lower with dropped subtrees.
253
+ // Surface it explicitly (warn) so the real cause is visible at build time.
254
+ if (m.lowercaseCallHeads.length > 0) {
255
+ const heads = m.lowercaseCallHeads.map((h) => `"${h}"`).join(", ");
256
+ findings.add("convention", `screen ${screenId}: lowercase call head${m.lowercaseCallHeads.length > 1 ? "s" : ""} ${heads} — a component must be Capitalised; a lowercase identifier is a reference and cannot take arguments (screen-lowercase-call-head)`);
257
+ }
258
+ if (empty) {
259
+ findings.add("coverage", `screen ${screenId}: composition is empty — root has no children (screen-empty-composition)`);
260
+ }
261
+ else if (flat) {
262
+ findings.add("coverage", `screen ${screenId}: composition is flat — root has ${m.rootChildCount} children at depth ${m.maxDepth} (screen-flat-composition)`);
263
+ }
264
+ // Status gate (keystone): `working` is the only maturity that asserts a
265
+ // faithful, complete composition (ADR 0168 §4.3). provisional/unknown/absent
266
+ // make no completeness claim and are not gated.
267
+ if (status === COMPLETENESS_STATUS && (empty || flat)) {
268
+ findings.add("convention", `screen ${screenId}: status "${COMPLETENESS_STATUS}" exceeds composition depth — block is ${empty ? "empty" : "flat"} (screen-status-exceeds-depth)`, "error");
269
+ }
270
+ // Mis-rooted (ADR 0168 §4.2 screen-root-is-leaf, archetype-aware per ADR 0169):
271
+ // root is a leaf/control rather than a screen container. isContainer combines
272
+ // the appears-with-children heuristic (containers) with the archetype map.
273
+ // Only fires when the screen declares a known `platform:` — without a map the
274
+ // heuristic alone can't distinguish an empty container root (Stack([])) from a
275
+ // genuine leaf root (Button([])), so the check no-ops (ADR 0168 §4.5).
276
+ if (m.rootComponent !== "" && m.rootChildCount === 0) {
277
+ const byMap = isContainerArchetype(m.rootComponent, platform, overlay);
278
+ if (byMap === false && !m.containers.has(m.rootComponent)) {
279
+ findings.add("convention", `screen ${screenId}: root component "${m.rootComponent}" is a leaf, not a container (screen-root-is-leaf)`, "error");
280
+ }
281
+ }
282
+ // Styling-primitive leakage (ADR 0168 §4.2 screen-styling-primitive-as-structure):
283
+ // an adopter-flagged decorative primitive composed as a structural component.
284
+ // No-ops when the adopter declares no decorative set.
285
+ if (decorativePrimitives !== undefined && decorativePrimitives.size > 0) {
286
+ const leaked = [...componentNames].filter((c) => decorativePrimitives.has(c)).sort();
287
+ if (leaked.length > 0) {
288
+ findings.add("convention", `screen ${screenId}: decorative primitive${leaked.length > 1 ? "s" : ""} ${leaked.map((c) => `"${c}"`).join(", ")} used as structure (screen-styling-primitive-as-structure)`);
289
+ }
290
+ }
291
+ }
292
+ function emptyResult() {
293
+ return { screens: [], uiComponents: [], screenComponent: [], screenFeature: [], personaScreen: [], screenAction: [] };
294
+ }
295
+ function isTemplate(name) {
296
+ if (/[{}]/.test(name))
297
+ return true; // `{placeholder}` scaffolding templates
298
+ // Skip the method-shipped screen boilerplate (`screen-spec-template.md`,
299
+ // `screen-template.md`, bare `template.md`) — but NOT domain screens whose
300
+ // name legitimately ends in "template" (script-template, config-template,
301
+ // email-template, …), which reverse extraction must catalogue.
302
+ const lower = name.toLowerCase();
303
+ return (lower === "template.md" ||
304
+ lower === "screen-template.md" ||
305
+ lower.endsWith("-spec-template.md"));
306
+ }
307
+ // entity-style slug: ALLCAPS-then-Pascal split so naming variation across
308
+ // `MyEcoDrive` / `map_tracking_container` / `TE_driver` resolves stably
309
+ // (per the Phase Pre real-menu finding; mirrors entitySlug).
310
+ function screenSlug(s) {
311
+ return s
312
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
313
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
314
+ .toLowerCase()
315
+ .replace(/[^a-z0-9]+/g, "-")
316
+ .replace(/^-+|-+$/g, "");
317
+ }
318
+ function normalise(s) {
319
+ return s.trim().toLowerCase().replace(/\s+/g, " ");
320
+ }
321
+ // Recognised direction discriminators (ADR 0135). Others pass through flagged.
322
+ const SCREEN_SOURCES = new Set(["screen-spec", "screen-extraction"]);
323
+ function screenSource(value, findings, screenId) {
324
+ if (typeof value !== "string" || value.trim() === "")
325
+ return "screen-spec";
326
+ const v = value.trim();
327
+ if (!SCREEN_SOURCES.has(v)) {
328
+ findings.add("convention", `screens: ${screenId} source "${v}" outside the standard set {screen-spec, screen-extraction} — passed through`);
329
+ }
330
+ return v;
331
+ }
332
+ /**
333
+ * Evidence class from frontmatter (M-ADR 0175, schema 2.42.0). Free-form per the
334
+ * `context`-column precedent — conventional values are `code-reverse`,
335
+ * `vendor-doc`, `operational`, but the column is not an enum and an unrecognised
336
+ * value passes through silently rather than raising: the method does not own an
337
+ * adopter's evidence taxonomy. Accepts the `evidence-class` hyphen alias, as the
338
+ * events and data-model sources do.
339
+ */
340
+ function evidenceClass(fm) {
341
+ const raw = fm["evidence_class"] ?? fm["evidence-class"];
342
+ return typeof raw === "string" ? raw.trim() : "";
343
+ }
344
+ function maturity(value, findings, screenId) {
345
+ const v = value.trim();
346
+ if (!STANDARD_MATURITY.has(v.toLowerCase())) {
347
+ findings.add("convention", `screens: ${screenId} status "${v}" outside standard maturity set — passed through`);
348
+ }
349
+ return v;
350
+ }
351
+ function parseFrontmatter(text, findings, filename) {
352
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
353
+ if (!m)
354
+ return {};
355
+ try {
356
+ const parsed = parseYaml(m[1] ?? "");
357
+ return parsed && typeof parsed === "object" ? parsed : {};
358
+ }
359
+ catch {
360
+ findings.add("convention", `screens: ${filename} has an unparseable frontmatter block — ignored`);
361
+ return {};
362
+ }
363
+ }
364
+ function extractH1(text) {
365
+ for (const raw of text.replace(/^---\r?\n[\s\S]*?\r?\n---/, "").split(/\r?\n/)) {
366
+ const line = raw.trim();
367
+ if (line === "")
368
+ continue;
369
+ const m = /^#\s+(.+)$/.exec(line);
370
+ if (!m)
371
+ return "";
372
+ const stripped = (m[1] ?? "").trim();
373
+ const prefix = /^Screen:\s*(.+)$/i.exec(stripped);
374
+ return prefix ? (prefix[1] ?? "").trim() : stripped;
375
+ }
376
+ return "";
377
+ }
378
+ function extractOpenUiLangBlock(text) {
379
+ const m = /```openui-lang\r?\n([\s\S]*?)```/.exec(text);
380
+ return m ? (m[1] ?? "") : null;
381
+ }
382
+ function matchAll(text, re) {
383
+ const out = [];
384
+ for (const m of text.matchAll(re))
385
+ if (m[1])
386
+ out.push(m[1]);
387
+ return out;
388
+ }
389
+ function toList(v) {
390
+ if (Array.isArray(v))
391
+ return v.map((x) => String(x).trim()).filter(Boolean);
392
+ if (typeof v === "string")
393
+ return v.split(",").map((x) => x.trim()).filter(Boolean);
394
+ return [];
395
+ }
396
+ //# sourceMappingURL=screens.js.map