@printpractical/cruze 0.0.9 → 0.0.11

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.
Files changed (131) hide show
  1. package/README.md +17 -13
  2. package/dist/adapters/inbound/cli/cli_context.js +4 -0
  3. package/dist/adapters/inbound/cli/commands/diff.js +23 -0
  4. package/dist/adapters/inbound/cli/commands/documents.js +1 -1
  5. package/dist/adapters/inbound/cli/commands/show.js +66 -2
  6. package/dist/adapters/inbound/cli/commands/status.js +7 -3
  7. package/dist/adapters/inbound/cli/commands.js +9 -3
  8. package/dist/adapters/inbound/cli/parse_command.js +8 -0
  9. package/dist/adapters/outbound/git_repository.js +9 -0
  10. package/dist/app/ports/repository.js +4 -0
  11. package/dist/app/use_cases/approve_artifact.js +18 -10
  12. package/dist/app/use_cases/check_code.js +5 -3
  13. package/dist/app/use_cases/journal_events.js +6 -2
  14. package/dist/app/use_cases/new_project_doc.js +10 -2
  15. package/dist/app/use_cases/run_review.js +10 -2
  16. package/dist/app/use_cases/show_elements.js +18 -0
  17. package/dist/app/use_cases/show_notes.js +11 -0
  18. package/dist/app/use_cases/show_scope.js +15 -1
  19. package/dist/app/use_cases/upstream_diff.js +55 -0
  20. package/dist/domain/approvals/rebase.js +39 -0
  21. package/dist/domain/approvals/upstream_text.js +36 -0
  22. package/dist/domain/check/check_source.js +18 -1
  23. package/dist/domain/check/decision_comments.js +38 -0
  24. package/dist/domain/decision_citations.js +62 -0
  25. package/dist/domain/edits/agents_file.js +9 -0
  26. package/dist/domain/ids.js +3 -1
  27. package/dist/domain/journal.js +6 -4
  28. package/dist/domain/land/merge_plan.js +7 -2
  29. package/dist/domain/line_diff.js +56 -0
  30. package/dist/domain/project/element_lookup.js +124 -0
  31. package/dist/domain/project/living_model.js +2 -1
  32. package/dist/domain/project/notes.js +74 -0
  33. package/dist/domain/skill_budget.js +36 -0
  34. package/dist/domain/status/next_step.js +27 -5
  35. package/dist/domain/status/pending_rethink.js +22 -0
  36. package/dist/domain/status/work_status.js +17 -2
  37. package/dist/domain/validation/decision_rules.js +76 -0
  38. package/dist/domain/validation/delta_rules.js +2 -0
  39. package/dist/domain/validation/future_rules.js +40 -0
  40. package/dist/domain/validation/living_rules.js +2 -0
  41. package/dist/domain/validation/validate_project.js +4 -0
  42. package/dist/domain/validation/vision_rules.js +41 -0
  43. package/dist/domain/versions/rule_versions.js +3 -0
  44. package/package.json +1 -1
  45. package/skills/architect/SKILL.md +2 -1
  46. package/skills/architect/feature.md +6 -4
  47. package/skills/architect/project.md +2 -2
  48. package/skills/behavioural-testing/SKILL.md +3 -2
  49. package/skills/behavioural-testing/fakes.md +3 -3
  50. package/skills/build/SKILL.md +6 -6
  51. package/skills/envision/SKILL.md +3 -2
  52. package/skills/envision/feature.md +3 -1
  53. package/skills/envision/intake.md +28 -0
  54. package/skills/envision/possibilities.md +13 -0
  55. package/skills/envision/project.md +5 -3
  56. package/skills/explore/SKILL.md +2 -2
  57. package/skills/formats/SKILL.md +13 -2
  58. package/skills/formats/reference/architecture.md +7 -3
  59. package/skills/formats/reference/durable-docs.md +4 -1
  60. package/skills/formats/reference/work-items.md +1 -1
  61. package/skills/formats/templates/vision.md +12 -0
  62. package/skills/grilling/SKILL.md +10 -0
  63. package/skills/hexagonal-design/languages/rust.md +2 -1
  64. package/skills/hexagonal-design/runtime.md +1 -0
  65. package/skills/land/SKILL.md +6 -2
  66. package/skills/plan/SKILL.md +13 -3
  67. package/skills/plan/tasks.md +3 -3
  68. package/skills/realign/SKILL.md +5 -3
  69. package/skills/realign/notes/0.0.10.md +131 -0
  70. package/skills/realign/notes/0.0.11.md +15 -0
  71. package/skills/realign/notes/0.0.2.md +1 -1
  72. package/skills/realign/notes/0.0.6.md +1 -1
  73. package/skills/realign/notes/0.0.8.md +1 -1
  74. package/skills/realign/notes/0.0.9.md +2 -2
  75. package/skills/realign/notes.md +2 -0
  76. package/skills/research/SKILL.md +3 -2
  77. package/skills/rethink/SKILL.md +7 -4
  78. package/skills/rethink/levels.md +15 -6
  79. package/skills/roadmap/SKILL.md +2 -1
  80. package/skills/roles/SKILL.md +4 -4
  81. package/skills/roles/auditor.md +1 -1
  82. package/skills/roles/code-reviewer.md +6 -5
  83. package/skills/roles/design-reviewer.md +4 -40
  84. package/skills/roles/design-rubric.md +40 -0
  85. package/skills/roles/plan-rubric.md +11 -0
  86. package/skills/roles/surveyor.md +3 -2
  87. package/skills/triage/SKILL.md +1 -1
  88. package/skills/verify/SKILL.md +6 -6
  89. package/src/adapters/inbound/cli/cli_context.ts +4 -0
  90. package/src/adapters/inbound/cli/commands/diff.ts +23 -0
  91. package/src/adapters/inbound/cli/commands/documents.ts +1 -1
  92. package/src/adapters/inbound/cli/commands/show.ts +66 -2
  93. package/src/adapters/inbound/cli/commands/status.ts +7 -3
  94. package/src/adapters/inbound/cli/commands.ts +9 -3
  95. package/src/adapters/inbound/cli/parse_command.ts +8 -0
  96. package/src/adapters/outbound/git_repository.ts +9 -0
  97. package/src/app/ports/repository.ts +4 -0
  98. package/src/app/use_cases/approve_artifact.ts +18 -10
  99. package/src/app/use_cases/check_code.ts +5 -3
  100. package/src/app/use_cases/journal_events.ts +6 -2
  101. package/src/app/use_cases/new_project_doc.ts +10 -2
  102. package/src/app/use_cases/run_review.ts +10 -2
  103. package/src/app/use_cases/show_elements.ts +18 -0
  104. package/src/app/use_cases/show_notes.ts +11 -0
  105. package/src/app/use_cases/show_scope.ts +15 -1
  106. package/src/app/use_cases/upstream_diff.ts +55 -0
  107. package/src/domain/approvals/rebase.ts +39 -0
  108. package/src/domain/approvals/upstream_text.ts +36 -0
  109. package/src/domain/check/check_source.ts +18 -1
  110. package/src/domain/check/decision_comments.ts +38 -0
  111. package/src/domain/decision_citations.ts +62 -0
  112. package/src/domain/edits/agents_file.ts +9 -0
  113. package/src/domain/ids.ts +3 -1
  114. package/src/domain/journal.ts +6 -4
  115. package/src/domain/land/merge_plan.ts +7 -2
  116. package/src/domain/line_diff.ts +56 -0
  117. package/src/domain/project/element_lookup.ts +124 -0
  118. package/src/domain/project/living_model.ts +2 -1
  119. package/src/domain/project/notes.ts +74 -0
  120. package/src/domain/skill_budget.ts +36 -0
  121. package/src/domain/status/next_step.ts +27 -5
  122. package/src/domain/status/pending_rethink.ts +22 -0
  123. package/src/domain/status/work_status.ts +17 -2
  124. package/src/domain/validation/decision_rules.ts +76 -0
  125. package/src/domain/validation/delta_rules.ts +2 -0
  126. package/src/domain/validation/future_rules.ts +40 -0
  127. package/src/domain/validation/living_rules.ts +2 -0
  128. package/src/domain/validation/validate_project.ts +4 -0
  129. package/src/domain/validation/vision_rules.ts +41 -0
  130. package/src/domain/versions/rule_versions.ts +3 -0
  131. package/templates/init/AGENTS.md +1 -1
