@avi2dg/checks 0.68.2 → 0.70.0
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 +16 -0
- package/docs/design.md +2 -1
- package/docs/gates/checks-docs.md +23 -3
- package/docs/how-to/set-up-a-repository.md +14 -0
- package/package.json +5 -1
- package/src/docs/doc-agents.ts +26 -3
- package/src/docs/docs.ts +62 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.70.0
|
|
6
|
+
|
|
7
|
+
Released 2026-11-07.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **docs:** require root AGENTS.md and CLAUDE.md with package.json [#58](https://github.com/avi2d/checks/pull/58)
|
|
12
|
+
|
|
13
|
+
## 0.69.0
|
|
14
|
+
|
|
15
|
+
Released 2026-11-06.
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- export git, main, test and test-report helpers [#57](https://github.com/avi2d/checks/pull/57)
|
|
20
|
+
|
|
5
21
|
## 0.68.2
|
|
6
22
|
|
|
7
23
|
Released 2026-11-06.
|
package/docs/design.md
CHANGED
|
@@ -84,12 +84,13 @@ Nothing would then say which gate a helper serves.
|
|
|
84
84
|
A mutation runner's default scope covers `src/`, so the kit's own Stryker run mutates its source with no `mutate` list.
|
|
85
85
|
The testing directory is named `testing` rather than `tests`, because a `src/tests/` beside the root `tests/` would read as a second suite.
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
Twelve `exports` keys name a path the file does not sit at, because consumers resolve them by that name.
|
|
88
88
|
`@avi2dg/checks/scripts/test-skips.ts`, `@avi2dg/checks/scripts/comment-matchers.ts` and `@avi2dg/checks/scripts/prose-matchers.ts` point at their files under `src/`.
|
|
89
89
|
`@avi2dg/checks/browser-hooks.ts` points at `src/quality/browser/hooks.ts`, whose types a product's browser hooks import.
|
|
90
90
|
`@avi2dg/checks/browser-checks.json` points at `src/quality/browser/browser-checks.json`, the starter a product copies.
|
|
91
91
|
`@avi2dg/checks/screen-sizes.ts` points at `src/quality/browser/screen-sizes.ts`, whose sizes a product imports.
|
|
92
92
|
`@avi2dg/checks/block-screens.ts` points at `src/testing/block-screens.ts`, whose screenshot suite a product's pixel tests call.
|
|
93
|
+
`@avi2dg/checks/core/git.ts`, `@avi2dg/checks/core/main.ts`, `@avi2dg/checks/testing/test.ts` and `@avi2dg/checks/testing/test-report.ts` point at their files under `src/`, whose git, entry-point, test-report and skip-judging helpers a repository's own test runner script builds on.
|
|
93
94
|
`@avi2dg/checks/templates/*` points at `dist/templates/`.
|
|
94
95
|
A path read without the resolver, such as `node_modules/@avi2dg/checks/scripts/test-skips.ts`, does not exist.
|
|
95
96
|
|
|
@@ -17,6 +17,7 @@ It fails when a living doc or an agent file names a path, link or command that d
|
|
|
17
17
|
It fails when an agent file holds more than 3,000 characters or a `## Maintaining this file` section, whatever the range touches, as [Agent files](#agent-files) says.
|
|
18
18
|
It fails when an entry in an agent file names no tracked path, link or `bun run` command, whatever the range touches.
|
|
19
19
|
It fails when a range edits a README that is over 6,000 characters or holds a table cell over 300, as [README ceilings](#readme-ceilings) says.
|
|
20
|
+
It fails when a range edits a doc file and a repository with a `package.json` is missing `AGENTS.md` or `CLAUDE.md` at its root, as [Root agent files](#root-agent-files) says.
|
|
20
21
|
It fails when a living doc or an agent file names a code span the range removed from every file outside the docs, on any line.
|
|
21
22
|
[Paths, links and commands](#paths-links-and-commands) says how each reference resolves.
|
|
22
23
|
The package ships one template per kind under `dist/templates/`, and a repository starts a new doc file by copying one:
|
|
@@ -206,6 +207,21 @@ No line in a complete same-named generated block counts toward either ceiling, f
|
|
|
206
207
|
A README fails only in a range that edits it.
|
|
207
208
|
A README the range leaves alone is listed as advisory, so the first change that edits an oversize README shrinks it.
|
|
208
209
|
|
|
210
|
+
## Root agent files
|
|
211
|
+
|
|
212
|
+
A repository with a `package.json` keeps `AGENTS.md` and `CLAUDE.md` at its root, since Claude Code reads `CLAUDE.md` and every other agent reads `AGENTS.md`.
|
|
213
|
+
The failure names the missing file and the template to copy:
|
|
214
|
+
|
|
215
|
+
- `AGENTS.md` starts from `dist/templates/agents.md`, and [Agent files](#agent-files) holds its shape.
|
|
216
|
+
- `CLAUDE.md` is `dist/templates/claude.md` word for word, the import that points Claude Code at `AGENTS.md`.
|
|
217
|
+
A symlink to `AGENTS.md` or any other text fails that template once a range touches the file.
|
|
218
|
+
|
|
219
|
+
A `package.json` anywhere in the repository asks for both files.
|
|
220
|
+
A repository with no `package.json` needs neither.
|
|
221
|
+
A missing file fails only in a range that adds, changes, renames or deletes a doc file, an ADR or the changelog included.
|
|
222
|
+
Markdown the gate does not judge, such as a changeset entry or a pull request template, does not end the advisory period.
|
|
223
|
+
Any other range lists the missing file as advisory, so the first change that edits a doc file adds both files.
|
|
224
|
+
|
|
209
225
|
## What it reads
|
|
210
226
|
|
|
211
227
|
It reads each Markdown file at the head commit, and uses its path or front matter to choose its kind.
|
|
@@ -217,6 +233,8 @@ It reads the repository's own name and its direct dependencies from the root `pa
|
|
|
217
233
|
It reads the versions the root `package.json` and `.bun-version` pin at the head commit.
|
|
218
234
|
It reads each agent file at the head commit for the ceiling, its sections and its entries, whatever the range touches.
|
|
219
235
|
It reads the root `README.md` and each `README.md` beside a tracked `package.json` at the head commit for the README ceilings.
|
|
236
|
+
It reads whether any `package.json` and the root `AGENTS.md` and `CLAUDE.md` are tracked at the head commit for the root agent files.
|
|
237
|
+
When a root agent file is missing, it reads a deleted or renamed Markdown file at the base commit to find its front matter kind.
|
|
220
238
|
A name that an installed direct dependency still holds counts as present.
|
|
221
239
|
From the working tree it reads the ignore files git reads, `node_modules/.bin`, and the directory of each direct dependency under `node_modules`.
|
|
222
240
|
|
|
@@ -234,14 +252,15 @@ With one it is that commit against its parent, or against the empty tree for a r
|
|
|
234
252
|
|
|
235
253
|
| Code | When |
|
|
236
254
|
| --- | --- |
|
|
237
|
-
| 0 | every doc file the range touches holds to its template, every line it adds to a living doc or an agent file holds to the prose rules and copies no pinned version, it adds or breaks no reference that does not resolve, every agent file holds to the ceiling and holds no `## Maintaining this file`, every entry in an agent file names a tracked path, a link or a command, every README the range edits holds to the README ceilings, and no code span a living doc or an agent file names vanished from every file outside the docs |
|
|
238
|
-
| 1 | a doc file the range touches does not hold to its template, a line the range adds to a living doc or an agent file breaks a prose rule or copies a pinned version, the range adds or breaks a reference that does not resolve, an agent file is over the ceiling or holds `## Maintaining this file`, an entry in an agent file names no tracked path, link or command, a README the range edits is over a README ceiling, or the range removes a name a living doc or an agent file still carries |
|
|
255
|
+
| 0 | every doc file the range touches holds to its template, every line it adds to a living doc or an agent file holds to the prose rules and copies no pinned version, it adds or breaks no reference that does not resolve, every agent file holds to the ceiling and holds no `## Maintaining this file`, every entry in an agent file names a tracked path, a link or a command, every README the range edits holds to the README ceilings, a range that edits a doc file leaves `AGENTS.md` and `CLAUDE.md` at the root of a repository with a `package.json`, and no code span a living doc or an agent file names vanished from every file outside the docs |
|
|
256
|
+
| 1 | a doc file the range touches does not hold to its template, a line the range adds to a living doc or an agent file breaks a prose rule or copies a pinned version, the range adds or breaks a reference that does not resolve, an agent file is over the ceiling or holds `## Maintaining this file`, an entry in an agent file names no tracked path, link or command, a README the range edits is over a README ceiling, a range that edits a doc file leaves either `AGENTS.md` or `CLAUDE.md` missing from the root in a repository with a `package.json`, or the range removes a name a living doc or an agent file still carries |
|
|
239
257
|
| 2 | a `package.json` does not decode, a ref does not resolve, or `grep` cannot read an installed direct dependency |
|
|
240
258
|
|
|
241
259
|
## Sample output
|
|
242
260
|
|
|
243
261
|
```
|
|
244
|
-
docs:
|
|
262
|
+
docs: 8 violation(s):
|
|
263
|
+
CLAUDE.md: is missing from the repository root, which a repository with a `package.json` keeps. Copy `node_modules/@avi2dg/checks/dist/templates/claude.md` to `CLAUDE.md` word for word, so Claude Code reads `AGENTS.md`
|
|
245
264
|
README.md:1: lacks `## Before you begin`
|
|
246
265
|
README.md:12: carries `;`, a semicolon. Use two sentences
|
|
247
266
|
README.md:13: carries `1.3.5`, a version .bun-version pins. Name .bun-version instead, or let a build write the line between generated markers
|
|
@@ -265,6 +284,7 @@ docs: advisory, 1 path(s), link(s) or command(s) the living docs or agent files
|
|
|
265
284
|
A repository adopts the templates as its files change, and the prose rules as its lines change, because an untouched file or line never fails those checks.
|
|
266
285
|
It reshapes its agent files when it adopts the gate, because [Agent files](#agent-files) judges each one whatever the range touches.
|
|
267
286
|
It shrinks an oversize README in the first change that edits it, because [README ceilings](#readme-ceilings) judges only a README the range edits.
|
|
287
|
+
It adds `AGENTS.md` and `CLAUDE.md` at its root in the first change that edits a doc file, because [Root agent files](#root-agent-files) judges only such a range.
|
|
268
288
|
A name the range removes fails wherever a doc still carries it, because the removal is what turned the line stale.
|
|
269
289
|
|
|
270
290
|
## Related topics
|
|
@@ -65,6 +65,20 @@ To give each tool the config it reads:
|
|
|
65
65
|
export default defineConfig({ orphans: ["^src/bin[.]ts$"] });
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
## Write the agent files
|
|
69
|
+
|
|
70
|
+
To give each agent the file it reads:
|
|
71
|
+
|
|
72
|
+
1. Copy the agent templates to the repository root:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
cp node_modules/@avi2dg/checks/dist/templates/agents.md AGENTS.md
|
|
76
|
+
cp node_modules/@avi2dg/checks/dist/templates/claude.md CLAUDE.md
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
1. Fill `AGENTS.md` with what an agent cannot infer from the code.
|
|
80
|
+
1. Keep `CLAUDE.md` word for word, so Claude Code reads `AGENTS.md`.
|
|
81
|
+
|
|
68
82
|
## Run the checks on every pull request
|
|
69
83
|
|
|
70
84
|
To run the gates in CI:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "avi2d",
|
|
@@ -152,6 +152,10 @@
|
|
|
152
152
|
"./browser-checks.json": "./src/quality/browser/browser-checks.json",
|
|
153
153
|
"./screen-sizes.ts": "./src/quality/browser/screen-sizes.ts",
|
|
154
154
|
"./block-screens.ts": "./src/testing/block-screens.ts",
|
|
155
|
+
"./core/git.ts": "./src/core/git.ts",
|
|
156
|
+
"./core/main.ts": "./src/core/main.ts",
|
|
157
|
+
"./testing/test-report.ts": "./src/testing/test-report.ts",
|
|
158
|
+
"./testing/test.ts": "./src/testing/test.ts",
|
|
155
159
|
"./oxlint": {
|
|
156
160
|
"types": "./dist/presets/oxlint.d.ts",
|
|
157
161
|
"default": "./dist/presets/oxlint.js"
|
package/src/docs/doc-agents.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Parser, type NodeType } from "commonmark";
|
|
2
|
-
import { commandNames, type Snapshot } from "./doc-references.ts";
|
|
2
|
+
import { commandNames, PACKAGE_MANIFEST, type Snapshot } from "./doc-references.ts";
|
|
3
3
|
import { handWritten } from "./doc-versions.ts";
|
|
4
4
|
import { AGENT_NAMES, scanMarkdown, type MarkdownLine } from "./prose-matchers.ts";
|
|
5
5
|
|
|
@@ -8,7 +8,6 @@ export const README_CEILING = 6000;
|
|
|
8
8
|
export const CELL_CEILING = 300;
|
|
9
9
|
|
|
10
10
|
const README = "README.md";
|
|
11
|
-
const MANIFEST = "package.json";
|
|
12
11
|
const SECOND_LEVEL = /^\s{0,3}##(?:\s+|$)(.*?)(?:\s+#+)?\s*$/;
|
|
13
12
|
const SETEXT_SECOND = /^\s{0,3}-+\s*$/;
|
|
14
13
|
const CELL_SEPARATOR = /(?<!\\)\|/;
|
|
@@ -37,7 +36,31 @@ export function ceilingFinding(text: string): AgentFinding | undefined {
|
|
|
37
36
|
|
|
38
37
|
export function isCeiledReadme(repositoryPath: string, files: ReadonlySet<string>): boolean {
|
|
39
38
|
if (repositoryPath === README) return true;
|
|
40
|
-
return repositoryPath.endsWith(`/${README}`) && files.has(`${repositoryPath.slice(0, -README.length)}${
|
|
39
|
+
return repositoryPath.endsWith(`/${README}`) && files.has(`${repositoryPath.slice(0, -README.length)}${PACKAGE_MANIFEST}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const KIT_TEMPLATES = "node_modules/@avi2dg/checks/dist/templates";
|
|
43
|
+
|
|
44
|
+
const ROOT_AGENT_FILES = [
|
|
45
|
+
{ path: "AGENTS.md", fix: `Copy \`${KIT_TEMPLATES}/agents.md\` to \`AGENTS.md\` and fill it with what an agent cannot infer from the code` },
|
|
46
|
+
{ path: "CLAUDE.md", fix: `Copy \`${KIT_TEMPLATES}/claude.md\` to \`CLAUDE.md\` word for word, so Claude Code reads \`AGENTS.md\`` },
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export type RootAgentFinding = {
|
|
50
|
+
readonly path: (typeof ROOT_AGENT_FILES)[number]["path"];
|
|
51
|
+
readonly message: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function hasManifest(files: ReadonlySet<string>): boolean {
|
|
55
|
+
return [...files].some((file) => file === PACKAGE_MANIFEST || file.endsWith(`/${PACKAGE_MANIFEST}`));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function missingAgentFiles(files: ReadonlySet<string>): readonly RootAgentFinding[] {
|
|
59
|
+
if (!hasManifest(files)) return [];
|
|
60
|
+
return ROOT_AGENT_FILES.filter(({ path }) => !files.has(path)).map(({ path, fix }) => ({
|
|
61
|
+
path,
|
|
62
|
+
message: `is missing from the repository root, which a repository with a \`package.json\` keeps. ${fix}`,
|
|
63
|
+
}));
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
type Part = { readonly title: string; readonly lines: readonly MarkdownLine[] };
|
package/src/docs/docs.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Console, Effect } from "effect";
|
|
3
|
-
import { ceilingFinding, entryFindings, isAgentFile, isCeiledReadme, maintainingFinding, readmeFindings } from "./doc-agents.ts";
|
|
4
|
-
import { rootsOf, snapshotOf, unresolvedIn, type Judging, type Unresolved } from "./doc-references.ts";
|
|
3
|
+
import { ceilingFinding, entryFindings, hasManifest, isAgentFile, isCeiledReadme, maintainingFinding, missingAgentFiles, readmeFindings, type RootAgentFinding } from "./doc-agents.ts";
|
|
4
|
+
import { rootsOf, snapshotOf, type Snapshot, unresolvedIn, type Judging, type Unresolved } from "./doc-references.ts";
|
|
5
5
|
import { ADR_DIRECTORY, judge, placementOf, placementProblem, recordsOf, speaksToConsumers, type Placement, type Records } from "./doc-rules.ts";
|
|
6
6
|
import { vanishedNames } from "./doc-names.ts";
|
|
7
7
|
import { pinsAt, versionFindings } from "./doc-versions.ts";
|
|
8
8
|
import { readTexts, snapshotAt, stillMissing } from "./doc-snapshot.ts";
|
|
9
|
-
import { changedLines, changedPaths, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
|
|
9
|
+
import { changedLines, changedPaths, git, pathsAt, pathsOf, rangeEnds, refArgs, type Change } from "../core/git.ts";
|
|
10
10
|
import { runMain } from "../core/main.ts";
|
|
11
11
|
import { proseFindings, readerOf } from "./prose-matchers.ts";
|
|
12
12
|
import { longSentences } from "./sentence-length.ts";
|
|
@@ -23,9 +23,11 @@ type Judged = {
|
|
|
23
23
|
readonly named: number;
|
|
24
24
|
readonly agents: number;
|
|
25
25
|
readonly readmes: number;
|
|
26
|
+
readonly hasManifest: boolean;
|
|
26
27
|
readonly findings: readonly Finding[];
|
|
27
28
|
readonly advisory: ReadonlyMap<string, number>;
|
|
28
29
|
readonly readmeAdvisory: readonly Finding[];
|
|
30
|
+
readonly agentFileAdvisory: readonly Finding[];
|
|
29
31
|
readonly sentenceAdvisory: readonly Finding[];
|
|
30
32
|
readonly brokenBefore: readonly Finding[];
|
|
31
33
|
};
|
|
@@ -45,6 +47,27 @@ const NAME = "docs";
|
|
|
45
47
|
const USAGE = "usage: docs.ts <ref> | <base-ref> <head-ref>";
|
|
46
48
|
const MARKDOWN = [":(glob)**/*.md"];
|
|
47
49
|
|
|
50
|
+
function agentFileFindings(path: string, text: string, tracked: Snapshot): readonly Finding[] {
|
|
51
|
+
const shaped = [ceilingFinding(text), maintainingFinding(text)].flatMap((finding) => (finding === undefined ? [] : [finding]));
|
|
52
|
+
return [...shaped, ...entryFindings(path, text, tracked)].map((finding) => ({ path, ...finding }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rangeEditsDocs = Effect.fn("rangeEditsDocs")(function* (root: string, base: string, changes: readonly Change[], text: (path: string) => string) {
|
|
56
|
+
const paths = changes.flatMap((change) => pathsOf(change));
|
|
57
|
+
if (paths.some((path) => placementOf(path, text(path)).type !== "unjudged" || readerOf(path) !== undefined)) return true;
|
|
58
|
+
const gone = [...new Set(changes.flatMap((change) => (change.kind === "deleted" ? [change.path] : change.kind === "renamed" ? [change.from] : [])))].filter(
|
|
59
|
+
(path) => readerOf(path) === undefined && placementOf(path).type === "unjudged",
|
|
60
|
+
);
|
|
61
|
+
if (gone.length === 0) return false;
|
|
62
|
+
const before = yield* readTexts(root, base, gone);
|
|
63
|
+
return gone.some((path) => placementOf(path, before.get(path) ?? "").type !== "unjudged");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function rootAgentFindings(absent: readonly RootAgentFinding[], editsDocs: boolean): { readonly failing: readonly Finding[]; readonly advisory: readonly Finding[] } {
|
|
67
|
+
const listed = absent.map((finding) => ({ ...finding, line: undefined }));
|
|
68
|
+
return editsDocs ? { failing: listed, advisory: [] } : { failing: [], advisory: listed };
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
function templateFindings(path: string, text: string, placement: Placement, records: Records): readonly Finding[] {
|
|
49
72
|
const misplaced = placementProblem(placement);
|
|
50
73
|
if (misplaced !== undefined) return [{ path, line: undefined, message: misplaced }];
|
|
@@ -114,9 +137,6 @@ const runDocs = Effect.fn("runDocs")(function* (root: string, base: string, head
|
|
|
114
137
|
const versions = edited.flatMap(({ path }) =>
|
|
115
138
|
versionFindings(text(path), pins, changed.get(path)).map(({ line, message }) => ({ path, line, message })),
|
|
116
139
|
);
|
|
117
|
-
const sentenceAdvisory = edited
|
|
118
|
-
.flatMap(({ path }) => longSentences(text(path), changed.get(path)).map(({ line, message }) => ({ path, line, message })))
|
|
119
|
-
.toSorted(inPathOrder);
|
|
120
140
|
const judging = (path: string): Judging => ({ commands: !speaksToConsumers(text(path)) });
|
|
121
141
|
// A directory the range deletes still belongs to this repository, so a path under it is stale rather than another repository's.
|
|
122
142
|
const roots = rootsOf(yield* pathsAt(base, [], root));
|
|
@@ -125,12 +145,11 @@ const runDocs = Effect.fn("runDocs")(function* (root: string, base: string, head
|
|
|
125
145
|
const vanished = yield* vanishedNames(root, base, head, referenced, references.failed);
|
|
126
146
|
const agents = proseDocs.filter(({ path }) => isAgentFile(path));
|
|
127
147
|
const tracked = snapshotOf(yield* pathsAt(head, [], root), new Map(), new Map());
|
|
128
|
-
const shapes = agents.flatMap(({ path }) =>
|
|
129
|
-
[ceilingFinding(text(path)), maintainingFinding(text(path))].flatMap((finding) => (finding === undefined ? [] : [{ path, ...finding }])),
|
|
130
|
-
);
|
|
131
|
-
const entries = agents.flatMap(({ path }) => entryFindings(path, text(path), tracked).map((finding) => ({ path, ...finding })));
|
|
148
|
+
const shapes = agents.flatMap(({ path }) => agentFileFindings(path, text(path), tracked));
|
|
132
149
|
const readmes = proseDocs.filter(({ path }) => isCeiledReadme(path, tracked.files));
|
|
133
150
|
const oversize = readmes.flatMap(({ path }) => readmeFindings(text(path)).map((finding) => ({ path, ...finding })));
|
|
151
|
+
const absent = missingAgentFiles(tracked.files);
|
|
152
|
+
const rootAgents = rootAgentFindings(absent, absent.length === 0 ? false : yield* rangeEditsDocs(root, base, changes, text));
|
|
134
153
|
const advisory = new Map<string, number>();
|
|
135
154
|
for (const { path } of templated.filter((finding) => !touched.has(finding.path))) advisory.set(path, (advisory.get(path) ?? 0) + 1);
|
|
136
155
|
return {
|
|
@@ -139,6 +158,7 @@ const runDocs = Effect.fn("runDocs")(function* (root: string, base: string, head
|
|
|
139
158
|
named: referenced.size,
|
|
140
159
|
agents: agents.length,
|
|
141
160
|
readmes: readmes.filter(({ path }) => touched.has(path)).length,
|
|
161
|
+
hasManifest: hasManifest(tracked.files),
|
|
142
162
|
findings: [
|
|
143
163
|
...templated.filter((finding) => touched.has(finding.path)),
|
|
144
164
|
...prose,
|
|
@@ -146,12 +166,15 @@ const runDocs = Effect.fn("runDocs")(function* (root: string, base: string, head
|
|
|
146
166
|
...references.failing,
|
|
147
167
|
...vanished,
|
|
148
168
|
...shapes,
|
|
149
|
-
...entries,
|
|
150
169
|
...oversize.filter((finding) => touched.has(finding.path)),
|
|
170
|
+
...rootAgents.failing,
|
|
151
171
|
].toSorted(inPathOrder),
|
|
152
172
|
advisory,
|
|
153
173
|
readmeAdvisory: oversize.filter((finding) => !touched.has(finding.path)).toSorted(inPathOrder),
|
|
154
|
-
|
|
174
|
+
agentFileAdvisory: rootAgents.advisory,
|
|
175
|
+
sentenceAdvisory: edited
|
|
176
|
+
.flatMap(({ path }) => longSentences(text(path), changed.get(path)).map(({ line, message }) => ({ path, line, message })))
|
|
177
|
+
.toSorted(inPathOrder),
|
|
155
178
|
brokenBefore: references.brokenBefore.toSorted(inPathOrder),
|
|
156
179
|
} satisfies Judged;
|
|
157
180
|
});
|
|
@@ -160,7 +183,24 @@ function describe({ path, line, message }: Finding): string {
|
|
|
160
183
|
return ` ${path}${line === undefined ? "" : `:${line}`}: ${message}`;
|
|
161
184
|
}
|
|
162
185
|
|
|
163
|
-
export function report({
|
|
186
|
+
export function report({
|
|
187
|
+
held,
|
|
188
|
+
edited,
|
|
189
|
+
named,
|
|
190
|
+
agents,
|
|
191
|
+
readmes,
|
|
192
|
+
hasManifest: manifestHeld,
|
|
193
|
+
findings,
|
|
194
|
+
advisory,
|
|
195
|
+
readmeAdvisory,
|
|
196
|
+
agentFileAdvisory,
|
|
197
|
+
sentenceAdvisory,
|
|
198
|
+
brokenBefore,
|
|
199
|
+
}: Judged): string {
|
|
200
|
+
const rootAgentFiles =
|
|
201
|
+
manifestHeld && agentFileAdvisory.length === 0
|
|
202
|
+
? [`${NAME}: the repository root holds the AGENTS.md and CLAUDE.md a repository with a \`package.json\` keeps`]
|
|
203
|
+
: [];
|
|
164
204
|
const verdict =
|
|
165
205
|
findings.length === 0
|
|
166
206
|
? [
|
|
@@ -170,6 +210,7 @@ export function report({ held, edited, named, agents, readmes, findings, advisor
|
|
|
170
210
|
`${NAME}: the range breaks no path, link or command the ${named} living doc(s) or agent file(s) name`,
|
|
171
211
|
`${NAME}: the ${agents} agent file(s) hold to the ceiling, and every entry names a tracked path, a link or a command`,
|
|
172
212
|
`${NAME}: the ${readmes} README(s) the range edits hold to the 6,000-character ceiling and the 300-character table cell ceiling`,
|
|
213
|
+
...rootAgentFiles,
|
|
173
214
|
]
|
|
174
215
|
: [`${NAME}: ${findings.length} violation(s):`, ...findings.map(describe)];
|
|
175
216
|
const unconformed =
|
|
@@ -183,6 +224,13 @@ export function report({ held, edited, named, agents, readmes, findings, advisor
|
|
|
183
224
|
readmeAdvisory.length === 0
|
|
184
225
|
? []
|
|
185
226
|
: [`${NAME}: advisory, ${readmeAdvisory.length} README ceiling finding(s) in README(s) the range leaves alone:`, ...readmeAdvisory.map(describe)];
|
|
227
|
+
const missing =
|
|
228
|
+
agentFileAdvisory.length === 0
|
|
229
|
+
? []
|
|
230
|
+
: [
|
|
231
|
+
`${NAME}: advisory, ${agentFileAdvisory.length} agent file(s) missing from the repository root, which the first range that edits a doc adds:`,
|
|
232
|
+
...agentFileAdvisory.map(describe),
|
|
233
|
+
];
|
|
186
234
|
const sentences =
|
|
187
235
|
sentenceAdvisory.length === 0
|
|
188
236
|
? []
|
|
@@ -197,7 +245,7 @@ export function report({ held, edited, named, agents, readmes, findings, advisor
|
|
|
197
245
|
`${NAME}: advisory, ${brokenBefore.length} path(s), link(s) or command(s) the living docs or agent files name were broken before the range:`,
|
|
198
246
|
...brokenBefore.map(describe),
|
|
199
247
|
];
|
|
200
|
-
return [...verdict, ...unconformed, ...oversize, ...sentences, ...broken].join("\n");
|
|
248
|
+
return [...verdict, ...unconformed, ...oversize, ...missing, ...sentences, ...broken].join("\n");
|
|
201
249
|
}
|
|
202
250
|
|
|
203
251
|
const docs = Effect.gen(function* () {
|