@pieai/doc-gov 0.9.6 → 0.9.8
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 +219 -116
- 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";
|
|
@@ -238,6 +238,95 @@ function toRepoPath2(rootDir, absolutePath) {
|
|
|
238
238
|
return relative2(rootDir, absolutePath).split(/\\/g).join("/");
|
|
239
239
|
}
|
|
240
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_PATTERN = /^(?:\.\.\/)+ProjectGovernanceSystem\/agent-assets\/rules\/pie-rules\/([^/]+\.md)$/;
|
|
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
|
+
const target = normalizeSymlinkTarget(readlinkSync(candidatePath));
|
|
266
|
+
const match = target.match(PGS_SHARED_RULE_TARGET_PATTERN);
|
|
267
|
+
return match?.[1] === filename;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function findUnavailableManagedSharedRuleEntries(rootDir) {
|
|
273
|
+
const manifestEntries = readManifestEntries(rootDir);
|
|
274
|
+
return manifestEntries.filter(
|
|
275
|
+
(entry) => entry.type === "policy" && isUnresolvedManagedSharedRuleSymlink(rootDir, join3(rootDir, entry.path))
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
function sharedRuleFilename(repoPath) {
|
|
279
|
+
if (!repoPath.startsWith(SHARED_RULE_PATH_PREFIX)) return void 0;
|
|
280
|
+
const filename = repoPath.slice(SHARED_RULE_PATH_PREFIX.length);
|
|
281
|
+
if (!filename || filename.includes("/") || !filename.endsWith(".md")) return void 0;
|
|
282
|
+
return filename;
|
|
283
|
+
}
|
|
284
|
+
function readManifestEntries(rootDir) {
|
|
285
|
+
const manifestPath = join3(rootDir, "docs/governance/MANIFEST.yml");
|
|
286
|
+
if (!existsSync2(manifestPath)) return [];
|
|
287
|
+
try {
|
|
288
|
+
const lines = readFileSync3(manifestPath, "utf8").split("\n");
|
|
289
|
+
const entries = [];
|
|
290
|
+
let current;
|
|
291
|
+
const flush = () => {
|
|
292
|
+
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") {
|
|
293
|
+
entries.push(current);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
for (const line of lines) {
|
|
297
|
+
const idMatch = line.match(/^\s+- id:\s*(.+?)\s*$/);
|
|
298
|
+
if (idMatch) {
|
|
299
|
+
flush();
|
|
300
|
+
current = { id: cleanValue(idMatch[1] ?? "") };
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (!current) continue;
|
|
304
|
+
const fieldMatch = line.match(
|
|
305
|
+
/^\s+(path|type|status|canonical|last_reviewed|pinned):\s*(.*?)\s*$/
|
|
306
|
+
);
|
|
307
|
+
if (!fieldMatch) continue;
|
|
308
|
+
const field = fieldMatch[1];
|
|
309
|
+
const value = cleanValue(fieldMatch[2] ?? "");
|
|
310
|
+
if (field === "path") current.path = value;
|
|
311
|
+
if (field === "type") current.type = value;
|
|
312
|
+
if (field === "status") current.status = value;
|
|
313
|
+
if (field === "canonical") current.canonical = value === "true";
|
|
314
|
+
if (field === "last_reviewed") current.lastReviewed = value;
|
|
315
|
+
if (field === "pinned") current.pinned = value === "true";
|
|
316
|
+
}
|
|
317
|
+
flush();
|
|
318
|
+
return entries;
|
|
319
|
+
} catch {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function cleanValue(value) {
|
|
324
|
+
return value.replace(/^(['"])(.*)\1$/, "$2");
|
|
325
|
+
}
|
|
326
|
+
function toRepoPath3(rootDir, candidatePath) {
|
|
327
|
+
return relative3(resolve2(rootDir), resolve2(candidatePath)).split(/\\/g).join("/");
|
|
328
|
+
}
|
|
329
|
+
|
|
241
330
|
// src/core/lifecycle.ts
|
|
242
331
|
var normalStatuses = [
|
|
243
332
|
"draft",
|
|
@@ -405,6 +494,24 @@ function checkDocs(rootDir = process.cwd()) {
|
|
|
405
494
|
issues.push(...result.issues);
|
|
406
495
|
if (result.record) records.push(result.record);
|
|
407
496
|
}
|
|
497
|
+
for (const entry of findUnavailableManagedSharedRuleEntries(rootDir)) {
|
|
498
|
+
records.push({
|
|
499
|
+
id: entry.id,
|
|
500
|
+
title: entry.id,
|
|
501
|
+
type: entry.type,
|
|
502
|
+
status: entry.status,
|
|
503
|
+
canonical: entry.canonical,
|
|
504
|
+
owner: "external-shared-rule",
|
|
505
|
+
created: entry.lastReviewed,
|
|
506
|
+
lastReviewed: entry.lastReviewed,
|
|
507
|
+
domain: "shared-rule",
|
|
508
|
+
tags: ["shared-rule"],
|
|
509
|
+
pinned: entry.pinned,
|
|
510
|
+
related: [],
|
|
511
|
+
supersedes: [],
|
|
512
|
+
path: entry.path
|
|
513
|
+
});
|
|
514
|
+
}
|
|
408
515
|
issues.push(...validateGlobalIntegrity(records));
|
|
409
516
|
return {
|
|
410
517
|
ok: issues.length === 0,
|
|
@@ -510,8 +617,8 @@ function pathToRepo(rootDir, path) {
|
|
|
510
617
|
}
|
|
511
618
|
|
|
512
619
|
// src/core/paths.ts
|
|
513
|
-
import { existsSync as
|
|
514
|
-
import { basename, join as
|
|
620
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
621
|
+
import { basename, join as join4 } from "node:path";
|
|
515
622
|
function planPath(rootDir, type, slugInput) {
|
|
516
623
|
const cleanSlug = slugInput.replace(/^\/+|\/+$/g, "");
|
|
517
624
|
if (!cleanSlug) throw new Error("Slug is required.");
|
|
@@ -580,8 +687,8 @@ function planPath(rootDir, type, slugInput) {
|
|
|
580
687
|
throw new Error(`Unknown type: ${type}`);
|
|
581
688
|
}
|
|
582
689
|
function nextSerial(rootDir, scanDir, regex) {
|
|
583
|
-
const root =
|
|
584
|
-
if (!
|
|
690
|
+
const root = join4(rootDir, scanDir);
|
|
691
|
+
if (!existsSync3(root)) return "0001";
|
|
585
692
|
let max = 0;
|
|
586
693
|
walkSerial(rootDir, scanDir, regex, (n) => {
|
|
587
694
|
if (n > max) max = n;
|
|
@@ -610,8 +717,8 @@ function todayIso() {
|
|
|
610
717
|
}
|
|
611
718
|
|
|
612
719
|
// src/core/manifest.ts
|
|
613
|
-
import { existsSync as
|
|
614
|
-
import { dirname as dirname2, join as
|
|
720
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
721
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
615
722
|
function buildManifest(rootDir = process.cwd()) {
|
|
616
723
|
const result = checkDocs(rootDir);
|
|
617
724
|
if (!result.ok) {
|
|
@@ -623,14 +730,14 @@ ${error}`);
|
|
|
623
730
|
}
|
|
624
731
|
function writeManifest(rootDir = process.cwd()) {
|
|
625
732
|
const manifest = buildManifest(rootDir);
|
|
626
|
-
const path =
|
|
733
|
+
const path = join5(rootDir, "docs/governance/MANIFEST.yml");
|
|
627
734
|
mkdirSync(dirname2(path), { recursive: true });
|
|
628
735
|
writeFileSync(path, manifest);
|
|
629
736
|
}
|
|
630
737
|
function manifestInSync(rootDir = process.cwd()) {
|
|
631
|
-
const path =
|
|
632
|
-
if (!
|
|
633
|
-
return normalizeManifest(
|
|
738
|
+
const path = join5(rootDir, "docs/governance/MANIFEST.yml");
|
|
739
|
+
if (!existsSync4(path)) return false;
|
|
740
|
+
return normalizeManifest(readFileSync4(path, "utf8")) === normalizeManifest(buildManifest(rootDir));
|
|
634
741
|
}
|
|
635
742
|
function normalizeManifest(value) {
|
|
636
743
|
return value.replace(/^generated_at: .*$/m, "generated_at: <ignored>").replace(/^generator_version: .*$/m, "generator_version: <ignored>");
|
|
@@ -659,7 +766,7 @@ function renderManifest(records) {
|
|
|
659
766
|
function readPackageVersion() {
|
|
660
767
|
for (const candidate of ["../package.json", "../../package.json"]) {
|
|
661
768
|
try {
|
|
662
|
-
const packageJson = JSON.parse(
|
|
769
|
+
const packageJson = JSON.parse(readFileSync4(new URL(candidate, import.meta.url), "utf8"));
|
|
663
770
|
if (typeof packageJson.version === "string") return packageJson.version;
|
|
664
771
|
} catch {
|
|
665
772
|
}
|
|
@@ -695,8 +802,8 @@ function runApprove(args2) {
|
|
|
695
802
|
);
|
|
696
803
|
return 1;
|
|
697
804
|
}
|
|
698
|
-
const filePath =
|
|
699
|
-
const content =
|
|
805
|
+
const filePath = join6(root, record.path);
|
|
806
|
+
const content = readFileSync5(filePath, "utf8");
|
|
700
807
|
let next = content;
|
|
701
808
|
next = updateFrontmatterField(next, "status", toStatus);
|
|
702
809
|
next = updateFrontmatterField(next, "canonical", "true");
|
|
@@ -742,8 +849,8 @@ ${newLines.join("\n")}${tail}`;
|
|
|
742
849
|
|
|
743
850
|
// src/commands/archive.ts
|
|
744
851
|
import { execFileSync } from "node:child_process";
|
|
745
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
746
|
-
import { basename as basename2, dirname as dirname3, join as
|
|
852
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
853
|
+
import { basename as basename2, dirname as dirname3, join as join7 } from "node:path";
|
|
747
854
|
function runArchive(args2) {
|
|
748
855
|
const positional = args2.filter((a) => !a.startsWith("--"));
|
|
749
856
|
const id = positional[0];
|
|
@@ -782,9 +889,9 @@ function runArchive(args2) {
|
|
|
782
889
|
const fileName = basename2(oldPath);
|
|
783
890
|
const archiveDir = `docs/archive/${quarterTag()}-${record.type}`;
|
|
784
891
|
const newPath = `${archiveDir}/${fileName}`;
|
|
785
|
-
const absNew =
|
|
786
|
-
const absOld =
|
|
787
|
-
let content =
|
|
892
|
+
const absNew = join7(root, newPath);
|
|
893
|
+
const absOld = join7(root, oldPath);
|
|
894
|
+
let content = readFileSync6(absOld, "utf8");
|
|
788
895
|
content = updateFrontmatterField(content, "type", "archive");
|
|
789
896
|
content = updateFrontmatterField(content, "status", "archived");
|
|
790
897
|
content = updateFrontmatterField(content, "canonical", "false");
|
|
@@ -813,12 +920,12 @@ function runArchive(args2) {
|
|
|
813
920
|
}
|
|
814
921
|
|
|
815
922
|
// src/commands/audit.ts
|
|
816
|
-
import { existsSync as
|
|
817
|
-
import { join as
|
|
923
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2 } from "node:fs";
|
|
924
|
+
import { join as join8 } from "node:path";
|
|
818
925
|
|
|
819
926
|
// src/core/link-checker.ts
|
|
820
|
-
import { existsSync as
|
|
821
|
-
import { dirname as dirname4, extname, resolve as
|
|
927
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
|
|
928
|
+
import { dirname as dirname4, extname, resolve as resolve3 } from "node:path";
|
|
822
929
|
var CURRENT_MARKDOWN_ROOTS = ["AGENTS.md", "README.md", "docs"];
|
|
823
930
|
var CURRENT_DOC_DIR_PREFIXES = [
|
|
824
931
|
"docs/canon/",
|
|
@@ -898,12 +1005,12 @@ function shouldIgnoreTarget(target) {
|
|
|
898
1005
|
function localTargetExists(rootDir, sourcePath, target) {
|
|
899
1006
|
const pathPart = decodeTarget(target).split("#")[0]?.split("?")[0] ?? "";
|
|
900
1007
|
if (!pathPart) return true;
|
|
901
|
-
const resolved = pathPart.startsWith("/") ?
|
|
1008
|
+
const resolved = pathPart.startsWith("/") ? resolve3(rootDir, `.${pathPart}`) : resolve3(dirname4(sourcePath), pathPart);
|
|
902
1009
|
const candidates = [resolved];
|
|
903
1010
|
if (!extname(resolved)) {
|
|
904
1011
|
candidates.push(`${resolved}.md`);
|
|
905
1012
|
}
|
|
906
|
-
return candidates.some((candidate) =>
|
|
1013
|
+
return candidates.some((candidate) => existsSync5(candidate));
|
|
907
1014
|
}
|
|
908
1015
|
function decodeTarget(target) {
|
|
909
1016
|
try {
|
|
@@ -920,7 +1027,7 @@ function lineNumberAt(content, index) {
|
|
|
920
1027
|
return line;
|
|
921
1028
|
}
|
|
922
1029
|
function readText(filePath) {
|
|
923
|
-
return
|
|
1030
|
+
return readFileSync7(filePath, "utf8");
|
|
924
1031
|
}
|
|
925
1032
|
|
|
926
1033
|
// src/commands/audit.ts
|
|
@@ -934,15 +1041,15 @@ function runAudit() {
|
|
|
934
1041
|
}
|
|
935
1042
|
return 1;
|
|
936
1043
|
}
|
|
937
|
-
const migrationSource =
|
|
938
|
-
if (
|
|
1044
|
+
const migrationSource = join8(root, "Docs-for trans");
|
|
1045
|
+
if (existsSync6(migrationSource)) {
|
|
939
1046
|
const count = countFiles(migrationSource);
|
|
940
1047
|
warnings += 1;
|
|
941
1048
|
console.log(
|
|
942
1049
|
`Migration source still exists: Docs-for trans (${count} files). This was a one-time migration shell; it should not be reintroduced.`
|
|
943
1050
|
);
|
|
944
1051
|
}
|
|
945
|
-
if (
|
|
1052
|
+
if (existsSync6(join8(root, "DocSystemStarter.md"))) {
|
|
946
1053
|
warnings += 1;
|
|
947
1054
|
console.log(
|
|
948
1055
|
"Stray root-level DocSystemStarter.md exists. The original draft has been archived; remove or re-archive."
|
|
@@ -968,7 +1075,7 @@ function runAudit() {
|
|
|
968
1075
|
function countFiles(dir) {
|
|
969
1076
|
let count = 0;
|
|
970
1077
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
971
|
-
const path =
|
|
1078
|
+
const path = join8(dir, entry.name);
|
|
972
1079
|
if (entry.isDirectory()) count += countFiles(path);
|
|
973
1080
|
if (entry.isFile() || entry.isSymbolicLink()) count += 1;
|
|
974
1081
|
}
|
|
@@ -990,19 +1097,12 @@ function runCheck() {
|
|
|
990
1097
|
|
|
991
1098
|
// src/commands/doctor.ts
|
|
992
1099
|
import { spawnSync } from "node:child_process";
|
|
993
|
-
import { existsSync as
|
|
994
|
-
import { isAbsolute as isAbsolute2, join as
|
|
995
|
-
|
|
996
|
-
// src/core/router-integrity.ts
|
|
997
|
-
import { existsSync as existsSync6, lstatSync as lstatSync2, readlinkSync, readFileSync as readFileSync7 } from "node:fs";
|
|
998
|
-
import { join as join8, relative as relative3 } from "node:path";
|
|
999
|
-
|
|
1000
|
-
// src/core/symlinks.ts
|
|
1001
|
-
function normalizeSymlinkTarget(target) {
|
|
1002
|
-
return target.replaceAll("\\", "/");
|
|
1003
|
-
}
|
|
1100
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
1101
|
+
import { isAbsolute as isAbsolute2, join as join10, resolve as resolve4 } from "node:path";
|
|
1004
1102
|
|
|
1005
1103
|
// src/core/router-integrity.ts
|
|
1104
|
+
import { existsSync as existsSync7, lstatSync as lstatSync3, readlinkSync as readlinkSync2, readFileSync as readFileSync8 } from "node:fs";
|
|
1105
|
+
import { join as join9, relative as relative4 } from "node:path";
|
|
1006
1106
|
var CENTRAL_REQUIRED_FILES = [
|
|
1007
1107
|
"AGENTS.md",
|
|
1008
1108
|
"CLAUDE.md",
|
|
@@ -1320,7 +1420,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1320
1420
|
const requiredFiles = isCentral ? CENTRAL_REQUIRED_FILES : PROJECT_REQUIRED_FILES;
|
|
1321
1421
|
const requiredNeedles = isCentral ? CENTRAL_REQUIRED_NEEDLES : PROJECT_REQUIRED_NEEDLES;
|
|
1322
1422
|
for (const file of requiredFiles) {
|
|
1323
|
-
if (!
|
|
1423
|
+
if (!existsSync7(join9(rootDir, file))) {
|
|
1324
1424
|
issues.push({
|
|
1325
1425
|
file,
|
|
1326
1426
|
code: "missing-router-file",
|
|
@@ -1333,7 +1433,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1333
1433
|
}
|
|
1334
1434
|
issues.push(...validateHostSsot(rootDir));
|
|
1335
1435
|
for (const file of FORBIDDEN_LEGACY_PATHS) {
|
|
1336
|
-
if (
|
|
1436
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1337
1437
|
issues.push({
|
|
1338
1438
|
file,
|
|
1339
1439
|
code: "legacy-governance-path",
|
|
@@ -1343,7 +1443,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1343
1443
|
}
|
|
1344
1444
|
if (!isCentral) {
|
|
1345
1445
|
for (const file of FORBIDDEN_PROJECT_PATHS) {
|
|
1346
|
-
if (
|
|
1446
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1347
1447
|
issues.push({
|
|
1348
1448
|
file,
|
|
1349
1449
|
code: "legacy-project-policy-path",
|
|
@@ -1352,7 +1452,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1352
1452
|
}
|
|
1353
1453
|
}
|
|
1354
1454
|
for (const file of SUPERSEDED_PROJECT_CONTRACT_PATHS) {
|
|
1355
|
-
if (
|
|
1455
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1356
1456
|
issues.push({
|
|
1357
1457
|
file,
|
|
1358
1458
|
code: "superseded-project-contract",
|
|
@@ -1361,7 +1461,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1361
1461
|
}
|
|
1362
1462
|
}
|
|
1363
1463
|
for (const file of FORBIDDEN_PROJECT_ROOTS) {
|
|
1364
|
-
if (
|
|
1464
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1365
1465
|
issues.push({
|
|
1366
1466
|
file,
|
|
1367
1467
|
code: "project-root-integration-path",
|
|
@@ -1371,7 +1471,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1371
1471
|
}
|
|
1372
1472
|
} else {
|
|
1373
1473
|
for (const file of FORBIDDEN_CENTRAL_ROOTS) {
|
|
1374
|
-
if (
|
|
1474
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1375
1475
|
issues.push({
|
|
1376
1476
|
file,
|
|
1377
1477
|
code: "central-external-shared-rule-copy",
|
|
@@ -1388,9 +1488,9 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1388
1488
|
});
|
|
1389
1489
|
}
|
|
1390
1490
|
for (const requirement of requiredNeedles) {
|
|
1391
|
-
const path =
|
|
1392
|
-
if (!
|
|
1393
|
-
const content =
|
|
1491
|
+
const path = join9(rootDir, requirement.file);
|
|
1492
|
+
if (!existsSync7(path)) continue;
|
|
1493
|
+
const content = readFileSync8(path, "utf8");
|
|
1394
1494
|
if (!content.includes(requirement.needle)) {
|
|
1395
1495
|
issues.push({
|
|
1396
1496
|
file: requirement.file,
|
|
@@ -1436,7 +1536,7 @@ function validateHostSsot(rootDir) {
|
|
|
1436
1536
|
validateExactSymlink(".claude/skills", "../.agents/skills");
|
|
1437
1537
|
return issues;
|
|
1438
1538
|
function validateRegularFile(file) {
|
|
1439
|
-
const stat = safeLstat2(
|
|
1539
|
+
const stat = safeLstat2(join9(rootDir, file));
|
|
1440
1540
|
if (!stat || stat.isFile()) return;
|
|
1441
1541
|
issues.push({
|
|
1442
1542
|
file,
|
|
@@ -1445,7 +1545,7 @@ function validateHostSsot(rootDir) {
|
|
|
1445
1545
|
});
|
|
1446
1546
|
}
|
|
1447
1547
|
function validateDirectory(file) {
|
|
1448
|
-
const stat = safeLstat2(
|
|
1548
|
+
const stat = safeLstat2(join9(rootDir, file));
|
|
1449
1549
|
if (!stat || stat.isDirectory()) return;
|
|
1450
1550
|
issues.push({
|
|
1451
1551
|
file,
|
|
@@ -1454,7 +1554,7 @@ function validateHostSsot(rootDir) {
|
|
|
1454
1554
|
});
|
|
1455
1555
|
}
|
|
1456
1556
|
function validateExactSymlink(file, expectedRawTarget) {
|
|
1457
|
-
const path =
|
|
1557
|
+
const path = join9(rootDir, file);
|
|
1458
1558
|
const stat = safeLstat2(path);
|
|
1459
1559
|
if (!stat) return;
|
|
1460
1560
|
if (!stat.isSymbolicLink()) {
|
|
@@ -1465,7 +1565,7 @@ function validateHostSsot(rootDir) {
|
|
|
1465
1565
|
});
|
|
1466
1566
|
return;
|
|
1467
1567
|
}
|
|
1468
|
-
const rawTarget = normalizeSymlinkTarget(
|
|
1568
|
+
const rawTarget = normalizeSymlinkTarget(readlinkSync2(path));
|
|
1469
1569
|
if (rawTarget !== expectedRawTarget) {
|
|
1470
1570
|
issues.push({
|
|
1471
1571
|
file,
|
|
@@ -1477,7 +1577,7 @@ function validateHostSsot(rootDir) {
|
|
|
1477
1577
|
}
|
|
1478
1578
|
function safeLstat2(path) {
|
|
1479
1579
|
try {
|
|
1480
|
-
return
|
|
1580
|
+
return lstatSync3(path);
|
|
1481
1581
|
} catch (error) {
|
|
1482
1582
|
const code = error.code;
|
|
1483
1583
|
if (code === "ENOENT" || code === "ENOTDIR") return void 0;
|
|
@@ -1485,27 +1585,27 @@ function safeLstat2(path) {
|
|
|
1485
1585
|
}
|
|
1486
1586
|
}
|
|
1487
1587
|
function isCentralRepository(rootDir) {
|
|
1488
|
-
const packageJsonPath =
|
|
1489
|
-
if (!
|
|
1588
|
+
const packageJsonPath = join9(rootDir, "package.json");
|
|
1589
|
+
if (!existsSync7(packageJsonPath)) return false;
|
|
1490
1590
|
const packageName = readPackageName(packageJsonPath);
|
|
1491
1591
|
const centralPackageNames = /* @__PURE__ */ new Set(["project-governance-system", "pro-gov"]);
|
|
1492
1592
|
return centralPackageNames.has(packageName) && hasCentralRepositoryShape(rootDir);
|
|
1493
1593
|
}
|
|
1494
1594
|
function readPackageName(packageJsonPath) {
|
|
1495
1595
|
try {
|
|
1496
|
-
const packageJson = JSON.parse(
|
|
1596
|
+
const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
|
|
1497
1597
|
return typeof packageJson.name === "string" ? packageJson.name : "";
|
|
1498
1598
|
} catch {
|
|
1499
1599
|
return "";
|
|
1500
1600
|
}
|
|
1501
1601
|
}
|
|
1502
1602
|
function hasCentralRepositoryShape(rootDir) {
|
|
1503
|
-
return
|
|
1603
|
+
return existsSync7(join9(rootDir, "profiles")) && existsSync7(join9(rootDir, "starter")) && existsSync7(join9(rootDir, "integrations"));
|
|
1504
1604
|
}
|
|
1505
1605
|
function validateProjectAgentsRouting(rootDir) {
|
|
1506
1606
|
const issues = [];
|
|
1507
1607
|
const existingRoutes = PROJECT_AGENTS_ROUTING_FILES.filter(
|
|
1508
|
-
(file) =>
|
|
1608
|
+
(file) => existsSync7(join9(rootDir, file))
|
|
1509
1609
|
);
|
|
1510
1610
|
if (existingRoutes.length === 0) {
|
|
1511
1611
|
issues.push({
|
|
@@ -1515,9 +1615,9 @@ function validateProjectAgentsRouting(rootDir) {
|
|
|
1515
1615
|
});
|
|
1516
1616
|
return issues;
|
|
1517
1617
|
}
|
|
1518
|
-
const agentsPath =
|
|
1519
|
-
if (!
|
|
1520
|
-
const agents =
|
|
1618
|
+
const agentsPath = join9(rootDir, "AGENTS.md");
|
|
1619
|
+
if (!existsSync7(agentsPath)) return issues;
|
|
1620
|
+
const agents = readFileSync8(agentsPath, "utf8");
|
|
1521
1621
|
if (!existingRoutes.some((file) => agents.includes(file))) {
|
|
1522
1622
|
issues.push({
|
|
1523
1623
|
file: "AGENTS.md",
|
|
@@ -1528,9 +1628,9 @@ function validateProjectAgentsRouting(rootDir) {
|
|
|
1528
1628
|
return issues;
|
|
1529
1629
|
}
|
|
1530
1630
|
function validateRouterBlock(rootDir, file) {
|
|
1531
|
-
const path =
|
|
1532
|
-
if (!
|
|
1533
|
-
const content =
|
|
1631
|
+
const path = join9(rootDir, file);
|
|
1632
|
+
if (!existsSync7(path)) return [];
|
|
1633
|
+
const content = readFileSync8(path, "utf8");
|
|
1534
1634
|
const begin = content.indexOf(ROUTER_BLOCK_BEGIN);
|
|
1535
1635
|
const end = content.indexOf(ROUTER_BLOCK_END);
|
|
1536
1636
|
const issues = [];
|
|
@@ -1555,9 +1655,9 @@ function validateRouterBlock(rootDir, file) {
|
|
|
1555
1655
|
return issues;
|
|
1556
1656
|
}
|
|
1557
1657
|
function validateBacktickedLocalPaths(rootDir, file, pathRoot = "") {
|
|
1558
|
-
const path =
|
|
1559
|
-
if (!
|
|
1560
|
-
const content =
|
|
1658
|
+
const path = join9(rootDir, file);
|
|
1659
|
+
if (!existsSync7(path)) return [];
|
|
1660
|
+
const content = readFileSync8(path, "utf8");
|
|
1561
1661
|
const issues = [];
|
|
1562
1662
|
const seen = /* @__PURE__ */ new Set();
|
|
1563
1663
|
const matches = content.matchAll(/`([^`]+)`/g);
|
|
@@ -1567,7 +1667,10 @@ function validateBacktickedLocalPaths(rootDir, file, pathRoot = "") {
|
|
|
1567
1667
|
seen.add(value);
|
|
1568
1668
|
if (!isLocalPathReference(value)) continue;
|
|
1569
1669
|
const normalized = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
1570
|
-
|
|
1670
|
+
const candidatePaths = [join9(rootDir, pathRoot, normalized), join9(rootDir, normalized)];
|
|
1671
|
+
if (candidatePaths.some(
|
|
1672
|
+
(candidatePath) => existsSync7(candidatePath) || isUnresolvedManagedSharedRuleSymlink(rootDir, candidatePath)
|
|
1673
|
+
)) {
|
|
1571
1674
|
continue;
|
|
1572
1675
|
}
|
|
1573
1676
|
issues.push({
|
|
@@ -1586,9 +1689,9 @@ function isLocalPathReference(value) {
|
|
|
1586
1689
|
return value === "README.md" || value.endsWith(".md") || value.endsWith("/") || value.startsWith("docs/") || value.startsWith("starter/") || value.startsWith("profiles/") || value.startsWith("integrations/");
|
|
1587
1690
|
}
|
|
1588
1691
|
function validatePortableRouterText(rootDir, file) {
|
|
1589
|
-
const path =
|
|
1590
|
-
if (!
|
|
1591
|
-
const content =
|
|
1692
|
+
const path = join9(rootDir, file);
|
|
1693
|
+
if (!existsSync7(path)) return [];
|
|
1694
|
+
const content = readFileSync8(path, "utf8");
|
|
1592
1695
|
if (!hasNonPortablePath(content)) return [];
|
|
1593
1696
|
return [
|
|
1594
1697
|
{
|
|
@@ -1610,7 +1713,7 @@ function hasNonPortablePath(content) {
|
|
|
1610
1713
|
function findGovernedReadmes(rootDir) {
|
|
1611
1714
|
return listMarkdownFiles(rootDir, ["docs", "starter/docs"], {
|
|
1612
1715
|
includeFile: (repoPath) => repoPath.toLowerCase().endsWith("/readme.md")
|
|
1613
|
-
}).map((path) =>
|
|
1716
|
+
}).map((path) => relative4(rootDir, path).split(/\\/g).join("/")).filter((repoPath) => repoPath !== "README.md");
|
|
1614
1717
|
}
|
|
1615
1718
|
|
|
1616
1719
|
// src/commands/doctor.ts
|
|
@@ -1678,8 +1781,8 @@ function collectDoctorIssues(rootDir = process.cwd()) {
|
|
|
1678
1781
|
return issues;
|
|
1679
1782
|
}
|
|
1680
1783
|
function checkLefthook(rootDir) {
|
|
1681
|
-
const path =
|
|
1682
|
-
if (!
|
|
1784
|
+
const path = join10(rootDir, "lefthook.yml");
|
|
1785
|
+
if (!existsSync8(path)) {
|
|
1683
1786
|
return [
|
|
1684
1787
|
{
|
|
1685
1788
|
severity: "warning",
|
|
@@ -1688,7 +1791,7 @@ function checkLefthook(rootDir) {
|
|
|
1688
1791
|
}
|
|
1689
1792
|
];
|
|
1690
1793
|
}
|
|
1691
|
-
const content =
|
|
1794
|
+
const content = readFileSync9(path, "utf8");
|
|
1692
1795
|
const issues = [];
|
|
1693
1796
|
for (const command2 of [
|
|
1694
1797
|
"pnpm doc-gov router-check",
|
|
@@ -1725,8 +1828,8 @@ function checkLefthook(rootDir) {
|
|
|
1725
1828
|
return issues;
|
|
1726
1829
|
}
|
|
1727
1830
|
function checkDocsCheckWorkflow(rootDir) {
|
|
1728
|
-
const path =
|
|
1729
|
-
if (!
|
|
1831
|
+
const path = join10(rootDir, ".github/workflows/docs-check.yml");
|
|
1832
|
+
if (!existsSync8(path)) {
|
|
1730
1833
|
return [
|
|
1731
1834
|
{
|
|
1732
1835
|
severity: "warning",
|
|
@@ -1735,7 +1838,7 @@ function checkDocsCheckWorkflow(rootDir) {
|
|
|
1735
1838
|
}
|
|
1736
1839
|
];
|
|
1737
1840
|
}
|
|
1738
|
-
const content =
|
|
1841
|
+
const content = readFileSync9(path, "utf8");
|
|
1739
1842
|
const issues = [];
|
|
1740
1843
|
for (const command2 of [
|
|
1741
1844
|
"pnpm doc-gov router-check",
|
|
@@ -1755,15 +1858,15 @@ function checkDocsCheckWorkflow(rootDir) {
|
|
|
1755
1858
|
return issues;
|
|
1756
1859
|
}
|
|
1757
1860
|
function hookCallsLefthook(path) {
|
|
1758
|
-
return
|
|
1861
|
+
return existsSync8(path) && readFileSync9(path, "utf8").includes("lefthook");
|
|
1759
1862
|
}
|
|
1760
1863
|
function resolveGitHookPath(rootDir, hookName) {
|
|
1761
1864
|
const result = spawnSync("git", ["-C", rootDir, "rev-parse", "--git-path", `hooks/${hookName}`], {
|
|
1762
1865
|
encoding: "utf8"
|
|
1763
1866
|
});
|
|
1764
1867
|
const gitPath = result.status === 0 ? result.stdout.trim() : "";
|
|
1765
|
-
if (!gitPath) return
|
|
1766
|
-
return isAbsolute2(gitPath) ? gitPath :
|
|
1868
|
+
if (!gitPath) return join10(rootDir, ".git/hooks", hookName);
|
|
1869
|
+
return isAbsolute2(gitPath) ? gitPath : resolve4(rootDir, gitPath);
|
|
1767
1870
|
}
|
|
1768
1871
|
|
|
1769
1872
|
// src/commands/find.ts
|
|
@@ -1801,12 +1904,12 @@ function runFind(args2) {
|
|
|
1801
1904
|
}
|
|
1802
1905
|
|
|
1803
1906
|
// src/commands/init.ts
|
|
1804
|
-
import { existsSync as
|
|
1805
|
-
import { join as
|
|
1907
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1908
|
+
import { join as join12 } from "node:path";
|
|
1806
1909
|
|
|
1807
1910
|
// src/core/templates.ts
|
|
1808
|
-
import { existsSync as
|
|
1809
|
-
import { join as
|
|
1911
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1912
|
+
import { join as join11 } from "node:path";
|
|
1810
1913
|
var TEMPLATE_FILES = {
|
|
1811
1914
|
decision: "adr.md",
|
|
1812
1915
|
spec: "spec.md",
|
|
@@ -2014,19 +2117,19 @@ var DEFAULT_TEMPLATES = {
|
|
|
2014
2117
|
function loadTemplate(rootDir, type) {
|
|
2015
2118
|
const file = TEMPLATE_FILES[type];
|
|
2016
2119
|
if (!file) throw new Error(`No template file mapped for type: ${type}`);
|
|
2017
|
-
const path =
|
|
2018
|
-
if (
|
|
2120
|
+
const path = join11(rootDir, "docs/governance/templates", file);
|
|
2121
|
+
if (existsSync9(path)) return readFileSync10(path, "utf8");
|
|
2019
2122
|
const fallback = DEFAULT_TEMPLATES[file];
|
|
2020
2123
|
if (fallback) return fallback;
|
|
2021
2124
|
throw new Error(`Template file is missing: docs/governance/templates/${file}`);
|
|
2022
2125
|
}
|
|
2023
2126
|
function ensureDefaultTemplates(rootDir) {
|
|
2024
|
-
const templatesDir =
|
|
2127
|
+
const templatesDir = join11(rootDir, "docs/governance/templates");
|
|
2025
2128
|
mkdirSync3(templatesDir, { recursive: true });
|
|
2026
2129
|
let created = 0;
|
|
2027
2130
|
for (const [file, content] of Object.entries(DEFAULT_TEMPLATES)) {
|
|
2028
|
-
const path =
|
|
2029
|
-
if (
|
|
2131
|
+
const path = join11(templatesDir, file);
|
|
2132
|
+
if (existsSync9(path)) continue;
|
|
2030
2133
|
writeFileSync4(path, content);
|
|
2031
2134
|
created++;
|
|
2032
2135
|
}
|
|
@@ -2081,16 +2184,16 @@ function runInit(args2) {
|
|
|
2081
2184
|
];
|
|
2082
2185
|
let created = 0;
|
|
2083
2186
|
for (const dir of dirs) {
|
|
2084
|
-
const abs =
|
|
2085
|
-
if (!
|
|
2187
|
+
const abs = join12(root, dir);
|
|
2188
|
+
if (!existsSync10(abs)) {
|
|
2086
2189
|
mkdirSync4(abs, { recursive: true });
|
|
2087
2190
|
created++;
|
|
2088
2191
|
} else if (!force) {
|
|
2089
2192
|
}
|
|
2090
2193
|
}
|
|
2091
2194
|
for (const dir of ["docs/specs/completed", "docs/plans/completed", "docs/archive"]) {
|
|
2092
|
-
const keep =
|
|
2093
|
-
if (!
|
|
2195
|
+
const keep = join12(root, dir, ".gitkeep");
|
|
2196
|
+
if (!existsSync10(keep)) writeFileSync5(keep, "");
|
|
2094
2197
|
}
|
|
2095
2198
|
const templatesCreated = ensureDefaultTemplates(root);
|
|
2096
2199
|
console.log(
|
|
@@ -2137,8 +2240,8 @@ function runLinks() {
|
|
|
2137
2240
|
}
|
|
2138
2241
|
|
|
2139
2242
|
// src/commands/migrate.ts
|
|
2140
|
-
import { existsSync as
|
|
2141
|
-
import { join as
|
|
2243
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "node:fs";
|
|
2244
|
+
import { join as join13 } from "node:path";
|
|
2142
2245
|
var PROFILE_ROUTES = {
|
|
2143
2246
|
"engineering-runtime": "docs/governance/agents-routing/engineering-runtime-v1.1.md",
|
|
2144
2247
|
"doc-only": "docs/governance/agents-routing/doc-only-v1.1.md"
|
|
@@ -2175,11 +2278,11 @@ function checkMigrationReadiness(rootDir, profile) {
|
|
|
2175
2278
|
issues.push(`${issue.file}: ${issue.code}: ${issue.message}`);
|
|
2176
2279
|
}
|
|
2177
2280
|
const route = PROFILE_ROUTES[profile];
|
|
2178
|
-
if (!
|
|
2281
|
+
if (!existsSync11(join13(rootDir, route))) {
|
|
2179
2282
|
issues.push(`missing selected profile route: ${route}`);
|
|
2180
2283
|
}
|
|
2181
|
-
const agentsPath =
|
|
2182
|
-
if (
|
|
2284
|
+
const agentsPath = join13(rootDir, "AGENTS.md");
|
|
2285
|
+
if (existsSync11(agentsPath) && !readFileSync11(agentsPath, "utf8").includes(route)) {
|
|
2183
2286
|
issues.push(`AGENTS.md must name selected profile route: ${route}`);
|
|
2184
2287
|
}
|
|
2185
2288
|
return issues;
|
|
@@ -2226,8 +2329,8 @@ function readFlag(args2, name) {
|
|
|
2226
2329
|
}
|
|
2227
2330
|
|
|
2228
2331
|
// src/commands/new.ts
|
|
2229
|
-
import { existsSync as
|
|
2230
|
-
import { dirname as dirname5, join as
|
|
2332
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2333
|
+
import { dirname as dirname5, join as join14 } from "node:path";
|
|
2231
2334
|
function runNew(args2) {
|
|
2232
2335
|
const positional = args2.filter((a) => !a.startsWith("--"));
|
|
2233
2336
|
const owner = readFlag2(args2, "--owner") ?? "human";
|
|
@@ -2253,8 +2356,8 @@ function runNew(args2) {
|
|
|
2253
2356
|
console.error(err.message);
|
|
2254
2357
|
return 1;
|
|
2255
2358
|
}
|
|
2256
|
-
const absPath =
|
|
2257
|
-
if (
|
|
2359
|
+
const absPath = join14(root, plan.filePath);
|
|
2360
|
+
if (existsSync12(absPath) && !force) {
|
|
2258
2361
|
console.error(`File already exists: ${plan.filePath}. Use --force to overwrite.`);
|
|
2259
2362
|
return 1;
|
|
2260
2363
|
}
|
|
@@ -2337,8 +2440,8 @@ function runScan(args2) {
|
|
|
2337
2440
|
}
|
|
2338
2441
|
|
|
2339
2442
|
// src/commands/supersede.ts
|
|
2340
|
-
import { readFileSync as
|
|
2341
|
-
import { join as
|
|
2443
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2444
|
+
import { join as join15 } from "node:path";
|
|
2342
2445
|
function runSupersede(args2) {
|
|
2343
2446
|
const [oldId, newId] = args2;
|
|
2344
2447
|
if (!oldId || !newId) {
|
|
@@ -2374,15 +2477,15 @@ function runSupersede(args2) {
|
|
|
2374
2477
|
return 1;
|
|
2375
2478
|
}
|
|
2376
2479
|
const today = todayIso();
|
|
2377
|
-
const oldPath =
|
|
2378
|
-
let oldContent =
|
|
2480
|
+
const oldPath = join15(root, oldRec.path);
|
|
2481
|
+
let oldContent = readFileSync12(oldPath, "utf8");
|
|
2379
2482
|
oldContent = updateFrontmatterField(oldContent, "status", "superseded");
|
|
2380
2483
|
oldContent = updateFrontmatterField(oldContent, "canonical", "false");
|
|
2381
2484
|
oldContent = updateFrontmatterField(oldContent, "superseded_by", newId);
|
|
2382
2485
|
oldContent = updateFrontmatterField(oldContent, "last_reviewed", today);
|
|
2383
2486
|
writeFileSync7(oldPath, oldContent);
|
|
2384
|
-
const newPath =
|
|
2385
|
-
let newContent =
|
|
2487
|
+
const newPath = join15(root, newRec.path);
|
|
2488
|
+
let newContent = readFileSync12(newPath, "utf8");
|
|
2386
2489
|
newContent = appendToFrontmatterList(newContent, "supersedes", newId === oldId ? "" : oldId);
|
|
2387
2490
|
newContent = updateFrontmatterField(newContent, "last_reviewed", today);
|
|
2388
2491
|
writeFileSync7(newPath, newContent);
|
|
@@ -2431,13 +2534,13 @@ ${lines.join("\n")}${tail}`;
|
|
|
2431
2534
|
|
|
2432
2535
|
// src/commands/verify-commit-msg.ts
|
|
2433
2536
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2434
|
-
import { existsSync as
|
|
2537
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
|
|
2435
2538
|
function runVerifyCommitMsg(args2) {
|
|
2436
2539
|
const msgFile = args2[0];
|
|
2437
|
-
if (!msgFile || !
|
|
2540
|
+
if (!msgFile || !existsSync13(msgFile)) {
|
|
2438
2541
|
return 0;
|
|
2439
2542
|
}
|
|
2440
|
-
const message =
|
|
2543
|
+
const message = readFileSync13(msgFile, "utf8");
|
|
2441
2544
|
if (/^Merge\b/m.test(message) || /^Revert\b/m.test(message)) return 0;
|
|
2442
2545
|
let stagedFiles = [];
|
|
2443
2546
|
try {
|
|
@@ -2449,7 +2552,7 @@ function runVerifyCommitMsg(args2) {
|
|
|
2449
2552
|
}
|
|
2450
2553
|
const failures = [];
|
|
2451
2554
|
for (const file of stagedFiles) {
|
|
2452
|
-
if (!
|
|
2555
|
+
if (!existsSync13(file)) continue;
|
|
2453
2556
|
const fm = readFrontmatterFile(file);
|
|
2454
2557
|
if (!fm) continue;
|
|
2455
2558
|
const id = stringValue(fm.data.id);
|