@proteus-vue/component-ir 0.3.0-beta.11 → 0.3.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -15,3 +15,12 @@ export { classifySpecApi, classifySpecComponent, auditSpecCoverage, auditSpecOve
15
15
  export type { MpSpecStatus, MpSpecClass, MpOfficialSpec, SpecCoverageReport } from './mp-spec-coverage';
16
16
  export { DEGRADATION_TABLE, MP_UNSUPPORTED_PROPS, degradeProp, degradationOf, auditDegradation, formatDegradationReport, checkPropDegradation, collectPropDiagnostics, RULE_REGISTERED_PROPS, HOST_FALLBACK_TAGS, PROP_NO_DEGRADATION, } from './degradation';
17
17
  export type { DegradationEnd, DegradationTier, DegradationEntry, DegradationMap, DegradationIssue, DegradationReport, PropDiagnostic, } from './degradation';
18
+ export type { PKind, ResolvedLength, Edges, LayoutProps, PaintProps, TextProps, PaintHint, PProps, PFlags, PNode, PTree, DynamicBinding, UpdateKind, PDiagnostic, } from './pnode';
19
+ export { resolveLength, parseStyleString, normalizeStyleDecls, normalizeStyleString, expandEdges, parseTransform, camel, splitTopLevel } from './pnode-style';
20
+ export type { LengthContext, NormalizeOptions, NormalizeResult } from './pnode-style';
21
+ export { analyzePTree, flattenRate, groupBindingsByNode } from './pnode-analyze';
22
+ export type { AnalyzeOptions, AnalyzeResult, NodeFacts } from './pnode-analyze';
23
+ export { buildPTree, buildPTreeFromComponentIR, rawFromComponentIR, kindFromSemantic, inferUpdateKind, inferOpCode } from './pnode-build';
24
+ export type { PRawNode, BuildOptions } from './pnode-build';
25
+ export { formatPTrace, formatPNodeLine, decisionOf, walkPTree } from './pnode-trace';
26
+ export type { PTraceOptions } from './pnode-trace';
package/dist/index.js CHANGED
@@ -20,6 +20,8 @@ var SEMANTIC_ENUM = [
20
20
  // ★#405 语义登记批:组件库扩展组件(Fluid S1-S4 + 工程兜底,语义层待 L2 落地——G-31.4 降级 planned)
21
21
  "layout.aspect",
22
22
  "layout.zone",
23
+ "layout.formfactor",
24
+ // ★★Fluid System v2:柔性形态容器(形态编排)
23
25
  // —— 基础 UI 原语(G-32 ② UI 18 + 既有按钮)——
24
26
  "ui.text",
25
27
  "ui.heading",
@@ -191,6 +193,8 @@ var TAG_SEMANTIC_MAP = {
191
193
  // ——同一语义由两个标签提供;本条与 p-view 是并列的别名情形,非「先例」关系)
192
194
  "p-aspect": "layout.aspect",
193
195
  "p-zone": "layout.zone",
196
+ "p-formfactor": "layout.formfactor",
197
+ // ★★Fluid System v2:柔性形态容器
194
198
  "p-loading": "ui.loading",
195
199
  "p-scale": "ui.scale",
196
200
  "p-skeleton": "ui.skeleton",
@@ -386,6 +390,16 @@ var SEMANTIC_BACKEND_MAP = {
386
390
  flutter: "NavigationRail",
387
391
  headless: "sidebar"
388
392
  },
393
+ // ★★Fluid System v2(2026-09-26):柔性形态容器——各端按形态画像编排拓扑/能力/密度/缩放
394
+ "layout.formfactor": {
395
+ "vue-dom": "div.proteus-formfactor",
396
+ "native-ios": "UIViewController.formFactor",
397
+ "native-android": "FormFactorLayout",
398
+ "native-harmony": "GridRow.formFactor",
399
+ skyline: "view.formfactor",
400
+ flutter: "LayoutBuilder.formFactor",
401
+ headless: "formfactor"
402
+ },
389
403
  // —— 基础 UI 原语 ——
390
404
  "ui.text": {
391
405
  "vue-dom": "span",
@@ -995,6 +1009,8 @@ var LAYOUT = [
995
1009
  // ★#405 语义登记批:Fluid 体系剩余组件(语义层待多端映射 → planned L2——G-31.4 不足 3 端降级)
996
1010
  { id: "L13", kind: "layout", semantic: "layout.aspect", tag: "p-aspect", props: ["ratio", "maxWidth"], mpEquiv: "\u65E0\uFF08\u7EB5\u6A2A\u6BD4\u5BB9\u5668\uFF09", tier: "L2", status: "planned" },
997
1011
  { id: "L14", kind: "layout", semantic: "layout.zone", tag: "p-zone", props: ["designWidth"], mpEquiv: "\u65E0\uFF08\u5BB9\u5668\u65AD\u70B9\u5206\u533A\uFF09", tier: "L2", status: "planned" },
1012
+ // ★★Fluid System v2(2026-09-26):柔性形态容器——按设备形态自动编排布局拓扑/能力/密度/缩放
1013
+ { id: "L26", kind: "layout", semantic: "layout.formfactor", tag: "p-formfactor", props: ["declared", "width", "height"], mpEquiv: "\u65E0\uFF08\u5F62\u6001\u7F16\u6392\u5BB9\u5668\uFF1Aglance/stack/duo/rail-split/rail-grid/dashboard/hero-focus-row\uFF09", tier: "L2", status: "implemented" },
998
1014
  // ★批次 7(2026-09-18)未登记组件补登记:安全区(刘海/折叠屏/降级)+ 侧边导航容器
999
1015
  { id: "L15", kind: "layout", semantic: "layout.safe", tag: "p-safe", props: ["area", "fold", "fallback"], mpEquiv: "\u65E0\uFF08\u5B89\u5168\u533A\u8BED\u4E49\u5BB9\u5668\uFF09", tier: "L1", status: "implemented" },
1000
1016
  { id: "L16", kind: "layout", semantic: "layout.sidebar", tag: "p-sidebar", props: ["minSidebarWidth", "navWidth", "designWidth", "toggleLabel"], mpEquiv: "\u65E0\uFF08\u5BB9\u5668\u65AD\u70B9\u4FA7\u680F\uFF09", tier: "L1", status: "implemented" }
@@ -2317,6 +2333,688 @@ function collectPropDiagnostics(tag, props, end, table = DEGRADATION_TABLE) {
2317
2333
  }
2318
2334
  return out;
2319
2335
  }
2336
+
2337
+ // src/pnode-style.ts
2338
+ function resolveLength(raw, ctx = { axis: "horizontal" }) {
2339
+ const v = raw.trim().toLowerCase();
2340
+ if (!v) return void 0;
2341
+ if (v === "0" || /^0(\.0+)?$/.test(v)) return { kind: "absolute", dp: 0 };
2342
+ if (v === "auto") return void 0;
2343
+ const m = /^(-?\d*\.?\d+)\s*([a-z%]*)$/.exec(v);
2344
+ if (!m) return void 0;
2345
+ const n = Number(m[1]);
2346
+ if (!Number.isFinite(n)) return void 0;
2347
+ switch (m[2]) {
2348
+ case "":
2349
+ case "px":
2350
+ return { kind: "absolute", dp: n };
2351
+ case "%":
2352
+ return {
2353
+ kind: "ratio",
2354
+ ratio: n / 100,
2355
+ base: ctx.axis === "vertical" ? "parentHeight" : "parentWidth"
2356
+ };
2357
+ case "vw":
2358
+ return { kind: "ratio", ratio: n / 100, base: "viewportWidth" };
2359
+ case "vh":
2360
+ return { kind: "ratio", ratio: n / 100, base: "viewportHeight" };
2361
+ case "rpx":
2362
+ return { kind: "ratio", ratio: n / 750, base: "viewportWidth" };
2363
+ case "em":
2364
+ return { kind: "ratio", ratio: n, base: "fontSize" };
2365
+ case "rem":
2366
+ return { kind: "ratio", ratio: n, base: "rootFontSize" };
2367
+ case "pt":
2368
+ return { kind: "absolute", dp: n };
2369
+ default:
2370
+ return void 0;
2371
+ }
2372
+ }
2373
+ function parseStyleString(style) {
2374
+ const out = {};
2375
+ for (const decl of style.split(";")) {
2376
+ const i = decl.indexOf(":");
2377
+ if (i <= 0) continue;
2378
+ const key = camel(decl.slice(0, i).trim());
2379
+ const val = decl.slice(i + 1).trim();
2380
+ if (key && val) out[key] = val;
2381
+ }
2382
+ return out;
2383
+ }
2384
+ function camel(k) {
2385
+ return k.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
2386
+ }
2387
+ function splitTopLevel(value) {
2388
+ const parts = [];
2389
+ let depth = 0;
2390
+ let cur = "";
2391
+ for (const ch of value) {
2392
+ if (ch === "(") depth++;
2393
+ else if (ch === ")") depth--;
2394
+ if (/\s/.test(ch) && depth === 0) {
2395
+ if (cur) parts.push(cur);
2396
+ cur = "";
2397
+ continue;
2398
+ }
2399
+ cur += ch;
2400
+ }
2401
+ if (cur) parts.push(cur);
2402
+ return parts;
2403
+ }
2404
+ function expandEdges(parts) {
2405
+ const [a, b, c, d] = parts;
2406
+ if (parts.length === 1) return { top: a, right: a, bottom: a, left: a };
2407
+ if (parts.length === 2) return { top: a, right: b, bottom: a, left: b };
2408
+ if (parts.length === 3) return { top: a, right: b, bottom: c, left: b };
2409
+ return { top: a, right: b, bottom: c, left: d };
2410
+ }
2411
+ function parseTransform(raw) {
2412
+ const t = {};
2413
+ let threeD = false;
2414
+ for (const m of raw.matchAll(/([a-zA-Z0-9]+)\(([^)]*)\)/g)) {
2415
+ const fn = m[1].toLowerCase();
2416
+ const args = m[2].split(",").map((s) => s.trim());
2417
+ const num = (i) => Number.parseFloat(args[i] ?? "0");
2418
+ switch (fn) {
2419
+ case "translatex":
2420
+ t.translateX = resolveLength(args[0] ?? "0")?.kind === "absolute" ? resolveLength(args[0]).dp : num(0);
2421
+ break;
2422
+ case "translatey":
2423
+ t.translateY = resolveLength(args[0] ?? "0")?.kind === "absolute" ? resolveLength(args[0]).dp : num(0);
2424
+ break;
2425
+ case "translate":
2426
+ t.translateX = num(0);
2427
+ t.translateY = num(1);
2428
+ break;
2429
+ case "scale":
2430
+ t.scale = args.length > 1 ? num(0) : num(0);
2431
+ break;
2432
+ case "rotate":
2433
+ t.rotate = args[0]?.endsWith("deg") ? num(0) : num(0);
2434
+ break;
2435
+ // 3D(L3:创建合成层)
2436
+ case "translatez":
2437
+ case "translate3d":
2438
+ case "rotatex":
2439
+ case "rotatey":
2440
+ case "rotate3d":
2441
+ case "perspective":
2442
+ case "matrix3d":
2443
+ threeD = true;
2444
+ break;
2445
+ default:
2446
+ threeD = true;
2447
+ break;
2448
+ }
2449
+ }
2450
+ return { t, threeD };
2451
+ }
2452
+ var L3_PROPS = /* @__PURE__ */ new Set([
2453
+ "zIndex",
2454
+ "boxShadow",
2455
+ "filter",
2456
+ "backdropFilter",
2457
+ "willChange",
2458
+ "position"
2459
+ // 仅 fixed/sticky 属 L3(由值判定)
2460
+ ]);
2461
+ var OUT_OF_PROFILE = /* @__PURE__ */ new Set([
2462
+ "gridTemplateColumns",
2463
+ "gridTemplateRows",
2464
+ "gridArea",
2465
+ "gridColumn",
2466
+ "gridRow",
2467
+ "float",
2468
+ "clear",
2469
+ "animation",
2470
+ "animationName",
2471
+ "animationDuration",
2472
+ "transition",
2473
+ "transitionProperty",
2474
+ "cursor",
2475
+ "userSelect",
2476
+ "content",
2477
+ "clipPath",
2478
+ "mask",
2479
+ "mixBlendMode"
2480
+ ]);
2481
+ var L4_PASSTHROUGH = /* @__PURE__ */ new Set(["fontFamily", "fontStyle", "fontVariant", "textDecoration", "direction", "unicodeBidi"]);
2482
+ function normalizeStyleDecls(decls, opts = {}) {
2483
+ const layout = {};
2484
+ const paint = {};
2485
+ const text = {};
2486
+ const diagnostics = [];
2487
+ const push = (severity, code, message) => {
2488
+ diagnostics.push({ severity, code, message, nodeId: opts.nodeId, source: opts.source });
2489
+ };
2490
+ const len = (raw, axis = "horizontal") => {
2491
+ const r = resolveLength(raw, { axis });
2492
+ if (r === void 0 && raw.trim().toLowerCase() !== "auto") {
2493
+ push("warn", "css.invalid-length", `\u65E0\u6CD5\u6298\u53E0\u7684\u957F\u5EA6\u503C\u300C${raw}\u300D\uFF08profile \u652F\u6301 px/%/vw/vh/rpx/em/rem/pt\uFF09`);
2494
+ }
2495
+ return r;
2496
+ };
2497
+ let needsCompositing = false;
2498
+ let hasTransform = false;
2499
+ for (const [kRaw, raw] of Object.entries(decls)) {
2500
+ const k = camel(kRaw);
2501
+ const v = raw.trim();
2502
+ if (!v) continue;
2503
+ if (OUT_OF_PROFILE.has(k)) {
2504
+ push("error", "css.out-of-profile", `\u5C5E\u6027 ${kRaw} \u4E0D\u5728 CSS Profile \u5185\uFF08\u7F16\u8BD1\u671F\u62E6\u622A\uFF0C\u4E0D\u7559\u8FD0\u884C\u65F6\u9759\u9ED8\u964D\u7EA7\uFF09`);
2505
+ continue;
2506
+ }
2507
+ if (L4_PASSTHROUGH.has(k)) continue;
2508
+ switch (k) {
2509
+ /* ── 布局(L2) ── */
2510
+ case "display":
2511
+ if (v === "flex" || v === "none" || v === "block" || v === "inline-block") layout.display = v;
2512
+ else if (v === "grid") push("error", "css.grid-unsupported", "display:grid \u4E0D\u5728 Profile\uFF08\u5F85\u5B9A\u2014\u2014\u5EFA\u8BAE\u6539\u5D4C\u5957 flex\uFF09");
2513
+ else push("warn", "css.invalid-value", `display:${v} \u672A\u652F\u6301`);
2514
+ break;
2515
+ case "flexDirection":
2516
+ if (v === "row" || v === "column" || v === "row-reverse" || v === "column-reverse") layout.flexDirection = v;
2517
+ break;
2518
+ case "flexWrap":
2519
+ if (v === "nowrap" || v === "wrap" || v === "wrap-reverse") layout.flexWrap = v;
2520
+ break;
2521
+ case "justifyContent":
2522
+ case "alignItems":
2523
+ case "alignSelf":
2524
+ case "alignContent":
2525
+ layout[k] = v;
2526
+ break;
2527
+ case "flexGrow":
2528
+ layout.flexGrow = Number(v);
2529
+ break;
2530
+ case "flexShrink":
2531
+ layout.flexShrink = Number(v);
2532
+ break;
2533
+ case "flexBasis":
2534
+ layout.flexBasis = v === "auto" ? "auto" : len(v);
2535
+ break;
2536
+ case "flex": {
2537
+ if (/^-?\d*\.?\d+$/.test(v)) {
2538
+ layout.flexGrow = Number(v);
2539
+ layout.flexShrink = 1;
2540
+ layout.flexBasis = { kind: "absolute", dp: 0 };
2541
+ }
2542
+ break;
2543
+ }
2544
+ case "gap":
2545
+ case "rowGap":
2546
+ case "columnGap": {
2547
+ layout[k] = len(v);
2548
+ break;
2549
+ }
2550
+ case "width":
2551
+ case "minWidth":
2552
+ case "maxWidth":
2553
+ layout[k] = len(v, "horizontal");
2554
+ break;
2555
+ case "height":
2556
+ case "minHeight":
2557
+ case "maxHeight":
2558
+ layout[k] = len(v, "vertical");
2559
+ break;
2560
+ case "margin":
2561
+ case "padding": {
2562
+ const parts = splitTopLevel(v);
2563
+ const axis = parts.map((p, i) => len(p, k === "margin" && i === 1 ? "horizontal" : "horizontal"));
2564
+ void axis;
2565
+ const resolved = parts.map((p) => resolveLength(p, { axis: "horizontal" })).filter((x) => !!x);
2566
+ if (resolved.length === parts.length) layout[k] = expandEdges(resolved);
2567
+ break;
2568
+ }
2569
+ case "marginTop":
2570
+ case "marginRight":
2571
+ case "marginBottom":
2572
+ case "marginLeft":
2573
+ case "paddingTop":
2574
+ case "paddingRight":
2575
+ case "paddingBottom":
2576
+ case "paddingLeft": {
2577
+ const side = k.replace(/^(margin|padding)([A-Z].*)$/, "$2");
2578
+ const short = k.startsWith("margin") ? "margin" : "padding";
2579
+ const l = resolveLength(v, { axis: "horizontal" });
2580
+ if (l) {
2581
+ layout[short] = layout[short] ?? {};
2582
+ layout[short][side.toLowerCase()] = l;
2583
+ }
2584
+ break;
2585
+ }
2586
+ case "position":
2587
+ if (v === "static" || v === "relative" || v === "absolute") layout.position = v;
2588
+ else if (v === "fixed" || v === "sticky") {
2589
+ needsCompositing = true;
2590
+ push("warn", "css.l3-opt-in", `position:${v} \u5C5E L3\uFF08\u72EC\u7ACB\u5408\u6210\u5C42\uFF0C\u5404\u5360\u4E00\u5757 backing store\uFF09\u2014\u2014\u9700\u5728 config \u6216\u7EC4\u4EF6\u7EA7 opt-in`);
2591
+ } else push("warn", "css.invalid-value", `position:${v} \u672A\u652F\u6301`);
2592
+ break;
2593
+ case "top":
2594
+ layout.top = len(v, "vertical");
2595
+ break;
2596
+ case "bottom":
2597
+ layout.bottom = len(v, "vertical");
2598
+ break;
2599
+ case "left":
2600
+ layout.left = len(v, "horizontal");
2601
+ break;
2602
+ case "right":
2603
+ layout.right = len(v, "horizontal");
2604
+ break;
2605
+ case "overflow":
2606
+ case "overflowX":
2607
+ case "overflowY": {
2608
+ const val = v === "visible" || v === "hidden" || v === "scroll" || v === "auto" ? v : void 0;
2609
+ if (val) {
2610
+ if (k === "overflow") layout.overflow = val;
2611
+ else layout.overflow = val;
2612
+ }
2613
+ break;
2614
+ }
2615
+ /* ── 绘制(L1 + L3) ── */
2616
+ case "backgroundColor":
2617
+ paint.backgroundColor = v;
2618
+ break;
2619
+ case "background": {
2620
+ if (/gradient\(/.test(v)) paint.backgroundImage = v;
2621
+ else if (/url\(/.test(v)) push("warn", "css.unsupported-image", "background \u7684\u56FE\u7247\u5F62\u6001\u9700\u6539\u7528 image \u7EC4\u4EF6\uFF08CSS Profile \u4E0D\u542B\u80CC\u666F\u56FE\uFF09");
2622
+ else {
2623
+ const first = splitTopLevel(v)[0] ?? "";
2624
+ const isPlainColor = !/\b(no-repeat|repeat|cover|contain|center|left|right|top|bottom)\b/.test(v);
2625
+ if (isPlainColor && first) paint.backgroundColor = first;
2626
+ else push("warn", "css.background-compound", `background \u590D\u5408\u503C\u300C${v}\u300D\u65E0\u6CD5\u6298\u53E0\u4E3A\u5355\u4E00\u989C\u8272\u2014\u2014\u8BF7\u6539\u7528 background-color`);
2627
+ }
2628
+ break;
2629
+ }
2630
+ case "backgroundImage":
2631
+ if (v.includes("gradient(")) paint.backgroundImage = v;
2632
+ else push("warn", "css.unsupported-image", "background-image \u4EC5\u652F\u6301 gradient\uFF08\u5176\u4F59\u9700\u8D70 image \u7EC4\u4EF6\uFF09");
2633
+ break;
2634
+ case "borderRadius": {
2635
+ const parts = splitTopLevel(v).map((p) => resolveLength(p, { axis: "horizontal" }));
2636
+ if (parts.every((x) => !!x)) paint.borderRadius = expandEdges(parts);
2637
+ break;
2638
+ }
2639
+ case "borderTopLeftRadius":
2640
+ case "borderTopRightRadius":
2641
+ case "borderBottomLeftRadius":
2642
+ case "borderBottomRightRadius": {
2643
+ const side = k.replace(/^border([A-Z].*)Radius$/, "$1");
2644
+ const l = resolveLength(v, { axis: "horizontal" });
2645
+ if (l) {
2646
+ paint.borderRadius = paint.borderRadius ?? {};
2647
+ paint.borderRadius[side.charAt(0).toLowerCase() + side.slice(1)] = l;
2648
+ }
2649
+ break;
2650
+ }
2651
+ case "border": {
2652
+ const parts = splitTopLevel(v);
2653
+ const w = parts.find((p) => /^-?\d*\.?\d+\s*(px|rpx|pt)?$/.test(p));
2654
+ const style = parts.find((p) => ["solid", "dashed", "dotted", "none"].includes(p));
2655
+ const color = parts.find((p) => !p.includes("px") && p !== style && p !== w);
2656
+ if (w) {
2657
+ const l = resolveLength(w, { axis: "horizontal" });
2658
+ if (l) paint.borderWidth = { top: l, right: l, bottom: l, left: l };
2659
+ }
2660
+ if (style) paint.borderStyle = style;
2661
+ if (color) paint.borderColor = color;
2662
+ break;
2663
+ }
2664
+ case "borderWidth": {
2665
+ const parts = splitTopLevel(v).map((p) => resolveLength(p, { axis: "horizontal" }));
2666
+ if (parts.every((x) => !!x)) paint.borderWidth = expandEdges(parts);
2667
+ break;
2668
+ }
2669
+ case "borderColor":
2670
+ paint.borderColor = v;
2671
+ break;
2672
+ case "opacity": {
2673
+ const n = Number(v);
2674
+ if (Number.isFinite(n)) paint.opacity = n;
2675
+ break;
2676
+ }
2677
+ case "transform": {
2678
+ const { t, threeD } = parseTransform(v);
2679
+ hasTransform = true;
2680
+ if (threeD) {
2681
+ needsCompositing = true;
2682
+ push("warn", "css.l3-opt-in", `3D \u53D8\u6362\u5C5E L3\uFF08\u5408\u6210\u5C42 + \u5149\u6805\u5316\uFF09\u2014\u2014\u9700 opt-in\uFF1B\u503C\u300C${v}\u300D\u4EC5\u4F9B\u8BCA\u65AD`);
2683
+ }
2684
+ if (Object.keys(t).length) paint.transform = t;
2685
+ break;
2686
+ }
2687
+ /* ── L3:标记合成层 + 需 opt-in(不静默丢弃——诊断保留原值) ── */
2688
+ case "zIndex":
2689
+ case "boxShadow":
2690
+ case "filter":
2691
+ case "backdropFilter":
2692
+ case "willChange": {
2693
+ needsCompositing = true;
2694
+ void L3_PROPS;
2695
+ push("warn", "css.l3-opt-in", `${kRaw} \u5C5E L3\uFF08\u5408\u6210\u5C42/\u79BB\u5C4F\u6E32\u67D3\u2014\u2014\u5404\u5360\u4E00\u5757 backing store\uFF09\u2014\u2014\u9700\u663E\u5F0F opt-in`);
2696
+ break;
2697
+ }
2698
+ /* ── 文本 ── */
2699
+ case "fontSize":
2700
+ text.fontSize = len(v, "horizontal");
2701
+ break;
2702
+ case "fontWeight":
2703
+ text.fontWeight = v === "normal" || v === "bold" ? v : Number(v);
2704
+ break;
2705
+ case "lineHeight": {
2706
+ const n = Number(v);
2707
+ if (Number.isFinite(n) && !/[a-z%]/.test(v)) text.lineHeight = n;
2708
+ else text.lineHeight = resolveLength(v, { axis: "vertical" });
2709
+ break;
2710
+ }
2711
+ case "letterSpacing":
2712
+ text.letterSpacing = len(v, "horizontal");
2713
+ break;
2714
+ case "color":
2715
+ text.color = v;
2716
+ break;
2717
+ case "textAlign":
2718
+ if (v === "left" || v === "center" || v === "right" || v === "justify") text.textAlign = v;
2719
+ break;
2720
+ case "webkitLineClamp": {
2721
+ const n = Number(v);
2722
+ if (Number.isFinite(n)) text.maxLines = n;
2723
+ break;
2724
+ }
2725
+ case "textOverflow":
2726
+ if (v === "clip" || v === "ellipsis") text.textOverflow = v;
2727
+ break;
2728
+ case "whiteSpace":
2729
+ if (v === "normal" || v === "nowrap" || v === "pre-wrap") text.whiteSpace = v;
2730
+ break;
2731
+ default:
2732
+ push("error", "css.out-of-profile", `\u5C5E\u6027 ${kRaw} \u4E0D\u5728 CSS Profile \u5185`);
2733
+ break;
2734
+ }
2735
+ }
2736
+ const hasText = Object.keys(text).length > 0;
2737
+ const paintHint = {
2738
+ // 单色文本 → 紧凑 backing store 格式(实测 −39% 内存)
2739
+ isMonochrome: !!(hasText && text.color && !paint.backgroundImage && !needsCompositing),
2740
+ // 纯色背景 → 走 backgroundColor 通道、**不分配 backing store**(实测:仅色块 4.9MB vs 带文本 186.7MB)
2741
+ isPureBackground: !!paint.backgroundColor && !paint.backgroundImage && !paint.borderWidth && !paint.borderRadius && !needsCompositing && !hasTransform && (paint.opacity === void 0 || paint.opacity === 1),
2742
+ // 由树级分析填充(本函数只管单节点)
2743
+ staticSubtree: false,
2744
+ needsCompositingLayer: needsCompositing
2745
+ };
2746
+ return {
2747
+ props: {
2748
+ layout,
2749
+ paint,
2750
+ ...hasText ? { text } : {},
2751
+ paintHint
2752
+ },
2753
+ diagnostics
2754
+ };
2755
+ }
2756
+ function normalizeStyleString(style, opts = {}) {
2757
+ return normalizeStyleDecls(parseStyleString(style), opts);
2758
+ }
2759
+
2760
+ // src/pnode-analyze.ts
2761
+ var FLATTENABLE_KINDS = /* @__PURE__ */ new Set(["view", "text", "image"]);
2762
+ function analyzePTree(tree, opts = {}) {
2763
+ const diagnostics = [];
2764
+ const decisions = [];
2765
+ const facts = opts.facts ?? /* @__PURE__ */ new Map();
2766
+ const boundary = opts.boundaryIds ?? /* @__PURE__ */ new Set();
2767
+ const boundProps = /* @__PURE__ */ new Map();
2768
+ for (const b of tree.bindings) {
2769
+ const list = boundProps.get(b.nodeId) ?? [];
2770
+ list.push(b.propKey);
2771
+ boundProps.set(b.nodeId, list);
2772
+ }
2773
+ let nodeCount = 0;
2774
+ let flattenableCount = 0;
2775
+ let compositingCount = 0;
2776
+ const walk = (node) => {
2777
+ nodeCount++;
2778
+ const f = facts.get(node.id);
2779
+ const ownDynamic = (boundProps.get(node.id)?.length ?? 0) > 0 || (f?.dynamicProps?.length ?? 0) > 0;
2780
+ const hasEvent = f?.hasEvent === true || node.flags.hasEvent === true;
2781
+ let childrenStatic = true;
2782
+ for (const child of node.children) {
2783
+ if (!walk(child)) childrenStatic = false;
2784
+ }
2785
+ const isStatic = !ownDynamic && !hasEvent && childrenStatic;
2786
+ node.flags.isStatic = isStatic;
2787
+ node.flags.hasEvent = hasEvent;
2788
+ node.flags.isNativeHost = node.kind === "native-host";
2789
+ node.props.paintHint.staticSubtree = isStatic;
2790
+ if (node.props.paintHint.needsCompositingLayer) compositingCount++;
2791
+ const reasons = [];
2792
+ if (!FLATTENABLE_KINDS.has(node.kind)) reasons.push(`kind=${node.kind} \u975E\u7ED8\u5236\u578B`);
2793
+ if (!isStatic) reasons.push("\u5B50\u6811\u542B\u52A8\u6001\u7ED1\u5B9A");
2794
+ if (hasEvent) reasons.push("\u7ED1\u5B9A\u4E8B\u4EF6\uFF08\u62CD\u5E73\u540E\u4E0D\u518D\u6709\u72EC\u7ACB\u7ED8\u5236\u5BF9\u8C61\uFF0C\u4E8B\u4EF6\u4E0D\u53EF\u8FBE\uFF09");
2795
+ if (f?.isAnimationTarget) reasons.push("\u52A8\u753B/\u8F6C\u573A\u76EE\u6807");
2796
+ if (f?.isComponentRoot) reasons.push("\u81EA\u5B9A\u4E49\u7EC4\u4EF6\u6839\uFF08\u7EC4\u4EF6\u8FB9\u754C\uFF09");
2797
+ if (node.props.paintHint.needsCompositingLayer) reasons.push("\u9700\u5408\u6210\u5C42\uFF08L3 \u7279\u6027\uFF09");
2798
+ if (node.props.paint.transform) reasons.push("\u542B transform");
2799
+ if (boundary.has(node.id)) reasons.push("\u7ED3\u6784\u8FB9\u754C\uFF08\u5BB9\u5668/\u5BBF\u4E3B\uFF09");
2800
+ const flattenEligible = reasons.length === 0;
2801
+ node.flags.flattenEligible = flattenEligible;
2802
+ if (flattenEligible) flattenableCount++;
2803
+ if (f?.requestFlatten && !flattenEligible) {
2804
+ diagnostics.push({
2805
+ severity: "error",
2806
+ code: "css.flatten-violation",
2807
+ message: `\u663E\u5F0F\u8BF7\u6C42\u62CD\u5E73\uFF0C\u4F46\u4E0D\u6EE1\u8DB3\u6761\u4EF6\uFF1A${reasons.join(" / ")}`,
2808
+ nodeId: node.id,
2809
+ source: node.source
2810
+ });
2811
+ }
2812
+ decisions.push({
2813
+ nodeId: node.id,
2814
+ kind: node.kind,
2815
+ semantic: node.semantic,
2816
+ isStatic,
2817
+ flattenEligible,
2818
+ reasons
2819
+ });
2820
+ return isStatic;
2821
+ };
2822
+ for (const root of tree.roots) walk(root);
2823
+ const stats = { nodeCount, flattenableCount, compositingCount };
2824
+ tree.stats = stats;
2825
+ tree.diagnostics.push(...diagnostics);
2826
+ return { diagnostics, stats, decisions };
2827
+ }
2828
+ function flattenRate(stats) {
2829
+ return stats.nodeCount > 0 ? stats.flattenableCount / stats.nodeCount : 0;
2830
+ }
2831
+ function groupBindingsByNode(bindings) {
2832
+ const out = /* @__PURE__ */ new Map();
2833
+ for (const b of bindings) {
2834
+ const list = out.get(b.nodeId) ?? [];
2835
+ list.push(b);
2836
+ out.set(b.nodeId, list);
2837
+ }
2838
+ return out;
2839
+ }
2840
+
2841
+ // src/pnode-build.ts
2842
+ function kindFromSemantic(semantic, tag) {
2843
+ const s = semantic ?? "";
2844
+ if (!s) {
2845
+ if (tag === "text" || tag === "p-text") return "text";
2846
+ if (tag === "image" || tag === "img") return "image";
2847
+ return "view";
2848
+ }
2849
+ if (s.startsWith("layout.virtual-list") || s.startsWith("layout.masonry")) return "list";
2850
+ if (s.startsWith("layout.")) return "view";
2851
+ if (s === "ui.text" || s === "ui.heading" || s === "ui.label") return "text";
2852
+ if (s === "ui.rich-text") return "rich-text";
2853
+ if (s.startsWith("ui.image") || s === "ui.icon" || s === "ui.avatar" || s === "ui.media" || s === "ui.svg" || s === "ui.canvas") return "image";
2854
+ if (s === "shell.webview" || s === "ui.map" || s === "ui.camera" || s === "shell.ad" || s === "ui.ad") return "native-host";
2855
+ return "view";
2856
+ }
2857
+ function normalizePropsOf(raw, kind, st, ctx) {
2858
+ const source = { tag: ctx.tag, line: void 0 };
2859
+ const res = typeof raw.style === "string" ? normalizeStyleString(raw.style, { kind, nodeId: ctx.id, source }) : normalizeStyleDecls(raw.style ?? {}, { kind, nodeId: ctx.id, source });
2860
+ st.diagnostics.push(...res.diagnostics);
2861
+ return res.props;
2862
+ }
2863
+ function buildNode(raw, st) {
2864
+ const id = st.nextId++;
2865
+ const tag = raw.tag ?? raw.kind ?? "view";
2866
+ const kind = raw.kind ?? kindFromSemantic(raw.semantic, tag);
2867
+ const props = normalizePropsOf(raw, kind, st, { id, tag });
2868
+ for (const b of raw.bindings ?? []) {
2869
+ const updateKind = b.updateKind ?? inferUpdateKind(b.propKey);
2870
+ st.bindings.push({
2871
+ slotId: st.slot++,
2872
+ nodeId: id,
2873
+ propKey: b.propKey,
2874
+ exprId: b.exprId,
2875
+ updateKind,
2876
+ // ★Vapor IR 扩展(方案 §3.2):opCode 编译期确定 ⇒ 运行时无类型判断
2877
+ opCode: b.opCode ?? inferOpCode(updateKind),
2878
+ // ★缺省 L0:保守优先(误判 L1 = 静默不更新;方案 §5.1)
2879
+ tier: b.tier ?? "L0",
2880
+ deps: b.deps ?? []
2881
+ });
2882
+ }
2883
+ const facts = {
2884
+ hasEvent: raw.facts?.hasEvent,
2885
+ isAnimationTarget: raw.facts?.isAnimationTarget,
2886
+ isComponentRoot: raw.facts?.isComponentRoot,
2887
+ requestFlatten: raw.facts?.requestFlatten,
2888
+ dynamicProps: (raw.bindings ?? []).map((b) => b.propKey)
2889
+ };
2890
+ st.facts.set(id, facts);
2891
+ const node = {
2892
+ id,
2893
+ kind,
2894
+ semantic: raw.semantic,
2895
+ props,
2896
+ children: [],
2897
+ flags: { isStatic: true, flattenEligible: false, isLayoutBoundary: false, hasEvent: false, isNativeHost: kind === "native-host" },
2898
+ source: { tag }
2899
+ };
2900
+ if (raw.text !== void 0) node.text = raw.text;
2901
+ node.children = (raw.children ?? []).map((c) => buildNode(c, st));
2902
+ return node;
2903
+ }
2904
+ function inferUpdateKind(propKey) {
2905
+ if (propKey.startsWith("text.")) return propKey === "text.color" || propKey === "text.fontSize" ? "style" : "text-content";
2906
+ if (propKey.startsWith("layout.")) return "style";
2907
+ if (propKey.startsWith("paint.")) return "style";
2908
+ if (propKey === "visible" || propKey === "display") return "visibility";
2909
+ if (propKey.startsWith("list.")) return "list-data";
2910
+ return "attr";
2911
+ }
2912
+ function inferOpCode(updateKind) {
2913
+ switch (updateKind) {
2914
+ case "attr":
2915
+ return 1 /* SET_PROP */;
2916
+ case "style":
2917
+ return 2 /* SET_STYLE */;
2918
+ case "text-content":
2919
+ return 3 /* SET_TEXT */;
2920
+ case "visibility":
2921
+ return 5 /* TOGGLE_VIS */;
2922
+ case "list-data":
2923
+ return 32 /* LIST_SET */;
2924
+ default: {
2925
+ const never = updateKind;
2926
+ throw new Error(`\u672A\u77E5\u66F4\u65B0\u79CD\u7C7B\uFF1A${String(never)}`);
2927
+ }
2928
+ }
2929
+ }
2930
+ function buildPTree(raws, opts = {}) {
2931
+ const st = {
2932
+ nextId: opts.startId ?? 1,
2933
+ bindings: [],
2934
+ diagnostics: [],
2935
+ facts: /* @__PURE__ */ new Map(),
2936
+ slot: 0
2937
+ };
2938
+ const roots = raws.map((r) => buildNode(r, st));
2939
+ const tree = {
2940
+ roots,
2941
+ bindings: st.bindings,
2942
+ diagnostics: st.diagnostics,
2943
+ stats: { nodeCount: 0, flattenableCount: 0, compositingCount: 0 }
2944
+ };
2945
+ analyzePTree(tree, { facts: st.facts, boundaryIds: opts.analyze?.boundaryIds });
2946
+ return tree;
2947
+ }
2948
+ function styleOf(ir) {
2949
+ const s = ir.props.style;
2950
+ if (typeof s === "string") return s;
2951
+ if (s && typeof s === "object") return s;
2952
+ return void 0;
2953
+ }
2954
+ function rawFromComponentIR(ir) {
2955
+ const children = (ir.children ?? []).map(rawFromComponentIR);
2956
+ const raw = {
2957
+ semantic: ir.semantic,
2958
+ tag: ir.tag,
2959
+ children
2960
+ };
2961
+ const st = styleOf(ir);
2962
+ if (st) raw.style = st;
2963
+ return raw;
2964
+ }
2965
+ function buildPTreeFromComponentIR(roots, opts = {}) {
2966
+ return buildPTree(roots.map(rawFromComponentIR), opts);
2967
+ }
2968
+
2969
+ // src/pnode-trace.ts
2970
+ function formatPNodeLine(decision) {
2971
+ const bits = [
2972
+ `#${decision.nodeId}`,
2973
+ decision.kind,
2974
+ decision.semantic ? `(${decision.semantic})` : "",
2975
+ decision.isStatic ? "\u9759\u6001" : "\u52A8\u6001",
2976
+ decision.flattenEligible ? "\u2705\u53EF\u62CD\u5E73" : `\u26D4\u53D7\u963B\uFF1A${decision.reasons.join(" / ")}`
2977
+ ];
2978
+ return bits.filter(Boolean).join(" ");
2979
+ }
2980
+ function formatPTrace(tree, analysis, opts = {}) {
2981
+ const max = opts.maxNodes ?? 200;
2982
+ const lines = [];
2983
+ const rate = (flattenRate(tree.stats) * 100).toFixed(1);
2984
+ lines.push("### \u6E32\u67D3 IR\uFF08\u6E32\u67D3\u5C42\u51B3\u7B56\uFF09");
2985
+ lines.push(` \u8282\u70B9 ${tree.stats.nodeCount} \xB7 \u53EF\u62CD\u5E73 ${tree.stats.flattenableCount}\uFF08\u62CD\u5E73\u7387 ${rate}%\uFF09\xB7 \u9700\u5408\u6210\u5C42 ${tree.stats.compositingCount}`);
2986
+ lines.push(` \u52A8\u6001\u7ED1\u5B9A ${tree.bindings.length} \u4E2A\u69FD\u4F4D`);
2987
+ lines.push(" \uFF08\u53C2\u7167\uFF1A\u771F\u673A\u5B9E\u6D4B 4050 \u5143\u7D20\u62CD\u5E73\u7387 98.8% \u2192 \u5185\u5B58 186.7\u219217.7MB \xB7 \u8017\u65F6 190.5\u2192129.9ms\uFF09");
2988
+ const ordered = [...analysis.decisions].sort((a, b) => a.nodeId - b.nodeId);
2989
+ const shown = ordered.filter((d) => !opts.onlyBlocked || !d.flattenEligible).slice(0, max);
2990
+ lines.push("");
2991
+ lines.push(` \u9010\u8282\u70B9\u51B3\u7B56${opts.onlyBlocked ? "\uFF08\u4EC5\u53D7\u963B\u9879\uFF09" : ""}\uFF1A`);
2992
+ for (const d of shown) lines.push(` ${formatPNodeLine(d)}`);
2993
+ const hidden = ordered.filter((d) => !opts.onlyBlocked || !d.flattenEligible).length - shown.length;
2994
+ if (hidden > 0) lines.push(` \u2026 \u7701\u7565 ${hidden} \u4E2A\u8282\u70B9\uFF08--max-nodes \u8C03\u6574\uFF09`);
2995
+ if (opts.includeDiagnostics !== false && tree.diagnostics.length) {
2996
+ const errs = tree.diagnostics.filter((d) => d.severity === "error");
2997
+ const warns = tree.diagnostics.filter((d) => d.severity === "warn");
2998
+ lines.push("");
2999
+ lines.push(` \u8BCA\u65AD\uFF1Aerror ${errs.length} \xB7 warn ${warns.length}`);
3000
+ for (const d of errs) lines.push(` \u2717 [${d.code}] ${d.message}${d.source ? `\uFF08${d.source.tag}\uFF09` : ""}`);
3001
+ for (const d of warns.slice(0, 20)) lines.push(` \u26A0 [${d.code}] ${d.message}`);
3002
+ if (warns.length > 20) lines.push(` \u2026 \u53E6\u6709 ${warns.length - 20} \u6761 warn`);
3003
+ }
3004
+ return lines.join("\n");
3005
+ }
3006
+ function decisionOf(analysis, nodeId) {
3007
+ return analysis.decisions.find((d) => d.nodeId === nodeId);
3008
+ }
3009
+ function walkPTree(roots, visit) {
3010
+ const walk = (nodes, depth) => {
3011
+ for (const n of nodes) {
3012
+ visit(n, depth);
3013
+ walk(n.children, depth + 1);
3014
+ }
3015
+ };
3016
+ walk(roots, 0);
3017
+ }
2320
3018
  export {
2321
3019
  COMPONENT_IR_SCHEMA,
2322
3020
  DEFAULT_DESIGN_WIDTH,
@@ -2339,12 +3037,16 @@ export {
2339
3037
  SPEC_RATCHET,
2340
3038
  TAG_SEMANTIC_ALIASES,
2341
3039
  TAG_SEMANTIC_MAP,
3040
+ analyzePTree,
2342
3041
  auditCatalogConsistency,
2343
3042
  auditDegradation,
2344
3043
  auditMatrixReferences,
2345
3044
  auditMiniprogramCoverage,
2346
3045
  auditSpecCoverage,
2347
3046
  auditSpecOverrideRefs,
3047
+ buildPTree,
3048
+ buildPTreeFromComponentIR,
3049
+ camel,
2348
3050
  checkComponentSnapshot,
2349
3051
  checkPrimitiveCatalog,
2350
3052
  checkPropDegradation,
@@ -2353,19 +3055,36 @@ export {
2353
3055
  classifySpecComponent,
2354
3056
  collectPropDiagnostics,
2355
3057
  componentPrimitives,
3058
+ decisionOf,
2356
3059
  degradationOf,
2357
3060
  degradeProp,
3061
+ expandEdges,
2358
3062
  extractSemanticTree,
3063
+ flattenRate,
2359
3064
  formatCoverageReport,
2360
3065
  formatDegradationReport,
3066
+ formatPNodeLine,
3067
+ formatPTrace,
3068
+ groupBindingsByNode,
2361
3069
  implementedPrimitives,
3070
+ inferOpCode,
3071
+ inferUpdateKind,
3072
+ kindFromSemantic,
2362
3073
  mapSemanticToBackend,
3074
+ normalizeStyleDecls,
3075
+ normalizeStyleString,
3076
+ parseStyleString,
3077
+ parseTransform,
2363
3078
  primitiveById,
2364
3079
  primitiveBySemantic,
2365
3080
  primitiveByTag,
3081
+ rawFromComponentIR,
3082
+ resolveLength,
3083
+ splitTopLevel,
2366
3084
  toComponentIR,
2367
3085
  toComponentTree,
2368
3086
  validateComponentIR,
2369
3087
  validateComponentTree,
2370
- validateGridConstraints
3088
+ validateGridConstraints,
3089
+ walkPTree
2371
3090
  };
@@ -0,0 +1,42 @@
1
+ import type { DynamicBinding, PDiagnostic, PTree } from './pnode';
2
+ /** 分析输入:节点的「非样式」事实(由上游从模板/脚本提取) */
3
+ export interface NodeFacts {
4
+ /** 该节点自身绑定的动态属性键(如 `text.color` / `layout.width`) */
5
+ dynamicProps?: string[];
6
+ /** 该节点是否绑定事件(click/touch/…) */
7
+ hasEvent?: boolean;
8
+ /** 是否为动画/转场目标(Transition · animate · worklet) */
9
+ isAnimationTarget?: boolean;
10
+ /** 是否为自定义组件根(组件边界不可拍平——否则跨组件通信失效) */
11
+ isComponentRoot?: boolean;
12
+ /** 显式请求拍平(声明式 directive / config)——不合资格时**报错** */
13
+ requestFlatten?: boolean;
14
+ }
15
+ export interface AnalyzeOptions {
16
+ /** 节点事实表(nodeId → facts);缺省视为「无动态、无事件」 */
17
+ facts?: Map<number, NodeFacts>;
18
+ /** 结构边界:不可拍平的节点 id(如滚动容器 / 原生宿主 / 列表项模板根) */
19
+ boundaryIds?: Set<number>;
20
+ }
21
+ export interface AnalyzeResult {
22
+ diagnostics: PDiagnostic[];
23
+ stats: PTree['stats'];
24
+ /** 逐节点判定记录(trace / `proteus explain` 消费) */
25
+ decisions: Array<{
26
+ nodeId: number;
27
+ kind: string;
28
+ semantic?: string;
29
+ isStatic: boolean;
30
+ flattenEligible: boolean;
31
+ reasons: string[];
32
+ }>;
33
+ }
34
+ /**
35
+ * 树级分析(后序遍历)——填充 `flags` / `paintHint.staticSubtree` 并产出决策记录与诊断。
36
+ * **原地更新** tree(便于 compiler 直接在构建流程里调用),同时返回分析结果。
37
+ */
38
+ export declare function analyzePTree(tree: PTree, opts?: AnalyzeOptions): AnalyzeResult;
39
+ /** 拍平率(实测关心指标:拍平率越高,绘制对象越少 → 内存与 commit 成本越低) */
40
+ export declare function flattenRate(stats: PTree['stats']): number;
41
+ /** 把动态绑定按 nodeId 分组(M2 的 PatchTable 生成与运行时槽位索引会用到) */
42
+ export declare function groupBindingsByNode(bindings: DynamicBinding[]): Map<number, DynamicBinding[]>;
@@ -0,0 +1,59 @@
1
+ import type { ComponentIR } from './schema';
2
+ import { OpCode } from '@proteus-vue/slot-runtime';
3
+ import type { UpdateTier } from '@proteus-vue/slot-runtime';
4
+ import type { PKind, PTree, UpdateKind } from './pnode';
5
+ import type { NodeFacts, AnalyzeOptions } from './pnode-analyze';
6
+ /**
7
+ * 语义 → 绘制类型(**框架层收敛点**:semantic 是后端映射依据,kind 是绘制层分工)。
8
+ * 映射表对齐 `render-backend/spi.ts` 的 `IRNode.semantic` 域前缀。
9
+ */
10
+ export declare function kindFromSemantic(semantic: string | undefined, tag: string): PKind;
11
+ /** 轻量声明节点(供测试与编译器直调——刻意不要求完整 ComponentIR) */
12
+ export interface PRawNode {
13
+ kind?: PKind;
14
+ semantic?: string;
15
+ tag?: string;
16
+ /** 内联样式:字符串或声明表 */
17
+ style?: string | Record<string, string>;
18
+ /** 文本字面量(静态文本) */
19
+ text?: string;
20
+ /** 事实(事件/动画/组件根/显式拍平请求——传给分析器) */
21
+ facts?: Omit<NodeFacts, 'dynamicProps'>;
22
+ /** 动态绑定声明(编译器从模板分析得出) */
23
+ bindings?: Array<{
24
+ propKey: string;
25
+ exprId: string;
26
+ updateKind?: UpdateKind;
27
+ /** ★显式指定操作码(如 item 级 `LIST_UPDATE`;缺省由 updateKind 推导) */
28
+ opCode?: OpCode;
29
+ /** ★分层(缺省 L0——保守:没证明安全就不许假设安全) */
30
+ tier?: UpdateTier;
31
+ /** ★编译期识别的响应式依赖(V2 填充;缺省空数组) */
32
+ deps?: string[];
33
+ }>;
34
+ children?: PRawNode[];
35
+ }
36
+ export interface BuildOptions {
37
+ /** 起始 id(多次构建可错开——同输入同输出) */
38
+ startId?: number;
39
+ /** 自定义分析选项透传(boundaryIds 等) */
40
+ analyze?: AnalyzeOptions;
41
+ }
42
+ /** 绑定属性键 → 更新种类(运行时按种类走特化写值——D1「按槽位写值」) */
43
+ export declare function inferUpdateKind(propKey: string): UpdateKind;
44
+ /**
45
+ * 更新种类 → 指令操作码(**编译期确定,运行时无分支**——方案 §2.1)
46
+ *
47
+ * ★为什么 `list-data` 默认映射到 `LIST_SET` 而不是 `LIST_UPDATE`:
48
+ * 同一个 `updateKind` 有两种列表语义——「整体换数据源」(LIST_SET)与
49
+ * 「单项内一个槽位变化」(LIST_UPDATE,方案 §2.3 的核心收益)。
50
+ * 后者**必须由编译器显式指定**(它要知道 listId / itemKey / 项内槽位),
51
+ * 不能在推进时猜——猜错会把「改一项」误当成「换整表」,是静默的性能回退。
52
+ */
53
+ export declare function inferOpCode(updateKind: UpdateKind): OpCode;
54
+ /** 从轻量声明构建渲染 IR(含分析) */
55
+ export declare function buildPTree(raws: PRawNode[], opts?: BuildOptions): PTree;
56
+ /** 从 ComponentIR 转 PRawNode(保留语义、样式、文本) */
57
+ export declare function rawFromComponentIR(ir: ComponentIR): PRawNode;
58
+ /** 从语义 IR 构建渲染 IR(编译器集成入口) */
59
+ export declare function buildPTreeFromComponentIR(roots: ComponentIR[], opts?: BuildOptions): PTree;
@@ -0,0 +1,50 @@
1
+ import type { Edges, PDiagnostic, PKind, PProps, ResolvedLength, Transform2D } from './pnode';
2
+ /** 长度解析上下文(基准语义) */
3
+ export interface LengthContext {
4
+ /** 属性归属轴——决定 `%` 的基准(width/margin/padding → parentWidth;height → parentHeight) */
5
+ axis: 'horizontal' | 'vertical';
6
+ }
7
+ /**
8
+ * 单位折叠(Profile §3.2:编译期折叠为逻辑像素或比例系数)。
9
+ * · `px` / 无单位 0 → 绝对 dp
10
+ * · `%` → ratio(基准按轴)
11
+ * · `vw` / `vh` → ratio(视口基准)
12
+ * · `rpx` → ratio = v/750(小程序 750 设计宽语义;App 端同样按视口宽比例求值)
13
+ * · `em` → ratio(父字号基准)· `rem` → ratio(根字号基准)
14
+ * 非法值 → undefined(由调用方出诊断)
15
+ */
16
+ export declare function resolveLength(raw: string, ctx?: LengthContext): ResolvedLength | undefined;
17
+ /** 声明列表解析(`color: red; font-size: 14px` → 键值对;键统一 camelCase) */
18
+ export declare function parseStyleString(style: string): Record<string, string>;
19
+ /** kebab-case → camelCase(CSS 与 Vue style 对象两种写法都吃) */
20
+ export declare function camel(k: string): string;
21
+ /** 空格分隔值拆分(跳过函数括号内的空格:`1px solid rgb(0, 0, 0)`) */
22
+ export declare function splitTopLevel(value: string): string[];
23
+ /** 四边简写展开(1/2/3/4 值语义,对齐 CSS) */
24
+ export declare function expandEdges<T>(parts: T[]): Partial<Edges<T>>;
25
+ /** 2D 变换解析;含 3D 分量 → 返回 `threeD: true`(由调用方标记 L3) */
26
+ export declare function parseTransform(raw: string): {
27
+ t: Transform2D;
28
+ threeD: boolean;
29
+ };
30
+ export interface NormalizeResult {
31
+ props: PProps;
32
+ diagnostics: PDiagnostic[];
33
+ }
34
+ export interface NormalizeOptions {
35
+ /** 节点类型(影响 PaintHint 推导——文本单色 / 纯背景) */
36
+ kind?: PKind;
37
+ /** 源码定位(诊断用) */
38
+ source?: {
39
+ tag: string;
40
+ line?: number;
41
+ };
42
+ nodeId?: number;
43
+ }
44
+ /**
45
+ * 归一化:声明表 → `PProps`(**编译期完成,运行时零解析**)。
46
+ * 诊断按 Profile 分级:L3 → warn(需 opt-in);Profile 外 → error;非法单位 → warn。
47
+ */
48
+ export declare function normalizeStyleDecls(decls: Record<string, string>, opts?: NormalizeOptions): NormalizeResult;
49
+ /** 便捷:样式字符串 → PProps(内部先拆声明列表) */
50
+ export declare function normalizeStyleString(style: string, opts?: NormalizeOptions): NormalizeResult;
@@ -0,0 +1,21 @@
1
+ import type { PNode, PTree } from './pnode';
2
+ import type { AnalyzeResult } from './pnode-analyze';
3
+ export interface PTraceOptions {
4
+ /** 只显示不能拍平的节点(排查用——默认全显示) */
5
+ onlyBlocked?: boolean;
6
+ /** 最多显示多少个节点(防输出爆炸——本仓效率规范明令「输出控制」) */
7
+ maxNodes?: number;
8
+ /** 是否包含诊断明细 */
9
+ includeDiagnostics?: boolean;
10
+ }
11
+ /** 单节点一行摘要:`#id kind semantic [静态|动态] [可拍平|受阻:理由]` */
12
+ export declare function formatPNodeLine(decision: AnalyzeResult['decisions'][number]): string;
13
+ /**
14
+ * 渲染 IR 决策 trace(供 `proteus explain` 输出)。
15
+ * 输出结构:统计摘要 → 逐节点决策(按树序)→ 诊断(error 优先)。
16
+ */
17
+ export declare function formatPTrace(tree: PTree, analysis: AnalyzeResult, opts?: PTraceOptions): string;
18
+ /** 从 PTree 反查某节点的决策(explain 定位用) */
19
+ export declare function decisionOf(analysis: AnalyzeResult, nodeId: number): AnalyzeResult['decisions'][number] | undefined;
20
+ /** 树序遍历(trace 输出与调试用) */
21
+ export declare function walkPTree(roots: PNode[], visit: (node: PNode, depth: number) => void): void;
@@ -0,0 +1,216 @@
1
+ /** 渲染节点类型(比语义层粗——绘制层只区分「怎么画」) */
2
+ export type PKind =
3
+ /** 盒子(背景/边框/圆角——可含子节点) */
4
+ 'view'
5
+ /** 文本(单个文本节点,内容来自 text-content 槽或静态字面量) */
6
+ | 'text'
7
+ /** 图像 */
8
+ | 'image'
9
+ /** 列表(复用池管理,M3) */
10
+ | 'list'
11
+ /** 富文本(平台文本引擎,L4) */
12
+ | 'rich-text'
13
+ /** 原生组件宿主(map/webview/广告——必须原生嵌入,CSS Profile §0.2 非目标) */
14
+ | 'native-host';
15
+ /**
16
+ * 归一化长度(**单位已在编译期折叠**——运行时不做单位换算,Profile §3.2)。
17
+ * · `absolute`:px → dp(App 端 1px = 1dp;密度缩放由平台层处理)
18
+ * · `ratio`:`%` / `vw` / `vh` / `rpx` / `em` / `rem` → 保留比例系数 + 基准,运行时按基准求值
19
+ */
20
+ export type ResolvedLength = {
21
+ kind: 'absolute';
22
+ dp: number;
23
+ } | {
24
+ kind: 'ratio';
25
+ ratio: number;
26
+ base: 'parentWidth' | 'parentHeight' | 'viewportWidth' | 'viewportHeight' | 'fontSize' | 'rootFontSize';
27
+ };
28
+ /** 四边值(margin / padding / border-radius 等) */
29
+ export interface Edges<T> {
30
+ top: T;
31
+ right: T;
32
+ bottom: T;
33
+ left: T;
34
+ }
35
+ /** 布局属性(flex 为主——Profile L2;grid 待定,不在此层) */
36
+ export interface LayoutProps {
37
+ display?: 'flex' | 'none' | 'block' | 'inline-block';
38
+ flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
39
+ flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
40
+ justifyContent?: string;
41
+ alignItems?: string;
42
+ alignSelf?: string;
43
+ alignContent?: string;
44
+ flexGrow?: number;
45
+ flexShrink?: number;
46
+ flexBasis?: ResolvedLength | 'auto';
47
+ gap?: ResolvedLength;
48
+ rowGap?: ResolvedLength;
49
+ columnGap?: ResolvedLength;
50
+ width?: ResolvedLength;
51
+ height?: ResolvedLength;
52
+ minWidth?: ResolvedLength;
53
+ maxWidth?: ResolvedLength;
54
+ minHeight?: ResolvedLength;
55
+ maxHeight?: ResolvedLength;
56
+ margin?: Partial<Edges<ResolvedLength>>;
57
+ padding?: Partial<Edges<ResolvedLength>>;
58
+ /** ★fixed / sticky 属 L3(独立合成层)——此处只接受 static/relative/absolute(Profile L2) */
59
+ position?: 'static' | 'relative' | 'absolute';
60
+ top?: ResolvedLength;
61
+ right?: ResolvedLength;
62
+ bottom?: ResolvedLength;
63
+ left?: ResolvedLength;
64
+ overflow?: 'visible' | 'hidden' | 'scroll' | 'auto';
65
+ }
66
+ /** 2D 变换(L1)。3D 变换属 L3(会创建合成层)——由 diagnostics 拦下 */
67
+ export interface Transform2D {
68
+ translateX?: number;
69
+ translateY?: number;
70
+ scale?: number;
71
+ rotate?: number;
72
+ }
73
+ /** 绘制属性 */
74
+ export interface PaintProps {
75
+ backgroundColor?: string;
76
+ /** 线性渐变(Profile L1;实测 Skyline 亦接受该属性) */
77
+ backgroundImage?: string;
78
+ borderRadius?: Partial<Edges<ResolvedLength>>;
79
+ borderWidth?: Partial<Edges<ResolvedLength>>;
80
+ borderColor?: string;
81
+ borderStyle?: 'solid' | 'dashed' | 'dotted' | 'none';
82
+ opacity?: number;
83
+ transform?: Transform2D;
84
+ }
85
+ /** 文本属性(**字体族/BiDi/emoji 均不在本层**——L4 复用平台文本栈,Profile §L4) */
86
+ export interface TextProps {
87
+ fontSize?: ResolvedLength;
88
+ fontWeight?: number | 'normal' | 'bold';
89
+ lineHeight?: ResolvedLength | number;
90
+ letterSpacing?: ResolvedLength;
91
+ color?: string;
92
+ textAlign?: 'left' | 'center' | 'right' | 'justify';
93
+ /** 截断行数(依赖平台文本度量——Profile L2 标 🟡) */
94
+ maxLines?: number;
95
+ textOverflow?: 'clip' | 'ellipsis';
96
+ whiteSpace?: 'normal' | 'nowrap' | 'pre-wrap';
97
+ }
98
+ /**
99
+ * 绘制提示(编译期推导,**禁止运行时判断**——Profile §12.4)。
100
+ * 每一条都对应一条已实测的绘制/内存策略:
101
+ * · `isMonochrome`:单色内容(纯色文本/纯色块)→ 可用紧凑 backing store 格式(**实测 −39%**)
102
+ * · `isPureBackground`:只有背景色、无其它绘制 → 走 backgroundColor 通道(**不分配 backing store**)
103
+ * · `staticSubtree`:整棵子树无动态绑定 → 可拍平 / 可光栅化缓存
104
+ * · `needsCompositingLayer`:L3 特性(z-index / fixed / filter / shadow / 3D)→ 各占一块 backing store
105
+ * · `shareableContent`:可共享图形的资源标识(同图多层共享内存,M2 消费)
106
+ */
107
+ export interface PaintHint {
108
+ isMonochrome: boolean;
109
+ isPureBackground: boolean;
110
+ staticSubtree: boolean;
111
+ needsCompositingLayer: boolean;
112
+ shareableContent?: string;
113
+ }
114
+ /** 归一化后的属性集(= CSS Profile §4.5 的 ComputedStyle 的渲染子集) */
115
+ export interface PProps {
116
+ layout: LayoutProps;
117
+ paint: PaintProps;
118
+ text?: TextProps;
119
+ paintHint: PaintHint;
120
+ }
121
+ /** 编译期判定的节点标志(供后端/代码生成直接消费,运行时零判断) */
122
+ export interface PFlags {
123
+ /** 整棵子树无动态绑定(**含自身与所有后代**) */
124
+ isStatic: boolean;
125
+ /**
126
+ * 可拍平(编译期判定,见 D4 与 §12.3)——
127
+ * 判定条件:kind ∈ view/text/image ∧ 无事件 ∧ 无 transform ∧ 非动画目标 ∧ 非自定义组件根
128
+ * ∧ 静态子树 ∧ 不创建合成层
129
+ */
130
+ flattenEligible: boolean;
131
+ /** 布局边界:自身尺寸变化不影响父级(可跳过向上传播重排) */
132
+ isLayoutBoundary: boolean;
133
+ hasEvent: boolean;
134
+ isNativeHost: boolean;
135
+ }
136
+ /** 渲染节点(稳定整数 id——编译期分配,非字符串,便于二进制序列化) */
137
+ export interface PNode {
138
+ id: number;
139
+ kind: PKind;
140
+ /** 语义标签(诊断用——`layout.box` / `ui.text`;不参与绘制决策) */
141
+ semantic?: string;
142
+ props: PProps;
143
+ children: PNode[];
144
+ flags: PFlags;
145
+ /** 文本节点的静态字面量(动态文本走 bindings 的 text-content 槽) */
146
+ text?: string;
147
+ /** 源码定位(诊断/trace 用) */
148
+ source?: {
149
+ tag: string;
150
+ line?: number;
151
+ };
152
+ }
153
+ /** 动态绑定更新种类(运行时按种类走特化写值函数,避免运行时类型判断) */
154
+ export type UpdateKind = 'attr' | 'style' | 'text-content' | 'list-data' | 'visibility';
155
+ /** 动态绑定(编译期分配的槽位——运行时只「按槽位写值」,**不存在 VNode diff**,D1) */
156
+ export interface DynamicBinding {
157
+ /** 槽位索引(编译期连续分配) */
158
+ slotId: number;
159
+ /** 目标节点 id */
160
+ nodeId: number;
161
+ /** 目标属性键(归一化名,如 `text.color` / `layout.width`) */
162
+ propKey: string;
163
+ /** 更新表达式标识(指向编译期生成的更新表达式) */
164
+ exprId: string;
165
+ updateKind: UpdateKind;
166
+ /**
167
+ * 更新指令操作码(**编译期确定,运行时无分支**)
168
+ *
169
+ * ★号值即线上格式的判别字节,与 `@proteus-vue/slot-runtime` 的 `OpCode` 同源
170
+ * (**只允许一处定义**——号值不一致会让 TS 与 Rust 的字节流静默分叉)。
171
+ * 由 `inferOpCode(updateKind)` 推导,或在 raw 声明里显式给出(如 item 级 `LIST_UPDATE`)。
172
+ */
173
+ opCode: OpCode;
174
+ /**
175
+ * 更新分层(方案 §5):`L1` 槽位直写 / `L0` VDOM 兜底
176
+ *
177
+ * ★缺省必须是 **L0**(保守):编译期没证明安全的绑定,不许假设它安全——
178
+ * 误判为 L1 会导致**静默的 UI 不更新**(比慢 10 倍严重,方案 §12 第 3 条)。
179
+ */
180
+ tier: UpdateTier;
181
+ /**
182
+ * 编译期识别到的**响应式依赖标识**(L1 槽位订阅用;方案 §4)
183
+ *
184
+ * 空数组 = 尚未做依赖分析(V2 的编译期响应式转换会填充)。
185
+ * ★不接受「运行时才确定」的标识符——那属于准入条件 C2 未通过,应判 L0。
186
+ */
187
+ deps: string[];
188
+ }
189
+ /** 编译期诊断(Profile「超出 Profile 即编译期报错,不留到运行时静默降级」) */
190
+ export interface PDiagnostic {
191
+ severity: 'error' | 'warn';
192
+ code: string;
193
+ message: string;
194
+ /** 源码定位(tag / 行号) */
195
+ nodeId?: number;
196
+ source?: {
197
+ tag: string;
198
+ line?: number;
199
+ };
200
+ }
201
+ /** 渲染 IR 树(M0 产物;M1/M2 将其序列化为 LayoutTemplate + PatchTable) */
202
+ export interface PTree {
203
+ roots: PNode[];
204
+ bindings: DynamicBinding[];
205
+ diagnostics: PDiagnostic[];
206
+ /** 统计(trace / perf-ratchet 消费) */
207
+ stats: {
208
+ /** 节点总数 */
209
+ nodeCount: number;
210
+ /** 可拍平节点数(拍平率 = flattenable / nodeCount) */
211
+ flattenableCount: number;
212
+ /** 需合成层的节点数(预算监控) */
213
+ compositingCount: number;
214
+ };
215
+ }
216
+ import type { OpCode, UpdateTier } from '@proteus-vue/slot-runtime';
package/dist/schema.d.ts CHANGED
@@ -15,7 +15,7 @@ export interface ComponentIR {
15
15
  degradation?: Record<string, 'supported' | 'fallback' | 'unsupported'>;
16
16
  }
17
17
  /** 语义枚举(与 plan component-ir.schema.json 一致)——<domain>.<kind> */
18
- export declare const SEMANTIC_ENUM: readonly ["layout.box", "layout.inline", "layout.stack", "layout.grid", "layout.fluid", "layout.adaptive", "layout.fit", "layout.spacer", "layout.divider", "layout.scroll", "layout.virtual-list", "layout.masonry", "layout.split", "layout.safe", "layout.sidebar", "layout.aspect", "layout.zone", "ui.text", "ui.heading", "ui.rich-text", "ui.icon", "ui.image", "ui.avatar", "ui.media", "ui.canvas", "ui.svg", "ui.input", "ui.textarea", "ui.select", "ui.checkbox", "ui.radio", "ui.switch", "ui.slider", "ui.picker", "ui.form", "ui.button", "ui.list", "ui.nav", "ui.loading", "ui.scale", "ui.skeleton", "ui.progress", "ui.label", "ui.selection", "ui.camera", "shell.page", "shell.nav", "shell.tabbar", "shell.segment", "shell.drawer", "shell.modal", "shell.popover", "shell.toast", "shell.action-sheet", "shell.mask", "shell.popup", "shell.toolbar", "shell.page-container", "shell.keyboard-accessory", "shell.webview", "shell.ad", "ui.map", "gesture.draggable", "gesture.scrollable", "engineering.router-link", "engineering.transition", "engineering.animate", "engineering.error-boundary", "engineering.share-element", "capability.location", "capability.qr-code", "capability.camera"];
18
+ export declare const SEMANTIC_ENUM: readonly ["layout.box", "layout.inline", "layout.stack", "layout.grid", "layout.fluid", "layout.adaptive", "layout.fit", "layout.spacer", "layout.divider", "layout.scroll", "layout.virtual-list", "layout.masonry", "layout.split", "layout.safe", "layout.sidebar", "layout.aspect", "layout.zone", "layout.formfactor", "ui.text", "ui.heading", "ui.rich-text", "ui.icon", "ui.image", "ui.avatar", "ui.media", "ui.canvas", "ui.svg", "ui.input", "ui.textarea", "ui.select", "ui.checkbox", "ui.radio", "ui.switch", "ui.slider", "ui.picker", "ui.form", "ui.button", "ui.list", "ui.nav", "ui.loading", "ui.scale", "ui.skeleton", "ui.progress", "ui.label", "ui.selection", "ui.camera", "shell.page", "shell.nav", "shell.tabbar", "shell.segment", "shell.drawer", "shell.modal", "shell.popover", "shell.toast", "shell.action-sheet", "shell.mask", "shell.popup", "shell.toolbar", "shell.page-container", "shell.keyboard-accessory", "shell.webview", "shell.ad", "ui.map", "gesture.draggable", "gesture.scrollable", "engineering.router-link", "engineering.transition", "engineering.animate", "engineering.error-boundary", "engineering.share-element", "capability.location", "capability.qr-code", "capability.camera"];
19
19
  /** C-IR JSON Schema(等价于 plan component-ir.schema.json——TS 内嵌供校验/工具消费) */
20
20
  export declare const COMPONENT_IR_SCHEMA: {
21
21
  readonly $schema: "https://json-schema.org/draft/2020-12/schema";
@@ -30,7 +30,7 @@ export declare const COMPONENT_IR_SCHEMA: {
30
30
  };
31
31
  readonly semantic: {
32
32
  readonly type: "string";
33
- readonly enum: readonly ["layout.box", "layout.inline", "layout.stack", "layout.grid", "layout.fluid", "layout.adaptive", "layout.fit", "layout.spacer", "layout.divider", "layout.scroll", "layout.virtual-list", "layout.masonry", "layout.split", "layout.safe", "layout.sidebar", "layout.aspect", "layout.zone", "ui.text", "ui.heading", "ui.rich-text", "ui.icon", "ui.image", "ui.avatar", "ui.media", "ui.canvas", "ui.svg", "ui.input", "ui.textarea", "ui.select", "ui.checkbox", "ui.radio", "ui.switch", "ui.slider", "ui.picker", "ui.form", "ui.button", "ui.list", "ui.nav", "ui.loading", "ui.scale", "ui.skeleton", "ui.progress", "ui.label", "ui.selection", "ui.camera", "shell.page", "shell.nav", "shell.tabbar", "shell.segment", "shell.drawer", "shell.modal", "shell.popover", "shell.toast", "shell.action-sheet", "shell.mask", "shell.popup", "shell.toolbar", "shell.page-container", "shell.keyboard-accessory", "shell.webview", "shell.ad", "ui.map", "gesture.draggable", "gesture.scrollable", "engineering.router-link", "engineering.transition", "engineering.animate", "engineering.error-boundary", "engineering.share-element", "capability.location", "capability.qr-code", "capability.camera"];
33
+ readonly enum: readonly ["layout.box", "layout.inline", "layout.stack", "layout.grid", "layout.fluid", "layout.adaptive", "layout.fit", "layout.spacer", "layout.divider", "layout.scroll", "layout.virtual-list", "layout.masonry", "layout.split", "layout.safe", "layout.sidebar", "layout.aspect", "layout.zone", "layout.formfactor", "ui.text", "ui.heading", "ui.rich-text", "ui.icon", "ui.image", "ui.avatar", "ui.media", "ui.canvas", "ui.svg", "ui.input", "ui.textarea", "ui.select", "ui.checkbox", "ui.radio", "ui.switch", "ui.slider", "ui.picker", "ui.form", "ui.button", "ui.list", "ui.nav", "ui.loading", "ui.scale", "ui.skeleton", "ui.progress", "ui.label", "ui.selection", "ui.camera", "shell.page", "shell.nav", "shell.tabbar", "shell.segment", "shell.drawer", "shell.modal", "shell.popover", "shell.toast", "shell.action-sheet", "shell.mask", "shell.popup", "shell.toolbar", "shell.page-container", "shell.keyboard-accessory", "shell.webview", "shell.ad", "ui.map", "gesture.draggable", "gesture.scrollable", "engineering.router-link", "engineering.transition", "engineering.animate", "engineering.error-boundary", "engineering.share-element", "capability.location", "capability.qr-code", "capability.camera"];
34
34
  };
35
35
  readonly props: {
36
36
  readonly type: "object";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteus-vue/component-ir",
3
- "version": "0.3.0-beta.11",
3
+ "version": "0.3.0-beta.21",
4
4
  "description": "G-31 组件与 API 语义化(component-semantics-plan B1):Component IR(C-IR)schema + 属性约束校验 + semantic 映射——组件=语义、属性=约束、Backend 消费 semantic 而非 tag(零依赖纯逻辑)",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -19,7 +19,8 @@
19
19
  "README.md"
20
20
  ],
21
21
  "dependencies": {
22
- "@proteus-vue/contracts": "0.3.0-beta.11"
22
+ "@proteus-vue/contracts": "0.3.0-beta.11",
23
+ "@proteus-vue/slot-runtime": "0.3.0-beta.11"
23
24
  },
24
25
  "scripts": {
25
26
  "build": "tsc -p tsconfig.build.json --emitDeclarationOnly && esbuild src/index.ts --bundle --format=esm --platform=neutral --outfile=dist/index.js"