@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,134 +0,0 @@
|
|
|
1
|
-
import { elementLines, type DeltaOp, type Element } from "../elements.ts";
|
|
2
|
-
import { findIds } from "../ids.ts";
|
|
3
|
-
import { findSection, type MarkdownDoc } from "../markdown.ts";
|
|
4
|
-
import { readDelta } from "./deltas.ts";
|
|
5
|
-
import { readScope } from "./plan_parts.ts";
|
|
6
|
-
import type { ProjectView } from "./project_view.ts";
|
|
7
|
-
import { featureOf, type WorkItem, type WorkItemKind } from "./work_items.ts";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The design a piece of work builds, cut out of the documents that hold it, so a step reads
|
|
11
|
-
* the elements in its scope instead of the whole architecture and the whole feature.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export interface ShownElement {
|
|
15
|
-
id: string;
|
|
16
|
-
title: string;
|
|
17
|
-
op?: DeltaOp;
|
|
18
|
-
/** Where the body comes from, as `<path>:<line>`. */
|
|
19
|
-
source: string;
|
|
20
|
-
body: string[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface CitedElement {
|
|
24
|
-
id: string;
|
|
25
|
-
title: string;
|
|
26
|
-
source: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface ScopeView {
|
|
30
|
-
ref: string;
|
|
31
|
-
kind: WorkItemKind;
|
|
32
|
-
feature?: string;
|
|
33
|
-
/** The architecture elements the work builds or removes; for a feature, its whole architecture delta. */
|
|
34
|
-
elements: ShownElement[];
|
|
35
|
-
/** The scenarios and requirements the work delivers; for a feature, its whole spec delta. */
|
|
36
|
-
scenarios: ShownElement[];
|
|
37
|
-
/** Elements the shown bodies cite but don't include, with where to read them. */
|
|
38
|
-
cited: CitedElement[];
|
|
39
|
-
/** The settled decisions of the feature, or of a standalone change. */
|
|
40
|
-
decisions: string[];
|
|
41
|
-
/** A feature's change's own settled decisions, numbered apart from the feature's. */
|
|
42
|
-
changeDecisions: string[];
|
|
43
|
-
/** Scope IDs defined nowhere: the delta, the living docs or the archive. */
|
|
44
|
-
missing: string[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface Located {
|
|
48
|
-
doc: MarkdownDoc;
|
|
49
|
-
path: string;
|
|
50
|
-
element: Element;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function scopeView(view: ProjectView, item: WorkItem): ScopeView {
|
|
54
|
-
const feature = featureOf(view.items, item);
|
|
55
|
-
const deltaItem = feature ?? item;
|
|
56
|
-
const locate = locator(view, deltaItem);
|
|
57
|
-
const wanted = item.kind === "change" || item.kind === "standalone" ? changeIds(item) : featureIds(deltaItem);
|
|
58
|
-
const missing: string[] = [];
|
|
59
|
-
const show = (ids: string[]): ShownElement[] =>
|
|
60
|
-
ids.flatMap((id) => {
|
|
61
|
-
const found = locate(id);
|
|
62
|
-
if (found === undefined) {
|
|
63
|
-
missing.push(id);
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
const shown: ShownElement = { id, title: found.element.title, source: `${found.path}:${found.element.start + 1}`, body: elementLines(found.doc, found.element) };
|
|
67
|
-
if (found.element.op !== undefined) shown.op = found.element.op;
|
|
68
|
-
return [shown];
|
|
69
|
-
});
|
|
70
|
-
const elements = show(wanted.elements);
|
|
71
|
-
const scenarios = withoutRepeatedScenarios(show(wanted.scenarios));
|
|
72
|
-
const shownIds = new Set([...elements, ...scenarios].map((e) => e.id));
|
|
73
|
-
const cited = [...new Set([...elements, ...scenarios].flatMap((e) => findIds(e.body.join("\n"))))]
|
|
74
|
-
.filter((id) => !shownIds.has(id))
|
|
75
|
-
.flatMap((id) => {
|
|
76
|
-
const found = locate(id);
|
|
77
|
-
return found === undefined ? [] : [{ id, title: found.element.title, source: `${found.path}:${found.element.start + 1}` }];
|
|
78
|
-
});
|
|
79
|
-
const decisions = decisionLines((feature ?? item).doc);
|
|
80
|
-
const changeDecisions = feature === undefined ? [] : decisionLines(item.doc);
|
|
81
|
-
const result: ScopeView = { ref: item.ref, kind: item.kind, elements, scenarios, cited, decisions, changeDecisions, missing };
|
|
82
|
-
if (feature !== undefined) result.feature = feature.ref;
|
|
83
|
-
return result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* A requirement shown beside its own scenarios keeps only its own text: its body would repeat
|
|
88
|
-
* each of them, which made up a third of one change's output.
|
|
89
|
-
*/
|
|
90
|
-
function withoutRepeatedScenarios(shown: ShownElement[]): ShownElement[] {
|
|
91
|
-
const ids = new Set(shown.map((e) => e.id));
|
|
92
|
-
return shown.map((element) => {
|
|
93
|
-
if (!element.id.startsWith("REQ-")) return element;
|
|
94
|
-
const first = element.body.findIndex((line, i) => i > 0 && /^#{2,6} /.test(line) && findIds(line).some((id) => ids.has(id)));
|
|
95
|
-
return first === -1 ? element : { ...element, body: element.body.slice(0, first) };
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** A change shows what its scope builds, removes and delivers. */
|
|
100
|
-
function changeIds(change: WorkItem): { elements: string[]; scenarios: string[] } {
|
|
101
|
-
const scope = readScope(change.doc);
|
|
102
|
-
return { elements: [...(scope?.builds ?? []), ...(scope?.removes ?? [])], scenarios: scope?.delivers ?? [] };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** A feature shows its whole delta: every operation, with a requirement's scenarios inside its body. */
|
|
106
|
-
function featureIds(feature: WorkItem): { elements: string[]; scenarios: string[] } {
|
|
107
|
-
const entries = readDelta(feature.doc).entries;
|
|
108
|
-
return {
|
|
109
|
-
elements: entries.filter((e) => e.section === "architecture").map((e) => e.element.id),
|
|
110
|
-
scenarios: entries.filter((e) => e.section === "spec").map((e) => e.element.id),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Finds an element's body in the work's own delta first, since that is the text the work builds, then in the living docs. */
|
|
115
|
-
function locator(view: ProjectView, deltaItem: WorkItem): (id: string) => Located | undefined {
|
|
116
|
-
const delta = readDelta(deltaItem.doc);
|
|
117
|
-
const inDelta = new Map<string, Element>();
|
|
118
|
-
for (const entry of delta.entries) {
|
|
119
|
-
inDelta.set(entry.element.id, entry.element);
|
|
120
|
-
for (const scenario of entry.scenarios) inDelta.set(scenario.id, scenario);
|
|
121
|
-
}
|
|
122
|
-
return (id) => {
|
|
123
|
-
const own = inDelta.get(id);
|
|
124
|
-
if (own !== undefined) return { doc: deltaItem.doc, path: deltaItem.path, element: own };
|
|
125
|
-
const living = view.living.elements.get(id);
|
|
126
|
-
return living === undefined ? undefined : { doc: living.doc, path: living.path, element: living.element };
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function decisionLines(doc: MarkdownDoc): string[] {
|
|
131
|
-
const section = findSection(doc, "Settled decisions");
|
|
132
|
-
if (section === null) return [];
|
|
133
|
-
return doc.lines.slice(section.start, section.end).filter((line) => line.startsWith("- "));
|
|
134
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { parseMarkdown, type MarkdownDoc } from "../markdown.ts";
|
|
2
|
-
import { PATHS, filesUnder, type Snapshot } from "./layout.ts";
|
|
3
|
-
|
|
4
|
-
export type WorkItemKind = "feature" | "change" | "standalone";
|
|
5
|
-
|
|
6
|
-
/** A feature, a change inside a feature, or a standalone change. */
|
|
7
|
-
export interface WorkItem {
|
|
8
|
-
kind: WorkItemKind;
|
|
9
|
-
/** `<feature id>`, `<feature id>/<change>` or `<standalone id>`. */
|
|
10
|
-
ref: string;
|
|
11
|
-
/** The ID its frontmatter must carry: the folder name. */
|
|
12
|
-
folderName: string;
|
|
13
|
-
folder: string;
|
|
14
|
-
path: string;
|
|
15
|
-
doc: MarkdownDoc;
|
|
16
|
-
archived: boolean;
|
|
17
|
-
/** For a change inside a feature: the feature's ref. */
|
|
18
|
-
featureRef?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const FEATURE = /^\.cruze\/(features|archive)\/([^/]+)\/feature\.md$/;
|
|
22
|
-
const FEATURE_CHANGE = /^\.cruze\/(features|archive)\/([^/]+)\/changes\/([^/]+)\/change\.md$/;
|
|
23
|
-
const STANDALONE = /^\.cruze\/(changes|archive)\/([^/]+)\/change\.md$/;
|
|
24
|
-
|
|
25
|
-
export function listWorkItems(snapshot: Snapshot): WorkItem[] {
|
|
26
|
-
const paths = [...filesUnder(snapshot, PATHS.featuresDir), ...filesUnder(snapshot, PATHS.changesDir), ...filesUnder(snapshot, PATHS.archiveDir)];
|
|
27
|
-
return paths.flatMap((path) => {
|
|
28
|
-
const item = classify(path);
|
|
29
|
-
return item === null ? [] : [{ ...item, path, doc: parseMarkdown(snapshot.get(path) ?? "") }];
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function findWorkItem(items: WorkItem[], ref: string): WorkItem | undefined {
|
|
34
|
-
return items.find((item) => item.ref === ref || item.path === ref || item.folder === ref);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** The feature a change belongs to. */
|
|
38
|
-
export function featureOf(items: WorkItem[], change: WorkItem): WorkItem | undefined {
|
|
39
|
-
return change.featureRef === undefined ? undefined : items.find((item) => item.kind === "feature" && item.ref === change.featureRef);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function changesOf(items: WorkItem[], feature: WorkItem): WorkItem[] {
|
|
43
|
-
return items.filter((item) => item.featureRef === feature.ref).sort((a, b) => a.ref.localeCompare(b.ref));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function classify(path: string): Omit<WorkItem, "path" | "doc"> | null {
|
|
47
|
-
const folder = path.slice(0, path.lastIndexOf("/"));
|
|
48
|
-
let match = FEATURE.exec(path);
|
|
49
|
-
if (match?.[2] !== undefined) {
|
|
50
|
-
return { kind: "feature", ref: match[2], folderName: match[2], folder, archived: match[1] === "archive" };
|
|
51
|
-
}
|
|
52
|
-
match = FEATURE_CHANGE.exec(path);
|
|
53
|
-
if (match?.[2] !== undefined && match[3] !== undefined) {
|
|
54
|
-
return {
|
|
55
|
-
kind: "change",
|
|
56
|
-
ref: `${match[2]}/${match[3]}`,
|
|
57
|
-
folderName: match[3],
|
|
58
|
-
folder,
|
|
59
|
-
archived: match[1] === "archive",
|
|
60
|
-
featureRef: match[2],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
match = STANDALONE.exec(path);
|
|
64
|
-
if (match?.[2] !== undefined) {
|
|
65
|
-
return { kind: "standalone", ref: match[2], folderName: match[2], folder, archived: match[1] === "archive" };
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { evaluateApproval, type ApprovalStatus } from "../approvals/evaluate.ts";
|
|
2
|
-
import { rebaseNeeded } from "../approvals/rebase.ts";
|
|
3
|
-
import { PATHS } from "../project/layout.ts";
|
|
4
|
-
import { isPlanned, readChangesTable } from "../project/plan_parts.ts";
|
|
5
|
-
import { readProgress } from "../project/progress.ts";
|
|
6
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
7
|
-
import { readRoadmapItems, readRoadmapStatus } from "../project/roadmap.ts";
|
|
8
|
-
import { changesOf, featureOf, type WorkItem } from "../project/work_items.ts";
|
|
9
|
-
import { compareVersions } from "../versions/cruze_version.ts";
|
|
10
|
-
import { FIRST_RELEASE } from "../versions/rule_versions.ts";
|
|
11
|
-
import { pendingReason, pendingRethinks } from "./pending_rethink.ts";
|
|
12
|
-
import { awaitsRethink, isVerified } from "./verification.ts";
|
|
13
|
-
import { activeChange } from "./work_status.ts";
|
|
14
|
-
|
|
15
|
-
/** The Cruze step to run next, computed from the project's state and the current branch. */
|
|
16
|
-
|
|
17
|
-
export type Step = "envision" | "architect" | "roadmap" | "plan" | "build" | "verify" | "land" | "rethink" | "realign" | "switch-branch";
|
|
18
|
-
|
|
19
|
-
export interface NextStep {
|
|
20
|
-
step: Step;
|
|
21
|
-
/** What the step works on: a feature or change ref, a roadmap slug, or a branch. */
|
|
22
|
-
target?: string;
|
|
23
|
-
reason: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface NextReport {
|
|
27
|
-
next: NextStep;
|
|
28
|
-
/** Other work that could proceed, such as changes waiting on their own branches. */
|
|
29
|
-
also: NextStep[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function nextSteps(view: ProjectView, branch: string | null): NextReport {
|
|
33
|
-
const project = projectStep(view);
|
|
34
|
-
if (project !== null) return { next: project, also: [] };
|
|
35
|
-
const active = activeChange(view, branch);
|
|
36
|
-
const inFlight = inRoadmapOrder(view, view.items.filter((item) => !item.archived && item.kind !== "change")).flatMap((item) => itemSteps(view, item, branch));
|
|
37
|
-
const roadmap = roadmapStep(view);
|
|
38
|
-
const realign = realignStep(view);
|
|
39
|
-
const candidates = [
|
|
40
|
-
...(active === undefined ? [] : [changeStep(view, active, branch)]),
|
|
41
|
-
...inFlight,
|
|
42
|
-
...(roadmap === null ? [] : [roadmap]),
|
|
43
|
-
...(realign === null ? [] : [realign]),
|
|
44
|
-
];
|
|
45
|
-
const unique = candidates.filter((c, i) => candidates.findIndex((o) => o.step === c.step && o.target === c.target) === i);
|
|
46
|
-
const [next, ...also] = unique;
|
|
47
|
-
return { next: next ?? { step: "roadmap", reason: "every item on the roadmap has landed; close the release and plan the next" }, also };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Work in flight in the roadmap's order, with items whose roadmap blockers haven't all landed
|
|
52
|
-
* last: a feature designed early still waits for the prefactors the user put before it.
|
|
53
|
-
*/
|
|
54
|
-
function inRoadmapOrder(view: ProjectView, items: WorkItem[]): WorkItem[] {
|
|
55
|
-
const text = view.snapshot.get(PATHS.roadmap);
|
|
56
|
-
if (text === undefined) return items;
|
|
57
|
-
const rows = readRoadmapStatus(text);
|
|
58
|
-
const state = new Map(rows.map((row) => [row.slug, row.state]));
|
|
59
|
-
const slugOf = new Map(rows.map((row) => [row.id, row.slug]));
|
|
60
|
-
const roadmap = readRoadmapItems(text);
|
|
61
|
-
const rank = (item: WorkItem): [number, number] => {
|
|
62
|
-
const at = roadmap.findIndex((r) => r.slug === slugOf.get(item.ref));
|
|
63
|
-
const blocked = at !== -1 && (roadmap[at]?.blockedBy ?? []).some((b) => state.get(b) !== "landed");
|
|
64
|
-
return [blocked ? 1 : 0, at === -1 ? roadmap.length : at];
|
|
65
|
-
};
|
|
66
|
-
return items
|
|
67
|
-
.map((item, i) => ({ item, key: [...rank(item), i] }))
|
|
68
|
-
.sort((a, b) => (a.key[0] ?? 0) - (b.key[0] ?? 0) || (a.key[1] ?? 0) - (b.key[1] ?? 0) || (a.key[2] ?? 0) - (b.key[2] ?? 0))
|
|
69
|
-
.map(({ item }) => item);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Realign comes when nothing else is due: the installed skills expect more than the code meets. */
|
|
73
|
-
function realignStep(view: ProjectView): NextStep | null {
|
|
74
|
-
const config = view.config?.config;
|
|
75
|
-
if (config?.cruze === undefined) return null;
|
|
76
|
-
const standards = config.standards ?? FIRST_RELEASE;
|
|
77
|
-
if (compareVersions(standards, config.cruze) >= 0) return null;
|
|
78
|
-
return { step: "realign", target: config.cruze, reason: `the code meets Cruze ${standards}'s expectations, and the installed skills are ${config.cruze}` };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** The project documents come first: nothing else can proceed without them. */
|
|
82
|
-
function projectStep(view: ProjectView): NextStep | null {
|
|
83
|
-
const docs: Array<{ path: string; missing: NextStep; unapproved: NextStep; changed: NextStep }> = [
|
|
84
|
-
{
|
|
85
|
-
path: PATHS.vision,
|
|
86
|
-
missing: { step: "envision", reason: "the project has no vision yet" },
|
|
87
|
-
unapproved: { step: "envision", reason: "finish the vision and approve it" },
|
|
88
|
-
changed: { step: "envision", reason: "the vision changed since its approval; review the change and approve it again" },
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
path: PATHS.architecture,
|
|
92
|
-
missing: { step: "architect", reason: "the project has no architecture yet" },
|
|
93
|
-
unapproved: { step: "architect", reason: "finish the architecture, review it and approve it" },
|
|
94
|
-
changed: { step: "rethink", target: "architecture", reason: "the architecture changed since its approval" },
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
path: PATHS.roadmap,
|
|
98
|
-
missing: { step: "architect", reason: "the project has no roadmap yet" },
|
|
99
|
-
unapproved: { step: "roadmap", reason: "approve the release ordering" },
|
|
100
|
-
changed: { step: "roadmap", reason: "the roadmap changed since its approval; approve the new ordering" },
|
|
101
|
-
},
|
|
102
|
-
];
|
|
103
|
-
for (const doc of docs) {
|
|
104
|
-
if (!view.snapshot.has(doc.path)) return doc.missing;
|
|
105
|
-
const state = evaluateApproval(view, doc.path).state;
|
|
106
|
-
if (state === "unapproved") return doc.unapproved;
|
|
107
|
-
if (state !== "approved") return doc.changed;
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function itemSteps(view: ProjectView, item: WorkItem, branch: string | null): NextStep[] {
|
|
113
|
-
if (item.kind === "standalone") return [changeStep(view, item, branch)];
|
|
114
|
-
const state = evaluateApproval(view, item.path);
|
|
115
|
-
if (readChangesTable(item.doc).length === 0) return [{ step: "architect", target: item.ref, reason: "the feature has no design or changes yet" }];
|
|
116
|
-
if (state.state === "unapproved") return [{ step: "architect", target: item.ref, reason: "review the feature's design and approve it" }];
|
|
117
|
-
if (state.state !== "approved") return [{ step: "rethink", target: item.ref, reason: staleReason(view, item, "the feature", state) }];
|
|
118
|
-
const pending = pendingRethinks(view.journal, item);
|
|
119
|
-
if (pending.length > 0) return [{ step: "rethink", target: item.ref, reason: pendingReason(item, pending) }];
|
|
120
|
-
const changes = changesOf(view.items, item);
|
|
121
|
-
const landed = new Set(changes.filter((c) => readProgress(c.doc).landed !== undefined).map((c) => c.folderName));
|
|
122
|
-
const steps: NextStep[] = [];
|
|
123
|
-
for (const row of readChangesTable(item.doc)) {
|
|
124
|
-
const change = changes.find((c) => c.folderName === row.change);
|
|
125
|
-
if (change !== undefined) {
|
|
126
|
-
if (!landed.has(row.change)) steps.push(changeStep(view, change, branch));
|
|
127
|
-
} else if (row.dependsOn.every((d) => landed.has(d))) {
|
|
128
|
-
steps.push({ step: "plan", target: `${item.ref}/${row.change}`, reason: "the next change of an approved feature, with its dependencies landed" });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return steps;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function changeStep(view: ProjectView, change: WorkItem, branch: string | null): NextStep {
|
|
135
|
-
const feature = featureOf(view.items, change);
|
|
136
|
-
const featureState = feature === undefined ? undefined : evaluateApproval(view, feature.path);
|
|
137
|
-
if (feature !== undefined && featureState !== undefined && featureState.state !== "approved") {
|
|
138
|
-
return { step: "rethink", target: feature.ref, reason: staleReason(view, feature, `the feature of ${change.ref}`, featureState) };
|
|
139
|
-
}
|
|
140
|
-
for (const part of [feature, change]) {
|
|
141
|
-
const pending = part === undefined ? [] : pendingRethinks(view.journal, part);
|
|
142
|
-
if (part !== undefined && pending.length > 0) return { step: "rethink", target: part.ref, reason: pendingReason(part, pending) };
|
|
143
|
-
}
|
|
144
|
-
const bound = readProgress(change.doc).branch;
|
|
145
|
-
const state = evaluateApproval(view, change.path);
|
|
146
|
-
if (!isPlanned(change.doc)) return { step: "plan", target: change.ref, reason: "the change is designed but has no test plan or tasks yet" };
|
|
147
|
-
if (state.state === "unapproved") return { step: "plan", target: change.ref, reason: "review the plan and approve it on the branch that will build it" };
|
|
148
|
-
if (state.state !== "approved") return { step: "rethink", target: change.ref, reason: staleReason(view, change, "the change", state) };
|
|
149
|
-
if (awaitsRethink(view.journal, change, feature)) return { step: "rethink", target: feature?.ref ?? change.ref, reason: `verify sent ${change.ref} back for a design change` };
|
|
150
|
-
if (bound !== undefined && bound !== branch) return { step: "switch-branch", target: bound, reason: `${change.ref} is built on branch ${bound}` };
|
|
151
|
-
const progress = readProgress(change.doc);
|
|
152
|
-
if ([...progress.tasks.values()].some((commit) => commit === null)) return { step: "build", target: change.ref, reason: "the change has open tasks" };
|
|
153
|
-
if (!isVerified(view.journal, change)) return { step: "verify", target: change.ref, reason: "every task is done; verify the change" };
|
|
154
|
-
return { step: "land", target: change.ref, reason: "the change is verified and accepted" };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Why work went stale, and what the rethink will take: a rebase when its delta still changes an
|
|
159
|
-
* element whose living text moved, or only a check of the diff and a re-stamp when nothing it cites
|
|
160
|
-
* asks anything of its own text. One project expected a re-stamp and met rebase-required.
|
|
161
|
-
*/
|
|
162
|
-
function staleReason(view: ProjectView, item: WorkItem, label: string, state: ApprovalStatus): string {
|
|
163
|
-
if (state.state === "edited") return `${label} changed since its approval`;
|
|
164
|
-
const changed = state.changed.join(", ");
|
|
165
|
-
const rebase = rebaseNeeded(view, item);
|
|
166
|
-
const then = rebase.length > 0
|
|
167
|
-
? `its delta changes ${rebase.join(", ")}, so rebase it, as cruze diff ${item.ref} shows`
|
|
168
|
-
: `read cruze diff ${item.ref}, and re-stamp it when the work still holds`;
|
|
169
|
-
return `something ${label} cites changed since its approval (${changed}); ${then}`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** The first roadmap item that hasn't started and whose blockers have all landed. */
|
|
173
|
-
function roadmapStep(view: ProjectView): NextStep | null {
|
|
174
|
-
const text = view.snapshot.get(PATHS.roadmap);
|
|
175
|
-
if (text === undefined) return null;
|
|
176
|
-
const status = new Map(readRoadmapStatus(text).map((row) => [row.slug, row.state]));
|
|
177
|
-
const item = readRoadmapItems(text).find((i) => !status.has(i.slug) && i.blockedBy.every((b) => status.get(b) === "landed"));
|
|
178
|
-
if (item === undefined) return null;
|
|
179
|
-
if (item.kind === "feature") return { step: "envision", target: item.slug, reason: "the next roadmap feature; skip to architect when its requirements are settled" };
|
|
180
|
-
return { step: "architect", target: item.slug, reason: "the next roadmap change; design it as a tweak" };
|
|
181
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { JournalEntry } from "../journal.ts";
|
|
2
|
-
import type { WorkItem } from "../project/work_items.ts";
|
|
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: JournalEntry[], item: WorkItem): JournalEntry[] {
|
|
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: WorkItem, pending: JournalEntry[]): string {
|
|
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
|
-
import type { JournalEntry } from "../journal.ts";
|
|
2
|
-
import type { WorkItem } from "../project/work_items.ts";
|
|
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: JournalEntry[], change: WorkItem): boolean {
|
|
11
|
-
const byTime = (a: JournalEntry, b: JournalEntry): number => 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: JournalEntry[], change: WorkItem, feature: WorkItem | undefined): boolean {
|
|
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"] as string | undefined));
|
|
29
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { evaluateApproval, type ApprovalStatus } from "../approvals/evaluate.ts";
|
|
2
|
-
import { PATHS } from "../project/layout.ts";
|
|
3
|
-
import { readDelta } from "../project/deltas.ts";
|
|
4
|
-
import { readChangesTable, readScope } from "../project/plan_parts.ts";
|
|
5
|
-
import { readProgress } from "../project/progress.ts";
|
|
6
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
7
|
-
import { changesOf, featureOf, type WorkItem } from "../project/work_items.ts";
|
|
8
|
-
import { pendingReason, pendingRethinks } from "./pending_rethink.ts";
|
|
9
|
-
|
|
10
|
-
export interface ChangeStatus {
|
|
11
|
-
ref: string;
|
|
12
|
-
path: string;
|
|
13
|
-
approval: ApprovalStatus;
|
|
14
|
-
branch?: string;
|
|
15
|
-
tasksDone: number;
|
|
16
|
-
tasksTotal: number;
|
|
17
|
-
/** When and in which commit the change landed, once it has. */
|
|
18
|
-
landed?: string;
|
|
19
|
-
/** Findings decided for a rethink of this change that hasn't happened yet. */
|
|
20
|
-
rethinkPending?: string[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface FeatureStatus {
|
|
24
|
-
ref: string;
|
|
25
|
-
path: string;
|
|
26
|
-
approval: ApprovalStatus;
|
|
27
|
-
changes: ChangeStatus[];
|
|
28
|
-
/** Findings decided for a rethink of this feature that hasn't happened yet. */
|
|
29
|
-
rethinkPending?: string[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ProjectStatus {
|
|
33
|
-
documents: ApprovalStatus[];
|
|
34
|
-
features: FeatureStatus[];
|
|
35
|
-
standalone: ChangeStatus[];
|
|
36
|
-
/** The change bound to the current branch, if any. */
|
|
37
|
-
active?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function projectStatus(view: ProjectView, branch: string | null): ProjectStatus {
|
|
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: ProjectStatus = { 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: ProjectView, branch: string | null): WorkItem | undefined {
|
|
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: ProjectView, change: WorkItem): ChangeStatus {
|
|
68
|
-
const progress = readProgress(change.doc);
|
|
69
|
-
const status: ChangeStatus = {
|
|
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: ProjectView, item: WorkItem): { rethinkPending?: string[] } {
|
|
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: ProjectView, branch: string | null): string[] {
|
|
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: ProjectView, change: WorkItem, feature: WorkItem | undefined): string[] {
|
|
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: ProjectView, change: WorkItem): Set<string> {
|
|
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, type DecisionCitation } from "../decision_citations.ts";
|
|
2
|
-
import type { Element } from "../elements.ts";
|
|
3
|
-
import { findSection, type MarkdownDoc } from "../markdown.ts";
|
|
4
|
-
import { readDelta } from "../project/deltas.ts";
|
|
5
|
-
import { PATHS } from "../project/layout.ts";
|
|
6
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
7
|
-
import type { WorkItem } from "../project/work_items.ts";
|
|
8
|
-
import { warning, type Problem } from "./problem.ts";
|
|
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: ProjectView): Problem[] {
|
|
17
|
-
const slugs = slugsOf(view);
|
|
18
|
-
const project = projectDecisions(view);
|
|
19
|
-
const problems: Problem[] = [];
|
|
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: ProjectView, item: WorkItem): Problem[] {
|
|
42
|
-
const slugs = slugsOf(view);
|
|
43
|
-
const project = projectDecisions(view);
|
|
44
|
-
const local = definedDecisions(sectionLines(item.doc, "Settled decisions"));
|
|
45
|
-
const problems: Problem[] = [];
|
|
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: number): boolean => 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: MarkdownDoc, slugs: Set<string>, element?: Element): DecisionCitation[] {
|
|
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: ProjectView): Set<number> {
|
|
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: MarkdownDoc, title: string): string[] {
|
|
70
|
-
const range = findSection(doc, title);
|
|
71
|
-
return range === null ? [] : doc.lines.slice(range.start, range.end);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function slugsOf(view: ProjectView): Set<string> {
|
|
75
|
-
return workItemSlugs(view.items.map((item) => item.folderName));
|
|
76
|
-
}
|