@printpractical/cruze 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -118
- package/dist/adapters/inbound/cli/cli_context.js +0 -27
- package/dist/adapters/inbound/cli/commands/code.js +5 -18
- package/dist/adapters/inbound/cli/commands/documents.js +11 -37
- package/dist/adapters/inbound/cli/commands/init.js +1 -1
- package/dist/adapters/inbound/cli/commands/install.js +1 -0
- package/dist/adapters/inbound/cli/commands/review.js +2 -9
- package/dist/adapters/inbound/cli/commands/status.js +16 -63
- package/dist/adapters/inbound/cli/commands.js +10 -37
- package/dist/adapters/inbound/cli/parse_command.js +0 -54
- package/dist/adapters/outbound/git_repository.js +2 -71
- package/dist/app/ports/repository.js +0 -12
- package/dist/app/project_context.js +15 -25
- package/dist/app/use_cases/approve_document.js +29 -0
- package/dist/app/use_cases/check_code.js +21 -56
- package/dist/app/use_cases/install_skills.js +7 -1
- package/dist/app/use_cases/new_document.js +25 -0
- package/dist/app/use_cases/project_status.js +43 -67
- package/dist/app/use_cases/run_review.js +11 -30
- package/dist/app/use_cases/validate_project.js +8 -5
- package/dist/domain/approvals.js +51 -0
- package/dist/domain/check/check_source.js +3 -32
- package/dist/domain/check/imports.js +3 -3
- package/dist/domain/check/rust_imports.js +25 -3
- package/dist/domain/config.js +8 -84
- package/dist/domain/hashing.js +16 -30
- package/dist/domain/ids.js +13 -27
- package/dist/domain/next_step.js +41 -0
- package/dist/domain/project/features.js +71 -0
- package/dist/domain/project/layout.js +11 -27
- package/dist/domain/project/project_docs.js +48 -0
- package/dist/domain/project/release.js +30 -0
- package/dist/domain/scaffold.js +0 -1
- package/dist/domain/skill_budget.js +4 -4
- package/dist/domain/trace.js +8 -25
- package/dist/domain/validation/document_rules.js +65 -0
- package/dist/domain/validation/release_rules.js +45 -0
- package/dist/domain/validation/review_rules.js +21 -0
- package/dist/domain/validation/validate_project.js +20 -27
- package/dist/domain/versions/cruze_version.js +3 -9
- package/dist/main.js +1 -1
- package/package.json +1 -1
- package/skills/asking/SKILL.md +51 -0
- package/skills/behavioural-testing/SKILL.md +14 -16
- package/skills/behavioural-testing/examples.md +1 -2
- package/skills/behavioural-testing/fakes.md +2 -2
- package/skills/build/SKILL.md +34 -38
- package/skills/dependency-approval/SKILL.md +34 -31
- package/skills/envision/SKILL.md +23 -18
- package/skills/envision/architecture.md +16 -0
- package/skills/envision/breakdown.md +14 -0
- package/skills/envision/possibilities.md +6 -7
- package/skills/fix/SKILL.md +34 -0
- package/skills/formats/SKILL.md +39 -87
- package/skills/formats/config.md +47 -0
- package/skills/formats/templates/architecture.md +18 -74
- package/skills/formats/templates/feature.md +18 -34
- package/skills/formats/templates/vision.md +15 -27
- package/skills/hexagonal-design/SKILL.md +11 -11
- package/skills/hexagonal-design/contracts.md +3 -3
- package/skills/hexagonal-design/fundamentals.md +1 -1
- package/skills/hexagonal-design/languages/cpp.md +1 -1
- package/skills/hexagonal-design/languages/rust.md +1 -1
- package/skills/hexagonal-design/languages/swift.md +2 -2
- package/skills/hexagonal-design/runtime.md +2 -2
- package/skills/research/SKILL.md +4 -4
- package/skills/research/evaluation.md +1 -1
- package/skills/review/SKILL.md +27 -0
- package/skills/roles/SKILL.md +8 -59
- package/skills/roles/code-reviewer.md +22 -32
- package/skills/roles/researcher.md +3 -3
- package/skills/shape/SKILL.md +37 -0
- package/skills/shape/self-check.md +12 -0
- package/src/adapters/inbound/cli/cli_context.ts +0 -27
- package/src/adapters/inbound/cli/commands/code.ts +5 -18
- package/src/adapters/inbound/cli/commands/documents.ts +11 -37
- package/src/adapters/inbound/cli/commands/init.ts +1 -1
- package/src/adapters/inbound/cli/commands/install.ts +1 -0
- package/src/adapters/inbound/cli/commands/review.ts +2 -9
- package/src/adapters/inbound/cli/commands/status.ts +16 -63
- package/src/adapters/inbound/cli/commands.ts +10 -37
- package/src/adapters/inbound/cli/parse_command.ts +0 -54
- package/src/adapters/outbound/git_repository.ts +2 -71
- package/src/app/ports/repository.ts +0 -12
- package/src/app/project_context.ts +15 -25
- package/src/app/use_cases/approve_document.ts +29 -0
- package/src/app/use_cases/check_code.ts +21 -56
- package/src/app/use_cases/install_skills.ts +7 -1
- package/src/app/use_cases/new_document.ts +25 -0
- package/src/app/use_cases/project_status.ts +49 -73
- package/src/app/use_cases/run_review.ts +11 -30
- package/src/app/use_cases/validate_project.ts +8 -5
- package/src/domain/approvals.ts +51 -0
- package/src/domain/check/check_source.ts +3 -32
- package/src/domain/check/imports.ts +3 -3
- package/src/domain/check/rust_imports.ts +25 -3
- package/src/domain/config.ts +9 -85
- package/src/domain/hashing.ts +16 -30
- package/src/domain/ids.ts +13 -27
- package/src/domain/next_step.ts +41 -0
- package/src/domain/project/features.ts +71 -0
- package/src/domain/project/layout.ts +11 -27
- package/src/domain/project/project_docs.ts +48 -0
- package/src/domain/project/release.ts +30 -0
- package/src/domain/scaffold.ts +0 -1
- package/src/domain/skill_budget.ts +4 -4
- package/src/domain/trace.ts +8 -25
- package/src/domain/validation/document_rules.ts +65 -0
- package/src/domain/validation/release_rules.ts +45 -0
- package/src/domain/validation/review_rules.ts +21 -0
- package/src/domain/validation/validate_project.ts +21 -28
- package/src/domain/versions/cruze_version.ts +3 -9
- package/src/main.ts +1 -1
- package/templates/init/AGENTS.md +4 -12
- package/templates/init/ci.yml +3 -3
- package/templates/init/config.yaml +8 -17
- package/templates/init/review/claude.yaml +2 -2
- package/dist/adapters/inbound/cli/argument_files.js +0 -29
- package/dist/adapters/inbound/cli/commands/diff.js +0 -23
- package/dist/adapters/inbound/cli/commands/journal.js +0 -32
- package/dist/adapters/inbound/cli/commands/progress.js +0 -55
- package/dist/adapters/inbound/cli/commands/realign.js +0 -28
- package/dist/adapters/inbound/cli/commands/show.js +0 -96
- package/dist/app/use_cases/abandon_work.js +0 -42
- package/dist/app/use_cases/approve_artifact.js +0 -197
- package/dist/app/use_cases/journal_events.js +0 -64
- package/dist/app/use_cases/land_change.js +0 -134
- package/dist/app/use_cases/new_project_doc.js +0 -48
- package/dist/app/use_cases/new_work_item.js +0 -75
- package/dist/app/use_cases/realign_done.js +0 -45
- package/dist/app/use_cases/realign_status.js +0 -106
- package/dist/app/use_cases/record_progress.js +0 -118
- package/dist/app/use_cases/show_elements.js +0 -18
- package/dist/app/use_cases/show_notes.js +0 -11
- package/dist/app/use_cases/show_scope.js +0 -51
- package/dist/app/use_cases/upstream_diff.js +0 -55
- package/dist/domain/approvals/agent_approval.js +0 -68
- package/dist/domain/approvals/evaluate.js +0 -36
- package/dist/domain/approvals/fingerprint.js +0 -52
- package/dist/domain/approvals/rebase.js +0 -39
- package/dist/domain/approvals/records.js +0 -50
- package/dist/domain/approvals/upstream_text.js +0 -36
- package/dist/domain/architecture_sections.js +0 -23
- package/dist/domain/check/decision_comments.js +0 -38
- package/dist/domain/decision_citations.js +0 -62
- package/dist/domain/edits/agents_file.js +0 -58
- package/dist/domain/edits/managed_tables.js +0 -60
- package/dist/domain/edits/progress_block.js +0 -33
- package/dist/domain/edits/status_lines.js +0 -52
- package/dist/domain/elements.js +0 -126
- package/dist/domain/journal.js +0 -109
- package/dist/domain/land/bookkeeping.js +0 -54
- package/dist/domain/land/merge_plan.js +0 -137
- package/dist/domain/land/text_ops.js +0 -66
- package/dist/domain/line_diff.js +0 -56
- package/dist/domain/project/artifact_ref.js +0 -42
- package/dist/domain/project/deltas.js +0 -49
- package/dist/domain/project/element_lookup.js +0 -124
- package/dist/domain/project/living_model.js +0 -55
- package/dist/domain/project/new_items.js +0 -48
- package/dist/domain/project/notes.js +0 -74
- package/dist/domain/project/plan_parts.js +0 -111
- package/dist/domain/project/progress.js +0 -43
- package/dist/domain/project/project_view.js +0 -72
- package/dist/domain/project/roadmap.js +0 -43
- package/dist/domain/project/scope_view.js +0 -134
- package/dist/domain/project/work_items.js +0 -68
- package/dist/domain/status/next_step.js +0 -181
- package/dist/domain/status/pending_rethink.js +0 -22
- package/dist/domain/status/verification.js +0 -29
- package/dist/domain/status/work_status.js +0 -125
- package/dist/domain/validation/decision_rules.js +0 -76
- package/dist/domain/validation/delta_rules.js +0 -81
- package/dist/domain/validation/element_rules.js +0 -104
- package/dist/domain/validation/future_rules.js +0 -40
- package/dist/domain/validation/living_rules.js +0 -99
- package/dist/domain/validation/plan_rules.js +0 -124
- package/dist/domain/validation/project_rules.js +0 -115
- package/dist/domain/validation/size_rules.js +0 -45
- package/dist/domain/validation/vision_rules.js +0 -41
- package/dist/domain/validation/work_item_rules.js +0 -175
- package/dist/domain/versions/realign_notes.js +0 -72
- package/dist/domain/versions/rule_versions.js +0 -29
- package/dist/domain/versions/scheduled_exceptions.js +0 -41
- package/skills/about/SKILL.md +0 -21
- package/skills/architect/SKILL.md +0 -49
- package/skills/architect/feature.md +0 -40
- package/skills/architect/project.md +0 -71
- package/skills/architect/tweak.md +0 -22
- package/skills/architect/walkthrough.md +0 -35
- package/skills/domain-language/SKILL.md +0 -37
- package/skills/envision/feature.md +0 -25
- package/skills/envision/intake.md +0 -28
- package/skills/envision/project.md +0 -24
- package/skills/explore/SKILL.md +0 -39
- package/skills/formats/reference/architecture.md +0 -84
- package/skills/formats/reference/config.md +0 -80
- package/skills/formats/reference/durable-docs.md +0 -74
- package/skills/formats/reference/specs.md +0 -65
- package/skills/formats/reference/work-items.md +0 -100
- package/skills/formats/templates/adr.md +0 -7
- package/skills/formats/templates/change.md +0 -29
- package/skills/formats/templates/glossary.md +0 -8
- package/skills/formats/templates/roadmap.md +0 -32
- package/skills/formats/templates/spec.md +0 -11
- package/skills/formats/templates/standalone-change.md +0 -46
- package/skills/grilling/SKILL.md +0 -90
- package/skills/grilling/illustrating.md +0 -38
- package/skills/improve/SKILL.md +0 -51
- package/skills/land/SKILL.md +0 -48
- package/skills/next/SKILL.md +0 -31
- package/skills/plan/SKILL.md +0 -62
- package/skills/plan/tasks.md +0 -35
- package/skills/plan/test-plan.md +0 -14
- package/skills/realign/SKILL.md +0 -37
- package/skills/realign/notes/0.0.10.md +0 -131
- package/skills/realign/notes/0.0.11.md +0 -15
- package/skills/realign/notes/0.0.12.md +0 -31
- package/skills/realign/notes/0.0.13.md +0 -11
- package/skills/realign/notes/0.0.2.md +0 -69
- package/skills/realign/notes/0.0.3.md +0 -11
- package/skills/realign/notes/0.0.4.md +0 -27
- package/skills/realign/notes/0.0.5.md +0 -38
- package/skills/realign/notes/0.0.6.md +0 -44
- package/skills/realign/notes/0.0.7.md +0 -11
- package/skills/realign/notes/0.0.8.md +0 -97
- package/skills/realign/notes/0.0.9.md +0 -59
- package/skills/realign/notes.md +0 -40
- package/skills/rethink/SKILL.md +0 -62
- package/skills/rethink/levels.md +0 -54
- package/skills/roadmap/SKILL.md +0 -54
- package/skills/roles/auditor.md +0 -35
- package/skills/roles/design-reviewer.md +0 -43
- package/skills/roles/design-rubric.md +0 -41
- package/skills/roles/plan-rubric.md +0 -11
- package/skills/roles/surveyor.md +0 -41
- package/skills/roles/verifier.md +0 -42
- package/skills/triage/SKILL.md +0 -37
- package/skills/verify/SKILL.md +0 -45
- package/skills/verify/manual-test.md +0 -23
- package/src/adapters/inbound/cli/argument_files.ts +0 -29
- package/src/adapters/inbound/cli/commands/diff.ts +0 -23
- package/src/adapters/inbound/cli/commands/journal.ts +0 -32
- package/src/adapters/inbound/cli/commands/progress.ts +0 -55
- package/src/adapters/inbound/cli/commands/realign.ts +0 -28
- package/src/adapters/inbound/cli/commands/show.ts +0 -96
- package/src/app/use_cases/abandon_work.ts +0 -42
- package/src/app/use_cases/approve_artifact.ts +0 -197
- package/src/app/use_cases/journal_events.ts +0 -64
- package/src/app/use_cases/land_change.ts +0 -134
- package/src/app/use_cases/new_project_doc.ts +0 -48
- package/src/app/use_cases/new_work_item.ts +0 -75
- package/src/app/use_cases/realign_done.ts +0 -45
- package/src/app/use_cases/realign_status.ts +0 -106
- package/src/app/use_cases/record_progress.ts +0 -118
- package/src/app/use_cases/show_elements.ts +0 -18
- package/src/app/use_cases/show_notes.ts +0 -11
- package/src/app/use_cases/show_scope.ts +0 -51
- package/src/app/use_cases/upstream_diff.ts +0 -55
- package/src/domain/approvals/agent_approval.ts +0 -68
- package/src/domain/approvals/evaluate.ts +0 -36
- package/src/domain/approvals/fingerprint.ts +0 -52
- package/src/domain/approvals/rebase.ts +0 -39
- package/src/domain/approvals/records.ts +0 -50
- package/src/domain/approvals/upstream_text.ts +0 -36
- package/src/domain/architecture_sections.ts +0 -23
- package/src/domain/check/decision_comments.ts +0 -38
- package/src/domain/decision_citations.ts +0 -62
- package/src/domain/edits/agents_file.ts +0 -58
- package/src/domain/edits/managed_tables.ts +0 -60
- package/src/domain/edits/progress_block.ts +0 -33
- package/src/domain/edits/status_lines.ts +0 -52
- package/src/domain/elements.ts +0 -126
- package/src/domain/journal.ts +0 -109
- package/src/domain/land/bookkeeping.ts +0 -54
- package/src/domain/land/merge_plan.ts +0 -137
- package/src/domain/land/text_ops.ts +0 -66
- package/src/domain/line_diff.ts +0 -56
- package/src/domain/project/artifact_ref.ts +0 -42
- package/src/domain/project/deltas.ts +0 -49
- package/src/domain/project/element_lookup.ts +0 -124
- package/src/domain/project/living_model.ts +0 -55
- package/src/domain/project/new_items.ts +0 -48
- package/src/domain/project/notes.ts +0 -74
- package/src/domain/project/plan_parts.ts +0 -111
- package/src/domain/project/progress.ts +0 -43
- package/src/domain/project/project_view.ts +0 -72
- package/src/domain/project/roadmap.ts +0 -43
- package/src/domain/project/scope_view.ts +0 -134
- package/src/domain/project/work_items.ts +0 -68
- package/src/domain/status/next_step.ts +0 -181
- package/src/domain/status/pending_rethink.ts +0 -22
- package/src/domain/status/verification.ts +0 -29
- package/src/domain/status/work_status.ts +0 -125
- package/src/domain/validation/decision_rules.ts +0 -76
- package/src/domain/validation/delta_rules.ts +0 -81
- package/src/domain/validation/element_rules.ts +0 -104
- package/src/domain/validation/future_rules.ts +0 -40
- package/src/domain/validation/living_rules.ts +0 -99
- package/src/domain/validation/plan_rules.ts +0 -124
- package/src/domain/validation/project_rules.ts +0 -115
- package/src/domain/validation/size_rules.ts +0 -45
- package/src/domain/validation/vision_rules.ts +0 -41
- package/src/domain/validation/work_item_rules.ts +0 -175
- package/src/domain/versions/realign_notes.ts +0 -72
- package/src/domain/versions/rule_versions.ts +0 -29
- package/src/domain/versions/scheduled_exceptions.ts +0 -41
- package/templates/init/gitattributes +0 -2
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Findings decided `rethink` for a feature or change that no rethink of it has followed yet.
|
|
6
|
-
* One project's realign recorded seven design findings on a feature as fixed "by a rethink before
|
|
7
|
-
* its next change is planned"; nothing showed them, and they surfaced only in a commit message.
|
|
8
|
-
*/
|
|
9
|
-
export function pendingRethinks(journal , item ) {
|
|
10
|
-
const lastRethink = journal
|
|
11
|
-
.filter((e) => e.event === "rethink" && e["item"] === item.ref)
|
|
12
|
-
.map((e) => e.at)
|
|
13
|
-
.sort()
|
|
14
|
-
.at(-1);
|
|
15
|
-
return journal.filter((e) => e.event === "disposition" && e["disposition"] === "rethink" && e["item"] === item.ref && (lastRethink === undefined || e.at > lastRethink));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Why work waits on a rethink, naming the first finding. */
|
|
19
|
-
export function pendingReason(item , pending ) {
|
|
20
|
-
const more = pending.length > 1 ? ` and ${pending.length - 1} more` : "";
|
|
21
|
-
return `${pending.length} finding(s) on ${item.ref} were decided for a rethink that hasn't happened yet: ${String(pending[0]?.["finding"] ?? "")}${more}`;
|
|
22
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Whether the user accepted a change at verify. The latest verification recorded for the change
|
|
6
|
-
* counts. A later approval keeps it only when the change's own design is unchanged, as when it is
|
|
7
|
-
* re-approved because something upstream moved; a changed plan means verifying again. Land's
|
|
8
|
-
* re-stamps don't count, since they follow the merge; an approval the agent gave does.
|
|
9
|
-
*/
|
|
10
|
-
export function isVerified(journal , change ) {
|
|
11
|
-
const byTime = (a , b ) => a.at.localeCompare(b.at);
|
|
12
|
-
const verification = journal.filter((e) => e.event === "verification" && e["item"] === change.ref).sort(byTime).at(-1);
|
|
13
|
-
if (verification?.["result"] !== "accepted") return false;
|
|
14
|
-
const approvals = journal.filter((e) => e.event === "approve" && e["artifact"] === change.path && !String(e["basis"] ?? "").startsWith("land ")).sort(byTime);
|
|
15
|
-
const verifiedHash = approvals.filter((e) => e.at < verification.at).at(-1)?.["hash"];
|
|
16
|
-
return approvals.filter((e) => e.at > verification.at).every((e) => e["hash"] === verifiedHash);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Whether verify sent the change back for a rethink that hasn't been recorded yet. Tasks the
|
|
21
|
-
* agent reopens at verify would otherwise make the change look ready to build again.
|
|
22
|
-
*/
|
|
23
|
-
export function awaitsRethink(journal , change , feature ) {
|
|
24
|
-
const verification = journal.filter((e) => e.event === "verification" && e["item"] === change.ref).sort((a, b) => a.at.localeCompare(b.at)).at(-1);
|
|
25
|
-
if (verification?.["result"] !== "sent-back" || verification["back_to"] !== "rethink") return false;
|
|
26
|
-
// An architecture-level rethink records no item, and counts too.
|
|
27
|
-
const refs = new Set([change.ref, feature?.ref, undefined]);
|
|
28
|
-
return !journal.some((e) => e.event === "rethink" && e.at > verification.at && refs.has(e["item"] ));
|
|
29
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { evaluateApproval, } from "../approvals/evaluate.js";
|
|
2
|
-
import { PATHS } from "../project/layout.js";
|
|
3
|
-
import { readDelta } from "../project/deltas.js";
|
|
4
|
-
import { readChangesTable, readScope } from "../project/plan_parts.js";
|
|
5
|
-
import { readProgress } from "../project/progress.js";
|
|
6
|
-
|
|
7
|
-
import { changesOf, featureOf, } from "../project/work_items.js";
|
|
8
|
-
import { pendingReason, pendingRethinks } from "./pending_rethink.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
export function projectStatus(view , branch ) {
|
|
41
|
-
const documents = [PATHS.vision, PATHS.architecture, PATHS.roadmap].filter((p) => view.snapshot.has(p)).map((p) => evaluateApproval(view, p));
|
|
42
|
-
const active = view.items.filter((item) => !item.archived);
|
|
43
|
-
const features = active
|
|
44
|
-
.filter((item) => item.kind === "feature")
|
|
45
|
-
.map((feature) => ({
|
|
46
|
-
ref: feature.ref,
|
|
47
|
-
path: feature.path,
|
|
48
|
-
approval: evaluateApproval(view, feature.path),
|
|
49
|
-
changes: changesOf(view.items, feature).map((change) => changeStatus(view, change)),
|
|
50
|
-
...withPending(view, feature),
|
|
51
|
-
}));
|
|
52
|
-
const standalone = active.filter((item) => item.kind === "standalone").map((change) => changeStatus(view, change));
|
|
53
|
-
const status = { documents, features, standalone };
|
|
54
|
-
const bound = activeChange(view, branch);
|
|
55
|
-
if (bound !== undefined) status.active = bound.ref;
|
|
56
|
-
return status;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function activeChange(view , branch ) {
|
|
60
|
-
if (branch === null) return undefined;
|
|
61
|
-
return view.items.find((item) => {
|
|
62
|
-
const progress = readProgress(item.doc);
|
|
63
|
-
return !item.archived && item.kind !== "feature" && progress.landed === undefined && progress.branch === branch;
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function changeStatus(view , change ) {
|
|
68
|
-
const progress = readProgress(change.doc);
|
|
69
|
-
const status = {
|
|
70
|
-
ref: change.ref,
|
|
71
|
-
path: change.path,
|
|
72
|
-
approval: evaluateApproval(view, change.path),
|
|
73
|
-
tasksDone: [...progress.tasks.values()].filter((commit) => commit !== null).length,
|
|
74
|
-
tasksTotal: progress.tasks.size,
|
|
75
|
-
};
|
|
76
|
-
if (progress.branch !== undefined) status.branch = progress.branch;
|
|
77
|
-
if (progress.landed !== undefined) status.landed = progress.landed;
|
|
78
|
-
return { ...status, ...withPending(view, change) };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function withPending(view , item ) {
|
|
82
|
-
const pending = pendingRethinks(view.journal, item);
|
|
83
|
-
return pending.length === 0 ? {} : { rethinkPending: pending.map((e) => String(e["finding"] ?? "")) };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Why building may not start on this branch; empty when the build gate passes. */
|
|
87
|
-
export function buildGateReasons(view , branch ) {
|
|
88
|
-
const change = activeChange(view, branch);
|
|
89
|
-
if (change === undefined) return [`no change is bound to branch ${branch ?? "(none)"}; approve a planned change on this branch first`];
|
|
90
|
-
const required = [change.path];
|
|
91
|
-
const feature = featureOf(view.items, change);
|
|
92
|
-
if (feature !== undefined) required.push(feature.path);
|
|
93
|
-
if (view.snapshot.has(PATHS.architecture)) required.push(PATHS.architecture);
|
|
94
|
-
const approvals = required.flatMap((path) => {
|
|
95
|
-
const status = evaluateApproval(view, path);
|
|
96
|
-
if (status.state === "approved") return [];
|
|
97
|
-
const detail = status.changed.length > 0 ? `: ${status.changed.join(", ")} changed` : status.unjournaled === true ? ": the approval has no journal entry" : "";
|
|
98
|
-
return [`${path} is ${status.state}${detail}`];
|
|
99
|
-
});
|
|
100
|
-
const rethinks = [feature, change].flatMap((part) => {
|
|
101
|
-
const pending = part === undefined ? [] : pendingRethinks(view.journal, part);
|
|
102
|
-
return part === undefined || pending.length === 0 ? [] : [`${pendingReason(part, pending)}; run rethink`];
|
|
103
|
-
});
|
|
104
|
-
return [...approvals, ...rethinks, ...unlandedDependencies(view, change, feature)];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The changes this change's row in its feature's Changes table depends on that haven't landed on this branch. */
|
|
108
|
-
function unlandedDependencies(view , change , feature ) {
|
|
109
|
-
if (feature === undefined) return [];
|
|
110
|
-
const row = readChangesTable(feature.doc).find((r) => r.change === change.folderName);
|
|
111
|
-
const landed = new Set(changesOf(view.items, feature).filter((c) => readProgress(c.doc).landed !== undefined).map((c) => c.folderName));
|
|
112
|
-
return (row?.dependsOn ?? [])
|
|
113
|
-
.filter((dependency) => !landed.has(dependency))
|
|
114
|
-
.map((dependency) => `${change.ref} depends on ${dependency}, which hasn't landed here; land it, then merge main into this branch`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** The elements a change touches: its scope and its delta, not the elements it only cites, such as a test seam. */
|
|
118
|
-
export function touchedIds(view , change ) {
|
|
119
|
-
const feature = featureOf(view.items, change);
|
|
120
|
-
const scope = readScope(change.doc);
|
|
121
|
-
const ids = new Set([...(scope?.delivers ?? []), ...(scope?.builds ?? []), ...(scope?.removes ?? [])]);
|
|
122
|
-
const deltaDoc = feature?.doc ?? change.doc;
|
|
123
|
-
for (const id of readDelta(deltaDoc).ids) ids.add(id);
|
|
124
|
-
return new Set([...ids].filter((id) => !id.startsWith("RULE-")));
|
|
125
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { definedDecisions, findDecisionCitations, workItemSlugs, } from "../decision_citations.js";
|
|
2
|
-
|
|
3
|
-
import { findSection, } from "../markdown.js";
|
|
4
|
-
import { readDelta } from "../project/deltas.js";
|
|
5
|
-
import { PATHS } from "../project/layout.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { warning, } from "./problem.js";
|
|
9
|
-
|
|
10
|
-
const INSTEAD = "write what it decided into this text, or cite the ADR that records it";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The architecture cites only the decisions its own `## Decisions` defines. The other living docs
|
|
14
|
-
* cite none by number, and no living doc cites a feature's or change's, which is archived when it finishes.
|
|
15
|
-
*/
|
|
16
|
-
export function livingDecisionProblems(view ) {
|
|
17
|
-
const slugs = slugsOf(view);
|
|
18
|
-
const project = projectDecisions(view);
|
|
19
|
-
const problems = [];
|
|
20
|
-
for (const [path, doc] of view.living.docs) {
|
|
21
|
-
for (const citation of citationsIn(doc, slugs)) {
|
|
22
|
-
if (citation.qualifier !== undefined) {
|
|
23
|
-
problems.push(warning(path, citation.line, "decision-citation", `cites ${citation.text}, a decision of ${citation.qualifier}, which the living docs don't hold; ${INSTEAD}`));
|
|
24
|
-
} else if (path !== PATHS.architecture) {
|
|
25
|
-
problems.push(warning(path, citation.line, "decision-citation", `cites ${citation.text}, but only the architecture cites decisions by number, its own; ${INSTEAD}`));
|
|
26
|
-
} else {
|
|
27
|
-
const unknown = citation.numbers.filter((n) => !project.has(n));
|
|
28
|
-
if (unknown.length > 0) {
|
|
29
|
-
problems.push(warning(path, citation.line, "decision-citation", `cites ${unknown.map((n) => `D${n}`).join(", ")}, which "## Decisions" doesn't define; ${INSTEAD}`));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return problems;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A delta's elements merge into the living docs, where the work's own decisions don't follow them.
|
|
39
|
-
* They may cite a project decision the work doesn't renumber, or one the living element already cites.
|
|
40
|
-
*/
|
|
41
|
-
export function deltaDecisionProblems(view , item ) {
|
|
42
|
-
const slugs = slugsOf(view);
|
|
43
|
-
const project = projectDecisions(view);
|
|
44
|
-
const local = definedDecisions(sectionLines(item.doc, "Settled decisions"));
|
|
45
|
-
const problems = [];
|
|
46
|
-
for (const { element } of readDelta(item.doc).entries) {
|
|
47
|
-
const living = view.living.elements.get(element.id);
|
|
48
|
-
const carried = new Set(living === undefined ? [] : citationsIn(living.doc, slugs, living.element).flatMap((c) => (c.qualifier === undefined ? c.numbers : [])));
|
|
49
|
-
const allowed = (n ) => project.has(n) && (!local.has(n) || carried.has(n));
|
|
50
|
-
for (const citation of citationsIn(item.doc, slugs, element)) {
|
|
51
|
-
if (citation.qualifier === undefined && citation.numbers.every(allowed)) continue;
|
|
52
|
-
problems.push(warning(item.path, citation.line, "decision-citation", `${element.id} cites ${citation.text}, which won't mean the same in the living docs once this delta merges; ${INSTEAD}`));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return problems;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function citationsIn(doc , slugs , element ) {
|
|
59
|
-
const from = element === undefined ? doc.bodyStart : element.start + 1;
|
|
60
|
-
const to = element === undefined ? doc.lines.length : element.end;
|
|
61
|
-
return findDecisionCitations(doc.lines, slugs, (line) => line < from || line >= to || doc.inFence[line] === true || doc.inManaged[line] === true);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function projectDecisions(view ) {
|
|
65
|
-
const architecture = view.living.docs.get(PATHS.architecture);
|
|
66
|
-
return architecture === undefined ? new Set() : definedDecisions(sectionLines(architecture, "Decisions"));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function sectionLines(doc , title ) {
|
|
70
|
-
const range = findSection(doc, title);
|
|
71
|
-
return range === null ? [] : doc.lines.slice(range.start, range.end);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function slugsOf(view ) {
|
|
75
|
-
return workItemSlugs(view.items.map((item) => item.folderName));
|
|
76
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { ARCHITECTURE_KINDS } from "../architecture_sections.js";
|
|
2
|
-
import { scopeOf } from "../ids.js";
|
|
3
|
-
import { specPath } from "../project/layout.js";
|
|
4
|
-
import { orphanDeltaElements, readDelta, } from "../project/deltas.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { deltaDecisionProblems } from "./decision_rules.js";
|
|
8
|
-
import { elementProblems } from "./element_rules.js";
|
|
9
|
-
import { error, } from "./problem.js";
|
|
10
|
-
|
|
11
|
-
/** Merge records use this key for a spec file a delta created. */
|
|
12
|
-
export function capabilityKey(capability ) {
|
|
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 , item ) {
|
|
21
|
-
const { path, doc } = item;
|
|
22
|
-
const delta = readDelta(doc);
|
|
23
|
-
const merged = view.approvals.get(item.folder)?.merged ?? {};
|
|
24
|
-
const problems = [];
|
|
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 ,
|
|
54
|
-
path ,
|
|
55
|
-
id ,
|
|
56
|
-
op ,
|
|
57
|
-
line ,
|
|
58
|
-
merged ,
|
|
59
|
-
underAddedRequirement ,
|
|
60
|
-
) {
|
|
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 , item , delta , id , line ) {
|
|
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, } from "../elements.js";
|
|
2
|
-
import { findIds, scopeOf, SCOPED_KINDS, } from "../ids.js";
|
|
3
|
-
|
|
4
|
-
import { error, } from "./problem.js";
|
|
5
|
-
|
|
6
|
-
/** Rules for a single element's body, shared by living documents and deltas. */
|
|
7
|
-
|
|
8
|
-
const REQUIRED_FACTS = {
|
|
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 = {
|
|
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 , doc , element ) {
|
|
25
|
-
const problems = [];
|
|
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 , doc , element ) {
|
|
62
|
-
const facts = factsOf(doc, element);
|
|
63
|
-
const problems = [];
|
|
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 , doc , element ) {
|
|
72
|
-
const problems = [];
|
|
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 , doc , element ) {
|
|
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 , element ) {
|
|
94
|
-
const lines = [];
|
|
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 , element ) {
|
|
103
|
-
return doc.lines.slice(element.start, element.end).join("\n");
|
|
104
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { factsOf, headingElements } from "../elements.js";
|
|
2
|
-
import { readRows } from "../edits/managed_tables.js";
|
|
3
|
-
import { findIds, kindOf } from "../ids.js";
|
|
4
|
-
|
|
5
|
-
import { PATHS } from "../project/layout.js";
|
|
6
|
-
import { readDelta } from "../project/deltas.js";
|
|
7
|
-
|
|
8
|
-
import { readRoadmapItems } from "../project/roadmap.js";
|
|
9
|
-
import { warning, } from "./problem.js";
|
|
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 ) {
|
|
17
|
-
const slugs = anchorSlugs(view);
|
|
18
|
-
const unanchored = (text ) =>
|
|
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 , doc , from = 0, to = doc.lines.length) =>
|
|
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 ) {
|
|
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.js";
|
|
2
|
-
import { headingElements, statusOf, } from "../elements.js";
|
|
3
|
-
import { findIds, scopeOf, STATUS_KINDS } from "../ids.js";
|
|
4
|
-
|
|
5
|
-
import { PATHS } from "../project/layout.js";
|
|
6
|
-
|
|
7
|
-
import { livingDecisionProblems } from "./decision_rules.js";
|
|
8
|
-
import { elementProblems } from "./element_rules.js";
|
|
9
|
-
import { error, warning, } from "./problem.js";
|
|
10
|
-
|
|
11
|
-
/** Rules for the living docs: vision, architecture, specs, ADRs and the IDs they cite. */
|
|
12
|
-
export function livingProblems(view ) {
|
|
13
|
-
const problems = [];
|
|
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 , doc ) {
|
|
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 , path ) {
|
|
45
|
-
return [...view.approvals.values()].some((file) => file.approvals.some((record) => record.artifact === path));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function architectureProblems(path , doc , approved ) {
|
|
49
|
-
const problems = [];
|
|
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 , doc ) {
|
|
63
|
-
const capability = path.slice(path.lastIndexOf("/") + 1, -".md".length);
|
|
64
|
-
const problems = [];
|
|
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 , doc ) {
|
|
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 , doc , element ) {
|
|
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 , needle ) {
|
|
97
|
-
const index = text.indexOf(needle);
|
|
98
|
-
return index === -1 ? undefined : text.slice(0, index).split("\n").length - 1;
|
|
99
|
-
}
|