@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,750 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
lstat,
|
|
4
|
+
open,
|
|
5
|
+
readdir,
|
|
6
|
+
realpath,
|
|
7
|
+
} from "node:fs/promises";
|
|
8
|
+
import {
|
|
9
|
+
isAbsolute,
|
|
10
|
+
join,
|
|
11
|
+
posix,
|
|
12
|
+
relative,
|
|
13
|
+
resolve,
|
|
14
|
+
sep,
|
|
15
|
+
} from "node:path";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
parseAgentContextMarker,
|
|
19
|
+
type AgentContextMarkerParseResult,
|
|
20
|
+
type AgentGuideSource,
|
|
21
|
+
} from "./agent-context.js";
|
|
22
|
+
|
|
23
|
+
export const defaultAgentGuideIgnoredDirectories: ReadonlySet<string> = new Set([
|
|
24
|
+
".cache",
|
|
25
|
+
".git",
|
|
26
|
+
".next",
|
|
27
|
+
".output",
|
|
28
|
+
".turbo",
|
|
29
|
+
".vercel",
|
|
30
|
+
"build",
|
|
31
|
+
"coverage",
|
|
32
|
+
"dist",
|
|
33
|
+
"node_modules",
|
|
34
|
+
"out",
|
|
35
|
+
"vendor",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export type AgentGuideShapeIssue =
|
|
39
|
+
| {
|
|
40
|
+
readonly kind: "headings";
|
|
41
|
+
readonly message: string;
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
readonly kind: "empty-contents";
|
|
45
|
+
readonly message: string;
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
readonly kind: "empty-guidelines";
|
|
49
|
+
readonly message: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type AgentGuideBullet = {
|
|
53
|
+
readonly line: number;
|
|
54
|
+
readonly text: string;
|
|
55
|
+
readonly words: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type AgentGuideSectionAudit = {
|
|
59
|
+
readonly words: number;
|
|
60
|
+
readonly bullets: readonly AgentGuideBullet[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type AgentGuideAudit = {
|
|
64
|
+
readonly path: string;
|
|
65
|
+
readonly scope: string;
|
|
66
|
+
readonly source: string;
|
|
67
|
+
readonly words: number;
|
|
68
|
+
readonly nonblankLines: number;
|
|
69
|
+
readonly contents: AgentGuideSectionAudit;
|
|
70
|
+
readonly guidelines: AgentGuideSectionAudit;
|
|
71
|
+
readonly marker: AgentContextMarkerParseResult;
|
|
72
|
+
readonly shapeIssues: readonly AgentGuideShapeIssue[];
|
|
73
|
+
readonly inheritedGuidePaths: readonly string[];
|
|
74
|
+
readonly inheritedWords: number;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type AgentGuideDuplicate = {
|
|
78
|
+
readonly text: string;
|
|
79
|
+
readonly words: number;
|
|
80
|
+
readonly guides: readonly {
|
|
81
|
+
readonly path: string;
|
|
82
|
+
readonly line: number;
|
|
83
|
+
}[];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type AgentGuideAdvisory =
|
|
87
|
+
| {
|
|
88
|
+
readonly kind: "contents-budget";
|
|
89
|
+
readonly path: string;
|
|
90
|
+
readonly actualWords: number;
|
|
91
|
+
readonly suggestedWords: number;
|
|
92
|
+
readonly actualBullets: number;
|
|
93
|
+
readonly suggestedBullets: number;
|
|
94
|
+
}
|
|
95
|
+
| {
|
|
96
|
+
readonly kind: "guidelines-budget";
|
|
97
|
+
readonly path: string;
|
|
98
|
+
readonly actualWords: number;
|
|
99
|
+
readonly suggestedWords: number;
|
|
100
|
+
readonly actualBullets: number;
|
|
101
|
+
readonly suggestedBullets: number;
|
|
102
|
+
}
|
|
103
|
+
| {
|
|
104
|
+
readonly kind: "long-guideline";
|
|
105
|
+
readonly path: string;
|
|
106
|
+
readonly line: number;
|
|
107
|
+
readonly words: number;
|
|
108
|
+
readonly suggestedWords: number;
|
|
109
|
+
}
|
|
110
|
+
| {
|
|
111
|
+
readonly kind: "inherited-budget";
|
|
112
|
+
readonly path: string;
|
|
113
|
+
readonly words: number;
|
|
114
|
+
readonly suggestedWords: number;
|
|
115
|
+
readonly guides: readonly string[];
|
|
116
|
+
}
|
|
117
|
+
| {
|
|
118
|
+
readonly kind: "duplicate-guideline";
|
|
119
|
+
readonly text: string;
|
|
120
|
+
readonly words: number;
|
|
121
|
+
readonly guides: readonly string[];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type AgentGuideAuditOptions = {
|
|
125
|
+
readonly contentsWords?: number;
|
|
126
|
+
readonly contentsBullets?: number;
|
|
127
|
+
readonly guidelineWords?: number;
|
|
128
|
+
readonly guidelineBullets?: number;
|
|
129
|
+
readonly guidelineBulletWords?: number;
|
|
130
|
+
readonly inheritedWords?: number;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type AgentGuideAuditReport = {
|
|
134
|
+
readonly guideCount: number;
|
|
135
|
+
readonly mappedGuideCount: number;
|
|
136
|
+
readonly words: number;
|
|
137
|
+
readonly contentsWords: number;
|
|
138
|
+
readonly guidelineWords: number;
|
|
139
|
+
readonly nonblankLines: number;
|
|
140
|
+
readonly guides: readonly AgentGuideAudit[];
|
|
141
|
+
readonly duplicates: readonly AgentGuideDuplicate[];
|
|
142
|
+
readonly advisories: readonly AgentGuideAdvisory[];
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export type AgentGuideWordRegression = {
|
|
146
|
+
readonly path: string;
|
|
147
|
+
readonly baseWords: number;
|
|
148
|
+
readonly currentWords: number;
|
|
149
|
+
readonly addedWords: number;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export type AgentGuideInheritedWordRegression = AgentGuideWordRegression & {
|
|
153
|
+
readonly guides: readonly string[];
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export type AgentGuideNewLongGuideline = {
|
|
157
|
+
readonly path: string;
|
|
158
|
+
readonly line: number;
|
|
159
|
+
readonly text: string;
|
|
160
|
+
readonly words: number;
|
|
161
|
+
readonly suggestedWords: number;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export type AgentGuideExpandedDuplicate = {
|
|
165
|
+
readonly text: string;
|
|
166
|
+
readonly words: number;
|
|
167
|
+
readonly baseGuides: readonly string[];
|
|
168
|
+
readonly currentGuides: readonly string[];
|
|
169
|
+
readonly addedGuides: readonly string[];
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export type AgentGuideAuditComparison = {
|
|
173
|
+
readonly guideWordRegressions: readonly AgentGuideWordRegression[];
|
|
174
|
+
readonly inheritedWordRegressions: readonly AgentGuideInheritedWordRegression[];
|
|
175
|
+
readonly newlyLongGuidelines: readonly AgentGuideNewLongGuideline[];
|
|
176
|
+
readonly expandedDuplicateGuidelines: readonly AgentGuideExpandedDuplicate[];
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export type AgentGuideDiscoveryIssue = {
|
|
180
|
+
readonly kind:
|
|
181
|
+
| "symlink-directory"
|
|
182
|
+
| "symlink-guide"
|
|
183
|
+
| "non-regular-guide";
|
|
184
|
+
readonly path: string;
|
|
185
|
+
readonly message: string;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export type AgentGuideDiscovery = {
|
|
189
|
+
readonly repositoryRoot: string;
|
|
190
|
+
readonly guides: readonly AgentGuideSource[];
|
|
191
|
+
readonly issues: readonly AgentGuideDiscoveryIssue[];
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const defaultAuditOptions = {
|
|
195
|
+
contentsWords: 120,
|
|
196
|
+
contentsBullets: 8,
|
|
197
|
+
guidelineWords: 250,
|
|
198
|
+
guidelineBullets: 10,
|
|
199
|
+
guidelineBulletWords: 40,
|
|
200
|
+
inheritedWords: 1_200,
|
|
201
|
+
} as const;
|
|
202
|
+
|
|
203
|
+
function wordCount(value: string): number {
|
|
204
|
+
const trimmed = value.trim();
|
|
205
|
+
return trimmed === "" ? 0 : trimmed.split(/\s+/u).length;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function fenceDelimiter(line: string): { readonly character: "`" | "~"; readonly length: number } | null {
|
|
209
|
+
const match = /^\s{0,3}(`{3,}|~{3,})/u.exec(line);
|
|
210
|
+
const delimiter = match?.[1];
|
|
211
|
+
const character = delimiter?.[0];
|
|
212
|
+
return delimiter !== undefined && (character === "`" || character === "~")
|
|
213
|
+
? { character, length: delimiter.length }
|
|
214
|
+
: null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function markdownHeadings(lines: readonly string[]): readonly {
|
|
218
|
+
readonly line: number;
|
|
219
|
+
readonly heading: string;
|
|
220
|
+
}[] {
|
|
221
|
+
const headings: { line: number; heading: string }[] = [];
|
|
222
|
+
let fence: { readonly character: "`" | "~"; readonly length: number } | null = null;
|
|
223
|
+
for (const [index, line = ""] of lines.entries()) {
|
|
224
|
+
const delimiter = fenceDelimiter(line);
|
|
225
|
+
if (fence !== null) {
|
|
226
|
+
if (delimiter?.character === fence.character && delimiter.length >= fence.length) {
|
|
227
|
+
fence = null;
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (delimiter !== null) {
|
|
232
|
+
fence = delimiter;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/u.exec(line);
|
|
236
|
+
if (match?.[1] !== undefined && match[2] !== undefined) {
|
|
237
|
+
headings.push({ line: index + 1, heading: `${match[1]} ${match[2]}` });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return headings;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function sectionBullets(
|
|
244
|
+
lines: readonly string[],
|
|
245
|
+
startIndex: number,
|
|
246
|
+
endIndex: number,
|
|
247
|
+
): readonly AgentGuideBullet[] {
|
|
248
|
+
const bullets: { line: number; parts: string[] }[] = [];
|
|
249
|
+
let current: { line: number; parts: string[] } | undefined;
|
|
250
|
+
let fence: { readonly character: "`" | "~"; readonly length: number } | null = null;
|
|
251
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
252
|
+
const line = lines[index] ?? "";
|
|
253
|
+
const delimiter = fenceDelimiter(line);
|
|
254
|
+
if (fence !== null) {
|
|
255
|
+
if (delimiter?.character === fence.character && delimiter.length >= fence.length) {
|
|
256
|
+
fence = null;
|
|
257
|
+
} else if (current !== undefined && line.trim() !== "") {
|
|
258
|
+
current.parts.push(line.trim());
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (delimiter !== null) {
|
|
263
|
+
fence = delimiter;
|
|
264
|
+
if (current !== undefined) current.parts.push(line.trim());
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const bullet = /^\s*-\s+(.+?)\s*$/u.exec(line);
|
|
268
|
+
if (bullet?.[1] !== undefined) {
|
|
269
|
+
current = { line: index + 1, parts: [bullet[1]] };
|
|
270
|
+
bullets.push(current);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (current !== undefined && line.trim() !== "") current.parts.push(line.trim());
|
|
274
|
+
}
|
|
275
|
+
return bullets.map(({ line, parts }) => {
|
|
276
|
+
const text = parts.join(" ").replace(/\s+/gu, " ").trim();
|
|
277
|
+
return { line, text, words: wordCount(text) };
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function scopeForGuide(path: string): string {
|
|
282
|
+
const directory = posix.dirname(path);
|
|
283
|
+
return directory === "." ? "." : directory;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Parse one guide into shape, section, marker, and size measurements. */
|
|
287
|
+
export function auditAgentGuideSource(guide: AgentGuideSource): Omit<
|
|
288
|
+
AgentGuideAudit,
|
|
289
|
+
"inheritedGuidePaths" | "inheritedWords"
|
|
290
|
+
> {
|
|
291
|
+
const lines = guide.source.split(/\r?\n/u);
|
|
292
|
+
const headings = markdownHeadings(lines);
|
|
293
|
+
const expected = ["# Contents", "# Guidelines"];
|
|
294
|
+
const shapeIssues: AgentGuideShapeIssue[] = [];
|
|
295
|
+
if (
|
|
296
|
+
headings.length !== expected.length
|
|
297
|
+
|| headings.some(({ heading }, index) => heading !== expected[index])
|
|
298
|
+
) {
|
|
299
|
+
shapeIssues.push({
|
|
300
|
+
kind: "headings",
|
|
301
|
+
message: "The guide must contain exactly '# Contents' then '# Guidelines' and no other headings.",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const contentsHeading = headings.find(({ heading }) => heading === "# Contents");
|
|
306
|
+
const guidelinesHeading = headings.find(({ heading }) => heading === "# Guidelines");
|
|
307
|
+
const contentsStart = contentsHeading?.line ?? 0;
|
|
308
|
+
const guidelinesStart = guidelinesHeading?.line ?? lines.length + 1;
|
|
309
|
+
const contentsBody = contentsStart > 0 && guidelinesStart > contentsStart
|
|
310
|
+
? lines.slice(contentsStart, guidelinesStart - 1).join("\n").trim()
|
|
311
|
+
: "";
|
|
312
|
+
const guidelinesBody = guidelinesStart > 0
|
|
313
|
+
? lines.slice(guidelinesStart).join("\n").trim()
|
|
314
|
+
: "";
|
|
315
|
+
if (contentsBody === "") {
|
|
316
|
+
shapeIssues.push({
|
|
317
|
+
kind: "empty-contents",
|
|
318
|
+
message: "Contents must describe useful direct children.",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (guidelinesBody === "") {
|
|
322
|
+
shapeIssues.push({
|
|
323
|
+
kind: "empty-guidelines",
|
|
324
|
+
message: "Guidelines must contain scoped rules or '_None._'.",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
path: guide.path,
|
|
330
|
+
scope: scopeForGuide(guide.path),
|
|
331
|
+
source: guide.source,
|
|
332
|
+
words: wordCount(guide.source),
|
|
333
|
+
nonblankLines: lines.filter((line) => line.trim() !== "").length,
|
|
334
|
+
contents: {
|
|
335
|
+
words: wordCount(contentsBody),
|
|
336
|
+
bullets: sectionBullets(
|
|
337
|
+
lines,
|
|
338
|
+
contentsStart,
|
|
339
|
+
Math.max(contentsStart, guidelinesStart - 1),
|
|
340
|
+
),
|
|
341
|
+
},
|
|
342
|
+
guidelines: {
|
|
343
|
+
words: wordCount(guidelinesBody),
|
|
344
|
+
bullets: sectionBullets(lines, guidelinesStart, lines.length),
|
|
345
|
+
},
|
|
346
|
+
marker: parseAgentContextMarker(guide.source),
|
|
347
|
+
shapeIssues,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function scopeIsAncestor(ancestor: string, descendant: string): boolean {
|
|
352
|
+
return ancestor === "."
|
|
353
|
+
|| ancestor === descendant
|
|
354
|
+
|| descendant.startsWith(`${ancestor}/`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function guideSort(
|
|
358
|
+
left: Pick<AgentGuideAudit, "path">,
|
|
359
|
+
right: Pick<AgentGuideAudit, "path">,
|
|
360
|
+
): number {
|
|
361
|
+
return left.path.localeCompare(right.path);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function duplicateGuidelines(
|
|
365
|
+
guides: readonly AgentGuideAudit[],
|
|
366
|
+
): AgentGuideDuplicate[] {
|
|
367
|
+
const groups = new Map<string, { path: string; line: number; words: number }[]>();
|
|
368
|
+
for (const guide of guides) {
|
|
369
|
+
for (const bullet of guide.guidelines.bullets) {
|
|
370
|
+
const occurrences = groups.get(bullet.text) ?? [];
|
|
371
|
+
occurrences.push({ path: guide.path, line: bullet.line, words: bullet.words });
|
|
372
|
+
groups.set(bullet.text, occurrences);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return [...groups.entries()]
|
|
376
|
+
.filter(([, occurrences]) => occurrences.length > 1)
|
|
377
|
+
.map(([text, occurrences]): AgentGuideDuplicate => ({
|
|
378
|
+
text,
|
|
379
|
+
words: occurrences[0]?.words ?? 0,
|
|
380
|
+
guides: occurrences
|
|
381
|
+
.map(({ path, line }) => ({ path, line }))
|
|
382
|
+
.toSorted((left, right) =>
|
|
383
|
+
left.path.localeCompare(right.path) || left.line - right.line),
|
|
384
|
+
}))
|
|
385
|
+
.toSorted((left, right) =>
|
|
386
|
+
right.guides.length - left.guides.length
|
|
387
|
+
|| right.words - left.words
|
|
388
|
+
|| left.text.localeCompare(right.text));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function advisoryTieBreakKey(advisory: AgentGuideAdvisory): string {
|
|
392
|
+
const path = "path" in advisory ? advisory.path : "";
|
|
393
|
+
const line = "line" in advisory ? advisory.line : 0;
|
|
394
|
+
const text = "text" in advisory ? advisory.text : "";
|
|
395
|
+
return `${advisory.kind}\0${path}\0${String(line).padStart(8, "0")}\0${text}`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function budgetRatio(actual: number, suggested: number): number {
|
|
399
|
+
return suggested > 0 ? actual / suggested : actual + 1;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function advisorySeverity(advisory: AgentGuideAdvisory): number {
|
|
403
|
+
if (advisory.kind === "contents-budget") {
|
|
404
|
+
return Math.max(
|
|
405
|
+
budgetRatio(advisory.actualWords, advisory.suggestedWords),
|
|
406
|
+
budgetRatio(advisory.actualBullets, advisory.suggestedBullets),
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
if (advisory.kind === "guidelines-budget") {
|
|
410
|
+
return Math.max(
|
|
411
|
+
budgetRatio(advisory.actualWords, advisory.suggestedWords),
|
|
412
|
+
budgetRatio(advisory.actualBullets, advisory.suggestedBullets),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if (advisory.kind === "long-guideline") {
|
|
416
|
+
return budgetRatio(advisory.words, advisory.suggestedWords);
|
|
417
|
+
}
|
|
418
|
+
if (advisory.kind === "inherited-budget") {
|
|
419
|
+
return budgetRatio(advisory.words, advisory.suggestedWords);
|
|
420
|
+
}
|
|
421
|
+
return advisory.guides.length;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Produce deterministic per-guide, duplicate, and inherited-chain advisories. */
|
|
425
|
+
export function auditAgentGuides(
|
|
426
|
+
guideSources: readonly AgentGuideSource[],
|
|
427
|
+
options: AgentGuideAuditOptions = {},
|
|
428
|
+
): AgentGuideAuditReport {
|
|
429
|
+
const thresholds = { ...defaultAuditOptions, ...options };
|
|
430
|
+
const baseGuides = guideSources
|
|
431
|
+
.map(auditAgentGuideSource)
|
|
432
|
+
.toSorted(guideSort);
|
|
433
|
+
const guides = baseGuides.map((guide): AgentGuideAudit => {
|
|
434
|
+
const inherited = baseGuides
|
|
435
|
+
.filter((candidate) => scopeIsAncestor(candidate.scope, guide.scope))
|
|
436
|
+
.toSorted((left, right) => {
|
|
437
|
+
const leftDepth = left.scope === "." ? 0 : left.scope.split("/").length;
|
|
438
|
+
const rightDepth = right.scope === "." ? 0 : right.scope.split("/").length;
|
|
439
|
+
return leftDepth - rightDepth || left.path.localeCompare(right.path);
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
...guide,
|
|
443
|
+
inheritedGuidePaths: inherited.map(({ path }) => path),
|
|
444
|
+
inheritedWords: inherited.reduce((total, candidate) => total + candidate.words, 0),
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
const duplicates = duplicateGuidelines(guides);
|
|
448
|
+
const advisories: AgentGuideAdvisory[] = [];
|
|
449
|
+
|
|
450
|
+
for (const guide of guides) {
|
|
451
|
+
if (
|
|
452
|
+
guide.contents.words > thresholds.contentsWords
|
|
453
|
+
|| guide.contents.bullets.length > thresholds.contentsBullets
|
|
454
|
+
) {
|
|
455
|
+
advisories.push({
|
|
456
|
+
kind: "contents-budget",
|
|
457
|
+
path: guide.path,
|
|
458
|
+
actualWords: guide.contents.words,
|
|
459
|
+
suggestedWords: thresholds.contentsWords,
|
|
460
|
+
actualBullets: guide.contents.bullets.length,
|
|
461
|
+
suggestedBullets: thresholds.contentsBullets,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
if (
|
|
465
|
+
guide.guidelines.words > thresholds.guidelineWords
|
|
466
|
+
|| guide.guidelines.bullets.length > thresholds.guidelineBullets
|
|
467
|
+
) {
|
|
468
|
+
advisories.push({
|
|
469
|
+
kind: "guidelines-budget",
|
|
470
|
+
path: guide.path,
|
|
471
|
+
actualWords: guide.guidelines.words,
|
|
472
|
+
suggestedWords: thresholds.guidelineWords,
|
|
473
|
+
actualBullets: guide.guidelines.bullets.length,
|
|
474
|
+
suggestedBullets: thresholds.guidelineBullets,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
for (const bullet of guide.guidelines.bullets) {
|
|
478
|
+
if (bullet.words > thresholds.guidelineBulletWords) {
|
|
479
|
+
advisories.push({
|
|
480
|
+
kind: "long-guideline",
|
|
481
|
+
path: guide.path,
|
|
482
|
+
line: bullet.line,
|
|
483
|
+
words: bullet.words,
|
|
484
|
+
suggestedWords: thresholds.guidelineBulletWords,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (guide.inheritedWords > thresholds.inheritedWords) {
|
|
489
|
+
advisories.push({
|
|
490
|
+
kind: "inherited-budget",
|
|
491
|
+
path: guide.path,
|
|
492
|
+
words: guide.inheritedWords,
|
|
493
|
+
suggestedWords: thresholds.inheritedWords,
|
|
494
|
+
guides: guide.inheritedGuidePaths,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
for (const duplicate of duplicates) {
|
|
499
|
+
advisories.push({
|
|
500
|
+
kind: "duplicate-guideline",
|
|
501
|
+
text: duplicate.text,
|
|
502
|
+
words: duplicate.words,
|
|
503
|
+
guides: duplicate.guides.map(({ path }) => path),
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
guideCount: guides.length,
|
|
509
|
+
mappedGuideCount: guides.filter(({ marker }) => marker.kind === "found").length,
|
|
510
|
+
words: guides.reduce((total, guide) => total + guide.words, 0),
|
|
511
|
+
contentsWords: guides.reduce((total, guide) => total + guide.contents.words, 0),
|
|
512
|
+
guidelineWords: guides.reduce((total, guide) => total + guide.guidelines.words, 0),
|
|
513
|
+
nonblankLines: guides.reduce((total, guide) => total + guide.nonblankLines, 0),
|
|
514
|
+
guides,
|
|
515
|
+
duplicates,
|
|
516
|
+
advisories: advisories.toSorted((left, right) =>
|
|
517
|
+
advisorySeverity(right) - advisorySeverity(left)
|
|
518
|
+
|| advisoryTieBreakKey(left).localeCompare(advisoryTieBreakKey(right))),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function longGuidelinesByIdentity(
|
|
523
|
+
report: AgentGuideAuditReport,
|
|
524
|
+
): ReadonlyMap<string, AgentGuideNewLongGuideline> {
|
|
525
|
+
const guides = new Map(report.guides.map((guide) => [guide.path, guide]));
|
|
526
|
+
const guidelines = new Map<string, AgentGuideNewLongGuideline>();
|
|
527
|
+
for (const advisory of report.advisories) {
|
|
528
|
+
if (advisory.kind !== "long-guideline") continue;
|
|
529
|
+
const bullet = guides.get(advisory.path)?.guidelines.bullets.find(
|
|
530
|
+
(candidate) => candidate.line === advisory.line,
|
|
531
|
+
);
|
|
532
|
+
if (bullet === undefined) continue;
|
|
533
|
+
const identity = bullet.text;
|
|
534
|
+
const existing = guidelines.get(identity);
|
|
535
|
+
if (
|
|
536
|
+
existing === undefined
|
|
537
|
+
|| advisory.path.localeCompare(existing.path) < 0
|
|
538
|
+
|| (advisory.path === existing.path && advisory.line < existing.line)
|
|
539
|
+
) {
|
|
540
|
+
guidelines.set(identity, {
|
|
541
|
+
path: advisory.path,
|
|
542
|
+
line: advisory.line,
|
|
543
|
+
text: bullet.text,
|
|
544
|
+
words: advisory.words,
|
|
545
|
+
suggestedWords: advisory.suggestedWords,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return guidelines;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function guidelineGuideSets(
|
|
553
|
+
report: AgentGuideAuditReport,
|
|
554
|
+
): ReadonlyMap<string, readonly string[]> {
|
|
555
|
+
const guideSets = new Map<string, Set<string>>();
|
|
556
|
+
for (const guide of report.guides) {
|
|
557
|
+
for (const bullet of guide.guidelines.bullets) {
|
|
558
|
+
const paths = guideSets.get(bullet.text) ?? new Set<string>();
|
|
559
|
+
paths.add(guide.path);
|
|
560
|
+
guideSets.set(bullet.text, paths);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return new Map([...guideSets].map(([text, paths]) => [
|
|
564
|
+
text,
|
|
565
|
+
[...paths].toSorted((left, right) => left.localeCompare(right)),
|
|
566
|
+
]));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Compare two audit snapshots and return only growth introduced by the current snapshot. */
|
|
570
|
+
export function compareAgentGuideAudits(
|
|
571
|
+
base: AgentGuideAuditReport,
|
|
572
|
+
current: AgentGuideAuditReport,
|
|
573
|
+
): AgentGuideAuditComparison {
|
|
574
|
+
const baseGuides = new Map(base.guides.map((guide) => [guide.path, guide]));
|
|
575
|
+
const guideWordRegressions: AgentGuideWordRegression[] = [];
|
|
576
|
+
const inheritedWordRegressions: AgentGuideInheritedWordRegression[] = [];
|
|
577
|
+
|
|
578
|
+
for (const guide of current.guides) {
|
|
579
|
+
const baseGuide = baseGuides.get(guide.path);
|
|
580
|
+
const baseWords = baseGuide?.words ?? 0;
|
|
581
|
+
if (guide.words > baseWords) {
|
|
582
|
+
guideWordRegressions.push({
|
|
583
|
+
path: guide.path,
|
|
584
|
+
baseWords,
|
|
585
|
+
currentWords: guide.words,
|
|
586
|
+
addedWords: guide.words - baseWords,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
const baseInheritedWords = baseGuide?.inheritedWords ?? 0;
|
|
590
|
+
if (guide.inheritedWords > baseInheritedWords) {
|
|
591
|
+
inheritedWordRegressions.push({
|
|
592
|
+
path: guide.path,
|
|
593
|
+
baseWords: baseInheritedWords,
|
|
594
|
+
currentWords: guide.inheritedWords,
|
|
595
|
+
addedWords: guide.inheritedWords - baseInheritedWords,
|
|
596
|
+
guides: guide.inheritedGuidePaths,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const baseLongGuidelines = longGuidelinesByIdentity(base);
|
|
602
|
+
const newlyLongGuidelines = [...longGuidelinesByIdentity(current)]
|
|
603
|
+
.filter(([identity]) => !baseLongGuidelines.has(identity))
|
|
604
|
+
.map(([, guideline]) => guideline)
|
|
605
|
+
.toSorted((left, right) =>
|
|
606
|
+
left.path.localeCompare(right.path)
|
|
607
|
+
|| left.text.localeCompare(right.text)
|
|
608
|
+
|| left.line - right.line);
|
|
609
|
+
|
|
610
|
+
const baseGuideSets = guidelineGuideSets(base);
|
|
611
|
+
const expandedDuplicateGuidelines = [...guidelineGuideSets(current)]
|
|
612
|
+
.flatMap(([text, currentGuides]): AgentGuideExpandedDuplicate[] => {
|
|
613
|
+
if (currentGuides.length < 2) return [];
|
|
614
|
+
const baseGuidesForText = baseGuideSets.get(text) ?? [];
|
|
615
|
+
if (currentGuides.length <= baseGuidesForText.length) return [];
|
|
616
|
+
const baseSet = new Set(baseGuidesForText);
|
|
617
|
+
const addedGuides = currentGuides.filter((path) => !baseSet.has(path));
|
|
618
|
+
if (addedGuides.length === 0) return [];
|
|
619
|
+
const words = current.guides
|
|
620
|
+
.flatMap((guide) => guide.guidelines.bullets)
|
|
621
|
+
.find((bullet) => bullet.text === text)?.words ?? 0;
|
|
622
|
+
return [{
|
|
623
|
+
text,
|
|
624
|
+
words,
|
|
625
|
+
baseGuides: baseGuidesForText,
|
|
626
|
+
currentGuides,
|
|
627
|
+
addedGuides,
|
|
628
|
+
}];
|
|
629
|
+
})
|
|
630
|
+
.toSorted((left, right) => left.text.localeCompare(right.text));
|
|
631
|
+
|
|
632
|
+
return {
|
|
633
|
+
guideWordRegressions: guideWordRegressions.toSorted(guideSort),
|
|
634
|
+
inheritedWordRegressions: inheritedWordRegressions.toSorted(guideSort),
|
|
635
|
+
newlyLongGuidelines,
|
|
636
|
+
expandedDuplicateGuidelines,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function isWithin(root: string, candidate: string): boolean {
|
|
641
|
+
const fromRoot = relative(root, candidate);
|
|
642
|
+
return fromRoot === ""
|
|
643
|
+
|| (
|
|
644
|
+
fromRoot !== ".."
|
|
645
|
+
&& !fromRoot.startsWith(`..${sep}`)
|
|
646
|
+
&& !isAbsolute(fromRoot)
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Discover regular AGENTS.md files without following repository symlinks. */
|
|
651
|
+
export async function discoverAgentGuides(
|
|
652
|
+
repositoryRoot: string,
|
|
653
|
+
ignoredDirectories: ReadonlySet<string> = defaultAgentGuideIgnoredDirectories,
|
|
654
|
+
): Promise<AgentGuideDiscovery> {
|
|
655
|
+
const root = await realpath(resolve(repositoryRoot));
|
|
656
|
+
const rootMetadata = await lstat(root);
|
|
657
|
+
if (!rootMetadata.isDirectory()) throw new Error("The repository root must be a directory.");
|
|
658
|
+
const guides: AgentGuideSource[] = [];
|
|
659
|
+
const issues: AgentGuideDiscoveryIssue[] = [];
|
|
660
|
+
|
|
661
|
+
async function visit(directory: string): Promise<void> {
|
|
662
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
663
|
+
for (const entry of entries.toSorted((left, right) =>
|
|
664
|
+
left.name.localeCompare(right.name))) {
|
|
665
|
+
if (entry.isDirectory() && ignoredDirectories.has(entry.name)) continue;
|
|
666
|
+
const absolutePath = join(directory, entry.name);
|
|
667
|
+
const path = relative(root, absolutePath).split(sep).join("/");
|
|
668
|
+
if (entry.isSymbolicLink()) {
|
|
669
|
+
if (entry.name === "AGENTS.md") {
|
|
670
|
+
issues.push({
|
|
671
|
+
kind: "symlink-guide",
|
|
672
|
+
path,
|
|
673
|
+
message: `The agent guide ${path} must be a regular file, not a symbolic link.`,
|
|
674
|
+
});
|
|
675
|
+
} else {
|
|
676
|
+
try {
|
|
677
|
+
const target = await realpath(absolutePath);
|
|
678
|
+
if ((await lstat(target)).isDirectory()) {
|
|
679
|
+
issues.push({
|
|
680
|
+
kind: "symlink-directory",
|
|
681
|
+
path,
|
|
682
|
+
message: `Agent-guide discovery does not traverse the symbolic-link directory ${path}.`,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
} catch {
|
|
686
|
+
// A broken non-guide symlink is outside this audit's ownership.
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (entry.name === "AGENTS.md" && !entry.isFile()) {
|
|
692
|
+
issues.push({
|
|
693
|
+
kind: "non-regular-guide",
|
|
694
|
+
path,
|
|
695
|
+
message: `The agent guide ${path} must be a regular file.`,
|
|
696
|
+
});
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
if (entry.isDirectory()) {
|
|
700
|
+
await visit(absolutePath);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (!entry.isFile() || entry.name !== "AGENTS.md") continue;
|
|
704
|
+
const canonicalPath = await realpath(absolutePath);
|
|
705
|
+
if (!isWithin(root, canonicalPath)) {
|
|
706
|
+
issues.push({
|
|
707
|
+
kind: "symlink-guide",
|
|
708
|
+
path,
|
|
709
|
+
message: `The agent guide ${path} resolves outside the repository.`,
|
|
710
|
+
});
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const handle = await open(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
714
|
+
try {
|
|
715
|
+
const metadata = await handle.stat();
|
|
716
|
+
if (!metadata.isFile()) {
|
|
717
|
+
issues.push({
|
|
718
|
+
kind: "symlink-guide",
|
|
719
|
+
path,
|
|
720
|
+
message: `The agent guide ${path} must be a regular file.`,
|
|
721
|
+
});
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
guides.push({ path, source: await handle.readFile({ encoding: "utf8" }) });
|
|
725
|
+
} finally {
|
|
726
|
+
await handle.close();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
await visit(root);
|
|
732
|
+
return {
|
|
733
|
+
repositoryRoot: root,
|
|
734
|
+
guides: guides.toSorted((left, right) => left.path.localeCompare(right.path)),
|
|
735
|
+
issues: issues.toSorted((left, right) =>
|
|
736
|
+
left.path.localeCompare(right.path) || left.kind.localeCompare(right.kind)),
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** Discover and audit every guide in a repository. */
|
|
741
|
+
export async function auditAgentGuideRepository(
|
|
742
|
+
repositoryRoot: string,
|
|
743
|
+
options: AgentGuideAuditOptions = {},
|
|
744
|
+
): Promise<AgentGuideDiscovery & { readonly audit: AgentGuideAuditReport }> {
|
|
745
|
+
const discovery = await discoverAgentGuides(repositoryRoot);
|
|
746
|
+
return {
|
|
747
|
+
...discovery,
|
|
748
|
+
audit: auditAgentGuides(discovery.guides, options),
|
|
749
|
+
};
|
|
750
|
+
}
|