@printpractical/cruze 0.0.13 → 0.0.15
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/install.js +1 -0
- 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/install_skills.js +7 -1
- 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 +8 -5
- package/dist/domain/approvals.js +51 -0
- package/dist/domain/check/check_source.js +3 -32
- package/dist/domain/check/imports.js +3 -3
- package/dist/domain/check/rust_imports.js +25 -3
- 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 +65 -0
- package/dist/domain/validation/release_rules.js +45 -0
- package/dist/domain/validation/review_rules.js +21 -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 +22 -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/install.ts +1 -0
- 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/install_skills.ts +7 -1
- 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 +8 -5
- package/src/domain/approvals.ts +51 -0
- package/src/domain/check/check_source.ts +3 -32
- package/src/domain/check/imports.ts +3 -3
- package/src/domain/check/rust_imports.ts +25 -3
- 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 +65 -0
- package/src/domain/validation/release_rules.ts +45 -0
- package/src/domain/validation/review_rules.ts +21 -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,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
|
-
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { hasErrors, validateProject } from "../../domain/validation/validate_project.ts";
|
|
2
2
|
import { warning, type Problem } from "../../domain/validation/problem.ts";
|
|
3
3
|
import { PATHS } from "../../domain/project/layout.ts";
|
|
4
|
+
import { reviewSettingTemplate } from "../../domain/scaffold.ts";
|
|
5
|
+
import { reviewCommandProblems } from "../../domain/validation/review_rules.ts";
|
|
4
6
|
import { versionSkew } from "../../domain/versions/cruze_version.ts";
|
|
5
7
|
import type { Bundle } from "../ports/bundle.ts";
|
|
6
|
-
import { loadView } from "../project_context.ts";
|
|
7
8
|
import type { ProjectFiles } from "../ports/project_files.ts";
|
|
9
|
+
import { loadDocs } from "../project_context.ts";
|
|
8
10
|
|
|
9
11
|
export interface ValidateReport {
|
|
10
12
|
valid: boolean;
|
|
@@ -13,12 +15,13 @@ export interface ValidateReport {
|
|
|
13
15
|
problems: Problem[];
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
/** Checks every document in the project against the cruze-formats rules,
|
|
18
|
+
/** Checks every document in the project against the cruze-formats rules, that its skills match this CLI, and that its reviewer can run the tests. */
|
|
17
19
|
export async function validate(deps: { files: ProjectFiles; bundle: Bundle }): Promise<ValidateReport> {
|
|
18
|
-
const
|
|
19
|
-
const config =
|
|
20
|
+
const docs = await loadDocs(deps.files);
|
|
21
|
+
const config = docs.config?.config;
|
|
20
22
|
const skew = config === null || config === undefined ? undefined : versionSkew(deps.bundle.version, config.cruze);
|
|
21
|
-
const
|
|
23
|
+
const review = reviewCommandProblems(docs, await deps.bundle.template(reviewSettingTemplate("claude")));
|
|
24
|
+
const problems = [...validateProject(docs), ...(skew === undefined ? [] : [warning(PATHS.config, undefined, "cruze-version", skew.message)]), ...review];
|
|
22
25
|
return {
|
|
23
26
|
valid: !hasErrors(problems),
|
|
24
27
|
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
|
+
}
|
|
@@ -1,36 +1,29 @@
|
|
|
1
1
|
import type { CruzeConfig, Layer } from "../config.ts";
|
|
2
2
|
import { matchesAny } from "../glob.ts";
|
|
3
3
|
import { budgetedText } from "./budget_text.ts";
|
|
4
|
-
import { decisionCitationsInComments } from "./decision_comments.ts";
|
|
5
4
|
import { importsOf, resolveImport } from "./imports.ts";
|
|
6
5
|
import { countTypes } from "./type_count.ts";
|
|
7
6
|
|
|
8
7
|
export interface CheckFinding {
|
|
9
8
|
path: string;
|
|
10
9
|
line?: number;
|
|
11
|
-
rule: "layer" | "max-lines" | "max-types" | "
|
|
10
|
+
rule: "layer" | "max-lines" | "max-types" | "no-layer" | "test-placement";
|
|
12
11
|
message: string;
|
|
13
12
|
/** Layer violations always fail; the rest fail in CI unless excepted. */
|
|
14
13
|
severity: "error" | "warning";
|
|
15
|
-
/** Set when the rule is newer than the project's `standards:`: it reports, but can't fail CI until the project realigns. */
|
|
16
|
-
pending?: true;
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
export interface CheckInput {
|
|
20
17
|
config: CruzeConfig;
|
|
21
18
|
/** Every source file matched by config.source, by path. */
|
|
22
19
|
sources: ReadonlyMap<string, string>;
|
|
23
|
-
/** Paths of the MOD elements in the architecture; files outside all of them are flagged. */
|
|
24
|
-
modulePaths: string[];
|
|
25
|
-
/** Test files outside config.source, by path: only their comments are checked. */
|
|
26
|
-
tests?: ReadonlyMap<string, string>;
|
|
27
20
|
/** Check only these files, when given; imports still resolve against every source. */
|
|
28
21
|
only?: string[];
|
|
29
22
|
}
|
|
30
23
|
|
|
31
24
|
/** Enforces the architecture's layer rules and the file budgets on source code. */
|
|
32
25
|
export function checkSource(input: CheckInput): CheckFinding[] {
|
|
33
|
-
const { config, sources
|
|
26
|
+
const { config, sources } = input;
|
|
34
27
|
const all = [...sources.keys()];
|
|
35
28
|
const targets = input.only ?? all;
|
|
36
29
|
const layerOf = (path: string): Layer | undefined => config.layers.find((layer) => matchesAny(path, layer.paths));
|
|
@@ -44,7 +37,7 @@ export function checkSource(input: CheckInput): CheckFinding[] {
|
|
|
44
37
|
findings.push({ path, rule: "no-layer", severity: "warning", message: "the file is in no layer, so its imports are unchecked" });
|
|
45
38
|
}
|
|
46
39
|
if (layer !== undefined) {
|
|
47
|
-
for (const found of importsOf(path, text)) {
|
|
40
|
+
for (const found of importsOf(path, text, all)) {
|
|
48
41
|
const target = resolveImport(found, all);
|
|
49
42
|
const targetLayer = target === undefined ? undefined : layerOf(target);
|
|
50
43
|
if (target === undefined || targetLayer === undefined || targetLayer.name === layer.name) continue;
|
|
@@ -67,32 +60,10 @@ export function checkSource(input: CheckInput): CheckFinding[] {
|
|
|
67
60
|
if (!excepted && isRustTestFileInSource(path)) {
|
|
68
61
|
findings.push({ path, rule: "test-placement", severity: "warning", message: "Rust tests that leave their inline module go in the crate's tests/ directory, not under src/" });
|
|
69
62
|
}
|
|
70
|
-
if (modulePaths.length > 0 && !insideModuleMap(path, modulePaths)) {
|
|
71
|
-
findings.push({ path, rule: "outside-module-map", severity: "warning", message: "the file is outside every module in the architecture's module map" });
|
|
72
|
-
}
|
|
73
|
-
findings.push(...decisionFindings(path, text));
|
|
74
|
-
}
|
|
75
|
-
for (const [path, text] of input.tests ?? []) {
|
|
76
|
-
if (!sources.has(path) && (input.only === undefined || input.only.includes(path))) findings.push(...decisionFindings(path, text));
|
|
77
63
|
}
|
|
78
64
|
return findings;
|
|
79
65
|
}
|
|
80
66
|
|
|
81
|
-
function decisionFindings(path: string, text: string): CheckFinding[] {
|
|
82
|
-
return decisionCitationsInComments(path, text).map((citation) => ({
|
|
83
|
-
path,
|
|
84
|
-
line: citation.line + 1,
|
|
85
|
-
rule: "decision-citation",
|
|
86
|
-
severity: "warning",
|
|
87
|
-
message: `the comment cites ${citation.text}, a decision number a reader of the code can't follow; say what was decided, or cite the element or ADR that holds it`,
|
|
88
|
-
}));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Whether a module's Path covers the file: a directory path ending in `/` covers everything under it, any other names one file. */
|
|
92
|
-
export function insideModuleMap(path: string, modulePaths: string[]): boolean {
|
|
93
|
-
return modulePaths.some((module) => (module.endsWith("/") ? path.startsWith(module) : path === module));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
67
|
/** A Rust file of tests split out of a module: `tests.rs`, `*_tests.rs`, or anything under a `tests/` folder inside `src/`. */
|
|
97
68
|
function isRustTestFileInSource(path: string): boolean {
|
|
98
69
|
if (!path.endsWith(".rs")) return false;
|
|
@@ -12,11 +12,11 @@ export interface SourceImport {
|
|
|
12
12
|
const JS = /\.(?:[cm]?[jt]sx?)$/;
|
|
13
13
|
const JS_EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", "/index.ts", "/index.js"];
|
|
14
14
|
|
|
15
|
-
/** Imports that point inside the project
|
|
16
|
-
export function importsOf(path: string, text: string): SourceImport[] {
|
|
15
|
+
/** Imports that point inside the project, among `files`. Imports of libraries and the standard library are left out. */
|
|
16
|
+
export function importsOf(path: string, text: string, files: readonly string[]): SourceImport[] {
|
|
17
17
|
if (JS.test(path)) return jsImports(path, text);
|
|
18
18
|
if (path.endsWith(".py")) return pythonImports(path, text);
|
|
19
|
-
if (path.endsWith(".rs")) return rustImportCandidates(path, text).map((i) => ({ ...i, bySuffix: false }));
|
|
19
|
+
if (path.endsWith(".rs")) return rustImportCandidates(path, text, files).map((i) => ({ ...i, bySuffix: false }));
|
|
20
20
|
if (/\.(?:c|h|cc|cpp|cxx|hh|hpp|hxx)$/.test(path)) return matches(text, /^\s*#\s*include\s+"([^"]+)"/gm, (target) => [target], true);
|
|
21
21
|
if (path.endsWith(".go")) return goImports(text);
|
|
22
22
|
if (/\.(?:java|kt)$/.test(path)) return matches(text, /^import\s+(?:static\s+)?([\w.]+)(?:\.\*)?;?\s*$/gm, (t) => [`${t.replace(/\./g, "/")}.java`, `${t.replace(/\./g, "/")}.kt`], true);
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
/** Resolves Rust `use` paths inside a crate to candidate source files. */
|
|
1
|
+
/** Resolves Rust `use` paths, inside a crate and between a workspace's crates, to candidate source files. */
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Candidate files for each `use crate::…`, `use super::…` and `use self::…` path in a Rust file,
|
|
5
|
+
* and for each `use other_crate::…` that names a library crate among `files`.
|
|
6
|
+
*/
|
|
7
|
+
export function rustImportCandidates(path: string, text: string, files: readonly string[]): Array<{ line: number; candidates: string[] }> {
|
|
5
8
|
const root = crateRoot(path);
|
|
6
9
|
if (root === null) return [];
|
|
7
10
|
const own = moduleSegments(path, root);
|
|
11
|
+
const crates = libraryCrates(files);
|
|
8
12
|
const results: Array<{ line: number; candidates: string[] }> = [];
|
|
9
13
|
const pattern = /^\s*(?:pub(?:\([^)]*\))?\s+)?use\s+([^;]+);/gms;
|
|
10
14
|
for (const match of text.matchAll(pattern)) {
|
|
@@ -12,11 +16,29 @@ export function rustImportCandidates(path: string, text: string): Array<{ line:
|
|
|
12
16
|
for (const segments of expandUse((match[1] ?? "").replace(/\s+/g, ""))) {
|
|
13
17
|
const absolute = absoluteSegments(segments, own);
|
|
14
18
|
if (absolute !== null) results.push({ line, candidates: candidatesFor(root, absolute) });
|
|
19
|
+
else {
|
|
20
|
+
const [head, ...rest] = segments;
|
|
21
|
+
const other = head === undefined ? undefined : crates.get(head);
|
|
22
|
+
if (other !== undefined && other !== root) results.push({ line, candidates: candidatesFor(other, rest) });
|
|
23
|
+
}
|
|
15
24
|
}
|
|
16
25
|
}
|
|
17
26
|
return results;
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Each library crate in the project by the name code imports it as: the directory
|
|
31
|
+
* holding `src/lib.rs`, with hyphens as underscores, as Cargo names it by default.
|
|
32
|
+
*/
|
|
33
|
+
function libraryCrates(files: readonly string[]): Map<string, string> {
|
|
34
|
+
const crates = new Map<string, string>();
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
const match = /^(?:.*\/)?([^/]+)\/src\/lib\.rs$/.exec(file);
|
|
37
|
+
if (match !== null) crates.set((match[1] ?? "").replace(/-/g, "_"), file.slice(0, -"lib.rs".length));
|
|
38
|
+
}
|
|
39
|
+
return crates;
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
/** The directory holding the crate root: everything up to and including `src/`. */
|
|
21
43
|
function crateRoot(path: string): string | null {
|
|
22
44
|
const index = path.lastIndexOf("src/");
|
package/src/domain/config.ts
CHANGED
|
@@ -2,13 +2,10 @@ import { parse as parseYaml } from "yaml";
|
|
|
2
2
|
|
|
3
3
|
/** `.cruze/config.yaml`, as described in the cruze-formats skill. */
|
|
4
4
|
export interface CruzeConfig {
|
|
5
|
-
version:
|
|
5
|
+
version: 2;
|
|
6
6
|
/** The Cruze version whose skills the project installed, recorded by `cruze install`. */
|
|
7
7
|
cruze?: string;
|
|
8
|
-
/** The Cruze version whose expectations the code meets, moved forward by `realign`. */
|
|
9
|
-
standards?: string;
|
|
10
8
|
project: string;
|
|
11
|
-
tracker: "markdown";
|
|
12
9
|
source: string[];
|
|
13
10
|
tests: string[];
|
|
14
11
|
check: {
|
|
@@ -17,35 +14,13 @@ export interface CruzeConfig {
|
|
|
17
14
|
exceptions: Array<{ path: string; reason: string }>;
|
|
18
15
|
};
|
|
19
16
|
layers: Layer[];
|
|
20
|
-
/** Named project commands, such as `test: cargo test`, shown in AGENTS.md. */
|
|
21
|
-
commands: Record<string, string>;
|
|
22
17
|
/**
|
|
23
18
|
* How `cruze review` starts an agent in a fresh context: the program and its arguments.
|
|
24
19
|
* Without one, the calling agent runs the role's prompt in a helper of its own.
|
|
25
20
|
*/
|
|
26
|
-
review: { command?: string[]
|
|
27
|
-
/** The size band of a change, so each one is worth its own plan, review and verify. */
|
|
28
|
-
changes: ChangeLimits;
|
|
29
|
-
/** External documents the design must comply with, such as a protocol specification, each with what it governs. */
|
|
30
|
-
references: Array<{ path: string; use: string }>;
|
|
21
|
+
review: { command?: string[] };
|
|
31
22
|
}
|
|
32
23
|
|
|
33
|
-
/** Which review findings the user decides: only the exceptions, or every one. */
|
|
34
|
-
export type ReviewDecide = "exceptions" | "all";
|
|
35
|
-
|
|
36
|
-
export interface ChangeLimits {
|
|
37
|
-
/** A feature's change building fewer elements than this merges into a neighbour. */
|
|
38
|
-
minBuilds: number;
|
|
39
|
-
/** A change delivering more scenarios than this splits. */
|
|
40
|
-
maxScenarios: number;
|
|
41
|
-
/** A plan with more tasks than this splits. */
|
|
42
|
-
maxTasks: number;
|
|
43
|
-
/** Features and changes the user accepted outside the band, by ref, each with the reason. */
|
|
44
|
-
exceptions: Array<{ item: string; reason: string }>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const DEFAULT_CHANGE_LIMITS: ChangeLimits = { minBuilds: 3, maxScenarios: 30, maxTasks: 25, exceptions: [] };
|
|
48
|
-
|
|
49
24
|
export interface Layer {
|
|
50
25
|
name: string;
|
|
51
26
|
paths: string[];
|
|
@@ -62,17 +37,14 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
62
37
|
} catch (error) {
|
|
63
38
|
return { config: null, problems: [`not valid YAML: ${error instanceof Error ? error.message : String(error)}`] };
|
|
64
39
|
}
|
|
65
|
-
const problems: string[] = [];
|
|
66
40
|
const root = asRecord(raw);
|
|
67
41
|
if (root === null) return { config: null, problems: ["the file must be a YAML mapping"] };
|
|
68
|
-
|
|
69
|
-
|
|
42
|
+
if (root["version"] === 1) return { config: null, problems: ["this config was created by an older Cruze, whose documents this release doesn't read; start the project's Cruze documents again with cruze init in a fresh checkout"] };
|
|
43
|
+
const problems: string[] = [];
|
|
44
|
+
if (root["version"] !== 2) problems.push("version must be 2");
|
|
70
45
|
const cruze = root["cruze"];
|
|
71
|
-
if (cruze !== undefined && (typeof cruze !== "string" || cruze.trim() === "")) problems.push("cruze must be a version string, such as \"0.0.
|
|
72
|
-
const standards = root["standards"];
|
|
73
|
-
if (standards !== undefined && (typeof standards !== "string" || standards.trim() === "")) problems.push("standards must be a version string, such as \"0.0.2\"");
|
|
46
|
+
if (cruze !== undefined && (typeof cruze !== "string" || cruze.trim() === "")) problems.push("cruze must be a version string, such as \"0.0.14\"");
|
|
74
47
|
if (typeof root["project"] !== "string" || root["project"] === "") problems.push("project must be a non-empty string");
|
|
75
|
-
if (root["tracker"] !== "markdown") problems.push("tracker must be markdown");
|
|
76
48
|
const source = stringList(root["source"], "source", problems);
|
|
77
49
|
const tests = stringList(root["tests"], "tests", problems);
|
|
78
50
|
|
|
@@ -104,71 +76,23 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
104
76
|
}
|
|
105
77
|
}
|
|
106
78
|
|
|
107
|
-
const commands: Record<string, string> = {};
|
|
108
|
-
const rawCommands = root["commands"];
|
|
109
|
-
if (rawCommands !== undefined && rawCommands !== null) {
|
|
110
|
-
const record = asRecord(rawCommands);
|
|
111
|
-
if (record === null) problems.push("commands must be a mapping of names to commands");
|
|
112
|
-
for (const [name, command] of Object.entries(record ?? {})) {
|
|
113
|
-
if (typeof command === "string" && command.trim() !== "") commands[name] = command;
|
|
114
|
-
else problems.push(`commands.${name} must be a command string`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
79
|
const review = asRecord(root["review"]) ?? {};
|
|
118
80
|
const reviewCommand = review["command"] === undefined ? undefined : stringList(review["command"], "review.command", problems);
|
|
119
81
|
if (reviewCommand?.length === 0) problems.push("review.command needs at least the program to run");
|
|
120
|
-
const decide = review["decide"] ?? "exceptions";
|
|
121
|
-
if (decide !== "exceptions" && decide !== "all") problems.push("review.decide must be exceptions or all");
|
|
122
|
-
const limits = asRecord(root["changes"]) ?? {};
|
|
123
|
-
const changes: ChangeLimits = {
|
|
124
|
-
minBuilds: positiveInt(limits["min_builds"], "changes.min_builds", problems) ?? DEFAULT_CHANGE_LIMITS.minBuilds,
|
|
125
|
-
maxScenarios: positiveInt(limits["max_scenarios"], "changes.max_scenarios", problems) ?? DEFAULT_CHANGE_LIMITS.maxScenarios,
|
|
126
|
-
maxTasks: positiveInt(limits["max_tasks"], "changes.max_tasks", problems) ?? DEFAULT_CHANGE_LIMITS.maxTasks,
|
|
127
|
-
exceptions: (Array.isArray(limits["exceptions"]) ? limits["exceptions"] : []).flatMap((entry: unknown, i: number) => {
|
|
128
|
-
const record = asRecord(entry);
|
|
129
|
-
if (typeof record?.["item"] !== "string" || typeof record["reason"] !== "string" || record["reason"].trim() === "") {
|
|
130
|
-
problems.push(`changes.exceptions[${i}] needs an item and a non-empty reason`);
|
|
131
|
-
return [];
|
|
132
|
-
}
|
|
133
|
-
return [{ item: record["item"], reason: record["reason"] }];
|
|
134
|
-
}),
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const references = (Array.isArray(root["references"]) ? root["references"] : root["references"] === undefined ? [] : [null]).flatMap((entry: unknown, i: number) => {
|
|
138
|
-
const record = asRecord(entry);
|
|
139
|
-
if (typeof record?.["path"] !== "string" || record["path"].trim() === "" || typeof record["use"] !== "string" || record["use"].trim() === "") {
|
|
140
|
-
problems.push(`references[${i}] needs a path and a non-empty use`);
|
|
141
|
-
return [];
|
|
142
|
-
}
|
|
143
|
-
return [{ path: record["path"], use: record["use"] }];
|
|
144
|
-
});
|
|
145
82
|
|
|
146
83
|
const config: CruzeConfig = {
|
|
147
|
-
version:
|
|
84
|
+
version: 2,
|
|
148
85
|
...(typeof cruze === "string" && cruze.trim() !== "" ? { cruze } : {}),
|
|
149
|
-
...(typeof standards === "string" && standards.trim() !== "" ? { standards } : {}),
|
|
150
86
|
project: typeof root["project"] === "string" ? root["project"] : "",
|
|
151
|
-
tracker: "markdown",
|
|
152
87
|
source,
|
|
153
88
|
tests,
|
|
154
|
-
check: { maxLines: maxLines ??
|
|
89
|
+
check: { maxLines: maxLines ?? 600, maxTypes: maxTypes ?? 12, exceptions },
|
|
155
90
|
layers,
|
|
156
|
-
|
|
157
|
-
review: {
|
|
158
|
-
...(reviewCommand === undefined || reviewCommand.length === 0 ? {} : { command: reviewCommand }),
|
|
159
|
-
decide: decide === "all" ? "all" : "exceptions",
|
|
160
|
-
},
|
|
161
|
-
changes,
|
|
162
|
-
references,
|
|
91
|
+
review: reviewCommand === undefined || reviewCommand.length === 0 ? {} : { command: reviewCommand },
|
|
163
92
|
};
|
|
164
93
|
return { config, problems };
|
|
165
94
|
}
|
|
166
95
|
|
|
167
|
-
/** The first layer whose paths match the file, as the formats define. */
|
|
168
|
-
export function layerOf(config: CruzeConfig, path: string, matches: (path: string, globs: string[]) => boolean): Layer | undefined {
|
|
169
|
-
return config.layers.find((layer) => matches(path, layer.paths));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
96
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
173
97
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
|
|
174
98
|
}
|