@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,81 +0,0 @@
|
|
|
1
|
-
import { ARCHITECTURE_KINDS } from "../architecture_sections.ts";
|
|
2
|
-
import { scopeOf } from "../ids.ts";
|
|
3
|
-
import { specPath } from "../project/layout.ts";
|
|
4
|
-
import { orphanDeltaElements, readDelta, type Delta } from "../project/deltas.ts";
|
|
5
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
6
|
-
import type { WorkItem } from "../project/work_items.ts";
|
|
7
|
-
import { deltaDecisionProblems } from "./decision_rules.ts";
|
|
8
|
-
import { elementProblems } from "./element_rules.ts";
|
|
9
|
-
import { error, type Problem } from "./problem.ts";
|
|
10
|
-
|
|
11
|
-
/** Merge records use this key for a spec file a delta created. */
|
|
12
|
-
export function capabilityKey(capability: string): string {
|
|
13
|
-
return `CAPABILITY-${capability}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Marks an element a landed change removed. */
|
|
17
|
-
export const REMOVED_MARK = "removed";
|
|
18
|
-
|
|
19
|
-
/** Rules for the spec and architecture deltas of a feature or standalone change. */
|
|
20
|
-
export function deltaProblems(view: ProjectView, item: WorkItem): Problem[] {
|
|
21
|
-
const { path, doc } = item;
|
|
22
|
-
const delta = readDelta(doc);
|
|
23
|
-
const merged = view.approvals.get(item.folder)?.merged ?? {};
|
|
24
|
-
const problems: Problem[] = [];
|
|
25
|
-
|
|
26
|
-
for (const orphan of orphanDeltaElements(doc)) {
|
|
27
|
-
problems.push(error(path, orphan.start, "delta-shape", `${orphan.id} must sit under an ADDED, MODIFIED or REMOVED requirement`));
|
|
28
|
-
}
|
|
29
|
-
for (const { element, section, scenarios } of delta.entries) {
|
|
30
|
-
const allowed = section === "spec" ? element.kind === "REQ" : ARCHITECTURE_KINDS.includes(element.kind);
|
|
31
|
-
if (!allowed) problems.push(error(path, element.start, "delta-shape", `${element.id} does not belong in the ${section} delta`));
|
|
32
|
-
problems.push(...elementProblems(path, doc, element));
|
|
33
|
-
for (const scenario of scenarios) {
|
|
34
|
-
problems.push(...elementProblems(path, doc, scenario));
|
|
35
|
-
if (scopeOf(scenario.id) !== scopeOf(element.id)) {
|
|
36
|
-
problems.push(error(path, scenario.start, "id-scope", `${scenario.id} must share its requirement's scope`));
|
|
37
|
-
}
|
|
38
|
-
problems.push(...opProblems(view, path, scenario.id, "ADDED", scenario.start, merged, element.op === "ADDED"));
|
|
39
|
-
}
|
|
40
|
-
problems.push(...opProblems(view, path, element.id, element.op ?? "ADDED", element.start, merged, false));
|
|
41
|
-
if (element.kind === "REQ" && element.op === "ADDED") problems.push(...capabilityProblems(view, item, delta, element.id, element.start));
|
|
42
|
-
}
|
|
43
|
-
for (const op of delta.capabilities) {
|
|
44
|
-
if (view.snapshot.has(specPath(op.capability)) && merged[capabilityKey(op.capability)] === undefined) {
|
|
45
|
-
problems.push(error(path, op.start, "capability-exists", `capability ${op.capability} already has ${specPath(op.capability)}`));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
problems.push(...deltaDecisionProblems(view, item));
|
|
49
|
-
return problems;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function opProblems(
|
|
53
|
-
view: ProjectView,
|
|
54
|
-
path: string,
|
|
55
|
-
id: string,
|
|
56
|
-
op: "ADDED" | "MODIFIED" | "REMOVED",
|
|
57
|
-
line: number,
|
|
58
|
-
merged: Record<string, string>,
|
|
59
|
-
underAddedRequirement: boolean,
|
|
60
|
-
): Problem[] {
|
|
61
|
-
const exists = view.living.elements.has(id);
|
|
62
|
-
const mergedHere = merged[id] !== undefined;
|
|
63
|
-
if (view.retired.has(id)) return [error(path, line, "retired-id", `${id} was removed earlier and can never be reused`)];
|
|
64
|
-
if (op === "ADDED" && exists && !mergedHere) {
|
|
65
|
-
// A scenario under a MODIFIED requirement may already exist; only a new requirement's scenarios must be new.
|
|
66
|
-
if (!id.startsWith("SCN-") || underAddedRequirement) return [error(path, line, "delta-op", `ADDED ${id} already exists in the living docs`)];
|
|
67
|
-
}
|
|
68
|
-
if ((op === "MODIFIED" || op === "REMOVED") && !exists && merged[id] !== REMOVED_MARK) {
|
|
69
|
-
return [error(path, line, "delta-op", `${op} ${id} does not exist in the living docs`)];
|
|
70
|
-
}
|
|
71
|
-
return [];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function capabilityProblems(view: ProjectView, item: WorkItem, delta: Delta, id: string, line: number): Problem[] {
|
|
75
|
-
const capability = scopeOf(id);
|
|
76
|
-
if (capability === undefined) return [];
|
|
77
|
-
const hasFile = view.snapshot.has(specPath(capability));
|
|
78
|
-
const addsFile = delta.capabilities.some((op) => op.capability === capability);
|
|
79
|
-
if (hasFile || addsFile) return [];
|
|
80
|
-
return [error(item.path, line, "capability-missing", `${id} needs ${specPath(capability)}, or an "ADDED CAPABILITY ${capability}" operation`)];
|
|
81
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { factsOf, type Element } from "../elements.ts";
|
|
2
|
-
import { findIds, scopeOf, SCOPED_KINDS, type Kind } from "../ids.ts";
|
|
3
|
-
import type { MarkdownDoc } from "../markdown.ts";
|
|
4
|
-
import { error, type Problem } from "./problem.ts";
|
|
5
|
-
|
|
6
|
-
/** Rules for a single element's body, shared by living documents and deltas. */
|
|
7
|
-
|
|
8
|
-
const REQUIRED_FACTS: Partial<Record<Kind, string[]>> = {
|
|
9
|
-
CTX: ["Purpose"],
|
|
10
|
-
ENT: ["Kind", "Module", "File"],
|
|
11
|
-
UC: ["Input", "Output", "Errors", "Uses", "Module", "File"],
|
|
12
|
-
PORT: ["Direction", "Operations", "Module", "File"],
|
|
13
|
-
ADP: ["Technology", "Module", "File"],
|
|
14
|
-
FLOW: ["Elements"],
|
|
15
|
-
MOD: ["Path", "Layer"],
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const ALLOWED_VALUES: Record<string, { kinds: Kind[]; values: string[] }> = {
|
|
19
|
-
Kind: { kinds: ["ENT"], values: ["aggregate", "entity", "value", "service", "policy", "event"] },
|
|
20
|
-
Direction: { kinds: ["PORT"], values: ["driven", "driving"] },
|
|
21
|
-
Layer: { kinds: ["MOD"], values: ["core", "domain", "application", "adapter", "composition"] },
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export function elementProblems(path: string, doc: MarkdownDoc, element: Element): Problem[] {
|
|
25
|
-
const problems: Problem[] = [];
|
|
26
|
-
if (SCOPED_KINDS.includes(element.kind) && scopeOf(element.id) === undefined) {
|
|
27
|
-
problems.push(error(path, element.start, "id-scope", `${element.id} needs a scope: ${element.kind}-<scope>.<name>`));
|
|
28
|
-
}
|
|
29
|
-
if (element.op === "REMOVED") return [...problems, ...removedProblems(path, doc, element)];
|
|
30
|
-
|
|
31
|
-
const facts = factsOf(doc, element);
|
|
32
|
-
for (const key of REQUIRED_FACTS[element.kind] ?? []) {
|
|
33
|
-
if (!facts.has(key)) problems.push(error(path, element.start, "required-fact", `${element.id} is missing "- ${key}:"`));
|
|
34
|
-
}
|
|
35
|
-
if (element.kind === "ADP" && !facts.has("Implements") && !facts.has("Drives")) {
|
|
36
|
-
problems.push(error(path, element.start, "required-fact", `${element.id} needs "- Implements:" or "- Drives:"`));
|
|
37
|
-
}
|
|
38
|
-
for (const [key, rule] of Object.entries(ALLOWED_VALUES)) {
|
|
39
|
-
const value = facts.get(key)?.[0];
|
|
40
|
-
if (rule.kinds.includes(element.kind) && value !== undefined && !rule.values.includes(value)) {
|
|
41
|
-
problems.push(error(path, element.start, "fact-value", `${element.id} ${key} must be one of ${rule.values.join(", ")}`));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (element.kind === "MOD") {
|
|
45
|
-
for (const glob of (facts.get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "")).filter((p) => /[*?[{]/.test(p))) {
|
|
46
|
-
problems.push(error(path, element.start, "module-path-glob", `${element.id} Path \`${glob}\` is a glob, which matches no file; name a directory ending in / or a single file, one per Path value`));
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const module = facts.get("Module")?.[0];
|
|
50
|
-
if (module !== undefined && !findIds(module).some((id) => id.startsWith("MOD-"))) {
|
|
51
|
-
problems.push(error(path, element.start, "fact-value", `${element.id} Module must name a MOD element`));
|
|
52
|
-
}
|
|
53
|
-
if (element.kind === "FLOW" && !bodyText(doc, element).includes("sequenceDiagram")) {
|
|
54
|
-
problems.push(error(path, element.start, "flow-diagram", `${element.id} needs a Mermaid sequenceDiagram`));
|
|
55
|
-
}
|
|
56
|
-
if (element.kind === "REQ") problems.push(...requirementProblems(path, doc, element));
|
|
57
|
-
if (element.kind === "SCN") problems.push(...scenarioProblems(path, doc, element));
|
|
58
|
-
return problems;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function removedProblems(path: string, doc: MarkdownDoc, element: Element): Problem[] {
|
|
62
|
-
const facts = factsOf(doc, element);
|
|
63
|
-
const problems: Problem[] = [];
|
|
64
|
-
if (!facts.has("Reason")) problems.push(error(path, element.start, "removed-reason", `REMOVED ${element.id} needs "- Reason:"`));
|
|
65
|
-
if (element.kind === "REQ" && !facts.has("Migration")) {
|
|
66
|
-
problems.push(error(path, element.start, "removed-reason", `REMOVED ${element.id} needs "- Migration:"`));
|
|
67
|
-
}
|
|
68
|
-
return problems;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function requirementProblems(path: string, doc: MarkdownDoc, element: Element): Problem[] {
|
|
72
|
-
const problems: Problem[] = [];
|
|
73
|
-
const ownBody = ownLines(doc, element).join("\n");
|
|
74
|
-
if (!/\b(SHALL|MUST)\b/.test(ownBody)) problems.push(error(path, element.start, "req-keyword", `${element.id} must state SHALL or MUST`));
|
|
75
|
-
const hasScenario = doc.headings.some((h) => h.line > element.start && h.line < element.end && h.level === element.level + 1 && h.text.startsWith("SCN-"));
|
|
76
|
-
if (!hasScenario) problems.push(error(path, element.start, "req-scenario", `${element.id} needs at least one scenario`));
|
|
77
|
-
return problems;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function scenarioProblems(path: string, doc: MarkdownDoc, element: Element): Problem[] {
|
|
81
|
-
const steps = ownLines(doc, element)
|
|
82
|
-
.map((line) => /^- (GIVEN|WHEN|THEN|AND) /.exec(line)?.[1])
|
|
83
|
-
.filter((step) => step !== undefined);
|
|
84
|
-
const order = steps.filter((step) => step !== "AND").join(" ");
|
|
85
|
-
if (!/^(GIVEN )*WHEN( THEN)+$/.test(order)) {
|
|
86
|
-
return [error(path, element.start, "scenario-steps", `${element.id} needs GIVEN steps (optional), one WHEN, then one or more THEN`)];
|
|
87
|
-
}
|
|
88
|
-
if (steps[0] === "AND") return [error(path, element.start, "scenario-steps", `${element.id} cannot start with AND`)];
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** The element's lines before its first nested heading, without the heading itself. */
|
|
93
|
-
function ownLines(doc: MarkdownDoc, element: Element): string[] {
|
|
94
|
-
const lines: string[] = [];
|
|
95
|
-
for (let i = element.start + 1; i < element.end; i++) {
|
|
96
|
-
if (!doc.inFence[i] && /^#{1,6} /.test(doc.lines[i] ?? "")) break;
|
|
97
|
-
lines.push(doc.lines[i] ?? "");
|
|
98
|
-
}
|
|
99
|
-
return lines;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function bodyText(doc: MarkdownDoc, element: Element): string {
|
|
103
|
-
return doc.lines.slice(element.start, element.end).join("\n");
|
|
104
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { factsOf, headingElements } from "../elements.ts";
|
|
2
|
-
import { readRows } from "../edits/managed_tables.ts";
|
|
3
|
-
import { findIds, kindOf } from "../ids.ts";
|
|
4
|
-
import type { MarkdownDoc } from "../markdown.ts";
|
|
5
|
-
import { PATHS } from "../project/layout.ts";
|
|
6
|
-
import { readDelta } from "../project/deltas.ts";
|
|
7
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
8
|
-
import { readRoadmapItems } from "../project/roadmap.ts";
|
|
9
|
-
import { warning, type Problem } from "./problem.ts";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* A `Future` fact keeps room for a need someone stated: a `LATER` capability of the vision, or a
|
|
13
|
-
* feature on the feature map or the roadmap. One that names neither reserves room for a need nobody
|
|
14
|
-
* stated, which the design reviewer treats as a finding; this makes it visible without a review.
|
|
15
|
-
*/
|
|
16
|
-
export function futureProblems(view: ProjectView): Problem[] {
|
|
17
|
-
const slugs = anchorSlugs(view);
|
|
18
|
-
const unanchored = (text: string): boolean =>
|
|
19
|
-
!findIds(text).some((id) => kindOf(id) === "LATER") && ![...slugs].some((slug) => new RegExp(`(^|[^a-z0-9-])${slug}([^a-z0-9-]|$)`).test(text));
|
|
20
|
-
const check = (path: string, doc: MarkdownDoc, from = 0, to = doc.lines.length): Problem[] =>
|
|
21
|
-
headingElements(doc, from, to).flatMap((element) =>
|
|
22
|
-
(factsOf(doc, element).get("Future") ?? [])
|
|
23
|
-
.filter(unanchored)
|
|
24
|
-
.map(() => warning(path, element.start, "future-unanchored", `${element.id} keeps room for a need its Future fact doesn't name; cite the LATER capability, or the feature on the map or roadmap, that it serves`)),
|
|
25
|
-
);
|
|
26
|
-
const living = view.living.docs.get(PATHS.architecture);
|
|
27
|
-
const deltas = view.items
|
|
28
|
-
.filter((item) => !item.archived && item.kind !== "change")
|
|
29
|
-
.flatMap((item) => readDelta(item.doc).entries.flatMap((entry) => check(item.path, item.doc, entry.element.start, entry.element.end)));
|
|
30
|
-
return [...(living === undefined ? [] : check(PATHS.architecture, living)), ...deltas];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Feature slugs a Future fact may name: the feature map's, implemented or future, and the roadmap's items. */
|
|
34
|
-
function anchorSlugs(view: ProjectView): Set<string> {
|
|
35
|
-
const vision = view.snapshot.get(PATHS.vision) ?? "";
|
|
36
|
-
const roadmap = view.snapshot.get(PATHS.roadmap);
|
|
37
|
-
const mapped = ["Implemented", "Future"].flatMap((heading) => readRows(vision, heading).map((row) => row[0] ?? ""));
|
|
38
|
-
const planned = roadmap === undefined ? [] : readRoadmapItems(roadmap).map((item) => item.slug);
|
|
39
|
-
return new Set([...mapped, ...planned].map((slug) => slug.trim()).filter((slug) => /^[a-z0-9-]+$/.test(slug)));
|
|
40
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { ARCHITECTURE_KINDS, sectionForKind } from "../architecture_sections.ts";
|
|
2
|
-
import { headingElements, statusOf, type Element } from "../elements.ts";
|
|
3
|
-
import { findIds, scopeOf, STATUS_KINDS } from "../ids.ts";
|
|
4
|
-
import type { MarkdownDoc } from "../markdown.ts";
|
|
5
|
-
import { PATHS } from "../project/layout.ts";
|
|
6
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
7
|
-
import { livingDecisionProblems } from "./decision_rules.ts";
|
|
8
|
-
import { elementProblems } from "./element_rules.ts";
|
|
9
|
-
import { error, warning, type Problem } from "./problem.ts";
|
|
10
|
-
|
|
11
|
-
/** Rules for the living docs: vision, architecture, specs, ADRs and the IDs they cite. */
|
|
12
|
-
export function livingProblems(view: ProjectView): Problem[] {
|
|
13
|
-
const problems: Problem[] = [];
|
|
14
|
-
for (const { id, places } of view.living.duplicates) {
|
|
15
|
-
problems.push(error(places[0]?.split(":")[0] ?? "", undefined, "duplicate-id", `${id} is defined more than once: ${places.join(", ")}`));
|
|
16
|
-
}
|
|
17
|
-
for (const [path, doc] of view.living.docs) {
|
|
18
|
-
problems.push(...malformedIdHeadings(path, doc));
|
|
19
|
-
if (path === PATHS.architecture) problems.push(...architectureProblems(path, doc, everApproved(view, path)));
|
|
20
|
-
else if (path.startsWith(`${PATHS.specsDir}/`)) problems.push(...specProblems(path, doc));
|
|
21
|
-
else if (path.startsWith(`${PATHS.adrDir}/`)) problems.push(...adrProblems(path, doc));
|
|
22
|
-
}
|
|
23
|
-
for (const path of [...view.living.docs.keys(), PATHS.roadmap]) {
|
|
24
|
-
const text = view.snapshot.get(path);
|
|
25
|
-
if (text === undefined) continue;
|
|
26
|
-
for (const id of findIds(text)) {
|
|
27
|
-
if (!view.living.elements.has(id)) problems.push(error(path, lineOf(text, id), "unknown-id", `${id} is cited but not defined in the living docs`));
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
problems.push(...livingDecisionProblems(view));
|
|
31
|
-
return problems;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** A heading shaped like `<KIND>-...: Title` whose ID breaks the grammar. */
|
|
35
|
-
export function malformedIdHeadings(path: string, doc: MarkdownDoc): Problem[] {
|
|
36
|
-
return doc.headings.flatMap((heading) => {
|
|
37
|
-
const match = /^(?:(?:ADDED|MODIFIED|REMOVED) )?([A-Z]{2,5}-\S*):\s/.exec(heading.text);
|
|
38
|
-
if (match?.[1] === undefined || findIds(match[1])[0] === match[1]) return [];
|
|
39
|
-
return [error(path, heading.line, "malformed-id", `"${match[1]}" is not a valid ID`)];
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Statuses are stamped by the first approval, so their absence before it is expected. */
|
|
44
|
-
function everApproved(view: ProjectView, path: string): boolean {
|
|
45
|
-
return [...view.approvals.values()].some((file) => file.approvals.some((record) => record.artifact === path));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function architectureProblems(path: string, doc: MarkdownDoc, approved: boolean): Problem[] {
|
|
49
|
-
const problems: Problem[] = [];
|
|
50
|
-
for (const element of headingElements(doc)) {
|
|
51
|
-
const section = [...doc.headings].reverse().find((h) => h.level === 2 && h.line < element.start);
|
|
52
|
-
if (!ARCHITECTURE_KINDS.includes(element.kind)) {
|
|
53
|
-
problems.push(error(path, element.start, "misplaced-element", `${element.id} does not belong in the architecture`));
|
|
54
|
-
} else if (element.kind !== "VIEW" && section?.text !== sectionForKind(element.kind)) {
|
|
55
|
-
problems.push(error(path, element.start, "misplaced-element", `${element.id} belongs under "## ${sectionForKind(element.kind)}"`));
|
|
56
|
-
}
|
|
57
|
-
problems.push(...elementProblems(path, doc, element), ...(approved ? statusProblems(path, doc, element) : []));
|
|
58
|
-
}
|
|
59
|
-
return problems;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function specProblems(path: string, doc: MarkdownDoc): Problem[] {
|
|
63
|
-
const capability = path.slice(path.lastIndexOf("/") + 1, -".md".length);
|
|
64
|
-
const problems: Problem[] = [];
|
|
65
|
-
const elements = headingElements(doc);
|
|
66
|
-
for (const element of elements) {
|
|
67
|
-
const expectedLevel = element.kind === "REQ" ? 2 : element.kind === "SCN" ? 3 : 0;
|
|
68
|
-
if (expectedLevel === 0) problems.push(error(path, element.start, "misplaced-element", `${element.id} does not belong in a spec`));
|
|
69
|
-
else if (element.level !== expectedLevel) problems.push(error(path, element.start, "heading-level", `${element.id} must be a level ${expectedLevel} heading`));
|
|
70
|
-
if (scopeOf(element.id) !== undefined && scopeOf(element.id) !== capability) {
|
|
71
|
-
problems.push(error(path, element.start, "id-scope", `${element.id} must use the scope "${capability}", the capability of this file`));
|
|
72
|
-
}
|
|
73
|
-
problems.push(...elementProblems(path, doc, element), ...statusProblems(path, doc, element));
|
|
74
|
-
}
|
|
75
|
-
return problems;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function adrProblems(path: string, doc: MarkdownDoc): Problem[] {
|
|
79
|
-
const text = doc.lines.join("\n");
|
|
80
|
-
return ["Status", "Date"]
|
|
81
|
-
.filter((key) => !new RegExp(`^- ${key}: `, "m").test(text))
|
|
82
|
-
.map((key) => error(path, undefined, "adr-fact", `the ADR is missing "- ${key}:"`));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function statusProblems(path: string, doc: MarkdownDoc, element: Element): Problem[] {
|
|
86
|
-
const status = statusOf(doc, element);
|
|
87
|
-
if (STATUS_KINDS.includes(element.kind) && status === undefined) {
|
|
88
|
-
return [warning(path, element.start, "missing-status", `${element.id} has no status yet; approving the document adds it`)];
|
|
89
|
-
}
|
|
90
|
-
if (!STATUS_KINDS.includes(element.kind) && status !== undefined) {
|
|
91
|
-
return [error(path, element.start, "unexpected-status", `${element.id} is not a kind that carries a status`)];
|
|
92
|
-
}
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function lineOf(text: string, needle: string): number | undefined {
|
|
97
|
-
const index = text.indexOf(needle);
|
|
98
|
-
return index === -1 ? undefined : text.slice(0, index).split("\n").length - 1;
|
|
99
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { insideModuleMap } from "../check/check_source.ts";
|
|
2
|
-
import { factsOf, isUnbuilt } from "../elements.ts";
|
|
3
|
-
import { matchesAny } from "../glob.ts";
|
|
4
|
-
import { findIds, kindOf } from "../ids.ts";
|
|
5
|
-
import type { MarkdownDoc } from "../markdown.ts";
|
|
6
|
-
import type { Delta } from "../project/deltas.ts";
|
|
7
|
-
import { isPlanned, readScope, readTasks, readTestPlan, type Scope, type Task } from "../project/plan_parts.ts";
|
|
8
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
9
|
-
import { error, warning, type Problem } from "./problem.ts";
|
|
10
|
-
import { planSizeProblems } from "./size_rules.ts";
|
|
11
|
-
|
|
12
|
-
const TEST_KINDS = ["behaviour", "contract", "domain", "smoke"];
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Rules for one buildable change: its scope names only things that can be built. Once it is
|
|
16
|
-
* planned, every scope ID is proved by a task, every delivered scenario has a behaviour test,
|
|
17
|
-
* and every port a built adapter implements has a contract test.
|
|
18
|
-
*/
|
|
19
|
-
export function changePlanProblems(
|
|
20
|
-
view: ProjectView,
|
|
21
|
-
path: string,
|
|
22
|
-
doc: MarkdownDoc,
|
|
23
|
-
delta: { delta: Delta; doc: MarkdownDoc },
|
|
24
|
-
available: Set<string>,
|
|
25
|
-
): Problem[] {
|
|
26
|
-
const scope = readScope(doc);
|
|
27
|
-
if (scope === null) return [error(path, undefined, "missing-section", 'the change needs a "## Scope" section')];
|
|
28
|
-
const problems: Problem[] = [...scopeTargetProblems(view, path, scope, delta.delta)];
|
|
29
|
-
if (!isPlanned(doc)) return [...problems, warning(path, undefined, "not-planned", "the change has no test plan or tasks yet; plan fills them")];
|
|
30
|
-
const { tasks, malformed } = readTasks(doc);
|
|
31
|
-
for (const line of malformed) {
|
|
32
|
-
problems.push(error(path, line, "task-format", "task lines read: - T<n>: `<owner>` in `<path>`[, `<path>`], proves <ID>[, <ID>]"));
|
|
33
|
-
}
|
|
34
|
-
if (tasks.length === 0 && malformed.length === 0) problems.push(error(path, undefined, "missing-tasks", "the change has no tasks"));
|
|
35
|
-
problems.push(...planSizeProblems(view, path, tasks.length));
|
|
36
|
-
|
|
37
|
-
const proved = new Set(tasks.flatMap((task) => task.proves));
|
|
38
|
-
const taskPaths = tasks.flatMap((task) => task.paths);
|
|
39
|
-
for (const task of tasks) {
|
|
40
|
-
for (const id of task.proves.filter((id) => !available.has(id))) {
|
|
41
|
-
problems.push(error(path, task.line, "unknown-id", `${task.number} proves ${id}, which is not defined`));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
problems.push(...outsideModuleMap(view, path, tasks, delta));
|
|
45
|
-
for (const id of [...scope.delivers, ...scope.builds]) {
|
|
46
|
-
if (kindOf(id) === "MOD") {
|
|
47
|
-
const modulePaths = modulePathsOf(view, delta.doc, delta.delta, id);
|
|
48
|
-
if (!taskPaths.some((p) => modulePaths.some((m) => p.startsWith(m)))) {
|
|
49
|
-
problems.push(error(path, scope.line, "unproved-scope", `${id} is in scope but no task writes inside ${modulePaths.join(" or ") || "its path"}; a module that owns other files, such as a build script, lists each under its Path`));
|
|
50
|
-
}
|
|
51
|
-
} else if (kindOf(id) === "REQ") {
|
|
52
|
-
const scenarios = delta.delta.entries.find((e) => e.element.id === id)?.scenarios.map((s) => s.id) ?? [];
|
|
53
|
-
if (!proved.has(id) && !scenarios.some((s) => proved.has(s))) problems.push(error(path, scope.line, "unproved-scope", `no task proves ${id} or its scenarios`));
|
|
54
|
-
} else if (!proved.has(id)) {
|
|
55
|
-
problems.push(error(path, scope.line, "unproved-scope", `${id} is in scope but no task proves it`));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const rows = readTestPlan(doc);
|
|
60
|
-
for (const row of rows) {
|
|
61
|
-
if (!TEST_KINDS.includes(row.kind)) problems.push(error(path, row.line, "test-kind", `test kind must be one of ${TEST_KINDS.join(", ")}`));
|
|
62
|
-
const subject = findIds(row.subject)[0];
|
|
63
|
-
if (subject === undefined || !available.has(subject)) problems.push(error(path, row.line, "unknown-id", `test subject "${row.subject}" is not a defined ID`));
|
|
64
|
-
if (row.file === "") problems.push(error(path, row.line, "test-file", "every test plan row names its test file"));
|
|
65
|
-
}
|
|
66
|
-
for (const id of scope.delivers.filter((id) => kindOf(id) === "SCN")) {
|
|
67
|
-
if (!rows.some((row) => findIds(row.subject)[0] === id && row.kind === "behaviour")) {
|
|
68
|
-
problems.push(error(path, scope.line, "missing-behaviour-test", `${id} is delivered but has no behaviour row in the test plan`));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
for (const adapter of scope.builds.filter((id) => kindOf(id) === "ADP")) {
|
|
72
|
-
const ports = (factsOfScoped(view, delta.doc, delta.delta, adapter).get("Implements") ?? []).flatMap(findIds).filter((id) => kindOf(id) === "PORT");
|
|
73
|
-
for (const port of ports.filter((p) => !rows.some((row) => findIds(row.subject)[0] === p && row.kind === "contract"))) {
|
|
74
|
-
problems.push(error(path, scope.line, "missing-contract-test", `${adapter} implements ${port} but the test plan has no contract row for ${port}`));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return problems;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Scope IDs must come from the item's deltas or be planned in the living docs. */
|
|
81
|
-
function scopeTargetProblems(view: ProjectView, path: string, scope: Scope, delta: Delta): Problem[] {
|
|
82
|
-
const problems: Problem[] = [];
|
|
83
|
-
for (const id of [...scope.delivers, ...scope.builds]) {
|
|
84
|
-
if (delta.ids.has(id)) continue;
|
|
85
|
-
const living = view.living.elements.get(id);
|
|
86
|
-
if (living === undefined) problems.push(error(path, scope.line, "unknown-id", `${id} is in scope but defined nowhere`));
|
|
87
|
-
else if (!isUnbuilt(living.doc, living.element)) {
|
|
88
|
-
problems.push(error(path, scope.line, "scope-not-planned", `${id} is already built; change it through a MODIFIED operation in the delta`));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
for (const id of scope.removes) {
|
|
92
|
-
const removal = delta.entries.find((e) => e.element.id === id && e.element.op === "REMOVED");
|
|
93
|
-
if (removal === undefined) problems.push(error(path, scope.line, "scope-removes", `${id} is in Removes but the delta has no REMOVED operation for it`));
|
|
94
|
-
}
|
|
95
|
-
if (scope.delivers.length + scope.builds.length + scope.removes.length === 0) {
|
|
96
|
-
problems.push(warning(path, scope.line, "empty-scope", "the scope delivers and builds nothing"));
|
|
97
|
-
}
|
|
98
|
-
return problems;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** An element's facts as this item will build it: from its delta when it has one, else from the living docs. */
|
|
102
|
-
function factsOfScoped(view: ProjectView, doc: MarkdownDoc, delta: Delta, id: string): Map<string, string[]> {
|
|
103
|
-
const fromDelta = delta.entries.find((e) => e.element.id === id);
|
|
104
|
-
const living = view.living.elements.get(id);
|
|
105
|
-
return fromDelta !== undefined ? factsOf(doc, fromDelta.element) : living !== undefined ? factsOf(living.doc, living.element) : new Map<string, string[]>();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function modulePathsOf(view: ProjectView, doc: MarkdownDoc, delta: Delta, id: string): string[] {
|
|
109
|
-
return (factsOfScoped(view, doc, delta, id).get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "")).filter((p) => p !== "");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Task paths that `cruze check` would report as outside every module, caught while the plan can still fix the module map. */
|
|
113
|
-
function outsideModuleMap(view: ProjectView, path: string, tasks: Task[], delta: { delta: Delta; doc: MarkdownDoc }): Problem[] {
|
|
114
|
-
const source = view.config?.config?.source ?? [];
|
|
115
|
-
const removed = new Set(delta.delta.entries.filter((e) => e.element.op === "REMOVED").map((e) => e.element.id));
|
|
116
|
-
const modules = new Set([...view.living.elements.keys(), ...delta.delta.entries.map((e) => e.element.id)].filter((id) => kindOf(id) === "MOD" && !removed.has(id)));
|
|
117
|
-
const modulePaths = [...modules].flatMap((id) => modulePathsOf(view, delta.doc, delta.delta, id));
|
|
118
|
-
if (modulePaths.length === 0) return [];
|
|
119
|
-
return tasks.flatMap((task) =>
|
|
120
|
-
task.paths
|
|
121
|
-
.filter((file) => matchesAny(file, source) && !insideModuleMap(file, modulePaths))
|
|
122
|
-
.map((file) => warning(path, task.line, "outside-module-map", `${task.number} writes ${file}, which no module's Path covers; add it to a module's Path or move the file`)),
|
|
123
|
-
);
|
|
124
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { factsOf } from "../elements.ts";
|
|
2
|
-
import { matchesAny } from "../glob.ts";
|
|
3
|
-
import { findIds } from "../ids.ts";
|
|
4
|
-
import { parseMarkdown, type MarkdownDoc } from "../markdown.ts";
|
|
5
|
-
import { PATHS } from "../project/layout.ts";
|
|
6
|
-
import { readRoadmapItems } from "../project/roadmap.ts";
|
|
7
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
8
|
-
import { error, warning, type Problem } from "./problem.ts";
|
|
9
|
-
|
|
10
|
-
/** Rules that span the project: leftover template guides, the roadmap, the config and the README. */
|
|
11
|
-
|
|
12
|
-
const GUIDE = /(?<!`)<(?!!--|\/|https?:|br\b)[A-Za-z][^<>\n]*>(?!`)/;
|
|
13
|
-
const PLACEHOLDER = /\{\{[a-z_]+\}\}/;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Unfilled `<guides>` and `{{placeholders}}` outside code, in living docs and active work.
|
|
17
|
-
* Managed blocks are skipped: the CLI writes them, and they may quote text such as `<COMMAND>`.
|
|
18
|
-
*/
|
|
19
|
-
export function templateLeftovers(view: ProjectView): Problem[] {
|
|
20
|
-
const paths = [...view.living.docs.keys(), PATHS.roadmap, PATHS.glossary, ...view.items.filter((i) => !i.archived).map((i) => i.path)];
|
|
21
|
-
return paths.flatMap((path) => {
|
|
22
|
-
const text = view.snapshot.get(path);
|
|
23
|
-
if (text === undefined) return [];
|
|
24
|
-
const doc = parseMarkdown(text);
|
|
25
|
-
return doc.lines.flatMap((line, i) => {
|
|
26
|
-
if (doc.inFence[i] || doc.inManaged[i]) return [];
|
|
27
|
-
const withoutCode = line.replace(/`[^`]*`/g, "");
|
|
28
|
-
if (GUIDE.test(withoutCode) || PLACEHOLDER.test(withoutCode)) {
|
|
29
|
-
return [error(path, i, "template-leftover", "replace the template guide or placeholder on this line")];
|
|
30
|
-
}
|
|
31
|
-
return [];
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function roadmapProblems(view: ProjectView): Problem[] {
|
|
37
|
-
const text = view.snapshot.get(PATHS.roadmap);
|
|
38
|
-
if (text === undefined) return [];
|
|
39
|
-
const doc = parseMarkdown(text);
|
|
40
|
-
const problems: Problem[] = [];
|
|
41
|
-
const items = new Map<string, { blockedBy: string[]; line: number }>();
|
|
42
|
-
for (const { slug: item, kind, blockedBy, line } of readRoadmapItems(text)) {
|
|
43
|
-
if (items.has(item)) problems.push(error(PATHS.roadmap, line, "roadmap-item", `item ${item} appears twice`));
|
|
44
|
-
if (!["feature", "change"].includes(kind)) problems.push(error(PATHS.roadmap, line, "roadmap-item", `item ${item} kind must be feature or change`));
|
|
45
|
-
items.set(item, { blockedBy, line });
|
|
46
|
-
}
|
|
47
|
-
for (const [item, { blockedBy, line }] of items) {
|
|
48
|
-
for (const blocker of blockedBy.filter((b) => !items.has(b))) problems.push(error(PATHS.roadmap, line, "roadmap-blocker", `${item} is blocked by unknown item ${blocker}`));
|
|
49
|
-
}
|
|
50
|
-
const cycle = findCycle(items);
|
|
51
|
-
if (cycle !== null) problems.push(error(PATHS.roadmap, undefined, "roadmap-cycle", `blocking cycle: ${cycle.join(" -> ")}`));
|
|
52
|
-
const coverage = doc.lines.join("\n").split(/^## Coverage$/m)[1] ?? "";
|
|
53
|
-
for (const goal of [...view.living.elements.keys()].filter((id) => id.startsWith("GOAL-"))) {
|
|
54
|
-
if (!findIds(coverage).includes(goal)) problems.push(warning(PATHS.roadmap, undefined, "goal-uncovered", `${goal} has no items in the coverage table`));
|
|
55
|
-
}
|
|
56
|
-
return problems;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function findCycle(items: Map<string, { blockedBy: string[] }>): string[] | null {
|
|
60
|
-
const state = new Map<string, "visiting" | "done">();
|
|
61
|
-
const visit = (item: string, path: string[]): string[] | null => {
|
|
62
|
-
if (state.get(item) === "done") return null;
|
|
63
|
-
if (state.get(item) === "visiting") return [...path.slice(path.indexOf(item)), item];
|
|
64
|
-
state.set(item, "visiting");
|
|
65
|
-
for (const next of items.get(item)?.blockedBy ?? []) {
|
|
66
|
-
const found = visit(next, [...path, item]);
|
|
67
|
-
if (found !== null) return found;
|
|
68
|
-
}
|
|
69
|
-
state.set(item, "done");
|
|
70
|
-
return null;
|
|
71
|
-
};
|
|
72
|
-
for (const item of items.keys()) {
|
|
73
|
-
const found = visit(item, []);
|
|
74
|
-
if (found !== null) return found;
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function configProblems(view: ProjectView): Problem[] {
|
|
80
|
-
if (view.config === null) return [error(PATHS.config, undefined, "config-missing", "the project has no .cruze/config.yaml; run cruze init")];
|
|
81
|
-
const problems = view.config.problems.map((message) => error(PATHS.config, undefined, "config", message));
|
|
82
|
-
const config = view.config.config;
|
|
83
|
-
const architecture = view.living.docs.get(PATHS.architecture);
|
|
84
|
-
if (config === null || architecture === undefined || config.layers.length === 0) return problems;
|
|
85
|
-
for (const { element } of [...view.living.elements.values()].filter((e) => e.id.startsWith("MOD-"))) {
|
|
86
|
-
for (const path of modulePaths(architecture, element)) {
|
|
87
|
-
const probe = path.endsWith("/") ? `${path}probe.file` : path;
|
|
88
|
-
const layers = config.layers.filter((layer) => matchesAny(probe, layer.paths));
|
|
89
|
-
if (layers.length !== 1) {
|
|
90
|
-
problems.push(warning(PATHS.config, undefined, "module-layer", `${element.id} path ${path} falls in ${layers.length} layers; it should fall in exactly one`));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return problems;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function modulePaths(doc: MarkdownDoc, element: Parameters<typeof factsOf>[1]): string[] {
|
|
98
|
-
return (factsOf(doc, element).get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? ""));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Once the vision is approved, the README says what the project is under its title.
|
|
103
|
-
* Images, badges and HTML lines don't count as that overview.
|
|
104
|
-
*/
|
|
105
|
-
export function readmeProblems(view: ProjectView): Problem[] {
|
|
106
|
-
const text = view.snapshot.get(PATHS.readme);
|
|
107
|
-
const visionApproved = [...view.approvals.values()].some((file) => file.approvals.some((record) => record.artifact === PATHS.vision));
|
|
108
|
-
if (text === undefined || !visionApproved) return [];
|
|
109
|
-
const doc = parseMarkdown(text);
|
|
110
|
-
const title = doc.lines.findIndex((line) => /^# /.test(line));
|
|
111
|
-
const next = doc.lines.findIndex((line, i) => i > title && /^#{1,6} /.test(line));
|
|
112
|
-
const intro = doc.lines.slice(title + 1, next === -1 ? undefined : next);
|
|
113
|
-
const overview = intro.some((line, i) => !doc.inFence[title + 1 + i] && line.trim() !== "" && !/^\s*(!\[|\[!\[|<)/.test(line));
|
|
114
|
-
return overview ? [] : [warning(PATHS.readme, Math.max(title, 0), "readme-overview", "add a paragraph under the title saying what the project is, who it is for and why, from docs/vision.md")];
|
|
115
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_CHANGE_LIMITS, type ChangeLimits } from "../config.ts";
|
|
2
|
-
import { kindOf } from "../ids.ts";
|
|
3
|
-
import type { ChangeRow } from "../project/plan_parts.ts";
|
|
4
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
5
|
-
import { warning, type Problem } from "./problem.ts";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Every change pays for its own plan review, code review, verifier run and land, so a change
|
|
9
|
-
* should carry enough work to be worth them, and no more than one review and one run can hold.
|
|
10
|
-
*/
|
|
11
|
-
export function changeLimits(view: ProjectView): ChangeLimits {
|
|
12
|
-
return view.config?.config?.changes ?? DEFAULT_CHANGE_LIMITS;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** The rules a size warning comes from. An item stays outside the band only with the user's recorded acceptance. */
|
|
16
|
-
export const SIZE_RULES = ["feature-size", "change-too-small", "change-too-large", "plan-too-large"];
|
|
17
|
-
|
|
18
|
-
/** Whether the user accepted this feature or change outside the band, under `changes.exceptions`. */
|
|
19
|
-
export function sizeAccepted(view: ProjectView, ref: string): boolean {
|
|
20
|
-
return changeLimits(view).exceptions.some((exception) => exception.item === ref);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** A feature's changes not yet landed that fall outside the size band. A feature of one change has nothing to merge into. */
|
|
24
|
-
export function changeRowSizeProblems(view: ProjectView, feature: { path: string; ref: string }, rows: ChangeRow[], landed: Set<string>): Problem[] {
|
|
25
|
-
const limits = changeLimits(view);
|
|
26
|
-
const problems: Problem[] = [];
|
|
27
|
-
for (const row of rows.filter((r) => !landed.has(r.change) && !sizeAccepted(view, `${feature.ref}/${r.change}`))) {
|
|
28
|
-
if (rows.length > 1 && row.builds.length < limits.minBuilds) {
|
|
29
|
-
problems.push(warning(feature.path, row.line, "change-too-small", `${row.change} builds ${row.builds.length} element(s), under ${limits.minBuilds}; merge it into the change it continues, since each change pays for its own plan, review and verify`));
|
|
30
|
-
}
|
|
31
|
-
const scenarios = row.delivers.filter((id) => kindOf(id) === "SCN").length;
|
|
32
|
-
if (scenarios > limits.maxScenarios) {
|
|
33
|
-
problems.push(warning(feature.path, row.line, "change-too-large", `${row.change} delivers ${scenarios} scenarios, over ${limits.maxScenarios}; split it, since one verifier run has to go through them all`));
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return problems;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** A plan with more tasks than one code review and one verify can hold. */
|
|
40
|
-
export function planSizeProblems(view: ProjectView, path: string, tasks: number): Problem[] {
|
|
41
|
-
const { maxTasks } = changeLimits(view);
|
|
42
|
-
const ref = view.items.find((item) => item.path === path)?.ref ?? path;
|
|
43
|
-
if (tasks <= maxTasks || sizeAccepted(view, ref)) return [];
|
|
44
|
-
return [warning(path, undefined, "plan-too-large", `the plan has ${tasks} tasks, over ${maxTasks}; split the change through rethink at feature level`)];
|
|
45
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { rowElements } from "../elements.ts";
|
|
2
|
-
import { findSection } from "../markdown.ts";
|
|
3
|
-
import { splitRow } from "../markdown_tables.ts";
|
|
4
|
-
import { PATHS } from "../project/layout.ts";
|
|
5
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
6
|
-
import { error, type Problem } from "./problem.ts";
|
|
7
|
-
|
|
8
|
-
/** The section and the cells after the ID that each long-term row of the vision holds. */
|
|
9
|
-
const INTENT_ROWS: Record<string, { section: string; cells: string[] }> = {
|
|
10
|
-
PRINCIPLE: { section: "Principles", cells: ["Principle", "Why"] },
|
|
11
|
-
LATER: { section: "Must stay possible", cells: ["Capability", "Likelihood", "Why", "Leave room for"] },
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export const LIKELIHOODS = ["likely", "possible"];
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* The vision's principles and the capabilities that must stay possible are rows, so designs cite
|
|
18
|
-
* them and approvals pin them. One project kept its principles in a seed note and its long-term
|
|
19
|
-
* plans in the user's head: a feature reversed a principle unnoticed, and a later review proposed
|
|
20
|
-
* removing what those plans needed.
|
|
21
|
-
*/
|
|
22
|
-
export function visionProblems(view: ProjectView): Problem[] {
|
|
23
|
-
const doc = view.living.docs.get(PATHS.vision);
|
|
24
|
-
if (doc === undefined) return [];
|
|
25
|
-
return rowElements(doc).flatMap((row) => {
|
|
26
|
-
const shape = INTENT_ROWS[row.kind];
|
|
27
|
-
if (shape === undefined) return [];
|
|
28
|
-
const section = findSection(doc, shape.section);
|
|
29
|
-
if (section === null || row.start < section.start || row.start >= section.end) {
|
|
30
|
-
return [error(PATHS.vision, row.start, "vision-row", `${row.id} belongs in the table under ## ${shape.section}`)];
|
|
31
|
-
}
|
|
32
|
-
const cells = splitRow(doc.lines[row.start] ?? "").slice(1);
|
|
33
|
-
if (cells.length !== shape.cells.length || cells.some((cell) => cell === "")) {
|
|
34
|
-
return [error(PATHS.vision, row.start, "vision-row", `${row.id} needs ${shape.cells.join(", ")}, each filled`)];
|
|
35
|
-
}
|
|
36
|
-
if (row.kind === "LATER" && !LIKELIHOODS.includes(cells[1] ?? "")) {
|
|
37
|
-
return [error(PATHS.vision, row.start, "vision-row", `${row.id}'s likelihood must be ${LIKELIHOODS.join(" or ")}`)];
|
|
38
|
-
}
|
|
39
|
-
return [];
|
|
40
|
-
});
|
|
41
|
-
}
|