@chrisdudek/yg 5.2.4 → 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 +352 -329
- package/dist/structure.js +157 -131
- package/package.json +1 -1
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) {
|
|
@@ -10080,6 +10084,14 @@ async function parseFile(filePath, content20) {
|
|
|
10080
10084
|
}
|
|
10081
10085
|
return tree;
|
|
10082
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
|
+
}
|
|
10083
10095
|
|
|
10084
10096
|
// src/structure/ctx-parsers.ts
|
|
10085
10097
|
var ParseAstNotPrewarmedError = class extends Error {
|
|
@@ -10404,6 +10416,14 @@ function validateCheckModuleExport(mod, opts) {
|
|
|
10404
10416
|
return { ok: true };
|
|
10405
10417
|
}
|
|
10406
10418
|
|
|
10419
|
+
// src/ast/parse-cache.ts
|
|
10420
|
+
function destroyParseCache(cache) {
|
|
10421
|
+
for (const { ast } of cache.values()) {
|
|
10422
|
+
ast.delete();
|
|
10423
|
+
}
|
|
10424
|
+
cache.clear();
|
|
10425
|
+
}
|
|
10426
|
+
|
|
10407
10427
|
// src/structure/hook-loader.ts
|
|
10408
10428
|
import * as fs4 from "fs";
|
|
10409
10429
|
import * as path29 from "path";
|
|
@@ -10694,236 +10714,247 @@ function companionInfra(what, why, next) {
|
|
|
10694
10714
|
}
|
|
10695
10715
|
async function runCompanionHook(params) {
|
|
10696
10716
|
const { aspectDir, aspectId, nodePath, graph, projectRoot, subjectScope } = params;
|
|
10717
|
+
const ownCache = !params.parseCache;
|
|
10697
10718
|
const astCache = params.parseCache ?? /* @__PURE__ */ new Map();
|
|
10698
10719
|
const touchedFiles = [];
|
|
10699
|
-
let mod;
|
|
10700
|
-
try {
|
|
10701
|
-
mod = await loadHookModule({
|
|
10702
|
-
aspectDir,
|
|
10703
|
-
projectRoot,
|
|
10704
|
-
filename: "companion.mjs",
|
|
10705
|
-
resolveFailedCode: "COMPANION_LOADER_RESOLVE_FAILED"
|
|
10706
|
-
});
|
|
10707
|
-
} catch (err) {
|
|
10708
|
-
if (err instanceof StructureRunnerError) {
|
|
10709
|
-
return { kind: "infra", messageData: err.messageData };
|
|
10710
|
-
}
|
|
10711
|
-
return companionInfra(
|
|
10712
|
-
`Failed to load companion.mjs for aspect '${aspectId}': ${err.message}`,
|
|
10713
|
-
`The runner dynamically imports the aspect's companion.mjs before resolving companions.`,
|
|
10714
|
-
`Ensure companion.mjs exists at the aspect directory and has no unresolved imports.`
|
|
10715
|
-
);
|
|
10716
|
-
}
|
|
10717
|
-
const fn = mod.companion;
|
|
10718
|
-
if (typeof fn !== "function") {
|
|
10719
|
-
return companionInfra(
|
|
10720
|
-
`companion.mjs does not export a function named 'companion' (aspect '${aspectId}'; got ${typeof fn}).`,
|
|
10721
|
-
`The runner imports the named export 'companion' and calls companion(ctx).`,
|
|
10722
|
-
`Add 'export function companion(ctx) { ... }' to companion.mjs (it may be async).`
|
|
10723
|
-
);
|
|
10724
|
-
}
|
|
10725
|
-
let built;
|
|
10726
10720
|
try {
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
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
|
+
);
|
|
10731
10738
|
}
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
const { ctx, recorder } = built;
|
|
10735
|
-
let out;
|
|
10736
|
-
try {
|
|
10737
|
-
out = await fn(ctx);
|
|
10738
|
-
} catch (err) {
|
|
10739
|
-
if (err instanceof UndeclaredFsReadError || err instanceof UndeclaredGraphReadError || err instanceof ParseAstNotPrewarmedError) {
|
|
10739
|
+
const fn = mod.companion;
|
|
10740
|
+
if (typeof fn !== "function") {
|
|
10740
10741
|
return companionInfra(
|
|
10741
|
-
`companion.mjs
|
|
10742
|
-
`
|
|
10743
|
-
`
|
|
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).`
|
|
10744
10745
|
);
|
|
10745
10746
|
}
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
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
|
+
}
|
|
10762
10768
|
return companionInfra(
|
|
10763
|
-
`companion
|
|
10764
|
-
|
|
10765
|
-
`
|
|
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`
|
|
10766
10772
|
);
|
|
10767
10773
|
}
|
|
10768
|
-
|
|
10769
|
-
if (dd.label !== void 0 && typeof dd.label !== "string") {
|
|
10774
|
+
if (!Array.isArray(out)) {
|
|
10770
10775
|
return companionInfra(
|
|
10771
|
-
`companion.mjs returned an
|
|
10772
|
-
`
|
|
10773
|
-
`
|
|
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.`
|
|
10774
10779
|
);
|
|
10775
10780
|
}
|
|
10776
|
-
descriptors
|
|
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);
|
|
10777
10809
|
}
|
|
10778
|
-
return {
|
|
10779
|
-
kind: "ok",
|
|
10780
|
-
descriptors,
|
|
10781
|
-
touchedFiles,
|
|
10782
|
-
observations: recorder.snapshot(),
|
|
10783
|
-
observationsTainted: recorder.tainted
|
|
10784
|
-
};
|
|
10785
10810
|
}
|
|
10786
10811
|
|
|
10787
10812
|
// src/structure/runner.ts
|
|
10788
10813
|
async function runStructureAspect(params) {
|
|
10789
10814
|
const { aspectDir, aspectId, nodePath, graph, projectRoot, subjectScope } = params;
|
|
10815
|
+
const ownCache = !params.parseCache;
|
|
10790
10816
|
const astCache = params.parseCache ?? /* @__PURE__ */ new Map();
|
|
10791
10817
|
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
10818
|
try {
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10819
|
+
let rangesFor2 = function(filePath) {
|
|
10820
|
+
const existing = rangesByFile.get(filePath);
|
|
10821
|
+
if (existing !== void 0) return existing;
|
|
10822
|
+
const cached = astCache.get(filePath);
|
|
10823
|
+
let ranges;
|
|
10824
|
+
if (cached) {
|
|
10825
|
+
ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
|
|
10826
|
+
} else {
|
|
10827
|
+
const content20 = contentByPath.get(filePath);
|
|
10828
|
+
ranges = content20 !== void 0 ? collectSuppressions(void 0, filePath, content20.split("\n").length, content20) : null;
|
|
10829
|
+
}
|
|
10830
|
+
rangesByFile.set(filePath, ranges);
|
|
10831
|
+
return ranges;
|
|
10832
|
+
};
|
|
10833
|
+
var rangesFor = rangesFor2;
|
|
10834
|
+
const mod = await loadHookModule({ aspectDir, projectRoot, filename: "check.mjs" });
|
|
10835
|
+
const exportCheck = validateCheckModuleExport(mod, {
|
|
10836
|
+
codePrefix: "STRUCTURE",
|
|
10837
|
+
runnerLabel: `aspect '${aspectId}'`
|
|
10838
|
+
});
|
|
10839
|
+
if (!exportCheck.ok) {
|
|
10840
|
+
throw new StructureRunnerError(exportCheck.code, exportCheck.message);
|
|
10826
10841
|
}
|
|
10827
|
-
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10842
|
+
const checkFn = mod.check;
|
|
10843
|
+
const { ctx, recorder, ownFiles, astInputSet } = await buildUnitCtx({
|
|
10844
|
+
aspectId,
|
|
10845
|
+
nodePath,
|
|
10846
|
+
graph,
|
|
10847
|
+
projectRoot,
|
|
10848
|
+
astCache,
|
|
10849
|
+
touchedFiles,
|
|
10850
|
+
subjectScope
|
|
10851
|
+
});
|
|
10852
|
+
let raw;
|
|
10853
|
+
try {
|
|
10854
|
+
raw = checkFn(ctx);
|
|
10855
|
+
} catch (err) {
|
|
10856
|
+
if (err instanceof UndeclaredFsReadError) {
|
|
10857
|
+
return {
|
|
10858
|
+
violations: [{
|
|
10859
|
+
message: `Aspect tried to read undeclared path '${err.path}'. Add a relation in yg-node.yaml to the node owning this path.`,
|
|
10860
|
+
kind: "structure-aspect-undeclared-fs-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 UndeclaredGraphReadError) {
|
|
10870
|
+
return {
|
|
10871
|
+
violations: [{
|
|
10872
|
+
message: `Aspect tried to read undeclared graph node '${err.nodePath}'. Add a relation in yg-node.yaml.`,
|
|
10873
|
+
kind: "structure-aspect-undeclared-graph-read",
|
|
10874
|
+
file: `.yggdrasil/aspects/${aspectId}/check.mjs`
|
|
10875
|
+
}],
|
|
10876
|
+
touchedFiles: [],
|
|
10877
|
+
succeeded: false,
|
|
10878
|
+
observations: recorder.snapshot(),
|
|
10879
|
+
observationsTainted: recorder.tainted
|
|
10880
|
+
};
|
|
10881
|
+
}
|
|
10882
|
+
if (err instanceof ParseAstNotPrewarmedError) {
|
|
10883
|
+
return {
|
|
10884
|
+
violations: [{
|
|
10885
|
+
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.`,
|
|
10886
|
+
kind: "structure-aspect-parseast-not-prewarmed",
|
|
10887
|
+
file: `.yggdrasil/model/${nodePath}/yg-node.yaml`
|
|
10888
|
+
}],
|
|
10889
|
+
touchedFiles: [],
|
|
10890
|
+
succeeded: false,
|
|
10891
|
+
observations: recorder.snapshot(),
|
|
10892
|
+
observationsTainted: recorder.tainted
|
|
10893
|
+
};
|
|
10894
|
+
}
|
|
10895
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_THROWN", {
|
|
10896
|
+
what: `check.mjs threw an exception while running (aspect '${aspectId}').`,
|
|
10897
|
+
why: `${err.message}
|
|
10898
|
+
${err.stack ?? ""}`,
|
|
10899
|
+
next: `Fix the bug in check.mjs, then re-run: yg check --approve`
|
|
10900
|
+
});
|
|
10839
10901
|
}
|
|
10840
|
-
if (
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
}],
|
|
10847
|
-
touchedFiles: [],
|
|
10848
|
-
succeeded: false,
|
|
10849
|
-
observations: recorder.snapshot(),
|
|
10850
|
-
observationsTainted: recorder.tainted
|
|
10851
|
-
};
|
|
10902
|
+
if (raw !== null && typeof raw === "object" && typeof raw.then === "function") {
|
|
10903
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_ASYNC", {
|
|
10904
|
+
what: `check.mjs returned a Promise; only synchronous returns are supported.`,
|
|
10905
|
+
why: `The runner does not await check's return value.`,
|
|
10906
|
+
next: `Refactor check to be synchronous.`
|
|
10907
|
+
});
|
|
10852
10908
|
}
|
|
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") {
|
|
10909
|
+
if (!Array.isArray(raw)) {
|
|
10879
10910
|
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
10880
|
-
what: `
|
|
10881
|
-
why: `The runner
|
|
10882
|
-
next: `Return
|
|
10911
|
+
what: `check.mjs returned ${typeof raw}, expected Violation[].`,
|
|
10912
|
+
why: `The runner reports violations from the array returned by check.`,
|
|
10913
|
+
next: `Return [] or Violation[] from check.`
|
|
10883
10914
|
});
|
|
10884
10915
|
}
|
|
10885
|
-
const
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10916
|
+
const contextFiles = new Set(ownFiles.map((f) => f.path));
|
|
10917
|
+
for (const t of touchedFiles) contextFiles.add(t);
|
|
10918
|
+
const violations = [];
|
|
10919
|
+
for (const v of raw) {
|
|
10920
|
+
if (typeof v !== "object" || v === null || typeof v.message !== "string") {
|
|
10921
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
10922
|
+
what: `Violation entry must be an object with a string 'message' field.`,
|
|
10923
|
+
why: `The runner renders each violation from its message and optional file/line.`,
|
|
10924
|
+
next: `Return objects shaped { message: string, file?: string, line?: number } from check.`
|
|
10925
|
+
});
|
|
10926
|
+
}
|
|
10927
|
+
const vv = v;
|
|
10928
|
+
if (typeof vv.file === "string" && !contextFiles.has(normalizeMappingPath(vv.file))) {
|
|
10929
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_FILE_NOT_IN_CONTEXT", {
|
|
10930
|
+
what: `Violation references file '${vv.file}' not in ctx (own mapping or touched via ctx.fs/ctx.graph).`,
|
|
10931
|
+
why: `Author cannot synthesize violations against files they were not given.`,
|
|
10932
|
+
next: `Return only violations for files in ctx, or declare a relation to the node owning '${vv.file}'.`
|
|
10933
|
+
});
|
|
10934
|
+
}
|
|
10935
|
+
violations.push(vv);
|
|
10892
10936
|
}
|
|
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;
|
|
10937
|
+
const contentByPath = /* @__PURE__ */ new Map();
|
|
10938
|
+
for (const f of [...ownFiles, ...astInputSet]) {
|
|
10939
|
+
contentByPath.set(normalizeMappingPath(f.path), f.content);
|
|
10910
10940
|
}
|
|
10911
|
-
rangesByFile
|
|
10912
|
-
|
|
10941
|
+
const rangesByFile = /* @__PURE__ */ new Map();
|
|
10942
|
+
const visible = violations.filter((v) => {
|
|
10943
|
+
if (typeof v.file !== "string" || typeof v.line !== "number") return true;
|
|
10944
|
+
const ranges = rangesFor2(normalizeMappingPath(v.file));
|
|
10945
|
+
if (!ranges) return true;
|
|
10946
|
+
return !isLineSuppressed(ranges, aspectId, v.line);
|
|
10947
|
+
});
|
|
10948
|
+
return {
|
|
10949
|
+
violations: visible,
|
|
10950
|
+
touchedFiles,
|
|
10951
|
+
succeeded: true,
|
|
10952
|
+
observations: recorder.snapshot(),
|
|
10953
|
+
observationsTainted: recorder.tainted
|
|
10954
|
+
};
|
|
10955
|
+
} finally {
|
|
10956
|
+
if (ownCache) destroyParseCache(astCache);
|
|
10913
10957
|
}
|
|
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
10958
|
}
|
|
10928
10959
|
|
|
10929
10960
|
// src/structure/suppress-ranges.ts
|
|
@@ -10933,12 +10964,12 @@ async function resolveSuppressedRangesForPrompt(subjects, aspectId) {
|
|
|
10933
10964
|
for (const subject of subjects) {
|
|
10934
10965
|
const content20 = subject.bytes.toString("utf8");
|
|
10935
10966
|
const hasGrammar = getLanguageForExtension(extname5(subject.path).toLowerCase()) !== null;
|
|
10936
|
-
let tree;
|
|
10937
|
-
if (hasGrammar) {
|
|
10938
|
-
tree = await parseFile(subject.path, content20);
|
|
10939
|
-
}
|
|
10940
10967
|
const totalLines = content20.split("\n").length;
|
|
10941
|
-
const all =
|
|
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);
|
|
10942
10973
|
const ranges = formatSuppressedRangesForAspect(all, aspectId);
|
|
10943
10974
|
if (ranges.length > 0) {
|
|
10944
10975
|
byFile.push({ path: subject.path, ranges });
|
|
@@ -12125,22 +12156,14 @@ async function runRelationPass(graph, projectRoot, deps) {
|
|
|
12125
12156
|
}
|
|
12126
12157
|
}
|
|
12127
12158
|
const ownerIndex = buildOwnerIndex(graph.nodes);
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
if (parseCache.has(record.path)) return parseCache.get(record.path) ?? null;
|
|
12131
|
-
if (!record.language) {
|
|
12132
|
-
parseCache.set(record.path, null);
|
|
12133
|
-
return null;
|
|
12134
|
-
}
|
|
12135
|
-
let parsed;
|
|
12159
|
+
async function parseSingle(record) {
|
|
12160
|
+
if (!record.language) return null;
|
|
12136
12161
|
try {
|
|
12137
12162
|
const tree = await parseFile(record.path, record.content);
|
|
12138
|
-
|
|
12163
|
+
return { path: record.path, content: record.content, tree, language: record.language };
|
|
12139
12164
|
} catch {
|
|
12140
|
-
|
|
12165
|
+
return null;
|
|
12141
12166
|
}
|
|
12142
|
-
parseCache.set(record.path, parsed);
|
|
12143
|
-
return parsed;
|
|
12144
12167
|
}
|
|
12145
12168
|
const symbolTable = new SymbolTable();
|
|
12146
12169
|
const recordsByLanguage = /* @__PURE__ */ new Map();
|
|
@@ -12164,11 +12187,15 @@ async function runRelationPass(graph, projectRoot, deps) {
|
|
|
12164
12187
|
}
|
|
12165
12188
|
const symbols = [];
|
|
12166
12189
|
for (const record of records) {
|
|
12167
|
-
const parsed = await
|
|
12190
|
+
const parsed = await parseSingle(record);
|
|
12168
12191
|
if (!parsed) continue;
|
|
12169
|
-
|
|
12170
|
-
|
|
12171
|
-
|
|
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();
|
|
12172
12199
|
}
|
|
12173
12200
|
}
|
|
12174
12201
|
const toPersist = { builtFrom, symbols };
|
|
@@ -12178,10 +12205,14 @@ async function runRelationPass(graph, projectRoot, deps) {
|
|
|
12178
12205
|
const projectGlobalUsings = /* @__PURE__ */ new Set();
|
|
12179
12206
|
const projectGlobalUsingAliases = /* @__PURE__ */ new Map();
|
|
12180
12207
|
for (const record of csharpRecords) {
|
|
12181
|
-
const parsed = await
|
|
12208
|
+
const parsed = await parseSingle(record);
|
|
12182
12209
|
if (!parsed) continue;
|
|
12183
|
-
|
|
12184
|
-
|
|
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
|
+
}
|
|
12185
12216
|
}
|
|
12186
12217
|
const csharpGlobalUsings = [...projectGlobalUsings];
|
|
12187
12218
|
const csharpGlobalUsingAliases = [...projectGlobalUsingAliases.entries()];
|
|
@@ -12216,17 +12247,21 @@ async function runRelationPass(graph, projectRoot, deps) {
|
|
|
12216
12247
|
if (!record.language) continue;
|
|
12217
12248
|
const extractor = deps.extractorFor(record.language);
|
|
12218
12249
|
if (!extractor) continue;
|
|
12219
|
-
const parsed = await
|
|
12250
|
+
const parsed = await parseSingle(record);
|
|
12220
12251
|
if (!parsed) continue;
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
const
|
|
12227
|
-
|
|
12228
|
-
|
|
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
|
+
}
|
|
12229
12262
|
}
|
|
12263
|
+
} finally {
|
|
12264
|
+
parsed.tree.delete();
|
|
12230
12265
|
}
|
|
12231
12266
|
}
|
|
12232
12267
|
const violations = verifyNodeDeps(nodeId, resolvedDeps, graphView);
|
|
@@ -15796,21 +15831,7 @@ async function runFill(graph, opts) {
|
|
|
15796
15831
|
}
|
|
15797
15832
|
|
|
15798
15833
|
// src/cli/check.ts
|
|
15799
|
-
import { execFileSync } from "child_process";
|
|
15800
15834
|
import path48 from "path";
|
|
15801
|
-
function collectGitFiles(projectRoot) {
|
|
15802
|
-
try {
|
|
15803
|
-
const output = execFileSync("git", ["ls-files", "."], {
|
|
15804
|
-
cwd: projectRoot,
|
|
15805
|
-
encoding: "utf-8",
|
|
15806
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
15807
|
-
});
|
|
15808
|
-
return output.trim().split("\n").filter((f) => f.length > 0);
|
|
15809
|
-
} catch (e) {
|
|
15810
|
-
debugWrite(`[check] git ls-files failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
15811
|
-
return null;
|
|
15812
|
-
}
|
|
15813
|
-
}
|
|
15814
15835
|
function registerCheckCommand(program2) {
|
|
15815
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) => {
|
|
15816
15837
|
try {
|
|
@@ -15818,7 +15839,7 @@ function registerCheckCommand(program2) {
|
|
|
15818
15839
|
const graph = await loadGraphOrAbort(cwd, { tolerateInvalidConfig: true });
|
|
15819
15840
|
initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
|
|
15820
15841
|
const projectRoot = path48.dirname(graph.rootPath);
|
|
15821
|
-
const gitFiles =
|
|
15842
|
+
const gitFiles = await walkRepoFiles(projectRoot);
|
|
15822
15843
|
const wantsTop = opts.top !== void 0;
|
|
15823
15844
|
if (wantsTop && opts.summary) {
|
|
15824
15845
|
process.stderr.write(chalk8.red(buildIssueMessage({
|
|
@@ -16212,86 +16233,96 @@ async function runAstAspect(params) {
|
|
|
16212
16233
|
throw new AstRunnerError(exportCheck.code, exportCheck.message);
|
|
16213
16234
|
}
|
|
16214
16235
|
const checkFn = mod.check;
|
|
16236
|
+
const localTrees = [];
|
|
16215
16237
|
const sourceFiles = [];
|
|
16216
|
-
|
|
16217
|
-
const
|
|
16218
|
-
|
|
16219
|
-
|
|
16220
|
-
|
|
16238
|
+
try {
|
|
16239
|
+
for (const f of params.files) {
|
|
16240
|
+
const cached = params.parseCache?.get(f.path);
|
|
16241
|
+
if (cached !== void 0) {
|
|
16242
|
+
sourceFiles.push({ path: f.path, content: cached.content, ast: cached.ast });
|
|
16243
|
+
continue;
|
|
16244
|
+
}
|
|
16245
|
+
const content20 = await readFile16(path49.resolve(params.projectRoot, f.path), "utf-8");
|
|
16246
|
+
if (getLanguageForExtension(path49.extname(f.path).toLowerCase()) === null) {
|
|
16247
|
+
sourceFiles.push({ path: f.path, content: content20, ast: void 0 });
|
|
16248
|
+
continue;
|
|
16249
|
+
}
|
|
16250
|
+
let ast;
|
|
16251
|
+
try {
|
|
16252
|
+
ast = await parseFile(f.path, content20);
|
|
16253
|
+
} catch (e) {
|
|
16254
|
+
const msg = e.message ?? String(e);
|
|
16255
|
+
throw new AstRunnerError("AST_GRAMMAR_LOAD_FAILED", {
|
|
16256
|
+
what: `Failed to load tree-sitter grammar for ${f.path}: ${msg}`,
|
|
16257
|
+
why: `The bundled WASM grammar could not be loaded.`,
|
|
16258
|
+
next: `Reinstall the CLI.`
|
|
16259
|
+
});
|
|
16260
|
+
}
|
|
16261
|
+
if (!params.parseCache) {
|
|
16262
|
+
localTrees.push(ast);
|
|
16263
|
+
}
|
|
16264
|
+
if (ast.rootNode.hasError) {
|
|
16265
|
+
const err = findFirstErrorNode(ast.rootNode);
|
|
16266
|
+
throw new AstRunnerError("AST_SOURCE_PARSE_ERROR", {
|
|
16267
|
+
what: `Source file ${f.path} has a syntax error at line ${(err?.startPosition.row ?? 0) + 1}.`,
|
|
16268
|
+
why: `Tree-sitter could not parse the file cleanly.`,
|
|
16269
|
+
next: `Fix the syntax error in ${f.path}.`
|
|
16270
|
+
});
|
|
16271
|
+
}
|
|
16272
|
+
if (params.parseCache) {
|
|
16273
|
+
params.parseCache.set(f.path, { content: content20, 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;
|
|
@@ -21792,7 +21823,6 @@ function registerSchemasCommand(program2) {
|
|
|
21792
21823
|
// src/cli/suppressions.ts
|
|
21793
21824
|
import chalk14 from "chalk";
|
|
21794
21825
|
import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
|
|
21795
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
21796
21826
|
import path57 from "path";
|
|
21797
21827
|
function isBinaryContent(buf) {
|
|
21798
21828
|
const checkLen = Math.min(buf.length, 8192);
|
|
@@ -21821,8 +21851,11 @@ async function scanMarkersForFile(relFile, text2) {
|
|
|
21821
21851
|
return scanSuppressionMarkers(text2);
|
|
21822
21852
|
}
|
|
21823
21853
|
try {
|
|
21824
|
-
|
|
21825
|
-
|
|
21854
|
+
return await withParsedFile(
|
|
21855
|
+
relFile,
|
|
21856
|
+
text2,
|
|
21857
|
+
(tree) => scanSuppressionMarkersInComments(tree, relFile)
|
|
21858
|
+
);
|
|
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);
|
|
@@ -21941,17 +21974,7 @@ function registerSuppressionsCommand(program2) {
|
|
|
21941
21974
|
const graph = await loadGraphOrAbort(cwd);
|
|
21942
21975
|
initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
|
|
21943
21976
|
const projectRoot = path57.dirname(graph.rootPath);
|
|
21944
|
-
|
|
21945
|
-
try {
|
|
21946
|
-
const output = execFileSync2("git", ["ls-files", "."], {
|
|
21947
|
-
cwd: projectRoot,
|
|
21948
|
-
encoding: "utf-8",
|
|
21949
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
21950
|
-
});
|
|
21951
|
-
gitFiles = output.trim().split("\n").filter((f) => f.length > 0);
|
|
21952
|
-
} catch (error) {
|
|
21953
|
-
debugWrite(`[suppressions] git ls-files fallback: ${error instanceof Error ? error.message : String(error)}`);
|
|
21954
|
-
}
|
|
21977
|
+
const gitFiles = await walkRepoFiles(projectRoot);
|
|
21955
21978
|
const knownAspectIds = new Set(graph.aspects.map((a) => a.id));
|
|
21956
21979
|
const report2 = await runSuppressionsScan(projectRoot, gitFiles, knownAspectIds);
|
|
21957
21980
|
process.stdout.write(formatSuppressionsOutput(report2));
|
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) {
|
|
@@ -602,6 +606,14 @@ async function parseFile(filePath, content) {
|
|
|
602
606
|
}
|
|
603
607
|
return tree;
|
|
604
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
|
+
}
|
|
605
617
|
|
|
606
618
|
// src/structure/ctx-parsers.ts
|
|
607
619
|
var ParseAstNotPrewarmedError = class extends Error {
|
|
@@ -876,6 +888,14 @@ function validateCheckModuleExport(mod, opts) {
|
|
|
876
888
|
return { ok: true };
|
|
877
889
|
}
|
|
878
890
|
|
|
891
|
+
// src/ast/parse-cache.ts
|
|
892
|
+
function destroyParseCache(cache) {
|
|
893
|
+
for (const { ast } of cache.values()) {
|
|
894
|
+
ast.delete();
|
|
895
|
+
}
|
|
896
|
+
cache.clear();
|
|
897
|
+
}
|
|
898
|
+
|
|
879
899
|
// src/structure/hook-loader.ts
|
|
880
900
|
import * as fs4 from "fs";
|
|
881
901
|
import * as path8 from "path";
|
|
@@ -1435,143 +1455,149 @@ async function buildUnitCtx(params) {
|
|
|
1435
1455
|
// src/structure/runner.ts
|
|
1436
1456
|
async function runStructureAspect(params) {
|
|
1437
1457
|
const { aspectDir, aspectId, nodePath, graph, projectRoot, subjectScope } = params;
|
|
1458
|
+
const ownCache = !params.parseCache;
|
|
1438
1459
|
const astCache = params.parseCache ?? /* @__PURE__ */ new Map();
|
|
1439
1460
|
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
1461
|
try {
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1462
|
+
let rangesFor2 = function(filePath) {
|
|
1463
|
+
const existing = rangesByFile.get(filePath);
|
|
1464
|
+
if (existing !== void 0) return existing;
|
|
1465
|
+
const cached = astCache.get(filePath);
|
|
1466
|
+
let ranges;
|
|
1467
|
+
if (cached) {
|
|
1468
|
+
ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
|
|
1469
|
+
} else {
|
|
1470
|
+
const content = contentByPath.get(filePath);
|
|
1471
|
+
ranges = content !== void 0 ? collectSuppressions(void 0, filePath, content.split("\n").length, content) : null;
|
|
1472
|
+
}
|
|
1473
|
+
rangesByFile.set(filePath, ranges);
|
|
1474
|
+
return ranges;
|
|
1475
|
+
};
|
|
1476
|
+
var rangesFor = rangesFor2;
|
|
1477
|
+
const mod = await loadHookModule({ aspectDir, projectRoot, filename: "check.mjs" });
|
|
1478
|
+
const exportCheck = validateCheckModuleExport(mod, {
|
|
1479
|
+
codePrefix: "STRUCTURE",
|
|
1480
|
+
runnerLabel: `aspect '${aspectId}'`
|
|
1481
|
+
});
|
|
1482
|
+
if (!exportCheck.ok) {
|
|
1483
|
+
throw new StructureRunnerError(exportCheck.code, exportCheck.message);
|
|
1474
1484
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1485
|
+
const checkFn = mod.check;
|
|
1486
|
+
const { ctx, recorder, ownFiles, astInputSet } = await buildUnitCtx({
|
|
1487
|
+
aspectId,
|
|
1488
|
+
nodePath,
|
|
1489
|
+
graph,
|
|
1490
|
+
projectRoot,
|
|
1491
|
+
astCache,
|
|
1492
|
+
touchedFiles,
|
|
1493
|
+
subjectScope
|
|
1494
|
+
});
|
|
1495
|
+
let raw;
|
|
1496
|
+
try {
|
|
1497
|
+
raw = checkFn(ctx);
|
|
1498
|
+
} catch (err) {
|
|
1499
|
+
if (err instanceof UndeclaredFsReadError) {
|
|
1500
|
+
return {
|
|
1501
|
+
violations: [{
|
|
1502
|
+
message: `Aspect tried to read undeclared path '${err.path}'. Add a relation in yg-node.yaml to the node owning this path.`,
|
|
1503
|
+
kind: "structure-aspect-undeclared-fs-read",
|
|
1504
|
+
file: `.yggdrasil/aspects/${aspectId}/check.mjs`
|
|
1505
|
+
}],
|
|
1506
|
+
touchedFiles: [],
|
|
1507
|
+
succeeded: false,
|
|
1508
|
+
observations: recorder.snapshot(),
|
|
1509
|
+
observationsTainted: recorder.tainted
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
if (err instanceof UndeclaredGraphReadError) {
|
|
1513
|
+
return {
|
|
1514
|
+
violations: [{
|
|
1515
|
+
message: `Aspect tried to read undeclared graph node '${err.nodePath}'. Add a relation in yg-node.yaml.`,
|
|
1516
|
+
kind: "structure-aspect-undeclared-graph-read",
|
|
1517
|
+
file: `.yggdrasil/aspects/${aspectId}/check.mjs`
|
|
1518
|
+
}],
|
|
1519
|
+
touchedFiles: [],
|
|
1520
|
+
succeeded: false,
|
|
1521
|
+
observations: recorder.snapshot(),
|
|
1522
|
+
observationsTainted: recorder.tainted
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
if (err instanceof ParseAstNotPrewarmedError) {
|
|
1526
|
+
return {
|
|
1527
|
+
violations: [{
|
|
1528
|
+
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.`,
|
|
1529
|
+
kind: "structure-aspect-parseast-not-prewarmed",
|
|
1530
|
+
file: `.yggdrasil/model/${nodePath}/yg-node.yaml`
|
|
1531
|
+
}],
|
|
1532
|
+
touchedFiles: [],
|
|
1533
|
+
succeeded: false,
|
|
1534
|
+
observations: recorder.snapshot(),
|
|
1535
|
+
observationsTainted: recorder.tainted
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_THROWN", {
|
|
1539
|
+
what: `check.mjs threw an exception while running (aspect '${aspectId}').`,
|
|
1540
|
+
why: `${err.message}
|
|
1541
|
+
${err.stack ?? ""}`,
|
|
1542
|
+
next: `Fix the bug in check.mjs, then re-run: yg check --approve`
|
|
1543
|
+
});
|
|
1487
1544
|
}
|
|
1488
|
-
if (
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
}],
|
|
1495
|
-
touchedFiles: [],
|
|
1496
|
-
succeeded: false,
|
|
1497
|
-
observations: recorder.snapshot(),
|
|
1498
|
-
observationsTainted: recorder.tainted
|
|
1499
|
-
};
|
|
1545
|
+
if (raw !== null && typeof raw === "object" && typeof raw.then === "function") {
|
|
1546
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_ASYNC", {
|
|
1547
|
+
what: `check.mjs returned a Promise; only synchronous returns are supported.`,
|
|
1548
|
+
why: `The runner does not await check's return value.`,
|
|
1549
|
+
next: `Refactor check to be synchronous.`
|
|
1550
|
+
});
|
|
1500
1551
|
}
|
|
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") {
|
|
1552
|
+
if (!Array.isArray(raw)) {
|
|
1527
1553
|
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
1528
|
-
what: `
|
|
1529
|
-
why: `The runner
|
|
1530
|
-
next: `Return
|
|
1554
|
+
what: `check.mjs returned ${typeof raw}, expected Violation[].`,
|
|
1555
|
+
why: `The runner reports violations from the array returned by check.`,
|
|
1556
|
+
next: `Return [] or Violation[] from check.`
|
|
1531
1557
|
});
|
|
1532
1558
|
}
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1559
|
+
const contextFiles = new Set(ownFiles.map((f) => f.path));
|
|
1560
|
+
for (const t of touchedFiles) contextFiles.add(t);
|
|
1561
|
+
const violations = [];
|
|
1562
|
+
for (const v of raw) {
|
|
1563
|
+
if (typeof v !== "object" || v === null || typeof v.message !== "string") {
|
|
1564
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
1565
|
+
what: `Violation entry must be an object with a string 'message' field.`,
|
|
1566
|
+
why: `The runner renders each violation from its message and optional file/line.`,
|
|
1567
|
+
next: `Return objects shaped { message: string, file?: string, line?: number } from check.`
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
const vv = v;
|
|
1571
|
+
if (typeof vv.file === "string" && !contextFiles.has(normalizeMappingPath(vv.file))) {
|
|
1572
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_FILE_NOT_IN_CONTEXT", {
|
|
1573
|
+
what: `Violation references file '${vv.file}' not in ctx (own mapping or touched via ctx.fs/ctx.graph).`,
|
|
1574
|
+
why: `Author cannot synthesize violations against files they were not given.`,
|
|
1575
|
+
next: `Return only violations for files in ctx, or declare a relation to the node owning '${vv.file}'.`
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
violations.push(vv);
|
|
1540
1579
|
}
|
|
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;
|
|
1580
|
+
const contentByPath = /* @__PURE__ */ new Map();
|
|
1581
|
+
for (const f of [...ownFiles, ...astInputSet]) {
|
|
1582
|
+
contentByPath.set(normalizeMappingPath(f.path), f.content);
|
|
1558
1583
|
}
|
|
1559
|
-
rangesByFile
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
return
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1584
|
+
const rangesByFile = /* @__PURE__ */ new Map();
|
|
1585
|
+
const visible = violations.filter((v) => {
|
|
1586
|
+
if (typeof v.file !== "string" || typeof v.line !== "number") return true;
|
|
1587
|
+
const ranges = rangesFor2(normalizeMappingPath(v.file));
|
|
1588
|
+
if (!ranges) return true;
|
|
1589
|
+
return !isLineSuppressed(ranges, aspectId, v.line);
|
|
1590
|
+
});
|
|
1591
|
+
return {
|
|
1592
|
+
violations: visible,
|
|
1593
|
+
touchedFiles,
|
|
1594
|
+
succeeded: true,
|
|
1595
|
+
observations: recorder.snapshot(),
|
|
1596
|
+
observationsTainted: recorder.tainted
|
|
1597
|
+
};
|
|
1598
|
+
} finally {
|
|
1599
|
+
if (ownCache) destroyParseCache(astCache);
|
|
1600
|
+
}
|
|
1575
1601
|
}
|
|
1576
1602
|
|
|
1577
1603
|
// src/structure/suppress-ranges.ts
|
|
@@ -1581,12 +1607,12 @@ async function resolveSuppressedRangesForPrompt(subjects, aspectId) {
|
|
|
1581
1607
|
for (const subject of subjects) {
|
|
1582
1608
|
const content = subject.bytes.toString("utf8");
|
|
1583
1609
|
const hasGrammar = getLanguageForExtension(extname5(subject.path).toLowerCase()) !== null;
|
|
1584
|
-
let tree;
|
|
1585
|
-
if (hasGrammar) {
|
|
1586
|
-
tree = await parseFile(subject.path, content);
|
|
1587
|
-
}
|
|
1588
1610
|
const totalLines = content.split("\n").length;
|
|
1589
|
-
const all =
|
|
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);
|
|
1590
1616
|
const ranges = formatSuppressedRangesForAspect(all, aspectId);
|
|
1591
1617
|
if (ranges.length > 0) {
|
|
1592
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.
|
|
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": {
|