@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
@@ -5,7 +5,7 @@ export function flattenComposition(root) {
5
5
  const entries = [];
6
6
  function visit(branch) {
7
7
  if (!branch) return;
8
- entries.push({ lane: branch.lane, file: branch.file, layers: branch.layers.map(layer => ({ file: layer.file, slots: layer.slots.map(slot => ({ name: slot.name, lane: slot.branch?.lane ?? null })) })) });
8
+ entries.push({ lane: branch.lane, file: branch.file, ...(branch.symbol ? { symbol: branch.symbol } : {}), layers: branch.layers.map(layer => ({ file: layer.file, ...(layer.symbol ? { symbol: layer.symbol } : {}), slots: layer.slots.map(slot => ({ name: slot.name, lane: slot.branch?.lane ?? null })) })) });
9
9
  for (const layer of branch.layers) for (const slot of layer.slots) visit(slot.branch);
10
10
  }
11
11
  visit(root);
@@ -36,6 +36,7 @@ export function renderTreeSchema(base) {
36
36
  p.renderNavigation=array(object({fromRenderKey:str,toRenderKey:str}),512);
37
37
  p.guards = array(object({ routeKey: str, selectorKeys: array({type:"integer",minimum:0,maximum:511}, 512) }), 200);
38
38
  p.compositions = array(object({ compositionKey: str, entries: array(object({ lane: str, file: str, layers: array(object({ file: str, slots: array(object({ name: str, lane: nullable(str) }), 32) }), 32) }), 128) }), 1000);
39
+ for (const shape of [p.compositions.items.properties.entries.items, p.compositions.items.properties.entries.items.properties.layers.items]) shape.properties.symbol = str;
39
40
  schema.required.push("renders", "guards", "compositions", "selectorPool", "renderNavigation");
40
41
  return schema;
41
42
  }
@@ -85,7 +86,7 @@ export function attachRenderIndex(graph, manifest, definitions, navigation=[]) {
85
86
  }
86
87
 
87
88
  export function validateRenderIndex(graph, manifest) {
88
- const contained = (file, lane) => lane === "." && ["react-router", "react-spa"].includes(manifest.routing.framework)
89
+ const contained = (file, lane) => lane === "." && ["react-router", "react-spa", "tanstack-router"].includes(manifest.routing.framework)
89
90
  ? !file.startsWith("/") && !file.split("/").includes("..")
90
91
  : file.startsWith(lane + "/");
91
92
  const unique = (rows, key) => {
@@ -95,14 +96,15 @@ export function validateRenderIndex(graph, manifest) {
95
96
  };
96
97
  const renders = unique(graph.renders, "renderKey"), compositions = unique(graph.compositions, "compositionKey"), guards = unique(graph.guards, "routeKey");
97
98
  const files = new Set(manifest.sourceFiles.map(file => file.file));
99
+ const knownSymbol = entry => !entry.symbol || manifest.components.some(component => component.file === entry.file && component.name === entry.symbol);
98
100
  for (const composition of compositions.values()) {
99
101
  if (composition.compositionKey !== compositionIdentity(composition.entries)) throw new Error("Composition identity mismatch");
100
102
  const lanes = new Set();
101
103
  for (const entry of composition.entries) {
102
- if (lanes.has(entry.lane) || !files.has(entry.file) || !contained(entry.file, entry.lane)) throw new Error("Invalid composition entry");
104
+ if (lanes.has(entry.lane) || !files.has(entry.file) || !contained(entry.file, entry.lane) || !knownSymbol(entry)) throw new Error("Invalid composition entry");
103
105
  lanes.add(entry.lane);
104
106
  for (const layer of entry.layers) {
105
- if (!files.has(layer.file) || !contained(layer.file, entry.lane)) throw new Error("Invalid composition layout");
107
+ if (!files.has(layer.file) || !contained(layer.file, entry.lane) || !knownSymbol(layer)) throw new Error("Invalid composition layout");
106
108
  const folder = layer.file.slice(0, layer.file.lastIndexOf("/"));
107
109
  for (const slot of layer.slots) if (slot.lane !== null && slot.lane !== folder + "/@" + slot.name) throw new Error("Invalid composition slot");
108
110
  }
@@ -0,0 +1,80 @@
1
+ /** Resolve a lazy route's component by AST proof only. No importer, mapper or loader is called. */
2
+ export function routeLazyComponent(node, module, { ts, resolution, resolveModule }) {
3
+ const unwrap = node => {
4
+ while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isAwaitExpression(node))) node = node.expression;
5
+ return node;
6
+ };
7
+ const fn = node => {
8
+ node = unwrap(node);
9
+ return node && !node.asteriskToken && (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) ? node : null;
10
+ };
11
+ const returned = node => node?.body && (ts.isBlock(node.body)
12
+ ? node.body.statements.length === 1 && ts.isReturnStatement(node.body.statements[0]) ? node.body.statements[0].expression : null
13
+ : node.body);
14
+ const plainParameter = parameter => parameter && ts.isIdentifier(parameter.name) && !parameter.initializer && !parameter.dotDotDotToken;
15
+ const reference = resolution.reference(node, module);
16
+ const lazy = reference && fn(reference.node);
17
+ if (!lazy || lazy.parameters.length) return null;
18
+ module = reference.module;
19
+ let body = unwrap(returned(lazy)), mapper;
20
+ if (body && ts.isCallExpression(body) && ts.isPropertyAccessExpression(body.expression) && body.expression.name.text === "then") {
21
+ if (body.arguments.length !== 1) return null;
22
+ mapper = resolution.reference(body.arguments[0], module);
23
+ if (!mapper) return null;
24
+ body = unwrap(body.expression.expression);
25
+ }
26
+ if (!body || !ts.isCallExpression(body) || body.expression.kind !== ts.SyntaxKind.ImportKeyword || body.arguments.length !== 1 || !ts.isStringLiteral(body.arguments[0])) return null;
27
+ const target = resolveModule(module.file, body.arguments[0].text);
28
+ if (!target) return null;
29
+ let exported = "Component";
30
+ if (mapper) {
31
+ let callback = fn(mapper.node);
32
+ if (!callback && ts.isCallExpression(unwrap(mapper.node))) {
33
+ const call = unwrap(mapper.node), factory = resolution.reference(call.expression, mapper.module), outer = factory && fn(factory.node);
34
+ if (!outer || outer.parameters.length !== call.arguments.length || outer.parameters.some(parameter => !plainParameter(parameter)) || outer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) return null;
35
+ callback = fn(returned(outer));
36
+ }
37
+ if (!callback || callback.parameters.length !== 1 || !plainParameter(callback.parameters[0]) || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) return null;
38
+ const namespace = callback.parameters[0].name.text, aliases = new Map();
39
+ let result = callback.body;
40
+ if (ts.isBlock(result)) {
41
+ const statements = result.statements;
42
+ if (!statements.length || !ts.isReturnStatement(statements.at(-1))) return null;
43
+ for (const statement of statements.slice(0, -1)) {
44
+ if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const)) return null;
45
+ for (const declaration of statement.declarationList.declarations) {
46
+ const value = unwrap(declaration.initializer);
47
+ if (!ts.isObjectBindingPattern(declaration.name) || !value || !ts.isIdentifier(value) || value.text !== namespace) return null;
48
+ for (const element of declaration.name.elements) {
49
+ if (!ts.isIdentifier(element.name) || element.initializer || element.name.text === namespace || aliases.has(element.name.text) || element.propertyName && ts.isComputedPropertyName(element.propertyName)) return null;
50
+ aliases.set(element.name.text, element.dotDotDotToken ? null : (element.propertyName?.text ?? element.name.text));
51
+ }
52
+ }
53
+ }
54
+ result = statements.at(-1).expression;
55
+ }
56
+ result = unwrap(result);
57
+ if (!result || !ts.isObjectLiteralExpression(result)) return null;
58
+ let selected, captured = false, unsafeBefore = false;
59
+ const effectful = node => {
60
+ if (ts.isCallExpression(node) || ts.isNewExpression(node) || ts.isAwaitExpression(node) || ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment || ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) return true;
61
+ return Boolean(ts.forEachChild(node, effectful));
62
+ };
63
+ for (const property of result.properties) {
64
+ if (ts.isSpreadAssignment(property)) { if (selected) return null; unsafeBefore = true; continue; }
65
+ if (!(ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) || ts.isComputedPropertyName(property.name)) return null;
66
+ if (property.name.text !== "Component") { if (!selected && ts.isPropertyAssignment(property) && effectful(property.initializer)) unsafeBefore = true; continue; }
67
+ if (selected) return null;
68
+ const value = unwrap(ts.isShorthandPropertyAssignment(property) ? property.name : property.initializer);
69
+ if (!value) return null;
70
+ if (ts.isIdentifier(value)) { selected = aliases.get(value.text); captured = Boolean(selected); }
71
+ else if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && value.expression.text === namespace && !value.questionDotToken) selected = value.name.text;
72
+ if (!selected || unsafeBefore && !captured) return null;
73
+ }
74
+ if (!selected) return null;
75
+ exported = selected;
76
+ }
77
+ const local = exported === "default" ? target.defaultName : target.exports.get(exported);
78
+ const component = local && resolution.component(target, local);
79
+ return component ? { file: component.module.file, symbol: component.name } : null;
80
+ }
@@ -4,6 +4,8 @@ import { dependency } from "./dependencies.mjs";
4
4
  import { canonical } from "./integrity.mjs";
