@hraness/kb 0.17.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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
parseAgentContextMarker
|
|
4
|
+
} from "./index-5vwpzb5a.js";
|
|
5
|
+
|
|
6
|
+
// src/agent-guide-audit.ts
|
|
7
|
+
import { constants } from "fs";
|
|
8
|
+
import {
|
|
9
|
+
lstat,
|
|
10
|
+
open,
|
|
11
|
+
readdir,
|
|
12
|
+
realpath
|
|
13
|
+
} from "fs/promises";
|
|
14
|
+
import {
|
|
15
|
+
isAbsolute,
|
|
16
|
+
join,
|
|
17
|
+
posix,
|
|
18
|
+
relative,
|
|
19
|
+
resolve,
|
|
20
|
+
sep
|
|
21
|
+
} from "path";
|
|
22
|
+
var defaultAgentGuideIgnoredDirectories = new Set([
|
|
23
|
+
".cache",
|
|
24
|
+
".git",
|
|
25
|
+
".next",
|
|
26
|
+
".output",
|
|
27
|
+
".turbo",
|
|
28
|
+
".vercel",
|
|
29
|
+
"build",
|
|
30
|
+
"coverage",
|
|
31
|
+
"dist",
|
|
32
|
+
"node_modules",
|
|
33
|
+
"out",
|
|
34
|
+
"vendor"
|
|
35
|
+
]);
|
|
36
|
+
var defaultAuditOptions = {
|
|
37
|
+
contentsWords: 120,
|
|
38
|
+
contentsBullets: 8,
|
|
39
|
+
guidelineWords: 250,
|
|
40
|
+
guidelineBullets: 10,
|
|
41
|
+
guidelineBulletWords: 40,
|
|
42
|
+
inheritedWords: 1200
|
|
43
|
+
};
|
|
44
|
+
function wordCount(value) {
|
|
45
|
+
const trimmed = value.trim();
|
|
46
|
+
return trimmed === "" ? 0 : trimmed.split(/\s+/u).length;
|
|
47
|
+
}
|
|
48
|
+
function fenceDelimiter(line) {
|
|
49
|
+
const match = /^\s{0,3}(`{3,}|~{3,})/u.exec(line);
|
|
50
|
+
const delimiter = match?.[1];
|
|
51
|
+
const character = delimiter?.[0];
|
|
52
|
+
return delimiter !== undefined && (character === "`" || character === "~") ? { character, length: delimiter.length } : null;
|
|
53
|
+
}
|
|
54
|
+
function markdownHeadings(lines) {
|
|
55
|
+
const headings = [];
|
|
56
|
+
let fence = null;
|
|
57
|
+
for (const [index, line = ""] of lines.entries()) {
|
|
58
|
+
const delimiter = fenceDelimiter(line);
|
|
59
|
+
if (fence !== null) {
|
|
60
|
+
if (delimiter?.character === fence.character && delimiter.length >= fence.length) {
|
|
61
|
+
fence = null;
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (delimiter !== null) {
|
|
66
|
+
fence = delimiter;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/u.exec(line);
|
|
70
|
+
if (match?.[1] !== undefined && match[2] !== undefined) {
|
|
71
|
+
headings.push({ line: index + 1, heading: `${match[1]} ${match[2]}` });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return headings;
|
|
75
|
+
}
|
|
76
|
+
function sectionBullets(lines, startIndex, endIndex) {
|
|
77
|
+
const bullets = [];
|
|
78
|
+
let current;
|
|
79
|
+
let fence = null;
|
|
80
|
+
for (let index = startIndex;index < endIndex; index += 1) {
|
|
81
|
+
const line = lines[index] ?? "";
|
|
82
|
+
const delimiter = fenceDelimiter(line);
|
|
83
|
+
if (fence !== null) {
|
|
84
|
+
if (delimiter?.character === fence.character && delimiter.length >= fence.length) {
|
|
85
|
+
fence = null;
|
|
86
|
+
} else if (current !== undefined && line.trim() !== "") {
|
|
87
|
+
current.parts.push(line.trim());
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (delimiter !== null) {
|
|
92
|
+
fence = delimiter;
|
|
93
|
+
if (current !== undefined)
|
|
94
|
+
current.parts.push(line.trim());
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const bullet = /^\s*-\s+(.+?)\s*$/u.exec(line);
|
|
98
|
+
if (bullet?.[1] !== undefined) {
|
|
99
|
+
current = { line: index + 1, parts: [bullet[1]] };
|
|
100
|
+
bullets.push(current);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (current !== undefined && line.trim() !== "")
|
|
104
|
+
current.parts.push(line.trim());
|
|
105
|
+
}
|
|
106
|
+
return bullets.map(({ line, parts }) => {
|
|
107
|
+
const text = parts.join(" ").replace(/\s+/gu, " ").trim();
|
|
108
|
+
return { line, text, words: wordCount(text) };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function scopeForGuide(path) {
|
|
112
|
+
const directory = posix.dirname(path);
|
|
113
|
+
return directory === "." ? "." : directory;
|
|
114
|
+
}
|
|
115
|
+
function auditAgentGuideSource(guide) {
|
|
116
|
+
const lines = guide.source.split(/\r?\n/u);
|
|
117
|
+
const headings = markdownHeadings(lines);
|
|
118
|
+
const expected = ["# Contents", "# Guidelines"];
|
|
119
|
+
const shapeIssues = [];
|
|
120
|
+
if (headings.length !== expected.length || headings.some(({ heading }, index) => heading !== expected[index])) {
|
|
121
|
+
shapeIssues.push({
|
|
122
|
+
kind: "headings",
|
|
123
|
+
message: "The guide must contain exactly '# Contents' then '# Guidelines' and no other headings."
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const contentsHeading = headings.find(({ heading }) => heading === "# Contents");
|
|
127
|
+
const guidelinesHeading = headings.find(({ heading }) => heading === "# Guidelines");
|
|
128
|
+
const contentsStart = contentsHeading?.line ?? 0;
|
|
129
|
+
const guidelinesStart = guidelinesHeading?.line ?? lines.length + 1;
|
|
130
|
+
const contentsBody = contentsStart > 0 && guidelinesStart > contentsStart ? lines.slice(contentsStart, guidelinesStart - 1).join(`
|
|
131
|
+
`).trim() : "";
|
|
132
|
+
const guidelinesBody = guidelinesStart > 0 ? lines.slice(guidelinesStart).join(`
|
|
133
|
+
`).trim() : "";
|
|
134
|
+
if (contentsBody === "") {
|
|
135
|
+
shapeIssues.push({
|
|
136
|
+
kind: "empty-contents",
|
|
137
|
+
message: "Contents must describe useful direct children."
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (guidelinesBody === "") {
|
|
141
|
+
shapeIssues.push({
|
|
142
|
+
kind: "empty-guidelines",
|
|
143
|
+
message: "Guidelines must contain scoped rules or '_None._'."
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
path: guide.path,
|
|
148
|
+
scope: scopeForGuide(guide.path),
|
|
149
|
+
source: guide.source,
|
|
150
|
+
words: wordCount(guide.source),
|
|
151
|
+
nonblankLines: lines.filter((line) => line.trim() !== "").length,
|
|
152
|
+
contents: {
|
|
153
|
+
words: wordCount(contentsBody),
|
|
154
|
+
bullets: sectionBullets(lines, contentsStart, Math.max(contentsStart, guidelinesStart - 1))
|
|
155
|
+
},
|
|
156
|
+
guidelines: {
|
|
157
|
+
words: wordCount(guidelinesBody),
|
|
158
|
+
bullets: sectionBullets(lines, guidelinesStart, lines.length)
|
|
159
|
+
},
|
|
160
|
+
marker: parseAgentContextMarker(guide.source),
|
|
161
|
+
shapeIssues
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function scopeIsAncestor(ancestor, descendant) {
|
|
165
|
+
return ancestor === "." || ancestor === descendant || descendant.startsWith(`${ancestor}/`);
|
|
166
|
+
}
|
|
167
|
+
function guideSort(left, right) {
|
|
168
|
+
return left.path.localeCompare(right.path);
|
|
169
|
+
}
|
|
170
|
+
function duplicateGuidelines(guides) {
|
|
171
|
+
const groups = new Map;
|
|
172
|
+
for (const guide of guides) {
|
|
173
|
+
for (const bullet of guide.guidelines.bullets) {
|
|
174
|
+
const occurrences = groups.get(bullet.text) ?? [];
|
|
175
|
+
occurrences.push({ path: guide.path, line: bullet.line, words: bullet.words });
|
|
176
|
+
groups.set(bullet.text, occurrences);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return [...groups.entries()].filter(([, occurrences]) => occurrences.length > 1).map(([text, occurrences]) => ({
|
|
180
|
+
text,
|
|
181
|
+
words: occurrences[0]?.words ?? 0,
|
|
182
|
+
guides: occurrences.map(({ path, line }) => ({ path, line })).toSorted((left, right) => left.path.localeCompare(right.path) || left.line - right.line)
|
|
183
|
+
})).toSorted((left, right) => right.guides.length - left.guides.length || right.words - left.words || left.text.localeCompare(right.text));
|
|
184
|
+
}
|
|
185
|
+
function advisoryTieBreakKey(advisory) {
|
|
186
|
+
const path = "path" in advisory ? advisory.path : "";
|
|
187
|
+
const line = "line" in advisory ? advisory.line : 0;
|
|
188
|
+
const text = "text" in advisory ? advisory.text : "";
|
|
189
|
+
return `${advisory.kind}\x00${path}\x00${String(line).padStart(8, "0")}\x00${text}`;
|
|
190
|
+
}
|
|
191
|
+
function budgetRatio(actual, suggested) {
|
|
192
|
+
return suggested > 0 ? actual / suggested : actual + 1;
|
|
193
|
+
}
|
|
194
|
+
function advisorySeverity(advisory) {
|
|
195
|
+
if (advisory.kind === "contents-budget") {
|
|
196
|
+
return Math.max(budgetRatio(advisory.actualWords, advisory.suggestedWords), budgetRatio(advisory.actualBullets, advisory.suggestedBullets));
|
|
197
|
+
}
|
|
198
|
+
if (advisory.kind === "guidelines-budget") {
|
|
199
|
+
return Math.max(budgetRatio(advisory.actualWords, advisory.suggestedWords), budgetRatio(advisory.actualBullets, advisory.suggestedBullets));
|
|
200
|
+
}
|
|
201
|
+
if (advisory.kind === "long-guideline") {
|
|
202
|
+
return budgetRatio(advisory.words, advisory.suggestedWords);
|
|
203
|
+
}
|
|
204
|
+
if (advisory.kind === "inherited-budget") {
|
|
205
|
+
return budgetRatio(advisory.words, advisory.suggestedWords);
|
|
206
|
+
}
|
|
207
|
+
return advisory.guides.length;
|
|
208
|
+
}
|
|
209
|
+
function auditAgentGuides(guideSources, options = {}) {
|
|
210
|
+
const thresholds = { ...defaultAuditOptions, ...options };
|
|
211
|
+
const baseGuides = guideSources.map(auditAgentGuideSource).toSorted(guideSort);
|
|
212
|
+
const guides = baseGuides.map((guide) => {
|
|
213
|
+
const inherited = baseGuides.filter((candidate) => scopeIsAncestor(candidate.scope, guide.scope)).toSorted((left, right) => {
|
|
214
|
+
const leftDepth = left.scope === "." ? 0 : left.scope.split("/").length;
|
|
215
|
+
const rightDepth = right.scope === "." ? 0 : right.scope.split("/").length;
|
|
216
|
+
return leftDepth - rightDepth || left.path.localeCompare(right.path);
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
...guide,
|
|
220
|
+
inheritedGuidePaths: inherited.map(({ path }) => path),
|
|
221
|
+
inheritedWords: inherited.reduce((total, candidate) => total + candidate.words, 0)
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
const duplicates = duplicateGuidelines(guides);
|
|
225
|
+
const advisories = [];
|
|
226
|
+
for (const guide of guides) {
|
|
227
|
+
if (guide.contents.words > thresholds.contentsWords || guide.contents.bullets.length > thresholds.contentsBullets) {
|
|
228
|
+
advisories.push({
|
|
229
|
+
kind: "contents-budget",
|
|
230
|
+
path: guide.path,
|
|
231
|
+
actualWords: guide.contents.words,
|
|
232
|
+
suggestedWords: thresholds.contentsWords,
|
|
233
|
+
actualBullets: guide.contents.bullets.length,
|
|
234
|
+
suggestedBullets: thresholds.contentsBullets
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (guide.guidelines.words > thresholds.guidelineWords || guide.guidelines.bullets.length > thresholds.guidelineBullets) {
|
|
238
|
+
advisories.push({
|
|
239
|
+
kind: "guidelines-budget",
|
|
240
|
+
path: guide.path,
|
|
241
|
+
actualWords: guide.guidelines.words,
|
|
242
|
+
suggestedWords: thresholds.guidelineWords,
|
|
243
|
+
actualBullets: guide.guidelines.bullets.length,
|
|
244
|
+
suggestedBullets: thresholds.guidelineBullets
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
for (const bullet of guide.guidelines.bullets) {
|
|
248
|
+
if (bullet.words > thresholds.guidelineBulletWords) {
|
|
249
|
+
advisories.push({
|
|
250
|
+
kind: "long-guideline",
|
|
251
|
+
path: guide.path,
|
|
252
|
+
line: bullet.line,
|
|
253
|
+
words: bullet.words,
|
|
254
|
+
suggestedWords: thresholds.guidelineBulletWords
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (guide.inheritedWords > thresholds.inheritedWords) {
|
|
259
|
+
advisories.push({
|
|
260
|
+
kind: "inherited-budget",
|
|
261
|
+
path: guide.path,
|
|
262
|
+
words: guide.inheritedWords,
|
|
263
|
+
suggestedWords: thresholds.inheritedWords,
|
|
264
|
+
guides: guide.inheritedGuidePaths
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const duplicate of duplicates) {
|
|
269
|
+
advisories.push({
|
|
270
|
+
kind: "duplicate-guideline",
|
|
271
|
+
text: duplicate.text,
|
|
272
|
+
words: duplicate.words,
|
|
273
|
+
guides: duplicate.guides.map(({ path }) => path)
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
guideCount: guides.length,
|
|
278
|
+
mappedGuideCount: guides.filter(({ marker }) => marker.kind === "found").length,
|
|
279
|
+
words: guides.reduce((total, guide) => total + guide.words, 0),
|
|
280
|
+
contentsWords: guides.reduce((total, guide) => total + guide.contents.words, 0),
|
|
281
|
+
guidelineWords: guides.reduce((total, guide) => total + guide.guidelines.words, 0),
|
|
282
|
+
nonblankLines: guides.reduce((total, guide) => total + guide.nonblankLines, 0),
|
|
283
|
+
guides,
|
|
284
|
+
duplicates,
|
|
285
|
+
advisories: advisories.toSorted((left, right) => advisorySeverity(right) - advisorySeverity(left) || advisoryTieBreakKey(left).localeCompare(advisoryTieBreakKey(right)))
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function longGuidelinesByIdentity(report) {
|
|
289
|
+
const guides = new Map(report.guides.map((guide) => [guide.path, guide]));
|
|
290
|
+
const guidelines = new Map;
|
|
291
|
+
for (const advisory of report.advisories) {
|
|
292
|
+
if (advisory.kind !== "long-guideline")
|
|
293
|
+
continue;
|
|
294
|
+
const bullet = guides.get(advisory.path)?.guidelines.bullets.find((candidate) => candidate.line === advisory.line);
|
|
295
|
+
if (bullet === undefined)
|
|
296
|
+
continue;
|
|
297
|
+
const identity = bullet.text;
|
|
298
|
+
const existing = guidelines.get(identity);
|
|
299
|
+
if (existing === undefined || advisory.path.localeCompare(existing.path) < 0 || advisory.path === existing.path && advisory.line < existing.line) {
|
|
300
|
+
guidelines.set(identity, {
|
|
301
|
+
path: advisory.path,
|
|
302
|
+
line: advisory.line,
|
|
303
|
+
text: bullet.text,
|
|
304
|
+
words: advisory.words,
|
|
305
|
+
suggestedWords: advisory.suggestedWords
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return guidelines;
|
|
310
|
+
}
|
|
311
|
+
function guidelineGuideSets(report) {
|
|
312
|
+
const guideSets = new Map;
|
|
313
|
+
for (const guide of report.guides) {
|
|
314
|
+
for (const bullet of guide.guidelines.bullets) {
|
|
315
|
+
const paths = guideSets.get(bullet.text) ?? new Set;
|
|
316
|
+
paths.add(guide.path);
|
|
317
|
+
guideSets.set(bullet.text, paths);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return new Map([...guideSets].map(([text, paths]) => [
|
|
321
|
+
text,
|
|
322
|
+
[...paths].toSorted((left, right) => left.localeCompare(right))
|
|
323
|
+
]));
|
|
324
|
+
}
|
|
325
|
+
function compareAgentGuideAudits(base, current) {
|
|
326
|
+
const baseGuides = new Map(base.guides.map((guide) => [guide.path, guide]));
|
|
327
|
+
const guideWordRegressions = [];
|
|
328
|
+
const inheritedWordRegressions = [];
|
|
329
|
+
for (const guide of current.guides) {
|
|
330
|
+
const baseGuide = baseGuides.get(guide.path);
|
|
331
|
+
const baseWords = baseGuide?.words ?? 0;
|
|
332
|
+
if (guide.words > baseWords) {
|
|
333
|
+
guideWordRegressions.push({
|
|
334
|
+
path: guide.path,
|
|
335
|
+
baseWords,
|
|
336
|
+
currentWords: guide.words,
|
|
337
|
+
addedWords: guide.words - baseWords
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
const baseInheritedWords = baseGuide?.inheritedWords ?? 0;
|
|
341
|
+
if (guide.inheritedWords > baseInheritedWords) {
|
|
342
|
+
inheritedWordRegressions.push({
|
|
343
|
+
path: guide.path,
|
|
344
|
+
baseWords: baseInheritedWords,
|
|
345
|
+
currentWords: guide.inheritedWords,
|
|
346
|
+
addedWords: guide.inheritedWords - baseInheritedWords,
|
|
347
|
+
guides: guide.inheritedGuidePaths
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const baseLongGuidelines = longGuidelinesByIdentity(base);
|
|
352
|
+
const newlyLongGuidelines = [...longGuidelinesByIdentity(current)].filter(([identity]) => !baseLongGuidelines.has(identity)).map(([, guideline]) => guideline).toSorted((left, right) => left.path.localeCompare(right.path) || left.text.localeCompare(right.text) || left.line - right.line);
|
|
353
|
+
const baseGuideSets = guidelineGuideSets(base);
|
|
354
|
+
const expandedDuplicateGuidelines = [...guidelineGuideSets(current)].flatMap(([text, currentGuides]) => {
|
|
355
|
+
if (currentGuides.length < 2)
|
|
356
|
+
return [];
|
|
357
|
+
const baseGuidesForText = baseGuideSets.get(text) ?? [];
|
|
358
|
+
if (currentGuides.length <= baseGuidesForText.length)
|
|
359
|
+
return [];
|
|
360
|
+
const baseSet = new Set(baseGuidesForText);
|
|
361
|
+
const addedGuides = currentGuides.filter((path) => !baseSet.has(path));
|
|
362
|
+
if (addedGuides.length === 0)
|
|
363
|
+
return [];
|
|
364
|
+
const words = current.guides.flatMap((guide) => guide.guidelines.bullets).find((bullet) => bullet.text === text)?.words ?? 0;
|
|
365
|
+
return [{
|
|
366
|
+
text,
|
|
367
|
+
words,
|
|
368
|
+
baseGuides: baseGuidesForText,
|
|
369
|
+
currentGuides,
|
|
370
|
+
addedGuides
|
|
371
|
+
}];
|
|
372
|
+
}).toSorted((left, right) => left.text.localeCompare(right.text));
|
|
373
|
+
return {
|
|
374
|
+
guideWordRegressions: guideWordRegressions.toSorted(guideSort),
|
|
375
|
+
inheritedWordRegressions: inheritedWordRegressions.toSorted(guideSort),
|
|
376
|
+
newlyLongGuidelines,
|
|
377
|
+
expandedDuplicateGuidelines
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function isWithin(root, candidate) {
|
|
381
|
+
const fromRoot = relative(root, candidate);
|
|
382
|
+
return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot);
|
|
383
|
+
}
|
|
384
|
+
async function discoverAgentGuides(repositoryRoot, ignoredDirectories = defaultAgentGuideIgnoredDirectories) {
|
|
385
|
+
const root = await realpath(resolve(repositoryRoot));
|
|
386
|
+
const rootMetadata = await lstat(root);
|
|
387
|
+
if (!rootMetadata.isDirectory())
|
|
388
|
+
throw new Error("The repository root must be a directory.");
|
|
389
|
+
const guides = [];
|
|
390
|
+
const issues = [];
|
|
391
|
+
async function visit(directory) {
|
|
392
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
393
|
+
for (const entry of entries.toSorted((left, right) => left.name.localeCompare(right.name))) {
|
|
394
|
+
if (entry.isDirectory() && ignoredDirectories.has(entry.name))
|
|
395
|
+
continue;
|
|
396
|
+
const absolutePath = join(directory, entry.name);
|
|
397
|
+
const path = relative(root, absolutePath).split(sep).join("/");
|
|
398
|
+
if (entry.isSymbolicLink()) {
|
|
399
|
+
if (entry.name === "AGENTS.md") {
|
|
400
|
+
issues.push({
|
|
401
|
+
kind: "symlink-guide",
|
|
402
|
+
path,
|
|
403
|
+
message: `The agent guide ${path} must be a regular file, not a symbolic link.`
|
|
404
|
+
});
|
|
405
|
+
} else {
|
|
406
|
+
try {
|
|
407
|
+
const target = await realpath(absolutePath);
|
|
408
|
+
if ((await lstat(target)).isDirectory()) {
|
|
409
|
+
issues.push({
|
|
410
|
+
kind: "symlink-directory",
|
|
411
|
+
path,
|
|
412
|
+
message: `Agent-guide discovery does not traverse the symbolic-link directory ${path}.`
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
} catch {}
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (entry.name === "AGENTS.md" && !entry.isFile()) {
|
|
420
|
+
issues.push({
|
|
421
|
+
kind: "non-regular-guide",
|
|
422
|
+
path,
|
|
423
|
+
message: `The agent guide ${path} must be a regular file.`
|
|
424
|
+
});
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (entry.isDirectory()) {
|
|
428
|
+
await visit(absolutePath);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (!entry.isFile() || entry.name !== "AGENTS.md")
|
|
432
|
+
continue;
|
|
433
|
+
const canonicalPath = await realpath(absolutePath);
|
|
434
|
+
if (!isWithin(root, canonicalPath)) {
|
|
435
|
+
issues.push({
|
|
436
|
+
kind: "symlink-guide",
|
|
437
|
+
path,
|
|
438
|
+
message: `The agent guide ${path} resolves outside the repository.`
|
|
439
|
+
});
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const handle = await open(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
443
|
+
try {
|
|
444
|
+
const metadata = await handle.stat();
|
|
445
|
+
if (!metadata.isFile()) {
|
|
446
|
+
issues.push({
|
|
447
|
+
kind: "symlink-guide",
|
|
448
|
+
path,
|
|
449
|
+
message: `The agent guide ${path} must be a regular file.`
|
|
450
|
+
});
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
guides.push({ path, source: await handle.readFile({ encoding: "utf8" }) });
|
|
454
|
+
} finally {
|
|
455
|
+
await handle.close();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
await visit(root);
|
|
460
|
+
return {
|
|
461
|
+
repositoryRoot: root,
|
|
462
|
+
guides: guides.toSorted((left, right) => left.path.localeCompare(right.path)),
|
|
463
|
+
issues: issues.toSorted((left, right) => left.path.localeCompare(right.path) || left.kind.localeCompare(right.kind))
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
async function auditAgentGuideRepository(repositoryRoot, options = {}) {
|
|
467
|
+
const discovery = await discoverAgentGuides(repositoryRoot);
|
|
468
|
+
return {
|
|
469
|
+
...discovery,
|
|
470
|
+
audit: auditAgentGuides(discovery.guides, options)
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export { defaultAgentGuideIgnoredDirectories, auditAgentGuideSource, auditAgentGuides, compareAgentGuideAudits, discoverAgentGuides, auditAgentGuideRepository };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
readCaptureBundle
|
|
4
|
+
} from "./index-npg9z1a4.js";
|
|
5
|
+
import {
|
|
6
|
+
runGitCommand
|
|
7
|
+
} from "./index-1gwbassd.js";
|
|
8
|
+
|
|
9
|
+
// src/clip/refresh.ts
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
import { lstat, realpath } from "fs/promises";
|
|
12
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
13
|
+
var MAX_GIT_DOCUMENT_BYTES = 64 * 1024 * 1024;
|
|
14
|
+
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
15
|
+
var GIT_TIMEOUT_MS = 1e4;
|
|
16
|
+
var REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$/u;
|
|
17
|
+
var OBJECT_ID_PATTERN = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/u;
|
|
18
|
+
function gitRef(value) {
|
|
19
|
+
const ref = value ?? "HEAD";
|
|
20
|
+
if (!REF_PATTERN.test(ref) || ref.includes("..") || ref.includes("//") || ref.endsWith("/") || ref.endsWith(".lock"))
|
|
21
|
+
throw new TypeError("capture diff ref is not a bounded Git revision name");
|
|
22
|
+
return ref;
|
|
23
|
+
}
|
|
24
|
+
function utf8(value) {
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
return Buffer.alloc(0);
|
|
27
|
+
return typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value);
|
|
28
|
+
}
|
|
29
|
+
function sha256(value) {
|
|
30
|
+
return createHash("sha256").update(value).digest("hex");
|
|
31
|
+
}
|
|
32
|
+
function gitMessage(result, label) {
|
|
33
|
+
return new Error(`${label}: ${result.message}`);
|
|
34
|
+
}
|
|
35
|
+
async function canonicalRepository(input) {
|
|
36
|
+
const repository = await realpath(resolve(input));
|
|
37
|
+
const stats = await lstat(repository);
|
|
38
|
+
if (!stats.isDirectory() || stats.isSymbolicLink())
|
|
39
|
+
throw new Error("capture diff repository must be a real directory");
|
|
40
|
+
return repository;
|
|
41
|
+
}
|
|
42
|
+
function repositoryPath(repository, path) {
|
|
43
|
+
const fromRepository = relative(repository, path);
|
|
44
|
+
if (fromRepository === "" || fromRepository === ".." || fromRepository.startsWith(`..${sep}`) || isAbsolute(fromRepository))
|
|
45
|
+
throw new Error("capture bundle must be inside the selected repository");
|
|
46
|
+
return fromRepository.split(sep).join("/");
|
|
47
|
+
}
|
|
48
|
+
async function assertCaptureBundleStable(options, readBundle, inspection) {
|
|
49
|
+
const confirmed = await readBundle(options.bundle, options.readOptions);
|
|
50
|
+
if (confirmed.root !== inspection.root || confirmed.document.path !== inspection.document.path || confirmed.document.sha256 !== inspection.document.sha256) {
|
|
51
|
+
throw new Error("Capture bundle changed while its Git comparison was generated; retry.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function diffCaptureBundle(options, dependencies = {}) {
|
|
55
|
+
const ref = gitRef(options.ref);
|
|
56
|
+
const selectedDirectory = await canonicalRepository(options.repository);
|
|
57
|
+
const readBundle = dependencies.readBundle ?? readCaptureBundle;
|
|
58
|
+
const inspection = await readBundle(options.bundle, options.readOptions);
|
|
59
|
+
const runGit = dependencies.runGit ?? runGitCommand;
|
|
60
|
+
const repositoryCheck = await runGit({
|
|
61
|
+
arguments: ["rev-parse", "--show-toplevel"],
|
|
62
|
+
cwd: selectedDirectory,
|
|
63
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
64
|
+
maxOutputBytes: 4 * 1024
|
|
65
|
+
});
|
|
66
|
+
if (repositoryCheck.status !== "ok") {
|
|
67
|
+
throw gitMessage(repositoryCheck, "Capture diff repository is not a Git work tree");
|
|
68
|
+
}
|
|
69
|
+
const topLevel = utf8(repositoryCheck.stdout).toString("utf8").trim();
|
|
70
|
+
if (!isAbsolute(topLevel))
|
|
71
|
+
throw new Error("Capture diff repository did not resolve to a canonical Git work tree");
|
|
72
|
+
const repository = await canonicalRepository(topLevel);
|
|
73
|
+
const selectedFromRepository = relative(repository, selectedDirectory);
|
|
74
|
+
if (selectedFromRepository === ".." || selectedFromRepository.startsWith(`..${sep}`) || isAbsolute(selectedFromRepository)) {
|
|
75
|
+
throw new Error("Capture diff repository did not resolve inside its Git work tree");
|
|
76
|
+
}
|
|
77
|
+
const path = repositoryPath(repository, resolve(inspection.root, inspection.document.path));
|
|
78
|
+
const refCheck = await runGit({
|
|
79
|
+
arguments: ["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`],
|
|
80
|
+
cwd: repository,
|
|
81
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
82
|
+
maxOutputBytes: 4 * 1024
|
|
83
|
+
});
|
|
84
|
+
if (refCheck.status !== "ok")
|
|
85
|
+
throw gitMessage(refCheck, "Could not resolve capture diff Git ref");
|
|
86
|
+
const objectId = utf8(refCheck.stdout).toString("utf8").trim();
|
|
87
|
+
if (!OBJECT_ID_PATTERN.test(objectId)) {
|
|
88
|
+
throw new Error("Could not resolve capture diff Git ref: Git returned a malformed commit object ID");
|
|
89
|
+
}
|
|
90
|
+
const literalPathspec = `:(literal)${path}`;
|
|
91
|
+
const specifier = `${objectId}:${path}`;
|
|
92
|
+
const tree = await runGit({
|
|
93
|
+
arguments: ["ls-tree", "-z", objectId, "--", literalPathspec],
|
|
94
|
+
cwd: repository,
|
|
95
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
96
|
+
maxOutputBytes: 8 * 1024
|
|
97
|
+
});
|
|
98
|
+
if (tree.status !== "ok")
|
|
99
|
+
throw gitMessage(tree, "Could not inspect capture path at the requested Git ref");
|
|
100
|
+
if (utf8(tree.stdout).byteLength === 0) {
|
|
101
|
+
await assertCaptureBundleStable(options, readBundle, inspection);
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
status: "missing-at-ref",
|
|
104
|
+
ref,
|
|
105
|
+
repositoryPath: path,
|
|
106
|
+
currentSha256: inspection.document.sha256,
|
|
107
|
+
referenceSha256: null,
|
|
108
|
+
diff: null
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const historical = await runGit({
|
|
112
|
+
arguments: ["show", "--no-ext-diff", "--no-textconv", "--format=", specifier],
|
|
113
|
+
cwd: repository,
|
|
114
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
115
|
+
maxOutputBytes: MAX_GIT_DOCUMENT_BYTES
|
|
116
|
+
});
|
|
117
|
+
if (historical.status !== "ok") {
|
|
118
|
+
throw gitMessage(historical, "Could not read capture at the requested Git ref");
|
|
119
|
+
}
|
|
120
|
+
const referenceBytes = utf8(historical.stdout);
|
|
121
|
+
const referenceSha256 = sha256(referenceBytes);
|
|
122
|
+
if (referenceSha256 === inspection.document.sha256) {
|
|
123
|
+
await assertCaptureBundleStable(options, readBundle, inspection);
|
|
124
|
+
return Object.freeze({
|
|
125
|
+
status: "unchanged",
|
|
126
|
+
ref,
|
|
127
|
+
repositoryPath: path,
|
|
128
|
+
currentSha256: inspection.document.sha256,
|
|
129
|
+
referenceSha256,
|
|
130
|
+
diff: ""
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const result = await runGit({
|
|
134
|
+
arguments: ["diff", "--no-ext-diff", "--no-textconv", "--unified=3", objectId, "--", literalPathspec],
|
|
135
|
+
cwd: repository,
|
|
136
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
137
|
+
maxOutputBytes: MAX_DIFF_BYTES
|
|
138
|
+
});
|
|
139
|
+
if (result.status !== "ok")
|
|
140
|
+
throw gitMessage(result, "Could not diff capture bundle");
|
|
141
|
+
await assertCaptureBundleStable(options, readBundle, inspection);
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
status: "changed",
|
|
144
|
+
ref,
|
|
145
|
+
repositoryPath: path,
|
|
146
|
+
currentSha256: inspection.document.sha256,
|
|
147
|
+
referenceSha256,
|
|
148
|
+
diff: utf8(result.stdout).toString("utf8")
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { diffCaptureBundle };
|