@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.
- package/LICENSE +201 -0
- package/NOTICE +17 -0
- package/README.md +6 -2
- package/dist/author-bin.js +8 -0
- package/dist/author-program.js +99 -0
- package/dist/lib/adr-origin.js +59 -0
- package/dist/lib/agent-harness.js +37 -0
- package/dist/lib/api-surface/__fixtures__/serverless-repo/serverless/serverless.yml +18 -0
- package/dist/lib/api-surface/analyzers/library.js +95 -0
- package/dist/lib/api-surface/analyzers/serverless.js +336 -0
- package/dist/lib/api-surface/analyzers/spring.js +237 -0
- package/dist/lib/api-surface/analyzers/terraform.js +109 -0
- package/dist/lib/api-surface/detect.js +25 -0
- package/dist/lib/api-surface/emit.js +109 -0
- package/dist/lib/api-surface/file-set.js +43 -0
- package/dist/lib/api-surface/index.js +134 -0
- package/dist/lib/api-surface/types.js +17 -0
- package/dist/lib/briefing/index.js +13 -0
- package/dist/lib/briefing/render.js +132 -0
- package/dist/lib/briefing/variables.js +92 -0
- package/dist/lib/catalogue/application-archetype/vocabulary.js +177 -0
- package/dist/lib/catalogue/archetype/maps/android.yaml +66 -0
- package/dist/lib/catalogue/archetype/maps/swiftui.yaml +58 -0
- package/dist/lib/catalogue/archetype/maps/uikit.yaml +48 -0
- package/dist/lib/catalogue/archetype/maps/web-generic.yaml +86 -0
- package/dist/lib/catalogue/archetype/openui-archetype-vocabulary.yaml +46 -0
- package/dist/lib/catalogue/archetype/resolver.js +103 -0
- package/dist/lib/catalogue/archetype/wireframe.js +418 -0
- package/dist/lib/catalogue/baseline-check.js +151 -0
- package/dist/lib/catalogue/build.js +1502 -0
- package/dist/lib/catalogue/builders/adr-supersedes.js +103 -0
- package/dist/lib/catalogue/builders/adrs.js +222 -0
- package/dist/lib/catalogue/builders/application-conformance-coverage.js +47 -0
- package/dist/lib/catalogue/builders/application-data-plane-bindings.js +115 -0
- package/dist/lib/catalogue/builders/c4-components.js +229 -0
- package/dist/lib/catalogue/builders/c4-containers.js +240 -0
- package/dist/lib/catalogue/builders/c4-entity-rollups.js +87 -0
- package/dist/lib/catalogue/builders/c4-external-systems.js +194 -0
- package/dist/lib/catalogue/builders/c4-l1-candidates.js +107 -0
- package/dist/lib/catalogue/builders/c4-systems.js +98 -0
- package/dist/lib/catalogue/builders/capabilities.js +488 -0
- package/dist/lib/catalogue/builders/capability-domain-model.js +74 -0
- package/dist/lib/catalogue/builders/capability-edges.js +256 -0
- package/dist/lib/catalogue/builders/capability-spec-walker.js +99 -0
- package/dist/lib/catalogue/builders/capability-spec.js +348 -0
- package/dist/lib/catalogue/builders/container-dependency.js +183 -0
- package/dist/lib/catalogue/builders/contracts.js +402 -0
- package/dist/lib/catalogue/builders/data-items.js +344 -0
- package/dist/lib/catalogue/builders/data-store-mapping.js +113 -0
- package/dist/lib/catalogue/builders/domain-models.js +175 -0
- package/dist/lib/catalogue/builders/domains.js +119 -0
- package/dist/lib/catalogue/builders/edge-label-merge.js +19 -0
- package/dist/lib/catalogue/builders/entities-manifest.js +160 -0
- package/dist/lib/catalogue/builders/entities-mermaid.js +189 -0
- package/dist/lib/catalogue/builders/entity-persistence.js +102 -0
- package/dist/lib/catalogue/builders/entity-slug.js +47 -0
- package/dist/lib/catalogue/builders/event-catalog-walker.js +186 -0
- package/dist/lib/catalogue/builders/event-edges.js +327 -0
- package/dist/lib/catalogue/builders/events.js +168 -0
- package/dist/lib/catalogue/builders/evidence-class.js +43 -0
- package/dist/lib/catalogue/builders/evidence-target-repo.js +67 -0
- package/dist/lib/catalogue/builders/external-system-exemption.js +120 -0
- package/dist/lib/catalogue/builders/feature-contract.js +77 -0
- package/dist/lib/catalogue/builders/feature-domain-model.js +50 -0
- package/dist/lib/catalogue/builders/feature-grounding-walker.js +254 -0
- package/dist/lib/catalogue/builders/feature-grounding.js +391 -0
- package/dist/lib/catalogue/builders/feature-groups.js +63 -0
- package/dist/lib/catalogue/builders/feature-subscription-channels.js +228 -0
- package/dist/lib/catalogue/builders/feature-tree-walker.js +295 -0
- package/dist/lib/catalogue/builders/features.js +300 -0
- package/dist/lib/catalogue/builders/glossary-xref.js +378 -0
- package/dist/lib/catalogue/builders/glossary.js +204 -0
- package/dist/lib/catalogue/builders/implementations.js +214 -0
- package/dist/lib/catalogue/builders/index.js +64 -0
- package/dist/lib/catalogue/builders/integration-components.js +344 -0
- package/dist/lib/catalogue/builders/integrations.js +181 -0
- package/dist/lib/catalogue/builders/persona-c4system.js +149 -0
- package/dist/lib/catalogue/builders/persona-container.js +133 -0
- package/dist/lib/catalogue/builders/persona-journey-edges.js +133 -0
- package/dist/lib/catalogue/builders/personas.js +152 -0
- package/dist/lib/catalogue/builders/product-edges.js +106 -0
- package/dist/lib/catalogue/builders/product-platform-dependencies.js +405 -0
- package/dist/lib/catalogue/builders/products.js +162 -0
- package/dist/lib/catalogue/builders/quality-scores.js +635 -0
- package/dist/lib/catalogue/builders/repo-capability-exemption.js +61 -0
- package/dist/lib/catalogue/builders/repo-dependency.js +127 -0
- package/dist/lib/catalogue/builders/repo-edges.js +340 -0
- package/dist/lib/catalogue/builders/repositories.js +458 -0
- package/dist/lib/catalogue/builders/scaffolds.js +197 -0
- package/dist/lib/catalogue/builders/screen-tree.js +222 -0
- package/dist/lib/catalogue/builders/screens.js +396 -0
- package/dist/lib/catalogue/builders/service-capability-exemption.js +63 -0
- package/dist/lib/catalogue/builders/service-contract-consumes.js +186 -0
- package/dist/lib/catalogue/builders/service-contract.js +74 -0
- package/dist/lib/catalogue/builders/service-entity-tables.js +187 -0
- package/dist/lib/catalogue/builders/services.js +232 -0
- package/dist/lib/catalogue/builders/system-external-dependency.js +115 -0
- package/dist/lib/catalogue/builders/user-journeys.js +253 -0
- package/dist/lib/catalogue/canonical-schema.yaml +4117 -0
- package/dist/lib/catalogue/cli-version.js +10 -0
- package/dist/lib/catalogue/contract-id.js +49 -0
- package/dist/lib/catalogue/coverage-predicates.js +603 -0
- package/dist/lib/catalogue/coverage-render.js +552 -0
- package/dist/lib/catalogue/coverage-run.js +115 -0
- package/dist/lib/catalogue/csv-reader.js +97 -0
- package/dist/lib/catalogue/csv-writer.js +96 -0
- package/dist/lib/catalogue/data-plane-coherence.js +77 -0
- package/dist/lib/catalogue/ddl-generator.js +233 -0
- package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
- package/dist/lib/catalogue/engine-validator.js +239 -0
- package/dist/lib/catalogue/ext-id.js +76 -0
- package/dist/lib/catalogue/feature-grounding-aliases-loader.js +65 -0
- package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
- package/dist/lib/catalogue/finding-identity.js +183 -0
- package/dist/lib/catalogue/findings.js +46 -0
- package/dist/lib/catalogue/fk-validator.js +136 -0
- package/dist/lib/catalogue/freshness.js +70 -0
- package/dist/lib/catalogue/gate-checks.js +338 -0
- package/dist/lib/catalogue/graph-projection.js +346 -0
- package/dist/lib/catalogue/impact-run.js +228 -0
- package/dist/lib/catalogue/manifest-loader.js +435 -0
- package/dist/lib/catalogue/platform-scope.js +7 -0
- package/dist/lib/catalogue/population-checks.js +87 -0
- package/dist/lib/catalogue/relationship-semantics.js +66 -0
- package/dist/lib/catalogue/reverse-product-quality.js +99 -0
- package/dist/lib/catalogue/rubric-coverage-map.yaml +871 -0
- package/dist/lib/catalogue/runway-vocabulary-loader.js +56 -0
- package/dist/lib/catalogue/scaffold-delta.js +261 -0
- package/dist/lib/catalogue/schema-compat.js +52 -0
- package/dist/lib/catalogue/schema-loader.js +87 -0
- package/dist/lib/catalogue/scores-coverage.js +573 -0
- package/dist/lib/catalogue/screen-validation-loader.js +89 -0
- package/dist/lib/catalogue/service-architecture-parser.js +193 -0
- package/dist/lib/catalogue/service-contract-aliases-loader.js +109 -0
- package/dist/lib/catalogue/staleness-verdicts.js +345 -0
- package/dist/lib/catalogue/traceability-loader.js +350 -0
- package/dist/lib/catalogue/validate.js +200 -0
- package/dist/lib/catalogue/version.js +18 -0
- package/dist/lib/conformance/binding.js +70 -0
- package/dist/lib/conformance/finding.js +69 -0
- package/dist/lib/conformance/index.js +578 -0
- package/dist/lib/context-pull/index.js +191 -0
- package/dist/lib/context-pull/resolve.js +70 -0
- package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
- package/dist/lib/contracts-availability/index.js +267 -0
- package/dist/lib/generate-briefing/index.js +77 -0
- package/dist/lib/list/discover.js +100 -0
- package/dist/lib/list/format.js +84 -0
- package/dist/lib/list/index.js +16 -0
- package/dist/lib/lock-file.js +437 -0
- package/dist/lib/pack/log.js +18 -0
- package/dist/lib/pack/schema.js +121 -0
- package/dist/lib/pack/seed.js +182 -0
- package/dist/lib/pack/validate.js +49 -0
- package/dist/lib/package-version.js +92 -0
- package/dist/lib/plan/index.js +69 -0
- package/dist/lib/plan/read-sdd.js +102 -0
- package/dist/lib/plan/render-template.js +84 -0
- package/dist/lib/repo-kind.js +129 -0
- package/dist/lib/scaffold/domain.js +234 -0
- package/dist/lib/scaffold/registry.js +136 -0
- package/dist/lib/scripts.js +7 -0
- package/dist/lib/sdd-index/repo-config.js +245 -0
- package/dist/lib/sdd-reader.js +91 -0
- package/dist/lib/snapshot/advance.js +454 -0
- package/dist/lib/snapshot/diff.js +190 -0
- package/dist/lib/snapshot/fold.js +208 -0
- package/dist/lib/snapshot/land.js +178 -0
- package/dist/lib/snapshot/manifest.js +206 -0
- package/dist/lib/snapshot/targets.js +169 -0
- package/dist/lib/snapshot/verify.js +91 -0
- package/dist/lib/sync/briefing-merge.js +106 -0
- package/dist/lib/sync/bundle-cache.js +109 -0
- package/dist/lib/sync/yaml-block-merge.js +120 -0
- package/dist/lib/validate/aggregate.js +51 -0
- package/dist/lib/validate/bundle-coherence.js +74 -0
- package/dist/lib/validate/dispatch.js +243 -0
- package/dist/lib/validate/index.js +110 -0
- package/dist/lib/validate/plan.js +54 -0
- package/dist/lib/validate/runner.js +85 -0
- package/dist/lib/validate/yaml-parse.js +112 -0
- package/dist/maintain-version.json +4 -0
- package/dist/verbs/api-surface.js +71 -0
- package/dist/verbs/catalogue-check.js +28 -0
- package/dist/verbs/catalogue-verify.js +114 -0
- package/dist/verbs/conformance.js +34 -0
- package/dist/verbs/context.js +39 -0
- package/dist/verbs/contracts.js +34 -0
- package/dist/verbs/generate-briefing.js +29 -0
- package/dist/verbs/list.js +45 -0
- package/dist/verbs/pack-validate.js +17 -0
- package/dist/verbs/path-option.js +25 -0
- package/dist/verbs/plan.js +42 -0
- package/dist/verbs/render.js +87 -0
- package/dist/verbs/scaffold.js +83 -0
- package/dist/verbs/snapshot.js +134 -0
- package/dist/verbs/validate.js +48 -0
- package/dist/verify-program.js +122 -0
- package/dist/verify-version.json +4 -0
- package/package.json +32 -4
- package/scripts/validate-app-dependencies.sh +664 -0
- package/scripts/validate-app-docs.sh +199 -0
- 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
|