@tinkerlandai/indiana-iam 0.2.0 → 0.4.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 (55) hide show
  1. package/README.md +127 -5
  2. package/assets/document-controller.js +18 -1
  3. package/assets/document-runtime.js +189 -1
  4. package/assets/viewer.js +74 -7
  5. package/package.json +6 -13
  6. package/src/analyze.mjs +897 -123
  7. package/src/anchors.mjs +5 -0
  8. package/src/build-options.d.ts +6 -1
  9. package/src/build-runtime.mjs +4 -1
  10. package/src/build-styles.mjs +49 -6
  11. package/src/build.mjs +13 -7
  12. package/src/cli.mjs +10 -4
  13. package/src/component-registry.mjs +229 -0
  14. package/src/dependencies.mjs +2 -2
  15. package/src/dom-payload-browser.mjs +28 -0
  16. package/src/dom-payload-json.mjs +12 -0
  17. package/src/dom-payload.mjs +50 -0
  18. package/src/framework-routes.mjs +44 -89
  19. package/src/generate.mjs +20 -7
  20. package/src/graph-sources.mjs +54 -0
  21. package/src/index.d.ts +13 -1
  22. package/src/index.mjs +1 -1
  23. package/src/integrity.mjs +4 -2
  24. package/src/library-entry.mjs +5 -0
  25. package/src/lucide-source.mjs +18 -0
  26. package/src/next-loader.cjs +7 -1
  27. package/src/next.cjs +5 -0
  28. package/src/next.mjs +4 -3
  29. package/src/node-hashes.mjs +13 -9
  30. package/src/render-index.mjs +6 -4
  31. package/src/route-lazy.mjs +80 -0
  32. package/src/route-tree.mjs +15 -3
  33. package/src/routing.mjs +1 -1
  34. package/src/schema-definition.mjs +8 -1
  35. package/src/source-attributes.mjs +24 -0
  36. package/src/source-charts.mjs +38 -0
  37. package/src/source-collections.mjs +241 -0
  38. package/src/source-entry.mjs +50 -0
  39. package/src/source-environment.mjs +26 -0
  40. package/src/source-media.mjs +21 -0
  41. package/src/source-query-evidence.mjs +64 -0
  42. package/src/source-redirects.mjs +388 -0
  43. package/src/source-resolution.mjs +268 -0
  44. package/src/static-primitives.mjs +48 -0
  45. package/src/static-query.mjs +39 -11
  46. package/src/tab-scenes.mjs +214 -0
  47. package/src/tanstack-routes.mjs +143 -0
  48. package/src/transform.mjs +2 -2
  49. package/src/ui-preview.mjs +225 -0
  50. package/src/ui-state-machine.mjs +76 -0
  51. package/src/ui-structure.mjs +645 -0
  52. package/src/upload.mjs +28 -6
  53. package/src/validate.mjs +19 -4
  54. package/src/vite.d.ts +4 -1
  55. package/src/vite.mjs +25 -10
package/src/analyze.mjs CHANGED
@@ -1,13 +1,25 @@
1
1
  import fs from "node:fs/promises";
2
+ import {referencedRasterSources} from "./source-media.mjs";
3
+ import {uiEnvironmentPrimitive} from "./source-environment.mjs";
4
+ import {sourceChart} from "./source-charts.mjs";
2
5
  import path from "node:path";
3
- import { automaticSourceAnchors, anchorAttributes } from "./anchors.mjs";
6
+ import { automaticSourceAnchors, anchorAttributes, sourceScriptKind } from "./anchors.mjs";
4
7
  import { sourceFiles } from "./source-files.mjs";
5
8
  import { dependency } from "./dependencies.mjs";
6
9
  import { assembleRouteTree, readVariables } from "./route-tree.mjs";
10
+ import { sourceEntry } from "./source-entry.mjs";
7
11
  import { frameworkRoutes, externalImport } from "./framework-routes.mjs";
8
- import { routeIndexDigest, refreshRenderHashes } from "./render-index.mjs";
12
+ import { sourceResolution } from "./source-resolution.mjs";
13
+ import { routeIndexDigest, refreshRenderHashes, flattenComposition } from "./render-index.mjs";
9
14
  import { storeDocumentNodes } from "./node-hashes.mjs";
10
15
  import { staticQuery } from "./static-query.mjs";
