@chrisdudek/yg 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/bin.js +1860 -373
  2. package/dist/structure.d.ts +7 -0
  3. package/dist/structure.js +10 -5
  4. package/dist/templates/portal/js/bootstrap.js +206 -0
  5. package/dist/templates/portal/js/consumer.js +128 -0
  6. package/dist/templates/portal/js/dispatch.js +181 -0
  7. package/dist/templates/portal/js/export.js +151 -0
  8. package/dist/templates/portal/js/glossary.js +72 -0
  9. package/dist/templates/portal/js/namespace.js +60 -0
  10. package/dist/templates/portal/js/palette-overlay.js +176 -0
  11. package/dist/templates/portal/js/palette.js +133 -0
  12. package/dist/templates/portal/js/router.js +169 -0
  13. package/dist/templates/portal/js/shell.js +264 -0
  14. package/dist/templates/portal/js/state-model.js +142 -0
  15. package/dist/templates/portal/js/tree.js +190 -0
  16. package/dist/templates/portal/js/views/coverage-view.js +247 -0
  17. package/dist/templates/portal/js/views/flows-view.js +204 -0
  18. package/dist/templates/portal/js/views/overview-view.js +167 -0
  19. package/dist/templates/portal/js/views/panel-aspect.js +141 -0
  20. package/dist/templates/portal/js/views/panel-view.js +314 -0
  21. package/dist/templates/portal/js/views/relations-matrix.js +185 -0
  22. package/dist/templates/portal/js/views/relations-view.js +185 -0
  23. package/dist/templates/portal/js/views/rulebook-view.js +174 -0
  24. package/dist/templates/portal/js/views/start-view.js +287 -0
  25. package/dist/templates/portal/js/views/suppressions-view.js +187 -0
  26. package/dist/templates/portal/js/views/tree-view.js +68 -0
  27. package/dist/templates/portal/js/views/types-view.js +154 -0
  28. package/dist/templates/portal/vendor/d3-hierarchy.js +350 -0
  29. package/package.json +5 -2
package/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin.ts
4
- import { Command as Command2 } from "commander";
4
+ import { Command as Command3 } from "commander";
5
5
 
6
6
  // src/cli/init.ts
7
7
  import chalk2 from "chalk";
@@ -128,7 +128,7 @@ Verification runs per \`(aspect, unit)\` pair. A **unit** is the subject of one
128
128
 
129
129
  Verification is all-or-nothing and repo-wide: \`yg check --approve\` fills EVERY unverified pair (deterministic first, free; then LLM). The only scoping flag is \`--only-deterministic\` (the free, keyless CI gate); \`yg impact\` is the pre-edit cost predictor. Deterministic checks run first; a node with an enforced deterministic refusal has its LLM pairs skipped that run, so a known-broken node never bills the reviewer. Refusals are cached like approvals.
130
130
 
131
- Interrupting \`yg check --approve\` is safe: finished verdicts are already committed to the lock; only in-flight pairs are lost and the next run resumes them. Read the raw output \u2014 never pipe it through \`| grep\`, \`| head\`, or \`| tail\`: those silently drop lines and the count you act on stops matching the count the build enforces. The default \`yg check\` output is **grouped**: issues with the same rule are collapsed into one block (shared why+fix shown once, affected nodes listed beneath). When the output is still large, the SANCTIONED way to narrow it further is \`yg check --summary\` (per-node counts only) or \`yg check --top [N]\` (the N highest-priority GROUPS; bare \`--top\` shows just the single suggested-next group) \u2014 both always print the TRUE aggregate \`Errors (N)\`/\`Warnings (N)\` header and preserve the real exit code, so a narrowed view can never read as a clean build. To drill into one rule: \`yg check --aspect <id>\`. For the old ungrouped per-pair view: \`yg check --details\`. Orient with \`--summary\`/\`--top\`, then drill with \`--aspect\` or plain \`yg check\`. The reviewer already ran; the output is the return on that cost.
131
+ Interrupting \`yg check --approve\` is safe: finished verdicts are already committed to the lock; only in-flight pairs are lost and the next run resumes them. Read the raw output \u2014 never pipe it through \`| grep\`, \`| head\`, or \`| tail\`: those silently drop lines and the count you act on stops matching the count the build enforces. The default \`yg check\` output is **grouped**: issues with the same rule are collapsed into one block (shared why+fix shown once, affected nodes listed beneath). When a rule's fix is NODE-SPECIFIC (the next command names the node \u2014 e.g. a per-node log entry, or declaring a dependency in one node's file), the block instead prints EACH affected node's own fix beneath its line, so following the block clears every node, not just the first. The triage and narrowing flags are READ-ONLY views over that same result and never combine with a fill flag (\`--approve\` / \`--only-deterministic\`). When the output is still large, the SANCTIONED way to narrow it further is \`yg check --summary\` (per-node counts only) or \`yg check --top [N]\` (the N highest-priority GROUPS; bare \`--top\` shows just the single suggested-next group) \u2014 both always print the TRUE aggregate \`Errors (N)\`/\`Warnings (N)\` header and preserve the real exit code, so a narrowed view can never read as a clean build. To drill into one rule: \`yg check --aspect <id>\`. For the old ungrouped per-pair view: \`yg check --details\`. Orient with \`--summary\`/\`--top\`, then drill with \`--aspect\` or plain \`yg check\`. The reviewer already ran; the output is the return on that cost.
132
132
 
133
133
  A draft aspect produces no expected pairs \u2014 nothing is verified, nothing recorded. Advisory refusals render as warnings (never block \`yg check\`); enforced refusals render as errors (block \`yg check\`). Verdicts survive status flips, including a \`draft\` round-trip: an entry for unchanged inputs stays valid when the aspect returns to enforced.
134
134
 
@@ -1440,7 +1440,7 @@ var PROVIDER_DEFAULTS = {
1440
1440
  "codex": { model: "o4-mini" },
1441
1441
  "gemini-cli": { model: "gemini-2.5-flash" }
1442
1442
  };
1443
- async function parseConfig(filePath) {
1443
+ async function parseConfig(filePath, opts) {
1444
1444
  const filename = path4.basename(filePath);
1445
1445
  const content20 = await readFile4(filePath, "utf-8");
1446
1446
  const baseRaw = parseYaml3(content20);
@@ -1451,7 +1451,7 @@ async function parseConfig(filePath) {
1451
1451
  next: "restore the file from version control, or regenerate it via `yg init`"
1452
1452
  }, "config-invalid");
1453
1453
  }
1454
- const overlay = await loadConfigOverlay(path4.dirname(filePath));
1454
+ const overlay = opts?.skipSecretsOverlay ? void 0 : await loadConfigOverlay(path4.dirname(filePath));
1455
1455
  const raw = overlay ? deepMerge(baseRaw, overlay) : baseRaw;
1456
1456
  const version = typeof raw.version === "string" ? raw.version.trim() : void 0;
1457
1457
  const qualityRaw = raw.quality;
@@ -2889,6 +2889,11 @@ async function parseArchitecture(filePath) {
2889
2889
  const nodeTypesObj = nodeTypesRaw ?? {};
2890
2890
  const nodeTypes = {};
2891
2891
  for (const [typeName, val] of Object.entries(nodeTypesObj)) {
2892
+ if (typeName === "*") {
2893
+ throw new Error(
2894
+ `yg-architecture.yaml: node type name '*' is reserved \u2014 '*' is the any-target wildcard in relation lists and cannot be used as a node type name.`
2895
+ );
2896
+ }
2892
2897
  const entry = val;
2893
2898
  if (!entry || typeof entry !== "object" || typeof entry.description !== "string" || entry.description.trim() === "") {
2894
2899
  throw new Error(
@@ -2925,7 +2930,7 @@ async function parseArchitecture(filePath) {
2925
2930
  `yg-architecture.yaml: node_types.${typeName}.parents`,
2926
2931
  "parent type name"
2927
2932
  ) : void 0;
2928
- const relations = parseRelations2(entry.relations, typeName);
2933
+ const { lists: relations, relationDefault } = parseRelations2(entry.relations, typeName);
2929
2934
  let logRequired;
2930
2935
  if (entry.log_required !== void 0) {
2931
2936
  if (typeof entry.log_required !== "boolean") {
@@ -2955,6 +2960,7 @@ async function parseArchitecture(filePath) {
2955
2960
  ...aspectStatus && { aspectStatus },
2956
2961
  parents: parents && parents.length > 0 ? parents : void 0,
2957
2962
  relations,
2963
+ ...relationDefault !== void 0 && { relationDefault },
2958
2964
  ...logRequired !== void 0 && { log_required: logRequired },
2959
2965
  ...when !== void 0 && { when },
2960
2966
  ...enforce !== void 0 && { enforce }
@@ -2966,33 +2972,42 @@ async function parseArchitecture(filePath) {
2966
2972
  }
2967
2973
  function parseRelations2(relationsRaw, typeName) {
2968
2974
  if (relationsRaw === void 0) {
2969
- return void 0;
2975
+ return { lists: void 0 };
2970
2976
  }
2971
2977
  if (typeof relationsRaw !== "object" || Array.isArray(relationsRaw)) {
2972
2978
  throw new Error(`yg-architecture.yaml: node_types.${typeName}.relations must be an object`);
2973
2979
  }
2974
2980
  const relations = {};
2975
- for (const [relType, targets] of Object.entries(relationsRaw)) {
2976
- if (!VALID_RELATION_TYPES.has(relType)) {
2981
+ let relationDefault;
2982
+ for (const [key, value] of Object.entries(relationsRaw)) {
2983
+ if (key === "default") {
2984
+ if (value !== "allow" && value !== "deny") {
2985
+ throw new Error(
2986
+ `yg-architecture.yaml: node_types.${typeName}.relations.default must be 'allow' or 'deny' (got: ${JSON.stringify(value)})`
2987
+ );
2988
+ }
2989
+ relationDefault = value;
2990
+ continue;
2991
+ }
2992
+ if (!VALID_RELATION_TYPES.has(key)) {
2977
2993
  throw new Error(
2978
- `yg-architecture.yaml: node_types.${typeName}.relations: unknown relation type '${relType}' (valid types: ${Array.from(VALID_RELATION_TYPES).join(", ")})`
2994
+ `yg-architecture.yaml: node_types.${typeName}.relations: unknown relation type '${key}' (valid types: ${Array.from(VALID_RELATION_TYPES).join(", ")}; or 'default')`
2979
2995
  );
2980
2996
  }
2981
- if (!Array.isArray(targets)) {
2997
+ if (!Array.isArray(value)) {
2982
2998
  throw new Error(
2983
- `yg-architecture.yaml: node_types.${typeName}.relations.${relType} must be an array`
2999
+ `yg-architecture.yaml: node_types.${typeName}.relations.${key} must be an array`
2984
3000
  );
2985
3001
  }
2986
3002
  const targetStrings = assertStringArray(
2987
- targets,
2988
- `yg-architecture.yaml: node_types.${typeName}.relations.${relType}`,
3003
+ value,
3004
+ `yg-architecture.yaml: node_types.${typeName}.relations.${key}`,
2989
3005
  "target type name"
2990
3006
  );
2991
- if (targetStrings.length > 0) {
2992
- relations[relType] = targetStrings;
2993
- }
3007
+ relations[key] = targetStrings;
2994
3008
  }
2995
- return Object.keys(relations).length > 0 ? relations : {};
3009
+ const hasLists = Object.keys(relations).length > 0;
3010
+ return { lists: hasLists ? relations : {}, relationDefault };
2996
3011
  }
2997
3012
  function assertStringArray(arr, fieldPath, itemLabel) {
2998
3013
  const offenders = arr.map((value, index) => ({ value, index })).filter((e) => typeof e.value !== "string");
@@ -3133,7 +3148,7 @@ async function loadGraph(projectRoot, options = {}) {
3133
3148
  let configErrorMessage;
3134
3149
  let config = FALLBACK_CONFIG;
3135
3150
  try {
3136
- config = await parseConfig(path9.join(yggRoot, "yg-config.yaml"));
3151
+ config = await parseConfig(path9.join(yggRoot, "yg-config.yaml"), { skipSecretsOverlay: options.noSecrets });
3137
3152
  } catch (error) {
3138
3153
  if (error instanceof ConfigParseError) {
3139
3154
  configErrorMessage = error.messageData;
@@ -3322,7 +3337,8 @@ ${msg.next}`;
3322
3337
  }
3323
3338
 
3324
3339
  // src/io/lock-store.ts
3325
- import { readFileSync, unlinkSync } from "fs";
3340
+ import { readFileSync, unlinkSync, existsSync as existsSync2 } from "fs";
3341
+ import { createHash } from "crypto";
3326
3342
  import path11 from "path";
3327
3343
 
3328
3344
  // src/model/lock.ts
@@ -3368,6 +3384,20 @@ function logsLockPath(yggRoot) {
3368
3384
  function detLockPath(yggRoot) {
3369
3385
  return path11.join(yggRoot, LOCK_DET_FILE_NAME);
3370
3386
  }
3387
+ function committedLockContentHash(yggRoot) {
3388
+ const parts = [];
3389
+ for (const file of [nondetLockPath(yggRoot), logsLockPath(yggRoot)]) {
3390
+ if (!existsSync2(file)) continue;
3391
+ try {
3392
+ parts.push(readFileSync(file));
3393
+ } catch {
3394
+ }
3395
+ }
3396
+ if (parts.length === 0) return "";
3397
+ const hash = createHash("sha256");
3398
+ for (const p2 of parts) hash.update(p2);
3399
+ return hash.digest("hex");
3400
+ }
3371
3401
  function recoveryNext(ctx) {
3372
3402
  if (ctx.committed) {
3373
3403
  return `restore the file from git (\`git checkout HEAD -- .yggdrasil/${ctx.fileName}\`), OR
@@ -3714,7 +3744,7 @@ import { rm as rm3, stat as stat3 } from "fs/promises";
3714
3744
 
3715
3745
  // src/migrations/split-lock.ts
3716
3746
  import path12 from "path";
3717
- import { readdirSync, existsSync as existsSync2 } from "fs";
3747
+ import { readdirSync, existsSync as existsSync3 } from "fs";
3718
3748
  import { readFile as readFile10, writeFile as writeFile4, rm as rm2 } from "fs/promises";
3719
3749
  async function splitLock(yggRoot) {
3720
3750
  const legacy = readLegacyLock(yggRoot);
@@ -3732,7 +3762,7 @@ async function splitLock(yggRoot) {
3732
3762
  function scanDeterministicAspectIds(yggRoot) {
3733
3763
  const aspectsRoot = path12.join(yggRoot, "aspects");
3734
3764
  const ids = /* @__PURE__ */ new Set();
3735
- if (!existsSync2(aspectsRoot)) return ids;
3765
+ if (!existsSync3(aspectsRoot)) return ids;
3736
3766
  const walk2 = (dir) => {
3737
3767
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
3738
3768
  const full = path12.join(dir, entry.name);
@@ -3828,6 +3858,9 @@ async function ensureGitattributes(repoRoot) {
3828
3858
  var YGGDRASIL_GITIGNORE_LINES = [
3829
3859
  "yg-secrets.yaml",
3830
3860
  ".symbols-cache/",
3861
+ // Content-addressed per-file AST fact cache: a local speed cache the relation pass rebuilds
3862
+ // free on the next run; never committed.
3863
+ ".ast-cache/",
3831
3864
  ".debug.log",
3832
3865
  // Deterministic-verdict lock: a local cache rebuilt for free by
3833
3866
  // `yg check --approve --only-deterministic`; never committed.
@@ -4658,6 +4691,15 @@ function attachmentMachineOrigin(att, node) {
4658
4691
  return `port:${att.port.name}@${att.port.target}`;
4659
4692
  }
4660
4693
  }
4694
+ function hasNonDraftEffectiveAspects(node, graph) {
4695
+ const statuses = computeEffectiveAspectStatuses(node, graph);
4696
+ for (const [aspectId, s] of statuses) {
4697
+ if (s === "draft") continue;
4698
+ if (isAggregateAspect(graph, aspectId)) continue;
4699
+ return true;
4700
+ }
4701
+ return false;
4702
+ }
4661
4703
  function isAggregateAspect(graph, aspectId) {
4662
4704
  return graph.aspects.find((a) => a.id === aspectId)?.reviewer.type === "aggregate";
4663
4705
  }
@@ -4886,11 +4928,11 @@ function normPath(p2) {
4886
4928
  }
4887
4929
  function countDependents(graph, nodePath) {
4888
4930
  const paths = [];
4889
- for (const [path58, node] of graph.nodes) {
4931
+ for (const [path62, node] of graph.nodes) {
4890
4932
  const hasRelation = (node.meta.relations ?? []).some(
4891
4933
  (r) => r.target === nodePath && (STRUCTURAL_RELATION_TYPES.has(r.type) || EVENT_RELATION_TYPES.has(r.type))
4892
4934
  );
4893
- if (hasRelation) paths.push(path58);
4935
+ if (hasRelation) paths.push(path62);
4894
4936
  }
4895
4937
  return { count: paths.length, paths };
4896
4938
  }
@@ -5457,7 +5499,7 @@ function renderNode(node, indent, lines) {
5457
5499
  // src/io/hash.ts
5458
5500
  import { readFile as readFile14, readdir as readdir4, stat as stat6 } from "fs/promises";
5459
5501
  import path16 from "path";
5460
- import { createHash } from "crypto";
5502
+ import { createHash as createHash2 } from "crypto";
5461
5503
  import { createRequire as createRequire2 } from "module";
5462
5504
 
5463
5505
  // src/io/repo-scanner.ts
@@ -5583,10 +5625,10 @@ function isIgnoredByStack2(candidatePath, stack) {
5583
5625
  return false;
5584
5626
  }
5585
5627
  function hashString(content20) {
5586
- return createHash("sha256").update(content20).digest("hex");
5628
+ return createHash2("sha256").update(content20).digest("hex");
5587
5629
  }
5588
5630
  function hashBytes(bytes) {
5589
- return createHash("sha256").update(normalizeLineEndings(bytes)).digest("hex");
5631
+ return createHash2("sha256").update(normalizeLineEndings(bytes)).digest("hex");
5590
5632
  }
5591
5633
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
5592
5634
  let stack = options.gitignoreStack ?? [];
@@ -5709,19 +5751,19 @@ function checkArchitectureParentCycles(graph) {
5709
5751
  const color = new Map(typeIds.map((id) => [id, WHITE]));
5710
5752
  const backEdges = /* @__PURE__ */ new Set();
5711
5753
  const recordedCycles = [];
5712
- function dfs(typeId, path58) {
5754
+ function dfs(typeId, path62) {
5713
5755
  if (color.get(typeId) === GRAY) {
5714
- const cycleStart = path58.indexOf(typeId);
5715
- if (cycleStart !== -1) recordedCycles.push([...path58.slice(cycleStart), typeId]);
5716
- const from = path58[path58.length - 1];
5756
+ const cycleStart = path62.indexOf(typeId);
5757
+ if (cycleStart !== -1) recordedCycles.push([...path62.slice(cycleStart), typeId]);
5758
+ const from = path62[path62.length - 1];
5717
5759
  if (from !== void 0) backEdges.add(`${from}->${typeId}`);
5718
5760
  return;
5719
5761
  }
5720
5762
  if (color.get(typeId) === BLACK) return;
5721
5763
  color.set(typeId, GRAY);
5722
- path58.push(typeId);
5723
- for (const parent of types[typeId]?.parents ?? []) dfs(parent, path58);
5724
- path58.pop();
5764
+ path62.push(typeId);
5765
+ for (const parent of types[typeId]?.parents ?? []) dfs(parent, path62);
5766
+ path62.pop();
5725
5767
  color.set(typeId, BLACK);
5726
5768
  }
5727
5769
  for (const id of typeIds) {
@@ -5947,18 +5989,23 @@ function checkArchitectureRelations(graph) {
5947
5989
  const issues = [];
5948
5990
  for (const [nodePath, node] of graph.nodes) {
5949
5991
  const typeConfig = graph.architecture.node_types[node.meta.type];
5950
- if (!typeConfig?.relations || !node.meta.relations || node.meta.relations.length === 0) {
5992
+ if (!typeConfig || !node.meta.relations || node.meta.relations.length === 0) {
5993
+ continue;
5994
+ }
5995
+ const lists = typeConfig.relations;
5996
+ const def = typeConfig.relationDefault ?? "allow";
5997
+ if ((!lists || Object.keys(lists).length === 0) && def === "allow") {
5951
5998
  continue;
5952
5999
  }
5953
6000
  for (const rel of node.meta.relations) {
5954
- const allowedTypes = typeConfig.relations[rel.type];
5955
- if (!allowedTypes) continue;
6001
+ const allowed = lists?.[rel.type];
5956
6002
  const target = graph.nodes.get(rel.target);
5957
6003
  if (!target) continue;
5958
- if (!allowedTypes.includes(target.meta.type)) {
6004
+ if (allowed !== void 0) {
6005
+ if (allowed.includes("*") || allowed.includes(target.meta.type)) continue;
5959
6006
  const msgData = {
5960
6007
  what: `Relation '${rel.type}' from '${nodePath}' to '${rel.target}' (type '${target.meta.type}') is not allowed by the architecture.`,
5961
- why: `Allowed targets for '${rel.type}' from type '${node.meta.type}': [${allowedTypes.join(", ")}]`,
6008
+ why: `Allowed targets for '${rel.type}' from type '${node.meta.type}': [${allowed.join(", ")}]`,
5962
6009
  next: `Either change the relation type, change the target node's type, or update yg-architecture.yaml to allow this relation.`
5963
6010
  };
5964
6011
  issues.push({
@@ -5969,6 +6016,20 @@ function checkArchitectureRelations(graph) {
5969
6016
  ...issueMsg(msgData),
5970
6017
  messageData: msgData
5971
6018
  });
6019
+ } else if (def === "deny") {
6020
+ const msgData = {
6021
+ what: `Relation '${rel.type}' from '${nodePath}' to '${rel.target}' (type '${target.meta.type}') is not allowed by the architecture.`,
6022
+ why: `Type '${node.meta.type}' denies relation '${rel.type}' by default (relations.default: deny, and no allow-list entry permits this target).`,
6023
+ next: `Open '${rel.type}' for type '${node.meta.type}' in yg-architecture.yaml (list target types, or ['*'] for any), change a node's type, or remove the relation.`
6024
+ };
6025
+ issues.push({
6026
+ severity: "error",
6027
+ code: "relation-target-forbidden",
6028
+ rule: "invalid-relation-target",
6029
+ nodePath,
6030
+ ...issueMsg(msgData),
6031
+ messageData: msgData
6032
+ });
5972
6033
  }
5973
6034
  }
5974
6035
  }
@@ -7860,7 +7921,7 @@ async function computeSourceFingerprint(graph, nodePath) {
7860
7921
  }
7861
7922
 
7862
7923
  // src/core/log/log-gate.ts
7863
- import { createHash as createHash2 } from "crypto";
7924
+ import { createHash as createHash3 } from "crypto";
7864
7925
  import path22 from "path";
7865
7926
 
7866
7927
  // src/core/parsing/log-parser.ts
@@ -7956,7 +8017,7 @@ function computeLogBaselineFromContent(content20) {
7956
8017
  const prefix = bytes.subarray(0, newest.offsetEnd);
7957
8018
  return {
7958
8019
  last_entry_datetime: newest.datetime,
7959
- prefix_hash: createHash2("sha256").update(prefix).digest("hex")
8020
+ prefix_hash: createHash3("sha256").update(prefix).digest("hex")
7960
8021
  };
7961
8022
  }
7962
8023
 
