@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.
Files changed (3) hide show
  1. package/cli-guide.md +3 -1
  2. package/dist/cli.js +217 -116
  3. 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 readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
5
- import { join as join5 } from "node:path";
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 existsSync2 } from "node:fs";
514
- import { basename, join as join3 } from "node:path";
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 = join3(rootDir, scanDir);
584
- if (!existsSync2(root)) return "0001";
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 existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
614
- import { dirname as dirname2, join as join4 } from "node:path";
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 = join4(rootDir, "docs/governance/MANIFEST.yml");
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 = join4(rootDir, "docs/governance/MANIFEST.yml");
632
- if (!existsSync3(path)) return false;
633
- return normalizeManifest(readFileSync3(path, "utf8")) === normalizeManifest(buildManifest(rootDir));
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(readFileSync3(new URL(candidate, import.meta.url), "utf8"));
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 = join5(root, record.path);
699
- const content = readFileSync4(filePath, "utf8");
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 readFileSync5, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
746
- import { basename as basename2, dirname as dirname3, join as join6 } from "node:path";
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 = join6(root, newPath);
786
- const absOld = join6(root, oldPath);
787
- let content = readFileSync5(absOld, "utf8");
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 existsSync5, readdirSync as readdirSync2 } from "node:fs";
817
- import { join as join7 } from "node:path";
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 existsSync4, readFileSync as readFileSync6 } from "node:fs";
821
- import { dirname as dirname4, extname, resolve as resolve2 } from "node:path";
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("/") ? resolve2(rootDir, `.${pathPart}`) : resolve2(dirname4(sourcePath), pathPart);
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) => existsSync4(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 readFileSync6(filePath, "utf8");
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 = join7(root, "Docs-for trans");
938
- if (existsSync5(migrationSource)) {
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 (existsSync5(join7(root, "DocSystemStarter.md"))) {
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 = join7(dir, entry.name);
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 existsSync7, readFileSync as readFileSync8 } from "node:fs";
994
- import { isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
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 (!existsSync6(join8(rootDir, file))) {
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 (existsSync6(join8(rootDir, file))) {
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 (existsSync6(join8(rootDir, file))) {
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 (existsSync6(join8(rootDir, file))) {
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 (existsSync6(join8(rootDir, file))) {
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 (existsSync6(join8(rootDir, file))) {
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 = join8(rootDir, requirement.file);
1392
- if (!existsSync6(path)) continue;
1393
- const content = readFileSync7(path, "utf8");
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(join8(rootDir, file));
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(join8(rootDir, file));
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 = join8(rootDir, file);
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(readlinkSync(path));
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 lstatSync2(path);
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 = join8(rootDir, "package.json");
1489
- if (!existsSync6(packageJsonPath)) return false;
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(readFileSync7(packageJsonPath, "utf8"));
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 existsSync6(join8(rootDir, "profiles")) && existsSync6(join8(rootDir, "starter")) && existsSync6(join8(rootDir, "integrations"));
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) => existsSync6(join8(rootDir, 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 = join8(rootDir, "AGENTS.md");
1519
- if (!existsSync6(agentsPath)) return issues;
1520
- const agents = readFileSync7(agentsPath, "utf8");
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 = join8(rootDir, file);
1532
- if (!existsSync6(path)) return [];
1533
- const content = readFileSync7(path, "utf8");
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 = join8(rootDir, file);
1559
- if (!existsSync6(path)) return [];
1560
- const content = readFileSync7(path, "utf8");
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
- if (existsSync6(join8(rootDir, pathRoot, normalized)) || existsSync6(join8(rootDir, normalized))) {
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 = join8(rootDir, file);
1590
- if (!existsSync6(path)) return [];
1591
- const content = readFileSync7(path, "utf8");
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) => relative3(rootDir, path).split(/\\/g).join("/")).filter((repoPath) => repoPath !== "README.md");
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 = join9(rootDir, "lefthook.yml");
1682
- if (!existsSync7(path)) {
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 = readFileSync8(path, "utf8");
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 = join9(rootDir, ".github/workflows/docs-check.yml");
1729
- if (!existsSync7(path)) {
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 = readFileSync8(path, "utf8");
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 existsSync7(path) && readFileSync8(path, "utf8").includes("lefthook");
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 join9(rootDir, ".git/hooks", hookName);
1766
- return isAbsolute2(gitPath) ? gitPath : resolve3(rootDir, 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 existsSync9, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
1805
- import { join as join11 } from "node:path";
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 existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
1809
- import { join as join10 } from "node:path";
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 = join10(rootDir, "docs/governance/templates", file);
2018
- if (existsSync8(path)) return readFileSync9(path, "utf8");
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 = join10(rootDir, "docs/governance/templates");
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 = join10(templatesDir, file);
2029
- if (existsSync8(path)) continue;
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 = join11(root, dir);
2085
- if (!existsSync9(abs)) {
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 = join11(root, dir, ".gitkeep");
2093
- if (!existsSync9(keep)) writeFileSync5(keep, "");
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 existsSync10, readFileSync as readFileSync10 } from "node:fs";
2141
- import { join as join12 } from "node:path";
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 (!existsSync10(join12(rootDir, route))) {
2279
+ if (!existsSync11(join13(rootDir, route))) {
2179
2280
  issues.push(`missing selected profile route: ${route}`);
2180
2281
  }
2181
- const agentsPath = join12(rootDir, "AGENTS.md");
2182
- if (existsSync10(agentsPath) && !readFileSync10(agentsPath, "utf8").includes(route)) {
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 existsSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
2230
- import { dirname as dirname5, join as join13 } from "node:path";
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 = join13(root, plan.filePath);
2257
- if (existsSync11(absPath) && !force) {
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 readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
2341
- import { join as join14 } from "node:path";
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 = join14(root, oldRec.path);
2378
- let oldContent = readFileSync11(oldPath, "utf8");
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 = join14(root, newRec.path);
2385
- let newContent = readFileSync11(newPath, "utf8");
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 existsSync12, readFileSync as readFileSync12 } from "node:fs";
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 || !existsSync12(msgFile)) {
2538
+ if (!msgFile || !existsSync13(msgFile)) {
2438
2539
  return 0;
2439
2540
  }
2440
- const message = readFileSync12(msgFile, "utf8");
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 (!existsSync12(file)) continue;
2553
+ if (!existsSync13(file)) continue;
2453
2554
  const fm = readFrontmatterFile(file);
2454
2555
  if (!fm) continue;
2455
2556
  const id = stringValue(fm.data.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pieai/doc-gov",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "description": "AI-native documentation governance CLI for project docs, agent routing, and lifecycle checks.",
5
5
  "keywords": [
6
6
  "ai-agents",