@chrisdudek/yg 5.2.3 → 5.2.5
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/README.md +2 -2
- package/dist/bin.js +237 -204
- package/dist/structure.js +145 -126
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @chrisdudek/yg
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Stop babysitting your agent.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Your architecture rules become checks it can't skip, run on every change before it moves on. A script runs them locally for free, or an LLM reviews the call a script can't make. Checks run against your code, not your diffs. The feedback is specific, and the agent has to fix before it can move on. Works with Claude Code, Cursor, Copilot, Codex, Cline, and more.
|
|
6
6
|
|
|
7
7
|
See the [main README](https://github.com/krzysztofdudek/Yggdrasil#readme) for documentation, or visit
|
|
8
8
|
[krzysztofdudek.github.io/Yggdrasil](https://krzysztofdudek.github.io/Yggdrasil/).
|
package/dist/bin.js
CHANGED
|
@@ -10027,6 +10027,7 @@ var GRAMMAR_DIRS = [
|
|
|
10027
10027
|
];
|
|
10028
10028
|
var initPromise = null;
|
|
10029
10029
|
var langCache = /* @__PURE__ */ new Map();
|
|
10030
|
+
var parserCache = /* @__PURE__ */ new Map();
|
|
10030
10031
|
function init() {
|
|
10031
10032
|
if (initPromise === null) {
|
|
10032
10033
|
initPromise = Parser.init();
|
|
@@ -10067,8 +10068,11 @@ async function getParser(extension) {
|
|
|
10067
10068
|
});
|
|
10068
10069
|
}
|
|
10069
10070
|
const lang = await langP;
|
|
10071
|
+
const existing = parserCache.get(cacheKey);
|
|
10072
|
+
if (existing) return existing;
|
|
10070
10073
|
const parser = new Parser();
|
|
10071
10074
|
parser.setLanguage(lang);
|
|
10075
|
+
parserCache.set(cacheKey, parser);
|
|
10072
10076
|
return parser;
|
|
10073
10077
|
}
|
|
10074
10078
|
async function parseFile(filePath, content20) {
|
|
@@ -10404,6 +10408,14 @@ function validateCheckModuleExport(mod, opts) {
|
|
|
10404
10408
|
return { ok: true };
|
|
10405
10409
|
}
|
|
10406
10410
|
|
|
10411
|
+
// src/ast/parse-cache.ts
|
|
10412
|
+
function destroyParseCache(cache) {
|
|
10413
|
+
for (const { ast } of cache.values()) {
|
|
10414
|
+
ast.delete();
|
|
10415
|
+
}
|
|
10416
|
+
cache.clear();
|
|
10417
|
+
}
|
|
10418
|
+
|
|
10407
10419
|
// src/structure/hook-loader.ts
|
|
10408
10420
|
import * as fs4 from "fs";
|
|
10409
10421
|
import * as path29 from "path";
|
|
@@ -10787,143 +10799,149 @@ async function runCompanionHook(params) {
|
|
|
10787
10799
|
// src/structure/runner.ts
|
|
10788
10800
|
async function runStructureAspect(params) {
|
|
10789
10801
|
const { aspectDir, aspectId, nodePath, graph, projectRoot, subjectScope } = params;
|
|
10802
|
+
const ownCache = !params.parseCache;
|
|
10790
10803
|
const astCache = params.parseCache ?? /* @__PURE__ */ new Map();
|
|
10791
10804
|
const touchedFiles = [];
|
|
10792
|
-
const mod = await loadHookModule({ aspectDir, projectRoot, filename: "check.mjs" });
|
|
10793
|
-
const exportCheck = validateCheckModuleExport(mod, {
|
|
10794
|
-
codePrefix: "STRUCTURE",
|
|
10795
|
-
runnerLabel: `aspect '${aspectId}'`
|
|
10796
|
-
});
|
|
10797
|
-
if (!exportCheck.ok) {
|
|
10798
|
-
throw new StructureRunnerError(exportCheck.code, exportCheck.message);
|
|
10799
|
-
}
|
|
10800
|
-
const checkFn = mod.check;
|
|
10801
|
-
const { ctx, recorder, ownFiles, astInputSet } = await buildUnitCtx({
|
|
10802
|
-
aspectId,
|
|
10803
|
-
nodePath,
|
|
10804
|
-
graph,
|
|
10805
|
-
projectRoot,
|
|
10806
|
-
astCache,
|
|
10807
|
-
touchedFiles,
|
|
10808
|
-
subjectScope
|
|
10809
|
-
});
|
|
10810
|
-
let raw;
|
|
10811
10805
|
try {
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10806
|
+
let rangesFor2 = function(filePath) {
|
|
10807
|
+
const existing = rangesByFile.get(filePath);
|
|
10808
|
+
if (existing !== void 0) return existing;
|
|
10809
|
+
const cached = astCache.get(filePath);
|
|
10810
|
+
let ranges;
|
|
10811
|
+
if (cached) {
|
|
10812
|
+
ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
|
|
10813
|
+
} else {
|
|
10814
|
+
const content20 = contentByPath.get(filePath);
|
|
10815
|
+
ranges = content20 !== void 0 ? collectSuppressions(void 0, filePath, content20.split("\n").length, content20) : null;
|
|
10816
|
+
}
|
|
10817
|
+
rangesByFile.set(filePath, ranges);
|
|
10818
|
+
return ranges;
|
|
10819
|
+
};
|
|
10820
|
+
var rangesFor = rangesFor2;
|
|
10821
|
+
const mod = await loadHookModule({ aspectDir, projectRoot, filename: "check.mjs" });
|
|
10822
|
+
const exportCheck = validateCheckModuleExport(mod, {
|
|
10823
|
+
codePrefix: "STRUCTURE",
|
|
10824
|
+
runnerLabel: `aspect '${aspectId}'`
|
|
10825
|
+
});
|
|
10826
|
+
if (!exportCheck.ok) {
|
|
10827
|
+
throw new StructureRunnerError(exportCheck.code, exportCheck.message);
|
|
10826
10828
|
}
|
|
10827
|
-
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10829
|
+
const checkFn = mod.check;
|
|
10830
|
+
const { ctx, recorder, ownFiles, astInputSet } = await buildUnitCtx({
|
|
10831
|
+
aspectId,
|
|
10832
|
+
nodePath,
|
|
10833
|
+
graph,
|
|
10834
|
+
projectRoot,
|
|
10835
|
+
astCache,
|
|
10836
|
+
touchedFiles,
|
|
10837
|
+
subjectScope
|
|
10838
|
+
});
|
|
10839
|
+
let raw;
|
|
10840
|
+
try {
|
|
10841
|
+
raw = checkFn(ctx);
|
|
10842
|
+
} catch (err) {
|
|
10843
|
+
if (err instanceof UndeclaredFsReadError) {
|
|
10844
|
+
return {
|
|
10845
|
+
violations: [{
|
|
10846
|
+
message: `Aspect tried to read undeclared path '${err.path}'. Add a relation in yg-node.yaml to the node owning this path.`,
|
|
10847
|
+
kind: "structure-aspect-undeclared-fs-read",
|
|
10848
|
+
file: `.yggdrasil/aspects/${aspectId}/check.mjs`
|
|
10849
|
+
}],
|
|
10850
|
+
touchedFiles: [],
|
|
10851
|
+
succeeded: false,
|
|
10852
|
+
observations: recorder.snapshot(),
|
|
10853
|
+
observationsTainted: recorder.tainted
|
|
10854
|
+
};
|
|
10855
|
+
}
|
|
10856
|
+
if (err instanceof UndeclaredGraphReadError) {
|
|
10857
|
+
return {
|
|
10858
|
+
violations: [{
|
|
10859
|
+
message: `Aspect tried to read undeclared graph node '${err.nodePath}'. Add a relation in yg-node.yaml.`,
|
|
10860
|
+
kind: "structure-aspect-undeclared-graph-read",
|
|
10861
|
+
file: `.yggdrasil/aspects/${aspectId}/check.mjs`
|
|
10862
|
+
}],
|
|
10863
|
+
touchedFiles: [],
|
|
10864
|
+
succeeded: false,
|
|
10865
|
+
observations: recorder.snapshot(),
|
|
10866
|
+
observationsTainted: recorder.tainted
|
|
10867
|
+
};
|
|
10868
|
+
}
|
|
10869
|
+
if (err instanceof ParseAstNotPrewarmedError) {
|
|
10870
|
+
return {
|
|
10871
|
+
violations: [{
|
|
10872
|
+
message: `Aspect called ctx.parseAst on '${err.filePath}', which was not pre-warmed by the dispatcher. Add a declared relation to the node owning this file, or use ctx.parseYaml/Json/Toml if AST is not required.`,
|
|
10873
|
+
kind: "structure-aspect-parseast-not-prewarmed",
|
|
10874
|
+
file: `.yggdrasil/model/${nodePath}/yg-node.yaml`
|
|
10875
|
+
}],
|
|
10876
|
+
touchedFiles: [],
|
|
10877
|
+
succeeded: false,
|
|
10878
|
+
observations: recorder.snapshot(),
|
|
10879
|
+
observationsTainted: recorder.tainted
|
|
10880
|
+
};
|
|
10881
|
+
}
|
|
10882
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_THROWN", {
|
|
10883
|
+
what: `check.mjs threw an exception while running (aspect '${aspectId}').`,
|
|
10884
|
+
why: `${err.message}
|
|
10885
|
+
${err.stack ?? ""}`,
|
|
10886
|
+
next: `Fix the bug in check.mjs, then re-run: yg check --approve`
|
|
10887
|
+
});
|
|
10839
10888
|
}
|
|
10840
|
-
if (
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
}],
|
|
10847
|
-
touchedFiles: [],
|
|
10848
|
-
succeeded: false,
|
|
10849
|
-
observations: recorder.snapshot(),
|
|
10850
|
-
observationsTainted: recorder.tainted
|
|
10851
|
-
};
|
|
10889
|
+
if (raw !== null && typeof raw === "object" && typeof raw.then === "function") {
|
|
10890
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_ASYNC", {
|
|
10891
|
+
what: `check.mjs returned a Promise; only synchronous returns are supported.`,
|
|
10892
|
+
why: `The runner does not await check's return value.`,
|
|
10893
|
+
next: `Refactor check to be synchronous.`
|
|
10894
|
+
});
|
|
10852
10895
|
}
|
|
10853
|
-
|
|
10854
|
-
what: `check.mjs threw an exception while running (aspect '${aspectId}').`,
|
|
10855
|
-
why: `${err.message}
|
|
10856
|
-
${err.stack ?? ""}`,
|
|
10857
|
-
next: `Fix the bug in check.mjs, then re-run: yg check --approve`
|
|
10858
|
-
});
|
|
10859
|
-
}
|
|
10860
|
-
if (raw !== null && typeof raw === "object" && typeof raw.then === "function") {
|
|
10861
|
-
throw new StructureRunnerError("STRUCTURE_CHECK_ASYNC", {
|
|
10862
|
-
what: `check.mjs returned a Promise; only synchronous returns are supported.`,
|
|
10863
|
-
why: `The runner does not await check's return value.`,
|
|
10864
|
-
next: `Refactor check to be synchronous.`
|
|
10865
|
-
});
|
|
10866
|
-
}
|
|
10867
|
-
if (!Array.isArray(raw)) {
|
|
10868
|
-
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
10869
|
-
what: `check.mjs returned ${typeof raw}, expected Violation[].`,
|
|
10870
|
-
why: `The runner reports violations from the array returned by check.`,
|
|
10871
|
-
next: `Return [] or Violation[] from check.`
|
|
10872
|
-
});
|
|
10873
|
-
}
|
|
10874
|
-
const contextFiles = new Set(ownFiles.map((f) => f.path));
|
|
10875
|
-
for (const t of touchedFiles) contextFiles.add(t);
|
|
10876
|
-
const violations = [];
|
|
10877
|
-
for (const v of raw) {
|
|
10878
|
-
if (typeof v !== "object" || v === null || typeof v.message !== "string") {
|
|
10896
|
+
if (!Array.isArray(raw)) {
|
|
10879
10897
|
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
10880
|
-
what: `
|
|
10881
|
-
why: `The runner
|
|
10882
|
-
next: `Return
|
|
10898
|
+
what: `check.mjs returned ${typeof raw}, expected Violation[].`,
|
|
10899
|
+
why: `The runner reports violations from the array returned by check.`,
|
|
10900
|
+
next: `Return [] or Violation[] from check.`
|
|
10883
10901
|
});
|
|
10884
10902
|
}
|
|
10885
|
-
const
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10903
|
+
const contextFiles = new Set(ownFiles.map((f) => f.path));
|
|
10904
|
+
for (const t of touchedFiles) contextFiles.add(t);
|
|
10905
|
+
const violations = [];
|
|
10906
|
+
for (const v of raw) {
|
|
10907
|
+
if (typeof v !== "object" || v === null || typeof v.message !== "string") {
|
|
10908
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
10909
|
+
what: `Violation entry must be an object with a string 'message' field.`,
|
|
10910
|
+
why: `The runner renders each violation from its message and optional file/line.`,
|
|
10911
|
+
next: `Return objects shaped { message: string, file?: string, line?: number } from check.`
|
|
10912
|
+
});
|
|
10913
|
+
}
|
|
10914
|
+
const vv = v;
|
|
10915
|
+
if (typeof vv.file === "string" && !contextFiles.has(normalizeMappingPath(vv.file))) {
|
|
10916
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_FILE_NOT_IN_CONTEXT", {
|
|
10917
|
+
what: `Violation references file '${vv.file}' not in ctx (own mapping or touched via ctx.fs/ctx.graph).`,
|
|
10918
|
+
why: `Author cannot synthesize violations against files they were not given.`,
|
|
10919
|
+
next: `Return only violations for files in ctx, or declare a relation to the node owning '${vv.file}'.`
|
|
10920
|
+
});
|
|
10921
|
+
}
|
|
10922
|
+
violations.push(vv);
|
|
10892
10923
|
}
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
for (const f of [...ownFiles, ...astInputSet]) {
|
|
10897
|
-
contentByPath.set(normalizeMappingPath(f.path), f.content);
|
|
10898
|
-
}
|
|
10899
|
-
const rangesByFile = /* @__PURE__ */ new Map();
|
|
10900
|
-
function rangesFor(filePath) {
|
|
10901
|
-
const existing = rangesByFile.get(filePath);
|
|
10902
|
-
if (existing !== void 0) return existing;
|
|
10903
|
-
const cached = astCache.get(filePath);
|
|
10904
|
-
let ranges;
|
|
10905
|
-
if (cached) {
|
|
10906
|
-
ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
|
|
10907
|
-
} else {
|
|
10908
|
-
const content20 = contentByPath.get(filePath);
|
|
10909
|
-
ranges = content20 !== void 0 ? collectSuppressions(void 0, filePath, content20.split("\n").length, content20) : null;
|
|
10924
|
+
const contentByPath = /* @__PURE__ */ new Map();
|
|
10925
|
+
for (const f of [...ownFiles, ...astInputSet]) {
|
|
10926
|
+
contentByPath.set(normalizeMappingPath(f.path), f.content);
|
|
10910
10927
|
}
|
|
10911
|
-
rangesByFile
|
|
10912
|
-
|
|
10928
|
+
const rangesByFile = /* @__PURE__ */ new Map();
|
|
10929
|
+
const visible = violations.filter((v) => {
|
|
10930
|
+
if (typeof v.file !== "string" || typeof v.line !== "number") return true;
|
|
10931
|
+
const ranges = rangesFor2(normalizeMappingPath(v.file));
|
|
10932
|
+
if (!ranges) return true;
|
|
10933
|
+
return !isLineSuppressed(ranges, aspectId, v.line);
|
|
10934
|
+
});
|
|
10935
|
+
return {
|
|
10936
|
+
violations: visible,
|
|
10937
|
+
touchedFiles,
|
|
10938
|
+
succeeded: true,
|
|
10939
|
+
observations: recorder.snapshot(),
|
|
10940
|
+
observationsTainted: recorder.tainted
|
|
10941
|
+
};
|
|
10942
|
+
} finally {
|
|
10943
|
+
if (ownCache) destroyParseCache(astCache);
|
|
10913
10944
|
}
|
|
10914
|
-
const visible = violations.filter((v) => {
|
|
10915
|
-
if (typeof v.file !== "string" || typeof v.line !== "number") return true;
|
|
10916
|
-
const ranges = rangesFor(normalizeMappingPath(v.file));
|
|
10917
|
-
if (!ranges) return true;
|
|
10918
|
-
return !isLineSuppressed(ranges, aspectId, v.line);
|
|
10919
|
-
});
|
|
10920
|
-
return {
|
|
10921
|
-
violations: visible,
|
|
10922
|
-
touchedFiles,
|
|
10923
|
-
succeeded: true,
|
|
10924
|
-
observations: recorder.snapshot(),
|
|
10925
|
-
observationsTainted: recorder.tainted
|
|
10926
|
-
};
|
|
10927
10945
|
}
|
|
10928
10946
|
|
|
10929
10947
|
// src/structure/suppress-ranges.ts
|
|
@@ -10939,6 +10957,7 @@ async function resolveSuppressedRangesForPrompt(subjects, aspectId) {
|
|
|
10939
10957
|
}
|
|
10940
10958
|
const totalLines = content20.split("\n").length;
|
|
10941
10959
|
const all = collectSuppressions(tree, subject.path, totalLines, content20);
|
|
10960
|
+
tree?.delete();
|
|
10942
10961
|
const ranges = formatSuppressedRangesForAspect(all, aspectId);
|
|
10943
10962
|
if (ranges.length > 0) {
|
|
10944
10963
|
byFile.push({ path: subject.path, ranges });
|
|
@@ -12237,6 +12256,9 @@ async function runRelationPass(graph, projectRoot, deps) {
|
|
|
12237
12256
|
violationsByNode.set(nodeId, { verdict: "approved", violations: [] });
|
|
12238
12257
|
}
|
|
12239
12258
|
}
|
|
12259
|
+
for (const entry of parseCache.values()) {
|
|
12260
|
+
if (entry) entry.tree.delete();
|
|
12261
|
+
}
|
|
12240
12262
|
return { violationsByNode };
|
|
12241
12263
|
}
|
|
12242
12264
|
|
|
@@ -15821,20 +15843,20 @@ function registerCheckCommand(program2) {
|
|
|
15821
15843
|
const gitFiles = collectGitFiles(projectRoot);
|
|
15822
15844
|
const wantsTop = opts.top !== void 0;
|
|
15823
15845
|
if (wantsTop && opts.summary) {
|
|
15824
|
-
process.
|
|
15846
|
+
process.stderr.write(chalk8.red(buildIssueMessage({
|
|
15825
15847
|
what: "--top and --summary cannot be combined.",
|
|
15826
15848
|
why: "Both are read-only triage VIEWS of the same `yg check` result \u2014 --top renders the N highest-priority blocks, --summary renders per-node counts only. Asking for both at once is ambiguous; pick one lens.",
|
|
15827
15849
|
next: "Run: yg check --top <n> (priority blocks), or yg check --summary (per-node counts)."
|
|
15828
|
-
}) + "\n");
|
|
15850
|
+
}) + "\n"));
|
|
15829
15851
|
await exitAfterFlush(1);
|
|
15830
15852
|
return;
|
|
15831
15853
|
}
|
|
15832
15854
|
if ((wantsTop || opts.summary) && opts.approve) {
|
|
15833
|
-
process.
|
|
15855
|
+
process.stderr.write(chalk8.red(buildIssueMessage({
|
|
15834
15856
|
what: `${wantsTop ? "--top" : "--summary"} cannot be combined with --approve.`,
|
|
15835
15857
|
why: "--top and --summary triage the READ-ONLY check wall (they narrow the output of plain `yg check`, which writes nothing). --approve is the writer path; its own free cost preview is --dry-run. Mixing a read-only triage view with the writer is contradictory.",
|
|
15836
15858
|
next: `Run: yg check ${wantsTop ? "--top <n>" : "--summary"} (read-only triage), or yg check --approve --dry-run (preview the writer's cost).`
|
|
15837
|
-
}) + "\n");
|
|
15859
|
+
}) + "\n"));
|
|
15838
15860
|
await exitAfterFlush(1);
|
|
15839
15861
|
return;
|
|
15840
15862
|
}
|
|
@@ -15844,22 +15866,22 @@ function registerCheckCommand(program2) {
|
|
|
15844
15866
|
} else if (wantsTop) {
|
|
15845
15867
|
const n = resolveTopValue(opts.top);
|
|
15846
15868
|
if (n === null) {
|
|
15847
|
-
process.
|
|
15869
|
+
process.stderr.write(chalk8.red(buildIssueMessage({
|
|
15848
15870
|
what: `--top expects a non-negative whole number; got "${String(opts.top)}".`,
|
|
15849
15871
|
why: "--top N prints the N highest-priority issue blocks. A negative, fractional, or non-numeric value is meaningless, and printing the full wall instead would silently hide that the flag was ignored \u2014 masking the very output you tried to narrow.",
|
|
15850
15872
|
next: "Run: yg check --top 5 (top 5 blocks), yg check --top (just the suggestedNext block), or yg check (full output)."
|
|
15851
|
-
}) + "\n");
|
|
15873
|
+
}) + "\n"));
|
|
15852
15874
|
await exitAfterFlush(1);
|
|
15853
15875
|
return;
|
|
15854
15876
|
}
|
|
15855
15877
|
view = { kind: "top", n };
|
|
15856
15878
|
}
|
|
15857
15879
|
if (opts.dryRun && !opts.approve) {
|
|
15858
|
-
process.
|
|
15880
|
+
process.stderr.write(chalk8.red(buildIssueMessage({
|
|
15859
15881
|
what: "--dry-run requires --approve.",
|
|
15860
15882
|
why: "--dry-run previews what `yg check --approve` would fill (the reviewer-call budget and per-node breakdown) without writing or calling the reviewer; it is a mode of --approve, not a variant of the plain read. Plain `yg check` is already a free, no-write read.",
|
|
15861
15883
|
next: "Run: yg check --approve --dry-run (cost preview), or yg check (plain read)."
|
|
15862
|
-
}) + "\n");
|
|
15884
|
+
}) + "\n"));
|
|
15863
15885
|
await exitAfterFlush(1);
|
|
15864
15886
|
return;
|
|
15865
15887
|
}
|
|
@@ -15879,7 +15901,7 @@ function registerCheckCommand(program2) {
|
|
|
15879
15901
|
return;
|
|
15880
15902
|
}
|
|
15881
15903
|
const hasErrors2 = fill.checkResult.issues.some((i) => i.severity === "error");
|
|
15882
|
-
|
|
15904
|
+
await exitAfterFlush(hasErrors2 ? 1 : 0);
|
|
15883
15905
|
return;
|
|
15884
15906
|
} catch (err) {
|
|
15885
15907
|
if (err instanceof FillGatingError) {
|
|
@@ -15892,7 +15914,7 @@ function registerCheckCommand(program2) {
|
|
|
15892
15914
|
const result = await runCheck(graph, gitFiles);
|
|
15893
15915
|
process.stdout.write(formatOutput(result, view));
|
|
15894
15916
|
const hasErrors = result.issues.some((i) => i.severity === "error");
|
|
15895
|
-
|
|
15917
|
+
await exitAfterFlush(hasErrors ? 1 : 0);
|
|
15896
15918
|
} catch (error) {
|
|
15897
15919
|
debugWrite(`[check] error: ${error.message}`);
|
|
15898
15920
|
abortOnUnexpectedError(error, "running check");
|
|
@@ -16212,86 +16234,95 @@ async function runAstAspect(params) {
|
|
|
16212
16234
|
throw new AstRunnerError(exportCheck.code, exportCheck.message);
|
|
16213
16235
|
}
|
|
16214
16236
|
const checkFn = mod.check;
|
|
16237
|
+
const localTrees = [];
|
|
16215
16238
|
const sourceFiles = [];
|
|
16216
|
-
|
|
16217
|
-
const
|
|
16218
|
-
|
|
16219
|
-
|
|
16220
|
-
|
|
16239
|
+
try {
|
|
16240
|
+
for (const f of params.files) {
|
|
16241
|
+
const cached = params.parseCache?.get(f.path);
|
|
16242
|
+
if (cached !== void 0) {
|
|
16243
|
+
sourceFiles.push({ path: f.path, content: cached.content, ast: cached.ast });
|
|
16244
|
+
continue;
|
|
16245
|
+
}
|
|
16246
|
+
const content20 = await readFile16(path49.resolve(params.projectRoot, f.path), "utf-8");
|
|
16247
|
+
if (getLanguageForExtension(path49.extname(f.path).toLowerCase()) === null) {
|
|
16248
|
+
sourceFiles.push({ path: f.path, content: content20, ast: void 0 });
|
|
16249
|
+
continue;
|
|
16250
|
+
}
|
|
16251
|
+
let ast;
|
|
16252
|
+
try {
|
|
16253
|
+
ast = await parseFile(f.path, content20);
|
|
16254
|
+
} catch (e) {
|
|
16255
|
+
const msg = e.message ?? String(e);
|
|
16256
|
+
throw new AstRunnerError("AST_GRAMMAR_LOAD_FAILED", {
|
|
16257
|
+
what: `Failed to load tree-sitter grammar for ${f.path}: ${msg}`,
|
|
16258
|
+
why: `The bundled WASM grammar could not be loaded.`,
|
|
16259
|
+
next: `Reinstall the CLI.`
|
|
16260
|
+
});
|
|
16261
|
+
}
|
|
16262
|
+
if (ast.rootNode.hasError) {
|
|
16263
|
+
const err = findFirstErrorNode(ast.rootNode);
|
|
16264
|
+
throw new AstRunnerError("AST_SOURCE_PARSE_ERROR", {
|
|
16265
|
+
what: `Source file ${f.path} has a syntax error at line ${(err?.startPosition.row ?? 0) + 1}.`,
|
|
16266
|
+
why: `Tree-sitter could not parse the file cleanly.`,
|
|
16267
|
+
next: `Fix the syntax error in ${f.path}.`
|
|
16268
|
+
});
|
|
16269
|
+
}
|
|
16270
|
+
if (params.parseCache) {
|
|
16271
|
+
params.parseCache.set(f.path, { content: content20, ast });
|
|
16272
|
+
} else {
|
|
16273
|
+
localTrees.push(ast);
|
|
16274
|
+
}
|
|
16275
|
+
sourceFiles.push({ path: f.path, content: content20, ast });
|
|
16221
16276
|
}
|
|
16222
|
-
const
|
|
16223
|
-
|
|
16224
|
-
|
|
16225
|
-
|
|
16277
|
+
const rangesPerFile = /* @__PURE__ */ new Map();
|
|
16278
|
+
for (const f of sourceFiles) {
|
|
16279
|
+
const totalLines = f.content.split("\n").length;
|
|
16280
|
+
rangesPerFile.set(f.path, collectSuppressions(f.ast, f.path, totalLines, f.content));
|
|
16226
16281
|
}
|
|
16227
|
-
|
|
16282
|
+
const ctx = { files: sourceFiles };
|
|
16283
|
+
let raw;
|
|
16228
16284
|
try {
|
|
16229
|
-
|
|
16285
|
+
raw = checkFn(ctx);
|
|
16230
16286
|
} catch (e) {
|
|
16231
|
-
|
|
16232
|
-
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
next: `Reinstall the CLI.`
|
|
16287
|
+
throw new AstRunnerError("AST_CHECK_THROWN", {
|
|
16288
|
+
what: `check.mjs threw an exception while running (aspect '${params.aspectId}').`,
|
|
16289
|
+
why: (e instanceof Error ? e.stack : void 0) ?? String(e),
|
|
16290
|
+
next: `Fix the bug in check.mjs, then re-run: yg check --approve`
|
|
16236
16291
|
});
|
|
16237
16292
|
}
|
|
16238
|
-
if (
|
|
16239
|
-
|
|
16240
|
-
|
|
16241
|
-
|
|
16242
|
-
|
|
16243
|
-
next: `Fix the syntax error in ${f.path}.`
|
|
16293
|
+
if (raw !== null && typeof raw === "object" && typeof raw.then === "function") {
|
|
16294
|
+
throw new AstRunnerError("AST_CHECK_ASYNC", {
|
|
16295
|
+
what: `check.mjs returned a Promise; only synchronous returns are supported in v1.`,
|
|
16296
|
+
why: `The runner does not await check's return value.`,
|
|
16297
|
+
next: `Refactor check to be synchronous.`
|
|
16244
16298
|
});
|
|
16245
16299
|
}
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
16251
|
-
const totalLines = f.content.split("\n").length;
|
|
16252
|
-
rangesPerFile.set(f.path, collectSuppressions(f.ast, f.path, totalLines, f.content));
|
|
16253
|
-
}
|
|
16254
|
-
const ctx = { files: sourceFiles };
|
|
16255
|
-
let raw;
|
|
16256
|
-
try {
|
|
16257
|
-
raw = checkFn(ctx);
|
|
16258
|
-
} catch (e) {
|
|
16259
|
-
throw new AstRunnerError("AST_CHECK_THROWN", {
|
|
16260
|
-
what: `check.mjs threw an exception while running (aspect '${params.aspectId}').`,
|
|
16261
|
-
why: (e instanceof Error ? e.stack : void 0) ?? String(e),
|
|
16262
|
-
next: `Fix the bug in check.mjs, then re-run: yg check --approve`
|
|
16263
|
-
});
|
|
16264
|
-
}
|
|
16265
|
-
if (raw !== null && typeof raw === "object" && typeof raw.then === "function") {
|
|
16266
|
-
throw new AstRunnerError("AST_CHECK_ASYNC", {
|
|
16267
|
-
what: `check.mjs returned a Promise; only synchronous returns are supported in v1.`,
|
|
16268
|
-
why: `The runner does not await check's return value.`,
|
|
16269
|
-
next: `Refactor check to be synchronous.`
|
|
16270
|
-
});
|
|
16271
|
-
}
|
|
16272
|
-
if (!Array.isArray(raw)) {
|
|
16273
|
-
throw new AstRunnerError("AST_CHECK_RETURN_SHAPE", {
|
|
16274
|
-
what: `check.mjs returned ${typeof raw}, expected Violation[].`,
|
|
16275
|
-
why: `The runner reports violations from the array returned by check.`,
|
|
16276
|
-
next: `Return [] or Violation[] from check.`
|
|
16277
|
-
});
|
|
16278
|
-
}
|
|
16279
|
-
const contextPaths = new Set(sourceFiles.map((f) => f.path));
|
|
16280
|
-
for (const v of raw) {
|
|
16281
|
-
if (!contextPaths.has(v.file)) {
|
|
16282
|
-
throw new AstRunnerError("AST_CHECK_FILE_NOT_IN_CONTEXT", {
|
|
16283
|
-
what: `check.mjs returned a Violation referencing file '${v.file}' which is not in ctx.files (aspect '${params.aspectId}').`,
|
|
16284
|
-
why: `Author cannot synthesize violations against files they were not given. Suppress markers cannot reach unknown files.`,
|
|
16285
|
-
next: `Return only violations for files in ctx.files (the array passed to check).`
|
|
16300
|
+
if (!Array.isArray(raw)) {
|
|
16301
|
+
throw new AstRunnerError("AST_CHECK_RETURN_SHAPE", {
|
|
16302
|
+
what: `check.mjs returned ${typeof raw}, expected Violation[].`,
|
|
16303
|
+
why: `The runner reports violations from the array returned by check.`,
|
|
16304
|
+
next: `Return [] or Violation[] from check.`
|
|
16286
16305
|
});
|
|
16287
16306
|
}
|
|
16307
|
+
const contextPaths = new Set(sourceFiles.map((f) => f.path));
|
|
16308
|
+
for (const v of raw) {
|
|
16309
|
+
if (!contextPaths.has(v.file)) {
|
|
16310
|
+
throw new AstRunnerError("AST_CHECK_FILE_NOT_IN_CONTEXT", {
|
|
16311
|
+
what: `check.mjs returned a Violation referencing file '${v.file}' which is not in ctx.files (aspect '${params.aspectId}').`,
|
|
16312
|
+
why: `Author cannot synthesize violations against files they were not given. Suppress markers cannot reach unknown files.`,
|
|
16313
|
+
next: `Return only violations for files in ctx.files (the array passed to check).`
|
|
16314
|
+
});
|
|
16315
|
+
}
|
|
16316
|
+
}
|
|
16317
|
+
const filtered = raw.filter((v) => {
|
|
16318
|
+
const ranges = rangesPerFile.get(v.file);
|
|
16319
|
+
if (!ranges) return true;
|
|
16320
|
+
return !isLineSuppressed(ranges, params.aspectId, v.line);
|
|
16321
|
+
});
|
|
16322
|
+
return { violations: filtered };
|
|
16323
|
+
} finally {
|
|
16324
|
+
for (const t of localTrees) t.delete();
|
|
16288
16325
|
}
|
|
16289
|
-
const filtered = raw.filter((v) => {
|
|
16290
|
-
const ranges = rangesPerFile.get(v.file);
|
|
16291
|
-
if (!ranges) return true;
|
|
16292
|
-
return !isLineSuppressed(ranges, params.aspectId, v.line);
|
|
16293
|
-
});
|
|
16294
|
-
return { violations: filtered };
|
|
16295
16326
|
}
|
|
16296
16327
|
function findFirstErrorNode(node) {
|
|
16297
16328
|
if (node.isError) return node;
|
|
@@ -21822,7 +21853,9 @@ async function scanMarkersForFile(relFile, text2) {
|
|
|
21822
21853
|
}
|
|
21823
21854
|
try {
|
|
21824
21855
|
const tree = await parseFile(relFile, text2);
|
|
21825
|
-
|
|
21856
|
+
const result = scanSuppressionMarkersInComments(tree, relFile);
|
|
21857
|
+
tree.delete();
|
|
21858
|
+
return result;
|
|
21826
21859
|
} catch (error) {
|
|
21827
21860
|
debugWrite(`[suppressions] parse fallback (raw scan): ${relFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
21828
21861
|
return scanSuppressionMarkers(text2);
|
package/dist/structure.js
CHANGED
|
@@ -549,6 +549,7 @@ var GRAMMAR_DIRS = [
|
|
|
549
549
|
];
|
|
550
550
|
var initPromise = null;
|
|
551
551
|
var langCache = /* @__PURE__ */ new Map();
|
|
552
|
+
var parserCache = /* @__PURE__ */ new Map();
|
|
552
553
|
function init() {
|
|
553
554
|
if (initPromise === null) {
|
|
554
555
|
initPromise = Parser.init();
|
|
@@ -589,8 +590,11 @@ async function getParser(extension) {
|
|
|
589
590
|
});
|
|
590
591
|
}
|
|
591
592
|
const lang = await langP;
|
|
593
|
+
const existing = parserCache.get(cacheKey);
|
|
594
|
+
if (existing) return existing;
|
|
592
595
|
const parser = new Parser();
|
|
593
596
|
parser.setLanguage(lang);
|
|
597
|
+
parserCache.set(cacheKey, parser);
|
|
594
598
|
return parser;
|
|
595
599
|
}
|
|
596
600
|
async function parseFile(filePath, content) {
|
|
@@ -876,6 +880,14 @@ function validateCheckModuleExport(mod, opts) {
|
|
|
876
880
|
return { ok: true };
|
|
877
881
|
}
|
|
878
882
|
|
|
883
|
+
// src/ast/parse-cache.ts
|
|
884
|
+
function destroyParseCache(cache) {
|
|
885
|
+
for (const { ast } of cache.values()) {
|
|
886
|
+
ast.delete();
|
|
887
|
+
}
|
|
888
|
+
cache.clear();
|
|
889
|
+
}
|
|
890
|
+
|
|
879
891
|
// src/structure/hook-loader.ts
|
|
880
892
|
import * as fs4 from "fs";
|
|
881
893
|
import * as path8 from "path";
|
|
@@ -1435,143 +1447,149 @@ async function buildUnitCtx(params) {
|
|
|
1435
1447
|
// src/structure/runner.ts
|
|
1436
1448
|
async function runStructureAspect(params) {
|
|
1437
1449
|
const { aspectDir, aspectId, nodePath, graph, projectRoot, subjectScope } = params;
|
|
1450
|
+
const ownCache = !params.parseCache;
|
|
1438
1451
|
const astCache = params.parseCache ?? /* @__PURE__ */ new Map();
|
|
1439
1452
|
const touchedFiles = [];
|
|
1440
|
-
const mod = await loadHookModule({ aspectDir, projectRoot, filename: "check.mjs" });
|
|
1441
|
-
const exportCheck = validateCheckModuleExport(mod, {
|
|
1442
|
-
codePrefix: "STRUCTURE",
|
|
1443
|
-
runnerLabel: `aspect '${aspectId}'`
|
|
1444
|
-
});
|
|
1445
|
-
if (!exportCheck.ok) {
|
|
1446
|
-
throw new StructureRunnerError(exportCheck.code, exportCheck.message);
|
|
1447
|
-
}
|
|
1448
|
-
const checkFn = mod.check;
|
|
1449
|
-
const { ctx, recorder, ownFiles, astInputSet } = await buildUnitCtx({
|
|
1450
|
-
aspectId,
|
|
1451
|
-
nodePath,
|
|
1452
|
-
graph,
|
|
1453
|
-
projectRoot,
|
|
1454
|
-
astCache,
|
|
1455
|
-
touchedFiles,
|
|
1456
|
-
subjectScope
|
|
1457
|
-
});
|
|
1458
|
-
let raw;
|
|
1459
1453
|
try {
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1454
|
+
let rangesFor2 = function(filePath) {
|
|
1455
|
+
const existing = rangesByFile.get(filePath);
|
|
1456
|
+
if (existing !== void 0) return existing;
|
|
1457
|
+
const cached = astCache.get(filePath);
|
|
1458
|
+
let ranges;
|
|
1459
|
+
if (cached) {
|
|
1460
|
+
ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
|
|
1461
|
+
} else {
|
|
1462
|
+
const content = contentByPath.get(filePath);
|
|
1463
|
+
ranges = content !== void 0 ? collectSuppressions(void 0, filePath, content.split("\n").length, content) : null;
|
|
1464
|
+
}
|
|
1465
|
+
rangesByFile.set(filePath, ranges);
|
|
1466
|
+
return ranges;
|
|
1467
|
+
};
|
|
1468
|
+
var rangesFor = rangesFor2;
|
|
1469
|
+
const mod = await loadHookModule({ aspectDir, projectRoot, filename: "check.mjs" });
|
|
1470
|
+
const exportCheck = validateCheckModuleExport(mod, {
|
|
1471
|
+
codePrefix: "STRUCTURE",
|
|
1472
|
+
runnerLabel: `aspect '${aspectId}'`
|
|
1473
|
+
});
|
|
1474
|
+
if (!exportCheck.ok) {
|
|
1475
|
+
throw new StructureRunnerError(exportCheck.code, exportCheck.message);
|
|
1474
1476
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1477
|
+
const checkFn = mod.check;
|
|
1478
|
+
const { ctx, recorder, ownFiles, astInputSet } = await buildUnitCtx({
|
|
1479
|
+
aspectId,
|
|
1480
|
+
nodePath,
|
|
1481
|
+
graph,
|
|
1482
|
+
projectRoot,
|
|
1483
|
+
astCache,
|
|
1484
|
+
touchedFiles,
|
|
1485
|
+
subjectScope
|
|
1486
|
+
});
|
|
1487
|
+
let raw;
|
|
1488
|
+
try {
|
|
1489
|
+
raw = checkFn(ctx);
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
if (err instanceof UndeclaredFsReadError) {
|
|
1492
|
+
return {
|
|
1493
|
+
violations: [{
|
|
1494
|
+
message: `Aspect tried to read undeclared path '${err.path}'. Add a relation in yg-node.yaml to the node owning this path.`,
|
|
1495
|
+
kind: "structure-aspect-undeclared-fs-read",
|
|
1496
|
+
file: `.yggdrasil/aspects/${aspectId}/check.mjs`
|
|
1497
|
+
}],
|
|
1498
|
+
touchedFiles: [],
|
|
1499
|
+
succeeded: false,
|
|
1500
|
+
observations: recorder.snapshot(),
|
|
1501
|
+
observationsTainted: recorder.tainted
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
if (err instanceof UndeclaredGraphReadError) {
|
|
1505
|
+
return {
|
|
1506
|
+
violations: [{
|
|
1507
|
+
message: `Aspect tried to read undeclared graph node '${err.nodePath}'. Add a relation in yg-node.yaml.`,
|
|
1508
|
+
kind: "structure-aspect-undeclared-graph-read",
|
|
1509
|
+
file: `.yggdrasil/aspects/${aspectId}/check.mjs`
|
|
1510
|
+
}],
|
|
1511
|
+
touchedFiles: [],
|
|
1512
|
+
succeeded: false,
|
|
1513
|
+
observations: recorder.snapshot(),
|
|
1514
|
+
observationsTainted: recorder.tainted
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
if (err instanceof ParseAstNotPrewarmedError) {
|
|
1518
|
+
return {
|
|
1519
|
+
violations: [{
|
|
1520
|
+
message: `Aspect called ctx.parseAst on '${err.filePath}', which was not pre-warmed by the dispatcher. Add a declared relation to the node owning this file, or use ctx.parseYaml/Json/Toml if AST is not required.`,
|
|
1521
|
+
kind: "structure-aspect-parseast-not-prewarmed",
|
|
1522
|
+
file: `.yggdrasil/model/${nodePath}/yg-node.yaml`
|
|
1523
|
+
}],
|
|
1524
|
+
touchedFiles: [],
|
|
1525
|
+
succeeded: false,
|
|
1526
|
+
observations: recorder.snapshot(),
|
|
1527
|
+
observationsTainted: recorder.tainted
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_THROWN", {
|
|
1531
|
+
what: `check.mjs threw an exception while running (aspect '${aspectId}').`,
|
|
1532
|
+
why: `${err.message}
|
|
1533
|
+
${err.stack ?? ""}`,
|
|
1534
|
+
next: `Fix the bug in check.mjs, then re-run: yg check --approve`
|
|
1535
|
+
});
|
|
1487
1536
|
}
|
|
1488
|
-
if (
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
}],
|
|
1495
|
-
touchedFiles: [],
|
|
1496
|
-
succeeded: false,
|
|
1497
|
-
observations: recorder.snapshot(),
|
|
1498
|
-
observationsTainted: recorder.tainted
|
|
1499
|
-
};
|
|
1537
|
+
if (raw !== null && typeof raw === "object" && typeof raw.then === "function") {
|
|
1538
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_ASYNC", {
|
|
1539
|
+
what: `check.mjs returned a Promise; only synchronous returns are supported.`,
|
|
1540
|
+
why: `The runner does not await check's return value.`,
|
|
1541
|
+
next: `Refactor check to be synchronous.`
|
|
1542
|
+
});
|
|
1500
1543
|
}
|
|
1501
|
-
|
|
1502
|
-
what: `check.mjs threw an exception while running (aspect '${aspectId}').`,
|
|
1503
|
-
why: `${err.message}
|
|
1504
|
-
${err.stack ?? ""}`,
|
|
1505
|
-
next: `Fix the bug in check.mjs, then re-run: yg check --approve`
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
if (raw !== null && typeof raw === "object" && typeof raw.then === "function") {
|
|
1509
|
-
throw new StructureRunnerError("STRUCTURE_CHECK_ASYNC", {
|
|
1510
|
-
what: `check.mjs returned a Promise; only synchronous returns are supported.`,
|
|
1511
|
-
why: `The runner does not await check's return value.`,
|
|
1512
|
-
next: `Refactor check to be synchronous.`
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
1515
|
-
if (!Array.isArray(raw)) {
|
|
1516
|
-
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
1517
|
-
what: `check.mjs returned ${typeof raw}, expected Violation[].`,
|
|
1518
|
-
why: `The runner reports violations from the array returned by check.`,
|
|
1519
|
-
next: `Return [] or Violation[] from check.`
|
|
1520
|
-
});
|
|
1521
|
-
}
|
|
1522
|
-
const contextFiles = new Set(ownFiles.map((f) => f.path));
|
|
1523
|
-
for (const t of touchedFiles) contextFiles.add(t);
|
|
1524
|
-
const violations = [];
|
|
1525
|
-
for (const v of raw) {
|
|
1526
|
-
if (typeof v !== "object" || v === null || typeof v.message !== "string") {
|
|
1544
|
+
if (!Array.isArray(raw)) {
|
|
1527
1545
|
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
1528
|
-
what: `
|
|
1529
|
-
why: `The runner
|
|
1530
|
-
next: `Return
|
|
1546
|
+
what: `check.mjs returned ${typeof raw}, expected Violation[].`,
|
|
1547
|
+
why: `The runner reports violations from the array returned by check.`,
|
|
1548
|
+
next: `Return [] or Violation[] from check.`
|
|
1531
1549
|
});
|
|
1532
1550
|
}
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1551
|
+
const contextFiles = new Set(ownFiles.map((f) => f.path));
|
|
1552
|
+
for (const t of touchedFiles) contextFiles.add(t);
|
|
1553
|
+
const violations = [];
|
|
1554
|
+
for (const v of raw) {
|
|
1555
|
+
if (typeof v !== "object" || v === null || typeof v.message !== "string") {
|
|
1556
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
1557
|
+
what: `Violation entry must be an object with a string 'message' field.`,
|
|
1558
|
+
why: `The runner renders each violation from its message and optional file/line.`,
|
|
1559
|
+
next: `Return objects shaped { message: string, file?: string, line?: number } from check.`
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
const vv = v;
|
|
1563
|
+
if (typeof vv.file === "string" && !contextFiles.has(normalizeMappingPath(vv.file))) {
|
|
1564
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_FILE_NOT_IN_CONTEXT", {
|
|
1565
|
+
what: `Violation references file '${vv.file}' not in ctx (own mapping or touched via ctx.fs/ctx.graph).`,
|
|
1566
|
+
why: `Author cannot synthesize violations against files they were not given.`,
|
|
1567
|
+
next: `Return only violations for files in ctx, or declare a relation to the node owning '${vv.file}'.`
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
violations.push(vv);
|
|
1540
1571
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
for (const f of [...ownFiles, ...astInputSet]) {
|
|
1545
|
-
contentByPath.set(normalizeMappingPath(f.path), f.content);
|
|
1546
|
-
}
|
|
1547
|
-
const rangesByFile = /* @__PURE__ */ new Map();
|
|
1548
|
-
function rangesFor(filePath) {
|
|
1549
|
-
const existing = rangesByFile.get(filePath);
|
|
1550
|
-
if (existing !== void 0) return existing;
|
|
1551
|
-
const cached = astCache.get(filePath);
|
|
1552
|
-
let ranges;
|
|
1553
|
-
if (cached) {
|
|
1554
|
-
ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
|
|
1555
|
-
} else {
|
|
1556
|
-
const content = contentByPath.get(filePath);
|
|
1557
|
-
ranges = content !== void 0 ? collectSuppressions(void 0, filePath, content.split("\n").length, content) : null;
|
|
1572
|
+
const contentByPath = /* @__PURE__ */ new Map();
|
|
1573
|
+
for (const f of [...ownFiles, ...astInputSet]) {
|
|
1574
|
+
contentByPath.set(normalizeMappingPath(f.path), f.content);
|
|
1558
1575
|
}
|
|
1559
|
-
rangesByFile
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
return
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1576
|
+
const rangesByFile = /* @__PURE__ */ new Map();
|
|
1577
|
+
const visible = violations.filter((v) => {
|
|
1578
|
+
if (typeof v.file !== "string" || typeof v.line !== "number") return true;
|
|
1579
|
+
const ranges = rangesFor2(normalizeMappingPath(v.file));
|
|
1580
|
+
if (!ranges) return true;
|
|
1581
|
+
return !isLineSuppressed(ranges, aspectId, v.line);
|
|
1582
|
+
});
|
|
1583
|
+
return {
|
|
1584
|
+
violations: visible,
|
|
1585
|
+
touchedFiles,
|
|
1586
|
+
succeeded: true,
|
|
1587
|
+
observations: recorder.snapshot(),
|
|
1588
|
+
observationsTainted: recorder.tainted
|
|
1589
|
+
};
|
|
1590
|
+
} finally {
|
|
1591
|
+
if (ownCache) destroyParseCache(astCache);
|
|
1592
|
+
}
|
|
1575
1593
|
}
|
|
1576
1594
|
|
|
1577
1595
|
// src/structure/suppress-ranges.ts
|
|
@@ -1587,6 +1605,7 @@ async function resolveSuppressedRangesForPrompt(subjects, aspectId) {
|
|
|
1587
1605
|
}
|
|
1588
1606
|
const totalLines = content.split("\n").length;
|
|
1589
1607
|
const all = collectSuppressions(tree, subject.path, totalLines, content);
|
|
1608
|
+
tree?.delete();
|
|
1590
1609
|
const ranges = formatSuppressedRangesForAspect(all, aspectId);
|
|
1591
1610
|
if (ranges.length > 0) {
|
|
1592
1611
|
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.
|
|
3
|
+
"version": "5.2.5",
|
|
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": {
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"@tree-sitter-grammars/tree-sitter-kotlin": "^1.1.0",
|
|
86
86
|
"@tree-sitter-grammars/tree-sitter-toml": "^0.7.0",
|
|
87
87
|
"@tree-sitter-grammars/tree-sitter-yaml": "^0.7.1",
|
|
88
|
-
"@types/node": "^
|
|
88
|
+
"@types/node": "^26.0.0",
|
|
89
89
|
"@types/semver": "^7.7.1",
|
|
90
90
|
"@vitest/coverage-v8": "^4.1.8",
|
|
91
91
|
"eslint": "^10.4.1",
|