@@ -1,4 +1,5 @@
1
- import { evaluateApproval } from "../approvals/evaluate.ts";
1
+ import { evaluateApproval, type ApprovalStatus } from "../approvals/evaluate.ts";
2
+ import { rebaseNeeded } from "../approvals/rebase.ts";
2
3
  import { PATHS } from "../project/layout.ts";
3
4
  import { isPlanned, readChangesTable } from "../project/plan_parts.ts";
4
5
  import { readProgress } from "../project/progress.ts";
@@ -7,6 +8,7 @@ import { readRoadmapItems, readRoadmapStatus } from "../project/roadmap.ts";
7
8
  import { changesOf, featureOf, type WorkItem } from "../project/work_items.ts";
8
9
  import { compareVersions } from "../versions/cruze_version.ts";
9
10
  import { FIRST_RELEASE } from "../versions/rule_versions.ts";
11
+ import { pendingReason, pendingRethinks } from "./pending_rethink.ts";
10
12
  import { awaitsRethink, isVerified } from "./verification.ts";
11
13
  import { activeChange } from "./work_status.ts";
12
14
 
@@ -112,7 +114,9 @@ function itemSteps(view: ProjectView, item: WorkItem, branch: string | null): Ne
112
114
  const state = evaluateApproval(view, item.path);
