@ctxr/skill-llm-wiki 1.0.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/CHANGELOG.md +134 -0
- package/LICENSE +21 -0
- package/README.md +484 -0
- package/SKILL.md +252 -0
- package/guide/basics/concepts.md +74 -0
- package/guide/basics/index.md +45 -0
- package/guide/basics/schema.md +140 -0
- package/guide/cli.md +256 -0
- package/guide/correctness/index.md +45 -0
- package/guide/correctness/invariants.md +89 -0
- package/guide/correctness/safety.md +96 -0
- package/guide/history/diff.md +110 -0
- package/guide/history/hidden-git.md +130 -0
- package/guide/history/index.md +52 -0
- package/guide/history/remote-sync.md +113 -0
- package/guide/index.md +134 -0
- package/guide/isolation/coexistence.md +134 -0
- package/guide/isolation/index.md +44 -0
- package/guide/isolation/scale.md +251 -0
- package/guide/layout/in-place-mode.md +97 -0
- package/guide/layout/index.md +53 -0
- package/guide/layout/layout-contract.md +131 -0
- package/guide/layout/layout-modes.md +115 -0
- package/guide/operations/index.md +76 -0
- package/guide/operations/ingest/build.md +75 -0
- package/guide/operations/ingest/extend.md +61 -0
- package/guide/operations/ingest/index.md +54 -0
- package/guide/operations/ingest/join.md +65 -0
- package/guide/operations/maintain/fix.md +66 -0
- package/guide/operations/maintain/index.md +47 -0
- package/guide/operations/maintain/rebuild.md +86 -0
- package/guide/operations/validate.md +48 -0
- package/guide/substrate/index.md +47 -0
- package/guide/substrate/operators.md +96 -0
- package/guide/substrate/tiered-ai.md +363 -0
- package/guide/ux/index.md +44 -0
- package/guide/ux/preflight.md +150 -0
- package/guide/ux/user-intent.md +135 -0
- package/package.json +55 -0
- package/scripts/cli.mjs +893 -0
- package/scripts/commands/remote.mjs +93 -0
- package/scripts/commands/review.mjs +253 -0
- package/scripts/commands/sync.mjs +84 -0
- package/scripts/lib/chunk.mjs +421 -0
- package/scripts/lib/cluster-detect.mjs +516 -0
- package/scripts/lib/decision-log.mjs +343 -0
- package/scripts/lib/draft.mjs +158 -0
- package/scripts/lib/embeddings.mjs +366 -0
- package/scripts/lib/frontmatter.mjs +497 -0
- package/scripts/lib/git-commands.mjs +155 -0
- package/scripts/lib/git.mjs +486 -0
- package/scripts/lib/gitignore.mjs +62 -0
- package/scripts/lib/history.mjs +331 -0
- package/scripts/lib/indices.mjs +510 -0
- package/scripts/lib/ingest.mjs +258 -0
- package/scripts/lib/intent.mjs +713 -0
- package/scripts/lib/interactive.mjs +99 -0
- package/scripts/lib/migrate.mjs +126 -0
- package/scripts/lib/nest-applier.mjs +260 -0
- package/scripts/lib/operators.mjs +1365 -0
- package/scripts/lib/orchestrator.mjs +718 -0
- package/scripts/lib/paths.mjs +197 -0
- package/scripts/lib/preflight.mjs +213 -0
- package/scripts/lib/provenance.mjs +672 -0
- package/scripts/lib/quality-metric.mjs +269 -0
- package/scripts/lib/query-fixture.mjs +71 -0
- package/scripts/lib/rollback.mjs +95 -0
- package/scripts/lib/shape-check.mjs +172 -0
- package/scripts/lib/similarity-cache.mjs +126 -0
- package/scripts/lib/similarity.mjs +230 -0
- package/scripts/lib/snapshot.mjs +54 -0
- package/scripts/lib/source-frontmatter.mjs +85 -0
- package/scripts/lib/tier2-protocol.mjs +470 -0
- package/scripts/lib/tiered.mjs +453 -0
- package/scripts/lib/validate.mjs +362 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// Validator: runs the hard invariants from the methodology against a wiki.
|
|
2
|
+
// Reports findings as structured objects so tools can consume them.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { parseFrontmatter } from "./frontmatter.mjs";
|
|
7
|
+
import { readIndex } from "./indices.mjs";
|
|
8
|
+
import { isWikiRoot } from "./paths.mjs";
|
|
9
|
+
import { gitFsck, gitRefExists, gitRevParse, gitRun } from "./git.mjs";
|
|
10
|
+
import { provenancePath, readProvenance, verifyCoverage } from "./provenance.mjs";
|
|
11
|
+
import { readOpLog } from "./history.mjs";
|
|
12
|
+
|
|
13
|
+
export function validateWiki(wikiRoot) {
|
|
14
|
+
const findings = [];
|
|
15
|
+
const push = (severity, code, target, message) =>
|
|
16
|
+
findings.push({ severity, code, target, message });
|
|
17
|
+
|
|
18
|
+
if (!isWikiRoot(wikiRoot)) {
|
|
19
|
+
push("error", "WIKI-01", wikiRoot, "path is not a valid wiki root (no index.md or wrong naming)");
|
|
20
|
+
return findings;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// GIT-01 — guarded: only fires when the private git repo exists.
|
|
24
|
+
// When it fires, git fsck must pass with no non-dangling errors AND
|
|
25
|
+
// the most recent operation's pre-op tag must be reachable from HEAD.
|
|
26
|
+
runGit01(wikiRoot, push);
|
|
27
|
+
|
|
28
|
+
// LOSS-01 — guarded: only fires when .llmwiki/provenance.yaml exists.
|
|
29
|
+
// Every source byte must be accounted for via target sources[] or
|
|
30
|
+
// discarded_ranges[], with no gaps or overlaps.
|
|
31
|
+
runLoss01(wikiRoot, push);
|
|
32
|
+
|
|
33
|
+
const allEntries = collectAll(wikiRoot, push);
|
|
34
|
+
|
|
35
|
+
// Index maps for cross-checks
|
|
36
|
+
const byId = new Map();
|
|
37
|
+
const aliasTo = new Map();
|
|
38
|
+
for (const e of allEntries) {
|
|
39
|
+
if (byId.has(e.data.id)) {
|
|
40
|
+
push("error", "DUP-ID", e.absolute, `duplicate id "${e.data.id}" (also in ${byId.get(e.data.id).absolute})`);
|
|
41
|
+
} else {
|
|
42
|
+
byId.set(e.data.id, e);
|
|
43
|
+
}
|
|
44
|
+
for (const a of e.data.aliases ?? []) {
|
|
45
|
+
if (byId.has(a)) {
|
|
46
|
+
push("error", "ALIAS-COLLIDES-ID", e.absolute, `alias "${a}" collides with a live id`);
|
|
47
|
+
}
|
|
48
|
+
aliasTo.set(a, e);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const e of allEntries) {
|
|
53
|
+
const data = e.data;
|
|
54
|
+
|
|
55
|
+
// #1 Required frontmatter fields
|
|
56
|
+
const required = ["id", "type", "depth_role", "focus"];
|
|
57
|
+
for (const f of required) {
|
|
58
|
+
if (!(f in data)) push("error", "MISSING-FIELD", e.absolute, `required field "${f}" missing`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// #2 id matches filename/directory
|
|
62
|
+
if (data.type === "index") {
|
|
63
|
+
if (data.id !== basename(dirname(e.absolute))) {
|
|
64
|
+
push("error", "ID-MISMATCH-DIR", e.absolute, `index id "${data.id}" must match directory name "${basename(dirname(e.absolute))}"`);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
const expected = basename(e.absolute, ".md");
|
|
68
|
+
if (data.id !== expected) {
|
|
69
|
+
push("error", "ID-MISMATCH-FILE", e.absolute, `id "${data.id}" must match filename "${expected}"`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// #3 depth_role matches tree position
|
|
74
|
+
const depth = depthOf(e.absolute, wikiRoot, data.type === "index");
|
|
75
|
+
const expectedRole = depth === 0 ? "category" : depth === 1 ? "category" : "subcategory";
|
|
76
|
+
if (data.type === "index") {
|
|
77
|
+
// Tolerate either category or subcategory at depth ≥1
|
|
78
|
+
if (depth === 0 && data.depth_role !== "category") {
|
|
79
|
+
push("error", "DEPTH-ROLE", e.absolute, `root index must have depth_role: category`);
|
|
80
|
+
}
|
|
81
|
+
} else if (data.depth_role !== "leaf") {
|
|
82
|
+
push("error", "DEPTH-ROLE", e.absolute, `leaf entry must have depth_role: leaf`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// #8 parents[] required and non-empty (except root)
|
|
86
|
+
const isRoot = data.type === "index" && dirname(e.absolute) === wikiRoot;
|
|
87
|
+
if (!isRoot) {
|
|
88
|
+
if (!Array.isArray(data.parents) || data.parents.length === 0) {
|
|
89
|
+
push("error", "PARENTS-REQUIRED", e.absolute, `non-root entry must declare parents[]`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// #11 leaf size cap
|
|
94
|
+
if (data.type === "primary" || data.type === "overlay") {
|
|
95
|
+
const lineCount = readFileSync(e.absolute, "utf8").split("\n").length;
|
|
96
|
+
const cap = data.type === "overlay" ? 200 : 500;
|
|
97
|
+
if (lineCount > cap) {
|
|
98
|
+
push("warning", "SIZE-CAP", e.absolute, `${data.type} entry exceeds ${cap}-line cap (${lineCount})`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// #12 parent file contract: index.md body must not contain leaf-content signatures
|
|
103
|
+
if (data.type === "index") {
|
|
104
|
+
const { body } = parseFrontmatter(readFileSync(e.absolute, "utf8"), e.absolute);
|
|
105
|
+
const authored = extractAuthoredZone(body);
|
|
106
|
+
if (authored && /^\s*- \[ \]/m.test(authored)) {
|
|
107
|
+
push("error", "PARENT-CONTRACT", e.absolute, `index body contains checklist items — content must live in a leaf`);
|
|
108
|
+
}
|
|
109
|
+
if (authored && /^\s*```/m.test(authored)) {
|
|
110
|
+
push("error", "PARENT-CONTRACT", e.absolute, `index body contains code fences — content must live in a leaf`);
|
|
111
|
+
}
|
|
112
|
+
const budget = 2048;
|
|
113
|
+
if (authored && authored.length > budget) {
|
|
114
|
+
push("error", "PARENT-CONTRACT", e.absolute, `index authored zone ${authored.length} bytes exceeds ${budget}-byte budget`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// #6 links[].id resolves
|
|
119
|
+
for (const link of data.links ?? []) {
|
|
120
|
+
if (!link || typeof link !== "object") continue;
|
|
121
|
+
const target = link.id;
|
|
122
|
+
if (!target) continue;
|
|
123
|
+
if (!byId.has(target) && !aliasTo.has(target)) {
|
|
124
|
+
push("error", "DANGLING-LINK", e.absolute, `links[].id "${target}" does not resolve`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// #5 overlay targets resolve
|
|
129
|
+
if (data.type === "overlay") {
|
|
130
|
+
for (const target of data.overlay_targets ?? []) {
|
|
131
|
+
if (!byId.has(target) && !aliasTo.has(target)) {
|
|
132
|
+
push("error", "DANGLING-OVERLAY", e.absolute, `overlay_targets "${target}" does not resolve`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return findings;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function collectAll(wikiRoot, push) {
|
|
142
|
+
const out = [];
|
|
143
|
+
const stack = [wikiRoot];
|
|
144
|
+
while (stack.length > 0) {
|
|
145
|
+
const dir = stack.pop();
|
|
146
|
+
let entries;
|
|
147
|
+
try {
|
|
148
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
149
|
+
} catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// index.md
|
|
153
|
+
const indexPath = join(dir, "index.md");
|
|
154
|
+
if (existsSync(indexPath)) {
|
|
155
|
+
try {
|
|
156
|
+
const { data } = parseFrontmatter(readFileSync(indexPath, "utf8"), indexPath);
|
|
157
|
+
out.push({ absolute: indexPath, data });
|
|
158
|
+
} catch (err) {
|
|
159
|
+
push("error", "PARSE", indexPath, err.message);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const e of entries) {
|
|
163
|
+
if (e.name.startsWith(".")) continue;
|
|
164
|
+
const full = join(dir, e.name);
|
|
165
|
+
if (e.isDirectory()) {
|
|
166
|
+
stack.push(full);
|
|
167
|
+
} else if (e.isFile() && e.name.endsWith(".md") && e.name !== "index.md") {
|
|
168
|
+
try {
|
|
169
|
+
const { data } = parseFrontmatter(readFileSync(full, "utf8"), full);
|
|
170
|
+
if (data && data.id) out.push({ absolute: full, data });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
push("error", "PARSE", full, err.message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function depthOf(absPath, wikiRoot, isIndex) {
|
|
181
|
+
const base = isIndex ? dirname(absPath) : dirname(absPath);
|
|
182
|
+
const rel = relative(wikiRoot, base);
|
|
183
|
+
if (rel === "" || rel === ".") return 0;
|
|
184
|
+
return rel.split("/").filter(Boolean).length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function extractAuthoredZone(body) {
|
|
188
|
+
const start = body.indexOf("<!-- BEGIN AUTHORED ORIENTATION -->");
|
|
189
|
+
const end = body.indexOf("<!-- END AUTHORED ORIENTATION -->");
|
|
190
|
+
if (start === -1 || end === -1) return "";
|
|
191
|
+
return body.slice(start + "<!-- BEGIN AUTHORED ORIENTATION -->".length, end);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// GIT-01 — private git repo integrity.
|
|
195
|
+
//
|
|
196
|
+
// Guarded: only runs when `.llmwiki/git/HEAD` exists. When it runs, it
|
|
197
|
+
// requires `git fsck --no-dangling --no-reflogs` to exit cleanly AND
|
|
198
|
+
// the pre-op tag of the most recent logged operation to be reachable
|
|
199
|
+
// from HEAD (git ancestor check). Unreachable pre-op tags indicate
|
|
200
|
+
// either a tampered tree or a bug in the orchestrator's rollback path.
|
|
201
|
+
function runGit01(wikiRoot, push) {
|
|
202
|
+
if (!existsSync(join(wikiRoot, ".llmwiki", "git", "HEAD"))) return;
|
|
203
|
+
const fsck = gitFsck(wikiRoot);
|
|
204
|
+
if (!fsck.ok) {
|
|
205
|
+
push(
|
|
206
|
+
"error",
|
|
207
|
+
"GIT-01",
|
|
208
|
+
wikiRoot,
|
|
209
|
+
`git fsck failed: ${(fsck.stderr || fsck.stdout || "").trim()}`,
|
|
210
|
+
);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Find the most recent op's pre-op tag from the op-log. Empty log is
|
|
214
|
+
// a legitimate "freshly initialised wiki, no ops yet" — skip ancestor
|
|
215
|
+
// check in that case.
|
|
216
|
+
let opLog = [];
|
|
217
|
+
try {
|
|
218
|
+
opLog = readOpLog(wikiRoot);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
push(
|
|
221
|
+
"error",
|
|
222
|
+
"GIT-01",
|
|
223
|
+
join(wikiRoot, ".llmwiki", "op-log.yaml"),
|
|
224
|
+
`unreadable op-log: ${err.message}`,
|
|
225
|
+
);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (opLog.length === 0) return;
|
|
229
|
+
const latest = opLog[opLog.length - 1];
|
|
230
|
+
const preTag = `pre-op/${latest.op_id}`;
|
|
231
|
+
if (!gitRefExists(wikiRoot, preTag)) {
|
|
232
|
+
push(
|
|
233
|
+
"error",
|
|
234
|
+
"GIT-01",
|
|
235
|
+
wikiRoot,
|
|
236
|
+
`pre-op tag ${preTag} for latest logged op not found in the private repo`,
|
|
237
|
+
);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const headSha = gitRevParse(wikiRoot, "HEAD");
|
|
241
|
+
const preSha = gitRevParse(wikiRoot, preTag);
|
|
242
|
+
if (!headSha || !preSha) {
|
|
243
|
+
push(
|
|
244
|
+
"error",
|
|
245
|
+
"GIT-01",
|
|
246
|
+
wikiRoot,
|
|
247
|
+
`unable to resolve HEAD or ${preTag}`,
|
|
248
|
+
);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Ancestor check: pre-op/<latest> must be reachable from HEAD. Use
|
|
252
|
+
// `git merge-base --is-ancestor`, which exits 0 when preSha is an
|
|
253
|
+
// ancestor of headSha and exit 1 when it is not — O(log N) walk
|
|
254
|
+
// without materialising the full ancestry. Equality short-circuits
|
|
255
|
+
// for the common post-rollback case where HEAD === pre-op.
|
|
256
|
+
if (headSha === preSha) return;
|
|
257
|
+
try {
|
|
258
|
+
const r = gitRun(wikiRoot, [
|
|
259
|
+
"merge-base",
|
|
260
|
+
"--is-ancestor",
|
|
261
|
+
preSha,
|
|
262
|
+
headSha,
|
|
263
|
+
]);
|
|
264
|
+
if (r.status === 0) return;
|
|
265
|
+
if (r.status === 1) {
|
|
266
|
+
push(
|
|
267
|
+
"error",
|
|
268
|
+
"GIT-01",
|
|
269
|
+
wikiRoot,
|
|
270
|
+
`pre-op/${latest.op_id} (${preSha.slice(0, 12)}) is not an ancestor of HEAD (${headSha.slice(0, 12)})`,
|
|
271
|
+
);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
push(
|
|
275
|
+
"error",
|
|
276
|
+
"GIT-01",
|
|
277
|
+
wikiRoot,
|
|
278
|
+
`merge-base --is-ancestor exited ${r.status}: ${(r.stderr || "").trim()}`,
|
|
279
|
+
);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
push("error", "GIT-01", wikiRoot, `ancestor check failed: ${err.message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// LOSS-01 — provenance coverage of every source byte.
|
|
286
|
+
//
|
|
287
|
+
// Guarded: only runs when `.llmwiki/provenance.yaml` exists. Walks the
|
|
288
|
+
// manifest's target entries, computes the reverse source → ranges
|
|
289
|
+
// index, and asserts that every byte is covered by either a target's
|
|
290
|
+
// preserved/split/merged/transformed range or an explicit discarded
|
|
291
|
+
// range. Source sizes come from the manifest's `source_size` field
|
|
292
|
+
// (authoritative at ingest time) so the check does NOT depend on the
|
|
293
|
+
// source file still being available at validation time.
|
|
294
|
+
function runLoss01(wikiRoot, push) {
|
|
295
|
+
if (!existsSync(provenancePath(wikiRoot))) return;
|
|
296
|
+
let doc;
|
|
297
|
+
try {
|
|
298
|
+
doc = readProvenance(wikiRoot);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
push(
|
|
301
|
+
"error",
|
|
302
|
+
"LOSS-01",
|
|
303
|
+
provenancePath(wikiRoot),
|
|
304
|
+
`unreadable provenance manifest: ${err.message}`,
|
|
305
|
+
);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// Build an in-memory lookup from the source_size fields recorded by
|
|
309
|
+
// recordSource at ingest time.
|
|
310
|
+
const sizeIndex = new Map();
|
|
311
|
+
for (const entry of Object.values(doc.targets)) {
|
|
312
|
+
for (const s of entry.sources || []) {
|
|
313
|
+
if (typeof s.source_size === "number") {
|
|
314
|
+
sizeIndex.set(s.source_path, s.source_size);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const result = verifyCoverage(wikiRoot, (path) => {
|
|
319
|
+
if (sizeIndex.has(path)) return sizeIndex.get(path);
|
|
320
|
+
return null;
|
|
321
|
+
});
|
|
322
|
+
if (result.ok) return;
|
|
323
|
+
for (const u of result.uncovered) {
|
|
324
|
+
const rangeDesc = u.byte_range
|
|
325
|
+
? ` bytes ${u.byte_range[0]}..${u.byte_range[1]}`
|
|
326
|
+
: "";
|
|
327
|
+
push(
|
|
328
|
+
"error",
|
|
329
|
+
"LOSS-01",
|
|
330
|
+
join(wikiRoot, u.source_path || "<unknown-source>"),
|
|
331
|
+
`${u.source_path ?? "<unknown>"}${rangeDesc}: ${u.reason}`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
for (const o of result.overlaps) {
|
|
335
|
+
push(
|
|
336
|
+
"error",
|
|
337
|
+
"LOSS-01",
|
|
338
|
+
join(wikiRoot, o.source_path),
|
|
339
|
+
`${o.source_path} bytes ${o.byte_range[0]}..${o.byte_range[1]} claimed by ${o.target} AND another target`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
for (const ob of result.out_of_bounds || []) {
|
|
343
|
+
push(
|
|
344
|
+
"error",
|
|
345
|
+
"LOSS-01",
|
|
346
|
+
join(wikiRoot, ob.source_path),
|
|
347
|
+
`${ob.source_path} bytes ${ob.byte_range[0]}..${ob.byte_range[1]} exceed source_size ${ob.source_size} (target ${ob.target})`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function summariseFindings(findings) {
|
|
353
|
+
const byCode = new Map();
|
|
354
|
+
let errors = 0;
|
|
355
|
+
let warnings = 0;
|
|
356
|
+
for (const f of findings) {
|
|
357
|
+
if (f.severity === "error") errors++;
|
|
358
|
+
else if (f.severity === "warning") warnings++;
|
|
359
|
+
byCode.set(f.code, (byCode.get(f.code) ?? 0) + 1);
|
|
360
|
+
}
|
|
361
|
+
return { errors, warnings, byCode };
|
|
362
|
+
}
|