@chrisdudek/yg 5.3.0 → 5.4.1

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 +1985 -397
  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";
@@ -17,6 +17,17 @@ var DEFAULT_CONFIG = `version: "5.1.0"
17
17
  quality:
18
18
  max_direct_relations: 10
19
19
 
20
+ # Coverage \u2014 which files must belong to a node.
21
+ # Fresh projects start in "require nothing" mode: every unmapped file surfaces as
22
+ # a NON-blocking warning (not a blocking error), so your very first \`yg check\` is
23
+ # green while you adopt incrementally. Add a path prefix to \`required\` (e.g.
24
+ # "src/") to make that area blocking once you start mapping it. NOTE: an ABSENT
25
+ # coverage block defaults to requiring the WHOLE repo \u2014 this explicit empty list
26
+ # is what opts a fresh project into require-nothing.
27
+ coverage:
28
+ required: []
29
+ excluded: []
30
+
20
31
  # Reviewer configuration added by: yg init
21
32
  # (see yg schemas read config + yg knowledge read configuration)
22
33
 
@@ -128,7 +139,7 @@ Verification runs per \`(aspect, unit)\` pair. A **unit** is the subject of one
128
139
 
129
140
  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
141
 
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.
142
+ 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
143
 
133
144
  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
145
 
@@ -1440,7 +1451,7 @@ var PROVIDER_DEFAULTS = {
1440
1451
  "codex": { model: "o4-mini" },
1441
1452
  "gemini-cli": { model: "gemini-2.5-flash" }
1442
1453
  };
1443
- async function parseConfig(filePath) {
1454
+ async function parseConfig(filePath, opts) {
1444
1455
  const filename = path4.basename(filePath);
1445
1456
  const content20 = await readFile4(filePath, "utf-8");
1446
1457
  const baseRaw = parseYaml3(content20);
@@ -1451,7 +1462,7 @@ async function parseConfig(filePath) {
1451
1462
  next: "restore the file from version control, or regenerate it via `yg init`"
1452
1463
  }, "config-invalid");
1453
1464
  }
1454
- const overlay = await loadConfigOverlay(path4.dirname(filePath));
1465
+ const overlay = opts?.skipSecretsOverlay ? void 0 : await loadConfigOverlay(path4.dirname(filePath));
1455
1466
  const raw = overlay ? deepMerge(baseRaw, overlay) : baseRaw;
1456
1467
  const version = typeof raw.version === "string" ? raw.version.trim() : void 0;
1457
1468
  const qualityRaw = raw.quality;
@@ -2889,6 +2900,11 @@ async function parseArchitecture(filePath) {
2889
2900
  const nodeTypesObj = nodeTypesRaw ?? {};
2890
2901
  const nodeTypes = {};
2891
2902
  for (const [typeName, val] of Object.entries(nodeTypesObj)) {
2903
+ if (typeName === "*") {
2904
+ throw new Error(
2905
+ `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.`
2906
+ );
2907
+ }
2892
2908
  const entry = val;
2893
2909
  if (!entry || typeof entry !== "object" || typeof entry.description !== "string" || entry.description.trim() === "") {
2894
2910
  throw new Error(
@@ -2925,7 +2941,7 @@ async function parseArchitecture(filePath) {
2925
2941
  `yg-architecture.yaml: node_types.${typeName}.parents`,
2926
2942
  "parent type name"
2927
2943
  ) : void 0;
2928
- const relations = parseRelations2(entry.relations, typeName);
2944
+ const { lists: relations, relationDefault } = parseRelations2(entry.relations, typeName);
2929
2945
  let logRequired;
2930
2946
  if (entry.log_required !== void 0) {
2931
2947
  if (typeof entry.log_required !== "boolean") {
@@ -2955,6 +2971,7 @@ async function parseArchitecture(filePath) {
2955
2971
  ...aspectStatus && { aspectStatus },
2956
2972
  parents: parents && parents.length > 0 ? parents : void 0,
2957
2973
  relations,
2974
+ ...relationDefault !== void 0 && { relationDefault },
2958
2975
  ...logRequired !== void 0 && { log_required: logRequired },
2959
2976
  ...when !== void 0 && { when },
2960
2977
  ...enforce !== void 0 && { enforce }
@@ -2966,33 +2983,42 @@ async function parseArchitecture(filePath) {
2966
2983
  }
2967
2984
  function parseRelations2(relationsRaw, typeName) {
2968
2985
  if (relationsRaw === void 0) {
2969
- return void 0;
2986
+ return { lists: void 0 };
2970
2987
  }
2971
2988
  if (typeof relationsRaw !== "object" || Array.isArray(relationsRaw)) {
2972
2989
  throw new Error(`yg-architecture.yaml: node_types.${typeName}.relations must be an object`);
2973
2990
  }
2974
2991
  const relations = {};
2975
- for (const [relType, targets] of Object.entries(relationsRaw)) {
2976
- if (!VALID_RELATION_TYPES.has(relType)) {
2992
+ let relationDefault;
2993
+ for (const [key, value] of Object.entries(relationsRaw)) {
2994
+ if (key === "default") {
2995
+ if (value !== "allow" && value !== "deny") {
2996
+ throw new Error(
2997
+ `yg-architecture.yaml: node_types.${typeName}.relations.default must be 'allow' or 'deny' (got: ${JSON.stringify(value)})`
2998
+ );
2999
+ }
3000
+ relationDefault = value;
3001
+ continue;
3002
+ }
3003
+ if (!VALID_RELATION_TYPES.has(key)) {
2977
3004
  throw new Error(
2978
- `yg-architecture.yaml: node_types.${typeName}.relations: unknown relation type '${relType}' (valid types: ${Array.from(VALID_RELATION_TYPES).join(", ")})`
3005
+ `yg-architecture.yaml: node_types.${typeName}.relations: unknown relation type '${key}' (valid types: ${Array.from(VALID_RELATION_TYPES).join(", ")}; or 'default')`
2979
3006
  );
2980
3007
  }
2981
- if (!Array.isArray(targets)) {
3008
+ if (!Array.isArray(value)) {
2982
3009
  throw new Error(
2983
- `yg-architecture.yaml: node_types.${typeName}.relations.${relType} must be an array`
3010
+ `yg-architecture.yaml: node_types.${typeName}.relations.${key} must be an array`
2984
3011
  );
2985
3012
  }
2986
3013
  const targetStrings = assertStringArray(
2987
- targets,
2988
- `yg-architecture.yaml: node_types.${typeName}.relations.${relType}`,
3014
+ value,
3015
+ `yg-architecture.yaml: node_types.${typeName}.relations.${key}`,
2989
3016
  "target type name"
2990
3017
  );
2991
- if (targetStrings.length > 0) {
2992
- relations[relType] = targetStrings;
2993
- }
3018
+ relations[key] = targetStrings;
2994
3019
  }
2995
- return Object.keys(relations).length > 0 ? relations : {};
3020
+ const hasLists = Object.keys(relations).length > 0;
3021
+ return { lists: hasLists ? relations : {}, relationDefault };
2996
3022
  }
2997
3023
  function assertStringArray(arr, fieldPath, itemLabel) {
2998
3024
  const offenders = arr.map((value, index) => ({ value, index })).filter((e) => typeof e.value !== "string");
@@ -3133,7 +3159,7 @@ async function loadGraph(projectRoot, options = {}) {
3133
3159
  let configErrorMessage;
3134
3160
  let config = FALLBACK_CONFIG;
3135
3161
  try {
3136
- config = await parseConfig(path9.join(yggRoot, "yg-config.yaml"));
3162
+ config = await parseConfig(path9.join(yggRoot, "yg-config.yaml"), { skipSecretsOverlay: options.noSecrets });
3137
3163
  } catch (error) {
3138
3164
  if (error instanceof ConfigParseError) {
3139
3165
  configErrorMessage = error.messageData;
@@ -3322,7 +3348,8 @@ ${msg.next}`;
3322
3348
  }
3323
3349
 
3324
3350
  // src/io/lock-store.ts
3325
- import { readFileSync, unlinkSync } from "fs";
3351
+ import { readFileSync, unlinkSync, existsSync as existsSync2 } from "fs";
3352
+ import { createHash } from "crypto";
3326
3353
  import path11 from "path";
3327
3354
 
3328
3355
  // src/model/lock.ts
