@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,36 +1,29 @@
|
|
|
1
1
|
import type { CruzeConfig, Layer } from "../config.ts";
|
|
2
2
|
import { matchesAny } from "../glob.ts";
|
|
3
3
|
import { budgetedText } from "./budget_text.ts";
|
|
4
|
-
import { decisionCitationsInComments } from "./decision_comments.ts";
|
|
5
4
|
import { importsOf, resolveImport } from "./imports.ts";
|
|
6
5
|
import { countTypes } from "./type_count.ts";
|
|
7
6
|
|
|
8
7
|
export interface CheckFinding {
|
|
9
8
|
path: string;
|
|
10
9
|
line?: number;
|
|
11
|
-
rule: "layer" | "max-lines" | "max-types" | "
|
|
10
|
+
rule: "layer" | "max-lines" | "max-types" | "no-layer" | "test-placement";
|
|
12
11
|
message: string;
|
|
13
12
|
/** Layer violations always fail; the rest fail in CI unless excepted. */
|
|
14
13
|
severity: "error" | "warning";
|
|
15
|
-
/** Set when the rule is newer than the project's `standards:`: it reports, but can't fail CI until the project realigns. */
|
|
16
|
-
pending?: true;
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
export interface CheckInput {
|
|
20
17
|
config: CruzeConfig;
|
|
21
18
|
/** Every source file matched by config.source, by path. */
|
|
22
19
|
sources: ReadonlyMap<string, string>;
|
|
23
|
-
/** Paths of the MOD elements in the architecture; files outside all of them are flagged. */
|
|
24
|
-
modulePaths: string[];
|
|
25
|
-
/** Test files outside config.source, by path: only their comments are checked. */
|
|
26
|
-
tests?: ReadonlyMap<string, string>;
|
|
27
20
|
/** Check only these files, when given; imports still resolve against every source. */
|
|
28
21
|
only?: string[];
|
|
29
22
|
}
|
|
30
23
|
|
|
31
24
|
/** Enforces the architecture's layer rules and the file budgets on source code. */
|
|
32
25
|
export function checkSource(input: CheckInput): CheckFinding[] {
|
|
33
|
-
const { config, sources
|
|
26
|
+
const { config, sources } = input;
|
|
34
27
|
const all = [...sources.keys()];
|
|
35
28
|
const targets = input.only ?? all;
|
|
36
29
|
const layerOf = (path: string): Layer | undefined => config.layers.find((layer) => matchesAny(path, layer.paths));
|
|
@@ -67,32 +60,10 @@ export function checkSource(input: CheckInput): CheckFinding[] {
|
|
|
67
60
|
if (!excepted && isRustTestFileInSource(path)) {
|
|
68
61
|
findings.push({ path, rule: "test-placement", severity: "warning", message: "Rust tests that leave their inline module go in the crate's tests/ directory, not under src/" });
|
|
69
62
|
}
|
|
70
|
-
if (modulePaths.length > 0 && !insideModuleMap(path, modulePaths)) {
|
|
71
|
-
findings.push({ path, rule: "outside-module-map", severity: "warning", message: "the file is outside every module in the architecture's module map" });
|
|
72
|
-
}
|
|
73
|
-
findings.push(...decisionFindings(path, text));
|
|
74
|
-
}
|
|
75
|
-
for (const [path, text] of input.tests ?? []) {
|
|
76
|
-
if (!sources.has(path) && (input.only === undefined || input.only.includes(path))) findings.push(...decisionFindings(path, text));
|
|
77
63
|
}
|
|
78
64
|
return findings;
|
|
79
65
|
}
|
|
80
66
|
|
|
81
|
-
function decisionFindings(path: string, text: string): CheckFinding[] {
|
|
82
|
-
return decisionCitationsInComments(path, text).map((citation) => ({
|
|
83
|
-
path,
|
|
84
|
-
line: citation.line + 1,
|
|
85
|
-
rule: "decision-citation",
|
|
86
|
-
severity: "warning",
|
|
87
|
-
message: `the comment cites ${citation.text}, a decision number a reader of the code can't follow; say what was decided, or cite the element or ADR that holds it`,
|
|
88
|
-
}));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Whether a module's Path covers the file: a directory path ending in `/` covers everything under it, any other names one file. */
|
|
92
|
-
export function insideModuleMap(path: string, modulePaths: string[]): boolean {
|
|
93
|
-
return modulePaths.some((module) => (module.endsWith("/") ? path.startsWith(module) : path === module));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
67
|
/** A Rust file of tests split out of a module: `tests.rs`, `*_tests.rs`, or anything under a `tests/` folder inside `src/`. */
|
|
97
68
|
function isRustTestFileInSource(path: string): boolean {
|
|
98
69
|
if (!path.endsWith(".rs")) return false;
|
package/src/domain/config.ts
CHANGED
|
@@ -2,13 +2,10 @@ import { parse as parseYaml } from "yaml";
|
|
|
2
2
|
|
|
3
3
|
/** `.cruze/config.yaml`, as described in the cruze-formats skill. */
|
|
4
4
|
export interface CruzeConfig {
|
|
5
|
-
version:
|
|
5
|
+
version: 2;
|
|
6
6
|
/** The Cruze version whose skills the project installed, recorded by `cruze install`. */
|
|
7
7
|
cruze?: string;
|
|
8
|
-
/** The Cruze version whose expectations the code meets, moved forward by `realign`. */
|
|
9
|
-
standards?: string;
|
|
10
8
|
project: string;
|
|
11
|
-
tracker: "markdown";
|
|
12
9
|
source: string[];
|
|
13
10
|
tests: string[];
|
|
14
11
|
check: {
|
|
@@ -17,35 +14,13 @@ export interface CruzeConfig {
|
|
|
17
14
|
exceptions: Array<{ path: string; reason: string }>;
|
|
18
15
|
};
|
|
19
16
|
layers: Layer[];
|
|
20
|
-
/** Named project commands, such as `test: cargo test`, shown in AGENTS.md. */
|
|
21
|
-
commands: Record<string, string>;
|
|
22
17
|
/**
|
|
23
18
|
* How `cruze review` starts an agent in a fresh context: the program and its arguments.
|
|
24
19
|
* Without one, the calling agent runs the role's prompt in a helper of its own.
|
|
25
20
|
*/
|
|
26
|
-
review: { command?: string[]
|
|
27
|
-
/** The size band of a change, so each one is worth its own plan, review and verify. */
|
|
28
|
-
changes: ChangeLimits;
|
|
29
|
-
/** External documents the design must comply with, such as a protocol specification, each with what it governs. */
|
|
30
|
-
references: Array<{ path: string; use: string }>;
|
|
21
|
+
review: { command?: string[] };
|
|
31
22
|
}
|
|
32
23
|
|
|
33
|
-
/** Which review findings the user decides: only the exceptions, or every one. */
|
|
34
|
-
export type ReviewDecide = "exceptions" | "all";
|
|
35
|
-
|
|
36
|
-
export interface ChangeLimits {
|
|
37
|
-
/** A feature's change building fewer elements than this merges into a neighbour. */
|
|
38
|
-
minBuilds: number;
|
|
39
|
-
/** A change delivering more scenarios than this splits. */
|
|
40
|
-
maxScenarios: number;
|
|
41
|
-
/** A plan with more tasks than this splits. */
|
|
42
|
-
maxTasks: number;
|
|
43
|
-
/** Features and changes the user accepted outside the band, by ref, each with the reason. */
|
|
44
|
-
exceptions: Array<{ item: string; reason: string }>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const DEFAULT_CHANGE_LIMITS: ChangeLimits = { minBuilds: 3, maxScenarios: 30, maxTasks: 25, exceptions: [] };
|
|
48
|
-
|
|
49
24
|
export interface Layer {
|
|
50
25
|
name: string;
|
|
51
26
|
paths: string[];
|
|
@@ -62,17 +37,14 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
62
37
|
} catch (error) {
|
|
63
38
|
return { config: null, problems: [`not valid YAML: ${error instanceof Error ? error.message : String(error)}`] };
|
|
64
39
|
}
|
|
65
|
-
const problems: string[] = [];
|
|
66
40
|
const root = asRecord(raw);
|
|
67
41
|
if (root === null) return { config: null, problems: ["the file must be a YAML mapping"] };
|
|
68
|
-
|
|
69
|
-
|
|
42
|
+
if (root["version"] === 1) return { config: null, problems: ["this config was created by an older Cruze, whose documents this release doesn't read; start the project's Cruze documents again with cruze init in a fresh checkout"] };
|
|
43
|
+
const problems: string[] = [];
|
|
44
|
+
if (root["version"] !== 2) problems.push("version must be 2");
|
|
70
45
|
const cruze = root["cruze"];
|
|
71
|
-
if (cruze !== undefined && (typeof cruze !== "string" || cruze.trim() === "")) problems.push("cruze must be a version string, such as \"0.0.
|
|
72
|
-
const standards = root["standards"];
|
|
73
|
-
if (standards !== undefined && (typeof standards !== "string" || standards.trim() === "")) problems.push("standards must be a version string, such as \"0.0.2\"");
|
|
46
|
+
if (cruze !== undefined && (typeof cruze !== "string" || cruze.trim() === "")) problems.push("cruze must be a version string, such as \"0.0.14\"");
|
|
74
47
|
if (typeof root["project"] !== "string" || root["project"] === "") problems.push("project must be a non-empty string");
|
|
75
|
-
if (root["tracker"] !== "markdown") problems.push("tracker must be markdown");
|
|
76
48
|
const source = stringList(root["source"], "source", problems);
|
|
77
49
|
const tests = stringList(root["tests"], "tests", problems);
|
|
78
50
|
|
|
@@ -104,71 +76,23 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
104
76
|
}
|
|
105
77
|
}
|
|
106
78
|
|
|
107
|
-
const commands: Record<string, string> = {};
|
|
108
|
-
const rawCommands = root["commands"];
|
|
109
|
-
if (rawCommands !== undefined && rawCommands !== null) {
|
|
110
|
-
const record = asRecord(rawCommands);
|
|
111
|
-
if (record === null) problems.push("commands must be a mapping of names to commands");
|
|
112
|
-
for (const [name, command] of Object.entries(record ?? {})) {
|
|
113
|
-
if (typeof command === "string" && command.trim() !== "") commands[name] = command;
|
|
114
|
-
else problems.push(`commands.${name} must be a command string`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
79
|
const review = asRecord(root["review"]) ?? {};
|
|
118
80
|
const reviewCommand = review["command"] === undefined ? undefined : stringList(review["command"], "review.command", problems);
|
|
119
81
|
if (reviewCommand?.length === 0) problems.push("review.command needs at least the program to run");
|
|
120
|
-
const decide = review["decide"] ?? "exceptions";
|
|
121
|
-
if (decide !== "exceptions" && decide !== "all") problems.push("review.decide must be exceptions or all");
|
|
122
|
-
const limits = asRecord(root["changes"]) ?? {};
|
|
123
|
-
const changes: ChangeLimits = {
|
|
124
|
-
minBuilds: positiveInt(limits["min_builds"], "changes.min_builds", problems) ?? DEFAULT_CHANGE_LIMITS.minBuilds,
|
|
125
|
-
maxScenarios: positiveInt(limits["max_scenarios"], "changes.max_scenarios", problems) ?? DEFAULT_CHANGE_LIMITS.maxScenarios,
|
|
126
|
-
maxTasks: positiveInt(limits["max_tasks"], "changes.max_tasks", problems) ?? DEFAULT_CHANGE_LIMITS.maxTasks,
|
|
127
|
-
exceptions: (Array.isArray(limits["exceptions"]) ? limits["exceptions"] : []).flatMap((entry: unknown, i: number) => {
|
|
128
|
-
const record = asRecord(entry);
|
|
129
|
-
if (typeof record?.["item"] !== "string" || typeof record["reason"] !== "string" || record["reason"].trim() === "") {
|
|
130
|
-
problems.push(`changes.exceptions[${i}] needs an item and a non-empty reason`);
|
|
131
|
-
return [];
|
|
132
|
-
}
|
|
133
|
-
return [{ item: record["item"], reason: record["reason"] }];
|
|
134
|
-
}),
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const references = (Array.isArray(root["references"]) ? root["references"] : root["references"] === undefined ? [] : [null]).flatMap((entry: unknown, i: number) => {
|
|
138
|
-
const record = asRecord(entry);
|
|
139
|
-
if (typeof record?.["path"] !== "string" || record["path"].trim() === "" || typeof record["use"] !== "string" || record["use"].trim() === "") {
|
|
140
|
-
problems.push(`references[${i}] needs a path and a non-empty use`);
|
|
141
|
-
return [];
|
|
142
|
-
}
|
|
143
|
-
return [{ path: record["path"], use: record["use"] }];
|
|
144
|
-
});
|
|
145
82
|
|
|
146
83
|
const config: CruzeConfig = {
|
|
147
|
-
version:
|
|
84
|
+
version: 2,
|
|
148
85
|
...(typeof cruze === "string" && cruze.trim() !== "" ? { cruze } : {}),
|
|
149
|
-
...(typeof standards === "string" && standards.trim() !== "" ? { standards } : {}),
|
|
150
86
|
project: typeof root["project"] === "string" ? root["project"] : "",
|
|
151
|
-
tracker: "markdown",
|
|
152
87
|
source,
|
|
153
88
|
tests,
|
|
154
|
-
check: { maxLines: maxLines ??
|
|
89
|
+
check: { maxLines: maxLines ?? 600, maxTypes: maxTypes ?? 12, exceptions },
|
|
155
90
|
layers,
|
|
156
|
-
|
|
157
|
-
review: {
|
|
158
|
-
...(reviewCommand === undefined || reviewCommand.length === 0 ? {} : { command: reviewCommand }),
|
|
159
|
-
decide: decide === "all" ? "all" : "exceptions",
|
|
160
|
-
},
|
|
161
|
-
changes,
|
|
162
|
-
references,
|
|
91
|
+
review: reviewCommand === undefined || reviewCommand.length === 0 ? {} : { command: reviewCommand },
|
|
163
92
|
};
|
|
164
93
|
return { config, problems };
|
|
165
94
|
}
|
|
166
95
|
|
|
167
|
-
/** The first layer whose paths match the file, as the formats define. */
|
|
168
|
-
export function layerOf(config: CruzeConfig, path: string, matches: (path: string, globs: string[]) => boolean): Layer | undefined {
|
|
169
|
-
return config.layers.find((layer) => matches(path, layer.paths));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
96
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
173
97
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
|
|
174
98
|
}
|
package/src/domain/hashing.ts
CHANGED
|
@@ -1,43 +1,29 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import type { MarkdownDoc } from "./markdown.ts";
|
|
2
|
+
import { findSection, type MarkdownDoc } from "./markdown.ts";
|
|
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: string): boolean {
|
|
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: MarkdownDoc, element: Element): string {
|
|
26
|
-
if (element.form === "row") return sha256(normalize(elementLines(doc, element)));
|
|
27
|
-
const kept: string[] = [];
|
|
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: string): string {
|
|
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: MarkdownDoc, leftOut: readonly string[] = []): string {
|
|
21
|
+
const skipped = leftOut.flatMap((title) => {
|
|
22
|
+
const section = findSection(doc, title);
|
|
23
|
+
return section === null ? [] : [[section.heading.line, section.end] as const];
|
|
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: string[]): string {
|
package/src/domain/ids.ts
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"] as const;
|
|
4
|
-
export type Kind = (typeof KINDS)[number];
|
|
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: readonly Kind[] = ["GOAL", "PRINCIPLE", "LATER"];
|
|
8
|
-
/** Kinds that describe code, and so carry a managed planned/built status. */
|
|
9
|
-
export const CODE_KINDS: readonly Kind[] = ["ENT", "UC", "PORT", "ADP", "FLOW", "MOD"];
|
|
10
|
-
/** Kinds whose elements carry a managed status: code kinds and scenarios. */
|
|
11
|
-
export const STATUS_KINDS: readonly Kind[] = [...CODE_KINDS, "SCN"];
|
|
12
|
-
/** Kinds that must name their context or capability as a scope. */
|
|
13
|
-
export const SCOPED_KINDS: readonly Kind[] = ["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: string): boolean {
|
|
22
10
|
return EXACT.test(text);
|
|
23
11
|
}
|
|
24
12
|
|
|
25
|
-
export function
|
|
26
|
-
return
|
|
13
|
+
export function isSlug(text: string): boolean {
|
|
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: string): string {
|
|
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: string): string[] {
|
|
38
24
|
return [...new Set(text.match(ANYWHERE) ?? [])];
|
|
39
25
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { approvalState } from "./approvals.ts";
|
|
2
|
+
import { PATHS } from "./project/layout.ts";
|
|
3
|
+
import type { ProjectDocs } from "./project/project_docs.ts";
|
|
4
|
+
|
|
5
|
+
export type Step = "init" | "envision" | "shape" | "build";
|
|
6
|
+
|
|
7
|
+
export interface NextStep {
|
|
8
|
+
step: Step;
|
|
9
|
+
/** The feature the step works on, for shape and build. */
|
|
10
|
+
target?: string;
|
|
11
|
+
reason: string;
|
|
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: ProjectDocs): NextStep {
|
|
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.ts";
|
|
2
|
+
import { findScenarioIds, isScenarioId } from "../ids.ts";
|
|
3
|
+
import { findSection, parseMarkdown, type MarkdownDoc } from "../markdown.ts";
|
|
4
|
+
import { featurePath } from "./layout.ts";
|
|
5
|
+
|
|
6
|
+
/** A feature document, docs/features/<slug>.md: one slice of behaviour, its public API and its stack sketch. */
|
|
7
|
+
export interface Feature {
|
|
8
|
+
slug: string;
|
|
9
|
+
path: string;
|
|
10
|
+
text: string;
|
|
11
|
+
doc: MarkdownDoc;
|
|
12
|
+
/** `shaped` until build finishes it, then `built`; undefined when the line is missing or unknown. */
|
|
13
|
+
status: FeatureStatus | undefined;
|
|
14
|
+
/** The scenarios under `## Behaviour`, by heading `### SCN-<slug>.<name>: <title>`. */
|
|
15
|
+
scenarios: Scenario[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type FeatureStatus = "shaped" | "built";
|
|
19
|
+
export const FEATURE_STATUSES: readonly FeatureStatus[] = ["shaped", "built"];
|
|
20
|
+
|
|
21
|
+
export interface Scenario {
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
/** 0-based line of its heading. */
|
|
25
|
+
line: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const FEATURE_SECTIONS = ["Intent", "Behaviour", "Public API", "Stack sketch", "Decisions"] as const;
|
|
29
|
+
|
|
30
|
+
export function readFeature(slug: string, text: string): Feature {
|
|
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: MarkdownDoc): { line: number; value: string } | undefined {
|
|
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: MarkdownDoc): FeatureStatus | undefined {
|
|
43
|
+
const value = statusLine(doc)?.value;
|
|
44
|
+
return (FEATURE_STATUSES as readonly string[]).includes(value ?? "") ? (value as FeatureStatus) : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Scenario headings under `## Behaviour`: any heading whose text starts with a scenario ID and a colon. */
|
|
48
|
+
function scenariosOf(doc: MarkdownDoc): Scenario[] {
|
|
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: Feature): Array<{ id: string; line: number }> {
|
|
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: Array<{ id: string; line: number }> = [];
|
|
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
|
export type Snapshot = ReadonlyMap<string, string>;
|
|
@@ -6,36 +6,20 @@ export type Snapshot = ReadonlyMap<string, string>;
|
|
|
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
|
} as const;
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export function specPath(capability: string): string {
|
|
27
|
-
return `${PATHS.specsDir}/${capability}.md`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function filesUnder(snapshot: Snapshot, dir: string): string[] {
|
|
31
|
-
const prefix = `${dir}/`;
|
|
32
|
-
return [...snapshot.keys()].filter((path) => path.startsWith(prefix)).sort();
|
|
15
|
+
export function featurePath(slug: string): string {
|
|
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: string): string | undefined {
|
|
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, type ApprovalRecord } from "../approvals.ts";
|
|
2
|
+
import { parseConfig, type ConfigResult } from "../config.ts";
|
|
3
|
+
import { parseMarkdown, type MarkdownDoc } from "../markdown.ts";
|
|
4
|
+
import { readFeature, type Feature } from "./features.ts";
|
|
5
|
+
import { PATHS, slugOfFeaturePath, type Snapshot } from "./layout.ts";
|
|
6
|
+
import { readRelease, type ReleaseFeature } from "./release.ts";
|
|
7
|
+
|
|
8
|
+
/** Every Cruze document of a project, parsed: what the use cases work on. */
|
|
9
|
+
export interface ProjectDocs {
|
|
10
|
+
snapshot: Snapshot;
|
|
11
|
+
config: ConfigResult | undefined;
|
|
12
|
+
vision: MarkdownDoc | undefined;
|
|
13
|
+
architecture: MarkdownDoc | undefined;
|
|
14
|
+
release: ReleaseFeature[];
|
|
15
|
+
/** Feature documents, by slug, in slug order. */
|
|
16
|
+
features: Feature[];
|
|
17
|
+
approvals: ApprovalRecord[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildProjectDocs(snapshot: Snapshot): ProjectDocs {
|
|
21
|
+
const read = (path: string): MarkdownDoc | undefined => {
|
|
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: ProjectDocs, ref: string): string | undefined {
|
|
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, type MarkdownDoc } from "../markdown.ts";
|
|
2
|
+
import { tablesIn } from "../markdown_tables.ts";
|
|
3
|
+
|
|
4
|
+
/** One row of the vision's `## Release` table: a feature of the release, in build order, and the features it needs. */
|
|
5
|
+
export interface ReleaseFeature {
|
|
6
|
+
slug: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
needs: string[];
|
|
9
|
+
/** 0-based line of its row. */
|
|
10
|
+
line: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The release's features, from the first table under `## Release` with Feature and Needs columns. */
|
|
14
|
+
export function readRelease(vision: MarkdownDoc): ReleaseFeature[] {
|
|
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: string): number => 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: string): string {
|
|
29
|
+
return cell.trim().replace(/^`(.*)`$/, "$1").trim();
|
|
30
|
+
}
|
package/src/domain/scaffold.ts
CHANGED
|
@@ -13,7 +13,6 @@ export const INIT_SCAFFOLD: readonly ScaffoldEntry[] = [
|
|
|
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 @@ import type { SkillFiles } from "./skill_links.ts";
|
|
|
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)\//;
|