@printpractical/cruze 0.0.9 → 0.0.10

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 (129) 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 +3 -1
  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 +2 -2
  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.2.md +1 -1
  71. package/skills/realign/notes/0.0.6.md +1 -1
  72. package/skills/realign/notes/0.0.8.md +1 -1
  73. package/skills/realign/notes/0.0.9.md +2 -2
  74. package/skills/realign/notes.md +1 -0
  75. package/skills/research/SKILL.md +3 -2
  76. package/skills/rethink/SKILL.md +5 -3
  77. package/skills/rethink/levels.md +6 -6
  78. package/skills/roadmap/SKILL.md +2 -1
  79. package/skills/roles/SKILL.md +3 -3
  80. package/skills/roles/auditor.md +1 -1
  81. package/skills/roles/code-reviewer.md +2 -2
  82. package/skills/roles/design-reviewer.md +4 -40
  83. package/skills/roles/design-rubric.md +40 -0
  84. package/skills/roles/plan-rubric.md +11 -0
  85. package/skills/roles/surveyor.md +3 -2
  86. package/skills/triage/SKILL.md +1 -1
  87. package/src/adapters/inbound/cli/cli_context.ts +4 -0
  88. package/src/adapters/inbound/cli/commands/diff.ts +23 -0
  89. package/src/adapters/inbound/cli/commands/documents.ts +1 -1
  90. package/src/adapters/inbound/cli/commands/show.ts +66 -2
  91. package/src/adapters/inbound/cli/commands/status.ts +7 -3
  92. package/src/adapters/inbound/cli/commands.ts +9 -3
  93. package/src/adapters/inbound/cli/parse_command.ts +8 -0
  94. package/src/adapters/outbound/git_repository.ts +9 -0
  95. package/src/app/ports/repository.ts +4 -0
  96. package/src/app/use_cases/approve_artifact.ts +18 -10
  97. package/src/app/use_cases/check_code.ts +5 -3
  98. package/src/app/use_cases/journal_events.ts +6 -2
  99. package/src/app/use_cases/new_project_doc.ts +10 -2
  100. package/src/app/use_cases/run_review.ts +10 -2
  101. package/src/app/use_cases/show_elements.ts +18 -0
  102. package/src/app/use_cases/show_notes.ts +11 -0
  103. package/src/app/use_cases/show_scope.ts +15 -1
  104. package/src/app/use_cases/upstream_diff.ts +55 -0
  105. package/src/domain/approvals/rebase.ts +39 -0
  106. package/src/domain/approvals/upstream_text.ts +36 -0
  107. package/src/domain/check/check_source.ts +18 -1
  108. package/src/domain/check/decision_comments.ts +38 -0
  109. package/src/domain/decision_citations.ts +62 -0
  110. package/src/domain/edits/agents_file.ts +9 -0
  111. package/src/domain/ids.ts +3 -1
  112. package/src/domain/journal.ts +3 -1
  113. package/src/domain/land/merge_plan.ts +7 -2
  114. package/src/domain/line_diff.ts +56 -0
  115. package/src/domain/project/element_lookup.ts +124 -0
  116. package/src/domain/project/living_model.ts +2 -1
  117. package/src/domain/project/notes.ts +74 -0
  118. package/src/domain/skill_budget.ts +36 -0
  119. package/src/domain/status/next_step.ts +27 -5
  120. package/src/domain/status/pending_rethink.ts +22 -0
  121. package/src/domain/status/work_status.ts +17 -2
  122. package/src/domain/validation/decision_rules.ts +76 -0
  123. package/src/domain/validation/delta_rules.ts +2 -0
  124. package/src/domain/validation/future_rules.ts +40 -0
  125. package/src/domain/validation/living_rules.ts +2 -0
  126. package/src/domain/validation/validate_project.ts +4 -0
  127. package/src/domain/validation/vision_rules.ts +41 -0
  128. package/src/domain/versions/rule_versions.ts +3 -0
  129. package/templates/init/AGENTS.md +1 -1
