@wcstack/lint 1.32.0 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -96,6 +96,9 @@ var WcsDiagnosticCode = {
96
96
  // 同名 tag / filter の後勝ち禁止(§5-3)。override:true が無い再定義もこの collision で表す。
97
97
  ManifestTagCollision: "wcs/manifest-tag-collision",
98
98
  ManifestFilterCollision: "wcs/manifest-filter-collision",
99
+ // 同名 state の stateSchema が複数の application artifact に宣言されている(§5-3 の
100
+ // application 版・D8)。勝者なし: その state は未宣言扱い(schema 検証は沈黙)。
101
+ ManifestStateCollision: "wcs/manifest-state-collision",
99
102
  // 明示 override:true(§5-4)。衝突ではなく意図的な shadow の告知(info)。
100
103
  ManifestOverride: "wcs/manifest-override",
101
104
  // --- sidecar vs live declaration drift ---
@@ -168,7 +171,11 @@ var WcsDiagnosticCode = {
168
171
  // router/auto があるのに <base href> がない(SPA の basename 誤導出)。
169
172
  BaseHrefMissing: "wcs/base-href-missing",
170
173
  // @wcstack/signals と /dom エントリの同一ページ混在(リアクティブコア二重化)。
171
- SignalsDualEntry: "wcs/signals-dual-entry"
174
+ SignalsDualEntry: "wcs/signals-dual-entry",
175
+ // --- deprecations ---
176
+ // 名前付き State(`<wcs-state name>` / `path@name`)。v2 でマウント(`mount=` と接頭辞付きパス)に
177
+ // 置き換わる(docs/state-mount-design.md D16)。1.x では warning、v2 では parse error と同時に error。
178
+ NamedStateDeprecated: "wcs/named-state-deprecated"
172
179
  };
173
180
  function sortDiagnostics(diagnostics) {
174
181
  const severityRank = { error: 0, warning: 1, info: 2 };
@@ -909,226 +916,6 @@ var STRUCTURAL_DIRECTIVES = [...STRUCTURAL_BINDING_TYPE_SET].map((name) => ({
909
916
  ...STRUCTURAL_DIRECTIVE_INFO[name]
910
917
  }));
911
918
 
912
- // src/language/htmlParse.ts
913
- function parseWcsScriptBlocks(html, stateTagName = "wcs-state") {
914
- const blocks = [];
915
- let pos = 0;
916
- const len = html.length;
917
- while (pos < len) {
918
- if (html.startsWith("<!--", pos)) {
919
- const commentEnd = html.indexOf("-->", pos + 4);
920
- if (commentEnd === -1) break;
921
- pos = commentEnd + 3;
922
- continue;
923
- }
924
- const wcsMatch = matchOpenTag(html, pos, stateTagName);
925
- if (wcsMatch === null) {
926
- pos++;
927
- continue;
928
- }
929
- const stateName = extractAttribute(wcsMatch.tagContent, "name") ?? "default";
930
- pos = wcsMatch.end;
931
- const wcsCloseIdx = findCloseTag(html, pos, stateTagName);
932
- const wcsEnd = wcsCloseIdx === -1 ? len : wcsCloseIdx;
933
- while (pos < wcsEnd) {
934
- if (html.startsWith("<!--", pos)) {
935
- const commentEnd = html.indexOf("-->", pos + 4);
936
- if (commentEnd === -1) break;
937
- pos = commentEnd + 3;
938
- continue;
939
- }
940
- const scriptMatch = matchOpenTag(html, pos, "script");
941
- if (scriptMatch === null) {
942
- pos++;
943
- continue;
944
- }
945
- const typeAttr = extractAttribute(scriptMatch.tagContent, "type");
946
- if (typeAttr?.toLowerCase() !== "module") {
947
- pos = scriptMatch.end;
948
- continue;
949
- }
950
- const contentStart = scriptMatch.end;
951
- const scriptCloseIdx = findCloseTag(html, contentStart, "script");
952
- if (scriptCloseIdx === -1) {
953
- pos = contentStart;
954
- break;
955
- }
956
- const contentEnd = scriptCloseIdx;
957
- blocks.push({
958
- contentStart,
959
- contentEnd,
960
- content: html.slice(contentStart, contentEnd),
961
- stateName
962
- });
963
- pos = html.indexOf(">", scriptCloseIdx) + 1;
964
- if (pos === 0) break;
965
- }
966
- pos = wcsEnd;
967
- if (wcsCloseIdx !== -1) {
968
- const closeEnd = html.indexOf(">", wcsCloseIdx);
969
- if (closeEnd !== -1) pos = closeEnd + 1;
970
- }
971
- }
972
- return blocks;
973
- }
974
- function parseWcsStateElements(html, stateTagName = "wcs-state") {
975
- const elements = [];
976
- let pos = 0;
977
- const len = html.length;
978
- while (pos < len) {
979
- if (html.startsWith("<!--", pos)) {
980
- const commentEnd = html.indexOf("-->", pos + 4);
981
- if (commentEnd === -1) break;
982
- pos = commentEnd + 3;
983
- continue;
984
- }
985
- const wcsMatch = matchOpenTag(html, pos, stateTagName);
986
- if (wcsMatch === null) {
987
- pos++;
988
- continue;
989
- }
990
- const stateName = extractAttribute(wcsMatch.tagContent, "name") ?? "default";
991
- const jsonAttr = extractAttribute(wcsMatch.tagContent, "json") ?? void 0;
992
- const stateAttr = extractAttribute(wcsMatch.tagContent, "state") ?? void 0;
993
- const srcAttr = extractAttribute(wcsMatch.tagContent, "src") ?? void 0;
994
- const tagStart = pos;
995
- const tagEnd = wcsMatch.end;
996
- pos = wcsMatch.end;
997
- const scriptBlocks = [];
998
- const wcsCloseIdx = findCloseTag(html, pos, stateTagName);
999
- const wcsEnd = wcsCloseIdx === -1 ? len : wcsCloseIdx;
1000
- while (pos < wcsEnd) {
1001
- if (html.startsWith("<!--", pos)) {
1002
- const commentEnd = html.indexOf("-->", pos + 4);
1003
- if (commentEnd === -1) break;
1004
- pos = commentEnd + 3;
1005
- continue;
1006
- }
1007
- const scriptMatch = matchOpenTag(html, pos, "script");
1008
- if (scriptMatch === null) {
1009
- pos++;
1010
- continue;
1011
- }
1012
- const typeAttr = extractAttribute(scriptMatch.tagContent, "type");
1013
- if (typeAttr?.toLowerCase() !== "module") {
1014
- pos = scriptMatch.end;
1015
- continue;
1016
- }
1017
- const contentStart = scriptMatch.end;
1018
- const scriptCloseIdx = findCloseTag(html, contentStart, "script");
1019
- if (scriptCloseIdx === -1) {
1020
- pos = contentStart;
1021
- break;
1022
- }
1023
- scriptBlocks.push({
1024
- contentStart,
1025
- contentEnd: scriptCloseIdx,
1026
- content: html.slice(contentStart, scriptCloseIdx),
1027
- stateName
1028
- });
1029
- pos = html.indexOf(">", scriptCloseIdx) + 1;
1030
- if (pos === 0) break;
1031
- }
1032
- elements.push({ stateName, jsonAttr, stateAttr, srcAttr, scriptBlocks, tagStart, tagEnd });
1033
- pos = wcsEnd;
1034
- if (wcsCloseIdx !== -1) {
1035
- const closeEnd = html.indexOf(">", wcsCloseIdx);
1036
- if (closeEnd !== -1) pos = closeEnd + 1;
1037
- }
1038
- }
1039
- return elements;
1040
- }
1041
- function findScriptJsonById(html, id2) {
1042
- let pos = 0;
1043
- const len = html.length;
1044
- while (pos < len) {
1045
- if (html.startsWith("<!--", pos)) {
1046
- const commentEnd = html.indexOf("-->", pos + 4);
1047
- if (commentEnd === -1) break;
1048
- pos = commentEnd + 3;
1049
- continue;
1050
- }
1051
- const scriptMatch = matchOpenTag(html, pos, "script");
1052
- if (scriptMatch === null) {
1053
- pos++;
1054
- continue;
1055
- }
1056
- const typeAttr = extractAttribute(scriptMatch.tagContent, "type");
1057
- const idAttr = extractAttribute(scriptMatch.tagContent, "id");
1058
- if (typeAttr?.toLowerCase() === "application/json" && idAttr === id2) {
1059
- const contentStart = scriptMatch.end;
1060
- const scriptCloseIdx = findCloseTag(html, contentStart, "script");
1061
- if (scriptCloseIdx === -1) return null;
1062
- return html.slice(contentStart, scriptCloseIdx);
1063
- }
1064
- pos = scriptMatch.end;
1065
- }
1066
- return null;
1067
- }
1068
- function matchOpenTag(html, pos, tagName) {
1069
- if (html[pos] !== "<") return null;
1070
- const nameStart = pos + 1;
1071
- const nameEnd = nameStart + tagName.length;
1072
- if (nameEnd > html.length) return null;
1073
- const slice3 = html.slice(nameStart, nameEnd);
1074
- if (slice3.toLowerCase() !== tagName.toLowerCase()) return null;
1075
- const charAfter = html[nameEnd];
1076
- if (charAfter !== ">" && charAfter !== " " && charAfter !== " " && charAfter !== "\n" && charAfter !== "\r" && charAfter !== "/") {
1077
- return null;
1078
- }
1079
- let i = nameEnd;
1080
- let inSingleQuote = false;
1081
- let inDoubleQuote = false;
1082
- while (i < html.length) {
1083
- const ch = html[i];
1084
- if (inSingleQuote) {
1085
- if (ch === "'") inSingleQuote = false;
1086
- } else if (inDoubleQuote) {
1087
- if (ch === '"') inDoubleQuote = false;
1088
- } else if (ch === "'") {
1089
- inSingleQuote = true;
1090
- } else if (ch === '"') {
1091
- inDoubleQuote = true;
1092
- } else if (ch === ">") {
1093
- return {
1094
- start: pos,
1095
- end: i + 1,
1096
- tagContent: html.slice(nameEnd, i)
1097
- };
1098
- }
1099
- i++;
1100
- }
1101
- return null;
1102
- }
1103
- function findCloseTag(html, startPos, tagName) {
1104
- const pattern = "</" + tagName;
1105
- const patternLower = pattern.toLowerCase();
1106
- const htmlLower = html.toLowerCase();
1107
- let pos = startPos;
1108
- while (pos < html.length) {
1109
- const idx = htmlLower.indexOf(patternLower, pos);
1110
- if (idx === -1) return -1;
1111
- const afterIdx = idx + pattern.length;
1112
- if (afterIdx < html.length) {
1113
- const ch = html[afterIdx];
1114
- if (ch === ">" || ch === " " || ch === " " || ch === "\n" || ch === "\r") {
1115
- return idx;
1116
- }
1117
- }
1118
- pos = idx + 1;
1119
- }
1120
- return -1;
1121
- }
1122
- function extractAttribute(tagContent, attrName) {
1123
- const regex = new RegExp(
1124
- `(?:^|\\s)${attrName}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|(\\S+))`,
1125
- "i"
1126
- );
1127
- const match = tagContent.match(regex);
1128
- if (!match) return null;
1129
- return match[1] ?? match[2] ?? match[3] ?? null;
1130
- }
1131
-
1132
919
  // src/service/stateAnalyzer.ts
1133
920
  var RESERVED_STREAMS_KEY = "$streams";
1134
921
  var RESERVED_COMMAND_TOKENS_KEY = "$commandTokens";
@@ -1622,32 +1409,361 @@ function inferTypeHint(valueStart) {
1622
1409
  if (v.startsWith("{")) return "object";
1623
1410
  return void 0;
1624
1411
  }
1625
-
1626
- // src/service/statePathResolver.ts
1627
- function getStatePathsFromHtml(html, stateTagName = "wcs-state", fileReader) {
1628
- const elements = parseWcsStateElements(html, stateTagName);
1629
- const allPaths = [];
1630
- for (const element of elements) {
1631
- const paths = resolveElementPaths(element, html, fileReader);
1632
- allPaths.push(...paths);
1412
+ function analyzeSchemaPaths(schema, stateName = "default") {
1413
+ const paths = [];
1414
+ const defs = schema.$defs ?? {};
1415
+ collectSchemaObjectPaths(schema, "", paths, stateName, defs, 0);
1416
+ return paths;
1417
+ }
1418
+ function mergeSchemaCandidates(candidates, applicationStates) {
1419
+ if (applicationStates === void 0 || applicationStates.size === 0) return candidates;
1420
+ const schemaCandidates = [];
1421
+ const schemaKeys = /* @__PURE__ */ new Set();
1422
+ for (const [stateName, schema] of applicationStates) {
1423
+ for (const p of analyzeSchemaPaths(schema, stateName)) {
1424
+ schemaCandidates.push(p);
1425
+ schemaKeys.add(`${stateName} ${p.path}`);
1426
+ }
1633
1427
  }
1634
- return allPaths;
1428
+ const kept = candidates.filter((p) => !schemaKeys.has(`${p.stateName} ${p.path}`));
1429
+ return [...kept, ...schemaCandidates];
1635
1430
  }
1636
- function resolveElementPaths(element, html, fileReader) {
1637
- if (element.stateAttr) {
1638
- const jsonContent = findScriptJsonById(html, element.stateAttr);
1639
- if (jsonContent) {
1640
- const paths = analyzeJsonPaths(jsonContent, element.stateName);
1641
- if (paths.length > 0) return paths;
1431
+ function derefSchemaNodes(node, defs) {
1432
+ const out = [];
1433
+ const stack = [{ node, chain: /* @__PURE__ */ new Set() }];
1434
+ while (stack.length > 0) {
1435
+ const { node: n, chain } = stack.pop();
1436
+ if (n === null || typeof n !== "object") continue;
1437
+ if (typeof n.$ref === "string") {
1438
+ const match = /^#\/\$defs\/(.+)$/.exec(n.$ref);
1439
+ if (match === null || chain.has(n.$ref)) continue;
1440
+ const target = defs[match[1].replace(/~1/g, "/").replace(/~0/g, "~")];
1441
+ if (target === void 0) continue;
1442
+ stack.push({ node: target, chain: /* @__PURE__ */ new Set([...chain, n.$ref]) });
1443
+ continue;
1444
+ }
1445
+ if (Array.isArray(n.anyOf)) {
1446
+ for (let i = n.anyOf.length - 1; i >= 0; i--) stack.push({ node: n.anyOf[i], chain });
1447
+ continue;
1642
1448
  }
1449
+ out.push(n);
1643
1450
  }
1644
- if (element.srcAttr && fileReader) {
1645
- const paths = resolveSrcAttribute(element.srcAttr, element.stateName, fileReader);
1646
- if (paths.length > 0) return paths;
1451
+ return out;
1452
+ }
1453
+ function schemaTypeHint(nodes) {
1454
+ const hints = /* @__PURE__ */ new Set();
1455
+ for (const n of nodes) {
1456
+ const types = typeof n.type === "string" ? [n.type] : Array.isArray(n.type) ? n.type : [];
1457
+ if (types.length > 0) {
1458
+ for (const t of types) {
1459
+ if (t === "null") continue;
1460
+ hints.add(t === "integer" ? "number" : t);
1461
+ }
1462
+ continue;
1463
+ }
1464
+ if (Array.isArray(n.enum)) {
1465
+ for (const v of n.enum) {
1466
+ const h = inferJsonTypeHint(v);
1467
+ if (h !== void 0 && h !== "null") hints.add(h);
1468
+ }
1469
+ } else if (n.const !== void 0) {
1470
+ const h = inferJsonTypeHint(n.const);
1471
+ if (h !== void 0 && h !== "null") hints.add(h);
1472
+ } else if (n.properties !== void 0) {
1473
+ hints.add("object");
1474
+ } else if (n.items !== void 0) {
1475
+ hints.add("array");
1476
+ }
1647
1477
  }
1648
- if (element.jsonAttr) {
1649
- const paths = analyzeJsonPaths(element.jsonAttr, element.stateName);
1650
- if (paths.length > 0) return paths;
1478
+ return hints.size === 0 ? void 0 : [...hints].join("|");
1479
+ }
1480
+ function collectSchemaObjectPaths(node, prefix, paths, stateName, defs, depth) {
1481
+ if (depth >= MAX_OBJECT_NEST_DEPTH) return;
1482
+ const seen = /* @__PURE__ */ new Set();
1483
+ for (const n of derefSchemaNodes(node, defs)) {
1484
+ for (const [key, child] of Object.entries(n.properties ?? {})) {
1485
+ if (prefix === "" && key.startsWith("$")) continue;
1486
+ if (seen.has(key)) continue;
1487
+ seen.add(key);
1488
+ const path = prefix ? `${prefix}.${key}` : key;
1489
+ pushSchemaValuePaths(path, child, paths, stateName, defs, depth);
1490
+ }
1491
+ }
1492
+ }
1493
+ function pushSchemaValuePaths(path, node, paths, stateName, defs, depth) {
1494
+ const nodes = derefSchemaNodes(node, defs);
1495
+ const typeHint = schemaTypeHint(nodes);
1496
+ paths.push(withHint({ path, kind: "data", stateName, fromSchema: true }, typeHint));
1497
+ const items = nodes.map((n) => n.items).find((i) => i !== void 0 && i !== null && typeof i === "object");
1498
+ const isArray = items !== void 0 || (typeHint?.split("|").includes("array") ?? false);
1499
+ if (isArray) {
1500
+ const itemNodes = items !== void 0 ? derefSchemaNodes(items, defs) : [];
1501
+ paths.push(withHint({ path: `${path}.*`, kind: "list", stateName, fromSchema: true }, schemaTypeHint(itemNodes)));
1502
+ paths.push({ path: `${path}.length`, kind: "data", typeHint: "number", stateName, fromSchema: true });
1503
+ if (depth >= MAX_OBJECT_NEST_DEPTH) return;
1504
+ const seen = /* @__PURE__ */ new Set();
1505
+ for (const n of itemNodes) {
1506
+ for (const [childKey, childNode] of Object.entries(n.properties ?? {})) {
1507
+ if (seen.has(childKey)) continue;
1508
+ seen.add(childKey);
1509
+ pushSchemaValuePaths(`${path}.*.${childKey}`, childNode, paths, stateName, defs, depth + 1);
1510
+ }
1511
+ }
1512
+ return;
1513
+ }
1514
+ if (nodes.some((n) => n.properties !== void 0)) {
1515
+ collectSchemaObjectPaths(node, path, paths, stateName, defs, depth + 1);
1516
+ }
1517
+ }
1518
+ function withHint(candidate, typeHint) {
1519
+ return typeHint === void 0 ? candidate : { ...candidate, typeHint };
1520
+ }
1521
+
1522
+ // src/language/htmlParse.ts
1523
+ function parseWcsScriptBlocks(html, stateTagName = "wcs-state") {
1524
+ const blocks = [];
1525
+ let pos = 0;
1526
+ const len = html.length;
1527
+ while (pos < len) {
1528
+ if (html.startsWith("<!--", pos)) {
1529
+ const commentEnd = html.indexOf("-->", pos + 4);
1530
+ if (commentEnd === -1) break;
1531
+ pos = commentEnd + 3;
1532
+ continue;
1533
+ }
1534
+ const wcsMatch = matchOpenTag(html, pos, stateTagName);
1535
+ if (wcsMatch === null) {
1536
+ pos++;
1537
+ continue;
1538
+ }
1539
+ const stateName = extractAttribute(wcsMatch.tagContent, "name") ?? "default";
1540
+ pos = wcsMatch.end;
1541
+ const wcsCloseIdx = findCloseTag(html, pos, stateTagName);
1542
+ const wcsEnd = wcsCloseIdx === -1 ? len : wcsCloseIdx;
1543
+ while (pos < wcsEnd) {
1544
+ if (html.startsWith("<!--", pos)) {
1545
+ const commentEnd = html.indexOf("-->", pos + 4);
1546
+ if (commentEnd === -1) break;
1547
+ pos = commentEnd + 3;
1548
+ continue;
1549
+ }
1550
+ const scriptMatch = matchOpenTag(html, pos, "script");
1551
+ if (scriptMatch === null) {
1552
+ pos++;
1553
+ continue;
1554
+ }
1555
+ const typeAttr = extractAttribute(scriptMatch.tagContent, "type");
1556
+ if (typeAttr?.toLowerCase() !== "module") {
1557
+ pos = scriptMatch.end;
1558
+ continue;
1559
+ }
1560
+ const contentStart = scriptMatch.end;
1561
+ const scriptCloseIdx = findCloseTag(html, contentStart, "script");
1562
+ if (scriptCloseIdx === -1) {
1563
+ pos = contentStart;
1564
+ break;
1565
+ }
1566
+ const contentEnd = scriptCloseIdx;
1567
+ blocks.push({
1568
+ contentStart,
1569
+ contentEnd,
1570
+ content: html.slice(contentStart, contentEnd),
1571
+ stateName
1572
+ });
1573
+ pos = html.indexOf(">", scriptCloseIdx) + 1;
1574
+ if (pos === 0) break;
1575
+ }
1576
+ pos = wcsEnd;
1577
+ if (wcsCloseIdx !== -1) {
1578
+ const closeEnd = html.indexOf(">", wcsCloseIdx);
1579
+ if (closeEnd !== -1) pos = closeEnd + 1;
1580
+ }
1581
+ }
1582
+ return blocks;
1583
+ }
1584
+ function parseWcsStateElements(html, stateTagName = "wcs-state") {
1585
+ const elements = [];
1586
+ let pos = 0;
1587
+ const len = html.length;
1588
+ while (pos < len) {
1589
+ if (html.startsWith("<!--", pos)) {
1590
+ const commentEnd = html.indexOf("-->", pos + 4);
1591
+ if (commentEnd === -1) break;
1592
+ pos = commentEnd + 3;
1593
+ continue;
1594
+ }
1595
+ const wcsMatch = matchOpenTag(html, pos, stateTagName);
1596
+ if (wcsMatch === null) {
1597
+ pos++;
1598
+ continue;
1599
+ }
1600
+ const stateName = extractAttribute(wcsMatch.tagContent, "name") ?? "default";
1601
+ const jsonAttr = extractAttribute(wcsMatch.tagContent, "json") ?? void 0;
1602
+ const stateAttr = extractAttribute(wcsMatch.tagContent, "state") ?? void 0;
1603
+ const srcAttr = extractAttribute(wcsMatch.tagContent, "src") ?? void 0;
1604
+ const tagStart = pos;
1605
+ const tagEnd = wcsMatch.end;
1606
+ pos = wcsMatch.end;
1607
+ const scriptBlocks = [];
1608
+ const wcsCloseIdx = findCloseTag(html, pos, stateTagName);
1609
+ const wcsEnd = wcsCloseIdx === -1 ? len : wcsCloseIdx;
1610
+ while (pos < wcsEnd) {
1611
+ if (html.startsWith("<!--", pos)) {
1612
+ const commentEnd = html.indexOf("-->", pos + 4);
1613
+ if (commentEnd === -1) break;
1614
+ pos = commentEnd + 3;
1615
+ continue;
1616
+ }
1617
+ const scriptMatch = matchOpenTag(html, pos, "script");
1618
+ if (scriptMatch === null) {
1619
+ pos++;
1620
+ continue;
1621
+ }
1622
+ const typeAttr = extractAttribute(scriptMatch.tagContent, "type");
1623
+ if (typeAttr?.toLowerCase() !== "module") {
1624
+ pos = scriptMatch.end;
1625
+ continue;
1626
+ }
1627
+ const contentStart = scriptMatch.end;
1628
+ const scriptCloseIdx = findCloseTag(html, contentStart, "script");
1629
+ if (scriptCloseIdx === -1) {
1630
+ pos = contentStart;
1631
+ break;
1632
+ }
1633
+ scriptBlocks.push({
1634
+ contentStart,
1635
+ contentEnd: scriptCloseIdx,
1636
+ content: html.slice(contentStart, scriptCloseIdx),
1637
+ stateName
1638
+ });
1639
+ pos = html.indexOf(">", scriptCloseIdx) + 1;
1640
+ if (pos === 0) break;
1641
+ }
1642
+ elements.push({ stateName, jsonAttr, stateAttr, srcAttr, scriptBlocks, tagStart, tagEnd });
1643
+ pos = wcsEnd;
1644
+ if (wcsCloseIdx !== -1) {
1645
+ const closeEnd = html.indexOf(">", wcsCloseIdx);
1646
+ if (closeEnd !== -1) pos = closeEnd + 1;
1647
+ }
1648
+ }
1649
+ return elements;
1650
+ }
1651
+ function findScriptJsonById(html, id2) {
1652
+ let pos = 0;
1653
+ const len = html.length;
1654
+ while (pos < len) {
1655
+ if (html.startsWith("<!--", pos)) {
1656
+ const commentEnd = html.indexOf("-->", pos + 4);
1657
+ if (commentEnd === -1) break;
1658
+ pos = commentEnd + 3;
1659
+ continue;
1660
+ }
1661
+ const scriptMatch = matchOpenTag(html, pos, "script");
1662
+ if (scriptMatch === null) {
1663
+ pos++;
1664
+ continue;
1665
+ }
1666
+ const typeAttr = extractAttribute(scriptMatch.tagContent, "type");
1667
+ const idAttr = extractAttribute(scriptMatch.tagContent, "id");
1668
+ if (typeAttr?.toLowerCase() === "application/json" && idAttr === id2) {
1669
+ const contentStart = scriptMatch.end;
1670
+ const scriptCloseIdx = findCloseTag(html, contentStart, "script");
1671
+ if (scriptCloseIdx === -1) return null;
1672
+ return html.slice(contentStart, scriptCloseIdx);
1673
+ }
1674
+ pos = scriptMatch.end;
1675
+ }
1676
+ return null;
1677
+ }
1678
+ function matchOpenTag(html, pos, tagName) {
1679
+ if (html[pos] !== "<") return null;
1680
+ const nameStart = pos + 1;
1681
+ const nameEnd = nameStart + tagName.length;
1682
+ if (nameEnd > html.length) return null;
1683
+ const slice3 = html.slice(nameStart, nameEnd);
1684
+ if (slice3.toLowerCase() !== tagName.toLowerCase()) return null;
1685
+ const charAfter = html[nameEnd];
1686
+ if (charAfter !== ">" && charAfter !== " " && charAfter !== " " && charAfter !== "\n" && charAfter !== "\r" && charAfter !== "/") {
1687
+ return null;
1688
+ }
1689
+ let i = nameEnd;
1690
+ let inSingleQuote = false;
1691
+ let inDoubleQuote = false;
1692
+ while (i < html.length) {
1693
+ const ch = html[i];
1694
+ if (inSingleQuote) {
1695
+ if (ch === "'") inSingleQuote = false;
1696
+ } else if (inDoubleQuote) {
1697
+ if (ch === '"') inDoubleQuote = false;
1698
+ } else if (ch === "'") {
1699
+ inSingleQuote = true;
1700
+ } else if (ch === '"') {
1701
+ inDoubleQuote = true;
1702
+ } else if (ch === ">") {
1703
+ return {
1704
+ start: pos,
1705
+ end: i + 1,
1706
+ tagContent: html.slice(nameEnd, i)
1707
+ };
1708
+ }
1709
+ i++;
1710
+ }
1711
+ return null;
1712
+ }
1713
+ function findCloseTag(html, startPos, tagName) {
1714
+ const pattern = "</" + tagName;
1715
+ const patternLower = pattern.toLowerCase();
1716
+ const htmlLower = html.toLowerCase();
1717
+ let pos = startPos;
1718
+ while (pos < html.length) {
1719
+ const idx = htmlLower.indexOf(patternLower, pos);
1720
+ if (idx === -1) return -1;
1721
+ const afterIdx = idx + pattern.length;
1722
+ if (afterIdx < html.length) {
1723
+ const ch = html[afterIdx];
1724
+ if (ch === ">" || ch === " " || ch === " " || ch === "\n" || ch === "\r") {
1725
+ return idx;
1726
+ }
1727
+ }
1728
+ pos = idx + 1;
1729
+ }
1730
+ return -1;
1731
+ }
1732
+ function extractAttribute(tagContent, attrName) {
1733
+ const regex = new RegExp(
1734
+ `(?:^|\\s)${attrName}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|(\\S+))`,
1735
+ "i"
1736
+ );
1737
+ const match = tagContent.match(regex);
1738
+ if (!match) return null;
1739
+ return match[1] ?? match[2] ?? match[3] ?? null;
1740
+ }
1741
+
1742
+ // src/service/statePathResolver.ts
1743
+ function getStatePathsFromHtml(html, stateTagName = "wcs-state", fileReader) {
1744
+ const elements = parseWcsStateElements(html, stateTagName);
1745
+ const allPaths = [];
1746
+ for (const element of elements) {
1747
+ const paths = resolveElementPaths(element, html, fileReader);
1748
+ allPaths.push(...paths);
1749
+ }
1750
+ return allPaths;
1751
+ }
1752
+ function resolveElementPaths(element, html, fileReader) {
1753
+ if (element.stateAttr) {
1754
+ const jsonContent = findScriptJsonById(html, element.stateAttr);
1755
+ if (jsonContent) {
1756
+ const paths = analyzeJsonPaths(jsonContent, element.stateName);
1757
+ if (paths.length > 0) return paths;
1758
+ }
1759
+ }
1760
+ if (element.srcAttr && fileReader) {
1761
+ const paths = resolveSrcAttribute(element.srcAttr, element.stateName, fileReader);
1762
+ if (paths.length > 0) return paths;
1763
+ }
1764
+ if (element.jsonAttr) {
1765
+ const paths = analyzeJsonPaths(element.jsonAttr, element.stateName);
1766
+ if (paths.length > 0) return paths;
1651
1767
  }
1652
1768
  if (element.scriptBlocks.length > 0) {
1653
1769
  return element.scriptBlocks.flatMap(
@@ -1812,6 +1928,8 @@ var ja = {
1812
1928
  commandTokenUndeclared: (t) => `\u30B3\u30DE\u30F3\u30C9\u30C8\u30FC\u30AF\u30F3 "${t}" \u306F $commandTokens \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093`,
1813
1929
  streamPathMissing: (p) => `\u30D1\u30B9 "${p}" \u306F $streams \u5BA3\u8A00\u306B\u5B58\u5728\u3057\u307E\u305B\u3093`,
1814
1930
  pathMissing: (p) => `\u30D1\u30B9 "${p}" \u306F\u72B6\u614B\u5B9A\u7FA9\u306B\u5B58\u5728\u3057\u307E\u305B\u3093`,
1931
+ pathNonexistent: (p) => `\u30D1\u30B9 "${p}" \u306F\u5BA3\u8A00\u3055\u308C\u305F stateSchema \u306B\u5B58\u5728\u3057\u307E\u305B\u3093`,
1932
+ pathTypeMismatch: (p, label, expected, actual) => `\u30D1\u30B9 "${p}" \u306F stateSchema \u4E0A\u3067 ${actual} \u578B\u3067\u3059\u304C\u3001${label} \u306B\u306F${JA_EXPECTED_LABEL[expected]}\u304C\u5FC5\u8981\u3067\u3059`,
1815
1933
  expansionSuffix: (x) => `\uFF08\u5C55\u958B: ${x}\uFF09`,
1816
1934
  patternPathOutsideFor: (p) => `\u30D1\u30BF\u30FC\u30F3\u30D1\u30B9 "${p}" \u306F <template for> \u306E\u5916\u5074\u3067\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093`,
1817
1935
  omittedPathOutsideFor: (p) => `\u7701\u7565\u30D1\u30B9 "${p}" \u306F <template for> \u306E\u5916\u5074\u3067\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093`,
@@ -1851,7 +1969,9 @@ var ja = {
1851
1969
  storageSeedClobber: (path, raw) => `<wcs-storage> \u306E value \u30D0\u30A4\u30F3\u30C9\u5148 "${path}" \u304C ${raw} \u3067\u30B7\u30FC\u30C9\u3055\u308C\u3066\u3044\u307E\u3059\u3002\u521D\u671F\u66F8\u304D\u623B\u3057\u304C\u4FDD\u5B58\u5024\u3092\u4E0A\u66F8\u304D\u3057\u307E\u3059 \u2014 undefined \u3067\u30B7\u30FC\u30C9\uFF08\`${path}: undefined\`\uFF09\u3059\u308B\u304B manual \u3092\u4ED8\u3051\u3066\u304F\u3060\u3055\u3044`,
1852
1970
  devtoolsAfterState: () => `@wcstack/devtools/auto \u306F @wcstack/state/auto \u3088\u308A\u5148\u306B\u8AAD\u307F\u8FBC\u3093\u3067\u304F\u3060\u3055\u3044\uFF08\u5F8C\u3060\u3068\u914D\u7DDA\u53F0\u5E33\u304C\u30E9\u30A4\u30D6\u3067 captured \u3055\u308C\u307E\u305B\u3093\uFF09`,
1853
1971
  baseHrefMissing: () => `@wcstack/router \u3092\u4F7F\u3046 SPA \u306B\u306F <head> \u5185\u306E <base href="/"> \u304C\u5FC5\u8981\u3067\u3059\uFF08\u7121\u3044\u3068\u30C7\u30A3\u30FC\u30D7\u30EA\u30F3\u30AF\u3067 basename \u304C\u8AA4\u5C0E\u51FA\u3055\u308C\u307E\u3059\uFF09`,
1854
- signalsDualEntry: () => `@wcstack/signals \u3068 @wcstack/signals/dom \u304C\u540C\u4E00\u30DA\u30FC\u30B8\u304B\u3089 import \u3055\u308C\u3066\u3044\u307E\u3059\u3002CDN \u3067\u306F\u5404\u30A8\u30F3\u30C8\u30EA\u304C\u81EA\u5DF1\u5B8C\u7D50\u30D0\u30F3\u30C9\u30EB\u306E\u305F\u3081\u30EA\u30A2\u30AF\u30C6\u30A3\u30D6\u30B3\u30A2\u304C\u4E8C\u91CD\u5316\u3057\u3001\u5883\u754C\u3067\u53CD\u5FDC\u304C\u58CA\u308C\u307E\u3059 \u2014 \u3059\u3079\u3066 /dom \u30A8\u30F3\u30C8\u30EA\u304B\u3089 import \u3057\u3066\u304F\u3060\u3055\u3044`
1972
+ signalsDualEntry: () => `@wcstack/signals \u3068 @wcstack/signals/dom \u304C\u540C\u4E00\u30DA\u30FC\u30B8\u304B\u3089 import \u3055\u308C\u3066\u3044\u307E\u3059\u3002CDN \u3067\u306F\u5404\u30A8\u30F3\u30C8\u30EA\u304C\u81EA\u5DF1\u5B8C\u7D50\u30D0\u30F3\u30C9\u30EB\u306E\u305F\u3081\u30EA\u30A2\u30AF\u30C6\u30A3\u30D6\u30B3\u30A2\u304C\u4E8C\u91CD\u5316\u3057\u3001\u5883\u754C\u3067\u53CD\u5FDC\u304C\u58CA\u308C\u307E\u3059 \u2014 \u3059\u3079\u3066 /dom \u30A8\u30F3\u30C8\u30EA\u304B\u3089 import \u3057\u3066\u304F\u3060\u3055\u3044`,
1973
+ namedStateAttrDeprecated: (name) => `<wcs-state name="${name}"> \u306F v2 \u3067\u5EC3\u6B62\u3055\u308C\u307E\u3059\u3002\u30EB\u30FC\u30C8\u30C4\u30EA\u30FC\u3078\u306E\u30DE\u30A6\u30F3\u30C8 <wcs-state mount="${name}"> \u306B\u7F6E\u304D\u63DB\u3048\u3001\u30D1\u30B9\u306F "${name}.<path>" \u3067\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\uFF08docs/state-mount-design.md \xA79\uFF09`,
1974
+ namedStatePathDeprecated: (name) => name === "default" ? `"@default" \u306F\u4E0D\u8981\u3067\u3001v2 \u3067\u5EC3\u6B62\u3055\u308C\u307E\u3059\u3002"@default" \u3092\u5916\u3057\u3066\u304F\u3060\u3055\u3044\uFF08docs/state-mount-design.md \xA79\uFF09` : `"@${name}" \u306B\u3088\u308B state \u6307\u5B9A\u306F v2 \u3067\u5EC3\u6B62\u3055\u308C\u307E\u3059\u3002\u30DE\u30A6\u30F3\u30C8\u3057\u305F\u30C4\u30EA\u30FC\u3092 "${name}.<path>" \u3067\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\uFF08docs/state-mount-design.md \xA79\uFF09`
1855
1975
  };
1856
1976
  var EN_EXPECTED_LABEL = {
1857
1977
  array: "an array-typed path",
@@ -1867,6 +1987,8 @@ var en = {
1867
1987
  commandTokenUndeclared: (t) => `Command token "${t}" is not declared in $commandTokens`,
1868
1988
  streamPathMissing: (p) => `Path "${p}" does not exist in the $streams declaration`,
1869
1989
  pathMissing: (p) => `Path "${p}" does not exist in the state definition`,
1990
+ pathNonexistent: (p) => `Path "${p}" does not exist in the declared stateSchema`,
1991
+ pathTypeMismatch: (p, label, expected, actual) => `Path "${p}" is ${actual} in the stateSchema, but ${label} requires ${expected === "array" ? "an array" : expected === "boolean" ? "a boolean" : "a string"}`,
1870
1992
  expansionSuffix: (x) => ` (expanded: ${x})`,
1871
1993
  patternPathOutsideFor: (p) => `Pattern path "${p}" cannot be used outside a <template for>`,
1872
1994
  omittedPathOutsideFor: (p) => `Shorthand path "${p}" cannot be used outside a <template for>`,
@@ -1906,19 +2028,213 @@ var en = {
1906
2028
  storageSeedClobber: (path, raw) => `The <wcs-storage> value-bound slot "${path}" is seeded with ${raw}. The initial write-back overwrites the persisted value \u2014 seed it with undefined (\`${path}: undefined\`) or add manual`,
1907
2029
  devtoolsAfterState: () => `Load @wcstack/devtools/auto BEFORE @wcstack/state/auto (otherwise the wiring ledger is not captured live)`,
1908
2030
  baseHrefMissing: () => `An SPA using @wcstack/router needs <base href="/"> in <head> (without it, deep links misderive the basename)`,
1909
- signalsDualEntry: () => `Both @wcstack/signals and @wcstack/signals/dom are imported on this page. On a CDN each entry is a self-contained bundle, so the reactive core is duplicated and reactivity breaks at the seam \u2014 import everything from the single /dom entry`
2031
+ signalsDualEntry: () => `Both @wcstack/signals and @wcstack/signals/dom are imported on this page. On a CDN each entry is a self-contained bundle, so the reactive core is duplicated and reactivity breaks at the seam \u2014 import everything from the single /dom entry`,
2032
+ namedStateAttrDeprecated: (name) => `<wcs-state name="${name}"> is deprecated and will be removed in v2. Mount the state onto the root tree with <wcs-state mount="${name}"> and read it as "${name}.<path>" (docs/state-mount-design.md \xA79)`,
2033
+ namedStatePathDeprecated: (name) => name === "default" ? `The "@default" selector is redundant and will be removed in v2; drop it (docs/state-mount-design.md \xA79)` : `The "@${name}" state selector is deprecated and will be removed in v2. Read the mounted tree as "${name}.<path>" instead (docs/state-mount-design.md \xA79)`
1910
2034
  };
1911
2035
  var CATALOGS = { ja, en };
1912
2036
  function getMessages(locale3) {
1913
2037
  return CATALOGS[resolveLocale(locale3)];
1914
2038
  }
1915
2039
 
2040
+ // src/core/sidecar/schemaSubset.ts
2041
+ var ALLOWED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
2042
+ "type",
2043
+ "properties",
2044
+ "required",
2045
+ "items",
2046
+ "enum",
2047
+ "const",
2048
+ "anyOf",
2049
+ "$defs",
2050
+ "$ref"
2051
+ ]);
2052
+ var DiagnosticContext = class {
2053
+ constructor(spans) {
2054
+ this.spans = spans;
2055
+ }
2056
+ diagnostics = [];
2057
+ add(code, pointer2, message, severity, extra = {}, useKeySpan = false) {
2058
+ const span = this.spans.get(pointer2);
2059
+ const start = span === void 0 ? 0 : useKeySpan ? span.keyStart ?? span.start : span.start;
2060
+ const end = span === void 0 ? 0 : useKeySpan ? span.keyEnd ?? span.end : span.end;
2061
+ this.diagnostics.push({ code, start, end, message, severity, ...extra });
2062
+ }
2063
+ };
2064
+ function isSchemaObject(value) {
2065
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2066
+ }
2067
+ function isSchemaMap(value) {
2068
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2069
+ }
2070
+ function validateSchemaSubset(schema, pointerBase, ctx, rootDefs) {
2071
+ walkKeywords(schema, pointerBase, ctx, rootDefs);
2072
+ const safe = /* @__PURE__ */ new Set();
2073
+ detectCycles(schema, pointerBase, ctx, rootDefs, /* @__PURE__ */ new Set(), safe);
2074
+ for (const [name, def] of Object.entries(rootDefs)) {
2075
+ detectCycles(def, `${pointerBase}/$defs/${escape(name)}`, ctx, rootDefs, /* @__PURE__ */ new Set(), safe);
2076
+ }
2077
+ }
2078
+ function walkKeywords(node, ptr, ctx, rootDefs) {
2079
+ if (!isSchemaObject(node)) return;
2080
+ for (const keyword of Object.keys(node)) {
2081
+ if (!ALLOWED_SCHEMA_KEYWORDS.has(keyword)) {
2082
+ ctx.add(
2083
+ WcsDiagnosticCode.ManifestUnknownKeyword,
2084
+ `${ptr}/${escape(keyword)}`,
2085
+ `Unsupported schema keyword "${keyword}". Allowed: ${[...ALLOWED_SCHEMA_KEYWORDS].join(", ")}.`,
2086
+ "warning",
2087
+ {},
2088
+ true
2089
+ );
2090
+ }
2091
+ }
2092
+ if (typeof node.$ref === "string") {
2093
+ if (!node.$ref.startsWith("#/")) {
2094
+ ctx.add(
2095
+ WcsDiagnosticCode.ManifestExternalRef,
2096
+ `${ptr}/$ref`,
2097
+ `External $ref "${node.$ref}" is forbidden; only local "#/$defs/..." references are allowed.`,
2098
+ "error"
2099
+ );
2100
+ } else if (resolveLocalRef(node.$ref, rootDefs) === void 0) {
2101
+ ctx.add(
2102
+ WcsDiagnosticCode.ManifestRefUnresolved,
2103
+ `${ptr}/$ref`,
2104
+ `Unresolved local $ref "${node.$ref}".`,
2105
+ "error"
2106
+ );
2107
+ }
2108
+ }
2109
+ if (isSchemaMap(node.properties)) {
2110
+ for (const [name, child] of Object.entries(node.properties)) {
2111
+ walkKeywords(child, `${ptr}/properties/${escape(name)}`, ctx, rootDefs);
2112
+ }
2113
+ }
2114
+ if (node.items !== void 0 && isSchemaObject(node.items)) {
2115
+ walkKeywords(node.items, `${ptr}/items`, ctx, rootDefs);
2116
+ }
2117
+ if (Array.isArray(node.anyOf)) {
2118
+ node.anyOf.forEach((child, i) => walkKeywords(child, `${ptr}/anyOf/${i}`, ctx, rootDefs));
2119
+ }
2120
+ if (isSchemaMap(node.$defs)) {
2121
+ for (const [name, child] of Object.entries(node.$defs)) {
2122
+ walkKeywords(child, `${ptr}/$defs/${escape(name)}`, ctx, rootDefs);
2123
+ }
2124
+ }
2125
+ }
2126
+ function detectCycles(node, ptr, ctx, rootDefs, refStack, safe) {
2127
+ if (!isSchemaObject(node)) return;
2128
+ if (typeof node.$ref === "string") {
2129
+ const ref = node.$ref;
2130
+ if (!ref.startsWith("#/")) return;
2131
+ if (refStack.has(ref)) {
2132
+ ctx.add(WcsDiagnosticCode.ManifestRefCycle, `${ptr}/$ref`, `Cyclic $ref detected at "${ref}".`, "error");
2133
+ return;
2134
+ }
2135
+ if (safe.has(ref)) return;
2136
+ const target = resolveLocalRef(ref, rootDefs);
2137
+ if (target === void 0) return;
2138
+ refStack.add(ref);
2139
+ detectCycles(target, ptr, ctx, rootDefs, refStack, safe);
2140
+ refStack.delete(ref);
2141
+ safe.add(ref);
2142
+ return;
2143
+ }
2144
+ if (isSchemaMap(node.properties)) {
2145
+ for (const child of Object.values(node.properties)) detectCycles(child, ptr, ctx, rootDefs, refStack, safe);
2146
+ }
2147
+ if (node.items !== void 0 && isSchemaObject(node.items)) {
2148
+ detectCycles(node.items, ptr, ctx, rootDefs, refStack, safe);
2149
+ }
2150
+ if (Array.isArray(node.anyOf)) {
2151
+ for (const child of node.anyOf) detectCycles(child, ptr, ctx, rootDefs, refStack, safe);
2152
+ }
2153
+ }
2154
+ function resolveLocalRef(ref, rootDefs) {
2155
+ const match = /^#\/\$defs\/(.+)$/.exec(ref);
2156
+ if (match === null) return void 0;
2157
+ const name = match[1].replace(/~1/g, "/").replace(/~0/g, "~");
2158
+ return rootDefs[name];
2159
+ }
2160
+ function resolveSchemaPath(root, rootDefs, segments) {
2161
+ let current = root;
2162
+ for (let depth = 0; depth < segments.length; depth++) {
2163
+ const segment = segments[depth];
2164
+ const resolved = derefUnion(current, rootDefs);
2165
+ if (resolved.kind === "ref-error") return resolved;
2166
+ const candidates = resolved.nodes;
2167
+ if (segment === "*") {
2168
+ const items = firstDefined(candidates, (n) => isSchemaObject(n.items) ? n.items : void 0);
2169
+ if (items === void 0) {
2170
+ return { kind: "unknown" };
2171
+ }
2172
+ current = items;
2173
+ continue;
2174
+ }
2175
+ if (segment === "length" && candidates.some((n) => hasType(n, "array"))) {
2176
+ current = { type: "number" };
2177
+ continue;
2178
+ }
2179
+ const child = firstDefined(candidates, (n) => isSchemaMap(n.properties) ? n.properties[segment] : void 0);
2180
+ if (child !== void 0) {
2181
+ current = child;
2182
+ continue;
2183
+ }
2184
+ const anyObject = candidates.some((n) => hasType(n, "object") || isSchemaMap(n.properties));
2185
+ if (anyObject) {
2186
+ return { kind: "nonexistent", segment, depth };
2187
+ }
2188
+ return { kind: "unknown" };
2189
+ }
2190
+ const final = derefUnion(current, rootDefs);
2191
+ if (final.kind === "ref-error") return final;
2192
+ return { kind: "resolved", schema: final.nodes.length === 1 ? final.nodes[0] : current };
2193
+ }
2194
+ function derefUnion(node, rootDefs) {
2195
+ const out = [];
2196
+ const stack = [{ node, chain: /* @__PURE__ */ new Set() }];
2197
+ while (stack.length > 0) {
2198
+ const { node: n, chain } = stack.pop();
2199
+ if (typeof n.$ref === "string") {
2200
+ if (!n.$ref.startsWith("#/") || chain.has(n.$ref)) {
2201
+ return { kind: "ref-error", ref: n.$ref };
2202
+ }
2203
+ const target = resolveLocalRef(n.$ref, rootDefs);
2204
+ if (target === void 0) return { kind: "ref-error", ref: n.$ref };
2205
+ stack.push({ node: target, chain: /* @__PURE__ */ new Set([...chain, n.$ref]) });
2206
+ continue;
2207
+ }
2208
+ if (Array.isArray(n.anyOf)) {
2209
+ for (const branch of n.anyOf) stack.push({ node: branch, chain });
2210
+ continue;
2211
+ }
2212
+ out.push(n);
2213
+ }
2214
+ return { kind: "ok", nodes: out };
2215
+ }
2216
+ function firstDefined(nodes, pick) {
2217
+ for (const n of nodes) {
2218
+ const v = pick(n);
2219
+ if (v !== void 0) return v;
2220
+ }
2221
+ return void 0;
2222
+ }
2223
+ function hasType(node, t) {
2224
+ const type = node.type;
2225
+ if (type === void 0) return false;
2226
+ return Array.isArray(type) ? type.includes(t) : type === t;
2227
+ }
2228
+ function escape(key) {
2229
+ return key.replace(/~/g, "~0").replace(/\//g, "~1");
2230
+ }
2231
+
1916
2232
  // src/service/bindingValidator.ts
1917
2233
  var filterMap = new Map(BUILTIN_FILTERS.map((f) => [f.name, f]));
1918
- function validateBindings(html, attrName, stateTagName = "wcs-state", locale3, fileReader) {
2234
+ function validateBindings(html, attrName, stateTagName = "wcs-state", locale3, fileReader, applicationStates) {
1919
2235
  const diagnostics = [];
1920
2236
  const msgs = getMessages(locale3);
1921
- const statePaths = getStatePathsFromHtml(html, stateTagName, fileReader);
2237
+ const statePaths = mergeSchemaCandidates(getStatePathsFromHtml(html, stateTagName, fileReader), applicationStates);
1922
2238
  const pathsByState = /* @__PURE__ */ new Map();
1923
2239
  for (const p of statePaths) {
1924
2240
  const list = pathsByState.get(p.stateName) ?? [];
@@ -2041,16 +2357,17 @@ function validateBindings(html, attrName, stateTagName = "wcs-state", locale3, f
2041
2357
  }
2042
2358
  }
2043
2359
  if (checkPath) {
2044
- const message = validatePathExistence(checkPath, pathTrimmed, scopedPaths, scopedPathSet, commandNames, msgs);
2045
- if (message) {
2360
+ const schema = applicationStates?.get(parsed.targetState);
2361
+ const verdict = schema !== void 0 ? validateSchemaPathExistence(checkPath, pathTrimmed, scopedPaths, scopedPathSet, commandNames, schema, msgs) : toMissingVerdict(validatePathExistence(checkPath, pathTrimmed, scopedPaths, scopedPathSet, commandNames, msgs));
2362
+ if (verdict) {
2046
2363
  const pathOffset = binding.indexOf(parsed.path);
2047
2364
  const pathStart = bindingStart + pathOffset;
2048
2365
  diagnostics.push({
2049
- code: WcsDiagnosticCode.BindingPathMissing,
2366
+ code: verdict.code,
2050
2367
  start: pathStart,
2051
2368
  end: pathStart + pathTrimmed.length,
2052
- message: `${message}${pathTrimmed.startsWith(".") ? msgs.expansionSuffix(checkPath) : ""}`,
2053
- severity: "warning"
2369
+ message: `${verdict.message}${pathTrimmed.startsWith(".") ? msgs.expansionSuffix(checkPath) : ""}`,
2370
+ severity: verdict.severity
2054
2371
  });
2055
2372
  }
2056
2373
  }
@@ -2169,12 +2486,13 @@ function validateBindings(html, attrName, stateTagName = "wcs-state", locale3, f
2169
2486
  if (typeReq && resultType !== typeReq.expected) {
2170
2487
  const pathOffset = binding.indexOf(parsed.path);
2171
2488
  const pathStart = bindingStart + pathOffset;
2489
+ const schemaDefinite = typeReq.expected === "array" && parsed.filters.length === 0 && applicationStates?.has(parsed.targetState) === true && scopedPaths.some((p) => p.path === pathTrimmed && p.fromSchema === true);
2172
2490
  diagnostics.push({
2173
- code: WcsDiagnosticCode.BindingTypeExpectation,
2491
+ code: schemaDefinite ? WcsDiagnosticCode.PathTypeMismatch : WcsDiagnosticCode.BindingTypeExpectation,
2174
2492
  start: pathStart,
2175
2493
  end: pathStart + pathTrimmed.length,
2176
- message: msgs.typeExpectation(typeReq.label, typeReq.expected, resultType),
2177
- severity: typeReq.severity
2494
+ message: schemaDefinite ? msgs.pathTypeMismatch(pathTrimmed, typeReq.label, typeReq.expected, resultType) : msgs.typeExpectation(typeReq.label, typeReq.expected, resultType),
2495
+ severity: schemaDefinite ? "error" : typeReq.severity
2178
2496
  });
2179
2497
  }
2180
2498
  }
@@ -2345,6 +2663,20 @@ function validatePathExistence(checkPath, displayPath, scopedPaths, scopedPathSe
2345
2663
  }
2346
2664
  return null;
2347
2665
  }
2666
+ function toMissingVerdict(message) {
2667
+ return message ? { code: WcsDiagnosticCode.BindingPathMissing, message, severity: "warning" } : null;
2668
+ }
2669
+ function validateSchemaPathExistence(checkPath, displayPath, scopedPaths, scopedPathSet, commandNames, schema, msgs) {
2670
+ if (checkPath.startsWith("$")) {
2671
+ return toMissingVerdict(validatePathExistence(checkPath, displayPath, scopedPaths, scopedPathSet, commandNames, msgs));
2672
+ }
2673
+ if (scopedPathSet.has(checkPath)) return null;
2674
+ const resolution = resolveSchemaPath(schema, schema.$defs ?? {}, checkPath.split("."));
2675
+ if (resolution.kind === "nonexistent") {
2676
+ return { code: WcsDiagnosticCode.PathNonexistent, message: msgs.pathNonexistent(displayPath), severity: "error" };
2677
+ }
2678
+ return null;
2679
+ }
2348
2680
  function collectStructuralTemplates(html, attrName) {
2349
2681
  const escaped = attrName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2350
2682
  const attrRegex = new RegExp(`${escaped}\\s*=\\s*(["'])`, "i");
@@ -2746,10 +3078,19 @@ function isInsideTag(html, offset, tagName) {
2746
3078
  }
2747
3079
 
2748
3080
  // src/service/templateSyntaxValidator.ts
2749
- function validateTemplateSyntax(html, stateTagName, bindAttrName = "data-wcs", locale3, fileReader) {
3081
+ function validateTemplateSyntax(html, stateTagName, bindAttrName = "data-wcs", locale3, fileReader, applicationStates) {
2750
3082
  const diagnostics = [];
2751
3083
  const msgs = getMessages(locale3);
2752
- const allPaths = getStatePathsFromHtml(html, stateTagName, fileReader);
3084
+ const allPaths = mergeSchemaCandidates(getStatePathsFromHtml(html, stateTagName, fileReader), applicationStates);
3085
+ const defaultSchema = applicationStates?.get("default");
3086
+ const missingVerdict = (path, displayPath, pathSet2, scoped) => {
3087
+ if (isValidTemplatePath(path, pathSet2, scoped)) return null;
3088
+ if (defaultSchema !== void 0 && !path.startsWith("$")) {
3089
+ const resolution = resolveSchemaPath(defaultSchema, defaultSchema.$defs ?? {}, path.split("."));
3090
+ return resolution.kind === "nonexistent" ? { code: WcsDiagnosticCode.PathNonexistent, severity: "error", message: msgs.pathNonexistent(displayPath) } : null;
3091
+ }
3092
+ return { code: WcsDiagnosticCode.BindingPathMissing, severity: "warning", message: msgs.pathMissing(displayPath) };
3093
+ };
2753
3094
  if (allPaths.length === 0) return diagnostics;
2754
3095
  const defaultPaths = allPaths.filter((p) => p.stateName === "default");
2755
3096
  const pathSet = new Set(defaultPaths.map((p) => p.path));
@@ -2829,24 +3170,28 @@ function validateTemplateSyntax(html, stateTagName, bindAttrName = "data-wcs", l
2829
3170
  const forPath = insideFor ? getInnermostForPath(html, item.matchStart, bindAttrName) : null;
2830
3171
  if (forPath && !forPath.startsWith(".")) {
2831
3172
  const expandedPath = pathPart === "." ? `${forPath}.*` : `${forPath}.*.${pathPart.slice(1)}`;
2832
- if (!isValidTemplatePath(expandedPath, pathSet, defaultPaths)) {
3173
+ const verdict = missingVerdict(expandedPath, pathPart, pathSet, defaultPaths);
3174
+ if (verdict) {
2833
3175
  diagnostics.push({
2834
- code: WcsDiagnosticCode.BindingPathMissing,
3176
+ code: verdict.code,
2835
3177
  start: item.exprStart,
2836
3178
  end: item.exprStart + pathPart.length,
2837
- message: msgs.pathMissing(pathPart) + msgs.expansionSuffix(expandedPath),
2838
- severity: "warning"
3179
+ message: verdict.message + msgs.expansionSuffix(expandedPath),
3180
+ severity: verdict.severity
2839
3181
  });
2840
3182
  }
2841
3183
  }
2842
- } else if (!isValidTemplatePath(pathPart, pathSet, defaultPaths)) {
2843
- diagnostics.push({
2844
- code: WcsDiagnosticCode.BindingPathMissing,
2845
- start: item.exprStart,
2846
- end: item.exprStart + pathPart.length,
2847
- message: msgs.pathMissing(pathPart),
2848
- severity: "warning"
2849
- });
3184
+ } else {
3185
+ const verdict = missingVerdict(pathPart, pathPart, pathSet, defaultPaths);
3186
+ if (verdict) {
3187
+ diagnostics.push({
3188
+ code: verdict.code,
3189
+ start: item.exprStart,
3190
+ end: item.exprStart + pathPart.length,
3191
+ message: verdict.message,
3192
+ severity: verdict.severity
3193
+ });
3194
+ }
2850
3195
  }
2851
3196
  }
2852
3197
  for (let i = 1; i < parts.length; i++) {
@@ -3732,11 +4077,14 @@ var BUILTIN_TAGS = {
3732
4077
  "wcs-raf": {
3733
4078
  "package": "raf",
3734
4079
  "hasWcBindable": true,
3735
- "observedAttributes": [],
4080
+ "observedAttributes": [
4081
+ "reduced-motion"
4082
+ ],
3736
4083
  "inputs": {
3737
4084
  "once": "once",
3738
4085
  "repeat": "repeat",
3739
4086
  "manual": "manual",
4087
+ "reducedMotion": "reduced-motion",
3740
4088
  "trigger": null
3741
4089
  },
3742
4090
  "properties": [
@@ -4617,6 +4965,78 @@ function validateEntry(entry, pathSet, msgs) {
4617
4965
  return null;
4618
4966
  }
4619
4967
 
4968
+ // src/service/namedStateValidator.ts
4969
+ function findStateSelector(expr, embedded = false) {
4970
+ const colon = embedded ? -1 : expr.indexOf(":");
4971
+ const from = colon + 1;
4972
+ let depth = 0;
4973
+ let end = expr.length;
4974
+ for (let i = from; i < expr.length; i++) {
4975
+ const ch = expr[i];
4976
+ if (ch === "(") depth++;
4977
+ else if (ch === ")") depth = Math.max(0, depth - 1);
4978
+ else if (ch === "|" && depth === 0) {
4979
+ end = i;
4980
+ break;
4981
+ }
4982
+ }
4983
+ const at = expr.indexOf("@", from);
4984
+ if (at === -1 || at >= end) return null;
4985
+ const raw = expr.slice(at + 1, end);
4986
+ const name = raw.trim();
4987
+ const nameStart = at + 1 + (raw.length - raw.trimStart().length);
4988
+ return { start: at, end: name.length === 0 ? at + 1 : nameStart + name.length, name: name.length === 0 ? "default" : name };
4989
+ }
4990
+ function validateNamedState(html, attrName, stateTagName = "wcs-state", locale3) {
4991
+ const msgs = getMessages(locale3);
4992
+ const diagnostics = [];
4993
+ for (const element of parseWcsStateElements(html, stateTagName)) {
4994
+ const tagText = html.slice(element.tagStart, element.tagEnd);
4995
+ if (/\sbind-component(?=[\s=>/])/i.test(tagText)) continue;
4996
+ const match = /(?:^|\s)name\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(tagText);
4997
+ if (match === null) continue;
4998
+ const value = match[1] ?? match[2] ?? match[3] ?? "";
4999
+ const quoted = match[1] !== void 0 || match[2] !== void 0;
5000
+ const valueEnd = element.tagStart + match.index + match[0].length - (quoted ? 1 : 0);
5001
+ diagnostics.push({
5002
+ code: WcsDiagnosticCode.NamedStateDeprecated,
5003
+ start: valueEnd - value.length,
5004
+ end: valueEnd,
5005
+ message: msgs.namedStateAttrDeprecated(value),
5006
+ severity: "warning"
5007
+ });
5008
+ }
5009
+ for (const attr of findAllBindAttributes(html, attrName)) {
5010
+ let pos = 0;
5011
+ for (const expr of splitBindingExpressions(attr.value)) {
5012
+ const selector = findStateSelector(expr);
5013
+ if (selector !== null) {
5014
+ diagnostics.push({
5015
+ code: WcsDiagnosticCode.NamedStateDeprecated,
5016
+ start: attr.valueStart + pos + selector.start,
5017
+ end: attr.valueStart + pos + selector.end,
5018
+ message: msgs.namedStatePathDeprecated(selector.name),
5019
+ severity: "warning"
5020
+ });
5021
+ }
5022
+ pos += expr.length + 1;
5023
+ }
5024
+ }
5025
+ for (const mustache of findAllMustacheSyntax(html)) {
5026
+ const selector = findStateSelector(mustache.expression, true);
5027
+ if (selector !== null) {
5028
+ diagnostics.push({
5029
+ code: WcsDiagnosticCode.NamedStateDeprecated,
5030
+ start: mustache.exprStart + selector.start,
5031
+ end: mustache.exprStart + selector.end,
5032
+ message: msgs.namedStatePathDeprecated(selector.name),
5033
+ severity: "warning"
5034
+ });
5035
+ }
5036
+ }
5037
+ return diagnostics;
5038
+ }
5039
+
4620
5040
  // ../state/dist/parser.esm.js
4621
5041
  var DELIMITER2 = ".";
4622
5042
  var WILDCARD2 = "*";
@@ -5480,6 +5900,7 @@ function parseStatePart(statePart) {
5480
5900
  } else {
5481
5901
  stateAndPath = statePart.trim();
5482
5902
  }
5903
+ if (stateAndPath.indexOf(STATE_NAME_SEPARATOR3) !== -1) ;
5483
5904
  const [statePathName, stateName = "default"] = stateAndPath.split(STATE_NAME_SEPARATOR3).map(trimFn);
5484
5905
  const pathInfo = getPathInfo(statePathName);
5485
5906
  return {
@@ -6077,154 +6498,6 @@ function validateSemantics(html, stateTagName = "wcs-state", locale3, bindAttrNa
6077
6498
  return out;
6078
6499
  }
6079
6500
 
6080
- // src/core/validateDocument.ts
6081
- function validateDocument(text, options = {}) {
6082
- const bindAttribute = options.bindAttribute ?? "data-wcs";
6083
- const stateTagName = options.stateTagName ?? "wcs-state";
6084
- const locale3 = options.locale;
6085
- const fileReader = options.fileReader;
6086
- const out = [];
6087
- out.push(...validateBindings(text, bindAttribute, stateTagName, locale3, fileReader));
6088
- out.push(...validateTemplateSyntax(text, stateTagName, bindAttribute, locale3, fileReader));
6089
- out.push(...validateIoNodes(text, bindAttribute, stateTagName, locale3, fileReader));
6090
- out.push(...validateAriaAttributes(text, bindAttribute, locale3));
6091
- out.push(...validateDocumentEnv(text, locale3));
6092
- out.push(...validateSemantics(text, stateTagName, locale3, bindAttribute));
6093
- out.push(...validateArrayMutations(text, stateTagName, locale3));
6094
- out.push(...validateWatchDeclarations(text, stateTagName, locale3));
6095
- for (const d of validateStateTypes(text, stateTagName, locale3)) {
6096
- out.push({ code: WcsDiagnosticCode.TypeAnnotation, start: d.start, end: d.end, message: d.message, severity: d.severity });
6097
- }
6098
- for (const d of validateNestedAssigns(text, stateTagName, locale3)) {
6099
- out.push({ code: WcsDiagnosticCode.NestedAssign, start: d.start, end: d.end, message: d.message, severity: d.severity });
6100
- }
6101
- return sortDiagnostics(out);
6102
- }
6103
-
6104
- // src/core/sidecar/schemaSubset.ts
6105
- var ALLOWED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
6106
- "type",
6107
- "properties",
6108
- "required",
6109
- "items",
6110
- "enum",
6111
- "const",
6112
- "anyOf",
6113
- "$defs",
6114
- "$ref"
6115
- ]);
6116
- var DiagnosticContext = class {
6117
- constructor(spans) {
6118
- this.spans = spans;
6119
- }
6120
- diagnostics = [];
6121
- add(code, pointer2, message, severity, extra = {}, useKeySpan = false) {
6122
- const span = this.spans.get(pointer2);
6123
- const start = span === void 0 ? 0 : useKeySpan ? span.keyStart ?? span.start : span.start;
6124
- const end = span === void 0 ? 0 : useKeySpan ? span.keyEnd ?? span.end : span.end;
6125
- this.diagnostics.push({ code, start, end, message, severity, ...extra });
6126
- }
6127
- };
6128
- function isSchemaObject(value) {
6129
- return value !== null && typeof value === "object" && !Array.isArray(value);
6130
- }
6131
- function isSchemaMap(value) {
6132
- return value !== null && typeof value === "object" && !Array.isArray(value);
6133
- }
6134
- function validateSchemaSubset(schema, pointerBase, ctx, rootDefs) {
6135
- walkKeywords(schema, pointerBase, ctx, rootDefs);
6136
- const safe = /* @__PURE__ */ new Set();
6137
- detectCycles(schema, pointerBase, ctx, rootDefs, /* @__PURE__ */ new Set(), safe);
6138
- for (const [name, def] of Object.entries(rootDefs)) {
6139
- detectCycles(def, `${pointerBase}/$defs/${escape(name)}`, ctx, rootDefs, /* @__PURE__ */ new Set(), safe);
6140
- }
6141
- }
6142
- function walkKeywords(node, ptr, ctx, rootDefs) {
6143
- if (!isSchemaObject(node)) return;
6144
- for (const keyword of Object.keys(node)) {
6145
- if (!ALLOWED_SCHEMA_KEYWORDS.has(keyword)) {
6146
- ctx.add(
6147
- WcsDiagnosticCode.ManifestUnknownKeyword,
6148
- `${ptr}/${escape(keyword)}`,
6149
- `Unsupported schema keyword "${keyword}". Allowed: ${[...ALLOWED_SCHEMA_KEYWORDS].join(", ")}.`,
6150
- "warning",
6151
- {},
6152
- true
6153
- );
6154
- }
6155
- }
6156
- if (typeof node.$ref === "string") {
6157
- if (!node.$ref.startsWith("#/")) {
6158
- ctx.add(
6159
- WcsDiagnosticCode.ManifestExternalRef,
6160
- `${ptr}/$ref`,
6161
- `External $ref "${node.$ref}" is forbidden; only local "#/$defs/..." references are allowed.`,
6162
- "error"
6163
- );
6164
- } else if (resolveLocalRef(node.$ref, rootDefs) === void 0) {
6165
- ctx.add(
6166
- WcsDiagnosticCode.ManifestRefUnresolved,
6167
- `${ptr}/$ref`,
6168
- `Unresolved local $ref "${node.$ref}".`,
6169
- "error"
6170
- );
6171
- }
6172
- }
6173
- if (isSchemaMap(node.properties)) {
6174
- for (const [name, child] of Object.entries(node.properties)) {
6175
- walkKeywords(child, `${ptr}/properties/${escape(name)}`, ctx, rootDefs);
6176
- }
6177
- }
6178
- if (node.items !== void 0 && isSchemaObject(node.items)) {
6179
- walkKeywords(node.items, `${ptr}/items`, ctx, rootDefs);
6180
- }
6181
- if (Array.isArray(node.anyOf)) {
6182
- node.anyOf.forEach((child, i) => walkKeywords(child, `${ptr}/anyOf/${i}`, ctx, rootDefs));
6183
- }
6184
- if (isSchemaMap(node.$defs)) {
6185
- for (const [name, child] of Object.entries(node.$defs)) {
6186
- walkKeywords(child, `${ptr}/$defs/${escape(name)}`, ctx, rootDefs);
6187
- }
6188
- }
6189
- }
6190
- function detectCycles(node, ptr, ctx, rootDefs, refStack, safe) {
6191
- if (!isSchemaObject(node)) return;
6192
- if (typeof node.$ref === "string") {
6193
- const ref = node.$ref;
6194
- if (!ref.startsWith("#/")) return;
6195
- if (refStack.has(ref)) {
6196
- ctx.add(WcsDiagnosticCode.ManifestRefCycle, `${ptr}/$ref`, `Cyclic $ref detected at "${ref}".`, "error");
6197
- return;
6198
- }
6199
- if (safe.has(ref)) return;
6200
- const target = resolveLocalRef(ref, rootDefs);
6201
- if (target === void 0) return;
6202
- refStack.add(ref);
6203
- detectCycles(target, ptr, ctx, rootDefs, refStack, safe);
6204
- refStack.delete(ref);
6205
- safe.add(ref);
6206
- return;
6207
- }
6208
- if (isSchemaMap(node.properties)) {
6209
- for (const child of Object.values(node.properties)) detectCycles(child, ptr, ctx, rootDefs, refStack, safe);
6210
- }
6211
- if (node.items !== void 0 && isSchemaObject(node.items)) {
6212
- detectCycles(node.items, ptr, ctx, rootDefs, refStack, safe);
6213
- }
6214
- if (Array.isArray(node.anyOf)) {
6215
- for (const child of node.anyOf) detectCycles(child, ptr, ctx, rootDefs, refStack, safe);
6216
- }
6217
- }
6218
- function resolveLocalRef(ref, rootDefs) {
6219
- const match = /^#\/\$defs\/(.+)$/.exec(ref);
6220
- if (match === null) return void 0;
6221
- const name = match[1].replace(/~1/g, "/").replace(/~0/g, "~");
6222
- return rootDefs[name];
6223
- }
6224
- function escape(key) {
6225
- return key.replace(/~/g, "~0").replace(/\//g, "~1");
6226
- }
6227
-
6228
6501
  // src/core/sidecar/jsonSource.ts
6229
6502
  var JsonReader = class {
6230
6503
  constructor(text) {
@@ -6488,8 +6761,13 @@ function resolvePackageContracts(loaded) {
6488
6761
  const collided = /* @__PURE__ */ new Set();
6489
6762
  const firstSource = /* @__PURE__ */ new Map();
6490
6763
  const filterOwner = /* @__PURE__ */ new Map();
6764
+ const stateOwner = /* @__PURE__ */ new Map();
6765
+ const stateWinners = /* @__PURE__ */ new Map();
6766
+ const collidedStates = /* @__PURE__ */ new Set();
6767
+ let hasApplicationArtifact = false;
6491
6768
  for (const lm of loaded) {
6492
6769
  if (lm.manifest === null) continue;
6770
+ if (lm.manifest.kind === "application") hasApplicationArtifact = true;
6493
6771
  const types = lm.manifest.manifestExtensions?.["wcstack.types"];
6494
6772
  if (lm.manifest.kind === "package" && types !== void 0) {
6495
6773
  for (const [tag, component] of Object.entries(types.components ?? {})) {
@@ -6541,13 +6819,102 @@ function resolvePackageContracts(loaded) {
6541
6819
  );
6542
6820
  }
6543
6821
  }
6822
+ if (lm.manifest.kind === "application" && application?.states !== void 0) {
6823
+ for (const [name, entry] of Object.entries(application.states)) {
6824
+ const schema = entry?.stateSchema;
6825
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) continue;
6826
+ const priorSource = stateOwner.get(name);
6827
+ if (priorSource === void 0) {
6828
+ stateOwner.set(name, lm.artifact.source);
6829
+ stateWinners.set(name, schema);
6830
+ continue;
6831
+ }
6832
+ collidedStates.add(name);
6833
+ stateWinners.delete(name);
6834
+ ctxFor(lm).add(
6835
+ WcsDiagnosticCode.ManifestStateCollision,
6836
+ pointer("manifestExtensions", "wcstack.application", "states", name),
6837
+ `State "${name}" declares a stateSchema in multiple application artifacts (also in "${priorSource}"); neither is used.`,
6838
+ "error",
6839
+ { statePath: name },
6840
+ true
6841
+ );
6842
+ }
6843
+ }
6544
6844
  }
6545
6845
  const diagnosticsBySource = /* @__PURE__ */ new Map();
6546
6846
  for (const [source, diags] of perSource) {
6547
6847
  const kept = diags.filter((d) => !(d.code === WcsDiagnosticCode.ManifestOverride && d.tag !== void 0 && collided.has(d.tag)));
6548
6848
  if (kept.length > 0) diagnosticsBySource.set(source, kept);
6549
6849
  }
6550
- return { tags: winners, diagnosticsBySource };
6850
+ return { tags: winners, applicationStates: stateWinners, hasApplicationArtifact, diagnosticsBySource };
6851
+ }
6852
+
6853
+ // src/core/sidecar/discover.ts
6854
+ var APPLICATION_MANIFEST_FILENAME = "wcstack.manifest.json";
6855
+ var MAX_ASCEND = 16;
6856
+ function discoverApplicationManifest(fileReader) {
6857
+ for (let up = 0; up <= MAX_ASCEND; up++) {
6858
+ const relativePath = `${"../".repeat(up)}${APPLICATION_MANIFEST_FILENAME}`;
6859
+ const text = fileReader(relativePath);
6860
+ if (text === void 0) continue;
6861
+ const loaded = loadManifest({ text, source: relativePath });
6862
+ return { relativePath, text, loaded, states: applicationStatesOf(loaded) };
6863
+ }
6864
+ return void 0;
6865
+ }
6866
+ function applicationStatesOf(loaded) {
6867
+ const states = /* @__PURE__ */ new Map();
6868
+ const manifest = loaded.manifest;
6869
+ if (manifest === null || manifest.kind !== "application") return states;
6870
+ const application = manifest.manifestExtensions?.["wcstack.application"];
6871
+ for (const [name, entry] of Object.entries(application?.states ?? {})) {
6872
+ const schema = entry?.stateSchema;
6873
+ if (schema !== null && typeof schema === "object" && !Array.isArray(schema)) {
6874
+ states.set(name, schema);
6875
+ }
6876
+ }
6877
+ return states;
6878
+ }
6879
+ function joinRelativeSource(htmlSource, relativePath) {
6880
+ const sepIndex = Math.max(htmlSource.lastIndexOf("/"), htmlSource.lastIndexOf("\\"));
6881
+ const dirSegments = sepIndex === -1 ? [] : htmlSource.slice(0, sepIndex).split(/[\\/]/);
6882
+ for (const segment of relativePath.split("/")) {
6883
+ if (segment === "" || segment === ".") continue;
6884
+ if (segment === "..") {
6885
+ if (dirSegments.length > 0 && dirSegments[dirSegments.length - 1] !== "..") dirSegments.pop();
6886
+ else dirSegments.push("..");
6887
+ continue;
6888
+ }
6889
+ dirSegments.push(segment);
6890
+ }
6891
+ return dirSegments.join("/");
6892
+ }
6893
+
6894
+ // src/core/validateDocument.ts
6895
+ function validateDocument(text, options = {}) {
6896
+ const bindAttribute = options.bindAttribute ?? "data-wcs";
6897
+ const stateTagName = options.stateTagName ?? "wcs-state";
6898
+ const locale3 = options.locale;
6899
+ const fileReader = options.fileReader;
6900
+ const applicationStates = options.applicationStates ?? (fileReader !== void 0 ? discoverApplicationManifest(fileReader)?.states : void 0);
6901
+ const out = [];
6902
+ out.push(...validateBindings(text, bindAttribute, stateTagName, locale3, fileReader, applicationStates));
6903
+ out.push(...validateTemplateSyntax(text, stateTagName, bindAttribute, locale3, fileReader, applicationStates));
6904
+ out.push(...validateIoNodes(text, bindAttribute, stateTagName, locale3, fileReader));
6905
+ out.push(...validateAriaAttributes(text, bindAttribute, locale3));
6906
+ out.push(...validateDocumentEnv(text, locale3));
6907
+ out.push(...validateSemantics(text, stateTagName, locale3, bindAttribute));
6908
+ out.push(...validateArrayMutations(text, stateTagName, locale3));
6909
+ out.push(...validateWatchDeclarations(text, stateTagName, locale3));
6910
+ out.push(...validateNamedState(text, bindAttribute, stateTagName, locale3));
6911
+ for (const d of validateStateTypes(text, stateTagName, locale3)) {
6912
+ out.push({ code: WcsDiagnosticCode.TypeAnnotation, start: d.start, end: d.end, message: d.message, severity: d.severity });
6913
+ }
6914
+ for (const d of validateNestedAssigns(text, stateTagName, locale3)) {
6915
+ out.push({ code: WcsDiagnosticCode.NestedAssign, start: d.start, end: d.end, message: d.message, severity: d.severity });
6916
+ }
6917
+ return sortDiagnostics(out);
6551
6918
  }
6552
6919
 
6553
6920
  // src/core/sidecar/drift.ts
@@ -6606,13 +6973,24 @@ function checkDrift(tag, component, live, ctx) {
6606
6973
  }
6607
6974
 
6608
6975
  // src/core/sidecar/validate.ts
6976
+ function validateManifestArtifact(artifact) {
6977
+ const loaded = loadManifest(artifact);
6978
+ validateLoadedSchemas(loaded);
6979
+ return sortDiagnostics(loaded.ctx.diagnostics);
6980
+ }
6609
6981
  function validateLoadedSchemas(loaded) {
6610
6982
  if (loaded.manifest === null) return;
6611
6983
  const types = loaded.manifest.manifestExtensions?.["wcstack.types"];
6612
- if (types === void 0) return;
6613
- for (const [tag, component] of Object.entries(types.components ?? {})) {
6984
+ for (const [tag, component] of Object.entries(types?.components ?? {})) {
6614
6985
  validateComponentSchemas(tag, component, loaded.ctx);
6615
6986
  }
6987
+ const application = loaded.manifest.manifestExtensions?.["wcstack.application"];
6988
+ for (const [name, entry] of Object.entries(application?.states ?? {})) {
6989
+ const schema = entry?.stateSchema;
6990
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) continue;
6991
+ const ptr = `${pointer("manifestExtensions", "wcstack.application", "states", name)}/stateSchema`;
6992
+ validateSchemaSubset(schema, ptr, loaded.ctx, schema.$defs ?? {});
6993
+ }
6616
6994
  }
6617
6995
  function validateComponentSchemas(tag, component, ctx) {
6618
6996
  const base = pointer("manifestExtensions", "wcstack.types", "components", tag);
@@ -6662,7 +7040,9 @@ function validateManifestSet(input) {
6662
7040
  return {
6663
7041
  diagnostics: sortDiagnostics(all),
6664
7042
  byArtifact: sortedByArtifact,
6665
- resolvedTags
7043
+ resolvedTags,
7044
+ resolvedStates: resolved.applicationStates,
7045
+ hasApplicationArtifact: resolved.hasApplicationArtifact
6666
7046
  };
6667
7047
  }
6668
7048
  function escapePtr(key) {
@@ -6673,13 +7053,9 @@ function escapePtr(key) {
6673
7053
  var severityLabel = { error: "error", warning: "warning", info: "info" };
6674
7054
  function runValidation(inputs, options = {}) {
6675
7055
  const diagnosticsBySource = /* @__PURE__ */ new Map();
6676
- for (const input of inputs) {
6677
- if (input.kind === "html") {
6678
- const docOptions = input.fileReader !== void 0 ? { ...options, fileReader: input.fileReader } : options;
6679
- diagnosticsBySource.set(input.source, validateDocument(input.text, docOptions));
6680
- }
6681
- }
7056
+ const textBySource = new Map(inputs.map((i) => [i.source, i.text]));
6682
7057
  const manifestInputs = inputs.filter((i) => i.kind === "manifest");
7058
+ let explicitStates;
6683
7059
  if (manifestInputs.length > 0) {
6684
7060
  const result = validateManifestSet({
6685
7061
  artifacts: manifestInputs.map((m) => ({ text: m.text, source: m.source })),
@@ -6688,8 +7064,29 @@ function runValidation(inputs, options = {}) {
6688
7064
  for (const input of manifestInputs) {
6689
7065
  diagnosticsBySource.set(input.source, result.byArtifact.get(input.source) ?? []);
6690
7066
  }
7067
+ if (result.hasApplicationArtifact) explicitStates = result.resolvedStates;
7068
+ }
7069
+ for (const input of inputs) {
7070
+ if (input.kind !== "html") continue;
7071
+ let applicationStates = explicitStates;
7072
+ if (applicationStates === void 0 && input.fileReader !== void 0) {
7073
+ const discovered = discoverApplicationManifest(input.fileReader);
7074
+ applicationStates = discovered?.states ?? /* @__PURE__ */ new Map();
7075
+ if (discovered !== void 0) {
7076
+ const source = joinRelativeSource(input.source, discovered.relativePath);
7077
+ if (!diagnosticsBySource.has(source)) {
7078
+ textBySource.set(source, discovered.text);
7079
+ diagnosticsBySource.set(source, validateManifestArtifact({ text: discovered.text, source }));
7080
+ }
7081
+ }
7082
+ }
7083
+ const docOptions = {
7084
+ ...options,
7085
+ ...input.fileReader !== void 0 ? { fileReader: input.fileReader } : {},
7086
+ ...applicationStates !== void 0 ? { applicationStates } : {}
7087
+ };
7088
+ diagnosticsBySource.set(input.source, validateDocument(input.text, docOptions));
6691
7089
  }
6692
- const textBySource = new Map(inputs.map((i) => [i.source, i.text]));
6693
7090
  const lines = [];
6694
7091
  let errorCount = 0;
6695
7092
  let warningCount = 0;
@@ -6711,7 +7108,7 @@ function runValidation(inputs, options = {}) {
6711
7108
  errorCount,
6712
7109
  warningCount,
6713
7110
  infoCount,
6714
- exitCode: errorCount > 0 ? 1 : 0,
7111
+ exitCode: errorCount > 0 || options.strict === true && warningCount > 0 ? 1 : 0,
6715
7112
  diagnosticsBySource
6716
7113
  };
6717
7114
  }
@@ -6728,6 +7125,7 @@ function parseArgs(argv) {
6728
7125
  else if (arg.startsWith("--state-tag=")) options.stateTagName = arg.slice("--state-tag=".length);
6729
7126
  else if (arg.startsWith("--lang=")) options.locale = arg.slice("--lang=".length);
6730
7127
  else if (arg === "--errors-only" || arg === "--quiet") options.errorsOnly = true;
7128
+ else if (arg === "--strict") options.strict = true;
6731
7129
  else if (!arg.startsWith("-")) files.push(arg);
6732
7130
  }
6733
7131
  return { options, files };
@@ -6746,7 +7144,7 @@ function main(argv) {
6746
7144
  const { options, files } = parseArgs(argv);
6747
7145
  const locale3 = resolveCliLocale(options.locale);
6748
7146
  if (files.length === 0) {
6749
- process.stderr.write("usage: wcs-validate [--attr=data-wcs] [--state-tag=wcs-state] [--lang=ja|en] <file> [<file> ...]\n");
7147
+ process.stderr.write("usage: wcs-validate [--attr=data-wcs] [--state-tag=wcs-state] [--lang=ja|en] [--errors-only] [--strict] <file> [<file> ...]\n");
6750
7148
  return 2;
6751
7149
  }
6752
7150
  const inputs = [];
@@ -6768,7 +7166,7 @@ function main(argv) {
6768
7166
  }
6769
7167
  process.stdout.write(
6770
7168
  `
6771
- ${result.errorCount} error(s), ${result.warningCount} warning(s), ${result.infoCount} info
7169
+ ${result.errorCount} error(s), ${result.warningCount} warning(s), ${result.infoCount} info${options.strict ? " (strict)" : ""}
6772
7170
  `
6773
7171
  );
6774
7172
  return result.exitCode;