aub-workspace 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +23 -0
  3. package/bin/aub-workspace.mjs +246 -0
  4. package/package.json +32 -0
  5. package/vendor/aub/apps/editor/dist/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
  6. package/vendor/aub/apps/editor/dist/assets/angular-importer.lib-dB_jK4mR.js +32 -0
  7. package/vendor/aub/apps/editor/dist/assets/canvas-tools-CuYC7cA2.js +364 -0
  8. package/vendor/aub/apps/editor/dist/assets/design-bridge.lib-DJvaK6AX.js +1 -0
  9. package/vendor/aub/apps/editor/dist/assets/export-agent-prompt.lib-BsP0KNqo.js +2 -0
  10. package/vendor/aub/apps/editor/dist/assets/export-md.lib-DdmdeWgO.js +3 -0
  11. package/vendor/aub/apps/editor/dist/assets/handoff-package.lib-DDYpcEma.js +20 -0
  12. package/vendor/aub/apps/editor/dist/assets/implementation-report.lib-CmsSB_8s.js +1 -0
  13. package/vendor/aub/apps/editor/dist/assets/index-BCH-ek3h.js +2 -0
  14. package/vendor/aub/apps/editor/dist/assets/index-lAnc928Q.css +1 -0
  15. package/vendor/aub/apps/editor/dist/assets/index-vt1nM1M4.js +507 -0
  16. package/vendor/aub/apps/editor/dist/assets/jszip.min-CRfXyL92.js +12 -0
  17. package/vendor/aub/apps/editor/dist/assets/react-vendor-ByX9Pqse.js +40 -0
  18. package/vendor/aub/apps/editor/dist/brand/android-chrome-192x192.png +0 -0
  19. package/vendor/aub/apps/editor/dist/brand/android-chrome-512x512.png +0 -0
  20. package/vendor/aub/apps/editor/dist/brand/app-icon-1024.png +0 -0
  21. package/vendor/aub/apps/editor/dist/brand/app-icon-192.png +0 -0
  22. package/vendor/aub/apps/editor/dist/brand/app-icon-512.png +0 -0
  23. package/vendor/aub/apps/editor/dist/brand/apple-touch-icon.png +0 -0
  24. package/vendor/aub/apps/editor/dist/brand/aub-logo-mark.svg +28 -0
  25. package/vendor/aub/apps/editor/dist/brand/favicon-16x16.png +0 -0
  26. package/vendor/aub/apps/editor/dist/brand/favicon-32x32.png +0 -0
  27. package/vendor/aub/apps/editor/dist/brand/favicon-48x48.png +0 -0
  28. package/vendor/aub/apps/editor/dist/brand/favicon.ico +0 -0
  29. package/vendor/aub/apps/editor/dist/brand/favicon.svg +9 -0
  30. package/vendor/aub/apps/editor/dist/brand/maskable-icon-512.png +0 -0
  31. package/vendor/aub/apps/editor/dist/brand/mstile-150x150.png +0 -0
  32. package/vendor/aub/apps/editor/dist/brand/safari-pinned-tab.svg +8 -0
  33. package/vendor/aub/apps/editor/dist/browserconfig.xml +9 -0
  34. package/vendor/aub/apps/editor/dist/index.html +22 -0
  35. package/vendor/aub/apps/editor/dist/manifest.webmanifest +28 -0
  36. package/vendor/aub/apps/editor/dist/template-previews/admin-table.png +0 -0
  37. package/vendor/aub/apps/editor/dist/template-previews/booking.png +0 -0
  38. package/vendor/aub/apps/editor/dist/template-previews/calendar.png +0 -0
  39. package/vendor/aub/apps/editor/dist/template-previews/catalog.png +0 -0
  40. package/vendor/aub/apps/editor/dist/template-previews/chat.png +0 -0
  41. package/vendor/aub/apps/editor/dist/template-previews/checkout.png +0 -0
  42. package/vendor/aub/apps/editor/dist/template-previews/crm.png +0 -0
  43. package/vendor/aub/apps/editor/dist/template-previews/dashboard.png +0 -0
  44. package/vendor/aub/apps/editor/dist/template-previews/feed.png +0 -0
  45. package/vendor/aub/apps/editor/dist/template-previews/files.png +0 -0
  46. package/vendor/aub/apps/editor/dist/template-previews/kanban.png +0 -0
  47. package/vendor/aub/apps/editor/dist/template-previews/landing.png +0 -0
  48. package/vendor/aub/apps/editor/dist/template-previews/mail.png +0 -0
  49. package/vendor/aub/apps/editor/dist/template-previews/onboarding.png +0 -0
  50. package/vendor/aub/apps/editor/dist/template-previews/pricing.png +0 -0
  51. package/vendor/aub/apps/editor/dist/template-previews/product-detail.png +0 -0
  52. package/vendor/aub/apps/editor/dist/template-previews/settings.png +0 -0
  53. package/vendor/aub/apps/editor/dist/template-previews/wiki.png +0 -0
  54. package/vendor/aub/apps/mcp-server/dist/aub.js +15 -0
  55. package/vendor/aub/apps/mcp-server/dist/context.js +1 -0
  56. package/vendor/aub/apps/mcp-server/dist/http.js +123 -0
  57. package/vendor/aub/apps/mcp-server/dist/index.js +23 -0
  58. package/vendor/aub/apps/mcp-server/dist/repo.js +17 -0
  59. package/vendor/aub/apps/mcp-server/dist/schema.js +42 -0
  60. package/vendor/aub/apps/mcp-server/dist/server.js +80 -0
  61. package/vendor/aub/apps/mcp-server/dist/tools/approve-component-candidate.js +27 -0
  62. package/vendor/aub/apps/mcp-server/dist/tools/diff-blueprints.js +27 -0
  63. package/vendor/aub/apps/mcp-server/dist/tools/export-handoff.js +87 -0
  64. package/vendor/aub/apps/mcp-server/dist/tools/export-prompt.js +35 -0
  65. package/vendor/aub/apps/mcp-server/dist/tools/export-template-authoring-prompt.js +13 -0
  66. package/vendor/aub/apps/mcp-server/dist/tools/generate-template-from-source.js +25 -0
  67. package/vendor/aub/apps/mcp-server/dist/tools/get-aub-session.js +13 -0
  68. package/vendor/aub/apps/mcp-server/dist/tools/get-blueprint.js +28 -0
  69. package/vendor/aub/apps/mcp-server/dist/tools/get-project.js +45 -0
  70. package/vendor/aub/apps/mcp-server/dist/tools/get-workspace-status.js +10 -0
  71. package/vendor/aub/apps/mcp-server/dist/tools/import-design-bridge.js +62 -0
  72. package/vendor/aub/apps/mcp-server/dist/tools/list-blueprints.js +11 -0
  73. package/vendor/aub/apps/mcp-server/dist/tools/list-projects.js +11 -0
  74. package/vendor/aub/apps/mcp-server/dist/tools/lock-blueprint.js +33 -0
  75. package/vendor/aub/apps/mcp-server/dist/tools/migrate-blueprint.js +38 -0
  76. package/vendor/aub/apps/mcp-server/dist/tools/resolve-component.js +51 -0
  77. package/vendor/aub/apps/mcp-server/dist/tools/scaffold-blueprint.js +53 -0
  78. package/vendor/aub/apps/mcp-server/dist/tools/scan-project-ui.js +18 -0
  79. package/vendor/aub/apps/mcp-server/dist/tools/submit-report.js +48 -0
  80. package/vendor/aub/apps/mcp-server/dist/tools/update-aub-session.js +14 -0
  81. package/vendor/aub/apps/mcp-server/dist/tools/validate-blueprint.js +67 -0
  82. package/vendor/aub/apps/mcp-server/dist/tools/validate-project.js +74 -0
  83. package/vendor/aub/apps/mcp-server/dist/tools/write-blueprint.js +72 -0
  84. package/vendor/aub/apps/mcp-server/dist/workspace.js +138 -0
  85. package/vendor/aub/docs/agent-handoff.md +85 -0
  86. package/vendor/aub/docs/agent-handoff.zh-Hant.md +85 -0
  87. package/vendor/aub/docs/template-authoring-agent.md +86 -0
  88. package/vendor/aub/schema/aub-ci.schema.json +34 -0
  89. package/vendor/aub/schema/aub.registry.schema.json +118 -0
  90. package/vendor/aub/schema/design-bridge.schema.json +44 -0
  91. package/vendor/aub/schema/implementation-report.schema.json +93 -0
  92. package/vendor/aub/schema/project-types.ts +72 -0
  93. package/vendor/aub/schema/registry/components.json +118 -0
  94. package/vendor/aub/schema/types.js +13 -0
  95. package/vendor/aub/schema/types.ts +348 -0
  96. package/vendor/aub/schema/ui-blueprint-lock.schema.json +61 -0
  97. package/vendor/aub/schema/ui-blueprint.schema.json +1339 -0
  98. package/vendor/aub/schema/ui-project.schema.json +139 -0
  99. package/vendor/aub/scripts/agent-implementation-benchmark.lib.mjs +125 -0
  100. package/vendor/aub/scripts/angular-importer.lib.mjs +982 -0
  101. package/vendor/aub/scripts/check-editor-bundle-budget.mjs +36 -0
  102. package/vendor/aub/scripts/ci-verify.lib.mjs +256 -0
  103. package/vendor/aub/scripts/ci-verify.mjs +45 -0
  104. package/vendor/aub/scripts/create-authoring-kit.mjs +84 -0
  105. package/vendor/aub/scripts/create-implementation-report.mjs +24 -0
  106. package/vendor/aub/scripts/design-bridge.lib.d.mts +32 -0
  107. package/vendor/aub/scripts/design-bridge.lib.mjs +69 -0
  108. package/vendor/aub/scripts/diff-blueprint.lib.d.mts +18 -0
  109. package/vendor/aub/scripts/diff-blueprint.lib.mjs +148 -0
  110. package/vendor/aub/scripts/diff-blueprint.mjs +25 -0
  111. package/vendor/aub/scripts/export-agent-prompt.lib.d.mts +10 -0
  112. package/vendor/aub/scripts/export-agent-prompt.lib.mjs +160 -0
  113. package/vendor/aub/scripts/export-agent-prompt.mjs +79 -0
  114. package/vendor/aub/scripts/export-md.lib.d.mts +3 -0
  115. package/vendor/aub/scripts/export-md.lib.mjs +302 -0
  116. package/vendor/aub/scripts/export-md.mjs +43 -0
  117. package/vendor/aub/scripts/generate-registry-artifacts.lib.mjs +118 -0
  118. package/vendor/aub/scripts/generate-registry-artifacts.mjs +65 -0
  119. package/vendor/aub/scripts/generate-site-locales.mjs +545 -0
  120. package/vendor/aub/scripts/handoff-package.lib.d.mts +20 -0
  121. package/vendor/aub/scripts/handoff-package.lib.mjs +111 -0
  122. package/vendor/aub/scripts/implementation-report.lib.d.mts +21 -0
  123. package/vendor/aub/scripts/implementation-report.lib.mjs +97 -0
  124. package/vendor/aub/scripts/import-angular-component.mjs +72 -0
  125. package/vendor/aub/scripts/import-design-bridge.mjs +59 -0
  126. package/vendor/aub/scripts/lock-blueprint.lib.d.mts +23 -0
  127. package/vendor/aub/scripts/lock-blueprint.lib.mjs +58 -0
  128. package/vendor/aub/scripts/lock-blueprint.mjs +36 -0
  129. package/vendor/aub/scripts/migrate-blueprint-cli.mjs +28 -0
  130. package/vendor/aub/scripts/migrate-blueprint.d.mts +5 -0
  131. package/vendor/aub/scripts/migrate-blueprint.mjs +95 -0
  132. package/vendor/aub/scripts/package-workspace-cli.mjs +34 -0
  133. package/vendor/aub/scripts/project.lib.d.mts +44 -0
  134. package/vendor/aub/scripts/project.lib.mjs +175 -0
  135. package/vendor/aub/scripts/project.mjs +332 -0
  136. package/vendor/aub/scripts/registry.lib.d.mts +52 -0
  137. package/vendor/aub/scripts/registry.lib.mjs +222 -0
  138. package/vendor/aub/scripts/run-agent-implementation.mjs +423 -0
  139. package/vendor/aub/scripts/run-agent-readability.mjs +145 -0
  140. package/vendor/aub/scripts/run-ollama-prompt.mjs +30 -0
  141. package/vendor/aub/scripts/scaffold-blueprint.lib.d.mts +38 -0
  142. package/vendor/aub/scripts/scaffold-blueprint.lib.mjs +316 -0
  143. package/vendor/aub/scripts/scaffold-blueprint.mjs +86 -0
  144. package/vendor/aub/scripts/score-agent-implementation.mjs +27 -0
  145. package/vendor/aub/scripts/score-agent-readability.mjs +54 -0
  146. package/vendor/aub/scripts/sync-brand-assets.mjs +33 -0
  147. package/vendor/aub/scripts/validate-blueprint.lib.d.mts +14 -0
  148. package/vendor/aub/scripts/validate-blueprint.lib.mjs +136 -0
  149. package/vendor/aub/scripts/validate.mjs +128 -0
  150. package/vendor/aub/scripts/verify-implementation-report.mjs +36 -0
  151. package/vendor/aub/scripts/workspace-loop.lib.d.mts +17 -0
  152. package/vendor/aub/scripts/workspace-loop.lib.mjs +674 -0