@@ -0,0 +1,36 @@
1
+
2
+
3
+ /**
4
+ * The words one instruction file may hold. A longer file holds material only some runs need, so it
5
+ * splits by branch, as `docs/contributing/writing-skills.md` says, and each run reads only its part.
6
+ */
7
+ export const MAX_FILE_WORDS = 2100;
8
+
9
+ /**
10
+ * The words agents may read across the whole bundle. Every retro adds rules, and between 0.0.1 and
11
+ * 0.0.10 the skills grew by half while nothing was removed. Lower this when text is cut. Raising it
12
+ * is the maintainer's decision, with the reason in the commit that raises it.
13
+ */
14
+ export const BUNDLE_WORD_BUDGET = 44500;
15
+
16
+ /** Data the CLI reads or renders, rather than instructions an agent reads. */
17
+ const NOT_INSTRUCTIONS = /(^|\/)(templates|notes)\//;
18
+
19
+ export function wordCount(text ) {
20
+ return text.split(/\s+/).filter((word) => word !== "").length;
21
+ }
22
+
23
+ /** Instruction files over their limit, and the bundle when it is over its budget. */
24
+ export function budgetProblems(skills ) {
25
+ const files = skills.flatMap((skill) =>
26
+ skill.files.filter((f) => f.path.endsWith(".md") && !NOT_INSTRUCTIONS.test(f.path)).map((f) => ({ path: `${skill.folder}/${f.path}`, words: wordCount(f.text) })),
27
+ );
28
+ const problems = files
29
+ .filter((f) => f.words > MAX_FILE_WORDS)
30
+ .map((f) => `${f.path}: ${f.words} words, over ${MAX_FILE_WORDS}; move what only some runs need into a sibling file`);
31
+ const total = files.reduce((sum, f) => sum + f.words, 0);
32
+ if (total > BUNDLE_WORD_BUDGET) {
33
+ problems.push(`the skills hold ${total} words, over the budget of ${BUNDLE_WORD_BUDGET}; cut or merge text to make room, or raise the budget with the reason in the commit`);
34
+ }
35
+ return problems;
36
+ }
@@ -1,4 +1,5 @@
1
- import { evaluateApproval } from "../approvals/evaluate.js";
1
+ import { evaluateApproval, } from "../approvals/evaluate.js";
2
+ import { rebaseNeeded } from "../approvals/rebase.js";
2
3
  import { PATHS } from "../project/layout.js";
3
4
  import { isPlanned, readChangesTable } from "../project/plan_parts.js";
4
5
  import { readProgress } from "../project/progress.js";
@@ -7,6 +8,7 @@ import { readRoadmapItems, readRoadmapStatus } from "../project/roadmap.js";
7
8
  import { changesOf, featureOf, } from "../project/work_items.js";
8
9
  import { compareVersions } from "../versions/cruze_version.js";
9
10
  import { FIRST_RELEASE } from "../versions/rule_versions.js";
11
+ import { pendingReason, pendingRethinks } from "./pending_rethink.js";
10
12
  import { awaitsRethink, isVerified } from "./verification.js";
11
13
  import { activeChange } from "./work_status.js";
12
14
 
@@ -112,7 +114,9 @@ function itemSteps(view , item , branch )
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 = [];
@@ -131,14 +135,17 @@ function changeStep(view , change , branch )
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 , change , branch )
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 , item , label , state ) {
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 ) {
152
174
  const text = view.snapshot.get(PATHS.roadmap);
@@ -0,0 +1,22 @@
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
+ }
@@ -5,6 +5,7 @@ import { readChangesTable, readScope } from "../project/plan_parts.js";
5
5
  import { readProgress } from "../project/progress.js";
6
6
 
7
7
  import { changesOf, featureOf, } from "../project/work_items.js";
8
+ import { pendingReason, pendingRethinks } from "./pending_rethink.js";
8
9
 
9
10
 
10
11
 
@@ -15,6 +16,8 @@ import { changesOf, featureOf, } from "../project/work_items.js";
15
16
 
16
17
 
17
18
 
19
+
20
+
18
21
 
19
22
 
20
23
 
@@ -22,6 +25,8 @@ import { changesOf, featureOf, } from "../project/work_items.js";
22
25
 
23
26
 
24
27
 
28
+
29
+
25
30
 
26
31
 
