peer-ai 1.0.0-next.0
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/LICENSE +21 -0
- package/README.md +403 -0
- package/dist/assess.d.ts +102 -0
- package/dist/assess.js +545 -0
- package/dist/check.d.ts +30 -0
- package/dist/check.js +253 -0
- package/dist/checks.d.ts +15 -0
- package/dist/checks.js +15 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +231 -0
- package/dist/detect.d.ts +42 -0
- package/dist/detect.js +459 -0
- package/dist/doctor.d.ts +36 -0
- package/dist/doctor.js +297 -0
- package/dist/document.d.ts +30 -0
- package/dist/document.js +72 -0
- package/dist/enforcers.d.ts +6 -0
- package/dist/enforcers.js +307 -0
- package/dist/feedback.d.ts +67 -0
- package/dist/feedback.js +209 -0
- package/dist/files.d.ts +1 -0
- package/dist/files.js +72 -0
- package/dist/init.d.ts +31 -0
- package/dist/init.js +158 -0
- package/dist/mcp.d.ts +13 -0
- package/dist/mcp.js +247 -0
- package/dist/package-info.d.ts +4 -0
- package/dist/package-info.js +6 -0
- package/dist/pipeline.d.ts +82 -0
- package/dist/pipeline.js +265 -0
- package/dist/prompter.d.ts +23 -0
- package/dist/prompter.js +56 -0
- package/dist/render.d.ts +58 -0
- package/dist/render.js +557 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +93 -0
- package/dist/routing.d.ts +24 -0
- package/dist/routing.js +121 -0
- package/dist/ruff.d.ts +19 -0
- package/dist/ruff.js +64 -0
- package/dist/standards.d.ts +46 -0
- package/dist/standards.js +130 -0
- package/dist/state.d.ts +22 -0
- package/dist/state.js +56 -0
- package/dist/test-helpers.d.ts +16 -0
- package/dist/test-helpers.js +62 -0
- package/dist/work.d.ts +147 -0
- package/dist/work.js +357 -0
- package/package.json +45 -0
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// peer-ai doctor: checks that Peer AI is set up correctly in a repository, and says how to fix
|
|
2
|
+
// what isn't. It only reads; it never changes a file. A failure stops Peer AI working as
|
|
3
|
+
// intended; a warning is something to tidy up. Every check reports, including the ones it had
|
|
4
|
+
// to skip, so a clean report means everything was looked at.
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { existsSync, statSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { CORE_RULES, PROFILE_RULES, PROFILES, overrideFits } from "peer-ai-standards";
|
|
9
|
+
import { DOMAINS } from "peer-ai-workflow";
|
|
10
|
+
import { LEGACY_MARKERS, MAP_FILE, assess, loadConfig } from "./assess.js";
|
|
11
|
+
import { count, fail, formatChecks, ok, plural, skip, warn } from "./checks.js";
|
|
12
|
+
import { checkEnforcers, checkProfiles } from "./enforcers.js";
|
|
13
|
+
import { WORKFLOW_FILE } from "./pipeline.js";
|
|
14
|
+
import { RUFF_FILE } from "./ruff.js";
|
|
15
|
+
import { CONFIG_FILE, detectDelivery, detectName, detectTools, detectTracks } from "./detect.js";
|
|
16
|
+
import { MIN_NODE_MAJOR } from "./package-info.js";
|
|
17
|
+
import { planRender } from "./render.js";
|
|
18
|
+
import { WORK_DIR, mapChanges, readMap, readWorkItems } from "./state.js";
|
|
19
|
+
const NEEDS_CONFIG = `needs a valid ${CONFIG_FILE}`;
|
|
20
|
+
const isDirectory = (path) => statSync(path, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
21
|
+
const isUrl = (value) => /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
|
|
22
|
+
const normalise = (path) => path.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
23
|
+
function checkNode(version) {
|
|
24
|
+
const major = Number(version.split(".")[0]);
|
|
25
|
+
if (major >= MIN_NODE_MAJOR)
|
|
26
|
+
return ok("node", `Node.js ${version}`);
|
|
27
|
+
return fail("node", `Node.js ${version} is older than ${String(MIN_NODE_MAJOR)}, which Peer AI needs.`, `Install Node.js ${String(MIN_NODE_MAJOR)} or later.`);
|
|
28
|
+
}
|
|
29
|
+
export function checkConfig(root) {
|
|
30
|
+
if (!existsSync(join(root, CONFIG_FILE))) {
|
|
31
|
+
return { check: fail("config", `No ${CONFIG_FILE} in this folder.`, "Run peer-ai init.") };
|
|
32
|
+
}
|
|
33
|
+
const { config, errors = [] } = loadConfig(root);
|
|
34
|
+
if (config === undefined) {
|
|
35
|
+
return {
|
|
36
|
+
check: fail("config", `${CONFIG_FILE} is not valid: ${errors.join("; ")}`, `Correct ${CONFIG_FILE}. An editor that reads its $schema shows each error in place.`),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { check: ok("config", `${CONFIG_FILE} is valid`), config };
|
|
40
|
+
}
|
|
41
|
+
/** Each track's folder exists, and every part found in the repository belongs to a track. */
|
|
42
|
+
export function checkTracks(root, config) {
|
|
43
|
+
const checks = [];
|
|
44
|
+
for (const track of config.tracks) {
|
|
45
|
+
// A dormant track hasn't been started, so its folder may not exist yet.
|
|
46
|
+
if (track.path === undefined || track.status === "dormant" || track.status === "external")
|
|
47
|
+
continue;
|
|
48
|
+
if (!isDirectory(join(root, track.path))) {
|
|
49
|
+
checks.push(fail("tracks", `Track "${track.id}" points to ${track.path}, which doesn't exist.`, `Correct its path in ${CONFIG_FILE}, or remove the track.`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// A track with no path is the repository root, so it covers a part found at the root and no
|
|
53
|
+
// other: a part in a subfolder of a monorepo needs a track of its own, or one whose folder holds it.
|
|
54
|
+
const paths = config.tracks
|
|
55
|
+
.filter((track) => track.status !== "external")
|
|
56
|
+
.map((track) => (track.path === undefined ? undefined : normalise(track.path)));
|
|
57
|
+
const covered = (found) => paths.some((path) => path === undefined || found === undefined ? path === found : found === path || found.startsWith(`${path}/`));
|
|
58
|
+
for (const found of detectTracks(root, detectName(root))) {
|
|
59
|
+
if (covered(found.path))
|
|
60
|
+
continue;
|
|
61
|
+
checks.push(warn("tracks", `Found ${found.path ?? "a part at the repository root"} (${found.kind}), which isn't in the config.`, `Add it to tracks in ${CONFIG_FILE}, or set the path of the track it belongs to.`));
|
|
62
|
+
}
|
|
63
|
+
if (checks.length > 0)
|
|
64
|
+
return checks;
|
|
65
|
+
return [ok("tracks", `${plural(config.tracks.length, "track")}, matching the repository`)];
|
|
66
|
+
}
|
|
67
|
+
/** Files the config points to, as sources to read. Destinations such as docs.dir may not exist yet. */
|
|
68
|
+
function referencedFiles(config) {
|
|
69
|
+
const refs = [
|
|
70
|
+
{ field: "design.reference", path: config.design?.reference },
|
|
71
|
+
{ field: "design.tokens", path: config.design?.tokens },
|
|
72
|
+
{ field: "delivery.pipeline", path: config.delivery?.pipeline },
|
|
73
|
+
{ field: "compliance.dataInventory", path: config.compliance?.dataInventory },
|
|
74
|
+
...(config.apis ?? []).map((api) => ({ field: `apis "${api.id}" contract`, path: api.contract?.location })),
|
|
75
|
+
...(config.standards?.documents ?? []).map((doc) => ({ field: "standards.documents", path: doc.path })),
|
|
76
|
+
...(config.rules ?? []).map((rule) => ({ field: "rules", path: rule.path })),
|
|
77
|
+
...Object.entries(config.capabilities ?? {}).flatMap(([skill, capability]) => (capability.checklists ?? []).map((path) => ({ field: `capabilities.${skill}.checklists`, path }))),
|
|
78
|
+
...Object.entries(config.activities ?? {}).flatMap(([activity, settings]) => (settings.inputs ?? []).map((path) => ({ field: `activities.${activity}.inputs`, path }))),
|
|
79
|
+
];
|
|
80
|
+
return refs.filter((ref) => ref.path !== undefined && !isUrl(ref.path) && !/[*?]/.test(ref.path));
|
|
81
|
+
}
|
|
82
|
+
function checkReferences(root, config) {
|
|
83
|
+
const refs = referencedFiles(config);
|
|
84
|
+
const missing = refs.filter((ref) => !existsSync(join(root, ref.path)));
|
|
85
|
+
if (missing.length === 0) {
|
|
86
|
+
return [
|
|
87
|
+
ok("references", refs.length === 0
|
|
88
|
+
? "The config names no other files"
|
|
89
|
+
: `The ${plural(refs.length, "file")} the config names exist`),
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
return missing.map((ref) => ref.path.startsWith("/")
|
|
93
|
+
? warn("references", `${ref.field} is ${ref.path}, which is neither a path in the repository nor a full URL.`, `Use a path relative to the project root, or a full URL starting with https://.`)
|
|
94
|
+
: warn("references", `${ref.field} points to ${ref.path}, which doesn't exist.`, `Create it, or correct the path in ${CONFIG_FILE}.`));
|
|
95
|
+
}
|
|
96
|
+
function checkTools(root, config) {
|
|
97
|
+
const listed = config.tools ?? [];
|
|
98
|
+
const unlisted = detectTools(root).filter((tool) => !listed.includes(tool));
|
|
99
|
+
if (unlisted.length > 0) {
|
|
100
|
+
return warn("tools", `Set up in this repository, but not listed in the config: ${unlisted.join(", ")}.`, `Add ${unlisted.map((tool) => `"${tool}"`).join(", ")} to tools in ${CONFIG_FILE}.`);
|
|
101
|
+
}
|
|
102
|
+
if (listed.length === 0) {
|
|
103
|
+
return warn("tools", "No AI tools are listed, so Peer AI doesn't know which tools to write instructions for.", `Add the tools you use to ${CONFIG_FILE}, for example "tools": ["claude-code"].`);
|
|
104
|
+
}
|
|
105
|
+
return ok("tools", `AI tools: ${listed.join(", ")}`);
|
|
106
|
+
}
|
|
107
|
+
/** What render writes for the AI tools still matches the config. */
|
|
108
|
+
const ENFORCER_FILES = [WORKFLOW_FILE, RUFF_FILE];
|
|
109
|
+
function checkRendered(root, config, skills) {
|
|
110
|
+
const plan = planRender(root, config);
|
|
111
|
+
const withSkills = skills || config.skills?.commit === true;
|
|
112
|
+
const stale = [
|
|
113
|
+
// The tools that enforce the stack profiles are checked on their own, with what each needs.
|
|
114
|
+
...plan.files
|
|
115
|
+
.filter((file) => file.action !== "unchanged" && !ENFORCER_FILES.includes(file.path))
|
|
116
|
+
.map((file) => file.path),
|
|
117
|
+
...(withSkills ? plan.skills : []).filter((skill) => skill.action !== "unchanged").map((skill) => `${skill.path}/`),
|
|
118
|
+
];
|
|
119
|
+
if (stale.length === 0)
|
|
120
|
+
return ok("render", "The AI tools' instructions, MCP registrations and skills are up to date");
|
|
121
|
+
return warn("render", `Out of date for the AI tools: ${stale.join(", ")}.`, "Run peer-ai render.");
|
|
122
|
+
}
|
|
123
|
+
/** The config's CI setting matches the repository, so Peer AI never adds a second pipeline. */
|
|
124
|
+
function checkDelivery(root, config) {
|
|
125
|
+
const found = detectDelivery(root);
|
|
126
|
+
if (config.delivery?.ci === "none" && found?.pipeline !== undefined) {
|
|
127
|
+
return warn("delivery", `The config says there is no CI, but there is a pipeline in ${found.pipeline}.`, `Set "delivery": { "ci": "existing", "pipeline": "${found.pipeline}" } in ${CONFIG_FILE}, so Peer AI extends it instead of adding another.`);
|
|
128
|
+
}
|
|
129
|
+
const pipeline = found?.pipeline ?? (config.delivery?.ci === "existing" ? config.delivery.pipeline : undefined);
|
|
130
|
+
return ok("delivery", pipeline === undefined ? "No CI pipeline yet" : `CI pipeline: ${pipeline}`);
|
|
131
|
+
}
|
|
132
|
+
const CORE_RULE_IDS = new Set(CORE_RULES.map((rule) => rule.id));
|
|
133
|
+
const CORE_PREFIXES = new Set(Object.values(DOMAINS));
|
|
134
|
+
const PROFILE_PREFIXES = new Set(PROFILES.map((profile) => profile.prefix));
|
|
135
|
+
/**
|
|
136
|
+
* Every rule the project sets aside or changes, listed so nothing is switched off silently (RFC
|
|
137
|
+
* 0003). An id with a core or stack profile prefix must be one of Peer AI's rules, and a changed
|
|
138
|
+
* rule must have a value to change (RFC 0006); other prefixes belong to project add-ons, which
|
|
139
|
+
* are listed as they are.
|
|
140
|
+
*/
|
|
141
|
+
export function checkStandards(config, today) {
|
|
142
|
+
const exceptions = config.standards?.exceptions ?? [];
|
|
143
|
+
const overrides = Object.entries(config.standards?.overrides ?? {});
|
|
144
|
+
if (exceptions.length + overrides.length === 0)
|
|
145
|
+
return [ok("standards", "No rules set aside or changed")];
|
|
146
|
+
const unknown = (rule) => {
|
|
147
|
+
const prefix = rule.split("-")[0] ?? "";
|
|
148
|
+
const ours = CORE_PREFIXES.has(prefix) || PROFILE_PREFIXES.has(prefix);
|
|
149
|
+
return ours && !CORE_RULE_IDS.has(rule) && !PROFILE_RULES.has(rule)
|
|
150
|
+
? warn("standards", `${rule} isn't one of Peer AI's rules, so setting it aside or changing it does nothing.`, `Check the rule id in ${CONFIG_FILE}. The rules are listed in peer-ai-standards.`)
|
|
151
|
+
: undefined;
|
|
152
|
+
};
|
|
153
|
+
// A value that can't stand in for the default, such as text for a number, or a count that isn't
|
|
154
|
+
// a whole number, is ignored, so the default stays.
|
|
155
|
+
const wrongType = (rule, value) => {
|
|
156
|
+
const profiled = PROFILE_RULES.get(rule);
|
|
157
|
+
if (profiled?.default === undefined || overrideFits(profiled, value))
|
|
158
|
+
return undefined;
|
|
159
|
+
const fallback = profiled.default.value;
|
|
160
|
+
const kind = typeof fallback === "number" && Number.isInteger(fallback) ? "whole number of 0 or more" : typeof fallback;
|
|
161
|
+
return warn("standards", `${rule}'s value is a ${kind}, such as ${JSON.stringify(fallback)}, so ${JSON.stringify(value)} is ignored and the default stays.`, `Write the value in standards.overrides as a ${kind}.`);
|
|
162
|
+
};
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
const listed = exceptions.map((exception) => {
|
|
165
|
+
const { rule, reason, decidedBy, until } = exception;
|
|
166
|
+
if (seen.has(rule)) {
|
|
167
|
+
return warn("standards", `${rule} is set aside more than once.`, "Keep one entry for it in standards.exceptions, with the decision that stands.");
|
|
168
|
+
}
|
|
169
|
+
seen.add(rule);
|
|
170
|
+
const problem = unknown(rule);
|
|
171
|
+
if (problem !== undefined)
|
|
172
|
+
return problem;
|
|
173
|
+
if (until !== undefined && until < today) {
|
|
174
|
+
return warn("standards", `The exception for ${rule} ended on ${until}, so the rule applies again.`, "Remove it from standards.exceptions, or extend it with a new decision and a new date.");
|
|
175
|
+
}
|
|
176
|
+
return ok("standards", `${rule} set aside: ${reason} (decided by ${decidedBy}${until === undefined ? "" : `, until ${until}`})`);
|
|
177
|
+
});
|
|
178
|
+
const changed = overrides.map(([rule, override]) => unknown(rule) ??
|
|
179
|
+
(CORE_RULE_IDS.has(rule) || (PROFILE_RULES.has(rule) && PROFILE_RULES.get(rule)?.default === undefined)
|
|
180
|
+
? warn("standards", `${rule} has no value to change, so the override does nothing.`, `Remove it from standards.overrides, or set the rule aside in standards.exceptions with the reason.`)
|
|
181
|
+
: (wrongType(rule, override.value) ??
|
|
182
|
+
ok("standards", `${rule} changed to ${String(override.value)}: ${override.reason}`))));
|
|
183
|
+
return [...listed, ...changed];
|
|
184
|
+
}
|
|
185
|
+
/** The map is valid, and still says what a fresh assessment would. */
|
|
186
|
+
function checkMap(root, config) {
|
|
187
|
+
const read = readMap(root);
|
|
188
|
+
if (read === undefined)
|
|
189
|
+
return warn("map", "There is no project map yet.", "Run peer-ai assess.");
|
|
190
|
+
if (!read.ok)
|
|
191
|
+
return fail("map", `${MAP_FILE} ${read.error}`, "Run peer-ai assess to write it again.");
|
|
192
|
+
const date = read.value.assessedAt.slice(0, 10);
|
|
193
|
+
if (config === undefined)
|
|
194
|
+
return skip("map", `The project map from ${date} is valid; not compared, ${NEEDS_CONFIG}`);
|
|
195
|
+
const changed = mapChanges(read.value, assess(root, config, config.project.stage ?? "mvp"));
|
|
196
|
+
if (changed.length > 0) {
|
|
197
|
+
return warn("map", `The project map from ${date} is out of date: ${changed.join(", ")}.`, "Run peer-ai assess.");
|
|
198
|
+
}
|
|
199
|
+
return ok("map", `The project map from ${date} is up to date`);
|
|
200
|
+
}
|
|
201
|
+
/** Every work item is valid, named after its id, and on a track the config has. */
|
|
202
|
+
export function checkWorkItems(root, config) {
|
|
203
|
+
const files = readWorkItems(root);
|
|
204
|
+
if (files.length === 0)
|
|
205
|
+
return [ok("work-items", "No work items yet")];
|
|
206
|
+
const tracks = config?.tracks.map((track) => track.id);
|
|
207
|
+
const checks = [];
|
|
208
|
+
for (const { path, item } of files) {
|
|
209
|
+
if (!item.ok) {
|
|
210
|
+
checks.push(fail("work-items", `${path} ${item.error}`, "Correct it. An editor that reads its $schema shows each error."));
|
|
211
|
+
}
|
|
212
|
+
else if (item.value.track !== undefined && tracks !== undefined && !tracks.includes(item.value.track)) {
|
|
213
|
+
checks.push(fail("work-items", `${path} is for the track "${item.value.track}", which isn't in the config.`, `Change its track, or add the track to ${CONFIG_FILE}.`));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (checks.length > 0)
|
|
217
|
+
return checks;
|
|
218
|
+
return [
|
|
219
|
+
ok("work-items", files.length === 1 ? "1 work item, valid" : `${plural(files.length, "work item")}, all valid`),
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
function checkGit(root) {
|
|
223
|
+
if (!existsSync(join(root, ".git"))) {
|
|
224
|
+
return warn("git", "This folder isn't a git repository, and Peer AI follows which work item you're on by its branch.", "Run git init.");
|
|
225
|
+
}
|
|
226
|
+
const ignored = [CONFIG_FILE, MAP_FILE, `${WORK_DIR}/`].filter((path) => {
|
|
227
|
+
try {
|
|
228
|
+
// Exit code 0 means ignored. Any other outcome, including git failing, reports nothing.
|
|
229
|
+
execFileSync("git", ["check-ignore", "-q", "--no-index", path], { cwd: root, stdio: "ignore" });
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
if (ignored.length === 0)
|
|
237
|
+
return ok("git", "Git doesn't ignore Peer AI's files");
|
|
238
|
+
return warn("git", `Git ignores ${ignored.join(", ")}, so the team, CI and other checkouts won't see ${ignored.length === 1 ? "it" : "them"}.`, "Remove the matching lines from .gitignore. Peer AI's files are meant to be committed.");
|
|
239
|
+
}
|
|
240
|
+
function checkLegacy(root) {
|
|
241
|
+
if (!LEGACY_MARKERS.some((marker) => existsSync(join(root, marker))))
|
|
242
|
+
return [];
|
|
243
|
+
return [
|
|
244
|
+
warn("legacy", "The peer-ai/ folder is a copy of the v0 playbook, which Peer AI 1.0 doesn't read.", `Move any changes your project made to it into ${CONFIG_FILE}, then remove it with: git rm -r peer-ai`),
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
export function diagnose(root, nodeVersion = process.versions.node, today = new Date(), options = {}) {
|
|
248
|
+
const { check: configCheck, config } = checkConfig(root);
|
|
249
|
+
const needsConfig = (id, what) => config === undefined ? [skip(id, `${what} not checked: ${NEEDS_CONFIG}`)] : [];
|
|
250
|
+
const checks = [
|
|
251
|
+
checkNode(nodeVersion),
|
|
252
|
+
configCheck,
|
|
253
|
+
...(config === undefined ? needsConfig("tracks", "Tracks") : checkTracks(root, config)),
|
|
254
|
+
...(config === undefined ? needsConfig("references", "Files the config names") : checkReferences(root, config)),
|
|
255
|
+
...(config === undefined ? needsConfig("tools", "AI tools") : [checkTools(root, config)]),
|
|
256
|
+
...(config === undefined
|
|
257
|
+
? needsConfig("render", "What render writes")
|
|
258
|
+
: [checkRendered(root, config, options.skills ?? true)]),
|
|
259
|
+
...(config === undefined ? needsConfig("delivery", "CI") : [checkDelivery(root, config)]),
|
|
260
|
+
...(config === undefined
|
|
261
|
+
? needsConfig("standards", "Rules set aside")
|
|
262
|
+
: checkStandards(config, today.toISOString().slice(0, 10))),
|
|
263
|
+
...(config === undefined ? [] : [...checkProfiles(config), ...checkEnforcers(root, config)]),
|
|
264
|
+
checkMap(root, config),
|
|
265
|
+
...checkWorkItems(root, config),
|
|
266
|
+
checkGit(root),
|
|
267
|
+
...checkLegacy(root),
|
|
268
|
+
];
|
|
269
|
+
return {
|
|
270
|
+
name: config?.project.name ?? detectName(root),
|
|
271
|
+
ok: checks.every((check) => check.status !== "fail"),
|
|
272
|
+
checks,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
export function formatDiagnosis(diagnosis) {
|
|
276
|
+
const lines = [`Peer AI doctor: ${diagnosis.name}`, "", ...formatChecks(diagnosis.checks)];
|
|
277
|
+
const failures = count(diagnosis.checks, "fail");
|
|
278
|
+
const warnings = count(diagnosis.checks, "warn");
|
|
279
|
+
lines.push("");
|
|
280
|
+
if (failures + warnings === 0)
|
|
281
|
+
lines.push("Everything is set up correctly.");
|
|
282
|
+
else if (failures === 0)
|
|
283
|
+
lines.push(`No problems, and ${plural(warnings, "warning")}.`);
|
|
284
|
+
else
|
|
285
|
+
lines.push(`${plural(failures, "problem")} to fix, and ${plural(warnings, "warning")}.`);
|
|
286
|
+
return lines;
|
|
287
|
+
}
|
|
288
|
+
/** Exit code 0 when nothing failed, even with warnings; 1 when something failed. */
|
|
289
|
+
export function runDoctor(options, out) {
|
|
290
|
+
const diagnosis = diagnose(options.cwd, options.nodeVersion);
|
|
291
|
+
if (options.json)
|
|
292
|
+
out.log(JSON.stringify(diagnosis, null, 2));
|
|
293
|
+
else
|
|
294
|
+
for (const line of formatDiagnosis(diagnosis))
|
|
295
|
+
out.log(line);
|
|
296
|
+
return diagnosis.ok ? 0 : 1;
|
|
297
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type DocumentProblems } from "peer-ai-skills";
|
|
2
|
+
import { type SkillId } from "peer-ai-workflow";
|
|
3
|
+
import type { Output } from "./init.ts";
|
|
4
|
+
import type { Result } from "./work.ts";
|
|
5
|
+
export interface DocumentInput {
|
|
6
|
+
skill: string;
|
|
7
|
+
/** The document's path, relative to the project root. */
|
|
8
|
+
path: string;
|
|
9
|
+
/** Which of the skill's templates it follows. Defaults to the main one. */
|
|
10
|
+
template?: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface CheckedDocument extends DocumentProblems {
|
|
13
|
+
skill: SkillId;
|
|
14
|
+
path: string;
|
|
15
|
+
template: string;
|
|
16
|
+
ready: boolean;
|
|
17
|
+
/** What to change, in sentences; empty when the document is ready. */
|
|
18
|
+
problems: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function checkDocumentFile(root: string, input: DocumentInput): Result<CheckedDocument>;
|
|
21
|
+
export interface CheckDocumentOptions extends DocumentInput {
|
|
22
|
+
cwd: string;
|
|
23
|
+
json: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* peer-ai check-document: the same check as the check_document tool, for CI and for a model or a
|
|
27
|
+
* person that works in a shell. Exit code 0 when the document is ready; 1 when it isn't, or can't
|
|
28
|
+
* be checked.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runCheckDocument(options: CheckDocumentOptions, out: Output): number;
|
package/dist/document.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// The check a document skill hands its output to (RFC 0004): the check_document tool and
|
|
2
|
+
// peer-ai check-document. It compares a document with its skill's template and names each missing
|
|
3
|
+
// part, each empty one, template text left in, and each rule id that doesn't exist, so the skill
|
|
4
|
+
// can fix them and check again.
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
7
|
+
import { availableSkills, checkDocument as compare, describeProblems, documentInfo, loadSkill, templatePath, } from "peer-ai-skills";
|
|
8
|
+
import { CORE_RULES } from "peer-ai-standards";
|
|
9
|
+
import { DOMAINS, SKILL_IDS, SKILL_KINDS } from "peer-ai-workflow";
|
|
10
|
+
const failed = (error) => ({ ok: false, error });
|
|
11
|
+
const RULES = {
|
|
12
|
+
ruleIds: new Set(CORE_RULES.map((rule) => rule.id)),
|
|
13
|
+
corePrefixes: new Set(Object.values(DOMAINS)),
|
|
14
|
+
};
|
|
15
|
+
export function checkDocumentFile(root, input) {
|
|
16
|
+
const { skill } = input;
|
|
17
|
+
if (!SKILL_IDS.includes(skill)) {
|
|
18
|
+
return failed(`${skill} isn't one of Peer AI's skills.`);
|
|
19
|
+
}
|
|
20
|
+
const id = skill;
|
|
21
|
+
if (SKILL_KINDS[id] !== "document") {
|
|
22
|
+
return failed(SKILL_KINDS[id] === "review"
|
|
23
|
+
? `${id} is a review skill: record its report with record_review, or check it with peer-ai check-report.`
|
|
24
|
+
: `${id} changes work items rather than writing a document; the work item's own gates check it.`);
|
|
25
|
+
}
|
|
26
|
+
if (!availableSkills().includes(id)) {
|
|
27
|
+
return failed(`This version of Peer AI doesn't have the ${id} skill yet, so there's no template to check against.`);
|
|
28
|
+
}
|
|
29
|
+
const files = loadSkill(id);
|
|
30
|
+
const { templates } = documentInfo(files);
|
|
31
|
+
const name = input.template ?? templates[0];
|
|
32
|
+
const template = name === undefined ? undefined : files.get(templatePath(name));
|
|
33
|
+
if (name === undefined || template === undefined) {
|
|
34
|
+
return failed(`${id} has no template called ${name ?? "(none)"}. Its templates are: ${templates.join(", ")}.`);
|
|
35
|
+
}
|
|
36
|
+
const full = resolve(root, input.path);
|
|
37
|
+
const inside = relative(root, full);
|
|
38
|
+
if (inside === "" || inside.startsWith("..") || isAbsolute(inside)) {
|
|
39
|
+
return failed(`${input.path} is outside the project. Save the document inside it.`);
|
|
40
|
+
}
|
|
41
|
+
if (!existsSync(full))
|
|
42
|
+
return failed(`There is no document at ${input.path}.`);
|
|
43
|
+
const found = compare(readFileSync(full, "utf8"), template, RULES);
|
|
44
|
+
const problems = describeProblems(found);
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
value: { skill: id, path: input.path, template: name, ready: problems.length === 0, problems, ...found },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* peer-ai check-document: the same check as the check_document tool, for CI and for a model or a
|
|
52
|
+
* person that works in a shell. Exit code 0 when the document is ready; 1 when it isn't, or can't
|
|
53
|
+
* be checked.
|
|
54
|
+
*/
|
|
55
|
+
export function runCheckDocument(options, out) {
|
|
56
|
+
const checked = checkDocumentFile(options.cwd, options);
|
|
57
|
+
if (options.json) {
|
|
58
|
+
out.log(JSON.stringify(checked.ok ? { ok: true, ...checked.value } : { ok: false, error: checked.error }, null, 2));
|
|
59
|
+
}
|
|
60
|
+
else if (!checked.ok) {
|
|
61
|
+
out.error(`✗ ${checked.error}`);
|
|
62
|
+
}
|
|
63
|
+
else if (checked.value.ready) {
|
|
64
|
+
out.log(`✓ ${options.path} has every part of the ${checked.value.skill} template.`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
out.error(`✗ ${options.path} isn't ready:`);
|
|
68
|
+
for (const problem of checked.value.problems)
|
|
69
|
+
out.error(`- ${problem}`);
|
|
70
|
+
}
|
|
71
|
+
return checked.ok && checked.value.ready ? 0 : 1;
|
|
72
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PeerAiConfig } from "peer-ai-workflow";
|
|
2
|
+
import { type Check } from "./checks.ts";
|
|
3
|
+
/** The profiles the config lists that Peer AI has no rules for yet. */
|
|
4
|
+
export declare function checkProfiles(config: PeerAiConfig): Check[];
|
|
5
|
+
/** Each enforcing tool is set up to use Peer AI's settings. */
|
|
6
|
+
export declare function checkEnforcers(root: string, config: PeerAiConfig): Check[];
|