@pieai/doc-gov 0.9.6 → 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 +217 -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,93 @@ 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_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
|
+
}
|
|
327
|
+
|
|
241
328
|
// src/core/lifecycle.ts
|
|
242
329
|
var normalStatuses = [
|
|
243
330
|
"draft",
|
|
@@ -405,6 +492,24 @@ function checkDocs(rootDir = process.cwd()) {
|
|
|
405
492
|
issues.push(...result.issues);
|
|
406
493
|
if (result.record) records.push(result.record);
|
|
407
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
|
+
}
|
|
408
513
|
issues.push(...validateGlobalIntegrity(records));
|
|
409
514
|
return {
|
|
410
515
|
ok: issues.length === 0,
|
|
@@ -510,8 +615,8 @@ function pathToRepo(rootDir, path) {
|
|
|
510
615
|
}
|
|
511
616
|
|
|
512
617
|
// src/core/paths.ts
|
|
513
|
-
import { existsSync as
|
|
514
|
-
import { basename, join as
|
|
618
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
619
|
+
import { basename, join as join4 } from "node:path";
|
|
515
620
|
function planPath(rootDir, type, slugInput) {
|
|
516
621
|
const cleanSlug = slugInput.replace(/^\/+|\/+$/g, "");
|
|
517
622
|
if (!cleanSlug) throw new Error("Slug is required.");
|
|
@@ -580,8 +685,8 @@ function planPath(rootDir, type, slugInput) {
|
|
|
580
685
|
throw new Error(`Unknown type: ${type}`);
|
|
581
686
|
}
|
|
582
687
|
function nextSerial(rootDir, scanDir, regex) {
|
|
583
|
-
const root =
|
|
584
|
-
if (!
|
|
688
|
+
const root = join4(rootDir, scanDir);
|
|
689
|
+
if (!existsSync3(root)) return "0001";
|
|
585
690
|
let max = 0;
|
|
586
691
|
walkSerial(rootDir, scanDir, regex, (n) => {
|
|
587
692
|
if (n > max) max = n;
|
|
@@ -610,8 +715,8 @@ function todayIso() {
|
|
|
610
715
|
}
|
|
611
716
|
|
|
612
717
|
// src/core/manifest.ts
|
|
613
|
-
import { existsSync as
|
|
614
|
-
import { dirname as dirname2, 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";
|
|
615
720
|
function buildManifest(rootDir = process.cwd()) {
|
|
616
721
|
const result = checkDocs(rootDir);
|
|
617
722
|
if (!result.ok) {
|
|
@@ -623,14 +728,14 @@ ${error}`);
|
|
|
623
728
|
}
|
|
624
729
|
function writeManifest(rootDir = process.cwd()) {
|
|
625
730
|
const manifest = buildManifest(rootDir);
|
|
626
|
-
const path =
|
|
731
|
+
const path = join5(rootDir, "docs/governance/MANIFEST.yml");
|
|
627
732
|
mkdirSync(dirname2(path), { recursive: true });
|
|
628
733
|
writeFileSync(path, manifest);
|
|
629
734
|
}
|
|
630
735
|
function manifestInSync(rootDir = process.cwd()) {
|
|
631
|
-
const path =
|
|
632
|
-
if (!
|
|
633
|
-
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));
|
|
634
739
|
}
|
|
635
740
|
function normalizeManifest(value) {
|
|
636
741
|
return value.replace(/^generated_at: .*$/m, "generated_at: <ignored>").replace(/^generator_version: .*$/m, "generator_version: <ignored>");
|
|
@@ -659,7 +764,7 @@ function renderManifest(records) {
|
|
|
659
764
|
function readPackageVersion() {
|
|
660
765
|
for (const candidate of ["../package.json", "../../package.json"]) {
|
|
661
766
|
try {
|
|
662
|
-
const packageJson = JSON.parse(
|
|
767
|
+
const packageJson = JSON.parse(readFileSync4(new URL(candidate, import.meta.url), "utf8"));
|
|
663
768
|
if (typeof packageJson.version === "string") return packageJson.version;
|
|
664
769
|
} catch {
|
|
665
770
|
}
|
|
@@ -695,8 +800,8 @@ function runApprove(args2) {
|
|
|
695
800
|
);
|
|
696
801
|
return 1;
|
|
697
802
|
}
|
|
698
|
-
const filePath =
|
|
699
|
-
const content =
|
|
803
|
+
const filePath = join6(root, record.path);
|
|
804
|
+
const content = readFileSync5(filePath, "utf8");
|
|
700
805
|
let next = content;
|
|
701
806
|
next = updateFrontmatterField(next, "status", toStatus);
|
|
702
807
|
next = updateFrontmatterField(next, "canonical", "true");
|
|
@@ -742,8 +847,8 @@ ${newLines.join("\n")}${tail}`;
|
|
|
742
847
|
|
|
743
848
|
// src/commands/archive.ts
|
|
744
849
|
import { execFileSync } from "node:child_process";
|
|
745
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
746
|
-
import { basename as basename2, dirname as dirname3, join 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";
|
|
747
852
|
function runArchive(args2) {
|
|
748
853
|
const positional = args2.filter((a) => !a.startsWith("--"));
|
|
749
854
|
const id = positional[0];
|
|
@@ -782,9 +887,9 @@ function runArchive(args2) {
|
|
|
782
887
|
const fileName = basename2(oldPath);
|
|
783
888
|
const archiveDir = `docs/archive/${quarterTag()}-${record.type}`;
|
|
784
889
|
const newPath = `${archiveDir}/${fileName}`;
|
|
785
|
-
const absNew =
|
|
786
|
-
const absOld =
|
|
787
|
-
let content =
|
|
890
|
+
const absNew = join7(root, newPath);
|
|
891
|
+
const absOld = join7(root, oldPath);
|
|
892
|
+
let content = readFileSync6(absOld, "utf8");
|
|
788
893
|
content = updateFrontmatterField(content, "type", "archive");
|
|
789
894
|
content = updateFrontmatterField(content, "status", "archived");
|
|
790
895
|
content = updateFrontmatterField(content, "canonical", "false");
|
|
@@ -813,12 +918,12 @@ function runArchive(args2) {
|
|
|
813
918
|
}
|
|
814
919
|
|
|
815
920
|
// src/commands/audit.ts
|
|
816
|
-
import { existsSync as
|
|
817
|
-
import { join as
|
|
921
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2 } from "node:fs";
|
|
922
|
+
import { join as join8 } from "node:path";
|
|
818
923
|
|
|
819
924
|
// src/core/link-checker.ts
|
|
820
|
-
import { existsSync as
|
|
821
|
-
import { dirname as dirname4, extname, resolve as
|
|
925
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
|
|
926
|
+
import { dirname as dirname4, extname, resolve as resolve3 } from "node:path";
|
|
822
927
|
var CURRENT_MARKDOWN_ROOTS = ["AGENTS.md", "README.md", "docs"];
|
|
823
928
|
var CURRENT_DOC_DIR_PREFIXES = [
|
|
824
929
|
"docs/canon/",
|
|
@@ -898,12 +1003,12 @@ function shouldIgnoreTarget(target) {
|
|
|
898
1003
|
function localTargetExists(rootDir, sourcePath, target) {
|
|
899
1004
|
const pathPart = decodeTarget(target).split("#")[0]?.split("?")[0] ?? "";
|
|
900
1005
|
if (!pathPart) return true;
|
|
901
|
-
const resolved = pathPart.startsWith("/") ?
|
|
1006
|
+
const resolved = pathPart.startsWith("/") ? resolve3(rootDir, `.${pathPart}`) : resolve3(dirname4(sourcePath), pathPart);
|
|
902
1007
|
const candidates = [resolved];
|
|
903
1008
|
if (!extname(resolved)) {
|
|
904
1009
|
candidates.push(`${resolved}.md`);
|
|
905
1010
|
}
|
|
906
|
-
return candidates.some((candidate) =>
|
|
1011
|
+
return candidates.some((candidate) => existsSync5(candidate));
|
|
907
1012
|
}
|
|
908
1013
|
function decodeTarget(target) {
|
|
909
1014
|
try {
|
|
@@ -920,7 +1025,7 @@ function lineNumberAt(content, index) {
|
|
|
920
1025
|
return line;
|
|
921
1026
|
}
|
|
922
1027
|
function readText(filePath) {
|
|
923
|
-
return
|
|
1028
|
+
return readFileSync7(filePath, "utf8");
|
|
924
1029
|
}
|
|
925
1030
|
|
|
926
1031
|
// src/commands/audit.ts
|
|
@@ -934,15 +1039,15 @@ function runAudit() {
|
|
|
934
1039
|
}
|
|
935
1040
|
return 1;
|
|
936
1041
|
}
|
|
937
|
-
const migrationSource =
|
|
938
|
-
if (
|
|
1042
|
+
const migrationSource = join8(root, "Docs-for trans");
|
|
1043
|
+
if (existsSync6(migrationSource)) {
|
|
939
1044
|
const count = countFiles(migrationSource);
|
|
940
1045
|
warnings += 1;
|
|
941
1046
|
console.log(
|
|
942
1047
|
`Migration source still exists: Docs-for trans (${count} files). This was a one-time migration shell; it should not be reintroduced.`
|
|
943
1048
|
);
|
|
944
1049
|
}
|
|
945
|
-
if (
|
|
1050
|
+
if (existsSync6(join8(root, "DocSystemStarter.md"))) {
|
|
946
1051
|
warnings += 1;
|
|
947
1052
|
console.log(
|
|
948
1053
|
"Stray root-level DocSystemStarter.md exists. The original draft has been archived; remove or re-archive."
|
|
@@ -968,7 +1073,7 @@ function runAudit() {
|
|
|
968
1073
|
function countFiles(dir) {
|
|
969
1074
|
let count = 0;
|
|
970
1075
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
971
|
-
const path =
|
|
1076
|
+
const path = join8(dir, entry.name);
|
|
972
1077
|
if (entry.isDirectory()) count += countFiles(path);
|
|
973
1078
|
if (entry.isFile() || entry.isSymbolicLink()) count += 1;
|
|
974
1079
|
}
|
|
@@ -990,19 +1095,12 @@ function runCheck() {
|
|
|
990
1095
|
|
|
991
1096
|
// src/commands/doctor.ts
|
|
992
1097
|
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
|
-
}
|
|
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";
|
|
1004
1100
|
|
|
1005
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";
|
|
1006
1104
|
var CENTRAL_REQUIRED_FILES = [
|
|
1007
1105
|
"AGENTS.md",
|
|
1008
1106
|
"CLAUDE.md",
|
|
@@ -1320,7 +1418,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1320
1418
|
const requiredFiles = isCentral ? CENTRAL_REQUIRED_FILES : PROJECT_REQUIRED_FILES;
|
|
1321
1419
|
const requiredNeedles = isCentral ? CENTRAL_REQUIRED_NEEDLES : PROJECT_REQUIRED_NEEDLES;
|
|
1322
1420
|
for (const file of requiredFiles) {
|
|
1323
|
-
if (!
|
|
1421
|
+
if (!existsSync7(join9(rootDir, file))) {
|
|
1324
1422
|
issues.push({
|
|
1325
1423
|
file,
|
|
1326
1424
|
code: "missing-router-file",
|
|
@@ -1333,7 +1431,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1333
1431
|
}
|
|
1334
1432
|
issues.push(...validateHostSsot(rootDir));
|
|
1335
1433
|
for (const file of FORBIDDEN_LEGACY_PATHS) {
|
|
1336
|
-
if (
|
|
1434
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1337
1435
|
issues.push({
|
|
1338
1436
|
file,
|
|
1339
1437
|
code: "legacy-governance-path",
|
|
@@ -1343,7 +1441,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1343
1441
|
}
|
|
1344
1442
|
if (!isCentral) {
|
|
1345
1443
|
for (const file of FORBIDDEN_PROJECT_PATHS) {
|
|
1346
|
-
if (
|
|
1444
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1347
1445
|
issues.push({
|
|
1348
1446
|
file,
|
|
1349
1447
|
code: "legacy-project-policy-path",
|
|
@@ -1352,7 +1450,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1352
1450
|
}
|
|
1353
1451
|
}
|
|
1354
1452
|
for (const file of SUPERSEDED_PROJECT_CONTRACT_PATHS) {
|
|
1355
|
-
if (
|
|
1453
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1356
1454
|
issues.push({
|
|
1357
1455
|
file,
|
|
1358
1456
|
code: "superseded-project-contract",
|
|
@@ -1361,7 +1459,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1361
1459
|
}
|
|
1362
1460
|
}
|
|
1363
1461
|
for (const file of FORBIDDEN_PROJECT_ROOTS) {
|
|
1364
|
-
if (
|
|
1462
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1365
1463
|
issues.push({
|
|
1366
1464
|
file,
|
|
1367
1465
|
code: "project-root-integration-path",
|
|
@@ -1371,7 +1469,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1371
1469
|
}
|
|
1372
1470
|
} else {
|
|
1373
1471
|
for (const file of FORBIDDEN_CENTRAL_ROOTS) {
|
|
1374
|
-
if (
|
|
1472
|
+
if (existsSync7(join9(rootDir, file))) {
|
|
1375
1473
|
issues.push({
|
|
1376
1474
|
file,
|
|
1377
1475
|
code: "central-external-shared-rule-copy",
|
|
@@ -1388,9 +1486,9 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
|
|
|
1388
1486
|
});
|
|
1389
1487
|
}
|
|
1390
1488
|
for (const requirement of requiredNeedles) {
|
|
1391
|
-
const path =
|
|
1392
|
-
if (!
|
|
1393
|
-
const content =
|
|
1489
|
+
const path = join9(rootDir, requirement.file);
|
|
1490
|
+
if (!existsSync7(path)) continue;
|
|
1491
|
+
const content = readFileSync8(path, "utf8");
|
|
1394
1492
|
if (!content.includes(requirement.needle)) {
|
|
1395
1493
|
issues.push({
|
|
1396
1494
|
file: requirement.file,
|
|
@@ -1436,7 +1534,7 @@ function validateHostSsot(rootDir) {
|
|
|
1436
1534
|
validateExactSymlink(".claude/skills", "../.agents/skills");
|
|
1437
1535
|
return issues;
|
|
1438
1536
|
function validateRegularFile(file) {
|
|
1439
|
-
const stat = safeLstat2(
|
|
1537
|
+
const stat = safeLstat2(join9(rootDir, file));
|
|
1440
1538
|
if (!stat || stat.isFile()) return;
|
|
1441
1539
|
issues.push({
|
|
1442
1540
|
file,
|
|
@@ -1445,7 +1543,7 @@ function validateHostSsot(rootDir) {
|
|
|
1445
1543
|
});
|
|
1446
1544
|
}
|
|
1447
1545
|
function validateDirectory(file) {
|
|
1448
|
-
const stat = safeLstat2(
|
|
1546
|
+
const stat = safeLstat2(join9(rootDir, file));
|
|
1449
1547
|
if (!stat || stat.isDirectory()) return;
|
|
1450
1548
|
issues.push({
|
|
1451
1549
|
file,
|
|
@@ -1454,7 +1552,7 @@ function validateHostSsot(rootDir) {
|
|
|
1454
1552
|
});
|
|
1455
1553
|
}
|
|
1456
1554
|
function validateExactSymlink(file, expectedRawTarget) {
|
|
1457
|
-
const path =
|
|
1555
|
+
const path = join9(rootDir, file);
|
|
1458
1556
|
const stat = safeLstat2(path);
|
|
1459
1557
|
if (!stat) return;
|
|
1460
1558
|
if (!stat.isSymbolicLink()) {
|
|
@@ -1465,7 +1563,7 @@ function validateHostSsot(rootDir) {
|
|
|
1465
1563
|
});
|
|
1466
1564
|
return;
|
|
1467
1565
|
}
|
|
1468
|
-
const rawTarget = normalizeSymlinkTarget(
|
|
1566
|
+
const rawTarget = normalizeSymlinkTarget(readlinkSync2(path));
|
|
1469
1567
|
if (rawTarget !== expectedRawTarget) {
|
|
1470
1568
|
issues.push({
|
|
1471
1569
|
file,
|
|
@@ -1477,7 +1575,7 @@ function validateHostSsot(rootDir) {
|
|
|
1477
1575
|
}
|
|
1478
1576
|
function safeLstat2(path) {
|
|
1479
1577
|
try {
|
|
1480
|
-
return
|
|
1578
|
+
return lstatSync3(path);
|
|
1481
1579
|
} catch (error) {
|
|
1482
1580
|
const code = error.code;
|
|
1483
1581
|
if (code === "ENOENT" || code === "ENOTDIR") return void 0;
|
|
@@ -1485,27 +1583,27 @@ function safeLstat2(path) {
|
|
|
1485
1583
|
}
|
|
1486
1584
|
}
|
|
1487
1585
|
function isCentralRepository(rootDir) {
|
|
1488
|
-
const packageJsonPath =
|
|
1489
|
-
if (!
|
|
1586
|
+
const packageJsonPath = join9(rootDir, "package.json");
|
|
1587
|
+
if (!existsSync7(packageJsonPath)) return false;
|
|
1490
1588
|
const packageName = readPackageName(packageJsonPath);
|
|
1491
1589
|
const centralPackageNames = /* @__PURE__ */ new Set(["project-governance-system", "pro-gov"]);
|
|
1492
1590
|
return centralPackageNames.has(packageName) && hasCentralRepositoryShape(rootDir);
|
|
1493
1591
|
}
|
|
1494
1592
|
function readPackageName(packageJsonPath) {
|
|
1495
1593
|
try {
|
|
1496
|
-
const packageJson = JSON.parse(
|
|
1594
|
+
const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
|
|
1497
1595
|
return typeof packageJson.name === "string" ? packageJson.name : "";
|
|
1498
1596
|
} catch {
|
|
1499
1597
|
return "";
|
|
1500
1598
|
}
|
|
1501
1599
|
}
|
|
1502
1600
|
function hasCentralRepositoryShape(rootDir) {
|
|
1503
|
-
return
|
|
1601
|
+
return existsSync7(join9(rootDir, "profiles")) && existsSync7(join9(rootDir, "starter")) && existsSync7(join9(rootDir, "integrations"));
|
|
1504
1602
|
}
|
|
1505
1603
|
function validateProjectAgentsRouting(rootDir) {
|
|
1506
1604
|
const issues = [];
|
|
1507
1605
|
const existingRoutes = PROJECT_AGENTS_ROUTING_FILES.filter(
|
|
1508
|
-
(file) =>
|
|
1606
|
+
(file) => existsSync7(join9(rootDir, file))
|
|
1509
1607
|
);
|
|
1510
1608
|
if (existingRoutes.length === 0) {
|
|
1511
1609
|
issues.push({
|
|
@@ -1515,9 +1613,9 @@ function validateProjectAgentsRouting(rootDir) {
|
|
|
1515
1613
|
});
|
|
1516
1614
|
return issues;
|
|
1517
1615
|
}
|
|
1518
|
-
const agentsPath =
|
|
1519
|
-
if (!
|
|
1520
|
-
const agents =
|
|
1616
|
+
const agentsPath = join9(rootDir, "AGENTS.md");
|
|
1617
|
+
if (!existsSync7(agentsPath)) return issues;
|
|
1618
|
+
const agents = readFileSync8(agentsPath, "utf8");
|
|
1521
1619
|
if (!existingRoutes.some((file) => agents.includes(file))) {
|
|
1522
1620
|
issues.push({
|
|
1523
1621
|
file: "AGENTS.md",
|
|
@@ -1528,9 +1626,9 @@ function validateProjectAgentsRouting(rootDir) {
|
|
|
1528
1626
|
return issues;
|
|
1529
1627
|
}
|
|
1530
1628
|
function validateRouterBlock(rootDir, file) {
|
|
1531
|
-
const path =
|
|
1532
|
-
if (!
|
|
1533
|
-
const content =
|
|
1629
|
+
const path = join9(rootDir, file);
|
|
1630
|
+
if (!existsSync7(path)) return [];
|
|
1631
|
+
const content = readFileSync8(path, "utf8");
|
|
1534
1632
|
const begin = content.indexOf(ROUTER_BLOCK_BEGIN);
|
|
1535
1633
|
const end = content.indexOf(ROUTER_BLOCK_END);
|
|
1536
1634
|
const issues = [];
|
|
@@ -1555,9 +1653,9 @@ function validateRouterBlock(rootDir, file) {
|
|
|
1555
1653
|
return issues;
|
|
1556
1654
|
}
|
|
1557
1655
|
function validateBacktickedLocalPaths(rootDir, file, pathRoot = "") {
|
|
1558
|
-
const path =
|
|
1559
|
-
if (!
|
|
1560
|
-
const content =
|
|
1656
|
+
const path = join9(rootDir, file);
|
|
1657
|
+
if (!existsSync7(path)) return [];
|
|
1658
|
+
const content = readFileSync8(path, "utf8");
|
|
1561
1659
|
const issues = [];
|
|
1562
1660
|
const seen = /* @__PURE__ */ new Set();
|
|
1563
1661
|
const matches = content.matchAll(/`([^`]+)`/g);
|
|
@@ -1567,7 +1665,10 @@ function validateBacktickedLocalPaths(rootDir, file, pathRoot = "") {
|
|
|
1567
1665
|
seen.add(value);
|
|
1568
1666
|
if (!isLocalPathReference(value)) continue;
|
|
1569
1667
|
const normalized = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
1570
|
-
|
|
1668
|
+
const candidatePaths = [join9(rootDir, pathRoot, normalized), join9(rootDir, normalized)];
|
|
1669
|
+
if (candidatePaths.some(
|
|
1670
|
+
(candidatePath) => existsSync7(candidatePath) || isUnresolvedManagedSharedRuleSymlink(rootDir, candidatePath)
|
|
1671
|
+
)) {
|
|
1571
1672
|
continue;
|
|
1572
1673
|
}
|
|
1573
1674
|
issues.push({
|
|
@@ -1586,9 +1687,9 @@ function isLocalPathReference(value) {
|
|
|
1586
1687
|
return value === "README.md" || value.endsWith(".md") || value.endsWith("/") || value.startsWith("docs/") || value.startsWith("starter/") || value.startsWith("profiles/") || value.startsWith("integrations/");
|
|
1587
1688
|
}
|
|
1588
1689
|
function validatePortableRouterText(rootDir, file) {
|
|
1589
|
-
const path =
|
|
1590
|
-
if (!
|
|
1591
|
-
const content =
|
|
1690
|
+
const path = join9(rootDir, file);
|
|
1691
|
+
if (!existsSync7(path)) return [];
|
|
1692
|
+
const content = readFileSync8(path, "utf8");
|
|
1592
1693
|
if (!hasNonPortablePath(content)) return [];
|
|
1593
1694
|
return [
|
|
1594
1695
|
{
|
|
@@ -1610,7 +1711,7 @@ function hasNonPortablePath(content) {
|
|
|
1610
1711
|
function findGovernedReadmes(rootDir) {
|
|
1611
1712
|
return listMarkdownFiles(rootDir, ["docs", "starter/docs"], {
|
|
1612
1713
|
includeFile: (repoPath) => repoPath.toLowerCase().endsWith("/readme.md")
|
|
1613
|
-
}).map((path) =>
|
|
1714
|
+
}).map((path) => relative4(rootDir, path).split(/\\/g).join("/")).filter((repoPath) => repoPath !== "README.md");
|
|
1614
1715
|
}
|
|
1615
1716
|
|
|
1616
1717
|
// src/commands/doctor.ts
|
|
@@ -1678,8 +1779,8 @@ function collectDoctorIssues(rootDir = process.cwd()) {
|
|
|
1678
1779
|
return issues;
|
|
1679
1780
|
}
|
|
1680
1781
|
function checkLefthook(rootDir) {
|
|
1681
|
-
const path =
|
|
1682
|
-
if (!
|
|
1782
|
+
const path = join10(rootDir, "lefthook.yml");
|
|
1783
|
+
if (!existsSync8(path)) {
|
|
1683
1784
|
return [
|
|
1684
1785
|
{
|
|
1685
1786
|
severity: "warning",
|
|
@@ -1688,7 +1789,7 @@ function checkLefthook(rootDir) {
|
|
|
1688
1789
|
}
|
|
1689
1790
|
];
|
|
1690
1791
|
}
|
|
1691
|
-
const content =
|
|
1792
|
+
const content = readFileSync9(path, "utf8");
|
|
1692
1793
|
const issues = [];
|
|
1693
1794
|
for (const command2 of [
|
|
1694
1795
|
"pnpm doc-gov router-check",
|
|
@@ -1725,8 +1826,8 @@ function checkLefthook(rootDir) {
|
|
|
1725
1826
|
return issues;
|
|
1726
1827
|
}
|
|
1727
1828
|
function checkDocsCheckWorkflow(rootDir) {
|
|
1728
|
-
const path =
|
|
1729
|
-
if (!
|
|
1829
|
+
const path = join10(rootDir, ".github/workflows/docs-check.yml");
|
|
1830
|
+
if (!existsSync8(path)) {
|
|
1730
1831
|
return [
|
|
1731
1832
|
{
|
|
1732
1833
|
severity: "warning",
|
|
@@ -1735,7 +1836,7 @@ function checkDocsCheckWorkflow(rootDir) {
|
|
|
1735
1836
|
}
|
|
1736
1837
|
];
|
|
1737
1838
|
}
|
|
1738
|
-
const content =
|
|
1839
|
+
const content = readFileSync9(path, "utf8");
|
|
1739
1840
|
const issues = [];
|
|
1740
1841
|
for (const command2 of [
|
|
1741
1842
|
"pnpm doc-gov router-check",
|
|
@@ -1755,15 +1856,15 @@ function checkDocsCheckWorkflow(rootDir) {
|
|
|
1755
1856
|
return issues;
|
|
1756
1857
|
}
|
|
1757
1858
|
function hookCallsLefthook(path) {
|
|
1758
|
-
return
|
|
1859
|
+
return existsSync8(path) && readFileSync9(path, "utf8").includes("lefthook");
|
|
1759
1860
|
}
|
|
1760
1861
|
function resolveGitHookPath(rootDir, hookName) {
|
|
1761
1862
|
const result = spawnSync("git", ["-C", rootDir, "rev-parse", "--git-path", `hooks/${hookName}`], {
|
|
1762
1863
|
encoding: "utf8"
|
|
1763
1864
|
});
|
|
1764
1865
|
const gitPath = result.status === 0 ? result.stdout.trim() : "";
|
|
1765
|
-
if (!gitPath) return
|
|
1766
|
-
return isAbsolute2(gitPath) ? gitPath :
|
|
1866
|
+
if (!gitPath) return join10(rootDir, ".git/hooks", hookName);
|
|
1867
|
+
return isAbsolute2(gitPath) ? gitPath : resolve4(rootDir, gitPath);
|
|
1767
1868
|
}
|
|
1768
1869
|
|
|
1769
1870
|
// src/commands/find.ts
|
|
@@ -1801,12 +1902,12 @@ function runFind(args2) {
|
|
|
1801
1902
|
}
|
|
1802
1903
|
|
|
1803
1904
|
// src/commands/init.ts
|
|
1804
|
-
import { existsSync as
|
|
1805
|
-
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";
|
|
1806
1907
|
|
|
1807
1908
|
// src/core/templates.ts
|
|
1808
|
-
import { existsSync as
|
|
1809
|
-
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";
|
|
1810
1911
|
var TEMPLATE_FILES = {
|
|
1811
1912
|
decision: "adr.md",
|
|
1812
1913
|
spec: "spec.md",
|
|
@@ -2014,19 +2115,19 @@ var DEFAULT_TEMPLATES = {
|
|
|
2014
2115
|
function loadTemplate(rootDir, type) {
|
|
2015
2116
|
const file = TEMPLATE_FILES[type];
|
|
2016
2117
|
if (!file) throw new Error(`No template file mapped for type: ${type}`);
|
|
2017
|
-
const path =
|
|
2018
|
-
if (
|
|
2118
|
+
const path = join11(rootDir, "docs/governance/templates", file);
|
|
2119
|
+
if (existsSync9(path)) return readFileSync10(path, "utf8");
|
|
2019
2120
|
const fallback = DEFAULT_TEMPLATES[file];
|
|
2020
2121
|
if (fallback) return fallback;
|
|
2021
2122
|
throw new Error(`Template file is missing: docs/governance/templates/${file}`);
|
|
2022
2123
|
}
|
|
2023
2124
|
function ensureDefaultTemplates(rootDir) {
|
|
2024
|
-
const templatesDir =
|
|
2125
|
+
const templatesDir = join11(rootDir, "docs/governance/templates");
|
|
2025
2126
|
mkdirSync3(templatesDir, { recursive: true });
|
|
2026
2127
|
let created = 0;
|
|
2027
2128
|
for (const [file, content] of Object.entries(DEFAULT_TEMPLATES)) {
|
|
2028
|
-
const path =
|
|
2029
|
-
if (
|
|
2129
|
+
const path = join11(templatesDir, file);
|
|
2130
|
+
if (existsSync9(path)) continue;
|
|
2030
2131
|
writeFileSync4(path, content);
|
|
2031
2132
|
created++;
|
|
2032
2133
|
}
|
|
@@ -2081,16 +2182,16 @@ function runInit(args2) {
|
|
|
2081
2182
|
];
|
|
2082
2183
|
let created = 0;
|
|
2083
2184
|
for (const dir of dirs) {
|
|
2084
|
-
const abs =
|
|
2085
|
-
if (!
|
|
2185
|
+
const abs = join12(root, dir);
|
|
2186
|
+
if (!existsSync10(abs)) {
|
|
2086
2187
|
mkdirSync4(abs, { recursive: true });
|
|
2087
2188
|
created++;
|
|
2088
2189
|
} else if (!force) {
|
|
2089
2190
|
}
|
|
2090
2191
|
}
|
|
2091
2192
|
for (const dir of ["docs/specs/completed", "docs/plans/completed", "docs/archive"]) {
|
|
2092
|
-
const keep =
|
|
2093
|
-
if (!
|
|
2193
|
+
const keep = join12(root, dir, ".gitkeep");
|
|
2194
|
+
if (!existsSync10(keep)) writeFileSync5(keep, "");
|
|
2094
2195
|
}
|
|
2095
2196
|
const templatesCreated = ensureDefaultTemplates(root);
|
|
2096
2197
|
console.log(
|
|
@@ -2137,8 +2238,8 @@ function runLinks() {
|
|
|
2137
2238
|
}
|
|
2138
2239
|
|
|
2139
2240
|
// src/commands/migrate.ts
|
|
2140
|
-
import { existsSync as
|
|
2141
|
-
import { join as
|
|
2241
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "node:fs";
|
|
2242
|
+
import { join as join13 } from "node:path";
|
|
2142
2243
|
var PROFILE_ROUTES = {
|
|
2143
2244
|
"engineering-runtime": "docs/governance/agents-routing/engineering-runtime-v1.1.md",
|
|
2144
2245
|
"doc-only": "docs/governance/agents-routing/doc-only-v1.1.md"
|
|
@@ -2175,11 +2276,11 @@ function checkMigrationReadiness(rootDir, profile) {
|
|
|
2175
2276
|
issues.push(`${issue.file}: ${issue.code}: ${issue.message}`);
|
|
2176
2277
|
}
|
|
2177
2278
|
const route = PROFILE_ROUTES[profile];
|
|
2178
|
-
if (!
|
|
2279
|
+
if (!existsSync11(join13(rootDir, route))) {
|
|
2179
2280
|
issues.push(`missing selected profile route: ${route}`);
|
|
2180
2281
|
}
|
|
2181
|
-
const agentsPath =
|
|
2182
|
-
if (
|
|
2282
|
+
const agentsPath = join13(rootDir, "AGENTS.md");
|
|
2283
|
+
if (existsSync11(agentsPath) && !readFileSync11(agentsPath, "utf8").includes(route)) {
|
|
2183
2284
|
issues.push(`AGENTS.md must name selected profile route: ${route}`);
|
|
2184
2285
|
}
|
|
2185
2286
|
return issues;
|
|
@@ -2226,8 +2327,8 @@ function readFlag(args2, name) {
|
|
|
2226
2327
|
}
|
|
2227
2328
|
|
|
2228
2329
|
// src/commands/new.ts
|
|
2229
|
-
import { existsSync as
|
|
2230
|
-
import { dirname as dirname5, join 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";
|
|
2231
2332
|
function runNew(args2) {
|
|
2232
2333
|
const positional = args2.filter((a) => !a.startsWith("--"));
|
|
2233
2334
|
const owner = readFlag2(args2, "--owner") ?? "human";
|
|
@@ -2253,8 +2354,8 @@ function runNew(args2) {
|
|
|
2253
2354
|
console.error(err.message);
|
|
2254
2355
|
return 1;
|
|
2255
2356
|
}
|
|
2256
|
-
const absPath =
|
|
2257
|
-
if (
|
|
2357
|
+
const absPath = join14(root, plan.filePath);
|
|
2358
|
+
if (existsSync12(absPath) && !force) {
|
|
2258
2359
|
console.error(`File already exists: ${plan.filePath}. Use --force to overwrite.`);
|
|
2259
2360
|
return 1;
|
|
2260
2361
|
}
|
|
@@ -2337,8 +2438,8 @@ function runScan(args2) {
|
|
|
2337
2438
|
}
|
|
2338
2439
|
|
|
2339
2440
|
// src/commands/supersede.ts
|
|
2340
|
-
import { readFileSync as
|
|
2341
|
-
import { join as
|
|
2441
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2442
|
+
import { join as join15 } from "node:path";
|
|
2342
2443
|
function runSupersede(args2) {
|
|
2343
2444
|
const [oldId, newId] = args2;
|
|
2344
2445
|
if (!oldId || !newId) {
|
|
@@ -2374,15 +2475,15 @@ function runSupersede(args2) {
|
|
|
2374
2475
|
return 1;
|
|
2375
2476
|
}
|
|
2376
2477
|
const today = todayIso();
|
|
2377
|
-
const oldPath =
|
|
2378
|
-
let oldContent =
|
|
2478
|
+
const oldPath = join15(root, oldRec.path);
|
|
2479
|
+
let oldContent = readFileSync12(oldPath, "utf8");
|
|
2379
2480
|
oldContent = updateFrontmatterField(oldContent, "status", "superseded");
|
|
2380
2481
|
oldContent = updateFrontmatterField(oldContent, "canonical", "false");
|
|
2381
2482
|
oldContent = updateFrontmatterField(oldContent, "superseded_by", newId);
|
|
2382
2483
|
oldContent = updateFrontmatterField(oldContent, "last_reviewed", today);
|
|
2383
2484
|
writeFileSync7(oldPath, oldContent);
|
|
2384
|
-
const newPath =
|
|
2385
|
-
let newContent =
|
|
2485
|
+
const newPath = join15(root, newRec.path);
|
|
2486
|
+
let newContent = readFileSync12(newPath, "utf8");
|
|
2386
2487
|
newContent = appendToFrontmatterList(newContent, "supersedes", newId === oldId ? "" : oldId);
|
|
2387
2488
|
newContent = updateFrontmatterField(newContent, "last_reviewed", today);
|
|
2388
2489
|
writeFileSync7(newPath, newContent);
|
|
@@ -2431,13 +2532,13 @@ ${lines.join("\n")}${tail}`;
|
|
|
2431
2532
|
|
|
2432
2533
|
// src/commands/verify-commit-msg.ts
|
|
2433
2534
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2434
|
-
import { existsSync as
|
|
2535
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
|
|
2435
2536
|
function runVerifyCommitMsg(args2) {
|
|
2436
2537
|
const msgFile = args2[0];
|
|
2437
|
-
if (!msgFile || !
|
|
2538
|
+
if (!msgFile || !existsSync13(msgFile)) {
|
|
2438
2539
|
return 0;
|
|
2439
2540
|
}
|
|
2440
|
-
const message =
|
|
2541
|
+
const message = readFileSync13(msgFile, "utf8");
|
|
2441
2542
|
if (/^Merge\b/m.test(message) || /^Revert\b/m.test(message)) return 0;
|
|
2442
2543
|
let stagedFiles = [];
|
|
2443
2544
|
try {
|
|
@@ -2449,7 +2550,7 @@ function runVerifyCommitMsg(args2) {
|
|
|
2449
2550
|
}
|
|
2450
2551
|
const failures = [];
|
|
2451
2552
|
for (const file of stagedFiles) {
|
|
2452
|
-
if (!
|
|
2553
|
+
if (!existsSync13(file)) continue;
|
|
2453
2554
|
const fm = readFrontmatterFile(file);
|
|
2454
2555
|
if (!fm) continue;
|
|
2455
2556
|
const id = stringValue(fm.data.id);
|