27
32
 
@@ -42,6 +47,7 @@ export function projectStatus(view , branch )
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 = { documents, features, standalone };
@@ -69,7 +75,12 @@ function changeStatus(view , change ) {
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 , item ) {
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 , branch )
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, } 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
+ }
@@ -4,6 +4,7 @@ import { specPath } from "../project/layout.js";
4
4
  import { orphanDeltaElements, readDelta, } from "../project/deltas.js";
5
5
 
6
6
 
7
+ import { deltaDecisionProblems } from "./decision_rules.js";
7
8
  import { elementProblems } from "./element_rules.js";
8
9
  import { error, } from "./problem.js";
9
10
 
@@ -44,6 +45,7 @@ export function deltaProblems(view , item ) {
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.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
+ }
@@ -4,6 +4,7 @@ import { findIds, scopeOf, STATUS_KINDS } from "../ids.js";
4
4
 
5
5
  import { PATHS } from "../project/layout.js";
6
6
 
7
+ import { livingDecisionProblems } from "./decision_rules.js";
7
8
  import { elementProblems } from "./element_rules.js";
8
9
  import { error, warning, } from "./problem.js";
9
10
 
@@ -26,6 +27,7 @@ export function livingProblems(view ) {
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
 
2
+ import { futureProblems } from "./future_rules.js";
2
3
  import { livingProblems } from "./living_rules.js";
3
4
  import { error, sortProblems, } from "./problem.js";
4
5
  import { configProblems, readmeProblems, roadmapProblems, templateLeftovers } from "./project_rules.js";
6
+ import { visionProblems } from "./vision_rules.js";
5
7
  import { workItemProblems } from "./work_item_rules.js";
6
8
 
7
9
  /** Every rule in the cruze-formats skill, applied to the whole project. */
@@ -13,6 +15,8 @@ export function validateProject(view ) {
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.js";
2
+ import { findSection } from "../markdown.js";
3
+ import { splitRow } from "../markdown_tables.js";
4
+ import { PATHS } from "../project/layout.js";
5
+
6
+ import { error, } from "./problem.js";
7
+
8
+ /** The section and the cells after the ID that each long-term row of the vision holds. */
9
+ const INTENT_ROWS = {
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 ) {
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 = {
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`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@printpractical/cruze",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "Spec-driven development for coding agents: one living model of the system, small deltas, convergent review.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Chris Buchter",
@@ -15,7 +15,7 @@ Architect decides the how and the how big. It designs the domain, the ports and
15
15
  - **Tweak scope:** the user asks for a small change to existing behaviour. When the design shows it needs any architecture change, it becomes a feature: say so and continue at feature scope.
16
16
  - Done when you can say which, and why.
17
17
  2. **Check what it builds on.** Run `cruze status`. `docs/vision.md` must be approved and current. At feature and tweak scope, `docs/architecture.md` must be approved and current as well. When a document isn't, stop and name the step that fixes it: `envision` for the vision, and `rethink` for an architecture that changed since its approval.
18
- 3. **Read the living model.** Read `docs/vision.md`, `docs/glossary.md`, `docs/architecture.md`, the ADRs in `docs/adr/`, `docs/roadmap.md`, `.cruze/config.yaml`, and the specs and code the work touches. Read any note in `.cruze/notes/` the user points to.
18
+ 3. **Read the living model.** Read `docs/vision.md`, `docs/glossary.md`, `docs/architecture.md` as "Reading it" in `.agents/skills/cruze-formats/reference/architecture.md` says, the ADRs in `docs/adr/`, `docs/roadmap.md`, `.cruze/config.yaml`, and the specs and code the work touches. Take up the notes and the parts of the user's input handed to this work, as "Taking up a hand-off later" in `.agents/skills/cruze-envision/intake.md` describes: at project scope, `cruze show --notes architect`, `cruze show --notes roadmap` and `cruze show --notes project`; at feature or tweak scope, the Notes section of `cruze show <feature>`. Read any other note the user points to.
19
19
  4. **Load the standards.** Read these, and follow them for the rest of this step:
20
20
  - `.agents/skills/cruze-hexagonal-design/SKILL.md`
21
21
  - `.agents/skills/cruze-grilling/SKILL.md`
@@ -31,6 +31,7 @@ Architect decides the how and the how big. It designs the domain, the ports and
31
31
  ## Rules
32
32
 
33
33
  - **Design for the roadmap, build for the phase.** Read the whole feature map and roadmap. When a later feature will need a seam, name it now in a `Future` fact, such as "transport will gain MQTT in phase 3, so inbound adapters stay behind `CommandSource`". Build only what the current work needs.
34
+ - **Answer to the vision's long-term intent.** Read `## Principles` and `## Must stay possible` in `docs/vision.md`. Every design follows each principle. Check the design against every `LATER` row, not only those in its area: nothing in it may rule one out. Where the design touches a capability's area, leave the room its row names, in a `Future` fact that cites its ID, and build nothing for it. A design that would depart from a principle or close off a capability goes to the user first, as "Long-term intent" in `.agents/skills/cruze-grilling/SKILL.md` describes.
34
35
  - **Adopt before you build.** Before designing any component that isn't domain logic, run the researcher role from `.agents/skills/cruze-roles/SKILL.md` on it. Record every decision in `## Adopt or build`, or at project scope in the adapter's `Adopts` fact. Every new dependency goes to the user through the dependency-approval skill.
35
36
  - **Every element with code has a home.** Each `ENT`, `UC`, `PORT`, `ADP` and `FLOW` names its `Module` and `File`. Each file has one responsibility. Never split an owner across files to keep them short.
36
37
  - **Comply with the references.** When the project must follow an external document, such as a protocol specification, it is listed under `references:` in `.cruze/config.yaml`; at project scope, ask the user whether there is one, and list it with what it governs. Read the parts each element relies on, and cite the section in the element's body, such as `RFC 4254 §5.2`, so plan, build and the reviewers can check it.
@@ -9,15 +9,17 @@ The result is a `feature.md` with a complete architecture delta and an ordered l
9
9
 
10
10
  ## Design the delta
11
11
 
12
- 3. **Trace each scenario through the model.** For every scenario in the spec delta, name the inbound adapter it enters through, the use case that runs it, the domain elements that decide it, and the ports it crosses. Done when every scenario has a path, and each missing piece is a named gap.
12
+ 3. **Trace each scenario through the model.** Find the elements with `cruze show --index`, and read each one the path crosses with `cruze show <ID>`. For every scenario in the spec delta, name the inbound adapter it enters through, the use case that runs it, the domain elements that decide it, and the ports it crosses. Done when every scenario has a path, and each missing piece is a named gap.
13
13
  4. **Place each gap,** following "Placing an owner" in the hexagonal-design skill. Prefer extending an existing element when it is the right owner. Each change to the living model becomes an operation under `## Architecture delta`:
14
14
  - `ADDED` for a new element, with its full body.
15
- - `MODIFIED` for an existing element, with its full new body. Copy the current body from `docs/architecture.md` and edit it, so nothing is lost by accident.
15
+ - `MODIFIED` for an existing element, with its full new body. Copy the current body from `cruze show <ID>` and edit it, so nothing is lost by accident.
16
16
  - `REMOVED` with a `Reason`, for an element the feature retires.
17
+ - When the delta changes or retires a rule, find every element, ADR, decision and sentence in the living docs and this feature that states the old rule, and modify or reword each.
18
+ - A new use of an existing port operation, such as the clock stamping a new kind of time, is a `MODIFIED` port whose `Operations` name the use, and each use case lists every port it calls under `Uses`.
17
19
  5. **Link behaviour to design.** Each use case that runs a requirement lists it under `Serves`. Add or modify a `FLOW` for each key scenario, with `Serves` naming its scenarios and the failure paths they need.
18
20
  6. **Research and decide.** For each new component that isn't domain logic, run the researcher role and record a row in `## Adopt or build`. Take every new dependency to the user. Then verify every claim the design relies on, following step 7 of adopt or build in `.agents/skills/cruze-research/SKILL.md`. Done when no claim in the design is unverified.
19
- 7. **Keep later seams open.** When the feature map shows a later feature that will extend this area, record the seam in a `Future` fact, and don't build it.
20
- 8. Done when `cruze validate` reports no errors in the delta itself, and every cited ID exists in the living docs or this delta.
21
+ 7. **Keep later seams open.** Check the design against every row of the vision's `## Must stay possible`: nothing in it may rule one out. When the feature map shows a later feature that will extend this area, or a capability needs room in it, record the seam in a `Future` fact on the element that keeps it, citing the `LATER` ID or the feature's slug, and don't build it.
22
+ 8. Done when `cruze validate` reports no errors and no `decision-citation` warning in the delta itself, and every cited ID exists in the living docs or this delta.
21
23
 
22
24
  ## Split it into changes
23
25
 
@@ -19,7 +19,7 @@ The result has four parts, reviewed together and shown at one walkthrough:
19
19
  8. **Modules (`MOD`).** Name the kind of system in the Overview, and choose the layout that is normal for that kind and the language, following `.agents/skills/cruze-hexagonal-design/shapes.md` and the language file. Give each context the fewest modules that keep its layer rules, usually one `core` module and its adapters, and give the shared CLI shell and the composition root the `system` scope. Add a `system` module in the `composition` layer for the build and CI files, such as the package manifest and `.github/workflows/`, so the tasks that write them have an owner. Every source file must fall inside some module's `Path`, including namespace files that join a context's modules, such as `src/inventory/mod.rs` or `index.ts`; list them in a `composition` module's `Path` when no other module fits. Every module gets a `Path` and a `Layer`, and every element above gets its `Module` and `File`.
20
20
  9. **Dependency rules (`RULE`).** State what may import what, in one or two sentences each.
21
21
  10. **Cross-cutting concerns (`XC`).** Cover errors, configuration, logging, concurrency and security, plus build and delivery (CI, formatting, linting). Read `.agents/skills/cruze-hexagonal-design/runtime.md` for concurrency.
22
- 11. **Decisions.** Write an ADR, with `cruze new adr <slug> --title "<title>"`, for each decision that passes the ADR test. Record the other project decisions as `D<n>` lines under `## Decisions`.
22
+ 11. **Decisions.** Write an ADR, with `cruze new adr <slug> --title "<title>"`, for each decision that passes the ADR test. Record the other project decisions as `D<n>` lines under `## Decisions`, including each design choice handed to `architect` that the user keeps.
23
23
  12. **Overview (`VIEW-context`).** Draw the context diagram last, once the adapters and external systems are known.
24
24
 
25
25
  Done when `cruze validate` reports no errors in `docs/architecture.md`. At this stage it warns that elements have no status yet; approval adds the statuses.
@@ -34,7 +34,7 @@ Add the language's build output and tool caches to `.gitignore` now, such as `/t
34
34
 
35
35
  ## Plan the release
36
36
 
37
- 1. **Create the roadmap.** Run `cruze new roadmap`. Agree the first release with the user: its name, and which features from the vision's feature map it delivers.
37
+ 1. **Create the roadmap.** Run `cruze new roadmap`. Agree the first release with the user: its name, and which features from the vision's feature map it delivers. Take up the delivery order handed to `roadmap`, as for any hand-off.
38
38
  2. **Order the work.** Phase 1 holds only the `walking-skeleton` change. Later phases hold the release's features. Done when the roadmap passes the rules in step 5 of `.agents/skills/cruze-roadmap/SKILL.md`.
39
39
  3. **Design the walking skeleton.** Run `cruze new change walking-skeleton --title "Walking skeleton"`. Then:
40
40
  - Size the elements it builds for the skeleton. Anything the walking skeleton lists under `Builds` is marked built when it lands, so write those elements, such as the CLI adapter or the first entity, at the size the skeleton needs. Record their planned growth in `Future` facts; later features grow them through `MODIFIED` operations.
@@ -11,7 +11,7 @@ Behaviour is the contract. Every scenario in the specs becomes a test that drive
11
11
 
12
12
  1. Find the test plan rows whose Subject the task proves. Done when you know the seam, test file and kind of every test the task needs.
13
13
  2. Write one test at its seam, carrying its subject ID. Take the inputs and expected values from the scenario's GIVEN, WHEN and THEN.
14
- 3. Run it and watch it fail for the reason you expect (red). A test that passes before the code exists proves nothing, so fix the test.
14
+ 3. Run it and watch it fail for the reason you expect (red). A test that passes before the code exists proves nothing, so fix the test. A test that guards a race or an ordering must be seen failing against code that has the flaw: run the racing work concurrently, on a multi-threaded runtime where there is one, and wait for a signal the system gives, such as a watch having opened, never for a fixed sleep.
15
15
  4. Write the least code that makes it pass (green). Then write the next test. Work in tracer bullets, one test and one slice of code at a time, never all the tests first.
16
16
  5. Done when every test plan row whose Subject the task proves has a passing test carrying that ID, and each test fails when the behaviour it checks is removed. Tasks that prove only a `UC`, `ADP` or `MOD` are proved by the rows of the scenarios and ports they serve.
17
17
 
@@ -39,13 +39,14 @@ A behaviour test drives a use case, not a single domain type, so it keeps passin
39
39
  ## Test support
40
40
 
41
41
  - Share fixtures and harnesses the way you share fakes. Keep one builder or constant for each domain value tests keep needing, such as a valid configuration or an ID from a string, and one harness per seam, such as an in-process service or the real binary started on a temporary directory. They live in the test support module the language file names.
42
- - Before writing a helper in a test file, look in the test support module. Use or extend what is there, and never redefine it.
42
+ - Before writing a helper in a test file, look in the test support module and in the test binary's other files. Use or extend what is there, and never redefine it. When a second test file needs a helper, move it into the test support module then, and use it from both.
43
43
  - Split a test file by the behaviour it covers, one use case, port or flow per file. File budgets apply to source, not to tests, so never split a test file to meet one.
44
44
 
45
45
  ## Expected values
46
46
 
47
47
  - Take expected values from an independent source: the scenario's worked example, a spec, or a known-good literal. Never compute the expected value the way the code does.
48
48
  - For a failure scenario, check both the error and the state left behind. After a refused second hop, the first hop is closed.
49
+ - Compare through the constructors and values the code already has. A test is not a caller: never add an accessor, variant or equality to production code only so a test can observe something.
49
50
  - Where a plausible wrong outcome exists, assert that it didn't happen.
50
51
 
51
52
  ## Protecting the check
@@ -4,9 +4,9 @@ A fake is a real, working implementation of a driven port that keeps its state i
4
4
 
5
5
  ## Writing a fake
6
6
 
7
- 1. Read the port's `Operations` in `docs/architecture.md`. The fake implements every operation with the guarantees and failures listed there, and nothing more.
7
+ 1. Read the port's `Operations`, with `cruze show <PORT ID>`. The fake implements every operation with the guarantees and failures listed there, and nothing more. That includes every way the real adapter ends on its own, such as a session closed when its stream ends or its server shuts down.
8
8
  2. Keep state in plain in-memory structures: a map for a catalog, a list for recorded effects, a settable value for a clock.
9
- 3. Give tests a way to arrange state and to inject each failure the contract lists. Use a constructor that takes the initial state, and a method such as `fail_next_with(HopError::Refused)`.
9
+ 3. Give tests a way to arrange state and to inject each failure the contract lists. Use a constructor that takes the initial state, and a method such as `fail_next_with(HopError::Refused)`. When a use case writes through the port, also let a test stall an operation, and fail one that writes several things partway through, so each partial failure and cancellation can be tested from every state it can leave.
10
10
  4. Record effects the contract makes observable, such as the order hops were closed in, so tests can assert them.
11
11
  5. Put the fake where every test can reach it, such as `tests/support/` or the language's test-only module, never in production code paths. One fake per port, shared by all tests.
12
12
 
@@ -17,7 +17,7 @@ Done when the fake passes the port's contract tests.
17
17
  A contract test states the port's contract once and runs it against every implementation.
18
18
 
19
19
  1. Write the cases as a function or suite that takes a constructor for the implementation under test.
20
- 2. Cover each operation's success, each failure the contract names, and the state after a failure.
20
+ 2. Cover each operation's success, each failure the contract names, each way the implementation ends on its own, and the state after a failure.
21
21
  3. Run the suite against the fake in the ordinary test run.
22
22
  4. Run it against each real adapter too. When the real adapter needs something external, such as an SSH server, a serial device or a database, use a local stand-in (a test server, a pseudo-terminal, a container), and mark the run so CI can provide it.
23
23
 
@@ -16,7 +16,7 @@ Build turns an approved plan into code, one task at a time. Each task starts wit
16
16
  - `.agents/skills/cruze-behavioural-testing/SKILL.md`
17
17
  - `.agents/skills/cruze-hexagonal-design/SKILL.md`, and its language file when one exists for the project's language
18
18
  - `.agents/skills/cruze-dependency-approval/SKILL.md`
19
- 3. **Read the plan and the design.** Read the change's `change.md`. Run `cruze show <ref>` and read its output: the full body of every element the change builds, every scenario it delivers, and the settled decisions. Read an element it lists as cited only when the task calls it, from the place it names, never `feature.md` or `docs/architecture.md` whole. Read a reference it lists at the sections the change's elements cite. Read `commands:` in `.cruze/config.yaml`.
19
+ 3. **Read the plan and the design.** Read the change's `change.md`. Run `cruze show <ref>` and read its output: the full body of every element the change builds, every scenario it delivers, and the settled decisions. Read an element it lists as cited only when the task calls it, with `cruze show <ID>`, never `feature.md` or `docs/architecture.md` whole. Read a reference it lists at the sections the change's elements cite. Read `commands:` in `.cruze/config.yaml`.
20
20
  4. **Build each open task in order,** following the task loop below. Skip the tasks already ticked in `## Progress`.
21
21
  5. **Finish.** When every task is ticked:
22
22
  - Run the format, lint and build commands and the full suite, as `.agents/skills/cruze-formats/reference/config.md` defines it under `commands`.
@@ -36,7 +36,7 @@ For task `T<n>`:
36
36
  5. **Commit** with a Conventional Commit message naming the task, such as `feat(access): console session state machine (T2)`.
37
37
  6. **Tick it.** Run `cruze task done T<n>`, which records the commit. Its progress edit rides along in the next commit.
38
38
 
39
- A task with no behaviour of its own, such as a package manifest or a plain data type, has no red step. Build it, check it, commit it and tick it, and the first task whose test uses it covers it.
39
+ A task with no behaviour of its own, such as a package manifest or a plain data type, has no red step. Build it, check it, commit it and tick it, and the first task whose test uses it covers it. Neither has a prefactor's task: run the tests its plan names under `## Risks` before the first task, and after each task, and they pass unchanged.
40
40
 
41
41
  Done for a task when its tests pass, `cruze check --ci` passes for its files, the commit exists and the task is ticked.
42
42
 
@@ -10,7 +10,7 @@ Envision decides the what: the problem, who has it, and the behaviour that solve
10
10
  ## Steps
11
11
 
12
12
  1. **Pick the scope.** It is project scope when `docs/vision.md` doesn't exist yet, or when the user asks to rework the project's goals. It is feature scope when the user names a feature. When unsure, ask. Done when you can say which.
13
- 2. **Gather the input.** Read what the user gave you: pasted drafts, files they point to, and notes in `.cruze/notes/`. Read whichever of `docs/vision.md`, `docs/glossary.md`, `docs/roadmap.md` and `docs/architecture.md` exist, and run `cruze status`.
13
+ 2. **Gather the input.** Read what the user gave you: pasted drafts, files they point to, and the notes `cruze show --notes` lists. Read whichever of `docs/vision.md`, `docs/glossary.md`, `docs/roadmap.md` and `docs/architecture.md` exist, and run `cruze status`.
14
14
  3. **Load the standards.** Read `.agents/skills/cruze-grilling/SKILL.md` and `.agents/skills/cruze-domain-language/SKILL.md`, and follow them for the rest of this step.
15
15
  4. **Follow the scope's procedure.** For project scope, follow [project.md](project.md). For feature scope, follow [feature.md](feature.md). Each one ends with the user's confirmation.
16
16
  5. **Check the documents.** Run `cruze validate`. Done when it reports no errors in the documents this step wrote. At feature scope, expect one `not-designed` warning for the feature, which `architect` clears.
@@ -20,7 +20,8 @@ Envision decides the what: the problem, who has it, and the behaviour that solve
20
20
  - Write in the user's terms and the glossary's words. Behaviour is described from outside the system: what a user does and what they observe.
21
21
  - Requirements and scenarios follow `.agents/skills/cruze-formats/reference/specs.md`. Every scenario uses concrete values: real names, real inputs, exact output. The scenarios become the tests, so their values are the expected results.
22
22
  - Keep implementation out: no modules, types, libraries or file formats unless the user names one as a constraint. When the user raises one, record it as a constraint or a settled decision and move on.
23
- - The vision stays within one page, excluding the feature map.
23
+ - Problem through Constraints stay within one page. A principle or a capability that must stay possible takes as much as its intent needs: never shorten one to fit.
24
+ - The product's long-term intent lives in the vision, in its Principles and Must stay possible tables, at either scope. Designs answer to it for the product's whole life, long after the conversation that settled it.
24
25
  - Only the CLI writes the feature map. Use `cruze features add`, `cruze features update` and `cruze features drop`.
25
26
 
26
27
  ## Next step
@@ -7,13 +7,15 @@ The result is a feature folder whose `feature.md` holds the intent, the settled
7
7
  - When it isn't in the roadmap's phases, ask the user whether it belongs in this release. If it does, add it by following "Adding an item" in `.agents/skills/cruze-roadmap/SKILL.md`, and re-approve the roadmap. If it doesn't, put it on the future list with `cruze features add` and stop here.
8
8
  - When its `Blocked by` items haven't landed (see the roadmap's `## Status`), tell the user. Continue only if they want to design ahead, and record that as a settled decision.
9
9
  3. **Create the folder.** When the feature has no folder yet, run `cruze new feature <slug> --title "<title>" --roadmap <slug>`, using its roadmap slug. When a folder exists, edit its `feature.md`.
10
- 4. **Read the behaviour it touches.** Read the specs in `docs/specs/` for every capability the feature changes, so you know each requirement and scenario it builds on.
10
+ 4. **Read the behaviour it touches.** Read the specs in `docs/specs/` for every capability the feature changes, so you know each requirement and scenario it builds on. Run `cruze show --notes <slug>`, and take up the notes about this feature and the parts of the user's input handed to it, open questions included, as "Taking up a hand-off later" in [intake.md](intake.md) describes.
11
11
  5. **List the open decisions:**
12
12
  - **Intent:** the problem and outcome in the user's words, who it is for, the constraints, and what is out of scope.
13
13
  - **Requirements:** each behaviour the feature adds, changes or removes, and the capability it belongs to. It goes in an existing capability when the behaviour fits one; otherwise it starts a new capability.
14
14
  - **Scenarios:** for each requirement, the main case, each edge and failure case a user will meet, and what they observe in each. Invent concrete edge cases to test the boundaries.
15
+ - **Later:** the vision's `PRINCIPLE` and `LATER` rows this feature touches, and the capabilities it may grow into, asked about as [possibilities.md](possibilities.md) describes. A feature that adds sign-in asks about single sign-on and multi-factor authentication, for example.
15
16
  6. **Grill in rounds** until every open decision is settled. Record decisions under `## Settled decisions` and terms in the glossary as they settle.
16
17
  - When a decision changes something the feature's summary on the feature map promises, such as a policy it names, correct the summary with `cruze features update <slug> --summary <text>`.
18
+ - When an answer names something the product may need later, or a decision departs from a principle, the vision changes, as "Long-term intent" in the grilling skill describes. A settled decision of this feature is archived when it lands, so it can't hold either.
17
19
  7. **Write the spec delta.** Use `ADDED` for new requirements, `MODIFIED` for a requirement whose text or scenarios change (writing its full new body with every scenario it keeps), `REMOVED` with a `Reason` and `Migration` for retired behaviour, and `ADDED CAPABILITY` for a new capability.
18
20
  - Name every ID with glossary terms. An ID is never reused.
19
21
  - Done when every requirement has a scenario for its main case and one for each failure a user can meet.