@pieai/doc-gov 0.9.5 → 0.9.7
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/cli-guide.md +3 -1
- package/dist/cli.js +387 -198
- package/package.json +1 -1
package/cli-guide.md
CHANGED
|
@@ -62,7 +62,9 @@ pnpm doc-gov migrate --profile doc-only --check
|
|
|
62
62
|
- It also checks that local paths written in backticks inside router-facing
|
|
63
63
|
files such as `AGENTS.md`, `CLAUDE.md`, `README.md`, and the starter
|
|
64
64
|
`AGENTS.template.md` can actually be opened, which catches stale startup
|
|
65
|
-
instructions early.
|
|
65
|
+
instructions early. An exact PGS-managed shared-rule symlink remains valid
|
|
66
|
+
when its private sibling checkout is unavailable in standalone CI; ordinary
|
|
67
|
+
dangling or noncanonical links still fail.
|
|
66
68
|
|
|
67
69
|
It does not choose a workflow for a specific task.
|
|
68
70
|
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/commands/approve.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5
|
+
import { join as join6 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/core/frontmatter.ts
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
@@ -65,8 +65,7 @@ function parseScalar(value) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// src/core/files.ts
|
|
68
|
-
import {
|
|
69
|
-
import { join, relative } from "node:path";
|
|
68
|
+
import { join as join2, relative as relative2 } from "node:path";
|
|
70
69
|
|
|
71
70
|
// src/core/external-artifacts.ts
|
|
72
71
|
var EXTERNAL_ARTIFACT_PREFIXES = ["docs/brainstorms/", "docs/pulse-reports/"];
|
|
@@ -80,35 +79,251 @@ function isExternalArtifactPath(repoPath) {
|
|
|
80
79
|
return false;
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
// src/core/
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
// src/core/markdown-traversal.ts
|
|
83
|
+
import {
|
|
84
|
+
existsSync,
|
|
85
|
+
lstatSync,
|
|
86
|
+
readFileSync as readFileSync2,
|
|
87
|
+
readdirSync,
|
|
88
|
+
realpathSync,
|
|
89
|
+
statSync
|
|
90
|
+
} from "node:fs";
|
|
91
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
92
|
+
function listMarkdownFiles(rootDir, roots, options = {}) {
|
|
93
|
+
const resolvedRootDir = resolve(rootDir);
|
|
94
|
+
const allowedRoots = canonicalizeRoots(resolvedRootDir, options.allowedRoots ?? []);
|
|
86
95
|
const files = [];
|
|
96
|
+
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
87
97
|
for (const root of roots) {
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
walkEntry(resolveFromRoot(resolvedRootDir, root));
|
|
99
|
+
}
|
|
100
|
+
return Array.from(new Set(files)).sort();
|
|
101
|
+
function walkEntry(path) {
|
|
102
|
+
const repoPath = toRepoPath(resolvedRootDir, path);
|
|
103
|
+
if (options.shouldSkipTree?.(repoPath)) return;
|
|
104
|
+
const stat = safeLstat(path);
|
|
105
|
+
if (!stat) return;
|
|
106
|
+
if (stat.isDirectory()) {
|
|
107
|
+
walkDirectory(path);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!stat.isFile() && !stat.isSymbolicLink()) return;
|
|
111
|
+
addMarkdownFile(path, repoPath, stat);
|
|
112
|
+
}
|
|
113
|
+
function walkDirectory(dir) {
|
|
114
|
+
const realDirectory = safeRealpath(dir);
|
|
115
|
+
if (!realDirectory || visitedDirectories.has(realDirectory)) return;
|
|
116
|
+
visitedDirectories.add(realDirectory);
|
|
117
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
118
|
+
const path = join(dir, entry.name);
|
|
119
|
+
const repoPath = toRepoPath(resolvedRootDir, path);
|
|
120
|
+
if (options.shouldSkipTree?.(repoPath)) continue;
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
walkDirectory(path);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (entry.isFile() || entry.isSymbolicLink()) {
|
|
126
|
+
addMarkdownFile(path, repoPath, entry);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
90
129
|
}
|
|
91
|
-
|
|
130
|
+
function addMarkdownFile(path, repoPath, entry) {
|
|
131
|
+
if (!path.endsWith(".md")) return;
|
|
132
|
+
if (options.includeFile && !options.includeFile(repoPath)) return;
|
|
133
|
+
if (!isAllowedMarkdownFile(resolvedRootDir, path, allowedRoots, repoPath, entry)) return;
|
|
134
|
+
files.push(path);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function canonicalizeRoots(rootDir, roots) {
|
|
138
|
+
return Array.from(
|
|
139
|
+
new Set(
|
|
140
|
+
[rootDir, ...roots].map((root) => {
|
|
141
|
+
const resolved = resolveFromRoot(rootDir, root);
|
|
142
|
+
return safeRealpath(resolved) ?? resolved;
|
|
143
|
+
}).filter(Boolean)
|
|
144
|
+
)
|
|
145
|
+
);
|
|
92
146
|
}
|
|
93
|
-
function
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
147
|
+
function isAllowedMarkdownFile(rootDir, path, allowedRoots, repoPath, entry) {
|
|
148
|
+
const targetPath = safeRealpath(path);
|
|
149
|
+
if (!targetPath) return false;
|
|
150
|
+
const targetStat = safeStat(targetPath);
|
|
151
|
+
if (!targetStat?.isFile()) return false;
|
|
152
|
+
if (allowedRoots.some((root) => isPathWithin(targetPath, root))) return true;
|
|
153
|
+
return entry.isSymbolicLink() && isTrustedPgsSharedRuleTarget(rootDir, repoPath, targetPath);
|
|
154
|
+
}
|
|
155
|
+
function isTrustedPgsSharedRuleTarget(rootDir, repoPath, targetPath) {
|
|
156
|
+
if (!isSharedRulePath(repoPath)) return false;
|
|
157
|
+
const centralRoot = findCentralRepositoryRoot(dirname(targetPath));
|
|
158
|
+
if (!centralRoot) return false;
|
|
159
|
+
const sourceRoot = safeRealpath(join(centralRoot, "agent-assets/rules/pie-rules"));
|
|
160
|
+
return Boolean(sourceRoot && isPathWithin(targetPath, sourceRoot));
|
|
161
|
+
}
|
|
162
|
+
function isSharedRulePath(repoPath) {
|
|
163
|
+
return repoPath.startsWith("docs/policy/shared-rules/") && repoPath.endsWith(".md");
|
|
164
|
+
}
|
|
165
|
+
function findCentralRepositoryRoot(startDir) {
|
|
166
|
+
let current = startDir;
|
|
167
|
+
while (true) {
|
|
168
|
+
if (isCentralRepositoryRoot(current)) return safeRealpath(current) ?? current;
|
|
169
|
+
const parent = dirname(current);
|
|
170
|
+
if (parent === current) return void 0;
|
|
171
|
+
current = parent;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function isCentralRepositoryRoot(path) {
|
|
175
|
+
const packageJsonPath = join(path, "package.json");
|
|
176
|
+
if (!existsSync(packageJsonPath)) return false;
|
|
177
|
+
try {
|
|
178
|
+
const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
|
|
179
|
+
if (packageJson.name !== "project-governance-system" && packageJson.name !== "pro-gov") {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
103
184
|
}
|
|
104
|
-
return
|
|
185
|
+
return existsSync(join(path, "profiles")) && existsSync(join(path, "starter")) && existsSync(join(path, "integrations"));
|
|
105
186
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
187
|
+
function isPathWithin(candidate, root) {
|
|
188
|
+
const relation = relative(root, candidate);
|
|
189
|
+
return relation === "" || !relation.startsWith(`..${sep}`) && relation !== ".." && !isAbsolute(relation);
|
|
190
|
+
}
|
|
191
|
+
function resolveFromRoot(rootDir, path) {
|
|
192
|
+
if (isAbsolute(path)) return path;
|
|
193
|
+
const resolvedRoot = resolve(rootDir);
|
|
194
|
+
const resolvedPath = resolve(path);
|
|
195
|
+
return isPathWithin(resolvedPath, resolvedRoot) ? resolvedPath : resolve(resolvedRoot, path);
|
|
108
196
|
}
|
|
109
197
|
function toRepoPath(rootDir, absolutePath) {
|
|
110
198
|
return relative(rootDir, absolutePath).split(/\\/g).join("/");
|
|
111
199
|
}
|
|
200
|
+
function safeLstat(path) {
|
|
201
|
+
try {
|
|
202
|
+
return lstatSync(path);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const code = error.code;
|
|
205
|
+
if (code === "ENOENT" || code === "ENOTDIR") return void 0;
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function safeStat(path) {
|
|
210
|
+
try {
|
|
211
|
+
return statSync(path);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const code = error.code;
|
|
214
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP") return void 0;
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function safeRealpath(path) {
|
|
219
|
+
try {
|
|
220
|
+
return realpathSync(path);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const code = error.code;
|
|
223
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP") return void 0;
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/core/files.ts
|
|
229
|
+
function listGovernedMarkdownFiles(rootDir) {
|
|
230
|
+
return listMarkdownFiles(rootDir, ["docs"], {
|
|
231
|
+
shouldSkipTree: shouldSkip
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function shouldSkip(repoPath) {
|
|
235
|
+
return isExternalArtifactPath(repoPath) || repoPath.startsWith("docs/governance/templates/") || repoPath === "docs/governance/MANIFEST.yml";
|
|
236
|
+
}
|
|
237
|
+
function toRepoPath2(rootDir, absolutePath) {
|
|
238
|
+
return relative2(rootDir, absolutePath).split(/\\/g).join("/");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/core/managed-shared-rules.ts
|
|
242
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync3, readlinkSync } from "node:fs";
|
|
243
|
+
import { join as join3, relative as relative3, resolve as resolve2 } from "node:path";
|
|
244
|
+
|
|
245
|
+
// src/core/symlinks.ts
|
|
246
|
+
function normalizeSymlinkTarget(target) {
|
|
247
|
+
return target.replaceAll("\\", "/");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/core/managed-shared-rules.ts
|
|
251
|
+
var SHARED_RULE_PATH_PREFIX = "docs/policy/shared-rules/";
|
|
252
|
+
var PGS_SHARED_RULE_TARGET_PREFIX = "../../../../ProjectGovernanceSystem/agent-assets/rules/pie-rules/";
|
|
253
|
+
function isUnresolvedManagedSharedRuleSymlink(rootDir, candidatePath) {
|
|
254
|
+
const repoPath = toRepoPath3(rootDir, candidatePath);
|
|
255
|
+
const filename = sharedRuleFilename(repoPath);
|
|
256
|
+
if (!filename) return false;
|
|
257
|
+
let stats;
|
|
258
|
+
try {
|
|
259
|
+
stats = lstatSync2(candidatePath);
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
if (!stats.isSymbolicLink() || existsSync2(candidatePath)) return false;
|
|
264
|
+
try {
|
|
265
|
+
return normalizeSymlinkTarget(readlinkSync(candidatePath)) === `${PGS_SHARED_RULE_TARGET_PREFIX}${filename}`;
|
|
266
|
+
} catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function findUnavailableManagedSharedRuleEntries(rootDir) {
|
|
271
|
+
const manifestEntries = readManifestEntries(rootDir);
|
|
272
|
+
return manifestEntries.filter(
|
|
273
|
+
(entry) => entry.type === "policy" && isUnresolvedManagedSharedRuleSymlink(rootDir, join3(rootDir, entry.path))
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
function sharedRuleFilename(repoPath) {
|
|
277
|
+
if (!repoPath.startsWith(SHARED_RULE_PATH_PREFIX)) return void 0;
|
|
278
|
+
const filename = repoPath.slice(SHARED_RULE_PATH_PREFIX.length);
|
|
279
|
+
if (!filename || filename.includes("/") || !filename.endsWith(".md")) return void 0;
|
|
280
|
+
return filename;
|
|
281
|
+
}
|
|
282
|
+
function readManifestEntries(rootDir) {
|
|
283
|
+
const manifestPath = join3(rootDir, "docs/governance/MANIFEST.yml");
|
|
284
|
+
if (!existsSync2(manifestPath)) return [];
|
|
285
|
+
try {
|
|
286
|
+
const lines = readFileSync3(manifestPath, "utf8").split("\n");
|
|
287
|
+
const entries = [];
|
|
288
|
+
let current;
|
|
289
|
+
const flush = () => {
|
|
290
|
+
if (current && typeof current.id === "string" && typeof current.path === "string" && typeof current.type === "string" && typeof current.status === "string" && typeof current.canonical === "boolean" && typeof current.lastReviewed === "string" && typeof current.pinned === "boolean") {
|
|
291
|
+
entries.push(current);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
for (const line of lines) {
|
|
295
|
+
const idMatch = line.match(/^\s+- id:\s*(.+?)\s*$/);
|
|
296
|
+
if (idMatch) {
|
|
297
|
+
flush();
|
|
298
|
+
current = { id: cleanValue(idMatch[1] ?? "") };
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (!current) continue;
|
|
302
|
+
const fieldMatch = line.match(
|
|
303
|
+
/^\s+(path|type|status|canonical|last_reviewed|pinned):\s*(.*?)\s*$/
|
|
304
|
+
);
|
|
305
|
+
if (!fieldMatch) continue;
|
|
306
|
+
const field = fieldMatch[1];
|
|
307
|
+
const value = cleanValue(fieldMatch[2] ?? "");
|
|
308
|
+
if (field === "path") current.path = value;
|
|
309
|
+
if (field === "type") current.type = value;
|
|
310
|
+
if (field === "status") current.status = value;
|
|
311
|
+
if (field === "canonical") current.canonical = value === "true";
|
|
312
|
+
if (field === "last_reviewed") current.lastReviewed = value;
|
|
313
|
+
if (field === "pinned") current.pinned = value === "true";
|
|
314
|
+
}
|
|
315
|
+
flush();
|
|
316
|
+
return entries;
|
|
317
|
+
} catch {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function cleanValue(value) {
|
|
322
|
+
return value.replace(/^(['"])(.*)\1$/, "$2");
|
|
323
|
+
}
|
|
324
|
+
function toRepoPath3(rootDir, candidatePath) {
|
|
325
|
+
return relative3(resolve2(rootDir), resolve2(candidatePath)).split(/\\/g).join("/");
|
|
326
|
+
}
|
|
112
327
|
|
|
113
328
|
// src/core/lifecycle.ts
|
|
114
329
|
var normalStatuses = [
|
|
@@ -132,7 +347,7 @@ var docTypes = [
|
|
|
132
347
|
"archive"
|
|
133
348
|
];
|
|
134
349
|
function validateFrontmatter(rootDir, file) {
|
|
135
|
-
const path =
|
|
350
|
+
const path = toRepoPath2(rootDir, file.path);
|
|
136
351
|
const issues = [];
|
|
137
352
|
const data = file.data;
|
|
138
353
|
const id = stringValue(data.id);
|
|
@@ -277,6 +492,24 @@ function checkDocs(rootDir = process.cwd()) {
|
|
|
277
492
|
issues.push(...result.issues);
|
|
278
493
|
if (result.record) records.push(result.record);
|
|
279
494
|
}
|
|
495
|
+
for (const entry of findUnavailableManagedSharedRuleEntries(rootDir)) {
|
|
496
|
+
records.push({
|
|
497
|
+
id: entry.id,
|
|
498
|
+
title: entry.id,
|
|
499
|
+
type: entry.type,
|
|
500
|
+
status: entry.status,
|
|
501
|
+
canonical: entry.canonical,
|
|
502
|
+
owner: "external-shared-rule",
|
|
503
|
+
created: entry.lastReviewed,
|
|
504
|
+
lastReviewed: entry.lastReviewed,
|
|
505
|
+
domain: "shared-rule",
|
|
506
|
+
tags: ["shared-rule"],
|
|
507
|
+
pinned: entry.pinned,
|
|
508
|
+
related: [],
|
|
509
|
+
supersedes: [],
|
|
510
|
+
path: entry.path
|
|
511
|
+
});
|
|
512
|
+
}
|
|
280
513
|
issues.push(...validateGlobalIntegrity(records));
|
|
281
514
|
return {
|
|
282
515
|
ok: issues.length === 0,
|
|
@@ -382,8 +615,8 @@ function pathToRepo(rootDir, path) {
|
|
|
382
615
|
}
|
|
383
616
|
|
|
384
617
|
// src/core/paths.ts
|
|
385
|
-
import { existsSync as
|
|
386
|
-
import { join as
|
|
618
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
619
|
+
import { basename, join as join4 } from "node:path";
|
|
387
620
|
function planPath(rootDir, type, slugInput) {
|
|
388
621
|
const cleanSlug = slugInput.replace(/^\/+|\/+$/g, "");
|
|
389
622
|
if (!cleanSlug) throw new Error("Slug is required.");
|
|
@@ -452,22 +685,18 @@ function planPath(rootDir, type, slugInput) {
|
|
|
452
685
|
throw new Error(`Unknown type: ${type}`);
|
|
453
686
|
}
|
|
454
687
|
function nextSerial(rootDir, scanDir, regex) {
|
|
455
|
-
const root =
|
|
456
|
-
if (!
|
|
688
|
+
const root = join4(rootDir, scanDir);
|
|
689
|
+
if (!existsSync3(root)) return "0001";
|
|
457
690
|
let max = 0;
|
|
458
|
-
walkSerial(
|
|
691
|
+
walkSerial(rootDir, scanDir, regex, (n) => {
|
|
459
692
|
if (n > max) max = n;
|
|
460
693
|
});
|
|
461
694
|
return String(max + 1).padStart(4, "0");
|
|
462
695
|
}
|
|
463
|
-
function walkSerial(dir, regex, onMatch) {
|
|
464
|
-
for (const
|
|
465
|
-
const
|
|
466
|
-
if (
|
|
467
|
-
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
468
|
-
const m = entry.name.match(regex);
|
|
469
|
-
if (m && m[1]) onMatch(parseInt(m[1], 10));
|
|
470
|
-
}
|
|
696
|
+
function walkSerial(rootDir, dir, regex, onMatch) {
|
|
697
|
+
for (const path of listMarkdownFiles(rootDir, [dir])) {
|
|
698
|
+
const m = basename(path).match(regex);
|
|
699
|
+
if (m && m[1]) onMatch(parseInt(m[1], 10));
|
|
471
700
|
}
|
|
472
701
|
}
|
|
473
702
|
function isKebabCase(slug) {
|
|
@@ -486,8 +715,8 @@ function todayIso() {
|
|
|
486
715
|
}
|
|
487
716
|
|
|
488
717
|
// src/core/manifest.ts
|
|
489
|
-
import { existsSync as
|
|
490
|
-
import { dirname, join as
|
|
718
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
719
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
491
720
|
function buildManifest(rootDir = process.cwd()) {
|
|
492
721
|
const result = checkDocs(rootDir);
|
|
493
722
|
if (!result.ok) {
|
|
@@ -499,14 +728,14 @@ ${error}`);
|
|
|
499
728
|
}
|
|
500
729
|
function writeManifest(rootDir = process.cwd()) {
|
|
501
730
|
const manifest = buildManifest(rootDir);
|
|
502
|
-
const path =
|
|
503
|
-
mkdirSync(
|
|
731
|
+
const path = join5(rootDir, "docs/governance/MANIFEST.yml");
|
|
732
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
504
733
|
writeFileSync(path, manifest);
|
|
505
734
|
}
|
|
506
735
|
function manifestInSync(rootDir = process.cwd()) {
|
|
507
|
-
const path =
|
|
508
|
-
if (!
|
|
509
|
-
return normalizeManifest(
|
|
736
|
+
const path = join5(rootDir, "docs/governance/MANIFEST.yml");
|
|
737
|
+
if (!existsSync4(path)) return false;
|
|
738
|
+
return normalizeManifest(readFileSync4(path, "utf8")) === normalizeManifest(buildManifest(rootDir));
|
|
510
739
|
}
|
|
511
740
|
function normalizeManifest(value) {
|
|
512
741
|
return value.replace(/^generated_at: .*$/m, "generated_at: <ignored>").replace(/^generator_version: .*$/m, "generator_version: <ignored>");
|
|
@@ -535,7 +764,7 @@ function renderManifest(records) {
|
|
|
535
764
|
function readPackageVersion() {
|
|
536
765
|
for (const candidate of ["../package.json", "../../package.json"]) {
|
|
537
766
|
try {
|
|
538
|
-
const packageJson = JSON.parse(
|
|
767
|
+
const packageJson = JSON.parse(readFileSync4(new URL(candidate, import.meta.url), "utf8"));
|
|
539
768
|
if (typeof packageJson.version === "string") return packageJson.version;
|
|
540
769
|
} catch {
|
|
541
770
|
}
|
|
@@ -571,8 +800,8 @@ function runApprove(args2) {
|
|
|
571
800
|
);
|
|
572
801
|
return 1;
|
|
573
802
|
}
|
|
574
|
-
const filePath =
|
|
575
|
-
const content =
|
|
803
|
+
const filePath = join6(root, record.path);
|
|
804
|
+
const content = readFileSync5(filePath, "utf8");
|
|
576
805
|
let next = content;
|
|
577
806
|
next = updateFrontmatterField(next, "status", toStatus);
|
|
578
807
|
next = updateFrontmatterField(next, "canonical", "true");
|
|
@@ -618,8 +847,8 @@ ${newLines.join("\n")}${tail}`;
|
|
|
618
847
|
|
|
619
848
|
// src/commands/archive.ts
|
|
620
849
|
import { execFileSync } from "node:child_process";
|
|
621
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
622
|
-
import { basename, dirname as
|
|
850
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
851
|
+
import { basename as basename2, dirname as dirname3, join as join7 } from "node:path";
|
|
623
852
|
function runArchive(args2) {
|
|
624
853
|
const positional = args2.filter((a) => !a.startsWith("--"));
|
|
625
854
|
const id = positional[0];
|
|
@@ -655,18 +884,18 @@ function runArchive(args2) {
|
|
|
655
884
|
return 1;
|
|
656
885
|
}
|
|
657
886
|
const oldPath = record.path;
|
|
658
|
-
const fileName =
|
|
887
|
+
const fileName = basename2(oldPath);
|
|
659
888
|
const archiveDir = `docs/archive/${quarterTag()}-${record.type}`;
|
|
660
889
|
const newPath = `${archiveDir}/${fileName}`;
|
|
661
|
-
const absNew =
|
|
662
|
-
const absOld =
|
|
663
|
-
let content =
|
|
890
|
+
const absNew = join7(root, newPath);
|
|
891
|
+
const absOld = join7(root, oldPath);
|
|
892
|
+
let content = readFileSync6(absOld, "utf8");
|
|
664
893
|
content = updateFrontmatterField(content, "type", "archive");
|
|
665
894
|
content = updateFrontmatterField(content, "status", "archived");
|
|
666
895
|
content = updateFrontmatterField(content, "canonical", "false");
|
|
667
896
|
content = updateFrontmatterField(content, "last_reviewed", todayIso());
|
|
668
897
|
content = updateFrontmatterField(content, "archive_reason", reason);
|
|
669
|
-
mkdirSync2(
|
|
898
|
+
mkdirSync2(dirname3(absNew), { recursive: true });
|
|
670
899
|
let movedByGit = false;
|
|
671
900
|
try {
|
|
672
901
|
execFileSync("git", ["mv", oldPath, newPath], { cwd: root, stdio: "ignore" });
|
|
@@ -689,12 +918,12 @@ function runArchive(args2) {
|
|
|
689
918
|
}
|
|
690
919
|
|
|
691
920
|
// src/commands/audit.ts
|
|
692
|
-
import { existsSync as
|
|
693
|
-
import { join as
|
|
921
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2 } from "node:fs";
|
|
922
|
+
import { join as join8 } from "node:path";
|
|
694
923
|
|
|
695
924
|
// src/core/link-checker.ts
|
|
696
|
-
import { existsSync as
|
|
697
|
-
import { dirname as
|
|
925
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
|
|
926
|
+
import { dirname as dirname4, extname, resolve as resolve3 } from "node:path";
|
|
698
927
|
var CURRENT_MARKDOWN_ROOTS = ["AGENTS.md", "README.md", "docs"];
|
|
699
928
|
var CURRENT_DOC_DIR_PREFIXES = [
|
|
700
929
|
"docs/canon/",
|
|
@@ -719,7 +948,7 @@ function checkCurrentMarkdownLinks(rootDir = process.cwd()) {
|
|
|
719
948
|
if (!target || shouldIgnoreTarget(target)) continue;
|
|
720
949
|
checkedLinks += 1;
|
|
721
950
|
if (!localTargetExists(rootDir, filePath, target)) {
|
|
722
|
-
const file =
|
|
951
|
+
const file = toRepoPath2(rootDir, filePath);
|
|
723
952
|
const line = lineNumberAt(content, match.index);
|
|
724
953
|
issues.push({
|
|
725
954
|
file,
|
|
@@ -738,30 +967,10 @@ function checkCurrentMarkdownLinks(rootDir = process.cwd()) {
|
|
|
738
967
|
};
|
|
739
968
|
}
|
|
740
969
|
function listCurrentMarkdownFiles(rootDir) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const stat = lstatSync(fullPath);
|
|
746
|
-
if (stat.isSymbolicLink()) continue;
|
|
747
|
-
if (stat.isDirectory()) files.push(...walkMarkdown(rootDir, fullPath));
|
|
748
|
-
else if (stat.isFile() && fullPath.endsWith(".md")) files.push(fullPath);
|
|
749
|
-
}
|
|
750
|
-
return Array.from(new Set(files)).sort();
|
|
751
|
-
}
|
|
752
|
-
function walkMarkdown(rootDir, dir) {
|
|
753
|
-
const files = [];
|
|
754
|
-
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
755
|
-
const fullPath = join6(dir, entry.name);
|
|
756
|
-
const repoPath = toRepoPath(rootDir, fullPath);
|
|
757
|
-
if (shouldSkipTree(repoPath)) continue;
|
|
758
|
-
if (entry.isSymbolicLink()) continue;
|
|
759
|
-
if (entry.isDirectory()) files.push(...walkMarkdown(rootDir, fullPath));
|
|
760
|
-
if (entry.isFile() && entry.name.endsWith(".md") && shouldIncludeSource(repoPath)) {
|
|
761
|
-
files.push(fullPath);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return files;
|
|
970
|
+
return listMarkdownFiles(rootDir, CURRENT_MARKDOWN_ROOTS, {
|
|
971
|
+
shouldSkipTree,
|
|
972
|
+
includeFile: (repoPath) => repoPath === "AGENTS.md" || repoPath === "README.md" || shouldIncludeSource(repoPath)
|
|
973
|
+
});
|
|
765
974
|
}
|
|
766
975
|
function shouldSkipTree(repoPath) {
|
|
767
976
|
if (isExternalArtifactPath(repoPath)) return true;
|
|
@@ -794,12 +1003,12 @@ function shouldIgnoreTarget(target) {
|
|
|
794
1003
|
function localTargetExists(rootDir, sourcePath, target) {
|
|
795
1004
|
const pathPart = decodeTarget(target).split("#")[0]?.split("?")[0] ?? "";
|
|
796
1005
|
if (!pathPart) return true;
|
|
797
|
-
const resolved = pathPart.startsWith("/") ?
|
|
1006
|
+
const resolved = pathPart.startsWith("/") ? resolve3(rootDir, `.${pathPart}`) : resolve3(dirname4(sourcePath), pathPart);
|
|
798
1007
|
const candidates = [resolved];
|
|
799
1008
|
if (!extname(resolved)) {
|
|
800
1009
|
candidates.push(`${resolved}.md`);
|
|
801
1010
|
}
|
|
802
|
-
return candidates.some((candidate) =>
|
|
1011
|
+
return candidates.some((candidate) => existsSync5(candidate));
|
|
803
1012
|
}
|
|
804
1013
|
function decodeTarget(target) {
|
|
805
1014
|
try {
|
|
@@ -816,7 +1025,7 @@ function lineNumberAt(content, index) {
|
|
|
816
1025
|
return line;
|
|
817
1026
|
}
|
|
818
1027
|
function readText(filePath) {
|
|
819
|
-
return
|
|
1028
|
+
return readFileSync7(filePath, "utf8");
|
|
820
1029
|
}
|
|
821
1030
|
|
|
822
1031
|
// src/commands/audit.ts
|
|
@@ -830,21 +1039,21 @@ function runAudit() {
|
|
|
830
1039
|
}
|
|
831
1040
|
return 1;
|
|
832
1041
|
}
|
|
833
|
-
const migrationSource =
|
|
834
|
-
if (
|
|
1042
|
+
const migrationSource = join8(root, "Docs-for trans");
|
|
1043
|
+
if (existsSync6(migrationSource)) {
|
|
835
1044
|
const count = countFiles(migrationSource);
|
|
836
1045
|
warnings += 1;
|
|
837
1046
|
console.log(
|
|
838
1047
|
`Migration source still exists: Docs-for trans (${count} files). This was a one-time migration shell; it should not be reintroduced.`
|
|
839
1048
|
);
|
|
840
1049
|
}
|
|
841
|
-
if (
|
|
1050
|
+
if (existsSync6(join8(root, "DocSystemStarter.md"))) {
|
|
842
1051
|
warnings += 1;
|
|
843
1052
|
console.log(
|
|
844
1053
|
"Stray root-level DocSystemStarter.md exists. The original draft has been archived; remove or re-archive."
|
|
845
1054
|
);
|
|
846
1055
|
}
|
|
847
|
-
const rootEntries = new Set(
|
|
1056
|
+
const rootEntries = new Set(readdirSync2(root));
|
|
848
1057
|
if (rootEntries.has("Docs")) {
|
|
849
1058
|
console.error(
|
|
850
1059
|
"Old Docs/ directory still exists. Move remaining files into docs/ or archive them."
|
|
@@ -863,8 +1072,8 @@ function runAudit() {
|
|
|
863
1072
|
}
|
|
864
1073
|
function countFiles(dir) {
|
|
865
1074
|
let count = 0;
|
|
866
|
-
for (const entry of
|
|
867
|
-
const path =
|
|
1075
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
1076
|
+
const path = join8(dir, entry.name);
|
|
868
1077
|
if (entry.isDirectory()) count += countFiles(path);
|
|
869
1078
|
if (entry.isFile() || entry.isSymbolicLink()) count += 1;
|
|
870
1079
|
}
|
|
@@ -886,19 +1095,12 @@ function runCheck() {
|
|
|
886
1095
|
|
|
887
1096
|
// src/commands/doctor.ts
|
|
888
1097
|
import { spawnSync } from "node:child_process";
|
|
889
|
-
import { existsSync as
|
|
890
|
-
import { isAbsolute, join as
|
|
891
|
-
|
|
892
|
-
// src/core/router-integrity.ts
|
|
893
|
-
import { existsSync as existsSync6, lstatSync as lstatSync2, readlinkSync, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
|
|
894
|
-
import { join as join8, relative as relative2 } from "node:path";
|
|
895
|
-
|
|
896
|
-
// src/core/symlinks.ts
|
|
897
|
-
function normalizeSymlinkTarget(target) {
|
|
898
|
-
return target.replaceAll("\\", "/");
|
|
899
|
-
}
|
|
1098
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
1099
|
+
import { isAbsolute as isAbsolute2, join as join10, resolve as resolve4 } from "node:path";
|
|
900
1100
|
|
|
901
1101
|
// src/core/router-integrity.ts
|
|
1102
|
+
import { existsSync as existsSync7, lstatSync as lstatSync3, readlinkSync as readlinkSync2, readFileSync as readFileSync8 } from "node:fs";
|
|
1103
|
+
import { join as join9, relative as relative4 } from "node:path";
|
|
902
1104
|
var CENTRAL_REQUIRED_FILES = [
|
|
903
1105
|
"AGENTS.md",
|
|
904
1106
|
"CLAUDE.md",
|
|
@@ -1216,7 +1418,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1216
1418
|
const requiredFiles = isCentral ? CENTRAL_REQUIRED_FILES : PROJECT_REQUIRED_FILES;
|
|
1217
1419
|
const requiredNeedles = isCentral ? CENTRAL_REQUIRED_NEEDLES : PROJECT_REQUIRED_NEEDLES;
|
|
1218
1420
|
for (const file of requiredFiles) {
|
|
1219
|
-
if (!
|
|
1421
|
+
if (!existsSync7(join9(rootDir, file))) {
|
|
1220
1422
|
issues.push({
|
|
1221
1423
|
file,
|
|
1222
1424
|
code: "missing-router-file",
|
|
@@ -1229,7 +1431,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1229
1431
|
}
|
|
1230
1432
|
issues.push(...validateHostSsot(rootDir));
|
|
1231
1433
|
for (const file of FORBIDDEN_LEGACY_PATHS) {
|
|
1232
|
-
if (
|
|
1434
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1233
1435
|
issues.push({
|
|
1234
1436
|
file,
|
|
1235
1437
|
code: "legacy-governance-path",
|
|
@@ -1239,7 +1441,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1239
1441
|
}
|
|
1240
1442
|
if (!isCentral) {
|
|
1241
1443
|
for (const file of FORBIDDEN_PROJECT_PATHS) {
|
|
1242
|
-
if (
|
|
1444
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1243
1445
|
issues.push({
|
|
1244
1446
|
file,
|
|
1245
1447
|
code: "legacy-project-policy-path",
|
|
@@ -1248,7 +1450,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1248
1450
|
}
|
|
1249
1451
|
}
|
|
1250
1452
|
for (const file of SUPERSEDED_PROJECT_CONTRACT_PATHS) {
|
|
1251
|
-
if (
|
|
1453
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1252
1454
|
issues.push({
|
|
1253
1455
|
file,
|
|
1254
1456
|
code: "superseded-project-contract",
|
|
@@ -1257,7 +1459,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1257
1459
|
}
|
|
1258
1460
|
}
|
|
1259
1461
|
for (const file of FORBIDDEN_PROJECT_ROOTS) {
|
|
1260
|
-
if (
|
|
1462
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1261
1463
|
issues.push({
|
|
1262
1464
|
file,
|
|
1263
1465
|
code: "project-root-integration-path",
|
|
@@ -1267,7 +1469,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1267
1469
|
}
|
|
1268
1470
|
} else {
|
|
1269
1471
|
for (const file of FORBIDDEN_CENTRAL_ROOTS) {
|
|
1270
|
-
if (
|
|
1472
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1271
1473
|
issues.push({
|
|
1272
1474
|
file,
|
|
1273
1475
|
code: "central-external-shared-rule-copy",
|
|
@@ -1284,9 +1486,9 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1284
1486
|
});
|
|
1285
1487
|
}
|
|
1286
1488
|
for (const requirement of requiredNeedles) {
|
|
1287
|
-
const path =
|
|
1288
|
-
if (!
|
|
1289
|
-
const content =
|
|
1489
|
+
const path = join9(rootDir, requirement.file);
|
|
1490
|
+
if (!existsSync7(path)) continue;
|
|
1491
|
+
const content = readFileSync8(path, "utf8");
|
|
1290
1492
|
if (!content.includes(requirement.needle)) {
|
|
1291
1493
|
issues.push({
|
|
1292
1494
|
file: requirement.file,
|
|
@@ -1332,7 +1534,7 @@ function validateHostSsot(rootDir) {
|
|
|
1332
1534
|
validateExactSymlink(".claude/skills", "../.agents/skills");
|
|
1333
1535
|
return issues;
|
|
1334
1536
|
function validateRegularFile(file) {
|
|
1335
|
-
const stat =
|
|
1537
|
+
const stat = safeLstat2(join9(rootDir, file));
|
|
1336
1538
|
if (!stat || stat.isFile()) return;
|
|
1337
1539
|
issues.push({
|
|
1338
1540
|
file,
|
|
@@ -1341,7 +1543,7 @@ function validateHostSsot(rootDir) {
|
|
|
1341
1543
|
});
|
|
1342
1544
|
}
|
|
1343
1545
|
function validateDirectory(file) {
|
|
1344
|
-
const stat =
|
|
1546
|
+
const stat = safeLstat2(join9(rootDir, file));
|
|
1345
1547
|
if (!stat || stat.isDirectory()) return;
|
|
1346
1548
|
issues.push({
|
|
1347
1549
|
file,
|
|
@@ -1350,8 +1552,8 @@ function validateHostSsot(rootDir) {
|
|
|
1350
1552
|
});
|
|
1351
1553
|
}
|
|
1352
1554
|
function validateExactSymlink(file, expectedRawTarget) {
|
|
1353
|
-
const path =
|
|
1354
|
-
const stat =
|
|
1555
|
+
const path = join9(rootDir, file);
|
|
1556
|
+
const stat = safeLstat2(path);
|
|
1355
1557
|
if (!stat) return;
|
|
1356
1558
|
if (!stat.isSymbolicLink()) {
|
|
1357
1559
|
issues.push({
|
|
@@ -1361,7 +1563,7 @@ function validateHostSsot(rootDir) {
|
|
|
1361
1563
|
});
|
|
1362
1564
|
return;
|
|
1363
1565
|
}
|
|
1364
|
-
const rawTarget = normalizeSymlinkTarget(
|
|
1566
|
+
const rawTarget = normalizeSymlinkTarget(readlinkSync2(path));
|
|
1365
1567
|
if (rawTarget !== expectedRawTarget) {
|
|
1366
1568
|
issues.push({
|
|
1367
1569
|
file,
|
|
@@ -1371,9 +1573,9 @@ function validateHostSsot(rootDir) {
|
|
|
1371
1573
|
}
|
|
1372
1574
|
}
|
|
1373
1575
|
}
|
|
1374
|
-
function
|
|
1576
|
+
function safeLstat2(path) {
|
|
1375
1577
|
try {
|
|
1376
|
-
return
|
|
1578
|
+
return lstatSync3(path);
|
|
1377
1579
|
} catch (error) {
|
|
1378
1580
|
const code = error.code;
|
|
1379
1581
|
if (code === "ENOENT" || code === "ENOTDIR") return void 0;
|
|
@@ -1381,27 +1583,27 @@ function safeLstat(path) {
|
|
|
1381
1583
|
}
|
|
1382
1584
|
}
|
|
1383
1585
|
function isCentralRepository(rootDir) {
|
|
1384
|
-
const packageJsonPath =
|
|
1385
|
-
if (!
|
|
1586
|
+
const packageJsonPath = join9(rootDir, "package.json");
|
|
1587
|
+
if (!existsSync7(packageJsonPath)) return false;
|
|
1386
1588
|
const packageName = readPackageName(packageJsonPath);
|
|
1387
1589
|
const centralPackageNames = /* @__PURE__ */ new Set(["project-governance-system", "pro-gov"]);
|
|
1388
1590
|
return centralPackageNames.has(packageName) && hasCentralRepositoryShape(rootDir);
|
|
1389
1591
|
}
|
|
1390
1592
|
function readPackageName(packageJsonPath) {
|
|
1391
1593
|
try {
|
|
1392
|
-
const packageJson = JSON.parse(
|
|
1594
|
+
const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
|
|
1393
1595
|
return typeof packageJson.name === "string" ? packageJson.name : "";
|
|
1394
1596
|
} catch {
|
|
1395
1597
|
return "";
|
|
1396
1598
|
}
|
|
1397
1599
|
}
|
|
1398
1600
|
function hasCentralRepositoryShape(rootDir) {
|
|
1399
|
-
return
|
|
1601
|
+
return existsSync7(join9(rootDir, "profiles")) && existsSync7(join9(rootDir, "starter")) && existsSync7(join9(rootDir, "integrations"));
|
|
1400
1602
|
}
|
|
1401
1603
|
function validateProjectAgentsRouting(rootDir) {
|
|
1402
1604
|
const issues = [];
|
|
1403
1605
|
const existingRoutes = PROJECT_AGENTS_ROUTING_FILES.filter(
|
|
1404
|
-
(file) =>
|
|
1606
|
+
(file) => existsSync7(join9(rootDir, file))
|
|
1405
1607
|
);
|
|
1406
1608
|
if (existingRoutes.length === 0) {
|
|
1407
1609
|
issues.push({
|
|
@@ -1411,9 +1613,9 @@ function validateProjectAgentsRouting(rootDir) {
|
|
|
1411
1613
|
});
|
|
1412
1614
|
return issues;
|
|
1413
1615
|
}
|
|
1414
|
-
const agentsPath =
|
|
1415
|
-
if (!
|
|
1416
|
-
const agents =
|
|
1616
|
+
const agentsPath = join9(rootDir, "AGENTS.md");
|
|
1617
|
+
if (!existsSync7(agentsPath)) return issues;
|
|
1618
|
+
const agents = readFileSync8(agentsPath, "utf8");
|
|
1417
1619
|
if (!existingRoutes.some((file) => agents.includes(file))) {
|
|
1418
1620
|
issues.push({
|
|
1419
1621
|
file: "AGENTS.md",
|
|
@@ -1424,9 +1626,9 @@ function validateProjectAgentsRouting(rootDir) {
|
|
|
1424
1626
|
return issues;
|
|
1425
1627
|
}
|
|
1426
1628
|
function validateRouterBlock(rootDir, file) {
|
|
1427
|
-
const path =
|
|
1428
|
-
if (!
|
|
1429
|
-
const content =
|
|
1629
|
+
const path = join9(rootDir, file);
|
|
1630
|
+
if (!existsSync7(path)) return [];
|
|
1631
|
+
const content = readFileSync8(path, "utf8");
|
|
1430
1632
|
const begin = content.indexOf(ROUTER_BLOCK_BEGIN);
|
|
1431
1633
|
const end = content.indexOf(ROUTER_BLOCK_END);
|
|
1432
1634
|
const issues = [];
|
|
@@ -1451,9 +1653,9 @@ function validateRouterBlock(rootDir, file) {
|
|
|
1451
1653
|
return issues;
|
|
1452
1654
|
}
|
|
1453
1655
|
function validateBacktickedLocalPaths(rootDir, file, pathRoot = "") {
|
|
1454
|
-
const path =
|
|
1455
|
-
if (!
|
|
1456
|
-
const content =
|
|
1656
|
+
const path = join9(rootDir, file);
|
|
1657
|
+
if (!existsSync7(path)) return [];
|
|
1658
|
+
const content = readFileSync8(path, "utf8");
|
|
1457
1659
|
const issues = [];
|
|
1458
1660
|
const seen = /* @__PURE__ */ new Set();
|
|
1459
1661
|
const matches = content.matchAll(/`([^`]+)`/g);
|
|
@@ -1463,7 +1665,10 @@ function validateBacktickedLocalPaths(rootDir, file, pathRoot = "") {
|
|
|
1463
1665
|
seen.add(value);
|
|
1464
1666
|
if (!isLocalPathReference(value)) continue;
|
|
1465
1667
|
const normalized = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
1466
|
-
|
|
1668
|
+
const candidatePaths = [join9(rootDir, pathRoot, normalized), join9(rootDir, normalized)];
|
|
1669
|
+
if (candidatePaths.some(
|
|
1670
|
+
(candidatePath) => existsSync7(candidatePath) || isUnresolvedManagedSharedRuleSymlink(rootDir, candidatePath)
|
|
1671
|
+
)) {
|
|
1467
1672
|
continue;
|
|
1468
1673
|
}
|
|
1469
1674
|
issues.push({
|
|
@@ -1482,9 +1687,9 @@ function isLocalPathReference(value) {
|
|
|
1482
1687
|
return value === "README.md" || value.endsWith(".md") || value.endsWith("/") || value.startsWith("docs/") || value.startsWith("starter/") || value.startsWith("profiles/") || value.startsWith("integrations/");
|
|
1483
1688
|
}
|
|
1484
1689
|
function validatePortableRouterText(rootDir, file) {
|
|
1485
|
-
const path =
|
|
1486
|
-
if (!
|
|
1487
|
-
const content =
|
|
1690
|
+
const path = join9(rootDir, file);
|
|
1691
|
+
if (!existsSync7(path)) return [];
|
|
1692
|
+
const content = readFileSync8(path, "utf8");
|
|
1488
1693
|
if (!hasNonPortablePath(content)) return [];
|
|
1489
1694
|
return [
|
|
1490
1695
|
{
|
|
@@ -1504,25 +1709,9 @@ function hasNonPortablePath(content) {
|
|
|
1504
1709
|
) || /\b(OneDrive|CloudStorage)\b/.test(content);
|
|
1505
1710
|
}
|
|
1506
1711
|
function findGovernedReadmes(rootDir) {
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
if (existsSync6(absRoot)) walk2(absRoot);
|
|
1511
|
-
}
|
|
1512
|
-
return matches.sort();
|
|
1513
|
-
function walk2(dir) {
|
|
1514
|
-
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
1515
|
-
if (entry.isSymbolicLink()) continue;
|
|
1516
|
-
if (entry.isDirectory()) {
|
|
1517
|
-
walk2(join8(dir, entry.name));
|
|
1518
|
-
continue;
|
|
1519
|
-
}
|
|
1520
|
-
if (!entry.isFile()) continue;
|
|
1521
|
-
if (entry.name.toLowerCase() !== "readme.md") continue;
|
|
1522
|
-
const repoPath = relative2(rootDir, join8(dir, entry.name)).split(/\\/g).join("/");
|
|
1523
|
-
if (repoPath !== "README.md") matches.push(repoPath);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1712
|
+
return listMarkdownFiles(rootDir, ["docs", "starter/docs"], {
|
|
1713
|
+
includeFile: (repoPath) => repoPath.toLowerCase().endsWith("/readme.md")
|
|
1714
|
+
}).map((path) => relative4(rootDir, path).split(/\\/g).join("/")).filter((repoPath) => repoPath !== "README.md");
|
|
1526
1715
|
}
|
|
1527
1716
|
|
|
1528
1717
|
// src/commands/doctor.ts
|
|
@@ -1590,8 +1779,8 @@ function collectDoctorIssues(rootDir = process.cwd()) {
|
|
|
1590
1779
|
return issues;
|
|
1591
1780
|
}
|
|
1592
1781
|
function checkLefthook(rootDir) {
|
|
1593
|
-
const path =
|
|
1594
|
-
if (!
|
|
1782
|
+
const path = join10(rootDir, "lefthook.yml");
|
|
1783
|
+
if (!existsSync8(path)) {
|
|
1595
1784
|
return [
|
|
1596
1785
|
{
|
|
1597
1786
|
severity: "warning",
|
|
@@ -1600,7 +1789,7 @@ function checkLefthook(rootDir) {
|
|
|
1600
1789
|
}
|
|
1601
1790
|
];
|
|
1602
1791
|
}
|
|
1603
|
-
const content =
|
|
1792
|
+
const content = readFileSync9(path, "utf8");
|
|
1604
1793
|
const issues = [];
|
|
1605
1794
|
for (const command2 of [
|
|
1606
1795
|
"pnpm doc-gov router-check",
|
|
@@ -1637,8 +1826,8 @@ function checkLefthook(rootDir) {
|
|
|
1637
1826
|
return issues;
|
|
1638
1827
|
}
|
|
1639
1828
|
function checkDocsCheckWorkflow(rootDir) {
|
|
1640
|
-
const path =
|
|
1641
|
-
if (!
|
|
1829
|
+
const path = join10(rootDir, ".github/workflows/docs-check.yml");
|
|
1830
|
+
if (!existsSync8(path)) {
|
|
1642
1831
|
return [
|
|
1643
1832
|
{
|
|
1644
1833
|
severity: "warning",
|
|
@@ -1647,7 +1836,7 @@ function checkDocsCheckWorkflow(rootDir) {
|
|
|
1647
1836
|
}
|
|
1648
1837
|
];
|
|
1649
1838
|
}
|
|
1650
|
-
const content =
|
|
1839
|
+
const content = readFileSync9(path, "utf8");
|
|
1651
1840
|
const issues = [];
|
|
1652
1841
|
for (const command2 of [
|
|
1653
1842
|
"pnpm doc-gov router-check",
|
|
@@ -1667,15 +1856,15 @@ function checkDocsCheckWorkflow(rootDir) {
|
|
|
1667
1856
|
return issues;
|
|
1668
1857
|
}
|
|
1669
1858
|
function hookCallsLefthook(path) {
|
|
1670
|
-
return
|
|
1859
|
+
return existsSync8(path) && readFileSync9(path, "utf8").includes("lefthook");
|
|
1671
1860
|
}
|
|
1672
1861
|
function resolveGitHookPath(rootDir, hookName) {
|
|
1673
1862
|
const result = spawnSync("git", ["-C", rootDir, "rev-parse", "--git-path", `hooks/${hookName}`], {
|
|
1674
1863
|
encoding: "utf8"
|
|
1675
1864
|
});
|
|
1676
1865
|
const gitPath = result.status === 0 ? result.stdout.trim() : "";
|
|
1677
|
-
if (!gitPath) return
|
|
1678
|
-
return
|
|
1866
|
+
if (!gitPath) return join10(rootDir, ".git/hooks", hookName);
|
|
1867
|
+
return isAbsolute2(gitPath) ? gitPath : resolve4(rootDir, gitPath);
|
|
1679
1868
|
}
|
|
1680
1869
|
|
|
1681
1870
|
// src/commands/find.ts
|
|
@@ -1713,12 +1902,12 @@ function runFind(args2) {
|
|
|
1713
1902
|
}
|
|
1714
1903
|
|
|
1715
1904
|
// src/commands/init.ts
|
|
1716
|
-
import { existsSync as
|
|
1717
|
-
import { join as
|
|
1905
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1906
|
+
import { join as join12 } from "node:path";
|
|
1718
1907
|
|
|
1719
1908
|
// src/core/templates.ts
|
|
1720
|
-
import { existsSync as
|
|
1721
|
-
import { join as
|
|
1909
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1910
|
+
import { join as join11 } from "node:path";
|
|
1722
1911
|
var TEMPLATE_FILES = {
|
|
1723
1912
|
decision: "adr.md",
|
|
1724
1913
|
spec: "spec.md",
|
|
@@ -1926,19 +2115,19 @@ var DEFAULT_TEMPLATES = {
|
|
|
1926
2115
|
function loadTemplate(rootDir, type) {
|
|
1927
2116
|
const file = TEMPLATE_FILES[type];
|
|
1928
2117
|
if (!file) throw new Error(`No template file mapped for type: ${type}`);
|
|
1929
|
-
const path =
|
|
1930
|
-
if (
|
|
2118
|
+
const path = join11(rootDir, "docs/governance/templates", file);
|
|
2119
|
+
if (existsSync9(path)) return readFileSync10(path, "utf8");
|
|
1931
2120
|
const fallback = DEFAULT_TEMPLATES[file];
|
|
1932
2121
|
if (fallback) return fallback;
|
|
1933
2122
|
throw new Error(`Template file is missing: docs/governance/templates/${file}`);
|
|
1934
2123
|
}
|
|
1935
2124
|
function ensureDefaultTemplates(rootDir) {
|
|
1936
|
-
const templatesDir =
|
|
2125
|
+
const templatesDir = join11(rootDir, "docs/governance/templates");
|
|
1937
2126
|
mkdirSync3(templatesDir, { recursive: true });
|
|
1938
2127
|
let created = 0;
|
|
1939
2128
|
for (const [file, content] of Object.entries(DEFAULT_TEMPLATES)) {
|
|
1940
|
-
const path =
|
|
1941
|
-
if (
|
|
2129
|
+
const path = join11(templatesDir, file);
|
|
2130
|
+
if (existsSync9(path)) continue;
|
|
1942
2131
|
writeFileSync4(path, content);
|
|
1943
2132
|
created++;
|
|
1944
2133
|
}
|
|
@@ -1993,16 +2182,16 @@ function runInit(args2) {
|
|
|
1993
2182
|
];
|
|
1994
2183
|
let created = 0;
|
|
1995
2184
|
for (const dir of dirs) {
|
|
1996
|
-
const abs =
|
|
1997
|
-
if (!
|
|
2185
|
+
const abs = join12(root, dir);
|
|
2186
|
+
if (!existsSync10(abs)) {
|
|
1998
2187
|
mkdirSync4(abs, { recursive: true });
|
|
1999
2188
|
created++;
|
|
2000
2189
|
} else if (!force) {
|
|
2001
2190
|
}
|
|
2002
2191
|
}
|
|
2003
2192
|
for (const dir of ["docs/specs/completed", "docs/plans/completed", "docs/archive"]) {
|
|
2004
|
-
const keep =
|
|
2005
|
-
if (!
|
|
2193
|
+
const keep = join12(root, dir, ".gitkeep");
|
|
2194
|
+
if (!existsSync10(keep)) writeFileSync5(keep, "");
|
|
2006
2195
|
}
|
|
2007
2196
|
const templatesCreated = ensureDefaultTemplates(root);
|
|
2008
2197
|
console.log(
|
|
@@ -2049,8 +2238,8 @@ function runLinks() {
|
|
|
2049
2238
|
}
|
|
2050
2239
|
|
|
2051
2240
|
// src/commands/migrate.ts
|
|
2052
|
-
import { existsSync as
|
|
2053
|
-
import { join as
|
|
2241
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "node:fs";
|
|
2242
|
+
import { join as join13 } from "node:path";
|
|
2054
2243
|
var PROFILE_ROUTES = {
|
|
2055
2244
|
"engineering-runtime": "docs/governance/agents-routing/engineering-runtime-v1.1.md",
|
|
2056
2245
|
"doc-only": "docs/governance/agents-routing/doc-only-v1.1.md"
|
|
@@ -2087,11 +2276,11 @@ function checkMigrationReadiness(rootDir, profile) {
|
|
|
2087
2276
|
issues.push(`${issue.file}: ${issue.code}: ${issue.message}`);
|
|
2088
2277
|
}
|
|
2089
2278
|
const route = PROFILE_ROUTES[profile];
|
|
2090
|
-
if (!
|
|
2279
|
+
if (!existsSync11(join13(rootDir, route))) {
|
|
2091
2280
|
issues.push(`missing selected profile route: ${route}`);
|
|
2092
2281
|
}
|
|
2093
|
-
const agentsPath =
|
|
2094
|
-
if (
|
|
2282
|
+
const agentsPath = join13(rootDir, "AGENTS.md");
|
|
2283
|
+
if (existsSync11(agentsPath) && !readFileSync11(agentsPath, "utf8").includes(route)) {
|
|
2095
2284
|
issues.push(`AGENTS.md must name selected profile route: ${route}`);
|
|
2096
2285
|
}
|
|
2097
2286
|
return issues;
|
|
@@ -2138,8 +2327,8 @@ function readFlag(args2, name) {
|
|
|
2138
2327
|
}
|
|
2139
2328
|
|
|
2140
2329
|
// src/commands/new.ts
|
|
2141
|
-
import { existsSync as
|
|
2142
|
-
import { dirname as
|
|
2330
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2331
|
+
import { dirname as dirname5, join as join14 } from "node:path";
|
|
2143
2332
|
function runNew(args2) {
|
|
2144
2333
|
const positional = args2.filter((a) => !a.startsWith("--"));
|
|
2145
2334
|
const owner = readFlag2(args2, "--owner") ?? "human";
|
|
@@ -2165,8 +2354,8 @@ function runNew(args2) {
|
|
|
2165
2354
|
console.error(err.message);
|
|
2166
2355
|
return 1;
|
|
2167
2356
|
}
|
|
2168
|
-
const absPath =
|
|
2169
|
-
if (
|
|
2357
|
+
const absPath = join14(root, plan.filePath);
|
|
2358
|
+
if (existsSync12(absPath) && !force) {
|
|
2170
2359
|
console.error(`File already exists: ${plan.filePath}. Use --force to overwrite.`);
|
|
2171
2360
|
return 1;
|
|
2172
2361
|
}
|
|
@@ -2188,7 +2377,7 @@ function runNew(args2) {
|
|
|
2188
2377
|
tags: [plan.slug.split("-")[0] ?? "replace-me"],
|
|
2189
2378
|
pinned: false
|
|
2190
2379
|
});
|
|
2191
|
-
mkdirSync5(
|
|
2380
|
+
mkdirSync5(dirname5(absPath), { recursive: true });
|
|
2192
2381
|
writeFileSync6(absPath, rendered);
|
|
2193
2382
|
console.log(`Created ${plan.filePath} with id ${plan.id}.`);
|
|
2194
2383
|
try {
|
|
@@ -2249,8 +2438,8 @@ function runScan(args2) {
|
|
|
2249
2438
|
}
|
|
2250
2439
|
|
|
2251
2440
|
// src/commands/supersede.ts
|
|
2252
|
-
import { readFileSync as
|
|
2253
|
-
import { join as
|
|
2441
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2442
|
+
import { join as join15 } from "node:path";
|
|
2254
2443
|
function runSupersede(args2) {
|
|
2255
2444
|
const [oldId, newId] = args2;
|
|
2256
2445
|
if (!oldId || !newId) {
|
|
@@ -2286,15 +2475,15 @@ function runSupersede(args2) {
|
|
|
2286
2475
|
return 1;
|
|
2287
2476
|
}
|
|
2288
2477
|
const today = todayIso();
|
|
2289
|
-
const oldPath =
|
|
2290
|
-
let oldContent =
|
|
2478
|
+
const oldPath = join15(root, oldRec.path);
|
|
2479
|
+
let oldContent = readFileSync12(oldPath, "utf8");
|
|
2291
2480
|
oldContent = updateFrontmatterField(oldContent, "status", "superseded");
|
|
2292
2481
|
oldContent = updateFrontmatterField(oldContent, "canonical", "false");
|
|
2293
2482
|
oldContent = updateFrontmatterField(oldContent, "superseded_by", newId);
|
|
2294
2483
|
oldContent = updateFrontmatterField(oldContent, "last_reviewed", today);
|
|
2295
2484
|
writeFileSync7(oldPath, oldContent);
|
|
2296
|
-
const newPath =
|
|
2297
|
-
let newContent =
|
|
2485
|
+
const newPath = join15(root, newRec.path);
|
|
2486
|
+
let newContent = readFileSync12(newPath, "utf8");
|
|
2298
2487
|
newContent = appendToFrontmatterList(newContent, "supersedes", newId === oldId ? "" : oldId);
|
|
2299
2488
|
newContent = updateFrontmatterField(newContent, "last_reviewed", today);
|
|
2300
2489
|
writeFileSync7(newPath, newContent);
|
|
@@ -2343,13 +2532,13 @@ ${lines.join("\n")}${tail}`;
|
|
|
2343
2532
|
|
|
2344
2533
|
// src/commands/verify-commit-msg.ts
|
|
2345
2534
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2346
|
-
import { existsSync as
|
|
2535
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
|
|
2347
2536
|
function runVerifyCommitMsg(args2) {
|
|
2348
2537
|
const msgFile = args2[0];
|
|
2349
|
-
if (!msgFile || !
|
|
2538
|
+
if (!msgFile || !existsSync13(msgFile)) {
|
|
2350
2539
|
return 0;
|
|
2351
2540
|
}
|
|
2352
|
-
const message =
|
|
2541
|
+
const message = readFileSync13(msgFile, "utf8");
|
|
2353
2542
|
if (/^Merge\b/m.test(message) || /^Revert\b/m.test(message)) return 0;
|
|
2354
2543
|
let stagedFiles = [];
|
|
2355
2544
|
try {
|
|
@@ -2361,7 +2550,7 @@ function runVerifyCommitMsg(args2) {
|
|
|
2361
2550
|
}
|
|
2362
2551
|
const failures = [];
|
|
2363
2552
|
for (const file of stagedFiles) {
|
|
2364
|
-
if (!
|
|
2553
|
+
if (!existsSync13(file)) continue;
|
|
2365
2554
|
const fm = readFrontmatterFile(file);
|
|
2366
2555
|
if (!fm) continue;
|
|
2367
2556
|
const id = stringValue(fm.data.id);
|