@@ -3368,6 +3395,20 @@ function logsLockPath(yggRoot) {
3368
3395
  function detLockPath(yggRoot) {
3369
3396
  return path11.join(yggRoot, LOCK_DET_FILE_NAME);
3370
3397
  }
3398
+ function committedLockContentHash(yggRoot) {
3399
+ const parts = [];
3400
+ for (const file of [nondetLockPath(yggRoot), logsLockPath(yggRoot)]) {
3401
+ if (!existsSync2(file)) continue;
3402
+ try {
3403
+ parts.push(readFileSync(file));
3404
+ } catch {
3405
+ }
3406
+ }
3407
+ if (parts.length === 0) return "";
3408
+ const hash = createHash("sha256");
3409
+ for (const p2 of parts) hash.update(p2);
3410
+ return hash.digest("hex");
3411
+ }
3371
3412
  function recoveryNext(ctx) {
3372
3413
  if (ctx.committed) {
3373
3414
  return `restore the file from git (\`git checkout HEAD -- .yggdrasil/${ctx.fileName}\`), OR
@@ -3714,7 +3755,7 @@ import { rm as rm3, stat as stat3 } from "fs/promises";
3714
3755
 
3715
3756
  // src/migrations/split-lock.ts
3716
3757
  import path12 from "path";
3717
- import { readdirSync, existsSync as existsSync2 } from "fs";
3758
+ import { readdirSync, existsSync as existsSync3 } from "fs";
3718
3759
  import { readFile as readFile10, writeFile as writeFile4, rm as rm2 } from "fs/promises";
3719
3760
  async function splitLock(yggRoot) {
3720
3761
  const legacy = readLegacyLock(yggRoot);
@@ -3732,7 +3773,7 @@ async function splitLock(yggRoot) {
3732
3773
  function scanDeterministicAspectIds(yggRoot) {
3733
3774
  const aspectsRoot = path12.join(yggRoot, "aspects");
3734
3775
  const ids = /* @__PURE__ */ new Set();
3735
- if (!existsSync2(aspectsRoot)) return ids;
3776
+ if (!existsSync3(aspectsRoot)) return ids;
3736
3777
  const walk2 = (dir) => {
3737
3778
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
3738
3779
  const full = path12.join(dir, entry.name);
@@ -3828,6 +3869,9 @@ async function ensureGitattributes(repoRoot) {
3828
3869
  var YGGDRASIL_GITIGNORE_LINES = [
3829
3870
  "yg-secrets.yaml",
3830
3871
  ".symbols-cache/",
3872
+ // Content-addressed per-file AST fact cache: a local speed cache the relation pass rebuilds
3873
+ // free on the next run; never committed.
3874
+ ".ast-cache/",
3831
3875
  ".debug.log",
3832
3876
  // Deterministic-verdict lock: a local cache rebuilt for free by
3833
3877
  // `yg check --approve --only-deterministic`; never committed.
@@ -3857,6 +3901,13 @@ async function ensureYggdrasilGitignore(yggRoot) {
3857
3901
  }
3858
3902
  var API_PROVIDERS = ["anthropic", "openai", "google", "openai-compatible", "ollama"];
3859
3903
  var CLI_PROVIDERS = ["claude-code", "codex", "gemini-cli"];
3904
+ var ALL_PROVIDERS = [...CLI_PROVIDERS, ...API_PROVIDERS];
3905
+ var API_KEY_ENV = {
3906
+ anthropic: "ANTHROPIC_API_KEY",
3907
+ openai: "OPENAI_API_KEY",
3908
+ google: "GOOGLE_API_KEY",
3909
+ "openai-compatible": "OPENAI_API_KEY"
3910
+ };
3860
3911
  var CLAUDE_CODE_ALIASES = [
3861
3912
  { value: "haiku", label: "haiku" },
3862
3913
  { value: "sonnet", label: "sonnet" },
@@ -3951,15 +4002,16 @@ async function promptModelText(provider) {
3951
4002
  async function runReviewerConfigFlow() {
3952
4003
  const provider = await p.select({
3953
4004
  message: "Which provider should verify your code?",
4005
+ initialValue: "claude-code",
3954
4006
  options: [
3955
- { value: "anthropic", label: "Anthropic", hint: "API \u2014 Claude models" },
3956
- { value: "openai", label: "OpenAI", hint: "API \u2014 GPT models" },
3957
- { value: "google", label: "Google", hint: "API \u2014 Gemini models" },
3958
- { value: "ollama", label: "Ollama", hint: "Local \u2014 no API costs" },
3959
- { value: "openai-compatible", label: "OpenAI-compatible", hint: "API \u2014 custom endpoint" },
3960
- { value: "claude-code", label: "Claude Code", hint: "CLI \u2014 uses installed claude" },
3961
- { value: "codex", label: "Codex", hint: "CLI \u2014 uses installed codex" },
3962
- { value: "gemini-cli", label: "Gemini CLI", hint: "CLI \u2014 uses installed gemini" }
4007
+ { value: "claude-code", label: "Claude Code", hint: "CLI \u2014 free, no API key; uses installed claude" },
4008
+ { value: "codex", label: "Codex", hint: "CLI \u2014 free, no API key; uses installed codex" },
4009
+ { value: "gemini-cli", label: "Gemini CLI", hint: "CLI \u2014 free, no API key; uses installed gemini" },
4010
+ { value: "ollama", label: "Ollama", hint: "Local \u2014 no API costs; needs a local install" },
4011
+ { value: "anthropic", label: "Anthropic", hint: "API key \u2014 Claude models" },
4012
+ { value: "openai", label: "OpenAI", hint: "API key \u2014 GPT models" },
4013
+ { value: "google", label: "Google", hint: "API key \u2014 Gemini models" },
4014
+ { value: "openai-compatible", label: "OpenAI-compatible", hint: "API key \u2014 custom endpoint" }
3963
4015
  ]
3964
4016
  });
3965
4017
  assertNotCancelled(provider);
@@ -4090,9 +4142,9 @@ async function freshInit(projectRoot) {
4090
4142
  const yggRoot = path14.join(projectRoot, ".yggdrasil");
4091
4143
  if (!isTTY()) {
4092
4144
  process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
4093
- what: "yg init requires an interactive terminal.",
4094
- why: "Setup requires interactive prompts to configure platform and reviewer.",
4095
- next: "Run yg init in an interactive terminal session."
4145
+ what: "yg init requires an interactive terminal (no --provider given).",
4146
+ why: "The interactive wizard needs prompts to configure platform and reviewer. Docker, devcontainer, and CI runs have no TTY.",
4147
+ next: `Run yg init in an interactive terminal, OR bootstrap non-interactively: yg init --platform <name> --provider <name> [--model <m>] [--endpoint <url>]. Supported providers: ${ALL_PROVIDERS.join(", ")}.`
4096
4148
  })}
4097
4149
  `));
4098
4150
  process.exit(1);
@@ -4106,7 +4158,7 @@ async function freshInit(projectRoot) {
4106
4158
  const platform = await promptPlatform();
4107
4159
  p.log.step("Step 2: Reviewer provider");
4108
4160
  p.log.info(
4109
- "The reviewer checks your source code against aspect rules during yg check --approve.\n API providers make HTTP calls. CLI providers delegate to an installed agent.\n For local review without API costs, use Ollama."
4161
+ "The reviewer checks your source code against aspect rules during yg check --approve.\n If you already run an agent CLI (Claude Code, Codex, Gemini), pick it \u2014 the\n reviewer then needs no API key and adds no separate API bill. Ollama runs\n locally with no API cost. API providers (Anthropic, OpenAI, Google) need a key."
4110
4162
  );
4111
4163
  const reviewerConfig = await runReviewerConfigFlow();
4112
4164
  await createYggdrasilStructure(projectRoot, yggRoot, platform);
@@ -4126,6 +4178,54 @@ async function createYggdrasilStructure(projectRoot, yggRoot, platform) {
4126
4178
  await ensureYggdrasilGitignore(yggRoot);
4127
4179
  await installRulesForPlatform(projectRoot, platform);
4128
4180
  }
4181
+ async function freshInitNonInteractive(projectRoot, yggRoot, opts) {
4182
+ const { platform, provider } = opts;
4183
+ const model = opts.model?.trim();
4184
+ if (!model) {
4185
+ process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
4186
+ what: "--model is required for non-interactive init.",
4187
+ why: "Non-interactive init records the reviewer model verbatim and applies no default \u2014 the model must be named explicitly.",
4188
+ next: `Re-run naming a model: yg init --platform ${platform} --provider ${provider} --model <name>.`
4189
+ })}
4190
+ `));
4191
+ process.exit(1);
4192
+ }
4193
+ let endpoint = opts.endpoint?.trim() || void 0;
4194
+ if (needsEndpoint(provider) && !endpoint) {
4195
+ if (provider === "ollama") {
4196
+ endpoint = "http://localhost:11434";
4197
+ } else {
4198
+ process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
4199
+ what: `--endpoint is required for provider '${provider}'.`,
4200
+ why: "An OpenAI-compatible provider has no default base URL \u2014 the reviewer needs an endpoint to call.",
4201
+ next: `Re-run naming an endpoint: yg init --platform ${platform} --provider ${provider} --model ${model} --endpoint <url>.`
4202
+ })}
4203
+ `));
4204
+ process.exit(1);
4205
+ }
4206
+ }
4207
+ await createYggdrasilStructure(projectRoot, yggRoot, platform);
4208
+ await writeReviewerConfig(yggRoot, { provider, model, endpoint });
4209
+ if (needsApiKey(provider)) {
4210
+ const envVar = API_KEY_ENV[provider];
4211
+ const apiKey = (envVar ? process.env[envVar] : void 0)?.trim();
4212
+ if (apiKey) {
4213
+ await writeSecretsFile(yggRoot, apiKey);
4214
+ } else {
4215
+ process.stdout.write(chalk2.yellow(`${buildIssueMessage({
4216
+ what: `No API key found${envVar ? ` in $${envVar}` : ""}; wrote the config without one.`,
4217
+ why: "An API provider needs a key before the reviewer can run; init records the config anyway so setup is not blocked.",
4218
+ next: `Set ${envVar ?? "the provider API key environment variable"} (or add the key to .yggdrasil/yg-secrets.yaml) before running yg check --approve.`
4219
+ })}
4220
+ `));
4221
+ }
4222
+ }
4223
+ await ensureGitattributes(projectRoot);
4224
+ process.stdout.write(chalk2.green(
4225
+ `Yggdrasil initialized (platform: ${platform}, provider: ${provider}, model: ${model}). Run yg check to get started.
4226
+ `
4227
+ ));
4228
+ }
4129
4229
  async function runVersionUpgrade2(projectRoot, yggRoot, platform) {
4130
4230
  const { migrationActions, migrationWarnings, withheld } = await runVersionUpgrade({
4131
4231
  yggRoot,
@@ -4216,7 +4316,7 @@ async function existingInit(projectRoot) {
4216
4316
  }
4217
4317
  }
4218
4318
  function registerInitCommand(program2) {
4219
- program2.command("init").description("Initialize Yggdrasil graph in current project").option("--upgrade", "Non-interactive: refresh rules").option("--platform <name>", `Platform for rules file (${PLATFORMS.join(", ")})`).action(async (options) => {
4319
+ program2.command("init").description("Initialize Yggdrasil graph in current project").option("--upgrade", "Non-interactive: refresh rules").option("--platform <name>", `Platform for rules file (${PLATFORMS.join(", ")})`).option("--provider <name>", `Non-interactive fresh init: reviewer provider (${ALL_PROVIDERS.join(", ")})`).option("--model <name>", "Non-interactive fresh init: reviewer model (required)").option("--endpoint <url>", "Non-interactive fresh init: reviewer endpoint (ollama / openai-compatible)").action(async (options) => {
4220
4320
  try {
4221
4321
  const projectRoot = process.cwd();
4222
4322
  const yggRoot = path14.join(projectRoot, ".yggdrasil");
@@ -4326,6 +4426,40 @@ function registerInitCommand(program2) {
4326
4426
  }
4327
4427
  if (exists) {
4328
4428
  await existingInit(projectRoot);
4429
+ } else if (options.provider) {
4430
+ if (!options.platform) {
4431
+ process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
4432
+ what: "--provider requires --platform for non-interactive init.",
4433
+ why: "A fresh graph must install the rules file for a specific agent platform; there is no prompt to ask in non-interactive mode.",
4434
+ next: `Pass --platform <name>. Supported: ${PLATFORMS.join(", ")}.`
4435
+ })}
4436
+ `));
4437
+ process.exit(1);
4438
+ }
4439
+ if (!PLATFORMS.includes(options.platform)) {
4440
+ process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
4441
+ what: `Unknown platform '${options.platform}'.`,
4442
+ why: "The --platform value must match one of the supported agent platforms.",
4443
+ next: `Use one of: ${PLATFORMS.join(", ")}`
4444
+ })}
4445
+ `));
4446
+ process.exit(1);
4447
+ }
4448
+ if (!ALL_PROVIDERS.includes(options.provider)) {
4449
+ process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
4450
+ what: `Unknown provider '${options.provider}'.`,
4451
+ why: "The --provider value must match one of the supported reviewer providers.",
4452
+ next: `Use one of: ${ALL_PROVIDERS.join(", ")}`
4453
+ })}
4454
+ `));
4455
+ process.exit(1);
4456
+ }
4457
+ await freshInitNonInteractive(projectRoot, yggRoot, {
4458
+ platform: options.platform,
4459
+ provider: options.provider,
4460
+ model: options.model,
4461
+ endpoint: options.endpoint
4462
+ });
4329
4463
  } else {
4330
4464
  await freshInit(projectRoot);
4331
4465
  }
@@ -4658,6 +4792,15 @@ function attachmentMachineOrigin(att, node) {
4658
4792
  return `port:${att.port.name}@${att.port.target}`;
4659
4793
  }
4660
4794
  }
4795
+ function hasNonDraftEffectiveAspects(node, graph) {
4796
+ const statuses = computeEffectiveAspectStatuses(node, graph);
4797
+ for (const [aspectId, s] of statuses) {
4798
+ if (s === "draft") continue;
4799
+ if (isAggregateAspect(graph, aspectId)) continue;
4800
+ return true;
4801
+ }
4802
+ return false;
4803
+ }
4661
4804
  function isAggregateAspect(graph, aspectId) {
4662
4805
  return graph.aspects.find((a) => a.id === aspectId)?.reviewer.type === "aggregate";
4663
4806
  }
@@ -4886,11 +5029,11 @@ function normPath(p2) {
4886
5029
  }
4887
5030
  function countDependents(graph, nodePath) {
4888
5031
  const paths = [];
4889
- for (const [path58, node] of graph.nodes) {
5032
+ for (const [path62, node] of graph.nodes) {
4890
5033
  const hasRelation = (node.meta.relations ?? []).some(
4891
5034
  (r) => r.target === nodePath && (STRUCTURAL_RELATION_TYPES.has(r.type) || EVENT_RELATION_TYPES.has(r.type))
4892
5035
  );
4893
- if (hasRelation) paths.push(path58);
5036
+ if (hasRelation) paths.push(path62);
4894
5037
  }
4895
5038
  return { count: paths.length, paths };
4896
5039
  }
@@ -5457,7 +5600,7 @@ function renderNode(node, indent, lines) {
5457
5600
  // src/io/hash.ts
5458
5601
  import { readFile as readFile14, readdir as readdir4, stat as stat6 } from "fs/promises";
5459
5602
  import path16 from "path";
5460
- import { createHash } from "crypto";
5603
+ import { createHash as createHash2 } from "crypto";
5461
5604
  import { createRequire as createRequire2 } from "module";
5462
5605
 
5463
5606
  // src/io/repo-scanner.ts
@@ -5583,10 +5726,10 @@ function isIgnoredByStack2(candidatePath, stack) {
5583
5726
  return false;
5584
5727
  }
5585
5728
  function hashString(content20) {
5586
- return createHash("sha256").update(content20).digest("hex");
5729
+ return createHash2("sha256").update(content20).digest("hex");
5587
5730
  }
5588
5731
  function hashBytes(bytes) {
5589
- return createHash("sha256").update(normalizeLineEndings(bytes)).digest("hex");
5732
+ return createHash2("sha256").update(normalizeLineEndings(bytes)).digest("hex");
5590
5733
  }
5591
5734
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
5592
5735
  let stack = options.gitignoreStack ?? [];
@@ -5709,19 +5852,19 @@ function checkArchitectureParentCycles(graph) {
5709
5852
  const color = new Map(typeIds.map((id) => [id, WHITE]));
5710
5853
  const backEdges = /* @__PURE__ */ new Set();
5711
5854
  const recordedCycles = [];
5712
- function dfs(typeId, path58) {
5855
+ function dfs(typeId, path62) {
5713
5856
  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];
5857
+ const cycleStart = path62.indexOf(typeId);
5858
+ if (cycleStart !== -1) recordedCycles.push([...path62.slice(cycleStart), typeId]);
5859
+ const from = path62[path62.length - 1];
5717
5860
  if (from !== void 0) backEdges.add(`${from}->${typeId}`);
5718
5861
  return;
5719
5862
  }
5720
5863
  if (color.get(typeId) === BLACK) return;
5721
5864
  color.set(typeId, GRAY);
5722
- path58.push(typeId);
5723
- for (const parent of types[typeId]?.parents ?? []) dfs(parent, path58);
5724
- path58.pop();
5865
+ path62.push(typeId);
5866
+ for (const parent of types[typeId]?.parents ?? []) dfs(parent, path62);
5867
+ path62.pop();
5725
5868
  color.set(typeId, BLACK);
5726
5869
  }
5727
5870
  for (const id of typeIds) {
@@ -5947,18 +6090,23 @@ function checkArchitectureRelations(graph) {
5947
6090
  const issues = [];
5948
6091
  for (const [nodePath, node] of graph.nodes) {
5949
6092
  const typeConfig = graph.architecture.node_types[node.meta.type];
5950
- if (!typeConfig?.relations || !node.meta.relations || node.meta.relations.length === 0) {
6093
+ if (!typeConfig || !node.meta.relations || node.meta.relations.length === 0) {
6094
+ continue;
6095
+ }
6096
+ const lists = typeConfig.relations;
6097
+ const def = typeConfig.relationDefault ?? "allow";
6098
+ if ((!lists || Object.keys(lists).length === 0) && def === "allow") {
5951
6099
  continue;
5952
6100
  }
5953
6101
  for (const rel of node.meta.relations) {
5954
- const allowedTypes = typeConfig.relations[rel.type];
5955
- if (!allowedTypes) continue;
6102
+ const allowed = lists?.[rel.type];
5956
6103
  const target = graph.nodes.get(rel.target);
5957
6104
  if (!target) continue;
5958
- if (!allowedTypes.includes(target.meta.type)) {
6105
+ if (allowed !== void 0) {
6106
+ if (allowed.includes("*") || allowed.includes(target.meta.type)) continue;
5959
6107
  const msgData = {
5960
6108
  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(", ")}]`,
6109
+ why: `Allowed targets for '${rel.type}' from type '${node.meta.type}': [${allowed.join(", ")}]`,
5962
6110
  next: `Either change the relation type, change the target node's type, or update yg-architecture.yaml to allow this relation.`
5963
6111
  };
5964
6112
  issues.push({
@@ -5969,6 +6117,20 @@ function checkArchitectureRelations(graph) {
5969
6117
  ...issueMsg(msgData),
5970
6118
  messageData: msgData
5971
6119
  });
6120
+ } else if (def === "deny") {
6121
+ const msgData = {
6122
+ what: `Relation '${rel.type}' from '${nodePath}' to '${rel.target}' (type '${target.meta.type}') is not allowed by the architecture.`,
6123
+ why: `Type '${node.meta.type}' denies relation '${rel.type}' by default (relations.default: deny, and no allow-list entry permits this target).`,
6124
+ 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.`
6125
+ };
6126
+ issues.push({
6127
+ severity: "error",
6128
+ code: "relation-target-forbidden",
6129
+ rule: "invalid-relation-target",
6130
+ nodePath,
6131
+ ...issueMsg(msgData),
6132
+ messageData: msgData
6133
+ });
5972
6134
  }
5973
6135
  }
5974
6136
  }
@@ -7860,7 +8022,7 @@ async function computeSourceFingerprint(graph, nodePath) {
7860
8022
  }
7861
8023
 
7862
8024
  // src/core/log/log-gate.ts
7863
- import { createHash as createHash2 } from "crypto";
8025
+ import { createHash as createHash3 } from "crypto";
7864
8026
  import path22 from "path";
7865
8027
 
7866
8028
  // src/core/parsing/log-parser.ts
@@ -7956,7 +8118,7 @@ function computeLogBaselineFromContent(content20) {
7956
8118
  const prefix = bytes.subarray(0, newest.offsetEnd);
7957
8119
  return {
7958
8120
  last_entry_datetime: newest.datetime,
7959
- prefix_hash: createHash2("sha256").update(prefix).digest("hex")
8121
+ prefix_hash: createHash3("sha256").update(prefix).digest("hex")
7960
8122
  };
7961
8123
  }
7962
8124
 
@@ -8184,11 +8346,11 @@ function registerTreeCommand(program2) {
8184
8346
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
8185
8347
  let roots;
8186
8348
  if (options.root?.trim()) {
8187
- const path58 = options.root.trim().replace(/\/$/, "");
8188
- const node = graph.nodes.get(path58);
8349
+ const path62 = options.root.trim().replace(/\/$/, "");
8350
+ const node = graph.nodes.get(path62);
8189
8351
  if (!node) {
8190
8352
  process.stderr.write(chalk4.red(buildIssueMessage({
8191
- what: `Node '${path58}' not found.`,
8353
+ what: `Node '${path62}' not found.`,
8192
8354
  why: `The --root path must be a valid node path in the graph.`,
8193
8355
  next: `Run yg tree (no --root) to list all nodes, then pick a valid path.`
8194
8356
  }) + "\n"));
@@ -8399,14 +8561,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
8399
8561
  }
8400
8562
  const chains = [];
8401
8563
  for (const node of transitiveOnly) {
8402
- const path58 = [];
8564
+ const path62 = [];
8403
8565
  let current = node;
8404
8566
  while (current) {
8405
- path58.unshift(current);
8567
+ path62.unshift(current);
8406
8568
  current = parent.get(current);
8407
8569
  }
8408
- if (path58.length >= 3) {
8409
- chains.push(path58.slice(1).map((p2) => `<- ${p2}`).join(" "));
8570
+ if (path62.length >= 3) {
8571
+ chains.push(path62.slice(1).map((p2) => `<- ${p2}`).join(" "));
8410
8572
  }
8411
8573
  }
8412
8574
  return chains.sort();
@@ -8438,14 +8600,14 @@ function collectIndirectDependents(graph, directlyAffected) {
8438
8600
  }
8439
8601
  for (const [node] of parent) {
8440
8602
  if (directSet.has(node)) continue;
8441
- const path58 = [node];
8603
+ const path62 = [node];
8442
8604
  let current = node;
8443
8605
  while (parent.has(current)) {
8444
8606
  current = parent.get(current);
8445
- path58.push(current);
8607
+ path62.push(current);
8446
8608
  }
8447
- const chain = path58.map((p2) => `<- ${p2}`).join(" ");
8448
- const depth = path58.length;
8609
+ const chain = path62.map((p2) => `<- ${p2}`).join(" ");
8610
+ const depth = path62.length;
8449
8611
  const existing = bestChain.get(node);
8450
8612
  if (!existing || depth < existing.depth) {
8451
8613
  bestChain.set(node, { chain, depth });
@@ -8546,14 +8708,14 @@ import { register } from "module";
8546
8708
  import { pathToFileURL } from "url";
8547
8709
  import path24 from "path";
8548
8710
  import { fileURLToPath as fileURLToPath2 } from "url";
8549
- import { existsSync as existsSync3 } from "fs";
8711
+ import { existsSync as existsSync4 } from "fs";
8550
8712
  var __filename = fileURLToPath2(import.meta.url);
8551
8713
  var __dirname = path24.dirname(__filename);
8552
8714
  var registered = false;
8553
8715
  function ensureLoaderRegistered() {
8554
8716
  if (registered) return;
8555
8717
  let implPath = path24.resolve(__dirname, "./loader-hook-impl.js");
8556
- if (!existsSync3(implPath)) {
8718
+ if (!existsSync4(implPath)) {
8557
8719
  const pkgRoot = path24.resolve(__dirname, "../../");
8558
8720
  implPath = path24.resolve(pkgRoot, "dist/loader-hook-impl.js");
8559
8721
  }
@@ -8565,9 +8727,9 @@ function ensureLoaderRegistered() {
8565
8727
  import * as fs from "fs";
8566
8728
  import * as path25 from "path";
8567
8729
  var UndeclaredFsReadError = class extends Error {
8568
- constructor(path58) {
8569
- super(`structure-aspect-undeclared-fs-read: ${path58}`);
8570
- this.path = path58;
8730
+ constructor(path62) {
8731
+ super(`structure-aspect-undeclared-fs-read: ${path62}`);
8732
+ this.path = path62;
8571
8733
  this.name = "UndeclaredFsReadError";
8572
8734
  }
8573
8735
  path;
@@ -8770,7 +8932,11 @@ function createCtxGraph(params) {
8770
8932
  }
8771
8933
  return {
8772
8934
  node(id) {
8773
- assertAllowed(id);
8935
+ const self = graph.nodes.get(currentNodePath);
8936
+ if (self) recordGraphNode(self);
8937
+ if (!allowed.has(id)) {
8938
+ assertAllowed(id);
8939
+ }
8774
8940
  const m = graph.nodes.get(id);
8775
8941
  if (!m) {
8776
8942
  if (recorder) recorder.recordGraphNodeAbsent(id);
@@ -8851,8 +9017,9 @@ import { parse as parseTomlSmol } from "smol-toml";
8851
9017
  import { Parser, Language } from "web-tree-sitter";
8852
9018
  import path27 from "path";
8853
9019
  import { fileURLToPath as fileURLToPath3 } from "url";
8854
- import { existsSync as existsSync5 } from "fs";
9020
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
8855
9021
  import { createRequire as createRequire3 } from "module";
9022
+ import { createHash as createHash4 } from "crypto";
8856
9023
  var _require = createRequire3(import.meta.url);
8857
9024
  var __filename2 = fileURLToPath3(import.meta.url);
8858
9025
  var __dirname2 = path27.dirname(__filename2);
@@ -8863,6 +9030,19 @@ var GRAMMAR_DIRS = [
8863
9030
  var initPromise = null;
8864
9031
  var langCache = /* @__PURE__ */ new Map();
8865
9032
  var parserCache = /* @__PURE__ */ new Map();
9033
+ var wasmHashCache = /* @__PURE__ */ new Map();
9034
+ function grammarWasmHash(extension) {
9035
+ const cached = wasmHashCache.get(extension);
9036
+ if (cached !== void 0) return cached;
9037
+ const info = getGrammarForExtension(extension);
9038
+ if (!info) {
9039
+ throw new Error(`no grammar for extension '${extension}'`);
9040
+ }
9041
+ const wasmPath = resolveWasm(info.wasmFile, info.wasmPackage);
9042
+ const hash = createHash4("sha256").update(readFileSync4(wasmPath)).digest("hex");
9043
+ wasmHashCache.set(extension, hash);
9044
+ return hash;
9045
+ }
8866
9046
  function init() {
8867
9047
  if (initPromise === null) {
8868
9048
  initPromise = Parser.init();
@@ -8875,12 +9055,12 @@ function init() {
8875
9055
  function resolveWasm(filename, pkg2) {
8876
9056
  for (const dir of GRAMMAR_DIRS) {
8877
9057
  const p2 = path27.join(dir, filename);
8878
- if (existsSync5(p2)) return p2;
9058
+ if (existsSync6(p2)) return p2;
8879
9059
  }
8880
9060
  try {
8881
9061
  const pkgDir = path27.dirname(_require.resolve(`${pkg2}/package.json`));
8882
9062
  for (const candidate of [path27.join(pkgDir, filename), path27.join(pkgDir, "bindings/node", filename)]) {
8883
- if (existsSync5(candidate)) return candidate;
9063
+ if (existsSync6(candidate)) return candidate;
8884
9064
  }
8885
9065
  } catch {
8886
9066
  }
@@ -10505,10 +10685,10 @@ function registerFlowsCommand(program2) {
10505
10685
  import chalk8 from "chalk";
10506
10686
 
10507
10687
  // src/core/check.ts
10508
- import path44 from "path";
10688
+ import path43 from "path";
10509
10689
 
10510
10690
  // src/core/log-integrity.ts
10511
- import { createHash as createHash3 } from "crypto";
10691
+ import { createHash as createHash5 } from "crypto";
10512
10692
  var DATETIME_STRICT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,3}Z$/;
10513
10693
  function validateAppendOnly(currentContent, storedDatetime, storedPrefixHash) {
10514
10694
  if (!DATETIME_STRICT.test(storedDatetime)) {
@@ -10521,7 +10701,7 @@ function validateAppendOnly(currentContent, storedDatetime, storedPrefixHash) {
10521
10701
  if (!boundary) return { ok: false, reason: "boundary_missing" };
10522
10702
  const bytes = Buffer.from(currentContent, "utf-8");
10523
10703
  const prefix = bytes.subarray(0, boundary.offsetEnd);
10524
- const computed = createHash3("sha256").update(prefix).digest("hex");
10704
+ const computed = createHash5("sha256").update(prefix).digest("hex");
10525
10705
  if (computed !== storedPrefixHash) return { ok: false, reason: "prefix_modified" };
10526
10706
  return { ok: true };
10527
10707
  }
@@ -11544,8 +11724,6 @@ import path33 from "path";
11544
11724
  import { readFile as readFile15 } from "fs/promises";
11545
11725
 
11546
11726
  // src/relations/symbol-table.ts
11547
- import path32 from "path";
11548
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
11549
11727
  var SymbolTable = class {
11550
11728
  defs = /* @__PURE__ */ new Map();
11551
11729
  // `${language}\0${symbolKey}` → set of defining files
@@ -11586,30 +11764,6 @@ var SymbolTable = class {
11586
11764
  return s ? [...s] : [];
11587
11765
  }
11588
11766
  };
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
11767
 
11614
11768
  // src/relations/resolver.ts
11615
11769
  function resolveCandidateGroup(candidates2, resolver, fromFile, language) {
@@ -11841,82 +11995,35 @@ function buildUsingScope(file) {
11841
11995
  });
11842
11996
  return { prefixes, globalPrefixes, aliases, globalAliases, staticTargets };
11843
11997
  }
11844
- function collectGlobalUsings(file) {
11845
- return buildUsingScope(file).globalPrefixes;
11846
- }
11847
- function collectGlobalUsingAliases(file) {
11848
- return [...buildUsingScope(file).globalAliases.entries()];
11849
- }
11850
11998
  function enclosingNamespaceChain(fileNs, node) {
11851
11999
  const segments = [fileNs, blockNamespace(node)].filter((p2) => p2 !== "").join(".").split(".").filter((s) => s !== "");
11852
12000
  const chain = [];
11853
12001
  for (let i = segments.length; i >= 1; i--) chain.push(segments.slice(0, i).join("."));
11854
12002
  return chain;
11855
12003
  }
11856
- function uses(file, options = {}) {
11857
- const out = [];
12004
+ function extractCsharpRefs(file) {
11858
12005
  const scope = buildUsingScope(file);
11859
12006
  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
- };
12007
+ const refs = [];
12008
+ const emitted = /* @__PURE__ */ new Set();
11881
12009
  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 });
12010
+ refs.push({
12011
+ kind: "plain",
12012
+ ref,
12013
+ line,
12014
+ rooted,
12015
+ enclosingNs: enclosingNamespaceChain(fileNs, node)
12016
+ });
12017
+ };
12018
+ const pushAttribute = (written, suffixed, node, line) => {
12019
+ refs.push({
12020
+ kind: "attr",
12021
+ written,
12022
+ suffixed,
12023
+ line,
12024
+ enclosingNs: enclosingNamespaceChain(fileNs, node)
12025
+ });
11918
12026
  };
11919
- const emitted = /* @__PURE__ */ new Set();
11920
12027
  const emitTypeNode = (typeNode) => {
11921
12028
  if (typeNode === null) return;
11922
12029
  switch (typeNode.type) {
@@ -11943,15 +12050,15 @@ function uses(file, options = {}) {
11943
12050
  return;
11944
12051
  }
11945
12052
  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
- }
12053
+ const nameField = typeNode.childForFieldName("name");
12054
+ if (nameField !== null) {
12055
+ refs.push({
12056
+ kind: "alias",
12057
+ aliasName: aliasField.text,
12058
+ tail: stripGlobalQualifier(nameField.text),
12059
+ line: typeNode.startPosition.row + 1,
12060
+ enclosingNs: enclosingNamespaceChain(fileNs, typeNode)
12061
+ });
11955
12062
  }
11956
12063
  }
11957
12064
  return;
@@ -11988,11 +12095,7 @@ function uses(file, options = {}) {
11988
12095
  return null;
11989
12096
  }
11990
12097
  for (const target of scope.staticTargets) {
11991
- out.push({
11992
- candidates: [{ kind: "symbol", symbolKey: target.fqn }],
11993
- kind: "import",
11994
- line: target.line
11995
- });
12098
+ refs.push({ kind: "static", fqn: target.fqn, line: target.line });
11996
12099
  }
11997
12100
  walk(file.tree.rootNode, (node) => {
11998
12101
  if (node.type !== "using_directive") return void 0;
@@ -12120,10 +12223,75 @@ function uses(file, options = {}) {
12120
12223
  }
12121
12224
  return void 0;
12122
12225
  });
12123
- function pushAttribute(written, suffixed, node, line) {
12226
+ return { fileNs, scope, refs };
12227
+ }
12228
+ function assembleCsharpCandidates(extract, options = {}) {
12229
+ const out = [];
12230
+ const { scope, refs } = extract;
12231
+ const aliases = new Map(scope.aliases);
12232
+ for (const [name, fqn] of options.projectGlobalUsingAliases ?? []) {
12233
+ if (!aliases.has(name)) aliases.set(name, fqn);
12234
+ }
12235
+ const usingPrefixes = [
12236
+ .../* @__PURE__ */ new Set([
12237
+ ...scope.prefixes,
12238
+ ...scope.globalPrefixes,
12239
+ ...options.projectGlobalUsings ?? []
12240
+ ])
12241
+ ].sort();
12242
+ const dedupKeys = (keys) => {
12243
+ const seen = /* @__PURE__ */ new Set();
12244
+ const out2 = [];
12245
+ for (const k of keys) {
12246
+ if (k === void 0 || k === "") continue;
12247
+ if (seen.has(k)) continue;
12248
+ seen.add(k);
12249
+ out2.push(k);
12250
+ }
12251
+ return out2;
12252
+ };
12253
+ const pushRef = (ref, enclosingNs, line, rooted) => {
12254
+ if (rooted) {
12255
+ out.push({ candidates: [{ kind: "symbol", symbolKey: ref }], kind: "import", line });
12256
+ return;
12257
+ }
12258
+ const lead = ref.split(".")[0];
12259
+ const multi = ref.includes(".");
12260
+ const aliasTarget = aliases.get(lead);
12261
+ const enclosing = enclosingNs.map((ns) => `${ns}.${ref}`);
12262
+ const usingMembers = usingPrefixes.map((p2) => ({
12263
+ symbolKey: `${p2}.${ref}`,
12264
+ nestedOnly: multi
12265
+ }));
12266
+ const candidates2 = [];
12267
+ const pushed = /* @__PURE__ */ new Set();
12268
+ const add = (hint) => {
12269
+ if (hint.kind === "symbol") {
12270
+ if (pushed.has(hint.symbolKey)) return;
12271
+ pushed.add(hint.symbolKey);
12272
+ }
12273
+ candidates2.push(hint);
12274
+ };
12275
+ if (aliasTarget !== void 0) {
12276
+ const tail = ref.slice(lead.length);
12277
+ const aliasKey = `${aliasTarget}${tail}`;
12278
+ const coDef = dedupKeys([aliasKey, ...enclosing, ref]).map((k) => ({
12279
+ symbolKey: k
12280
+ }));
12281
+ add({ kind: "symbol", symbolKey: aliasKey, set: coDef });
12282
+ }
12283
+ for (const k of enclosing) add({ kind: "symbol", symbolKey: k });
12284
+ for (const m of usingMembers) {
12285
+ add({ kind: "symbol", symbolKey: m.symbolKey, nestedOnly: m.nestedOnly, set: usingMembers });
12286
+ }
12287
+ add({ kind: "symbol", symbolKey: ref });
12288
+ if (candidates2.length === 0) return;
12289
+ out.push({ candidates: candidates2, kind: "import", line });
12290
+ };
12291
+ const pushAttribute = (written, suffixed, enclosingNs, line) => {
12124
12292
  const before = out.length;
12125
- pushRef(written, node, line);
12126
- if (suffixed !== void 0) pushRef(suffixed, node, line);
12293
+ pushRef(written, enclosingNs, line, false);
12294
+ if (suffixed !== void 0) pushRef(suffixed, enclosingNs, line, false);
12127
12295
  if (out.length > before + 1) {
12128
12296
  const merged = [];
12129
12297
  const seenK = /* @__PURE__ */ new Set();
@@ -12138,31 +12306,142 @@ function uses(file, options = {}) {
12138
12306
  }
12139
12307
  out.push({ candidates: merged, kind: "import", line });
12140
12308
  }
12309
+ };
12310
+ for (const r of refs) {
12311
+ switch (r.kind) {
12312
+ case "static":
12313
+ out.push({ candidates: [{ kind: "symbol", symbolKey: r.fqn }], kind: "import", line: r.line });
12314
+ break;
12315
+ case "plain":
12316
+ pushRef(r.ref, r.enclosingNs, r.line, r.rooted);
12317
+ break;
12318
+ case "alias": {
12319
+ const aliasFqn = aliases.get(r.aliasName);
12320
+ if (aliasFqn !== void 0) {
12321
+ pushRef(`${aliasFqn}.${r.tail}`, r.enclosingNs, r.line, true);
12322
+ }
12323
+ break;
12324
+ }
12325
+ case "attr":
12326
+ pushAttribute(r.written, r.suffixed, r.enclosingNs, r.line);
12327
+ break;
12328
+ }
12141
12329
  }
12142
12330
  return out;
12143
12331
  }
12144
- function csharpUses(file, options) {
12145
- return uses(file, options);
12332
+ function uses(file, options = {}) {
12333
+ return assembleCsharpCandidates(extractCsharpRefs(file), options);
12146
12334
  }
12147
12335
  var csharpExtractor = {
12148
12336
  languages: /* @__PURE__ */ new Set(["csharp"]),
12337
+ rev: 2,
12149
12338
  declarations,
12150
12339
  uses
12151
12340
  };
12152
12341
 
12153
- // src/relations/verifier.ts
12154
- function verifyNodeDeps(nodeId, deps, g) {
12155
- const out = [];
12156
- const declared = g.declaredTargets(nodeId);
12157
- for (const d of deps) {
12158
- const m = d.ownerNode;
12159
- if (m === nodeId) continue;
12160
- if (g.isAncestorOf(m, nodeId) || g.isAncestorOf(nodeId, m)) continue;
12161
- if (declared.has(m)) continue;
12162
- if (g.parentChain(m).some((anc) => declared.has(anc))) continue;
12163
- out.push({ fromFile: d.fromFile, line: d.line, ownerNode: m });
12342
+ // src/relations/facts-cache.ts
12343
+ import path32 from "path";
12344
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
12345
+ import { createHash as createHash6 } from "crypto";
12346
+ var CACHE_SCHEMA_VERSION = 1;
12347
+ function serializeCsharp(c) {
12348
+ return {
12349
+ fileNs: c.fileNs,
12350
+ scope: {
12351
+ prefixes: c.scope.prefixes,
12352
+ globalPrefixes: c.scope.globalPrefixes,
12353
+ aliases: [...c.scope.aliases],
12354
+ globalAliases: [...c.scope.globalAliases],
12355
+ staticTargets: c.scope.staticTargets
12356
+ },
12357
+ refs: c.refs
12358
+ };
12359
+ }
12360
+ function deserializeCsharp(raw) {
12361
+ if (!raw || typeof raw !== "object") return null;
12362
+ const s = raw;
12363
+ if (typeof s.fileNs !== "string") return null;
12364
+ if (!s.scope || typeof s.scope !== "object") return null;
12365
+ const sc = s.scope;
12366
+ if (!Array.isArray(sc.prefixes) || !Array.isArray(sc.globalPrefixes) || !Array.isArray(sc.aliases) || !Array.isArray(sc.globalAliases) || !Array.isArray(sc.staticTargets)) {
12367
+ return null;
12164
12368
  }
12165
- return out;
12369
+ if (!Array.isArray(s.refs)) return null;
12370
+ return {
12371
+ fileNs: s.fileNs,
12372
+ scope: {
12373
+ prefixes: sc.prefixes,
12374
+ globalPrefixes: sc.globalPrefixes,
12375
+ aliases: new Map(sc.aliases),
12376
+ globalAliases: new Map(sc.globalAliases),
12377
+ staticTargets: sc.staticTargets
12378
+ },
12379
+ refs: s.refs
12380
+ };
12381
+ }
12382
+ function astCacheDir(graphRoot) {
12383
+ return path32.join(graphRoot, ".ast-cache");
12384
+ }
12385
+ function factsKey(args) {
12386
+ const payload = `${args.contentHash}\0${args.language}\0${args.grammarHash}\0${args.rev}`;
12387
+ return createHash6("sha256").update(payload).digest("hex").slice(0, 32);
12388
+ }
12389
+ function shardPath(dir, language, key) {
12390
+ return path32.join(dir, `v${CACHE_SCHEMA_VERSION}`, language, `${key}.json`);
12391
+ }
12392
+ async function loadFacts(dir, language, key) {
12393
+ const p2 = shardPath(dir, language, key);
12394
+ if (!existsSync7(p2)) return null;
12395
+ let parsed;
12396
+ try {
12397
+ parsed = JSON.parse(readFileSync7(p2, "utf-8"));
12398
+ } catch {
12399
+ return null;
12400
+ }
12401
+ if (!parsed || typeof parsed !== "object" || parsed.v !== CACHE_SCHEMA_VERSION) return null;
12402
+ if (typeof parsed.key !== "string") return null;
12403
+ if (!Array.isArray(parsed.declarations)) return null;
12404
+ if (!Array.isArray(parsed.uses)) return null;
12405
+ if (parsed.key !== key) return null;
12406
+ const facts = {
12407
+ declarations: parsed.declarations,
12408
+ uses: parsed.uses
12409
+ };
12410
+ if (parsed.csharp !== void 0) {
12411
+ const csharp = deserializeCsharp(parsed.csharp);
12412
+ if (csharp === null) return null;
12413
+ facts.csharp = csharp;
12414
+ }
12415
+ return facts;
12416
+ }
12417
+ async function writeFacts(dir, language, key, facts) {
12418
+ const p2 = shardPath(dir, language, key);
12419
+ if (existsSync7(p2)) return;
12420
+ const body = {
12421
+ v: CACHE_SCHEMA_VERSION,
12422
+ key,
12423
+ declarations: facts.declarations,
12424
+ uses: facts.uses,
12425
+ // Flatten the C# extract's `Map`s to entry arrays BEFORE JSON.stringify — a bare `Map`
12426
+ // stringifies to `"{}"` and would silently lose every alias entry (the false-green vector).
12427
+ ...facts.csharp !== void 0 ? { csharp: serializeCsharp(facts.csharp) } : {}
12428
+ };
12429
+ await atomicWriteFile(p2, JSON.stringify(body));
12430
+ }
12431
+
12432
+ // src/relations/verifier.ts
12433
+ function verifyNodeDeps(nodeId, deps, g) {
12434
+ const out = [];
12435
+ const declared = g.declaredTargets(nodeId);
12436
+ for (const d of deps) {
12437
+ const m = d.ownerNode;
12438
+ if (m === nodeId) continue;
12439
+ if (g.isAncestorOf(m, nodeId) || g.isAncestorOf(nodeId, m)) continue;
12440
+ if (declared.has(m)) continue;
12441
+ if (g.parentChain(m).some((anc) => declared.has(anc))) continue;
12442
+ out.push({ fromFile: d.fromFile, line: d.line, ownerNode: m });
12443
+ }
12444
+ return out;
12166
12445
  }
12167
12446
 
12168
12447
  // src/relations/pass.ts
@@ -12204,6 +12483,63 @@ async function runRelationPass(graph, projectRoot, deps) {
12204
12483
  return null;
12205
12484
  }
12206
12485
  }
12486
+ async function extractFileFacts(record, extractor) {
12487
+ const parsed = await parseSingle(record);
12488
+ if (!parsed) return null;
12489
+ try {
12490
+ const declarations12 = extractor.declarations(parsed);
12491
+ const isCsharp = record.language === "csharp";
12492
+ const uses12 = isCsharp ? null : extractor.uses(parsed);
12493
+ const csharp = isCsharp ? extractCsharpRefs(parsed) : null;
12494
+ return { declarations: declarations12, uses: uses12, csharp };
12495
+ } finally {
12496
+ parsed.tree.delete();
12497
+ }
12498
+ }
12499
+ const grammarHashByExt = /* @__PURE__ */ new Map();
12500
+ const grammarHashForExt = (ext) => {
12501
+ const hit = grammarHashByExt.get(ext);
12502
+ if (hit !== void 0) return hit;
12503
+ let h;
12504
+ try {
12505
+ h = grammarWasmHash(ext);
12506
+ } catch {
12507
+ h = null;
12508
+ }
12509
+ grammarHashByExt.set(ext, h);
12510
+ return h;
12511
+ };
12512
+ async function loadOrExtractFacts(record, extractor) {
12513
+ const language = record.language;
12514
+ const isCsharp = language === "csharp";
12515
+ const grammarHash = grammarHashForExt(path33.extname(record.path));
12516
+ if (grammarHash === null) return extractFileFacts(record, extractor);
12517
+ if (!deps.disableCache) {
12518
+ const key = factsKey({
12519
+ contentHash: record.hash,
12520
+ language,
12521
+ grammarHash,
12522
+ rev: extractor.rev
12523
+ });
12524
+ const cached = await loadFacts(deps.symbolIndexDir, language, key);
12525
+ if (cached && (!isCsharp || cached.csharp !== void 0)) {
12526
+ return {
12527
+ declarations: cached.declarations,
12528
+ uses: isCsharp ? null : cached.uses,
12529
+ csharp: isCsharp ? cached.csharp : null
12530
+ };
12531
+ }
12532
+ const facts = await extractFileFacts(record, extractor);
12533
+ if (!facts) return null;
12534
+ await writeFacts(deps.symbolIndexDir, language, key, {
12535
+ declarations: facts.declarations,
12536
+ uses: facts.uses ?? [],
12537
+ ...facts.csharp !== null ? { csharp: facts.csharp } : {}
12538
+ });
12539
+ return facts;
12540
+ }
12541
+ return extractFileFacts(record, extractor);
12542
+ }
12207
12543
  const symbolTable = new SymbolTable();
12208
12544
  const recordsByLanguage = /* @__PURE__ */ new Map();
12209
12545
  for (const record of fileRecords) {
@@ -12216,42 +12552,31 @@ async function runRelationPass(graph, projectRoot, deps) {
12216
12552
  }
12217
12553
  list.push(record);
12218
12554
  }
12555
+ const factsByPath = /* @__PURE__ */ new Map();
12556
+ for (const record of fileRecords) {
12557
+ if (!record.language) continue;
12558
+ const extractor = deps.extractorFor(record.language);
12559
+ if (!extractor) continue;
12560
+ const facts = await loadOrExtractFacts(record, extractor);
12561
+ if (facts) factsByPath.set(record.path, facts);
12562
+ }
12219
12563
  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
12564
  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();
12565
+ const facts = factsByPath.get(record.path);
12566
+ if (!facts) continue;
12567
+ for (const decl of facts.declarations) {
12568
+ symbolTable.declare(language, decl.symbolKey, record.path);
12238
12569
  }
12239
12570
  }
12240
- const toPersist = { builtFrom, symbols };
12241
- await writeSymbolIndex(deps.symbolIndexDir, language, toPersist);
12242
12571
  }
12243
12572
  const csharpRecords = recordsByLanguage.get("csharp") ?? [];
12244
12573
  const projectGlobalUsings = /* @__PURE__ */ new Set();
12245
12574
  const projectGlobalUsingAliases = /* @__PURE__ */ new Map();
12246
12575
  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
- }
12576
+ const facts = factsByPath.get(record.path);
12577
+ if (!facts || facts.csharp === null) continue;
12578
+ for (const prefix of facts.csharp.scope.globalPrefixes) projectGlobalUsings.add(prefix);
12579
+ for (const [name, fqn] of facts.csharp.scope.globalAliases) projectGlobalUsingAliases.set(name, fqn);
12255
12580
  }
12256
12581
  const csharpGlobalUsings = [...projectGlobalUsings];
12257
12582
  const csharpGlobalUsingAliases = [...projectGlobalUsingAliases.entries()];
@@ -12278,6 +12603,15 @@ async function runRelationPass(graph, projectRoot, deps) {
12278
12603
  }
12279
12604
  };
12280
12605
  const violationsByNode = /* @__PURE__ */ new Map();
12606
+ const detectedEdgesByNode = /* @__PURE__ */ new Map();
12607
+ const resolveDetected = (record, detected, resolvedDeps) => {
12608
+ for (const dep of detected) {
12609
+ const ownerNode = resolveCandidateGroup(dep.candidates, resolver, record.path, record.language);
12610
+ if (ownerNode !== void 0) {
12611
+ resolvedDeps.push({ fromFile: record.path, line: dep.line, ownerNode });
12612
+ }
12613
+ }
12614
+ };
12281
12615
  for (const [nodeId] of graph.nodes) {
12282
12616
  const records = fileRecords.filter((r) => r.nodeId === nodeId);
12283
12617
  if (records.length === 0) continue;
@@ -12286,23 +12620,28 @@ async function runRelationPass(graph, projectRoot, deps) {
12286
12620
  if (!record.language) continue;
12287
12621
  const extractor = deps.extractorFor(record.language);
12288
12622
  if (!extractor) continue;
12289
- const parsed = await parseSingle(record);
12290
- if (!parsed) continue;
12291
- try {
12292
- const detected = record.language === "csharp" ? csharpUses(parsed, {
12623
+ if (record.language === "csharp") {
12624
+ const facts2 = factsByPath.get(record.path);
12625
+ if (!facts2 || facts2.csharp === null) continue;
12626
+ const detected = assembleCsharpCandidates(facts2.csharp, {
12293
12627
  projectGlobalUsings: csharpGlobalUsings,
12294
12628
  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();
12629
+ });
12630
+ resolveDetected(record, detected, resolvedDeps);
12631
+ continue;
12304
12632
  }
12633
+ const facts = factsByPath.get(record.path);
12634
+ if (!facts || facts.uses === null) continue;
12635
+ resolveDetected(record, facts.uses, resolvedDeps);
12636
+ }
12637
+ const edges = /* @__PURE__ */ new Set();
12638
+ for (const d of resolvedDeps) {
12639
+ const m = d.ownerNode;
12640
+ if (m === nodeId) continue;
12641
+ if (graphView.isAncestorOf(m, nodeId) || graphView.isAncestorOf(nodeId, m)) continue;
12642
+ edges.add(m);
12305
12643
  }
12644
+ if (edges.size > 0) detectedEdgesByNode.set(nodeId, edges);
12306
12645
  const violations = verifyNodeDeps(nodeId, resolvedDeps, graphView);
12307
12646
  if (violations.length) {
12308
12647
  const reason = violations.map((v) => `${v.fromFile}:${v.line} \u2192 undeclared dependency on ${v.ownerNode}`).join("\n");
@@ -12311,7 +12650,7 @@ async function runRelationPass(graph, projectRoot, deps) {
12311
12650
  violationsByNode.set(nodeId, { verdict: "approved", violations: [] });
12312
12651
  }
12313
12652
  }
12314
- return { violationsByNode };
12653
+ return { violationsByNode, factsByPath, detectedEdgesByNode };
12315
12654
  }
12316
12655
 
12317
12656
  // src/relations/extractors/types.ts
@@ -12331,6 +12670,18 @@ function specifierFromStringNode(stringNode) {
12331
12670
  function specifierFromSource(node) {
12332
12671
  return specifierFromStringNode(node.childForFieldName("source"));
12333
12672
  }
12673
+ function specifierFromCallArg(arg) {
12674
+ if (arg.type === "string") return specifierFromStringNode(arg);
12675
+ if (arg.type !== "template_string") return void 0;
12676
+ let fragment;
12677
+ for (let i = 0; i < arg.namedChildCount; i++) {
12678
+ const child = arg.namedChild(i);
12679
+ if (child === null) continue;
12680
+ if (child.type === "template_substitution") return void 0;
12681
+ if (child.type === "string_fragment") fragment = child.text;
12682
+ }
12683
+ return fragment ?? "";
12684
+ }
12334
12685
  function isRelative(specifier) {
12335
12686
  return specifier.startsWith(".") || specifier.startsWith("/");
12336
12687
  }
@@ -12381,7 +12732,8 @@ function uses2(file) {
12381
12732
  const emit = (specifier, node) => {
12382
12733
  if (specifier === void 0 || specifier === "" || !isRelative(specifier)) return;
12383
12734
  const line = node.startPosition.row + 1;
12384
- const dedupKey = `${specifier}\0${line}`;
12735
+ const dedupKey = `${specifier}
12736
+ ${line}`;
12385
12737
  if (seen.has(dedupKey)) return;
12386
12738
  seen.add(dedupKey);
12387
12739
  out.push(single({ kind: "path", specifier }, "import", line));
@@ -12428,8 +12780,8 @@ function uses2(file) {
12428
12780
  const isRequire = fn.type === "identifier" && fn.text === "require";
12429
12781
  if (!isDynamicImport && !isRequire) break;
12430
12782
  const arg = firstArgument(node);
12431
- if (arg === null || arg.type !== "string") break;
12432
- emit(specifierFromStringNode(arg), node);
12783
+ if (arg === null) break;
12784
+ emit(specifierFromCallArg(arg), node);
12433
12785
  break;
12434
12786
  }
12435
12787
  default:
@@ -12470,6 +12822,7 @@ function declarations2(file) {
12470
12822
  }
12471
12823
  var typescriptExtractor = {
12472
12824
  languages: /* @__PURE__ */ new Set(["typescript", "tsx", "javascript"]),
12825
+ rev: 1,
12473
12826
  declarations: declarations2,
12474
12827
  uses: uses2
12475
12828
  };
@@ -12575,6 +12928,7 @@ function declarations3(file) {
12575
12928
  }
12576
12929
  var pythonExtractor = {
12577
12930
  languages: /* @__PURE__ */ new Set(["python"]),
12931
+ rev: 1,
12578
12932
  declarations: declarations3,
12579
12933
  uses: uses3
12580
12934
  };
@@ -12624,6 +12978,7 @@ function declarations4(file) {
12624
12978
  }
12625
12979
  var goExtractor = {
12626
12980
  languages: /* @__PURE__ */ new Set(["go"]),
12981
+ rev: 1,
12627
12982
  declarations: declarations4,
12628
12983
  uses: uses4
12629
12984
  };
@@ -12795,6 +13150,7 @@ function declarations5(file) {
12795
13150
  }
12796
13151
  var javaExtractor = {
12797
13152
  languages: /* @__PURE__ */ new Set(["java"]),
13153
+ rev: 3,
12798
13154
  declarations: declarations5,
12799
13155
  uses: uses5
12800
13156
  };
@@ -12923,6 +13279,7 @@ function declarations6(file) {
12923
13279
  }
12924
13280
  var phpExtractor = {
12925
13281
  languages: /* @__PURE__ */ new Set(["php"]),
13282
+ rev: 1,
12926
13283
  declarations: declarations6,
12927
13284
  uses: uses6
12928
13285
  };
@@ -13047,6 +13404,7 @@ function declarations7(file) {
13047
13404
  }
13048
13405
  var kotlinExtractor = {
13049
13406
  languages: /* @__PURE__ */ new Set(["kotlin"]),
13407
+ rev: 2,
13050
13408
  declarations: declarations7,
13051
13409
  uses: uses7
13052
13410
  };
@@ -13219,6 +13577,7 @@ function declarations8(file) {
13219
13577
  }
13220
13578
  var rustExtractor = {
13221
13579
  languages: /* @__PURE__ */ new Set(["rust"]),
13580
+ rev: 1,
13222
13581
  declarations: declarations8,
13223
13582
  uses: uses8
13224
13583
  };
@@ -13312,6 +13671,7 @@ function functionName(def) {
13312
13671
  }
13313
13672
  var cExtractor = {
13314
13673
  languages: /* @__PURE__ */ new Set(["c"]),
13674
+ rev: 1,
13315
13675
  declarations: declarations9,
13316
13676
  uses: uses9
13317
13677
  };
@@ -13356,6 +13716,7 @@ function functionName2(def) {
13356
13716
  }
13357
13717
  var cppExtractor = {
13358
13718
  languages: /* @__PURE__ */ new Set(["cpp"]),
13719
+ rev: 1,
13359
13720
  declarations: declarations10,
13360
13721
  uses: uses10
13361
13722
  };
@@ -13526,6 +13887,7 @@ function declarations11(file) {
13526
13887
  }
13527
13888
  var rubyExtractor = {
13528
13889
  languages: /* @__PURE__ */ new Set(["ruby"]),
13890
+ rev: 1,
13529
13891
  declarations: declarations11,
13530
13892
  uses: uses11
13531
13893
  };
@@ -13561,12 +13923,6 @@ function extractorForLanguage(language) {
13561
13923
  return byLanguage.get(language);
13562
13924
  }
13563
13925
 
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
13926
  // src/relations/allowed-types.ts
13571
13927
  var RELATION_TYPES3 = [
13572
13928
  "uses",
@@ -13579,15 +13935,16 @@ var RELATION_TYPES3 = [
13579
13935
  function allowedRelationTypes(architecture, fromType, toType) {
13580
13936
  const fromConfig = architecture.node_types[fromType];
13581
13937
  if (!fromConfig) return [];
13582
- const relations = fromConfig.relations;
13938
+ const lists = fromConfig.relations;
13939
+ const def = fromConfig.relationDefault ?? "allow";
13583
13940
  const out = [];
13584
13941
  for (const rt of RELATION_TYPES3) {
13585
- const allowedTargets = relations?.[rt];
13586
- if (!allowedTargets) {
13587
- out.push(rt);
13942
+ const allowedTargets = lists?.[rt];
13943
+ if (allowedTargets === void 0) {
13944
+ if (def === "allow") out.push(rt);
13588
13945
  continue;
13589
13946
  }
13590
- if (allowedTargets.includes(toType)) out.push(rt);
13947
+ if (allowedTargets.includes("*") || allowedTargets.includes(toType)) out.push(rt);
13591
13948
  }
13592
13949
  return out;
13593
13950
  }
@@ -13631,15 +13988,15 @@ ${blocks.join("\n")}`
13631
13988
  }
13632
13989
 
13633
13990
  // 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";
13991
+ import path42 from "path";
13992
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
13636
13993
 
13637
13994
  // src/relations/extractors/typescript-resolve.ts
13638
- import path35 from "path";
13995
+ import path34 from "path";
13639
13996
  function resolveTsPath(specifier, fromFile, exists) {
13640
13997
  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));
13998
+ const baseDir = path34.posix.dirname(toPosix3(fromFile));
13999
+ const joined = path34.posix.normalize(path34.posix.join(baseDir, specifier));
13643
14000
  for (const cand of candidates(joined)) {
13644
14001
  if (exists(cand)) return cand;
13645
14002
  }
@@ -13647,7 +14004,7 @@ function resolveTsPath(specifier, fromFile, exists) {
13647
14004
  }
13648
14005
  function candidates(joined) {
13649
14006
  const out = [];
13650
- const ext = path35.posix.extname(joined);
14007
+ const ext = path34.posix.extname(joined);
13651
14008
  const rewrites = {
13652
14009
  ".js": [".ts", ".tsx", ".js", ".jsx"],
13653
14010
  ".jsx": [".tsx", ".jsx"],
@@ -13662,7 +14019,7 @@ function candidates(joined) {
13662
14019
  } else {
13663
14020
  for (const e of [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]) out.push(joined + e);
13664
14021
  }
13665
- for (const e of [".ts", ".tsx", ".js", ".jsx"]) out.push(path35.posix.join(joined, "index" + e));
14022
+ for (const e of [".ts", ".tsx", ".js", ".jsx"]) out.push(path34.posix.join(joined, "index" + e));
13666
14023
  return out;
13667
14024
  }
13668
14025
  function toPosix3(p2) {
@@ -13670,7 +14027,7 @@ function toPosix3(p2) {
13670
14027
  }
13671
14028
 
13672
14029
  // src/relations/extractors/python-resolve.ts
13673
- import path36 from "path";
14030
+ import path35 from "path";
13674
14031
  function resolvePythonModule(specifier, fromFile, exists) {
13675
14032
  if (specifier.startsWith(".")) {
13676
14033
  return resolveRelative(specifier, fromFile, exists);
@@ -13683,7 +14040,7 @@ function resolveAbsolute(specifier, fromFile, exists) {
13683
14040
  const modulePath = segments.join("/");
13684
14041
  const parentPath = segments.slice(0, -1).join("/");
13685
14042
  const matches = /* @__PURE__ */ new Set();
13686
- for (const dir of ancestorDirs(path36.posix.dirname(toPosix4(fromFile)))) {
14043
+ for (const dir of ancestorDirs(path35.posix.dirname(toPosix4(fromFile)))) {
13687
14044
  const candidates2 = [
13688
14045
  // module-as-file / package at this root
13689
14046
  joinUnder(dir, modulePath + ".py"),
@@ -13707,17 +14064,17 @@ function resolveRelative(specifier, fromFile, exists) {
13707
14064
  if (dotMatch === null) return void 0;
13708
14065
  const dots = dotMatch[0].length;
13709
14066
  const tail = specifier.slice(dots);
13710
- let base = path36.posix.dirname(toPosix4(fromFile));
14067
+ let base = path35.posix.dirname(toPosix4(fromFile));
13711
14068
  for (let i = 0; i < dots - 1; i++) {
13712
- const parent = path36.posix.dirname(base);
14069
+ const parent = path35.posix.dirname(base);
13713
14070
  if (parent === base) return void 0;
13714
14071
  base = parent;
13715
14072
  }
13716
14073
  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);
14074
+ const target = tailPath.length > 0 ? path35.posix.join(base, tailPath) : base;
14075
+ const normalized = path35.posix.normalize(target);
13719
14076
  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")];
14077
+ const candidates2 = tailPath.length > 0 ? [normalized + ".py", path35.posix.join(normalized, "__init__.py")] : [path35.posix.join(normalized, "__init__.py")];
13721
14078
  for (const cand of candidates2) {
13722
14079
  if (exists(cand)) return cand;
13723
14080
  }
@@ -13729,20 +14086,20 @@ function ancestorDirs(dir) {
13729
14086
  for (; ; ) {
13730
14087
  out.push(cur);
13731
14088
  if (cur === "") break;
13732
- const parent = path36.posix.dirname(cur);
14089
+ const parent = path35.posix.dirname(cur);
13733
14090
  cur = parent === "." ? "" : parent;
13734
14091
  }
13735
14092
  return out;
13736
14093
  }
13737
14094
  function joinUnder(dir, sub) {
13738
- return path36.posix.normalize(dir === "" ? sub : path36.posix.join(dir, sub));
14095
+ return path35.posix.normalize(dir === "" ? sub : path35.posix.join(dir, sub));
13739
14096
  }
13740
14097
  function toPosix4(p2) {
13741
14098
  return p2.replace(/\\/g, "/");
13742
14099
  }
13743
14100
 
13744
14101
  // src/relations/extractors/go-resolve.ts
13745
- import path37 from "path";
14102
+ import path36 from "path";
13746
14103
  function resolveGoImport(importPath, fromFile, deps) {
13747
14104
  const resolved = deps.modulePathFor(fromFile);
13748
14105
  if (resolved === void 0) return void 0;
@@ -13756,8 +14113,8 @@ function resolveGoImport(importPath, fromFile, deps) {
13756
14113
  } else {
13757
14114
  return void 0;
13758
14115
  }
13759
- const rel = remainder === "" ? moduleDir : moduleDir === "" ? remainder : path37.posix.join(moduleDir, remainder);
13760
- const repoRelDir = path37.posix.normalize(rel === "" ? "." : rel);
14116
+ const rel = remainder === "" ? moduleDir : moduleDir === "" ? remainder : path36.posix.join(moduleDir, remainder);
14117
+ const repoRelDir = path36.posix.normalize(rel === "" ? "." : rel);
13761
14118
  const cleanDir = repoRelDir === "." ? "" : repoRelDir;
13762
14119
  if (!deps.dirExists(cleanDir)) return void 0;
13763
14120
  const goFiles = deps.goFilesIn(cleanDir);
@@ -13785,7 +14142,7 @@ function resolveGoImport(importPath, fromFile, deps) {
13785
14142
  }
13786
14143
 
13787
14144
  // src/relations/extractors/java-resolve.ts
13788
- import path38 from "path";
14145
+ import path37 from "path";
13789
14146
  function resolveJavaFqn(specifier, fromFile, deps) {
13790
14147
  const segments = specifier.split(".").filter((s) => s.length > 0);
13791
14148
  if (segments.length === 0) return void 0;
@@ -13795,7 +14152,7 @@ function resolveJavaPackageFiles(packageFqn2, fromFile, deps) {
13795
14152
  const segments = packageFqn2.split(".").filter((s) => s.length > 0);
13796
14153
  if (segments.length === 0) return [];
13797
14154
  const pkgDir = segments.join("/");
13798
- for (const dir of ancestorDirs2(path38.posix.dirname(toPosix5(fromFile)))) {
14155
+ for (const dir of ancestorDirs2(path37.posix.dirname(toPosix5(fromFile)))) {
13799
14156
  const repoRelDir = joinUnder2(dir, pkgDir);
13800
14157
  const files = deps.javaFilesIn(repoRelDir);
13801
14158
  if (files.length > 0) return [...files].sort();
@@ -13805,7 +14162,7 @@ function resolveJavaPackageFiles(packageFqn2, fromFile, deps) {
13805
14162
  function resolveType(segments, fromFile, deps) {
13806
14163
  const typePath = segments.join("/") + ".java";
13807
14164
  const parentTypePath = segments.length >= 2 ? segments.slice(0, -1).join("/") + ".java" : void 0;
13808
- for (const dir of ancestorDirs2(path38.posix.dirname(toPosix5(fromFile)))) {
14165
+ for (const dir of ancestorDirs2(path37.posix.dirname(toPosix5(fromFile)))) {
13809
14166
  const candidates2 = [joinUnder2(dir, typePath)];
13810
14167
  if (parentTypePath !== void 0) candidates2.push(joinUnder2(dir, parentTypePath));
13811
14168
  for (const cand of candidates2) {
@@ -13820,20 +14177,20 @@ function ancestorDirs2(dir) {
13820
14177
  for (; ; ) {
13821
14178
  out.push(cur);
13822
14179
  if (cur === "") break;
13823
- const parent = path38.posix.dirname(cur);
14180
+ const parent = path37.posix.dirname(cur);
13824
14181
  cur = parent === "." ? "" : parent;
13825
14182
  }
13826
14183
  return out;
13827
14184
  }
13828
14185
  function joinUnder2(dir, sub) {
13829
- return path38.posix.normalize(dir === "" ? sub : path38.posix.join(dir, sub));
14186
+ return path37.posix.normalize(dir === "" ? sub : path37.posix.join(dir, sub));
13830
14187
  }
13831
14188
  function toPosix5(p2) {
13832
14189
  return p2.replace(/\\/g, "/");
13833
14190
  }
13834
14191
 
13835
14192
  // src/relations/extractors/php-resolve.ts
13836
- import path39 from "path";
14193
+ import path38 from "path";
13837
14194
  function resolvePhpFqn(specifier, fromFile, deps) {
13838
14195
  const fqn = specifier.startsWith("\\") ? specifier.slice(1) : specifier;
13839
14196
  if (fqn === "") return void 0;
@@ -13859,7 +14216,7 @@ function resolvePhpFqn(specifier, fromFile, deps) {
13859
14216
  return [...hits][0];
13860
14217
  }
13861
14218
  function joinUnder3(dir, sub) {
13862
- return path39.posix.normalize(dir === "" ? sub : path39.posix.join(dir, sub));
14219
+ return path38.posix.normalize(dir === "" ? sub : path38.posix.join(dir, sub));
13863
14220
  }
13864
14221
  function parsePsr4(composerJsonText, composerDir) {
13865
14222
  const out = /* @__PURE__ */ new Map();
@@ -13897,19 +14254,19 @@ function parsePsr4(composerJsonText, composerDir) {
13897
14254
  function normalizeDir(value, composerDir) {
13898
14255
  const trimmed = value.replace(/\/+$/, "");
13899
14256
  if (trimmed === "" || trimmed === ".") return composerDir;
13900
- const joined = composerDir === "" ? trimmed : path39.posix.join(composerDir, trimmed);
13901
- const norm = path39.posix.normalize(joined);
14257
+ const joined = composerDir === "" ? trimmed : path38.posix.join(composerDir, trimmed);
14258
+ const norm = path38.posix.normalize(joined);
13902
14259
  return norm === "." ? "" : norm;
13903
14260
  }
13904
14261
 
13905
14262
  // src/relations/extractors/rust-resolve.ts
13906
- import path40 from "path";
14263
+ import path39 from "path";
13907
14264
  function resolveRustPath(specifier, fromFile, exists, deps) {
13908
14265
  const segments = specifier.split("::").filter((s) => s.length > 0);
13909
14266
  if (segments.length === 0) return void 0;
13910
14267
  const root = segments[0];
13911
14268
  const rest = segments.slice(1);
13912
- const fromDir = path40.posix.dirname(toPosix6(fromFile));
14269
+ const fromDir = path39.posix.dirname(toPosix6(fromFile));
13913
14270
  if (root === "super" || root === "self") {
13914
14271
  return resolveRelative2(root, segments, fromFile, exists, deps);
13915
14272
  }
@@ -13929,9 +14286,9 @@ function resolveRelative2(root, segments, fromFile, exists, deps) {
13929
14286
  if (crate === void 0) return void 0;
13930
14287
  const srcDir = crate.srcDir;
13931
14288
  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);
14289
+ const fromDir = path39.posix.dirname(fromPosix);
14290
+ const baseName = path39.posix.basename(fromPosix, ".rs");
14291
+ let moduleDir = baseName === "mod" || baseName === "lib" || baseName === "main" ? fromDir : path39.posix.join(fromDir, baseName);
13935
14292
  let i = 0;
13936
14293
  for (; i < segments.length; i++) {
13937
14294
  const seg = segments[i];
@@ -13939,7 +14296,7 @@ function resolveRelative2(root, segments, fromFile, exists, deps) {
13939
14296
  continue;
13940
14297
  }
13941
14298
  if (seg === "super") {
13942
- const parent = path40.posix.dirname(moduleDir);
14299
+ const parent = path39.posix.dirname(moduleDir);
13943
14300
  if (!withinSrc(parent, srcDir)) return void 0;
13944
14301
  moduleDir = parent;
13945
14302
  continue;
@@ -13971,13 +14328,13 @@ function resolveFromModuleDir(moduleDir, tail, exists) {
13971
14328
  return void 0;
13972
14329
  }
13973
14330
  function withinSrc(dir, srcDir) {
13974
- const d = path40.posix.normalize(dir);
13975
- const s = path40.posix.normalize(srcDir);
14331
+ const d = path39.posix.normalize(dir);
14332
+ const s = path39.posix.normalize(srcDir);
13976
14333
  if (s === "" || s === ".") return !d.startsWith("..");
13977
14334
  return d === s || d.startsWith(s + "/");
13978
14335
  }
13979
14336
  function joinUnder4(dir, sub) {
13980
- const joined = path40.posix.normalize(dir === "" ? sub : path40.posix.join(dir, sub));
14337
+ const joined = path39.posix.normalize(dir === "" ? sub : path39.posix.join(dir, sub));
13981
14338
  if (joined.startsWith("..")) return void 0;
13982
14339
  return joined;
13983
14340
  }
@@ -13986,16 +14343,16 @@ function toPosix6(p2) {
13986
14343
  }
13987
14344
 
13988
14345
  // src/relations/extractors/include-resolve.ts
13989
- import path41 from "path";
14346
+ import path40 from "path";
13990
14347
  function resolveIncludePath(headerPath, fromFile, exists) {
13991
14348
  if (headerPath === "") return void 0;
13992
- const fromDir = path41.posix.dirname(toPosix7(fromFile));
13993
- const relative3 = normalizeRepoRel(path41.posix.join(fromDir, headerPath));
14349
+ const fromDir = path40.posix.dirname(toPosix7(fromFile));
14350
+ const relative3 = normalizeRepoRel(path40.posix.join(fromDir, headerPath));
13994
14351
  if (relative3 !== void 0 && exists(relative3)) return relative3;
13995
14352
  return void 0;
13996
14353
  }
13997
14354
  function normalizeRepoRel(p2) {
13998
- const norm = path41.posix.normalize(p2);
14355
+ const norm = path40.posix.normalize(p2);
13999
14356
  if (norm.startsWith("..")) return void 0;
14000
14357
  return norm;
14001
14358
  }
@@ -14004,13 +14361,13 @@ function toPosix7(p2) {
14004
14361
  }
14005
14362
 
14006
14363
  // src/relations/extractors/ruby-resolve.ts
14007
- import path42 from "path";
14364
+ import path41 from "path";
14008
14365
  function resolveRubyRequireRelative(specifier, fromFile, exists) {
14009
14366
  if (specifier === "") return void 0;
14010
- const fromDir = path42.posix.dirname(toPosix8(fromFile));
14367
+ const fromDir = path41.posix.dirname(toPosix8(fromFile));
14011
14368
  const withExt = /\.rb$/.test(specifier) ? specifier : `${specifier}.rb`;
14012
- const joined = path42.posix.join(fromDir, withExt);
14013
- const normalized = path42.posix.normalize(joined);
14369
+ const joined = path41.posix.join(fromDir, withExt);
14370
+ const normalized = path41.posix.normalize(joined);
14014
14371
  if (normalized.startsWith("..")) return void 0;
14015
14372
  return exists(normalized) ? normalized : void 0;
14016
14373
  }
@@ -14020,7 +14377,7 @@ function toPosix8(p2) {
14020
14377
 
14021
14378
  // src/relations/resolve-path.ts
14022
14379
  function makeResolvePathToFile(projectRoot, ownerOf) {
14023
- const exists = (repoRelPosix) => existsSync7(path43.resolve(projectRoot, repoRelPosix));
14380
+ const exists = (repoRelPosix) => existsSync8(path42.resolve(projectRoot, repoRelPosix));
14024
14381
  const goDeps = makeGoResolveDeps(projectRoot, ownerOf);
14025
14382
  const javaDeps = makeJavaResolveDeps(projectRoot, exists);
14026
14383
  const phpDeps = makePhpResolveDeps(projectRoot, exists);
@@ -14068,10 +14425,10 @@ function makeResolvePathToFile(projectRoot, ownerOf) {
14068
14425
  function makeRustResolveDeps(projectRoot) {
14069
14426
  const byDir = /* @__PURE__ */ new Map();
14070
14427
  function readCrateName(repoRelDir) {
14071
- const abs = path43.join(projectRoot, repoRelDir, "Cargo.toml");
14428
+ const abs = path42.join(projectRoot, repoRelDir, "Cargo.toml");
14072
14429
  let text2;
14073
14430
  try {
14074
- text2 = readFileSync7(abs, "utf-8");
14431
+ text2 = readFileSync8(abs, "utf-8");
14075
14432
  } catch {
14076
14433
  return void 0;
14077
14434
  }
@@ -14089,14 +14446,14 @@ function makeRustResolveDeps(projectRoot) {
14089
14446
  return void 0;
14090
14447
  }
14091
14448
  function crateRootFor(fromFile) {
14092
- let dir = path43.posix.dirname(toPosix9(fromFile));
14449
+ let dir = path42.posix.dirname(toPosix9(fromFile));
14093
14450
  if (dir === ".") dir = "";
14094
14451
  for (; ; ) {
14095
14452
  if (byDir.has(dir)) {
14096
14453
  const cached = byDir.get(dir);
14097
14454
  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");
14455
+ } else if (existsSync8(path42.join(projectRoot, dir, "Cargo.toml"))) {
14456
+ const srcDir = dir === "" ? "src" : path42.posix.join(dir, "src");
14100
14457
  const entry = { srcDir, crateName: readCrateName(dir) };
14101
14458
  byDir.set(dir, entry);
14102
14459
  return entry;
@@ -14104,7 +14461,7 @@ function makeRustResolveDeps(projectRoot) {
14104
14461
  byDir.set(dir, void 0);
14105
14462
  }
14106
14463
  if (dir === "") return void 0;
14107
- const parent = path43.posix.dirname(dir);
14464
+ const parent = path42.posix.dirname(dir);
14108
14465
  dir = parent === "." ? "" : parent;
14109
14466
  }
14110
14467
  }
@@ -14113,10 +14470,10 @@ function makeRustResolveDeps(projectRoot) {
14113
14470
  function makeGoResolveDeps(projectRoot, ownerOf) {
14114
14471
  const moduleByDir = /* @__PURE__ */ new Map();
14115
14472
  function readModulePath(repoRelDir) {
14116
- const abs = path43.join(projectRoot, repoRelDir, "go.mod");
14473
+ const abs = path42.join(projectRoot, repoRelDir, "go.mod");
14117
14474
  let text2;
14118
14475
  try {
14119
- text2 = readFileSync7(abs, "utf-8");
14476
+ text2 = readFileSync8(abs, "utf-8");
14120
14477
  } catch {
14121
14478
  return void 0;
14122
14479
  }
@@ -14129,24 +14486,24 @@ function makeGoResolveDeps(projectRoot, ownerOf) {
14129
14486
  return void 0;
14130
14487
  }
14131
14488
  function modulePathFor(fromFile) {
14132
- let dir = path43.posix.dirname(toPosix9(fromFile));
14489
+ let dir = path42.posix.dirname(toPosix9(fromFile));
14133
14490
  if (dir === ".") dir = "";
14134
14491
  for (; ; ) {
14135
14492
  if (moduleByDir.has(dir)) {
14136
14493
  const cached = moduleByDir.get(dir);
14137
14494
  if (cached !== void 0) return { modulePath: cached, moduleDir: dir };
14138
14495
  } else {
14139
- const mod = existsSync7(path43.join(projectRoot, dir, "go.mod")) ? readModulePath(dir) : void 0;
14496
+ const mod = existsSync8(path42.join(projectRoot, dir, "go.mod")) ? readModulePath(dir) : void 0;
14140
14497
  moduleByDir.set(dir, mod);
14141
14498
  if (mod !== void 0) return { modulePath: mod, moduleDir: dir };
14142
14499
  }
14143
14500
  if (dir === "") return void 0;
14144
- const parent = path43.posix.dirname(dir);
14501
+ const parent = path42.posix.dirname(dir);
14145
14502
  dir = parent === "." ? "" : parent;
14146
14503
  }
14147
14504
  }
14148
14505
  function dirExists(repoRelDir) {
14149
- const abs = path43.resolve(projectRoot, repoRelDir);
14506
+ const abs = path42.resolve(projectRoot, repoRelDir);
14150
14507
  try {
14151
14508
  return statSync3(abs).isDirectory();
14152
14509
  } catch {
@@ -14154,7 +14511,7 @@ function makeGoResolveDeps(projectRoot, ownerOf) {
14154
14511
  }
14155
14512
  }
14156
14513
  function goFilesIn(repoRelDir) {
14157
- const abs = path43.resolve(projectRoot, repoRelDir);
14514
+ const abs = path42.resolve(projectRoot, repoRelDir);
14158
14515
  let entries;
14159
14516
  try {
14160
14517
  entries = readdirSync3(abs, { withFileTypes: true });
@@ -14164,7 +14521,7 @@ function makeGoResolveDeps(projectRoot, ownerOf) {
14164
14521
  const out = [];
14165
14522
  for (const e of entries) {
14166
14523
  if (e.isFile() && e.name.endsWith(".go")) {
14167
- out.push(repoRelDir === "" ? e.name : path43.posix.join(repoRelDir, e.name));
14524
+ out.push(repoRelDir === "" ? e.name : path42.posix.join(repoRelDir, e.name));
14168
14525
  }
14169
14526
  }
14170
14527
  return out;
@@ -14173,7 +14530,7 @@ function makeGoResolveDeps(projectRoot, ownerOf) {
14173
14530
  }
14174
14531
  function makeJavaResolveDeps(projectRoot, exists) {
14175
14532
  function javaFilesIn(repoRelDir) {
14176
- const abs = path43.resolve(projectRoot, repoRelDir);
14533
+ const abs = path42.resolve(projectRoot, repoRelDir);
14177
14534
  let entries;
14178
14535
  try {
14179
14536
  entries = readdirSync3(abs, { withFileTypes: true });
@@ -14183,7 +14540,7 @@ function makeJavaResolveDeps(projectRoot, exists) {
14183
14540
  const out = [];
14184
14541
  for (const e of entries) {
14185
14542
  if (e.isFile() && e.name.endsWith(".java")) {
14186
- out.push(repoRelDir === "" ? e.name : path43.posix.join(repoRelDir, e.name));
14543
+ out.push(repoRelDir === "" ? e.name : path42.posix.join(repoRelDir, e.name));
14187
14544
  }
14188
14545
  }
14189
14546
  return out;
@@ -14193,23 +14550,23 @@ function makeJavaResolveDeps(projectRoot, exists) {
14193
14550
  function makePhpResolveDeps(projectRoot, exists) {
14194
14551
  const psr4ByDir = /* @__PURE__ */ new Map();
14195
14552
  function readPsr4(repoRelDir) {
14196
- const abs = path43.join(projectRoot, repoRelDir, "composer.json");
14553
+ const abs = path42.join(projectRoot, repoRelDir, "composer.json");
14197
14554
  let text2;
14198
14555
  try {
14199
- text2 = readFileSync7(abs, "utf-8");
14556
+ text2 = readFileSync8(abs, "utf-8");
14200
14557
  } catch {
14201
14558
  return /* @__PURE__ */ new Map();
14202
14559
  }
14203
14560
  return parsePsr4(text2, repoRelDir);
14204
14561
  }
14205
14562
  function psr4For(fromFile) {
14206
- let dir = path43.posix.dirname(toPosix9(fromFile));
14563
+ let dir = path42.posix.dirname(toPosix9(fromFile));
14207
14564
  if (dir === ".") dir = "";
14208
14565
  for (; ; ) {
14209
14566
  if (psr4ByDir.has(dir)) {
14210
14567
  const cached = psr4ByDir.get(dir);
14211
14568
  if (cached !== void 0 && cached.size > 0) return cached;
14212
- } else if (existsSync7(path43.join(projectRoot, dir, "composer.json"))) {
14569
+ } else if (existsSync8(path42.join(projectRoot, dir, "composer.json"))) {
14213
14570
  const map = readPsr4(dir);
14214
14571
  psr4ByDir.set(dir, map);
14215
14572
  if (map.size > 0) return map;
@@ -14217,7 +14574,7 @@ function makePhpResolveDeps(projectRoot, exists) {
14217
14574
  psr4ByDir.set(dir, /* @__PURE__ */ new Map());
14218
14575
  }
14219
14576
  if (dir === "") return /* @__PURE__ */ new Map();
14220
- const parent = path43.posix.dirname(dir);
14577
+ const parent = path42.posix.dirname(dir);
14221
14578
  dir = parent === "." ? "" : parent;
14222
14579
  }
14223
14580
  }
@@ -14387,7 +14744,7 @@ function emitPairIssue(vp) {
14387
14744
  async function classifyLogStateFromLock(graph, projectRoot, lock, issues) {
14388
14745
  for (const [nodePath] of graph.nodes) {
14389
14746
  const logRel = `.yggdrasil/model/${nodePath}/log.md`;
14390
- const logAbs = path44.join(projectRoot, logRel);
14747
+ const logAbs = path43.join(projectRoot, logRel);
14391
14748
  let logContent = null;
14392
14749
  try {
14393
14750
  logContent = await readTextFile(logAbs);
@@ -14472,8 +14829,8 @@ function scanUncoveredFiles(graph, gitTrackedFiles) {
14472
14829
  const paths = normalizeMappingPaths(node.meta.mapping);
14473
14830
  allMappings.push(...paths);
14474
14831
  }
14475
- const projectRoot = path44.dirname(graph.rootPath);
14476
- const yggPrefix = toPosixPath(path44.relative(projectRoot, graph.rootPath));
14832
+ const projectRoot = path43.dirname(graph.rootPath);
14833
+ const yggPrefix = toPosixPath(path43.relative(projectRoot, graph.rootPath));
14477
14834
  const uncovered = [];
14478
14835
  const tracked = excludeNestedGraphSubtrees(gitTrackedFiles);
14479
14836
  for (const file of tracked) {
@@ -14495,8 +14852,8 @@ async function scanGitignoredCoveredFiles(graph, gitTrackedFiles) {
14495
14852
  if (!isGlobPattern(raw)) directlyNamed.add(normalizeMappingPath(raw));
14496
14853
  }
14497
14854
  }
14498
- const projectRoot = path44.dirname(graph.rootPath);
14499
- const yggPrefix = toPosixPath(path44.relative(projectRoot, graph.rootPath));
14855
+ const projectRoot = path43.dirname(graph.rootPath);
14856
+ const yggPrefix = toPosixPath(path43.relative(projectRoot, graph.rootPath));
14500
14857
  let gitignoreStack;
14501
14858
  try {
14502
14859
  gitignoreStack = await loadRootGitignoreStack(projectRoot);
@@ -14514,7 +14871,7 @@ async function scanGitignoredCoveredFiles(graph, gitTrackedFiles) {
14514
14871
  if (directlyNamed.has(normalizeMappingPath(normalized))) continue;
14515
14872
  let ignored;
14516
14873
  try {
14517
- ignored = isIgnoredByStack(path44.join(projectRoot, normalized), gitignoreStack);
14874
+ ignored = isIgnoredByStack(path43.join(projectRoot, normalized), gitignoreStack);
14518
14875
  } catch (err) {
14519
14876
  debugWrite(`[check] scanGitignoredCoveredFiles: isIgnoredByStack threw for ${normalized}: ${err.message}`);
14520
14877
  continue;
@@ -14537,7 +14894,7 @@ function buildGitignoredCoveredIssues(offending) {
14537
14894
  }));
14538
14895
  }
14539
14896
  async function runCheck(graph, gitTrackedFiles) {
14540
- const projectRoot = path44.dirname(graph.rootPath);
14897
+ const projectRoot = path43.dirname(graph.rootPath);
14541
14898
  const validation = await validate(graph);
14542
14899
  const validationIssues = validation.issues.filter((vi) => vi.code).map((vi) => ({ ...vi, code: vi.code }));
14543
14900
  const lockIssues = [];
@@ -14560,7 +14917,9 @@ async function runCheck(graph, gitTrackedFiles) {
14560
14917
  const relResult = await runRelationPass(graph, projectRoot, {
14561
14918
  extractorFor: extractorForLanguage,
14562
14919
  resolvePathToFile: makeResolvePathToFile(projectRoot, buildOwnerIndex(graph.nodes).ownerOf),
14563
- symbolIndexDir: relationIndexDir(graph.rootPath)
14920
+ // Repurposed field: now the content-addressed AST fact cache root (.ast-cache), the
14921
+ // per-file parse cache that skips the tree-sitter re-parse of unchanged files.
14922
+ symbolIndexDir: astCacheDir(graph.rootPath)
14564
14923
  });
14565
14924
  for (const [nodeId, nv] of relResult.violationsByNode) {
14566
14925
  if (nv.verdict !== "refused") continue;
@@ -14591,7 +14950,7 @@ async function runCheck(graph, gitTrackedFiles) {
14591
14950
  let coveredFiles = 0;
14592
14951
  let totalFiles = 0;
14593
14952
  if (gitTrackedFiles !== null) {
14594
- const yggPrefix = toPosixPath(path44.relative(projectRoot, graph.rootPath));
14953
+ const yggPrefix = toPosixPath(path43.relative(projectRoot, graph.rootPath));
14595
14954
  const sourceFiles = excludeNestedGraphSubtrees(gitTrackedFiles).filter((f) => {
14596
14955
  const normalized = toPosixPath(f.trim());
14597
14956
  return !normalized.startsWith(yggPrefix + "/") && normalized !== yggPrefix;
@@ -14622,7 +14981,7 @@ async function runCheck(graph, gitTrackedFiles) {
14622
14981
  const advisoryWarnings = allIssues.filter((i) => i.code === "aspect-violation-advisory").length;
14623
14982
  const draftSkipped = countDraftAspectsAcrossGraph(graph);
14624
14983
  return {
14625
- projectName: path44.basename(projectRoot),
14984
+ projectName: path43.basename(projectRoot),
14626
14985
  nodeCount: graph.nodes.size,
14627
14986
  nodeTypeCounts,
14628
14987
  aspectCount: graph.aspects.length,
@@ -14705,7 +15064,7 @@ function computeSuggestedNext(issues) {
14705
15064
  }
14706
15065
 
14707
15066
  // src/core/fill.ts
14708
- import path47 from "path";
15067
+ import path46 from "path";
14709
15068
 
14710
15069
  // src/llm/api-utils.ts
14711
15070
  var ENV_VAR_MAP = {
@@ -15166,13 +15525,13 @@ var GeminiCliProvider = class extends CliAgentProvider {
15166
15525
  registerProvider("gemini-cli", (c) => new GeminiCliProvider({ model: c.model, timeout: c.timeout }));
15167
15526
 
15168
15527
  // src/core/fill-det.ts
15169
- import path45 from "path";
15528
+ import path44 from "path";
15170
15529
 
15171
15530
  // src/core/fill-shared.ts
15172
15531
  async function readBytesOrEmpty(absPath) {
15173
- const { readFile: readFile20 } = await import("fs/promises");
15532
+ const { readFile: readFile21 } = await import("fs/promises");
15174
15533
  try {
15175
- return await readFile20(absPath);
15534
+ return await readFile21(absPath);
15176
15535
  } catch (e) {
15177
15536
  debugWrite(`[fill] readBytesOrEmpty failed for ${absPath}: ${e instanceof Error ? e.message : String(e)}`);
15178
15537
  return Buffer.alloc(0);
@@ -15181,7 +15540,7 @@ async function readBytesOrEmpty(absPath) {
15181
15540
 
15182
15541
  // src/core/fill-det.ts
15183
15542
  async function fillDetPair(graph, projectRoot, pair, aspect) {
15184
- const aspectDirAbs = path45.join(projectRoot, ".yggdrasil", "aspects", aspect.id);
15543
+ const aspectDirAbs = path44.join(projectRoot, ".yggdrasil", "aspects", aspect.id);
15185
15544
  const fullMapping = await computeNodeMappedFiles(graph, pair.nodePath);
15186
15545
  const subjectScope = pair.subjectFiles.length < fullMapping.length ? pair.subjectFiles : void 0;
15187
15546
  const runOnce = async () => {
@@ -15222,7 +15581,7 @@ async function fillDetPair(graph, projectRoot, pair, aspect) {
15222
15581
  const observations = run.result.observations;
15223
15582
  const files = [];
15224
15583
  for (const rel of pair.subjectFiles) {
15225
- const abs = path45.resolve(projectRoot, rel);
15584
+ const abs = path44.resolve(projectRoot, rel);
15226
15585
  const bytes = await readBytesOrEmpty(abs);
15227
15586
  files.push([rel, hashBytes(bytes)]);
15228
15587
  }
@@ -15254,7 +15613,7 @@ function detRuntimeNotice(aspectId, unitKey, reason) {
15254
15613
  }
15255
15614
 
15256
15615
  // src/core/fill-llm.ts
15257
- import path46 from "path";
15616
+ import path45 from "path";
15258
15617
 
15259
15618
  // src/llm/aspect-verifier.ts
15260
15619
  async function verifyWithConsensus(provider, prompt, consensus) {
@@ -15283,14 +15642,14 @@ async function verifyWithConsensus(provider, prompt, consensus) {
15283
15642
  async function fillLlmPair(graph, projectRoot, pair, aspect, tier, tierName, mergedTier, provider, referencesCache) {
15284
15643
  const subjects = [];
15285
15644
  for (const rel of pair.subjectFiles) {
15286
- const bytes = await readBytesOrEmpty(path46.resolve(projectRoot, rel));
15645
+ const bytes = await readBytesOrEmpty(path45.resolve(projectRoot, rel));
15287
15646
  subjects.push({ path: rel, bytes });
15288
15647
  }
15289
15648
  const refInputs = aspect.references ?? [];
15290
15649
  const referencesForHash = [];
15291
15650
  const referencesForPrompt = [];
15292
15651
  for (const ref of refInputs) {
15293
- const absRef = path46.resolve(projectRoot, ref.path);
15652
+ const absRef = path45.resolve(projectRoot, ref.path);
15294
15653
  let bytes = referencesCache.get(absRef);
15295
15654
  if (bytes === void 0) {
15296
15655
  bytes = await readFileBytes(absRef);
@@ -15770,7 +16129,7 @@ async function runFill(graph, opts) {
15770
16129
  });
15771
16130
  const emitIssue = opts.emitIssue ?? (() => {
15772
16131
  });
15773
- const projectRoot = path47.dirname(graph.rootPath);
16132
+ const projectRoot = path46.dirname(graph.rootPath);
15774
16133
  const onlyDeterministic = opts.onlyDeterministic ?? false;
15775
16134
  const dryRun = opts.dryRun ?? false;
15776
16135
  const isTTY2 = opts.isTTY ?? (process.stderr.isTTY ?? false);
@@ -16050,7 +16409,7 @@ async function runFill(graph, opts) {
16050
16409
  }
16051
16410
 
16052
16411
  // src/cli/check.ts
16053
- import path48 from "path";
16412
+ import path47 from "path";
16054
16413
 
16055
16414
  // src/cli/group-issues.ts
16056
16415
  var CODE_ONLY_GROUP_CODES = /* @__PURE__ */ new Set(["unverified"]);
@@ -16112,6 +16471,8 @@ function groupIssues(issues) {
16112
16471
  const rep = sorted[0];
16113
16472
  const nodes = new Set(sorted.map((m) => m.nodePath ?? ""));
16114
16473
  const isCodeOnly = CODE_ONLY_GROUP_CODES.has(rep.code);
16474
+ const distinctNext = new Set(sorted.map((m) => m.messageData.next ?? ""));
16475
+ const distinctWhy = new Set(sorted.map((m) => m.messageData.why ?? ""));
16115
16476
  groups.push({
16116
16477
  code: rep.code,
16117
16478
  aspectId: isCodeOnly ? void 0 : rep.aspectId,
@@ -16122,6 +16483,8 @@ function groupIssues(issues) {
16122
16483
  sharedWhy: rep.messageData.why,
16123
16484
  sharedNext: rep.messageData.next,
16124
16485
  perMemberReason: FULL_WHAT_CODES.has(rep.code),
16486
+ divergentNext: distinctNext.size > 1,
16487
+ divergentWhy: distinctWhy.size > 1,
16125
16488
  members: sorted
16126
16489
  });
16127
16490
  }
@@ -16156,12 +16519,12 @@ function resolveApproveMode(opts, config) {
16156
16519
  return { approve: false, onlyDeterministic: false };
16157
16520
  }
16158
16521
  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) => {
16522
+ 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
16523
  try {
16161
16524
  const cwd = process.cwd();
16162
16525
  const graph = await loadGraphOrAbort(cwd, { tolerateInvalidConfig: true });
16163
16526
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
16164
- const projectRoot = path48.dirname(graph.rootPath);
16527
+ const projectRoot = path47.dirname(graph.rootPath);
16165
16528
  const gitFiles = await walkRepoFiles(projectRoot);
16166
16529
  const wantsTop = opts.top !== void 0;
16167
16530
  if (wantsTop && opts.summary) {
@@ -16182,6 +16545,16 @@ function registerCheckCommand(program2) {
16182
16545
  await exitAfterFlush(1);
16183
16546
  return;
16184
16547
  }
16548
+ if (opts.onlyDeterministic && (wantsTop || opts.summary || opts.details || opts.aspect !== void 0)) {
16549
+ const viewFlag = wantsTop ? "--top" : opts.summary ? "--summary" : opts.details ? "--details" : "--aspect";
16550
+ process.stderr.write(chalk8.red(`Error: ${buildIssueMessage({
16551
+ what: `${viewFlag} cannot be combined with --only-deterministic.`,
16552
+ 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.`,
16553
+ next: `Run: yg check ${viewFlag}${opts.aspect !== void 0 ? " <id>" : wantsTop ? " <n>" : ""} (read-only view), or yg check --approve --only-deterministic (deterministic fill).`
16554
+ })}`) + "\n");
16555
+ await exitAfterFlush(1);
16556
+ return;
16557
+ }
16185
16558
  if (opts.details && (wantsTop || opts.summary)) {
16186
16559
  process.stderr.write(chalk8.red(`Error: ${buildIssueMessage({
16187
16560
  what: "--details cannot be combined with --top or --summary.",
@@ -16229,6 +16602,18 @@ function registerCheckCommand(program2) {
16229
16602
  await exitAfterFlush(1);
16230
16603
  return;
16231
16604
  }
16605
+ const knownAspectIds = (graph.aspects ?? []).map((a) => a.id);
16606
+ if (!knownAspectIds.includes(opts.aspect)) {
16607
+ const idList = knownAspectIds.slice().sort((a, b) => a.localeCompare(b, "en"));
16608
+ const known = idList.length === 0 ? "The graph defines no aspects." : idList.length <= 30 ? `Known aspect ids: ${idList.join(", ")}.` : `The graph defines ${idList.length} aspects.`;
16609
+ process.stderr.write(chalk8.red(`Error: ${buildIssueMessage({
16610
+ what: `Unknown aspect '${opts.aspect}'.`,
16611
+ 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}`,
16612
+ next: "Run: yg aspects (list every aspect id), then yg check --aspect <id> with a real id; or yg check (full wall)."
16613
+ })}`) + "\n");
16614
+ await exitAfterFlush(1);
16615
+ return;
16616
+ }
16232
16617
  }
16233
16618
  let view = { kind: "full" };
16234
16619
  if (opts.aspect !== void 0) {
@@ -16274,9 +16659,9 @@ function registerCheckCommand(program2) {
16274
16659
  gitTrackedFiles: gitFiles,
16275
16660
  onlyDeterministic: mode.onlyDeterministic,
16276
16661
  dryRun: isDryRun,
16277
- write: isQuiet ? () => {
16278
- } : isDryRun ? (s) => {
16662
+ write: isDryRun ? (s) => {
16279
16663
  process.stdout.write(s);
16664
+ } : isQuiet ? () => {
16280
16665
  } : (s) => {
16281
16666
  process.stderr.write(s);
16282
16667
  },
@@ -16375,9 +16760,9 @@ function formatOutput(result, view = { kind: "full" }, autoFilled = false, emoji
16375
16760
  const nextCmd = firstFiltered.messageData.next.split("\n")[0];
16376
16761
  sections.push("");
16377
16762
  sections.push(`Next (this group): ${nextCmd}`);
16763
+ sections.push("");
16764
+ return sections.join("\n");
16378
16765
  }
16379
- sections.push("");
16380
- return sections.join("\n");
16381
16766
  } else if (view.kind === "details") {
16382
16767
  if (errors.length > 0) {
16383
16768
  sections.push("");
@@ -16618,12 +17003,23 @@ function glossLabel(label) {
16618
17003
  function renderGroup(group, lines, opts) {
16619
17004
  const aspectSeg = group.aspectId ? ` aspect '${group.aspectId}'` : "";
16620
17005
  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) {
17006
+ if (group.sharedWhy && !group.divergentWhy) lines.push(`${BLOCK_INDENT}${group.sharedWhy}`);
17007
+ if (group.sharedNext && !group.divergentNext) {
16623
17008
  const nextLines = group.sharedNext.split("\n");
16624
17009
  lines.push(`${BLOCK_INDENT}Fix: ${nextLines[0]}`);
16625
17010
  for (const extra of nextLines.slice(1)) lines.push(`${BLOCK_INDENT}${extra}`);
16626
17011
  }
17012
+ const MEMBER_DETAIL_INDENT = `${BLOCK_INDENT} `;
17013
+ const emitDivergentDetail = (m) => {
17014
+ if (group.divergentWhy && m.messageData.why) {
17015
+ lines.push(`${MEMBER_DETAIL_INDENT}Why: ${m.messageData.why.split("\n")[0]}`);
17016
+ }
17017
+ if (group.divergentNext && m.messageData.next) {
17018
+ const nextLines = m.messageData.next.split("\n");
17019
+ lines.push(`${MEMBER_DETAIL_INDENT}Fix: ${nextLines[0]}`);
17020
+ for (const extra of nextLines.slice(1)) lines.push(`${MEMBER_DETAIL_INDENT}${extra}`);
17021
+ }
17022
+ };
16627
17023
  const members = group.members;
16628
17024
  const truncate = opts.isTTY && members.length > CAP_NODES2;
16629
17025
  const shown = truncate ? members.slice(0, CAP_NODES2) : members;
@@ -16639,10 +17035,12 @@ function renderGroup(group, lines, opts) {
16639
17035
  lines.push(`${BLOCK_INDENT} ${extra}`);
16640
17036
  }
16641
17037
  }
17038
+ emitDivergentDetail(m);
16642
17039
  } else {
16643
17040
  const memberAspectSeg = group.aspectId === void 0 && m.aspectId !== void 0 ? ` aspect '${m.aspectId}'` : "";
16644
17041
  const whatSeg = !memberAspectSeg && m.messageData.what ? ` ${m.messageData.what.split("\n")[0]}` : "";
16645
17042
  lines.push(`${BLOCK_INDENT}- ${node}${memberAspectSeg || whatSeg}`);
17043
+ emitDivergentDetail(m);
16646
17044
  }
16647
17045
  }
16648
17046
  if (truncate) {
@@ -16652,10 +17050,10 @@ function renderGroup(group, lines, opts) {
16652
17050
  }
16653
17051
 
16654
17052
  // src/cli/aspect-test.ts
16655
- import path50 from "path";
17053
+ import path49 from "path";
16656
17054
 
16657
17055
  // src/ast/runner.ts
16658
- import path49 from "path";
17056
+ import path48 from "path";
16659
17057
  import { readFile as readFile16 } from "fs/promises";
16660
17058
  import { pathToFileURL as pathToFileURL3 } from "url";
16661
17059
  var AstRunnerError = class extends Error {
@@ -16672,7 +17070,7 @@ ${data.next}`);
16672
17070
  };
16673
17071
  async function runAstAspect(params) {
16674
17072
  ensureLoaderRegistered();
16675
- const checkPath = path49.resolve(params.projectRoot, params.aspectDir, "check.mjs");
17073
+ const checkPath = path48.resolve(params.projectRoot, params.aspectDir, "check.mjs");
16676
17074
  let mod;
16677
17075
  try {
16678
17076
  mod = await import(pathToFileURL3(checkPath).href);
@@ -16704,8 +17102,8 @@ async function runAstAspect(params) {
16704
17102
  sourceFiles.push({ path: f.path, content: cached.content, ast: cached.ast });
16705
17103
  continue;
16706
17104
  }
16707
- const content20 = await readFile16(path49.resolve(params.projectRoot, f.path), "utf-8");
16708
- if (getLanguageForExtension(path49.extname(f.path).toLowerCase()) === null) {
17105
+ const content20 = await readFile16(path48.resolve(params.projectRoot, f.path), "utf-8");
17106
+ if (getLanguageForExtension(path48.extname(f.path).toLowerCase()) === null) {
16709
17107
  sourceFiles.push({ path: f.path, content: content20, ast: void 0 });
16710
17108
  continue;
16711
17109
  }
@@ -16891,7 +17289,7 @@ function registerAspectTestCommand(program2) {
16891
17289
  process.exit(1);
16892
17290
  return;
16893
17291
  }
16894
- const aspectDir = path50.join(".yggdrasil", "aspects", aspect.id);
17292
+ const aspectDir = path49.join(".yggdrasil", "aspects", aspect.id);
16895
17293
  if (hasNode) {
16896
17294
  const nodePath = opts.node.trim().replace(/\/$/, "");
16897
17295
  const node = graph.nodes.get(nodePath);
@@ -16951,12 +17349,17 @@ function registerAspectTestCommand(program2) {
16951
17349
  await exitAfterFlush(1);
16952
17350
  } catch (e) {
16953
17351
  debugWrite(`[aspect-test] run failed: ${e instanceof Error ? e.message : String(e)}`);
17352
+ if (e instanceof StructureRunnerError || e instanceof AstRunnerError) {
17353
+ process.stderr.write(buildIssueMessage(e.messageData) + "\n");
17354
+ await exitAfterFlush(1);
17355
+ return;
17356
+ }
16954
17357
  abortOnUnexpectedError(e, "running aspect-test");
16955
17358
  }
16956
17359
  });
16957
17360
  }
16958
17361
  async function resolveCompanionsForTest(graph, projectRoot, pair, aspect) {
16959
- const aspectDirAbs = path50.join(projectRoot, ".yggdrasil", "aspects", aspect.id);
17362
+ const aspectDirAbs = path49.join(projectRoot, ".yggdrasil", "aspects", aspect.id);
16960
17363
  const fullMapping = await computeNodeMappedFiles(graph, pair.nodePath);
16961
17364
  const subjectScope = pair.subjectFiles.length < fullMapping.length ? pair.subjectFiles : void 0;
16962
17365
  const run = await runCompanionHook({
@@ -17032,7 +17435,7 @@ async function runLlmAspectTest(graph, projectRoot, aspect, nodePath, dryRun) {
17032
17435
  const refInputs = aspect.references ?? [];
17033
17436
  const referencesForPrompt = [];
17034
17437
  for (const ref of refInputs) {
17035
- const absRef = path50.resolve(projectRoot, ref.path);
17438
+ const absRef = path49.resolve(projectRoot, ref.path);
17036
17439
  let content20;
17037
17440
  try {
17038
17441
  content20 = await readTextFile(absRef);
@@ -17078,7 +17481,7 @@ async function runLlmAspectTest(graph, projectRoot, aspect, nodePath, dryRun) {
17078
17481
  for (const rel of pair.subjectFiles) {
17079
17482
  let content20;
17080
17483
  try {
17081
- const bytes = await import("fs/promises").then((fs5) => fs5.readFile(path50.resolve(projectRoot, rel)));
17484
+ const bytes = await import("fs/promises").then((fs5) => fs5.readFile(path49.resolve(projectRoot, rel)));
17082
17485
  content20 = bytes.toString("utf8");
17083
17486
  } catch (e) {
17084
17487
  debugWrite(`[aspect-test] subject file read failed for ${rel} on ${pair.unitKey}: ${e instanceof Error ? e.message : String(e)}`);
@@ -17134,7 +17537,7 @@ async function runLlmAspectTest(graph, projectRoot, aspect, nodePath, dryRun) {
17134
17537
  for (const rel of pair.subjectFiles) {
17135
17538
  let content20;
17136
17539
  try {
17137
- const bytes = await import("fs/promises").then((fs5) => fs5.readFile(path50.resolve(projectRoot, rel)));
17540
+ const bytes = await import("fs/promises").then((fs5) => fs5.readFile(path49.resolve(projectRoot, rel)));
17138
17541
  content20 = bytes.toString("utf8");
17139
17542
  } catch (e) {
17140
17543
  debugWrite(`[aspect-test] subject file read failed for ${rel} on ${pair.unitKey}: ${e instanceof Error ? e.message : String(e)}`);
@@ -17250,10 +17653,10 @@ function printStructureViolations(violations) {
17250
17653
  // src/cli/log.ts
17251
17654
  import chalk9 from "chalk";
17252
17655
  import { readFile as readFile18, stat as stat7 } from "fs/promises";
17253
- import path54 from "path";
17656
+ import path53 from "path";
17254
17657
 
17255
17658
  // src/core/log/log-add.ts
17256
- import path51 from "path";
17659
+ import path50 from "path";
17257
17660
 
17258
17661
  // src/utils/node-path-validator.ts
17259
17662
  function validateNodePath(raw) {
@@ -17366,7 +17769,7 @@ async function logAdd(input) {
17366
17769
  }
17367
17770
  };
17368
17771
  }
17369
- const logPath2 = path51.join(graph.rootPath, "model", nodePath, "log.md");
17772
+ const logPath2 = path50.join(graph.rootPath, "model", nodePath, "log.md");
17370
17773
  const stats = await statLogFile(logPath2);
17371
17774
  if (stats !== null) {
17372
17775
  if (stats.isSymbolicLink) {
@@ -17435,7 +17838,7 @@ function reasonHasLevel2HeaderOutsideFence(reason) {
17435
17838
  }
17436
17839
 
17437
17840
  // src/core/log/log-read.ts
17438
- import path52 from "path";
17841
+ import path51 from "path";
17439
17842
  var DEFAULT_TOP = 10;
17440
17843
  async function logRead(input) {
17441
17844
  const { graph } = input;
@@ -17481,7 +17884,7 @@ async function logRead(input) {
17481
17884
  }
17482
17885
  };
17483
17886
  }
17484
- const logPath2 = path52.join(graph.rootPath, "model", nodePath, "log.md");
17887
+ const logPath2 = path51.join(graph.rootPath, "model", nodePath, "log.md");
17485
17888
  const content20 = await readLogSafe(logPath2);
17486
17889
  if (content20 === "") {
17487
17890
  return { ok: true, entries: [] };
@@ -17504,8 +17907,8 @@ async function logRead(input) {
17504
17907
  }
17505
17908
 
17506
17909
  // src/core/log/log-merge-resolve.ts
17507
- import { createHash as createHash4 } from "crypto";
17508
- import path53 from "path";
17910
+ import { createHash as createHash7 } from "crypto";
17911
+ import path52 from "path";
17509
17912
 
17510
17913
  // src/utils/git-introspect.ts
17511
17914
  import { exec } from "child_process";
@@ -17585,7 +17988,7 @@ async function logMergeResolve(input) {
17585
17988
  }
17586
17989
  };
17587
17990
  }
17588
- const logPath2 = path53.join(yggRoot, "model", nodePath, "log.md");
17991
+ const logPath2 = path52.join(yggRoot, "model", nodePath, "log.md");
17589
17992
  let currentLog;
17590
17993
  try {
17591
17994
  currentLog = await readTextFile(logPath2);
@@ -17633,7 +18036,7 @@ async function logMergeResolve(input) {
17633
18036
  const p2New = parseLog(parent2Log).slice(ancestorEntries.length);
17634
18037
  const currentEntries = parseLog(currentLog);
17635
18038
  const currentNew = currentEntries.slice(ancestorEntries.length);
17636
- const entryKey = (e) => createHash4("sha256").update(`${e.datetime}
18039
+ const entryKey = (e) => createHash7("sha256").update(`${e.datetime}
17637
18040
  ${e.body}`).digest("hex");
17638
18041
  const parentNew = [...p1New, ...p2New];
17639
18042
  const parentNewKeys = new Set(parentNew.map(entryKey));
@@ -17792,7 +18195,7 @@ ${entry.body}`);
17792
18195
  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
18196
  try {
17794
18197
  const graph = await loadGraphOrAbort(process.cwd(), { tolerateInvalidConfig: true });
17795
- const repoRoot = path54.dirname(graph.rootPath);
18198
+ const repoRoot = path53.dirname(graph.rootPath);
17796
18199
  const nodePath = toPosix(opts.node.trim()).replace(/\/$/, "");
17797
18200
  const result = await logMergeResolve({ graph, nodePath, repoRoot });
17798
18201
  if (!result.ok) {
@@ -17817,15 +18220,15 @@ import chalk10 from "chalk";
17817
18220
 
17818
18221
  // src/io/find-index.ts
17819
18222
  import { readFile as readFile19, lstat as lstat3 } from "fs/promises";
17820
- import path55 from "path";
18223
+ import path54 from "path";
17821
18224
  import MiniSearch from "minisearch";
17822
18225
  var MAX_BODY_BYTES = 1048576;
17823
18226
  async function buildIndex(graph) {
17824
- const projectRoot = path55.dirname(graph.rootPath);
18227
+ const projectRoot = path54.dirname(graph.rootPath);
17825
18228
  const docs = [];
17826
18229
  for (const [nodePath, node] of graph.nodes) {
17827
18230
  const displayPath = `model/${nodePath}/`;
17828
- const logPath2 = path55.join(graph.rootPath, "model", nodePath, "log.md");
18231
+ const logPath2 = path54.join(graph.rootPath, "model", nodePath, "log.md");
17829
18232
  let body = "";
17830
18233
  try {
17831
18234
  const st = await lstat3(logPath2);
@@ -17842,7 +18245,7 @@ This does not affect append-only integrity. No action required.
17842
18245
  }
17843
18246
  body = truncated;
17844
18247
  } else if (st.isSymbolicLink()) {
17845
- process.stderr.write(`Warning: skipping symlinked log.md at ${path55.relative(projectRoot, logPath2)}
18248
+ process.stderr.write(`Warning: skipping symlinked log.md at ${path54.relative(projectRoot, logPath2)}
17846
18249
  `);
17847
18250
  } else {
17848
18251
  }
@@ -18010,18 +18413,18 @@ function registerFindCommand(program2) {
18010
18413
 
18011
18414
  // src/cli/type-suggest.ts
18012
18415
  import chalk11 from "chalk";
18013
- import { existsSync as existsSync8 } from "fs";
18416
+ import { existsSync as existsSync9 } from "fs";
18014
18417
  import { resolve as resolve5 } from "path";
18015
18418
 
18016
18419
  // src/core/type-classifier.ts
18017
- import path56 from "path";
18420
+ import path55 from "path";
18018
18421
  async function classifyFile(absPath, repoRelPath, graph, cache) {
18019
18422
  const matches = [];
18020
18423
  const partialScores = [];
18021
18424
  const ctx = {
18022
18425
  absPath,
18023
18426
  repoRelPath,
18024
- projectRoot: path56.dirname(graph.rootPath),
18427
+ projectRoot: path55.dirname(graph.rootPath),
18025
18428
  cache
18026
18429
  };
18027
18430
  for (const [typeId, def] of Object.entries(graph.architecture.node_types)) {
@@ -18077,7 +18480,7 @@ Type matching does not apply here.
18077
18480
  return;
18078
18481
  }
18079
18482
  const gitignoreStack = await loadRootGitignoreStack(projectRoot);
18080
- if (existsSync8(absPath) && isIgnoredByStack(absPath, gitignoreStack)) {
18483
+ if (existsSync9(absPath) && isIgnoredByStack(absPath, gitignoreStack)) {
18081
18484
  process.stderr.write(
18082
18485
  chalk11.yellow(
18083
18486
  `
@@ -18089,7 +18492,7 @@ file-mapping-gitignored. Proceeding with classification for context.
18089
18492
  )
18090
18493
  );
18091
18494
  }
18092
- if (!existsSync8(absPath)) {
18495
+ if (!existsSync9(absPath)) {
18093
18496
  process.stdout.write(`
18094
18497
  (File does not exist \u2014 evaluating path predicates only)
18095
18498
 
@@ -18317,6 +18720,10 @@ Examples:
18317
18720
  Plain (non-glob) entries remain unchanged: an exact file path or a directory
18318
18721
  prefix (e.g. \`src/handlers\`) covers that file or all files beneath it.
18319
18722
 
18723
+ ## Allowed relations and default policy
18724
+
18725
+ 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.
18726
+
18320
18727
  ## log_required \u2014 when to enable the log gate
18321
18728
 
18322
18729
  Each node type may set \`log_required\` (default \`false\`). When \`true\`, a node of
@@ -20552,7 +20959,8 @@ to the \`.yggdrasil/\` directory):
20552
20959
 
20553
20960
  \`\`\`
20554
20961
  yg-secrets.yaml # provider API keys
20555
- .symbols-cache/ # the relation pass's per-language symbol-index cache
20962
+ .symbols-cache/ # the relation pass's legacy per-language symbol-index cache
20963
+ .ast-cache/ # the relation pass's content-addressed per-file AST fact cache
20556
20964
  .debug.log # the opt-in command debug log
20557
20965
  \`\`\`
20558
20966
 
@@ -21231,9 +21639,7 @@ Event relations must be paired: if A emits to B, B must declare a
21231
21639
 
21232
21640
  ## Architecture controls allowed relations
21233
21641
 
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.
21642
+ 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
21643
 
21238
21644
  When a needed relation is not allowed by the architecture:
21239
21645
  1. Use a different relation type that IS allowed
@@ -22243,8 +22649,23 @@ node_types:
22243
22649
 
22244
22650
  parents: [<type-id>, ...] # optional \u2014 allowed parent node types in the hierarchy.
22245
22651
 
22246
- relations: # optional \u2014 allowed relation targets by relation type.
22247
- <relation-type>: [<type-id>, ...] # calls | uses | extends | implements | emits | listens
22652
+ relations: # optional \u2014 per-relation-type allow-list.
22653
+ # A relation type is constrained by listing its allowed target node types.
22654
+ # uses: [domain, data-access] \u2192 only those target types
22655
+ # uses: ['*'] \u2192 any target type
22656
+ # uses: [] \u2192 no target (relation type forbidden)
22657
+ # (relation type omitted) \u2192 governed by \`default\` below
22658
+ # default: allow | deny \u2192 policy for relation types NOT listed.
22659
+ # omitted \u21D2 allow (every unlisted relation type may target any type).
22660
+ # deny \u21D2 unlisted relation types target nothing (a sink).
22661
+ # Note: '*' is reserved as the any-target wildcard in relation lists and
22662
+ # must not be used as a node-type name (the parser rejects it).
22663
+ # Examples:
22664
+ # { default: deny } \u2192 pure sink
22665
+ # { default: deny, listens: ['*'] } \u2192 sink that may listen to anything
22666
+ # { default: allow, uses: [] } \u2192 everything open except \`uses\`
22667
+ uses: [<target-type>, ...]
22668
+ default: allow
22248
22669
  `;
22249
22670
 
22250
22671
  // src/templates/schemas/config.ts
@@ -22423,9 +22844,12 @@ function registerSchemasCommand(program2) {
22423
22844
  }
22424
22845
 
22425
22846
  // src/cli/suppressions.ts
22426
- import chalk14 from "chalk";
22427
- import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
22428
22847
  import path57 from "path";
22848
+
22849
+ // src/portal/api/suppress-scan.ts
22850
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
22851
+ import path56 from "path";
22852
+ import chalk14 from "chalk";
22429
22853
  function isBinaryContent(buf) {
22430
22854
  const checkLen = Math.min(buf.length, 8192);
22431
22855
  for (let i = 0; i < checkLen; i++) {
@@ -22448,7 +22872,7 @@ function isNoiseFile(relFile) {
22448
22872
  return false;
22449
22873
  }
22450
22874
  async function scanMarkersForFile(relFile, text2) {
22451
- const ext = path57.extname(relFile).toLowerCase();
22875
+ const ext = path56.extname(relFile).toLowerCase();
22452
22876
  if (getLanguageForExtension(ext) === null) {
22453
22877
  return scanSuppressionMarkers(text2);
22454
22878
  }
@@ -22470,11 +22894,11 @@ async function runSuppressionsScan(projectRoot, gitTrackedFiles, knownAspectIds)
22470
22894
  const openDisables = /* @__PURE__ */ new Map();
22471
22895
  for (const relFile of gitTrackedFiles) {
22472
22896
  if (isNoiseFile(relFile)) continue;
22473
- const absFile = path57.join(projectRoot, relFile);
22474
- if (!existsSync9(absFile)) continue;
22897
+ const absFile = path56.join(projectRoot, relFile);
22898
+ if (!existsSync10(absFile)) continue;
22475
22899
  let buf;
22476
22900
  try {
22477
- buf = readFileSync8(absFile);
22901
+ buf = readFileSync9(absFile);
22478
22902
  } catch (error) {
22479
22903
  debugWrite(`[suppressions] read fallback: ${relFile}: ${error instanceof Error ? error.message : String(error)}`);
22480
22904
  continue;
@@ -22569,6 +22993,53 @@ function formatSuppressionsOutput(report2) {
22569
22993
  }
22570
22994
  return lines.join("\n") + "\n";
22571
22995
  }
22996
+ function scanPortalSuppressions(report2, knownAspectIds, draftAspectIds) {
22997
+ const unboundedByFile = /* @__PURE__ */ new Map();
22998
+ for (const { file, markers } of report2.fileEntries) {
22999
+ const disableStack = /* @__PURE__ */ new Map();
23000
+ for (const m of markers) {
23001
+ if (m.kind === "disable") {
23002
+ const stack = disableStack.get(m.aspectId) ?? [];
23003
+ stack.push(m.line);
23004
+ disableStack.set(m.aspectId, stack);
23005
+ } else if (m.kind === "enable") {
23006
+ const stack = disableStack.get(m.aspectId);
23007
+ if (stack && stack.length > 0) {
23008
+ stack.pop();
23009
+ if (stack.length === 0) disableStack.delete(m.aspectId);
23010
+ }
23011
+ }
23012
+ }
23013
+ const open = /* @__PURE__ */ new Set();
23014
+ for (const lines of disableStack.values()) for (const l of lines) open.add(l);
23015
+ if (open.size > 0) unboundedByFile.set(file, open);
23016
+ }
23017
+ const out = [];
23018
+ for (const { file, markers } of report2.fileEntries) {
23019
+ const open = unboundedByFile.get(file);
23020
+ for (const m of markers) {
23021
+ if (m.kind === "enable") continue;
23022
+ const risk = resolveRisk(m, file, open, knownAspectIds, draftAspectIds);
23023
+ out.push({
23024
+ file,
23025
+ line: m.line,
23026
+ aspectId: m.aspectId,
23027
+ reason: m.reason,
23028
+ ...risk ? { risk } : {}
23029
+ });
23030
+ }
23031
+ }
23032
+ return out;
23033
+ }
23034
+ function resolveRisk(m, file, openLines, knownAspectIds, draftAspectIds) {
23035
+ if (m.wildcard) return "wildcard";
23036
+ if (!knownAspectIds.has(m.aspectId)) return "typo";
23037
+ if (draftAspectIds.has(m.aspectId)) return "inert";
23038
+ if (m.kind === "disable" && openLines?.has(m.line)) return "unbounded";
23039
+ return void 0;
23040
+ }
23041
+
23042
+ // src/cli/suppressions.ts
22572
23043
  function registerSuppressionsCommand(program2) {
22573
23044
  program2.command("suppressions").description("Inventory active yg-suppress waivers and warn about footguns").action(async () => {
22574
23045
  try {
@@ -22586,14 +23057,1130 @@ function registerSuppressionsCommand(program2) {
22586
23057
  });
22587
23058
  }
22588
23059
 
22589
- // src/bin.ts
22590
- import { readFileSync as readFileSync9 } from "fs";
23060
+ // src/cli/portal.ts
23061
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
23062
+ import path61 from "path";
23063
+
23064
+ // src/portal/engine-api.ts
23065
+ import { readFileSync as readFileSync10, existsSync as existsSync11 } from "fs";
23066
+ import path58 from "path";
23067
+
23068
+ // src/portal/api/boundary.ts
23069
+ var STRUCTURAL_RELATIONS = ["calls", "uses", "extends", "implements"];
23070
+ function isAnyStructuralRelationAllowed(graph, sourceType, targetType) {
23071
+ const typeConfig = graph.architecture?.node_types?.[sourceType];
23072
+ if (!typeConfig) return true;
23073
+ const lists = typeConfig.relations;
23074
+ const def = typeConfig.relationDefault ?? "allow";
23075
+ for (const relType of STRUCTURAL_RELATIONS) {
23076
+ const allowed = lists?.[relType];
23077
+ if (allowed !== void 0) {
23078
+ if (allowed.includes("*") || allowed.includes(targetType)) return true;
23079
+ } else if (def === "allow") {
23080
+ return true;
23081
+ }
23082
+ }
23083
+ return false;
23084
+ }
23085
+ function isLineage(a, b) {
23086
+ return a === b || b.startsWith(a + "/") || a.startsWith(b + "/");
23087
+ }
23088
+ async function computePortalBoundary(graph, projectRoot) {
23089
+ let pass;
23090
+ try {
23091
+ pass = await runRelationPass(graph, projectRoot, {
23092
+ extractorFor: extractorForLanguage,
23093
+ resolvePathToFile: makeResolvePathToFile(projectRoot, buildOwnerIndex(graph.nodes).ownerOf),
23094
+ symbolIndexDir: astCacheDir(graph.rootPath)
23095
+ });
23096
+ } catch {
23097
+ return null;
23098
+ }
23099
+ const phantom = [];
23100
+ const declaredOnly = [];
23101
+ const forbiddenType = [];
23102
+ for (const [nodeId, nv] of pass.violationsByNode) {
23103
+ if (nv.verdict !== "refused") continue;
23104
+ const seen = /* @__PURE__ */ new Set();
23105
+ for (const v of nv.violations) {
23106
+ if (seen.has(v.ownerNode)) continue;
23107
+ seen.add(v.ownerNode);
23108
+ phantom.push({ source: nodeId, target: v.ownerNode });
23109
+ }
23110
+ }
23111
+ const detected = pass.detectedEdgesByNode;
23112
+ for (const [nodeId, node] of graph.nodes) {
23113
+ const sourceType = node.meta.type;
23114
+ const detectedTargets = detected.get(nodeId) ?? /* @__PURE__ */ new Set();
23115
+ for (const rel of node.meta.relations ?? []) {
23116
+ if (!STRUCTURAL_RELATIONS.includes(rel.type)) continue;
23117
+ if (isLineage(nodeId, rel.target)) continue;
23118
+ if (!graph.nodes.has(rel.target)) continue;
23119
+ if (!detectedTargets.has(rel.target)) {
23120
+ declaredOnly.push({ source: nodeId, target: rel.target });
23121
+ }
23122
+ }
23123
+ for (const target of detectedTargets) {
23124
+ const targetNode = graph.nodes.get(target);
23125
+ if (!targetNode) continue;
23126
+ if (!isAnyStructuralRelationAllowed(graph, sourceType, targetNode.meta.type)) {
23127
+ forbiddenType.push({ source: nodeId, target });
23128
+ }
23129
+ }
23130
+ }
23131
+ return { phantom, declaredOnly, forbiddenType };
23132
+ }
23133
+
23134
+ // src/portal/engine-api.ts
23135
+ var PORTAL_SCHEMA_SUPPORTED = CLI_SUPPORTED_SCHEMA;
23136
+ async function loadPortalGraph(projectRoot) {
23137
+ return loadGraphOrAbort(projectRoot, {
23138
+ tolerateInvalidConfig: true,
23139
+ noSecrets: true
23140
+ });
23141
+ }
23142
+ async function walkPortalFiles(projectRoot) {
23143
+ return walkRepoFiles(projectRoot);
23144
+ }
23145
+ async function runPortalCheck(graph, gitFiles) {
23146
+ return runCheck(graph, gitFiles);
23147
+ }
23148
+ function readAndVerifyLock(graph) {
23149
+ const lock = readLock(graph.rootPath);
23150
+ return { lock, verification: verifyLock(graph, lock) };
23151
+ }
23152
+ async function computePortalPairs(graph) {
23153
+ return computeExpectedPairs(graph);
23154
+ }
23155
+ function scanPortalUncovered(graph, gitFiles) {
23156
+ return scanUncoveredFiles(graph, gitFiles);
23157
+ }
23158
+ async function readNodeLog(projectRoot, nodePath) {
23159
+ return readLogContent(projectRoot, nodePath);
23160
+ }
23161
+ function groupPortalIssues(issues) {
23162
+ return groupIssues(issues);
23163
+ }
23164
+ async function computePortalBoundary2(graph, projectRoot) {
23165
+ return computePortalBoundary(graph, projectRoot);
23166
+ }
23167
+ async function scanPortalSuppressions2(graph, projectRoot, gitFiles) {
23168
+ const knownAspectIds = new Set(graph.aspects.map((a) => a.id));
23169
+ const draftAspectIds = new Set(
23170
+ graph.aspects.filter((a) => (a.status ?? "enforced") === "draft").map((a) => a.id)
23171
+ );
23172
+ const report2 = await runSuppressionsScan(projectRoot, gitFiles, knownAspectIds);
23173
+ return scanPortalSuppressions(report2, knownAspectIds, draftAspectIds);
23174
+ }
23175
+ function computePortalLockHash(graph) {
23176
+ return committedLockContentHash(graph.rootPath);
23177
+ }
23178
+ function readGitCommitRef(projectRoot) {
23179
+ const gitDir = path58.join(projectRoot, ".git");
23180
+ const headFile = path58.join(gitDir, "HEAD");
23181
+ if (!existsSync11(headFile)) return null;
23182
+ let head;
23183
+ try {
23184
+ head = readFileSync10(headFile, "utf-8").trim();
23185
+ } catch {
23186
+ return null;
23187
+ }
23188
+ if (/^[0-9a-f]{40}$/i.test(head)) return head;
23189
+ const refMatch = head.match(/^ref:\s*(.+)$/);
23190
+ if (!refMatch) return null;
23191
+ const refName = refMatch[1].trim();
23192
+ const looseRef = path58.join(gitDir, refName);
23193
+ if (existsSync11(looseRef)) {
23194
+ try {
23195
+ const sha = readFileSync10(looseRef, "utf-8").trim();
23196
+ if (/^[0-9a-f]{40}$/i.test(sha)) return sha;
23197
+ } catch {
23198
+ }
23199
+ }
23200
+ const packed = path58.join(gitDir, "packed-refs");
23201
+ if (existsSync11(packed)) {
23202
+ try {
23203
+ const lines = readFileSync10(packed, "utf-8").split("\n");
23204
+ for (const line of lines) {
23205
+ const m = line.match(/^([0-9a-f]{40})\s+(.+)$/i);
23206
+ if (m && m[2].trim() === refName) return m[1];
23207
+ }
23208
+ } catch {
23209
+ return null;
23210
+ }
23211
+ }
23212
+ return null;
23213
+ }
23214
+ async function computePortalFreshness(graph, lock) {
23215
+ const out = [];
23216
+ for (const nodePath of graph.nodes.keys()) {
23217
+ const stored = lock.nodes[nodePath]?.source;
23218
+ if (stored === void 0) {
23219
+ out.push({ nodePath, sourceChanged: false });
23220
+ continue;
23221
+ }
23222
+ let fingerprint;
23223
+ try {
23224
+ fingerprint = await computeSourceFingerprint(graph, nodePath);
23225
+ } catch {
23226
+ out.push({ nodePath, sourceChanged: true });
23227
+ continue;
23228
+ }
23229
+ if (fingerprint === void 0) {
23230
+ out.push({ nodePath, sourceChanged: false });
23231
+ continue;
23232
+ }
23233
+ out.push({ nodePath, sourceChanged: fingerprint !== stored });
23234
+ }
23235
+ return out;
23236
+ }
23237
+
23238
+ // src/portal/derive-node-state.ts
23239
+ var PAIR_RANK = {
23240
+ refused: 4,
23241
+ unverified: 3,
23242
+ warning: 2,
23243
+ verified: 1,
23244
+ "n/a": 0
23245
+ };
23246
+ function worstPairState(states) {
23247
+ return states.reduce((worst, s) => PAIR_RANK[s] > PAIR_RANK[worst] ? s : worst, "verified");
23248
+ }
23249
+ var STATE_RANK = {
23250
+ refused: 4,
23251
+ unverified: 3,
23252
+ warning: 2,
23253
+ verified: 1,
23254
+ "no-rule": 0
23255
+ };
23256
+ function computeOwnState(checked, effectiveAspects, nodeIssues, fresh) {
23257
+ let worst = checked ? "verified" : "no-rule";
23258
+ if (checked) {
23259
+ for (const ea of effectiveAspects) {
23260
+ if (ea.pairState === "refused" && STATE_RANK["refused"] > STATE_RANK[worst]) worst = "refused";
23261
+ else if (ea.pairState === "unverified" && STATE_RANK["unverified"] > STATE_RANK[worst]) worst = "unverified";
23262
+ else if (ea.pairState === "warning" && STATE_RANK["warning"] > STATE_RANK[worst]) worst = "warning";
23263
+ }
23264
+ const hasWarning = nodeIssues.some((i) => i.severity === "warning");
23265
+ if (hasWarning && STATE_RANK["warning"] > STATE_RANK[worst]) worst = "warning";
23266
+ }
23267
+ if (fresh && STATE_RANK["unverified"] > STATE_RANK[worst]) worst = "unverified";
23268
+ return worst;
23269
+ }
23270
+ function computeRollup(node, built) {
23271
+ let worst = built.get(node.path).state;
23272
+ for (const child of node.children) {
23273
+ const childRollup = built.get(child.path)?.rollupState ?? "no-rule";
23274
+ if (STATE_RANK[childRollup] > STATE_RANK[worst]) worst = childRollup;
23275
+ }
23276
+ return worst;
23277
+ }
23278
+ function collectNodeSuppressions(mapping, suppressions) {
23279
+ const out = [];
23280
+ const owned = new Set(mapping);
23281
+ for (const file of owned) {
23282
+ const hits = suppressions.byFile.get(file);
23283
+ if (hits) out.push(...hits);
23284
+ }
23285
+ return out;
23286
+ }
23287
+ function computeNotApplicable(node, graph, effectiveIds) {
23288
+ const attachedIds = /* @__PURE__ */ new Set();
23289
+ for (const id of node.meta.aspects ?? []) attachedIds.add(id);
23290
+ const typeDef = graph.architecture?.node_types?.[node.meta.type];
23291
+ for (const id of typeDef?.aspects ?? []) attachedIds.add(id);
23292
+ const out = [];
23293
+ for (const id of [...attachedIds].sort()) {
23294
+ if (effectiveIds.has(id)) continue;
23295
+ out.push({ aspectId: id, why: "filtered out by a `when` predicate on this node" });
23296
+ }
23297
+ return out;
23298
+ }
23299
+
23300
+ // src/portal/derive-nodes.ts
23301
+ function displayPairState(rawKind, status) {
23302
+ if (rawKind === "verified") return "verified";
23303
+ if (rawKind === "refused") return status === "advisory" ? "warning" : "refused";
23304
+ return "unverified";
23305
+ }
23306
+ function buildPortalNodes(graph, _lock, verification, check, logContents, suppressions, freshByNode = /* @__PURE__ */ new Map()) {
23307
+ const pairsByNode = indexPairsByNode(verification.pairs);
23308
+ const issuesByNode = indexIssuesByNode(check.issues);
23309
+ const relationsIn = invertRelations(graph);
23310
+ const built = /* @__PURE__ */ new Map();
23311
+ for (const [path62, node] of graph.nodes) {
23312
+ built.set(
23313
+ path62,
23314
+ buildOne(
23315
+ node,
23316
+ graph,
23317
+ pairsByNode,
23318
+ issuesByNode,
23319
+ relationsIn,
23320
+ logContents,
23321
+ suppressions,
23322
+ freshByNode.get(path62) === true
23323
+ )
23324
+ );
23325
+ }
23326
+ for (const [path62, node] of graph.nodes) {
23327
+ const portal = built.get(path62);
23328
+ portal.rollupState = computeRollup(node, built);
23329
+ }
23330
+ return [...built.values()];
23331
+ }
23332
+ function indexPairsByNode(pairs) {
23333
+ const out = /* @__PURE__ */ new Map();
23334
+ for (const vp of pairs) {
23335
+ const np = vp.pair.nodePath;
23336
+ let byAspect = out.get(np);
23337
+ if (!byAspect) {
23338
+ byAspect = /* @__PURE__ */ new Map();
23339
+ out.set(np, byAspect);
23340
+ }
23341
+ const list = byAspect.get(vp.pair.aspectId) ?? [];
23342
+ list.push(toPairRecord(vp));
23343
+ byAspect.set(vp.pair.aspectId, list);
23344
+ }
23345
+ return out;
23346
+ }
23347
+ function toPairRecord(vp) {
23348
+ const subjectFiles = vp.pair.subjectFiles ?? [];
23349
+ const state = displayPairState(vp.state.kind, vp.pair.status);
23350
+ if (vp.state.kind === "refused") {
23351
+ return { state, subjectFiles, reason: vp.state.reason };
23352
+ }
23353
+ return { state, subjectFiles };
23354
+ }
23355
+ function indexIssuesByNode(issues) {
23356
+ const out = /* @__PURE__ */ new Map();
23357
+ for (const i of issues) {
23358
+ if (!i.nodePath) continue;
23359
+ const list = out.get(i.nodePath) ?? [];
23360
+ list.push(i);
23361
+ out.set(i.nodePath, list);
23362
+ }
23363
+ return out;
23364
+ }
23365
+ function invertRelations(graph) {
23366
+ const out = /* @__PURE__ */ new Map();
23367
+ for (const [path62, node] of graph.nodes) {
23368
+ for (const rel of node.meta.relations ?? []) {
23369
+ const list = out.get(rel.target) ?? [];
23370
+ list.push({ source: path62, type: rel.type });
23371
+ out.set(rel.target, list);
23372
+ }
23373
+ }
23374
+ return out;
23375
+ }
23376
+ function buildOne(node, graph, pairsByNode, issuesByNode, relationsIn, logContents, suppressions, fresh) {
23377
+ const path62 = node.path;
23378
+ const mapping = node.meta.mapping ?? [];
23379
+ const effectiveIds = computeEffectiveAspects(node, graph);
23380
+ const statuses = computeEffectiveAspectStatuses(node, graph);
23381
+ const nodePairs = pairsByNode.get(path62);
23382
+ const effectiveAspects = [];
23383
+ for (const aspectId of [...effectiveIds].sort()) {
23384
+ effectiveAspects.push(
23385
+ buildEffectiveAspect(aspectId, node, graph, statuses, nodePairs)
23386
+ );
23387
+ }
23388
+ const checked = effectiveAspects.some((ea) => ea.pairState !== "n/a");
23389
+ const ownState = computeOwnState(
23390
+ checked,
23391
+ effectiveAspects,
23392
+ issuesByNode.get(path62) ?? [],
23393
+ fresh
23394
+ );
23395
+ const relationsOut = (node.meta.relations ?? []).map((r) => ({
23396
+ target: r.target,
23397
+ type: r.type,
23398
+ ...r.consumes ? { consumes: r.consumes } : {}
23399
+ }));
23400
+ const log2 = parseLog(logContents.get(path62) ?? "").map((e) => ({
23401
+ when: e.datetime,
23402
+ body: e.body
23403
+ }));
23404
+ const nodeSuppressions = collectNodeSuppressions(mapping, suppressions);
23405
+ const notApplicable = computeNotApplicable(node, graph, effectiveIds);
23406
+ return {
23407
+ path: path62,
23408
+ name: node.meta.name,
23409
+ type: node.meta.type,
23410
+ ...node.meta.description ? { description: node.meta.description } : {},
23411
+ parent: node.parent ? node.parent.path : null,
23412
+ mapping,
23413
+ sourceFileCount: mapping.length,
23414
+ isTest: node.meta.type === "test-suite",
23415
+ checked,
23416
+ fresh,
23417
+ state: ownState,
23418
+ // Provisional — overwritten by the bottom-up roll-up pass; seeded to own state.
23419
+ rollupState: ownState,
23420
+ effectiveAspects,
23421
+ notApplicable,
23422
+ relationsOut,
23423
+ relationsIn: relationsIn.get(path62) ?? [],
23424
+ suppressions: nodeSuppressions,
23425
+ log: log2
23426
+ };
23427
+ }
23428
+ function buildEffectiveAspect(aspectId, node, graph, statuses, nodePairs) {
23429
+ const def = graph.aspects.find((a) => a.id === aspectId);
23430
+ const aggregate = isAggregateAspect(graph, aspectId);
23431
+ const kind = aggregate ? "aggregate" : def?.reviewer.type === "deterministic" ? "deterministic" : "llm";
23432
+ const sources = getAspectStatusSources(node, aspectId, graph);
23433
+ const primary = sources.length > 0 ? sources.reduce((a, b) => a.channel <= b.channel ? a : b) : void 0;
23434
+ const channel = primary?.channel ?? 7;
23435
+ const origin = primary?.origin ?? impliedOrigin(graph, aspectId);
23436
+ const status = statuses.get(aspectId) ?? "enforced";
23437
+ let tier;
23438
+ let consensus;
23439
+ let cost = "free";
23440
+ if (kind === "llm" && def) {
23441
+ cost = "billed";
23442
+ const reviewer = graph.config?.reviewer;
23443
+ if (reviewer) {
23444
+ const sel = selectTierForAspect(def, reviewer);
23445
+ if (sel.ok) {
23446
+ tier = sel.tierName;
23447
+ consensus = sel.tier.consensus;
23448
+ }
23449
+ }
23450
+ }
23451
+ const records = nodePairs?.get(aspectId);
23452
+ const pairState = aggregate || status === "draft" || !records || records.length === 0 ? "n/a" : worstPairState(records.map((r) => r.state));
23453
+ let foldedInputs;
23454
+ let reason;
23455
+ if (!aggregate && status !== "draft" && records && records.length > 0) {
23456
+ if (pairState === "verified") {
23457
+ const files = /* @__PURE__ */ new Set();
23458
+ for (const r of records) for (const f of r.subjectFiles) files.add(f);
23459
+ const sorted = [...files].sort();
23460
+ if (sorted.length > 0) foldedInputs = sorted;
23461
+ } else if (pairState === "refused" || pairState === "warning") {
23462
+ reason = records.find((r) => r.reason)?.reason;
23463
+ }
23464
+ }
23465
+ return {
23466
+ aspectId,
23467
+ kind,
23468
+ ...tier !== void 0 ? { tier } : {},
23469
+ ...consensus !== void 0 ? { consensus } : {},
23470
+ cost,
23471
+ status,
23472
+ channel,
23473
+ origin,
23474
+ pairState,
23475
+ ...reason !== void 0 ? { reason } : {},
23476
+ ...foldedInputs !== void 0 ? { foldedInputs } : {}
23477
+ };
23478
+ }
23479
+ function impliedOrigin(graph, aspectId) {
23480
+ for (const other of graph.aspects) {
23481
+ if (other.implies?.includes(aspectId)) return `implied:${other.id}`;
23482
+ }
23483
+ return "unknown";
23484
+ }
23485
+
23486
+ // src/portal/derive-catalogue.ts
23487
+ function buildAspects(graph, pairs) {
23488
+ const byAspect = /* @__PURE__ */ new Map();
23489
+ for (const vp of pairs) {
23490
+ const list = byAspect.get(vp.pair.aspectId) ?? [];
23491
+ list.push(collapsePairState(vp));
23492
+ byAspect.set(vp.pair.aspectId, list);
23493
+ }
23494
+ return graph.aspects.map((def) => buildAspect(def, graph, byAspect.get(def.id) ?? [])).sort((a, b) => a.id.localeCompare(b.id, "en"));
23495
+ }
23496
+ function collapsePairState(vp) {
23497
+ return displayPairState(vp.state.kind, vp.pair.status);
23498
+ }
23499
+ function buildAspect(def, graph, states) {
23500
+ const kind = def.reviewer.type;
23501
+ const status = def.status ?? "enforced";
23502
+ const scope = def.scope?.per === "file" ? "file" : "node";
23503
+ const ruleProse = def.artifacts.find((a) => a.filename === "content.md")?.content;
23504
+ const checkSource = def.artifacts.find((a) => a.filename === "check.mjs")?.content;
23505
+ return {
23506
+ id: def.id,
23507
+ name: def.name,
23508
+ kind,
23509
+ status,
23510
+ scope,
23511
+ hasWhen: def.when !== void 0,
23512
+ implies: def.implies ?? [],
23513
+ // Optional content fields assigned directly — an undefined value is omitted by the
23514
+ // JSON.stringify embed, so no conditional spread is needed (and none is left half-covered).
23515
+ description: def.description,
23516
+ ruleProse,
23517
+ checkSource,
23518
+ tally: buildTally(def, graph, status, states)
23519
+ };
23520
+ }
23521
+ function buildTally(def, graph, status, states) {
23522
+ if (def.reviewer.type === "aggregate") {
23523
+ return { render: "aggregate" };
23524
+ }
23525
+ if (states.length === 0) {
23526
+ return { render: "vacuous", reason: vacuousReason(def, graph, status) };
23527
+ }
23528
+ let verified = 0;
23529
+ let refused = 0;
23530
+ let warning = 0;
23531
+ let unverified = 0;
23532
+ for (const s of states) {
23533
+ if (s === "verified") verified += 1;
23534
+ else if (s === "refused") refused += 1;
23535
+ else if (s === "warning") warning += 1;
23536
+ else unverified += 1;
23537
+ }
23538
+ return { render: "normal", verified, refused, warning, unverified, units: states.length };
23539
+ }
23540
+ function vacuousReason(def, graph, status) {
23541
+ if ((status ?? "enforced") === "draft") {
23542
+ return "draft \u2014 produces no expected pairs (parked, not yet enforced)";
23543
+ }
23544
+ const reachesANode = [...graph.nodes.values()].some(
23545
+ (n) => computeEffectiveAspects(n, graph).has(def.id)
23546
+ );
23547
+ if (!reachesANode) {
23548
+ return "not effective on any node \u2014 no attach channel reaches a node";
23549
+ }
23550
+ return "effective on a node but every subject set is empty (scope/when excludes all files, or no mapped source)";
23551
+ }
23552
+ function buildFlows(graph, nodeStateOf) {
23553
+ return graph.flows.map((flow) => buildFlow(flow, graph, nodeStateOf)).sort((a, b) => a.name.localeCompare(b.name, "en"));
23554
+ }
23555
+ function buildFlow(flow, graph, nodeStateOf) {
23556
+ const participants = expandParticipants(flow, graph);
23557
+ const state = computeFlowState(participants, graph, nodeStateOf);
23558
+ return {
23559
+ name: flow.name,
23560
+ ...flow.description ? { description: flow.description } : {},
23561
+ participants,
23562
+ aspects: flow.aspects ?? [],
23563
+ state
23564
+ };
23565
+ }
23566
+ function expandParticipants(flow, graph) {
23567
+ const out = /* @__PURE__ */ new Set();
23568
+ for (const path62 of flow.nodes) {
23569
+ out.add(path62);
23570
+ const node = graph.nodes.get(path62);
23571
+ if (node) {
23572
+ for (const d of collectDescendants(node)) out.add(d.path);
23573
+ }
23574
+ }
23575
+ return [...out].sort((a, b) => a.localeCompare(b, "en"));
23576
+ }
23577
+ function computeFlowState(participants, graph, nodeStateOf) {
23578
+ let anyChecked = false;
23579
+ let anyAttention = false;
23580
+ for (const path62 of participants) {
23581
+ const node = graph.nodes.get(path62);
23582
+ if (!node) continue;
23583
+ if (!hasNonDraftEffectiveAspects(node, graph)) continue;
23584
+ anyChecked = true;
23585
+ const states = nodeStateOf(path62) ?? [];
23586
+ if (states.some((s) => s === "refused" || s === "unverified")) anyAttention = true;
23587
+ }
23588
+ if (!anyChecked) return "nothing-checked";
23589
+ return anyAttention ? "attention" : "verified";
23590
+ }
23591
+ function buildTypes(graph) {
23592
+ const countByType = /* @__PURE__ */ new Map();
23593
+ for (const node of graph.nodes.values()) {
23594
+ countByType.set(node.meta.type, (countByType.get(node.meta.type) ?? 0) + 1);
23595
+ }
23596
+ const types = [];
23597
+ for (const [id, def] of Object.entries(graph.architecture?.node_types ?? {})) {
23598
+ const allowedRelations = {};
23599
+ for (const [relType, targets] of Object.entries(def.relations ?? {})) {
23600
+ allowedRelations[relType] = [...targets ?? []];
23601
+ }
23602
+ types.push({
23603
+ id,
23604
+ ...def.description ? { description: def.description } : {},
23605
+ parents: def.parents ?? [],
23606
+ allowedRelations,
23607
+ defaultAspects: def.aspects ?? [],
23608
+ strict: def.enforce === "strict",
23609
+ logRequired: def.log_required === true,
23610
+ nodeCount: countByType.get(id) ?? 0
23611
+ });
23612
+ }
23613
+ return types.sort((a, b) => a.id.localeCompare(b.id, "en"));
23614
+ }
23615
+
23616
+ // src/portal/derive-rest.ts
23617
+ function buildBoundary(input) {
23618
+ if (input === null) {
23619
+ return { phantom: [], declaredOnly: [], forbiddenType: [], unknown: true };
23620
+ }
23621
+ return {
23622
+ phantom: dedupeEdges(input.phantom),
23623
+ declaredOnly: dedupeEdges(input.declaredOnly),
23624
+ forbiddenType: dedupeEdges(input.forbiddenType),
23625
+ unknown: false
23626
+ };
23627
+ }
23628
+ function dedupeEdges(edges) {
23629
+ const seen = /* @__PURE__ */ new Set();
23630
+ const out = [];
23631
+ for (const e of edges) {
23632
+ const key = `${e.source}\0${e.target}`;
23633
+ if (seen.has(key)) continue;
23634
+ seen.add(key);
23635
+ out.push(e);
23636
+ }
23637
+ return out.sort((a, b) => a.source.localeCompare(b.source, "en") || a.target.localeCompare(b.target, "en"));
23638
+ }
23639
+ function buildSuppressions(markers) {
23640
+ return markers.map((m) => ({
23641
+ aspectId: m.aspectId,
23642
+ file: m.file,
23643
+ line: m.line,
23644
+ reason: m.reason,
23645
+ ...m.risk ? { risk: m.risk } : {}
23646
+ })).sort((a, b) => a.file.localeCompare(b.file, "en") || a.line - b.line);
23647
+ }
23648
+ function buildHubs(nodes) {
23649
+ const fanOut = [];
23650
+ const fanIn = [];
23651
+ for (const n of nodes) {
23652
+ if (n.relationsOut.length > 0) fanOut.push({ path: n.path, count: n.relationsOut.length });
23653
+ if (n.relationsIn.length > 0) fanIn.push({ path: n.path, count: n.relationsIn.length });
23654
+ }
23655
+ return { fanOut: rankHubs(fanOut), fanIn: rankHubs(fanIn) };
23656
+ }
23657
+ function rankHubs(entries) {
23658
+ return entries.sort((a, b) => b.count - a.count || a.path.localeCompare(b.path, "en"));
23659
+ }
23660
+ function buildResidue(nodes, uncoveredFiles) {
23661
+ const noRuleNodes = nodes.filter((n) => n.state === "no-rule" && n.mapping.length > 0).map((n) => n.path).sort((a, b) => a.localeCompare(b, "en"));
23662
+ return {
23663
+ noRuleNodes,
23664
+ uncoveredFiles: [...uncoveredFiles].sort((a, b) => a.localeCompare(b, "en"))
23665
+ };
23666
+ }
23667
+ function buildWorklist(check) {
23668
+ const groups = groupPortalIssues(check.issues);
23669
+ return groups.map((g) => ({
23670
+ rule: g.label,
23671
+ severity: g.severity,
23672
+ why: g.sharedWhy,
23673
+ fix: g.sharedNext,
23674
+ nodes: [...new Set(g.members.map((m) => m.nodePath ?? "").filter((p2) => p2.length > 0))].sort(
23675
+ (a, b) => a.localeCompare(b, "en")
23676
+ )
23677
+ }));
23678
+ }
23679
+
23680
+ // src/portal/extract.ts
23681
+ async function extractPortalData(projectRoot, opts) {
23682
+ const graph = await loadPortalGraph(projectRoot);
23683
+ const gitFiles = await walkPortalFiles(projectRoot);
23684
+ const checkResult = await runPortalCheck(graph, gitFiles);
23685
+ const { lock, verification: verificationPromise } = readAndVerifyLock(graph);
23686
+ const verification = await verificationPromise;
23687
+ const expected = await computePortalPairs(graph);
23688
+ const counts = buildCounts(graph, checkResult, verification.pairs, expected.pairs);
23689
+ const logContents = /* @__PURE__ */ new Map();
23690
+ for (const nodePath of graph.nodes.keys()) {
23691
+ logContents.set(nodePath, await readNodeLog(projectRoot, nodePath));
23692
+ }
23693
+ const suppressionMarkers = await scanPortalSuppressions2(graph, projectRoot, gitFiles);
23694
+ const flatSuppressions = buildSuppressions(suppressionMarkers);
23695
+ const suppressions = indexSuppressionsByFile(flatSuppressions);
23696
+ const freshnessMarkers = await computePortalFreshness(graph, lock);
23697
+ const freshByNode = /* @__PURE__ */ new Map();
23698
+ for (const m of freshnessMarkers) freshByNode.set(m.nodePath, m.sourceChanged);
23699
+ const nodes = buildPortalNodes(graph, lock, verification, checkResult, logContents, suppressions, freshByNode);
23700
+ const pairStatesByNode = /* @__PURE__ */ new Map();
23701
+ for (const vp of verification.pairs) {
23702
+ const list = pairStatesByNode.get(vp.pair.nodePath) ?? [];
23703
+ list.push(collapsePairState2(vp));
23704
+ pairStatesByNode.set(vp.pair.nodePath, list);
23705
+ }
23706
+ const aspects = buildAspects(graph, verification.pairs);
23707
+ const flows = buildFlows(graph, (path62) => pairStatesByNode.get(path62));
23708
+ const types = buildTypes(graph);
23709
+ const hubs = buildHubs(nodes);
23710
+ const uncovered = scanPortalUncovered(graph, gitFiles);
23711
+ const residue = buildResidue(nodes, uncovered);
23712
+ const worklist = buildWorklist(checkResult);
23713
+ counts.suppressed = flatSuppressions.length;
23714
+ counts.noRule = residue.noRuleNodes.length;
23715
+ counts.notApplicable = nodes.reduce((sum, n) => sum + n.notApplicable.length, 0);
23716
+ const boundary = buildBoundary(await computePortalBoundary2(graph, projectRoot));
23717
+ const lockHash = computePortalLockHash(graph);
23718
+ const commitRef = readGitCommitRef(projectRoot);
23719
+ const data = {
23720
+ meta: {
23721
+ projectName: checkResult.projectName,
23722
+ generatedAt: "",
23723
+ // stamped below, after generation
23724
+ autoApprove: normalizeAutoApprove(graph.config.auto_approve),
23725
+ writeEnabled: opts.writeEnabled,
23726
+ schemaSupported: PORTAL_SCHEMA_SUPPORTED,
23727
+ lockHash,
23728
+ commitRef,
23729
+ counts
23730
+ },
23731
+ nodes,
23732
+ aspects,
23733
+ flows,
23734
+ types,
23735
+ boundary,
23736
+ suppressions: flatSuppressions,
23737
+ hubs,
23738
+ worklist,
23739
+ residue
23740
+ };
23741
+ data.meta.generatedAt = (/* @__PURE__ */ new Date()).toISOString();
23742
+ return data;
23743
+ }
23744
+ function normalizeAutoApprove(value) {
23745
+ if (value === "deterministic" || value === "full") return value;
23746
+ return "false";
23747
+ }
23748
+ function collapsePairState2(vp) {
23749
+ return displayPairState(vp.state.kind, vp.pair.status);
23750
+ }
23751
+ function indexSuppressionsByFile(flat) {
23752
+ const byFile = /* @__PURE__ */ new Map();
23753
+ for (const s of flat) {
23754
+ const list = byFile.get(s.file) ?? [];
23755
+ list.push(s);
23756
+ byFile.set(s.file, list);
23757
+ }
23758
+ return { byFile };
23759
+ }
23760
+ function buildCounts(graph, check, pairs, expectedPairs) {
23761
+ let verified = 0;
23762
+ let refused = 0;
23763
+ let unverified = 0;
23764
+ let advisoryRefused = 0;
23765
+ for (const vp of pairs) {
23766
+ switch (displayPairState(vp.state.kind, vp.pair.status)) {
23767
+ case "verified":
23768
+ verified += 1;
23769
+ break;
23770
+ case "refused":
23771
+ refused += 1;
23772
+ break;
23773
+ case "warning":
23774
+ advisoryRefused += 1;
23775
+ break;
23776
+ default:
23777
+ unverified += 1;
23778
+ break;
23779
+ }
23780
+ }
23781
+ let pairsLLM = 0;
23782
+ let pairsDet = 0;
23783
+ for (const p2 of expectedPairs) {
23784
+ if (p2.kind === "llm") pairsLLM += 1;
23785
+ else pairsDet += 1;
23786
+ }
23787
+ const errors = check.issues.filter((i) => i.severity === "error").length;
23788
+ const warnings = check.issues.filter((i) => i.severity === "warning").length;
23789
+ return {
23790
+ nodes: graph.nodes.size,
23791
+ aspects: graph.aspects.length,
23792
+ flows: graph.flows.length,
23793
+ pairsTotal: expectedPairs.length,
23794
+ pairsLLM,
23795
+ pairsDet,
23796
+ verified,
23797
+ refused,
23798
+ unverified,
23799
+ advisoryRefused,
23800
+ // The residue-track counts (noRule / notApplicable / suppressed) are NOT part of the
23801
+ // count-parity identity and cannot be computed here — each depends on data derived AFTER
23802
+ // this seam (the built node array, the residue ledger, the suppression inventory). They
23803
+ // are seeded 0 and OVERWRITTEN by the post-pass in extractPortalData once that data exists.
23804
+ // (Never leave them 0: that prints "0 waived / 0 no rule / 0 not applicable" over a list.)
23805
+ noRule: 0,
23806
+ draft: check.draftSkipped,
23807
+ notApplicable: 0,
23808
+ suppressed: 0,
23809
+ uncoveredFiles: check.totalFiles - check.coveredFiles,
23810
+ coveredFiles: check.coveredFiles,
23811
+ totalFiles: check.totalFiles,
23812
+ errors,
23813
+ warnings
23814
+ };
23815
+ }
23816
+
23817
+ // src/portal/serializer.ts
23818
+ import { readFile as readFile20 } from "fs/promises";
23819
+ import path59 from "path";
22591
23820
  import { fileURLToPath as fileURLToPath4 } from "url";
23821
+ import { existsSync as existsSync12 } from "fs";
23822
+ var LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 400 400">
23823
+ <defs>
23824
+ <style>
23825
+ .st0 {
23826
+ fill: #f8d794;
23827
+ fill-rule: evenodd;
23828
+ }
23829
+ </style>
23830
+ </defs>
23831
+ <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"/>
23832
+ </svg>`;
23833
+ var LOGO_DATA_URI = `data:image/svg+xml;base64,${Buffer.from(LOGO_SVG, "utf8").toString("base64")}`;
23834
+ var __dirname3 = path59.dirname(fileURLToPath4(import.meta.url));
23835
+ function portalTemplateDir() {
23836
+ const distDir = path59.resolve(__dirname3, "templates", "portal");
23837
+ if (existsSync12(distDir)) return distDir;
23838
+ const srcDir = path59.resolve(__dirname3, "..", "templates", "portal");
23839
+ if (existsSync12(srcDir)) return srcDir;
23840
+ return path59.resolve(__dirname3, "..", "dist", "templates", "portal");
23841
+ }
23842
+ var MODULE_ORDER = [
23843
+ "js/namespace.js",
23844
+ "js/state-model.js",
23845
+ "js/glossary.js",
23846
+ "js/router.js",
23847
+ "js/palette.js",
23848
+ "js/palette-overlay.js",
23849
+ "js/consumer.js",
23850
+ "js/export.js",
23851
+ "js/tree.js",
23852
+ "js/shell.js",
23853
+ "js/dispatch.js",
23854
+ // Per-surface view modules — registered onto Yg.views, consumed by the dispatcher. The
23855
+ // matrix module precedes the relations view that calls Yg.matrix; the panel module is read
23856
+ // by the bootstrap's panel slot. All before the bootstrap, which orchestrates the rest.
23857
+ "js/views/overview-view.js",
23858
+ "js/views/coverage-view.js",
23859
+ "js/views/tree-view.js",
23860
+ "js/views/relations-matrix.js",
23861
+ "js/views/relations-view.js",
23862
+ "js/views/rulebook-view.js",
23863
+ "js/views/types-view.js",
23864
+ "js/views/flows-view.js",
23865
+ "js/views/suppressions-view.js",
23866
+ "js/views/start-view.js",
23867
+ "js/views/panel-aspect.js",
23868
+ "js/views/panel-view.js",
23869
+ "js/bootstrap.js"
23870
+ ];
23871
+ var CSS_ORDER = [
23872
+ "tokens.css",
23873
+ "shell.css",
23874
+ "app.css",
23875
+ "views.css",
23876
+ "views-audit.css",
23877
+ "views-panel.css",
23878
+ "views-relations.css",
23879
+ "views-rulebook.css",
23880
+ "views-flows.css",
23881
+ "views-start.css"
23882
+ ];
23883
+ async function readAssets() {
23884
+ const dir = portalTemplateDir();
23885
+ const read = (rel) => readFile20(path59.join(dir, rel), "utf-8");
23886
+ const [shell, vendor] = await Promise.all([read("shell.html"), read("vendor/d3-hierarchy.js")]);
23887
+ const cssSources = await Promise.all(CSS_ORDER.map((rel) => read(rel)));
23888
+ const css = cssSources.join("\n");
23889
+ const moduleSources = await Promise.all(MODULE_ORDER.map((rel) => read(rel)));
23890
+ const modules = moduleSources.join("\n");
23891
+ return { shell, css, vendor, modules };
23892
+ }
23893
+ function renderStaticHtml(data, assets) {
23894
+ const json = safeJsonForScript(data);
23895
+ 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);
23896
+ }
23897
+ var LINE_SEPARATORS = new RegExp(`[${String.fromCharCode(8232)}${String.fromCharCode(8233)}]`, "g");
23898
+ function safeJsonForScript(data) {
23899
+ 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");
23900
+ }
23901
+ async function emitStatic(data, outPath) {
23902
+ const assets = await readAssets();
23903
+ const html = renderStaticHtml(data, assets);
23904
+ await atomicWriteFile(outPath, html);
23905
+ }
23906
+ async function renderPortalPage(data) {
23907
+ const assets = await readAssets();
23908
+ return renderStaticHtml(data, assets);
23909
+ }
23910
+ function contentTypeFor(relPath) {
23911
+ if (relPath.endsWith(".html")) return "text/html; charset=utf-8";
23912
+ if (relPath.endsWith(".css")) return "text/css; charset=utf-8";
23913
+ if (relPath.endsWith(".js")) return "text/javascript; charset=utf-8";
23914
+ if (relPath.endsWith(".json")) return "application/json; charset=utf-8";
23915
+ if (relPath.endsWith(".svg")) return "image/svg+xml";
23916
+ return "application/octet-stream";
23917
+ }
23918
+ async function readPortalAsset(relPath) {
23919
+ const root = portalTemplateDir();
23920
+ const cleaned = relPath.replace(/^\/+/, "");
23921
+ if (cleaned.length === 0) return null;
23922
+ const resolved = path59.resolve(root, cleaned);
23923
+ const rel = path59.relative(root, resolved);
23924
+ if (rel.startsWith("..") || path59.isAbsolute(rel)) return null;
23925
+ if (!existsSync12(resolved)) return null;
23926
+ const content20 = await readFile20(resolved);
23927
+ return { content: content20, contentType: contentTypeFor(cleaned) };
23928
+ }
23929
+
23930
+ // src/portal/server/server.ts
23931
+ import { createServer } from "http";
23932
+
23933
+ // src/portal/server/page.ts
23934
+ async function freshPortalData(projectRoot, writeEnabled) {
23935
+ return extractPortalData(projectRoot, { writeEnabled });
23936
+ }
23937
+ async function renderLivePage(data) {
23938
+ return renderPortalPage(data);
23939
+ }
23940
+ async function readStaticAsset(relPath) {
23941
+ return readPortalAsset(relPath);
23942
+ }
23943
+
23944
+ // src/portal/server/approve.ts
23945
+ import { spawn as spawn2 } from "child_process";
23946
+ import path60 from "path";
23947
+ import { existsSync as existsSync13 } from "fs";
23948
+ import { fileURLToPath as fileURLToPath5 } from "url";
23949
+ function resolveCliBin() {
23950
+ const here = fileURLToPath5(import.meta.url);
23951
+ if (path60.basename(here) === "bin.js") return here;
23952
+ let dir = path60.dirname(here);
23953
+ for (let i = 0; i < 6; i++) {
23954
+ const candidate = path60.join(dir, "dist", "bin.js");
23955
+ if (existsSync13(candidate)) return candidate;
23956
+ dir = path60.dirname(dir);
23957
+ }
23958
+ return process.argv[1];
23959
+ }
23960
+ var CLI_BIN = resolveCliBin();
23961
+ function approveArgs(llm) {
23962
+ return llm ? ["check", "--approve"] : ["check", "--approve", "--only-deterministic"];
23963
+ }
23964
+ function dryRunArgs(llm) {
23965
+ return llm ? ["check", "--approve", "--dry-run"] : ["check", "--approve", "--only-deterministic", "--dry-run"];
23966
+ }
23967
+ function spawnCli(args, cwd) {
23968
+ return new Promise((resolve6, reject) => {
23969
+ const child = spawn2(process.execPath, [CLI_BIN, ...args], { cwd });
23970
+ let stdout = "";
23971
+ let stderr = "";
23972
+ child.stdout.on("data", (chunk) => stdout += chunk.toString("utf-8"));
23973
+ child.stderr.on("data", (chunk) => stderr += chunk.toString("utf-8"));
23974
+ child.on("error", (err) => reject(err));
23975
+ child.on("close", (code) => resolve6({ exitCode: code ?? 1, stdout, stderr }));
23976
+ });
23977
+ }
23978
+ async function runApproveViaCli(projectRoot, llm) {
23979
+ return spawnCli(approveArgs(llm), projectRoot);
23980
+ }
23981
+ 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/;
23982
+ function parseDryRunBudget(output) {
23983
+ const m = output.match(BUDGET_RE);
23984
+ if (!m) {
23985
+ throw new Error(
23986
+ `Could not parse the dry-run cost preview from the CLI output. Raw output:
23987
+ ${output.trim()}`
23988
+ );
23989
+ }
23990
+ return {
23991
+ pairs: Number.parseInt(m[1], 10),
23992
+ deterministic: Number.parseInt(m[2], 10),
23993
+ reviewerCalls: Number.parseInt(m[3], 10),
23994
+ raw: m[0]
23995
+ };
23996
+ }
23997
+ async function dryRunApproveViaCli(projectRoot, llm) {
23998
+ const result = await spawnCli(dryRunArgs(llm), projectRoot);
23999
+ return parseDryRunBudget(`${result.stdout}
24000
+ ${result.stderr}`);
24001
+ }
24002
+
24003
+ // src/portal/server/router.ts
24004
+ function sendJson(res, status, body) {
24005
+ const payload = JSON.stringify(body);
24006
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
24007
+ res.end(payload);
24008
+ }
24009
+ function sendText(res, status, contentType, body) {
24010
+ res.writeHead(status, { "content-type": contentType });
24011
+ res.end(body);
24012
+ }
24013
+ async function readJsonBody(req) {
24014
+ const chunks = [];
24015
+ for await (const chunk of req) chunks.push(chunk);
24016
+ const text2 = Buffer.concat(chunks).toString("utf-8").trim();
24017
+ if (text2.length === 0) return {};
24018
+ try {
24019
+ const parsed = JSON.parse(text2);
24020
+ return parsed && typeof parsed === "object" ? parsed : {};
24021
+ } catch {
24022
+ return {};
24023
+ }
24024
+ }
24025
+ async function handleRequest(req, res, config) {
24026
+ const method = req.method ?? "GET";
24027
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
24028
+ const pathname = url.pathname;
24029
+ try {
24030
+ if (method === "GET" && pathname === "/") {
24031
+ const data = await freshPortalData(config.projectRoot, config.writeEnabled);
24032
+ const html = await renderLivePage(data);
24033
+ sendText(res, 200, "text/html; charset=utf-8", html);
24034
+ return;
24035
+ }
24036
+ if (method === "GET" && pathname === "/data") {
24037
+ const data = await freshPortalData(config.projectRoot, config.writeEnabled);
24038
+ sendJson(res, 200, data);
24039
+ return;
24040
+ }
24041
+ if (method === "GET" && pathname.startsWith("/static/")) {
24042
+ const rel = pathname.slice("/static/".length);
24043
+ const asset = await readStaticAsset(rel);
24044
+ if (!asset) {
24045
+ sendJson(res, 404, { error: "not-found", path: pathname });
24046
+ return;
24047
+ }
24048
+ res.writeHead(200, { "content-type": asset.contentType });
24049
+ res.end(asset.content);
24050
+ return;
24051
+ }
24052
+ if (method === "GET" && pathname === "/approve/dry-run") {
24053
+ const llm = url.searchParams.get("llm") !== "false";
24054
+ const preview = await dryRunApproveViaCli(config.projectRoot, llm);
24055
+ sendJson(res, 200, preview);
24056
+ return;
24057
+ }
24058
+ if (method === "POST" && pathname === "/approve") {
24059
+ if (!config.writeEnabled) {
24060
+ sendJson(res, 409, {
24061
+ error: "view-only",
24062
+ message: "This portal runs in view-only mode (--no-write); the Approve write action is disabled."
24063
+ });
24064
+ return;
24065
+ }
24066
+ const body = await readJsonBody(req);
24067
+ const llm = body.llm !== false;
24068
+ const result = await runApproveViaCli(config.projectRoot, llm);
24069
+ sendJson(res, 200, {
24070
+ ok: result.exitCode === 0,
24071
+ exitCode: result.exitCode,
24072
+ stdout: result.stdout,
24073
+ stderr: result.stderr
24074
+ });
24075
+ return;
24076
+ }
24077
+ sendJson(res, 404, { error: "not-found", method, path: pathname });
24078
+ } catch (err) {
24079
+ const message = err instanceof Error ? err.message : String(err);
24080
+ sendJson(res, 500, { error: "internal", message });
24081
+ }
24082
+ }
24083
+
24084
+ // src/portal/server/server.ts
24085
+ var LOOPBACK_HOST = "127.0.0.1";
24086
+ function startServer(opts) {
24087
+ const config = { projectRoot: opts.projectRoot, writeEnabled: opts.writeEnabled };
24088
+ const server = createServer((req, res) => {
24089
+ void handleRequest(req, res, config);
24090
+ });
24091
+ return new Promise((resolve6, reject) => {
24092
+ server.on("error", reject);
24093
+ server.listen(opts.port, LOOPBACK_HOST, () => {
24094
+ const address = server.address();
24095
+ const port = typeof address === "object" && address ? address.port : opts.port;
24096
+ resolve6({
24097
+ url: `http://${LOOPBACK_HOST}:${port}`,
24098
+ port,
24099
+ close: () => new Promise((res, rej) => {
24100
+ server.close((err) => err ? rej(err) : res());
24101
+ })
24102
+ });
24103
+ });
24104
+ });
24105
+ }
24106
+
24107
+ // src/cli/portal.ts
24108
+ function registerPortalCommand(program2) {
24109
+ 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) => {
24110
+ const n = parseInt(v, 10);
24111
+ if (Number.isNaN(n) || n < 0 || n > 65535) {
24112
+ throw new InvalidArgumentError2("--port must be an integer between 0 and 65535.");
24113
+ }
24114
+ return n;
24115
+ }).option("--open", "Open the page in the default browser").option("--no-write", "View-only mode \u2014 disable the Approve write action").action(async (options) => {
24116
+ try {
24117
+ await runPortal(options);
24118
+ } catch (error) {
24119
+ abortOnUnexpectedError(error, "opening the portal");
24120
+ }
24121
+ });
24122
+ }
24123
+ async function runPortal(options) {
24124
+ const graph = await loadGraphOrAbort(process.cwd());
24125
+ initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
24126
+ const projectRoot = projectRootFromGraph(graph.rootPath);
24127
+ if (options.static) {
24128
+ const data = await extractPortalData(projectRoot, { writeEnabled: options.write });
24129
+ const outPath = path61.resolve(projectRoot, options.out ?? "yg-portal.html");
24130
+ await emitStatic(data, outPath);
24131
+ process.stdout.write(`Portal page written to ${outPath}
24132
+ `);
24133
+ if (options.open) {
24134
+ await openInBrowser(outPath);
24135
+ }
24136
+ return;
24137
+ }
24138
+ await servePortal(projectRoot, options);
24139
+ }
24140
+ async function servePortal(projectRoot, options) {
24141
+ const handle = await startServer({
24142
+ projectRoot,
24143
+ port: options.port ?? DEFAULT_PORT,
24144
+ writeEnabled: options.write
24145
+ });
24146
+ const mode = options.write ? "" : " (view-only \u2014 Approve disabled)";
24147
+ process.stdout.write(`Portal running at ${handle.url}${mode}
24148
+ `);
24149
+ process.stdout.write("Press Ctrl+C to stop.\n");
24150
+ const shutdown = () => {
24151
+ void handle.close().finally(() => process.exit(0));
24152
+ };
24153
+ process.once("SIGINT", shutdown);
24154
+ process.once("SIGTERM", shutdown);
24155
+ if (options.open) {
24156
+ await openInBrowser(handle.url);
24157
+ }
24158
+ }
24159
+ var DEFAULT_PORT = 4317;
24160
+ async function openInBrowser(filePath) {
24161
+ const { spawn: spawn3 } = await import("child_process");
24162
+ const platform = process.platform;
24163
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
24164
+ const args = platform === "win32" ? ["/c", "start", "", filePath] : [filePath];
24165
+ try {
24166
+ const child = spawn3(command, args, { stdio: "ignore", detached: true });
24167
+ child.on("error", (error) => {
24168
+ debugWrite(`[portal] openInBrowser spawn error: ${error instanceof Error ? error.message : String(error)}`);
24169
+ });
24170
+ child.unref();
24171
+ } catch (error) {
24172
+ debugWrite(`[portal] openInBrowser failed: ${error instanceof Error ? error.message : String(error)}`);
24173
+ }
24174
+ }
24175
+
24176
+ // src/bin.ts
24177
+ import { readFileSync as readFileSync11 } from "fs";
24178
+ import { fileURLToPath as fileURLToPath6 } from "url";
22592
24179
  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();
24180
+ var __filename3 = fileURLToPath6(import.meta.url);
24181
+ var __dirname4 = dirname2(__filename3);
24182
+ var pkg = JSON.parse(readFileSync11(join7(__dirname4, "..", "package.json"), "utf-8"));
24183
+ var program = new Command3();
22597
24184
  program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(pkg.version);
22598
24185
  registerInitCommand(program);
22599
24186
  registerBuildCommand(program);
@@ -22610,6 +24197,7 @@ registerTypeSuggestCommand(program);
22610
24197
  registerKnowledgeCommand(program);
22611
24198
  registerSchemasCommand(program);
22612
24199
  registerSuppressionsCommand(program);
24200
+ registerPortalCommand(program);
22613
24201
  process.on("unhandledRejection", (reason) => {
22614
24202
  const msg = reason instanceof Error ? reason.message : String(reason);
22615
24203
  process.stderr.write(`Error: ${msg}