@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
package/dist/domain/hashing.js
CHANGED
|
@@ -1,43 +1,29 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { findSection, } from "./markdown.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
* Hashes cover design content only.
|
|
7
|
-
*
|
|
5
|
+
* Hashes cover design content only. A feature's `- Status:` line is bookkeeping, so marking a
|
|
6
|
+
* feature built never makes its approved design look edited.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
return `sha256:${createHash("sha256").update(text).digest("hex")}`;
|
|
12
|
-
}
|
|
9
|
+
const STATUS_LINE = /^- Status: \S+\s*$/;
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const kept = doc.lines.filter((line, i) => !doc.inManaged[i] && (doc.inFence[i] || !isStatusLine(line)));
|
|
17
|
-
return sha256(normalize(kept));
|
|
11
|
+
export function isStatusLine(line ) {
|
|
12
|
+
return STATUS_LINE.test(line);
|
|
18
13
|
}
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* relative to the element and a delta operation prefix is dropped, so an element
|
|
23
|
-
* hashes the same in a delta and after it is merged into a living document.
|
|
24
|
-
*/
|
|
25
|
-
export function elementHash(doc , element ) {
|
|
26
|
-
if (element.form === "row") return sha256(normalize(elementLines(doc, element)));
|
|
27
|
-
const kept = [];
|
|
28
|
-
for (let i = element.start; i < element.end; i++) {
|
|
29
|
-
const line = doc.lines[i] ?? "";
|
|
30
|
-
if (doc.inManaged[i] || (!doc.inFence[i] && isStatusLine(line))) continue;
|
|
31
|
-
kept.push(doc.inFence[i] ? line : relativeHeading(line, element.level, i === element.start));
|
|
32
|
-
}
|
|
33
|
-
return sha256(normalize(kept));
|
|
15
|
+
export function sha256(text ) {
|
|
16
|
+
return `sha256:${createHash("sha256").update(text).digest("hex")}`;
|
|
34
17
|
}
|
|
35
18
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
19
|
+
/** Hash of a document's design content, leaving out the `##` sections named. */
|
|
20
|
+
export function designHash(doc , leftOut = []) {
|
|
21
|
+
const skipped = leftOut.flatMap((title) => {
|
|
22
|
+
const section = findSection(doc, title);
|
|
23
|
+
return section === null ? [] : [[section.heading.line, section.end] ];
|
|
24
|
+
});
|
|
25
|
+
const kept = doc.lines.filter((line, i) => !skipped.some(([start, end]) => i >= start && i < end) && (doc.inFence[i] || !isStatusLine(line)));
|
|
26
|
+
return sha256(normalize(kept));
|
|
41
27
|
}
|
|
42
28
|
|
|
43
29
|
function normalize(lines ) {
|
package/dist/domain/ids.js
CHANGED
|
@@ -1,39 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
export const KINDS = ["GOAL", "PRINCIPLE", "LATER", "CTX", "ENT", "UC", "PORT", "ADP", "FLOW", "MOD", "RULE", "VIEW", "XC", "REQ", "SCN", "ADR"] ;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/** Kinds of the vision's row elements: goals, the principles every design follows, and what must stay possible later. */
|
|
7
|
-
export const VISION_KINDS = ["GOAL", "PRINCIPLE", "LATER"];
|
|
8
|
-
/** Kinds that describe code, and so carry a managed planned/built status. */
|
|
9
|
-
export const CODE_KINDS = ["ENT", "UC", "PORT", "ADP", "FLOW", "MOD"];
|
|
10
|
-
/** Kinds whose elements carry a managed status: code kinds and scenarios. */
|
|
11
|
-
export const STATUS_KINDS = [...CODE_KINDS, "SCN"];
|
|
12
|
-
/** Kinds that must name their context or capability as a scope. */
|
|
13
|
-
export const SCOPED_KINDS = ["ENT", "UC", "PORT", "ADP", "FLOW", "REQ", "SCN"];
|
|
1
|
+
/** Scenario IDs: `SCN-<feature>.<name>`, lowercase words joined by single hyphens, named and never reused. */
|
|
14
2
|
|
|
15
3
|
const WORD = "[a-z0-9]+(?:-[a-z0-9]+)*";
|
|
16
|
-
export const
|
|
17
|
-
|
|
18
|
-
const EXACT = new RegExp(`^${
|
|
19
|
-
const ANYWHERE = new RegExp(`(?<![A-Za-z0-9_-])${
|
|
4
|
+
export const SLUG = new RegExp(`^${WORD}$`);
|
|
5
|
+
const SCENARIO = `SCN-${WORD}\\.${WORD}`;
|
|
6
|
+
const EXACT = new RegExp(`^${SCENARIO}$`);
|
|
7
|
+
const ANYWHERE = new RegExp(`(?<![A-Za-z0-9_-])${SCENARIO}(?![A-Za-z0-9_.-])`, "g");
|
|
20
8
|
|
|
21
|
-
export function
|
|
9
|
+
export function isScenarioId(text ) {
|
|
22
10
|
return EXACT.test(text);
|
|
23
11
|
}
|
|
24
12
|
|
|
25
|
-
export function
|
|
26
|
-
return
|
|
13
|
+
export function isSlug(text ) {
|
|
14
|
+
return SLUG.test(text);
|
|
27
15
|
}
|
|
28
16
|
|
|
29
|
-
/** The
|
|
30
|
-
export function
|
|
31
|
-
|
|
32
|
-
const dot = rest.indexOf(".");
|
|
33
|
-
return dot === -1 ? undefined : rest.slice(0, dot);
|
|
17
|
+
/** The feature a scenario belongs to. */
|
|
18
|
+
export function featureOfScenario(id ) {
|
|
19
|
+
return id.slice(4, id.indexOf("."));
|
|
34
20
|
}
|
|
35
21
|
|
|
36
|
-
/** Every ID mentioned in the text, once each, in order of first mention. */
|
|
37
|
-
export function
|
|
22
|
+
/** Every scenario ID mentioned in the text, once each, in order of first mention. */
|
|
23
|
+
export function findScenarioIds(text ) {
|
|
38
24
|
return [...new Set(text.match(ANYWHERE) ?? [])];
|
|
39
25
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { approvalState } from "./approvals.js";
|
|
2
|
+
import { PATHS } from "./project/layout.js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The step to run next, from the documents alone. Work in progress comes first, in release order,
|
|
16
|
+
* then the first release feature not yet shaped whose needs are all built.
|
|
17
|
+
*/
|
|
18
|
+
export function nextStep(docs ) {
|
|
19
|
+
if (docs.config === undefined) return { step: "init", reason: `${PATHS.config} is missing` };
|
|
20
|
+
if (docs.vision === undefined) return { step: "envision", reason: "the project has no vision yet" };
|
|
21
|
+
if (approvalState(docs.approvals, PATHS.vision, docs.vision) !== "approved") return { step: "envision", reason: "the vision isn't approved as it stands" };
|
|
22
|
+
if (docs.architecture === undefined) return { step: "envision", reason: "the project has no architecture map yet" };
|
|
23
|
+
if (approvalState(docs.approvals, PATHS.architecture, docs.architecture) !== "approved") return { step: "envision", reason: "the architecture map isn't approved as it stands" };
|
|
24
|
+
|
|
25
|
+
const order = [...docs.release.map((r) => r.slug), ...docs.features.map((f) => f.slug).filter((slug) => !docs.release.some((r) => r.slug === slug))];
|
|
26
|
+
for (const slug of order) {
|
|
27
|
+
const feature = docs.features.find((f) => f.slug === slug);
|
|
28
|
+
if (feature === undefined || feature.status === "built") continue;
|
|
29
|
+
const state = approvalState(docs.approvals, feature.path, feature.doc);
|
|
30
|
+
if (state === "unapproved") return { step: "shape", target: slug, reason: `${slug} is shaped but not approved` };
|
|
31
|
+
if (state === "edited") return { step: "shape", target: slug, reason: `${slug} changed since its approval; show the user what changed and approve it again` };
|
|
32
|
+
return { step: "build", target: slug, reason: `${slug} is approved and not built` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const built = new Set(docs.features.filter((f) => f.status === "built").map((f) => f.slug));
|
|
36
|
+
const pending = docs.release.filter((r) => !built.has(r.slug));
|
|
37
|
+
const ready = pending.find((r) => !docs.features.some((f) => f.slug === r.slug) && r.needs.every((need) => built.has(need)));
|
|
38
|
+
if (ready !== undefined) return { step: "shape", target: ready.slug, reason: ready.needs.length === 0 ? `${ready.slug} is next in the release` : `${ready.slug} is next in the release, and everything it needs is built` };
|
|
39
|
+
if (pending.length > 0) return { step: "envision", reason: `no remaining release feature has all its needs built (${pending.map((r) => r.slug).join(", ")}); fix the Needs column` };
|
|
40
|
+
return { step: "envision", reason: docs.release.length === 0 ? "the vision's Release table lists no features" : "every release feature is built; plan the next release" };
|
|
41
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isStatusLine } from "../hashing.js";
|
|
2
|
+
import { findScenarioIds, isScenarioId } from "../ids.js";
|
|
3
|
+
import { findSection, parseMarkdown, } from "../markdown.js";
|
|
4
|
+
import { featurePath } from "./layout.js";
|
|
5
|
+
|
|
6
|
+
/** A feature document, docs/features/<slug>.md: one slice of behaviour, its public API and its stack sketch. */
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export const FEATURE_STATUSES = ["shaped", "built"];
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export const FEATURE_SECTIONS = ["Intent", "Behaviour", "Public API", "Stack sketch", "Decisions"] ;
|
|
29
|
+
|
|
30
|
+
export function readFeature(slug , text ) {
|
|
31
|
+
const doc = parseMarkdown(text);
|
|
32
|
+
return { slug, path: featurePath(slug), text, doc, status: statusOf(doc), scenarios: scenariosOf(doc) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The feature's `- Status:` line, the first one outside code. */
|
|
36
|
+
export function statusLine(doc ) {
|
|
37
|
+
const index = doc.lines.findIndex((line, i) => !doc.inFence[i] && isStatusLine(line));
|
|
38
|
+
if (index === -1) return undefined;
|
|
39
|
+
return { line: index, value: (doc.lines[index] ?? "").replace("- Status:", "").trim() };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function statusOf(doc ) {
|
|
43
|
+
const value = statusLine(doc)?.value;
|
|
44
|
+
return (FEATURE_STATUSES ).includes(value ?? "") ? (value ) : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Scenario headings under `## Behaviour`: any heading whose text starts with a scenario ID and a colon. */
|
|
48
|
+
function scenariosOf(doc ) {
|
|
49
|
+
const section = findSection(doc, "Behaviour");
|
|
50
|
+
if (section === null) return [];
|
|
51
|
+
return doc.headings
|
|
52
|
+
.filter((h) => h.line >= section.start && h.line < section.end)
|
|
53
|
+
.flatMap((h) => {
|
|
54
|
+
const match = /^(\S+): (.+)$/.exec(h.text);
|
|
55
|
+
if (match?.[1] === undefined || match[2] === undefined || !isScenarioId(match[1])) return [];
|
|
56
|
+
return [{ id: match[1], title: match[2], line: h.line }];
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Scenario-like IDs under `## Behaviour` that aren't a proper scenario heading, for validate to report. */
|
|
61
|
+
export function strayScenarioIds(feature ) {
|
|
62
|
+
const section = findSection(feature.doc, "Behaviour");
|
|
63
|
+
if (section === null) return [];
|
|
64
|
+
const headed = new Set(feature.scenarios.map((s) => s.line));
|
|
65
|
+
const stray = [];
|
|
66
|
+
for (let i = section.start; i < section.end; i++) {
|
|
67
|
+
if (headed.has(i) || feature.doc.inFence[i] || !/^#{1,6} /.test(feature.doc.lines[i] ?? "")) continue;
|
|
68
|
+
for (const id of findScenarioIds(feature.doc.lines[i] ?? "")) stray.push({ id, line: i });
|
|
69
|
+
}
|
|
70
|
+
return stray;
|
|
71
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Where Cruze documents live in a project
|
|
1
|
+
/** Where Cruze documents live in a project. */
|
|
2
2
|
|
|
3
3
|
/** Project files by path, relative to the project root, with forward slashes. */
|
|
4
4
|
|
|
@@ -6,36 +6,20 @@
|
|
|
6
6
|
export const PATHS = {
|
|
7
7
|
readme: "README.md",
|
|
8
8
|
vision: "docs/vision.md",
|
|
9
|
-
glossary: "docs/glossary.md",
|
|
10
9
|
architecture: "docs/architecture.md",
|
|
11
|
-
|
|
12
|
-
adrDir: "docs/adr",
|
|
13
|
-
specsDir: "docs/specs",
|
|
10
|
+
featuresDir: "docs/features",
|
|
14
11
|
config: ".cruze/config.yaml",
|
|
15
|
-
|
|
16
|
-
changesDir: ".cruze/changes",
|
|
17
|
-
archiveDir: ".cruze/archive",
|
|
18
|
-
notesDir: ".cruze/notes",
|
|
19
|
-
projectApprovals: ".cruze/approvals.json",
|
|
20
|
-
projectJournal: ".cruze/journal.jsonl",
|
|
12
|
+
approvals: ".cruze/approvals.json",
|
|
21
13
|
} ;
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export function specPath(capability ) {
|
|
27
|
-
return `${PATHS.specsDir}/${capability}.md`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function filesUnder(snapshot , dir ) {
|
|
31
|
-
const prefix = `${dir}/`;
|
|
32
|
-
return [...snapshot.keys()].filter((path) => path.startsWith(prefix)).sort();
|
|
15
|
+
export function featurePath(slug ) {
|
|
16
|
+
return `${PATHS.featuresDir}/${slug}.md`;
|
|
33
17
|
}
|
|
34
18
|
|
|
35
|
-
/**
|
|
36
|
-
export function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
19
|
+
/** The feature slug of a path directly under docs/features/, or undefined. */
|
|
20
|
+
export function slugOfFeaturePath(path ) {
|
|
21
|
+
const prefix = `${PATHS.featuresDir}/`;
|
|
22
|
+
if (!path.startsWith(prefix) || !path.endsWith(".md")) return undefined;
|
|
23
|
+
const rest = path.slice(prefix.length, -3);
|
|
24
|
+
return rest.includes("/") ? undefined : rest;
|
|
41
25
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { parseApprovals, } from "../approvals.js";
|
|
2
|
+
import { parseConfig, } from "../config.js";
|
|
3
|
+
import { parseMarkdown, } from "../markdown.js";
|
|
4
|
+
import { readFeature, } from "./features.js";
|
|
5
|
+
import { PATHS, slugOfFeaturePath, } from "./layout.js";
|
|
6
|
+
import { readRelease, } from "./release.js";
|
|
7
|
+
|
|
8
|
+
/** Every Cruze document of a project, parsed: what the use cases work on. */
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export function buildProjectDocs(snapshot ) {
|
|
21
|
+
const read = (path ) => {
|
|
22
|
+
const text = snapshot.get(path);
|
|
23
|
+
return text === undefined ? undefined : parseMarkdown(text);
|
|
24
|
+
};
|
|
25
|
+
const configText = snapshot.get(PATHS.config);
|
|
26
|
+
const vision = read(PATHS.vision);
|
|
27
|
+
const features = [...snapshot.keys()]
|
|
28
|
+
.flatMap((path) => {
|
|
29
|
+
const slug = slugOfFeaturePath(path);
|
|
30
|
+
return slug === undefined ? [] : [readFeature(slug, snapshot.get(path) ?? "")];
|
|
31
|
+
})
|
|
32
|
+
.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
33
|
+
return {
|
|
34
|
+
snapshot,
|
|
35
|
+
config: configText === undefined ? undefined : parseConfig(configText),
|
|
36
|
+
vision,
|
|
37
|
+
architecture: read(PATHS.architecture),
|
|
38
|
+
release: vision === undefined ? [] : readRelease(vision),
|
|
39
|
+
features,
|
|
40
|
+
approvals: parseApprovals(snapshot.get(PATHS.approvals)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The document a reference names: `vision`, `architecture`, a feature slug, or a path. */
|
|
45
|
+
export function resolveDocument(docs , ref ) {
|
|
46
|
+
const path = ref === "vision" ? PATHS.vision : ref === "architecture" ? PATHS.architecture : (docs.features.find((f) => f.slug === ref)?.path ?? ref);
|
|
47
|
+
return docs.snapshot.has(path) ? path : undefined;
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { findSection, } from "../markdown.js";
|
|
2
|
+
import { tablesIn } from "../markdown_tables.js";
|
|
3
|
+
|
|
4
|
+
/** One row of the vision's `## Release` table: a feature of the release, in build order, and the features it needs. */
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/** The release's features, from the first table under `## Release` with Feature and Needs columns. */
|
|
14
|
+
export function readRelease(vision ) {
|
|
15
|
+
const section = findSection(vision, "Release");
|
|
16
|
+
if (section === null) return [];
|
|
17
|
+
const table = tablesIn(vision.lines, vision.inFence, section.start, section.end).find((t) => t.header.includes("Feature") && t.header.includes("Needs"));
|
|
18
|
+
if (table === undefined) return [];
|
|
19
|
+
const at = (name ) => table.header.indexOf(name);
|
|
20
|
+
return table.rows.map(({ cells, line }) => ({
|
|
21
|
+
slug: unquote(cells[at("Feature")] ?? ""),
|
|
22
|
+
summary: at("Summary") === -1 ? "" : (cells[at("Summary")] ?? ""),
|
|
23
|
+
needs: (cells[at("Needs")] ?? "").split(",").map(unquote).filter((need) => need !== "" && !/^[-—–]+$/.test(need)),
|
|
24
|
+
line,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function unquote(cell ) {
|
|
29
|
+
return cell.trim().replace(/^`(.*)`$/, "$1").trim();
|
|
30
|
+
}
|
package/dist/domain/scaffold.js
CHANGED
|
@@ -13,7 +13,6 @@ export const INIT_SCAFFOLD = [
|
|
|
13
13
|
{ path: "AGENTS.md", template: "init/AGENTS.md" },
|
|
14
14
|
{ path: ".cruze/config.yaml", template: "init/config.yaml" },
|
|
15
15
|
{ path: ".github/workflows/ci.yml", template: "init/ci.yml" },
|
|
16
|
-
{ path: ".gitattributes", template: "init/gitattributes" },
|
|
17
16
|
];
|
|
18
17
|
|
|
19
18
|
/**
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
export const MAX_FILE_WORDS = 2100;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* The words agents may read across the whole bundle.
|
|
11
|
-
*
|
|
12
|
-
* is the maintainer's decision, with the reason in the commit that raises it.
|
|
10
|
+
* The words agents may read across the whole bundle. By 0.0.13 the skills had grown to 45,800 words
|
|
11
|
+
* as every fix added rules and nothing was removed; 0.0.14 cut them to this. Lower it when text is
|
|
12
|
+
* cut. Raising it is the maintainer's decision, with the reason in the commit that raises it.
|
|
13
13
|
*/
|
|
14
|
-
export const BUNDLE_WORD_BUDGET =
|
|
14
|
+
export const BUNDLE_WORD_BUDGET = 17725;
|
|
15
15
|
|
|
16
16
|
/** Data the CLI reads or renders, rather than instructions an agent reads. */
|
|
17
17
|
const NOT_INSTRUCTIONS = /(^|\/)(templates|notes)\//;
|
package/dist/domain/trace.js
CHANGED
|
@@ -1,44 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { PATHS } from "./project/layout.js";
|
|
4
|
-
|
|
5
|
-
import { readScope } from "./project/plan_parts.js";
|
|
6
|
-
import { readDelta } from "./project/deltas.js";
|
|
7
|
-
|
|
1
|
+
import { findScenarioIds } from "./ids.js";
|
|
2
|
+
|
|
8
3
|
|
|
9
4
|
|
|
10
5
|
|
|
11
6
|
|
|
12
7
|
|
|
13
|
-
|
|
8
|
+
|
|
14
9
|
|
|
15
10
|
|
|
16
11
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
export function traceScenarios(view , tests , change ) {
|
|
22
|
-
const required = change === undefined ? builtScenarios(view) : (readScope(change.doc)?.delivers ?? []).filter((id) => id.startsWith("SCN-"));
|
|
23
|
-
const known = new Set([...view.living.elements.keys()]);
|
|
24
|
-
for (const item of view.items.filter((i) => !i.archived)) for (const id of readDelta(item.doc).ids) known.add(id);
|
|
25
|
-
|
|
12
|
+
/** Links the required features' scenarios to the tests that carry their IDs. */
|
|
13
|
+
export function traceScenarios(features , required , tests ) {
|
|
14
|
+
const known = new Set(features.flatMap((f) => f.scenarios.map((s) => s.id)));
|
|
26
15
|
const carriers = new Map ();
|
|
27
16
|
const unknown = [];
|
|
28
17
|
for (const [path, text] of tests) {
|
|
29
18
|
text.split("\n").forEach((line, index) => {
|
|
30
|
-
for (const id of
|
|
19
|
+
for (const id of findScenarioIds(line)) {
|
|
31
20
|
carriers.set(id, [...new Set([...(carriers.get(id) ?? []), path])]);
|
|
32
21
|
if (!known.has(id)) unknown.push({ id, path, line: index + 1 });
|
|
33
22
|
}
|
|
34
23
|
});
|
|
35
24
|
}
|
|
36
|
-
const scenarios = required.map((
|
|
25
|
+
const scenarios = required.flatMap((f) => f.scenarios).map((s) => ({ id: s.id, tests: carriers.get(s.id) ?? [] }));
|
|
37
26
|
return { scenarios, missing: scenarios.filter((s) => s.tests.length === 0).map((s) => s.id), unknown };
|
|
38
27
|
}
|
|
39
|
-
|
|
40
|
-
function builtScenarios(view ) {
|
|
41
|
-
return [...view.living.docs]
|
|
42
|
-
.filter(([path]) => path.startsWith(`${PATHS.specsDir}/`))
|
|
43
|
-
.flatMap(([, doc]) => headingElements(doc).filter((e) => e.kind === "SCN" && statusOf(doc, e) === "built").map((e) => e.id));
|
|
44
|
-
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { featureOfScenario } from "../ids.js";
|
|
2
|
+
import { findSection, } from "../markdown.js";
|
|
3
|
+
import { FEATURE_SECTIONS, FEATURE_STATUSES, statusLine, strayScenarioIds, } from "../project/features.js";
|
|
4
|
+
import { PATHS } from "../project/layout.js";
|
|
5
|
+
|
|
6
|
+
import { error, } from "./problem.js";
|
|
7
|
+
|
|
8
|
+
/** Each document's required sections, its scenarios, and leftover template guides. */
|
|
9
|
+
|
|
10
|
+
const VISION_SECTIONS = ["Problem", "Users", "Release"];
|
|
11
|
+
const ARCHITECTURE_SECTIONS = ["Overview", "Ports", "Adapters", "Runtime"];
|
|
12
|
+
|
|
13
|
+
const GUIDE = /(?<!`)<(?!!--|\/|https?:|br\b)[A-Za-z][^<>\n]*>(?!`)/;
|
|
14
|
+
const PLACEHOLDER = /\{\{[a-z_]+\}\}/;
|
|
15
|
+
|
|
16
|
+
export function documentProblems(docs ) {
|
|
17
|
+
const problems = [];
|
|
18
|
+
if (docs.vision !== undefined) problems.push(...shapeOf(PATHS.vision, docs.vision, VISION_SECTIONS));
|
|
19
|
+
if (docs.architecture !== undefined) problems.push(...shapeOf(PATHS.architecture, docs.architecture, ARCHITECTURE_SECTIONS));
|
|
20
|
+
for (const feature of docs.features) {
|
|
21
|
+
problems.push(...shapeOf(feature.path, feature.doc, [...FEATURE_SECTIONS]), ...featureProblems(feature));
|
|
22
|
+
}
|
|
23
|
+
problems.push(...duplicateScenarios(docs.features));
|
|
24
|
+
return problems;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shapeOf(path , doc , sections ) {
|
|
28
|
+
const problems = sections.filter((title) => findSection(doc, title) === null).map((title) => error(path, undefined, "missing-section", `needs a ## ${title} section`));
|
|
29
|
+
doc.lines.forEach((line, i) => {
|
|
30
|
+
if (doc.inFence[i]) return;
|
|
31
|
+
const withoutCode = line.replace(/`[^`]*`/g, "");
|
|
32
|
+
if (GUIDE.test(withoutCode) || PLACEHOLDER.test(withoutCode)) problems.push(error(path, i, "template-leftover", "replace the template guide or placeholder on this line"));
|
|
33
|
+
});
|
|
34
|
+
return problems;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function featureProblems(feature ) {
|
|
38
|
+
const problems = [];
|
|
39
|
+
const status = statusLine(feature.doc);
|
|
40
|
+
if (status === undefined) problems.push(error(feature.path, undefined, "status", `needs a "- Status: shaped" line under the title`));
|
|
41
|
+
else if (feature.status === undefined) problems.push(error(feature.path, status.line, "status", `the status is one of ${FEATURE_STATUSES.join(" or ")}`));
|
|
42
|
+
if (feature.scenarios.length === 0 && findSection(feature.doc, "Behaviour") !== null) {
|
|
43
|
+
problems.push(error(feature.path, undefined, "no-scenarios", `## Behaviour needs at least one ### SCN-${feature.slug}.<name>: <title> scenario`));
|
|
44
|
+
}
|
|
45
|
+
for (const scenario of feature.scenarios) {
|
|
46
|
+
if (featureOfScenario(scenario.id) !== feature.slug) problems.push(error(feature.path, scenario.line, "scenario-id", `${scenario.id} belongs to this feature, so it starts SCN-${feature.slug}.`));
|
|
47
|
+
}
|
|
48
|
+
for (const stray of strayScenarioIds(feature)) {
|
|
49
|
+
problems.push(error(feature.path, stray.line, "scenario-heading", `a scenario heading reads exactly "${stray.id}: <title>"`));
|
|
50
|
+
}
|
|
51
|
+
return problems;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function duplicateScenarios(features ) {
|
|
55
|
+
const seen = new Map ();
|
|
56
|
+
const problems = [];
|
|
57
|
+
for (const feature of features) {
|
|
58
|
+
for (const scenario of feature.scenarios) {
|
|
59
|
+
const first = seen.get(scenario.id);
|
|
60
|
+
if (first !== undefined) problems.push(error(feature.path, scenario.line, "duplicate-scenario", `${scenario.id} is already defined in ${first}`));
|
|
61
|
+
else seen.set(scenario.id, feature.path);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return problems;
|
|
65
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isSlug } from "../ids.js";
|
|
2
|
+
import { PATHS } from "../project/layout.js";
|
|
3
|
+
|
|
4
|
+
import { error, warning, } from "./problem.js";
|
|
5
|
+
|
|
6
|
+
/** The vision's Release table: feature slugs, what each needs, and no cycles. */
|
|
7
|
+
export function releaseProblems(docs ) {
|
|
8
|
+
if (docs.vision === undefined) return [];
|
|
9
|
+
if (docs.release.length === 0) return [warning(PATHS.vision, undefined, "release", "## Release has no Feature | Summary | Needs table with features in it yet")];
|
|
10
|
+
const problems = [];
|
|
11
|
+
const slugs = new Set ();
|
|
12
|
+
for (const row of docs.release) {
|
|
13
|
+
if (!isSlug(row.slug)) problems.push(error(PATHS.vision, row.line, "release", `"${row.slug}" must be a feature slug: lowercase words joined by single hyphens`));
|
|
14
|
+
if (slugs.has(row.slug)) problems.push(error(PATHS.vision, row.line, "release", `${row.slug} is listed twice`));
|
|
15
|
+
slugs.add(row.slug);
|
|
16
|
+
}
|
|
17
|
+
for (const row of docs.release) {
|
|
18
|
+
for (const need of row.needs.filter((n) => !slugs.has(n))) {
|
|
19
|
+
problems.push(error(PATHS.vision, row.line, "release-needs", `${row.slug} needs ${need}, which isn't a feature of this release`));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const cycle = findCycle(docs.release.map((r) => [r.slug, r.needs.filter((n) => slugs.has(n))] ));
|
|
23
|
+
if (cycle !== undefined) problems.push(error(PATHS.vision, docs.release.find((r) => r.slug === cycle[0])?.line, "release-needs", `features need each other in a circle: ${cycle.join(" → ")}`));
|
|
24
|
+
return problems;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findCycle(graph ) {
|
|
28
|
+
const edges = new Map(graph);
|
|
29
|
+
const done = new Set ();
|
|
30
|
+
const visit = (node , path ) => {
|
|
31
|
+
if (path.includes(node)) return [...path.slice(path.indexOf(node)), node];
|
|
32
|
+
if (done.has(node)) return undefined;
|
|
33
|
+
for (const next of edges.get(node) ?? []) {
|
|
34
|
+
const found = visit(next, [...path, node]);
|
|
35
|
+
if (found !== undefined) return found;
|
|
36
|
+
}
|
|
37
|
+
done.add(node);
|
|
38
|
+
return undefined;
|
|
39
|
+
};
|
|
40
|
+
for (const [node] of graph) {
|
|
41
|
+
const found = visit(node, []);
|
|
42
|
+
if (found !== undefined) return found;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
import { PATHS } from "../project/layout.js";
|
|
3
|
+
|
|
4
|
+
import { warning, } from "./problem.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Warns when a feature is built but `review.command` is still the one `cruze init`
|
|
8
|
+
* wrote: the foundation never added the project's test commands, so the code
|
|
9
|
+
* reviewer can only read the code. `initSetting` is the review template init renders.
|
|
10
|
+
*/
|
|
11
|
+
export function reviewCommandProblems(docs , initSetting ) {
|
|
12
|
+
const command = docs.config?.config?.review.command;
|
|
13
|
+
if (command === undefined || !docs.features.some((feature) => feature.status === "built")) return [];
|
|
14
|
+
if (JSON.stringify(command) !== JSON.stringify(initCommand(initSetting))) return [];
|
|
15
|
+
return [warning(PATHS.config, undefined, "review-tests", "review.command is still the one cruze init wrote, so the code reviewer can't run the project's tests; add its test and lint commands as .agents/skills/cruze-formats/config.md shows")];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function initCommand(setting ) {
|
|
19
|
+
const parsed = parseYaml(setting);
|
|
20
|
+
return (parsed )?.review?.command;
|
|
21
|
+
}
|
|
@@ -1,36 +1,29 @@
|
|
|
1
|
+
import { PATHS } from "../project/layout.js";
|
|
1
2
|
|
|
2
|
-
import {
|
|
3
|
-
import { livingProblems } from "./living_rules.js";
|
|
3
|
+
import { documentProblems } from "./document_rules.js";
|
|
4
4
|
import { error, sortProblems, } from "./problem.js";
|
|
5
|
-
import {
|
|
6
|
-
import { visionProblems } from "./vision_rules.js";
|
|
7
|
-
import { workItemProblems } from "./work_item_rules.js";
|
|
5
|
+
import { releaseProblems } from "./release_rules.js";
|
|
8
6
|
|
|
9
7
|
/** Every rule in the cruze-formats skill, applied to the whole project. */
|
|
10
|
-
export function validateProject(
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
);
|
|
15
|
-
return sortProblems([
|
|
16
|
-
...configProblems(view),
|
|
17
|
-
...livingProblems(view),
|
|
18
|
-
...visionProblems(view),
|
|
19
|
-
...futureProblems(view),
|
|
20
|
-
...roadmapProblems(view),
|
|
21
|
-
...workItemProblems(view),
|
|
22
|
-
...templateLeftovers(view),
|
|
23
|
-
...readmeProblems(view),
|
|
24
|
-
...approvalProblems,
|
|
25
|
-
...journalProblems,
|
|
26
|
-
]);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Problems that concern one document. */
|
|
30
|
-
export function problemsFor(problems , path ) {
|
|
31
|
-
return problems.filter((problem) => problem.path === path);
|
|
8
|
+
export function validateProject(docs ) {
|
|
9
|
+
const config = docs.config === undefined
|
|
10
|
+
? [error(PATHS.config, undefined, "config", "the project has no config; run cruze init")]
|
|
11
|
+
: docs.config.problems.map((message) => error(PATHS.config, undefined, "config", message));
|
|
12
|
+
const approvals = approvalsFileProblems(docs.snapshot.get(PATHS.approvals));
|
|
13
|
+
return sortProblems([...config, ...approvals, ...documentProblems(docs), ...releaseProblems(docs)]);
|
|
32
14
|
}
|
|
33
15
|
|
|
34
16
|
export function hasErrors(problems ) {
|
|
35
17
|
return problems.some((problem) => problem.severity === "error");
|
|
36
18
|
}
|
|
19
|
+
|
|
20
|
+
function approvalsFileProblems(text ) {
|
|
21
|
+
if (text === undefined) return [];
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(text);
|
|
24
|
+
if (!Array.isArray((parsed ).approvals)) return [error(PATHS.approvals, undefined, "approvals-file", "needs an approvals list; only cruze approve writes this file")];
|
|
25
|
+
return [];
|
|
26
|
+
} catch {
|
|
27
|
+
return [error(PATHS.approvals, undefined, "approvals-file", "not valid JSON; only cruze approve writes this file")];
|
|
28
|
+
}
|
|
29
|
+
}
|