@pieai/doc-gov 0.9.4 → 0.9.6

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 (2) hide show
  1. package/dist/cli.js +220 -132
  2. package/package.json +1 -1
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 readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
5
- import { join as join4 } from "node:path";
4
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
5
+ import { join as join5 } from "node:path";
6
6
 
7
7
  // src/core/frontmatter.ts
8
8
  import { readFileSync } from "node:fs";
@@ -65,8 +65,7 @@ function parseScalar(value) {
65
65
  }
66
66
 
67
67
  // src/core/files.ts
68
- import { existsSync, readdirSync, statSync } from "node:fs";
69
- import { join, relative } from "node:path";
68
+ import { join as join2, relative as relative2 } from "node:path";
70
69
 
71
70
  // src/core/external-artifacts.ts
72
71
  var EXTERNAL_ARTIFACT_PREFIXES = ["docs/brainstorms/", "docs/pulse-reports/"];
@@ -80,35 +79,164 @@ function isExternalArtifactPath(repoPath) {
80
79
  return false;
81
80
  }
82
81
 
83
- // src/core/files.ts
84
- function listGovernedMarkdownFiles(rootDir) {
85
- const roots = [join(rootDir, "docs")];
82
+ // src/core/markdown-traversal.ts
83
+ import {
84
+ existsSync,
85
+ lstatSync,
86
+ readFileSync as readFileSync2,
87
+ readdirSync,
88
+ realpathSync,
89
+ statSync
90
+ } from "node:fs";
91
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
92
+ function listMarkdownFiles(rootDir, roots, options = {}) {
93
+ const resolvedRootDir = resolve(rootDir);
94
+ const allowedRoots = canonicalizeRoots(resolvedRootDir, options.allowedRoots ?? []);
86
95
  const files = [];
96
+ const visitedDirectories = /* @__PURE__ */ new Set();
87
97
  for (const root of roots) {
88
- if (!existsSync(root)) continue;
89
- files.push(...walk(rootDir, root));
98
+ walkEntry(resolveFromRoot(resolvedRootDir, root));
90
99
  }
91
- return files.sort();
100
+ return Array.from(new Set(files)).sort();
101
+ function walkEntry(path) {
102
+ const repoPath = toRepoPath(resolvedRootDir, path);
103
+ if (options.shouldSkipTree?.(repoPath)) return;
104
+ const stat = safeLstat(path);
105
+ if (!stat) return;
106
+ if (stat.isDirectory()) {
107
+ walkDirectory(path);
108
+ return;
109
+ }
110
+ if (!stat.isFile() && !stat.isSymbolicLink()) return;
111
+ addMarkdownFile(path, repoPath, stat);
112
+ }
113
+ function walkDirectory(dir) {
114
+ const realDirectory = safeRealpath(dir);
115
+ if (!realDirectory || visitedDirectories.has(realDirectory)) return;
116
+ visitedDirectories.add(realDirectory);
117
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
118
+ const path = join(dir, entry.name);
119
+ const repoPath = toRepoPath(resolvedRootDir, path);
120
+ if (options.shouldSkipTree?.(repoPath)) continue;
121
+ if (entry.isDirectory()) {
122
+ walkDirectory(path);
123
+ continue;
124
+ }
125
+ if (entry.isFile() || entry.isSymbolicLink()) {
126
+ addMarkdownFile(path, repoPath, entry);
127
+ }
128
+ }
129
+ }
130
+ function addMarkdownFile(path, repoPath, entry) {
131
+ if (!path.endsWith(".md")) return;
132
+ if (options.includeFile && !options.includeFile(repoPath)) return;
133
+ if (!isAllowedMarkdownFile(resolvedRootDir, path, allowedRoots, repoPath, entry)) return;
134
+ files.push(path);
135
+ }
136
+ }
137
+ function canonicalizeRoots(rootDir, roots) {
138
+ return Array.from(
139
+ new Set(
140
+ [rootDir, ...roots].map((root) => {
141
+ const resolved = resolveFromRoot(rootDir, root);
142
+ return safeRealpath(resolved) ?? resolved;
143
+ }).filter(Boolean)
144
+ )
145
+ );
92
146
  }