113
115
  if (readChangesTable(item.doc).length === 0) return [{ step: "architect", target: item.ref, reason: "the feature has no design or changes yet" }];
114
116
  if (state.state === "unapproved") return [{ step: "architect", target: item.ref, reason: "review the feature's design and approve it" }];
115
- if (state.state !== "approved") return [{ step: "rethink", target: item.ref, reason: `the feature changed since its approval${state.changed.length > 0 ? ` (${state.changed.join(", ")})` : ""}` }];
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) }];
116
120
  const changes = changesOf(view.items, item);
117
121
  const landed = new Set(changes.filter((c) => readProgress(c.doc).landed !== undefined).map((c) => c.folderName));
118
122
  const steps: NextStep[] = [];
@@ -131,14 +135,17 @@ function changeStep(view: ProjectView, change: WorkItem, branch: string | null):
131
135
  const feature = featureOf(view.items, change);
132
136
  const featureState = feature === undefined ? undefined : evaluateApproval(view, feature.path);
133
137
  if (feature !== undefined && featureState !== undefined && featureState.state !== "approved") {
134
- const why = featureState.changed.length > 0 ? ` (${featureState.changed.join(", ")})` : "";
135
- return { step: "rethink", target: feature.ref, reason: `the feature of ${change.ref} changed since its approval${why}` };
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) };
136
143
  }
137
144
  const bound = readProgress(change.doc).branch;
138
145
  const state = evaluateApproval(view, change.path);
139
146
  if (!isPlanned(change.doc)) return { step: "plan", target: change.ref, reason: "the change is designed but has no test plan or tasks yet" };