5
5
  import { documentNodes } from './node-hashes.mjs';
6
6
  import { renderTreeSchema, attachRenderIndex, validateRenderIndex, routeIndexDigest } from "./render-index.mjs";
7
+ import { uiStructureSchema, validateUiStructure } from './ui-structure.mjs';
8
+ import {graphSource} from './graph-sources.mjs';
7
9
  const str = { type: "string", minLength: 1, maxLength: 2048 };
8
10
  const arr = (items, maxItems = 20000) => ({ type: "array", items, maxItems });
9
11
  const obj = properties => ({ type: "object", properties, required: Object.keys(properties), additionalProperties: false });
@@ -36,6 +38,12 @@ const routeTreeShape = obj({
36
38
  routeTreeShape.properties.targets.items.required=routeTreeShape.properties.targets.items.required.filter(key=>key!=='source');
37
39
  routeTreeShape.properties.targets.items.properties.subtreeHash = { type: "string", pattern: "^[a-f0-9]{64}$" };
38
40
  export const routeTreeSchema = renderTreeSchema(routeTreeShape);
41
+ routeTreeSchema.properties.sourcePool=arr(source,20000);
42
+ routeTreeSchema.properties.scopePool=arr(str,20000);
43
+ routeTreeSchema.properties.variables.items.properties.scopeKey={anyOf:[str,{type:'integer',minimum:0,maximum:19999}]};
44
+ for(const field of ['variables','bindings','transitions','states','navigation','uncovered'])routeTreeSchema.properties[field].items.properties.source={anyOf:[source,{type:'integer',minimum:0,maximum:19999}]};
45
+ routeTreeSchema.properties.navigation.items.properties.interactionKey={anyOf:[str,{type:'integer',minimum:0,maximum:19999}]};
46
+ routeTreeSchema.properties.ui=uiStructureSchema;
39
47
  const Ajv = dependency("ajv").value;
40
48
  const check = new Ajv({ allErrors: true }).compile({ ...routeTreeSchema, definitions: { uiExpression } });
41
49
  export function validateRouteTree(graph, manifest, verifiedSelectors, concreteDocuments = manifest.documents.map(d => ({...d, nodes:documentNodes(d)}))) {
@@ -54,7 +62,8 @@ export function validateRouteTree(graph, manifest, verifiedSelectors, concreteDo
54
62
  return node;
55
63
  }
56
64
  function sourceCheck(row) {
57
- const node=nodes.get(row.nodeId),source=row.source??node?.source??manifest.elements.find(e=>e.elementKey===node?.elementKey)?.source;
65
+ if(typeof row.source==='number'&&(!Number.isInteger(row.source)||!graph.sourcePool?.[row.source]))throw Error('Invalid pooled graph source');
66
+ const node=nodes.get(row.nodeId),source=graphSource(graph,row)??node?.source??manifest.elements.find(e=>e.elementKey===node?.elementKey)?.source;
58
67
  if (!source||!manifest.sourceFiles.some(file => file.file === source.file && file.sha256 === source.fileHash)) throw new Error("Route tree source mismatch");
59
68
  }
60
69
  for (const route of graph.routes) {
@@ -106,8 +115,8 @@ export function validateRouteTree(graph, manifest, verifiedSelectors, concreteDo
106
115
  }
107
116
  for (const navigation of graph.navigation) {
108
117
  mapped(navigation); require(maps.routes, navigation.targetRouteKey);
109
- const original = manifest.interactions.find(i => i.interactionKey === navigation.interactionKey);
110
- if (!original || original.pageKey !== navigation.routeKey || original.targetPageKey !== navigation.targetRouteKey || original.targetUrl !== navigation.targetUrl || canonical(original.targetParams) !== canonical(navigation.targetParams)) throw new Error("Route tree navigation mismatch");
118
+ const original = typeof navigation.interactionKey==='number'?manifest.interactions[navigation.interactionKey]:manifest.interactions.find(i => i.interactionKey === navigation.interactionKey);
119
+ if (!original || original.pageKey !== navigation.routeKey || original.nodeId !== navigation.nodeId || original.targetPageKey !== navigation.targetRouteKey || original.targetUrl !== navigation.targetUrl || canonical(original.targetParams) !== canonical(navigation.targetParams)) throw new Error("Route tree navigation mismatch");
111
120
  }
112
121
  const { JSDOM } = dependency("jsdom").value, documents = new Map(), targetIds = new Set();
113
122
  try {
@@ -124,7 +133,10 @@ export function validateRouteTree(graph, manifest, verifiedSelectors, concreteDo
124
133
  }
125
134
  } finally { for (const dom of documents.values()) dom.window.close(); }
126
135
  for (const collection of [graph.routes, graph.variables, graph.states, graph.bindings, graph.transitions, graph.navigation, graph.uncovered, graph.targets]) for (const row of collection) sourceCheck(row);
136
+ const refs=new Set([...graph.variables,...graph.bindings,...graph.transitions,...graph.states,...graph.navigation,...graph.uncovered].map(r=>r.source).filter(s=>typeof s==='number'));if(refs.size!==(graph.sourcePool?.length??0)||new Set((graph.sourcePool??[]).map(canonical)).size!==(graph.sourcePool?.length??0))throw Error('Invalid graph source pool');
137
+ const scopeRefs=new Set(graph.variables.map(v=>v.scopeKey).filter(k=>typeof k==='number'));if(scopeRefs.size!==(graph.scopePool?.length??0)||[...scopeRefs].some(k=>!Number.isInteger(k)||!graph.scopePool?.[k])||new Set(graph.scopePool??[]).size!==(graph.scopePool?.length??0))throw Error('Invalid graph scope pool');
127
138
  validateRenderIndex(graph, {...manifest, documents:concreteDocuments});
139
+ validateUiStructure(graph,manifest,concreteDocuments);
128
140
  return { routes: graph.routes.length, renders: graph.renders.length, transitions: graph.transitions.length, bindings: graph.bindings.length, digest: routeIndexDigest(graph) };
129
141
  }
130
142
 
package/src/routing.mjs CHANGED
@@ -26,7 +26,7 @@ export function pagesRoute(file) {
26
26
 
27
27
  /** Router configuration provenance is explicit for source-declared React routes. */
28
28
  export function sourcePageRoute(page, routing) {
29
- if (["react-router", "react-spa"].includes(routing.framework)) {
29
+ if (["react-router", "react-spa", "tanstack-router"].includes(routing.framework)) {
30
30
  const rows = routing.routeSources?.filter(
31
31
  (row) =>
32
32
  row.path === page.route &&
@@ -39,7 +39,7 @@ export const schema = {
39
39
  platforms: array({ enum: ["web", "ios", "android"] }, 3),
40
40
  }),
41
41
  routing: object({
42
- framework: { enum: ["next-app-router", "next-pages-router", "react-router", "react-spa"] },
42
+ framework: { enum: ["next-app-router", "next-pages-router", "react-router", "react-spa", "tanstack-router"] },
43
43
  basePath: str,
44
44
  trailingSlash: { type: "boolean" },
45
45
  routeSources: array(object({ path: str, source, componentFile: str, componentSymbol: str }), 200),
@@ -176,3 +176,10 @@ schema.properties.styles=array(object({styleKey:{type:'string',pattern:'^style_[
176
176
  schema.properties.documents.items.properties.styleKeys=array({type:'string',pattern:'^style_[a-f0-9]{64}$'},16);
177
177
  schema.properties.scripts=array(object({scriptKey:{type:'string',pattern:'^script_[a-f0-9]{64}$'},sha256:hash,js:{type:'string',maxLength:100000}}),8);
178
178
  schema.properties.documents.items.properties.scriptKey={type:'string',pattern:'^script_[a-f0-9]{64}$'};
179
+
180
+ // HTML and preorder mappings may use bounded lossless storage compression.
181
+ schema.properties.documents.items.properties.domPayload = object({
182
+ encoding: { const: "gzip-json-v1" },
183
+ bytes: { type: "integer", minimum: 1, maximum: 8_388_608 },
184
+ data: { type: "string", minLength: 1, maxLength: 11_184_812 },
185
+ });
@@ -0,0 +1,24 @@
1
+ /** Finite source-derived attributes. Only presentation text and classes can change. */
2
+ export function applySourceAttributes(doc, ui, renderKey, values) {
3
+ const sets = new Map((ui?.attributeSets ?? []).map((s) => [s.setKey, s])),
4
+ nodes = new Map(
5
+ [...doc.querySelectorAll("[data-iam-node]")].map((n) => [
6
+ n.getAttribute("data-iam-node"),
7
+ n,
8
+ ]),
9
+ );
10
+ for (const row of ui?.attributes ?? []) {
11
+ if (row.renderKey !== renderKey) continue;
12
+ const set = sets.get(row.setKey),
13
+ node = nodes.get(row.nodeId),
14
+ chosen = set?.cases.find((c) => c.value === values[row.variableKey]);
15
+ if (!node || !chosen) continue;
16
+ if (
17
+ !["class", "placeholder", "aria-label"].includes(set.property) ||
18
+ typeof chosen.text !== "string" ||
19
+ chosen.text.length > 4000
20
+ )
21
+ throw Error("Invalid source presentation attribute");
22
+ node.setAttribute(set.property, chosen.text);
23
+ }
24
+ }
@@ -0,0 +1,38 @@
1
+ // Bounded inert geometry from source data. Never imports a chart library or runs callbacks.
2
+ const number=v=>typeof v==='number'&&Number.isFinite(v)&&Math.abs(v)<=1e12;
3
+ const color=(v,fallback)=>typeof v==='string'&&/^(?:#[\da-f]{3,8}|[a-z]{1,24}|var\(--[a-z0-9-]+\)|rgba?\([\d.,% /]+\))$/i.test(v)?v:fallback;
4
+ const palette=['#8b5cf6','#94a3b8','#38bdf8','#22c55e','#f59e0b','#ef4444'];
5
+ const shape=(tag,attrs={},children=[])=>({tag,attrs,children});
6
+ export function chartGeometry({type,data,width=640,height=280,axes=true}){
7
+ width=number(width)&&width>=80&&width<=4096?width:640;height=number(height)&&height>=40&&height<=2048?height:280;
8
+ const labels=Array.isArray(data?.labels)?data.labels:[],sets=Array.isArray(data?.datasets)?data.datasets:[];
9
+ const valid=labels.length>0&&labels.length<=256&&sets.length>0&&sets.length<=16&&labels.every(v=>typeof v==='string'||number(v))&&sets.every(s=>Array.isArray(s?.data)&&s.data.length===labels.length&&s.data.every(number));
10
+ const children=[],text=(x,y,value,extra={})=>shape('text',{x,y,fill:'#888888','font-size':11,'font-family':'sans-serif',...extra},[String(value)]);
11
+ const pad=axes?{l:52,r:16,t:16,b:30}:{l:20,r:20,t:16,b:16},w=width-pad.l-pad.r,h=height-pad.t-pad.b;
12
+ if(!valid){children.push(shape('rect',{x:1,y:1,width:width-2,height:height-2,fill:'none',stroke:'#cbd5e1','stroke-dasharray':'5 5'}),text(width/2,height/2,'数据未观测 / Unobserved data',{'text-anchor':'middle'}));return {width,height,children,unobserved:true};}
13
+ if(!['line','bar','doughnut','pie','area'].includes(type))return null;
14
+ if(type==='doughnut'||type==='pie'){
15
+ const values=sets[0].data,total=values.reduce((a,b)=>a+b,0);if(total<=0||values.some(v=>v<0))return null;
16
+ const cx=width/2,cy=height/2,r=Math.max(1,Math.min(width,height)/2-16),inner=type==='pie'?0:r*.7;let start=-Math.PI/2;
17
+ values.forEach((v,i)=>{if(!v)return;const end=start+v/total*Math.PI*2,angle=Math.min(end-start,Math.PI*2-1e-7),e=start+angle,p=(radius,a)=>[cx+Math.cos(a)*radius,cy+Math.sin(a)*radius].map(n=>+n.toFixed(3)).join(' '),d=inner?`M ${p(r,start)} A ${r} ${r} 0 ${angle>Math.PI?1:0} 1 ${p(r,e)} L ${p(inner,e)} A ${inner} ${inner} 0 ${angle>Math.PI?1:0} 0 ${p(inner,start)} Z`:`M ${cx} ${cy} L ${p(r,start)} A ${r} ${r} 0 ${angle>Math.PI?1:0} 1 ${p(r,e)} Z`;children.push(shape('path',{d,fill:color(sets[0].backgroundColor?.[i],palette[i%palette.length])}));start=end;});
18
+ }else{
19
+ const values=sets.flatMap(s=>s.data),min=Math.min(0,...values),max=Math.max(1,...values),y=v=>pad.t+(max-v)/(max-min)*h,x=i=>pad.l+i/Math.max(1,labels.length-1)*w;
20
+ if(axes){for(let i=0;i<=4;i++){const v=min+(max-min)*i/4,at=y(v);children.push(shape('line',{x1:pad.l,x2:width-pad.r,y1:at,y2:at,stroke:'#e2e8f0','stroke-width':.5}),text(pad.l-8,at+4,Math.round(v),{'text-anchor':'end'}));}labels.forEach((label,i)=>{if(i%Math.max(1,Math.ceil(labels.length/12))===0)children.push(text(type==='bar'?pad.l+(i+.5)*w/labels.length:x(i),height-8,String(label).slice(0,18),{'text-anchor':'middle'}));});}
21
+ sets.forEach((set,j)=>{const stroke=color(set.borderColor,palette[j%palette.length]),fill=color(set.backgroundColor,stroke);if(type==='bar'){const band=w/labels.length,bw=band*.7/sets.length;set.data.forEach((v,i)=>children.push(shape('rect',{x:pad.l+i*band+band*.15+j*bw,y:Math.min(y(v),y(0)),width:bw,height:Math.abs(y(v)-y(0)),fill,rx:2})));}else{const points=set.data.map((v,i)=>`${+x(i).toFixed(3)},${+y(v).toFixed(3)}`).join(' ');if(set.fill===true||type==='area')children.push(shape('polygon',{points:`${pad.l},${y(0)} ${points} ${x(labels.length-1)},${y(0)}`,fill:stroke,'fill-opacity':.1}));children.push(shape('polyline',{points,fill:'none',stroke,'stroke-width':number(set.borderWidth)?Math.max(.5,Math.min(8,set.borderWidth)):2,'stroke-linejoin':'round'}));}});
22
+ }
23
+ return {width,height,children,unobserved:false};
24
+ }
25
+ export function sourceChart({ts,node,tag,owner,context,evaluate,imported}){
26
+ const module=owner.module,ev=n=>evaluate(n,module,context),primitive=imported(module,tag),props=opening=>Object.fromEntries(opening.attributes.properties.filter(a=>ts.isJsxAttribute(a)).map(a=>[a.name.text,!a.initializer?true:ts.isStringLiteral(a.initializer)?a.initializer.text:ts.isJsxExpression(a.initializer)?ev(a.initializer.expression):undefined]));
27
+ if(tag==='canvas'){
28
+ const configs=[];function visit(n){if(ts.isNewExpression(n)&&imported(module,n.expression.getText(module.source))?.specifier==='chart.js'&&imported(module,n.expression.getText(module.source))?.name==='Chart'&&n.arguments?.length===2&&ts.isObjectLiteralExpression(n.arguments[1]))configs.push(n.arguments[1]);ts.forEachChild(n,visit);}visit(owner.fn);
29
+ if(configs.length!==1)return null;
30
+ const config=ev(configs[0]),p=props(node),type=config?.type;if(typeof type!=='string')return null;
31
+ return chartGeometry({type,data:config.data,width:p.width,height:p.height,axes:config.options?.scales?.x?.display!==false||config.options?.scales?.y?.display!==false});
32
+ }
33
+ if(primitive?.specifier!=='recharts'||!['BarChart','LineChart','AreaChart','PieChart'].includes(primitive.name))return null;
34
+ const p=props(node),children=node.parent&&ts.isJsxElement(node.parent)?node.parent.children:[],parts=children.filter(c=>ts.isJsxElement(c)||ts.isJsxSelfClosingElement(c)).map(c=>ts.isJsxElement(c)?c.openingElement:c),series=[],type=primitive.name==='BarChart'?'bar':primitive.name==='AreaChart'?'area':primitive.name==='PieChart'?'pie':'line';let key;
35
+ for(const part of parts){const imp=imported(module,part.tagName.getText(module.source)),v=props(part);if(imp?.specifier!=='recharts')continue;if(imp.name==='XAxis')key=v.dataKey;if(['Bar','Line','Area','Pie'].includes(imp.name))series.push(v);}
36
+ const data=p.data,labels=Array.isArray(data)?data.map((r,i)=>key?r?.[key]:i+1):[];
37
+ return chartGeometry({type,data:{labels,datasets:series.map(s=>({data:Array.isArray(data)?data.map(r=>r?.[s.dataKey]):[],borderColor:s.stroke,backgroundColor:s.fill,fill:type==='area'}))},width:p.width,height:p.height??350});
38
+ }
@@ -0,0 +1,241 @@
1
+ import { JSDOM, VirtualConsole } from "jsdom";
2
+ import { identity } from "./integrity.mjs";
3
+
4
+ /** Read state dependencies from local AST aliases. This never invokes source functions. */
5
+ export function collectionStateReads(ts, node, ctx) {
6
+ const reads = new Set(),
7
+ seen = new Set();
8
+ let steps = 0;
9
+ function visit(n) {
10
+ if (!n || ++steps > 3000) return;
11
+ if (ts.isIdentifier(n)) {
12
+ if (ctx.__iamVariableKeys[n.text])
13
+ reads.add(ctx.__iamVariableKeys[n.text]);
14
+ else if (ctx.__iamPropBindings?.has(n.text)) {
15
+ const walk = (e) => {
16
+ if (e?.op === "var") reads.add(e.key);
17
+ for (const v of Object.values(e ?? {}))
18
+ if (v && typeof v === "object") {
19
+ if (Array.isArray(v)) v.forEach(walk);
20
+ else walk(v);
21
+ }
22
+ };
23
+ walk(ctx.__iamPropBindings.get(n.text));
24
+ } else if (ctx.__iamLocals.has(n.text) && !seen.has(n.text)) {
25
+ seen.add(n.text);
26
+ visit(ctx.__iamLocals.get(n.text));
27
+ }
28
+ }
29
+ ts.forEachChild(n, visit);
30
+ }
31
+ visit(node);
32
+ return [...reads];
33
+ }
34
+
35
+ /** Store one inert source list, shared by its repeated source instances. No SDK targets or handlers. */
36
+ export function sourceCollectionFragment(html, source, itemCount, record) {
37
+ const dom = new JSDOM("<body>" + html + "</body>", {
38
+ virtualConsole: new VirtualConsole(),
39
+ });
40
+ try {
41
+ const body = dom.window.document.body,
42
+ elementKeys = [],
43
+ sourceElements = [],
44
+ classNames = [];
45
+ for (const node of body.querySelectorAll("*")) {
46
+ const mapped = record.get(node.getAttribute("data-iam-node"));
47
+ if (!mapped) throw Error("Collection source node is unmapped");
48
+ let ref = sourceElements.indexOf(mapped.elementKey);
49
+ if (ref < 0) {
50
+ ref = sourceElements.length;
51
+ sourceElements.push(mapped.elementKey);
52
+ }
53
+ elementKeys.push(ref);
54
+ for (const attr of [...node.attributes])
55
+ if (
56
+ attr.name.startsWith("data-iam-") ||
57
+ [
58
+ "id",
59
+ "data-indiana-id",
60
+ "data-testid",
61
+ "data-test",
62
+ "data-qa",
63
+ ].includes(attr.name)
64
+ )
65
+ node.removeAttribute(attr.name);
66
+ const classes = node.getAttribute("class");
67
+ if (classes && classes.length > 64) {
68
+ let index = classNames.indexOf(classes);
69
+ if (index < 0) {
70
+ index = classNames.length;
71
+ classNames.push(classes);
72
+ }
73
+ node.removeAttribute("class");
74
+ node.setAttribute("data-iam-fragment-class", String(index));
75
+ }
76
+ }
77
+ const fragment = {
78
+ html: body.innerHTML,
79
+ elementKeys,
80
+ sourceElements,
81
+ classNames,
82
+ source,
83
+ itemCount,
84
+ };
85
+ return { fragmentKey: identity("ui-fragment", fragment), ...fragment };
86
+ } finally {
87
+ dom.window.close();
88
+ }
89
+ }
90
+
91
+ /** Restore presentation only; injected rows deliberately have no canonical DOM identity. */
92
+ export function checkedCollectionFragment(doc, fragment) {
93
+ if (
94
+ typeof fragment?.html !== "string" ||
95
+ fragment.html.length > 262144 ||
96
+ !Array.isArray(fragment.elementKeys) ||
97
+ !Array.isArray(fragment.sourceElements) ||
98
+ !Array.isArray(fragment.classNames) ||
99
+ fragment.sourceElements.length > 2000 ||
100
+ fragment.classNames.length > 128 ||
101
+ fragment.classNames.some((s) => typeof s !== "string" || s.length > 4000) ||
102
+ fragment.elementKeys.some(
103
+ (i) =>
104
+ !Number.isInteger(i) || i < 0 || i >= fragment.sourceElements.length,
105
+ ) ||
106
+ fragment.elementKeys.length > 2000
107
+ )
108
+ throw Error("Invalid shared source fragment");
109
+ const template = doc.createElement("template");
110
+ template.innerHTML = fragment.html;
111
+ const nodes = [...template.content.querySelectorAll("*")],
112
+ allowed = new Set(
113
+ "div span p button input svg path circle rect line polyline polygon ellipse g title".split(
114
+ " ",
115
+ ),
116
+ );
117
+ if (nodes.length !== fragment.elementKeys.length)
118
+ throw Error("Shared source fragment mapping mismatch");
119
+ for (const node of nodes) {
120
+ if (node.hasAttribute("data-iam-fragment-class")) {
121
+ const raw = node.getAttribute("data-iam-fragment-class"),
122
+ index = Number(raw);
123
+ if (
124
+ String(index) !== raw ||
125
+ !Number.isInteger(index) ||
126
+ index < 0 ||
127
+ index >= fragment.classNames.length
128
+ )
129
+ throw Error("Invalid source fragment class");
130
+ node.setAttribute("class", fragment.classNames[index]);
131
+ node.removeAttribute("data-iam-fragment-class");
132
+ }
133
+ if (!allowed.has(node.tagName.toLowerCase()))
134
+ throw Error("Active shared source fragment element");
135
+ for (const attr of node.attributes)
136
+ if (
137
+ !/^(?:class|title|type|value|placeholder|disabled|hidden|checked|tabindex|width|height|viewBox|viewbox|xmlns|d|fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|cx|cy|r|rx|ry|x|y|x1|x2|y1|y2|points|aria-[a-z-]+|data-(?:slot|state|active|disabled|pressed))$/.test(
138
+ attr.name,
139
+ )
140
+ )
141
+ throw Error("Active shared source fragment attribute: " + attr.name);
142
+ for (const key of ["fill", "stroke"])
143
+ if (
144
+ node.hasAttribute(key) &&
145
+ !/^(?:none|currentColor|transparent|#[a-f\d]{3,8})$/i.test(
146
+ node.getAttribute(key),
147
+ )
148
+ )
149
+ throw Error("External source fragment paint");
150
+ if (
151
+ node.hasAttribute("xmlns") &&
152
+ node.getAttribute("xmlns") !== "http://www.w3.org/2000/svg"
153
+ )
154
+ throw Error("Invalid source fragment namespace");
155
+ }
156
+ return { content: template.content, nodes };
157
+ }
158
+ export function applySourceCollections(doc, ui, renderKey, values, enabled) {
159
+ const choices = [],
160
+ fragments = new Map((ui?.fragments ?? []).map((f) => [f.fragmentKey, f]));
161
+ for (const row of ui?.collections ?? []) {
162
+ if (row.renderKey !== renderKey) continue;
163
+ const root = [...doc.querySelectorAll("[data-iam-node]")].find(
164
+ (n) => n.getAttribute("data-iam-node") === row.nodeId,
165
+ );
166
+ if (!root) continue;
167
+ root.replaceChildren();
168
+ if (!enabled) continue;
169
+ if (root.parentElement?.closest("[hidden]")) continue;
170
+ const selected = row.cases.find((c) => c.value === values[row.variableKey]),
171
+ satisfied = row.requires.every((r) => values[r.variableKey] === r.value);
172
+ root.hidden = false;
173
+ root.setAttribute("inert", "");
174
+ if (!selected || !satisfied) {
175
+ root.textContent = "未观测 · 此筛选组合未重建";
176
+ root.setAttribute("data-iam-observation", "unobserved-collection");
177
+ continue;
178
+ }
179
+ const fragment = fragments.get(selected.fragmentKey);
180
+ if (!fragment) throw Error("Missing source collection fragment");
181
+ const parsed = checkedCollectionFragment(doc, fragment);
182
+ root.replaceChildren(parsed.content);
183
+ parsed.nodes.forEach((n, index) => {
184
+ n.setAttribute("data-iam-preview-only", "true");
185
+ n.setAttribute(
186
+ "data-iam-source-element",
187
+ fragment.sourceElements[fragment.elementKeys[index]],
188
+ );
189
+ });
190
+ choices.push({
191
+ collectionKey: row.collectionKey,
192
+ value: selected.value,
193
+ itemCount: fragment.itemCount,
194
+ source: row.source,
195
+ });
196
+ }
197
+ return choices;
198
+ }
199
+
200
+ /** Result, error and empty branches of an unexecuted query are all unobserved. */
201
+ export function markUnobservedData(doc, ui, enabled, selections = new Map()) {
202
+ doc.querySelectorAll("[data-iam-preview-note]").forEach((n) => n.remove());
203
+ const unobserved = [];
204
+ if (!enabled) return unobserved;
205
+ const source = new Map(
206
+ (ui?.conditions ?? []).map((c) => [c.conditionKey, c]),
207
+ );
208
+ for (const root of doc.querySelectorAll(
209
+ '[data-iam-condition][data-iam-alternative="true"]',
210
+ )) {
211
+ const condition = source.get(root.getAttribute("data-iam-condition")),
212
+ id = root.getAttribute("data-iam-node");
213
+ if (!condition?.observation || selections.has(id)) continue;
214
+ const note = doc.createElement("div");
215
+ note.setAttribute("data-iam-preview-note", "unobserved");
216
+ note.className = "iam-observation-note";
217
+ note.setAttribute("role", "status");
218
+ note.textContent =
219
+ condition.observation === "remote" ? "远程数据未观测" : "数据未观测";
220
+ note.title = condition.expression;
221
+ root.before(note);
222
+ if (!note.closest("[hidden]"))
223
+ unobserved.push({
224
+ groupKey: id,
225
+ expression: condition.expression,
226
+ source: condition.source,
227
+ });
228
+ }
229
+ for (const root of doc.querySelectorAll(
230
+ '[data-iam-observation="remote"],[data-iam-observation="unobserved"]',
231
+ )) {
232
+ root.hidden = false;
233
+ root.textContent = "未观测";
234
+ root.classList.add("iam-unobserved-value");
235
+ }
236
+ for (const field of doc.querySelectorAll("[data-iam-input-unobserved]")) {
237
+ field.setAttribute("placeholder", "未观测");
238
+ field.setAttribute("title", "此字段的数据未观测");
239
+ }
240
+ return unobserved;
241
+ }
@@ -0,0 +1,50 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { dependency } from "./dependencies.mjs";
4
+ import { sha256 } from "./integrity.mjs";
5
+
6
+ /** Select a browser import graph without evaluating the entry or its configuration. */
7
+ export async function sourceEntry({ root, modules, resolveModule, ts, entry }) {
8
+ let htmlSource;
9
+ if (entry === undefined) {
10
+ const file = path.join(root, "index.html");
11
+ let stat;
12
+ try { stat = await fs.lstat(file); }
13
+ catch (error) { if (error.code !== "ENOENT") throw error; }
14
+ if (stat) {
15
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256_000)
16
+ throw Error("Browser entry HTML must be a bounded local file");
17
+ const bytes = await fs.readFile(file), text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
18
+ const { JSDOM } = dependency("jsdom").value;
19
+ const dom = new JSDOM(text);
20
+ try {
21
+ const scripts = [...dom.window.document.querySelectorAll('script[type="module"]')];
22
+ if (scripts.length !== 1 || !scripts[0].hasAttribute("src") || scripts[0].textContent.trim() || dom.window.document.querySelector("base[href]"))
23
+ throw Error("Ambiguous browser entry: set entry to one local source module");
24
+ entry = scripts[0].getAttribute("src").replace(/^\/(?!\/)/, "");
25
+ htmlSource = { file: "index.html", sha256: sha256(bytes), bytes: bytes.length };
26
+ } finally { dom.window.close(); }
27
+ }
28
+ }
29
+ if (entry === undefined) return { discoveryModules: modules };
30
+ if (typeof entry !== "string" || !entry || entry.startsWith("/") || /[\\?#:\s]/.test(entry) || entry.split("/").some(part => !part || part === "." || part === "..") || !modules.has(entry))
31
+ throw Error("entry must name one source module relative to the scanned root");
32
+ const selected = new Map();
33
+ function visit(module) {
34
+ if (selected.has(module.file)) return;
35
+ selected.set(module.file, module);
36
+ const dependencies = new Set([...module.imports.values()].map(ref => ref.specifier));
37
+ function imports(node) {
38
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) dependencies.add(node.moduleSpecifier.text);
39
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length === 1 && ts.isStringLiteral(node.arguments[0])) dependencies.add(node.arguments[0].text);
40
+ ts.forEachChild(node, imports);
41
+ }
42
+ imports(module.source);
43
+ for (const specifier of dependencies) {
44
+ const target = resolveModule(module.file, specifier);
45
+ if (target) visit(target);
46
+ }
47
+ }
48
+ visit(modules.get(entry));
49
+ return { discoveryModules: selected, htmlSource, entry };
50
+ }
@@ -0,0 +1,26 @@
1
+ // Reviewable UI environment projections. No browser globals or merchant callbacks are accessed.
2
+ export function uiEnvironmentPrimitive({ts,node,module,binding,imported,evaluate}){
3
+ const returned=fn=>!fn?.body?null:ts.isArrowFunction(fn)&&!ts.isBlock(fn.body)?fn.body:ts.isBlock(fn?.body)&&fn.body.statements.length===1&&ts.isReturnStatement(fn.body.statements[0])?fn.body.statements[0].expression:null;
4
+ const call=node&&ts.isCallExpression(node)?node:null;if(!call)return null;
5
+ const ref=ts.isIdentifier(call.expression)&&binding(module,call.expression.text),fn=ref?.node;
6
+ const expression=fn&&returned(fn),store=expression&&ts.isCallExpression(expression)?expression:call,owner=expression?ref.module:module;
7
+ const primitive=imported(owner,store.expression.getText(owner.source));
8
+ if(primitive?.specifier==='react'&&primitive.name==='useSyncExternalStore'&&store.arguments.length===3){
9
+ const snapshot=returned(store.arguments[1]),fallback=returned(store.arguments[2]);
10
+ if(!snapshot||!ts.isPropertyAccessExpression(snapshot)||snapshot.name.text!=='matches'||!ts.isCallExpression(snapshot.expression))return null;
11
+ const media=snapshot.expression;if(media.expression.getText(owner.source)!=='window.matchMedia'||media.arguments.length!==1||fallback?.kind!==ts.SyntaxKind.FalseKeyword)return null;
12
+ const query=evaluate(media.arguments[0],owner,{});
13
+ if(typeof query!=='string'||!/^\((?:max|min)-width: [1-9]\d{1,3}px\)$/.test(query))return null;
14
+ return {kind:'viewport',query};
15
+ }
16
+ // Only presentation preference names, with an explicit source SSR guard. Auth/session cookies stay opaque.
17
+ if(ref&&ts.isFunctionDeclaration(fn)&&fn.body&&ts.isBlock(fn.body)&&call.arguments.length===1){
18
+ const key=evaluate(call.arguments[0],module,{}),guard=fn.body.statements[0];
19
+ if(typeof key!=='string'||!/^(?:sidebar_state|layout_(?:variant|collapsible)|theme|color_scheme)$/.test(key))return null;
20
+ if(!guard||!ts.isIfStatement(guard)||guard.expression.getText(ref.module.source).replace(/\s+/g,'')!=="typeofdocument==='undefined'"&&guard.expression.getText(ref.module.source).replace(/\s+/g,'')!=='typeofdocument==="undefined"')return null;
21
+ const branch=ts.isBlock(guard.thenStatement)?guard.thenStatement.statements[0]:guard.thenStatement;
22
+ if(!branch||!ts.isReturnStatement(branch)||branch.expression?.getText(ref.module.source)!=='undefined')return null;
23
+ return {kind:'empty-preference'};
24
+ }
25
+ return null;
26
+ }
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {sha256} from './integrity.mjs';
4
+ /** Only literal raster references in projected JSX. No crawling, URL fetch or symlink expansion. */
5
+ export async function referencedRasterSources(root,urls){
6
+ const files=[],seen=new Set();let total=0;
7
+ for(const url of [...urls].sort()){
8
+ if(typeof url!=='string'||!/^\/(?!\/)[A-Za-z0-9_./-]+\.(?:png|jpe?g|webp|gif)$/i.test(url))continue;
9
+ const file=url.startsWith('/__iam_source__/')?url.slice('/__iam_source__/'.length):'public'+url;
10
+ if(seen.has(file))continue;seen.add(file);
11
+ if(file.split('/').some(p=>!p||p.startsWith('.'))||/(?:^|\/)(?:api|server|secrets?|credentials)(?:\/|\.)/i.test(file))throw Error('Unsafe referenced image path');
12
+ try{
13
+ let current=root;for(const part of file.split('/')){current=path.join(current,part);if((await fs.lstat(current)).isSymbolicLink())throw Error('Referenced image symlinks are not admitted');}
14
+ const real=await fs.realpath(current),stat=await fs.stat(real);
15
+ if(!real.startsWith(root+path.sep)||!stat.isFile()||stat.size>1048576||(total+=stat.size)>6291456||files.length>=128)throw Error('Referenced image scope or budget exceeded');
16
+ const bytes=await fs.readFile(real);if(bytes.length!==stat.size)throw Error('Referenced image changed while reading');
17
+ files.push({file,sha256:sha256(bytes),bytes:bytes.length});
18
+ }catch(error){if(error.code!=='ENOENT')throw error;}
19
+ }
20
+ return files;
21
+ }