@chrisdudek/yg 5.4.2 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +3007 -1766
- package/dist/det-worker.d.ts +2 -0
- package/dist/det-worker.js +1715 -0
- package/dist/structure.d.ts +8 -0
- package/dist/structure.js +126 -41
- package/dist/templates/portal/js/export.js +1 -1
- package/dist/templates/portal/js/views/coverage-view.js +11 -3
- package/package.json +3 -3
package/dist/structure.d.ts
CHANGED
|
@@ -491,6 +491,14 @@ declare class SuppressMarkerError extends Error {
|
|
|
491
491
|
file: string;
|
|
492
492
|
line: number;
|
|
493
493
|
readonly code = "SUPPRESS_MARKER_MISSING_REASON";
|
|
494
|
+
/**
|
|
495
|
+
* Self-describing what/why/next for the malformed marker. A reasonless marker
|
|
496
|
+
* is a fault in the SOURCE file's marker — NOT in the aspect's check.mjs — so
|
|
497
|
+
* the deterministic runners re-surface this as its own diagnostic instead of an
|
|
498
|
+
* `aspect-check-runtime-error`. (The LLM-sizing paths build their own message
|
|
499
|
+
* from `file`/`line`; only the deterministic consumers read `messageData`.)
|
|
500
|
+
*/
|
|
501
|
+
readonly messageData: IssueMessage;
|
|
494
502
|
constructor(message: string, file: string, line: number);
|
|
495
503
|
}
|
|
496
504
|
|
package/dist/structure.js
CHANGED
|
@@ -164,6 +164,26 @@ var UndeclaredGraphReadError = class extends Error {
|
|
|
164
164
|
}
|
|
165
165
|
nodePath;
|
|
166
166
|
};
|
|
167
|
+
function makeGraphFile(repoRelPosixPath, content, bytes, recorder, subjectFiles) {
|
|
168
|
+
if (!recorder || subjectFiles?.has(repoRelPosixPath)) {
|
|
169
|
+
return { path: repoRelPosixPath, content };
|
|
170
|
+
}
|
|
171
|
+
const rec = recorder;
|
|
172
|
+
let recorded = false;
|
|
173
|
+
const file = { path: repoRelPosixPath };
|
|
174
|
+
Object.defineProperty(file, "content", {
|
|
175
|
+
enumerable: true,
|
|
176
|
+
configurable: true,
|
|
177
|
+
get() {
|
|
178
|
+
if (!recorded) {
|
|
179
|
+
rec.recordRead(repoRelPosixPath, bytes);
|
|
180
|
+
recorded = true;
|
|
181
|
+
}
|
|
182
|
+
return content;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return file;
|
|
186
|
+
}
|
|
167
187
|
function computeAllowedNodePaths(currentPath, graph) {
|
|
168
188
|
const allowed = /* @__PURE__ */ new Set([currentPath]);
|
|
169
189
|
const current = graph.nodes.get(currentPath);
|
|
@@ -225,11 +245,8 @@ function createCtxGraph(params) {
|
|
|
225
245
|
if (stat2.isFile()) {
|
|
226
246
|
const bytes = fs2.readFileSync(abs);
|
|
227
247
|
const content = bytes.toString("utf8");
|
|
228
|
-
files.push({ path: p, content });
|
|
229
248
|
touchedFiles.push(p);
|
|
230
|
-
|
|
231
|
-
recorder.recordRead(p, bytes);
|
|
232
|
-
}
|
|
249
|
+
files.push(makeGraphFile(p, content, bytes, recorder, subjectFiles));
|
|
233
250
|
}
|
|
234
251
|
} catch {
|
|
235
252
|
}
|
|
@@ -734,45 +751,96 @@ var SuppressMarkerError = class extends Error {
|
|
|
734
751
|
this.file = file;
|
|
735
752
|
this.line = line;
|
|
736
753
|
this.name = "SuppressMarkerError";
|
|
754
|
+
const loc = `${toPosixPath(file)}:${line}`;
|
|
755
|
+
this.messageData = {
|
|
756
|
+
what: `Malformed yg-suppress marker at ${loc} \u2014 it is missing the required reason after the aspect id.`,
|
|
757
|
+
why: `A single-line or disable marker must carry a reason after its closing parenthesis; a reasonless marker cannot be honored. This is a fault in the source file's marker, not in the aspect's check.`,
|
|
758
|
+
next: `Add a reason to the marker at ${loc}, or remove it.`
|
|
759
|
+
};
|
|
737
760
|
}
|
|
738
761
|
file;
|
|
739
762
|
line;
|
|
740
763
|
code = "SUPPRESS_MARKER_MISSING_REASON";
|
|
764
|
+
/**
|
|
765
|
+
* Self-describing what/why/next for the malformed marker. A reasonless marker
|
|
766
|
+
* is a fault in the SOURCE file's marker — NOT in the aspect's check.mjs — so
|
|
767
|
+
* the deterministic runners re-surface this as its own diagnostic instead of an
|
|
768
|
+
* `aspect-check-runtime-error`. (The LLM-sizing paths build their own message
|
|
769
|
+
* from `file`/`line`; only the deterministic consumers read `messageData`.)
|
|
770
|
+
*/
|
|
771
|
+
messageData;
|
|
741
772
|
};
|
|
742
|
-
var
|
|
743
|
-
var
|
|
744
|
-
var
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
773
|
+
var LEADER_DELIM = String.raw`\/\/[/!]*|\/\*+|\*+|#+|--+|;+|<!--|\{-|\(\*|[!%'"]`;
|
|
774
|
+
var RE_LEADER_OPTIONAL = new RegExp(String.raw`^\s*(${LEADER_DELIM})?\s*`);
|
|
775
|
+
var RE_LEADER_REQUIRED = new RegExp(String.raw`^\s*(${LEADER_DELIM})\s*`);
|
|
776
|
+
var RE_MARKER = /^yg-suppress(-disable|-enable)?\(\s*([^)]*?)\s*\)\s*(.*)$/;
|
|
777
|
+
var RE_ASPECT_ID = /^(?:\*|[A-Za-z0-9_][A-Za-z0-9_./-]*)$/;
|
|
778
|
+
var RE_ANY_CLOSER = /\s*(?:\*+\/|-->|-\}|\*\))\s*$/;
|
|
779
|
+
var RE_CLOSER_CSTYLE = /\s*\*+\/\s*$/;
|
|
780
|
+
var RE_CLOSER_HTML = /\s*-->\s*$/;
|
|
781
|
+
var RE_CLOSER_HASKELL = /\s*-\}\s*$/;
|
|
782
|
+
var RE_CLOSER_OCAML = /\s*\*\)\s*$/;
|
|
783
|
+
function stripCloser(reason, leader) {
|
|
784
|
+
if (leader === void 0) return reason.replace(RE_ANY_CLOSER, "");
|
|
785
|
+
if (leader.startsWith("//")) return reason;
|
|
786
|
+
if (leader.startsWith("/*") || /^\*+$/.test(leader)) return reason.replace(RE_CLOSER_CSTYLE, "");
|
|
787
|
+
if (leader === "<!--") return reason.replace(RE_CLOSER_HTML, "");
|
|
788
|
+
if (leader === "{-") return reason.replace(RE_CLOSER_HASKELL, "");
|
|
789
|
+
if (leader === "(*") return reason.replace(RE_CLOSER_OCAML, "");
|
|
790
|
+
return reason;
|
|
749
791
|
}
|
|
750
792
|
function splitAspectList(raw) {
|
|
751
793
|
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
752
794
|
}
|
|
753
|
-
function
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
if (
|
|
795
|
+
function matchMarkerLine(line, requireDelimiter) {
|
|
796
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
797
|
+
const leader = (requireDelimiter ? RE_LEADER_REQUIRED : RE_LEADER_OPTIONAL).exec(line);
|
|
798
|
+
if (leader === null) return null;
|
|
799
|
+
const m = RE_MARKER.exec(line.slice(leader[0].length));
|
|
800
|
+
if (m === null) return null;
|
|
801
|
+
const aspectIds = splitAspectList(m[2]);
|
|
802
|
+
if (aspectIds.length === 0) return null;
|
|
803
|
+
if (!aspectIds.every((id) => RE_ASPECT_ID.test(id))) return null;
|
|
804
|
+
const kind = m[1] === "-disable" ? "disable" : m[1] === "-enable" ? "enable" : "single";
|
|
805
|
+
const reason = kind === "enable" ? "" : stripCloser(m[3], leader[1]).trim();
|
|
806
|
+
return { kind, aspectIds, reason };
|
|
807
|
+
}
|
|
808
|
+
function parseMarker(lineText, line, file, requireDelimiter) {
|
|
809
|
+
const m = matchMarkerLine(lineText, requireDelimiter);
|
|
810
|
+
if (m === null) return null;
|
|
811
|
+
if (m.kind !== "enable" && m.reason === "") {
|
|
757
812
|
throw new SuppressMarkerError(
|
|
758
|
-
`yg-suppress${kind === "disable" ? "-disable" : ""}(${
|
|
813
|
+
`yg-suppress${m.kind === "disable" ? "-disable" : ""}(${m.aspectIds.join(", ")}) missing reason at ${file}:${line}`,
|
|
759
814
|
file,
|
|
760
815
|
line
|
|
761
816
|
);
|
|
762
817
|
}
|
|
763
|
-
return { kind, aspectIds, reason, line };
|
|
818
|
+
return { kind: m.kind, aspectIds: m.aspectIds, reason: m.reason, line };
|
|
764
819
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
820
|
+
var RE_FENCE = /^ {0,3}((?:`{3,})|(?:~{3,}))[ \t]*(.*)$/;
|
|
821
|
+
var MARKDOWN_EXTS = /* @__PURE__ */ new Set([".md", ".markdown"]);
|
|
822
|
+
function isMarkdownExt(file) {
|
|
823
|
+
return MARKDOWN_EXTS.has(extname3(file).toLowerCase());
|
|
824
|
+
}
|
|
825
|
+
function markdownFencedLines(text) {
|
|
826
|
+
const fenced = /* @__PURE__ */ new Set();
|
|
827
|
+
const lines = text.split("\n");
|
|
828
|
+
let open = null;
|
|
829
|
+
for (let i = 0; i < lines.length; i++) {
|
|
830
|
+
const m = RE_FENCE.exec(lines[i]);
|
|
831
|
+
if (open === null) {
|
|
832
|
+
if (m) {
|
|
833
|
+
open = { char: m[1][0], len: m[1].length };
|
|
834
|
+
fenced.add(i + 1);
|
|
835
|
+
}
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
fenced.add(i + 1);
|
|
839
|
+
if (m && m[1][0] === open.char && m[1].length >= open.len && m[2].trim() === "") {
|
|
840
|
+
open = null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return fenced;
|
|
776
844
|
}
|
|
777
845
|
function collectSuppressions(tree, file, totalLines, content) {
|
|
778
846
|
const hasGrammar = getLanguageForExtension(extname3(file)) !== null;
|
|
@@ -780,13 +848,18 @@ function collectSuppressions(tree, file, totalLines, content) {
|
|
|
780
848
|
if (hasGrammar && tree) {
|
|
781
849
|
const comments = findComments({ path: file, ast: tree });
|
|
782
850
|
for (const c of comments) {
|
|
783
|
-
const
|
|
784
|
-
|
|
851
|
+
const commentLines = c.text.split("\n");
|
|
852
|
+
for (let i = 0; i < commentLines.length; i++) {
|
|
853
|
+
const m = parseMarker(commentLines[i], c.startPosition.row + i + 1, file, false);
|
|
854
|
+
if (m) markers.push(m);
|
|
855
|
+
}
|
|
785
856
|
}
|
|
786
857
|
} else if (content !== void 0) {
|
|
787
858
|
const lines = content.split("\n");
|
|
859
|
+
const fenced = isMarkdownExt(file) ? markdownFencedLines(content) : null;
|
|
788
860
|
for (let i = 0; i < lines.length; i++) {
|
|
789
|
-
|
|
861
|
+
if (fenced && fenced.has(i + 1)) continue;
|
|
862
|
+
const m = parseMarker(lines[i], i + 1, file, true);
|
|
790
863
|
if (m) markers.push(m);
|
|
791
864
|
}
|
|
792
865
|
} else {
|
|
@@ -812,22 +885,26 @@ function collectSuppressions(tree, file, totalLines, content) {
|
|
|
812
885
|
for (const id of m.aspectIds) {
|
|
813
886
|
if (id === "*") {
|
|
814
887
|
if (openWildcard !== null) {
|
|
815
|
-
|
|
888
|
+
if (m.line - 1 >= openWildcard) {
|
|
889
|
+
ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: m.line - 1, isWildcard: true });
|
|
890
|
+
}
|
|
816
891
|
openWildcard = null;
|
|
817
892
|
}
|
|
818
893
|
} else {
|
|
819
894
|
const start = openSpecific.get(id);
|
|
820
895
|
if (start !== void 0) {
|
|
821
|
-
|
|
896
|
+
if (m.line - 1 >= start) {
|
|
897
|
+
ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: m.line - 1, isWildcard: false });
|
|
898
|
+
}
|
|
822
899
|
openSpecific.delete(id);
|
|
823
900
|
}
|
|
824
901
|
}
|
|
825
902
|
}
|
|
826
903
|
}
|
|
827
904
|
}
|
|
828
|
-
if (openWildcard !== null) ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: totalLines, isWildcard: true });
|
|
905
|
+
if (openWildcard !== null && openWildcard <= totalLines) ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: totalLines, isWildcard: true });
|
|
829
906
|
for (const [id, start] of openSpecific) {
|
|
830
|
-
ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: totalLines, isWildcard: false });
|
|
907
|
+
if (start <= totalLines) ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: totalLines, isWildcard: false });
|
|
831
908
|
}
|
|
832
909
|
return ranges;
|
|
833
910
|
}
|
|
@@ -838,7 +915,7 @@ function isLineSuppressed(ranges, aspectId, line) {
|
|
|
838
915
|
});
|
|
839
916
|
}
|
|
840
917
|
function formatSuppressedRangesForAspect(ranges, aspectId) {
|
|
841
|
-
return ranges.filter((r) => r.isWildcard || r.aspectIds.has(aspectId)).map((r) => ({ startLine: r.startLine, endLine: r.endLine })).sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
|
|
918
|
+
return ranges.filter((r) => r.isWildcard || r.aspectIds.has(aspectId)).filter((r) => r.startLine <= r.endLine).map((r) => ({ startLine: r.startLine, endLine: r.endLine })).sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
|
|
842
919
|
}
|
|
843
920
|
|
|
844
921
|
// src/utils/validate-check-module.ts
|
|
@@ -1458,6 +1535,7 @@ async function buildUnitCtx(params) {
|
|
|
1458
1535
|
}
|
|
1459
1536
|
|
|
1460
1537
|
// src/structure/runner.ts
|
|
1538
|
+
var SUPPRESS_MARKER_MALFORMED_CODE = "STRUCTURE_SUPPRESS_MARKER_MALFORMED";
|
|
1461
1539
|
async function runStructureAspect(params) {
|
|
1462
1540
|
const { aspectDir, aspectId, nodePath, graph, projectRoot, subjectScope } = params;
|
|
1463
1541
|
const ownCache = !params.parseCache;
|
|
@@ -1469,11 +1547,18 @@ async function runStructureAspect(params) {
|
|
|
1469
1547
|
if (existing !== void 0) return existing;
|
|
1470
1548
|
const cached = astCache.get(filePath);
|
|
1471
1549
|
let ranges;
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1550
|
+
try {
|
|
1551
|
+
if (cached) {
|
|
1552
|
+
ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
|
|
1553
|
+
} else {
|
|
1554
|
+
const content = contentByPath.get(filePath);
|
|
1555
|
+
ranges = content !== void 0 ? collectSuppressions(void 0, filePath, content.split("\n").length, content) : null;
|
|
1556
|
+
}
|
|
1557
|
+
} catch (err) {
|
|
1558
|
+
if (err instanceof SuppressMarkerError) {
|
|
1559
|
+
throw new StructureRunnerError(SUPPRESS_MARKER_MALFORMED_CODE, err.messageData);
|
|
1560
|
+
}
|
|
1561
|
+
throw err;
|
|
1477
1562
|
}
|
|
1478
1563
|
rangesByFile.set(filePath, ranges);
|
|
1479
1564
|
return ranges;
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
var rows = [['metric', 'value']];
|
|
63
63
|
var keys = [
|
|
64
64
|
'nodes', 'aspects', 'flows', 'pairsTotal', 'pairsLLM', 'pairsDet',
|
|
65
|
-
'verified', 'refused', 'advisoryRefused', 'unverified', 'noRule', 'draft', 'notApplicable',
|
|
65
|
+
'verified', 'verifiedDet', 'verifiedLlm', 'refused', 'advisoryRefused', 'unverified', 'noRule', 'draft', 'notApplicable',
|
|
66
66
|
'suppressed', 'coveredFiles', 'uncoveredFiles', 'totalFiles', 'errors', 'warnings',
|
|
67
67
|
];
|
|
68
68
|
for (var i = 0; i < keys.length; i += 1) rows.push([keys[i], c[keys[i]]]);
|
|
@@ -75,8 +75,11 @@
|
|
|
75
75
|
|
|
76
76
|
// The bar is sized by the real pair STATES (verified / refused / unverified), never by the
|
|
77
77
|
// expected-pair kind totals — a verified segment must be exactly as wide as the verified
|
|
78
|
-
// count, so an unverified pair can never paint green. The verified label states the
|
|
79
|
-
// deterministic makeup of the expected universe
|
|
78
|
+
// count, so an unverified pair can never paint green. The verified label states BOTH the
|
|
79
|
+
// LLM-vs-deterministic makeup of the expected universe AND the real split of the verified
|
|
80
|
+
// count itself (verifiedDet / verifiedLlm — tallied off the identical pairs loop `yg check`
|
|
81
|
+
// uses for its own header), so a green bar can never hide how many pairs were machine-checked
|
|
82
|
+
// for free vs actually reviewed by an LLM.
|
|
80
83
|
// Advisory refusals are a real expected-pair state, but per the honesty model they render
|
|
81
84
|
// as a NON-BLOCKING warning, never a blocking `refused`. They get their own warning-coloured
|
|
82
85
|
// segment + key so the bar still accounts for every expected pair without ever showing an
|
|
@@ -96,7 +99,12 @@
|
|
|
96
99
|
ledger.appendChild(bar);
|
|
97
100
|
|
|
98
101
|
var labels = dom.el('div', 'cov-barlabels');
|
|
99
|
-
labels.appendChild(key(
|
|
102
|
+
labels.appendChild(key(
|
|
103
|
+
'verified',
|
|
104
|
+
c.verified,
|
|
105
|
+
'of ' + c.pairsLLM + ' LLM + ' + c.pairsDet + ' deterministic expected'
|
|
106
|
+
+ ' (' + (c.verifiedDet || 0) + ' deterministic, ' + (c.verifiedLlm || 0) + ' LLM verified)',
|
|
107
|
+
));
|
|
100
108
|
labels.appendChild(key('refused', c.refused, 'enforced — blocks (== yg check)'));
|
|
101
109
|
if (advisoryRefused > 0) labels.appendChild(key('warning', advisoryRefused, 'advisory refusal — does not block'));
|
|
102
110
|
labels.appendChild(key('unverified', c.unverified));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrisdudek/yg",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.1",
|
|
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": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"test:external": "vitest run --config vitest.external.config.ts",
|
|
26
26
|
"test:coverage": "vitest run --coverage",
|
|
27
27
|
"test:e2e:portal": "playwright test",
|
|
28
|
-
"lint": "eslint src/",
|
|
29
|
-
"lint:fix": "eslint src/ --fix",
|
|
28
|
+
"lint": "eslint src/ tests/",
|
|
29
|
+
"lint:fix": "eslint src/ tests/ --fix",
|
|
30
30
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
31
31
|
"typecheck": "tsc -p tsconfig.check.json",
|
|
32
32
|
"typecheck:e2e": "tsc -p tests/portal-e2e/tsconfig.json"
|