140
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" };
141
- if (state.state !== "approved") return { step: "rethink", target: change.ref, reason: `the change or something it cites changed since its approval${state.changed.length > 0 ? ` (${state.changed.join(", ")})` : ""}` };
148
+ if (state.state !== "approved") return { step: "rethink", target: change.ref, reason: staleReason(view, change, "the change", state) };
142
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` };
143
150
  if (bound !== undefined && bound !== branch) return { step: "switch-branch", target: bound, reason: `${change.ref} is built on branch ${bound}` };
144
151
  const progress = readProgress(change.doc);
@@ -147,6 +154,21 @@ function changeStep(view: ProjectView, change: WorkItem, branch: string | null):
147
154
  return { step: "land", target: change.ref, reason: "the change is verified and accepted" };
148
155
  }
149
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
+
150
172
  /** The first roadmap item that hasn't started and whose blockers have all landed. */
151
173
  function roadmapStep(view: ProjectView): NextStep | null {
152
174
  const text = view.snapshot.get(PATHS.roadmap);
@@ -0,0 +1,22 @@
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
+ }
@@ -5,6 +5,7 @@ import { readChangesTable, readScope } from "../project/plan_parts.ts";
5
5
  import { readProgress } from "../project/progress.ts";
6
6
  import type { ProjectView } from "../project/project_view.ts";
7
7
  import { changesOf, featureOf, type WorkItem } from "../project/work_items.ts";
8
+ import { pendingReason, pendingRethinks } from "./pending_rethink.ts";
8
9
 
9
10
  export interface ChangeStatus {
10
11
  ref: string;
@@ -15,6 +16,8 @@ export interface ChangeStatus {
15
16
  tasksTotal: number;
16
17
  /** When and in which commit the change landed, once it has. */
17
18
  landed?: string;
19
+ /** Findings decided for a rethink of this change that hasn't happened yet. */
20
+ rethinkPending?: string[];
18
21
  }
19
22
 
20
23
  export interface FeatureStatus {
@@ -22,6 +25,8 @@ export interface FeatureStatus {
22
25
  path: string;
23
26
  approval: ApprovalStatus;
24
27
  changes: ChangeStatus[];
28
+ /** Findings decided for a rethink of this feature that hasn't happened yet. */
29
+ rethinkPending?: string[];
25
30
  }
26
31
 
27
32
  export interface ProjectStatus {
@@ -42,6 +47,7 @@ export function projectStatus(view: ProjectView, branch: string | null): Project
42
47
  path: feature.path,
43
48
  approval: evaluateApproval(view, feature.path),
44
49
  changes: changesOf(view.items, feature).map((change) => changeStatus(view, change)),
50
+ ...withPending(view, feature),
45
51
  }));
46
52
  const standalone = active.filter((item) => item.kind === "standalone").map((change) => changeStatus(view, change));
47
53
  const status: ProjectStatus = { documents, features, standalone };
@@ -69,7 +75,12 @@ function changeStatus(view: ProjectView, change: WorkItem): ChangeStatus {
69
75
  };
70
76
  if (progress.branch !== undefined) status.branch = progress.branch;
71
77
  if (progress.landed !== undefined) status.landed = progress.landed;
72
- return status;
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"] ?? "")) };
73
84
  }
74
85
 
75
86
  /** Why building may not start on this branch; empty when the build gate passes. */
@@ -86,7 +97,11 @@ export function buildGateReasons(view: ProjectView, branch: string | null): stri
86
97
  const detail = status.changed.length > 0 ? `: ${status.changed.join(", ")} changed` : status.unjournaled === true ? ": the approval has no journal entry" : "";
87
98
  return [`${path} is ${status.state}${detail}`];
88
99
  });
89
- return [...approvals, ...unlandedDependencies(view, change, feature)];
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)];
90
105
  }
91
106
 
92
107
  /** The changes this change's row in its feature's Changes table depends on that haven't landed on this branch. */
@@ -0,0 +1,76 @@
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
+ }
@@ -4,6 +4,7 @@ import { specPath } from "../project/layout.ts";
4
4
  import { orphanDeltaElements, readDelta, type Delta } from "../project/deltas.ts";
5
5
  import type { ProjectView } from "../project/project_view.ts";
6
6
  import type { WorkItem } from "../project/work_items.ts";
7
+ import { deltaDecisionProblems } from "./decision_rules.ts";
7
8
  import { elementProblems } from "./element_rules.ts";
8
9
  import { error, type Problem } from "./problem.ts";
9
10
 
@@ -44,6 +45,7 @@ export function deltaProblems(view: ProjectView, item: WorkItem): Problem[] {
44
45
  problems.push(error(path, op.start, "capability-exists", `capability ${op.capability} already has ${specPath(op.capability)}`));
45
46
  }
46
47
  }
48
+ problems.push(...deltaDecisionProblems(view, item));
47
49
  return problems;
48
50
  }
49
51
 
@@ -0,0 +1,40 @@
1
+ import { factsOf, headingElements } from "../elements.ts";
2
+ import { readRows } from "../edits/managed_tables.ts";
3
+ import { findIds, kindOf } from "../ids.ts";
4
+ import type { MarkdownDoc } from "../markdown.ts";
5
+ import { PATHS } from "../project/layout.ts";
6
+ import { readDelta } from "../project/deltas.ts";
7
+ import type { ProjectView } from "../project/project_view.ts";
8
+ import { readRoadmapItems } from "../project/roadmap.ts";
9
+ import { warning, type Problem } from "./problem.ts";
10
+
11
+ /**
12
+ * A `Future` fact keeps room for a need someone stated: a `LATER` capability of the vision, or a
13
+ * feature on the feature map or the roadmap. One that names neither reserves room for a need nobody
14
+ * stated, which the design reviewer treats as a finding; this makes it visible without a review.
15
+ */
16
+ export function futureProblems(view: ProjectView): Problem[] {
17
+ const slugs = anchorSlugs(view);
18
+ const unanchored = (text: string): boolean =>
19
+ !findIds(text).some((id) => kindOf(id) === "LATER") && ![...slugs].some((slug) => new RegExp(`(^|[^a-z0-9-])${slug}([^a-z0-9-]|$)`).test(text));
20
+ const check = (path: string, doc: MarkdownDoc, from = 0, to = doc.lines.length): Problem[] =>
21
+ headingElements(doc, from, to).flatMap((element) =>
22
+ (factsOf(doc, element).get("Future") ?? [])
23
+ .filter(unanchored)
24
+ .map(() => warning(path, element.start, "future-unanchored", `${element.id} keeps room for a need its Future fact doesn't name; cite the LATER capability, or the feature on the map or roadmap, that it serves`)),
25
+ );
26
+ const living = view.living.docs.get(PATHS.architecture);
27
+ const deltas = view.items
28
+ .filter((item) => !item.archived && item.kind !== "change")
29
+ .flatMap((item) => readDelta(item.doc).entries.flatMap((entry) => check(item.path, item.doc, entry.element.start, entry.element.end)));
30
+ return [...(living === undefined ? [] : check(PATHS.architecture, living)), ...deltas];
31
+ }
32
+
33
+ /** Feature slugs a Future fact may name: the feature map's, implemented or future, and the roadmap's items. */
34
+ function anchorSlugs(view: ProjectView): Set<string> {
35
+ const vision = view.snapshot.get(PATHS.vision) ?? "";
36
+ const roadmap = view.snapshot.get(PATHS.roadmap);
37
+ const mapped = ["Implemented", "Future"].flatMap((heading) => readRows(vision, heading).map((row) => row[0] ?? ""));
38
+ const planned = roadmap === undefined ? [] : readRoadmapItems(roadmap).map((item) => item.slug);
39
+ return new Set([...mapped, ...planned].map((slug) => slug.trim()).filter((slug) => /^[a-z0-9-]+$/.test(slug)));
40
+ }
@@ -4,6 +4,7 @@ import { findIds, scopeOf, STATUS_KINDS } from "../ids.ts";
4
4
  import type { MarkdownDoc } from "../markdown.ts";
