@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.
- package/dist/cli.js +220 -132
- 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
|
|
5
|
-
import { join as
|
|
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 {
|
|
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/
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
// src/core/markdown-traversal.ts
|
|
83
|
+
import {
|
|
84
|
+
existsSync,
|
|
85
|
+
lstatSync,
|
|
86
|
+
readFileSync as readFileSync2,
|
|
87
|
+
readdirSync,
|
|
88
|
+
realpathSync,
|
|
89
|
+
statSync
|
|
90
|
+
} from "node:fs";
|
|
91
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
92
|
+
function listMarkdownFiles(rootDir, roots, options = {}) {
|
|
93
|
+
const resolvedRootDir = resolve(rootDir);
|
|
94
|
+
const allowedRoots = canonicalizeRoots(resolvedRootDir, options.allowedRoots ?? []);
|
|
86
95
|
const files = [];
|
|
96
|
+
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
87
97
|
for (const root of roots) {
|
|
88
|
-
|
|
89
|
-
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
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
147
|
+
function isAllowedMarkdownFile(rootDir, path, allowedRoots, repoPath, entry) {
|
|
148
|
+
const targetPath = safeRealpath(path);
|
|
149
|
+
if (!targetPath) return false;
|
|
150
|
+
const targetStat = safeStat(targetPath);
|
|
151
|
+
if (!targetStat?.isFile()) return false;
|
|
152
|
+
if (allowedRoots.some((root) => isPathWithin(targetPath, root))) return true;
|
|
153
|
+
return entry.isSymbolicLink() && isTrustedPgsSharedRuleTarget(rootDir, repoPath, targetPath);
|
|
154
|
+
}
|
|
155
|
+
function isTrustedPgsSharedRuleTarget(rootDir, repoPath, targetPath) {
|
|
156
|
+
if (!isSharedRulePath(repoPath)) return false;
|
|
157
|
+
const centralRoot = findCentralRepositoryRoot(dirname(targetPath));
|
|
158
|
+
if (!centralRoot) return false;
|
|
159
|
+
const sourceRoot = safeRealpath(join(centralRoot, "agent-assets/rules/pie-rules"));
|
|
160
|
+
return Boolean(sourceRoot && isPathWithin(targetPath, sourceRoot));
|
|
161
|
+
}
|
|
162
|
+
function isSharedRulePath(repoPath) {
|
|
163
|
+
return repoPath.startsWith("docs/policy/shared-rules/") && repoPath.endsWith(".md");
|
|
164
|
+
}
|
|
165
|
+
function findCentralRepositoryRoot(startDir) {
|
|
166
|
+
let current = startDir;
|
|
167
|
+
while (true) {
|
|
168
|
+
if (isCentralRepositoryRoot(current)) return safeRealpath(current) ?? current;
|
|
169
|
+
const parent = dirname(current);
|
|
170
|
+
if (parent === current) return void 0;
|
|
171
|
+
current = parent;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function isCentralRepositoryRoot(path) {
|
|
175
|
+
const packageJsonPath = join(path, "package.json");
|
|
176
|
+
if (!existsSync(packageJsonPath)) return false;
|
|
177
|
+
try {
|
|
178
|
+
const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
|
|
179
|
+
if (packageJson.name !== "project-governance-system" && packageJson.name !== "pro-gov") {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
103
184
|
}
|
|
104
|
-
return
|
|
185
|
+
return existsSync(join(path, "profiles")) && existsSync(join(path, "starter")) && existsSync(join(path, "integrations"));
|
|
105
186
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
187
|
+
function isPathWithin(candidate, root) {
|
|
188
|
+
const relation = relative(root, candidate);
|
|
189
|
+
return relation === "" || !relation.startsWith(`..${sep}`) && relation !== ".." && !isAbsolute(relation);
|
|
190
|
+
}
|
|
191
|
+
function resolveFromRoot(rootDir, path) {
|
|
192
|
+
if (isAbsolute(path)) return path;
|
|
193
|
+
const resolvedRoot = resolve(rootDir);
|
|
194
|
+
const resolvedPath = resolve(path);
|
|
195
|
+
return isPathWithin(resolvedPath, resolvedRoot) ? resolvedPath : resolve(resolvedRoot, path);
|
|
108
196
|
}
|
|
109
197
|
function toRepoPath(rootDir, absolutePath) {
|
|
110
198
|
return relative(rootDir, absolutePath).split(/\\/g).join("/");
|
|
111
199
|
}
|
|
200
|
+
function safeLstat(path) {
|
|
201
|
+
try {
|
|
202
|
+
return lstatSync(path);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const code = error.code;
|
|
205
|
+
if (code === "ENOENT" || code === "ENOTDIR") return void 0;
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function safeStat(path) {
|
|
210
|
+
try {
|
|
211
|
+
return statSync(path);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const code = error.code;
|
|
214
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP") return void 0;
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function safeRealpath(path) {
|
|
219
|
+
try {
|
|
220
|
+
return realpathSync(path);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const code = error.code;
|
|
223
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP") return void 0;
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/core/files.ts
|
|
229
|
+
function listGovernedMarkdownFiles(rootDir) {
|
|
230
|
+
return listMarkdownFiles(rootDir, ["docs"], {
|
|
231
|
+
shouldSkipTree: shouldSkip
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function shouldSkip(repoPath) {
|
|
235
|
+
return isExternalArtifactPath(repoPath) || repoPath.startsWith("docs/governance/templates/") || repoPath === "docs/governance/MANIFEST.yml";
|
|
236
|
+
}
|
|
237
|
+
function toRepoPath2(rootDir, absolutePath) {
|
|
238
|
+
return relative2(rootDir, absolutePath).split(/\\/g).join("/");
|
|
239
|
+
}
|
|
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 =
|
|
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
|
|
386
|
-
import { join as
|
|
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 =
|
|
583
|
+
const root = join3(rootDir, scanDir);
|
|
456
584
|
if (!existsSync2(root)) return "0001";
|
|
457
585
|
let max = 0;
|
|
458
|
-
walkSerial(
|
|
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
|
|
465
|
-
const
|
|
466
|
-
if (
|
|
467
|
-
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
468
|
-
const m = entry.name.match(regex);
|
|
469
|
-
if (m && m[1]) onMatch(parseInt(m[1], 10));
|
|
470
|
-
}
|
|
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
|
|
490
|
-
import { dirname, join as
|
|
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 =
|
|
503
|
-
mkdirSync(
|
|
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 =
|
|
631
|
+
const path = join4(rootDir, "docs/governance/MANIFEST.yml");
|
|
508
632
|
if (!existsSync3(path)) return false;
|
|
509
|
-
return normalizeManifest(
|
|
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(
|
|
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 =
|
|
575
|
-
const content =
|
|
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
|
|
622
|
-
import { basename, dirname as
|
|
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 =
|
|
782
|
+
const fileName = basename2(oldPath);
|
|
659
783
|
const archiveDir = `docs/archive/${quarterTag()}-${record.type}`;
|
|
660
784
|
const newPath = `${archiveDir}/${fileName}`;
|
|
661
|
-
const absNew =
|
|
662
|
-
const absOld =
|
|
663
|
-
let content =
|
|
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(
|
|
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
|
|
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,
|
|
697
|
-
import { dirname as
|
|
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 =
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const stat = lstatSync(fullPath);
|
|
746
|
-
if (stat.isSymbolicLink()) continue;
|
|
747
|
-
if (stat.isDirectory()) files.push(...walkMarkdown(rootDir, fullPath));
|
|
748
|
-
else if (stat.isFile() && fullPath.endsWith(".md")) files.push(fullPath);
|
|
749
|
-
}
|
|
750
|
-
return Array.from(new Set(files)).sort();
|
|
751
|
-
}
|
|
752
|
-
function walkMarkdown(rootDir, dir) {
|
|
753
|
-
const files = [];
|
|
754
|
-
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
755
|
-
const fullPath = join6(dir, entry.name);
|
|
756
|
-
const repoPath = toRepoPath(rootDir, fullPath);
|
|
757
|
-
if (shouldSkipTree(repoPath)) continue;
|
|
758
|
-
if (entry.isSymbolicLink()) continue;
|
|
759
|
-
if (entry.isDirectory()) files.push(...walkMarkdown(rootDir, fullPath));
|
|
760
|
-
if (entry.isFile() && entry.name.endsWith(".md") && shouldIncludeSource(repoPath)) {
|
|
761
|
-
files.push(fullPath);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return files;
|
|
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("/") ?
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
890
|
-
import { isAbsolute, join as join9, resolve as
|
|
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,
|
|
894
|
-
import { join as join8, relative as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
if (existsSync6(absRoot)) walk2(absRoot);
|
|
1511
|
-
}
|
|
1512
|
-
return matches.sort();
|
|
1513
|
-
function walk2(dir) {
|
|
1514
|
-
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
1515
|
-
if (entry.isSymbolicLink()) continue;
|
|
1516
|
-
if (entry.isDirectory()) {
|
|
1517
|
-
walk2(join8(dir, entry.name));
|
|
1518
|
-
continue;
|
|
1519
|
-
}
|
|
1520
|
-
if (!entry.isFile()) continue;
|
|
1521
|
-
if (entry.name.toLowerCase() !== "readme.md") continue;
|
|
1522
|
-
const repoPath = relative2(rootDir, join8(dir, entry.name)).split(/\\/g).join("/");
|
|
1523
|
-
if (repoPath !== "README.md") matches.push(repoPath);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
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 =
|
|
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 =
|
|
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) &&
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) && !
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 {
|