@printpractical/cruze 0.0.13 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -118
- package/dist/adapters/inbound/cli/cli_context.js +0 -27
- package/dist/adapters/inbound/cli/commands/code.js +5 -18
- package/dist/adapters/inbound/cli/commands/documents.js +11 -37
- package/dist/adapters/inbound/cli/commands/init.js +1 -1
- package/dist/adapters/inbound/cli/commands/review.js +2 -9
- package/dist/adapters/inbound/cli/commands/status.js +16 -63
- package/dist/adapters/inbound/cli/commands.js +10 -37
- package/dist/adapters/inbound/cli/parse_command.js +0 -54
- package/dist/adapters/outbound/git_repository.js +2 -71
- package/dist/app/ports/repository.js +0 -12
- package/dist/app/project_context.js +15 -25
- package/dist/app/use_cases/approve_document.js +29 -0
- package/dist/app/use_cases/check_code.js +21 -56
- package/dist/app/use_cases/new_document.js +25 -0
- package/dist/app/use_cases/project_status.js +43 -67
- package/dist/app/use_cases/run_review.js +11 -30
- package/dist/app/use_cases/validate_project.js +4 -4
- package/dist/domain/approvals.js +51 -0
- package/dist/domain/check/check_source.js +2 -31
- package/dist/domain/config.js +8 -84
- package/dist/domain/hashing.js +16 -30
- package/dist/domain/ids.js +13 -27
- package/dist/domain/next_step.js +41 -0
- package/dist/domain/project/features.js +71 -0
- package/dist/domain/project/layout.js +11 -27
- package/dist/domain/project/project_docs.js +48 -0
- package/dist/domain/project/release.js +30 -0
- package/dist/domain/scaffold.js +0 -1
- package/dist/domain/skill_budget.js +4 -4
- package/dist/domain/trace.js +8 -25
- package/dist/domain/validation/document_rules.js +70 -0
- package/dist/domain/validation/release_rules.js +45 -0
- package/dist/domain/validation/validate_project.js +20 -27
- package/dist/domain/versions/cruze_version.js +3 -9
- package/dist/main.js +1 -1
- package/package.json +1 -1
- package/skills/asking/SKILL.md +51 -0
- package/skills/behavioural-testing/SKILL.md +14 -16
- package/skills/behavioural-testing/examples.md +1 -2
- package/skills/behavioural-testing/fakes.md +2 -2
- package/skills/build/SKILL.md +34 -38
- package/skills/dependency-approval/SKILL.md +34 -31
- package/skills/envision/SKILL.md +23 -18
- package/skills/envision/architecture.md +16 -0
- package/skills/envision/breakdown.md +14 -0
- package/skills/envision/possibilities.md +6 -7
- package/skills/fix/SKILL.md +34 -0
- package/skills/formats/SKILL.md +39 -87
- package/skills/formats/config.md +47 -0
- package/skills/formats/templates/architecture.md +18 -74
- package/skills/formats/templates/feature.md +18 -34
- package/skills/formats/templates/vision.md +15 -27
- package/skills/hexagonal-design/SKILL.md +11 -11
- package/skills/hexagonal-design/contracts.md +3 -3
- package/skills/hexagonal-design/fundamentals.md +1 -1
- package/skills/hexagonal-design/languages/cpp.md +1 -1
- package/skills/hexagonal-design/languages/rust.md +1 -1
- package/skills/hexagonal-design/languages/swift.md +2 -2
- package/skills/hexagonal-design/runtime.md +2 -2
- package/skills/research/SKILL.md +4 -4
- package/skills/research/evaluation.md +1 -1
- package/skills/review/SKILL.md +27 -0
- package/skills/roles/SKILL.md +8 -59
- package/skills/roles/code-reviewer.md +21 -32
- package/skills/roles/researcher.md +3 -3
- package/skills/shape/SKILL.md +37 -0
- package/skills/shape/self-check.md +12 -0
- package/src/adapters/inbound/cli/cli_context.ts +0 -27
- package/src/adapters/inbound/cli/commands/code.ts +5 -18
- package/src/adapters/inbound/cli/commands/documents.ts +11 -37
- package/src/adapters/inbound/cli/commands/init.ts +1 -1
- package/src/adapters/inbound/cli/commands/review.ts +2 -9
- package/src/adapters/inbound/cli/commands/status.ts +16 -63
- package/src/adapters/inbound/cli/commands.ts +10 -37
- package/src/adapters/inbound/cli/parse_command.ts +0 -54
- package/src/adapters/outbound/git_repository.ts +2 -71
- package/src/app/ports/repository.ts +0 -12
- package/src/app/project_context.ts +15 -25
- package/src/app/use_cases/approve_document.ts +29 -0
- package/src/app/use_cases/check_code.ts +21 -56
- package/src/app/use_cases/new_document.ts +25 -0
- package/src/app/use_cases/project_status.ts +49 -73
- package/src/app/use_cases/run_review.ts +11 -30
- package/src/app/use_cases/validate_project.ts +4 -4
- package/src/domain/approvals.ts +51 -0
- package/src/domain/check/check_source.ts +2 -31
- package/src/domain/config.ts +9 -85
- package/src/domain/hashing.ts +16 -30
- package/src/domain/ids.ts +13 -27
- package/src/domain/next_step.ts +41 -0
- package/src/domain/project/features.ts +71 -0
- package/src/domain/project/layout.ts +11 -27
- package/src/domain/project/project_docs.ts +48 -0
- package/src/domain/project/release.ts +30 -0
- package/src/domain/scaffold.ts +0 -1
- package/src/domain/skill_budget.ts +4 -4
- package/src/domain/trace.ts +8 -25
- package/src/domain/validation/document_rules.ts +70 -0
- package/src/domain/validation/release_rules.ts +45 -0
- package/src/domain/validation/validate_project.ts +21 -28
- package/src/domain/versions/cruze_version.ts +3 -9
- package/src/main.ts +1 -1
- package/templates/init/AGENTS.md +4 -12
- package/templates/init/ci.yml +3 -3
- package/templates/init/config.yaml +8 -17
- package/templates/init/review/claude.yaml +2 -2
- package/dist/adapters/inbound/cli/argument_files.js +0 -29
- package/dist/adapters/inbound/cli/commands/diff.js +0 -23
- package/dist/adapters/inbound/cli/commands/journal.js +0 -32
- package/dist/adapters/inbound/cli/commands/progress.js +0 -55
- package/dist/adapters/inbound/cli/commands/realign.js +0 -28
- package/dist/adapters/inbound/cli/commands/show.js +0 -96
- package/dist/app/use_cases/abandon_work.js +0 -42
- package/dist/app/use_cases/approve_artifact.js +0 -197
- package/dist/app/use_cases/journal_events.js +0 -64
- package/dist/app/use_cases/land_change.js +0 -134
- package/dist/app/use_cases/new_project_doc.js +0 -48
- package/dist/app/use_cases/new_work_item.js +0 -75
- package/dist/app/use_cases/realign_done.js +0 -45
- package/dist/app/use_cases/realign_status.js +0 -106
- package/dist/app/use_cases/record_progress.js +0 -118
- package/dist/app/use_cases/show_elements.js +0 -18
- package/dist/app/use_cases/show_notes.js +0 -11
- package/dist/app/use_cases/show_scope.js +0 -51
- package/dist/app/use_cases/upstream_diff.js +0 -55
- package/dist/domain/approvals/agent_approval.js +0 -68
- package/dist/domain/approvals/evaluate.js +0 -36
- package/dist/domain/approvals/fingerprint.js +0 -52
- package/dist/domain/approvals/rebase.js +0 -39
- package/dist/domain/approvals/records.js +0 -50
- package/dist/domain/approvals/upstream_text.js +0 -36
- package/dist/domain/architecture_sections.js +0 -23
- package/dist/domain/check/decision_comments.js +0 -38
- package/dist/domain/decision_citations.js +0 -62
- package/dist/domain/edits/agents_file.js +0 -58
- package/dist/domain/edits/managed_tables.js +0 -60
- package/dist/domain/edits/progress_block.js +0 -33
- package/dist/domain/edits/status_lines.js +0 -52
- package/dist/domain/elements.js +0 -126
- package/dist/domain/journal.js +0 -109
- package/dist/domain/land/bookkeeping.js +0 -54
- package/dist/domain/land/merge_plan.js +0 -137
- package/dist/domain/land/text_ops.js +0 -66
- package/dist/domain/line_diff.js +0 -56
- package/dist/domain/project/artifact_ref.js +0 -42
- package/dist/domain/project/deltas.js +0 -49
- package/dist/domain/project/element_lookup.js +0 -124
- package/dist/domain/project/living_model.js +0 -55
- package/dist/domain/project/new_items.js +0 -48
- package/dist/domain/project/notes.js +0 -74
- package/dist/domain/project/plan_parts.js +0 -111
- package/dist/domain/project/progress.js +0 -43
- package/dist/domain/project/project_view.js +0 -72
- package/dist/domain/project/roadmap.js +0 -43
- package/dist/domain/project/scope_view.js +0 -134
- package/dist/domain/project/work_items.js +0 -68
- package/dist/domain/status/next_step.js +0 -181
- package/dist/domain/status/pending_rethink.js +0 -22
- package/dist/domain/status/verification.js +0 -29
- package/dist/domain/status/work_status.js +0 -125
- package/dist/domain/validation/decision_rules.js +0 -76
- package/dist/domain/validation/delta_rules.js +0 -81
- package/dist/domain/validation/element_rules.js +0 -104
- package/dist/domain/validation/future_rules.js +0 -40
- package/dist/domain/validation/living_rules.js +0 -99
- package/dist/domain/validation/plan_rules.js +0 -124
- package/dist/domain/validation/project_rules.js +0 -115
- package/dist/domain/validation/size_rules.js +0 -45
- package/dist/domain/validation/vision_rules.js +0 -41
- package/dist/domain/validation/work_item_rules.js +0 -175
- package/dist/domain/versions/realign_notes.js +0 -72
- package/dist/domain/versions/rule_versions.js +0 -29
- package/dist/domain/versions/scheduled_exceptions.js +0 -41
- package/skills/about/SKILL.md +0 -21
- package/skills/architect/SKILL.md +0 -49
- package/skills/architect/feature.md +0 -40
- package/skills/architect/project.md +0 -71
- package/skills/architect/tweak.md +0 -22
- package/skills/architect/walkthrough.md +0 -35
- package/skills/domain-language/SKILL.md +0 -37
- package/skills/envision/feature.md +0 -25
- package/skills/envision/intake.md +0 -28
- package/skills/envision/project.md +0 -24
- package/skills/explore/SKILL.md +0 -39
- package/skills/formats/reference/architecture.md +0 -84
- package/skills/formats/reference/config.md +0 -80
- package/skills/formats/reference/durable-docs.md +0 -74
- package/skills/formats/reference/specs.md +0 -65
- package/skills/formats/reference/work-items.md +0 -100
- package/skills/formats/templates/adr.md +0 -7
- package/skills/formats/templates/change.md +0 -29
- package/skills/formats/templates/glossary.md +0 -8
- package/skills/formats/templates/roadmap.md +0 -32
- package/skills/formats/templates/spec.md +0 -11
- package/skills/formats/templates/standalone-change.md +0 -46
- package/skills/grilling/SKILL.md +0 -90
- package/skills/grilling/illustrating.md +0 -38
- package/skills/improve/SKILL.md +0 -51
- package/skills/land/SKILL.md +0 -48
- package/skills/next/SKILL.md +0 -31
- package/skills/plan/SKILL.md +0 -62
- package/skills/plan/tasks.md +0 -35
- package/skills/plan/test-plan.md +0 -14
- package/skills/realign/SKILL.md +0 -37
- package/skills/realign/notes/0.0.10.md +0 -131
- package/skills/realign/notes/0.0.11.md +0 -15
- package/skills/realign/notes/0.0.12.md +0 -31
- package/skills/realign/notes/0.0.13.md +0 -11
- package/skills/realign/notes/0.0.2.md +0 -69
- package/skills/realign/notes/0.0.3.md +0 -11
- package/skills/realign/notes/0.0.4.md +0 -27
- package/skills/realign/notes/0.0.5.md +0 -38
- package/skills/realign/notes/0.0.6.md +0 -44
- package/skills/realign/notes/0.0.7.md +0 -11
- package/skills/realign/notes/0.0.8.md +0 -97
- package/skills/realign/notes/0.0.9.md +0 -59
- package/skills/realign/notes.md +0 -40
- package/skills/rethink/SKILL.md +0 -62
- package/skills/rethink/levels.md +0 -54
- package/skills/roadmap/SKILL.md +0 -54
- package/skills/roles/auditor.md +0 -35
- package/skills/roles/design-reviewer.md +0 -43
- package/skills/roles/design-rubric.md +0 -41
- package/skills/roles/plan-rubric.md +0 -11
- package/skills/roles/surveyor.md +0 -41
- package/skills/roles/verifier.md +0 -42
- package/skills/triage/SKILL.md +0 -37
- package/skills/verify/SKILL.md +0 -45
- package/skills/verify/manual-test.md +0 -23
- package/src/adapters/inbound/cli/argument_files.ts +0 -29
- package/src/adapters/inbound/cli/commands/diff.ts +0 -23
- package/src/adapters/inbound/cli/commands/journal.ts +0 -32
- package/src/adapters/inbound/cli/commands/progress.ts +0 -55
- package/src/adapters/inbound/cli/commands/realign.ts +0 -28
- package/src/adapters/inbound/cli/commands/show.ts +0 -96
- package/src/app/use_cases/abandon_work.ts +0 -42
- package/src/app/use_cases/approve_artifact.ts +0 -197
- package/src/app/use_cases/journal_events.ts +0 -64
- package/src/app/use_cases/land_change.ts +0 -134
- package/src/app/use_cases/new_project_doc.ts +0 -48
- package/src/app/use_cases/new_work_item.ts +0 -75
- package/src/app/use_cases/realign_done.ts +0 -45
- package/src/app/use_cases/realign_status.ts +0 -106
- package/src/app/use_cases/record_progress.ts +0 -118
- package/src/app/use_cases/show_elements.ts +0 -18
- package/src/app/use_cases/show_notes.ts +0 -11
- package/src/app/use_cases/show_scope.ts +0 -51
- package/src/app/use_cases/upstream_diff.ts +0 -55
- package/src/domain/approvals/agent_approval.ts +0 -68
- package/src/domain/approvals/evaluate.ts +0 -36
- package/src/domain/approvals/fingerprint.ts +0 -52
- package/src/domain/approvals/rebase.ts +0 -39
- package/src/domain/approvals/records.ts +0 -50
- package/src/domain/approvals/upstream_text.ts +0 -36
- package/src/domain/architecture_sections.ts +0 -23
- package/src/domain/check/decision_comments.ts +0 -38
- package/src/domain/decision_citations.ts +0 -62
- package/src/domain/edits/agents_file.ts +0 -58
- package/src/domain/edits/managed_tables.ts +0 -60
- package/src/domain/edits/progress_block.ts +0 -33
- package/src/domain/edits/status_lines.ts +0 -52
- package/src/domain/elements.ts +0 -126
- package/src/domain/journal.ts +0 -109
- package/src/domain/land/bookkeeping.ts +0 -54
- package/src/domain/land/merge_plan.ts +0 -137
- package/src/domain/land/text_ops.ts +0 -66
- package/src/domain/line_diff.ts +0 -56
- package/src/domain/project/artifact_ref.ts +0 -42
- package/src/domain/project/deltas.ts +0 -49
- package/src/domain/project/element_lookup.ts +0 -124
- package/src/domain/project/living_model.ts +0 -55
- package/src/domain/project/new_items.ts +0 -48
- package/src/domain/project/notes.ts +0 -74
- package/src/domain/project/plan_parts.ts +0 -111
- package/src/domain/project/progress.ts +0 -43
- package/src/domain/project/project_view.ts +0 -72
- package/src/domain/project/roadmap.ts +0 -43
- package/src/domain/project/scope_view.ts +0 -134
- package/src/domain/project/work_items.ts +0 -68
- package/src/domain/status/next_step.ts +0 -181
- package/src/domain/status/pending_rethink.ts +0 -22
- package/src/domain/status/verification.ts +0 -29
- package/src/domain/status/work_status.ts +0 -125
- package/src/domain/validation/decision_rules.ts +0 -76
- package/src/domain/validation/delta_rules.ts +0 -81
- package/src/domain/validation/element_rules.ts +0 -104
- package/src/domain/validation/future_rules.ts +0 -40
- package/src/domain/validation/living_rules.ts +0 -99
- package/src/domain/validation/plan_rules.ts +0 -124
- package/src/domain/validation/project_rules.ts +0 -115
- package/src/domain/validation/size_rules.ts +0 -45
- package/src/domain/validation/vision_rules.ts +0 -41
- package/src/domain/validation/work_item_rules.ts +0 -175
- package/src/domain/versions/realign_notes.ts +0 -72
- package/src/domain/versions/rule_versions.ts +0 -29
- package/src/domain/versions/scheduled_exceptions.ts +0 -41
- package/templates/init/gitattributes +0 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** 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 = 17605;
|
|
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,70 @@
|
|
|
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, warning, } from "./problem.js";
|
|
7
|
+
|
|
8
|
+
/** Each document's required sections, its size, its scenarios, and leftover template guides. */
|
|
9
|
+
|
|
10
|
+
const VISION_SECTIONS = ["Problem", "Users", "Release"];
|
|
11
|
+
const ARCHITECTURE_SECTIONS = ["Overview", "Ports", "Adapters", "Runtime"];
|
|
12
|
+
|
|
13
|
+
/** Words past which a document stops being something a person reads in one sitting. */
|
|
14
|
+
export const WORD_LIMITS = { vision: 1500, architecture: 3000, feature: 2500 } ;
|
|
15
|
+
|
|
16
|
+
const GUIDE = /(?<!`)<(?!!--|\/|https?:|br\b)[A-Za-z][^<>\n]*>(?!`)/;
|
|
17
|
+
const PLACEHOLDER = /\{\{[a-z_]+\}\}/;
|
|
18
|
+
|
|
19
|
+
export function documentProblems(docs ) {
|
|
20
|
+
const problems = [];
|
|
21
|
+
if (docs.vision !== undefined) problems.push(...shapeOf(PATHS.vision, docs.vision, VISION_SECTIONS, WORD_LIMITS.vision));
|
|
22
|
+
if (docs.architecture !== undefined) problems.push(...shapeOf(PATHS.architecture, docs.architecture, ARCHITECTURE_SECTIONS, WORD_LIMITS.architecture));
|
|
23
|
+
for (const feature of docs.features) {
|
|
24
|
+
problems.push(...shapeOf(feature.path, feature.doc, [...FEATURE_SECTIONS], WORD_LIMITS.feature), ...featureProblems(feature));
|
|
25
|
+
}
|
|
26
|
+
problems.push(...duplicateScenarios(docs.features));
|
|
27
|
+
return problems;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function shapeOf(path , doc , sections , limit ) {
|
|
31
|
+
const problems = sections.filter((title) => findSection(doc, title) === null).map((title) => error(path, undefined, "missing-section", `needs a ## ${title} section`));
|
|
32
|
+
const words = doc.lines.filter((_, i) => !doc.inFence[i]).join(" ").split(/\s+/).filter((w) => w !== "").length;
|
|
33
|
+
if (words > limit) problems.push(warning(path, undefined, "too-long", `${words} words, over the ${limit} a person reads in one sitting; cut what the agent decided and the user needn't read, or split it`));
|
|
34
|
+
doc.lines.forEach((line, i) => {
|
|
35
|
+
if (doc.inFence[i]) return;
|
|
36
|
+
const withoutCode = line.replace(/`[^`]*`/g, "");
|
|
37
|
+
if (GUIDE.test(withoutCode) || PLACEHOLDER.test(withoutCode)) problems.push(error(path, i, "template-leftover", "replace the template guide or placeholder on this line"));
|
|
38
|
+
});
|
|
39
|
+
return problems;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function featureProblems(feature ) {
|
|
43
|
+
const problems = [];
|
|
44
|
+
const status = statusLine(feature.doc);
|
|
45
|
+
if (status === undefined) problems.push(error(feature.path, undefined, "status", `needs a "- Status: shaped" line under the title`));
|
|
46
|
+
else if (feature.status === undefined) problems.push(error(feature.path, status.line, "status", `the status is one of ${FEATURE_STATUSES.join(" or ")}`));
|
|
47
|
+
if (feature.scenarios.length === 0 && findSection(feature.doc, "Behaviour") !== null) {
|
|
48
|
+
problems.push(error(feature.path, undefined, "no-scenarios", `## Behaviour needs at least one ### SCN-${feature.slug}.<name>: <title> scenario`));
|
|
49
|
+
}
|
|
50
|
+
for (const scenario of feature.scenarios) {
|
|
51
|
+
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}.`));
|
|
52
|
+
}
|
|
53
|
+
for (const stray of strayScenarioIds(feature)) {
|
|
54
|
+
problems.push(error(feature.path, stray.line, "scenario-heading", `a scenario heading reads exactly "${stray.id}: <title>"`));
|
|
55
|
+
}
|
|
56
|
+
return problems;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function duplicateScenarios(features ) {
|
|
60
|
+
const seen = new Map ();
|
|
61
|
+
const problems = [];
|
|
62
|
+
for (const feature of features) {
|
|
63
|
+
for (const scenario of feature.scenarios) {
|
|
64
|
+
const first = seen.get(scenario.id);
|
|
65
|
+
if (first !== undefined) problems.push(error(feature.path, scenario.line, "duplicate-scenario", `${scenario.id} is already defined in ${first}`));
|
|
66
|
+
else seen.set(scenario.id, feature.path);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return problems;
|
|
70
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -36,15 +36,9 @@ export function versionSkew(cli , recorded )
|
|
|
36
36
|
* after `version:`, so the file's comments and layout survive.
|
|
37
37
|
*/
|
|
38
38
|
export function withRecordedVersion(configText , version ) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
/** The config text with a top-level version key, such as `cruze:` or `standards:`, set in place. */
|
|
43
|
-
export function withVersionLine(configText , key , version ) {
|
|
44
|
-
const line = `${key}: ${JSON.stringify(version)}`;
|
|
45
|
-
const existing = new RegExp(`^${key}:.*$`, "m");
|
|
46
|
-
if (existing.test(configText)) return configText.replace(existing, line);
|
|
47
|
-
const anchor = key === "standards" ? /^cruze:.*$/m.exec(configText) ?? /^version:.*$/m.exec(configText) : /^version:.*$/m.exec(configText);
|
|
39
|
+
const line = `cruze: ${JSON.stringify(version)}`;
|
|
40
|
+
if (/^cruze:.*$/m.test(configText)) return configText.replace(/^cruze:.*$/m, line);
|
|
41
|
+
const anchor = /^version:.*$/m.exec(configText);
|
|
48
42
|
if (anchor === null) return `${line}\n${configText}`;
|
|
49
43
|
const end = anchor.index + anchor[0].length;
|
|
50
44
|
return `${configText.slice(0, end)}\n${line}${configText.slice(end)}`;
|
package/dist/main.js
CHANGED
|
@@ -10,7 +10,7 @@ import { ProcessAgentRunner } from "./adapters/outbound/process_agent_runner.js"
|
|
|
10
10
|
import { SystemClock } from "./adapters/outbound/system_clock.js";
|
|
11
11
|
import { DefaultAnswerPrompter, TerminalPrompter } from "./adapters/outbound/terminal_prompter.js";
|
|
12
12
|
|
|
13
|
-
// A reader that stops early, such as `cruze
|
|
13
|
+
// A reader that stops early, such as `cruze status --text | head`, closes the pipe; that is not an error worth a stack trace.
|
|
14
14
|
process.stdout.on("error", (error ) => {
|
|
15
15
|
if (error.code === "EPIPE") process.exit(0);
|
|
16
16
|
throw error;
|
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cruze-asking
|
|
3
|
+
description: When to ask the user and when to decide, and how to show a question so the user can answer it cold, with plain words, code sketches and diagrams. Use before asking the user anything during envision, shape, build, review or fix.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Asking the user
|
|
7
|
+
|
|
8
|
+
The user's time is the scarcest thing in the project. Every question costs them a context switch, and a long run of questions turns into rubber-stamping: in one project the user answered 71% of 187 design questions with a bare "agree". So ask little, ask what only the user can answer, and show it so it can be answered at a glance.
|
|
9
|
+
|
|
10
|
+
## What to ask and what to decide
|
|
11
|
+
|
|
12
|
+
Ask when a choice changes something the user will see or live with:
|
|
13
|
+
|
|
14
|
+
- behaviour a user of the product will notice, including what they see when something fails;
|
|
15
|
+
- a public API: anything another program, person or team calls, types, or reads;
|
|
16
|
+
- scope: what is in a feature and what is out;
|
|
17
|
+
- a significant dependency, through `.agents/skills/cruze-dependency-approval/SKILL.md`;
|
|
18
|
+
- security, privacy, stored data, compatibility, or a commitment that is costly to reverse.
|
|
19
|
+
|
|
20
|
+
Decide everything else yourself: internal structure, private names, which module owns what, internal types, file layout, equivalent idioms, limits and timeouts with an obvious value. Pick the conventional option. Uncertainty alone doesn't make a choice the user's: investigate first. Then list the decisions a user might question in the document's `## Decisions`, one line each with the reason, so the user sees them when they approve, without being asked.
|
|
21
|
+
|
|
22
|
+
## How many
|
|
23
|
+
|
|
24
|
+
At most one question round per step, of five questions or fewer, before you show the document. When a choice has a sensible default, take it and list it under Decisions instead of asking. When the user defers ("you decide"), decide, and don't ask again.
|
|
25
|
+
|
|
26
|
+
## Showing a question
|
|
27
|
+
|
|
28
|
+
The user knows what the product should do, not the design's internals, and is often coming back from other work.
|
|
29
|
+
|
|
30
|
+
- **Say where things stand** in one line first: the step and the feature, such as "Shaping export-csv."
|
|
31
|
+
- **Explain the part before asking about it:** what it is, what it does, and why the question comes up now.
|
|
32
|
+
- **Use plain words.** Name a scenario or decision by what it says, and add its ID after it in parentheses only when the user may want to look it up. Never let an ID stand alone.
|
|
33
|
+
- **Show each option** with the form that fits:
|
|
34
|
+
|
|
35
|
+
| The question decides | Show |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| What a user does or sees | The command and its exact output, or a sketch of the screen, with real values |
|
|
38
|
+
| A public API or a port | A code sketch in the project's language: types, signatures, errors, and one call |
|
|
39
|
+
| Where something lives | A box-and-arrow diagram, or the directory tree |
|
|
40
|
+
| An order of steps, or what happens on failure | A numbered sequence with the failure branch |
|
|
41
|
+
| A stored format or config | A sample of the file |
|
|
42
|
+
| A dependency | The code with it and without it |
|
|
43
|
+
|
|
44
|
+
- **Keep sketches short:** shape only, bodies left out, about 15 lines per option. Draw diagrams in plain text, which reads in a terminal; documents keep their Mermaid.
|
|
45
|
+
- **Mark your recommendation** and why, in one line.
|
|
46
|
+
|
|
47
|
+
A shape the user chose from a sketch is part of the decision: write it into the document, so build makes it as the user saw it.
|
|
48
|
+
|
|
49
|
+
## Challenge, then defer
|
|
50
|
+
|
|
51
|
+
When the user steers toward something that breaks the architecture or a standard, name the rule, the concrete risk here and your alternative, once. Then the user decides, and their decision stands.
|
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cruze-behavioural-testing
|
|
3
|
-
description: The testing standard for a Cruze project, covering which tests to write, the seam each one drives, the ID it carries, and fakes instead of mocks. Use when writing
|
|
3
|
+
description: The testing standard for a Cruze project, covering which tests to write, the seam each one drives, the ID it carries, and fakes instead of mocks. Use when writing, changing or fixing a test, when building a fake for a port, when deciding whether a unit test is warranted, or when reviewing tests.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Behavioural testing
|
|
7
7
|
|
|
8
|
-
Behaviour is the contract. Every scenario in
|
|
8
|
+
Behaviour is the contract. Every scenario in a feature becomes a test that drives the system through a use case and checks what a caller would see. Tests pin behaviour, not structure, so the code behind them can be rewritten freely.
|
|
9
9
|
|
|
10
|
-
## Writing tests for a
|
|
10
|
+
## Writing tests for a feature
|
|
11
11
|
|
|
12
|
-
1.
|
|
13
|
-
2. Write one test at its seam, carrying
|
|
12
|
+
1. For each scenario, pick its seam from the table below. Done when you know the seam, test file and kind of every test the feature needs.
|
|
13
|
+
2. Write one test at its seam, carrying the scenario's ID. Take the inputs and expected values from the scenario's GIVEN, WHEN and THEN.
|
|
14
14
|
3. Run it and watch it fail for the reason you expect (red). A test that passes before the code exists proves nothing, so fix the test. A test that guards a race or an ordering must be seen failing against code that has the flaw: run the racing work concurrently, on a multi-threaded runtime where there is one, and wait for a signal the system gives, such as a watch having opened, never for a fixed sleep.
|
|
15
15
|
4. Write the least code that makes it pass (green). Then write the next test. Work in tracer bullets, one test and one slice of code at a time, never all the tests first.
|
|
16
|
-
5. Done when every
|
|
16
|
+
5. Done when every scenario has a passing test carrying its ID, every port with a real adapter has contract tests, and each test fails when the behaviour it checks is removed.
|
|
17
17
|
|
|
18
18
|
## The four kinds
|
|
19
19
|
|
|
20
|
-
These match the test plan in `.agents/skills/cruze-formats/reference/work-items.md`.
|
|
21
|
-
|
|
22
20
|
| Kind | Subject | Seam | When |
|
|
23
21
|
| --- | --- | --- | --- |
|
|
24
|
-
| `behaviour` |
|
|
25
|
-
| `contract` |
|
|
26
|
-
| `domain` |
|
|
27
|
-
| `smoke` |
|
|
22
|
+
| `behaviour` | a scenario | The use case's API with a fake for each driven port, or the inbound adapter when the outcome is what that adapter shows, such as CLI output, an exit code or an HTTP status | One or more per scenario, always |
|
|
23
|
+
| `contract` | a port | The same cases run against the fake and every real adapter | Every driven port that has a real adapter |
|
|
24
|
+
| `domain` | a domain type | The entity's own API | Only for real logic: state machines, calculations, invariants with edge cases |
|
|
25
|
+
| `smoke` | the main path | The real inbound adapter, wired by the real composition root | The top 1 to 3 paths through the system |
|
|
28
26
|
|
|
29
27
|
A behaviour test drives a use case, not a single domain type, so it keeps passing when rules move between entities. In a library with no use cases, the seam is the library's public API. Write a domain test only when a rule's edge cases are too many to reach through use cases one by one.
|
|
30
28
|
|
|
@@ -51,9 +49,9 @@ A behaviour test drives a use case, not a single domain type, so it keeps passin
|
|
|
51
49
|
|
|
52
50
|
## Protecting the check
|
|
53
51
|
|
|
54
|
-
- Behaviour, contract and smoke tests for approved scenarios
|
|
52
|
+
- Behaviour, contract and smoke tests for approved scenarios and ports are the safety net. Never edit one to get green. When one fails, either the code is wrong, or the scenario is. A wrong scenario goes back through shape, and the test changes only after the user approves the changed scenario.
|
|
55
53
|
- Domain tests are disposable. Rewrite or delete them along with the code they test.
|
|
56
|
-
- Never skip a test, delete one, or weaken an assertion to finish
|
|
54
|
+
- Never skip a test, delete one, or weaken an assertion to finish. Review flags any weakened safety-net test.
|
|
57
55
|
|
|
58
56
|
## What not to write
|
|
59
57
|
|
|
@@ -65,6 +63,6 @@ Side-by-side examples are in [examples.md](examples.md).
|
|
|
65
63
|
|
|
66
64
|
## Naming and tracing
|
|
67
65
|
|
|
68
|
-
- Every test carries its
|
|
66
|
+
- Every behaviour test carries its scenario's ID in a comment or string, such as `// SCN-open-serial.attach` on the line above it. A test name alone doesn't count, since `cruze trace` looks for the literal ID.
|
|
69
67
|
- A scenario may need several tests, and each of them carries its ID. One test covers one scenario.
|
|
70
|
-
- `cruze trace
|
|
68
|
+
- `cruze trace <slug>` fails when one of the feature's scenarios has no test carrying its ID, or when a test cites a scenario no feature defines. Run it before calling a feature's tests complete, and quote its result.
|
|
@@ -7,7 +7,7 @@ The examples are in Rust, from a console-access tool. The shape is the same in a
|
|
|
7
7
|
Good. It drives the use case, fakes the ports, and checks what the caller sees:
|
|
8
8
|
|
|
9
9
|
```rust
|
|
10
|
-
// SCN-
|
|
10
|
+
// SCN-ssh-hops.second-hop-refused
|
|
11
11
|
#[test]
|
|
12
12
|
fn second_hop_refused_closes_the_first_hop() {
|
|
13
13
|
let catalog = InMemoryDeviceCatalog::with(device("lab-router", ssh_via("jump-1")));
|
|
@@ -89,7 +89,6 @@ The file format is the TOML adapter's business. Its contract tests cover it, and
|
|
|
89
89
|
Good. A state machine with real rules, tested through the entity's own API:
|
|
90
90
|
|
|
91
91
|
```rust
|
|
92
|
-
// ENT-access.console-session
|
|
93
92
|
#[test]
|
|
94
93
|
fn a_closed_session_cannot_reopen() {
|
|
95
94
|
let mut session = ConsoleSession::opening();
|
|
@@ -4,7 +4,7 @@ A fake is a real, working implementation of a driven port that keeps its state i
|
|
|
4
4
|
|
|
5
5
|
## Writing a fake
|
|
6
6
|
|
|
7
|
-
1. Read the port's
|
|
7
|
+
1. Read the port's operations in `docs/architecture.md`. The fake implements every operation with the guarantees and failures listed there, and nothing more. That includes every way the real adapter ends on its own, such as a session closed when its stream ends or its server shuts down.
|
|
8
8
|
2. Keep state in plain in-memory structures: a map for a catalog, a list for recorded effects, a settable value for a clock.
|
|
9
9
|
3. Give tests a way to arrange state and to inject each failure the contract lists. Use a constructor that takes the initial state, and a method such as `fail_next_with(HopError::Refused)`. When a use case writes through the port, also let a test stall an operation, and fail one that writes several things partway through, so each partial failure and cancellation can be tested from every state it can leave.
|
|
10
10
|
4. Record effects the contract makes observable, such as the order hops were closed in, so tests can assert them.
|
|
@@ -21,7 +21,7 @@ A contract test states the port's contract once and runs it against every implem
|
|
|
21
21
|
3. Run the suite against the fake in the ordinary test run.
|
|
22
22
|
4. Run it against each real adapter too. When the real adapter needs something external, such as an SSH server, a serial device or a database, use a local stand-in (a test server, a pseudo-terminal, a container), and mark the run so CI can provide it.
|
|
23
23
|
|
|
24
|
-
When the contract changes, the port's `
|
|
24
|
+
When the contract changes, the port's operations in `docs/architecture.md` change first, then the contract suite, then the fake and the adapters.
|
|
25
25
|
|
|
26
26
|
## What a fake is not
|
|
27
27
|
|