@@ -8184,11 +8245,11 @@ function registerTreeCommand(program2) {
8184
8245
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
8185
8246
  let roots;
8186
8247
  if (options.root?.trim()) {
8187
- const path58 = options.root.trim().replace(/\/$/, "");
8188
- const node = graph.nodes.get(path58);
8248
+ const path62 = options.root.trim().replace(/\/$/, "");
8249
+ const node = graph.nodes.get(path62);
8189
8250
  if (!node) {
8190
8251
  process.stderr.write(chalk4.red(buildIssueMessage({
8191
- what: `Node '${path58}' not found.`,
8252
+ what: `Node '${path62}' not found.`,
8192
8253
  why: `The --root path must be a valid node path in the graph.`,
8193
8254
  next: `Run yg tree (no --root) to list all nodes, then pick a valid path.`
8194
8255
  }) + "\n"));
@@ -8399,14 +8460,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
8399
8460
  }
8400
8461
  const chains = [];
8401
8462
  for (const node of transitiveOnly) {
8402
- const path58 = [];
8463
+ const path62 = [];
8403
8464
  let current = node;
8404
8465
  while (current) {
8405
- path58.unshift(current);
8466
+ path62.unshift(current);
8406
8467
  current = parent.get(current);
8407
8468
  }
8408
- if (path58.length >= 3) {
8409
- chains.push(path58.slice(1).map((p2) => `<- ${p2}`).join(" "));
8469
+ if (path62.length >= 3) {
8470
+ chains.push(path62.slice(1).map((p2) => `<- ${p2}`).join(" "));
8410
8471
  }
8411
8472
  }
8412
8473
  return chains.sort();
@@ -8438,14 +8499,14 @@ function collectIndirectDependents(graph, directlyAffected) {
8438
8499
  }
8439
8500
  for (const [node] of parent) {
8440
8501
  if (directSet.has(node)) continue;
8441
- const path58 = [node];
8502
+ const path62 = [node];
8442
8503
  let current = node;
8443
8504
  while (parent.has(current)) {
8444
8505
  current = parent.get(current);
8445
- path58.push(current);
8506
+ path62.push(current);
8446
8507
  }
8447
- const chain = path58.map((p2) => `<- ${p2}`).join(" ");
8448
- const depth = path58.length;
8508
+ const chain = path62.map((p2) => `<- ${p2}`).join(" ");
8509
+ const depth = path62.length;
8449
8510
  const existing = bestChain.get(node);
8450
8511
  if (!existing || depth < existing.depth) {
8451
8512
  bestChain.set(node, { chain, depth });
@@ -8546,14 +8607,14 @@ import { register } from "module";
8546
8607
  import { pathToFileURL } from "url";
8547
8608
  import path24 from "path";
8548
8609
  import { fileURLToPath as fileURLToPath2 } from "url";
8549
- import { existsSync as existsSync3 } from "fs";
8610
+ import { existsSync as existsSync4 } from "fs";
8550
8611
  var __filename = fileURLToPath2(import.meta.url);
8551
8612
  var __dirname = path24.dirname(__filename);
8552
8613
  var registered = false;
8553
8614
  function ensureLoaderRegistered() {
8554
8615
  if (registered) return;
8555
8616
  let implPath = path24.resolve(__dirname, "./loader-hook-impl.js");
8556
- if (!existsSync3(implPath)) {
8617
+ if (!existsSync4(implPath)) {
8557
8618
  const pkgRoot = path24.resolve(__dirname, "../../");
8558
8619
  implPath = path24.resolve(pkgRoot, "dist/loader-hook-impl.js");
8559
8620
  }
@@ -8565,9 +8626,9 @@ function ensureLoaderRegistered() {
8565
8626
  import * as fs from "fs";
8566
8627
  import * as path25 from "path";
8567
8628
  var UndeclaredFsReadError = class extends Error {
8568
- constructor(path58) {
8569
- super(`structure-aspect-undeclared-fs-read: ${path58}`);
8570
- this.path = path58;
8629
+ constructor(path62) {
8630
+ super(`structure-aspect-undeclared-fs-read: ${path62}`);
8631
+ this.path = path62;
8571
8632
  this.name = "UndeclaredFsReadError";
8572
8633
  }
8573
8634
  path;
@@ -8770,7 +8831,11 @@ function createCtxGraph(params) {
8770
8831
  }
8771
8832
  return {
8772
8833
  node(id) {
8773
- assertAllowed(id);
8834
+ const self = graph.nodes.get(currentNodePath);
8835
+ if (self) recordGraphNode(self);
8836
+ if (!allowed.has(id)) {
8837
+ assertAllowed(id);
8838
+ }
8774
8839
  const m = graph.nodes.get(id);
8775
8840
  if (!m) {
8776
8841
  if (recorder) recorder.recordGraphNodeAbsent(id);
@@ -8851,8 +8916,9 @@ import { parse as parseTomlSmol } from "smol-toml";
8851
8916
  import { Parser, Language } from "web-tree-sitter";
8852
8917
  import path27 from "path";
8853
8918
  import { fileURLToPath as fileURLToPath3 } from "url";
8854
- import { existsSync as existsSync5 } from "fs";
8919
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
8855
8920
  import { createRequire as createRequire3 } from "module";
8921
+ import { createHash as createHash4 } from "crypto";
8856
8922
  var _require = createRequire3(import.meta.url);
8857
8923
  var __filename2 = fileURLToPath3(import.meta.url);
8858
8924
  var __dirname2 = path27.dirname(__filename2);
@@ -8863,6 +8929,19 @@ var GRAMMAR_DIRS = [
8863
8929
  var initPromise = null;
8864
8930
  var langCache = /* @__PURE__ */ new Map();
8865
8931
  var parserCache = /* @__PURE__ */ new Map();
8932
+ var wasmHashCache = /* @__PURE__ */ new Map();
8933
+ function grammarWasmHash(extension) {
8934
+ const cached = wasmHashCache.get(extension);
8935
+ if (cached !== void 0) return cached;
8936
+ const info = getGrammarForExtension(extension);
8937
+ if (!info) {
8938
+ throw new Error(`no grammar for extension '${extension}'`);
8939
+ }
8940
+ const wasmPath = resolveWasm(info.wasmFile, info.wasmPackage);
8941
+ const hash = createHash4("sha256").update(readFileSync4(wasmPath)).digest("hex");
8942
+ wasmHashCache.set(extension, hash);
8943
+ return hash;
8944
+ }
8866
8945
  function init() {
8867
8946
  if (initPromise === null) {
8868
8947
  initPromise = Parser.init();
@@ -8875,12 +8954,12 @@ function init() {
8875
8954
  function resolveWasm(filename, pkg2) {
8876
8955
  for (const dir of GRAMMAR_DIRS) {
8877
8956
  const p2 = path27.join(dir, filename);
8878
- if (existsSync5(p2)) return p2;
8957
+ if (existsSync6(p2)) return p2;
8879
8958
  }
8880
8959
  try {
8881
8960
  const pkgDir = path27.dirname(_require.resolve(`${pkg2}/package.json`));
8882
8961
  for (const candidate of [path27.join(pkgDir, filename), path27.join(pkgDir, "bindings/node", filename)]) {
8883
- if (existsSync5(candidate)) return candidate;
8962
+ if (existsSync6(candidate)) return candidate;
8884
8963
  }
8885
8964
  } catch {
8886
8965
  }
@@ -10505,10 +10584,10 @@ function registerFlowsCommand(program2) {
10505
10584
  import chalk8 from "chalk";
10506
10585
 
10507
10586
  // src/core/check.ts
10508
- import path44 from "path";
10587
+ import path43 from "path";
10509
10588
 
10510
10589
  // src/core/log-integrity.ts
10511
- import { createHash as createHash3 } from "crypto";
10590
+ import { createHash as createHash5 } from "crypto";
10512
10591
  var DATETIME_STRICT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,3}Z$/;
10513
10592
  function validateAppendOnly(currentContent, storedDatetime, storedPrefixHash) {
10514
10593
  if (!DATETIME_STRICT.test(storedDatetime)) {
@@ -10521,7 +10600,7 @@ function validateAppendOnly(currentContent, storedDatetime, storedPrefixHash) {
10521
10600
  if (!boundary) return { ok: false, reason: "boundary_missing" };
10522
10601
  const bytes = Buffer.from(currentContent, "utf-8");
10523
10602
  const prefix = bytes.subarray(0, boundary.offsetEnd);
10524
- const computed = createHash3("sha256").update(prefix).digest("hex");
10603
+ const computed = createHash5("sha256").update(prefix).digest("hex");
10525
10604
  if (computed !== storedPrefixHash) return { ok: false, reason: "prefix_modified" };
10526
10605
  return { ok: true };
10527
10606
  }
@@ -11544,8 +11623,6 @@ import path33 from "path";
11544
11623
  import { readFile as readFile15 } from "fs/promises";
11545
11624
 
11546
11625
  // src/relations/symbol-table.ts
11547
- import path32 from "path";
11548
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
11549
11626
  var SymbolTable = class {
11550
11627
  defs = /* @__PURE__ */ new Map();
11551
11628
  // `${language}\0${symbolKey}` → set of defining files
@@ -11586,30 +11663,6 @@ var SymbolTable = class {
11586
11663
  return s ? [...s] : [];
11587
11664
  }
11588
11665
  };
11589
- var SYMBOL_INDEX_VERSION = 3;
11590
- function indexPath(cacheDir, language) {
11591
- return path32.join(cacheDir, `symbols-${language}.json`);
11592
- }
11593
- function canonBuiltFrom(b) {
11594
- return JSON.stringify([...b].sort((x, y) => x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0));
11595
- }
11596
- async function writeSymbolIndex(cacheDir, language, idx) {
11597
- await atomicWriteFile(indexPath(cacheDir, language), JSON.stringify({ ...idx, version: SYMBOL_INDEX_VERSION }));
11598
- }
11599
- function loadSymbolIndex(cacheDir, language, currentBuiltFrom) {
11600
- const p2 = indexPath(cacheDir, language);
11601
- if (!existsSync6(p2)) return null;
11602
- let parsed;
11603
- try {
11604
- parsed = JSON.parse(readFileSync6(p2, "utf-8"));
11605
- } catch {
11606
- return null;
11607
- }
11608
- if (!parsed || !Array.isArray(parsed.builtFrom) || !Array.isArray(parsed.symbols)) return null;
11609
- if (parsed.version !== SYMBOL_INDEX_VERSION) return null;
11610
- if (canonBuiltFrom(parsed.builtFrom) !== canonBuiltFrom(currentBuiltFrom)) return null;
11611
- return parsed;
11612
- }
11613
11666
 
11614
11667
  // src/relations/resolver.ts
11615
11668
  function resolveCandidateGroup(candidates2, resolver, fromFile, language) {
@@ -11841,82 +11894,35 @@ function buildUsingScope(file) {
11841
11894
  });
11842
11895
  return { prefixes, globalPrefixes, aliases, globalAliases, staticTargets };
11843
11896
  }
11844
- function collectGlobalUsings(file) {
11845
- return buildUsingScope(file).globalPrefixes;
11846
- }
11847
- function collectGlobalUsingAliases(file) {
11848
- return [...buildUsingScope(file).globalAliases.entries()];
11849
- }
11850
11897
  function enclosingNamespaceChain(fileNs, node) {
11851
11898
  const segments = [fileNs, blockNamespace(node)].filter((p2) => p2 !== "").join(".").split(".").filter((s) => s !== "");
11852
11899
  const chain = [];
11853
11900
  for (let i = segments.length; i >= 1; i--) chain.push(segments.slice(0, i).join("."));
11854
11901
  return chain;
11855
11902
  }
11856
- function uses(file, options = {}) {
11857
- const out = [];
11903
+ function extractCsharpRefs(file) {
11858
11904
  const scope = buildUsingScope(file);
11859
11905
  const fileNs = fileScopedNamespace(file.tree.rootNode);
11860
- for (const [name, fqn] of options.projectGlobalUsingAliases ?? []) {
11861
- if (!scope.aliases.has(name)) scope.aliases.set(name, fqn);
11862
- }
11863
- const usingPrefixes = [
11864
- .../* @__PURE__ */ new Set([
11865
- ...scope.prefixes,
11866
- ...scope.globalPrefixes,
11867
- ...options.projectGlobalUsings ?? []
11868
- ])
11869
- ].sort();
11870
- const dedupKeys = (keys) => {
11871
- const seen = /* @__PURE__ */ new Set();
11872
- const out2 = [];
11873
- for (const k of keys) {
11874
- if (k === void 0 || k === "") continue;
11875
- if (seen.has(k)) continue;
11876
- seen.add(k);
11877
- out2.push(k);
11878
- }
11879
- return out2;
11880
- };
11906
+ const refs = [];
11907
+ const emitted = /* @__PURE__ */ new Set();
11881
11908
  const pushRef = (ref, node, line, rooted = false) => {
11882
- if (rooted) {
11883
- out.push({ candidates: [{ kind: "symbol", symbolKey: ref }], kind: "import", line });
11884
- return;
11885
- }
11886
- const lead = ref.split(".")[0];
11887
- const multi = ref.includes(".");
11888
- const aliasTarget = scope.aliases.get(lead);
11889
- const enclosing = enclosingNamespaceChain(fileNs, node).map((ns) => `${ns}.${ref}`);
11890
- const usingMembers = usingPrefixes.map((p2) => ({
11891
- symbolKey: `${p2}.${ref}`,
11892
- nestedOnly: multi
11893
- }));
11894
- const candidates2 = [];
11895
- const pushed = /* @__PURE__ */ new Set();
11896
- const add = (hint) => {
11897
- if (hint.kind === "symbol") {
11898
- if (pushed.has(hint.symbolKey)) return;
11899
- pushed.add(hint.symbolKey);
11900
- }
11901
- candidates2.push(hint);
11902
- };
11903
- if (aliasTarget !== void 0) {
11904
- const tail = ref.slice(lead.length);
11905
- const aliasKey = `${aliasTarget}${tail}`;
11906
- const coDef = dedupKeys([aliasKey, ...enclosing, ref]).map((k) => ({
11907
- symbolKey: k
11908
- }));
11909
- add({ kind: "symbol", symbolKey: aliasKey, set: coDef });
11910
- }
11911
- for (const k of enclosing) add({ kind: "symbol", symbolKey: k });
11912
- for (const m of usingMembers) {
11913
- add({ kind: "symbol", symbolKey: m.symbolKey, nestedOnly: m.nestedOnly, set: usingMembers });
11914
- }
11915
- add({ kind: "symbol", symbolKey: ref });
11916
- if (candidates2.length === 0) return;
11917
- out.push({ candidates: candidates2, kind: "import", line });
11909
+ refs.push({
11910
+ kind: "plain",
11911
+ ref,
11912
+ line,
11913
+ rooted,
11914
+ enclosingNs: enclosingNamespaceChain(fileNs, node)
11915
+ });
11916
+ };
11917
+ const pushAttribute = (written, suffixed, node, line) => {
11918
+ refs.push({
11919
+ kind: "attr",
11920
+ written,
11921
+ suffixed,
11922
+ line,
11923
+ enclosingNs: enclosingNamespaceChain(fileNs, node)
11924
+ });
11918
11925
  };
11919
- const emitted = /* @__PURE__ */ new Set();
11920
11926
  const emitTypeNode = (typeNode) => {
11921
11927
  if (typeNode === null) return;
11922
11928
  switch (typeNode.type) {
@@ -11943,15 +11949,15 @@ function uses(file, options = {}) {
11943
11949
  return;
11944
11950
  }
11945
11951
  if (aliasField !== null) {
11946
- const aliasFqn = scope.aliases.get(aliasField.text);
11947
- if (aliasFqn !== void 0) {
11948
- if (emitted.has(typeNode.id)) return;
11949
- emitted.add(typeNode.id);
11950
- const nameField = typeNode.childForFieldName("name");
11951
- if (nameField !== null) {
11952
- const tail = stripGlobalQualifier(nameField.text);
11953
- pushRef(`${aliasFqn}.${tail}`, typeNode, typeNode.startPosition.row + 1, true);
11954
- }
11952
+ const nameField = typeNode.childForFieldName("name");
11953
+ if (nameField !== null) {
11954
+ refs.push({
11955
+ kind: "alias",
11956
+ aliasName: aliasField.text,
11957
+ tail: stripGlobalQualifier(nameField.text),
11958
+ line: typeNode.startPosition.row + 1,
11959
+ enclosingNs: enclosingNamespaceChain(fileNs, typeNode)
11960
+ });
11955
11961
  }
11956
11962
  }
11957
11963
  return;
@@ -11988,11 +11994,7 @@ function uses(file, options = {}) {
11988
11994
  return null;
11989
11995
  }
11990
11996
  for (const target of scope.staticTargets) {
11991
- out.push({
11992
- candidates: [{ kind: "symbol", symbolKey: target.fqn }],
11993
- kind: "import",
11994
- line: target.line
11995
- });
11997
+ refs.push({ kind: "static", fqn: target.fqn, line: target.line });
11996
11998
  }
11997
11999
  walk(file.tree.rootNode, (node) => {
11998
12000
  if (node.type !== "using_directive") return void 0;
@@ -12120,10 +12122,75 @@ function uses(file, options = {}) {
12120
12122
  }
12121
12123
  return void 0;
12122
12124
  });
12123
- function pushAttribute(written, suffixed, node, line) {
12125
+ return { fileNs, scope, refs };
12126
+ }
12127
+ function assembleCsharpCandidates(extract, options = {}) {
12128
+ const out = [];
12129
+ const { scope, refs } = extract;
12130
+ const aliases = new Map(scope.aliases);
12131
+ for (const [name, fqn] of options.projectGlobalUsingAliases ?? []) {
12132
+ if (!aliases.has(name)) aliases.set(name, fqn);
12133
+ }
12134
+ const usingPrefixes = [
12135
+ .../* @__PURE__ */ new Set([
12136
+ ...scope.prefixes,
12137
+ ...scope.globalPrefixes,
12138
+ ...options.projectGlobalUsings ?? []
12139
+ ])
12140
+ ].sort();
12141
+ const dedupKeys = (keys) => {
12142
+ const seen = /* @__PURE__ */ new Set();
12143
+ const out2 = [];
12144
+ for (const k of keys) {
12145
+ if (k === void 0 || k === "") continue;
12146
+ if (seen.has(k)) continue;
12147
+ seen.add(k);
12148
+ out2.push(k);
12149
+ }
12150
+ return out2;
12151
+ };
12152
+ const pushRef = (ref, enclosingNs, line, rooted) => {
12153
+ if (rooted) {
12154
+ out.push({ candidates: [{ kind: "symbol", symbolKey: ref }], kind: "import", line });
12155
+ return;
12156
+ }
12157
+ const lead = ref.split(".")[0];
12158
+ const multi = ref.includes(".");
12159
+ const aliasTarget = aliases.get(lead);
12160
+ const enclosing = enclosingNs.map((ns) => `${ns}.${ref}`);
12161
+ const usingMembers = usingPrefixes.map((p2) => ({
12162
+ symbolKey: `${p2}.${ref}`,
12163
+ nestedOnly: multi
12164
+ }));
12165
+ const candidates2 = [];
12166
+ const pushed = /* @__PURE__ */ new Set();
12167
+ const add = (hint) => {
12168
+ if (hint.kind === "symbol") {
12169
+ if (pushed.has(hint.symbolKey)) return;
12170
+ pushed.add(hint.symbolKey);
12171
+ }
12172
+ candidates2.push(hint);
12173
+ };
12174
+ if (aliasTarget !== void 0) {
12175
+ const tail = ref.slice(lead.length);
12176
+ const aliasKey = `${aliasTarget}${tail}`;
12177
+ const coDef = dedupKeys([aliasKey, ...enclosing, ref]).map((k) => ({
12178
+ symbolKey: k
12179
+ }));
12180
+ add({ kind: "symbol", symbolKey: aliasKey, set: coDef });
12181
+ }
12182
+ for (const k of enclosing) add({ kind: "symbol", symbolKey: k });
12183
+ for (const m of usingMembers) {
12184
+ add({ kind: "symbol", symbolKey: m.symbolKey, nestedOnly: m.nestedOnly, set: usingMembers });
12185
+ }
12186
+ add({ kind: "symbol", symbolKey: ref });
12187
+ if (candidates2.length === 0) return;
12188
+ out.push({ candidates: candidates2, kind: "import", line });
12189
+ };
12190
+ const pushAttribute = (written, suffixed, enclosingNs, line) => {
12124
12191
  const before = out.length;
12125
- pushRef(written, node, line);
12126
- if (suffixed !== void 0) pushRef(suffixed, node, line);
12192
+ pushRef(written, enclosingNs, line, false);
12193
+ if (suffixed !== void 0) pushRef(suffixed, enclosingNs, line, false);
12127
12194
  if (out.length > before + 1) {
12128
12195
  const merged = [];
12129
12196
  const seenK = /* @__PURE__ */ new Set();
@@ -12138,18 +12205,129 @@ function uses(file, options = {}) {
12138
12205
  }
12139
12206
  out.push({ candidates: merged, kind: "import", line });
12140
12207
  }
12208
+ };
12209
+ for (const r of refs) {
12210
+ switch (r.kind) {
12211
+ case "static":
12212
+ out.push({ candidates: [{ kind: "symbol", symbolKey: r.fqn }], kind: "import", line: r.line });
12213
+ break;
12214
+ case "plain":
12215
+ pushRef(r.ref, r.enclosingNs, r.line, r.rooted);
12216
+ break;
12217
+ case "alias": {
12218
+ const aliasFqn = aliases.get(r.aliasName);
12219
+ if (aliasFqn !== void 0) {
12220
+ pushRef(`${aliasFqn}.${r.tail}`, r.enclosingNs, r.line, true);
12221
+ }
12222
+ break;
12223
+ }
12224
+ case "attr":
12225
+ pushAttribute(r.written, r.suffixed, r.enclosingNs, r.line);
12226
+ break;
12227
+ }
12141
12228
  }
12142
12229
  return out;
12143
12230
  }
12144
- function csharpUses(file, options) {
12145
- return uses(file, options);
12231
+ function uses(file, options = {}) {
12232
+ return assembleCsharpCandidates(extractCsharpRefs(file), options);
12146
12233
  }
12147
12234
  var csharpExtractor = {
12148
12235
  languages: /* @__PURE__ */ new Set(["csharp"]),
12236
+ rev: 2,
12149
12237
  declarations,
12150
12238
  uses
12151
12239
  };
12152
12240
 
12241
+ // src/relations/facts-cache.ts
12242
+ import path32 from "path";
12243
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
12244
+ import { createHash as createHash6 } from "crypto";
12245
+ var CACHE_SCHEMA_VERSION = 1;
12246
+ function serializeCsharp(c) {
12247
+ return {
12248
+ fileNs: c.fileNs,
12249
+ scope: {
12250
+ prefixes: c.scope.prefixes,
12251
+ globalPrefixes: c.scope.globalPrefixes,
12252
+ aliases: [...c.scope.aliases],
12253
+ globalAliases: [...c.scope.globalAliases],
12254
+ staticTargets: c.scope.staticTargets
12255
+ },
12256
+ refs: c.refs
12257
+ };
12258
+ }
12259
+ function deserializeCsharp(raw) {
12260
+ if (!raw || typeof raw !== "object") return null;
12261
+ const s = raw;
12262
+ if (typeof s.fileNs !== "string") return null;
12263
+ if (!s.scope || typeof s.scope !== "object") return null;
12264
+ const sc = s.scope;
12265
+ if (!Array.isArray(sc.prefixes) || !Array.isArray(sc.globalPrefixes) || !Array.isArray(sc.aliases) || !Array.isArray(sc.globalAliases) || !Array.isArray(sc.staticTargets)) {
12266
+ return null;
12267
+ }
12268
+ if (!Array.isArray(s.refs)) return null;
12269
+ return {
12270
+ fileNs: s.fileNs,
12271
+ scope: {
12272
+ prefixes: sc.prefixes,
12273
+ globalPrefixes: sc.globalPrefixes,
12274
+ aliases: new Map(sc.aliases),
12275
+ globalAliases: new Map(sc.globalAliases),
12276
+ staticTargets: sc.staticTargets
12277
+ },
12278
+ refs: s.refs
12279
+ };
12280
+ }
12281
+ function astCacheDir(graphRoot) {
12282
+ return path32.join(graphRoot, ".ast-cache");
12283
+ }
12284
+ function factsKey(args) {
12285
+ const payload = `${args.contentHash}\0${args.language}\0${args.grammarHash}\0${args.rev}`;
12286
+ return createHash6("sha256").update(payload).digest("hex").slice(0, 32);
12287
+ }
12288
+ function shardPath(dir, language, key) {
12289
+ return path32.join(dir, `v${CACHE_SCHEMA_VERSION}`, language, `${key}.json`);
12290
+ }
12291
+ async function loadFacts(dir, language, key) {
12292
+ const p2 = shardPath(dir, language, key);
12293
+ if (!existsSync7(p2)) return null;
12294
+ let parsed;
12295
+ try {
12296
+ parsed = JSON.parse(readFileSync7(p2, "utf-8"));
12297
+ } catch {
12298
+ return null;
12299
+ }
12300
+ if (!parsed || typeof parsed !== "object" || parsed.v !== CACHE_SCHEMA_VERSION) return null;
12301
+ if (typeof parsed.key !== "string") return null;
12302
+ if (!Array.isArray(parsed.declarations)) return null;
12303
+ if (!Array.isArray(parsed.uses)) return null;
12304
+ if (parsed.key !== key) return null;
12305
+ const facts = {
12306
+ declarations: parsed.declarations,
12307
+ uses: parsed.uses
12308
+ };
12309
+ if (parsed.csharp !== void 0) {
12310
+ const csharp = deserializeCsharp(parsed.csharp);
12311
+ if (csharp === null) return null;
12312
+ facts.csharp = csharp;
12313
+ }
12314
+ return facts;
12315
+ }
12316
+ async function writeFacts(dir, language, key, facts) {
12317
+ const p2 = shardPath(dir, language, key);
12318
+ if (existsSync7(p2)) return;
12319
+ const body = {
12320
+ v: CACHE_SCHEMA_VERSION,
12321
+ key,
12322
+ declarations: facts.declarations,
12323
+ uses: facts.uses,
12324
+ // Flatten the C# extract's `Map`s to entry arrays BEFORE JSON.stringify — a bare `Map`
12325
+ // stringifies to `"{}"` and would silently lose every alias entry (the false-green vector).
12326
+ ...facts.csharp !== void 0 ? { csharp: serializeCsharp(facts.csharp) } : {}
12327
+ };
12328
+ await atomicWriteFile(p2, JSON.stringify(body));
12329
+ }
12330
+
12153
12331
  // src/relations/verifier.ts
12154
12332
  function verifyNodeDeps(nodeId, deps, g) {
12155
12333
  const out = [];
@@ -12204,6 +12382,63 @@ async function runRelationPass(graph, projectRoot, deps) {
12204
12382
  return null;
12205
12383
  }
12206
12384
  }
12385
+ async function extractFileFacts(record, extractor) {
12386
+ const parsed = await parseSingle(record);
12387
+ if (!parsed) return null;
12388
+ try {
12389
+ const declarations12 = extractor.declarations(parsed);
12390
+ const isCsharp = record.language === "csharp";
12391
+ const uses12 = isCsharp ? null : extractor.uses(parsed);
12392
+ const csharp = isCsharp ? extractCsharpRefs(parsed) : null;
12393
+ return { declarations: declarations12, uses: uses12, csharp };
12394
+ } finally {
12395
+ parsed.tree.delete();
12396
+ }
12397
+ }
12398
+ const grammarHashByExt = /* @__PURE__ */ new Map();
12399
+ const grammarHashForExt = (ext) => {
12400
+ const hit = grammarHashByExt.get(ext);
12401
+ if (hit !== void 0) return hit;
12402
+ let h;
12403
+ try {
12404
+ h = grammarWasmHash(ext);
12405
+ } catch {
12406
+ h = null;
12407
+ }
12408
+ grammarHashByExt.set(ext, h);
12409
+ return h;
12410
+ };
12411
+ async function loadOrExtractFacts(record, extractor) {
12412
+ const language = record.language;
12413
+ const isCsharp = language === "csharp";
12414
+ const grammarHash = grammarHashForExt(path33.extname(record.path));
12415
+ if (grammarHash === null) return extractFileFacts(record, extractor);
12416
+ if (!deps.disableCache) {
12417
+ const key = factsKey({
12418
+ contentHash: record.hash,
12419
+ language,
12420
+ grammarHash,
12421
+ rev: extractor.rev
12422
+ });
12423
+ const cached = await loadFacts(deps.symbolIndexDir, language, key);
12424
+ if (cached && (!isCsharp || cached.csharp !== void 0)) {
12425
+ return {
12426
+ declarations: cached.declarations,
12427
+ uses: isCsharp ? null : cached.uses,
12428
+ csharp: isCsharp ? cached.csharp : null
12429
+ };
12430
+ }
12431
+ const facts = await extractFileFacts(record, extractor);
12432
+ if (!facts) return null;
12433
+ await writeFacts(deps.symbolIndexDir, language, key, {
12434
+ declarations: facts.declarations,
12435
+ uses: facts.uses ?? [],
12436
+ ...facts.csharp !== null ? { csharp: facts.csharp } : {}
12437
+ });
12438
+ return facts;
12439
+ }
12440
+ return extractFileFacts(record, extractor);
12441
+ }
12207
12442
  const symbolTable = new SymbolTable();
12208
12443
  const recordsByLanguage = /* @__PURE__ */ new Map();
12209
12444
  for (const record of fileRecords) {
@@ -12216,42 +12451,31 @@ async function runRelationPass(graph, projectRoot, deps) {
12216
12451
  }
12217
12452
  list.push(record);
12218
12453
  }
12454
+ const factsByPath = /* @__PURE__ */ new Map();
12455
+ for (const record of fileRecords) {
12456
+ if (!record.language) continue;
12457
+ const extractor = deps.extractorFor(record.language);
12458
+ if (!extractor) continue;
12459
+ const facts = await loadOrExtractFacts(record, extractor);
12460
+ if (facts) factsByPath.set(record.path, facts);
12461
+ }
12219
12462
  for (const [language, records] of recordsByLanguage) {
12220
- const extractor = deps.extractorFor(language);
12221
- const builtFrom = records.map((r) => [r.path, r.hash]).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
12222
- const cached = loadSymbolIndex(deps.symbolIndexDir, language, builtFrom);
12223
- if (cached) {
12224
- for (const [symbolKey, file] of cached.symbols) symbolTable.declare(language, symbolKey, file);
12225
- continue;
12226
- }
12227
- const symbols = [];
12228
12463
  for (const record of records) {
12229
- const parsed = await parseSingle(record);
12230
- if (!parsed) continue;
12231
- try {
12232
- for (const decl of extractor.declarations(parsed)) {
12233
- symbols.push([decl.symbolKey, record.path]);
12234
- symbolTable.declare(language, decl.symbolKey, record.path);
12235
- }
12236
- } finally {
12237
- parsed.tree.delete();
12464
+ const facts = factsByPath.get(record.path);
12465
+ if (!facts) continue;
12466
+ for (const decl of facts.declarations) {
12467
+ symbolTable.declare(language, decl.symbolKey, record.path);
12238
12468
  }
12239
12469
  }
12240
- const toPersist = { builtFrom, symbols };
12241
- await writeSymbolIndex(deps.symbolIndexDir, language, toPersist);
12242
12470
  }
12243
12471
  const csharpRecords = recordsByLanguage.get("csharp") ?? [];
12244
12472
  const projectGlobalUsings = /* @__PURE__ */ new Set();
12245
12473
  const projectGlobalUsingAliases = /* @__PURE__ */ new Map();
12246
12474
  for (const record of csharpRecords) {
12247
- const parsed = await parseSingle(record);
12248
- if (!parsed) continue;
12249
- try {
12250
- for (const prefix of collectGlobalUsings(parsed)) projectGlobalUsings.add(prefix);
12251
- for (const [name, fqn] of collectGlobalUsingAliases(parsed)) projectGlobalUsingAliases.set(name, fqn);
12252
- } finally {
12253
- parsed.tree.delete();
12254
- }
12475
+ const facts = factsByPath.get(record.path);
12476
+ if (!facts || facts.csharp === null) continue;
12477
+ for (const prefix of facts.csharp.scope.globalPrefixes) projectGlobalUsings.add(prefix);
12478
+ for (const [name, fqn] of facts.csharp.scope.globalAliases) projectGlobalUsingAliases.set(name, fqn);
12255
12479
  }
12256
12480
  const csharpGlobalUsings = [...projectGlobalUsings];
12257
12481
  const csharpGlobalUsingAliases = [...projectGlobalUsingAliases.entries()];
@@ -12278,6 +12502,15 @@ async function runRelationPass(graph, projectRoot, deps) {
12278
12502
  }
12279
12503
  };
12280
12504
  const violationsByNode = /* @__PURE__ */ new Map();
12505
+ const detectedEdgesByNode = /* @__PURE__ */ new Map();
12506
+ const resolveDetected = (record, detected, resolvedDeps) => {
12507
+ for (const dep of detected) {
12508
+ const ownerNode = resolveCandidateGroup(dep.candidates, resolver, record.path, record.language);
12509
+ if (ownerNode !== void 0) {
12510
+ resolvedDeps.push({ fromFile: record.path, line: dep.line, ownerNode });
12511
+ }
12512
+ }
12513
+ };
12281
12514
  for (const [nodeId] of graph.nodes) {
12282
12515
  const records = fileRecords.filter((r) => r.nodeId === nodeId);
12283
12516
  if (records.length === 0) continue;
@@ -12286,23 +12519,28 @@ async function runRelationPass(graph, projectRoot, deps) {
12286
12519
  if (!record.language) continue;
12287
12520
  const extractor = deps.extractorFor(record.language);
12288
12521
  if (!extractor) continue;
12289
- const parsed = await parseSingle(record);
12290
- if (!parsed) continue;
12291
- try {
12292
- const detected = record.language === "csharp" ? csharpUses(parsed, {
12522
+ if (record.language === "csharp") {
12523
+ const facts2 = factsByPath.get(record.path);
12524
+ if (!facts2 || facts2.csharp === null) continue;
12525
+ const detected = assembleCsharpCandidates(facts2.csharp, {
12293
12526
  projectGlobalUsings: csharpGlobalUsings,
12294
12527
  projectGlobalUsingAliases: csharpGlobalUsingAliases
12295
- }) : extractor.uses(parsed);
12296
- for (const dep of detected) {
12297
- const ownerNode = resolveCandidateGroup(dep.candidates, resolver, record.path, record.language);
12298
- if (ownerNode !== void 0) {
12299
- resolvedDeps.push({ fromFile: record.path, line: dep.line, ownerNode });
12300
- }
12301
- }
12302
- } finally {
12303
- parsed.tree.delete();
12528
+ });
12529
+ resolveDetected(record, detected, resolvedDeps);
12530
+ continue;
12304
12531
  }
12532
+ const facts = factsByPath.get(record.path);
12533
+ if (!facts || facts.uses === null) continue;
12534
+ resolveDetected(record, facts.uses, resolvedDeps);
12305
12535
  }
12536
+ const edges = /* @__PURE__ */ new Set();
12537
+ for (const d of resolvedDeps) {
12538
+ const m = d.ownerNode;
12539
+ if (m === nodeId) continue;
12540
+ if (graphView.isAncestorOf(m, nodeId) || graphView.isAncestorOf(nodeId, m)) continue;
12541
+ edges.add(m);
12542
+ }
12543
+ if (edges.size > 0) detectedEdgesByNode.set(nodeId, edges);
12306
12544
  const violations = verifyNodeDeps(nodeId, resolvedDeps, graphView);
12307
12545
  if (violations.length) {
12308
12546
  const reason = violations.map((v) => `${v.fromFile}:${v.line} \u2192 undeclared dependency on ${v.ownerNode}`).join("\n");
@@ -12311,7 +12549,7 @@ async function runRelationPass(graph, projectRoot, deps) {
12311
12549
  violationsByNode.set(nodeId, { verdict: "approved", violations: [] });
12312
12550
  }
12313
12551
  }
12314
- return { violationsByNode };
12552
+ return { violationsByNode, factsByPath, detectedEdgesByNode };
12315
12553
  }
12316
12554
 
12317
12555
  // src/relations/extractors/types.ts
@@ -12331,7 +12569,19 @@ function specifierFromStringNode(stringNode) {
12331
12569
  function specifierFromSource(node) {
12332
12570
  return specifierFromStringNode(node.childForFieldName("source"));
12333
12571
  }
12334
- function isRelative(specifier) {
12572
+ function specifierFromCallArg(arg) {
12573
+ if (arg.type === "string") return specifierFromStringNode(arg);
12574
+ if (arg.type !== "template_string") return void 0;
12575
+ let fragment;
12576
+ for (let i = 0; i < arg.namedChildCount; i++) {
12577
+ const child = arg.namedChild(i);
12578
+ if (child === null) continue;
12579
+ if (child.type === "template_substitution") return void 0;
12580
+ if (child.type === "string_fragment") fragment = child.text;
12581
+ }
12582
+ return fragment ?? "";
12583
+ }
12584
+ function isRelative(specifier) {
12335
12585
  return specifier.startsWith(".") || specifier.startsWith("/");
12336
12586
  }
12337
12587
  function isWholeStatementTypeToken(child) {
@@ -12381,7 +12631,8 @@ function uses2(file) {
12381
12631
  const emit = (specifier, node) => {
12382
12632
  if (specifier === void 0 || specifier === "" || !isRelative(specifier)) return;
12383
12633
  const line = node.startPosition.row + 1;
12384
- const dedupKey = `${specifier}\0${line}`;
12634
+ const dedupKey = `${specifier}
12635
+ ${line}`;
12385
12636
  if (seen.has(dedupKey)) return;
12386
12637
  seen.add(dedupKey);
12387
12638
  out.push(single({ kind: "path", specifier }, "import", line));
@@ -12428,8 +12679,8 @@ function uses2(file) {
12428
12679
  const isRequire = fn.type === "identifier" && fn.text === "require";
12429
12680
  if (!isDynamicImport && !isRequire) break;
12430
12681
  const arg = firstArgument(node);
12431
- if (arg === null || arg.type !== "string") break;
12432
- emit(specifierFromStringNode(arg), node);
12682
+ if (arg === null) break;
12683
+ emit(specifierFromCallArg(arg), node);
12433
12684
  break;
12434
12685
  }
12435
12686
  default:
@@ -12470,6 +12721,7 @@ function declarations2(file) {
12470
12721
  }
12471
12722
  var typescriptExtractor = {
12472
12723
  languages: /* @__PURE__ */ new Set(["typescript", "tsx", "javascript"]),
12724
+ rev: 1,
12473
12725
  declarations: declarations2,
12474
12726
  uses: uses2
12475
12727
  };
@@ -12575,6 +12827,7 @@ function declarations3(file) {
12575
12827
  }
12576
12828
  var pythonExtractor = {
12577
12829
  languages: /* @__PURE__ */ new Set(["python"]),
12830
+ rev: 1,
12578
12831
  declarations: declarations3,
12579
12832
  uses: uses3
12580
12833
  };
@@ -12624,6 +12877,7 @@ function declarations4(file) {
12624
12877
  }
12625
12878
  var goExtractor = {
12626
12879
  languages: /* @__PURE__ */ new Set(["go"]),
12880
+ rev: 1,
12627
12881
  declarations: declarations4,
12628
12882
  uses: uses4
12629
12883
  };
@@ -12795,6 +13049,7 @@ function declarations5(file) {
12795
13049
  }
12796
13050
  var javaExtractor = {
12797
13051
  languages: /* @__PURE__ */ new Set(["java"]),
13052
+ rev: 3,
12798
13053
  declarations: declarations5,
12799
13054
  uses: uses5
12800
13055
  };
@@ -12923,6 +13178,7 @@ function declarations6(file) {
12923
13178
  }
12924
13179
  var phpExtractor = {
12925
13180
  languages: /* @__PURE__ */ new Set(["php"]),
13181
+ rev: 1,
12926
13182
  declarations: declarations6,
12927
13183
  uses: uses6
12928
13184
  };
@@ -13047,6 +13303,7 @@ function declarations7(file) {
13047
13303
  }
13048
13304
  var kotlinExtractor = {
13049
13305
  languages: /* @__PURE__ */ new Set(["kotlin"]),
13306
+ rev: 2,
13050
13307
  declarations: declarations7,
13051
13308
  uses: uses7
13052
13309
  };
@@ -13219,6 +13476,7 @@ function declarations8(file) {
13219
13476
  }
13220
13477
  var rustExtractor = {
13221
13478
  languages: /* @__PURE__ */ new Set(["rust"]),
13479
+ rev: 1,
13222
13480
  declarations: declarations8,
13223
13481
  uses: uses8
13224
13482
  };
@@ -13312,6 +13570,7 @@ function functionName(def) {
13312
13570
  }
13313
13571
  var cExtractor = {
13314
13572
  languages: /* @__PURE__ */ new Set(["c"]),
13573
+ rev: 1,
13315
13574
  declarations: declarations9,
13316
13575
  uses: uses9
13317
13576
  };
@@ -13356,6 +13615,7 @@ function functionName2(def) {
13356
13615
  }
13357
13616
  var cppExtractor = {
13358
13617
  languages: /* @__PURE__ */ new Set(["cpp"]),
13618
+ rev: 1,
13359
13619
  declarations: declarations10,
13360
13620
  uses: uses10
13361
13621
  };
@@ -13526,6 +13786,7 @@ function declarations11(file) {
13526
13786
  }
13527
13787
  var rubyExtractor = {
13528
13788
  languages: /* @__PURE__ */ new Set(["ruby"]),
13789
+ rev: 1,
13529
13790
  declarations: declarations11,
13530
13791
  uses: uses11
13531
13792
  };
@@ -13561,12 +13822,6 @@ function extractorForLanguage(language) {
13561
13822
  return byLanguage.get(language);
13562
13823
  }
13563
13824
 
13564
- // src/relations/index-dir.ts
13565
- import path34 from "path";
13566
- function relationIndexDir(graphRootPath) {
13567
- return path34.join(graphRootPath, ".symbols-cache");
13568
- }
13569
-
13570
13825
  // src/relations/allowed-types.ts
13571
13826
  var RELATION_TYPES3 = [
13572
13827
  "uses",
@@ -13579,15 +13834,16 @@ var RELATION_TYPES3 = [
13579
13834
  function allowedRelationTypes(architecture, fromType, toType) {
13580
13835
  const fromConfig = architecture.node_types[fromType];
13581
13836
  if (!fromConfig) return [];
13582
- const relations = fromConfig.relations;
13837
+ const lists = fromConfig.relations;
13838
+ const def = fromConfig.relationDefault ?? "allow";
13583
13839
  const out = [];
13584
13840
  for (const rt of RELATION_TYPES3) {
13585
- const allowedTargets = relations?.[rt];
13586
- if (!allowedTargets) {
13587
- out.push(rt);
13841
+ const allowedTargets = lists?.[rt];
13842
+ if (allowedTargets === void 0) {
13843
+ if (def === "allow") out.push(rt);
13588
13844
  continue;
13589
13845
  }
13590
- if (allowedTargets.includes(toType)) out.push(rt);
13846
+ if (allowedTargets.includes("*") || allowedTargets.includes(toType)) out.push(rt);
13591
13847
  }
13592
13848
  return out;
13593
13849
  }
@@ -13631,15 +13887,15 @@ ${blocks.join("\n")}`
13631
13887
  }
13632
13888
 
13633
13889
  // src/relations/resolve-path.ts
13634
- import path43 from "path";
13635
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
13890
+ import path42 from "path";
13891
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
13636
13892
 
13637
13893
  // src/relations/extractors/typescript-resolve.ts
13638
- import path35 from "path";
13894
+ import path34 from "path";
13639
13895
  function resolveTsPath(specifier, fromFile, exists) {
13640
13896
  if (!specifier.startsWith(".") && !specifier.startsWith("/")) return void 0;
13641
- const baseDir = path35.posix.dirname(toPosix3(fromFile));
13642
- const joined = path35.posix.normalize(path35.posix.join(baseDir, specifier));
13897
+ const baseDir = path34.posix.dirname(toPosix3(fromFile));
13898
+ const joined = path34.posix.normalize(path34.posix.join(baseDir, specifier));
13643
13899
  for (const cand of candidates(joined)) {
13644
13900
  if (exists(cand)) return cand;
13645
13901
  }
@@ -13647,7 +13903,7 @@ function resolveTsPath(specifier, fromFile, exists) {
13647
13903
  }
13648
13904
  function candidates(joined) {
13649
13905
  const out = [];
13650
- const ext = path35.posix.extname(joined);
13906
+ const ext = path34.posix.extname(joined);
13651
13907
  const rewrites = {
13652
13908
  ".js": [".ts", ".tsx", ".js", ".jsx"],
13653
13909
  ".jsx": [".tsx", ".jsx"],
@@ -13662,7 +13918,7 @@ function candidates(joined) {
13662
13918
  } else {
13663
13919
  for (const e of [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]) out.push(joined + e);
13664
13920
  }
13665
- for (const e of [".ts", ".tsx", ".js", ".jsx"]) out.push(path35.posix.join(joined, "index" + e));
13921
+ for (const e of [".ts", ".tsx", ".js", ".jsx"]) out.push(path34.posix.join(joined, "index" + e));
13666
13922
  return out;
13667
13923
  }
13668
13924
  function toPosix3(p2) {
@@ -13670,7 +13926,7 @@ function toPosix3(p2) {
13670
13926
  }
13671
13927
 
13672
13928
  // src/relations/extractors/python-resolve.ts
13673
- import path36 from "path";
13929
+ import path35 from "path";
13674
13930
  function resolvePythonModule(specifier, fromFile, exists) {
13675
13931
  if (specifier.startsWith(".")) {
13676
13932
  return resolveRelative(specifier, fromFile, exists);
@@ -13683,7 +13939,7 @@ function resolveAbsolute(specifier, fromFile, exists) {
13683
13939
  const modulePath = segments.join("/");
13684
13940
  const parentPath = segments.slice(0, -1).join("/");
13685
13941
  const matches = /* @__PURE__ */ new Set();
13686
- for (const dir of ancestorDirs(path36.posix.dirname(toPosix4(fromFile)))) {
13942
+ for (const dir of ancestorDirs(path35.posix.dirname(toPosix4(fromFile)))) {
13687
13943
  const candidates2 = [
13688
13944
  // module-as-file / package at this root
13689
13945
  joinUnder(dir, modulePath + ".py"),
@@ -13707,17 +13963,17 @@ function resolveRelative(specifier, fromFile, exists) {
13707
13963
  if (dotMatch === null) return void 0;
13708
13964
  const dots = dotMatch[0].length;
13709
13965
  const tail = specifier.slice(dots);
13710
- let base = path36.posix.dirname(toPosix4(fromFile));
13966
+ let base = path35.posix.dirname(toPosix4(fromFile));
13711
13967
  for (let i = 0; i < dots - 1; i++) {
13712
- const parent = path36.posix.dirname(base);
13968
+ const parent = path35.posix.dirname(base);
13713
13969
  if (parent === base) return void 0;
13714
13970
  base = parent;
13715
13971
  }
13716
13972
  const tailPath = tail.length > 0 ? tail.split(".").filter((s) => s.length > 0).join("/") : "";
13717
- const target = tailPath.length > 0 ? path36.posix.join(base, tailPath) : base;
13718
- const normalized = path36.posix.normalize(target);
13973
+ const target = tailPath.length > 0 ? path35.posix.join(base, tailPath) : base;
13974
+ const normalized = path35.posix.normalize(target);
13719
13975
  if (normalized.startsWith("..")) return void 0;
13720
- const candidates2 = tailPath.length > 0 ? [normalized + ".py", path36.posix.join(normalized, "__init__.py")] : [path36.posix.join(normalized, "__init__.py")];
13976
+ const candidates2 = tailPath.length > 0 ? [normalized + ".py", path35.posix.join(normalized, "__init__.py")] : [path35.posix.join(normalized, "__init__.py")];
13721
13977
  for (const cand of candidates2) {
13722
13978
  if (exists(cand)) return cand;
13723
13979
  }
@@ -13729,20 +13985,20 @@ function ancestorDirs(dir) {
13729
13985
  for (; ; ) {
13730
13986
  out.push(cur);
13731
13987
  if (cur === "") break;
13732
- const parent = path36.posix.dirname(cur);
13988
+ const parent = path35.posix.dirname(cur);
13733
13989
  cur = parent === "." ? "" : parent;
13734
13990
  }
13735
13991
  return out;
13736
13992
  }
13737
13993
  function joinUnder(dir, sub) {
13738
- return path36.posix.normalize(dir === "" ? sub : path36.posix.join(dir, sub));
13994
+ return path35.posix.normalize(dir === "" ? sub : path35.posix.join(dir, sub));
13739
13995
  }
13740
13996
  function toPosix4(p2) {
13741
13997
  return p2.replace(/\\/g, "/");
13742
13998
  }
13743
13999
 
13744
14000
  // src/relations/extractors/go-resolve.ts
13745
- import path37 from "path";
14001
+ import path36 from "path";
13746
14002
  function resolveGoImport(importPath, fromFile, deps) {
13747
14003
  const resolved = deps.modulePathFor(fromFile);
13748
14004
  if (resolved === void 0) return void 0;
@@ -13756,8 +14012,8 @@ function resolveGoImport(importPath, fromFile, deps) {
13756
14012
  } else {
13757
14013
  return void 0;
13758
14014
  }
13759
- const rel = remainder === "" ? moduleDir : moduleDir === "" ? remainder : path37.posix.join(moduleDir, remainder);
13760
- const repoRelDir = path37.posix.normalize(rel === "" ? "." : rel);
14015
+ const rel = remainder === "" ? moduleDir : moduleDir === "" ? remainder : path36.posix.join(moduleDir, remainder);
14016
+ const repoRelDir = path36.posix.normalize(rel === "" ? "." : rel);
13761
14017
  const cleanDir = repoRelDir === "." ? "" : repoRelDir;
13762
14018
  if (!deps.dirExists(cleanDir)) return void 0;
13763
14019
  const goFiles = deps.goFilesIn(cleanDir);
@@ -13785,7 +14041,7 @@ function resolveGoImport(importPath, fromFile, deps) {
13785
14041
  }
13786
14042
 
13787
14043
  // src/relations/extractors/java-resolve.ts
13788
- import path38 from "path";
14044
+ import path37 from "path";
13789
14045
  function resolveJavaFqn(specifier, fromFile, deps) {
13790
14046
  const segments = specifier.split(".").filter((s) => s.length > 0);
13791
14047
  if (segments.length === 0) return void 0;
@@ -13795,7 +14051,7 @@ function resolveJavaPackageFiles(packageFqn2, fromFile, deps) {
13795
14051
  const segments = packageFqn2.split(".").filter((s) => s.length > 0);
13796
14052
  if (segments.length === 0) return [];
13797
14053
  const pkgDir = segments.join("/");
13798
- for (const dir of ancestorDirs2(path38.posix.dirname(toPosix5(fromFile)))) {
14054
+ for (const dir of ancestorDirs2(path37.posix.dirname(toPosix5(fromFile)))) {
13799
14055
  const repoRelDir = joinUnder2(dir, pkgDir);
13800
14056
  const files = deps.javaFilesIn(repoRelDir);
13801
14057
  if (files.length > 0) return [...files].sort();
@@ -13805,7 +14061,7 @@ function resolveJavaPackageFiles(packageFqn2, fromFile, deps) {
13805
14061
  function resolveType(segments, fromFile, deps) {
13806
14062
  const typePath = segments.join("/") + ".java";
13807
14063
  const parentTypePath = segments.length >= 2 ? segments.slice(0, -1).join("/") + ".java" : void 0;
13808
- for (const dir of ancestorDirs2(path38.posix.dirname(toPosix5(fromFile)))) {
14064
+ for (const dir of ancestorDirs2(path37.posix.dirname(toPosix5(fromFile)))) {
13809
14065
  const candidates2 = [joinUnder2(dir, typePath)];
13810
14066
  if (parentTypePath !== void 0) candidates2.push(joinUnder2(dir, parentTypePath));
13811
14067
  for (const cand of candidates2) {
@@ -13820,20 +14076,20 @@ function ancestorDirs2(dir) {
13820
14076
  for (; ; ) {
13821
14077
  out.push(cur);
13822
14078
  if (cur === "") break;
13823
- const parent = path38.posix.dirname(cur);
14079
+ const parent = path37.posix.dirname(cur);
13824
14080
  cur = parent === "." ? "" : parent;
13825
14081
  }
13826
14082
  return out;
13827
14083
  }
13828
14084
  function joinUnder2(dir, sub) {
13829
- return path38.posix.normalize(dir === "" ? sub : path38.posix.join(dir, sub));
14085
+ return path37.posix.normalize(dir === "" ? sub : path37.posix.join(dir, sub));
13830
14086
  }
13831
14087
  function toPosix5(p2) {
13832
14088
  return p2.replace(/\\/g, "/");
13833
14089
  }
13834
14090
 
13835
14091
  // src/relations/extractors/php-resolve.ts
13836
- import path39 from "path";
14092
+ import path38 from "path";
13837
14093
  function resolvePhpFqn(specifier, fromFile, deps) {
13838
14094
  const fqn = specifier.startsWith("\\") ? specifier.slice(1) : specifier;
13839
14095
  if (fqn === "") return void 0;
@@ -13859,7 +14115,7 @@ function resolvePhpFqn(specifier, fromFile, deps) {
13859
14115
  return [...hits][0];
13860
14116
  }
13861
14117
  function joinUnder3(dir, sub) {
13862
- return path39.posix.normalize(dir === "" ? sub : path39.posix.join(dir, sub));
14118
+ return path38.posix.normalize(dir === "" ? sub : path38.posix.join(dir, sub));
13863
14119
  }
13864
14120
  function parsePsr4(composerJsonText, composerDir) {
13865
14121
  const out = /* @__PURE__ */ new Map();
@@ -13897,19 +14153,19 @@ function parsePsr4(composerJsonText, composerDir) {
13897
14153
  function normalizeDir(value, composerDir) {
13898
14154
  const trimmed = value.replace(/\/+$/, "");
13899
14155
  if (trimmed === "" || trimmed === ".") return composerDir;
13900
- const joined = composerDir === "" ? trimmed : path39.posix.join(composerDir, trimmed);
13901
- const norm = path39.posix.normalize(joined);
14156
+ const joined = composerDir === "" ? trimmed : path38.posix.join(composerDir, trimmed);
14157
+ const norm = path38.posix.normalize(joined);
13902
14158
  return norm === "." ? "" : norm;
13903
14159
  }
13904
14160
 
13905
14161
  // src/relations/extractors/rust-resolve.ts
13906
- import path40 from "path";
14162
+ import path39 from "path";
13907
14163
  function resolveRustPath(specifier, fromFile, exists, deps) {
13908
14164
  const segments = specifier.split("::").filter((s) => s.length > 0);
13909
14165
  if (segments.length === 0) return void 0;
13910
14166
  const root = segments[0];
13911
14167
  const rest = segments.slice(1);
13912
- const fromDir = path40.posix.dirname(toPosix6(fromFile));
14168
+ const fromDir = path39.posix.dirname(toPosix6(fromFile));
13913
14169
  if (root === "super" || root === "self") {
13914
14170
  return resolveRelative2(root, segments, fromFile, exists, deps);
13915
14171
  }
@@ -13929,9 +14185,9 @@ function resolveRelative2(root, segments, fromFile, exists, deps) {
13929
14185
  if (crate === void 0) return void 0;
13930
14186
  const srcDir = crate.srcDir;
13931
14187
  const fromPosix = toPosix6(fromFile);
13932
- const fromDir = path40.posix.dirname(fromPosix);
13933
- const baseName = path40.posix.basename(fromPosix, ".rs");
13934
- let moduleDir = baseName === "mod" || baseName === "lib" || baseName === "main" ? fromDir : path40.posix.join(fromDir, baseName);
14188
+ const fromDir = path39.posix.dirname(fromPosix);
14189
+ const baseName = path39.posix.basename(fromPosix, ".rs");
14190
+ let moduleDir = baseName === "mod" || baseName === "lib" || baseName === "main" ? fromDir : path39.posix.join(fromDir, baseName);
13935
14191
  let i = 0;
13936
14192
  for (; i < segments.length; i++) {
13937
14193
  const seg = segments[i];
@@ -13939,7 +14195,7 @@ function resolveRelative2(root, segments, fromFile, exists, deps) {
13939
14195
  continue;
13940
14196
  }
13941
14197
  if (seg === "super") {
13942
- const parent = path40.posix.dirname(moduleDir);
14198
+ const parent = path39.posix.dirname(moduleDir);
13943
14199
  if (!withinSrc(parent, srcDir)) return void 0;
13944
14200
  moduleDir = parent;
13945
14201
  continue;
@@ -13971,13 +14227,13 @@ function resolveFromModuleDir(moduleDir, tail, exists) {
13971
14227
  return void 0;
13972
14228
  }
13973
14229
  function withinSrc(dir, srcDir) {
13974
- const d = path40.posix.normalize(dir);
13975
- const s = path40.posix.normalize(srcDir);
14230
+ const d = path39.posix.normalize(dir);
14231
+ const s = path39.posix.normalize(srcDir);
13976
14232
  if (s === "" || s === ".") return !d.startsWith("..");
13977
14233
  return d === s || d.startsWith(s + "/");
13978
14234
  }
13979
14235
  function joinUnder4(dir, sub) {
13980
- const joined = path40.posix.normalize(dir === "" ? sub : path40.posix.join(dir, sub));
14236
+ const joined = path39.posix.normalize(dir === "" ? sub : path39.posix.join(dir, sub));
13981
14237
  if (joined.startsWith("..")) return void 0;
13982
14238
  return joined;
13983
14239
  }
@@ -13986,16 +14242,16 @@ function toPosix6(p2) {
13986
14242
  }
13987
14243
 
13988
14244
  // src/relations/extractors/include-resolve.ts
13989
- import path41 from "path";
14245
+ import path40 from "path";
13990
14246
  function resolveIncludePath(headerPath, fromFile, exists) {
13991
14247
  if (headerPath === "") return void 0;
13992
- const fromDir = path41.posix.dirname(toPosix7(fromFile));
13993
- const relative3 = normalizeRepoRel(path41.posix.join(fromDir, headerPath));
14248
+ const fromDir = path40.posix.dirname(toPosix7(fromFile));
14249
+ const relative3 = normalizeRepoRel(path40.posix.join(fromDir, headerPath));
13994
14250
  if (relative3 !== void 0 && exists(relative3)) return relative3;
13995
14251
  return void 0;
13996
14252
  }
13997
14253
  function normalizeRepoRel(p2) {
13998
- const norm = path41.posix.normalize(p2);
14254
+ const norm = path40.posix.normalize(p2);
13999
14255
  if (norm.startsWith("..")) return void 0;
14000
14256
  return norm;
14001
14257
  }
@@ -14004,13 +14260,13 @@ function toPosix7(p2) {
14004
14260
  }
14005
14261
 
14006
14262
  // src/relations/extractors/ruby-resolve.ts
14007
- import path42 from "path";
14263
+ import path41 from "path";
14008
14264
  function resolveRubyRequireRelative(specifier, fromFile, exists) {
14009
14265
  if (specifier === "") return void 0;
14010
- const fromDir = path42.posix.dirname(toPosix8(fromFile));
14266
+ const fromDir = path41.posix.dirname(toPosix8(fromFile));
14011
14267
  const withExt = /\.rb$/.test(specifier) ? specifier : `${specifier}.rb`;
14012
- const joined = path42.posix.join(fromDir, withExt);
14013
- const normalized = path42.posix.normalize(joined);
14268
+ const joined = path41.posix.join(fromDir, withExt);
14269
+ const normalized = path41.posix.normalize(joined);
14014
14270
  if (normalized.startsWith("..")) return void 0;
14015
14271
  return exists(normalized) ? normalized : void 0;
14016
14272
  }
@@ -14020,7 +14276,7 @@ function toPosix8(p2) {
14020
14276
 
14021
14277
  // src/relations/resolve-path.ts
14022
14278
  function makeResolvePathToFile(projectRoot, ownerOf) {
14023
- const exists = (repoRelPosix) => existsSync7(path43.resolve(projectRoot, repoRelPosix));
14279
+ const exists = (repoRelPosix) => existsSync8(path42.resolve(projectRoot, repoRelPosix));
14024
14280
  const goDeps = makeGoResolveDeps(projectRoot, ownerOf);
14025
14281
  const javaDeps = makeJavaResolveDeps(projectRoot, exists);
14026
14282
  const phpDeps = makePhpResolveDeps(projectRoot, exists);
@@ -14068,10 +14324,10 @@ function makeResolvePathToFile(projectRoot, ownerOf) {
14068
14324
  function makeRustResolveDeps(projectRoot) {
14069
14325
  const byDir = /* @__PURE__ */ new Map();
14070
14326
  function readCrateName(repoRelDir) {
14071
- const abs = path43.join(projectRoot, repoRelDir, "Cargo.toml");
14327
+ const abs = path42.join(projectRoot, repoRelDir, "Cargo.toml");
14072
14328
  let text2;
14073
14329
  try {
14074
- text2 = readFileSync7(abs, "utf-8");
14330
+ text2 = readFileSync8(abs, "utf-8");
14075
14331
  } catch {
14076
14332
  return void 0;
14077
14333
  }
@@ -14089,14 +14345,14 @@ function makeRustResolveDeps(projectRoot) {
14089
14345
  return void 0;
14090
14346
  }
14091
14347
  function crateRootFor(fromFile) {
14092
- let dir = path43.posix.dirname(toPosix9(fromFile));
14348
+ let dir = path42.posix.dirname(toPosix9(fromFile));
14093
14349
  if (dir === ".") dir = "";
14094
14350
  for (; ; ) {
14095
14351
  if (byDir.has(dir)) {
14096
14352
  const cached = byDir.get(dir);
14097
14353
  if (cached !== void 0) return cached;
14098
- } else if (existsSync7(path43.join(projectRoot, dir, "Cargo.toml"))) {
14099
- const srcDir = dir === "" ? "src" : path43.posix.join(dir, "src");
14354
+ } else if (existsSync8(path42.join(projectRoot, dir, "Cargo.toml"))) {
14355
+ const srcDir = dir === "" ? "src" : path42.posix.join(dir, "src");
14100
14356
  const entry = { srcDir, crateName: readCrateName(dir) };
14101
14357
  byDir.set(dir, entry);
14102
14358
  return entry;
@@ -14104,7 +14360,7 @@ function makeRustResolveDeps(projectRoot) {
14104
14360
  byDir.set(dir, void 0);
14105
14361
  }
14106
14362
  if (dir === "") return void 0;
14107
- const parent = path43.posix.dirname(dir);
14363
+ const parent = path42.posix.dirname(dir);
14108
14364
  dir = parent === "." ? "" : parent;
14109
14365
  }
14110
14366
  }
@@ -14113,10 +14369,10 @@ function makeRustResolveDeps(projectRoot) {
14113
14369
  function makeGoResolveDeps(projectRoot, ownerOf) {
14114
14370
  const moduleByDir = /* @__PURE__ */ new Map();
14115
14371
  function readModulePath(repoRelDir) {
14116
- const abs = path43.join(projectRoot, repoRelDir, "go.mod");
14372
+ const abs = path42.join(projectRoot, repoRelDir, "go.mod");
14117
14373
  let text2;
14118
14374
  try {
14119
- text2 = readFileSync7(abs, "utf-8");
14375
+ text2 = readFileSync8(abs, "utf-8");
14120
14376
  } catch {
14121
14377
  return void 0;
14122
14378
  }
@@ -14129,24 +14385,24 @@ function makeGoResolveDeps(projectRoot, ownerOf) {
14129
14385
  return void 0;
14130
14386
  }
14131
14387
  function modulePathFor(fromFile) {
14132
- let dir = path43.posix.dirname(toPosix9(fromFile));
14388
+ let dir = path42.posix.dirname(toPosix9(fromFile));
14133
14389
  if (dir === ".") dir = "";
14134
14390
  for (; ; ) {
14135
14391
  if (moduleByDir.has(dir)) {
14136
14392
  const cached = moduleByDir.get(dir);
14137
14393
  if (cached !== void 0) return { modulePath: cached, moduleDir: dir };
14138
14394
  } else {
14139
- const mod = existsSync7(path43.join(projectRoot, dir, "go.mod")) ? readModulePath(dir) : void 0;
14395
+ const mod = existsSync8(path42.join(projectRoot, dir, "go.mod")) ? readModulePath(dir) : void 0;
14140
14396
  moduleByDir.set(dir, mod);
14141
14397
  if (mod !== void 0) return { modulePath: mod, moduleDir: dir };
14142
14398
  }
14143
14399
  if (dir === "") return void 0;
14144
- const parent = path43.posix.dirname(dir);
14400
+ const parent = path42.posix.dirname(dir);
14145
14401
  dir = parent === "." ? "" : parent;
14146
14402
  }
14147
14403
  }
14148
14404
  function dirExists(repoRelDir) {
14149
- const abs = path43.resolve(projectRoot, repoRelDir);
14405
+ const abs = path42.resolve(projectRoot, repoRelDir);
14150
14406
  try {
14151
14407
  return statSync3(abs).isDirectory();
14152
14408
  } catch {
@@ -14154,7 +14410,7 @@ function makeGoResolveDeps(projectRoot, ownerOf) {
14154
14410
  }
14155
14411
  }
14156
14412
  function goFilesIn(repoRelDir) {
14157
- const abs = path43.resolve(projectRoot, repoRelDir);
14413
+ const abs = path42.resolve(projectRoot, repoRelDir);
14158
14414
  let entries;
14159
14415
  try {
14160
14416
  entries = readdirSync3(abs, { withFileTypes: true });
@@ -14164,7 +14420,7 @@ function makeGoResolveDeps(projectRoot, ownerOf) {
14164
14420
  const out = [];
14165
14421
  for (const e of entries) {
14166
14422
  if (e.isFile() && e.name.endsWith(".go")) {
14167
- out.push(repoRelDir === "" ? e.name : path43.posix.join(repoRelDir, e.name));
14423
+ out.push(repoRelDir === "" ? e.name : path42.posix.join(repoRelDir, e.name));
14168
14424
  }
14169
14425
  }
14170
14426
  return out;
@@ -14173,7 +14429,7 @@ function makeGoResolveDeps(projectRoot, ownerOf) {
14173
14429
  }
14174
14430
  function makeJavaResolveDeps(projectRoot, exists) {
14175
14431
  function javaFilesIn(repoRelDir) {
14176
- const abs = path43.resolve(projectRoot, repoRelDir);
14432
+ const abs = path42.resolve(projectRoot, repoRelDir);
14177
14433
  let entries;
14178
14434
  try {
14179
14435
  entries = readdirSync3(abs, { withFileTypes: true });
@@ -14183,7 +14439,7 @@ function makeJavaResolveDeps(projectRoot, exists) {
14183
14439
  const out = [];
14184
14440
  for (const e of entries) {
14185
14441
  if (e.isFile() && e.name.endsWith(".java")) {
14186
- out.push(repoRelDir === "" ? e.name : path43.posix.join(repoRelDir, e.name));
14442
+ out.push(repoRelDir === "" ? e.name : path42.posix.join(repoRelDir, e.name));
14187
14443
  }
14188
14444
  }
14189
14445
  return out;
@@ -14193,23 +14449,23 @@ function makeJavaResolveDeps(projectRoot, exists) {
14193
14449
  function makePhpResolveDeps(projectRoot, exists) {
14194
14450
  const psr4ByDir = /* @__PURE__ */ new Map();
14195
14451
  function readPsr4(repoRelDir) {
14196
- const abs = path43.join(projectRoot, repoRelDir, "composer.json");
14452
+ const abs = path42.join(projectRoot, repoRelDir, "composer.json");
14197
14453
  let text2;
14198
14454
  try {
14199
- text2 = readFileSync7(abs, "utf-8");
14455
+ text2 = readFileSync8(abs, "utf-8");
14200
14456
  } catch {
14201
14457
  return /* @__PURE__ */ new Map();
14202
14458
  }
14203
14459
  return parsePsr4(text2, repoRelDir);
14204
14460
  }
14205
14461
  function psr4For(fromFile) {
14206
- let dir = path43.posix.dirname(toPosix9(fromFile));
14462
+ let dir = path42.posix.dirname(toPosix9(fromFile));
14207
14463
  if (dir === ".") dir = "";
14208
14464
  for (; ; ) {
14209
14465
  if (psr4ByDir.has(dir)) {
14210
14466
  const cached = psr4ByDir.get(dir);
14211
14467
  if (cached !== void 0 && cached.size > 0) return cached;
14212
- } else if (existsSync7(path43.join(projectRoot, dir, "composer.json"))) {
14468
+ } else if (existsSync8(path42.join(projectRoot, dir, "composer.json"))) {
14213
14469
  const map = readPsr4(dir);
14214
14470
  psr4ByDir.set(dir, map);
14215
14471
  if (map.size > 0) return map;
@@ -14217,7 +14473,7 @@ function makePhpResolveDeps(projectRoot, exists) {
14217
14473
  psr4ByDir.set(dir, /* @__PURE__ */ new Map());
14218
14474
  }
14219
14475
  if (dir === "") return /* @__PURE__ */ new Map();
14220
- const parent = path43.posix.dirname(dir);
14476
+ const parent = path42.posix.dirname(dir);
14221
14477
  dir = parent === "." ? "" : parent;
14222
14478
  }
14223
14479
  }
@@ -14387,7 +14643,7 @@ function emitPairIssue(vp) {
14387
14643
  async function classifyLogStateFromLock(graph, projectRoot, lock, issues) {
14388
14644
  for (const [nodePath] of graph.nodes) {
14389
14645
  const logRel = `.yggdrasil/model/${nodePath}/log.md`;
14390
- const logAbs = path44.join(projectRoot, logRel);
14646
+ const logAbs = path43.join(projectRoot, logRel);
14391
14647
  let logContent = null;
14392
14648
  try {
14393
14649
  logContent = await readTextFile(logAbs);
@@ -14472,8 +14728,8 @@ function scanUncoveredFiles(graph, gitTrackedFiles) {
14472
14728
  const paths = normalizeMappingPaths(node.meta.mapping);
14473
14729
  allMappings.push(...paths);
14474
14730
  }
14475
- const projectRoot = path44.dirname(graph.rootPath);
14476
- const yggPrefix = toPosixPath(path44.relative(projectRoot, graph.rootPath));
14731
+ const projectRoot = path43.dirname(graph.rootPath);
14732
+ const yggPrefix = toPosixPath(path43.relative(projectRoot, graph.rootPath));
14477
14733
  const uncovered = [];
14478
14734
  const tracked = excludeNestedGraphSubtrees(gitTrackedFiles);
14479
14735
  for (const file of tracked) {
@@ -14495,8 +14751,8 @@ async function scanGitignoredCoveredFiles(graph, gitTrackedFiles) {
14495
14751
  if (!isGlobPattern(raw)) directlyNamed.add(normalizeMappingPath(raw));
14496
14752
  }
14497
14753
  }
14498
- const projectRoot = path44.dirname(graph.rootPath);
14499
- const yggPrefix = toPosixPath(path44.relative(projectRoot, graph.rootPath));
14754
+ const projectRoot = path43.dirname(graph.rootPath);
14755
+ const yggPrefix = toPosixPath(path43.relative(projectRoot, graph.rootPath));
14500
14756
  let gitignoreStack;
14501
14757
  try {
14502
14758
  gitignoreStack = await loadRootGitignoreStack(projectRoot);
@@ -14514,7 +14770,7 @@ async function scanGitignoredCoveredFiles(graph, gitTrackedFiles) {
14514
14770
  if (directlyNamed.has(normalizeMappingPath(normalized))) continue;
14515
14771
  let ignored;
14516
14772
  try {
14517
- ignored = isIgnoredByStack(path44.join(projectRoot, normalized), gitignoreStack);
14773
+ ignored = isIgnoredByStack(path43.join(projectRoot, normalized), gitignoreStack);
14518
14774
  } catch (err) {
14519
14775
  debugWrite(`[check] scanGitignoredCoveredFiles: isIgnoredByStack threw for ${normalized}: ${err.message}`);
14520
14776
  continue;
@@ -14537,7 +14793,7 @@ function buildGitignoredCoveredIssues(offending) {
14537
14793
  }));
14538
14794
  }
14539
14795
  async function runCheck(graph, gitTrackedFiles) {
14540
- const projectRoot = path44.dirname(graph.rootPath);
14796
+ const projectRoot = path43.dirname(graph.rootPath);
14541
14797
  const validation = await validate(graph);
14542
14798
  const validationIssues = validation.issues.filter((vi) => vi.code).map((vi) => ({ ...vi, code: vi.code }));
14543
14799
  const lockIssues = [];
@@ -14560,7 +14816,9 @@ async function runCheck(graph, gitTrackedFiles) {
14560
14816
  const relResult = await runRelationPass(graph, projectRoot, {
14561
14817
  extractorFor: extractorForLanguage,
14562
14818
  resolvePathToFile: makeResolvePathToFile(projectRoot, buildOwnerIndex(graph.nodes).ownerOf),
14563
- symbolIndexDir: relationIndexDir(graph.rootPath)
14819
+ // Repurposed field: now the content-addressed AST fact cache root (.ast-cache), the
14820
+ // per-file parse cache that skips the tree-sitter re-parse of unchanged files.
14821
+ symbolIndexDir: astCacheDir(graph.rootPath)
14564
14822
  });
14565
14823
  for (const [nodeId, nv] of relResult.violationsByNode) {
14566
14824
  if (nv.verdict !== "refused") continue;
@@ -14591,7 +14849,7 @@ async function runCheck(graph, gitTrackedFiles) {
14591
14849
  let coveredFiles = 0;
14592
14850
  let totalFiles = 0;
14593
14851
  if (gitTrackedFiles !== null) {
14594
- const yggPrefix = toPosixPath(path44.relative(projectRoot, graph.rootPath));
14852
+ const yggPrefix = toPosixPath(path43.relative(projectRoot, graph.rootPath));
14595
14853
  const sourceFiles = excludeNestedGraphSubtrees(gitTrackedFiles).filter((f) => {
14596
14854
  const normalized = toPosixPath(f.trim());
14597
14855
  return !normalized.startsWith(yggPrefix + "/") && normalized !== yggPrefix;
@@ -14622,7 +14880,7 @@ async function runCheck(graph, gitTrackedFiles) {
14622
14880
  const advisoryWarnings = allIssues.filter((i) => i.code === "aspect-violation-advisory").length;
14623
14881
  const draftSkipped = countDraftAspectsAcrossGraph(graph);
14624
14882
  return {
14625
- projectName: path44.basename(projectRoot),
14883
+ projectName: path43.basename(projectRoot),
14626
14884
  nodeCount: graph.nodes.size,
14627
14885
  nodeTypeCounts,
14628
14886
  aspectCount: graph.aspects.length,
@@ -14705,7 +14963,7 @@ function computeSuggestedNext(issues) {
14705
14963
  }
14706
14964
 
14707
14965
  // src/core/fill.ts
14708
- import path47 from "path";
14966
+ import path46 from "path";
14709
14967
 
14710
14968
  // src/llm/api-utils.ts
14711
14969
  var ENV_VAR_MAP = {
@@ -15166,13 +15424,13 @@ var GeminiCliProvider = class extends CliAgentProvider {
15166
15424
  registerProvider("gemini-cli", (c) => new GeminiCliProvider({ model: c.model, timeout: c.timeout }));
15167
15425
 
15168
15426
  // src/core/fill-det.ts
15169
- import path45 from "path";
15427
+ import path44 from "path";
15170
15428
 
15171
15429
  // src/core/fill-shared.ts
15172
15430
  async function readBytesOrEmpty(absPath) {
15173
- const { readFile: readFile20 } = await import("fs/promises");
15431
+ const { readFile: readFile21 } = await import("fs/promises");
15174
15432
  try {
15175
- return await readFile20(absPath);
15433
+ return await readFile21(absPath);
15176
15434
  } catch (e) {
15177
15435
  debugWrite(`[fill] readBytesOrEmpty failed for ${absPath}: ${e instanceof Error ? e.message : String(e)}`);
15178
15436
  return Buffer.alloc(0);
@@ -15181,7 +15439,7 @@ async function readBytesOrEmpty(absPath) {
15181
15439
 
15182
15440
  // src/core/fill-det.ts
15183
15441
  async function fillDetPair(graph, projectRoot, pair, aspect) {
15184
- const aspectDirAbs = path45.join(projectRoot, ".yggdrasil", "aspects", aspect.id);
15442
+ const aspectDirAbs = path44.join(projectRoot, ".yggdrasil", "aspects", aspect.id);
15185
15443
  const fullMapping = await computeNodeMappedFiles(graph, pair.nodePath);
15186
15444
  const subjectScope = pair.subjectFiles.length < fullMapping.length ? pair.subjectFiles : void 0;
15187
15445
  const runOnce = async () => {
@@ -15222,7 +15480,7 @@ async function fillDetPair(graph, projectRoot, pair, aspect) {
15222
15480
  const observations = run.result.observations;
15223
15481
  const files = [];
15224
15482
  for (const rel of pair.subjectFiles) {
15225
- const abs = path45.resolve(projectRoot, rel);
15483
+ const abs = path44.resolve(projectRoot, rel);
15226
15484
  const bytes = await readBytesOrEmpty(abs);
15227
15485
  files.push([rel, hashBytes(bytes)]);
15228
15486
  }
@@ -15254,7 +15512,7 @@ function detRuntimeNotice(aspectId, unitKey, reason) {
15254
15512
  }
15255
15513
 
15256
15514
  // src/core/fill-llm.ts
15257
- import path46 from "path";
15515
+ import path45 from "path";
15258
15516
 
15259
15517
  // src/llm/aspect-verifier.ts
15260
15518
  async function verifyWithConsensus(provider, prompt, consensus) {
@@ -15283,14 +15541,14 @@ async function verifyWithConsensus(provider, prompt, consensus) {
15283
15541
  async function fillLlmPair(graph, projectRoot, pair, aspect, tier, tierName, mergedTier, provider, referencesCache) {
15284
15542
  const subjects = [];
15285
15543
  for (const rel of pair.subjectFiles) {
15286
- const bytes = await readBytesOrEmpty(path46.resolve(projectRoot, rel));
15544
+ const bytes = await readBytesOrEmpty(path45.resolve(projectRoot, rel));
15287
15545
  subjects.push({ path: rel, bytes });
15288
15546
  }
15289
15547
  const refInputs = aspect.references ?? [];
15290
15548
  const referencesForHash = [];
15291
15549
  const referencesForPrompt = [];
15292
15550
  for (const ref of refInputs) {
15293
- const absRef = path46.resolve(projectRoot, ref.path);
15551
+ const absRef = path45.resolve(projectRoot, ref.path);
15294
15552
  let bytes = referencesCache.get(absRef);
15295
15553
  if (bytes === void 0) {
15296
15554
  bytes = await readFileBytes(absRef);
@@ -15770,7 +16028,7 @@ async function runFill(graph, opts) {
15770
16028
  });
15771
16029
  const emitIssue = opts.emitIssue ?? (() => {
15772
16030
  });
15773
- const projectRoot = path47.dirname(graph.rootPath);
16031
+ const projectRoot = path46.dirname(graph.rootPath);
15774
16032
  const onlyDeterministic = opts.onlyDeterministic ?? false;
15775
16033
  const dryRun = opts.dryRun ?? false;
15776
16034
  const isTTY2 = opts.isTTY ?? (process.stderr.isTTY ?? false);
@@ -16050,7 +16308,7 @@ async function runFill(graph, opts) {
16050
16308
  }
16051
16309
 
16052
16310
  // src/cli/check.ts
16053
- import path48 from "path";
16311
+ import path47 from "path";
16054
16312
 
16055
16313
  // src/cli/group-issues.ts
16056
16314
  var CODE_ONLY_GROUP_CODES = /* @__PURE__ */ new Set(["unverified"]);
@@ -16112,6 +16370,8 @@ function groupIssues(issues) {
16112
16370
  const rep = sorted[0];
16113
16371
  const nodes = new Set(sorted.map((m) => m.nodePath ?? ""));
16114
16372
  const isCodeOnly = CODE_ONLY_GROUP_CODES.has(rep.code);
16373
+ const distinctNext = new Set(sorted.map((m) => m.messageData.next ?? ""));
16374
+ const distinctWhy = new Set(sorted.map((m) => m.messageData.why ?? ""));
16115
16375
  groups.push({
16116
16376
  code: rep.code,
16117
16377
  aspectId: isCodeOnly ? void 0 : rep.aspectId,
@@ -16122,6 +16382,8 @@ function groupIssues(issues) {
16122
16382
  sharedWhy: rep.messageData.why,
16123
16383
  sharedNext: rep.messageData.next,
16124
16384
  perMemberReason: FULL_WHAT_CODES.has(rep.code),
16385
+ divergentNext: distinctNext.size > 1,
16386
+ divergentWhy: distinctWhy.size > 1,
16125
16387
  members: sorted
16126
16388
  });
16127
16389
  }
@@ -16156,12 +16418,12 @@ function resolveApproveMode(opts, config) {
16156
16418
  return { approve: false, onlyDeterministic: false };
16157
16419
  }
16158
16420
  function registerCheckCommand(program2) {
16159
- program2.command("check").description("Unified graph gate \u2014 verification, coverage, completeness").option("--approve", "Fill every unverified pair (deterministic first, then LLM), then report").option("--no-approve", "Force read-only mode even when auto_approve is configured (overrides config)").option("--only-deterministic", "With --approve: fill ONLY deterministic pairs (keyless, free); committed locks stay untouched. For CI and pre-commit.").option("--dry-run", "With --approve: free cost preview \u2014 print the budget + per-node/per-aspect breakdown, then exit 0 WITHOUT writing anything or calling the reviewer.").option("--top [n]", "Read-only triage: print only the N highest-priority issue blocks (bare --top = just the single suggestedNext block). Header counts + exit code stay TRUE.").option("--summary", "Read-only triage: print per-node counts only (no per-issue blocks). Header counts + exit code stay TRUE.").option("--details", "Read-only: ungrouped, one block per issue (full per-pair detail). Opposite of the default grouped view.").option("--aspect <id>", "Read-only: drill into one rule \u2014 show only that aspect's issues, grouped, with the full per-node detail.").option("-q, --quiet", "Suppress --approve progress on stderr (only the final report + exit code). No-op with a plain read or --dry-run.").action(async (opts) => {
16421
+ program2.command("check").description("Unified graph gate \u2014 verification, coverage, completeness").option("--approve", "Fill every unverified pair (deterministic first, then LLM), then report").option("--no-approve", "Force read-only mode even when auto_approve is configured (overrides config)").option("--only-deterministic", "With --approve: fill ONLY deterministic pairs (keyless, free); committed locks stay untouched. For CI and pre-commit.").option("--dry-run", "With --approve: free cost preview \u2014 print the budget + per-node/per-aspect breakdown, then exit 0 WITHOUT writing anything or calling the reviewer.").option("--top [n]", "Read-only triage: print only the N highest-priority issue blocks (bare --top = just the single suggestedNext block). Header counts + exit code stay TRUE.").option("--summary", "Read-only triage: print per-node counts only (no per-issue blocks). Header counts + exit code stay TRUE.").option("--details", "Read-only: ungrouped, one block per issue (full per-pair detail). Opposite of the default grouped view.").option("--aspect <id>", "Read-only: drill into one rule \u2014 show only that aspect's issues, grouped, with the full per-node detail.").option("-q, --quiet", "Suppress --approve progress on stderr (only the final report + exit code). No-op with a plain read; with --dry-run the budget preview still prints (--dry-run wins).").action(async (opts) => {
16160
16422
  try {
16161
16423
  const cwd = process.cwd();
16162
16424
  const graph = await loadGraphOrAbort(cwd, { tolerateInvalidConfig: true });
16163
16425
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
16164
- const projectRoot = path48.dirname(graph.rootPath);
16426
+ const projectRoot = path47.dirname(graph.rootPath);
16165
16427
  const gitFiles = await walkRepoFiles(projectRoot);
16166
16428
  const wantsTop = opts.top !== void 0;
16167
16429
  if (wantsTop && opts.summary) {
@@ -16182,6 +16444,16 @@ function registerCheckCommand(program2) {
16182
16444
  await exitAfterFlush(1);
16183
16445
  return;
16184
16446
  }
16447
+ if (opts.onlyDeterministic && (wantsTop || opts.summary || opts.details || opts.aspect !== void 0)) {
16448
+ const viewFlag = wantsTop ? "--top" : opts.summary ? "--summary" : opts.details ? "--details" : "--aspect";
16449
+ process.stderr.write(chalk8.red(`Error: ${buildIssueMessage({
16450
+ what: `${viewFlag} cannot be combined with --only-deterministic.`,
16451
+ why: `${viewFlag} is a READ-ONLY view of the plain \`yg check\` result (it narrows output and writes nothing). --only-deterministic is a FILL flag (it implies --approve, writing the deterministic verdict cache). Mixing a read-only view with the writer would silently drop the fill \u2014 the deterministic pairs would NOT be filled.`,
16452
+ next: `Run: yg check ${viewFlag}${opts.aspect !== void 0 ? " <id>" : wantsTop ? " <n>" : ""} (read-only view), or yg check --approve --only-deterministic (deterministic fill).`
16453
+ })}`) + "\n");
16454
+ await exitAfterFlush(1);
16455
+ return;
16456
+ }
16185
16457
  if (opts.details && (wantsTop || opts.summary)) {
16186
16458
  process.stderr.write(chalk8.red(`Error: ${buildIssueMessage({
16187
16459
  what: "--details cannot be combined with --top or --summary.",
@@ -16229,6 +16501,18 @@ function registerCheckCommand(program2) {
16229
16501
  await exitAfterFlush(1);
16230
16502
  return;
16231
16503
  }
16504
+ const knownAspectIds = (graph.aspects ?? []).map((a) => a.id);
16505
+ if (!knownAspectIds.includes(opts.aspect)) {
16506
+ const idList = knownAspectIds.slice().sort((a, b) => a.localeCompare(b, "en"));
16507
+ const known = idList.length === 0 ? "The graph defines no aspects." : idList.length <= 30 ? `Known aspect ids: ${idList.join(", ")}.` : `The graph defines ${idList.length} aspects.`;
16508
+ process.stderr.write(chalk8.red(`Error: ${buildIssueMessage({
16509
+ what: `Unknown aspect '${opts.aspect}'.`,
16510
+ why: `--aspect drills into ONE rule by its aspect id, but '${opts.aspect}' is not an aspect defined in this graph \u2014 so the filter would match nothing and render a misleading "0 of N errors" view. ${known}`,
16511
+ next: "Run: yg aspects (list every aspect id), then yg check --aspect <id> with a real id; or yg check (full wall)."
16512
+ })}`) + "\n");
16513
+ await exitAfterFlush(1);
16514
+ return;
16515
+ }
16232
16516
  }
16233
16517
  let view = { kind: "full" };
16234
16518
  if (opts.aspect !== void 0) {
@@ -16274,9 +16558,9 @@ function registerCheckCommand(program2) {
16274
16558
  gitTrackedFiles: gitFiles,
16275
16559
  onlyDeterministic: mode.onlyDeterministic,
16276
16560
  dryRun: isDryRun,
16277
- write: isQuiet ? () => {
16278
- } : isDryRun ? (s) => {
16561
+ write: isDryRun ? (s) => {
16279
16562
  process.stdout.write(s);
16563
+ } : isQuiet ? () => {
16280
16564
  } : (s) => {
16281
16565
  process.stderr.write(s);
16282
16566
  },
@@ -16375,9 +16659,9 @@ function formatOutput(result, view = { kind: "full" }, autoFilled = false, emoji
16375
16659
  const nextCmd = firstFiltered.messageData.next.split("\n")[0];
16376
16660
  sections.push("");
16377
16661
  sections.push(`Next (this group): ${nextCmd}`);
16662
+ sections.push("");
16663
+ return sections.join("\n");
16378
16664
  }
16379
- sections.push("");
16380
- return sections.join("\n");
16381
16665
  } else if (view.kind === "details") {
16382
16666
  if (errors.length > 0) {
16383
16667
  sections.push("");
@@ -16618,12 +16902,23 @@ function glossLabel(label) {
16618
16902
  function renderGroup(group, lines, opts) {
16619
16903
  const aspectSeg = group.aspectId ? ` aspect '${group.aspectId}'` : "";
16620
16904
  lines.push(` ${glossLabel(group.label)} ${group.pairCount} pairs ${group.nodeCount} nodes${aspectSeg}`);
16621
- if (group.sharedWhy) lines.push(`${BLOCK_INDENT}${group.sharedWhy}`);
16622
- if (group.sharedNext) {
16905
+ if (group.sharedWhy && !group.divergentWhy) lines.push(`${BLOCK_INDENT}${group.sharedWhy}`);
16906
+ if (group.sharedNext && !group.divergentNext) {
16623
16907
  const nextLines = group.sharedNext.split("\n");
16624
16908
  lines.push(`${BLOCK_INDENT}Fix: ${nextLines[0]}`);
16625
16909
  for (const extra of nextLines.slice(1)) lines.push(`${BLOCK_INDENT}${extra}`);
16626
16910
  }
16911
+ const MEMBER_DETAIL_INDENT = `${BLOCK_INDENT} `;
16912
+ const emitDivergentDetail = (m) => {
16913
+ if (group.divergentWhy && m.messageData.why) {
16914
+ lines.push(`${MEMBER_DETAIL_INDENT}Why: ${m.messageData.why.split("\n")[0]}`);
16915
+ }
16916
+ if (group.divergentNext && m.messageData.next) {
16917
+ const nextLines = m.messageData.next.split("\n");
16918
+ lines.push(`${MEMBER_DETAIL_INDENT}Fix: ${nextLines[0]}`);
16919
+ for (const extra of nextLines.slice(1)) lines.push(`${MEMBER_DETAIL_INDENT}${extra}`);
16920
+ }
16921
+ };
16627
16922
  const members = group.members;
16628
16923
  const truncate = opts.isTTY && members.length > CAP_NODES2;
16629
16924
  const shown = truncate ? members.slice(0, CAP_NODES2) : members;
@@ -16639,10 +16934,12 @@ function renderGroup(group, lines, opts) {
16639
16934
  lines.push(`${BLOCK_INDENT} ${extra}`);
16640
16935
  }
16641
16936
  }
16937
+ emitDivergentDetail(m);
16642
16938
  } else {
16643
16939
  const memberAspectSeg = group.aspectId === void 0 && m.aspectId !== void 0 ? ` aspect '${m.aspectId}'` : "";
16644
16940
  const whatSeg = !memberAspectSeg && m.messageData.what ? ` ${m.messageData.what.split("\n")[0]}` : "";
16645
16941
  lines.push(`${BLOCK_INDENT}- ${node}${memberAspectSeg || whatSeg}`);
16942
+ emitDivergentDetail(m);
16646
16943
  }
16647
16944
  }
16648
16945
  if (truncate) {
@@ -16652,10 +16949,10 @@ function renderGroup(group, lines, opts) {
16652
16949
  }
16653
16950
 
16654
16951
  // src/cli/aspect-test.ts
16655
- import path50 from "path";
16952
+ import path49 from "path";
16656
16953
 
16657
16954
  // src/ast/runner.ts
16658
- import path49 from "path";
16955
+ import path48 from "path";
16659
16956
  import { readFile as readFile16 } from "fs/promises";
16660
16957
  import { pathToFileURL as pathToFileURL3 } from "url";
16661
16958
  var AstRunnerError = class extends Error {
@@ -16672,7 +16969,7 @@ ${data.next}`);
16672
16969
  };
16673
16970
  async function runAstAspect(params) {
16674
16971
  ensureLoaderRegistered();
16675
- const checkPath = path49.resolve(params.projectRoot, params.aspectDir, "check.mjs");
16972
+ const checkPath = path48.resolve(params.projectRoot, params.aspectDir, "check.mjs");
16676
16973
  let mod;
16677
16974
  try {
16678
16975
  mod = await import(pathToFileURL3(checkPath).href);
@@ -16704,8 +17001,8 @@ async function runAstAspect(params) {
16704
17001
  sourceFiles.push({ path: f.path, content: cached.content, ast: cached.ast });
16705
17002
  continue;
16706
17003
  }
16707
- const content20 = await readFile16(path49.resolve(params.projectRoot, f.path), "utf-8");
16708
- if (getLanguageForExtension(path49.extname(f.path).toLowerCase()) === null) {
17004
+ const content20 = await readFile16(path48.resolve(params.projectRoot, f.path), "utf-8");
17005
+ if (getLanguageForExtension(path48.extname(f.path).toLowerCase()) === null) {
16709
17006
  sourceFiles.push({ path: f.path, content: content20, ast: void 0 });
16710
17007
  continue;
16711
17008
  }
@@ -16891,7 +17188,7 @@ function registerAspectTestCommand(program2) {
16891
17188
  process.exit(1);
16892
17189
  return;
16893
17190
  }
16894
- const aspectDir = path50.join(".yggdrasil", "aspects", aspect.id);
17191
+ const aspectDir = path49.join(".yggdrasil", "aspects", aspect.id);
16895
17192
  if (hasNode) {
16896
17193
  const nodePath = opts.node.trim().replace(/\/$/, "");
16897
17194
  const node = graph.nodes.get(nodePath);
@@ -16951,12 +17248,17 @@ function registerAspectTestCommand(program2) {
16951
17248
  await exitAfterFlush(1);
16952
17249
  } catch (e) {
16953
17250
  debugWrite(`[aspect-test] run failed: ${e instanceof Error ? e.message : String(e)}`);
17251
+ if (e instanceof StructureRunnerError || e instanceof AstRunnerError) {
17252
+ process.stderr.write(buildIssueMessage(e.messageData) + "\n");
17253
+ await exitAfterFlush(1);
17254
+ return;
17255
+ }
16954
17256
  abortOnUnexpectedError(e, "running aspect-test");
16955
17257
  }
16956
17258
  });
16957
17259
  }
16958
17260
  async function resolveCompanionsForTest(graph, projectRoot, pair, aspect) {
16959
- const aspectDirAbs = path50.join(projectRoot, ".yggdrasil", "aspects", aspect.id);
17261
+ const aspectDirAbs = path49.join(projectRoot, ".yggdrasil", "aspects", aspect.id);
16960
17262
  const fullMapping = await computeNodeMappedFiles(graph, pair.nodePath);
16961
17263
  const subjectScope = pair.subjectFiles.length < fullMapping.length ? pair.subjectFiles : void 0;
16962
17264
  const run = await runCompanionHook({
@@ -17032,7 +17334,7 @@ async function runLlmAspectTest(graph, projectRoot, aspect, nodePath, dryRun) {
17032
17334
  const refInputs = aspect.references ?? [];
17033
17335
  const referencesForPrompt = [];
17034
17336
  for (const ref of refInputs) {
17035
- const absRef = path50.resolve(projectRoot, ref.path);
17337
+ const absRef = path49.resolve(projectRoot, ref.path);
17036
17338
  let content20;
17037
17339
  try {
17038
17340
  content20 = await readTextFile(absRef);
@@ -17078,7 +17380,7 @@ async function runLlmAspectTest(graph, projectRoot, aspect, nodePath, dryRun) {
17078
17380
  for (const rel of pair.subjectFiles) {
17079
17381
  let content20;
17080
17382
  try {
17081
- const bytes = await import("fs/promises").then((fs5) => fs5.readFile(path50.resolve(projectRoot, rel)));
17383
+ const bytes = await import("fs/promises").then((fs5) => fs5.readFile(path49.resolve(projectRoot, rel)));
17082
17384
  content20 = bytes.toString("utf8");
17083
17385
  } catch (e) {
17084
17386
  debugWrite(`[aspect-test] subject file read failed for ${rel} on ${pair.unitKey}: ${e instanceof Error ? e.message : String(e)}`);
@@ -17134,7 +17436,7 @@ async function runLlmAspectTest(graph, projectRoot, aspect, nodePath, dryRun) {
17134
17436
  for (const rel of pair.subjectFiles) {
17135
17437
  let content20;
17136
17438
  try {
17137
- const bytes = await import("fs/promises").then((fs5) => fs5.readFile(path50.resolve(projectRoot, rel)));
17439
+ const bytes = await import("fs/promises").then((fs5) => fs5.readFile(path49.resolve(projectRoot, rel)));
17138
17440
  content20 = bytes.toString("utf8");
17139
17441
  } catch (e) {
17140
17442
  debugWrite(`[aspect-test] subject file read failed for ${rel} on ${pair.unitKey}: ${e instanceof Error ? e.message : String(e)}`);
@@ -17250,10 +17552,10 @@ function printStructureViolations(violations) {
17250
17552
  // src/cli/log.ts
17251
17553
  import chalk9 from "chalk";
17252
17554
  import { readFile as readFile18, stat as stat7 } from "fs/promises";
17253
- import path54 from "path";
17555
+ import path53 from "path";
17254
17556
 
17255
17557
  // src/core/log/log-add.ts
17256
- import path51 from "path";
17558
+ import path50 from "path";
17257
17559
 
17258
17560
  // src/utils/node-path-validator.ts
17259
17561
  function validateNodePath(raw) {
@@ -17366,7 +17668,7 @@ async function logAdd(input) {
17366
17668
  }
17367
17669
  };
17368
17670
  }
17369
- const logPath2 = path51.join(graph.rootPath, "model", nodePath, "log.md");
17671
+ const logPath2 = path50.join(graph.rootPath, "model", nodePath, "log.md");
17370
17672
  const stats = await statLogFile(logPath2);
17371
17673
  if (stats !== null) {
17372
17674
  if (stats.isSymbolicLink) {
@@ -17435,7 +17737,7 @@ function reasonHasLevel2HeaderOutsideFence(reason) {
17435
17737
  }
17436
17738
 
17437
17739
  // src/core/log/log-read.ts
17438
- import path52 from "path";
17740
+ import path51 from "path";
17439
17741
  var DEFAULT_TOP = 10;
17440
17742
  async function logRead(input) {
17441
17743
  const { graph } = input;
@@ -17481,7 +17783,7 @@ async function logRead(input) {
17481
17783
  }
17482
17784
  };
17483
17785
  }
17484
- const logPath2 = path52.join(graph.rootPath, "model", nodePath, "log.md");
17786
+ const logPath2 = path51.join(graph.rootPath, "model", nodePath, "log.md");
17485
17787
  const content20 = await readLogSafe(logPath2);
17486
17788
  if (content20 === "") {
17487
17789
  return { ok: true, entries: [] };
@@ -17504,8 +17806,8 @@ async function logRead(input) {
17504
17806
  }
17505
17807
 
17506
17808
  // src/core/log/log-merge-resolve.ts
17507
- import { createHash as createHash4 } from "crypto";
17508
- import path53 from "path";
17809
+ import { createHash as createHash7 } from "crypto";
17810
+ import path52 from "path";
17509
17811
 
17510
17812
  // src/utils/git-introspect.ts
17511
17813
  import { exec } from "child_process";
@@ -17585,7 +17887,7 @@ async function logMergeResolve(input) {
17585
17887
  }
17586
17888
  };
17587
17889
  }
17588
- const logPath2 = path53.join(yggRoot, "model", nodePath, "log.md");
17890
+ const logPath2 = path52.join(yggRoot, "model", nodePath, "log.md");
17589
17891
  let currentLog;
17590
17892
  try {
17591
17893
  currentLog = await readTextFile(logPath2);
@@ -17633,7 +17935,7 @@ async function logMergeResolve(input) {
17633
17935
  const p2New = parseLog(parent2Log).slice(ancestorEntries.length);
17634
17936
  const currentEntries = parseLog(currentLog);
17635
17937
  const currentNew = currentEntries.slice(ancestorEntries.length);
17636
- const entryKey = (e) => createHash4("sha256").update(`${e.datetime}
17938
+ const entryKey = (e) => createHash7("sha256").update(`${e.datetime}
17637
17939
  ${e.body}`).digest("hex");
17638
17940
  const parentNew = [...p1New, ...p2New];
17639
17941
  const parentNewKeys = new Set(parentNew.map(entryKey));
@@ -17792,7 +18094,7 @@ ${entry.body}`);
17792
18094
  log2.command("merge-resolve").description("Reconcile log.md after a git merge (HEAD must be a merge commit)").requiredOption("--node <path>", "Node path (relative to .yggdrasil/model/)").action(async (opts) => {
17793
18095
  try {
17794
18096
  const graph = await loadGraphOrAbort(process.cwd(), { tolerateInvalidConfig: true });
17795
- const repoRoot = path54.dirname(graph.rootPath);
18097
+ const repoRoot = path53.dirname(graph.rootPath);
17796
18098
  const nodePath = toPosix(opts.node.trim()).replace(/\/$/, "");
17797
18099
  const result = await logMergeResolve({ graph, nodePath, repoRoot });
17798
18100
  if (!result.ok) {
@@ -17817,15 +18119,15 @@ import chalk10 from "chalk";
17817
18119
 
17818
18120
  // src/io/find-index.ts
17819
18121
  import { readFile as readFile19, lstat as lstat3 } from "fs/promises";
17820
- import path55 from "path";
18122
+ import path54 from "path";
17821
18123
  import MiniSearch from "minisearch";
17822
18124
  var MAX_BODY_BYTES = 1048576;
17823
18125
  async function buildIndex(graph) {
17824
- const projectRoot = path55.dirname(graph.rootPath);
18126
+ const projectRoot = path54.dirname(graph.rootPath);
17825
18127
  const docs = [];
17826
18128
  for (const [nodePath, node] of graph.nodes) {
17827
18129
  const displayPath = `model/${nodePath}/`;
17828
- const logPath2 = path55.join(graph.rootPath, "model", nodePath, "log.md");
18130
+ const logPath2 = path54.join(graph.rootPath, "model", nodePath, "log.md");
17829
18131
  let body = "";
17830
18132
  try {
17831
18133
  const st = await lstat3(logPath2);
@@ -17842,7 +18144,7 @@ This does not affect append-only integrity. No action required.
17842
18144
  }
17843
18145
  body = truncated;
17844
18146
  } else if (st.isSymbolicLink()) {
17845
- process.stderr.write(`Warning: skipping symlinked log.md at ${path55.relative(projectRoot, logPath2)}
18147
+ process.stderr.write(`Warning: skipping symlinked log.md at ${path54.relative(projectRoot, logPath2)}
17846
18148
  `);
17847
18149
  } else {
17848
18150
  }
@@ -18010,18 +18312,18 @@ function registerFindCommand(program2) {
18010
18312
 
18011
18313
  // src/cli/type-suggest.ts
18012
18314
  import chalk11 from "chalk";
18013
- import { existsSync as existsSync8 } from "fs";
18315
+ import { existsSync as existsSync9 } from "fs";
18014
18316
  import { resolve as resolve5 } from "path";
18015
18317
 
18016
18318
  // src/core/type-classifier.ts
18017
- import path56 from "path";
18319
+ import path55 from "path";
18018
18320
  async function classifyFile(absPath, repoRelPath, graph, cache) {
18019
18321
  const matches = [];
18020
18322
  const partialScores = [];
18021
18323
  const ctx = {
18022
18324
  absPath,
18023
18325
  repoRelPath,
18024
- projectRoot: path56.dirname(graph.rootPath),
18326
+ projectRoot: path55.dirname(graph.rootPath),
18025
18327
  cache
18026
18328
  };
18027
18329
  for (const [typeId, def] of Object.entries(graph.architecture.node_types)) {
@@ -18077,7 +18379,7 @@ Type matching does not apply here.
18077
18379
  return;
18078
18380
  }
18079
18381
  const gitignoreStack = await loadRootGitignoreStack(projectRoot);
18080
- if (existsSync8(absPath) && isIgnoredByStack(absPath, gitignoreStack)) {
18382
+ if (existsSync9(absPath) && isIgnoredByStack(absPath, gitignoreStack)) {
18081
18383
  process.stderr.write(
18082
18384
  chalk11.yellow(
18083
18385
  `
@@ -18089,7 +18391,7 @@ file-mapping-gitignored. Proceeding with classification for context.
18089
18391
  )
18090
18392
  );
18091
18393
  }
18092
- if (!existsSync8(absPath)) {
18394
+ if (!existsSync9(absPath)) {
18093
18395
  process.stdout.write(`
18094
18396
  (File does not exist \u2014 evaluating path predicates only)
18095
18397
 
@@ -18317,6 +18619,10 @@ Examples:
18317
18619
  Plain (non-glob) entries remain unchanged: an exact file path or a directory
18318
18620
  prefix (e.g. \`src/handlers\`) covers that file or all files beneath it.
18319
18621
 
18622
+ ## Allowed relations and default policy
18623
+
18624
+ A node type may also set \`relations.default: deny\` to forbid every relation type it does not explicitly list (a sink), open a single relation type to any target with \`['*']\`, or forbid one with an empty list \`[]\`. An omitted \`default\` means allow.
18625
+
18320
18626
  ## log_required \u2014 when to enable the log gate
18321
18627
 
18322
18628
  Each node type may set \`log_required\` (default \`false\`). When \`true\`, a node of
@@ -20552,7 +20858,8 @@ to the \`.yggdrasil/\` directory):
20552
20858
 
20553
20859
  \`\`\`
20554
20860
  yg-secrets.yaml # provider API keys
20555
- .symbols-cache/ # the relation pass's per-language symbol-index cache
20861
+ .symbols-cache/ # the relation pass's legacy per-language symbol-index cache
20862
+ .ast-cache/ # the relation pass's content-addressed per-file AST fact cache
20556
20863
  .debug.log # the opt-in command debug log
20557
20864
  \`\`\`
20558
20865
 
@@ -21231,9 +21538,7 @@ Event relations must be paired: if A emits to B, B must declare a
21231
21538
 
21232
21539
  ## Architecture controls allowed relations
21233
21540
 
21234
- \`yg-architecture.yaml\`'s allowed-relations configuration per type controls
21235
- which relation types may target which node types. The validator rejects
21236
- relations not permitted by the architecture.
21541
+ Architecture controls which relation types are allowed between which node types. By default a relation type is unconstrained \u2014 it may target any node type. A type constrains a relation type by listing its allowed targets (e.g. \`uses: [domain, data-access]\`); the validator then rejects any target not in that list. Three more controls let you lock a type down: \`default: deny\` makes every unlisted relation type target nothing (a sink); an empty list \`uses: []\` forbids that one relation type entirely; and the wildcard \`uses: ['*']\` opens a relation type to any target (useful under \`default: deny\`). An omitted \`default\` means allow, so existing graphs are unaffected.
21237
21542
 
21238
21543
  When a needed relation is not allowed by the architecture:
21239
21544
  1. Use a different relation type that IS allowed
@@ -22243,8 +22548,23 @@ node_types:
22243
22548
 
22244
22549
  parents: [<type-id>, ...] # optional \u2014 allowed parent node types in the hierarchy.
22245
22550
 
22246
- relations: # optional \u2014 allowed relation targets by relation type.
22247
- <relation-type>: [<type-id>, ...] # calls | uses | extends | implements | emits | listens
22551
+ relations: # optional \u2014 per-relation-type allow-list.
22552
+ # A relation type is constrained by listing its allowed target node types.
22553
+ # uses: [domain, data-access] \u2192 only those target types
22554
+ # uses: ['*'] \u2192 any target type
22555
+ # uses: [] \u2192 no target (relation type forbidden)
22556
+ # (relation type omitted) \u2192 governed by \`default\` below
22557
+ # default: allow | deny \u2192 policy for relation types NOT listed.
22558
+ # omitted \u21D2 allow (every unlisted relation type may target any type).
22559
+ # deny \u21D2 unlisted relation types target nothing (a sink).
22560
+ # Note: '*' is reserved as the any-target wildcard in relation lists and
22561
+ # must not be used as a node-type name (the parser rejects it).
22562
+ # Examples:
22563
+ # { default: deny } \u2192 pure sink
22564
+ # { default: deny, listens: ['*'] } \u2192 sink that may listen to anything
22565
+ # { default: allow, uses: [] } \u2192 everything open except \`uses\`
22566
+ uses: [<target-type>, ...]
22567
+ default: allow
22248
22568
  `;
22249
22569
 
22250
22570
  // src/templates/schemas/config.ts
@@ -22423,9 +22743,12 @@ function registerSchemasCommand(program2) {
22423
22743
  }
22424
22744
 
22425
22745
  // src/cli/suppressions.ts
22426
- import chalk14 from "chalk";
22427
- import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
22428
22746
  import path57 from "path";
22747
+
22748
+ // src/portal/api/suppress-scan.ts
22749
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
22750
+ import path56 from "path";
22751
+ import chalk14 from "chalk";
22429
22752
  function isBinaryContent(buf) {
22430
22753
  const checkLen = Math.min(buf.length, 8192);
22431
22754
  for (let i = 0; i < checkLen; i++) {
@@ -22448,7 +22771,7 @@ function isNoiseFile(relFile) {
22448
22771
  return false;
22449
22772
  }
22450
22773
  async function scanMarkersForFile(relFile, text2) {
22451
- const ext = path57.extname(relFile).toLowerCase();
22774
+ const ext = path56.extname(relFile).toLowerCase();
22452
22775
  if (getLanguageForExtension(ext) === null) {
22453
22776
  return scanSuppressionMarkers(text2);
22454
22777
  }
@@ -22470,11 +22793,11 @@ async function runSuppressionsScan(projectRoot, gitTrackedFiles, knownAspectIds)
22470
22793
  const openDisables = /* @__PURE__ */ new Map();
22471
22794
  for (const relFile of gitTrackedFiles) {
22472
22795
  if (isNoiseFile(relFile)) continue;
22473
- const absFile = path57.join(projectRoot, relFile);
22474
- if (!existsSync9(absFile)) continue;
22796
+ const absFile = path56.join(projectRoot, relFile);
22797
+ if (!existsSync10(absFile)) continue;
22475
22798
  let buf;
22476
22799
  try {
22477
- buf = readFileSync8(absFile);
22800
+ buf = readFileSync9(absFile);
22478
22801
  } catch (error) {
22479
22802
  debugWrite(`[suppressions] read fallback: ${relFile}: ${error instanceof Error ? error.message : String(error)}`);
22480
22803
  continue;
@@ -22569,6 +22892,53 @@ function formatSuppressionsOutput(report2) {
22569
22892
  }
22570
22893
  return lines.join("\n") + "\n";
22571
22894
  }
22895
+ function scanPortalSuppressions(report2, knownAspectIds, draftAspectIds) {
22896
+ const unboundedByFile = /* @__PURE__ */ new Map();
22897
+ for (const { file, markers } of report2.fileEntries) {
22898
+ const disableStack = /* @__PURE__ */ new Map();
22899
+ for (const m of markers) {
22900
+ if (m.kind === "disable") {
22901
+ const stack = disableStack.get(m.aspectId) ?? [];
22902
+ stack.push(m.line);
22903
+ disableStack.set(m.aspectId, stack);
22904
+ } else if (m.kind === "enable") {
22905
+ const stack = disableStack.get(m.aspectId);
22906
+ if (stack && stack.length > 0) {
22907
+ stack.pop();
22908
+ if (stack.length === 0) disableStack.delete(m.aspectId);
22909
+ }
22910
+ }
22911
+ }
22912
+ const open = /* @__PURE__ */ new Set();
22913
+ for (const lines of disableStack.values()) for (const l of lines) open.add(l);
22914
+ if (open.size > 0) unboundedByFile.set(file, open);
22915
+ }
22916
+ const out = [];
22917
+ for (const { file, markers } of report2.fileEntries) {
22918
+ const open = unboundedByFile.get(file);
22919
+ for (const m of markers) {
22920
+ if (m.kind === "enable") continue;
22921
+ const risk = resolveRisk(m, file, open, knownAspectIds, draftAspectIds);
22922
+ out.push({
22923
+ file,
22924
+ line: m.line,
22925
+ aspectId: m.aspectId,
22926
+ reason: m.reason,
22927
+ ...risk ? { risk } : {}
22928
+ });
22929
+ }
22930
+ }
22931
+ return out;
22932
+ }
22933
+ function resolveRisk(m, file, openLines, knownAspectIds, draftAspectIds) {
22934
+ if (m.wildcard) return "wildcard";
22935
+ if (!knownAspectIds.has(m.aspectId)) return "typo";
22936
+ if (draftAspectIds.has(m.aspectId)) return "inert";
22937
+ if (m.kind === "disable" && openLines?.has(m.line)) return "unbounded";
22938
+ return void 0;
22939
+ }
22940
+
22941
+ // src/cli/suppressions.ts
22572
22942
  function registerSuppressionsCommand(program2) {
22573
22943
  program2.command("suppressions").description("Inventory active yg-suppress waivers and warn about footguns").action(async () => {
22574
22944
  try {
@@ -22586,14 +22956,1130 @@ function registerSuppressionsCommand(program2) {
22586
22956
  });
22587
22957
  }
22588
22958
 
22589
- // src/bin.ts
22590
- import { readFileSync as readFileSync9 } from "fs";
22959
+ // src/cli/portal.ts
22960
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
22961
+ import path61 from "path";
22962
+
22963
+ // src/portal/engine-api.ts
22964
+ import { readFileSync as readFileSync10, existsSync as existsSync11 } from "fs";
22965
+ import path58 from "path";
22966
+
22967
+ // src/portal/api/boundary.ts
22968
+ var STRUCTURAL_RELATIONS = ["calls", "uses", "extends", "implements"];
22969
+ function isAnyStructuralRelationAllowed(graph, sourceType, targetType) {
22970
+ const typeConfig = graph.architecture?.node_types?.[sourceType];
22971
+ if (!typeConfig) return true;
22972
+ const lists = typeConfig.relations;
22973
+ const def = typeConfig.relationDefault ?? "allow";
22974
+ for (const relType of STRUCTURAL_RELATIONS) {
22975
+ const allowed = lists?.[relType];
22976
+ if (allowed !== void 0) {
22977
+ if (allowed.includes("*") || allowed.includes(targetType)) return true;
22978
+ } else if (def === "allow") {
22979
+ return true;
22980
+ }
22981
+ }
22982
+ return false;
22983
+ }
22984
+ function isLineage(a, b) {
22985
+ return a === b || b.startsWith(a + "/") || a.startsWith(b + "/");
22986
+ }
22987
+ async function computePortalBoundary(graph, projectRoot) {
22988
+ let pass;
22989
+ try {
22990
+ pass = await runRelationPass(graph, projectRoot, {
22991
+ extractorFor: extractorForLanguage,
22992
+ resolvePathToFile: makeResolvePathToFile(projectRoot, buildOwnerIndex(graph.nodes).ownerOf),
22993
+ symbolIndexDir: astCacheDir(graph.rootPath)
22994
+ });
22995
+ } catch {
22996
+ return null;
22997
+ }
22998
+ const phantom = [];
22999
+ const declaredOnly = [];
23000
+ const forbiddenType = [];
23001
+ for (const [nodeId, nv] of pass.violationsByNode) {
23002
+ if (nv.verdict !== "refused") continue;
23003
+ const seen = /* @__PURE__ */ new Set();
23004
+ for (const v of nv.violations) {
23005
+ if (seen.has(v.ownerNode)) continue;
23006
+ seen.add(v.ownerNode);
23007
+ phantom.push({ source: nodeId, target: v.ownerNode });
23008
+ }
23009
+ }
23010
+ const detected = pass.detectedEdgesByNode;
23011
+ for (const [nodeId, node] of graph.nodes) {
23012
+ const sourceType = node.meta.type;
23013
+ const detectedTargets = detected.get(nodeId) ?? /* @__PURE__ */ new Set();
23014
+ for (const rel of node.meta.relations ?? []) {
23015
+ if (!STRUCTURAL_RELATIONS.includes(rel.type)) continue;
23016
+ if (isLineage(nodeId, rel.target)) continue;
23017
+ if (!graph.nodes.has(rel.target)) continue;
23018
+ if (!detectedTargets.has(rel.target)) {
23019
+ declaredOnly.push({ source: nodeId, target: rel.target });
23020
+ }
23021
+ }
23022
+ for (const target of detectedTargets) {
23023
+ const targetNode = graph.nodes.get(target);
23024
+ if (!targetNode) continue;
23025
+ if (!isAnyStructuralRelationAllowed(graph, sourceType, targetNode.meta.type)) {
23026
+ forbiddenType.push({ source: nodeId, target });
23027
+ }
23028
+ }
23029
+ }
23030
+ return { phantom, declaredOnly, forbiddenType };
23031
+ }
23032
+
23033
+ // src/portal/engine-api.ts
23034
+ var PORTAL_SCHEMA_SUPPORTED = CLI_SUPPORTED_SCHEMA;
23035
+ async function loadPortalGraph(projectRoot) {
23036
+ return loadGraphOrAbort(projectRoot, {
23037
+ tolerateInvalidConfig: true,
23038
+ noSecrets: true
23039
+ });
23040
+ }
23041
+ async function walkPortalFiles(projectRoot) {
23042
+ return walkRepoFiles(projectRoot);
23043
+ }
23044
+ async function runPortalCheck(graph, gitFiles) {
23045
+ return runCheck(graph, gitFiles);
23046
+ }
23047
+ function readAndVerifyLock(graph) {
23048
+ const lock = readLock(graph.rootPath);
23049
+ return { lock, verification: verifyLock(graph, lock) };
23050
+ }
23051
+ async function computePortalPairs(graph) {
23052
+ return computeExpectedPairs(graph);
23053
+ }
23054
+ function scanPortalUncovered(graph, gitFiles) {
23055
+ return scanUncoveredFiles(graph, gitFiles);
23056
+ }
23057
+ async function readNodeLog(projectRoot, nodePath) {
23058
+ return readLogContent(projectRoot, nodePath);
23059
+ }
23060
+ function groupPortalIssues(issues) {
23061
+ return groupIssues(issues);
23062
+ }
23063
+ async function computePortalBoundary2(graph, projectRoot) {
23064
+ return computePortalBoundary(graph, projectRoot);
23065
+ }
23066
+ async function scanPortalSuppressions2(graph, projectRoot, gitFiles) {
23067
+ const knownAspectIds = new Set(graph.aspects.map((a) => a.id));
23068
+ const draftAspectIds = new Set(
23069
+ graph.aspects.filter((a) => (a.status ?? "enforced") === "draft").map((a) => a.id)
23070
+ );
23071
+ const report2 = await runSuppressionsScan(projectRoot, gitFiles, knownAspectIds);
23072
+ return scanPortalSuppressions(report2, knownAspectIds, draftAspectIds);
23073
+ }
23074
+ function computePortalLockHash(graph) {
23075
+ return committedLockContentHash(graph.rootPath);
23076
+ }
23077
+ function readGitCommitRef(projectRoot) {
23078
+ const gitDir = path58.join(projectRoot, ".git");
23079
+ const headFile = path58.join(gitDir, "HEAD");
23080
+ if (!existsSync11(headFile)) return null;
23081
+ let head;
23082
+ try {
23083
+ head = readFileSync10(headFile, "utf-8").trim();
23084
+ } catch {
23085
+ return null;
23086
+ }
23087
+ if (/^[0-9a-f]{40}$/i.test(head)) return head;
23088
+ const refMatch = head.match(/^ref:\s*(.+)$/);
23089
+ if (!refMatch) return null;
23090
+ const refName = refMatch[1].trim();
23091
+ const looseRef = path58.join(gitDir, refName);
23092
+ if (existsSync11(looseRef)) {
23093
+ try {
23094
+ const sha = readFileSync10(looseRef, "utf-8").trim();
23095
+ if (/^[0-9a-f]{40}$/i.test(sha)) return sha;
23096
+ } catch {
23097
+ }
23098
+ }
23099
+ const packed = path58.join(gitDir, "packed-refs");
23100
+ if (existsSync11(packed)) {
23101
+ try {
23102
+ const lines = readFileSync10(packed, "utf-8").split("\n");
23103
+ for (const line of lines) {
23104
+ const m = line.match(/^([0-9a-f]{40})\s+(.+)$/i);
23105
+ if (m && m[2].trim() === refName) return m[1];
23106
+ }
23107
+ } catch {
23108
+ return null;
23109
+ }
23110
+ }
23111
+ return null;
23112
+ }
23113
+ async function computePortalFreshness(graph, lock) {
23114
+ const out = [];
23115
+ for (const nodePath of graph.nodes.keys()) {
23116
+ const stored = lock.nodes[nodePath]?.source;
23117
+ if (stored === void 0) {
23118
+ out.push({ nodePath, sourceChanged: false });
23119
+ continue;
23120
+ }
23121
+ let fingerprint;
23122
+ try {
23123
+ fingerprint = await computeSourceFingerprint(graph, nodePath);
23124
+ } catch {
23125
+ out.push({ nodePath, sourceChanged: true });
23126
+ continue;
23127
+ }
23128
+ if (fingerprint === void 0) {
23129
+ out.push({ nodePath, sourceChanged: false });
23130
+ continue;
23131
+ }
23132
+ out.push({ nodePath, sourceChanged: fingerprint !== stored });
23133
+ }
23134
+ return out;
23135
+ }
23136
+
23137
+ // src/portal/derive-node-state.ts
23138
+ var PAIR_RANK = {
23139
+ refused: 4,
23140
+ unverified: 3,
23141
+ warning: 2,
23142
+ verified: 1,
23143
+ "n/a": 0
23144
+ };
23145
+ function worstPairState(states) {
23146
+ return states.reduce((worst, s) => PAIR_RANK[s] > PAIR_RANK[worst] ? s : worst, "verified");
23147
+ }
23148
+ var STATE_RANK = {
23149
+ refused: 4,
23150
+ unverified: 3,
23151
+ warning: 2,
23152
+ verified: 1,
23153
+ "no-rule": 0
23154
+ };
23155
+ function computeOwnState(checked, effectiveAspects, nodeIssues, fresh) {
23156
+ let worst = checked ? "verified" : "no-rule";
23157
+ if (checked) {
23158
+ for (const ea of effectiveAspects) {
23159
+ if (ea.pairState === "refused" && STATE_RANK["refused"] > STATE_RANK[worst]) worst = "refused";
23160
+ else if (ea.pairState === "unverified" && STATE_RANK["unverified"] > STATE_RANK[worst]) worst = "unverified";
23161
+ else if (ea.pairState === "warning" && STATE_RANK["warning"] > STATE_RANK[worst]) worst = "warning";
23162
+ }
23163
+ const hasWarning = nodeIssues.some((i) => i.severity === "warning");
23164
+ if (hasWarning && STATE_RANK["warning"] > STATE_RANK[worst]) worst = "warning";
23165
+ }
23166
+ if (fresh && STATE_RANK["unverified"] > STATE_RANK[worst]) worst = "unverified";
23167
+ return worst;
23168
+ }
23169
+ function computeRollup(node, built) {
23170
+ let worst = built.get(node.path).state;
23171
+ for (const child of node.children) {
23172
+ const childRollup = built.get(child.path)?.rollupState ?? "no-rule";
23173
+ if (STATE_RANK[childRollup] > STATE_RANK[worst]) worst = childRollup;
23174
+ }
23175
+ return worst;
23176
+ }
23177
+ function collectNodeSuppressions(mapping, suppressions) {
23178
+ const out = [];
23179
+ const owned = new Set(mapping);
23180
+ for (const file of owned) {
23181
+ const hits = suppressions.byFile.get(file);
23182
+ if (hits) out.push(...hits);
23183
+ }
23184
+ return out;
23185
+ }
23186
+ function computeNotApplicable(node, graph, effectiveIds) {
23187
+ const attachedIds = /* @__PURE__ */ new Set();
23188
+ for (const id of node.meta.aspects ?? []) attachedIds.add(id);
23189
+ const typeDef = graph.architecture?.node_types?.[node.meta.type];
23190
+ for (const id of typeDef?.aspects ?? []) attachedIds.add(id);
23191
+ const out = [];
23192
+ for (const id of [...attachedIds].sort()) {
23193
+ if (effectiveIds.has(id)) continue;
23194
+ out.push({ aspectId: id, why: "filtered out by a `when` predicate on this node" });
23195
+ }
23196
+ return out;
23197
+ }
23198
+
23199
+ // src/portal/derive-nodes.ts
23200
+ function displayPairState(rawKind, status) {
23201
+ if (rawKind === "verified") return "verified";
23202
+ if (rawKind === "refused") return status === "advisory" ? "warning" : "refused";
23203
+ return "unverified";
23204
+ }
23205
+ function buildPortalNodes(graph, _lock, verification, check, logContents, suppressions, freshByNode = /* @__PURE__ */ new Map()) {
23206
+ const pairsByNode = indexPairsByNode(verification.pairs);
23207
+ const issuesByNode = indexIssuesByNode(check.issues);
23208
+ const relationsIn = invertRelations(graph);
23209
+ const built = /* @__PURE__ */ new Map();
23210
+ for (const [path62, node] of graph.nodes) {
23211
+ built.set(
23212
+ path62,
23213
+ buildOne(
23214
+ node,
23215
+ graph,
23216
+ pairsByNode,
23217
+ issuesByNode,
23218
+ relationsIn,
23219
+ logContents,
23220
+ suppressions,
23221
+ freshByNode.get(path62) === true
23222
+ )
23223
+ );
23224
+ }
23225
+ for (const [path62, node] of graph.nodes) {
23226
+ const portal = built.get(path62);
23227
+ portal.rollupState = computeRollup(node, built);
23228
+ }
23229
+ return [...built.values()];
23230
+ }
23231
+ function indexPairsByNode(pairs) {
23232
+ const out = /* @__PURE__ */ new Map();
23233
+ for (const vp of pairs) {
23234
+ const np = vp.pair.nodePath;
23235
+ let byAspect = out.get(np);
23236
+ if (!byAspect) {
23237
+ byAspect = /* @__PURE__ */ new Map();
23238
+ out.set(np, byAspect);
23239
+ }
23240
+ const list = byAspect.get(vp.pair.aspectId) ?? [];
23241
+ list.push(toPairRecord(vp));
23242
+ byAspect.set(vp.pair.aspectId, list);
23243
+ }
23244
+ return out;
23245
+ }
23246
+ function toPairRecord(vp) {
23247
+ const subjectFiles = vp.pair.subjectFiles ?? [];
23248
+ const state = displayPairState(vp.state.kind, vp.pair.status);
23249
+ if (vp.state.kind === "refused") {
23250
+ return { state, subjectFiles, reason: vp.state.reason };
23251
+ }
23252
+ return { state, subjectFiles };
23253
+ }
23254
+ function indexIssuesByNode(issues) {
23255
+ const out = /* @__PURE__ */ new Map();
23256
+ for (const i of issues) {
23257
+ if (!i.nodePath) continue;
23258
+ const list = out.get(i.nodePath) ?? [];
23259
+ list.push(i);
23260
+ out.set(i.nodePath, list);
23261
+ }
23262
+ return out;
23263
+ }
23264
+ function invertRelations(graph) {
23265
+ const out = /* @__PURE__ */ new Map();
23266
+ for (const [path62, node] of graph.nodes) {
23267
+ for (const rel of node.meta.relations ?? []) {
23268
+ const list = out.get(rel.target) ?? [];
23269
+ list.push({ source: path62, type: rel.type });
23270
+ out.set(rel.target, list);
23271
+ }
23272
+ }
23273
+ return out;
23274
+ }
23275
+ function buildOne(node, graph, pairsByNode, issuesByNode, relationsIn, logContents, suppressions, fresh) {
23276
+ const path62 = node.path;
23277
+ const mapping = node.meta.mapping ?? [];
23278
+ const effectiveIds = computeEffectiveAspects(node, graph);
23279
+ const statuses = computeEffectiveAspectStatuses(node, graph);
23280
+ const nodePairs = pairsByNode.get(path62);
23281
+ const effectiveAspects = [];
23282
+ for (const aspectId of [...effectiveIds].sort()) {
23283
+ effectiveAspects.push(
23284
+ buildEffectiveAspect(aspectId, node, graph, statuses, nodePairs)
23285
+ );
23286
+ }
23287
+ const checked = effectiveAspects.some((ea) => ea.pairState !== "n/a");
23288
+ const ownState = computeOwnState(
23289
+ checked,
23290
+ effectiveAspects,
23291
+ issuesByNode.get(path62) ?? [],
23292
+ fresh
23293
+ );
23294
+ const relationsOut = (node.meta.relations ?? []).map((r) => ({
23295
+ target: r.target,
23296
+ type: r.type,
23297
+ ...r.consumes ? { consumes: r.consumes } : {}
23298
+ }));
23299
+ const log2 = parseLog(logContents.get(path62) ?? "").map((e) => ({
23300
+ when: e.datetime,
23301
+ body: e.body
23302
+ }));
23303
+ const nodeSuppressions = collectNodeSuppressions(mapping, suppressions);
23304
+ const notApplicable = computeNotApplicable(node, graph, effectiveIds);
23305
+ return {
23306
+ path: path62,
23307
+ name: node.meta.name,
23308
+ type: node.meta.type,
23309
+ ...node.meta.description ? { description: node.meta.description } : {},
23310
+ parent: node.parent ? node.parent.path : null,
23311
+ mapping,
23312
+ sourceFileCount: mapping.length,
23313
+ isTest: node.meta.type === "test-suite",
23314
+ checked,
23315
+ fresh,
23316
+ state: ownState,
23317
+ // Provisional — overwritten by the bottom-up roll-up pass; seeded to own state.
23318
+ rollupState: ownState,
23319
+ effectiveAspects,
23320
+ notApplicable,
23321
+ relationsOut,
23322
+ relationsIn: relationsIn.get(path62) ?? [],
23323
+ suppressions: nodeSuppressions,
23324
+ log: log2
23325
+ };
23326
+ }
23327
+ function buildEffectiveAspect(aspectId, node, graph, statuses, nodePairs) {
23328
+ const def = graph.aspects.find((a) => a.id === aspectId);
23329
+ const aggregate = isAggregateAspect(graph, aspectId);
23330
+ const kind = aggregate ? "aggregate" : def?.reviewer.type === "deterministic" ? "deterministic" : "llm";
23331
+ const sources = getAspectStatusSources(node, aspectId, graph);
23332
+ const primary = sources.length > 0 ? sources.reduce((a, b) => a.channel <= b.channel ? a : b) : void 0;
23333
+ const channel = primary?.channel ?? 7;
23334
+ const origin = primary?.origin ?? impliedOrigin(graph, aspectId);
23335
+ const status = statuses.get(aspectId) ?? "enforced";
23336
+ let tier;
23337
+ let consensus;
23338
+ let cost = "free";
23339
+ if (kind === "llm" && def) {
23340
+ cost = "billed";
23341
+ const reviewer = graph.config?.reviewer;
23342
+ if (reviewer) {
23343
+ const sel = selectTierForAspect(def, reviewer);
23344
+ if (sel.ok) {
23345
+ tier = sel.tierName;
23346
+ consensus = sel.tier.consensus;
23347
+ }
23348
+ }
23349
+ }
23350
+ const records = nodePairs?.get(aspectId);
23351
+ const pairState = aggregate || status === "draft" || !records || records.length === 0 ? "n/a" : worstPairState(records.map((r) => r.state));
23352
+ let foldedInputs;
23353
+ let reason;
23354
+ if (!aggregate && status !== "draft" && records && records.length > 0) {
23355
+ if (pairState === "verified") {
23356
+ const files = /* @__PURE__ */ new Set();
23357
+ for (const r of records) for (const f of r.subjectFiles) files.add(f);
23358
+ const sorted = [...files].sort();
23359
+ if (sorted.length > 0) foldedInputs = sorted;
23360
+ } else if (pairState === "refused" || pairState === "warning") {
23361
+ reason = records.find((r) => r.reason)?.reason;
23362
+ }
23363
+ }
23364
+ return {
23365
+ aspectId,
23366
+ kind,
23367
+ ...tier !== void 0 ? { tier } : {},
23368
+ ...consensus !== void 0 ? { consensus } : {},
23369
+ cost,
23370
+ status,
23371
+ channel,
23372
+ origin,
23373
+ pairState,
23374
+ ...reason !== void 0 ? { reason } : {},
23375
+ ...foldedInputs !== void 0 ? { foldedInputs } : {}
23376
+ };
23377
+ }
23378
+ function impliedOrigin(graph, aspectId) {
23379
+ for (const other of graph.aspects) {
23380
+ if (other.implies?.includes(aspectId)) return `implied:${other.id}`;
23381
+ }
23382
+ return "unknown";
23383
+ }
23384
+
23385
+ // src/portal/derive-catalogue.ts
23386
+ function buildAspects(graph, pairs) {
23387
+ const byAspect = /* @__PURE__ */ new Map();
23388
+ for (const vp of pairs) {
23389
+ const list = byAspect.get(vp.pair.aspectId) ?? [];
23390
+ list.push(collapsePairState(vp));
23391
+ byAspect.set(vp.pair.aspectId, list);
23392
+ }
23393
+ return graph.aspects.map((def) => buildAspect(def, graph, byAspect.get(def.id) ?? [])).sort((a, b) => a.id.localeCompare(b.id, "en"));
23394
+ }
23395
+ function collapsePairState(vp) {
23396
+ return displayPairState(vp.state.kind, vp.pair.status);
23397
+ }
23398
+ function buildAspect(def, graph, states) {
23399
+ const kind = def.reviewer.type;
23400
+ const status = def.status ?? "enforced";
23401
+ const scope = def.scope?.per === "file" ? "file" : "node";
23402
+ const ruleProse = def.artifacts.find((a) => a.filename === "content.md")?.content;
23403
+ const checkSource = def.artifacts.find((a) => a.filename === "check.mjs")?.content;
23404
+ return {
23405
+ id: def.id,
23406
+ name: def.name,
23407
+ kind,
23408
+ status,
23409
+ scope,
23410
+ hasWhen: def.when !== void 0,
23411
+ implies: def.implies ?? [],
23412
+ // Optional content fields assigned directly — an undefined value is omitted by the
23413
+ // JSON.stringify embed, so no conditional spread is needed (and none is left half-covered).
23414
+ description: def.description,
23415
+ ruleProse,
23416
+ checkSource,
23417
+ tally: buildTally(def, graph, status, states)
23418
+ };
23419
+ }
23420
+ function buildTally(def, graph, status, states) {
23421
+ if (def.reviewer.type === "aggregate") {
23422
+ return { render: "aggregate" };
23423
+ }
23424
+ if (states.length === 0) {
23425
+ return { render: "vacuous", reason: vacuousReason(def, graph, status) };
23426
+ }
23427
+ let verified = 0;
23428
+ let refused = 0;
23429
+ let warning = 0;
23430
+ let unverified = 0;
23431
+ for (const s of states) {
23432
+ if (s === "verified") verified += 1;
23433
+ else if (s === "refused") refused += 1;
23434
+ else if (s === "warning") warning += 1;
23435
+ else unverified += 1;
23436
+ }
23437
+ return { render: "normal", verified, refused, warning, unverified, units: states.length };
23438
+ }
23439
+ function vacuousReason(def, graph, status) {
23440
+ if ((status ?? "enforced") === "draft") {
23441
+ return "draft \u2014 produces no expected pairs (parked, not yet enforced)";
23442
+ }
23443
+ const reachesANode = [...graph.nodes.values()].some(
23444
+ (n) => computeEffectiveAspects(n, graph).has(def.id)
23445
+ );
23446
+ if (!reachesANode) {
23447
+ return "not effective on any node \u2014 no attach channel reaches a node";
23448
+ }
23449
+ return "effective on a node but every subject set is empty (scope/when excludes all files, or no mapped source)";
23450
+ }
23451
+ function buildFlows(graph, nodeStateOf) {
23452
+ return graph.flows.map((flow) => buildFlow(flow, graph, nodeStateOf)).sort((a, b) => a.name.localeCompare(b.name, "en"));
23453
+ }
23454
+ function buildFlow(flow, graph, nodeStateOf) {
23455
+ const participants = expandParticipants(flow, graph);
23456
+ const state = computeFlowState(participants, graph, nodeStateOf);
23457
+ return {
23458
+ name: flow.name,
23459
+ ...flow.description ? { description: flow.description } : {},
23460
+ participants,
23461
+ aspects: flow.aspects ?? [],
23462
+ state
23463
+ };
23464
+ }
23465
+ function expandParticipants(flow, graph) {
23466
+ const out = /* @__PURE__ */ new Set();
23467
+ for (const path62 of flow.nodes) {
23468
+ out.add(path62);
23469
+ const node = graph.nodes.get(path62);
23470
+ if (node) {
23471
+ for (const d of collectDescendants(node)) out.add(d.path);
23472
+ }
23473
+ }
23474
+ return [...out].sort((a, b) => a.localeCompare(b, "en"));
23475
+ }
23476
+ function computeFlowState(participants, graph, nodeStateOf) {
23477
+ let anyChecked = false;
23478
+ let anyAttention = false;
23479
+ for (const path62 of participants) {
23480
+ const node = graph.nodes.get(path62);
23481
+ if (!node) continue;
23482
+ if (!hasNonDraftEffectiveAspects(node, graph)) continue;
23483
+ anyChecked = true;
23484
+ const states = nodeStateOf(path62) ?? [];
23485
+ if (states.some((s) => s === "refused" || s === "unverified")) anyAttention = true;
23486
+ }
23487
+ if (!anyChecked) return "nothing-checked";
23488
+ return anyAttention ? "attention" : "verified";
23489
+ }
23490
+ function buildTypes(graph) {
23491
+ const countByType = /* @__PURE__ */ new Map();
23492
+ for (const node of graph.nodes.values()) {
23493
+ countByType.set(node.meta.type, (countByType.get(node.meta.type) ?? 0) + 1);
23494
+ }
23495
+ const types = [];
23496
+ for (const [id, def] of Object.entries(graph.architecture?.node_types ?? {})) {
23497
+ const allowedRelations = {};
23498
+ for (const [relType, targets] of Object.entries(def.relations ?? {})) {
23499
+ allowedRelations[relType] = [...targets ?? []];
23500
+ }
23501
+ types.push({
23502
+ id,
23503
+ ...def.description ? { description: def.description } : {},
23504
+ parents: def.parents ?? [],
23505
+ allowedRelations,
23506
+ defaultAspects: def.aspects ?? [],
23507
+ strict: def.enforce === "strict",
23508
+ logRequired: def.log_required === true,
23509
+ nodeCount: countByType.get(id) ?? 0
23510
+ });
23511
+ }
23512
+ return types.sort((a, b) => a.id.localeCompare(b.id, "en"));
23513
+ }
23514
+
23515
+ // src/portal/derive-rest.ts
23516
+ function buildBoundary(input) {
23517
+ if (input === null) {
23518
+ return { phantom: [], declaredOnly: [], forbiddenType: [], unknown: true };
23519
+ }
23520
+ return {
23521
+ phantom: dedupeEdges(input.phantom),
23522
+ declaredOnly: dedupeEdges(input.declaredOnly),
23523
+ forbiddenType: dedupeEdges(input.forbiddenType),
23524
+ unknown: false
23525
+ };
23526
+ }
23527
+ function dedupeEdges(edges) {
23528
+ const seen = /* @__PURE__ */ new Set();
23529
+ const out = [];
23530
+ for (const e of edges) {
23531
+ const key = `${e.source}\0${e.target}`;
23532
+ if (seen.has(key)) continue;
23533
+ seen.add(key);
23534
+ out.push(e);
23535
+ }
23536
+ return out.sort((a, b) => a.source.localeCompare(b.source, "en") || a.target.localeCompare(b.target, "en"));
23537
+ }
23538
+ function buildSuppressions(markers) {
23539
+ return markers.map((m) => ({
23540
+ aspectId: m.aspectId,
23541
+ file: m.file,
23542
+ line: m.line,
23543
+ reason: m.reason,
23544
+ ...m.risk ? { risk: m.risk } : {}
23545
+ })).sort((a, b) => a.file.localeCompare(b.file, "en") || a.line - b.line);
23546
+ }
23547
+ function buildHubs(nodes) {
23548
+ const fanOut = [];
23549
+ const fanIn = [];
23550
+ for (const n of nodes) {
23551
+ if (n.relationsOut.length > 0) fanOut.push({ path: n.path, count: n.relationsOut.length });
23552
+ if (n.relationsIn.length > 0) fanIn.push({ path: n.path, count: n.relationsIn.length });
23553
+ }
23554
+ return { fanOut: rankHubs(fanOut), fanIn: rankHubs(fanIn) };
23555
+ }
23556
+ function rankHubs(entries) {
23557
+ return entries.sort((a, b) => b.count - a.count || a.path.localeCompare(b.path, "en"));
23558
+ }
23559
+ function buildResidue(nodes, uncoveredFiles) {
23560
+ const noRuleNodes = nodes.filter((n) => n.state === "no-rule" && n.mapping.length > 0).map((n) => n.path).sort((a, b) => a.localeCompare(b, "en"));
23561
+ return {
23562
+ noRuleNodes,
23563
+ uncoveredFiles: [...uncoveredFiles].sort((a, b) => a.localeCompare(b, "en"))
23564
+ };
23565
+ }
23566
+ function buildWorklist(check) {
23567
+ const groups = groupPortalIssues(check.issues);
23568
+ return groups.map((g) => ({
23569
+ rule: g.label,
23570
+ severity: g.severity,
23571
+ why: g.sharedWhy,
23572
+ fix: g.sharedNext,
23573
+ nodes: [...new Set(g.members.map((m) => m.nodePath ?? "").filter((p2) => p2.length > 0))].sort(
23574
+ (a, b) => a.localeCompare(b, "en")
23575
+ )
23576
+ }));
23577
+ }
23578
+
23579
+ // src/portal/extract.ts
23580
+ async function extractPortalData(projectRoot, opts) {
23581
+ const graph = await loadPortalGraph(projectRoot);
23582
+ const gitFiles = await walkPortalFiles(projectRoot);
23583
+ const checkResult = await runPortalCheck(graph, gitFiles);
23584
+ const { lock, verification: verificationPromise } = readAndVerifyLock(graph);
23585
+ const verification = await verificationPromise;
23586
+ const expected = await computePortalPairs(graph);
23587
+ const counts = buildCounts(graph, checkResult, verification.pairs, expected.pairs);
23588
+ const logContents = /* @__PURE__ */ new Map();
23589
+ for (const nodePath of graph.nodes.keys()) {
23590
+ logContents.set(nodePath, await readNodeLog(projectRoot, nodePath));
23591
+ }
23592
+ const suppressionMarkers = await scanPortalSuppressions2(graph, projectRoot, gitFiles);
23593
+ const flatSuppressions = buildSuppressions(suppressionMarkers);
23594
+ const suppressions = indexSuppressionsByFile(flatSuppressions);
23595
+ const freshnessMarkers = await computePortalFreshness(graph, lock);
23596
+ const freshByNode = /* @__PURE__ */ new Map();
23597
+ for (const m of freshnessMarkers) freshByNode.set(m.nodePath, m.sourceChanged);
23598
+ const nodes = buildPortalNodes(graph, lock, verification, checkResult, logContents, suppressions, freshByNode);
23599
+ const pairStatesByNode = /* @__PURE__ */ new Map();
23600
+ for (const vp of verification.pairs) {
23601
+ const list = pairStatesByNode.get(vp.pair.nodePath) ?? [];
23602
+ list.push(collapsePairState2(vp));
23603
+ pairStatesByNode.set(vp.pair.nodePath, list);
23604
+ }
23605
+ const aspects = buildAspects(graph, verification.pairs);
23606
+ const flows = buildFlows(graph, (path62) => pairStatesByNode.get(path62));
23607
+ const types = buildTypes(graph);
23608
+ const hubs = buildHubs(nodes);
23609
+ const uncovered = scanPortalUncovered(graph, gitFiles);
23610
+ const residue = buildResidue(nodes, uncovered);
23611
+ const worklist = buildWorklist(checkResult);
23612
+ counts.suppressed = flatSuppressions.length;
23613
+ counts.noRule = residue.noRuleNodes.length;
23614
+ counts.notApplicable = nodes.reduce((sum, n) => sum + n.notApplicable.length, 0);
23615
+ const boundary = buildBoundary(await computePortalBoundary2(graph, projectRoot));
23616
+ const lockHash = computePortalLockHash(graph);
23617
+ const commitRef = readGitCommitRef(projectRoot);
23618
+ const data = {
23619
+ meta: {
23620
+ projectName: checkResult.projectName,
23621
+ generatedAt: "",
23622
+ // stamped below, after generation
23623
+ autoApprove: normalizeAutoApprove(graph.config.auto_approve),
23624
+ writeEnabled: opts.writeEnabled,
23625
+ schemaSupported: PORTAL_SCHEMA_SUPPORTED,
23626
+ lockHash,
23627
+ commitRef,
23628
+ counts
23629
+ },
23630
+ nodes,
23631
+ aspects,
23632
+ flows,
23633
+ types,
23634
+ boundary,
23635
+ suppressions: flatSuppressions,
23636
+ hubs,
23637
+ worklist,
23638
+ residue
23639
+ };
23640
+ data.meta.generatedAt = (/* @__PURE__ */ new Date()).toISOString();
23641
+ return data;
23642
+ }
23643
+ function normalizeAutoApprove(value) {
23644
+ if (value === "deterministic" || value === "full") return value;
23645
+ return "false";
23646
+ }
23647
+ function collapsePairState2(vp) {
23648
+ return displayPairState(vp.state.kind, vp.pair.status);
23649
+ }
23650
+ function indexSuppressionsByFile(flat) {
23651
+ const byFile = /* @__PURE__ */ new Map();
23652
+ for (const s of flat) {
23653
+ const list = byFile.get(s.file) ?? [];
23654
+ list.push(s);
23655
+ byFile.set(s.file, list);
23656
+ }
23657
+ return { byFile };
23658
+ }
23659
+ function buildCounts(graph, check, pairs, expectedPairs) {
23660
+ let verified = 0;
23661
+ let refused = 0;
23662
+ let unverified = 0;
23663
+ let advisoryRefused = 0;
23664
+ for (const vp of pairs) {
23665
+ switch (displayPairState(vp.state.kind, vp.pair.status)) {
23666
+ case "verified":
23667
+ verified += 1;
23668
+ break;
23669
+ case "refused":
23670
+ refused += 1;
23671
+ break;
23672
+ case "warning":
23673
+ advisoryRefused += 1;
23674
+ break;
23675
+ default:
23676
+ unverified += 1;
23677
+ break;
23678
+ }
23679
+ }
23680
+ let pairsLLM = 0;
23681
+ let pairsDet = 0;
23682
+ for (const p2 of expectedPairs) {
23683
+ if (p2.kind === "llm") pairsLLM += 1;
23684
+ else pairsDet += 1;
23685
+ }
23686
+ const errors = check.issues.filter((i) => i.severity === "error").length;
23687
+ const warnings = check.issues.filter((i) => i.severity === "warning").length;
23688
+ return {
23689
+ nodes: graph.nodes.size,
23690
+ aspects: graph.aspects.length,
23691
+ flows: graph.flows.length,
23692
+ pairsTotal: expectedPairs.length,
23693
+ pairsLLM,
23694
+ pairsDet,
23695
+ verified,
23696
+ refused,
23697
+ unverified,
23698
+ advisoryRefused,
23699
+ // The residue-track counts (noRule / notApplicable / suppressed) are NOT part of the
23700
+ // count-parity identity and cannot be computed here — each depends on data derived AFTER
23701
+ // this seam (the built node array, the residue ledger, the suppression inventory). They
23702
+ // are seeded 0 and OVERWRITTEN by the post-pass in extractPortalData once that data exists.
23703
+ // (Never leave them 0: that prints "0 waived / 0 no rule / 0 not applicable" over a list.)
23704
+ noRule: 0,
23705
+ draft: check.draftSkipped,
23706
+ notApplicable: 0,
23707
+ suppressed: 0,
23708
+ uncoveredFiles: check.totalFiles - check.coveredFiles,
23709
+ coveredFiles: check.coveredFiles,
23710
+ totalFiles: check.totalFiles,
23711
+ errors,
23712
+ warnings
23713
+ };
23714
+ }
23715
+
23716
+ // src/portal/serializer.ts
23717
+ import { readFile as readFile20 } from "fs/promises";
23718
+ import path59 from "path";
22591
23719
  import { fileURLToPath as fileURLToPath4 } from "url";
23720
+ import { existsSync as existsSync12 } from "fs";
23721
+ var LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 400 400">
23722
+ <defs>
23723
+ <style>
23724
+ .st0 {
23725
+ fill: #f8d794;
23726
+ fill-rule: evenodd;
23727
+ }
23728
+ </style>
23729
+ </defs>
23730
+ <path class="st0" d="M197.03,1.63c-6.22,3.3-11.49,8.23-13.84,12.96-.35.7-.67,1.32-.73,1.37-.05.06-.92-.42-1.92-1.05-2.2-1.39-6.63-3.27-6.93-2.95-.3.32-2.1,5.23-2.66,7.27-.26.93-.52,1.86-.59,2.07-.07.23-.57-.19-1.26-1.07-1.96-2.48-6.02-4.97-9.39-5.77l-.81-.19.15,3.32c.09,1.85.36,3.99.62,4.83.61,1.96.48,2.01-1.31.59-3.35-2.66-8.96-4.65-14-4.96-2.87-.17-8.36.34-8.65.81-.54.87,4.59,9.19,7.48,12.13l2.16,2.2-.95.59-.95.59-2.16-1c-7.44-3.45-16.8-2.91-25.31,1.45l-.97.5,2.4,2.66c1.32,1.46,3.18,3.34,4.13,4.18l1.73,1.52-2.03.52c-2.99.76-10.14,4.5-10.14,5.31,0,.12.52.69,1.16,1.27l1.16,1.04-1.93-.24c-6.1-.75-15.41,1.23-14.43,3.06.9,1.7,3.27,5.33,4.51,6.9,1.66,2.11,1.64,1.96.27,3.3-1.05,1.02-1.26,1.11-3.87,1.46-10.22,1.4-17.81,7.51-21.74,17.49l-.58,1.47,1.75,1.18c.96.65,2.5,1.75,3.41,2.45l1.66,1.27-.86.77c-.47.42-1.76,1.29-2.85,1.93-8.64,5.04-13.74,12.18-16.51,23.1-1.06,4.17-1.49,3.78,4.08,3.59,3.09-.1,4.79-.06,4.79.12,0,1.5-1.8,4.44-3.18,5.18-8.37,4.51-13.05,11.61-14.44,21.93-.5,3.74-.9,3.41,4.05,3.4,6.23-.02,9.25-.98,15.41-4.89.51-.32.85-.19,3.07,1.2,2.12,1.33,4.65,2.55,7.06,3.39.45.16.26.32-1.03.84-5.8,2.36-9.65,7.15-10.93,13.58-.18.91-.4,1.9-.48,2.19-.15.51-.03.53,2,.35,7.76-.66,13.37-5.48,15.94-13.7l.75-2.39,1.13.08c2.87.21,7.55-.86,11.88-2.7l2.36-1,.88.52c.48.29,2.13,1.18,3.66,1.98l2.78,1.46-2.05.35c-8.87,1.5-14.74,6.32-17.22,14.16-1.07,3.36-1.28,3.16,3.38,3.13,9.3-.07,15.85-4.12,20.15-12.47l1.34-2.6,2.16.17c1.19.09,5.01-.01,8.5-.22,3.49-.21,6.38-.35,6.44-.31.47.4-2.74,3.58-4.67,4.64-5.27,2.88-8.24,7.94-8.24,14,0,2.11.05,2.34.49,2.21.27-.08,1.06-.29,1.76-.47,6.44-1.65,10.98-7.78,10.98-14.82,0-1.87,1.14-3.79,3.1-5.2,1.77-1.29,18.19-1.51,21.69-.29,13.13,4.57,22.36,15.41,23.62,27.73.61,5.93-.8,26.41-2.36,34.39l-.71,3.6-3.73,3.99c-6.67,7.13-4.88,6.03-14.01,8.62-2.25.64-5.52,1.6-7.28,2.14-6.1,1.88-5.54,1.85-31.68,1.79l-23.61-.05-3.01,3c-2.89,2.88-3.05,2.99-3.87,2.75-.74-.21-2.74.16-3.05.57-1.15,1.53-1.32,2.87-.53,4.2,1.63,2.76,5.81,1.63,5.87-1.59.02-1.23.1-1.34,2.87-3.93l2.85-2.67h11.74l-9.57,9.58-9.57,9.58h-16.71l-.74-1.27c-2.6-4.43-9.45-2.67-9.45,2.42s7.31,6.94,9.42,2.26l.52-1.16,10.93.18,10.93.18,8.6-8.51c5.11-5.06,8.99-8.67,9.55-8.91.81-.34,4.71-.37,25.76-.22l24.8.17,10.01-9.8,10.01-9.8v-18.88c0-13.87-.09-19.13-.33-19.81-.29-.8-.26-1,.18-1.5,1.36-1.5,3.48.19,2.25,1.8-.35.47-.41,3.3-.41,19.72v19.18l-10.4,10.2-10.4,10.2h-37.46l-.84.71c-.81.68-.96.7-5.16.71-2.38,0-4.47.1-4.64.21-.18.11-5.48,5.31-11.78,11.54l-11.46,11.34h-15.46v13.48l-.63.2c-3.44,1.07-4.42,5.82-1.72,8.34,2.62,2.45,7.22,1.29,8.35-2.11.08-.25,1.99-.36,7.17-.43l7.06-.09-.14,1.25c-.28,2.47,1.16,1.58,8.12-5.04,5.59-5.31,4.28-4.83,13.63-4.96,6.62-.09,8.05-.18,8.96-.58.7-.31,5.48-4.8,13.71-12.88,15.22-14.94,13.17-13.63,21.47-13.75l6.13-.09v7.26l-2.46,2.37c-1.36,1.3-5.09,4.94-8.3,8.08l-5.83,5.71h-1.7c-1.1,0-1.7-.12-1.7-.33,0-.63-1.45-2.22-2.52-2.77-6.37-3.25-12.07,5.01-6.58,9.53,3.28,2.71,9.36.59,9.39-3.27,0-.59.13-.63,1.9-.64h1.9s7.75-7.59,7.75-7.59c4.26-4.17,7.84-7.59,7.96-7.6.12,0,.21,1.28.21,2.86v2.87l-8.8,8.89c-4.84,4.89-9.18,9.11-9.64,9.38-.78.46-2.27.49-20.2.49h-19.36l-5.93,5.66-5.93,5.66-2.1-2-2.11-2h-3.79c-5.14,0-5.35.16-5.4,4.12-.02,1.81-.15,2.8-.36,2.87-4.33,1.44-3.21,8.33,1.35,8.33,4.2,0,5.49-4.98,2.04-7.87-.95-.79-.95-.8-.79-2.71.21-2.55.12-2.48,3.27-2.48h2.76l1.94,1.71c1.38,1.22,2.01,1.99,2.18,2.68.4,1.59,3,1,3-.68,0-.59,10.18-10.37,11.21-10.77.77-.3,3.09-.32,8.03-.07l3.62.18-10.55,10.59c-13.01,13.06-11.47,10.52-11.47,18.91v6.93l-.96.46c-4.59,2.19-2.93,9.51,2.15,9.51s6.98-7.59,2.13-9.6l-.83-.34.09-5.07.09-5.07,11.11-10.94c8.65-8.52,11.33-11,12.11-11.22,1.1-.3,7.35-.32,13.05-.04l3.59.18-4.86,5.01-4.86,5h-4.45c-2.63,0-4.69-.13-5.02-.31-.51-.27-.54-.25-.26.26.17.31.3.94.3,1.39v.83l2.5-.14c1.38-.08,2.98-.05,3.56.06l1.06.2-1.47,1.49-1.47,1.49-2.63-.14c-2.72-.14-4.2.32-4.86,1.5-.12.21-2.28,2.47-4.79,5.01-4.55,4.6-5.02,5.29-4.08,5.99.44.33,1.08-.22,5.86-4.97l5.37-5.33h3.62l-2.95,2.96-2.95,2.96-.09,14.43-.09,14.43-1.24.76c-3.82,2.34-2.99,7.8,1.35,8.98,5.34,1.44,8.41-5.98,3.71-8.95l-1.25-.79v-11.14c0-14.88-1.29-12.04,10.36-22.92l9.5-8.88.09,5.42.09,5.42-3.68,3.52-3.68,3.52v12.53s0,12.53,0,12.53l-1.28.68c-4.77,2.53-2.65,9.77,2.7,9.25,4.91-.47,6.28-7.03,1.94-9.24l-1.1-.56v-12.04s0-12.03,0-12.03l3.66-3.47,3.66-3.47v-14.22l4.14-3.78c2.28-2.08,6.81-6.38,10.08-9.55s7.74-7.52,9.94-9.65l4.01-3.87v-31.96l6.36-6.37,6.36-6.37-.02-16.95c-.01-11.3-.12-17.13-.32-17.5-.6-1.11.78-2.32,1.93-1.7.78.42.96,1.13.47,1.88-.37.57-.43,2.92-.43,17.78v17.12l-6.51,6.47-6.51,6.48.14,20.98c.07,11.54.03,21.72-.09,22.63-.46,3.31-.69,3.59-14.22,17.09-6.91,6.89-13.07,13.15-13.67,13.9l-1.1,1.36v43.81l-1.32.68c-3.36,1.73-3.52,6.6-.27,8.44,3.77,2.13,8.17-1.01,7.37-5.25-.22-1.17-1.9-3.15-2.87-3.4l-.67-.17v-18.63c0-12.73.1-18.98.31-19.75.35-1.28-.59-.3,19.75-20.27,10.15-9.97,13.89-13.81,14.36-14.77.63-1.27.64-1.56.78-12.43l.14-11.13.76-1.55c.76-1.55,4.55-6.19,5.23-6.41.29-.1.35,3.48.28,18.71l-.08,18.83-.76,1.55c-.63,1.28-2.68,3.44-11.49,12.08-7.6,7.46-10.92,10.9-11.39,11.82-.44.87-2.33,2.9-5.76,6.18l-5.1,4.88v18.62l-.99.96c-2.41,2.34-.3,6.29,2.96,5.58,2.54-.56,3.44-3.74,1.57-5.55l-.96-.93-.2-8.73-.2-8.73,3.06-3c1.68-1.65,3.47-3.34,3.98-3.76l.92-.76v1.93c0,3.52.82,34.54,1,37.84.22,4.13-.11,3.63,7.44,10.94l6.21,6.01v8.57c0,7.08.07,8.57.39,8.57.21,0,.76.07,1.22.17l.83.17.19-9.89c.23-11.76.95-9.73-5.89-16.64-6.45-6.51-5.98-5.44-5.73-12.96.11-3.33.29-10.81.39-16.62.27-15.2.4-16.58,1.67-18.43.9-1.3,8.31-8.61,8.73-8.61.09,0,.17,6.37.17,14.17v14.17l-1.27.34c-7.59,2.03-6.33,13.91,1.55,14.63,8.86.8,11.56-12.07,3.06-14.62l-1.08-.32v-30.76l3.06-3.05,3.06-3.05.2,10.24c.47,24.38.21,22.87,4.71,27.18,1.76,1.68,3.67,3.52,4.25,4.08l1.06,1.02v6.03l-4.08,3.96-4.08,3.95v22.96l-.85.35c-4.98,2.06-3.29,9.58,2.15,9.58s6.84-6.61,2.32-9.46l-1.09-.69v-19.81l4.47-4.53c5.57-5.63,5.87-6.34,5.25-12.14l-.21-1.94-3.77-3.86c-2.08-2.12-3.79-4.09-3.81-4.38-.12-1.48.13-14.91.29-15.48.16-.62.97.09,8.96,7.93,10.8,10.58,10.32,9.84,10.46,16.32l.1,4.49-5.39,5.24-5.39,5.24.04,4.26.04,4.26-1.01,1.01c-2.55,2.55.32,6.74,3.46,5.06,2.12-1.13,2.54-4.19.73-5.37-.68-.45-.69-.52-.69-4.42v-3.96l4.07-4.1c2.24-2.25,4.13-4.03,4.2-3.96.07.07.22,4.38.33,9.57l.2,9.44-5.81,5.91-5.81,5.92v5.85s0,5.85,0,5.85l1.2-.18c.66-.1,1.23-.21,1.27-.24.04-.03.15-1.97.25-4.32l.19-4.27,6.08-5.82c8.21-7.85,7.3-4.98,7.8-24.62.27-10.53-.12-9.88,3.71-6.25l3.17,3.01v7.86s0,7.86,0,7.86l-1.1,1.01c-2.92,2.7-1.45,7.07,2.38,7.07s5.06-5.1,1.92-7.14l-.94-.61v-8.49s0-8.49,0-8.49l-4.42-4.36-4.42-4.36v-4.18c-.03-5.91.71-4.84-12.03-17.35-8.68-8.52-10.83-10.78-11.45-12.04l-.77-1.55-.04-27.25c-.02-14.99.06-27.25.18-27.25.82,0,5.48,5.94,5.86,7.46.16.63.36,8.37.46,17.71l.18,16.58,6.4,6.55,6.4,6.55-.1,5.03-.1,5.03,4.12,3.91,4.12,3.91-.18,1.2c-.47,3.11,2.97,5.03,5.32,2.96,2.35-2.06.69-5.92-2.38-5.54-.99.12-1.14,0-4.84-3.72l-3.82-3.84.11-4.93.11-4.93-6.46-6.48-6.46-6.49v-6.94l1.62,1.42c.89.78,4.79,4.52,8.66,8.29l7.04,6.87-.06,4.63-.06,4.63,7.74,7.73,7.74,7.73v1.42c0,1.68-3.85,5.93-5.98,6.61-2.82.89-2.36,4.52.57,4.52,1.44,0,2.36-.94,2.38-2.44.01-1.02,2.06-3.76,2.81-3.76.59,0,.25,23.05-.34,23.24-4.7,1.49-3.92,8.85,1.02,9.6,5.5.82,7.97-7.04,2.93-9.31l-1.19-.54.1-21.99c.1-21.58.09-22-.46-22.85-.31-.47-6.26-6.48-13.23-13.34-13.28-13.08-13.91-13.77-14.89-16.14-.6-1.46-.6-1.62-.6-23.64v-22.17l-6.41-6.43-6.41-6.43v-17.14c0-15.67-.04-17.19-.47-17.67-.58-.64-.22-2,.57-2.15,1.65-.32,2.64,1,1.76,2.34-.4.62-.45,2.5-.45,17.25v16.56l6.48,6.47,6.48,6.47v32.17l1.76,1.66c.97.91,5.43,5.22,9.93,9.58,4.49,4.36,9.97,9.62,12.18,11.69l4.01,3.77v14.18s3.8,3.59,3.8,3.59l3.8,3.59v23.98s-1.34.7-1.34.7c-3.95,2.08-3.24,7.86,1.11,9.03,5.96,1.6,8.7-6.43,3.23-9.45-.48-.27-.52-1.03-.58-12.79l-.06-12.5-3.73-3.55-3.73-3.55.09-5.44.09-5.44,3.93,3.61c11.08,10.18,14.41,13.42,15.09,14.68l.7,1.31.09,11.7.09,11.71-.98.47c-4.49,2.14-2.95,9.35,2,9.34,5.37-.01,7.19-7.38,2.32-9.4l-.85-.35v-28.97l-2.82-2.82c-3.33-3.33-3.24-3.07-1.07-3.07h1.74l5.03,5c4.76,4.72,5.06,4.97,5.44,4.45.68-.92.79-.77-5.83-7.55l-4.06-4.15h-6.19s-1.53-1.55-1.53-1.55l-1.53-1.55h4.43c3.05,0,4.61.11,5.01.35,1.04.64,1.63-.82.78-1.93-.15-.2-2.42-.38-6.22-.5l-5.99-.19-4.85-4.85-4.85-4.85,2.03-.17c5.1-.44,14.38-.27,15.27.27,1.48.91,21.89,21.25,22.29,22.21.26.63.38,2.27.38,5.41v4.51l-1.1.56c-4.47,2.28-2.62,9.31,2.45,9.31,4.97,0,6.69-7.06,2.27-9.31l-1.11-.57.04-6.9c.03-5.78-.03-7-.39-7.52-.24-.34-5.22-5.47-11.08-11.4l-10.65-10.78,5.49-.07c3.02-.04,5.82-.03,6.22.02.88.1,11.52,10.36,11.52,11.1,0,1.33,2.39,1.8,2.67.53.12-.55.92-1.48,2.43-2.82l2.25-2h2.2c2.85,0,3.13.27,3.13,3.03,0,1.9-.04,2.03-.85,2.61-2.61,1.86-2.2,6.06.72,7.35,5.05,2.24,8.08-5.24,3.16-7.81-.75-.39-.77-.47-.77-2.79s-.03-2.42-.96-3.44l-.96-1.05-3.13-.1c-3.58-.11-3.48-.15-6.33,2.51l-1.83,1.72-.78-.57c-.43-.31-3.06-2.86-5.85-5.66l-5.07-5.09h-19.01c-10.77,0-19.41-.11-19.92-.26-.65-.19-3.5-2.86-10.07-9.45l-9.16-9.19v-2.8c0-1.54.07-2.8.17-2.8s3.67,3.42,7.95,7.6l7.79,7.61h3.71l.33,1.11c1.93,6.43,11.68,4.6,11.25-2.12-.36-5.64-7.91-7.47-10.65-2.59-.59,1.04-.63,1.06-2.27,1.06h-1.68l-8.09-7.95c-4.45-4.37-8.17-8.05-8.26-8.18-.09-.13-.19-1.82-.23-3.77l-.06-3.53,6.29.1c8.59.13,6.53-1.19,21.35,13.61,15.01,14.98,12.43,13.46,23.24,13.68,4.49.09,8.2.19,8.25.21.04.03,1.96,1.87,4.25,4.09,4.27,4.14,8.13,7.3,8.57,7.03.14-.08.2-.64.14-1.25l-.11-1.09h14.17l.34.82c1.23,2.97,5.56,3.78,8,1.49,2.61-2.44,1.78-6.86-1.55-8.24l-.81-.33v-13.45h-15.34l-11.56-11.55-11.56-11.55h-4.76c-4.72,0-4.77,0-5.51-.72l-.75-.72-18.61.02-18.6.02-10.38-10.18-10.38-10.18-.15-23.27c-.08-12.8-.25-23.54-.37-23.87-.48-1.25,1.71-2.15,2.35-.96.23.42.19.77-.13,1.39-.38.73-.43,3.69-.43,23.34v22.51l10.16,9.94,10.16,9.94,25-.18,25-.18,1.24.84c.68.46,4.85,4.39,9.26,8.72l8.03,7.87h11.05s11.05.01,11.05.01l.38.92c1.6,3.83,6.96,3.96,8.85.21,2.84-5.62-5.06-10-8.61-4.77-.47.69-.94,1.01-1.69,1.16-1.31.25-14.32.29-15.51.05-.67-.13-3.12-2.39-10.39-9.57l-9.51-9.39h5.87s5.87-.02,5.87-.02l2.38,2.32c2.11,2.06,2.36,2.41,2.23,3.12-.48,2.47,2.29,4.36,4.62,3.15,3.21-1.66,1.77-5.95-1.93-5.73-1.15.07-1.26-.01-3.78-2.51l-2.6-2.58-24.64-.09-24.64-.09-2.96-.87c-3.88-1.14-13.44-3.94-15.8-4.62-2.01-.58-11.63-9.9-12.15-11.76-1.54-5.6-3.5-31.85-2.79-37.48,1.55-12.43,10.38-22.76,23.28-27.24,4.67-1.62,20.78-1.24,22.61.54,1.95,1.89,2.59,3.28,2.6,5.58,0,4.9,2.51,9.5,6.6,12.15,1.42.92,2.91,1.49,6.18,2.39.5.14.52-.02.39-2.77-.29-6.08-3.19-10.76-8.31-13.43-1.94-1.01-5.08-4.25-4.53-4.68.05-.04,1.74.03,3.76.15,2.01.12,5.78.24,8.37.25l4.71.03,1.19,2.17c4.79,8.76,11.2,12.76,20.57,12.84,4.24.04,4.13.11,3.53-2.39-1.83-7.64-10.52-14.9-17.83-14.9-1.73,0-1.52-.38.92-1.69,1.37-.73,3-1.62,3.62-1.98l1.14-.64,2.8,1.14c3.92,1.6,6.74,2.33,9.88,2.55l2.7.19.91,2.54c2.75,7.67,6.75,11.57,13.28,12.96,4.55.96,4.77.78,3.78-3.12-1.53-6.05-7.85-13.07-11.77-13.07-.21,0-.3-.13-.2-.28.1-.16.37-.28.61-.28.73,0,5.05-2.14,7.11-3.53l1.95-1.31,1.73,1.17c4.7,3.19,10.93,4.72,16.7,4.1l1.89-.2v-1.23c0-8.69-5.86-18.8-13.47-23.22-2.53-1.47-2.66-1.59-3.43-3.29-1.44-3.18-1.86-2.88,3.8-2.7l4.94.16-.18-.96c-2.09-11.08-7.41-19.51-15.88-25.1-1.78-1.18-3.59-2.52-4.03-2.97l-.79-.83,1.36-.98c.75-.54,2.16-1.52,3.13-2.17,2.34-1.57,2.33-1.51.53-5.18-4.61-9.4-10.72-13.86-20.89-15.23-2.77-.37-2.82-.4-3.87-1.57l-1.06-1.19,1.22-1.59c1.44-1.88,3.55-5.14,4.39-6.78l.59-1.17-1.77-.82c-3.6-1.68-8.83-2.31-13.32-1.61l-1.41.22,1.22-1.07c1.44-1.27,1.5-1.18-2.06-3.32-3.32-2-4.79-2.6-8.14-3.36-.81-.18-.77-.23,3.17-4.2,4.68-4.72,4.56-4.34,1.8-5.6-8.6-3.91-16.74-4.24-23.86-.94l-2.14.99-.99-.58-.99-.59,2.09-2.15c2.58-2.65,7.8-10.85,7.8-12.25,0-.36-5.98-.81-8.78-.66-5.93.31-10.16,1.89-15.35,5.7l-.61.45.58-1.78c.38-1.17.62-2.87.72-4.98l.14-3.2-.95.2c-2.99.63-7.56,3.33-9.3,5.5q-1.16,1.44-1.74-.79c-1.01-3.88-2.55-7.26-3.26-7.18-1.06.12-4.7,1.74-6.47,2.88l-1.67,1.08-1.42-2.32c-2.76-4.52-7.38-8.68-13.14-11.83-3.46-1.89-3.06-1.88-6.3-.16M201.93,51.02c2.01,4.18,2.37,5.58,2.37,9.19,0,4.3,1.4,10.35,3.51,15.15l.99,2.25-2.18,2.13c-2.61,2.56-2.1,3.09-4.74-5.01l-1.92-5.9-2.09,6.4c-1.15,3.52-2.19,6.37-2.3,6.32-.12-.04-1.16-.98-2.32-2.09l-2.11-2.01.81-1.65c2.19-4.46,3.57-10.14,3.9-16.02.17-3.16.38-4.58.8-5.63.98-2.43,3.12-6.9,3.3-6.9.1,0,.99,1.7,1.99,3.77M169.17,64.2c3.58,2.29,5.83,6.79,5.84,11.71,0,1.43.09,1.42-2.07.28-4.26-2.25-6.66-6.32-6.66-11.29v-1.94l.77.19c.43.11,1.38.58,2.12,1.05M233.77,64.07c.78,4.14-2,9.34-6.37,11.94-2.33,1.38-2.25,1.4-2.25-.39,0-5.05,2.2-9.34,5.93-11.53,2.23-1.31,2.44-1.31,2.69-.02M221.48,71.47c1.09,3.16,1.07,4.07-.3,20-.65,7.51-1.66,9.2-11.05,18.55l-5.2,5.17-.19-3.63c-1.13-22.16-1.07-22.42,7.29-31.09,3.39-3.51,5.67-6.5,7.61-9.95.88-1.57.98-1.52,1.83.94M180.97,71.57c1.41,2.8,2.38,4.05,6.3,8.09,4.74,4.89,5.88,6.35,7.29,9.28l1.15,2.39-.02,8.87c-.01,4.88-.12,10.24-.24,11.92l-.22,3.04-5.48-5.44c-10.11-10.03-10.35-10.51-11.25-22.06-.92-11.94-.86-13.71.58-17.12.55-1.31.77-1.19,1.89,1.02M158.37,71.92c1,.4,1.14.59,1.4,1.79,1.29,5.99,5.28,10.62,12.19,14.12l2.16,1.09.15,1.49c.09.82.23,2.37.32,3.46.09,1.08.24,2.21.33,2.51.15.51.1.51-.79-.08-.52-.34-2.28-1.33-3.91-2.19-9.03-4.81-11.86-9.03-13.09-19.54-.1-.85-.25-1.9-.33-2.32-.17-.9.03-.94,1.56-.33M243.04,73.61c-.96,10.28-4.25,15.63-12.3,20.04-1.76.96-3.74,2.08-4.41,2.49l-1.21.74.19-2.21c.1-1.21.27-2.96.37-3.89l.18-1.68,1.97-1.07c7.22-3.91,11.08-8.27,12.28-13.83.33-1.54.43-1.69,1.5-2.16,1.66-.74,1.65-.75,1.43,1.58M137.78,89.12c4.83,2.31,7.58,6.79,8.32,13.54.16,1.44.23,1.44-2.84.21-5.73-2.3-8.68-6.37-9.02-12.47l-.14-2.62.81.18c.45.1,1.74.62,2.87,1.17M265.7,90.03c0,6.43-3.37,10.82-10.3,13.43l-1.73.65.13-1.32c.77-7.69,6.15-14.58,11.69-14.97.12,0,.21.98.21,2.21M250.48,90.8c-.41,1.08-.46,3.37-.16,8.04.38,5.95-.66,7.23-9.54,11.75-5.11,2.6-5.02,2.57-4.64,1.57.16-.42.91-3.25,1.67-6.29,2.37-9.53,3.26-10.86,9.6-14.3,3.53-1.92,3.52-1.92,3.08-.76M153.1,91.82c5.83,3.26,6.78,4.7,9.11,13.82.84,3.28,1.61,6.26,1.71,6.63.11.37.08.62-.06.55-13.48-6.38-14.41-7.32-14.07-14.17.25-5.03.21-7-.14-7.92-.37-.99-.08-.9,3.45,1.08M171.49,100.15c4.64,3.5,15.53,15.39,17.21,18.79,1.38,2.8,2.71,12.81,1.71,12.81-.06,0-.59-.46-1.18-1.02-.59-.56-2.67-2.28-4.62-3.83-7.73-6.14-9.61-7.92-11.87-11.24-2.91-4.28-5.89-13.35-5.89-17.9v-.69l1.1.62c.6.34,2.2,1.45,3.55,2.46M232.62,101.42c-1.71,10.77-4.96,15.72-16.35,24.86-2.95,2.36-5.6,4.56-5.89,4.88-1,1.11-1.1.75-.92-3.43.32-7.69,1.63-9.94,12.22-21,4.28-4.47,7.15-6.99,10.09-8.85l1.13-.71.09.7c.05.38-.11,1.98-.36,3.55M148.9,109.41c6.34,4.45,16.25,12.03,16.25,12.43,0,.1-1.3.1-2.89-.01-1.59-.11-4-.12-5.36-.03-2.97.2-2.74.36-5.96-3.99-2.75-3.7-4.35-5.17-6.76-6.22l-1.8-.78.42-.81c.23-.44.49-1.42.58-2.18l.16-1.37,1.53.67c.84.37,2.57,1.4,3.84,2.3M256.42,107.31c0,.5.24,1.43.52,2.06l.52,1.14-1.86.96c-2.24,1.16-3.79,2.65-6.43,6.2-3.12,4.2-2.51,3.9-8.41,4.08-2.78.08-5.28.22-5.55.3-1.77.56,5.22-5.06,15.34-12.33,1.95-1.4,5.19-3.31,5.64-3.32.12,0,.21.41.22.91M281.43,109.13c1.11.19,1.08.75-.13,2.72-3.26,5.27-9.97,7.63-16.39,5.76l-1.23-.36,1.24-1.69c4.1-5.56,9.44-7.64,16.51-6.43M127.4,109.65c3.29,1.04,8.75,5.8,8.75,7.62,0,1.01-7.43,1.32-10.01.41-3.97-1.4-8.29-5.62-8.29-8.1,0-.91,6.61-.87,9.56.06M139.57,122.07c2.84,2.46,5.63,3.35,13.12,4.17,2.21.24,5.82.64,8.03.88,8.28.9,11.79,2.47,17.2,7.7,4.67,4.51,5.66,6.65,5.76,12.43l.08,4.65,8.44,8.31,8.44,8.31.19,34.45.18,34.45,3.22,3.01c1.77,1.66,3.55,3.1,3.94,3.21,1.28.35,1.62,1.72.64,2.61-.78.71-1.92.38-2.15-.63-.11-.47-1.55-2.16-3.65-4.29l-3.47-3.52-.14-8.87c-.08-4.88-.15-16.68-.15-26.23l-.02-17.35-6.48-6.12-6.48-6.12v-6.83l-5.14-5.14c-3.41-3.41-5.34-5.14-5.73-5.14-.88,0-1.63-.96-1.41-1.81.37-1.48,2.21-1.52,2.58-.05.09.36,2.63,3.11,5.64,6.1l5.47,5.45v6.77l1.66,1.56c.91.86,3.48,3.27,5.7,5.36l4.04,3.8.08-6.93.07-6.93-8.24-8.18c-11.45-11.36-10.86-10.93-19.51-14.16-15.71-5.86-34.99-16.39-43.33-23.67-.49-.43,6.61-2.46,8.73-2.5,1.01-.02,1.39.16,2.68,1.28M266.55,121.2c2.1.49,5.4,1.52,5.69,1.78.25.23-2.24,2.07-7.96,5.88-11.08,7.39-20.66,12.25-36.61,18.59-9.84,3.91-8.98,3.38-14.91,9.24l-5.21,5.15.17,9.1c.09,5.01.27,9.51.38,10.01.19.81.13.94-.54,1.25-1.09.5-1.73.02-1.6-1.21.05-.52.15-4.64.22-9.16l.12-8.21-3.39-3.23-3.39-3.23v-12.75l-.84-.44c-2.88-1.49-2.16-6.1,1.01-6.53,3.78-.51,5.39,5.39,1.79,6.53-.52.17-.55.47-.55,6.33v6.15l2.53,2.36,2.53,2.36,5.01-4.99,5.01-4.99.15-4.17c.17-4.71.31-5.14,2.83-8.56,1.55-2.1,5.69-6.19,7.58-7.49,4.1-2.81,6.07-3.28,19.15-4.62,9.34-.95,11.27-1.51,14.66-4.22,2-1.6,2.88-1.73,6.19-.97M127.7,130.06c3.48,1.17,7.63,4.02,15.5,10.61,2.41,2.02,4.98,4.13,5.72,4.7.74.57,1.28,1.03,1.2,1.03s-3.91.38-8.51.85c-4.6.47-8.38.82-8.4.77-3.14-8-8.34-11.85-16.02-11.86-3.89,0-3.91.02-2.53,2.96,2.5,5.33,8.2,9.11,13.99,9.28l1.15.03-1.41.27c-3.07.59-10.78.43-13.19-.27-4.65-1.35-6.04-2.26-11.48-7.59l-4.96-4.85,1.02-.16c3.21-.49,4.96-1.18,9.61-3.8,2.18-1.23,2.61-1.36,4.79-1.52,1.32-.1,3.41-.42,4.65-.73,2.85-.7,6.3-.59,8.88.28M281.9,129.92c.77.25,2.55.55,3.95.67,2.47.21,2.66.28,6.48,2.36,3.6,1.96,6.41,3.02,8.01,3.02.87,0,.91-.05-4.22,4.93-7.2,6.99-9.97,8.18-18.87,8.12-3.15-.02-5.79-.16-6.2-.33-.64-.27-.59-.3.56-.32,5.53-.1,10.86-3.49,13.38-8.52,1.86-3.72,1.78-3.87-1.97-3.87-7.01,0-12.25,3.38-15.3,9.85-.55,1.16-1.02,2.15-1.05,2.19-.05.07-16.1-1.58-16.53-1.71-.11-.03,1.09-1.1,2.67-2.38,1.59-1.27,4.02-3.28,5.42-4.46,10.93-9.26,16.95-11.69,23.66-9.55M198.97,139.81c-.7.89-.72,1.19-.15,2.01,1.14,1.63,3.83.04,2.9-1.72-.67-1.27-1.88-1.4-2.75-.3M307.84,242.86c1.01,1.54-.74,3.28-2.28,2.28-.86-.56-.92-1.64-.14-2.42.78-.78,1.85-.72,2.42.14M93.58,243.58c.56,1.01.23,2.17-.69,2.41-1.66.42-2.86-1.21-1.94-2.61.62-.94,2.06-.83,2.63.21M126.44,247.86c-1.11,1.66-19.41,19.24-20.39,19.58-2.48.86-2.63,3.72-.25,4.56,1.64.57,3.75-1.23,3.18-2.71-.14-.37,2.16-2.83,9.75-10.43l9.94-9.94h11.06c6.08,0,11.06.1,11.06.21,0,.23-6.18,6.97-7.75,8.45l-.97.92h-4.67c-2.83,0-4.9.12-5.24.31-.47.25-.71.18-1.32-.38-3.15-2.89-4.88-2.27-10.39,3.77-2.46,2.69-2.47,2.85-.45,5.04,3.56,3.84,4.99,3.32,11.41-4.14l2.25-2.62h3.07c1.69,0,3.07.09,3.07.21s-2.88,3.09-6.41,6.62l-6.4,6.41-5.35.03c-2.94.02-6.14.11-7.09.21l-1.74.18-5.66,5.56-5.66,5.56h-6.96s-6.96-.03-6.96-.03l-.58-1.18c-.42-.86-.94-1.38-1.9-1.89l-1.33-.7.07-5.49.07-5.49,7.04-.08,7.04-.07,11.56-11.54,11.55-11.54h5.77l-.42.63M290.68,258.77l11.45,11.54,7.02.08,7.02.07.13,5.39.13,5.39-1.3.76c-.9.53-1.48,1.14-1.9,2.01l-.61,1.25h-14.17l-5.61-5.64-5.61-5.64-7.11-.18-7.11-.18-6.23-6.36-6.23-6.36,3.06-.08,3.06-.09.7.93c1.39,1.85,6.59,6.97,7.55,7.43,1.96.93,5.24-.99,6.44-3.76.54-1.24.32-1.5-6.16-7.31-1.93-1.73-3.81-1.67-5.95.21-.83.73-1.1.83-1.56.58-.33-.17-2.42-.3-5.04-.3h-4.48l-3.5-3.59c-3.39-3.49-5.23-5.62-5-5.82.06-.05,4.92-.18,10.81-.29l10.7-.19,10,9.99c9.32,9.31,9.99,10.05,9.91,10.87-.28,2.72,3.41,3.74,4.58,1.27.75-1.58-.1-3.24-1.66-3.24-.37,0-4.06-3.49-10.57-9.99l-10-9.99,1.41-.1c.78-.05,2.08-.12,2.89-.15l1.48-.05,11.45,11.54M73.49,256.64c2.02,2.02-.82,5-3.07,3.23-1.55-1.22-.51-3.92,1.51-3.92.55,0,1.13.26,1.56.69M329.81,256.61c2.18,1.87-.78,5.13-3.01,3.32-1.41-1.14-.37-3.98,1.47-3.98.43,0,1.13.3,1.55.66M128.74,259.95q1.27,1.23-1.7,4.28c-3.09,3.18-3.79,3.36-5.24,1.33l-.58-.82,2.77-2.85c3.05-3.13,3.38-3.27,4.75-1.95M275.51,261.37c3.62,3.44,3.56,3.34,2.42,4.7-1.22,1.44-1.81,1.22-4.94-1.86q-3.14-3.08-1.78-4.28c1.39-1.22,1.56-1.16,4.31,1.45M107.42,269.2c.64.64.35,1.68-.46,1.68-1.24,0-1.95-1.15-1.18-1.92.49-.49.99-.42,1.64.23M294.58,269.49c.25.78-.36,1.44-1.24,1.34-.74-.09-1.06-.73-.76-1.51.25-.65,1.79-.52,2.01.17M253.71,273.96c2.17,1.43,2.11,4.35-.13,5.51-2.33,1.21-5.03-.53-4.7-3.03.33-2.44,2.89-3.76,4.82-2.48M150.08,274.31c1.82,1.43,1.78,3.66-.08,4.98-2.56,1.82-6.19-.97-4.82-3.71.93-1.85,3.36-2.48,4.9-1.27M84.2,284.81c.97.97.92,2.22-.13,3.27-1.02,1.02-2.06,1.06-2.99.13-1.46-1.46-.45-4.1,1.56-4.1.55,0,1.13.26,1.56.69M319.03,284.81c1.81,1.68-.2,4.97-2.31,3.79-1.01-.57-1.58-1.67-1.34-2.6.46-1.84,2.3-2.44,3.66-1.18M92.62,303.34c1.16,1.02,1.24,2.27.21,3.3-1.05,1.05-2.08,1.11-3.03.17-2.03-2.03.66-5.35,2.81-3.46M310.36,303.4c1.02,1.02.83,2.78-.41,3.74-1.26.97-3.24-.27-3.24-2.03,0-2.04,2.26-3.09,3.65-1.7M126.64,304.3c-.49.2-.44.77.15,1.5l.5.62.7-.59.7-.59-.66-.56c-.69-.59-.79-.61-1.38-.38M124.89,306.08c-.62.38-.04,1.81.77,1.93.92.13,1.07-.64.3-1.48-.47-.5-.78-.63-1.07-.45M273.75,306.63c-1,1.11.28,2.24,1.34,1.18.48-.48.54-.71.3-1.1-.44-.7-1.05-.74-1.64-.09M122.96,308.14c-.39.46.81,2.09,1.31,1.78.49-.3.4-1.15-.19-1.67-.62-.56-.74-.57-1.13-.1M275.46,308.29c-.57.63-.58.82-.06,1.34.46.46.58.33.85-.94.22-1.02-.09-1.18-.79-.4M276.99,309.45c-.68.68-.71.87-.2,1.56.31.43.42.45.61.12.13-.22.41-.66.61-.99.6-.94-.22-1.49-1.02-.69M121.36,309.46c-.2.32.21,1.17.79,1.65.77.64,1.14-.32.46-1.18-.62-.79-.97-.92-1.24-.47M245.06,322.5c.66.95.76,1.4.88,4.08l.13,3.01-6.59-6.43c-5.08-4.96-6.58-6.59-6.57-7.11.01-.37.04-1.76.07-3.09l.05-2.42,5.64,5.44c3.1,3,5.98,5.93,6.38,6.51M278.48,311.27c-.74.65-.77.73-.34,1.17.51.52,1.93-.1,1.93-.85,0-.87-.77-1.02-1.59-.31M119.99,311.07c-.34.34-.18,1.13.34,1.6.56.5,1.45.36,1.45-.23s-1.47-1.69-1.79-1.37M279.76,312.35c-.3.9.56,3.05.97,2.42.45-.68.68-1.66.42-1.81-.17-.1-.41-.01-.55.2-.18.29-.25.22-.25-.25,0-.49-.47-.93-.59-.56M117.97,313.15c-.22.35.93,1.67,1.45,1.67.62,0,.74-.56.27-1.28-.47-.71-1.39-.92-1.72-.39M281.54,313.97c-.76,1.22-.27,1.85.65.83.45-.5.47-.62.11-.98s-.45-.34-.76.15M116.3,315.03c-.13.34.05.7.57,1.15.98.85,1.48.45.97-.78-.39-.95-1.24-1.16-1.54-.37M282.25,315.12c-.6.35-.63.92-.08,1.37.48.4,1.82-.4,1.84-1.09,0-.29-.15-.26-.6.14q-.6.53-.27-.1c.37-.71,0-.85-.89-.33M114.81,316.27c-.49.5-.44.92.2,1.5.55.49,1.42.43,1.42-.11,0-.67-1.26-1.75-1.62-1.39M283.85,316.92c-.78,1.19.16,1.79,1.1.69.39-.46.43-.67.17-.98-.47-.57-.75-.5-1.27.29M115.61,321.12c-.36.43.71,2.47,1.22,2.33.64-.18.87-1.05.36-1.34-.33-.19-.45-.47-.33-.83.21-.66-.73-.77-1.25-.15M282.6,321.4c0,1.04.65,1.42,1.27.73.5-.55.5-.58-.03-.98-.8-.6-1.24-.51-1.24.24M113.93,322.64c-.36.67.31,1.75,1.09,1.75s1.01-.57.33-.75c-.31-.08-.51-.31-.44-.5.25-.65-.66-1.11-.98-.5M284.5,322.96c-.63.71-.68.87-.33,1.16.52.43,1.97-.89,1.71-1.56-.24-.62-.56-.53-1.39.4M109.18,328.9c2.13,1.67.06,5.04-2.34,3.8-1.54-.8-1.8-2.14-.66-3.49.86-1.03,1.95-1.14,3-.31M293.89,329.16c.92.92,1,1.39.42,2.52-.88,1.7-3.24,1.68-3.96-.05-.99-2.35,1.74-4.26,3.54-2.47M236.15,330.39c1.27.97,1.29,2.61.05,3.59-2.14,1.68-4.67-1.71-2.57-3.46.68-.57,1.88-.63,2.53-.13M234.21,331.4c-.68.43-.29,1.85.54,1.95.93.11,1.5-.56,1.27-1.49-.18-.73-1.04-.95-1.81-.46M241.3,342.73c.5.71.33,1.29-.42,1.53-1.05.33-1.82-1-1.03-1.79.5-.5.99-.41,1.46.26M191.47,348.45c4.65,2.73.94,10.5-4.03,8.43-4.06-1.69-4.17-7.02-.19-8.88,1.39-.65,2.57-.52,4.22.45M129.28,348.99c2.12,1.82-.59,5.04-2.89,3.43-1.22-.85-1.05-3.11.28-3.76.99-.48,1.8-.38,2.62.32M258.19,348.66c1.41.75,1.72,2.19.74,3.44-1.24,1.57-3.92.59-3.92-1.43,0-1.74,1.69-2.8,3.19-2M273.61,348.69c1.27.98,1.38,2.22.28,3.32-1.53,1.53-3.8.63-3.74-1.49.05-1.66,2.2-2.8,3.46-1.83M144.22,349.03c2.02,1.41-.11,4.83-2.34,3.76-2.19-1.05-1.53-4.18.88-4.18.48,0,1.14.19,1.46.42M163.46,352.56c1.08,1.08.43,2.54-1.14,2.54-.82,0-1.4-.63-1.4-1.53,0-.56.99-1.57,1.55-1.57.23,0,.68.25.98.56M154.66,365.97c1.6,1.76-.58,4.46-2.71,3.36-1.77-.91-1.72-3.2.09-3.98,1.07-.46,1.8-.29,2.63.62M248.48,365.4c1.5,1.05,1.28,3.24-.4,3.94-1.04.43-1.64.31-2.58-.54-1.89-1.7.89-4.87,2.98-3.4M214.6,374.65c1.72,1.72-.33,4.35-2.49,3.21-.84-.44-1.03-.79-1.03-1.86,0-1.9,2.15-2.73,3.53-1.35M212.04,375.39c-.31.49.12,1.63.67,1.8,1.14.36,2.15-.83,1.44-1.69-.39-.47-1.84-.55-2.11-.11M237.66,375.53c.71.71.57,2.28-.26,2.82-1.88,1.23-3.81-1.24-2.22-2.84.55-.55,1.92-.54,2.48.02M201.42,390.75c1.42,1.59.35,4.06-1.76,4.06-2.77,0-3.33-3.88-.7-4.88.73-.28,1.81.08,2.46.82"/>
23731
+ </svg>`;
23732
+ var LOGO_DATA_URI = `data:image/svg+xml;base64,${Buffer.from(LOGO_SVG, "utf8").toString("base64")}`;
23733
+ var __dirname3 = path59.dirname(fileURLToPath4(import.meta.url));
23734
+ function portalTemplateDir() {
23735
+ const distDir = path59.resolve(__dirname3, "templates", "portal");
23736
+ if (existsSync12(distDir)) return distDir;
23737
+ const srcDir = path59.resolve(__dirname3, "..", "templates", "portal");
23738
+ if (existsSync12(srcDir)) return srcDir;
23739
+ return path59.resolve(__dirname3, "..", "dist", "templates", "portal");
23740
+ }
23741
+ var MODULE_ORDER = [
23742
+ "js/namespace.js",
23743
+ "js/state-model.js",
23744
+ "js/glossary.js",
23745
+ "js/router.js",
23746
+ "js/palette.js",
23747
+ "js/palette-overlay.js",
23748
+ "js/consumer.js",
23749
+ "js/export.js",
23750
+ "js/tree.js",
23751
+ "js/shell.js",
23752
+ "js/dispatch.js",
23753
+ // Per-surface view modules — registered onto Yg.views, consumed by the dispatcher. The
23754
+ // matrix module precedes the relations view that calls Yg.matrix; the panel module is read
23755
+ // by the bootstrap's panel slot. All before the bootstrap, which orchestrates the rest.
23756
+ "js/views/overview-view.js",
23757
+ "js/views/coverage-view.js",
23758
+ "js/views/tree-view.js",
23759
+ "js/views/relations-matrix.js",
23760
+ "js/views/relations-view.js",
23761
+ "js/views/rulebook-view.js",
23762
+ "js/views/types-view.js",
23763
+ "js/views/flows-view.js",
23764
+ "js/views/suppressions-view.js",
23765
+ "js/views/start-view.js",
23766
+ "js/views/panel-aspect.js",
23767
+ "js/views/panel-view.js",
23768
+ "js/bootstrap.js"
23769
+ ];
23770
+ var CSS_ORDER = [
23771
+ "tokens.css",
23772
+ "shell.css",
23773
+ "app.css",
23774
+ "views.css",
23775
+ "views-audit.css",
23776
+ "views-panel.css",
23777
+ "views-relations.css",
23778
+ "views-rulebook.css",
23779
+ "views-flows.css",
23780
+ "views-start.css"
23781
+ ];
23782
+ async function readAssets() {
23783
+ const dir = portalTemplateDir();
23784
+ const read = (rel) => readFile20(path59.join(dir, rel), "utf-8");
23785
+ const [shell, vendor] = await Promise.all([read("shell.html"), read("vendor/d3-hierarchy.js")]);
23786
+ const cssSources = await Promise.all(CSS_ORDER.map((rel) => read(rel)));
23787
+ const css = cssSources.join("\n");
23788
+ const moduleSources = await Promise.all(MODULE_ORDER.map((rel) => read(rel)));
23789
+ const modules = moduleSources.join("\n");
23790
+ return { shell, css, vendor, modules };
23791
+ }
23792
+ function renderStaticHtml(data, assets) {
23793
+ const json = safeJsonForScript(data);
23794
+ return assets.shell.replace("__PORTAL_FAVICON_HREF__", () => LOGO_DATA_URI).replace("/* __PORTAL_CSS__ */", () => assets.css).replace("/* __PORTAL_DATA__ */", () => json).replace("/* __PORTAL_VENDOR__ */", () => assets.vendor).replace("/* __PORTAL_MODULES__ */", () => assets.modules);
23795
+ }
23796
+ var LINE_SEPARATORS = new RegExp(`[${String.fromCharCode(8232)}${String.fromCharCode(8233)}]`, "g");
23797
+ function safeJsonForScript(data) {
23798
+ return JSON.stringify(data).replace(LINE_SEPARATORS, (ch) => "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0")).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
23799
+ }
23800
+ async function emitStatic(data, outPath) {
23801
+ const assets = await readAssets();
23802
+ const html = renderStaticHtml(data, assets);
23803
+ await atomicWriteFile(outPath, html);
23804
+ }
23805
+ async function renderPortalPage(data) {
23806
+ const assets = await readAssets();
23807
+ return renderStaticHtml(data, assets);
23808
+ }
23809
+ function contentTypeFor(relPath) {
23810
+ if (relPath.endsWith(".html")) return "text/html; charset=utf-8";
23811
+ if (relPath.endsWith(".css")) return "text/css; charset=utf-8";
23812
+ if (relPath.endsWith(".js")) return "text/javascript; charset=utf-8";
23813
+ if (relPath.endsWith(".json")) return "application/json; charset=utf-8";
23814
+ if (relPath.endsWith(".svg")) return "image/svg+xml";
23815
+ return "application/octet-stream";
23816
+ }
23817
+ async function readPortalAsset(relPath) {
23818
+ const root = portalTemplateDir();
23819
+ const cleaned = relPath.replace(/^\/+/, "");
23820
+ if (cleaned.length === 0) return null;
23821
+ const resolved = path59.resolve(root, cleaned);
23822
+ const rel = path59.relative(root, resolved);
23823
+ if (rel.startsWith("..") || path59.isAbsolute(rel)) return null;
23824
+ if (!existsSync12(resolved)) return null;
23825
+ const content20 = await readFile20(resolved);
23826
+ return { content: content20, contentType: contentTypeFor(cleaned) };
23827
+ }
23828
+
23829
+ // src/portal/server/server.ts
23830
+ import { createServer } from "http";
23831
+
23832
+ // src/portal/server/page.ts
23833
+ async function freshPortalData(projectRoot, writeEnabled) {
23834
+ return extractPortalData(projectRoot, { writeEnabled });
23835
+ }
23836
+ async function renderLivePage(data) {
23837
+ return renderPortalPage(data);
23838
+ }
23839
+ async function readStaticAsset(relPath) {
23840
+ return readPortalAsset(relPath);
23841
+ }
23842
+
23843
+ // src/portal/server/approve.ts
23844
+ import { spawn as spawn2 } from "child_process";
23845
+ import path60 from "path";
23846
+ import { existsSync as existsSync13 } from "fs";
23847
+ import { fileURLToPath as fileURLToPath5 } from "url";
23848
+ function resolveCliBin() {
23849
+ const here = fileURLToPath5(import.meta.url);
23850
+ if (path60.basename(here) === "bin.js") return here;
23851
+ let dir = path60.dirname(here);
23852
+ for (let i = 0; i < 6; i++) {
23853
+ const candidate = path60.join(dir, "dist", "bin.js");
23854
+ if (existsSync13(candidate)) return candidate;
23855
+ dir = path60.dirname(dir);
23856
+ }
23857
+ return process.argv[1];
23858
+ }
23859
+ var CLI_BIN = resolveCliBin();
23860
+ function approveArgs(llm) {
23861
+ return llm ? ["check", "--approve"] : ["check", "--approve", "--only-deterministic"];
23862
+ }
23863
+ function dryRunArgs(llm) {
23864
+ return llm ? ["check", "--approve", "--dry-run"] : ["check", "--approve", "--only-deterministic", "--dry-run"];
23865
+ }
23866
+ function spawnCli(args, cwd) {
23867
+ return new Promise((resolve6, reject) => {
23868
+ const child = spawn2(process.execPath, [CLI_BIN, ...args], { cwd });
23869
+ let stdout = "";
23870
+ let stderr = "";
23871
+ child.stdout.on("data", (chunk) => stdout += chunk.toString("utf-8"));
23872
+ child.stderr.on("data", (chunk) => stderr += chunk.toString("utf-8"));
23873
+ child.on("error", (err) => reject(err));
23874
+ child.on("close", (code) => resolve6({ exitCode: code ?? 1, stdout, stderr }));
23875
+ });
23876
+ }
23877
+ async function runApproveViaCli(projectRoot, llm) {
23878
+ return spawnCli(approveArgs(llm), projectRoot);
23879
+ }
23880
+ var BUDGET_RE = /Filling\s+(\d+)\s+unverified pairs across\s+\d+\s+nodes\s+—\s+(\d+)\s+deterministic\s+\(no cost\),\s+(\d+)\s+reviewer calls/;
23881
+ function parseDryRunBudget(output) {
23882
+ const m = output.match(BUDGET_RE);
23883
+ if (!m) {
23884
+ throw new Error(
23885
+ `Could not parse the dry-run cost preview from the CLI output. Raw output:
23886
+ ${output.trim()}`
23887
+ );
23888
+ }
23889
+ return {
23890
+ pairs: Number.parseInt(m[1], 10),
23891
+ deterministic: Number.parseInt(m[2], 10),
23892
+ reviewerCalls: Number.parseInt(m[3], 10),
23893
+ raw: m[0]
23894
+ };
23895
+ }
23896
+ async function dryRunApproveViaCli(projectRoot, llm) {
23897
+ const result = await spawnCli(dryRunArgs(llm), projectRoot);
23898
+ return parseDryRunBudget(`${result.stdout}
23899
+ ${result.stderr}`);
23900
+ }
23901
+
23902
+ // src/portal/server/router.ts
23903
+ function sendJson(res, status, body) {
23904
+ const payload = JSON.stringify(body);
23905
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
23906
+ res.end(payload);
23907
+ }
23908
+ function sendText(res, status, contentType, body) {
23909
+ res.writeHead(status, { "content-type": contentType });
23910
+ res.end(body);
23911
+ }
23912
+ async function readJsonBody(req) {
23913
+ const chunks = [];
23914
+ for await (const chunk of req) chunks.push(chunk);
23915
+ const text2 = Buffer.concat(chunks).toString("utf-8").trim();
23916
+ if (text2.length === 0) return {};
23917
+ try {
23918
+ const parsed = JSON.parse(text2);
23919
+ return parsed && typeof parsed === "object" ? parsed : {};
23920
+ } catch {
23921
+ return {};
23922
+ }
23923
+ }
23924
+ async function handleRequest(req, res, config) {
23925
+ const method = req.method ?? "GET";
23926
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
23927
+ const pathname = url.pathname;
23928
+ try {
23929
+ if (method === "GET" && pathname === "/") {
23930
+ const data = await freshPortalData(config.projectRoot, config.writeEnabled);
23931
+ const html = await renderLivePage(data);
23932
+ sendText(res, 200, "text/html; charset=utf-8", html);
23933
+ return;
23934
+ }
23935
+ if (method === "GET" && pathname === "/data") {
23936
+ const data = await freshPortalData(config.projectRoot, config.writeEnabled);
23937
+ sendJson(res, 200, data);
23938
+ return;
23939
+ }
23940
+ if (method === "GET" && pathname.startsWith("/static/")) {
23941
+ const rel = pathname.slice("/static/".length);
23942
+ const asset = await readStaticAsset(rel);
23943
+ if (!asset) {
23944
+ sendJson(res, 404, { error: "not-found", path: pathname });
23945
+ return;
23946
+ }
23947
+ res.writeHead(200, { "content-type": asset.contentType });
23948
+ res.end(asset.content);
23949
+ return;
23950
+ }
23951
+ if (method === "GET" && pathname === "/approve/dry-run") {
23952
+ const llm = url.searchParams.get("llm") !== "false";
23953
+ const preview = await dryRunApproveViaCli(config.projectRoot, llm);
23954
+ sendJson(res, 200, preview);
23955
+ return;
23956
+ }
23957
+ if (method === "POST" && pathname === "/approve") {
23958
+ if (!config.writeEnabled) {
23959
+ sendJson(res, 409, {
23960
+ error: "view-only",
23961
+ message: "This portal runs in view-only mode (--no-write); the Approve write action is disabled."
23962
+ });
23963
+ return;
23964
+ }
23965
+ const body = await readJsonBody(req);
23966
+ const llm = body.llm !== false;
23967
+ const result = await runApproveViaCli(config.projectRoot, llm);
23968
+ sendJson(res, 200, {
23969
+ ok: result.exitCode === 0,
23970
+ exitCode: result.exitCode,
23971
+ stdout: result.stdout,
23972
+ stderr: result.stderr
23973
+ });
23974
+ return;
23975
+ }
23976
+ sendJson(res, 404, { error: "not-found", method, path: pathname });
23977
+ } catch (err) {
23978
+ const message = err instanceof Error ? err.message : String(err);
23979
+ sendJson(res, 500, { error: "internal", message });
23980
+ }
23981
+ }
23982
+
23983
+ // src/portal/server/server.ts
23984
+ var LOOPBACK_HOST = "127.0.0.1";
23985
+ function startServer(opts) {
23986
+ const config = { projectRoot: opts.projectRoot, writeEnabled: opts.writeEnabled };
23987
+ const server = createServer((req, res) => {
23988
+ void handleRequest(req, res, config);
23989
+ });
23990
+ return new Promise((resolve6, reject) => {
23991
+ server.on("error", reject);
23992
+ server.listen(opts.port, LOOPBACK_HOST, () => {
23993
+ const address = server.address();
23994
+ const port = typeof address === "object" && address ? address.port : opts.port;
23995
+ resolve6({
23996
+ url: `http://${LOOPBACK_HOST}:${port}`,
23997
+ port,
23998
+ close: () => new Promise((res, rej) => {
23999
+ server.close((err) => err ? rej(err) : res());
24000
+ })
24001
+ });
24002
+ });
24003
+ });
24004
+ }
24005
+
24006
+ // src/cli/portal.ts
24007
+ function registerPortalCommand(program2) {
24008
+ program2.command("portal").description("Open a read-only local web portal onto the graph and its verification state").option("--static", "Emit a self-contained static page instead of serving").option("--out <path>", "Output path for the static page (with --static)").option("--port <n>", "Port for the local loopback server", (v) => {
24009
+ const n = parseInt(v, 10);
24010
+ if (Number.isNaN(n) || n < 0 || n > 65535) {
24011
+ throw new InvalidArgumentError2("--port must be an integer between 0 and 65535.");
24012
+ }
24013
+ return n;
24014
+ }).option("--open", "Open the page in the default browser").option("--no-write", "View-only mode \u2014 disable the Approve write action").action(async (options) => {
24015
+ try {
24016
+ await runPortal(options);
24017
+ } catch (error) {
24018
+ abortOnUnexpectedError(error, "opening the portal");
24019
+ }
24020
+ });
24021
+ }
24022
+ async function runPortal(options) {
24023
+ const graph = await loadGraphOrAbort(process.cwd());
24024
+ initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
24025
+ const projectRoot = projectRootFromGraph(graph.rootPath);
24026
+ if (options.static) {
24027
+ const data = await extractPortalData(projectRoot, { writeEnabled: options.write });
24028
+ const outPath = path61.resolve(projectRoot, options.out ?? "yg-portal.html");
24029
+ await emitStatic(data, outPath);
24030
+ process.stdout.write(`Portal page written to ${outPath}
24031
+ `);
24032
+ if (options.open) {
24033
+ await openInBrowser(outPath);
24034
+ }
24035
+ return;
24036
+ }
24037
+ await servePortal(projectRoot, options);
24038
+ }
24039
+ async function servePortal(projectRoot, options) {
24040
+ const handle = await startServer({
24041
+ projectRoot,
24042
+ port: options.port ?? DEFAULT_PORT,
24043
+ writeEnabled: options.write
24044
+ });
24045
+ const mode = options.write ? "" : " (view-only \u2014 Approve disabled)";
24046
+ process.stdout.write(`Portal running at ${handle.url}${mode}
24047
+ `);
24048
+ process.stdout.write("Press Ctrl+C to stop.\n");
24049
+ const shutdown = () => {
24050
+ void handle.close().finally(() => process.exit(0));
24051
+ };
24052
+ process.once("SIGINT", shutdown);
24053
+ process.once("SIGTERM", shutdown);
24054
+ if (options.open) {
24055
+ await openInBrowser(handle.url);
24056
+ }
24057
+ }
24058
+ var DEFAULT_PORT = 4317;
24059
+ async function openInBrowser(filePath) {
24060
+ const { spawn: spawn3 } = await import("child_process");
24061
+ const platform = process.platform;
24062
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
24063
+ const args = platform === "win32" ? ["/c", "start", "", filePath] : [filePath];
24064
+ try {
24065
+ const child = spawn3(command, args, { stdio: "ignore", detached: true });
24066
+ child.on("error", (error) => {
24067
+ debugWrite(`[portal] openInBrowser spawn error: ${error instanceof Error ? error.message : String(error)}`);
24068
+ });
24069
+ child.unref();
24070
+ } catch (error) {
24071
+ debugWrite(`[portal] openInBrowser failed: ${error instanceof Error ? error.message : String(error)}`);
24072
+ }
24073
+ }
24074
+
24075
+ // src/bin.ts
24076
+ import { readFileSync as readFileSync11 } from "fs";
24077
+ import { fileURLToPath as fileURLToPath6 } from "url";
22592
24078
  import { dirname as dirname2, join as join7 } from "path";
22593
- var __filename3 = fileURLToPath4(import.meta.url);
22594
- var __dirname3 = dirname2(__filename3);
22595
- var pkg = JSON.parse(readFileSync9(join7(__dirname3, "..", "package.json"), "utf-8"));
22596
- var program = new Command2();
24079
+ var __filename3 = fileURLToPath6(import.meta.url);
24080
+ var __dirname4 = dirname2(__filename3);
24081
+ var pkg = JSON.parse(readFileSync11(join7(__dirname4, "..", "package.json"), "utf-8"));
24082
+ var program = new Command3();
22597
24083
  program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(pkg.version);
22598
24084
  registerInitCommand(program);
22599
24085
  registerBuildCommand(program);
@@ -22610,6 +24096,7 @@ registerTypeSuggestCommand(program);
22610
24096
  registerKnowledgeCommand(program);
22611
24097
  registerSchemasCommand(program);
22612
24098
  registerSuppressionsCommand(program);
24099
+ registerPortalCommand(program);
22613
24100
  process.on("unhandledRejection", (reason) => {
22614
24101
  const msg = reason instanceof Error ? reason.message : String(reason);
22615
24102
  process.stderr.write(`Error: ${msg}