@printpractical/cruze 0.0.13 → 0.0.14
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 +61 -118
- package/dist/adapters/inbound/cli/cli_context.js +0 -27
- package/dist/adapters/inbound/cli/commands/code.js +5 -18
- package/dist/adapters/inbound/cli/commands/documents.js +11 -37
- package/dist/adapters/inbound/cli/commands/init.js +1 -1
- package/dist/adapters/inbound/cli/commands/review.js +2 -9
- package/dist/adapters/inbound/cli/commands/status.js +16 -63
- package/dist/adapters/inbound/cli/commands.js +10 -37
- package/dist/adapters/inbound/cli/parse_command.js +0 -54
- package/dist/adapters/outbound/git_repository.js +2 -71
- package/dist/app/ports/repository.js +0 -12
- package/dist/app/project_context.js +15 -25
- package/dist/app/use_cases/approve_document.js +29 -0
- package/dist/app/use_cases/check_code.js +21 -56
- package/dist/app/use_cases/new_document.js +25 -0
- package/dist/app/use_cases/project_status.js +43 -67
- package/dist/app/use_cases/run_review.js +11 -30
- package/dist/app/use_cases/validate_project.js +4 -4
- package/dist/domain/approvals.js +51 -0
- package/dist/domain/check/check_source.js +2 -31
- package/dist/domain/config.js +8 -84
- package/dist/domain/hashing.js +16 -30
- package/dist/domain/ids.js +13 -27
- package/dist/domain/next_step.js +41 -0
- package/dist/domain/project/features.js +71 -0
- package/dist/domain/project/layout.js +11 -27
- package/dist/domain/project/project_docs.js +48 -0
- package/dist/domain/project/release.js +30 -0
- package/dist/domain/scaffold.js +0 -1
- package/dist/domain/skill_budget.js +4 -4
- package/dist/domain/trace.js +8 -25
- package/dist/domain/validation/document_rules.js +70 -0
- package/dist/domain/validation/release_rules.js +45 -0
- package/dist/domain/validation/validate_project.js +20 -27
- package/dist/domain/versions/cruze_version.js +3 -9
- package/dist/main.js +1 -1
- package/package.json +1 -1
- package/skills/asking/SKILL.md +51 -0
- package/skills/behavioural-testing/SKILL.md +14 -16
- package/skills/behavioural-testing/examples.md +1 -2
- package/skills/behavioural-testing/fakes.md +2 -2
- package/skills/build/SKILL.md +34 -38
- package/skills/dependency-approval/SKILL.md +34 -31
- package/skills/envision/SKILL.md +23 -18
- package/skills/envision/architecture.md +16 -0
- package/skills/envision/breakdown.md +14 -0
- package/skills/envision/possibilities.md +6 -7
- package/skills/fix/SKILL.md +34 -0
- package/skills/formats/SKILL.md +39 -87
- package/skills/formats/config.md +47 -0
- package/skills/formats/templates/architecture.md +18 -74
- package/skills/formats/templates/feature.md +18 -34
- package/skills/formats/templates/vision.md +15 -27
- package/skills/hexagonal-design/SKILL.md +11 -11
- package/skills/hexagonal-design/contracts.md +3 -3
- package/skills/hexagonal-design/fundamentals.md +1 -1
- package/skills/hexagonal-design/languages/cpp.md +1 -1
- package/skills/hexagonal-design/languages/rust.md +1 -1
- package/skills/hexagonal-design/languages/swift.md +2 -2
- package/skills/hexagonal-design/runtime.md +2 -2
- package/skills/research/SKILL.md +4 -4
- package/skills/research/evaluation.md +1 -1
- package/skills/review/SKILL.md +27 -0
- package/skills/roles/SKILL.md +8 -59
- package/skills/roles/code-reviewer.md +21 -32
- package/skills/roles/researcher.md +3 -3
- package/skills/shape/SKILL.md +37 -0
- package/skills/shape/self-check.md +12 -0
- package/src/adapters/inbound/cli/cli_context.ts +0 -27
- package/src/adapters/inbound/cli/commands/code.ts +5 -18
- package/src/adapters/inbound/cli/commands/documents.ts +11 -37
- package/src/adapters/inbound/cli/commands/init.ts +1 -1
- package/src/adapters/inbound/cli/commands/review.ts +2 -9
- package/src/adapters/inbound/cli/commands/status.ts +16 -63
- package/src/adapters/inbound/cli/commands.ts +10 -37
- package/src/adapters/inbound/cli/parse_command.ts +0 -54
- package/src/adapters/outbound/git_repository.ts +2 -71
- package/src/app/ports/repository.ts +0 -12
- package/src/app/project_context.ts +15 -25
- package/src/app/use_cases/approve_document.ts +29 -0
- package/src/app/use_cases/check_code.ts +21 -56
- package/src/app/use_cases/new_document.ts +25 -0
- package/src/app/use_cases/project_status.ts +49 -73
- package/src/app/use_cases/run_review.ts +11 -30
- package/src/app/use_cases/validate_project.ts +4 -4
- package/src/domain/approvals.ts +51 -0
- package/src/domain/check/check_source.ts +2 -31
- package/src/domain/config.ts +9 -85
- package/src/domain/hashing.ts +16 -30
- package/src/domain/ids.ts +13 -27
- package/src/domain/next_step.ts +41 -0
- package/src/domain/project/features.ts +71 -0
- package/src/domain/project/layout.ts +11 -27
- package/src/domain/project/project_docs.ts +48 -0
- package/src/domain/project/release.ts +30 -0
- package/src/domain/scaffold.ts +0 -1
- package/src/domain/skill_budget.ts +4 -4
- package/src/domain/trace.ts +8 -25
- package/src/domain/validation/document_rules.ts +70 -0
- package/src/domain/validation/release_rules.ts +45 -0
- package/src/domain/validation/validate_project.ts +21 -28
- package/src/domain/versions/cruze_version.ts +3 -9
- package/src/main.ts +1 -1
- package/templates/init/AGENTS.md +4 -12
- package/templates/init/ci.yml +3 -3
- package/templates/init/config.yaml +8 -17
- package/templates/init/review/claude.yaml +2 -2
- package/dist/adapters/inbound/cli/argument_files.js +0 -29
- package/dist/adapters/inbound/cli/commands/diff.js +0 -23
- package/dist/adapters/inbound/cli/commands/journal.js +0 -32
- package/dist/adapters/inbound/cli/commands/progress.js +0 -55
- package/dist/adapters/inbound/cli/commands/realign.js +0 -28
- package/dist/adapters/inbound/cli/commands/show.js +0 -96
- package/dist/app/use_cases/abandon_work.js +0 -42
- package/dist/app/use_cases/approve_artifact.js +0 -197
- package/dist/app/use_cases/journal_events.js +0 -64
- package/dist/app/use_cases/land_change.js +0 -134
- package/dist/app/use_cases/new_project_doc.js +0 -48
- package/dist/app/use_cases/new_work_item.js +0 -75
- package/dist/app/use_cases/realign_done.js +0 -45
- package/dist/app/use_cases/realign_status.js +0 -106
- package/dist/app/use_cases/record_progress.js +0 -118
- package/dist/app/use_cases/show_elements.js +0 -18
- package/dist/app/use_cases/show_notes.js +0 -11
- package/dist/app/use_cases/show_scope.js +0 -51
- package/dist/app/use_cases/upstream_diff.js +0 -55
- package/dist/domain/approvals/agent_approval.js +0 -68
- package/dist/domain/approvals/evaluate.js +0 -36
- package/dist/domain/approvals/fingerprint.js +0 -52
- package/dist/domain/approvals/rebase.js +0 -39
- package/dist/domain/approvals/records.js +0 -50
- package/dist/domain/approvals/upstream_text.js +0 -36
- package/dist/domain/architecture_sections.js +0 -23
- package/dist/domain/check/decision_comments.js +0 -38
- package/dist/domain/decision_citations.js +0 -62
- package/dist/domain/edits/agents_file.js +0 -58
- package/dist/domain/edits/managed_tables.js +0 -60
- package/dist/domain/edits/progress_block.js +0 -33
- package/dist/domain/edits/status_lines.js +0 -52
- package/dist/domain/elements.js +0 -126
- package/dist/domain/journal.js +0 -109
- package/dist/domain/land/bookkeeping.js +0 -54
- package/dist/domain/land/merge_plan.js +0 -137
- package/dist/domain/land/text_ops.js +0 -66
- package/dist/domain/line_diff.js +0 -56
- package/dist/domain/project/artifact_ref.js +0 -42
- package/dist/domain/project/deltas.js +0 -49
- package/dist/domain/project/element_lookup.js +0 -124
- package/dist/domain/project/living_model.js +0 -55
- package/dist/domain/project/new_items.js +0 -48
- package/dist/domain/project/notes.js +0 -74
- package/dist/domain/project/plan_parts.js +0 -111
- package/dist/domain/project/progress.js +0 -43
- package/dist/domain/project/project_view.js +0 -72
- package/dist/domain/project/roadmap.js +0 -43
- package/dist/domain/project/scope_view.js +0 -134
- package/dist/domain/project/work_items.js +0 -68
- package/dist/domain/status/next_step.js +0 -181
- package/dist/domain/status/pending_rethink.js +0 -22
- package/dist/domain/status/verification.js +0 -29
- package/dist/domain/status/work_status.js +0 -125
- package/dist/domain/validation/decision_rules.js +0 -76
- package/dist/domain/validation/delta_rules.js +0 -81
- package/dist/domain/validation/element_rules.js +0 -104
- package/dist/domain/validation/future_rules.js +0 -40
- package/dist/domain/validation/living_rules.js +0 -99
- package/dist/domain/validation/plan_rules.js +0 -124
- package/dist/domain/validation/project_rules.js +0 -115
- package/dist/domain/validation/size_rules.js +0 -45
- package/dist/domain/validation/vision_rules.js +0 -41
- package/dist/domain/validation/work_item_rules.js +0 -175
- package/dist/domain/versions/realign_notes.js +0 -72
- package/dist/domain/versions/rule_versions.js +0 -29
- package/dist/domain/versions/scheduled_exceptions.js +0 -41
- package/skills/about/SKILL.md +0 -21
- package/skills/architect/SKILL.md +0 -49
- package/skills/architect/feature.md +0 -40
- package/skills/architect/project.md +0 -71
- package/skills/architect/tweak.md +0 -22
- package/skills/architect/walkthrough.md +0 -35
- package/skills/domain-language/SKILL.md +0 -37
- package/skills/envision/feature.md +0 -25
- package/skills/envision/intake.md +0 -28
- package/skills/envision/project.md +0 -24
- package/skills/explore/SKILL.md +0 -39
- package/skills/formats/reference/architecture.md +0 -84
- package/skills/formats/reference/config.md +0 -80
- package/skills/formats/reference/durable-docs.md +0 -74
- package/skills/formats/reference/specs.md +0 -65
- package/skills/formats/reference/work-items.md +0 -100
- package/skills/formats/templates/adr.md +0 -7
- package/skills/formats/templates/change.md +0 -29
- package/skills/formats/templates/glossary.md +0 -8
- package/skills/formats/templates/roadmap.md +0 -32
- package/skills/formats/templates/spec.md +0 -11
- package/skills/formats/templates/standalone-change.md +0 -46
- package/skills/grilling/SKILL.md +0 -90
- package/skills/grilling/illustrating.md +0 -38
- package/skills/improve/SKILL.md +0 -51
- package/skills/land/SKILL.md +0 -48
- package/skills/next/SKILL.md +0 -31
- package/skills/plan/SKILL.md +0 -62
- package/skills/plan/tasks.md +0 -35
- package/skills/plan/test-plan.md +0 -14
- package/skills/realign/SKILL.md +0 -37
- package/skills/realign/notes/0.0.10.md +0 -131
- package/skills/realign/notes/0.0.11.md +0 -15
- package/skills/realign/notes/0.0.12.md +0 -31
- package/skills/realign/notes/0.0.13.md +0 -11
- package/skills/realign/notes/0.0.2.md +0 -69
- package/skills/realign/notes/0.0.3.md +0 -11
- package/skills/realign/notes/0.0.4.md +0 -27
- package/skills/realign/notes/0.0.5.md +0 -38
- package/skills/realign/notes/0.0.6.md +0 -44
- package/skills/realign/notes/0.0.7.md +0 -11
- package/skills/realign/notes/0.0.8.md +0 -97
- package/skills/realign/notes/0.0.9.md +0 -59
- package/skills/realign/notes.md +0 -40
- package/skills/rethink/SKILL.md +0 -62
- package/skills/rethink/levels.md +0 -54
- package/skills/roadmap/SKILL.md +0 -54
- package/skills/roles/auditor.md +0 -35
- package/skills/roles/design-reviewer.md +0 -43
- package/skills/roles/design-rubric.md +0 -41
- package/skills/roles/plan-rubric.md +0 -11
- package/skills/roles/surveyor.md +0 -41
- package/skills/roles/verifier.md +0 -42
- package/skills/triage/SKILL.md +0 -37
- package/skills/verify/SKILL.md +0 -45
- package/skills/verify/manual-test.md +0 -23
- package/src/adapters/inbound/cli/argument_files.ts +0 -29
- package/src/adapters/inbound/cli/commands/diff.ts +0 -23
- package/src/adapters/inbound/cli/commands/journal.ts +0 -32
- package/src/adapters/inbound/cli/commands/progress.ts +0 -55
- package/src/adapters/inbound/cli/commands/realign.ts +0 -28
- package/src/adapters/inbound/cli/commands/show.ts +0 -96
- package/src/app/use_cases/abandon_work.ts +0 -42
- package/src/app/use_cases/approve_artifact.ts +0 -197
- package/src/app/use_cases/journal_events.ts +0 -64
- package/src/app/use_cases/land_change.ts +0 -134
- package/src/app/use_cases/new_project_doc.ts +0 -48
- package/src/app/use_cases/new_work_item.ts +0 -75
- package/src/app/use_cases/realign_done.ts +0 -45
- package/src/app/use_cases/realign_status.ts +0 -106
- package/src/app/use_cases/record_progress.ts +0 -118
- package/src/app/use_cases/show_elements.ts +0 -18
- package/src/app/use_cases/show_notes.ts +0 -11
- package/src/app/use_cases/show_scope.ts +0 -51
- package/src/app/use_cases/upstream_diff.ts +0 -55
- package/src/domain/approvals/agent_approval.ts +0 -68
- package/src/domain/approvals/evaluate.ts +0 -36
- package/src/domain/approvals/fingerprint.ts +0 -52
- package/src/domain/approvals/rebase.ts +0 -39
- package/src/domain/approvals/records.ts +0 -50
- package/src/domain/approvals/upstream_text.ts +0 -36
- package/src/domain/architecture_sections.ts +0 -23
- package/src/domain/check/decision_comments.ts +0 -38
- package/src/domain/decision_citations.ts +0 -62
- package/src/domain/edits/agents_file.ts +0 -58
- package/src/domain/edits/managed_tables.ts +0 -60
- package/src/domain/edits/progress_block.ts +0 -33
- package/src/domain/edits/status_lines.ts +0 -52
- package/src/domain/elements.ts +0 -126
- package/src/domain/journal.ts +0 -109
- package/src/domain/land/bookkeeping.ts +0 -54
- package/src/domain/land/merge_plan.ts +0 -137
- package/src/domain/land/text_ops.ts +0 -66
- package/src/domain/line_diff.ts +0 -56
- package/src/domain/project/artifact_ref.ts +0 -42
- package/src/domain/project/deltas.ts +0 -49
- package/src/domain/project/element_lookup.ts +0 -124
- package/src/domain/project/living_model.ts +0 -55
- package/src/domain/project/new_items.ts +0 -48
- package/src/domain/project/notes.ts +0 -74
- package/src/domain/project/plan_parts.ts +0 -111
- package/src/domain/project/progress.ts +0 -43
- package/src/domain/project/project_view.ts +0 -72
- package/src/domain/project/roadmap.ts +0 -43
- package/src/domain/project/scope_view.ts +0 -134
- package/src/domain/project/work_items.ts +0 -68
- package/src/domain/status/next_step.ts +0 -181
- package/src/domain/status/pending_rethink.ts +0 -22
- package/src/domain/status/verification.ts +0 -29
- package/src/domain/status/work_status.ts +0 -125
- package/src/domain/validation/decision_rules.ts +0 -76
- package/src/domain/validation/delta_rules.ts +0 -81
- package/src/domain/validation/element_rules.ts +0 -104
- package/src/domain/validation/future_rules.ts +0 -40
- package/src/domain/validation/living_rules.ts +0 -99
- package/src/domain/validation/plan_rules.ts +0 -124
- package/src/domain/validation/project_rules.ts +0 -115
- package/src/domain/validation/size_rules.ts +0 -45
- package/src/domain/validation/vision_rules.ts +0 -41
- package/src/domain/validation/work_item_rules.ts +0 -175
- package/src/domain/versions/realign_notes.ts +0 -72
- package/src/domain/versions/rule_versions.ts +0 -29
- package/src/domain/versions/scheduled_exceptions.ts +0 -41
- package/templates/init/gitattributes +0 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import type { Repository } from "../../app/ports/repository.ts";
|
|
4
4
|
|
|
@@ -12,15 +12,6 @@ export class GitRepository implements Repository {
|
|
|
12
12
|
this.root = root;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
async currentBranch(): Promise<string | null> {
|
|
16
|
-
const branch = await this.git("rev-parse", "--abbrev-ref", "HEAD");
|
|
17
|
-
return branch === null || branch === "HEAD" ? null : branch;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async headCommit(): Promise<string | null> {
|
|
21
|
-
return this.git("rev-parse", "--short", "HEAD");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
15
|
async userName(): Promise<string | null> {
|
|
25
16
|
const name = await this.git("config", "user.name");
|
|
26
17
|
const email = await this.git("config", "user.email");
|
|
@@ -28,72 +19,12 @@ export class GitRepository implements Repository {
|
|
|
28
19
|
return email === null ? name : `${name} <${email}>`;
|
|
29
20
|
}
|
|
30
21
|
|
|
31
|
-
async otherBranches(): Promise<string[]> {
|
|
32
|
-
const current = await this.currentBranch();
|
|
33
|
-
const listing = await this.git("for-each-ref", "--format=%(refname:short)", "refs/heads", "refs/remotes");
|
|
34
|
-
return (listing ?? "")
|
|
35
|
-
.split("\n")
|
|
36
|
-
.filter((ref) => ref !== "" && ref !== current && !ref.endsWith("/HEAD") && ref !== `origin/${current}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async filesOnBranch(branch: string, dir: string): Promise<Map<string, string>> {
|
|
40
|
-
const listing = await this.git("ls-tree", "-r", "--name-only", branch, "--", dir);
|
|
41
|
-
const paths = (listing ?? "").split("\n").filter((p) => p !== "");
|
|
42
|
-
if (paths.length === 0) return new Map();
|
|
43
|
-
// One git process for every file: a process per file took up to two minutes on a project with 26 branches.
|
|
44
|
-
const blobs = await this.catFiles(paths.map((path) => `${branch}:${path}`));
|
|
45
|
-
return new Map(paths.flatMap((path, i) => (blobs[i] === undefined ? [] : [[path, blobs[i]] as [string, string]])));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async firstCommitAdding(path: string, text: string): Promise<string | null> {
|
|
49
|
-
const commits = await this.git("log", "--reverse", "--format=%H", `-S${text}`, "--", path);
|
|
50
|
-
return commits?.split("\n").find((c) => c !== "") ?? null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async fileAt(commit: string, path: string): Promise<string | null> {
|
|
54
|
-
return (await this.catFiles([`${commit}:${path}`]))[0] ?? null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** The contents of each object, in order, through one `git cat-file --batch`; undefined for one that is missing. */
|
|
58
|
-
private catFiles(objects: string[]): Promise<Array<string | undefined>> {
|
|
59
|
-
return new Promise((resolve) => {
|
|
60
|
-
const child = spawn("git", ["cat-file", "--batch"], { cwd: this.root, stdio: ["pipe", "pipe", "ignore"] });
|
|
61
|
-
const chunks: Buffer[] = [];
|
|
62
|
-
child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
63
|
-
child.on("error", () => resolve(objects.map(() => undefined)));
|
|
64
|
-
child.on("close", () => resolve(parseBatch(Buffer.concat(chunks), objects.length)));
|
|
65
|
-
child.stdin.end(`${objects.join("\n")}\n`);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
22
|
private async git(...args: string[]): Promise<string | null> {
|
|
70
23
|
try {
|
|
71
|
-
const { stdout } = await run("git", args, { cwd: this.root
|
|
24
|
+
const { stdout } = await run("git", args, { cwd: this.root });
|
|
72
25
|
return stdout.trimEnd();
|
|
73
26
|
} catch {
|
|
74
27
|
return null;
|
|
75
28
|
}
|
|
76
29
|
}
|
|
77
30
|
}
|
|
78
|
-
|
|
79
|
-
/** Splits `git cat-file --batch` output: a `<sha> <type> <size>` header, the bytes and a newline, or `<name> missing`. */
|
|
80
|
-
function parseBatch(output: Buffer, count: number): Array<string | undefined> {
|
|
81
|
-
const results: Array<string | undefined> = [];
|
|
82
|
-
let at = 0;
|
|
83
|
-
while (results.length < count && at < output.length) {
|
|
84
|
-
const headerEnd = output.indexOf(10, at);
|
|
85
|
-
if (headerEnd === -1) break;
|
|
86
|
-
const header = output.subarray(at, headerEnd).toString("utf8");
|
|
87
|
-
const size = /^[0-9a-f]+ \w+ (\d+)$/.exec(header)?.[1];
|
|
88
|
-
if (size === undefined) {
|
|
89
|
-
results.push(undefined);
|
|
90
|
-
at = headerEnd + 1;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
const start = headerEnd + 1;
|
|
94
|
-
results.push(output.subarray(start, start + Number(size)).toString("utf8"));
|
|
95
|
-
at = start + Number(size) + 1;
|
|
96
|
-
}
|
|
97
|
-
while (results.length < count) results.push(undefined);
|
|
98
|
-
return results;
|
|
99
|
-
}
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
/** The version-control repository the project lives in. Every operation tolerates a project outside git. */
|
|
2
2
|
export interface Repository {
|
|
3
|
-
/** The checked-out branch, or null when detached or outside git. */
|
|
4
|
-
currentBranch(): Promise<string | null>;
|
|
5
|
-
/** The short hash of the checked-out commit, or null. */
|
|
6
|
-
headCommit(): Promise<string | null>;
|
|
7
3
|
/** Who is approving: the configured user name and email, or null. */
|
|
8
4
|
userName(): Promise<string | null>;
|
|
9
|
-
/** Local and remote-tracking branches other than the current one. */
|
|
10
|
-
otherBranches(): Promise<string[]>;
|
|
11
|
-
/** Files under a directory as they are on another branch, by project-relative path. */
|
|
12
|
-
filesOnBranch(branch: string, dir: string): Promise<Map<string, string>>;
|
|
13
|
-
/** The oldest commit whose change to a file added the text, or null. */
|
|
14
|
-
firstCommitAdding(path: string, text: string): Promise<string | null>;
|
|
15
|
-
/** A file's contents at a commit, or null when it didn't exist there. */
|
|
16
|
-
fileAt(commit: string, path: string): Promise<string | null>;
|
|
17
5
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { buildProjectDocs, type ProjectDocs } from "../domain/project/project_docs.ts";
|
|
2
|
+
import { CruzeError } from "../domain/cruze_error.ts";
|
|
3
|
+
import type { CruzeConfig } from "../domain/config.ts";
|
|
4
4
|
import type { Bundle } from "./ports/bundle.ts";
|
|
5
5
|
import type { Clock } from "./ports/clock.ts";
|
|
6
6
|
import type { ProjectFiles } from "./ports/project_files.ts";
|
|
@@ -14,34 +14,24 @@ export interface ProjectDeps {
|
|
|
14
14
|
bundle: Bundle;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/** Directories whose files make up the project's documents. */
|
|
18
|
+
const DOC_ROOTS = ["docs", ".cruze"] as const;
|
|
19
|
+
|
|
20
|
+
/** Reads every document under docs/ and .cruze/. */
|
|
21
|
+
export async function loadDocs(files: ProjectFiles): Promise<ProjectDocs> {
|
|
19
22
|
const snapshot = new Map<string, string>();
|
|
20
|
-
for (const root of
|
|
23
|
+
for (const root of DOC_ROOTS) {
|
|
21
24
|
for (const path of await files.listFiles(root)) {
|
|
22
25
|
const text = await files.readText(path);
|
|
23
26
|
if (text !== undefined) snapshot.set(path, text);
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
|
-
|
|
27
|
-
if (readme !== undefined) snapshot.set(PATHS.readme, readme);
|
|
28
|
-
return buildProjectView(snapshot);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** A new view with some files replaced, for checking edits before writing them. */
|
|
32
|
-
export function withFiles(view: ProjectView, changes: Map<string, string>): ProjectView {
|
|
33
|
-
return buildProjectView(new Map([...view.snapshot, ...changes]));
|
|
29
|
+
return buildProjectDocs(snapshot);
|
|
34
30
|
}
|
|
35
31
|
|
|
36
|
-
/**
|
|
37
|
-
export
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
by: (await deps.repository.userName()) ?? "unknown",
|
|
42
|
-
cruze: deps.bundle.version,
|
|
43
|
-
...fields,
|
|
44
|
-
};
|
|
45
|
-
await deps.files.appendText(journalPath(folder), serializeEntry(entry));
|
|
46
|
-
return entry;
|
|
32
|
+
/** The project's config, or an error telling the user to fix it first. */
|
|
33
|
+
export function requireConfig(docs: ProjectDocs): CruzeConfig {
|
|
34
|
+
const config = docs.config?.config;
|
|
35
|
+
if (config === null || config === undefined) throw new CruzeError("config", "fix .cruze/config.yaml first; run cruze validate for details");
|
|
36
|
+
return config;
|
|
47
37
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { approvalHash, serializeApprovals, withApproval, type ApprovalRecord } from "../../domain/approvals.ts";
|
|
2
|
+
import { CruzeError } from "../../domain/cruze_error.ts";
|
|
3
|
+
import { parseMarkdown } from "../../domain/markdown.ts";
|
|
4
|
+
import { PATHS } from "../../domain/project/layout.ts";
|
|
5
|
+
import { resolveDocument } from "../../domain/project/project_docs.ts";
|
|
6
|
+
import { validateProject } from "../../domain/validation/validate_project.ts";
|
|
7
|
+
import { loadDocs, type ProjectDeps } from "../project_context.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Records that the user approved a document as it reads now: the vision, the architecture map or a
|
|
11
|
+
* feature. Any later edit to its design content shows it as edited until it is approved again.
|
|
12
|
+
*/
|
|
13
|
+
export async function approveDocument(deps: ProjectDeps, ref: string): Promise<ApprovalRecord> {
|
|
14
|
+
const docs = await loadDocs(deps.files);
|
|
15
|
+
const path = resolveDocument(docs, ref);
|
|
16
|
+
if (path === undefined) throw new CruzeError("not-found", `no document matches "${ref}"; name vision, architecture, a feature slug or a path`);
|
|
17
|
+
const errors = validateProject(docs).filter((p) => p.path === path && p.severity === "error");
|
|
18
|
+
if (errors.length > 0) {
|
|
19
|
+
throw new CruzeError("invalid", `${path} has ${errors.length} problem(s) to fix first: ${errors.map((p) => `${p.line === undefined ? "" : `line ${p.line}: `}${p.message}`).join("; ")}`);
|
|
20
|
+
}
|
|
21
|
+
const record: ApprovalRecord = {
|
|
22
|
+
path,
|
|
23
|
+
hash: approvalHash(path, parseMarkdown(docs.snapshot.get(path) ?? "")),
|
|
24
|
+
approvedAt: deps.clock.now().toISOString(),
|
|
25
|
+
by: (await deps.repository.userName()) ?? "unknown",
|
|
26
|
+
};
|
|
27
|
+
await deps.files.writeText(PATHS.approvals, serializeApprovals(withApproval(docs.approvals, record)));
|
|
28
|
+
return record;
|
|
29
|
+
}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import { factsOf } from "../../domain/elements.ts";
|
|
2
1
|
import { checkSource, type CheckFinding } from "../../domain/check/check_source.ts";
|
|
3
2
|
import { CruzeError } from "../../domain/cruze_error.ts";
|
|
4
3
|
import { matchesAny } from "../../domain/glob.ts";
|
|
5
|
-
import { FIRST_RELEASE, isPending } from "../../domain/versions/rule_versions.ts";
|
|
6
|
-
import type { CruzeConfig } from "../../domain/config.ts";
|
|
7
|
-
import type { ProjectView } from "../../domain/project/project_view.ts";
|
|
8
|
-
import { resolveChange } from "../../domain/project/artifact_ref.ts";
|
|
9
|
-
import { readDelta } from "../../domain/project/deltas.ts";
|
|
10
|
-
import { activeChange } from "../../domain/status/work_status.ts";
|
|
11
4
|
import { traceScenarios, type TraceReport } from "../../domain/trace.ts";
|
|
12
|
-
import {
|
|
5
|
+
import { loadDocs, requireConfig, type ProjectDeps } from "../project_context.ts";
|
|
13
6
|
import type { ProjectFiles } from "../ports/project_files.ts";
|
|
14
7
|
|
|
15
8
|
export interface CheckReport {
|
|
@@ -18,30 +11,19 @@ export interface CheckReport {
|
|
|
18
11
|
findings: CheckFinding[];
|
|
19
12
|
}
|
|
20
13
|
|
|
21
|
-
/**
|
|
22
|
-
* Enforces layer rules and file budgets. Layer violations always fail; budget and
|
|
23
|
-
* module-map warnings fail only with `ci`. A rule newer than the project's `standards:`
|
|
24
|
-
* is pending: it reports, but fails nothing until the project realigns.
|
|
25
|
-
*/
|
|
14
|
+
/** Enforces layer rules and file budgets. Layer violations always fail; budget warnings fail only with `ci`. */
|
|
26
15
|
export async function checkCode(deps: ProjectDeps, options: { files?: string[]; ci: boolean }): Promise<CheckReport> {
|
|
27
|
-
const
|
|
28
|
-
const config = requireConfig(view);
|
|
16
|
+
const config = requireConfig(await loadDocs(deps.files));
|
|
29
17
|
const sources = await readMatching(deps.files, config.source);
|
|
30
|
-
const tests = await readMatching(deps.files, config.tests);
|
|
31
18
|
for (const file of options.files ?? []) {
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
else if (matchesAny(file, config.tests)) tests.set(file, text);
|
|
37
|
-
}
|
|
19
|
+
if (sources.has(file) || !matchesAny(file, config.source)) continue;
|
|
20
|
+
const text = await deps.files.readText(file);
|
|
21
|
+
if (text === undefined) throw new CruzeError("not-found", `${file} does not exist`);
|
|
22
|
+
sources.set(file, text);
|
|
38
23
|
}
|
|
39
|
-
const only = options.files?.filter((file) => sources.has(file)
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
isPending("check", f.rule, standards) ? { ...f, pending: true as const } : f,
|
|
43
|
-
);
|
|
44
|
-
const failing = findings.filter((f) => f.pending !== true && (f.severity === "error" || options.ci));
|
|
24
|
+
const only = options.files?.filter((file) => sources.has(file));
|
|
25
|
+
const findings = checkSource({ config, sources, ...(only === undefined ? {} : { only }) });
|
|
26
|
+
const failing = findings.filter((f) => f.severity === "error" || options.ci);
|
|
45
27
|
return { passed: failing.length === 0, checked: only?.length ?? sources.size, findings };
|
|
46
28
|
}
|
|
47
29
|
|
|
@@ -50,23 +32,18 @@ export interface TraceResult extends TraceReport {
|
|
|
50
32
|
scope: string;
|
|
51
33
|
}
|
|
52
34
|
|
|
53
|
-
/** Scenario-to-test traceability for
|
|
54
|
-
export async function traceTests(deps: ProjectDeps,
|
|
55
|
-
const
|
|
56
|
-
const config = requireConfig(
|
|
57
|
-
let
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
if (
|
|
35
|
+
/** Scenario-to-test traceability for one feature, or for every built feature. */
|
|
36
|
+
export async function traceTests(deps: ProjectDeps, slug?: string): Promise<TraceResult> {
|
|
37
|
+
const docs = await loadDocs(deps.files);
|
|
38
|
+
const config = requireConfig(docs);
|
|
39
|
+
let required = docs.features.filter((f) => f.status === "built");
|
|
40
|
+
if (slug !== undefined) {
|
|
41
|
+
const feature = docs.features.find((f) => f.slug === slug);
|
|
42
|
+
if (feature === undefined) throw new CruzeError("not-found", `no feature ${slug} in docs/features/`);
|
|
43
|
+
required = [feature];
|
|
61
44
|
}
|
|
62
|
-
const report = traceScenarios(
|
|
63
|
-
return { ...report, passed: report.missing.length === 0 && report.unknown.length === 0, scope:
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function requireConfig(view: ProjectView): CruzeConfig {
|
|
67
|
-
const config = view.config?.config;
|
|
68
|
-
if (config === null || config === undefined) throw new CruzeError("config", "fix .cruze/config.yaml first; run cruze validate for details");
|
|
69
|
-
return config;
|
|
45
|
+
const report = traceScenarios(docs.features, required, await readMatching(deps.files, config.tests));
|
|
46
|
+
return { ...report, passed: report.missing.length === 0 && report.unknown.length === 0, scope: slug ?? "every built feature" };
|
|
70
47
|
}
|
|
71
48
|
|
|
72
49
|
/** Files matching any glob, read from the smallest directories that can contain them. */
|
|
@@ -90,15 +67,3 @@ function globBase(glob: string): string {
|
|
|
90
67
|
const fixed = segments.slice(0, segments.findIndex((s) => /[*?{]/.test(s)));
|
|
91
68
|
return fixed.join("/");
|
|
92
69
|
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Every module's Path: the living docs' modules, and the modules that work in progress adds or
|
|
96
|
-
* changes in its architecture delta, since their files are written before land merges them.
|
|
97
|
-
*/
|
|
98
|
-
function modulePaths(view: ProjectView): string[] {
|
|
99
|
-
const living = [...view.living.elements.values()].filter((e) => e.id.startsWith("MOD-")).map((e) => ({ doc: e.doc, element: e.element }));
|
|
100
|
-
const inFlight = view.items
|
|
101
|
-
.filter((item) => item.kind !== "change" && !item.archived)
|
|
102
|
-
.flatMap((item) => readDelta(item.doc).entries.filter((entry) => entry.element.id.startsWith("MOD-") && entry.element.op !== "REMOVED").map((entry) => ({ doc: item.doc, element: entry.element })));
|
|
103
|
-
return [...living, ...inFlight].flatMap(({ doc, element }) => (factsOf(doc, element).get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "")));
|
|
104
|
-
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CruzeError } from "../../domain/cruze_error.ts";
|
|
2
|
+
import { isSlug } from "../../domain/ids.ts";
|
|
3
|
+
import { PATHS, featurePath } from "../../domain/project/layout.ts";
|
|
4
|
+
import { renderTemplate } from "../../domain/scaffold.ts";
|
|
5
|
+
import { loadDocs, requireConfig, type ProjectDeps } from "../project_context.ts";
|
|
6
|
+
|
|
7
|
+
export const DOCUMENT_KINDS = ["vision", "architecture", "feature"] as const;
|
|
8
|
+
export type DocumentKind = (typeof DOCUMENT_KINDS)[number];
|
|
9
|
+
|
|
10
|
+
export interface NewDocReport {
|
|
11
|
+
kind: DocumentKind;
|
|
12
|
+
path: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Creates the vision, the architecture map or a feature from its template. A feature needs a slug, and a title defaults to it. */
|
|
16
|
+
export async function createDocument(deps: ProjectDeps, kind: DocumentKind, slug?: string, title?: string): Promise<NewDocReport> {
|
|
17
|
+
const docs = await loadDocs(deps.files);
|
|
18
|
+
const project = requireConfig(docs).project;
|
|
19
|
+
if (kind === "feature" && (slug === undefined || !isSlug(slug))) throw new CruzeError("invalid-slug", `a feature needs a slug of lowercase words joined by single hyphens, such as "sign-in"`);
|
|
20
|
+
const path = kind === "feature" ? featurePath(slug ?? "") : PATHS[kind];
|
|
21
|
+
if (docs.snapshot.has(path)) throw new CruzeError("exists", `${path} already exists`);
|
|
22
|
+
const values = { project_name: project, slug: slug ?? "", title: title ?? slug ?? "" };
|
|
23
|
+
await deps.files.writeText(path, renderTemplate(await deps.bundle.formatTemplate(`${kind}.md`), values));
|
|
24
|
+
return { kind, path };
|
|
25
|
+
}
|
|
@@ -1,82 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { approvalState, type ApprovalState } from "../../domain/approvals.ts";
|
|
2
2
|
import { PATHS } from "../../domain/project/layout.ts";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { activeChange, buildGateReasons, projectStatus, touchedIds, type ProjectStatus } from "../../domain/status/work_status.ts";
|
|
8
|
-
import { appendJournal, loadView, type ProjectDeps } from "../project_context.ts";
|
|
3
|
+
import type { MarkdownDoc } from "../../domain/markdown.ts";
|
|
4
|
+
import { nextStep, type NextStep } from "../../domain/next_step.ts";
|
|
5
|
+
import type { FeatureStatus } from "../../domain/project/features.ts";
|
|
6
|
+
import { loadDocs, type ProjectDeps } from "../project_context.ts";
|
|
9
7
|
|
|
10
|
-
export interface
|
|
11
|
-
|
|
8
|
+
export interface DocumentStatus {
|
|
9
|
+
path: string;
|
|
10
|
+
state: ApprovalState | "missing";
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface GateReport {
|
|
26
|
-
gate: "build";
|
|
27
|
-
passed: boolean;
|
|
28
|
-
reasons: string[];
|
|
29
|
-
overridden: boolean;
|
|
13
|
+
export interface FeatureReport {
|
|
14
|
+
slug: string;
|
|
15
|
+
path?: string;
|
|
16
|
+
/** Missing when the release lists it but nobody has shaped it yet. */
|
|
17
|
+
status: FeatureStatus | "not-shaped" | "unknown";
|
|
18
|
+
approval: ApprovalState | "missing";
|
|
19
|
+
inRelease: boolean;
|
|
20
|
+
needs: string[];
|
|
21
|
+
scenarios: number;
|
|
30
22
|
}
|
|
31
23
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export async function checkBuildGate(deps: ProjectDeps, override?: string): Promise<GateReport> {
|
|
37
|
-
const view = await loadView(deps.files);
|
|
38
|
-
const branch = await deps.repository.currentBranch();
|
|
39
|
-
const reasons = buildGateReasons(view, branch);
|
|
40
|
-
if (reasons.length === 0) return { gate: "build", passed: true, reasons, overridden: false };
|
|
41
|
-
if (override === undefined) return { gate: "build", passed: false, reasons, overridden: false };
|
|
42
|
-
if (override.trim() === "") throw new CruzeError("override-reason", "an override needs a reason");
|
|
43
|
-
const folder = activeChange(view, branch)?.folder ?? ".cruze";
|
|
44
|
-
await appendJournal(deps, folder, "override", { gate: "build", reason: override, blocked_by: reasons });
|
|
45
|
-
return { gate: "build", passed: true, reasons, overridden: true };
|
|
24
|
+
export interface StatusReport {
|
|
25
|
+
documents: DocumentStatus[];
|
|
26
|
+
features: FeatureReport[];
|
|
27
|
+
next: NextStep;
|
|
46
28
|
}
|
|
47
29
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
return { change: change.ref, overlaps };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function landed(item: WorkItem): boolean {
|
|
81
|
-
return item.archived || readProgress(item.doc).landed !== undefined;
|
|
30
|
+
/** Each document's approval, and each feature's progress, in release order. */
|
|
31
|
+
export async function showStatus(deps: ProjectDeps): Promise<StatusReport> {
|
|
32
|
+
const docs = await loadDocs(deps.files);
|
|
33
|
+
const state = (path: string, doc: MarkdownDoc | undefined): ApprovalState | "missing" => (doc === undefined ? "missing" : approvalState(docs.approvals, path, doc));
|
|
34
|
+
const documents = [
|
|
35
|
+
{ path: PATHS.vision, state: state(PATHS.vision, docs.vision) },
|
|
36
|
+
{ path: PATHS.architecture, state: state(PATHS.architecture, docs.architecture) },
|
|
37
|
+
];
|
|
38
|
+
const slugs = [...docs.release.map((r) => r.slug), ...docs.features.map((f) => f.slug).filter((s) => !docs.release.some((r) => r.slug === s))];
|
|
39
|
+
const features = slugs.map((slug): FeatureReport => {
|
|
40
|
+
const feature = docs.features.find((f) => f.slug === slug);
|
|
41
|
+
const row = docs.release.find((r) => r.slug === slug);
|
|
42
|
+
return {
|
|
43
|
+
slug,
|
|
44
|
+
...(feature === undefined ? {} : { path: feature.path }),
|
|
45
|
+
status: feature === undefined ? "not-shaped" : (feature.status ?? "unknown"),
|
|
46
|
+
approval: feature === undefined ? "missing" : approvalState(docs.approvals, feature.path, feature.doc),
|
|
47
|
+
inRelease: row !== undefined,
|
|
48
|
+
needs: row?.needs ?? [],
|
|
49
|
+
scenarios: feature?.scenarios.length ?? 0,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
return { documents, features, next: nextStep(docs) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The step to run next, and why. */
|
|
56
|
+
export async function suggestNext(deps: ProjectDeps): Promise<NextStep> {
|
|
57
|
+
return nextStep(await loadDocs(deps.files));
|
|
82
58
|
}
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import { CruzeError } from "../../domain/cruze_error.ts";
|
|
2
|
-
import { resolveArtifact } from "../../domain/project/artifact_ref.ts";
|
|
3
|
-
import { isPlanned } from "../../domain/project/plan_parts.ts";
|
|
4
|
-
import { featureOf, type WorkItem } from "../../domain/project/work_items.ts";
|
|
5
2
|
import type { AgentRunner } from "../ports/agent_runner.ts";
|
|
6
|
-
import {
|
|
3
|
+
import { loadDocs, type ProjectDeps } from "../project_context.ts";
|
|
7
4
|
|
|
8
|
-
export const ROLES = ["
|
|
5
|
+
export const ROLES = ["code-reviewer", "researcher"] as const;
|
|
9
6
|
export type Role = (typeof ROLES)[number];
|
|
10
7
|
|
|
11
8
|
export interface ReviewRequest {
|
|
12
9
|
role: string;
|
|
13
|
-
/** The feature
|
|
10
|
+
/** The feature under review, by slug. */
|
|
14
11
|
item?: string;
|
|
15
|
-
round?: number;
|
|
16
|
-
/** The round-1 blockers, for round 2. */
|
|
17
|
-
blockers?: string;
|
|
18
12
|
/** The commit the diff under review starts from. */
|
|
19
13
|
base?: string;
|
|
20
14
|
}
|
|
@@ -30,37 +24,31 @@ export type ReviewReport = { role: Role; item?: string; command?: string[] } & (
|
|
|
30
24
|
|
|
31
25
|
/**
|
|
32
26
|
* Runs a Cruze role in a fresh agent context: the role's prompt, what it reviews, and nothing
|
|
33
|
-
* from the conversation that produced the work.
|
|
34
|
-
* prompt to run in a helper when the configured agent isn't installed.
|
|
27
|
+
* from the conversation that produced the work.
|
|
35
28
|
*/
|
|
36
29
|
export async function runReview(deps: ProjectDeps & { runner: AgentRunner }, request: ReviewRequest): Promise<ReviewReport> {
|
|
37
30
|
if (!(ROLES as readonly string[]).includes(request.role)) throw new CruzeError("unknown-role", `the roles are ${ROLES.join(", ")}`);
|
|
38
31
|
const role = request.role as Role;
|
|
39
|
-
const
|
|
32
|
+
const docs = await loadDocs(deps.files);
|
|
40
33
|
const roleText = (await deps.files.readText(`.agents/skills/cruze-roles/${role}.md`)) ?? (await bundledRole(deps, role));
|
|
41
34
|
|
|
42
|
-
const context = [
|
|
35
|
+
const context: string[] = [];
|
|
43
36
|
if (request.item !== undefined) {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (feature !== undefined) context.push(`Its feature: \`${feature.path}\``);
|
|
48
|
-
if (role === "design-reviewer" && item !== undefined) context.push(rubricLine(item));
|
|
49
|
-
} else {
|
|
50
|
-
context.push("Under review: the project documents the role lists for project scope");
|
|
37
|
+
const feature = docs.features.find((f) => f.slug === request.item);
|
|
38
|
+
if (feature === undefined) throw new CruzeError("not-found", `no feature ${request.item} in docs/features/`);
|
|
39
|
+
context.push(`Under review: the feature in \`${feature.path}\``);
|
|
51
40
|
}
|
|
52
41
|
if (request.base !== undefined) context.push(`The changes under review: run \`git diff ${request.base}\`.`);
|
|
53
|
-
if (request.blockers !== undefined) context.push(`Round-1 blockers to check:\n\n${request.blockers}`);
|
|
54
42
|
|
|
55
43
|
const prompt = [
|
|
56
44
|
roleText.trim(),
|
|
57
45
|
"## This run",
|
|
58
46
|
"You are running the role above in a fresh context, in the project's repository. Read every input it lists from the files, and run the commands it names. Edit no files. End with the report the role describes.",
|
|
59
|
-
context.map((line) => `- ${line}`).join("\n"),
|
|
47
|
+
...(context.length === 0 ? [] : [context.map((line) => `- ${line}`).join("\n")]),
|
|
60
48
|
].join("\n\n");
|
|
61
49
|
|
|
62
50
|
const subject = { role, ...(request.item === undefined ? {} : { item: request.item }) };
|
|
63
|
-
const command =
|
|
51
|
+
const command = docs.config?.config?.review.command;
|
|
64
52
|
if (command === undefined) return { ...subject, status: "run-in-helper", instruction: helperInstruction(undefined), prompt };
|
|
65
53
|
const run = await deps.runner.run(command, prompt);
|
|
66
54
|
if (run.kind === "missing") return { ...subject, command, status: "run-in-helper", instruction: helperInstruction(command[0]), prompt };
|
|
@@ -84,10 +72,3 @@ async function bundledRole(deps: ProjectDeps, role: Role): Promise<string> {
|
|
|
84
72
|
if (file === undefined) throw new CruzeError("role-missing", `the ${role} role is not installed; run cruze install`);
|
|
85
73
|
return file.text;
|
|
86
74
|
}
|
|
87
|
-
|
|
88
|
-
/** A design reviewer reads one rubric: the plan rubric for a planned change, the design rubric otherwise. */
|
|
89
|
-
function rubricLine(item: WorkItem): string {
|
|
90
|
-
return item.kind !== "feature" && isPlanned(item.doc)
|
|
91
|
-
? "This is a change's plan: review it against `.agents/skills/cruze-roles/plan-rubric.md`."
|
|
92
|
-
: "This is a design: review it against `.agents/skills/cruze-roles/design-rubric.md`.";
|
|
93
|
-
}
|
|
@@ -3,8 +3,8 @@ import { warning, type Problem } from "../../domain/validation/problem.ts";
|
|
|
3
3
|
import { PATHS } from "../../domain/project/layout.ts";
|
|
4
4
|
import { versionSkew } from "../../domain/versions/cruze_version.ts";
|
|
5
5
|
import type { Bundle } from "../ports/bundle.ts";
|
|
6
|
-
import { loadView } from "../project_context.ts";
|
|
7
6
|
import type { ProjectFiles } from "../ports/project_files.ts";
|
|
7
|
+
import { loadDocs } from "../project_context.ts";
|
|
8
8
|
|
|
9
9
|
export interface ValidateReport {
|
|
10
10
|
valid: boolean;
|
|
@@ -15,10 +15,10 @@ export interface ValidateReport {
|
|
|
15
15
|
|
|
16
16
|
/** Checks every document in the project against the cruze-formats rules, and that its skills match this CLI. */
|
|
17
17
|
export async function validate(deps: { files: ProjectFiles; bundle: Bundle }): Promise<ValidateReport> {
|
|
18
|
-
const
|
|
19
|
-
const config =
|
|
18
|
+
const docs = await loadDocs(deps.files);
|
|
19
|
+
const config = docs.config?.config;
|
|
20
20
|
const skew = config === null || config === undefined ? undefined : versionSkew(deps.bundle.version, config.cruze);
|
|
21
|
-
const problems = [...validateProject(
|
|
21
|
+
const problems = [...validateProject(docs), ...(skew === undefined ? [] : [warning(PATHS.config, undefined, "cruze-version", skew.message)])];
|
|
22
22
|
return {
|
|
23
23
|
valid: !hasErrors(problems),
|
|
24
24
|
errors: problems.filter((p) => p.severity === "error").length,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { designHash } from "./hashing.ts";
|
|
2
|
+
import type { MarkdownDoc } from "./markdown.ts";
|
|
3
|
+
import { slugOfFeaturePath } from "./project/layout.ts";
|
|
4
|
+
|
|
5
|
+
/** One approval of one document: the hash of its design content when the user approved it. */
|
|
6
|
+
export interface ApprovalRecord {
|
|
7
|
+
path: string;
|
|
8
|
+
hash: string;
|
|
9
|
+
approvedAt: string;
|
|
10
|
+
by: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ApprovalState = "approved" | "edited" | "unapproved";
|
|
14
|
+
|
|
15
|
+
/** Reads `.cruze/approvals.json`; anything unreadable counts as no approvals, and validate reports it. */
|
|
16
|
+
export function parseApprovals(text: string | undefined): ApprovalRecord[] {
|
|
17
|
+
if (text === undefined) return [];
|
|
18
|
+
try {
|
|
19
|
+
const parsed: unknown = JSON.parse(text);
|
|
20
|
+
const list = (parsed as { approvals?: unknown }).approvals;
|
|
21
|
+
if (!Array.isArray(list)) return [];
|
|
22
|
+
return list.filter((r): r is ApprovalRecord => typeof r?.path === "string" && typeof r?.hash === "string" && typeof r?.approvedAt === "string" && typeof r?.by === "string");
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function serializeApprovals(records: ApprovalRecord[]): string {
|
|
29
|
+
const sorted = [...records].sort((a, b) => a.path.localeCompare(b.path));
|
|
30
|
+
return `${JSON.stringify({ version: 2, approvals: sorted }, null, 2)}\n`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The records with this one replacing any earlier approval of the same document. */
|
|
34
|
+
export function withApproval(records: ApprovalRecord[], record: ApprovalRecord): ApprovalRecord[] {
|
|
35
|
+
return [...records.filter((r) => r.path !== record.path), record];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The hash an approval records. A feature's `## Decisions` is the agent's record of what it chose,
|
|
40
|
+
* which build and review add to, so it stays out: the user's approval covers the rest.
|
|
41
|
+
*/
|
|
42
|
+
export function approvalHash(path: string, doc: MarkdownDoc): string {
|
|
43
|
+
return designHash(doc, slugOfFeaturePath(path) === undefined ? [] : ["Decisions"]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether the document still reads as it did when it was approved. */
|
|
47
|
+
export function approvalState(records: readonly ApprovalRecord[], path: string, doc: MarkdownDoc): ApprovalState {
|
|
48
|
+
const record = records.find((r) => r.path === path);
|
|
49
|
+
if (record === undefined) return "unapproved";
|
|
50
|
+
return record.hash === approvalHash(path, doc) ? "approved" : "edited";
|
|
51
|
+
}
|