@forwardimpact/libcoaligned 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/coaligned.js +41 -29
- package/package.json +4 -1
- package/src/instructions.js +54 -37
- package/src/jtbd.js +29 -22
package/bin/coaligned.js
CHANGED
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import "@forwardimpact/libpreflight/node22";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { createDefaultRuntime } from "@forwardimpact/libutil/runtime";
|
|
6
6
|
import { createCli } from "@forwardimpact/libcli";
|
|
7
7
|
import { emitFindingsJson, emitFindingsText } from "@forwardimpact/libutil";
|
|
8
8
|
import { checkInstructions, checkJtbd } from "../src/index.js";
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
12
|
-
);
|
|
10
|
+
const runtime = createDefaultRuntime();
|
|
13
11
|
|
|
14
12
|
const definition = {
|
|
15
13
|
name: "coaligned",
|
|
16
|
-
version: VERSION,
|
|
17
14
|
description:
|
|
18
15
|
"Enforce the layered instruction architecture defined in COALIGNED.md (no subcommand: run every check)",
|
|
19
16
|
commands: [
|
|
@@ -46,57 +43,72 @@ const definition = {
|
|
|
46
43
|
examples: ["coaligned", "coaligned instructions", "coaligned jtbd --fix"],
|
|
47
44
|
};
|
|
48
45
|
|
|
49
|
-
const cli = createCli(definition
|
|
46
|
+
const cli = createCli(definition, {
|
|
47
|
+
runtime,
|
|
48
|
+
packageJsonUrl: new URL("../package.json", import.meta.url),
|
|
49
|
+
});
|
|
50
50
|
|
|
51
|
-
function writeFindings(findings, passMessage, jsonOutput, cwd) {
|
|
51
|
+
function writeFindings(findings, passMessage, jsonOutput, cwd, rt) {
|
|
52
52
|
if (jsonOutput) {
|
|
53
|
-
|
|
53
|
+
rt.proc.stdout.write(emitFindingsJson(findings));
|
|
54
54
|
} else if (findings.length > 0) {
|
|
55
|
-
|
|
55
|
+
rt.proc.stderr.write(emitFindingsText(findings, { cwd, passMessage }));
|
|
56
56
|
} else {
|
|
57
|
-
|
|
57
|
+
rt.proc.stdout.write(emitFindingsText(findings, { cwd, passMessage }));
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
async function runInstructions(root, jsonOutput) {
|
|
62
|
-
const findings = await checkInstructions({ root });
|
|
63
|
-
writeFindings(
|
|
61
|
+
async function runInstructions(root, jsonOutput, rt) {
|
|
62
|
+
const findings = await checkInstructions({ root, runtime: rt });
|
|
63
|
+
writeFindings(
|
|
64
|
+
findings,
|
|
65
|
+
"coaligned instructions passed",
|
|
66
|
+
jsonOutput,
|
|
67
|
+
root,
|
|
68
|
+
rt,
|
|
69
|
+
);
|
|
64
70
|
return findings.length > 0 ? 1 : 0;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
async function runJtbd(root, fix, jsonOutput) {
|
|
68
|
-
const { findings, stale, fixed } = await checkJtbd({
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
async function runJtbd(root, fix, jsonOutput, rt) {
|
|
74
|
+
const { findings, stale, fixed } = await checkJtbd({
|
|
75
|
+
root,
|
|
76
|
+
fix,
|
|
77
|
+
runtime: rt,
|
|
78
|
+
});
|
|
79
|
+
writeFindings(findings, "coaligned jtbd passed", jsonOutput, root, rt);
|
|
80
|
+
for (const f of fixed) rt.proc.stdout.write(`Regenerated ${f}.\n`);
|
|
71
81
|
if (stale.length > 0 && !jsonOutput) {
|
|
72
|
-
|
|
82
|
+
rt.proc.stderr.write(
|
|
73
83
|
`\n${stale.length} file${stale.length === 1 ? "" : "s"} out of date — run \`coaligned jtbd --fix\` to regenerate:\n`,
|
|
74
84
|
);
|
|
75
|
-
for (const s of stale)
|
|
85
|
+
for (const s of stale) rt.proc.stderr.write(` - ${s}\n`);
|
|
76
86
|
}
|
|
77
87
|
return findings.length > 0 || stale.length > 0 ? 1 : 0;
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
async function instructionsHandler(ctx) {
|
|
81
|
-
|
|
91
|
+
const rt = ctx.deps.runtime;
|
|
92
|
+
return runInstructions(ctx.data.root, !!ctx.options.json, rt);
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
async function jtbdHandler(ctx) {
|
|
85
|
-
|
|
96
|
+
const rt = ctx.deps.runtime;
|
|
97
|
+
return runJtbd(ctx.data.root, !!ctx.options.fix, !!ctx.options.json, rt);
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
async function main() {
|
|
89
|
-
const parsed = cli.parse(
|
|
90
|
-
if (!parsed) return 0;
|
|
101
|
+
const parsed = cli.parse(runtime.proc.argv.slice(2));
|
|
102
|
+
if (!parsed) return runtime.proc.exit(0);
|
|
91
103
|
|
|
92
|
-
const root =
|
|
104
|
+
const root = runtime.proc.cwd();
|
|
93
105
|
const jsonOutput = !!parsed.values.json;
|
|
94
106
|
|
|
95
107
|
// No subcommand → run every check; --fix stays jtbd-only and must be opted
|
|
96
108
|
// into explicitly via `coaligned jtbd --fix`.
|
|
97
109
|
if (parsed.positionals.length === 0) {
|
|
98
|
-
const a = await runInstructions(root, jsonOutput);
|
|
99
|
-
const b = await runJtbd(root, false, jsonOutput);
|
|
110
|
+
const a = await runInstructions(root, jsonOutput, runtime);
|
|
111
|
+
const b = await runJtbd(root, false, jsonOutput, runtime);
|
|
100
112
|
return a || b;
|
|
101
113
|
}
|
|
102
114
|
|
|
@@ -106,12 +118,12 @@ async function main() {
|
|
|
106
118
|
return 2;
|
|
107
119
|
}
|
|
108
120
|
|
|
109
|
-
return await cli.dispatch(parsed, { data: { root } });
|
|
121
|
+
return await cli.dispatch(parsed, { data: { root }, deps: { runtime } });
|
|
110
122
|
}
|
|
111
123
|
|
|
112
124
|
main()
|
|
113
|
-
.then((code) =>
|
|
125
|
+
.then((code) => runtime.proc.exit(code ?? 0))
|
|
114
126
|
.catch((err) => {
|
|
115
127
|
cli.error(err.message);
|
|
116
|
-
|
|
128
|
+
runtime.proc.exit(1);
|
|
117
129
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libcoaligned",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Co-Aligned architecture checks — enforce instruction-layer length caps and JTBD invariants across the repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coaligned",
|
|
@@ -50,6 +50,9 @@
|
|
|
50
50
|
"@forwardimpact/libutil": "*",
|
|
51
51
|
"prettier": "^3.0.0"
|
|
52
52
|
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@forwardimpact/libmock": "^0.1.0"
|
|
55
|
+
},
|
|
53
56
|
"engines": {
|
|
54
57
|
"bun": ">=1.2.0",
|
|
55
58
|
"node": ">=22.0.0"
|
package/src/instructions.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
2
1
|
import { resolve } from "node:path";
|
|
3
2
|
import { runRules } from "@forwardimpact/libutil";
|
|
4
3
|
|
|
@@ -24,10 +23,10 @@ const ITEM_SPLIT_RE = /^\s*-\s*\[[ xX]\]\s*/m;
|
|
|
24
23
|
const lineCount = (text) => (text.match(/\n/g) || []).length;
|
|
25
24
|
const wordCount = (text) => (text.match(/\S+/g) || []).length;
|
|
26
25
|
|
|
27
|
-
async function walk(root, dir, visit) {
|
|
26
|
+
async function walk(root, dir, visit, fs) {
|
|
28
27
|
let entries;
|
|
29
28
|
try {
|
|
30
|
-
entries = await readdir(resolve(root, dir), { withFileTypes: true });
|
|
29
|
+
entries = await fs.readdir(resolve(root, dir), { withFileTypes: true });
|
|
31
30
|
} catch {
|
|
32
31
|
return;
|
|
33
32
|
}
|
|
@@ -35,88 +34,103 @@ async function walk(root, dir, visit) {
|
|
|
35
34
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
36
35
|
const path = dir === "." ? e.name : `${dir}/${e.name}`;
|
|
37
36
|
await visit(e, path);
|
|
38
|
-
if (e.isDirectory()) await walk(root, path, visit);
|
|
37
|
+
if (e.isDirectory()) await walk(root, path, visit, fs);
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
async function listFiles(root, dir, match) {
|
|
41
|
+
async function listFiles(root, dir, match, fs) {
|
|
43
42
|
try {
|
|
44
|
-
const entries = await readdir(resolve(root, dir), {
|
|
43
|
+
const entries = await fs.readdir(resolve(root, dir), {
|
|
44
|
+
withFileTypes: true,
|
|
45
|
+
});
|
|
45
46
|
return entries.filter(match).map((e) => `${dir}/${e.name}`);
|
|
46
47
|
} catch {
|
|
47
48
|
return [];
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
async function readText(root, path) {
|
|
52
|
+
async function readText(root, path, fs) {
|
|
52
53
|
try {
|
|
53
|
-
return await readFile(resolve(root, path), "utf8");
|
|
54
|
+
return await fs.readFile(resolve(root, path), "utf8");
|
|
54
55
|
} catch {
|
|
55
56
|
return null;
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
async function findByName(root, name, kind) {
|
|
60
|
+
async function findByName(root, name, kind, fs) {
|
|
60
61
|
const out = [];
|
|
61
|
-
await walk(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
await walk(
|
|
63
|
+
root,
|
|
64
|
+
".",
|
|
65
|
+
(e, path) => {
|
|
66
|
+
const isMatch = kind === "file" ? e.isFile() : e.isDirectory();
|
|
67
|
+
if (isMatch && e.name === name) out.push(path);
|
|
68
|
+
},
|
|
69
|
+
fs,
|
|
70
|
+
);
|
|
65
71
|
return out;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
async function findAgentProfiles(root, claudeDirs) {
|
|
74
|
+
async function findAgentProfiles(root, claudeDirs, fs) {
|
|
69
75
|
const out = [];
|
|
70
76
|
for (const d of claudeDirs) {
|
|
71
77
|
const files = await listFiles(
|
|
72
78
|
root,
|
|
73
79
|
`${d}/agents`,
|
|
74
80
|
(e) => e.isFile() && e.name.endsWith(".md"),
|
|
81
|
+
fs,
|
|
75
82
|
);
|
|
76
83
|
out.push(...files);
|
|
77
84
|
}
|
|
78
85
|
return out;
|
|
79
86
|
}
|
|
80
87
|
|
|
81
|
-
async function findAgentReferences(root, claudeDirs) {
|
|
88
|
+
async function findAgentReferences(root, claudeDirs, fs) {
|
|
82
89
|
const out = [];
|
|
83
90
|
for (const d of claudeDirs) {
|
|
84
91
|
const files = await listFiles(
|
|
85
92
|
root,
|
|
86
93
|
`${d}/agents/references`,
|
|
87
94
|
(e) => e.isFile() && e.name.endsWith(".md"),
|
|
95
|
+
fs,
|
|
88
96
|
);
|
|
89
97
|
out.push(...files);
|
|
90
98
|
}
|
|
91
99
|
return out;
|
|
92
100
|
}
|
|
93
101
|
|
|
94
|
-
async function findSkillDirs(root, claudeDirs) {
|
|
102
|
+
async function findSkillDirs(root, claudeDirs, fs) {
|
|
95
103
|
const out = [];
|
|
96
104
|
for (const d of claudeDirs) {
|
|
97
|
-
const dirs = await listFiles(
|
|
105
|
+
const dirs = await listFiles(
|
|
106
|
+
root,
|
|
107
|
+
`${d}/skills`,
|
|
108
|
+
(e) => e.isDirectory(),
|
|
109
|
+
fs,
|
|
110
|
+
);
|
|
98
111
|
out.push(...dirs);
|
|
99
112
|
}
|
|
100
113
|
return out;
|
|
101
114
|
}
|
|
102
115
|
|
|
103
|
-
async function findSkillReferences(root, skillDirs) {
|
|
116
|
+
async function findSkillReferences(root, skillDirs, fs) {
|
|
104
117
|
const out = [];
|
|
105
118
|
for (const d of skillDirs) {
|
|
106
119
|
const files = await listFiles(
|
|
107
120
|
root,
|
|
108
121
|
`${d}/references`,
|
|
109
122
|
(e) => e.isFile() && e.name.endsWith(".md"),
|
|
123
|
+
fs,
|
|
110
124
|
);
|
|
111
125
|
out.push(...files);
|
|
112
126
|
}
|
|
113
127
|
return out;
|
|
114
128
|
}
|
|
115
129
|
|
|
116
|
-
async function buildLayers(root) {
|
|
117
|
-
const claudeDirs = await findByName(root, ".claude", "dir");
|
|
118
|
-
const skillDirs = await findSkillDirs(root, claudeDirs);
|
|
119
|
-
const allClaude = await findByName(root, "CLAUDE.md", "file");
|
|
130
|
+
async function buildLayers(root, fs) {
|
|
131
|
+
const claudeDirs = await findByName(root, ".claude", "dir", fs);
|
|
132
|
+
const skillDirs = await findSkillDirs(root, claudeDirs, fs);
|
|
133
|
+
const allClaude = await findByName(root, "CLAUDE.md", "file", fs);
|
|
120
134
|
const rootClaude = allClaude.filter((p) => p === "CLAUDE.md");
|
|
121
135
|
const subdirClaude = allClaude.filter((p) => p !== "CLAUDE.md");
|
|
122
136
|
return {
|
|
@@ -156,14 +170,14 @@ async function buildLayers(root) {
|
|
|
156
170
|
name: "agent profile",
|
|
157
171
|
maxLines: 72,
|
|
158
172
|
maxWords: 448,
|
|
159
|
-
files: await findAgentProfiles(root, claudeDirs),
|
|
173
|
+
files: await findAgentProfiles(root, claudeDirs, fs),
|
|
160
174
|
},
|
|
161
175
|
{
|
|
162
176
|
id: "L4",
|
|
163
177
|
name: "agent reference",
|
|
164
178
|
maxLines: 192,
|
|
165
179
|
maxWords: 1280,
|
|
166
|
-
files: await findAgentReferences(root, claudeDirs),
|
|
180
|
+
files: await findAgentReferences(root, claudeDirs, fs),
|
|
167
181
|
},
|
|
168
182
|
{
|
|
169
183
|
id: "L5",
|
|
@@ -177,7 +191,7 @@ async function buildLayers(root) {
|
|
|
177
191
|
name: "skill reference",
|
|
178
192
|
maxLines: 128,
|
|
179
193
|
maxWords: 768,
|
|
180
|
-
files: await findSkillReferences(root, skillDirs),
|
|
194
|
+
files: await findSkillReferences(root, skillDirs, fs),
|
|
181
195
|
},
|
|
182
196
|
],
|
|
183
197
|
};
|
|
@@ -193,11 +207,11 @@ function offsetToLine(text, offset) {
|
|
|
193
207
|
|
|
194
208
|
// -- Subject builders ----------------------------------------------------
|
|
195
209
|
|
|
196
|
-
async function buildFileSubjects(root, layers) {
|
|
210
|
+
async function buildFileSubjects(root, layers, fs) {
|
|
197
211
|
const subjects = [];
|
|
198
212
|
for (const layer of layers) {
|
|
199
213
|
for (const relPath of layer.files) {
|
|
200
|
-
const text = await readText(root, relPath);
|
|
214
|
+
const text = await readText(root, relPath, fs);
|
|
201
215
|
if (text == null) continue;
|
|
202
216
|
subjects.push({
|
|
203
217
|
path: resolve(root, relPath),
|
|
@@ -212,10 +226,10 @@ async function buildFileSubjects(root, layers) {
|
|
|
212
226
|
return subjects;
|
|
213
227
|
}
|
|
214
228
|
|
|
215
|
-
async function buildChecklistSubjects(root, sources) {
|
|
229
|
+
async function buildChecklistSubjects(root, sources, fs) {
|
|
216
230
|
const subjects = [];
|
|
217
231
|
for (const relPath of sources) {
|
|
218
|
-
const text = await readText(root, relPath);
|
|
232
|
+
const text = await readText(root, relPath, fs);
|
|
219
233
|
if (text == null) continue;
|
|
220
234
|
const absPath = resolve(root, relPath);
|
|
221
235
|
CHECKLIST_RE.lastIndex = 0;
|
|
@@ -301,18 +315,21 @@ export const INSTRUCTION_RULES = [
|
|
|
301
315
|
* Walk the repo rooted at `root`, applying the L1–L7 caps from COALIGNED.md.
|
|
302
316
|
* Each layer is gated by a line cap AND a word cap; either breach fails.
|
|
303
317
|
*
|
|
304
|
-
* @param {{ root: string }} options
|
|
318
|
+
* @param {{ root: string, runtime?: import('@forwardimpact/libutil/runtime').Runtime }} options
|
|
305
319
|
* @returns {Promise<Finding[]>} Structured findings; empty when conformant.
|
|
306
320
|
* Each Finding is `{ id, level, path, lineNo?, message, hint? }` for use
|
|
307
321
|
* with `emitFindingsText` / `emitFindingsJson` from libutil.
|
|
308
322
|
*/
|
|
309
|
-
export async function checkInstructions({ root }) {
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
323
|
+
export async function checkInstructions({ root, runtime }) {
|
|
324
|
+
if (!runtime) throw new Error("runtime is required");
|
|
325
|
+
const { fs } = runtime;
|
|
326
|
+
const { layers, skillDirs } = await buildLayers(root, fs);
|
|
327
|
+
const fileSubjects = await buildFileSubjects(root, layers, fs);
|
|
328
|
+
const checklistSubjects = await buildChecklistSubjects(
|
|
329
|
+
root,
|
|
330
|
+
["CONTRIBUTING.md", ...skillDirs.map((d) => `${d}/SKILL.md`)],
|
|
331
|
+
fs,
|
|
332
|
+
);
|
|
316
333
|
|
|
317
334
|
const ctx = {
|
|
318
335
|
subjects: {
|
package/src/jtbd.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
1
|
import { join } from "node:path";
|
|
3
2
|
import * as prettier from "prettier";
|
|
4
3
|
import { runRules } from "@forwardimpact/libutil";
|
|
@@ -39,15 +38,15 @@ function catalogs(root) {
|
|
|
39
38
|
];
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
function loadPackages(dir, filter) {
|
|
43
|
-
if (!existsSync(dir)) return [];
|
|
41
|
+
function loadPackages(dir, filter, fsSync) {
|
|
42
|
+
if (!fsSync.existsSync(dir)) return [];
|
|
44
43
|
const out = [];
|
|
45
|
-
for (const name of readdirSync(dir)) {
|
|
44
|
+
for (const name of fsSync.readdirSync(dir)) {
|
|
46
45
|
if (!filter(name)) continue;
|
|
47
46
|
const pkgPath = join(dir, name, "package.json");
|
|
48
47
|
let pkg;
|
|
49
48
|
try {
|
|
50
|
-
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
49
|
+
pkg = JSON.parse(fsSync.readFileSync(pkgPath, "utf8"));
|
|
51
50
|
} catch {
|
|
52
51
|
continue;
|
|
53
52
|
}
|
|
@@ -404,18 +403,18 @@ async function buildDescription(content, description, formatMarkdown) {
|
|
|
404
403
|
);
|
|
405
404
|
}
|
|
406
405
|
|
|
407
|
-
function commitUpdate(filePath, label, original, updated, fix, result) {
|
|
406
|
+
function commitUpdate(filePath, label, original, updated, fix, result, fsSync) {
|
|
408
407
|
if (updated === null || updated === original) return;
|
|
409
408
|
if (!fix) {
|
|
410
409
|
result.stale.push(label);
|
|
411
410
|
return;
|
|
412
411
|
}
|
|
413
|
-
writeFileSync(filePath, updated);
|
|
412
|
+
fsSync.writeFileSync(filePath, updated);
|
|
414
413
|
result.fixed.push(label);
|
|
415
414
|
}
|
|
416
415
|
|
|
417
|
-
async function processCatalog(catalog, fix, formatMarkdown, result) {
|
|
418
|
-
const packages = loadPackages(catalog.dir, catalog.filter);
|
|
416
|
+
async function processCatalog(catalog, fix, formatMarkdown, result, fsSync) {
|
|
417
|
+
const packages = loadPackages(catalog.dir, catalog.filter, fsSync);
|
|
419
418
|
const findings = validate(packages, catalog.dir, catalog.name, {
|
|
420
419
|
skipUniqueHires: catalog.skipUniqueHires ?? false,
|
|
421
420
|
});
|
|
@@ -424,8 +423,8 @@ async function processCatalog(catalog, fix, formatMarkdown, result) {
|
|
|
424
423
|
return;
|
|
425
424
|
}
|
|
426
425
|
|
|
427
|
-
if (existsSync(catalog.readme)) {
|
|
428
|
-
const original = readFileSync(catalog.readme, "utf8");
|
|
426
|
+
if (fsSync.existsSync(catalog.readme)) {
|
|
427
|
+
const original = fsSync.readFileSync(catalog.readme, "utf8");
|
|
429
428
|
let content = original;
|
|
430
429
|
content = await buildCatalog(
|
|
431
430
|
content,
|
|
@@ -441,14 +440,15 @@ async function processCatalog(catalog, fix, formatMarkdown, result) {
|
|
|
441
440
|
content,
|
|
442
441
|
fix,
|
|
443
442
|
result,
|
|
443
|
+
fsSync,
|
|
444
444
|
);
|
|
445
445
|
}
|
|
446
446
|
|
|
447
447
|
for (const { dir, pkg } of packages) {
|
|
448
448
|
if (!pkg.description) continue;
|
|
449
449
|
const pkgReadme = join(catalog.dir, dir, "README.md");
|
|
450
|
-
if (!existsSync(pkgReadme)) continue;
|
|
451
|
-
const original = readFileSync(pkgReadme, "utf8");
|
|
450
|
+
if (!fsSync.existsSync(pkgReadme)) continue;
|
|
451
|
+
const original = fsSync.readFileSync(pkgReadme, "utf8");
|
|
452
452
|
const updated = await buildDescription(
|
|
453
453
|
original,
|
|
454
454
|
pkg.description,
|
|
@@ -461,15 +461,20 @@ async function processCatalog(catalog, fix, formatMarkdown, result) {
|
|
|
461
461
|
updated,
|
|
462
462
|
fix,
|
|
463
463
|
result,
|
|
464
|
+
fsSync,
|
|
464
465
|
);
|
|
465
466
|
}
|
|
466
467
|
}
|
|
467
468
|
|
|
468
|
-
async function processJtbdMd(root, fix, formatMarkdown, result) {
|
|
469
|
+
async function processJtbdMd(root, fix, formatMarkdown, result, fsSync) {
|
|
469
470
|
const jtbdPath = join(root, "JTBD.md");
|
|
470
|
-
if (!existsSync(jtbdPath)) return;
|
|
471
|
+
if (!fsSync.existsSync(jtbdPath)) return;
|
|
471
472
|
const productsCatalog = catalogs(root).find((c) => c.name === "products");
|
|
472
|
-
const packages = loadPackages(
|
|
473
|
+
const packages = loadPackages(
|
|
474
|
+
productsCatalog.dir,
|
|
475
|
+
productsCatalog.filter,
|
|
476
|
+
fsSync,
|
|
477
|
+
);
|
|
473
478
|
const findings = validate(packages, productsCatalog.dir, "products", {
|
|
474
479
|
skipUniqueHires: true,
|
|
475
480
|
});
|
|
@@ -477,11 +482,11 @@ async function processJtbdMd(root, fix, formatMarkdown, result) {
|
|
|
477
482
|
if (result.findings.length === 0) result.findings.push(...findings);
|
|
478
483
|
return;
|
|
479
484
|
}
|
|
480
|
-
const original = readFileSync(jtbdPath, "utf8");
|
|
485
|
+
const original = fsSync.readFileSync(jtbdPath, "utf8");
|
|
481
486
|
const updated = await buildJobs(original, packages, formatMarkdown, {
|
|
482
487
|
capitalize: true,
|
|
483
488
|
});
|
|
484
|
-
commitUpdate(jtbdPath, "JTBD.md", original, updated, fix, result);
|
|
489
|
+
commitUpdate(jtbdPath, "JTBD.md", original, updated, fix, result, fsSync);
|
|
485
490
|
}
|
|
486
491
|
|
|
487
492
|
/**
|
|
@@ -489,22 +494,24 @@ async function processJtbdMd(root, fix, formatMarkdown, result) {
|
|
|
489
494
|
* libraries/, and (when `fix` is true) regenerate the marker-delimited catalog,
|
|
490
495
|
* jobs, and description blocks in the corresponding README.md and JTBD.md.
|
|
491
496
|
*
|
|
492
|
-
* @param {{ root: string, fix?: boolean }} options
|
|
497
|
+
* @param {{ root: string, fix?: boolean, runtime?: import('@forwardimpact/libutil/runtime').Runtime }} options
|
|
493
498
|
* @returns {Promise<{ findings: Finding[], stale: string[], fixed: string[] }>}
|
|
494
499
|
* `findings` are validation failures (structured for `emitFindingsText` /
|
|
495
500
|
* `emitFindingsJson` from libutil); `stale` is files whose generated blocks
|
|
496
501
|
* are out of date (only populated when `fix` is false); `fixed` is files
|
|
497
502
|
* that were rewritten in place.
|
|
498
503
|
*/
|
|
499
|
-
export async function checkJtbd({ root, fix = false }) {
|
|
504
|
+
export async function checkJtbd({ root, fix = false, runtime }) {
|
|
505
|
+
if (!runtime) throw new Error("runtime is required");
|
|
506
|
+
const { fsSync } = runtime;
|
|
500
507
|
const result = { findings: [], stale: [], fixed: [] };
|
|
501
508
|
const prettierConfig = await prettier.resolveConfig(join(root, "JTBD.md"));
|
|
502
509
|
const formatMarkdown = makeFormatter(prettierConfig);
|
|
503
510
|
|
|
504
511
|
for (const catalog of catalogs(root)) {
|
|
505
|
-
await processCatalog(catalog, fix, formatMarkdown, result);
|
|
512
|
+
await processCatalog(catalog, fix, formatMarkdown, result, fsSync);
|
|
506
513
|
}
|
|
507
|
-
await processJtbdMd(root, fix, formatMarkdown, result);
|
|
514
|
+
await processJtbdMd(root, fix, formatMarkdown, result, fsSync);
|
|
508
515
|
|
|
509
516
|
return result;
|
|
510
517
|
}
|