@forwardimpact/libcoaligned 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/bin/coaligned.js +42 -25
- package/package.json +1 -1
- package/src/instructions.js +82 -45
- package/src/jtbd.js +29 -22
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ invariants across the repo.
|
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
13
|
npx coaligned # run every check (instructions + jtbd)
|
|
14
|
-
npx coaligned instructions # enforce L1–
|
|
14
|
+
npx coaligned instructions # enforce L1–L7 length and checklist caps
|
|
15
15
|
npx coaligned jtbd # validate JTBD entries against package.json
|
|
16
16
|
npx coaligned jtbd --fix # regenerate catalog and job blocks in place
|
|
17
17
|
```
|
|
@@ -20,8 +20,9 @@ The two subcommands implement the contract described in
|
|
|
20
20
|
[COALIGNED.md](https://github.com/forwardimpact/monorepo/blob/main/COALIGNED.md):
|
|
21
21
|
|
|
22
22
|
- `instructions` — every layer (L1 CLAUDE.md, L2 CONTRIBUTING.md / JTBD.md,
|
|
23
|
-
L3 agent profile, L4
|
|
24
|
-
a line cap **and** a word cap. Either breach
|
|
23
|
+
L3 agent profile, L4 agent reference, L5 SKILL.md, L6 skill reference,
|
|
24
|
+
L7 checklist block) is gated by a line cap **and** a word cap. Either breach
|
|
25
|
+
fails.
|
|
25
26
|
- `jtbd` — each `package.json .jobs` entry is validated against the JTBD
|
|
26
27
|
schema; with `--fix`, marker-delimited blocks in `<dir>/README.md`,
|
|
27
28
|
`<dir>/<pkg>/README.md`, and root `JTBD.md` are regenerated.
|
package/bin/coaligned.js
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
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 runtime = createDefaultRuntime();
|
|
11
|
+
|
|
10
12
|
const { version: VERSION } = JSON.parse(
|
|
11
|
-
readFileSync(
|
|
13
|
+
runtime.fsSync.readFileSync(
|
|
14
|
+
new URL("../package.json", import.meta.url),
|
|
15
|
+
"utf8",
|
|
16
|
+
),
|
|
12
17
|
);
|
|
13
18
|
|
|
14
19
|
const definition = {
|
|
@@ -48,55 +53,67 @@ const definition = {
|
|
|
48
53
|
|
|
49
54
|
const cli = createCli(definition);
|
|
50
55
|
|
|
51
|
-
function writeFindings(findings, passMessage, jsonOutput, cwd) {
|
|
56
|
+
function writeFindings(findings, passMessage, jsonOutput, cwd, rt) {
|
|
52
57
|
if (jsonOutput) {
|
|
53
|
-
|
|
58
|
+
rt.proc.stdout.write(emitFindingsJson(findings));
|
|
54
59
|
} else if (findings.length > 0) {
|
|
55
|
-
|
|
60
|
+
rt.proc.stderr.write(emitFindingsText(findings, { cwd, passMessage }));
|
|
56
61
|
} else {
|
|
57
|
-
|
|
62
|
+
rt.proc.stdout.write(emitFindingsText(findings, { cwd, passMessage }));
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
async function runInstructions(root, jsonOutput) {
|
|
62
|
-
const findings = await checkInstructions({ root });
|
|
63
|
-
writeFindings(
|
|
66
|
+
async function runInstructions(root, jsonOutput, rt) {
|
|
67
|
+
const findings = await checkInstructions({ root, runtime: rt });
|
|
68
|
+
writeFindings(
|
|
69
|
+
findings,
|
|
70
|
+
"coaligned instructions passed",
|
|
71
|
+
jsonOutput,
|
|
72
|
+
root,
|
|
73
|
+
rt,
|
|
74
|
+
);
|
|
64
75
|
return findings.length > 0 ? 1 : 0;
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
async function runJtbd(root, fix, jsonOutput) {
|
|
68
|
-
const { findings, stale, fixed } = await checkJtbd({
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
async function runJtbd(root, fix, jsonOutput, rt) {
|
|
79
|
+
const { findings, stale, fixed } = await checkJtbd({
|
|
80
|
+
root,
|
|
81
|
+
fix,
|
|
82
|
+
runtime: rt,
|
|
83
|
+
});
|
|
84
|
+
writeFindings(findings, "coaligned jtbd passed", jsonOutput, root, rt);
|
|
85
|
+
for (const f of fixed) rt.proc.stdout.write(`Regenerated ${f}.\n`);
|
|
71
86
|
if (stale.length > 0 && !jsonOutput) {
|
|
72
|
-
|
|
87
|
+
rt.proc.stderr.write(
|
|
73
88
|
`\n${stale.length} file${stale.length === 1 ? "" : "s"} out of date — run \`coaligned jtbd --fix\` to regenerate:\n`,
|
|
74
89
|
);
|
|
75
|
-
for (const s of stale)
|
|
90
|
+
for (const s of stale) rt.proc.stderr.write(` - ${s}\n`);
|
|
76
91
|
}
|
|
77
92
|
return findings.length > 0 || stale.length > 0 ? 1 : 0;
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
async function instructionsHandler(ctx) {
|
|
81
|
-
|
|
96
|
+
const rt = ctx.deps.runtime;
|
|
97
|
+
return runInstructions(ctx.data.root, !!ctx.options.json, rt);
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
async function jtbdHandler(ctx) {
|
|
85
|
-
|
|
101
|
+
const rt = ctx.deps.runtime;
|
|
102
|
+
return runJtbd(ctx.data.root, !!ctx.options.fix, !!ctx.options.json, rt);
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
async function main() {
|
|
89
|
-
const parsed = cli.parse(
|
|
90
|
-
if (!parsed) return 0;
|
|
106
|
+
const parsed = cli.parse(runtime.proc.argv.slice(2));
|
|
107
|
+
if (!parsed) return runtime.proc.exit(0);
|
|
91
108
|
|
|
92
|
-
const root =
|
|
109
|
+
const root = runtime.proc.cwd();
|
|
93
110
|
const jsonOutput = !!parsed.values.json;
|
|
94
111
|
|
|
95
112
|
// No subcommand → run every check; --fix stays jtbd-only and must be opted
|
|
96
113
|
// into explicitly via `coaligned jtbd --fix`.
|
|
97
114
|
if (parsed.positionals.length === 0) {
|
|
98
|
-
const a = await runInstructions(root, jsonOutput);
|
|
99
|
-
const b = await runJtbd(root, false, jsonOutput);
|
|
115
|
+
const a = await runInstructions(root, jsonOutput, runtime);
|
|
116
|
+
const b = await runJtbd(root, false, jsonOutput, runtime);
|
|
100
117
|
return a || b;
|
|
101
118
|
}
|
|
102
119
|
|
|
@@ -106,12 +123,12 @@ async function main() {
|
|
|
106
123
|
return 2;
|
|
107
124
|
}
|
|
108
125
|
|
|
109
|
-
return await cli.dispatch(parsed, { data: { root } });
|
|
126
|
+
return await cli.dispatch(parsed, { data: { root }, deps: { runtime } });
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
main()
|
|
113
|
-
.then((code) =>
|
|
130
|
+
.then((code) => runtime.proc.exit(code ?? 0))
|
|
114
131
|
.catch((err) => {
|
|
115
132
|
cli.error(err.message);
|
|
116
|
-
|
|
133
|
+
runtime.proc.exit(1);
|
|
117
134
|
});
|
package/package.json
CHANGED
package/src/instructions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
2
1
|
import { resolve } from "node:path";
|
|
2
|
+
import { createDefaultRuntime } from "@forwardimpact/libutil/runtime";
|
|
3
3
|
import { runRules } from "@forwardimpact/libutil";
|
|
4
4
|
|
|
5
5
|
const SKIP_DIRS = new Set([
|
|
@@ -14,8 +14,8 @@ const SKIP_DIRS = new Set([
|
|
|
14
14
|
"worktrees",
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const L7_MAX_ITEMS = 9;
|
|
18
|
+
const L7_MAX_WORDS_PER_ITEM = 32;
|
|
19
19
|
|
|
20
20
|
const CHECKLIST_RE =
|
|
21
21
|
/<(read_do_checklist|do_confirm_checklist)\b[^>]*>([\s\S]*?)<\/\1>/g;
|
|
@@ -24,10 +24,10 @@ const ITEM_SPLIT_RE = /^\s*-\s*\[[ xX]\]\s*/m;
|
|
|
24
24
|
const lineCount = (text) => (text.match(/\n/g) || []).length;
|
|
25
25
|
const wordCount = (text) => (text.match(/\S+/g) || []).length;
|
|
26
26
|
|
|
27
|
-
async function walk(root, dir, visit) {
|
|
27
|
+
async function walk(root, dir, visit, fs) {
|
|
28
28
|
let entries;
|
|
29
29
|
try {
|
|
30
|
-
entries = await readdir(resolve(root, dir), { withFileTypes: true });
|
|
30
|
+
entries = await fs.readdir(resolve(root, dir), { withFileTypes: true });
|
|
31
31
|
} catch {
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
@@ -35,75 +35,103 @@ async function walk(root, dir, visit) {
|
|
|
35
35
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
36
36
|
const path = dir === "." ? e.name : `${dir}/${e.name}`;
|
|
37
37
|
await visit(e, path);
|
|
38
|
-
if (e.isDirectory()) await walk(root, path, visit);
|
|
38
|
+
if (e.isDirectory()) await walk(root, path, visit, fs);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async function listFiles(root, dir, match) {
|
|
42
|
+
async function listFiles(root, dir, match, fs) {
|
|
43
43
|
try {
|
|
44
|
-
const entries = await readdir(resolve(root, dir), {
|
|
44
|
+
const entries = await fs.readdir(resolve(root, dir), {
|
|
45
|
+
withFileTypes: true,
|
|
46
|
+
});
|
|
45
47
|
return entries.filter(match).map((e) => `${dir}/${e.name}`);
|
|
46
48
|
} catch {
|
|
47
49
|
return [];
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
async function readText(root, path) {
|
|
53
|
+
async function readText(root, path, fs) {
|
|
52
54
|
try {
|
|
53
|
-
return await readFile(resolve(root, path), "utf8");
|
|
55
|
+
return await fs.readFile(resolve(root, path), "utf8");
|
|
54
56
|
} catch {
|
|
55
57
|
return null;
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
async function findByName(root, name, kind) {
|
|
61
|
+
async function findByName(root, name, kind, fs) {
|
|
60
62
|
const out = [];
|
|
61
|
-
await walk(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
await walk(
|
|
64
|
+
root,
|
|
65
|
+
".",
|
|
66
|
+
(e, path) => {
|
|
67
|
+
const isMatch = kind === "file" ? e.isFile() : e.isDirectory();
|
|
68
|
+
if (isMatch && e.name === name) out.push(path);
|
|
69
|
+
},
|
|
70
|
+
fs,
|
|
71
|
+
);
|
|
65
72
|
return out;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
async function findAgentProfiles(root, claudeDirs) {
|
|
75
|
+
async function findAgentProfiles(root, claudeDirs, fs) {
|
|
69
76
|
const out = [];
|
|
70
77
|
for (const d of claudeDirs) {
|
|
71
78
|
const files = await listFiles(
|
|
72
79
|
root,
|
|
73
80
|
`${d}/agents`,
|
|
74
81
|
(e) => e.isFile() && e.name.endsWith(".md"),
|
|
82
|
+
fs,
|
|
83
|
+
);
|
|
84
|
+
out.push(...files);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function findAgentReferences(root, claudeDirs, fs) {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const d of claudeDirs) {
|
|
92
|
+
const files = await listFiles(
|
|
93
|
+
root,
|
|
94
|
+
`${d}/agents/references`,
|
|
95
|
+
(e) => e.isFile() && e.name.endsWith(".md"),
|
|
96
|
+
fs,
|
|
75
97
|
);
|
|
76
98
|
out.push(...files);
|
|
77
99
|
}
|
|
78
100
|
return out;
|
|
79
101
|
}
|
|
80
102
|
|
|
81
|
-
async function findSkillDirs(root, claudeDirs) {
|
|
103
|
+
async function findSkillDirs(root, claudeDirs, fs) {
|
|
82
104
|
const out = [];
|
|
83
105
|
for (const d of claudeDirs) {
|
|
84
|
-
const dirs = await listFiles(
|
|
106
|
+
const dirs = await listFiles(
|
|
107
|
+
root,
|
|
108
|
+
`${d}/skills`,
|
|
109
|
+
(e) => e.isDirectory(),
|
|
110
|
+
fs,
|
|
111
|
+
);
|
|
85
112
|
out.push(...dirs);
|
|
86
113
|
}
|
|
87
114
|
return out;
|
|
88
115
|
}
|
|
89
116
|
|
|
90
|
-
async function findSkillReferences(root, skillDirs) {
|
|
117
|
+
async function findSkillReferences(root, skillDirs, fs) {
|
|
91
118
|
const out = [];
|
|
92
119
|
for (const d of skillDirs) {
|
|
93
120
|
const files = await listFiles(
|
|
94
121
|
root,
|
|
95
122
|
`${d}/references`,
|
|
96
123
|
(e) => e.isFile() && e.name.endsWith(".md"),
|
|
124
|
+
fs,
|
|
97
125
|
);
|
|
98
126
|
out.push(...files);
|
|
99
127
|
}
|
|
100
128
|
return out;
|
|
101
129
|
}
|
|
102
130
|
|
|
103
|
-
async function buildLayers(root) {
|
|
104
|
-
const claudeDirs = await findByName(root, ".claude", "dir");
|
|
105
|
-
const skillDirs = await findSkillDirs(root, claudeDirs);
|
|
106
|
-
const allClaude = await findByName(root, "CLAUDE.md", "file");
|
|
131
|
+
async function buildLayers(root, fs) {
|
|
132
|
+
const claudeDirs = await findByName(root, ".claude", "dir", fs);
|
|
133
|
+
const skillDirs = await findSkillDirs(root, claudeDirs, fs);
|
|
134
|
+
const allClaude = await findByName(root, "CLAUDE.md", "file", fs);
|
|
107
135
|
const rootClaude = allClaude.filter((p) => p === "CLAUDE.md");
|
|
108
136
|
const subdirClaude = allClaude.filter((p) => p !== "CLAUDE.md");
|
|
109
137
|
return {
|
|
@@ -143,21 +171,28 @@ async function buildLayers(root) {
|
|
|
143
171
|
name: "agent profile",
|
|
144
172
|
maxLines: 72,
|
|
145
173
|
maxWords: 448,
|
|
146
|
-
files: await findAgentProfiles(root, claudeDirs),
|
|
174
|
+
files: await findAgentProfiles(root, claudeDirs, fs),
|
|
147
175
|
},
|
|
148
176
|
{
|
|
149
177
|
id: "L4",
|
|
178
|
+
name: "agent reference",
|
|
179
|
+
maxLines: 192,
|
|
180
|
+
maxWords: 1280,
|
|
181
|
+
files: await findAgentReferences(root, claudeDirs, fs),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "L5",
|
|
150
185
|
name: "skill procedure",
|
|
151
186
|
maxLines: 192,
|
|
152
187
|
maxWords: 1280,
|
|
153
188
|
files: skillDirs.map((d) => `${d}/SKILL.md`),
|
|
154
189
|
},
|
|
155
190
|
{
|
|
156
|
-
id: "
|
|
191
|
+
id: "L6",
|
|
157
192
|
name: "skill reference",
|
|
158
193
|
maxLines: 128,
|
|
159
194
|
maxWords: 768,
|
|
160
|
-
files: await findSkillReferences(root, skillDirs),
|
|
195
|
+
files: await findSkillReferences(root, skillDirs, fs),
|
|
161
196
|
},
|
|
162
197
|
],
|
|
163
198
|
};
|
|
@@ -173,11 +208,11 @@ function offsetToLine(text, offset) {
|
|
|
173
208
|
|
|
174
209
|
// -- Subject builders ----------------------------------------------------
|
|
175
210
|
|
|
176
|
-
async function buildFileSubjects(root, layers) {
|
|
211
|
+
async function buildFileSubjects(root, layers, fs) {
|
|
177
212
|
const subjects = [];
|
|
178
213
|
for (const layer of layers) {
|
|
179
214
|
for (const relPath of layer.files) {
|
|
180
|
-
const text = await readText(root, relPath);
|
|
215
|
+
const text = await readText(root, relPath, fs);
|
|
181
216
|
if (text == null) continue;
|
|
182
217
|
subjects.push({
|
|
183
218
|
path: resolve(root, relPath),
|
|
@@ -192,10 +227,10 @@ async function buildFileSubjects(root, layers) {
|
|
|
192
227
|
return subjects;
|
|
193
228
|
}
|
|
194
229
|
|
|
195
|
-
async function buildChecklistSubjects(root, sources) {
|
|
230
|
+
async function buildChecklistSubjects(root, sources, fs) {
|
|
196
231
|
const subjects = [];
|
|
197
232
|
for (const relPath of sources) {
|
|
198
|
-
const text = await readText(root, relPath);
|
|
233
|
+
const text = await readText(root, relPath, fs);
|
|
199
234
|
if (text == null) continue;
|
|
200
235
|
const absPath = resolve(root, relPath);
|
|
201
236
|
CHECKLIST_RE.lastIndex = 0;
|
|
@@ -241,29 +276,29 @@ export const INSTRUCTION_RULES = [
|
|
|
241
276
|
hint: HINT_LAYER_BUDGET,
|
|
242
277
|
},
|
|
243
278
|
{
|
|
244
|
-
id: "
|
|
279
|
+
id: "L7.too-many-items",
|
|
245
280
|
scope: "checklist-block",
|
|
246
281
|
severity: "fail",
|
|
247
282
|
check: (s) =>
|
|
248
|
-
s.items.length >
|
|
249
|
-
? { count: s.items.length, max:
|
|
283
|
+
s.items.length > L7_MAX_ITEMS
|
|
284
|
+
? { count: s.items.length, max: L7_MAX_ITEMS }
|
|
250
285
|
: null,
|
|
251
286
|
message: (s, r) =>
|
|
252
287
|
`checklist #${s.blockIndex} (${s.type}) has ${r.count} items (max ${r.max})`,
|
|
253
288
|
hint: "split the checklist into multiple sections, or remove items not load-bearing for the goal",
|
|
254
289
|
},
|
|
255
290
|
{
|
|
256
|
-
id: "
|
|
291
|
+
id: "L7.item-too-many-words",
|
|
257
292
|
scope: "checklist-block",
|
|
258
293
|
severity: "fail",
|
|
259
294
|
check: (s) => {
|
|
260
295
|
const offenders = [];
|
|
261
296
|
s.items.forEach((item, i) => {
|
|
262
|
-
if (item.words >
|
|
297
|
+
if (item.words > L7_MAX_WORDS_PER_ITEM) {
|
|
263
298
|
offenders.push({
|
|
264
299
|
itemIndex: i + 1,
|
|
265
300
|
words: item.words,
|
|
266
|
-
max:
|
|
301
|
+
max: L7_MAX_WORDS_PER_ITEM,
|
|
267
302
|
});
|
|
268
303
|
}
|
|
269
304
|
});
|
|
@@ -278,21 +313,23 @@ export const INSTRUCTION_RULES = [
|
|
|
278
313
|
// -- Public entry --------------------------------------------------------
|
|
279
314
|
|
|
280
315
|
/**
|
|
281
|
-
* Walk the repo rooted at `root`, applying the L1–
|
|
316
|
+
* Walk the repo rooted at `root`, applying the L1–L7 caps from COALIGNED.md.
|
|
282
317
|
* Each layer is gated by a line cap AND a word cap; either breach fails.
|
|
283
318
|
*
|
|
284
|
-
* @param {{ root: string }} options
|
|
319
|
+
* @param {{ root: string, runtime?: import('@forwardimpact/libutil/runtime').Runtime }} options
|
|
285
320
|
* @returns {Promise<Finding[]>} Structured findings; empty when conformant.
|
|
286
321
|
* Each Finding is `{ id, level, path, lineNo?, message, hint? }` for use
|
|
287
322
|
* with `emitFindingsText` / `emitFindingsJson` from libutil.
|
|
288
323
|
*/
|
|
289
|
-
export async function checkInstructions({ root }) {
|
|
290
|
-
const {
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
324
|
+
export async function checkInstructions({ root, runtime }) {
|
|
325
|
+
const { fs } = runtime ?? createDefaultRuntime();
|
|
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
|
+
);
|
|
296
333
|
|
|
297
334
|
const ctx = {
|
|
298
335
|
subjects: {
|
package/src/jtbd.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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";
|
|
4
|
+
import { createDefaultRuntime } from "@forwardimpact/libutil/runtime";
|
|
5
5
|
|
|
6
6
|
const VALID_USERS = [
|
|
7
7
|
"Engineering Leaders",
|
|
@@ -39,15 +39,15 @@ function catalogs(root) {
|
|
|
39
39
|
];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function loadPackages(dir, filter) {
|
|
43
|
-
if (!existsSync(dir)) return [];
|
|
42
|
+
function loadPackages(dir, filter, fsSync) {
|
|
43
|
+
if (!fsSync.existsSync(dir)) return [];
|
|
44
44
|
const out = [];
|
|
45
|
-
for (const name of readdirSync(dir)) {
|
|
45
|
+
for (const name of fsSync.readdirSync(dir)) {
|
|
46
46
|
if (!filter(name)) continue;
|
|
47
47
|
const pkgPath = join(dir, name, "package.json");
|
|
48
48
|
let pkg;
|
|
49
49
|
try {
|
|
50
|
-
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
50
|
+
pkg = JSON.parse(fsSync.readFileSync(pkgPath, "utf8"));
|
|
51
51
|
} catch {
|
|
52
52
|
continue;
|
|
53
53
|
}
|
|
@@ -404,18 +404,18 @@ async function buildDescription(content, description, formatMarkdown) {
|
|
|
404
404
|
);
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
function commitUpdate(filePath, label, original, updated, fix, result) {
|
|
407
|
+
function commitUpdate(filePath, label, original, updated, fix, result, fsSync) {
|
|
408
408
|
if (updated === null || updated === original) return;
|
|
409
409
|
if (!fix) {
|
|
410
410
|
result.stale.push(label);
|
|
411
411
|
return;
|
|
412
412
|
}
|
|
413
|
-
writeFileSync(filePath, updated);
|
|
413
|
+
fsSync.writeFileSync(filePath, updated);
|
|
414
414
|
result.fixed.push(label);
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
async function processCatalog(catalog, fix, formatMarkdown, result) {
|
|
418
|
-
const packages = loadPackages(catalog.dir, catalog.filter);
|
|
417
|
+
async function processCatalog(catalog, fix, formatMarkdown, result, fsSync) {
|
|
418
|
+
const packages = loadPackages(catalog.dir, catalog.filter, fsSync);
|
|
419
419
|
const findings = validate(packages, catalog.dir, catalog.name, {
|
|
420
420
|
skipUniqueHires: catalog.skipUniqueHires ?? false,
|
|
421
421
|
});
|
|
@@ -424,8 +424,8 @@ async function processCatalog(catalog, fix, formatMarkdown, result) {
|
|
|
424
424
|
return;
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
if (existsSync(catalog.readme)) {
|
|
428
|
-
const original = readFileSync(catalog.readme, "utf8");
|
|
427
|
+
if (fsSync.existsSync(catalog.readme)) {
|
|
428
|
+
const original = fsSync.readFileSync(catalog.readme, "utf8");
|
|
429
429
|
let content = original;
|
|
430
430
|
content = await buildCatalog(
|
|
431
431
|
content,
|
|
@@ -441,14 +441,15 @@ async function processCatalog(catalog, fix, formatMarkdown, result) {
|
|
|
441
441
|
content,
|
|
442
442
|
fix,
|
|
443
443
|
result,
|
|
444
|
+
fsSync,
|
|
444
445
|
);
|
|
445
446
|
}
|
|
446
447
|
|
|
447
448
|
for (const { dir, pkg } of packages) {
|
|
448
449
|
if (!pkg.description) continue;
|
|
449
450
|
const pkgReadme = join(catalog.dir, dir, "README.md");
|
|
450
|
-
if (!existsSync(pkgReadme)) continue;
|
|
451
|
-
const original = readFileSync(pkgReadme, "utf8");
|
|
451
|
+
if (!fsSync.existsSync(pkgReadme)) continue;
|
|
452
|
+
const original = fsSync.readFileSync(pkgReadme, "utf8");
|
|
452
453
|
const updated = await buildDescription(
|
|
453
454
|
original,
|
|
454
455
|
pkg.description,
|
|
@@ -461,15 +462,20 @@ async function processCatalog(catalog, fix, formatMarkdown, result) {
|
|
|
461
462
|
updated,
|
|
462
463
|
fix,
|
|
463
464
|
result,
|
|
465
|
+
fsSync,
|
|
464
466
|
);
|
|
465
467
|
}
|
|
466
468
|
}
|
|
467
469
|
|
|
468
|
-
async function processJtbdMd(root, fix, formatMarkdown, result) {
|
|
470
|
+
async function processJtbdMd(root, fix, formatMarkdown, result, fsSync) {
|
|
469
471
|
const jtbdPath = join(root, "JTBD.md");
|
|
470
|
-
if (!existsSync(jtbdPath)) return;
|
|
472
|
+
if (!fsSync.existsSync(jtbdPath)) return;
|
|
471
473
|
const productsCatalog = catalogs(root).find((c) => c.name === "products");
|
|
472
|
-
const packages = loadPackages(
|
|
474
|
+
const packages = loadPackages(
|
|
475
|
+
productsCatalog.dir,
|
|
476
|
+
productsCatalog.filter,
|
|
477
|
+
fsSync,
|
|
478
|
+
);
|
|
473
479
|
const findings = validate(packages, productsCatalog.dir, "products", {
|
|
474
480
|
skipUniqueHires: true,
|
|
475
481
|
});
|
|
@@ -477,11 +483,11 @@ async function processJtbdMd(root, fix, formatMarkdown, result) {
|
|
|
477
483
|
if (result.findings.length === 0) result.findings.push(...findings);
|
|
478
484
|
return;
|
|
479
485
|
}
|
|
480
|
-
const original = readFileSync(jtbdPath, "utf8");
|
|
486
|
+
const original = fsSync.readFileSync(jtbdPath, "utf8");
|
|
481
487
|
const updated = await buildJobs(original, packages, formatMarkdown, {
|
|
482
488
|
capitalize: true,
|
|
483
489
|
});
|
|
484
|
-
commitUpdate(jtbdPath, "JTBD.md", original, updated, fix, result);
|
|
490
|
+
commitUpdate(jtbdPath, "JTBD.md", original, updated, fix, result, fsSync);
|
|
485
491
|
}
|
|
486
492
|
|
|
487
493
|
/**
|
|
@@ -489,22 +495,23 @@ async function processJtbdMd(root, fix, formatMarkdown, result) {
|
|
|
489
495
|
* libraries/, and (when `fix` is true) regenerate the marker-delimited catalog,
|
|
490
496
|
* jobs, and description blocks in the corresponding README.md and JTBD.md.
|
|
491
497
|
*
|
|
492
|
-
* @param {{ root: string, fix?: boolean }} options
|
|
498
|
+
* @param {{ root: string, fix?: boolean, runtime?: import('@forwardimpact/libutil/runtime').Runtime }} options
|
|
493
499
|
* @returns {Promise<{ findings: Finding[], stale: string[], fixed: string[] }>}
|
|
494
500
|
* `findings` are validation failures (structured for `emitFindingsText` /
|
|
495
501
|
* `emitFindingsJson` from libutil); `stale` is files whose generated blocks
|
|
496
502
|
* are out of date (only populated when `fix` is false); `fixed` is files
|
|
497
503
|
* that were rewritten in place.
|
|
498
504
|
*/
|
|
499
|
-
export async function checkJtbd({ root, fix = false }) {
|
|
505
|
+
export async function checkJtbd({ root, fix = false, runtime }) {
|
|
506
|
+
const { fsSync } = runtime ?? createDefaultRuntime();
|
|
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
|
}
|