-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcheck-docs.mjs
More file actions
137 lines (118 loc) · 4.21 KB
/
check-docs.mjs
File metadata and controls
137 lines (118 loc) · 4.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import path from "node:path";
import { collectFiles, parseMetadata, readText } from "./lib/doc-metadata.mjs";
import { findWorkspaceAbsolutePathMatches } from "./lib/doc-link-hygiene.mjs";
const rootDirectory = process.cwd();
const today = new Date();
const freshnessWindowDays = 180;
const requiredFiles = [
"AGENTS.md",
"ARCHITECTURE.md",
"README.md",
"docs/DESIGN.md",
"docs/FRONTEND.md",
"docs/PLANS.md",
"docs/PRODUCT_SENSE.md",
"docs/QUALITY_SCORE.md",
"docs/RELIABILITY.md",
"docs/SECURITY.md",
"docs/design-docs/index.md",
"docs/design-docs/core-beliefs.md",
"docs/product-specs/index.md",
"docs/product-specs/work-items-demo.md",
"docs/exec-plans/active/README.md",
"docs/exec-plans/completed/harness-template-bootstrap.md",
"docs/exec-plans/tech-debt-tracker.md",
"docs/generated/README.md",
"docs/generated/domain-schema.md"
];
const metadataFiles = requiredFiles.filter((filePath) => filePath.endsWith(".md"));
const errors = [];
for (const requiredFile of requiredFiles) {
const absolutePath = path.join(rootDirectory, requiredFile);
try {
await readText(absolutePath);
} catch {
errors.push(`Missing required file: ${requiredFile}`);
}
}
for (const filePath of metadataFiles) {
const absolutePath = path.join(rootDirectory, filePath);
let contents = "";
try {
contents = await readText(absolutePath);
} catch {
continue;
}
const metadata = parseMetadata(contents);
for (const [key, value] of Object.entries(metadata)) {
if (!value) {
errors.push(`${filePath}: missing metadata field ${key}`);
}
}
if (metadata.updated) {
const updatedAt = new Date(metadata.updated);
const ageMs = today.getTime() - updatedAt.getTime();
const ageDays = ageMs / (24 * 60 * 60 * 1000);
if (Number.isNaN(updatedAt.getTime())) {
errors.push(`${filePath}: Updated must be a valid YYYY-MM-DD date.`);
} else if (updatedAt > today) {
errors.push(`${filePath}: Updated cannot be in the future.`);
} else if (ageDays > freshnessWindowDays) {
errors.push(
`${filePath}: Updated is older than ${freshnessWindowDays} days. Refresh or archive the document.`
);
}
}
}
const agentsContents = await readText(path.join(rootDirectory, "AGENTS.md"));
if (agentsContents.split("\n").length > 140) {
errors.push("AGENTS.md: keep the file small enough to act as a map.");
}
if (!agentsContents.includes("docs/PLANS.md")) {
errors.push("AGENTS.md: must link to docs/PLANS.md.");
}
const designIndex = await readText(path.join(rootDirectory, "docs/design-docs/index.md"));
if (!designIndex.includes("core-beliefs.md")) {
errors.push("docs/design-docs/index.md: must link to core-beliefs.md.");
}
const productIndex = await readText(path.join(rootDirectory, "docs/product-specs/index.md"));
if (!productIndex.includes("work-items-demo.md")) {
errors.push("docs/product-specs/index.md: must link to work-items-demo.md.");
}
const plansContents = await readText(
path.join(rootDirectory, "docs/exec-plans/completed/harness-template-bootstrap.md")
);
for (const section of [
"## Progress",
"## Surprises & Discoveries",
"## Decision Log",
"## Outcomes & Retrospective"
]) {
if (!plansContents.includes(section)) {
errors.push(`Completed ExecPlan is missing required section: ${section}`);
}
}
const docFiles = await collectFiles(path.join(rootDirectory, "docs"), (filePath) => filePath.endsWith(".md"));
if (docFiles.length < 10) {
errors.push("docs/: expected a richer system of record, found too few documents.");
}
const rootMarkdownFiles = ["AGENTS.md", "ARCHITECTURE.md", "README.md", "README.en.md"].map((filePath) =>
path.join(rootDirectory, filePath)
);
for (const filePath of [...docFiles, ...rootMarkdownFiles]) {
const contents = await readText(filePath);
const matches = findWorkspaceAbsolutePathMatches(contents);
if (matches.length > 0) {
errors.push(
`${path.relative(rootDirectory, filePath)}: replace workspace-specific absolute paths with relative links. Found ${matches.join(", ")}`
);
}
}
if (errors.length > 0) {
console.error("Documentation check failed:");
for (const error of errors) {
console.error(`- ${error}`);
}
process.exit(1);
}
console.log("Documentation check passed.");