@forwardimpact/libcoaligned 0.1.6 → 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/bin/coaligned.js +42 -25
- package/package.json +1 -1
- package/src/instructions.js +54 -37
- package/src/jtbd.js +29 -22
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([
|
|
@@ -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,88 +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,
|
|
75
83
|
);
|
|
76
84
|
out.push(...files);
|
|
77
85
|
}
|
|
78
86
|
return out;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
async function findAgentReferences(root, claudeDirs) {
|
|
89
|
+
async function findAgentReferences(root, claudeDirs, fs) {
|
|
82
90
|
const out = [];
|
|
83
91
|
for (const d of claudeDirs) {
|
|
84
92
|
const files = await listFiles(
|
|
85
93
|
root,
|
|
86
94
|
`${d}/agents/references`,
|
|
87
95
|
(e) => e.isFile() && e.name.endsWith(".md"),
|
|
96
|
+
fs,
|
|
88
97
|
);
|
|
89
98
|
out.push(...files);
|
|
90
99
|
}
|
|
91
100
|
return out;
|
|
92
101
|
}
|
|
93
102
|
|
|
94
|
-
async function findSkillDirs(root, claudeDirs) {
|
|
103
|
+
async function findSkillDirs(root, claudeDirs, fs) {
|
|
95
104
|
const out = [];
|
|
96
105
|
for (const d of claudeDirs) {
|
|
97
|
-
const dirs = await listFiles(
|
|
106
|
+
const dirs = await listFiles(
|
|
107
|
+
root,
|
|
108
|
+
`${d}/skills`,
|
|
109
|
+
(e) => e.isDirectory(),
|
|
110
|
+
fs,
|
|
111
|
+
);
|
|
98
112
|
out.push(...dirs);
|
|
99
113
|
}
|
|
100
114
|
return out;
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
async function findSkillReferences(root, skillDirs) {
|
|
117
|
+
async function findSkillReferences(root, skillDirs, fs) {
|
|
104
118
|
const out = [];
|
|
105
119
|
for (const d of skillDirs) {
|
|
106
120
|
const files = await listFiles(
|
|
107
121
|
root,
|
|
108
122
|
`${d}/references`,
|
|
109
123
|
(e) => e.isFile() && e.name.endsWith(".md"),
|
|
124
|
+
fs,
|
|
110
125
|
);
|
|
111
126
|
out.push(...files);
|
|
112
127
|
}
|
|
113
128
|
return out;
|
|
114
129
|
}
|
|
115
130
|
|
|
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");
|
|
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);
|
|
120
135
|
const rootClaude = allClaude.filter((p) => p === "CLAUDE.md");
|
|
121
136
|
const subdirClaude = allClaude.filter((p) => p !== "CLAUDE.md");
|
|
122
137
|
return {
|
|
@@ -156,14 +171,14 @@ async function buildLayers(root) {
|
|
|
156
171
|
name: "agent profile",
|
|
157
172
|
maxLines: 72,
|
|
158
173
|
maxWords: 448,
|
|
159
|
-
files: await findAgentProfiles(root, claudeDirs),
|
|
174
|
+
files: await findAgentProfiles(root, claudeDirs, fs),
|
|
160
175
|
},
|
|
161
176
|
{
|
|
162
177
|
id: "L4",
|
|
163
178
|
name: "agent reference",
|
|
164
179
|
maxLines: 192,
|
|
165
180
|
maxWords: 1280,
|
|
166
|
-
files: await findAgentReferences(root, claudeDirs),
|
|
181
|
+
files: await findAgentReferences(root, claudeDirs, fs),
|
|
167
182
|
},
|
|
168
183
|
{
|
|
169
184
|
id: "L5",
|
|
@@ -177,7 +192,7 @@ async function buildLayers(root) {
|
|
|
177
192
|
name: "skill reference",
|
|
178
193
|
maxLines: 128,
|
|
179
194
|
maxWords: 768,
|
|
180
|
-
files: await findSkillReferences(root, skillDirs),
|
|
195
|
+
files: await findSkillReferences(root, skillDirs, fs),
|
|
181
196
|
},
|
|
182
197
|
],
|
|
183
198
|
};
|
|
@@ -193,11 +208,11 @@ function offsetToLine(text, offset) {
|
|
|
193
208
|
|
|
194
209
|
// -- Subject builders ----------------------------------------------------
|
|
195
210
|
|
|
196
|
-
async function buildFileSubjects(root, layers) {
|
|
211
|
+
async function buildFileSubjects(root, layers, fs) {
|
|
197
212
|
const subjects = [];
|
|
198
213
|
for (const layer of layers) {
|
|
199
214
|
for (const relPath of layer.files) {
|
|
200
|
-
const text = await readText(root, relPath);
|
|
215
|
+
const text = await readText(root, relPath, fs);
|
|
201
216
|
if (text == null) continue;
|
|
202
217
|
subjects.push({
|
|
203
218
|
path: resolve(root, relPath),
|
|
@@ -212,10 +227,10 @@ async function buildFileSubjects(root, layers) {
|
|
|
212
227
|
return subjects;
|
|
213
228
|
}
|
|
214
229
|
|
|
215
|
-
async function buildChecklistSubjects(root, sources) {
|
|
230
|
+
async function buildChecklistSubjects(root, sources, fs) {
|
|
216
231
|
const subjects = [];
|
|
217
232
|
for (const relPath of sources) {
|
|
218
|
-
const text = await readText(root, relPath);
|
|
233
|
+
const text = await readText(root, relPath, fs);
|
|
219
234
|
if (text == null) continue;
|
|
220
235
|
const absPath = resolve(root, relPath);
|
|
221
236
|
CHECKLIST_RE.lastIndex = 0;
|
|
@@ -301,18 +316,20 @@ export const INSTRUCTION_RULES = [
|
|
|
301
316
|
* Walk the repo rooted at `root`, applying the L1–L7 caps from COALIGNED.md.
|
|
302
317
|
* Each layer is gated by a line cap AND a word cap; either breach fails.
|
|
303
318
|
*
|
|
304
|
-
* @param {{ root: string }} options
|
|
319
|
+
* @param {{ root: string, runtime?: import('@forwardimpact/libutil/runtime').Runtime }} options
|
|
305
320
|
* @returns {Promise<Finding[]>} Structured findings; empty when conformant.
|
|
306
321
|
* Each Finding is `{ id, level, path, lineNo?, message, hint? }` for use
|
|
307
322
|
* with `emitFindingsText` / `emitFindingsJson` from libutil.
|
|
308
323
|
*/
|
|
309
|
-
export async function checkInstructions({ root }) {
|
|
310
|
-
const {
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
);
|
|
316
333
|
|
|
317
334
|
const ctx = {
|
|
318
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
|
}
|