@@ -0,0 +1,982 @@
1
+ import { parse, parseFragment } from 'parse5';
2
+ import postcss from 'postcss';
3
+ import scssSyntax from 'postcss-scss';
4
+ import { defaultDesignSystem } from './migrate-blueprint.mjs';
5
+
6
+ export const ANGULAR_IMPORTER_VERSION = '1.0.0';
7
+
8
+ const CONTAINER_TYPES = new Set([
9
+ 'app_shell', 'page', 'section', 'header', 'sidebar', 'top_bar', 'bottom_nav',
10
+ 'stack', 'grid', 'split_pane', 'scroll_area', 'list', 'detail_panel',
11
+ 'timeline', 'activity_feed', 'form', 'field_group', 'menu', 'toolbar',
12
+ 'button_group', 'command_palette', 'modal', 'drawer', 'tabs', 'stepper',
13
+ 'card', 'kanban_board', 'kanban_column', 'rich_text_editor',
14
+ ]);
15
+
16
+ const ANGULAR_EVENT_TO_TRIGGER = {
17
+ click: 'click',
18
+ change: 'change',
19
+ submit: 'submit',
20
+ focus: 'focus',
21
+ blur: 'blur',
22
+ mouseenter: 'hover',
23
+ keyup: 'change',
24
+ };
25
+
26
+ export function normalizeAngularBundle(input) {
27
+ const files = Array.isArray(input) ? input : input?.files;
28
+ if (!Array.isArray(files) || files.length === 0) {
29
+ throw new Error('Angular import requires at least one source file.');
30
+ }
31
+ const seen = new Set();
32
+ return files.map((file) => {
33
+ const path = sanitizeSourcePath(file.path ?? file.name ?? '');
34
+ if (!path) throw new Error('Every Angular source file requires a relative path.');
35
+ if (seen.has(path)) throw new Error(`Duplicate source path: ${path}`);
36
+ seen.add(path);
37
+ return { path, content: String(file.content ?? '') };
38
+ });
39
+ }
40
+
41
+ export function discoverAngularComponents(input) {
42
+ const files = normalizeAngularBundle(input);
43
+ const components = [];
44
+ for (const file of files.filter((candidate) => candidate.path.endsWith('.ts'))) {
45
+ const metadata = readComponentMetadata(file);
46
+ if (!metadata) continue;
47
+ const templatePath = metadata.templateUrl
48
+ ? resolveRelativeSource(file.path, metadata.templateUrl)
49
+ : null;
50
+ components.push({
51
+ selector: metadata.selector,
52
+ className: metadata.className,
53
+ tsPath: file.path,
54
+ templatePath,
55
+ stylePaths: metadata.styleUrls.map((stylePath) => resolveRelativeSource(file.path, stylePath)),
56
+ label: metadata.selector || metadata.className || file.path,
57
+ });
58
+ }
59
+ if (components.length === 0) {
60
+ for (const file of files.filter((candidate) => candidate.path.endsWith('.html'))) {
61
+ components.push({
62
+ selector: null,
63
+ className: null,
64
+ tsPath: null,
65
+ templatePath: file.path,
66
+ stylePaths: [],
67
+ label: file.path.replace(/\.component\.html$|\.html$/i, ''),
68
+ });
69
+ }
70
+ }
71
+ return components;
72
+ }
73
+
74
+ export async function importAngularComponent(input, options = {}) {
75
+ const files = normalizeAngularBundle(input);
76
+ const byPath = new Map(files.map((file) => [file.path, file]));
77
+ const components = discoverAngularComponents(files);
78
+ if (components.length === 0) throw new Error('No Angular component or HTML template was found.');
79
+ const entry = selectEntry(components, options.entry);
80
+ const diagnostics = [];
81
+ const sourceMap = {};
82
+ const componentBySelector = new Map(
83
+ components.filter((component) => component.selector).map((component) => [component.selector, component])
84
+ );
85
+ const forms = collectForms(files);
86
+ const styleResult = collectDesignSystem(files, diagnostics);
87
+ const templateFile = entry.templatePath ? byPath.get(entry.templatePath) : null;
88
+ if (!templateFile) throw new Error(`Template file not found for ${entry.label}.`);
89
+
90
+ const builder = createBuilder({
91
+ files,
92
+ byPath,
93
+ components,
94
+ componentBySelector,
95
+ forms,
96
+ diagnostics,
97
+ sourceMap,
98
+ styleResult,
99
+ language: options.language ?? 'zh-Hant',
100
+ });
101
+ const rootName = inferScreenName(templateFile.content, entry.label);
102
+ const root = builder.addNode({
103
+ idHint: 'root',
104
+ type: 'page',
105
+ name: rootName,
106
+ role: 'Imported Angular screen root.',
107
+ parentId: null,
108
+ file: templateFile.path,
109
+ line: 1,
110
+ layout: {
111
+ mode: 'auto',
112
+ display: 'flex',
113
+ direction: 'column',
114
+ align: 'stretch',
115
+ gap: { x: 12, y: 12 },
116
+ padding: { top: 16, right: 16, bottom: 16, left: 16 },
117
+ },
118
+ });
119
+
120
+ builder.parseTemplate(templateFile, root.id, entry, []);
121
+ builder.finalizeChildren();
122
+ const screenId = slug(entry.selector || entry.className || rootName || 'angular-screen');
123
+ const tableNodes = builder.nodes.filter((node) => node.type === 'data_table');
124
+ const blueprint = {
125
+ version: '0.3.0',
126
+ screen: {
127
+ id: screenId,
128
+ name: rootName,
129
+ type: tableNodes.length > 0 ? 'admin_table' : 'form',
130
+ platform: 'web',
131
+ primary_user_goal: `Use the imported ${rootName} Angular screen.`,
132
+ notes: 'Imported from Angular source. Review diagnostics before treating inferred behavior as authoritative.',
133
+ },
134
+ viewports: [
135
+ { id: 'desktop', width: 1440, height: 900 },
136
+ { id: 'tablet', width: 1024, height: 768 },
137
+ { id: 'mobile', width: 390, height: 844 },
138
+ ],
139
+ provenance: {
140
+ source_kind: 'angular-component',
141
+ framework: 'Angular',
142
+ importer_version: ANGULAR_IMPORTER_VERSION,
143
+ entry_file: templateFile.path,
144
+ source_files: files.map((file) => file.path).sort(),
145
+ },
146
+ design_system: styleResult.designSystem,
147
+ nodes: builder.nodes,
148
+ interactions: builder.interactions,
149
+ responsive: [
150
+ { viewport: 'tablet', rule: 'keep', target_node_id: root.id, changes: { layout: 'auto' } },
151
+ { viewport: 'mobile', rule: 'stack', target_node_id: root.id, changes: { direction: 'column' } },
152
+ ...tableNodes.map((node) => ({
153
+ viewport: 'mobile',
154
+ rule: 'scroll',
155
+ target_node_id: node.id,
156
+ changes: { overflow_x: 'auto' },
157
+ })),
158
+ ],
159
+ acceptance: importedAcceptance(root.id, tableNodes.map((node) => node.id)),
160
+ };
161
+
162
+ for (const diagnostic of builder.postDiagnostics()) diagnostics.push(diagnostic);
163
+ return {
164
+ blueprint,
165
+ diagnostics,
166
+ sourceMap,
167
+ components,
168
+ entry: {
169
+ selector: entry.selector,
170
+ label: entry.label,
171
+ templatePath: templateFile.path,
172
+ },
173
+ confidenceSummary: summarizeDiagnostics(diagnostics, builder.nodes.length),
174
+ unresolvedComponents: diagnostics
175
+ .filter((diagnostic) => diagnostic.code === 'unknown-custom-component')
176
+ .map((diagnostic) => diagnostic.detail)
177
+ .filter(Boolean),
178
+ };
179
+ }
180
+
181
+ function createBuilder(context) {
182
+ const nodes = [];
183
+ const interactions = [];
184
+ const children = new Map();
185
+ const ids = new Map();
186
+ const signatures = new Map();
187
+ const parsedComponents = new Set();
188
+
189
+ function addNode(input) {
190
+ const id = uniqueId(input.idHint || input.name || input.type, ids);
191
+ const signature = input.parentId && input.action
192
+ ? `${input.parentId}|${input.type}|${input.name}|${input.action}`
193
+ : null;
194
+ if (signature && signatures.has(signature)) return signatures.get(signature);
195
+ const node = {
196
+ id,
197
+ type: input.type,
198
+ name: clamp(input.name || humanize(input.type), 128),
199
+ role: clamp(input.role || `Imported ${humanize(input.type)}.`, 280),
200
+ parent_id: input.parentId,
201
+ ...(CONTAINER_TYPES.has(input.type) ? { children: [] } : {}),
202
+ ...(input.layout ? { layout: input.layout } : {}),
203
+ ...(input.content && Object.keys(input.content).length ? { content: input.content } : {}),
204
+ ...(input.style && Object.keys(input.style).length ? { style: input.style } : {}),
205
+ ...(input.states?.length ? { states: [...new Set(input.states)] } : {}),
206
+ ...(input.constraints && Object.keys(input.constraints).length ? { constraints: input.constraints } : {}),
207
+ ...(input.bindings && Object.keys(input.bindings).length ? { bindings: input.bindings } : {}),
208
+ ...(input.validation && Object.keys(input.validation).length ? { validation: input.validation } : {}),
209
+ ...(input.initialState && Object.keys(input.initialState).length ? { initial_state: input.initialState } : {}),
210
+ source: {
211
+ file: sanitizeSourcePath(input.file),
212
+ ...(input.line ? { line: input.line } : {}),
213
+ ...(input.column ? { column: input.column } : {}),
214
+ ...(input.selector ? { selector: input.selector } : {}),
215
+ },
216
+ };
217
+ nodes.push(node);
218
+ context.sourceMap[id] = node.source;
219
+ if (input.parentId) {
220
+ const list = children.get(input.parentId) ?? [];
221
+ list.push(id);
222
+ children.set(input.parentId, list);
223
+ }
224
+ if (signature) signatures.set(signature, node);
225
+ if (input.action) {
226
+ const trigger = input.trigger ?? 'click';
227
+ interactions.push({
228
+ id: uniqueId(`interaction_${id}_${trigger}`, ids),
229
+ trigger,
230
+ source_node_id: id,
231
+ action: normalizeAction(input.action),
232
+ result_state: `The ${node.name} ${trigger} action is observable.`,
233
+ });
234
+ }
235
+ return node;
236
+ }
237
+
238
+ function parseTemplate(file, parentId, component, ancestors) {
239
+ const cycleKey = component?.selector || file.path;
240
+ if (ancestors.includes(cycleKey)) {
241
+ context.diagnostics.push(diagnostic('warning', 'component-cycle', `Skipped cyclic component ${cycleKey}.`, file.path, 1, parentId, 0.4, cycleKey));
242
+ return;
243
+ }
244
+ parsedComponents.add(cycleKey);
245
+ const document = parseFragment(file.content, { sourceCodeLocationInfo: true });
246
+ walkChildren(document.childNodes ?? [], parentId, file, [...ancestors, cycleKey]);
247
+ }
248
+
249
+ function walkChildren(astNodes, parentId, file, ancestors) {
250
+ for (const astNode of astNodes) walk(astNode, parentId, file, ancestors);
251
+ }
252
+
253
+ function walk(astNode, parentId, file, ancestors) {
254
+ if (!astNode || astNode.nodeName === '#comment') return;
255
+ if (astNode.nodeName === '#text') return;
256
+ const tag = astNode.tagName || astNode.nodeName;
257
+ const attrs = attributes(astNode);
258
+ const classes = new Set((attrs.class ?? '').split(/\s+/).filter(Boolean));
259
+ const line = astNode.sourceCodeLocation?.startLine ?? 1;
260
+ const column = astNode.sourceCodeLocation?.startCol ?? 1;
261
+ const text = visibleText(astNode);
262
+ const custom = context.componentBySelector.get(tag);
263
+ const semantic = classifyElement({ tag, attrs, classes, text, astNode, file, forms: context.forms });
264
+
265
+ if (custom) {
266
+ const customNode = addNode({
267
+ idHint: attrs.id || custom.selector,
268
+ type: 'section',
269
+ name: text || humanize(custom.selector),
270
+ role: `Imported child Angular component ${custom.selector}.`,
271
+ parentId,
272
+ file: file.path,
273
+ line,
274
+ column,
275
+ selector: custom.selector,
276
+ layout: columnLayout(),
277
+ bindings: bindingsFromAttrs(attrs),
278
+ initialState: initialStateFromAttrs(attrs, classes),
279
+ });
280
+ const childTemplate = custom.templatePath ? context.byPath.get(custom.templatePath) : null;
281
+ if (childTemplate) parseTemplate(childTemplate, customNode.id, custom, ancestors);
282
+ else context.diagnostics.push(diagnostic('warning', 'missing-child-template', `Template for ${custom.selector} was not provided.`, file.path, line, customNode.id, 0.45, custom.selector));
283
+ return;
284
+ }
285
+
286
+ if (tag.includes('-') && !isKnownCustomTag(tag)) {
287
+ const unknownNode = addNode({
288
+ idHint: attrs.id || tag,
289
+ type: customType(tag),
290
+ name: text || humanize(tag),
291
+ role: `Unresolved custom Angular component ${tag}.`,
292
+ parentId,
293
+ file: file.path,
294
+ line,
295
+ column,
296
+ selector: tag,
297
+ layout: columnLayout(),
298
+ content: { label: text || tag, variant: 'unresolved-component' },
299
+ bindings: bindingsFromAttrs(attrs),
300
+ initialState: initialStateFromAttrs(attrs, classes),
301
+ });
302
+ context.diagnostics.push(diagnostic('warning', 'unknown-custom-component', `Custom component ${tag} requires a mapping or its source files.`, file.path, line, unknownNode.id, 0.35, tag));
303
+ return;
304
+ }
305
+
306
+ if (!semantic) {
307
+ walkChildren(astNode.childNodes ?? [], parentId, file, ancestors);
308
+ return;
309
+ }
310
+
311
+ const controlName = stripExpression(attrs.formcontrolname);
312
+ const formInfo = controlName ? context.forms.controls.get(controlName) : null;
313
+ const label = semantic.label || nearestLabel(astNode) || controlName || text || humanize(semantic.type);
314
+ const event = eventFromAttrs(attrs);
315
+ const content = {
316
+ ...semantic.content,
317
+ ...(['text_input', 'select', 'checkbox', 'radio_group', 'toggle', 'slider', 'date_picker', 'file_upload', 'button', 'icon_button'].includes(semantic.type)
318
+ && !semantic.content?.label ? { label } : {}),
319
+ ...(controlName ? { data_binding: controlName } : {}),
320
+ };
321
+ const node = addNode({
322
+ idHint: attrs.id || controlName || semantic.idHint || label,
323
+ type: semantic.type,
324
+ name: label,
325
+ role: semantic.role,
326
+ parentId,
327
+ file: file.path,
328
+ line,
329
+ column,
330
+ selector: selectorFor(tag, attrs, classes),
331
+ layout: semantic.layout,
332
+ content,
333
+ style: semantic.style,
334
+ constraints: semantic.constraints,
335
+ bindings: {
336
+ ...semantic.bindings,
337
+ ...bindingsFromAttrs(attrs),
338
+ ...(controlName ? { value: controlName } : {}),
339
+ ...(formInfo?.disabled ? { enabled: 'false' } : {}),
340
+ },
341
+ validation: {
342
+ ...validationFromAttrs(attrs),
343
+ ...formInfo?.validation,
344
+ },
345
+ initialState: initialStateFromAttrs(attrs, classes),
346
+ states: formInfo?.disabled || attrs.disabled !== undefined ? ['default', 'disabled'] : undefined,
347
+ action: event?.handler || semantic.action,
348
+ trigger: event?.trigger,
349
+ });
350
+ if (semantic.type === 'data_table') return;
351
+ if (CONTAINER_TYPES.has(semantic.type)) {
352
+ walkChildren(astNode.childNodes ?? [], node.id, file, ancestors);
353
+ }
354
+ }
355
+
356
+ function finalizeChildren() {
357
+ for (const node of nodes) {
358
+ if (!CONTAINER_TYPES.has(node.type)) continue;
359
+ node.children = children.get(node.id) ?? [];
360
+ }
361
+ }
362
+
363
+ function postDiagnostics() {
364
+ const result = [];
365
+ for (const node of nodes) {
366
+ if (['text_input', 'select', 'checkbox', 'radio_group', 'date_picker', 'button', 'icon_button'].includes(node.type)) {
367
+ if (!node.content?.label && !node.content?.text) {
368
+ result.push(diagnostic('warning', 'missing-label', `${node.name} has no reliable accessible label.`, node.source.file, node.source.line, node.id, 0.6));
369
+ }
370
+ }
371
+ }
372
+ return result;
373
+ }
374
+
375
+ return { nodes, interactions, addNode, parseTemplate, finalizeChildren, postDiagnostics };
376
+ }
377
+
378
+ function classifyElement({ tag, attrs, classes, text, astNode, forms }) {
379
+ if (classes.has('txn-title')) {
380
+ return { type: 'heading', label: text || 'Page title', role: 'Imported transaction title.', content: { text: text || 'Page title' } };
381
+ }
382
+ if (tag === 'form') {
383
+ return { type: 'form', label: humanize(stripExpression(attrs['[formgroup]']) || 'Form'), role: 'Imported Angular form.', layout: columnLayout(), content: { action: 'submit:form' } };
384
+ }
385
+ if (tag === 'table') {
386
+ return {
387
+ type: 'data_table',
388
+ label: attrs['aria-label'] || 'Data table',
389
+ role: 'Imported data table with declared columns.',
390
+ content: {
391
+ label: attrs['aria-label'] || 'Imported data',
392
+ data_binding: repeatExpression(astNode) || 'table.rows',
393
+ columns: tableColumns(astNode),
394
+ },
395
+ constraints: { min_height: 160 },
396
+ };
397
+ }
398
+ if (tag === 'input') {
399
+ const inputType = (attrs.type || 'text').toLowerCase();
400
+ if (inputType === 'checkbox') return { type: 'checkbox', label: '', role: 'Imported checkbox.', content: {} };
401
+ if (inputType === 'radio') return { type: 'radio_group', label: '', role: 'Imported radio choice.', content: {} };
402
+ if (inputType === 'file') return { type: 'file_upload', label: '', role: 'Imported file input.', content: {} };
403
+ if (inputType === 'range') return { type: 'slider', label: '', role: 'Imported range control.', content: {} };
404
+ return { type: 'text_input', label: '', role: 'Imported text input.', content: { placeholder: attrs.placeholder || '' } };
405
+ }
406
+ if (tag === 'textarea') return { type: 'textarea', label: '', role: 'Imported multiline input.', content: { placeholder: attrs.placeholder || '' } };
407
+ if (tag === 'select') {
408
+ const optionRepeat = findAttributeDeep(astNode, '*ngfor');
409
+ return { type: 'select', label: '', role: 'Imported select control.', content: { placeholder: firstOptionText(astNode) || 'Choose option' }, bindings: optionRepeat ? { options: optionRepeat } : undefined };
410
+ }
411
+ if (tag === 'button' || tag === 'app-auth-button') {
412
+ const label = attrs.title || text || humanize(tag);
413
+ const iconOnly = !text && !attrs.title;
414
+ return { type: iconOnly ? 'icon_button' : 'button', label, role: 'Imported action control.', content: { label, action: normalizeAction(eventFromAttrs(attrs)?.handler || label) } };
415
+ }
416
+ if (tag === 'sfap-datepicker') return { type: 'date_picker', label: '', role: 'Imported date picker.', content: {} };
417
+ if (tag === 'a' && (text || eventFromAttrs(attrs))) {
418
+ return { type: 'link', label: text || 'Link', role: 'Imported link.', content: { text: text || 'Link', action: normalizeAction(eventFromAttrs(attrs)?.handler || attrs.href || 'navigate') } };
419
+ }
420
+ if (tag === 'ul' && (classes.has('nav-tabs') || attrs.role === 'tablist')) return { type: 'tabs', label: 'Tabs', role: 'Imported tab selector.', layout: rowLayout() };
421
+ if (tag === 'li' && classes.has('nav-item')) return { type: 'nav_item', label: text || 'Tab', role: 'Imported tab item.', content: { label: text || 'Tab', action: normalizeAction(eventFromAttrs(attrs)?.handler || `select:${slug(text)}`) } };
422
+ if (classes.has('btn-area')) return { type: 'button_group', label: 'Actions', role: 'Imported action group.', layout: rowLayout() };
423
+ if (classes.has('fix-table-container')) return { type: 'scroll_area', label: 'Scrollable table region', role: 'Imported horizontally scrollable region.', layout: columnLayout() };
424
+ if (classes.has('txn-query-area')) return { type: 'section', label: 'Query area', role: 'Imported query and filter region.', layout: columnLayout() };
425
+ if (classes.has('txn-edit-area')) return { type: 'toolbar', label: 'Table actions', role: 'Imported table action toolbar.', layout: rowLayout() };
426
+ if (classes.has('collapse')) return { type: 'field_group', label: textHeading(astNode) || humanize(attrs.id || 'Collapsible group'), role: 'Imported collapsible field group.', layout: columnLayout() };
427
+ if (classes.has('card') && classes.has('card-body')) return { type: 'section', label: textHeading(astNode) || 'Content section', role: 'Imported card body.', layout: columnLayout() };
428
+ if ([...classes].some((className) => className.startsWith('txn-form-group'))) return { type: 'field_group', label: nearestLabel(astNode) || 'Field group', role: 'Imported labeled field group.', layout: rowLayout() };
429
+ if (classes.has('row')) return { type: 'stack', label: 'Row', role: 'Imported Bootstrap row.', layout: rowLayout() };
430
+ return null;
431
+ }
432
+
433
+ function readComponentMetadata(file) {
434
+ const className = readClassName(file.content);
435
+ const selector = matchStringProperty(file.content, 'selector');
436
+ const templateUrl = matchStringProperty(file.content, 'templateUrl');
437
+ const styleUrlsBlock = file.content.match(/styleUrls\s*:\s*\[([\s\S]*?)\]/m)?.[1] ?? '';
438
+ const styleUrls = [...styleUrlsBlock.matchAll(/['"]([^'"]+)['"]/g)].map((match) => match[1]);
439
+ if (!selector && !templateUrl && !/@Component\s*\(/.test(file.content)) return null;
440
+ return { selector, templateUrl, styleUrls, className };
441
+ }
442
+
443
+ function collectForms(files) {
444
+ const controls = new Map();
445
+ for (const file of files.filter((candidate) => candidate.path.endsWith('.ts'))) {
446
+ for (const objectBlock of findGroupObjectBlocks(file.content)) {
447
+ for (const property of splitObjectProperties(objectBlock)) {
448
+ const name = property.name.replace(/^['"]|['"]$/g, '');
449
+ const raw = property.value;
450
+ const pattern = raw.match(/Validators\.pattern\(\s*(['"`])([\s\S]*?)\1\s*\)/)?.[2];
451
+ controls.set(name, {
452
+ disabled: /disabled\s*:\s*true/.test(raw),
453
+ validation: compactObject({
454
+ required: /Validators\.required/.test(raw) || undefined,
455
+ pattern: pattern || undefined,
456
+ }),
457
+ });
458
+ }
459
+ }
460
+ }
461
+ return { controls };
462
+ }
463
+
464
+ function collectDesignSystem(files, diagnostics) {
465
+ const designSystem = defaultDesignSystem();
466
+ designSystem.name = 'Imported Angular styles';
467
+ const values = { colors: new Set(), radii: new Set(), shadows: new Set(), spacing: new Set(), typography: new Set() };
468
+ for (const file of files.filter((candidate) => /\.(scss|css)$/i.test(candidate.path))) {
469
+ try {
470
+ const root = file.path.endsWith('.scss')
471
+ ? scssSyntax.parse(file.content, { from: file.path })
472
+ : postcss.parse(file.content, { from: file.path });
473
+ root.walkDecls((decl) => {
474
+ if (/color|background|border-color/i.test(decl.prop)) {
475
+ for (const value of decl.value.match(/#[0-9a-f]{3,8}\b|rgba?\([^)]+\)/gi) ?? []) values.colors.add(value);
476
+ }
477
+ if (/border-radius/i.test(decl.prop)) values.radii.add(decl.value);
478
+ if (/box-shadow/i.test(decl.prop)) values.shadows.add(decl.value);
479
+ if (/^(gap|padding|margin)/i.test(decl.prop) && /^\d+(?:\.\d+)?(?:px|rem)$/.test(decl.value)) values.spacing.add(decl.value);
480
+ if (/^(font|font-size|font-weight|line-height)/i.test(decl.prop)) values.typography.add(`${decl.prop}: ${decl.value}`);
481
+ });
482
+ } catch (error) {
483
+ diagnostics.push(diagnostic('warning', 'scss-parse-failed', `Could not fully parse ${file.path}: ${error.message}`, file.path, 1, null, 0.5));
484
+ }
485
+ }
486
+ assignTokens(designSystem.colors, 'imported.color', values.colors);
487
+ assignTokens(designSystem.radii, 'imported.radius', values.radii);
488
+ assignTokens(designSystem.shadows, 'imported.shadow', values.shadows);
489
+ assignTokens(designSystem.spacing, 'imported.space', values.spacing);
490
+ assignTokens(designSystem.typography, 'imported.type', values.typography);
491
+ return { designSystem };
492
+ }
493
+
494
+ function tableColumns(tableNode) {
495
+ const headers = [];
496
+ const cells = firstTableDataCells(tableNode);
497
+ walkHtml(tableNode, (node) => {
498
+ if (node.tagName !== 'th') return;
499
+ const attrs = attributes(node);
500
+ const cell = cells[headers.length];
501
+ const semanticNode = cell ?? node;
502
+ const semanticAttrs = collectAttributesDeep(semanticNode);
503
+ const header = visibleText(node) || `Column ${headers.length + 1}`;
504
+ const style = `${attrs.style || ''};${semanticAttrs.style || ''}`;
505
+ const widthMatch = style.match(/width\s*:\s*([\d.]+)(px|%|rem)/i);
506
+ const binding = attrs['mat-sort-header'];
507
+ const event = eventFromAttrs(semanticAttrs) ?? eventFromAttrs(collectAttributesDeep(node));
508
+ headers.push(compactObject({
509
+ id: uniqueColumnId(binding || header, headers),
510
+ header: clamp(header, 128),
511
+ data_binding: binding || interpolationBinding(semanticNode) || undefined,
512
+ sortable: attrs['mat-sort-header'] !== undefined || undefined,
513
+ cell_kind: inferColumnKind(semanticNode, header),
514
+ icon: inferIcon(semanticNode),
515
+ action: event?.handler ? normalizeAction(event.handler) : undefined,
516
+ sticky: /position\s*:\s*sticky/i.test(style) || undefined,
517
+ align: /text-align\s*:\s*center/i.test(style) ? 'center' : /text-align\s*:\s*right/i.test(style) ? 'end' : undefined,
518
+ visible_when: attrs['*ngif'] || semanticAttrs['*ngif'] || undefined,
519
+ width: widthMatch ? { value: Number(widthMatch[1]), unit: widthMatch[2] } : undefined,
520
+ }));
521
+ });
522
+ return headers.length > 0 ? headers : [{ id: 'column_1', header: 'Column 1' }];
523
+ }
524
+
525
+ function firstTableDataCells(tableNode) {
526
+ let cells = [];
527
+ walkHtml(tableNode, (node) => {
528
+ if (cells.length > 0 || node.tagName !== 'tr') return;
529
+ const rowCells = (node.childNodes ?? []).filter((child) => child.tagName === 'td');
530
+ if (rowCells.length > 0) cells = rowCells;
531
+ });
532
+ return cells;
533
+ }
534
+
535
+ function interpolationBinding(node) {
536
+ let source = '';
537
+ walkHtml(node, (child) => {
538
+ if (child.nodeName === '#text') source += ` ${child.value || ''}`;
539
+ });
540
+ return stripExpression(source.match(/\{\{\s*([^{}|]+?)(?:\|[^{}]+)?\s*\}\}/)?.[1]);
541
+ }
542
+
543
+ function importedAcceptance(rootId, tableIds) {
544
+ return [
545
+ { id: 'acc_import_layout', type: 'layout', statement: 'Imported groups preserve their source order and hierarchy.', target: rootId, priority: 'must', verification_method: 'manual_visual' },
546
+ { id: 'acc_import_interactions', type: 'interaction', statement: 'Imported event bindings remain declared as interactions.', target: '*', priority: 'must', verification_method: 'manual_ia_review' },
547
+ { id: 'acc_import_responsive', type: 'responsive', statement: 'The imported screen remains readable at desktop, tablet, and mobile widths.', target: 'desktop,tablet,mobile', priority: 'must', verification_method: 'screenshot_diff' },
548
+ { id: 'acc_import_a11y', type: 'a11y', statement: 'Imported form and action controls have accessible labels.', target: '*', priority: 'must', verification_method: 'axe_audit' },
549
+ { id: 'acc_import_content', type: 'content', statement: 'Visible source labels and table headers are represented in the Blueprint.', target: '*', priority: 'must', verification_method: 'code_diff' },
550
+ { id: 'acc_import_tables', type: 'layout', statement: 'Wide imported tables remain horizontally scrollable.', target: tableIds.join(',') || rootId, priority: 'should', verification_method: 'computed_style' },
551
+ ];
552
+ }
553
+
554
+ function bindingsFromAttrs(attrs) {
555
+ return compactObject({
556
+ value: stripExpression(attrs.formcontrolname || attrs['[(ngmodel)]'] || attrs['[value]']),
557
+ options: stripExpression(attrs['*ngfor']),
558
+ visibility: stripExpression(attrs['*ngif'] || attrs['[hidden]']),
559
+ enabled: attrs['[disabled]'] ? `!(${stripExpression(attrs['[disabled]'])})` : undefined,
560
+ repeat: stripExpression(attrs['*ngfor']),
561
+ selected: stripExpression(attrs['[checked]'] || attrs['[(ngmodel)]']),
562
+ });
563
+ }
564
+
565
+ function initialStateFromAttrs(attrs, classes) {
566
+ const hidden = attrs.hidden !== undefined || attrs['[hidden]'] === 'true' || /display\s*:\s*none/i.test(attrs.style || '');
567
+ const collapse = classes.has('collapse');
568
+ return compactObject({
569
+ visibility: hidden ? 'hidden' : undefined,
570
+ expanded: collapse ? classes.has('show') : undefined,
571
+ });
572
+ }
573
+
574
+ function validationFromAttrs(attrs) {
575
+ return compactObject({
576
+ required: attrs.required !== undefined || undefined,
577
+ pattern: attrs.pattern,
578
+ min_length: numeric(attrs.minlength),
579
+ max_length: numeric(attrs.maxlength),
580
+ min: numeric(attrs.min),
581
+ max: numeric(attrs.max),
582
+ });
583
+ }
584
+
585
+ function eventFromAttrs(attrs) {
586
+ for (const [name, value] of Object.entries(attrs)) {
587
+ const match = name.match(/^\(([^)]+)\)$/);
588
+ if (!match || !value) continue;
589
+ return { trigger: ANGULAR_EVENT_TO_TRIGGER[match[1].toLowerCase()] ?? 'click', handler: value };
590
+ }
591
+ return null;
592
+ }
593
+
594
+ function attributes(node) {
595
+ return Object.fromEntries((node.attrs ?? []).map((attr) => [attr.name.toLowerCase(), attr.value]));
596
+ }
597
+
598
+ function collectAttributesDeep(node) {
599
+ const result = { ...attributes(node) };
600
+ walkHtml(node, (child) => Object.assign(result, attributes(child)));
601
+ return result;
602
+ }
603
+
604
+ function walkHtml(node, visitor) {
605
+ visitor(node);
606
+ for (const child of node.childNodes ?? []) walkHtml(child, visitor);
607
+ }
608
+
609
+ function visibleText(node) {
610
+ const parts = [];
611
+ walkHtml(node, (child) => {
612
+ if (child.nodeName === '#text') parts.push(child.value || '');
613
+ });
614
+ return clamp(parts.join(' ').replace(/\{\{[\s\S]*?\}\}/g, ' ').replace(/\s+/g, ' ').trim(), 160);
615
+ }
616
+
617
+ function nearestLabel(node) {
618
+ let current = node;
619
+ for (let depth = 0; current && depth < 5; depth += 1, current = current.parentNode) {
620
+ const direct = (current.childNodes ?? []).find((child) => child.tagName === 'label');
621
+ if (direct) return visibleText(direct);
622
+ let found = '';
623
+ walkHtml(current, (child) => {
624
+ if (!found && child.tagName === 'label') found = visibleText(child);
625
+ });
626
+ if (found) return found;
627
+ }
628
+ return '';
629
+ }
630
+
631
+ function textHeading(node) {
632
+ let found = '';
633
+ walkHtml(node, (child) => {
634
+ if (!found && ['h1', 'h2', 'h3', 'h4', 'legend'].includes(child.tagName)) found = visibleText(child);
635
+ });
636
+ return found;
637
+ }
638
+
639
+ function firstOptionText(node) {
640
+ let result = '';
641
+ walkHtml(node, (child) => {
642
+ if (!result && child.tagName === 'option') result = visibleText(child);
643
+ });
644
+ return result;
645
+ }
646
+
647
+ function findAttributeDeep(node, name) {
648
+ let result;
649
+ walkHtml(node, (child) => {
650
+ result ??= attributes(child)[name];
651
+ });
652
+ return result;
653
+ }
654
+
655
+ function repeatExpression(node) {
656
+ return stripExpression(findAttributeDeep(node, '*ngfor'));
657
+ }
658
+
659
+ function inferColumnKind(node, header) {
660
+ const html = serializeNodeHint(node).toLowerCase();
661
+ if (/checkbox/.test(html)) return 'checkbox';
662
+ if (/glyphicon|fa\s/.test(html)) return /click/.test(html) ? 'action' : 'icon';
663
+ if (/<a\b/.test(html)) return 'link';
664
+ if (/date|日期|生日/.test(header)) return 'date';
665
+ if (/amt|amount|balance|餘額|金額|點數|次數|月數/.test(`${header} ${html}`)) return 'number';
666
+ return 'text';
667
+ }
668
+
669
+ function inferIcon(node) {
670
+ let icon;
671
+ walkHtml(node, (child) => {
672
+ const className = attributes(child).class || '';
673
+ const match = className.match(/(?:glyphicon|fa)-([a-z0-9-]+)/);
674
+ if (!icon && match) icon = match[1];
675
+ });
676
+ return icon;
677
+ }
678
+
679
+ function serializeNodeHint(node) {
680
+ const attrs = (node.attrs ?? []).map((attr) => `${attr.name}="${attr.value}"`).join(' ');
681
+ return `<${node.tagName || node.nodeName} ${attrs}>`;
682
+ }
683
+
684
+ function selectorFor(tag, attrs, classes) {
685
+ if (attrs.id) return `#${attrs.id}`;
686
+ if (classes.size) return `${tag}.${[...classes].slice(0, 2).join('.')}`;
687
+ return tag;
688
+ }
689
+
690
+ function customType(tag) {
691
+ if (/button/.test(tag)) return 'button';
692
+ if (/date/.test(tag)) return 'date_picker';
693
+ if (/table|grid/.test(tag)) return 'data_table';
694
+ return 'section';
695
+ }
696
+
697
+ function isKnownCustomTag(tag) {
698
+ return ['sfap-datepicker', 'app-auth-button'].includes(tag);
699
+ }
700
+
701
+ function selectEntry(components, requested) {
702
+ if (!requested) return components[0];
703
+ const entry = components.find((component) => (
704
+ component.selector === requested
705
+ || component.templatePath === sanitizeSourcePath(requested)
706
+ || component.tsPath === sanitizeSourcePath(requested)
707
+ ));
708
+ if (!entry) throw new Error(`Angular entry component not found: ${requested}`);
709
+ return entry;
710
+ }
711
+
712
+ function inferScreenName(html, fallback) {
713
+ const fragment = parseFragment(html);
714
+ let result = '';
715
+ walkHtml(fragment, (node) => {
716
+ const classes = new Set((attributes(node).class || '').split(/\s+/));
717
+ if (!result && classes.has('txn-title')) result = visibleText(node);
718
+ });
719
+ return clamp(result || humanize(fallback), 128);
720
+ }
721
+
722
+ function summarizeDiagnostics(diagnostics, nodeCount) {
723
+ const counts = { high: 0, medium: 0, low: 0 };
724
+ for (const item of diagnostics) {
725
+ if (item.confidence >= 0.8) counts.high += 1;
726
+ else if (item.confidence >= 0.5) counts.medium += 1;
727
+ else counts.low += 1;
728
+ }
729
+ const penalty = diagnostics.reduce((total, item) => total + (item.severity === 'error' ? 0.2 : item.severity === 'warning' ? 0.05 : 0.01), 0);
730
+ return {
731
+ score: Math.max(0, Math.min(1, 1 - penalty / Math.max(1, nodeCount))),
732
+ nodeCount,
733
+ diagnosticCount: diagnostics.length,
734
+ ...counts,
735
+ };
736
+ }
737
+
738
+ function diagnostic(severity, code, message, file, line, nodeId, confidence = 0.5, detail) {
739
+ return compactObject({
740
+ severity,
741
+ code,
742
+ message,
743
+ file: sanitizeSourcePath(file),
744
+ line,
745
+ node_id: nodeId || undefined,
746
+ confidence,
747
+ detail,
748
+ });
749
+ }
750
+
751
+ function assignTokens(target, prefix, values) {
752
+ let index = 1;
753
+ for (const value of values) {
754
+ if (Object.values(target).includes(value)) continue;
755
+ target[`${prefix}.${index}`] = value;
756
+ index += 1;
757
+ }
758
+ }
759
+
760
+ function matchStringProperty(source, property) {
761
+ return source.match(new RegExp(`${property}\\s*:\\s*['"\`]([^'"\`]+)['"\`]`))?.[1] ?? null;
762
+ }
763
+
764
+ function readClassName(source) {
765
+ return source.match(/\bexport\s+(?:default\s+)?class\s+([A-Za-z_$][\w$]*)\b/)?.[1]
766
+ ?? source.match(/\bclass\s+([A-Za-z_$][\w$]*)\b/)?.[1]
767
+ ?? null;
768
+ }
769
+
770
+ function findGroupObjectBlocks(source) {
771
+ const blocks = [];
772
+ const pattern = /\b(?:[A-Za-z_$][\w$]*\.)*group\s*\(/g;
773
+ let match;
774
+ while ((match = pattern.exec(source))) {
775
+ const openParen = source.indexOf('(', match.index);
776
+ const firstArgument = nextNonWhitespace(source, openParen + 1);
777
+ if (source[firstArgument] !== '{') continue;
778
+ const closeBrace = findMatchingDelimiter(source, firstArgument, '{', '}');
779
+ if (closeBrace === -1) continue;
780
+ blocks.push(source.slice(firstArgument + 1, closeBrace));
781
+ pattern.lastIndex = closeBrace + 1;
782
+ }
783
+ return blocks;
784
+ }
785
+
786
+ function splitObjectProperties(source) {
787
+ const properties = [];
788
+ let index = 0;
789
+ while (index < source.length) {
790
+ index = nextNonWhitespace(source, index);
791
+ if (index >= source.length) break;
792
+ const key = readObjectKey(source, index);
793
+ if (!key) break;
794
+ let cursor = nextNonWhitespace(source, key.end);
795
+ if (source[cursor] !== ':') {
796
+ index = nextPropertyBoundary(source, cursor) + 1;
797
+ continue;
798
+ }
799
+ cursor = nextNonWhitespace(source, cursor + 1);
800
+ const valueEnd = nextPropertyBoundary(source, cursor);
801
+ properties.push({
802
+ name: key.name,
803
+ value: source.slice(cursor, valueEnd).trim(),
804
+ });
805
+ index = valueEnd + 1;
806
+ }
807
+ return properties;
808
+ }
809
+
810
+ function readObjectKey(source, index) {
811
+ const quote = source[index];
812
+ if (quote === '\'' || quote === '"' || quote === '`') {
813
+ const end = readQuotedEnd(source, index);
814
+ if (end === -1) return null;
815
+ return { name: source.slice(index + 1, end), end: end + 1 };
816
+ }
817
+ const match = source.slice(index).match(/^([A-Za-z_$][\w$-]*)/);
818
+ return match ? { name: match[1], end: index + match[0].length } : null;
819
+ }
820
+
821
+ function nextPropertyBoundary(source, index) {
822
+ let cursor = index;
823
+ let depth = 0;
824
+ while (cursor < source.length) {
825
+ const char = source[cursor];
826
+ if (char === '\'' || char === '"' || char === '`') {
827
+ const end = readQuotedEnd(source, cursor);
828
+ cursor = end === -1 ? source.length : end + 1;
829
+ continue;
830
+ }
831
+ if (source.startsWith('//', cursor)) {
832
+ const end = source.indexOf('\n', cursor + 2);
833
+ cursor = end === -1 ? source.length : end + 1;
834
+ continue;
835
+ }
836
+ if (source.startsWith('/*', cursor)) {
837
+ const end = source.indexOf('*/', cursor + 2);
838
+ cursor = end === -1 ? source.length : end + 2;
839
+ continue;
840
+ }
841
+ if ('([{'.includes(char)) depth += 1;
842
+ else if (')]}'.includes(char)) depth = Math.max(0, depth - 1);
843
+ else if (char === ',' && depth === 0) return cursor;
844
+ cursor += 1;
845
+ }
846
+ return source.length;
847
+ }
848
+
849
+ function findMatchingDelimiter(source, openIndex, open, close) {
850
+ let depth = 0;
851
+ for (let index = openIndex; index < source.length; index += 1) {
852
+ const char = source[index];
853
+ if (char === '\'' || char === '"' || char === '`') {
854
+ const end = readQuotedEnd(source, index);
855
+ index = end === -1 ? source.length : end;
856
+ continue;
857
+ }
858
+ if (source.startsWith('//', index)) {
859
+ const end = source.indexOf('\n', index + 2);
860
+ index = end === -1 ? source.length : end;
861
+ continue;
862
+ }
863
+ if (source.startsWith('/*', index)) {
864
+ const end = source.indexOf('*/', index + 2);
865
+ index = end === -1 ? source.length : end + 1;
866
+ continue;
867
+ }
868
+ if (char === open) depth += 1;
869
+ if (char === close) {
870
+ depth -= 1;
871
+ if (depth === 0) return index;
872
+ }
873
+ }
874
+ return -1;
875
+ }
876
+
877
+ function readQuotedEnd(source, start) {
878
+ const quote = source[start];
879
+ for (let index = start + 1; index < source.length; index += 1) {
880
+ if (source[index] === '\\') {
881
+ index += 1;
882
+ continue;
883
+ }
884
+ if (source[index] === quote) return index;
885
+ }
886
+ return -1;
887
+ }
888
+
889
+ function nextNonWhitespace(source, index) {
890
+ let cursor = index;
891
+ while (cursor < source.length && /\s/.test(source[cursor])) cursor += 1;
892
+ return cursor;
893
+ }
894
+
895
+ function resolveRelativeSource(fromPath, relativePath) {
896
+ const base = fromPath.split('/').slice(0, -1);
897
+ for (const part of relativePath.replace(/\\/g, '/').split('/')) {
898
+ if (!part || part === '.') continue;
899
+ if (part === '..') base.pop();
900
+ else base.push(part);
901
+ }
902
+ return sanitizeSourcePath(base.join('/'));
903
+ }
904
+
905
+ function sanitizeSourcePath(path) {
906
+ const parts = String(path).replace(/\\/g, '/').split('/');
907
+ const safe = [];
908
+ for (const part of parts) {
909
+ if (!part || part === '.') continue;
910
+ if (part === '..') safe.pop();
911
+ else safe.push(part);
912
+ }
913
+ return safe.join('/');
914
+ }
915
+
916
+ function normalizeAction(action) {
917
+ const value = String(action || 'action').trim();
918
+ if (/^[a-z][a-z0-9_-]*:/.test(value)) return value;
919
+ const call = value.match(/^([a-zA-Z_$][\w$]*)\s*\(([\s\S]*)\)$/);
920
+ return call ? `invoke:${call[1]}` : `invoke:${slug(value) || 'action'}`;
921
+ }
922
+
923
+ function stripExpression(value) {
924
+ if (value == null) return undefined;
925
+ return String(value).trim().replace(/^{{\s*|\s*}}$/g, '') || undefined;
926
+ }
927
+
928
+ function humanize(value) {
929
+ return String(value || '')
930
+ .replace(/^app-|^sfap-/, '')
931
+ .replace(/\.component\.(html|ts)$/i, '')
932
+ .replace(/[-_.]+/g, ' ')
933
+ .replace(/\b\w/g, (character) => character.toUpperCase())
934
+ .trim();
935
+ }
936
+
937
+ function slug(value) {
938
+ const ascii = String(value || '')
939
+ .normalize('NFKD')
940
+ .replace(/[\u0300-\u036f]/g, '')
941
+ .toLowerCase()
942
+ .replace(/[^a-z0-9._-]+/g, '-')
943
+ .replace(/^-+|-+$/g, '');
944
+ return ascii || 'imported-screen';
945
+ }
946
+
947
+ function uniqueId(hint, ids) {
948
+ const base = slug(hint).replace(/\./g, '_').slice(0, 96) || 'node';
949
+ const count = (ids.get(base) ?? 0) + 1;
950
+ ids.set(base, count);
951
+ return count === 1 ? base : `${base}_${count}`;
952
+ }
953
+
954
+ function uniqueColumnId(hint, columns) {
955
+ const base = slug(hint).replace(/\./g, '_').slice(0, 96) || `column_${columns.length + 1}`;
956
+ let id = base;
957
+ let index = 2;
958
+ while (columns.some((column) => column.id === id)) id = `${base}_${index++}`;
959
+ return id;
960
+ }
961
+
962
+ function rowLayout() {
963
+ return { mode: 'auto', display: 'flex', direction: 'row', wrap: true, align: 'center', gap: { x: 12, y: 12 } };
964
+ }
965
+
966
+ function columnLayout() {
967
+ return { mode: 'auto', display: 'flex', direction: 'column', align: 'stretch', gap: { x: 12, y: 12 } };
968
+ }
969
+
970
+ function compactObject(input) {
971
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== ''));
972
+ }
973
+
974
+ function numeric(value) {
975
+ if (value == null || value === '') return undefined;
976
+ const number = Number(value);
977
+ return Number.isFinite(number) ? number : undefined;
978
+ }
979
+
980
+ function clamp(value, max) {
981
+ return String(value || '').slice(0, max);
982
+ }