93
- function walk(rootDir, dir) {
94
- const entries = readdirSync(dir, { withFileTypes: true });
95
- const files = [];
96
- for (const entry of entries) {
97
- const path = join(dir, entry.name);
98
- const rel = toRepoPath(rootDir, path);
99
- if (shouldSkip(rel)) continue;
100
- if (entry.isSymbolicLink()) continue;
101
- if (entry.isDirectory()) files.push(...walk(rootDir, path));
102
- if (entry.isFile() && entry.name.endsWith(".md")) files.push(path);
147
+ function isAllowedMarkdownFile(rootDir, path, allowedRoots, repoPath, entry) {
148
+ const targetPath = safeRealpath(path);
149
+ if (!targetPath) return false;
150
+ const targetStat = safeStat(targetPath);
151
+ if (!targetStat?.isFile()) return false;
152
+ if (allowedRoots.some((root) => isPathWithin(targetPath, root))) return true;
153
+ return entry.isSymbolicLink() && isTrustedPgsSharedRuleTarget(rootDir, repoPath, targetPath);
154
+ }
155
+ function isTrustedPgsSharedRuleTarget(rootDir, repoPath, targetPath) {
156
+ if (!isSharedRulePath(repoPath)) return false;
157
+ const centralRoot = findCentralRepositoryRoot(dirname(targetPath));
158
+ if (!centralRoot) return false;
159
+ const sourceRoot = safeRealpath(join(centralRoot, "agent-assets/rules/pie-rules"));
160
+ return Boolean(sourceRoot && isPathWithin(targetPath, sourceRoot));
161
+ }
162
+ function isSharedRulePath(repoPath) {
163
+ return repoPath.startsWith("docs/policy/shared-rules/") && repoPath.endsWith(".md");
164
+ }
165
+ function findCentralRepositoryRoot(startDir) {
166
+ let current = startDir;
167
+ while (true) {
168
+ if (isCentralRepositoryRoot(current)) return safeRealpath(current) ?? current;
169
+ const parent = dirname(current);
170
+ if (parent === current) return void 0;
171
+ current = parent;
172
+ }
173
+ }
174
+ function isCentralRepositoryRoot(path) {
175
+ const packageJsonPath = join(path, "package.json");
176
+ if (!existsSync(packageJsonPath)) return false;
177
+ try {
178
+ const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
179
+ if (packageJson.name !== "project-governance-system" && packageJson.name !== "pro-gov") {
180
+ return false;
181
+ }
182
+ } catch {
183
+ return false;
103
184
  }
104
- return files;
185
+ return existsSync(join(path, "profiles")) && existsSync(join(path, "starter")) && existsSync(join(path, "integrations"));
105
186
  }
106
- function shouldSkip(repoPath) {
107
- return isExternalArtifactPath(repoPath) || repoPath.startsWith("docs/governance/templates/") || repoPath === "docs/governance/MANIFEST.yml";
187
+ function isPathWithin(candidate, root) {
188
+ const relation = relative(root, candidate);
189
+ return relation === "" || !relation.startsWith(`..${sep}`) && relation !== ".." && !isAbsolute(relation);
190
+ }
191
+ function resolveFromRoot(rootDir, path) {
192
+ if (isAbsolute(path)) return path;
193
+ const resolvedRoot = resolve(rootDir);
194
+ const resolvedPath = resolve(path);
195
+ return isPathWithin(resolvedPath, resolvedRoot) ? resolvedPath : resolve(resolvedRoot, path);
108
196
  }
109
197
  function toRepoPath(rootDir, absolutePath) {
110
198
  return relative(rootDir, absolutePath).split(/\\/g).join("/");
111
199
  }
200
+ function safeLstat(path) {
201
+ try {
202
+ return lstatSync(path);
203
+ } catch (error) {
204
+ const code = error.code;
205
+ if (code === "ENOENT" || code === "ENOTDIR") return void 0;
206
+ throw error;
207
+ }
208
+ }
209
+ function safeStat(path) {
210
+ try {
211
+ return statSync(path);
212
+ } catch (error) {
213
+ const code = error.code;
214
+ if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP") return void 0;
215
+ throw error;
216
+ }
217
+ }
218
+ function safeRealpath(path) {
219
+ try {
220
+ return realpathSync(path);
221
+ } catch (error) {
222
+ const code = error.code;
223
+ if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP") return void 0;
224
+ throw error;
225
+ }
226
+ }
227
+
228
+ // src/core/files.ts
229
+ function listGovernedMarkdownFiles(rootDir) {
230
+ return listMarkdownFiles(rootDir, ["docs"], {
231
+ shouldSkipTree: shouldSkip
232
+ });
233
+ }
234
+ function shouldSkip(repoPath) {
235
+ return isExternalArtifactPath(repoPath) || repoPath.startsWith("docs/governance/templates/") || repoPath === "docs/governance/MANIFEST.yml";
236
+ }
237
+ function toRepoPath2(rootDir, absolutePath) {
238
+ return relative2(rootDir, absolutePath).split(/\\/g).join("/");
239
+ }
112
240
 
113
241
  // src/core/lifecycle.ts
114
242
  var normalStatuses = [
@@ -132,7 +260,7 @@ var docTypes = [
132
260
  "archive"
133
261
  ];
134
262
  function validateFrontmatter(rootDir, file) {
135
- const path = toRepoPath(rootDir, file.path);
263
+ const path = toRepoPath2(rootDir, file.path);
136
264
  const issues = [];
137
265
  const data = file.data;
138
266
  const id = stringValue(data.id);
@@ -382,8 +510,8 @@ function pathToRepo(rootDir, path) {
382
510
  }
383
511
 
384
512
  // src/core/paths.ts
385
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "node:fs";
386
- import { join as join2 } from "node:path";
513
+ import { existsSync as existsSync2 } from "node:fs";
514
+ import { basename, join as join3 } from "node:path";
387
515
  function planPath(rootDir, type, slugInput) {
388
516
  const cleanSlug = slugInput.replace(/^\/+|\/+$/g, "");
389
517
  if (!cleanSlug) throw new Error("Slug is required.");
@@ -452,22 +580,18 @@ function planPath(rootDir, type, slugInput) {
452
580
  throw new Error(`Unknown type: ${type}`);
453
581
  }
454
582
  function nextSerial(rootDir, scanDir, regex) {
455
- const root = join2(rootDir, scanDir);
583
+ const root = join3(rootDir, scanDir);
456
584
  if (!existsSync2(root)) return "0001";
457
585
  let max = 0;
458
- walkSerial(root, regex, (n) => {
586
+ walkSerial(rootDir, scanDir, regex, (n) => {
459
587
  if (n > max) max = n;
460
588
  });
461
589
  return String(max + 1).padStart(4, "0");
462
590
  }
463
- function walkSerial(dir, regex, onMatch) {
464
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
465
- const path = join2(dir, entry.name);
466
- if (entry.isDirectory()) walkSerial(path, regex, onMatch);
467
- else if (entry.isFile() && entry.name.endsWith(".md")) {
468
- const m = entry.name.match(regex);
469
- if (m && m[1]) onMatch(parseInt(m[1], 10));
470
- }
591
+ function walkSerial(rootDir, dir, regex, onMatch) {
592
+ for (const path of listMarkdownFiles(rootDir, [dir])) {
593
+ const m = basename(path).match(regex);
594
+ if (m && m[1]) onMatch(parseInt(m[1], 10));
471
595
  }
472
596
  }
473
597
  function isKebabCase(slug) {
@@ -486,8 +610,8 @@ function todayIso() {
486
610
  }
487
611
 
488
612
  // src/core/manifest.ts
489
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
490
- import { dirname, join as join3 } from "node:path";
613
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
614
+ import { dirname as dirname2, join as join4 } from "node:path";
491
615
  function buildManifest(rootDir = process.cwd()) {
492
616
  const result = checkDocs(rootDir);
493
617
  if (!result.ok) {
@@ -499,14 +623,14 @@ ${error}`);
499
623
  }
500
624
  function writeManifest(rootDir = process.cwd()) {
501
625
  const manifest = buildManifest(rootDir);
502
- const path = join3(rootDir, "docs/governance/MANIFEST.yml");
503
- mkdirSync(dirname(path), { recursive: true });
626
+ const path = join4(rootDir, "docs/governance/MANIFEST.yml");
627
+ mkdirSync(dirname2(path), { recursive: true });
504
628
  writeFileSync(path, manifest);
505
629
  }
506
630
  function manifestInSync(rootDir = process.cwd()) {
507
- const path = join3(rootDir, "docs/governance/MANIFEST.yml");
631
+ const path = join4(rootDir, "docs/governance/MANIFEST.yml");
508
632
  if (!existsSync3(path)) return false;
509
- return normalizeManifest(readFileSync2(path, "utf8")) === normalizeManifest(buildManifest(rootDir));
633
+ return normalizeManifest(readFileSync3(path, "utf8")) === normalizeManifest(buildManifest(rootDir));
510
634
  }
511
635
  function normalizeManifest(value) {
512
636
  return value.replace(/^generated_at: .*$/m, "generated_at: <ignored>").replace(/^generator_version: .*$/m, "generator_version: <ignored>");
@@ -535,7 +659,7 @@ function renderManifest(records) {
535
659
  function readPackageVersion() {
536
660
  for (const candidate of ["../package.json", "../../package.json"]) {
537
661
  try {
538
- const packageJson = JSON.parse(readFileSync2(new URL(candidate, import.meta.url), "utf8"));
662
+ const packageJson = JSON.parse(readFileSync3(new URL(candidate, import.meta.url), "utf8"));
539
663
  if (typeof packageJson.version === "string") return packageJson.version;
540
664
  } catch {
541
665
  }
@@ -571,8 +695,8 @@ function runApprove(args2) {
571
695
  );
572
696
  return 1;
573
697
  }
574
- const filePath = join4(root, record.path);
575
- const content = readFileSync3(filePath, "utf8");
698
+ const filePath = join5(root, record.path);
699
+ const content = readFileSync4(filePath, "utf8");
576
700
  let next = content;
577
701
  next = updateFrontmatterField(next, "status", toStatus);
578
702
  next = updateFrontmatterField(next, "canonical", "true");
@@ -618,8 +742,8 @@ ${newLines.join("\n")}${tail}`;
618
742
 
619
743
  // src/commands/archive.ts
620
744
  import { execFileSync } from "node:child_process";
621
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
622
- import { basename, dirname as dirname2, join as join5 } from "node:path";
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";
623
747
  function runArchive(args2) {
624
748
  const positional = args2.filter((a) => !a.startsWith("--"));
625
749
  const id = positional[0];
@@ -655,18 +779,18 @@ function runArchive(args2) {
655
779
  return 1;
656
780
  }
657
781
  const oldPath = record.path;
658
- const fileName = basename(oldPath);
782
+ const fileName = basename2(oldPath);
659
783
  const archiveDir = `docs/archive/${quarterTag()}-${record.type}`;
660
784
  const newPath = `${archiveDir}/${fileName}`;
661
- const absNew = join5(root, newPath);
662
- const absOld = join5(root, oldPath);
663
- let content = readFileSync4(absOld, "utf8");
785
+ const absNew = join6(root, newPath);
786
+ const absOld = join6(root, oldPath);
787
+ let content = readFileSync5(absOld, "utf8");
664
788
  content = updateFrontmatterField(content, "type", "archive");
665
789
  content = updateFrontmatterField(content, "status", "archived");
666
790
  content = updateFrontmatterField(content, "canonical", "false");
667
791
  content = updateFrontmatterField(content, "last_reviewed", todayIso());
668
792
  content = updateFrontmatterField(content, "archive_reason", reason);
669
- mkdirSync2(dirname2(absNew), { recursive: true });
793
+ mkdirSync2(dirname3(absNew), { recursive: true });
670
794
  let movedByGit = false;
671
795
  try {
672
796
  execFileSync("git", ["mv", oldPath, newPath], { cwd: root, stdio: "ignore" });
@@ -689,12 +813,12 @@ function runArchive(args2) {
689
813
  }
690
814
 
691
815
  // src/commands/audit.ts
692
- import { existsSync as existsSync5, readdirSync as readdirSync4 } from "node:fs";
816
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "node:fs";
693
817
  import { join as join7 } from "node:path";
694
818
 
695
819
  // src/core/link-checker.ts
696
- import { existsSync as existsSync4, lstatSync, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "node:fs";
697
- import { dirname as dirname3, extname, join as join6, resolve } from "node:path";
820
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "node:fs";
821
+ import { dirname as dirname4, extname, resolve as resolve2 } from "node:path";
698
822
  var CURRENT_MARKDOWN_ROOTS = ["AGENTS.md", "README.md", "docs"];
699
823
  var CURRENT_DOC_DIR_PREFIXES = [
700
824
  "docs/canon/",
@@ -719,7 +843,7 @@ function checkCurrentMarkdownLinks(rootDir = process.cwd()) {
719
843
  if (!target || shouldIgnoreTarget(target)) continue;
720
844
  checkedLinks += 1;
721
845
  if (!localTargetExists(rootDir, filePath, target)) {
722
- const file = toRepoPath(rootDir, filePath);
846
+ const file = toRepoPath2(rootDir, filePath);
723
847
  const line = lineNumberAt(content, match.index);
724
848
  issues.push({
725
849
  file,
@@ -738,30 +862,10 @@ function checkCurrentMarkdownLinks(rootDir = process.cwd()) {
738
862
  };
739
863
  }
740
864
  function listCurrentMarkdownFiles(rootDir) {
741
- const files = [];
742
- for (const root of CURRENT_MARKDOWN_ROOTS) {
743
- const fullPath = join6(rootDir, root);
744
- if (!existsSync4(fullPath)) continue;
745
- const stat = lstatSync(fullPath);
746
- if (stat.isSymbolicLink()) continue;
747
- if (stat.isDirectory()) files.push(...walkMarkdown(rootDir, fullPath));
748
- else if (stat.isFile() && fullPath.endsWith(".md")) files.push(fullPath);
749
- }
750
- return Array.from(new Set(files)).sort();
751
- }
752
- function walkMarkdown(rootDir, dir) {
753
- const files = [];
754
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
755
- const fullPath = join6(dir, entry.name);
756
- const repoPath = toRepoPath(rootDir, fullPath);
757
- if (shouldSkipTree(repoPath)) continue;
758
- if (entry.isSymbolicLink()) continue;
759
- if (entry.isDirectory()) files.push(...walkMarkdown(rootDir, fullPath));
760
- if (entry.isFile() && entry.name.endsWith(".md") && shouldIncludeSource(repoPath)) {
761
- files.push(fullPath);
762
- }
763
- }
764
- return files;
865
+ return listMarkdownFiles(rootDir, CURRENT_MARKDOWN_ROOTS, {
866
+ shouldSkipTree,
867
+ includeFile: (repoPath) => repoPath === "AGENTS.md" || repoPath === "README.md" || shouldIncludeSource(repoPath)
868
+ });
765
869
  }
766
870
  function shouldSkipTree(repoPath) {
767
871
  if (isExternalArtifactPath(repoPath)) return true;
@@ -794,7 +898,7 @@ function shouldIgnoreTarget(target) {
794
898
  function localTargetExists(rootDir, sourcePath, target) {
795
899
  const pathPart = decodeTarget(target).split("#")[0]?.split("?")[0] ?? "";
796
900
  if (!pathPart) return true;
797
- const resolved = pathPart.startsWith("/") ? resolve(rootDir, `.${pathPart}`) : resolve(dirname3(sourcePath), pathPart);
901
+ const resolved = pathPart.startsWith("/") ? resolve2(rootDir, `.${pathPart}`) : resolve2(dirname4(sourcePath), pathPart);
798
902
  const candidates = [resolved];
799
903
  if (!extname(resolved)) {
800
904
  candidates.push(`${resolved}.md`);
@@ -816,7 +920,7 @@ function lineNumberAt(content, index) {
816
920
  return line;
817
921
  }
818
922
  function readText(filePath) {
819
- return readFileSync5(filePath, "utf8");
923
+ return readFileSync6(filePath, "utf8");
820
924
  }
821
925
 
822
926
  // src/commands/audit.ts
@@ -844,7 +948,7 @@ function runAudit() {
844
948
  "Stray root-level DocSystemStarter.md exists. The original draft has been archived; remove or re-archive."
845
949
  );
846
950
  }
847
- const rootEntries = new Set(readdirSync4(root));
951
+ const rootEntries = new Set(readdirSync2(root));
848
952
  if (rootEntries.has("Docs")) {
849
953
  console.error(
850
954
  "Old Docs/ directory still exists. Move remaining files into docs/ or archive them."
@@ -863,7 +967,7 @@ function runAudit() {
863
967
  }
864
968
  function countFiles(dir) {
865
969
  let count = 0;
866
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
970
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
867
971
  const path = join7(dir, entry.name);
868
972
  if (entry.isDirectory()) count += countFiles(path);
869
973
  if (entry.isFile() || entry.isSymbolicLink()) count += 1;
@@ -886,12 +990,12 @@ function runCheck() {
886
990
 
887
991
  // src/commands/doctor.ts
888
992
  import { spawnSync } from "node:child_process";
889
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
890
- import { isAbsolute, join as join9, resolve as resolve2 } from "node:path";
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";
891
995
 
892
996
  // src/core/router-integrity.ts
893
- import { existsSync as existsSync6, lstatSync as lstatSync2, readlinkSync, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
894
- import { join as join8, relative as relative2 } from "node:path";
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";
895
999
 
896
1000
  // src/core/symlinks.ts
897
1001
  function normalizeSymlinkTarget(target) {
@@ -1286,7 +1390,7 @@ function checkRouterIntegrity(rootDir = process.cwd()) {
1286
1390
  for (const requirement of requiredNeedles) {
1287
1391
  const path = join8(rootDir, requirement.file);
1288
1392
  if (!existsSync6(path)) continue;
1289
- const content = readFileSync6(path, "utf8");
1393
+ const content = readFileSync7(path, "utf8");
1290
1394
  if (!content.includes(requirement.needle)) {
1291
1395
  issues.push({
1292
1396
  file: requirement.file,
@@ -1332,7 +1436,7 @@ function validateHostSsot(rootDir) {
1332
1436
  validateExactSymlink(".claude/skills", "../.agents/skills");
1333
1437
  return issues;
1334
1438
  function validateRegularFile(file) {
1335
- const stat = safeLstat(join8(rootDir, file));
1439
+ const stat = safeLstat2(join8(rootDir, file));
1336
1440
  if (!stat || stat.isFile()) return;
1337
1441
  issues.push({
1338
1442
  file,
@@ -1341,7 +1445,7 @@ function validateHostSsot(rootDir) {
1341
1445
  });
1342
1446
  }
1343
1447
  function validateDirectory(file) {
1344
- const stat = safeLstat(join8(rootDir, file));
1448
+ const stat = safeLstat2(join8(rootDir, file));
1345
1449
  if (!stat || stat.isDirectory()) return;
1346
1450
  issues.push({
1347
1451
  file,
@@ -1351,7 +1455,7 @@ function validateHostSsot(rootDir) {
1351
1455
  }
1352
1456
  function validateExactSymlink(file, expectedRawTarget) {
1353
1457
  const path = join8(rootDir, file);
1354
- const stat = safeLstat(path);
1458
+ const stat = safeLstat2(path);
1355
1459
  if (!stat) return;
1356
1460
  if (!stat.isSymbolicLink()) {
1357
1461
  issues.push({
@@ -1371,7 +1475,7 @@ function validateHostSsot(rootDir) {
1371
1475
  }
1372
1476
  }
1373
1477
  }
1374
- function safeLstat(path) {
1478
+ function safeLstat2(path) {
1375
1479
  try {
1376
1480
  return lstatSync2(path);
1377
1481
  } catch (error) {
@@ -1389,7 +1493,7 @@ function isCentralRepository(rootDir) {
1389
1493
  }
1390
1494
  function readPackageName(packageJsonPath) {
1391
1495
  try {
1392
- const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
1496
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
1393
1497
  return typeof packageJson.name === "string" ? packageJson.name : "";
1394
1498
  } catch {
1395
1499
  return "";
@@ -1413,7 +1517,7 @@ function validateProjectAgentsRouting(rootDir) {
1413
1517
  }
1414
1518
  const agentsPath = join8(rootDir, "AGENTS.md");
1415
1519
  if (!existsSync6(agentsPath)) return issues;
1416
- const agents = readFileSync6(agentsPath, "utf8");
1520
+ const agents = readFileSync7(agentsPath, "utf8");
1417
1521
  if (!existingRoutes.some((file) => agents.includes(file))) {
1418
1522
  issues.push({
1419
1523
  file: "AGENTS.md",
@@ -1426,7 +1530,7 @@ function validateProjectAgentsRouting(rootDir) {
1426
1530
  function validateRouterBlock(rootDir, file) {
1427
1531
  const path = join8(rootDir, file);
1428
1532
  if (!existsSync6(path)) return [];
1429
- const content = readFileSync6(path, "utf8");
1533
+ const content = readFileSync7(path, "utf8");
1430
1534
  const begin = content.indexOf(ROUTER_BLOCK_BEGIN);
1431
1535
  const end = content.indexOf(ROUTER_BLOCK_END);
1432
1536
  const issues = [];
@@ -1453,7 +1557,7 @@ function validateRouterBlock(rootDir, file) {
1453
1557
  function validateBacktickedLocalPaths(rootDir, file, pathRoot = "") {
1454
1558
  const path = join8(rootDir, file);
1455
1559
  if (!existsSync6(path)) return [];
1456
- const content = readFileSync6(path, "utf8");
1560
+ const content = readFileSync7(path, "utf8");
1457
1561
  const issues = [];
1458
1562
  const seen = /* @__PURE__ */ new Set();
1459
1563
  const matches = content.matchAll(/`([^`]+)`/g);
@@ -1484,7 +1588,7 @@ function isLocalPathReference(value) {
1484
1588
  function validatePortableRouterText(rootDir, file) {
1485
1589
  const path = join8(rootDir, file);
1486
1590
  if (!existsSync6(path)) return [];
1487
- const content = readFileSync6(path, "utf8");
1591
+ const content = readFileSync7(path, "utf8");
1488
1592
  if (!hasNonPortablePath(content)) return [];
1489
1593
  return [
1490
1594
  {
@@ -1504,25 +1608,9 @@ function hasNonPortablePath(content) {
1504
1608
  ) || /\b(OneDrive|CloudStorage)\b/.test(content);
1505
1609
  }
1506
1610
  function findGovernedReadmes(rootDir) {
1507
- const matches = [];
1508
- for (const relRoot of ["docs", "starter/docs"]) {
1509
- const absRoot = join8(rootDir, relRoot);
1510
- if (existsSync6(absRoot)) walk2(absRoot);
1511
- }
1512
- return matches.sort();
1513
- function walk2(dir) {
1514
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
1515
- if (entry.isSymbolicLink()) continue;
1516
- if (entry.isDirectory()) {
1517
- walk2(join8(dir, entry.name));
1518
- continue;
1519
- }
1520
- if (!entry.isFile()) continue;
1521
- if (entry.name.toLowerCase() !== "readme.md") continue;
1522
- const repoPath = relative2(rootDir, join8(dir, entry.name)).split(/\\/g).join("/");
1523
- if (repoPath !== "README.md") matches.push(repoPath);
1524
- }
1525
- }
1611
+ return listMarkdownFiles(rootDir, ["docs", "starter/docs"], {
1612
+ includeFile: (repoPath) => repoPath.toLowerCase().endsWith("/readme.md")
1613
+ }).map((path) => relative3(rootDir, path).split(/\\/g).join("/")).filter((repoPath) => repoPath !== "README.md");
1526
1614
  }
1527
1615
 
1528
1616
  // src/commands/doctor.ts
@@ -1600,7 +1688,7 @@ function checkLefthook(rootDir) {
1600
1688
  }
1601
1689
  ];
1602
1690
  }
1603
- const content = readFileSync7(path, "utf8");
1691
+ const content = readFileSync8(path, "utf8");
1604
1692
  const issues = [];
1605
1693
  for (const command2 of [
1606
1694
  "pnpm doc-gov router-check",
@@ -1647,7 +1735,7 @@ function checkDocsCheckWorkflow(rootDir) {
1647
1735
  }
1648
1736
  ];
1649
1737
  }
1650
- const content = readFileSync7(path, "utf8");
1738
+ const content = readFileSync8(path, "utf8");
1651
1739
  const issues = [];
1652
1740
  for (const command2 of [
1653
1741
  "pnpm doc-gov router-check",
@@ -1667,7 +1755,7 @@ function checkDocsCheckWorkflow(rootDir) {
1667
1755
  return issues;
1668
1756
  }
1669
1757
  function hookCallsLefthook(path) {
1670
- return existsSync7(path) && readFileSync7(path, "utf8").includes("lefthook");
1758
+ return existsSync7(path) && readFileSync8(path, "utf8").includes("lefthook");
1671
1759
  }
1672
1760
  function resolveGitHookPath(rootDir, hookName) {
1673
1761
  const result = spawnSync("git", ["-C", rootDir, "rev-parse", "--git-path", `hooks/${hookName}`], {
@@ -1675,7 +1763,7 @@ function resolveGitHookPath(rootDir, hookName) {
1675
1763
  });
1676
1764
  const gitPath = result.status === 0 ? result.stdout.trim() : "";
1677
1765
  if (!gitPath) return join9(rootDir, ".git/hooks", hookName);
1678
- return isAbsolute(gitPath) ? gitPath : resolve2(rootDir, gitPath);
1766
+ return isAbsolute2(gitPath) ? gitPath : resolve3(rootDir, gitPath);
1679
1767
  }
1680
1768
 
1681
1769
  // src/commands/find.ts
@@ -1717,7 +1805,7 @@ import { existsSync as existsSync9, mkdirSync as mkdirSync4, writeFileSync as wr
1717
1805
  import { join as join11 } from "node:path";
1718
1806
 
1719
1807
  // src/core/templates.ts
1720
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
1808
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
1721
1809
  import { join as join10 } from "node:path";
1722
1810
  var TEMPLATE_FILES = {
1723
1811
  decision: "adr.md",
@@ -1927,7 +2015,7 @@ function loadTemplate(rootDir, type) {
1927
2015
  const file = TEMPLATE_FILES[type];
1928
2016
  if (!file) throw new Error(`No template file mapped for type: ${type}`);
1929
2017
  const path = join10(rootDir, "docs/governance/templates", file);
1930
- if (existsSync8(path)) return readFileSync8(path, "utf8");
2018
+ if (existsSync8(path)) return readFileSync9(path, "utf8");
1931
2019
  const fallback = DEFAULT_TEMPLATES[file];
1932
2020
  if (fallback) return fallback;
1933
2021
  throw new Error(`Template file is missing: docs/governance/templates/${file}`);
@@ -2049,7 +2137,7 @@ function runLinks() {
2049
2137
  }
2050
2138
 
2051
2139
  // src/commands/migrate.ts
2052
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "node:fs";
2140
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
2053
2141
  import { join as join12 } from "node:path";
2054
2142
  var PROFILE_ROUTES = {
2055
2143
  "engineering-runtime": "docs/governance/agents-routing/engineering-runtime-v1.1.md",
@@ -2091,7 +2179,7 @@ function checkMigrationReadiness(rootDir, profile) {
2091
2179
  issues.push(`missing selected profile route: ${route}`);
2092
2180
  }
2093
2181
  const agentsPath = join12(rootDir, "AGENTS.md");
2094
- if (existsSync10(agentsPath) && !readFileSync9(agentsPath, "utf8").includes(route)) {
2182
+ if (existsSync10(agentsPath) && !readFileSync10(agentsPath, "utf8").includes(route)) {
2095
2183
  issues.push(`AGENTS.md must name selected profile route: ${route}`);
2096
2184
  }
2097
2185
  return issues;
@@ -2139,7 +2227,7 @@ function readFlag(args2, name) {
2139
2227
 
2140
2228
  // src/commands/new.ts
2141
2229
  import { existsSync as existsSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
2142
- import { dirname as dirname4, join as join13 } from "node:path";
2230
+ import { dirname as dirname5, join as join13 } from "node:path";
2143
2231
  function runNew(args2) {
2144
2232
  const positional = args2.filter((a) => !a.startsWith("--"));
2145
2233
  const owner = readFlag2(args2, "--owner") ?? "human";
@@ -2188,7 +2276,7 @@ function runNew(args2) {
2188
2276
  tags: [plan.slug.split("-")[0] ?? "replace-me"],
2189
2277
  pinned: false
2190
2278
  });
2191
- mkdirSync5(dirname4(absPath), { recursive: true });
2279
+ mkdirSync5(dirname5(absPath), { recursive: true });
2192
2280
  writeFileSync6(absPath, rendered);
2193
2281
  console.log(`Created ${plan.filePath} with id ${plan.id}.`);
2194
2282
  try {
@@ -2249,7 +2337,7 @@ function runScan(args2) {
2249
2337
  }
2250
2338
 
2251
2339
  // src/commands/supersede.ts
2252
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
2340
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
2253
2341
  import { join as join14 } from "node:path";
2254
2342
  function runSupersede(args2) {
2255
2343
  const [oldId, newId] = args2;
@@ -2287,14 +2375,14 @@ function runSupersede(args2) {
2287
2375
  }
2288
2376
  const today = todayIso();
2289
2377
  const oldPath = join14(root, oldRec.path);
2290
- let oldContent = readFileSync10(oldPath, "utf8");
2378
+ let oldContent = readFileSync11(oldPath, "utf8");
2291
2379
  oldContent = updateFrontmatterField(oldContent, "status", "superseded");
2292
2380
  oldContent = updateFrontmatterField(oldContent, "canonical", "false");
2293
2381
  oldContent = updateFrontmatterField(oldContent, "superseded_by", newId);
2294
2382
  oldContent = updateFrontmatterField(oldContent, "last_reviewed", today);
2295
2383
  writeFileSync7(oldPath, oldContent);
2296
2384
  const newPath = join14(root, newRec.path);
2297
- let newContent = readFileSync10(newPath, "utf8");
2385
+ let newContent = readFileSync11(newPath, "utf8");
2298
2386
  newContent = appendToFrontmatterList(newContent, "supersedes", newId === oldId ? "" : oldId);
2299
2387
  newContent = updateFrontmatterField(newContent, "last_reviewed", today);
2300
2388
  writeFileSync7(newPath, newContent);
@@ -2343,13 +2431,13 @@ ${lines.join("\n")}${tail}`;
2343
2431
 
2344
2432
  // src/commands/verify-commit-msg.ts
2345
2433
  import { execFileSync as execFileSync2 } from "node:child_process";
2346
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
2434
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
2347
2435
  function runVerifyCommitMsg(args2) {
2348
2436
  const msgFile = args2[0];
2349
2437
  if (!msgFile || !existsSync12(msgFile)) {
2350
2438
  return 0;
2351
2439
  }
2352
- const message = readFileSync11(msgFile, "utf8");
2440
+ const message = readFileSync12(msgFile, "utf8");
2353
2441
  if (/^Merge\b/m.test(message) || /^Revert\b/m.test(message)) return 0;
2354
2442
  let stagedFiles = [];
2355
2443
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pieai/doc-gov",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "AI-native documentation governance CLI for project docs, agent routing, and lifecycle checks.",
5
5
  "keywords": [
6
6
  "ai-agents",