16
+ import { attachUiStructure } from './ui-structure.mjs';
17
+ import { componentRegistry } from './component-registry.mjs';
18
+ import { sourceRedirects } from './source-redirects.mjs';
19
+ import {internGraphSources} from './graph-sources.mjs';
20
+ import {collectionStateReads,sourceCollectionFragment} from './source-collections.mjs';
21
+ import {sourceQueryEvidence} from './source-query-evidence.mjs';
22
+ import {lucideSource,lucideGeometry,LUCIDE_SOURCE,LUCIDE_SOURCE_VERSION} from './lucide-source.mjs';
11
23
  import {
12
24
  publicRoute,
13
25
  assertRoutes,
@@ -27,6 +39,27 @@ const { JSDOM } = dependency("jsdom").value;
27
39
  const UNKNOWN = Symbol("unknown");
28
40
  import { sensitiveName, redactLiteral, privateUrl } from "./privacy.mjs";
29
41
  const isUnknown = (value) => value === UNKNOWN;
42
+ const COMPONENT=Symbol('source-component');
43
+ const TRANSLATION=Symbol('source-translation-catalog');
44
+ const UI_SETTER=Symbol('source-ui-setter');
45
+ const UI_PRIMITIVE=Symbol('source-ui-primitive');
46
+
47
+ function uiReturnExpression(fn) {
48
+ if(!fn?.body||!ts.isBlock(fn.body))return returnExpression(fn);
49
+ const returned=statement=>ts.isReturnStatement(statement)?statement.expression:ts.isBlock(statement)&&statement.statements.every(s=>ts.isVariableStatement(s)||ts.isReturnStatement(s))?returned(statement.statements.at(-1)):null;
50
+ function sequence(statements) {
51
+ for(let i=0;i<statements.length;i++) {
52
+ const statement=statements[i];
53
+ if(ts.isReturnStatement(statement))return statement.expression;
54
+ if(ts.isIfStatement(statement)) {
55
+ const yes=returned(statement.thenStatement),no=statement.elseStatement?returned(statement.elseStatement):sequence(statements.slice(i+1));
56
+ if(yes&&no)return {kind:ts.SyntaxKind.ConditionalExpression,condition:statement.expression,whenTrue:yes,whenFalse:no};
57
+ }
58
+ }
59
+ return returnExpression(fn);
60
+ }
61
+ return sequence([...fn.body.statements]);
62
+ }
30
63
  // React numeric styles use px except for unitless properties and CSS variables.
31
64
  const unitlessStyles = new Set("animationIterationCount aspectRatio borderImageOutset borderImageSlice borderImageWidth boxFlex boxFlexGroup boxOrdinalGroup columnCount columns flex flexGrow flexPositive flexShrink flexNegative flexOrder gridArea gridRow gridRowEnd gridRowSpan gridRowStart gridColumn gridColumnEnd gridColumnSpan gridColumnStart fontWeight lineClamp lineHeight opacity order orphans scale tabSize widows zIndex zoom fillOpacity floodOpacity stopOpacity strokeDasharray strokeDashoffset strokeMiterlimit strokeOpacity strokeWidth".split(" "));
32
65
  function staticStyle(value, unsupported) {
@@ -140,7 +173,8 @@ function functionFrom(node) {
140
173
  if (
141
174
  ts.isFunctionDeclaration(node) ||
142
175
  ts.isArrowFunction(node) ||
143
- ts.isFunctionExpression(node)
176
+ ts.isFunctionExpression(node) ||
177
+ ts.isMethodDeclaration(node)
144
178
  )
145
179
  return node;
146
180
  if (ts.isCallExpression(node))
@@ -156,6 +190,7 @@ export async function analyze({
156
190
  maxFiles = 5000,
157
191
  maxBytes = 100_000_000,
158
192
  navigationPaths = [],
193
+ entry,
159
194
  automaticAnchors = false,
160
195
  }) {
161
196
  root = await fs.realpath(root);
@@ -167,6 +202,8 @@ export async function analyze({
167
202
  components = new Map(),
168
203
  assets = new Map();
169
204
  const cssSources = [];
205
+ const jsonResources=new Map(),translationResources=new Set();
206
+ const presentation={conditions:[],tabs:[],navigation:[],regions:[],collections:[],fragments:[],attributes:[],attributeSets:[]},collectionTemplates=new Map();
170
207
  const ui = { variables: [], bindings: [], transitions: [], uncovered: [] };
171
208
  const uiVariableKeys = new Set();
172
209
  let byteCount = 0;
@@ -196,6 +233,7 @@ export async function analyze({
196
233
  sha256: sha256(bytes),
197
234
  bytes: bytes.length,
198
235
  });
236
+ if(file.endsWith('.json')){try{jsonResources.set(file,JSON.parse(text));}catch{/* Non-resource JSON stays opaque. */}continue;}
199
237
  if (file.endsWith(".css")) {
200
238
  gap(
201
239
  file.endsWith(".module.css")
@@ -207,13 +245,17 @@ export async function analyze({
207
245
  if (!file.endsWith(".module.css")) cssSources.push({ file, text });
208
246
  continue;
209
247
  }
210
- if (!/\.[cm]?[jt]sx?$/.test(file)) continue;
248
+ if (!/\.[cm]?[jt]sx?$/.test(file)) {
249
+ if (/^(?:src\/)?app\/(?:.*\/)?(?:page|layout)\.mdx$/.test(file) || /^(?:src\/)?pages\/.+\.mdx$/.test(file))
250
+ gap("ROUTE_MDX_UNSUPPORTED", file, "MDX route/layout source requires a dedicated adapter; its page and layout coverage is incomplete");
251
+ continue;
252
+ }
211
253
  const source = ts.createSourceFile(
212
254
  file,
213
255
  text,
214
256
  ts.ScriptTarget.Latest,
215
257
  true,
216
- /\.jsx?$/.test(file) ? ts.ScriptKind.JSX : ts.ScriptKind.TSX,
258
+ sourceScriptKind(file, ts),
217
259
  );
218
260
  const module = {
219
261
  file,
@@ -250,6 +292,7 @@ export async function analyze({
250
292
  }
251
293
  if (ts.isFunctionDeclaration(statement) && (statement.name||statement.modifiers?.some(m=>m.kind===ts.SyntaxKind.DefaultKeyword)))
252
294
  module.bindings.set(statement.name?.text||'$default', statement);
295
+ if (ts.isEnumDeclaration(statement)) module.bindings.set(statement.name.text, statement);
253
296
  if (ts.isVariableStatement(statement))
254
297
  for (const d of statement.declarationList.declarations)
255
298
  if (ts.isIdentifier(d.name) && d.initializer)
@@ -333,6 +376,52 @@ export async function analyze({
333
376
  .find(Boolean) || null
334
377
  );
335
378
  }
379
+ function jsonResource(from,specifier){
380
+ let file=specifier.startsWith('.')?path.posix.normalize(path.posix.join(path.posix.dirname(from),specifier)):null;
381
+ if(!file)for(const [pattern,values] of Object.entries(alias)){const prefix=pattern.split('*')[0];if(pattern.includes('*')&&specifier.startsWith(prefix)){file=values[0].replace('*',specifier.slice(prefix.length)).replace(/^\.\//,'');break;}}
382
+ return file&&!file.startsWith('../')?jsonResources.get(file):undefined;
383
+ }
384
+ function contextKey(module,name){
385
+ const ref=binding(module,name),call=ref&&unwrap(ref.node);
386
+ const primitive=call&&ts.isCallExpression(call)&&externalImport(ref.module,call.expression.getText(ref.module.source));
387
+ return primitive?.specifier==='react'&&primitive.name==='createContext'?ref.module.file+'#'+ref.name:null;
388
+ }
389
+ function sourceContextCall(node,module,ctx){
390
+ const imported=externalImport(module,node.expression.getText(module.source));
391
+ if(imported?.specifier==='react'&&imported.name==='useContext'&&ts.isIdentifier(node.arguments[0]))return ctx.__iamContexts?.get(contextKey(module,node.arguments[0].text))??UNKNOWN;
392
+ const ref=ts.isIdentifier(node.expression)&&binding(module,node.expression.text),fn=ref&&functionFrom(ref.node);
393
+ if(!fn?.body||!ts.isBlock(fn.body))return UNKNOWN;
394
+ const result=unwrap(returnExpression(fn));
395
+ if(!result||!ts.isIdentifier(result))return UNKNOWN;
396
+ for(const statement of fn.body.statements)if(ts.isVariableStatement(statement))for(const decl of statement.declarationList.declarations){
397
+ const call=unwrap(decl.initializer),primitive=call&&ts.isCallExpression(call)&&externalImport(ref.module,call.expression.getText(ref.module.source));
398
+ if(primitive?.specifier==='react'&&primitive.name==='useContext'&&ts.isIdentifier(decl.name)&&decl.name.text===result.text&&ts.isIdentifier(call.arguments[0]))return ctx.__iamContexts?.get(contextKey(ref.module,call.arguments[0].text))??UNKNOWN;
399
+ if(primitive?.specifier==='react-i18next'&&primitive.name==='useTranslation'&&ts.isObjectBindingPattern(decl.name)&&decl.name.elements.some(e=>(e.propertyName?.text||e.name.text)==='t'&&e.name.text===result.text)){
400
+ const catalogs=[...ref.module.imports.values()].filter(i=>i.specifier.endsWith('.json')).map(i=>jsonResource(ref.module.file,i.specifier)).filter(Boolean);
401
+ if(catalogs.length===1){translationResources.add(catalogs[0]);return {[TRANSLATION]:catalogs[0]};}
402
+ }
403
+ }
404
+ return UNKNOWN;
405
+ }
406
+ function basePrimitive(module,tag,seen=new Set()){
407
+ if(seen.has(module.file+'#'+tag)||seen.size>8)return null;seen.add(module.file+'#'+tag);
408
+ const [name,part,extra]=tag.split('.'),ref=module.imports.get(name);
409
+ const imported=externalImport(module,tag);
410
+ const radixFamilies={'dialog':'Dialog','alert-dialog':'AlertDialog','popover':'Popover','dropdown-menu':'Menu','context-menu':'Menu','tooltip':'Tooltip','hover-card':'Popover','tabs':'Tabs','collapsible':'Collapsible'};
411
+ const radix=imported?.specifier?.match(/^@radix-ui\/react-([a-z-]+)$/)?.[1];
412
+ if(radixFamilies[radix]) {
413
+ const family=radixFamilies[radix],prefix=radix.split('-').map(v=>v[0].toUpperCase()+v.slice(1)).join('');
414
+ const exported=imported.name===prefix?'Root':imported.name.startsWith(prefix)?imported.name.slice(prefix.length):imported.name;
415
+ const mapped=family==='Tabs'?({Trigger:'Tab',Content:'Panel'}[exported]??exported):({Content:'Popup',Overlay:'Backdrop',Sub:'SubmenuRoot',SubTrigger:'SubmenuTrigger',SubContent:'Popup'}[exported]??exported);
416
+ return {family,part:mapped};
417
+ }
418
+ if(imported?.specifier==='react-transition-group'&&['Transition','CSSTransition'].includes(imported.name))return {family:'Transition',part:'Root'};
419
+ const families={'@base-ui/react/tabs':'Tabs','@base-ui/react/popover':'Popover','@base-ui/react/dialog':'Dialog','@base-ui/react/alert-dialog':'AlertDialog','@base-ui/react/menu':'Menu','@base-ui/react/tooltip':'Tooltip'};
420
+ if(!extra&&part&&ref&&families[ref.specifier]===ref.name)return {family:ref.name,part};
421
+ if(!part){const target=binding(module,tag),node=target&&unwrap(target.node);if(node&&ts.isPropertyAccessExpression(node))return basePrimitive(target.module,node.getText(target.module.source),seen);}
422
+ return null;
423
+ }
424
+ function tabsPrimitive(module,tag){const primitive=basePrimitive(module,tag);return primitive?.family==='Tabs'&&['Root','List','Tab','Panel','Indicator'].includes(primitive.part)?primitive.part:null;}
336
425
  function binding(module, name, seen=new Set()) {
337
426
  if(!name||seen.size>=16||seen.has(module.file+'#'+name))return null;seen.add(module.file+'#'+name);
338
427
  if (module.bindings.has(name))
@@ -347,11 +436,10 @@ export async function analyze({
347
436
  : target.exports.get(imported.name) || imported.name;
348
437
  return binding(target,key,seen);
349
438
  }
350
- const componentDefs = new Map();
351
- for (const module of modules.values())
352
- for (const [symbol, node] of module.bindings) {
439
+ const componentDefs = new Map(),componentsByFunction=new WeakMap();
440
+ function registerComponent(module, symbol, node) {
353
441
  const fn = functionFrom(node);
354
- if (!fn) continue;
442
+ if (!fn) return null;
355
443
  let hasJsx = false;
356
444
  function visit(n) {
357
445
  if (
@@ -363,7 +451,7 @@ export async function analyze({
363
451
  ts.forEachChild(n, visit);
364
452
  }
365
453
  visit(fn);
366
- if (!hasJsx) continue;
454
+ if (!hasJsx) return null;
367
455
  const key = identity("component", { file: module.file, symbol }),
368
456
  ordinals = new Map();
369
457
  let ordinal = 0;
@@ -373,7 +461,7 @@ export async function analyze({
373
461
  ts.forEachChild(n, number);
374
462
  }
375
463
  number(fn);
376
- const def = { module, symbol, fn, key, ordinals };
464
+ const def = { module, symbol, fn, key, ordinals };componentsByFunction.set(fn,def);
377
465
  let returns = 0;
378
466
  function countReturns(item) {
379
467
  if (
@@ -403,12 +491,21 @@ export async function analyze({
403
491
  source: loc,
404
492
  dependencies: [],
405
493
  });
406
- }
407
- for (const def of componentDefs.values()) {
494
+ return def;
495
+ }
496
+ for (const module of modules.values())
497
+ for (const [symbol, node] of module.bindings) registerComponent(module, symbol, node);
498
+ const resolution = sourceResolution({ ts, resolveModule, resolveBinding: binding });
499
+ const componentBinding = (module, name) => {
500
+ const ref = resolution.component(module, name);
501
+ if (ref?.wrapped) gap("LAZY_WRAPPER_UNOBSERVED", module.file, name + ": only the statically forwarded successful module is projected; retry/reload behavior is not executed");
502
+ return ref;
503
+ };
504
+ function collectComponentDependencies(def) {
408
505
  function collectDependencies(node) {
409
506
  if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
410
507
  const opening = ts.isJsxElement(node) ? node.openingElement : node,
411
- ref = binding(def.module, opening.tagName.getText(def.module.source)),
508
+ ref = componentBinding(def.module, opening.tagName.getText(def.module.source)),
412
509
  target = ref && componentDefs.get(ref.module.file + "#" + ref.name);
413
510
  if (
414
511
  target &&
@@ -420,6 +517,20 @@ export async function analyze({
420
517
  }
421
518
  collectDependencies(def.fn);
422
519
  }
520
+ for (const def of componentDefs.values()) collectComponentDependencies(def);
521
+ const inlineNodes = new WeakMap(), inlineCounts = new Map();
522
+ function inlineComponent(module, node, route) {
523
+ if (inlineNodes.has(node)) return inlineNodes.get(node);
524
+ const group = module.file + "#" + route, occurrence = inlineCounts.get(group) ?? 0;
525
+ inlineCounts.set(group, occurrence + 1);
526
+ const symbol = "$route_" + hashObject("iam/inline-route-component/v1", { route, occurrence }).slice(0, 24);
527
+ module.bindings.set(symbol, node);
528
+ const def = registerComponent(module, symbol, node);
529
+ if (def) collectComponentDependencies(def);
530
+ const target = { file: module.file, symbol };
531
+ inlineNodes.set(node, target);
532
+ return target;
533
+ }
423
534
  function sourceLocation(module, node, symbol) {
424
535
  const loc = module.source.getLineAndCharacterOfPosition(
425
536
  node.getStart(module.source),
@@ -432,6 +543,17 @@ export async function analyze({
432
543
  fileHash: module.hash,
433
544
  };
434
545
  }
546
+ function sourceComponent(node,module){
547
+ const imported=ts.isIdentifier(node)&&externalImport(module,node.text);if(imported?.specifier==='@radix-ui/react-slot'&&['Slot','Root'].includes(imported.name))return {[UI_PRIMITIVE]:imported};if(imported?.specifier==='lucide-react'){const icon=lucideSource(imported.name);if(icon)return icon;}
548
+ const ref=ts.isIdentifier(node)?binding(module,node.text):null;const def=componentsByFunction.get(node)??(ref&&componentDefs.get(ref.module.file+'#'+ref.name));
549
+ return def?{[COMPONENT]:def}:null;
550
+ }
551
+ function environment(node,module,context){
552
+ const value=uiEnvironmentPrimitive({ts,node,module,binding,imported:externalImport,evaluate:evalExpression});
553
+ if(value?.kind==='empty-preference'){gap('UI_PREFERENCE_EMPTY_PREVIEW',module.file,'Source-declared SSR fallback for a UI preference; preview starts without cookies, never reads merchant browser state.');return [undefined];}
554
+ return value;
555
+ }
556
+ function query(node,module,context){return staticQuery({ts,node,module,context,binding,unknown:UNKNOWN,redact:redactLiteral,sensitive:sensitiveName,reference:sourceComponent,isReference:v=>Boolean(v?.[COMPONENT]||v?.[LUCIDE_SOURCE]||v?.[UI_PRIMITIVE]),imported:externalImport,project:(n,m,c)=>{const v=environment(n,m,c);return Array.isArray(v)?v:null;}});}
435
557
  function evalExpression(
436
558
  node,
437
559
  module,
@@ -442,6 +564,7 @@ export async function analyze({
442
564
  node = unwrap(node);
443
565
  if (!node || depth > 30) return UNKNOWN;
444
566
  const ev = (n) => evalExpression(n, module, context, stack, depth + 1);
567
+ const component=!(ts.isIdentifier(node)&&Object.hasOwn(context,node.text))&&sourceComponent(node,module);if(component)return component;
445
568
  if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node))
446
569
  return ts.isNumericLiteral(node) ? Number(node.text) : redactLiteral(node.text);
447
570
  if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
@@ -449,7 +572,14 @@ export async function analyze({
449
572
  if (node.kind === ts.SyntaxKind.NullKeyword) return null;
450
573
  if (ts.isIdentifier(node)) {
451
574
  if (Object.hasOwn(context, node.text)) return context[node.text];
575
+ const variableKey=context.__iamVariableKeys?.[context.__iamSetters?.[node.text]];
576
+ if(variableKey)return {[UI_SETTER]:{variableKey,value:{op:'event',key:'value'},payload:{value:ui.variables.find(v=>v.variableKey===variableKey)?.valueType}}};
452
577
  if (node.text === "undefined") return undefined;
578
+ const image=module.imports.get(node.text);
579
+ if(image?.name==='default'&&/^\.\.?\/[^?#]+\.(?:png|jpe?g|webp|gif)$/i.test(image.specifier)){
580
+ const file=path.posix.normalize(path.posix.join(path.posix.dirname(module.file),image.specifier));
581
+ if(!file.startsWith('../')&&!file.split('/').some(p=>p.startsWith('.'))&&!sensitiveName(file))return '/__iam_source__/'+file;
582
+ }
453
583
  const token = module.file + "#" + node.text;
454
584
  if (stack.has(token)) return UNKNOWN;
455
585
  const ref = binding(module, node.text);
@@ -460,12 +590,16 @@ export async function analyze({
460
590
  if (ts.isArrayLiteralExpression(node)) return node.elements.map(ev);
461
591
  if (ts.isObjectLiteralExpression(node)) {
462
592
  const obj = {};
593
+ const fields=new Map();
463
594
  for (const prop of node.properties) {
464
595
  if (ts.isPropertyAssignment(prop))
465
596
  obj[prop.name.getText(module.source).replace(/^['"]|['"]$/g, "")] =
466
597
  ev(prop.initializer);
467
- else if (ts.isShorthandPropertyAssignment(prop))
598
+ else if (ts.isShorthandPropertyAssignment(prop)){
468
599
  obj[prop.name.text] = ev(prop.name);
600
+ const variable=context.__iamVariableKeys?.[prop.name.text],inherited=context.__iamPropBindings?.get(prop.name.text);
601
+ if(variable||inherited)fields.set(prop.name.text,inherited||{op:'var',key:variable});
602
+ }
469
603
  else if (ts.isSpreadAssignment(prop)) {
470
604
  const v = ev(prop.expression);
471
605
  if (isUnknown(v)) return UNKNOWN;
@@ -473,6 +607,7 @@ export async function analyze({
473
607
  Object.assign(obj, v);
474
608
  }
475
609
  }
610
+ if(fields.size)objectBindings.set(obj,fields);
476
611
  return obj;
477
612
  }
478
613
  if (
@@ -526,6 +661,12 @@ export async function analyze({
526
661
  return isUnknown(a) ? UNKNOWN : (a ?? b);
527
662
  if (isUnknown(a) || isUnknown(b)) return UNKNOWN;
528
663
  if (op === ts.SyntaxKind.PlusToken) return a + b;
664
+ if(op === ts.SyntaxKind.MinusToken && typeof a==='number' && typeof b==='number')return a-b;
665
+ if(['number','string'].includes(typeof a)&&typeof a===typeof b){
666
+ if(op===ts.SyntaxKind.GreaterThanToken)return a>b;if(op===ts.SyntaxKind.GreaterThanEqualsToken)return a>=b;
667
+ if(op===ts.SyntaxKind.LessThanToken)return a<b;if(op===ts.SyntaxKind.LessThanEqualsToken)return a<=b;
668
+ }
669
+ if(op===ts.SyntaxKind.InKeyword&&typeof a==='string'&&b&&typeof b==='object'&&!['__proto__','constructor','prototype'].includes(a))return Object.hasOwn(b,a);
529
670
  if (
530
671
  [
531
672
  ts.SyntaxKind.EqualsEqualsEqualsToken,
@@ -543,9 +684,21 @@ export async function analyze({
543
684
  }
544
685
  if (ts.isCallExpression(node)) {
545
686
  const callee = node.expression.getText(module.source);
687
+ const projected=environment(node,module,context);if(Array.isArray(projected))return projected[0];
688
+ const renderCallback=ev(node.expression)?.iamRenderProp;
689
+ if(renderCallback)return {iamRenderInvocation:{callback:renderCallback,args:node.arguments.map(ev),caller:context}};
690
+ const primitive=externalImport(module,callee);
691
+ if(['react-router','react-router-dom'].includes(primitive?.specifier)&&primitive.name==='useLocation'&&typeof context.__iamRoutePath==='string')return {pathname:context.__iamRoutePath,search:UNKNOWN,hash:UNKNOWN};
692
+ const translation=ts.isIdentifier(node.expression)&&context[node.expression.text]?.[TRANSLATION];
693
+ if(translation){const key=ev(node.arguments[0]);if(typeof key!=='string')return UNKNOWN;const value=key.split('.').reduce((value,key)=>value&&Object.hasOwn(Object(value),key)?value[key]:undefined,translation);return typeof value==='string'&&!/\{\{/.test(value)?redactLiteral(value):UNKNOWN;}
694
+ if(primitive?.specifier==='react'&&primitive.name==='useMemo'&&node.arguments.length===2){
695
+ const fn=node.arguments[0];if(ts.isArrowFunction(fn)&&!fn.modifiers?.length&&!fn.parameters.length&&!ts.isBlock(fn.body))return ev(fn.body);
696
+ }
697
+ const contextValue=sourceContextCall(node,module,context);
698
+ if(!isUnknown(contextValue))return contextValue;
546
699
  if (/(?:^|\.)useState$/.test(callee))
547
700
  return {
548
- stateInitial: node.arguments.length ? ev(node.arguments[0]) : null,
701
+ stateInitial: node.arguments.length ? (ts.isArrowFunction(node.arguments[0])&&node.arguments[0].parameters.length===0 ? query({kind:ts.SyntaxKind.CallExpression,expression:node.arguments[0],arguments:[]},module,context) : ev(node.arguments[0])) : null,
549
702
  };
550
703
  if (callee === "cn" || callee === "clsx") {
551
704
  let valid=true;
@@ -580,6 +733,7 @@ export async function analyze({
580
733
  ([key, values]) =>
581
734
  values[isUnknown((props || {})[key]) ? options.defaultVariants?.[key] : (props || {})[key] ?? options.defaultVariants?.[key]],
582
735
  ),
736
+ ...(options?.compoundVariants??[]).flatMap(row=>Object.entries(row).filter(([key])=>!['class','className'].includes(key)).every(([key,value])=>{const actual=Object.hasOwn(props??{},key)&&props[key]!==undefined?props[key]:options.defaultVariants?.[key];return Array.isArray(value)?value.includes(actual):value===actual;})?[row.class,row.className]:[]),
583
737
  props?.class,
584
738
  props?.className,
585
739
  ]
@@ -590,7 +744,7 @@ export async function analyze({
590
744
  const target = ev(node.expression.expression),
591
745
  args = node.arguments.map(ev),
592
746
  method = node.expression.name.text;
593
- if (isUnknown(target) || args.some(isUnknown)) return staticQuery({ ts, node, module, context, binding, unknown: UNKNOWN, redact: redactLiteral, sensitive: sensitiveName });
747
+ if (isUnknown(target) || args.some(isUnknown)) return query(node,module,context);
594
748
  if (
595
749
  typeof target === "string" &&
596
750
  [
@@ -606,7 +760,7 @@ export async function analyze({
606
760
  return target.filter((v) => !isUnknown(v)).join(args[0]);
607
761
  }
608
762
  }
609
- if (ts.isCallExpression(node)) return staticQuery({ ts, node, module, context, binding, unknown: UNKNOWN, redact: redactLiteral, sensitive: sensitiveName });
763
+ if (ts.isCallExpression(node)) return query(node,module,context);
610
764
  return UNKNOWN;
611
765
  }
612
766
  // Preserve only definitely present class tokens when a template contains an
@@ -662,15 +816,14 @@ export async function analyze({
662
816
  const omitted = new Set(param.name.elements.filter(item => !item.dotDotDotToken).map(item => item.propertyName?.getText() || item.name.getText()));
663
817
  const rest = Object.fromEntries(Object.entries(props).filter(([key]) => !omitted.has(key)));
664
818
  const parent = forwarded.get(props);
665
- if (parent) forwarded.set(rest, { bindings: parent.bindings.filter(b => !omitted.has(b.property)), handlers: parent.handlers.filter(h => !omitted.has(h.key)) });
819
+ if (parent) forwarded.set(rest, { bindings: parent.bindings.filter(b => !omitted.has(b.property)), handlers: parent.handlers.filter(h => !omitted.has(h.key)), attributes:(parent.attributes??[]).filter(a=>!omitted.has(a.key)) });
666
820
  ctx[el.name.text] = rest; continue;
667
821
  }
668
822
  const key = el.propertyName?.getText() || el.name.getText();
669
823
  ctx[el.name.getText()] =
670
- props[key] ??
671
- (el.initializer
672
- ? evalExpression(el.initializer, def.module, ctx)
673
- : UNKNOWN);
824
+ Object.hasOwn(props, key) && props[key] !== undefined ? props[key] :
825
+ (el.initializer ? evalExpression(el.initializer, def.module, ctx) :
826
+ props.__iamOpaqueProps ? UNKNOWN : undefined);
674
827
  const inherited = forwarded.get(props);
675
828
  const binding = inherited?.bindings.find(b => b.property === key), handler = inherited?.handlers.find(h => h.key === key);
676
829
  if (binding) ctx.__iamPropBindings.set(el.name.getText(), binding.value);
@@ -682,8 +835,22 @@ export async function analyze({
682
835
  if (ts.isVariableStatement(s))
683
836
  for (const d of s.declarationList.declarations) {
684
837
  if (ts.isIdentifier(d.name)) {
685
- ctx[d.name.text] = sensitiveName(d.name.text) ? UNKNOWN : evalExpression(d.initializer, def.module, ctx);
838
+ const projected=environment(d.initializer,def.module,ctx);
839
+ if(projected?.kind==='viewport'){
840
+ const width=Number(projected.query.match(/\d+/)[0]);ctx[d.name.text]=projected.query.includes('max-width')?1280<=width:1280>=width;ctx.__iamStates.add(d.name.text);ctx.__iamStateNodes[d.name.text]=d;ctx.__iamViewportQueries??={};ctx.__iamViewportQueries[d.name.text]=projected.query;
841
+ gap('VIEWPORT_SOURCE_PROJECTION',def.module.file,'Source matchMedia query projected against the preview viewport; subscription effects are not executed.');
842
+ }else ctx[d.name.text] = sensitiveName(d.name.text) ? UNKNOWN : evalExpression(d.initializer, def.module, ctx);
686
843
  ctx.__iamLocals.set(d.name.text, d.initializer);
844
+ } else if(ts.isObjectBindingPattern(d.name)){
845
+ const value=evalExpression(d.initializer,def.module,ctx);
846
+ for(const field of d.name.elements)if(ts.isIdentifier(field.name)&&!field.dotDotDotToken){
847
+ const key=field.propertyName?.text||field.name.text;
848
+ ctx[field.name.text]=!isUnknown(value)&&value&&typeof value==='object'&&Object.hasOwn(value,key)?value[key]:field.initializer?evalExpression(field.initializer,def.module,ctx):UNKNOWN;
849
+ const inherited=value&&typeof value==='object'&&objectBindings.get(value)?.get(key);
850
+ if(inherited)ctx.__iamPropBindings.set(field.name.text,inherited);
851
+ const setter=value?.[key]?.[UI_SETTER];
852
+ if(setter)ctx.__iamPropHandlers.set(field.name.text,setter);
853
+ }
687
854
  } else if (ts.isArrayBindingPattern(d.name)) {
688
855
  const call = unwrap(d.initializer), hook = call && ts.isCallExpression(call) && ts.isIdentifier(call.expression) && def.module.imports.get(call.expression.text);
689
856
  if (hook?.specifier === "react" && hook.name === "useActionState") {
@@ -707,11 +874,16 @@ export async function analyze({
707
874
  Object.hasOwn(Object(value), "stateInitial") &&
708
875
  d.name.elements[0]
709
876
  ) {
710
- const key = d.name.elements[0].name?.text;
711
- ctx[key] = sensitiveName(key) ? UNKNOWN : value.stateInitial;
877
+ const stateBinding = d.name.elements[0];
878
+ if (!ts.isBindingElement(stateBinding) || stateBinding.dotDotDotToken || !ts.isIdentifier(stateBinding.name)) {
879
+ gap("STATE_BINDING_UNSUPPORTED", def.module.file, "Omitted, nested or rest state bindings have no named scalar value for UI simulation");
880
+ continue;
881
+ }
882
+ const key = stateBinding.name.text;
883
+ ctx[key] = sensitiveName(key) && typeof value.stateInitial !== 'boolean' ? UNKNOWN : value.stateInitial;
712
884
  ctx.__iamStates.add(key);
713
885
  ctx.__iamStateNodes[key] = d;
714
- if (d.name.elements[1]?.name)
886
+ if (d.name.elements[1]?.name && !d.name.elements[1].dotDotDotToken && ts.isIdentifier(d.name.elements[1].name))
715
887
  ctx.__iamSetters[d.name.elements[1].name.text] = key;
716
888
  }
717
889
  }
@@ -768,7 +940,10 @@ export async function analyze({
768
940
  !/^\/(?:[^/?#[\]\\\s]+\/)*[^/?#[\]\\\s]+$/.test(routing.basePath)
769
941
  )
770
942
  throw new Error("Invalid source basePath");
771
- const compositions = frameworkRoutes({ modules, resolveModule, ts, sourceLocation, navigationPaths, gap });
943
+ const selection = await sourceEntry({ root, modules, resolveModule, ts, entry });
944
+ if (selection.htmlSource) inventory.push(selection.htmlSource);
945
+ inventory.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
946
+ const compositions = frameworkRoutes({ modules, ...selection, resolveModule, ts, sourceLocation, navigationPaths, gap, resolution, inlineComponent });
772
947
  const pageFiles = compositions.pageFiles;
773
948
  routing.framework = compositions.framework;
774
949
  if (compositions.routeSources) routing.routeSources = compositions.routeSources;
@@ -823,19 +998,20 @@ export async function analyze({
823
998
  };
824
999
  const record = new Map(),
825
1000
  used = new Set(),
826
- branches = [];
1001
+ branches = [],attributeRequests=[];
827
1002
  let renderedCount = 0;
828
1003
  function renderComponent(def, props, instance, depth) {
829
1004
  if (depth === 0 && composition.entryMode === "soft") instance = composition.compositionKey + ":" + instance;
830
1005
  // Component recursion and JSX nesting are separate budgets. Counting DOM
831
1006
  // wrappers as component calls silently dropped ordinary nested navigation.
832
- if (used.size >= 18 || used.has(def.key + ":" + instance)) {
1007
+ if (used.size >= 24 || used.has(def.key + ":" + instance)) {
833
1008
  gap("COMPONENT_DEPTH_LIMIT", def.module.file, def.symbol + ": active component call budget exceeded");
834
1009
  return "";
835
1010
  }
836
1011
  used.add(def.key + ":" + instance);
837
1012
  if (!page.componentKeys.includes(def.key))
838
1013
  page.componentKeys.push(def.key);
1014
+ props.__iamRoutePath=/[\[\]]/.test(route)?UNKNOWN:route;
839
1015
  const ctx = componentContext(def, props);
840
1016
  ctx.__iamOutlet = props.__iamOutlet;
841
1017
  ctx.__iamScope = identity("state-scope", {
@@ -860,11 +1036,17 @@ export async function analyze({
860
1036
  }
861
1037
  if (!uiVariableKeys.has(variableKey)) {
862
1038
  uiVariableKeys.add(variableKey);
863
- ui.variables.push({ variableKey, routeKey: pageKey, scopeKey: ctx.__iamScope, label, valueType: typeof initialValue, initialValue, source: sourceLocation(def.module, ctx.__iamStateNodes[label], def.symbol) });
1039
+ ui.variables.push({ variableKey, routeKey: pageKey, scopeKey: ctx.__iamScope, label:ctx.__iamViewportQueries?.[label]?'@media '+ctx.__iamViewportQueries[label]:label, valueType: typeof initialValue, initialValue, source: sourceLocation(def.module, ctx.__iamStateNodes[label], def.symbol) });
864
1040
  }
865
1041
  }
1042
+ // Rebind pure local records after scalar state identities exist. This preserves
1043
+ // useMemo/context field relationships without invoking a hook or callback.
1044
+ for(const [name,expression]of ctx.__iamLocals){
1045
+ if(ctx.__iamStates.has(name)||sensitiveName(name))continue;
1046
+ const value=evalExpression(expression,def.module,ctx);if(!isUnknown(value))ctx[name]=value;
1047
+ }
866
1048
  const result = renderExpr(
867
- returnExpression(def.fn),
1049
+ uiReturnExpression(def.fn),
868
1050
  def,
869
1051
  ctx,
870
1052
  instance,
@@ -899,6 +1081,7 @@ export async function analyze({
899
1081
  }
900
1082
  if (ts.isIdentifier(node)) {
901
1083
  if (Object.hasOwn(aliases, node.text)) return aliases[node.text];
1084
+ if(eventParam&&node.text===eventParam)return {op:'event',key:'value'};
902
1085
  if (ctx.__iamVariableKeys[node.text]) return { op: "var", key: ctx.__iamVariableKeys[node.text] };
903
1086
  if (ctx.__iamPropBindings.has(node.text)) return ctx.__iamPropBindings.get(node.text);
904
1087
  if (ctx.__iamLocals.has(node.text)) return compile(ctx.__iamLocals.get(node.text));
@@ -927,43 +1110,385 @@ export async function analyze({
927
1110
  const value = evalExpression(node, owner.module, ctx);
928
1111
  return ["string", "boolean", "number"].includes(typeof value) ? { op: "const", value } : null;
929
1112
  }
930
- function uiHandler(initializer, owner, ctx) {
1113
+ function uiExpressionType(expression, payload = {}) {
1114
+ if (!expression) return undefined;
1115
+ if (expression.op === "const") return typeof expression.value;
1116
+ if (expression.op === "var") return ui.variables.find(variable => variable.variableKey === expression.key)?.valueType;
1117
+ if (expression.op === "event") return payload[expression.key];
1118
+ const type = child => uiExpressionType(child, payload);
1119
+ if (expression.op === "not") return type(expression.arg) ? "boolean" : undefined;
1120
+ if (expression.op === "classes") return expression.parts.every(part => type(part.when) === "boolean") ? "string" : undefined;
1121
+ if (["all", "any"].includes(expression.op)) return expression.args.every(child => type(child) === "boolean") ? "boolean" : undefined;
1122
+ const left = type(expression.left), right = type(expression.right);
1123
+ if (["eq", "ne"].includes(expression.op)) return left && left === right ? "boolean" : undefined;
1124
+ if (["add", "subtract"].includes(expression.op)) return left === "number" && right === "number" ? "number" : undefined;
1125
+ return undefined;
1126
+ }
1127
+ function uiHandler(initializer, owner, ctx, property = '') {
931
1128
  let fn = initializer && ts.isJsxExpression(initializer) ? unwrap(initializer.expression) : null;
932
- if (fn && ts.isIdentifier(fn) && ctx.__iamPropHandlers.has(fn.text)) return ctx.__iamPropHandlers.get(fn.text);
933
- if (fn && ts.isIdentifier(fn) && ctx.__iamLocals.has(fn.text)) fn = unwrap(ctx.__iamLocals.get(fn.text));
934
- if (fn && ts.isPropertyAccessExpression(fn) && ts.isIdentifier(fn.expression)) {
935
- const props = ctx[fn.expression.text], handler = props && typeof props === "object" && forwarded.get(props)?.handlers.find(h => h.key === fn.name.text);
936
- if (handler) return handler;
1129
+ const eventValue = (variableKey) => ({ variableKey, value: {op:'event',key:'value'}, payload:{value:ui.variables.find(v=>v.variableKey===variableKey)?.valueType} });
1130
+ if(fn&&ts.isIdentifier(fn)&&ctx.__iamPropHandlers.has(fn.text))return ctx.__iamPropHandlers.get(fn.text);
1131
+ if(fn&&ts.isIdentifier(fn)&&ctx.__iamSetters[fn.text]&&['onValueChange','onOpenChange'].includes(property)) {
1132
+ const key=ctx.__iamVariableKeys[ctx.__iamSetters[fn.text]];return key?eventValue(key):null;
1133
+ }
1134
+ if(fn&&ts.isIdentifier(fn)&&ctx.__iamLocals.has(fn.text))fn=unwrap(ctx.__iamLocals.get(fn.text));
1135
+ if(fn&&ts.isPropertyAccessExpression(fn)&&ts.isIdentifier(fn.expression)) {
1136
+ const props=ctx[fn.expression.text],handler=props&&typeof props==='object'&&forwarded.get(props)?.handlers.find(h=>h.key===fn.name.text);
1137
+ if(handler)return handler;
1138
+ }
1139
+ if(!fn||!ts.isArrowFunction(fn)||fn.modifiers?.some(m=>m.kind===ts.SyntaxKind.AsyncKeyword))return null;
1140
+ let statements=ts.isBlock(fn.body)?[...fn.body.statements]:[fn.body];
1141
+ if(statements.length>16)return null;
1142
+ const writes=[],payload={};
1143
+ const inputs=e=>{if(e.op==='event')payload[e.key]=e.key==='checked'?'boolean':'string';for(const c of [e.arg,e.left,e.right,...(e.args||[]),...(e.parts||[]).map(p=>p.when)])if(c)inputs(c);};
1144
+ const replaceEvent=(value,arg)=>value.op==='event'&&value.key==='value'?arg:Object.fromEntries(Object.entries(value).map(([k,v])=>[k,Array.isArray(v)?v.map(item=>item&&typeof item==='object'?replaceEvent(item,arg):item):v&&typeof v==='object'?replaceEvent(v,arg):v]));
1145
+ for(let body of statements) {
1146
+ if(ts.isExpressionStatement(body)||ts.isReturnStatement(body))body=body.expression;
1147
+ body=unwrap(body);
1148
+ if(body&&ts.isCallExpression(body)&&!body.arguments.length&&ts.isPropertyAccessExpression(body.expression)&&ts.isIdentifier(body.expression.expression)&&body.expression.expression.text===fn.parameters[0]?.name?.text&&['preventDefault','stopPropagation'].includes(body.expression.name.text))continue;
1149
+ if(body&&ts.isBinaryExpression(body)&&body.operatorToken.kind===ts.SyntaxKind.AmpersandAmpersandToken) {
1150
+ const guard=unwrap(body.left);
1151
+ if(!ts.isBinaryExpression(guard)||guard.operatorToken.kind!==ts.SyntaxKind.EqualsEqualsEqualsToken||!ts.isTypeOfExpression(guard.left)||!ts.isIdentifier(guard.left.expression)||guard.left.expression.text!==fn.parameters[0]?.name?.text||!ts.isStringLiteral(guard.right)||!['string','number','boolean'].includes(guard.right.text))return null;
1152
+ body=unwrap(body.right);
1153
+ }
1154
+ if(!body||!ts.isCallExpression(body)||!ts.isIdentifier(body.expression)||body.arguments.length!==1)return null;
1155
+ const inherited=ctx.__iamPropHandlers.get(body.expression.text);
1156
+ if(inherited) {
1157
+ const arg=uiExpression(body.arguments[0],owner,ctx,fn.parameters[0]?.name?.text||null);
1158
+ if(!arg)return null;
1159
+ const forwardedWrites=(inherited.writes||[{variableKey:inherited.variableKey,value:inherited.value}]).map(write=>({...write,value:replaceEvent(write.value,arg)}));
1160
+ for(const write of forwardedWrites)inputs(write.value);
1161
+ if(['onValueChange','onOpenChange'].includes(property))for(const [key,type]of Object.entries(inherited.payload))if(Object.hasOwn(payload,key))payload[key]=type;
1162
+ if(forwardedWrites.some(write=>uiExpressionType(write.value,payload)!==ui.variables.find(v=>v.variableKey===write.variableKey)?.valueType))return null;
1163
+ writes.push(...forwardedWrites);continue;
1164
+ }
1165
+ const label=ctx.__iamSetters[body.expression.text],variableKey=ctx.__iamVariableKeys[label];
1166
+ if(!variableKey||writes.some(w=>w.variableKey===variableKey))return null;
1167
+ const input=unwrap(body.arguments[0]),functional=input&&ts.isArrowFunction(input)&&!ts.isBlock(input.body)&&input.parameters.length===1&&ts.isIdentifier(input.parameters[0].name)?input:null;
1168
+ const aliases=functional?{[functional.parameters[0].name.text]:{op:'var',key:variableKey}}:{};
1169
+ const value=uiExpression(functional?functional.body:input,owner,ctx,fn.parameters[0]?.name?.text||null,aliases);
1170
+ if(!value)return null;
1171
+ inputs(value);const expected=ui.variables.find(v=>v.variableKey===variableKey)?.valueType;
1172
+ if(value.op==='event'&&value.key==='value'&&['onValueChange','onOpenChange'].includes(property))payload.value=expected;
1173
+ if(uiExpressionType(value,payload)!==expected)return null;
1174
+ writes.push({variableKey,value});
1175
+ }
1176
+ if(!writes.length||new Set(writes.map(w=>w.variableKey)).size!==writes.length)return null;
1177
+ return {...writes[0],writes,payload};
1178
+ }
1179
+ function queryBacked(node, owner, ctx) {
1180
+ return sourceQueryEvidence({
1181
+ ts,
1182
+ node,
1183
+ module: owner.module,
1184
+ locals: ctx.__iamLocals,
1185
+ binding,
1186
+ functionFrom,
1187
+ imported: externalImport,
1188
+ });
1189
+ }
1190
+ function remoteResultCondition(node, owner, ctx) {
1191
+ let shape = false;
1192
+ const root =
1193
+ ts.isIdentifier(node) && ctx.__iamLocals.has(node.text)
1194
+ ? ctx.__iamLocals.get(node.text)
1195
+ : node;
1196
+ const inspect = (n) => {
1197
+ if (!n || shape) return;
1198
+ if (
1199
+ ts.isPropertyAccessExpression(n) &&
1200
+ [
1201
+ "isLoading",
1202
+ "isPending",
1203
+ "isError",
1204
+ "error",
1205
+ "data",
1206
+ "length",
1207
+ "size",
1208
+ ].includes(n.name.text)
1209
+ )
1210
+ shape = true;
1211
+ ts.forEachChild(n, inspect);
1212
+ };
1213
+ inspect(root);
1214
+ return shape
1215
+ ? queryBacked(node, owner, ctx)
1216
+ ? "remote"
1217
+ : "unobserved-data"
1218
+ : null;
1219
+ }
1220
+ function updatedCollectionContext(ctx, label, value, owner) {
1221
+ const next = { ...ctx, [label]: value, __iamCollectionTemplate: true };
1222
+ for (const [key, init] of ctx.__iamLocals)
1223
+ if (!ctx.__iamStates.has(key))
1224
+ next[key] = evalExpression(init, owner.module, next);
1225
+ return next;
1226
+ }
1227
+ function finiteCollection(node, owner, ctx, instance, depth) {
1228
+ if (ctx.__iamCollectionTemplate || !ts.isConditionalExpression(node))
1229
+ return null;
1230
+ const maps = [];
1231
+ const scan = (n) => {
1232
+ if (
1233
+ ts.isCallExpression(n) &&
1234
+ ts.isPropertyAccessExpression(n.expression) &&
1235
+ n.expression.name.text === "map"
1236
+ )
1237
+ maps.push(n);
1238
+ if (!ts.isFunctionLike(n)) ts.forEachChild(n, scan);
1239
+ };
1240
+ scan(node);
1241
+ if (maps.length !== 1) return null;
1242
+ const receiver = maps[0].expression.expression,
1243
+ reads = collectionStateReads(ts, receiver, ctx),
1244
+ tabs = presentation.tabs.filter(
1245
+ (t) =>
1246
+ reads.includes(t.variableKey) &&
1247
+ Object.values(ctx.__iamVariableKeys).includes(t.variableKey),
1248
+ );
1249
+ if (tabs.length !== 1 || tabs[0].items.length < 2 || tabs[0].items.length > 8)
1250
+ return null;
1251
+ const tab = tabs[0],
1252
+ label = Object.keys(ctx.__iamVariableKeys).find(
1253
+ (k) => ctx.__iamVariableKeys[k] === tab.variableKey,
1254
+ );
1255
+ const candidates = tab.items.map((item) => {
1256
+ const next = updatedCollectionContext(ctx, label, item.value, owner);
1257
+ return {
1258
+ value: item.value,
1259
+ ctx: next,
1260
+ items: evalExpression(receiver, owner.module, next),
1261
+ };
1262
+ });
1263
+ if (
1264
+ candidates.some((c) => !Array.isArray(c.items) || c.items.length > 128) ||
1265
+ new Set(candidates.map((c) => JSON.stringify(c.items))).size < 2
1266
+ )
1267
+ return null;
1268
+ const source = sourceLocation(owner.module, node, owner.symbol),
1269
+ captured = {};
1270
+ const capture = (n) => {
1271
+ if (
1272
+ ts.isIdentifier(n) &&
1273
+ Object.hasOwn(ctx, n.text) &&
1274
+ !n.text.startsWith("__iam")
1275
+ ) {
1276
+ const value = ctx[n.text];
1277
+ captured[n.text] = isUnknown(value)
1278
+ ? { kind: "unknown" }
1279
+ : value === undefined
1280
+ ? { kind: "undefined" }
1281
+ : value?.[COMPONENT]
1282
+ ? { kind: "component", key: value[COMPONENT].key }
1283
+ : value?.[LUCIDE_SOURCE]
1284
+ ? { kind: "lucide", name: value[LUCIDE_SOURCE] }
1285
+ : { kind: "literal", value };
1286
+ }
1287
+ ts.forEachChild(n, capture);
1288
+ };
1289
+ capture(node);
1290
+ let cacheKey;
1291
+ try {
1292
+ cacheKey = identity("collection-template", {
1293
+ source,
1294
+ captured,
1295
+ cases: candidates.map((c) => ({ value: c.value, items: c.items })),
1296
+ });
1297
+ } catch {
1298
+ return null;
1299
+ }
1300
+ let cases = collectionTemplates.get(cacheKey);
1301
+ if (!cases) {
1302
+ cases = [];
1303
+ for (const candidate of candidates) {
1304
+ const sizes = {
1305
+ bindings: ui.bindings.length,
1306
+ transitions: ui.transitions.length,
1307
+ variables: ui.variables.length,
1308
+ conditions: presentation.conditions.length,
1309
+ tabs: presentation.tabs.length,
1310
+ regions: presentation.regions.length,
1311
+ };
1312
+ const html = renderExpr(
1313
+ node,
1314
+ owner,
1315
+ candidate.ctx,
1316
+ cacheKey + "/" + candidate.value,
1317
+ // A shared source fragment owns its bounded subtree budget. App wrapper
1318
+ // depth must not erase leaf text from an otherwise identical collection.
1319
+ 0,
1320
+ ),
1321
+ fragment = sourceCollectionFragment(
1322
+ html,
1323
+ source,
1324
+ candidate.items.length,
1325
+ record,
1326
+ );
1327
+ for (const variable of ui.variables.slice(sizes.variables))
1328
+ uiVariableKeys.delete(variable.variableKey);
1329
+ ui.bindings.length = sizes.bindings;
1330
+ ui.transitions.length = sizes.transitions;
1331
+ ui.variables.length = sizes.variables;
1332
+ for (const key of ["conditions", "tabs", "regions"])
1333
+ presentation[key].length = sizes[key];
1334
+ if (
1335
+ !presentation.fragments.some(
1336
+ (f) => f.fragmentKey === fragment.fragmentKey,
1337
+ )
1338
+ )
1339
+ presentation.fragments.push(fragment);
1340
+ cases.push({ value: candidate.value, fragmentKey: fragment.fragmentKey });
1341
+ }
1342
+ collectionTemplates.set(cacheKey, cases);
1343
+ }
1344
+ const collectionKey = identity("ui-collection", {
1345
+ page: pageKey,
1346
+ instance,
1347
+ source,
1348
+ }),
1349
+ nodeId = identity("node", { page: pageKey, collectionKey }),
1350
+ elementKey = identity("element", {
1351
+ component: owner.key,
1352
+ sourceCollection: node.pos,
1353
+ });
1354
+ elements.set(elementKey, {
1355
+ elementKey,
1356
+ componentKey: owner.key,
1357
+ tag: "div",
1358
+ source,
1359
+ identityStrategy: "provisional_ast_ordinal",
1360
+ editableProperties: [],
1361
+ binding: "read_only",
1362
+ });
1363
+ record.set(nodeId, { elementKey, source, synthetic: true });
1364
+ const requires = reads
1365
+ .filter((key) => key !== tab.variableKey)
1366
+ .map((variableKey) => ({
1367
+ variableKey,
1368
+ value: ui.variables.find((v) => v.variableKey === variableKey)
1369
+ ?.initialValue,
1370
+ }));
1371
+ presentation.collections.push({
1372
+ collectionKey,
1373
+ routeKey: pageKey,
1374
+ renderKey: "pending",
1375
+ nodeId,
1376
+ variableKey: tab.variableKey,
1377
+ requires,
1378
+ cases,
1379
+ source,
1380
+ });
1381
+ return (
1382
+ '<div data-iam-node="' +
1383
+ nodeId +
1384
+ '" data-iam-collection="' +
1385
+ collectionKey +
1386
+ '" data-iam-unobserved="true" hidden=""></div>'
1387
+ );
1388
+ }
1389
+ const iconGlyphs=new Set();
1390
+ function renderLucide(icon, node, owner, props, instance, shared=true) {
1391
+ const geometry = lucideGeometry(icon),
1392
+ source = sourceLocation(owner.module, node, owner.symbol);
1393
+ if (!geometry) return "";
1394
+ const element = (tag, attributes, children, part) => {
1395
+ const elementKey = identity("element", {
1396
+ component: owner.key,
1397
+ lucide: node.pos,
1398
+ part,
1399
+ tag,
1400
+ }),
1401
+ nodeId = identity("node", {
1402
+ page: pageKey,
1403
+ elementKey,
1404
+ instance,
1405
+ icon: icon[LUCIDE_SOURCE],
1406
+ });
1407
+ elements.set(elementKey, {
1408
+ elementKey,
1409
+ componentKey: owner.key,
1410
+ tag,
1411
+ source,
1412
+ identityStrategy: "provisional_ast_ordinal",
1413
+ editableProperties: [],
1414
+ binding: "read_only",
1415
+ });
1416
+ record.set(nodeId, { elementKey, source, synthetic: true });
1417
+ return (
1418
+ "<" +
1419
+ tag +
1420
+ ' data-iam-node="' +
1421
+ nodeId +
1422
+ '" ' +
1423
+ Object.entries(attributes)
1424
+ .map(([key, value]) => key + '="' + escape(value) + '"')
1425
+ .join(" ") +
1426
+ ">" +
1427
+ children +
1428
+ "</" +
1429
+ tag +
1430
+ ">"
1431
+ );
1432
+ };
1433
+ const className = typeof props.className === "string" ? props.className : "";
1434
+ className
1435
+ .split(/\s+/)
1436
+ .filter(Boolean)
1437
+ .forEach((c) => allClasses.add(c));
1438
+ const glyphId='iam-glyph-'+sha256(String(icon[LUCIDE_SOURCE])).slice(0,24);
1439
+ let glyph='';if(shared&&!iconGlyphs.has(glyphId)){
1440
+ iconGlyphs.add(glyphId);glyph=element('defs',{},element('g',{id:glyphId},geometry.map(([tag,attrs],index)=>element(tag,attrs,'','glyph-'+glyphId+'-'+index)).join(''),'glyph-'+glyphId),'defs-'+glyphId);
937
1441
  }
938
- if (!fn || !ts.isArrowFunction(fn) || fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) return null;
939
- const body = ts.isBlock(fn.body) ? fn.body.statements.length === 1 && ts.isExpressionStatement(fn.body.statements[0]) ? fn.body.statements[0].expression : null : unwrap(fn.body);
940
- if (!body || !ts.isCallExpression(body) || !ts.isIdentifier(body.expression) || body.arguments.length !== 1) return null;
941
- const label = ctx.__iamSetters[body.expression.text], variableKey = ctx.__iamVariableKeys[label];
942
- if (!variableKey) return null;
943
- const input = unwrap(body.arguments[0]);
944
- const functional = input && ts.isArrowFunction(input) && !ts.isBlock(input.body) && input.parameters.length === 1 && ts.isIdentifier(input.parameters[0].name) ? input : null;
945
- const aliases = functional ? { [functional.parameters[0].name.text]: { op: "var", key: variableKey } } : {};
946
- const value = uiExpression(functional ? functional.body : input, owner, ctx, fn.parameters[0]?.name?.text || null, aliases);
947
- if (!value) return null;
948
- const payload = {};
949
- function eventInputs(e) { if (e.op === "event") payload[e.key] = e.key === "checked" ? "boolean" : "string"; for (const child of [e.arg, e.left, e.right, ...(e.args || []),...(e.parts||[]).map(part=>part.when)]) if (child) eventInputs(child); }
950
- eventInputs(value);
951
- return { variableKey, value, payload };
1442
+ return element(
1443
+ "svg",
1444
+ {
1445
+ xmlns: "http://www.w3.org/2000/svg",
1446
+ width: 24,
1447
+ height: 24,
1448
+ viewBox: "0 0 24 24",
1449
+ fill: "none",
1450
+ stroke: "currentColor",
1451
+ "stroke-width":
1452
+ typeof props.strokeWidth === "number" ? props.strokeWidth : 2,
1453
+ "stroke-linecap": "round",
1454
+ "stroke-linejoin": "round",
1455
+ "aria-hidden": "true",
1456
+ class: className,
1457
+ },
1458
+ shared?glyph+element('use',{href:'#'+glyphId},'','use'):geometry.map(([tag,attrs],index)=>element(tag,attrs,'',index)).join(''),
1459
+ "root",
1460
+ );
952
1461
  }
953
- function unobservedValue(node, owner, instance) {
1462
+ function unobservedValue(node, owner, instance,remote=false) {
954
1463
  const source = sourceLocation(owner.module, node, owner.symbol);
955
1464
  const elementKey = identity("element", { component: owner.key, unobservedValue: node.getStart(owner.module.source) - owner.fn.getStart(owner.module.source) });
956
1465
  const nodeId = identity("node", { page: pageKey, element: elementKey, instance });
957
1466
  elements.set(elementKey, { elementKey, componentKey: owner.key, tag: "span", source, identityStrategy: "provisional_ast_ordinal", editableProperties: [], binding: "preview_only" });
958
1467
  record.set(nodeId, { elementKey, source, synthetic: true });
959
1468
  // Preserve an unknown child boundary. An empty projection must never prove that replacing text is safe.
960
- return '<span data-iam-node="' + nodeId + '" hidden=""></span>';
1469
+ return '<span data-iam-node="' + nodeId + '" data-iam-observation="'+(remote?'remote':'unobserved')+'" hidden=""></span>';
1470
+ }
1471
+ function renderInvocation(invocation,instance,depth){
1472
+ const {callback,args,caller}=invocation,next={...callback.ctx,__iamContexts:caller.__iamContexts??callback.ctx.__iamContexts,__iamOverlays:caller.__iamOverlays??callback.ctx.__iamOverlays};
1473
+ for(const [index,parameter]of callback.fn.parameters.entries()){
1474
+ const value=args[index];
1475
+ if(ts.isIdentifier(parameter.name))next[parameter.name.text]=value;
1476
+ else if(ts.isObjectBindingPattern(parameter.name))for(const field of parameter.name.elements)if(ts.isIdentifier(field.name))next[field.name.text]=!isUnknown(value)&&value&&typeof value==='object'?value[field.propertyName?.text||field.name.text]??UNKNOWN:UNKNOWN;
1477
+ }
1478
+ gap('RENDER_PROP_SOURCE_ONLY',callback.owner.module.file,callback.owner.symbol+': invoked JSX render-prop reconstructed from source; effects and business callbacks are not executed');
1479
+ return renderExpr(uiReturnExpression(callback.fn),callback.owner,next,callback.instance+'/call-'+instance,depth+1);
961
1480
  }
1481
+ function hasSourceJsx(node){let found=false;const visit=n=>{if(ts.isJsxElement(n)||ts.isJsxSelfClosingElement(n)||ts.isJsxFragment(n)){found=true;return;}if(!ts.isFunctionLike(n))ts.forEachChild(n,visit);};if(node)visit(node);return found;}
962
1482
  function renderExpr(node, owner, ctx, instance, depth) {
963
1483
  node = unwrap(node);
964
1484
  if (!node) return "";
965
1485
  if (depth > 45) { gap("RENDER_DEPTH_LIMIT", owner.module.file, owner.symbol + ": JSX traversal budget exceeded"); return ""; }
966
1486
  const m = owner.module;
1487
+ const collection=finiteCollection(node,owner,ctx,instance,depth);if(collection!==null)return collection;
1488
+ if(ts.isIdentifier(node)&&ctx.__iamLocals.has(node.text)&&hasSourceJsx(ctx.__iamLocals.get(node.text))){
1489
+ if(ctx.__iamActiveJsx?.has(node.text))return '';
1490
+ return renderExpr(ctx.__iamLocals.get(node.text),owner,{...ctx,__iamActiveJsx:new Set([...(ctx.__iamActiveJsx??[]),node.text])},instance+'/local-'+node.text,depth+1);
1491
+ }
967
1492
  if (ts.isJsxFragment(node))
968
1493
  return node.children
969
1494
  .map((n, i) =>
@@ -973,6 +1498,13 @@ export async function analyze({
973
1498
  if (ts.isJsxText(node)) return escape(jsxText(node.text));
974
1499
  if (ts.isJsxExpression(node))
975
1500
  return renderExpr(node.expression, owner, ctx, instance, depth + 1);
1501
+ if (ts.isBinaryExpression(node) && [ts.SyntaxKind.QuestionQuestionToken, ts.SyntaxKind.BarBarToken].includes(node.operatorToken.kind)) {
1502
+ const left = evalExpression(node.left, m, ctx);
1503
+ if (!isUnknown(left)) {
1504
+ const fallback = node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ? left == null : !left;
1505
+ return renderExpr(fallback ? node.right : node.left, owner, ctx, instance + '/fallback', depth + 1);
1506
+ }
1507
+ }
976
1508
  if(ts.isArrowFunction(node)){
977
1509
  const parent=node.parent?.parent,tag=parent&&ts.isJsxElement(parent)?parent.openingElement.tagName.getText(m.source):'',imported=m.imports.get(tag),body=unwrap(node.body);
978
1510
  if(imported?.specifier==='react-basics'&&imported.name==='Modal'&&!node.modifiers?.some(v=>v.kind===ts.SyntaxKind.AsyncKeyword)&&node.parameters.length<=3&&node.parameters.every(p=>ts.isIdentifier(p.name))&&(ts.isJsxElement(body)||ts.isJsxFragment(body)||ts.isJsxSelfClosingElement(body))){
@@ -1063,12 +1595,14 @@ export async function analyze({
1063
1595
  gap("UNOBSERVED_CONDITION", m.file, label);
1064
1596
  // Preserve both source alternatives for exact runtime binding. Neither
1065
1597
  // is asserted to be authorized/visible, nor executed in source preview.
1598
+ const dataBranches=[yes,no].map(branch=>branch?evalExpression(branch,m,ctx):null),observation=remoteResultCondition(condition,owner,ctx)??(dataBranches.every(v=>v===null||['string','number'].includes(typeof v))?'unobserved-data':null),conditionRow={conditionKey:identity('ui-condition',{file:m.file,offset:condition.getStart(m.source)}),expression:label,source:sourceLocation(m,condition,owner.symbol),...(observation?{observation}:{})};
1599
+ presentation.conditions.push(conditionRow);
1066
1600
  return [yes, no].map((branch,index)=>{
1067
1601
  if(!branch)return '';
1068
1602
  const elementKey=identity('element',{component:owner.key,state:label,unobserved:index,sourceOffset:condition.getStart(m.source)-owner.fn.getStart(m.source)}),id=identity('node',{page:pageKey,element:elementKey,instance:instance+'/unobserved-'+index}),source=sourceLocation(m,condition,owner.symbol);
1069
1603
  record.set(id,{elementKey,source,synthetic:true});
1070
1604
  elements.set(elementKey,{elementKey,componentKey:owner.key,tag:'div',source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});
1071
- return '<div data-iam-node="'+id+'" data-iam-unobserved="true" hidden="">'+renderExpr(branch,owner,ctx,instance+'/unobserved-'+index,depth+1)+'</div>';
1605
+ return '<div data-iam-node="'+id+'" data-iam-unobserved="true" hidden="" data-iam-condition="'+conditionRow.conditionKey+'" data-iam-alternative="'+(index===0)+'">'+renderExpr(branch,owner,ctx,instance+'/unobserved-'+index,depth+1)+'</div>';
1072
1606
  }).join('');
1073
1607
  }
1074
1608
  if (
@@ -1083,22 +1617,28 @@ export async function analyze({
1083
1617
  fn &&
1084
1618
  (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))
1085
1619
  ) {
1086
- if (values.length > 50)
1620
+ const limit=ctx.__iamCollectionTemplate?128:50;
1621
+ if (values.length > limit)
1087
1622
  gap(
1088
1623
  "COLLECTION_TRUNCATED",
1089
1624
  m.file,
1090
1625
  "Static collection limited to 50 preview items",
1091
1626
  );
1092
1627
  return values
1093
- .slice(0, 50)
1628
+ .slice(0, limit)
1094
1629
  .map((value, i) => {
1095
1630
  const next = { ...ctx };
1096
1631
  if (fn.parameters[0] && ts.isIdentifier(fn.parameters[0].name))
1097
1632
  next[fn.parameters[0].name.text] = value;
1098
1633
  if (fn.parameters[1] && ts.isIdentifier(fn.parameters[1].name))
1099
1634
  next[fn.parameters[1].name.text] = i;
1635
+ if(fn.parameters[0]&&ts.isObjectBindingPattern(fn.parameters[0].name))for(const field of fn.parameters[0].name.elements){
1636
+ if(!ts.isIdentifier(field.name)||field.dotDotDotToken)continue;
1637
+ const key=field.propertyName?.text||field.name.text;next[field.name.text]=value&&typeof value==='object'&&!isUnknown(value)&&Object.hasOwn(value,key)?value[key]:field.initializer?evalExpression(field.initializer,m,next):undefined;
1638
+ }
1639
+ if(ts.isBlock(fn.body))for(const statement of fn.body.statements)if(ts.isVariableStatement(statement))for(const decl of statement.declarationList.declarations)if(ts.isIdentifier(decl.name))next[decl.name.text]=evalExpression(decl.initializer,m,next);
1100
1640
  return renderExpr(
1101
- returnExpression(fn),
1641
+ uiReturnExpression(fn),
1102
1642
  owner,
1103
1643
  next,
1104
1644
  instance + "/item-" + i,
@@ -1108,7 +1648,7 @@ export async function analyze({
1108
1648
  .join("");
1109
1649
  }
1110
1650
  gap("DYNAMIC_COLLECTION", m.file, node.expression.getText(m.source));
1111
- return unobservedValue(node, owner, instance);
1651
+ return unobservedValue(node, owner, instance,queryBacked(node.expression.expression,owner,ctx));
1112
1652
  }
1113
1653
  if (!ts.isJsxElement(node) && !ts.isJsxSelfClosingElement(node)) {
1114
1654
  if (
@@ -1118,6 +1658,9 @@ export async function analyze({
1118
1658
  )
1119
1659
  return ctx[node.text].iamChildren;
1120
1660
  const value = evalExpression(node, m, ctx), expr = uiExpression(node, owner, ctx);
1661
+ if(value?.iamRenderInvocation)return renderInvocation(value.iamRenderInvocation,instance,depth);
1662
+ if(value?.iamRenderChildren)return value.iamRenderChildren(ctx);
1663
+ if(value?.iamChildren!==undefined)return value.iamChildren;
1121
1664
  if (expr && readVariables(expr).length && ["string", "number"].includes(typeof value)) {
1122
1665
  const source = sourceLocation(m, node, owner.symbol), elementKey = identity("element", { component: owner.key, stateTextOffset: node.getStart(m.source) - owner.fn.getStart(m.source) }), id = identity("node", { page: pageKey, element: elementKey, instance });
1123
1666
  record.set(id, { elementKey, source, synthetic: true });
@@ -1134,7 +1677,7 @@ export async function analyze({
1134
1677
  return escape(value);
1135
1678
  if (isUnknown(value)) {
1136
1679
  gap("DYNAMIC_VALUE", m.file, node.getText(m.source).slice(0, 100));
1137
- return unobservedValue(node, owner, instance);
1680
+ return unobservedValue(node, owner, instance,queryBacked(node,owner,ctx));
1138
1681
  }
1139
1682
  return "";
1140
1683
  }
@@ -1142,12 +1685,13 @@ export async function analyze({
1142
1685
  throw new Error("Rendered node budget exceeded");
1143
1686
  const opening = ts.isJsxElement(node) ? node.openingElement : node,
1144
1687
  tag = opening.tagName.getText(m.source),
1145
- props = m.anchors.get(opening) ? { "data-indiana-id": m.anchors.get(opening) } : {}, uiHandlers = [], uiBindings = [], forwardedHandlers=[];
1688
+ props = m.anchors.get(opening) ? { "data-indiana-id": m.anchors.get(opening) } : {}, uiHandlers = [], uiBindings = [], forwardedHandlers=[],uiAttributes=[];
1146
1689
  for (const attr of opening.attributes.properties) {
1147
1690
  if (ts.isJsxSpreadAttribute(attr)) {
1148
1691
  const v = evalExpression(attr.expression, m, ctx);
1149
1692
  if (isUnknown(v)) {
1150
1693
  for (const attribute of anchorAttributes) delete props[attribute];
1694
+ props.__iamOpaqueProps = true;
1151
1695
  gap("UNKNOWN_ANCHOR_SPREAD", m.file, "Opaque spread may override element identity");
1152
1696
  continue;
1153
1697
  }
@@ -1157,17 +1701,22 @@ export async function analyze({
1157
1701
  for (const key of Object.keys(v)) {
1158
1702
  for (let i = uiBindings.length - 1; i >= 0; i--) if (uiBindings[i].property === key) uiBindings.splice(i, 1);
1159
1703
  for (let i = uiHandlers.length - 1; i >= 0; i--) if (uiHandlers[i].key === key) uiHandlers.splice(i, 1);
1704
+ for (let i = uiAttributes.length - 1; i >= 0; i--) if (uiAttributes[i].key === key) uiAttributes.splice(i, 1);
1160
1705
  }
1161
- if (inherited) { uiBindings.push(...inherited.bindings); uiHandlers.push(...inherited.handlers); }
1706
+ if (inherited) { uiBindings.push(...inherited.bindings); uiHandlers.push(...inherited.handlers); uiAttributes.push(...(inherited.attributes??[])); }
1162
1707
  }
1163
1708
  continue;
1164
1709
  }
1165
1710
  const key = attr.name.getText(m.source);
1711
+ for(let i=uiAttributes.length-1;i>=0;i--)if(uiAttributes[i].key===key)uiAttributes.splice(i,1);
1712
+ if(!ctx.__iamCollectionTemplate&&['className','placeholder','aria-label'].includes(key)&&attr.initializer&&ts.isJsxExpression(attr.initializer)){
1713
+ const reads=collectionStateReads(ts,attr.initializer.expression,ctx);if(reads.length)uiAttributes.push({key,owner,ctx,expression:attr.initializer.expression,property:key==='className'?'class':key,reads,source:sourceLocation(m,attr,owner.symbol)});
1714
+ }
1166
1715
  for (let i = uiBindings.length - 1; i >= 0; i--) if (uiBindings[i].property === key) uiBindings.splice(i, 1);
1167
1716
  for (let i = uiHandlers.length - 1; i >= 0; i--) if (uiHandlers[i].key === key) uiHandlers.splice(i, 1);
1168
- if (["onClick", "onChange", "onInput"].includes(key)) {
1717
+ if (["onClick", "onChange", "onInput", "onValueChange", "onOpenChange"].includes(key)) {
1169
1718
  props[key] = undefined;
1170
- const supported = uiHandler(attr.initializer, owner, ctx);
1719
+ const supported = ctx.__iamCollectionTemplate?null:uiHandler(attr.initializer, owner, ctx, key);
1171
1720
  if (supported) { props[key] = undefined; uiHandlers.push({ ...supported, key, source: sourceLocation(m, attr, owner.symbol) }); }
1172
1721
  const fn =
1173
1722
  attr.initializer && ts.isJsxExpression(attr.initializer)
@@ -1199,7 +1748,7 @@ export async function analyze({
1199
1748
  key +
1200
1749
  " at line " +
1201
1750
  sourceLocation(m, attr, owner.symbol).line +
1202
- ": callback is not a supported UI simulation; business/data behavior is not executed",
1751
+ ": handler is not simulated; no business execution",
1203
1752
  );
1204
1753
  continue;
1205
1754
  }
@@ -1225,6 +1774,16 @@ export async function analyze({
1225
1774
  const expr = uiExpression(attr.initializer.expression, owner, ctx);
1226
1775
  if (expr && readVariables(expr).length) uiBindings.push({ property: key, value: expr, source: sourceLocation(m, attr, owner.symbol) });
1227
1776
  }
1777
+ if(attr.initializer&&ts.isJsxExpression(attr.initializer)){
1778
+ let fn=unwrap(attr.initializer.expression);const visited=new Set();
1779
+ while(fn&&ts.isIdentifier(fn)&&ctx.__iamLocals.has(fn.text)&&!visited.has(fn.text)){visited.add(fn.text);fn=unwrap(ctx.__iamLocals.get(fn.text));}
1780
+ if(fn&&(ts.isArrowFunction(fn)||ts.isFunctionExpression(fn))){
1781
+ let jsx=false;const inspect=n=>{if(ts.isJsxElement(n)||ts.isJsxSelfClosingElement(n)||ts.isJsxFragment(n))jsx=true;ts.forEachChild(n,inspect);};inspect(fn.body);
1782
+ const safe=!fn.modifiers?.some(v=>v.kind===ts.SyntaxKind.AsyncKeyword)&&(!ts.isBlock(fn.body)||fn.body.statements.every(s=>ts.isVariableStatement(s)||ts.isIfStatement(s)||ts.isReturnStatement(s)));
1783
+ if(jsx&&safe){props[key]={iamRenderProp:{fn,owner,ctx,instance:instance+'/prop-'+key}};continue;}
1784
+ }
1785
+ }
1786
+ if(attr.initializer&&ts.isJsxExpression(attr.initializer)&&hasSourceJsx(attr.initializer.expression)){const value=attr.initializer.expression;props[key]={iamRenderChildren:inherited=>renderExpr(value,owner,{...ctx,__iamContexts:inherited.__iamContexts??ctx.__iamContexts,__iamTabs:inherited.__iamTabs??ctx.__iamTabs,__iamOverlays:inherited.__iamOverlays??ctx.__iamOverlays},instance+'/prop-'+key,depth+1)};continue;}
1228
1787
  props[key] = !attr.initializer
1229
1788
  ? true
1230
1789
  : ts.isStringLiteral(attr.initializer)
@@ -1235,24 +1794,96 @@ export async function analyze({
1235
1794
  gap('DYNAMIC_CLASS_PARTIAL',m.file,'className at line '+sourceLocation(m,attr,owner.symbol).line+': only definitely present class tokens retained; unresolved styling needs runtime observation');
1236
1795
  }
1237
1796
  }
1238
- const children = ts.isJsxElement(node)
1239
- ? node.children
1240
- .map((child, i) =>
1241
- renderExpr(
1242
- child,
1243
- owner,
1244
- ctx,
1245
- instance + "/child-" + i,
1246
- depth + 1,
1247
- ),
1248
- )
1249
- .join("")
1250
- : props.children?.iamChildren || "";
1797
+ const chart=sourceChart({ts,node:opening,tag,owner,context:ctx,evaluate:evalExpression,imported:externalImport});
1798
+ if(chart){
1799
+ gap(chart.unobserved?'CHART_DATA_UNOBSERVED':'CHART_SOURCE_PROJECTION',m.file,chart.unobserved?'Chart data is dynamic, random or outside bounds; no values are invented.':'Bounded source data rendered as inert SVG; library effects, animation and tooltips are not executed.');
1800
+ const source=sourceLocation(m,node,owner.symbol);
1801
+ const draw=(item,part)=>{
1802
+ if(typeof item==='string')return escape(item);
1803
+ const elementKey=identity('element',{component:owner.key,chart:node.pos,part}),nodeId=identity('node',{page:pageKey,elementKey,instance});
1804
+ elements.set(elementKey,{elementKey,componentKey:owner.key,tag:item.tag,source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});record.set(nodeId,{elementKey,source,synthetic:true});
1805
+ return '<'+item.tag+' data-iam-node="'+nodeId+'" '+Object.entries(item.attrs).map(([k,v])=>k+'="'+escape(v)+'"').join(' ')+'>'+item.children.map((child,i)=>draw(child,part+'/'+i)).join('')+'</'+item.tag+'>';
1806
+ };
1807
+ return draw({tag:'svg',attrs:{xmlns:'http://www.w3.org/2000/svg',viewBox:'0 0 '+chart.width+' '+chart.height,width:chart.width,height:chart.height,role:'img','aria-label':chart.unobserved?'Chart: data unobserved':'Source chart projection','data-iam-chart':chart.unobserved?'unobserved':'source',style:'display:block;width:100%;height:auto;max-height:'+chart.height+'px'},children:chart.children},'root');
1808
+ }
1809
+ const sourceIcon=evalExpression(opening.tagName,m,ctx);
1810
+ if(sourceIcon?.[LUCIDE_SOURCE])return renderLucide(sourceIcon,node,owner,props,instance,!ctx.__iamCollectionTemplate);
1811
+ const tabPart=tabsPrimitive(m,tag),basePart=basePrimitive(m,tag),overlay=basePart&&basePart.family!=='Tabs'?basePart:null;
1812
+ let overlayGroup=overlay?ctx.__iamOverlays?.get(overlay.family):null;
1813
+ let childCtx=ctx,tabGroup=ctx.__iamTabs;
1814
+ const provider=tag.endsWith('.Provider')?contextKey(m,tag.slice(0,-9)):contextKey(m,tag);
1815
+ if(provider){const contexts=new Map(ctx.__iamContexts);contexts.set(provider,props.value);childCtx={...childCtx,__iamContexts:contexts};}
1816
+ if(overlay&&['Root','SubmenuRoot'].includes(overlay.part)){
1817
+ const openProperty=overlay.family==='Transition'?'in':'open',open=props[openProperty];
1818
+ overlayGroup={open:open===true||open===undefined&&props.defaultOpen===true,variable:uiBindings.find(b=>b.property===openProperty)?.value,controller:uiHandlers.find(h=>h.key==='onOpenChange')};
1819
+ if(overlay.family!=='Transition'&&open===undefined&&!props.__iamOpaqueProps&&(props.defaultOpen===undefined||typeof props.defaultOpen==='boolean')){
1820
+ const scopeKey=identity('primitive-state',{page:pageKey,instance,family:overlay.family}),variableKey=identity('ui-variable',{scope:scopeKey,label:'open'});
1821
+ ui.variables.push({variableKey,routeKey:pageKey,scopeKey,label:overlay.family+'.open',valueType:'boolean',initialValue:overlayGroup.open,source:sourceLocation(m,node,owner.symbol)});
1822
+ overlayGroup.variable={op:'var',key:variableKey};
1823
+ overlayGroup.controller={variableKey,value:{op:'event',key:'value'},payload:{value:'boolean'}};
1824
+ }
1825
+ if(overlay.family==='Transition'){
1826
+ const attr=opening.attributes.properties.find(a=>ts.isJsxAttribute(a)&&a.name.text==='onEnter');
1827
+ const callback=attr?.initializer&&ts.isJsxExpression(attr.initializer)?unwrap(attr.initializer.expression):null;
1828
+ let statement=callback&&ts.isArrowFunction(callback)&&!callback.parameters.length&&ts.isBlock(callback.body)?callback.body.statements[0]:null;
1829
+ if(statement&&ts.isIfStatement(statement))statement=evalExpression(statement.expression,m,ctx)===true?statement.thenStatement:null;
1830
+ if(statement&&ts.isBlock(statement))statement=statement.statements[0];
1831
+ const assignment=statement&&ts.isExpressionStatement(statement)?unwrap(statement.expression):null;
1832
+ const left=assignment&&ts.isBinaryExpression(assignment)&&assignment.operatorToken.kind===ts.SyntaxKind.EqualsToken?assignment.left:null;
1833
+ if(left&&ts.isPropertyAccessExpression(left)&&left.name.text==='display'&&ts.isPropertyAccessExpression(left.expression)&&left.expression.name.text==='style'&&ts.isPropertyAccessExpression(left.expression.expression)&&left.expression.expression.name.text==='current'&&ts.isIdentifier(left.expression.expression.expression)&&(assignment.right.kind===ts.SyntaxKind.NullKeyword||ts.isStringLiteral(assignment.right)&&assignment.right.text==='')){
1834
+ overlayGroup.reveal={ref:left.expression.expression.expression.text,file:m.file,symbol:owner.symbol};
1835
+ gap('TRANSITION_VISIBILITY_SOURCE_ONLY',m.file,'Source onEnter clears the referenced child display style; only that UI assignment is projected, without running callbacks or effects.');
1836
+ }
1837
+ }
1838
+ if(isUnknown(open)){
1839
+ const attribute=opening.attributes.properties.find(a=>ts.isJsxAttribute(a)&&a.name.text===openProperty),expression=attribute?.initializer&&ts.isJsxExpression(attribute.initializer)?attribute.initializer.expression:opening;
1840
+ overlayGroup.condition={conditionKey:identity('ui-condition',{file:m.file,offset:expression.getStart(m.source)}),expression:attribute?redactLiteral(expression.getText(m.source)).slice(0,160):'open (forwarded)',source:sourceLocation(m,expression,owner.symbol)};
1841
+ presentation.conditions.push(overlayGroup.condition);
1842
+ }
1843
+ const overlays=new Map(ctx.__iamOverlays);overlays.set(overlay.family,overlayGroup);childCtx={...childCtx,__iamOverlays:overlays};
1844
+ }
1845
+ if(tabPart==='Root') {
1846
+ const valueBinding=uiBindings.find(b=>b.property==='value')?.value;
1847
+ const controller=uiHandlers.find(h=>h.key==='onValueChange');
1848
+ tabGroup={tabKey:identity('ui-tab',{page:pageKey,scope:ctx.__iamScope,instance}),routeKey:pageKey,renderKey:'pending',nodeId:'pending',variableKey:valueBinding?.op==='var'?valueBinding.key:null,initialValue:typeof props.value==='string'?props.value:typeof props.defaultValue==='string'?props.defaultValue:null,items:[],source:sourceLocation(m,node,owner.symbol),scopeSource:ui.variables.find(v=>v.variableKey===valueBinding?.key)?.source??sourceLocation(m,node,owner.symbol),controller};
1849
+ childCtx={...childCtx,__iamTabs:tabGroup};
1850
+ }
1851
+ const localRef=componentBinding(m,tag),localTarget=ctx[tag]?.[COMPONENT]??(localRef&&componentDefs.get(localRef.module.file+'#'+localRef.name));
1852
+ const dynamicNode=ctx.__iamLocals.get(tag);
1853
+ const registry=!localTarget&&dynamicNode?componentRegistry({ts,node:dynamicNode,module:m,locals:ctx.__iamLocals,binding,functionFrom,value:(n,module)=>evalExpression(n,module,ctx),component:(n,module)=>sourceComponent(n,module)?.[COMPONENT],variable:name=>ctx.__iamVariableKeys[name]??(ctx.__iamPropBindings.get(name)?.op==='var'?ctx.__iamPropBindings.get(name).key:null),location:sourceLocation,unknown:UNKNOWN,imported:externalImport}):null;
1854
+ const renderChildren=(inherited=childCtx)=>ts.isJsxElement(node)?node.children.map((child,i)=>renderExpr(child,owner,{...childCtx,__iamContexts:inherited.__iamContexts??childCtx.__iamContexts,__iamTabs:inherited.__iamTabs??childCtx.__iamTabs,__iamOverlays:inherited.__iamOverlays??childCtx.__iamOverlays},instance+'/child-'+i,depth+1)).join(''):props.children?.iamRenderChildren?props.children.iamRenderChildren(inherited):props.children?.iamChildren||'';
1855
+ const children = localTarget||registry ? '' : renderChildren();
1856
+
1857
+ // Project the documented Controller render callback from source, with no
1858
+ // form initialization, live field values or business callback execution.
1859
+ const formPrimitive = externalImport(m, tag);
1860
+ if (formPrimitive?.specifier === 'react-hook-form') {
1861
+ if (formPrimitive.name === 'FormProvider') return children;
1862
+ if (formPrimitive.name === 'Controller' && props.render?.iamRenderProp)
1863
+ return renderInvocation({ callback: props.render.iamRenderProp,
1864
+ args: [{ field: { name: typeof props.name === 'string' ? props.name : UNKNOWN }, fieldState: UNKNOWN, formState: UNKNOWN }],
1865
+ caller: childCtx }, instance + '/controller', depth + 1);
1866
+ }
1867
+ const slotPrimitive = ctx[tag]?.[UI_PRIMITIVE]??externalImport(m, tag);
1868
+ if (slotPrimitive?.specifier === '@radix-ui/react-slot' && ['Slot', 'Root'].includes(slotPrimitive.name)) {
1869
+ const fragment=JSDOM.fragment(children),only=[...fragment.childNodes].filter(n=>n.nodeType!==3||n.textContent.trim());
1870
+ if(only.length!==1||only[0].nodeType!==1){gap('SLOT_CHILD_UNSUPPORTED',m.file,'Slot requires one concrete element; source-only children retained without prop merging');return children;}
1871
+ const child=only[0];
1872
+ if(typeof props.className==='string'){
1873
+ const names=[props.className,child.getAttribute('class')].filter(Boolean).join(' ');child.setAttribute('class',names);names.split(/\s+/).filter(Boolean).forEach(c=>allClasses.add(c));
1874
+ }
1875
+ for(const [key,value]of Object.entries(props))if(/^(?:title|role|data-(?:slot|state|side|variant|collapsible|active)|aria-[a-z-]+)$/.test(key)&&['string','number','boolean'].includes(typeof value)&&!child.hasAttribute(key))child.setAttribute(key,String(value));
1876
+ if(props.style&&typeof props.style==='object'){const css=staticStyle(props.style,()=>gap('INLINE_STYLE_UNSUPPORTED',m.file,'Unresolved Slot style'));if(css)child.setAttribute('style',css+';'+(child.getAttribute('style')||''));}
1877
+ return [...fragment.childNodes].map(n=>n.nodeType===1?n.outerHTML:n.textContent).join('');
1878
+ }
1879
+
1251
1880
  if (ctx[tag]?.iamChildren !== undefined) return ctx[tag].iamChildren;
1881
+ if (ctx[tag]?.iamRenderChildren) return ctx[tag].iamRenderChildren(ctx);
1252
1882
  const routerPrimitive = externalImport(m, tag);
1253
- if (["react-router", "react-router-dom"].includes(routerPrimitive?.specifier) && routerPrimitive.name === "Outlet") return ctx.__iamOutlet?.iamChildren ?? "";
1883
+ if (['react-router','react-router-dom'].includes(routerPrimitive?.specifier)&&routerPrimitive.name==='Navigate')return '';
1884
+ if (["react-router", "react-router-dom", "@tanstack/react-router"].includes(routerPrimitive?.specifier) && routerPrimitive.name === "Outlet") return ctx.__iamOutlet?.iamRenderChildren ? ctx.__iamOutlet.iamRenderChildren(ctx) : ctx.__iamOutlet?.iamChildren ?? "";
1254
1885
  if (
1255
- /^(?:ThemeProvider|Providers?|Analytics|TailwindIndicator|Toaster)$/.test(
1886
+ !localTarget && /^(?:ThemeProvider|Providers?|Analytics|TailwindIndicator|Toaster)$/.test(
1256
1887
  tag,
1257
1888
  )
1258
1889
  )
@@ -1260,12 +1891,26 @@ export async function analyze({
1260
1891
  const intrinsicAlias = typeof ctx[tag] === "string" && allowedTags.has(ctx[tag]);
1261
1892
  const importedPrimitive=m.imports.get(tag),knownExternalPrimitive=importedPrimitive?.specifier==='react-basics'&&['Text','Button','SubmitButton'].includes(importedPrimitive.name);
1262
1893
  let htmlTag = intrinsicAlias ? ctx[tag] : tag;
1894
+ if(provider)return children;
1263
1895
  if (tag === "html" || tag === "body") {
1264
1896
  htmlTag = "div";
1265
1897
  props["data-iam-adapter"] = tag;
1266
1898
  }
1267
1899
  if (/^[A-Z]/.test(tag) && !intrinsicAlias) {
1268
- if (tag === "Link" || ["react-router", "react-router-dom"].includes(routerPrimitive?.specifier) && ["Link", "NavLink"].includes(routerPrimitive.name)) {
1900
+ if(tabPart){
1901
+ htmlTag=tabPart==='Tab'?'button':'div';
1902
+ if(tabPart==='List')props.role='tablist';
1903
+ if(tabPart==='Tab'){props.role='tab';props.type='button';props['aria-selected']=tabGroup?.initialValue===props.value;}
1904
+ if(tabPart==='Panel'){props.role='tabpanel';props.hidden=tabGroup?.initialValue!==props.value;}
1905
+ }
1906
+ else if(overlay){
1907
+ htmlTag=['Trigger','Close','Item','CheckboxItem','RadioItem','SubmenuTrigger'].includes(overlay.part)&&props.asChild!==true?'button':overlay.part==='Title'?'h2':overlay.part==='Description'?'p':'div';
1908
+ if(htmlTag==='button')props.type='button';
1909
+ if(['Portal','Popup','Backdrop'].includes(overlay.part)||overlay.family==='Transition'){props.hidden=!overlayGroup?.open;if(overlayGroup?.condition){props['data-iam-condition']=overlayGroup.condition.conditionKey;props['data-iam-alternative']='true';props['data-iam-condition-default']='false';props['data-iam-unobserved']='true';}if(overlayGroup?.variable)uiBindings.push({property:'visible',value:overlayGroup.variable,source:sourceLocation(m,node,owner.symbol)});}
1910
+ if(['Root','SubmenuRoot','Portal','Provider'].includes(overlay.part)||props.asChild===true)props.style={display:'contents'};
1911
+ if(overlay.part==='Popup')props.role=overlay.family==='Menu'?'menu':overlay.family==='Tooltip'?'tooltip':'dialog';
1912
+ }
1913
+ else if (tag === "Link" || ["react-router", "react-router-dom", "@tanstack/react-router"].includes(routerPrimitive?.specifier) && ["Link", "NavLink"].includes(routerPrimitive.name)) {
1269
1914
  htmlTag = "a";
1270
1915
  if (typeof props.to === "string") props.href = props.to;
1271
1916
  }
@@ -1284,14 +1929,18 @@ export async function analyze({
1284
1929
  .filter((v) => typeof v === "string")
1285
1930
  .join(" ");
1286
1931
  } else {
1287
- const ref = binding(m, tag),
1288
- target = ref && componentDefs.get(ref.module.file + "#" + ref.name);
1932
+ const ref = componentBinding(m, tag),
1933
+ target = ctx[tag]?.[COMPONENT]??(ref && componentDefs.get(ref.module.file + "#" + ref.name));
1289
1934
  if (target) {
1290
1935
  const own = components.get(owner.key);
1291
1936
  if (!own.dependencies.includes(target.key))
1292
1937
  own.dependencies.push(target.key);
1293
- const childProps = { ...props, children: { iamChildren: children }, __iamOutlet: ctx.__iamOutlet };
1294
- forwarded.set(childProps, { bindings: uiBindings, handlers: [...uiHandlers,...forwardedHandlers] });
1938
+ const hasChildren=ts.isJsxElement(node)&&node.children.some(n=>!ts.isJsxText(n)||jsxText(n.text).trim());
1939
+ const meaningful=ts.isJsxElement(node)?node.children.filter(n=>!ts.isJsxText(n)||jsxText(n.text).trim()):[];
1940
+ const childCallback=meaningful.length===1&&ts.isJsxExpression(meaningful[0])?unwrap(meaningful[0].expression):null;
1941
+ const callbackBody=childCallback&&(ts.isArrowFunction(childCallback)||ts.isFunctionExpression(childCallback))&&!childCallback.modifiers?.some(v=>v.kind===ts.SyntaxKind.AsyncKeyword)&&(!ts.isBlock(childCallback.body)||childCallback.body.statements.every(s=>ts.isVariableStatement(s)||ts.isIfStatement(s)||ts.isReturnStatement(s)))&&hasSourceJsx(uiReturnExpression(childCallback));
1942
+ const childProps = { ...props, ...(hasChildren?{children:callbackBody?{iamRenderProp:{fn:childCallback,owner,ctx,instance:instance+'/render-child'}}:{iamRenderChildren:renderChildren}}:{}), __iamCollectionTemplate:ctx.__iamCollectionTemplate, __iamOutlet: ctx.__iamOutlet, __iamContexts:ctx.__iamContexts, __iamTabs:ctx.__iamTabs, __iamOverlays:ctx.__iamOverlays };
1943
+ forwarded.set(childProps, { bindings: uiBindings, handlers: [...uiHandlers,...forwardedHandlers], attributes:uiAttributes });
1295
1944
  return renderComponent(
1296
1945
  target,
1297
1946
  childProps,
@@ -1299,6 +1948,23 @@ export async function analyze({
1299
1948
  depth + 1,
1300
1949
  );
1301
1950
  }
1951
+ if(registry){
1952
+ const regionKey=identity('ui-region',{page:pageKey,instance,tag}),regionNode=identity('node',{page:pageKey,instance,regionKey});
1953
+ const source=sourceLocation(m,opening,owner.symbol),elementKey=identity('element',{component:owner.key,regionOffset:opening.getStart(m.source)-owner.fn.getStart(m.source)});
1954
+ elements.set(elementKey,{elementKey,componentKey:owner.key,tag:'div',source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});record.set(regionNode,{elementKey,source,synthetic:true});
1955
+ const row={regionKey,routeKey:pageKey,renderKey:'pending',nodeId:regionNode,expression:redactLiteral(dynamicNode.getText(m.source)).slice(0,160),variableKey:registry.variableKey,initialValue:ui.variables.find(v=>v.variableKey===registry.variableKey)?.initialValue??null,options:[],source};
1956
+ if(typeof row.initialValue!=='string'||!registry.options.some(o=>o.value===row.initialValue))row.initialValue=null;
1957
+ let html='<div data-iam-node="'+regionNode+'" data-iam-region="'+regionKey+'">';
1958
+ for(const option of registry.options){
1959
+ const id=identity('node',{page:pageKey,regionKey,value:option.value}),optionElement=identity('element',{component:owner.key,regionKey,value:option.value});
1960
+ elements.set(optionElement,{elementKey:optionElement,componentKey:owner.key,tag:'div',source:option.source,identityStrategy:'provisional_ast_ordinal',editableProperties:[],binding:'preview_only'});record.set(id,{elementKey:optionElement,source:option.source,synthetic:true});
1961
+ const catalog=ctx.t?.[TRANSLATION]??(translationResources.size===1?[...translationResources][0]:null),title=catalog?option.label.split('.').reduce((v,key)=>v?.[key],catalog):undefined;
1962
+ row.options.push({value:option.value,label:typeof title==='string'?title:option.label,nodeId:id,componentKey:option.component.key,source:option.source,scope:option.scope});
1963
+ const own=components.get(owner.key);if(!own.dependencies.includes(option.component.key))own.dependencies.push(option.component.key);
1964
+ html+='<div data-iam-node="'+id+'" data-iam-region-value="'+escape(option.value)+'" data-iam-unobserved="true" hidden="">'+renderComponent(option.component,{...props,children:{iamRenderChildren:renderChildren},__iamOutlet:ctx.__iamOutlet,__iamContexts:ctx.__iamContexts,__iamOverlays:ctx.__iamOverlays},instance+'/registry-'+option.value,depth+1)+'</div>';
1965
+ }
1966
+ html+='</div>';presentation.regions.push(row);gap('DYNAMIC_COMPONENT_CANDIDATES',m.file,tag+': finite registry alternatives retained as source-only UI previews; selection and scope are unobserved');return html;
1967
+ }
1302
1968
  htmlTag = /Button|Trigger/.test(tag)
1303
1969
  ? "button"
1304
1970
  : /Input/.test(tag)
@@ -1311,6 +1977,10 @@ export async function analyze({
1311
1977
  }
1312
1978
  }
1313
1979
  if (!allowedTags.has(htmlTag)) return children;
1980
+ const reveal=ctx.__iamOverlays?.get('Transition')?.reveal,ref=opening.attributes.properties.find(a=>ts.isJsxAttribute(a)&&a.name.text==='ref');
1981
+ if(reveal?.file===m.file&&reveal.symbol===owner.symbol&&ref?.initializer&&ts.isJsxExpression(ref.initializer)&&ts.isIdentifier(ref.initializer.expression)&&ref.initializer.expression.text===reveal.ref&&props.style?.display==='none'){
1982
+ const {display,...style}=props.style;props.style=style;
1983
+ }
1314
1984
  const anchor = anchorAttributes
1315
1985
  .map((key) =>
1316
1986
  typeof props[key] === "string" ? key + "=" + props[key] : null,
@@ -1335,6 +2005,7 @@ export async function analyze({
1335
2005
  element: elementKey,
1336
2006
  instance,
1337
2007
  });
2008
+ if(!ctx.__iamCollectionTemplate)for(const request of uiAttributes)attributeRequests.push({nodeId,...request});
1338
2009
  elements.set(elementKey, {
1339
2010
  elementKey,
1340
2011
  componentKey: owner.key,
@@ -1351,12 +2022,38 @@ export async function analyze({
1351
2022
  source,
1352
2023
  synthetic: htmlTag !== tag && tag !== "Link" && tag !== "Image"&&!intrinsicAlias&&!knownExternalPrimitive,
1353
2024
  });
1354
- for (const handler of uiHandlers) {
2025
+ if(tabPart==='Root'){
2026
+ // Tab initialization below shares the same concrete source container.
2027
+ tabGroup.nodeId=nodeId;
2028
+ const {controller,...row}=tabGroup;
2029
+ if(row.items.length&&row.initialValue!==null&&!row.items.some(item=>item.value===row.initialValue))row.initialValue=null;
2030
+ presentation.tabs.push(row);
2031
+ }
2032
+ if(overlayGroup?.controller&&overlayGroup.variable&&['Dialog','AlertDialog','Menu','Popover','Collapsible'].includes(overlay?.family)&&['Trigger','Close'].includes(overlay.part)&&!uiHandlers.some(h=>h.key==='onClick')){
2033
+ const next=overlay.part==='Close'?{op:'const',value:false}:{op:'not',arg:overlayGroup.variable};
2034
+ const replace=expression=>expression.op==='event'&&expression.key==='value'?next:Object.fromEntries(Object.entries(expression).map(([key,value])=>[key,Array.isArray(value)?value.map(v=>v&&typeof v==='object'?replace(v):v):value&&typeof value==='object'?replace(value):value]));
2035
+ const controller=overlayGroup.controller,writes=(controller.writes||[{variableKey:controller.variableKey,value:controller.value}]).map(w=>({...w,value:replace(w.value)}));
2036
+ uiHandlers.push({...writes[0],writes,key:'onClick',payload:{},source});
2037
+ }
2038
+ if(tabPart==='Tab'&&tabGroup&&typeof props.value==='string'&&props.value.length<=2048){
2039
+ if(!tabGroup.items.some(item=>item.value===props.value))tabGroup.items.push({value:props.value,nodeId});
2040
+ props['data-iam-tab-value']=props.value;props['data-iam-tab-key']=tabGroup.tabKey;
2041
+ const replace=value=>value.op==='event'&&value.key==='value'?{op:'const',value:props.value}:Object.fromEntries(Object.entries(value).map(([k,v])=>[k,Array.isArray(v)?v.map(item=>item&&typeof item==='object'?replace(item):item):v&&typeof v==='object'?replace(v):v]));
2042
+ if(tabGroup.controller){const writes=(tabGroup.controller.writes||[{variableKey:tabGroup.controller.variableKey,value:tabGroup.controller.value}]).map(w=>({...w,value:replace(w.value)}));uiHandlers.push({...writes[0],writes,key:'onClick',payload:{},source});}
2043
+ }
2044
+ if(tabPart==='Panel'&&tabGroup?.variableKey&&typeof props.value==='string')uiBindings.push({property:'visible',value:{op:'eq',left:{op:'var',key:tabGroup.variableKey},right:{op:'const',value:props.value}},source});
2045
+ for (const handler of uiHandlers.filter(h=>!['onValueChange','onOpenChange'].includes(h.key))) {
1355
2046
  const event = handler.key === "onClick" ? "click" : htmlTag === "select" || handler.payload.checked ? "change" : "input";
1356
- ui.transitions.push({ transitionKey: identity("ui-transition", { page: pageKey, node: nodeId, event }), routeKey: pageKey, fromStateKey: "pending", toStateKey: "pending", trigger: { nodeId, event, payload: handler.payload }, guard: { op: "const", value: true }, stateReads: readVariables(handler.value), stateWrites: [{ variableKey: handler.variableKey, value: handler.value }], simulation: "implemented", evidence: "source_inferred", source: handler.source });
2047
+ const writes=handler.writes||[{variableKey:handler.variableKey,value:handler.value}];
2048
+ ui.transitions.push({ transitionKey: identity("ui-transition", { page: pageKey, node: nodeId, event }), routeKey: pageKey, fromStateKey: "pending", toStateKey: "pending", trigger: { nodeId, event, payload: handler.payload }, guard: { op: "const", value: true }, stateReads: [...new Set(writes.flatMap(w=>readVariables(w.value)))].sort(), stateWrites: writes, simulation: "implemented", evidence: "source_inferred", source: handler.source });
1357
2049
  }
1358
- for (const binding of uiBindings.filter(b => ["value", "checked", "disabled", "className"].includes(b.property))) {
2050
+ for (const binding of uiBindings.filter(b => !ctx.__iamCollectionTemplate&&["value", "checked", "disabled", "className", "visible"].includes(b.property)&&!(tabPart==='Root'&&b.property==='value'))) {
1359
2051
  if(['value','checked'].includes(binding.property)&&!['input','select','textarea'].includes(htmlTag)){gap('UI_BINDING_TARGET_UNSUPPORTED',m.file,tag+': input property cannot be simulated on a non-input adapter');continue;}
2052
+ const expectedType = ["checked", "disabled", "visible"].includes(binding.property) ? "boolean" : "string";
2053
+ if (uiExpressionType(binding.value) !== expectedType) {
2054
+ gap("UI_BINDING_TYPE_UNSUPPORTED", m.file, tag + "." + binding.property + ": source value cannot be represented by the typed UI simulation");
2055
+ continue;
2056
+ }
1360
2057
  ui.bindings.push({ bindingKey: identity("ui-binding", { node: nodeId, property: binding.property }), routeKey: pageKey, nodeId, elementKey, ...binding });
1361
2058
  }
1362
2059
  const sensitiveInput = htmlTag === "input" && (["password", "hidden"].includes(String(props.type).toLowerCase()) || sensitiveName(String(props.name || props.id || "")));
@@ -1366,7 +2063,7 @@ export async function analyze({
1366
2063
  ui.transitions = ui.transitions.filter(t => t.trigger.nodeId !== nodeId);
1367
2064
  gap("SENSITIVE_INPUT_REDACTED", m.file, "Input value and state bridge omitted");
1368
2065
  }
1369
- const attrs = { "data-iam-node": nodeId };
2066
+ const attrs = { "data-iam-node": nodeId,...(['input','textarea','select'].includes(htmlTag)&&Object.hasOwn(props,'value')&&isUnknown(props.value)?{'data-iam-input-unobserved':'true'}:{}) };
1370
2067
  for (const [key, value] of Object.entries(props)) {
1371
2068
  if (key === "style") {
1372
2069
  const css = staticStyle(value, () => gap("INLINE_STYLE_UNSUPPORTED", m.file,
@@ -1394,7 +2091,7 @@ export async function analyze({
1394
2091
  clipRule: "clip-rule",
1395
2092
  }[key] || key;
1396
2093
  if (
1397
- !/^(?:class|id|name|type|placeholder|value|title|alt|href|src|role|for|disabled|hidden|checked|width|height|viewBox|d|fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|fill-rule|clip-rule|cx|cy|r|x|y|x1|x2|y1|y2|points|aria-[a-z-]+|data-indiana-id|data-testid|data-test|data-qa|data-iam-(?:adapter|toggle))$/.test(
2094
+ !/^(?:class|id|name|type|placeholder|value|title|alt|href|src|role|for|disabled|hidden|checked|width|height|viewBox|d|fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|fill-rule|clip-rule|cx|cy|r|x|y|x1|x2|y1|y2|points|aria-[a-z-]+|data-indiana-id|data-indiana-map-(?:area|flow|kind|auth|edge)|data-(?:slot|state|side|variant|collapsible|orientation|active)|data-testid|data-test|data-qa|data-iam-(?:adapter|toggle|tab-value|tab-key|condition|alternative|condition-default|unobserved))$/.test(
1398
2095
  attr,
1399
2096
  )
1400
2097
  )
@@ -1436,26 +2133,50 @@ export async function analyze({
1436
2133
  );
1437
2134
  }
1438
2135
  function renderEntry(file, props, instance, symbol) {
1439
- const entryModule = modules.get(file), entryRef = entryModule && binding(entryModule, symbol ?? entryModule.defaultName);
2136
+ const entryModule = modules.get(file), entryRef = entryModule && componentBinding(entryModule, symbol ?? entryModule.defaultName);
1440
2137
  const entryDef = entryRef && componentDefs.get(entryRef.module.file + "#" + entryRef.name);
1441
- if (!entryDef) { gap("PAGE_NO_JSX", file, "Composition entry has no inspectable JSX; null defaults are inert"); return ""; }
2138
+ if (!entryDef) {
2139
+ if (entryRef) {
2140
+ const key = identity("component", { file: entryRef.module.file, symbol: entryRef.name });
2141
+ if (!components.has(key)) components.set(key, { componentKey: key, file: entryRef.module.file, name: entryRef.name, source: sourceLocation(entryRef.module, entryRef.node, entryRef.name), dependencies: [] });
2142
+ }
2143
+ gap("PAGE_NO_JSX", file, "Composition entry has no inspectable JSX; null defaults are inert");
2144
+ return "";
2145
+ }
1442
2146
  return renderComponent(entryDef, props, instance, 0);
1443
2147
  }
1444
- function renderBranch(branch) {
1445
- let content = renderEntry(branch.file, {}, branch === composition.composition ? "page" : "slot-page:" + branch.file, branch.symbol);
1446
- for (const layer of branch.layers) {
1447
- const props = { children: { iamChildren: content } };
1448
- if (routing.framework === "react-router") props.__iamOutlet = { iamChildren: content };
1449
- if (/^(?:src\/)?pages\/_app\.[jt]sx?$/.test(layer.file)) { props.Component = { iamChildren: content }; props.pageProps = {}; }
2148
+ function renderBranch(branch, inherited = {}) {
2149
+ const contentId=identity('node',{page:pageKey,lane:branch.lane,pageContent:true,...(composition.entryMode==='soft'?{composition:composition.compositionKey}:{})}),contentElement=identity('element',{page:pageKey,lane:branch.lane,pageContent:true});
2150
+ const contentSource=sourceLocation(modules.get(branch.file),modules.get(branch.file).source,branch.symbol??'$page-content');
2151
+ elements.set(contentElement,{elementKey:contentElement,componentKey:def.key,tag:'div',source:contentSource,identityStrategy:'page_root',editableProperties:[],binding:'preview_only'});record.set(contentId,{elementKey:contentElement,source:contentSource,synthetic:true});
2152
+ const contextProps=ctx=>({__iamContexts:ctx.__iamContexts,__iamTabs:ctx.__iamTabs,__iamOverlays:ctx.__iamOverlays});
2153
+ function renderLayer(index,ctx){
2154
+ if(index<0){
2155
+ const content=renderEntry(branch.file,contextProps(ctx),branch===composition.composition?'page':'slot-page:'+branch.file,branch.symbol);
2156
+ return '<div data-iam-node="'+contentId+'" data-iam-page-content="true">'+content+'</div>';
2157
+ }
2158
+ const layer=branch.layers[index],child={iamRenderChildren:next=>renderLayer(index-1,next)};
2159
+ const props = ["react-router", "tanstack-router"].includes(routing.framework)
2160
+ ? { ...contextProps(ctx), __iamOutlet: child }
2161
+ : { ...contextProps(ctx), children: child };
2162
+ if (/^(?:src\/)?pages\/_app\.[jt]sx?$/.test(layer.file)) { props.Component = child; props.pageProps = {}; }
1450
2163
  for (const slot of layer.slots) {
1451
- props[slot.name] = { iamChildren: renderBranch(slot.branch) };
2164
+ props[slot.name] = { iamRenderChildren:next=>renderBranch(slot.branch,next) };
1452
2165
  if (composition.entryMode === "hard") gap("SOFT_NAVIGATION_STATE_UNOBSERVED", layer.file, "Slot " + slot.name + ": hard-load composition alone does not establish retained soft-navigation state");
1453
2166
  }
1454
- content = renderEntry(layer.file, props, "layout:" + path.posix.dirname(layer.file), layer.symbol);
2167
+ return renderEntry(layer.file, props, "layout:" + path.posix.dirname(layer.file), layer.symbol);
1455
2168
  }
1456
- return content;
2169
+ return renderLayer(branch.layers.length-1,inherited);
1457
2170
  }
1458
2171
  let html = composition.gaps.length ? "" : renderBranch(composition.composition);
2172
+ for(const request of attributeRequests){
2173
+ const tabs=presentation.tabs.filter(t=>request.reads.includes(t.variableKey));if(tabs.length!==1||tabs[0].items.length>8)continue;
2174
+ const tab=tabs[0],cases=[];
2175
+ for(const item of tab.items){const next={...request.ctx};for(const [label,key]of Object.entries(next.__iamVariableKeys))if(key===tab.variableKey)next[label]=item.value;for(const [label,expr]of next.__iamPropBindings)if(expr.op==='var'&&expr.key===tab.variableKey)next[label]=item.value;for(const [label,init]of next.__iamLocals)if(!next.__iamStates.has(label))next[label]=evalExpression(init,request.owner.module,next);const value=evalExpression(request.expression,request.owner.module,next);if(typeof value!=='string'||value.length>4000){cases.length=0;break;}cases.push({value:item.value,text:value});if(request.property==='class')value.split(/\s+/).filter(Boolean).forEach(c=>allClasses.add(c));}
2176
+ if(cases.length<2||new Set(cases.map(c=>c.text)).size<2)continue;
2177
+ const set={property:request.property,cases,source:request.source},setKey=identity('ui-attribute-set',set);if(!presentation.attributeSets.some(s=>s.setKey===setKey))presentation.attributeSets.push({setKey,...set});
2178
+ const row={routeKey:pageKey,renderKey:'pending',nodeId:request.nodeId,variableKey:tab.variableKey,setKey};presentation.attributes.push({attributeKey:identity('ui-attribute',row),...row});
2179
+ }
1459
2180
  const rootId = identity("node", { page: pageKey, root: true, ...(composition.entryMode === "soft" ? { composition: composition.compositionKey } : {}) }),
1460
2181
  rootElement = identity("element", { page: pageKey, root: true });
1461
2182
  record.set(rootId, {
@@ -1481,6 +2202,27 @@ export async function analyze({
1481
2202
  const dom = new JSDOM(
1482
2203
  "<!doctype html><html><body>" + html + "</body></html>",
1483
2204
  );
2205
+ // JSX children may be forwarded more than once, and HTML repair may clone
2206
+ // an invalid nested anchor. Each concrete occurrence needs a unique node;
2207
+ // ambiguous copies remain preview-only and have no simulated state bridge.
2208
+ const occurrences = new Map();
2209
+ for (const element of dom.window.document.querySelectorAll("[data-iam-node]")) {
2210
+ const id = element.getAttribute("data-iam-node");
2211
+ const matches = occurrences.get(id) ?? [];
2212
+ matches.push(element);
2213
+ occurrences.set(id, matches);
2214
+ }
2215
+ for (const [original, matches] of occurrences) {
2216
+ if (matches.length < 2) continue;
2217
+ const rec = record.get(original);
2218
+ if (!rec) throw Error("Unknown repeated source node");
2219
+ gap("REPEATED_SOURCE_INSTANCE", rec.source.file, "Repeated forwarded children or HTML repair: copies are preview-only without a state bridge");
2220
+ for (const [index, element] of matches.entries()) {
2221
+ const id = identity("node", { page: pageKey, original, occurrence: index });
2222
+ element.setAttribute("data-iam-node", id);
2223
+ record.set(id, { ...rec, synthetic: true });
2224
+ }
2225
+ }
1484
2226
  const nodes = [];
1485
2227
  function collect(node, parentId, index) {
1486
2228
  if (node.nodeType !== 1 && node.nodeType !== 3) return null;
@@ -1552,21 +2294,12 @@ export async function analyze({
1552
2294
  source: page.source,
1553
2295
  documentKey,
1554
2296
  });
1555
- for (const branch of branches.slice(0, 30)) {
1556
- const key = identity("state", {
1557
- page: pageKey,
1558
- source: branch.source,
1559
- label: branch.label,
1560
- });
1561
- if (!states.some((s) => s.stateKey === key)) {
1562
- states.push({
1563
- stateKey: key,
1564
- pageKey,
1565
- label: branch.label,
1566
- evidence: branch.evidence,
1567
- source: branch.source,
1568
- documentKey: null,
1569
- });
2297
+ // Unknown source alternatives live in ui.conditions. Representable local
2298
+ // state candidates remain separate from concrete source documents.
2299
+ for (const branch of branches.filter(b=>b.evidence==='source_inferred').slice(0, 30)) {
2300
+ const key = identity("state", {page:pageKey,source:branch.source,label:branch.label});
2301
+ if(!states.some(state=>state.stateKey===key)){
2302
+ states.push({stateKey:key,pageKey,label:branch.label,evidence:branch.evidence,source:branch.source,documentKey:null});
1570
2303
  page.stateKeys.push(key);
1571
2304
  }
1572
2305
  }
@@ -1595,7 +2328,7 @@ export async function analyze({
1595
2328
  for (const node of doc.nodes) {
1596
2329
  if (
1597
2330
  node.kind !== "element" ||
1598
- !["a", "button", "form"].includes(node.tag)
2331
+ node.tag !== "a"
1599
2332
  )
1600
2333
  continue;
1601
2334
  const href = node.attributes.href || "",
@@ -1629,9 +2362,18 @@ export async function analyze({
1629
2362
  });
1630
2363
  }
1631
2364
  const packagedAssets = [];
2365
+ inventory.push(...await referencedRasterSources(root,assets.keys()));
1632
2366
  for (const [url] of assets) {
1633
2367
  gap("PUBLIC_ASSET_NOT_PACKAGED", "public", url + ": requires an explicit public-asset build input");
1634
2368
  }
2369
+ // Only statically imported styles belong to the source entry's default presentation.
2370
+ const styleFiles=new Map(cssSources.map(s=>[s.file,s])),activeStyles=new Map();
2371
+ const visitStyle=file=>{const style=styleFiles.get(file);if(!style||activeStyles.has(file))return;activeStyles.set(file,style);for(const match of style.text.matchAll(/@import\s+["']([^"']+)["']/g))if(match[1].startsWith('.'))visitStyle(path.posix.normalize(path.posix.join(path.posix.dirname(file),match[1])));};
2372
+ for(const module of selection.discoveryModules.values())for(const statement of module.source.statements)if(ts.isImportDeclaration(statement)&&statement.moduleSpecifier&&ts.isStringLiteral(statement.moduleSpecifier)){
2373
+ const spec=statement.moduleSpecifier.text;if(spec.endsWith('.css')&&spec.startsWith('.'))visitStyle(path.posix.normalize(path.posix.join(path.posix.dirname(module.file),spec)));
2374
+ }
2375
+ const sourceStyles=[...activeStyles.values()],sourceThemes=sourceStyles.flatMap(s=>[...s.text.matchAll(/@theme(?:\s+inline)?\s*\{([^{}]*)\}/g)].map(m=>m[0])).join('\n');
2376
+ const color=t=>sourceStyles.some(s=>new RegExp('--'+t+':\\s*(?:oklch|oklab|rgb|hsl|lab|lch|color|var)\\(|--'+t+':\\s*#').test(s.text))?'var(--'+t+')':'hsl(var(--'+t+'))';
1635
2377
  let css = "";
1636
2378
  try {
1637
2379
  const tw = dependency("tailwindcss");
@@ -1662,16 +2404,16 @@ export async function analyze({
1662
2404
  ];
1663
2405
  const custom =
1664
2406
  "@theme inline {" +
1665
- tokens.map((t) => "--color-" + t + ":hsl(var(--" + t + "));").join("") +
2407
+ tokens.map((t) => "--color-" + t + ":" + color(t) + ";").join("") +
1666
2408
  "--font-heading: ui-sans-serif,system-ui;--radius-lg:var(--radius);--radius-md:calc(var(--radius) - 2px);}";
1667
2409
  const compile = await tw.value.compile(
1668
- theme + "\n" + custom + "\n@tailwind utilities;",
2410
+ theme + "\n" + custom + "\n" + sourceThemes + "\n@tailwind utilities;",
1669
2411
  );
1670
2412
  css = compile.build([...allClasses].sort());
1671
2413
  } catch (error) {
1672
2414
  gap("STYLE_COMPILER", "", error.message);
1673
2415
  }
1674
- const vars = cssSources
2416
+ const vars = sourceStyles
1675
2417
  .flatMap((s) =>
1676
2418
  [...s.text.matchAll(/(?::root|\.dark)\s*\{([^{}]*)\}/g)].map(
1677
2419
  (m) => m[0],
@@ -1681,7 +2423,7 @@ export async function analyze({
1681
2423
  css +=
1682
2424
  "\n" +
1683
2425
  vars +
1684
- "\n*{box-sizing:border-box;border-color:hsl(var(--border));border-style:solid;border-width:0}body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;color:hsl(var(--foreground));background:hsl(var(--background))}h1,h2,h3,p,ul,ol{margin:0}a{color:inherit;text-decoration:none}button,input,textarea,select{font:inherit}button,a{cursor:pointer}button{background:transparent}svg{display:block}img{max-width:100%;height:auto}.container{width:100%;max-width:1400px;padding-inline:2rem;margin-inline:auto}.iam-icon{display:inline-flex;align-items:center;justify-content:center;width:1.1em;height:1.1em}.iam-page{min-height:100vh}.iam-placeholder{padding:1rem;border:1px dashed #d4d4d4}";
2426
+ "\n*{box-sizing:border-box;border-color:"+color("border")+";border-style:solid;border-width:0}[hidden]{display:none!important}body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;color:"+color("foreground")+";background:"+color("background")+"}h1,h2,h3,p,ul,ol{margin:0}a{color:inherit;text-decoration:none}button,input,textarea,select{font:inherit}button,a{cursor:pointer}button{background:transparent}svg{display:block}img{max-width:100%;height:auto}.container{width:100%;max-width:1400px;padding-inline:2rem;margin-inline:auto}.iam-icon{display:inline-flex;align-items:center;justify-content:center;width:1.1em;height:1.1em}.iam-page{min-height:100vh}.iam-placeholder{padding:1rem;border:1px dashed #d4d4d4}";
1685
2427
  const runtime = await fs.readFile(
1686
2428
  new URL("../assets/document-runtime.js", import.meta.url),
1687
2429
  "utf8",
@@ -1697,6 +2439,16 @@ export async function analyze({
1697
2439
  doc.js = runtime;
1698
2440
  sealDocument(doc);
1699
2441
  }
2442
+ // Inventory every source file, but serialize UI definitions reachable from
2443
+ // projected routes (including retained alternatives and shared fragments).
2444
+ const liveElements=new Set([...documents.flatMap(d=>d.nodes.map(n=>n.elementKey)),...presentation.fragments.flatMap(f=>f.sourceElements)]);
2445
+ for(const key of elements.keys())if(!liveElements.has(key))elements.delete(key);
2446
+ const liveComponents=new Set([...pages.flatMap(p=>p.componentKeys),...[...elements.values()].map(e=>e.componentKey)]);
2447
+ const compositionFiles=new Set(renderDefinitions.flatMap(d=>flattenComposition(d.composition).flatMap(entry=>[entry.file,...entry.layers.map(layer=>layer.file)])));
2448
+ for(const component of components.values())if(compositionFiles.has(component.file))liveComponents.add(component.componentKey);
2449
+ const retainComponent=key=>{const c=components.get(key);if(!c)return;for(const dep of c.dependencies)if(!liveComponents.has(dep)){liveComponents.add(dep);retainComponent(dep);}};
2450
+ for(const key of [...liveComponents])retainComponent(key);
2451
+ for(const key of components.keys())if(!liveComponents.has(key))components.delete(key);
1700
2452
  const sourceGraphDigest = hashObject("iam/source-graph/v1", {
1701
2453
  files: inventory,
1702
2454
  components: [...components.values()],
@@ -1706,8 +2458,8 @@ export async function analyze({
1706
2458
  schemaVersion: 2,
1707
2459
  producer: {
1708
2460
  name: "indiana-app-manifest-local",
1709
- version: "0.17.0",
1710
- method: "typescript-ast-and-bounded-static-projection",
2461
+ version: "0.24.0",
2462
+ method: "typescript-ast-and-bounded-static-projection;lucide-static-"+LUCIDE_SOURCE_VERSION,
1711
2463
  },
1712
2464
  app: { name, platforms: ["web"] },
1713
2465
  routing,
@@ -1740,7 +2492,29 @@ export async function analyze({
1740
2492
  ui.bindings = ui.bindings.filter(b => concrete.has(b.nodeId));
1741
2493
  ui.transitions = ui.transitions.filter(t => concrete.has(t.trigger.nodeId));
1742
2494
  for (const doc of documents) for (const node of doc.nodes) if (node.kind === "element" && ["button", "form", "input", "select", "textarea"].includes(node.tag) && !ui.transitions.some(t => t.trigger.nodeId === node.nodeId)) ui.uncovered.push({ routeKey: doc.pageKey, nodeId: node.nodeId, event: "source-event", reason: "No supported UI-only handler; business callbacks are not executed", source: node.source });
2495
+ // State with no representable UI read/write has no preview effect.
2496
+ const usedVariables=new Set([...ui.bindings.flatMap(b=>readVariables(b.value)),...ui.transitions.flatMap(t=>[...t.stateReads,...t.stateWrites.map(w=>w.variableKey)]),...[...presentation.tabs,...presentation.regions,...presentation.collections].map(t=>t.variableKey).filter(Boolean),...presentation.collections.flatMap(c=>c.requires.map(r=>r.variableKey))]);
2497
+ ui.variables=ui.variables.filter(v=>usedVariables.has(v.variableKey));
2498
+ // Repeated instances of the same unsupported source control share one source gap.
2499
+ const sourceGaps=new Map();
2500
+ for(const row of ui.uncovered){const key=JSON.stringify([row.routeKey,row.source,row.event,row.reason]);const prior=sourceGaps.get(key);if(prior)prior.nodeId=null;else sourceGaps.set(key,{...row});}
2501
+ ui.uncovered=[...sourceGaps.values()];
1743
2502
  manifest.routeTree = assembleRouteTree(manifest, ui, renderDefinitions, compositions.navigation);
2503
+ presentation.conditions=[...new Map(presentation.conditions.map(c=>[c.conditionKey,c])).values()];
2504
+ presentation.regions=presentation.regions.filter(r=>concrete.has(r.nodeId)&&r.options.every(o=>concrete.has(o.nodeId)));
2505
+ presentation.tabs=presentation.tabs.filter(t=>concrete.has(t.nodeId)&&t.items.length&&t.items.every(i=>concrete.has(i.nodeId)));
2506
+ presentation.attributes=presentation.attributes.filter(a=>concrete.has(a.nodeId));presentation.attributeSets=presentation.attributeSets.filter(s=>presentation.attributes.some(a=>a.setKey===s.setKey));
2507
+ presentation.navigation=sourceRedirects({ts,modules,binding,functionFrom,externalImport,sourceLocation,constant:(node,module)=>{const result=evalExpression(node,module,{});return isUnknown(result)?undefined:result;},pages,components:[...components.values()],documents});
2508
+ manifest.routeTree=attachUiStructure(manifest.routeTree,presentation,documents);
2509
+ // Discarded adapter children have no concrete UI. Keep every document and
2510
+ // shared-fragment mapping, but do not serialize unreachable element records.
2511
+ const referencedElements=new Set([...documents.flatMap(d=>d.nodes.map(n=>n.elementKey)),...presentation.fragments.flatMap(f=>f.sourceElements)]);
2512
+ manifest.elements=manifest.elements.filter(element=>referencedElements.has(element.elementKey));
2513
+ // Each navigation row references its exact existing interaction. The original
2514
+ // interaction ID remains in the authoritative array, instead of being repeated.
2515
+ const interactionIndices=new Map(manifest.interactions.map((row,index)=>[row.interactionKey,index]));
2516
+ for(const row of manifest.routeTree.navigation)row.interactionKey=interactionIndices.get(row.interactionKey);
2517
+ internGraphSources(manifest.routeTree);
1744
2518
  manifest.integrity.routeTreeDigest = routeIndexDigest(manifest.routeTree);
1745
2519
  const concreteNodes = new Map(documents.flatMap(doc => doc.nodes.map(node => [node.nodeId, node])));
1746
2520
  for (const target of manifest.routeTree.targets) {