5
5
  import { PATHS } from "../project/layout.ts";
6
6
  import type { ProjectView } from "../project/project_view.ts";
7
+ import { livingDecisionProblems } from "./decision_rules.ts";
7
8
  import { elementProblems } from "./element_rules.ts";
8
9
  import { error, warning, type Problem } from "./problem.ts";
9
10
 
@@ -26,6 +27,7 @@ export function livingProblems(view: ProjectView): Problem[] {
26
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`));
27
28
  }
28
29
  }
30
+ problems.push(...livingDecisionProblems(view));
29
31
  return problems;
30
32
  }
31
33
 
@@ -1,7 +1,9 @@
1
1
  import type { ProjectView } from "../project/project_view.ts";
2
+ import { futureProblems } from "./future_rules.ts";
2
3
  import { livingProblems } from "./living_rules.ts";
3
4
  import { error, sortProblems, type Problem } from "./problem.ts";
4
5
  import { configProblems, readmeProblems, roadmapProblems, templateLeftovers } from "./project_rules.ts";
6
+ import { visionProblems } from "./vision_rules.ts";
5
7
  import { workItemProblems } from "./work_item_rules.ts";
6
8
 
7
9
  /** Every rule in the cruze-formats skill, applied to the whole project. */
@@ -13,6 +15,8 @@ export function validateProject(view: ProjectView): Problem[] {
13
15
  return sortProblems([
14
16
  ...configProblems(view),
15
17
  ...livingProblems(view),
18
+ ...visionProblems(view),
19
+ ...futureProblems(view),
16
20
  ...roadmapProblems(view),
17
21
  ...workItemProblems(view),
18
22
  ...templateLeftovers(view),
@@ -0,0 +1,41 @@
1
+ import { rowElements } from "../elements.ts";
2
+ import { findSection } from "../markdown.ts";
3
+ import { splitRow } from "../markdown_tables.ts";
4
+ import { PATHS } from "../project/layout.ts";
5
+ import type { ProjectView } from "../project/project_view.ts";
6
+ import { error, type Problem } from "./problem.ts";
7
+
8
+ /** The section and the cells after the ID that each long-term row of the vision holds. */
9
+ const INTENT_ROWS: Record<string, { section: string; cells: string[] }> = {
10
+ PRINCIPLE: { section: "Principles", cells: ["Principle", "Why"] },
11
+ LATER: { section: "Must stay possible", cells: ["Capability", "Likelihood", "Why", "Leave room for"] },
12
+ };
13
+
14
+ export const LIKELIHOODS = ["likely", "possible"];
15
+
16
+ /**
17
+ * The vision's principles and the capabilities that must stay possible are rows, so designs cite
18
+ * them and approvals pin them. One project kept its principles in a seed note and its long-term
19
+ * plans in the user's head: a feature reversed a principle unnoticed, and a later review proposed
20
+ * removing what those plans needed.
21
+ */
22
+ export function visionProblems(view: ProjectView): Problem[] {
23
+ const doc = view.living.docs.get(PATHS.vision);
24
+ if (doc === undefined) return [];
25
+ return rowElements(doc).flatMap((row) => {
26
+ const shape = INTENT_ROWS[row.kind];
27
+ if (shape === undefined) return [];
28
+ const section = findSection(doc, shape.section);
29
+ if (section === null || row.start < section.start || row.start >= section.end) {
30
+ return [error(PATHS.vision, row.start, "vision-row", `${row.id} belongs in the table under ## ${shape.section}`)];
31
+ }
32
+ const cells = splitRow(doc.lines[row.start] ?? "").slice(1);
33
+ if (cells.length !== shape.cells.length || cells.some((cell) => cell === "")) {
34
+ return [error(PATHS.vision, row.start, "vision-row", `${row.id} needs ${shape.cells.join(", ")}, each filled`)];
35
+ }
36
+ if (row.kind === "LATER" && !LIKELIHOODS.includes(cells[1] ?? "")) {
37
+ return [error(PATHS.vision, row.start, "vision-row", `${row.id}'s likelihood must be ${LIKELIHOODS.join(" or ")}`)];
38
+ }
39
+ return [];
40
+ });
41
+ }
@@ -17,6 +17,9 @@ export const RULE_SINCE: Record<string, string> = {
17
17
  "validate:change-too-large": "0.0.5",
18
18
  "validate:plan-too-large": "0.0.5",
19
19
  "validate:module-path-glob": "0.0.8",
20
+ "validate:decision-citation": "0.0.10",
21
+ "check:decision-citation": "0.0.10",
22
+ "validate:future-unanchored": "0.0.10",
20
23
  };
21
24
 
22
25
  /** Whether a rule arrived after the version the project's code meets, so it waits for `realign`. */
@@ -25,4 +25,4 @@ Not defined yet. `cruze land` fills this in from the module map in `docs/archite
25
25
 
26
26
  ## Known pitfalls
27
27
 
28
- None recorded yet. `land` adds a line here when the same review finding or defect happens twice.
28
+ None recorded yet. `land` adds a line here when the same review finding or defect about this project's code happens twice.