@chrisdudek/yg 5.2.5 → 5.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -10084,6 +10084,14 @@ async function parseFile(filePath, content20) {
10084
10084
  }
10085
10085
  return tree;
10086
10086
  }
10087
+ async function withParsedFile(filePath, content20, fn) {
10088
+ const tree = await parseFile(filePath, content20);
10089
+ try {
10090
+ return await fn(tree);
10091
+ } finally {
10092
+ tree.delete();
10093
+ }
10094
+ }
10087
10095
 
10088
10096
  // src/structure/ctx-parsers.ts
10089
10097
  var ParseAstNotPrewarmedError = class extends Error {
@@ -10706,94 +10714,99 @@ function companionInfra(what, why, next) {
10706
10714
  }
10707
10715
  async function runCompanionHook(params) {
10708
10716
  const { aspectDir, aspectId, nodePath, graph, projectRoot, subjectScope } = params;
10717
+ const ownCache = !params.parseCache;
10709
10718
  const astCache = params.parseCache ?? /* @__PURE__ */ new Map();
10710
10719
  const touchedFiles = [];
10711
- let mod;
10712
10720
  try {
10713
- mod = await loadHookModule({
10714
- aspectDir,
10715
- projectRoot,
10716
- filename: "companion.mjs",
10717
- resolveFailedCode: "COMPANION_LOADER_RESOLVE_FAILED"
10718
- });
10719
- } catch (err) {
10720
- if (err instanceof StructureRunnerError) {
10721
- return { kind: "infra", messageData: err.messageData };
10722
- }
10723
- return companionInfra(
10724
- `Failed to load companion.mjs for aspect '${aspectId}': ${err.message}`,
10725
- `The runner dynamically imports the aspect's companion.mjs before resolving companions.`,
10726
- `Ensure companion.mjs exists at the aspect directory and has no unresolved imports.`
10727
- );
10728
- }
10729
- const fn = mod.companion;
10730
- if (typeof fn !== "function") {
10731
- return companionInfra(
10732
- `companion.mjs does not export a function named 'companion' (aspect '${aspectId}'; got ${typeof fn}).`,
10733
- `The runner imports the named export 'companion' and calls companion(ctx).`,
10734
- `Add 'export function companion(ctx) { ... }' to companion.mjs (it may be async).`
10735
- );
10736
- }
10737
- let built;
10738
- try {
10739
- built = await buildUnitCtx({ aspectId, nodePath, graph, projectRoot, astCache, touchedFiles, subjectScope });
10740
- } catch (err) {
10741
- if (err instanceof StructureRunnerError) {
10742
- return { kind: "infra", messageData: err.messageData };
10721
+ let mod;
10722
+ try {
10723
+ mod = await loadHookModule({
10724
+ aspectDir,
10725
+ projectRoot,
10726
+ filename: "companion.mjs",
10727
+ resolveFailedCode: "COMPANION_LOADER_RESOLVE_FAILED"
10728
+ });
10729
+ } catch (err) {
10730
+ if (err instanceof StructureRunnerError) {
10731
+ return { kind: "infra", messageData: err.messageData };
10732
+ }
10733
+ return companionInfra(
10734
+ `Failed to load companion.mjs for aspect '${aspectId}': ${err.message}`,
10735
+ `The runner dynamically imports the aspect's companion.mjs before resolving companions.`,
10736
+ `Ensure companion.mjs exists at the aspect directory and has no unresolved imports.`
10737
+ );
10743
10738
  }
10744
- throw err;
10745
- }
10746
- const { ctx, recorder } = built;
10747
- let out;
10748
- try {
10749
- out = await fn(ctx);
10750
- } catch (err) {
10751
- if (err instanceof UndeclaredFsReadError || err instanceof UndeclaredGraphReadError || err instanceof ParseAstNotPrewarmedError) {
10739
+ const fn = mod.companion;
10740
+ if (typeof fn !== "function") {
10752
10741
  return companionInfra(
10753
- `companion.mjs for aspect '${aspectId}' read an undeclared path or node: ${err.message}`,
10754
- `A companion resolves the prompt only \u2014 it cannot judge code, so an undeclared read is an infrastructure fault, not a code violation.`,
10755
- `Declare a relation in yg-node.yaml to the node owning that path, or read only relation-reachable files.`
10742
+ `companion.mjs does not export a function named 'companion' (aspect '${aspectId}'; got ${typeof fn}).`,
10743
+ `The runner imports the named export 'companion' and calls companion(ctx).`,
10744
+ `Add 'export function companion(ctx) { ... }' to companion.mjs (it may be async).`
10756
10745
  );
10757
10746
  }
10758
- return companionInfra(
10759
- `companion hook threw while resolving companions (aspect '${aspectId}'): ${err.message}`,
10760
- `${err.stack ?? ""}`,
10761
- `Fix the bug in companion.mjs, then re-run: yg check --approve`
10762
- );
10763
- }
10764
- if (!Array.isArray(out)) {
10765
- return companionInfra(
10766
- `companion.mjs returned ${typeof out}, expected an array of { path: string, label?: string } (aspect '${aspectId}').`,
10767
- `The runner attaches each returned path to the reviewer prompt; a non-array return cannot be interpreted.`,
10768
- `Return [] or { path, label? }[] from companion.`
10769
- );
10770
- }
10771
- const descriptors = [];
10772
- for (const d of out) {
10773
- if (typeof d !== "object" || d === null || typeof d.path !== "string") {
10747
+ let built;
10748
+ try {
10749
+ built = await buildUnitCtx({ aspectId, nodePath, graph, projectRoot, astCache, touchedFiles, subjectScope });
10750
+ } catch (err) {
10751
+ if (err instanceof StructureRunnerError) {
10752
+ return { kind: "infra", messageData: err.messageData };
10753
+ }
10754
+ throw err;
10755
+ }
10756
+ const { ctx, recorder } = built;
10757
+ let out;
10758
+ try {
10759
+ out = await fn(ctx);
10760
+ } catch (err) {
10761
+ if (err instanceof UndeclaredFsReadError || err instanceof UndeclaredGraphReadError || err instanceof ParseAstNotPrewarmedError) {
10762
+ return companionInfra(
10763
+ `companion.mjs for aspect '${aspectId}' read an undeclared path or node: ${err.message}`,
10764
+ `A companion resolves the prompt only \u2014 it cannot judge code, so an undeclared read is an infrastructure fault, not a code violation.`,
10765
+ `Declare a relation in yg-node.yaml to the node owning that path, or read only relation-reachable files.`
10766
+ );
10767
+ }
10774
10768
  return companionInfra(
10775
- `companion.mjs returned an entry that is not { path: string, label?: string } (aspect '${aspectId}').`,
10776
- `Each companion descriptor must carry a string 'path' (and an optional string 'label').`,
10777
- `Return objects shaped { path: string, label?: string } from companion.`
10769
+ `companion hook threw while resolving companions (aspect '${aspectId}'): ${err.message}`,
10770
+ `${err.stack ?? ""}`,
10771
+ `Fix the bug in companion.mjs, then re-run: yg check --approve`
10778
10772
  );
10779
10773
  }
10780
- const dd = d;
10781
- if (dd.label !== void 0 && typeof dd.label !== "string") {
10774
+ if (!Array.isArray(out)) {
10782
10775
  return companionInfra(
10783
- `companion.mjs returned an entry whose 'label' is not a string (aspect '${aspectId}'; got ${typeof dd.label}).`,
10784
- `A companion descriptor's optional 'label' is a human tag and must be a string when present.`,
10785
- `Omit 'label' or set it to a string in companion.`
10776
+ `companion.mjs returned ${typeof out}, expected an array of { path: string, label?: string } (aspect '${aspectId}').`,
10777
+ `The runner attaches each returned path to the reviewer prompt; a non-array return cannot be interpreted.`,
10778
+ `Return [] or { path, label? }[] from companion.`
10786
10779
  );
10787
10780
  }
10788
- descriptors.push({ path: dd.path, ...dd.label !== void 0 ? { label: dd.label } : {} });
10781
+ const descriptors = [];
10782
+ for (const d of out) {
10783
+ if (typeof d !== "object" || d === null || typeof d.path !== "string") {
10784
+ return companionInfra(
10785
+ `companion.mjs returned an entry that is not { path: string, label?: string } (aspect '${aspectId}').`,
10786
+ `Each companion descriptor must carry a string 'path' (and an optional string 'label').`,
10787
+ `Return objects shaped { path: string, label?: string } from companion.`
10788
+ );
10789
+ }
10790
+ const dd = d;
10791
+ if (dd.label !== void 0 && typeof dd.label !== "string") {
10792
+ return companionInfra(
10793
+ `companion.mjs returned an entry whose 'label' is not a string (aspect '${aspectId}'; got ${typeof dd.label}).`,
10794
+ `A companion descriptor's optional 'label' is a human tag and must be a string when present.`,
10795
+ `Omit 'label' or set it to a string in companion.`
10796
+ );
10797
+ }
10798
+ descriptors.push({ path: dd.path, ...dd.label !== void 0 ? { label: dd.label } : {} });
10799
+ }
10800
+ return {
10801
+ kind: "ok",
10802
+ descriptors,
10803
+ touchedFiles,
10804
+ observations: recorder.snapshot(),
10805
+ observationsTainted: recorder.tainted
10806
+ };
10807
+ } finally {
10808
+ if (ownCache) destroyParseCache(astCache);
10789
10809
  }
10790
- return {
10791
- kind: "ok",
10792
- descriptors,
10793
- touchedFiles,
10794
- observations: recorder.snapshot(),
10795
- observationsTainted: recorder.tainted
10796
- };
10797
10810
  }
10798
10811
 
10799
10812
  // src/structure/runner.ts
@@ -10951,13 +10964,12 @@ async function resolveSuppressedRangesForPrompt(subjects, aspectId) {
10951
10964
  for (const subject of subjects) {
10952
10965
  const content20 = subject.bytes.toString("utf8");
10953
10966
  const hasGrammar = getLanguageForExtension(extname5(subject.path).toLowerCase()) !== null;
10954
- let tree;
10955
- if (hasGrammar) {
10956
- tree = await parseFile(subject.path, content20);
10957
- }
10958
10967
  const totalLines = content20.split("\n").length;
10959
- const all = collectSuppressions(tree, subject.path, totalLines, content20);
10960
- tree?.delete();
10968
+ const all = hasGrammar ? await withParsedFile(
10969
+ subject.path,
10970
+ content20,
10971
+ (tree) => collectSuppressions(tree, subject.path, totalLines, content20)
10972
+ ) : collectSuppressions(void 0, subject.path, totalLines, content20);
10961
10973
  const ranges = formatSuppressedRangesForAspect(all, aspectId);
10962
10974
  if (ranges.length > 0) {
10963
10975
  byFile.push({ path: subject.path, ranges });
@@ -12144,22 +12156,14 @@ async function runRelationPass(graph, projectRoot, deps) {
12144
12156
  }
12145
12157
  }
12146
12158
  const ownerIndex = buildOwnerIndex(graph.nodes);
12147
- const parseCache = /* @__PURE__ */ new Map();
12148
- async function getParsed(record) {
12149
- if (parseCache.has(record.path)) return parseCache.get(record.path) ?? null;
12150
- if (!record.language) {
12151
- parseCache.set(record.path, null);
12152
- return null;
12153
- }
12154
- let parsed;
12159
+ async function parseSingle(record) {
12160
+ if (!record.language) return null;
12155
12161
  try {
12156
12162
  const tree = await parseFile(record.path, record.content);
12157
- parsed = { path: record.path, content: record.content, tree, language: record.language };
12163
+ return { path: record.path, content: record.content, tree, language: record.language };
12158
12164
  } catch {
12159
- parsed = null;
12165
+ return null;
12160
12166
  }
12161
- parseCache.set(record.path, parsed);
12162
- return parsed;
12163
12167
  }
12164
12168
  const symbolTable = new SymbolTable();
12165
12169
  const recordsByLanguage = /* @__PURE__ */ new Map();
@@ -12183,11 +12187,15 @@ async function runRelationPass(graph, projectRoot, deps) {
12183
12187
  }
12184
12188
  const symbols = [];
12185
12189
  for (const record of records) {
12186
- const parsed = await getParsed(record);
12190
+ const parsed = await parseSingle(record);
12187
12191
  if (!parsed) continue;
12188
- for (const decl of extractor.declarations(parsed)) {
12189
- symbols.push([decl.symbolKey, record.path]);
12190
- symbolTable.declare(language, decl.symbolKey, record.path);
12192
+ try {
12193
+ for (const decl of extractor.declarations(parsed)) {
12194
+ symbols.push([decl.symbolKey, record.path]);
12195
+ symbolTable.declare(language, decl.symbolKey, record.path);
12196
+ }
12197
+ } finally {
12198
+ parsed.tree.delete();
12191
12199
  }
12192
12200
  }
12193
12201
  const toPersist = { builtFrom, symbols };
@@ -12197,10 +12205,14 @@ async function runRelationPass(graph, projectRoot, deps) {
12197
12205
  const projectGlobalUsings = /* @__PURE__ */ new Set();
12198
12206
  const projectGlobalUsingAliases = /* @__PURE__ */ new Map();
12199
12207
  for (const record of csharpRecords) {
12200
- const parsed = await getParsed(record);
12208
+ const parsed = await parseSingle(record);
12201
12209
  if (!parsed) continue;
12202
- for (const prefix of collectGlobalUsings(parsed)) projectGlobalUsings.add(prefix);
12203
- for (const [name, fqn] of collectGlobalUsingAliases(parsed)) projectGlobalUsingAliases.set(name, fqn);
12210
+ try {
12211
+ for (const prefix of collectGlobalUsings(parsed)) projectGlobalUsings.add(prefix);
12212
+ for (const [name, fqn] of collectGlobalUsingAliases(parsed)) projectGlobalUsingAliases.set(name, fqn);
12213
+ } finally {
12214
+ parsed.tree.delete();
12215
+ }
12204
12216
  }
12205
12217
  const csharpGlobalUsings = [...projectGlobalUsings];
12206
12218
  const csharpGlobalUsingAliases = [...projectGlobalUsingAliases.entries()];
@@ -12235,17 +12247,21 @@ async function runRelationPass(graph, projectRoot, deps) {
12235
12247
  if (!record.language) continue;
12236
12248
  const extractor = deps.extractorFor(record.language);
12237
12249
  if (!extractor) continue;
12238
- const parsed = await getParsed(record);
12250
+ const parsed = await parseSingle(record);
12239
12251
  if (!parsed) continue;
12240
- const detected = record.language === "csharp" ? csharpUses(parsed, {
12241
- projectGlobalUsings: csharpGlobalUsings,
12242
- projectGlobalUsingAliases: csharpGlobalUsingAliases
12243
- }) : extractor.uses(parsed);
12244
- for (const dep of detected) {
12245
- const ownerNode = resolveCandidateGroup(dep.candidates, resolver, record.path, record.language);
12246
- if (ownerNode !== void 0) {
12247
- resolvedDeps.push({ fromFile: record.path, line: dep.line, ownerNode });
12252
+ try {
12253
+ const detected = record.language === "csharp" ? csharpUses(parsed, {
12254
+ projectGlobalUsings: csharpGlobalUsings,
12255
+ projectGlobalUsingAliases: csharpGlobalUsingAliases
12256
+ }) : extractor.uses(parsed);
12257
+ for (const dep of detected) {
12258
+ const ownerNode = resolveCandidateGroup(dep.candidates, resolver, record.path, record.language);
12259
+ if (ownerNode !== void 0) {
12260
+ resolvedDeps.push({ fromFile: record.path, line: dep.line, ownerNode });
12261
+ }
12248
12262
  }
12263
+ } finally {
12264
+ parsed.tree.delete();
12249
12265
  }
12250
12266
  }
12251
12267
  const violations = verifyNodeDeps(nodeId, resolvedDeps, graphView);
@@ -12256,9 +12272,6 @@ async function runRelationPass(graph, projectRoot, deps) {
12256
12272
  violationsByNode.set(nodeId, { verdict: "approved", violations: [] });
12257
12273
  }
12258
12274
  }
12259
- for (const entry of parseCache.values()) {
12260
- if (entry) entry.tree.delete();
12261
- }
12262
12275
  return { violationsByNode };
12263
12276
  }
12264
12277
 
@@ -15818,21 +15831,7 @@ async function runFill(graph, opts) {
15818
15831
  }
15819
15832
 
15820
15833
  // src/cli/check.ts
15821
- import { execFileSync } from "child_process";
15822
15834
  import path48 from "path";
15823
- function collectGitFiles(projectRoot) {
15824
- try {
15825
- const output = execFileSync("git", ["ls-files", "."], {
15826
- cwd: projectRoot,
15827
- encoding: "utf-8",
15828
- stdio: ["pipe", "pipe", "pipe"]
15829
- });
15830
- return output.trim().split("\n").filter((f) => f.length > 0);
15831
- } catch (e) {
15832
- debugWrite(`[check] git ls-files failed: ${e instanceof Error ? e.message : String(e)}`);
15833
- return null;
15834
- }
15835
- }
15836
15835
  function registerCheckCommand(program2) {
15837
15836
  program2.command("check").description("Unified graph gate \u2014 verification, coverage, completeness").option("--approve", "Fill every unverified pair (deterministic first, then LLM), then report").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.").action(async (opts) => {
15838
15837
  try {
@@ -15840,7 +15839,7 @@ function registerCheckCommand(program2) {
15840
15839
  const graph = await loadGraphOrAbort(cwd, { tolerateInvalidConfig: true });
15841
15840
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
15842
15841
  const projectRoot = path48.dirname(graph.rootPath);
15843
- const gitFiles = collectGitFiles(projectRoot);
15842
+ const gitFiles = await walkRepoFiles(projectRoot);
15844
15843
  const wantsTop = opts.top !== void 0;
15845
15844
  if (wantsTop && opts.summary) {
15846
15845
  process.stderr.write(chalk8.red(buildIssueMessage({
@@ -16259,6 +16258,9 @@ async function runAstAspect(params) {
16259
16258
  next: `Reinstall the CLI.`
16260
16259
  });
16261
16260
  }
16261
+ if (!params.parseCache) {
16262
+ localTrees.push(ast);
16263
+ }
16262
16264
  if (ast.rootNode.hasError) {
16263
16265
  const err = findFirstErrorNode(ast.rootNode);
16264
16266
  throw new AstRunnerError("AST_SOURCE_PARSE_ERROR", {
@@ -16269,8 +16271,6 @@ async function runAstAspect(params) {
16269
16271
  }
16270
16272
  if (params.parseCache) {
16271
16273
  params.parseCache.set(f.path, { content: content20, ast });
16272
- } else {
16273
- localTrees.push(ast);
16274
16274
  }
16275
16275
  sourceFiles.push({ path: f.path, content: content20, ast });
16276
16276
  }
@@ -21823,7 +21823,6 @@ function registerSchemasCommand(program2) {
21823
21823
  // src/cli/suppressions.ts
21824
21824
  import chalk14 from "chalk";
21825
21825
  import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
21826
- import { execFileSync as execFileSync2 } from "child_process";
21827
21826
  import path57 from "path";
21828
21827
  function isBinaryContent(buf) {
21829
21828
  const checkLen = Math.min(buf.length, 8192);
@@ -21852,10 +21851,11 @@ async function scanMarkersForFile(relFile, text2) {
21852
21851
  return scanSuppressionMarkers(text2);
21853
21852
  }
21854
21853
  try {
21855
- const tree = await parseFile(relFile, text2);
21856
- const result = scanSuppressionMarkersInComments(tree, relFile);
21857
- tree.delete();
21858
- return result;
21854
+ return await withParsedFile(
21855
+ relFile,
21856
+ text2,
21857
+ (tree) => scanSuppressionMarkersInComments(tree, relFile)
21858
+ );
21859
21859
  } catch (error) {
21860
21860
  debugWrite(`[suppressions] parse fallback (raw scan): ${relFile}: ${error instanceof Error ? error.message : String(error)}`);
21861
21861
  return scanSuppressionMarkers(text2);
@@ -21974,17 +21974,7 @@ function registerSuppressionsCommand(program2) {
21974
21974
  const graph = await loadGraphOrAbort(cwd);
21975
21975
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
21976
21976
  const projectRoot = path57.dirname(graph.rootPath);
21977
- let gitFiles = [];
21978
- try {
21979
- const output = execFileSync2("git", ["ls-files", "."], {
21980
- cwd: projectRoot,
21981
- encoding: "utf-8",
21982
- stdio: ["pipe", "pipe", "pipe"]
21983
- });
21984
- gitFiles = output.trim().split("\n").filter((f) => f.length > 0);
21985
- } catch (error) {
21986
- debugWrite(`[suppressions] git ls-files fallback: ${error instanceof Error ? error.message : String(error)}`);
21987
- }
21977
+ const gitFiles = await walkRepoFiles(projectRoot);
21988
21978
  const knownAspectIds = new Set(graph.aspects.map((a) => a.id));
21989
21979
  const report2 = await runSuppressionsScan(projectRoot, gitFiles, knownAspectIds);
21990
21980
  process.stdout.write(formatSuppressionsOutput(report2));
package/dist/structure.js CHANGED
@@ -606,6 +606,14 @@ async function parseFile(filePath, content) {
606
606
  }
607
607
  return tree;
608
608
  }
609
+ async function withParsedFile(filePath, content, fn) {
610
+ const tree = await parseFile(filePath, content);
611
+ try {
612
+ return await fn(tree);
613
+ } finally {
614
+ tree.delete();
615
+ }
616
+ }
609
617
 
610
618
  // src/structure/ctx-parsers.ts
611
619
  var ParseAstNotPrewarmedError = class extends Error {
@@ -1599,13 +1607,12 @@ async function resolveSuppressedRangesForPrompt(subjects, aspectId) {
1599
1607
  for (const subject of subjects) {
1600
1608
  const content = subject.bytes.toString("utf8");
1601
1609
  const hasGrammar = getLanguageForExtension(extname5(subject.path).toLowerCase()) !== null;
1602
- let tree;
1603
- if (hasGrammar) {
1604
- tree = await parseFile(subject.path, content);
1605
- }
1606
1610
  const totalLines = content.split("\n").length;
1607
- const all = collectSuppressions(tree, subject.path, totalLines, content);
1608
- tree?.delete();
1611
+ const all = hasGrammar ? await withParsedFile(
1612
+ subject.path,
1613
+ content,
1614
+ (tree) => collectSuppressions(tree, subject.path, totalLines, content)
1615
+ ) : collectSuppressions(void 0, subject.path, totalLines, content);
1609
1616
  const ranges = formatSuppressedRangesForAspect(all, aspectId);
1610
1617
  if (ranges.length > 0) {
1611
1618
  byFile.push({ path: subject.path, ranges });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrisdudek/yg",
3
- "version": "5.2.5",
3
+ "version": "5.2.6",
4
4
  "description": "Architecture rules your AI coding agent can't ignore. It gets the rules for a file before it edits, and every change is checked — by a free local script or an LLM reviewer — before it moves on. Works with Claude Code, Cursor, Copilot, Codex, Cline.",
5
5
  "type": "module",
6
6
  "bin": {