@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.
- package/README.md +17 -13
- package/dist/adapters/inbound/cli/cli_context.js +4 -0
- package/dist/adapters/inbound/cli/commands/diff.js +23 -0
- package/dist/adapters/inbound/cli/commands/documents.js +1 -1
- package/dist/adapters/inbound/cli/commands/show.js +66 -2
- package/dist/adapters/inbound/cli/commands/status.js +7 -3
- package/dist/adapters/inbound/cli/commands.js +9 -3
- package/dist/adapters/inbound/cli/parse_command.js +8 -0
- package/dist/adapters/outbound/git_repository.js +9 -0
- package/dist/app/ports/repository.js +4 -0
- package/dist/app/use_cases/approve_artifact.js +18 -10
- package/dist/app/use_cases/check_code.js +5 -3
- package/dist/app/use_cases/journal_events.js +6 -2
- package/dist/app/use_cases/new_project_doc.js +10 -2
- package/dist/app/use_cases/run_review.js +10 -2
- package/dist/app/use_cases/show_elements.js +18 -0
- package/dist/app/use_cases/show_notes.js +11 -0
- package/dist/app/use_cases/show_scope.js +15 -1
- package/dist/app/use_cases/upstream_diff.js +55 -0
- package/dist/domain/approvals/rebase.js +39 -0
- package/dist/domain/approvals/upstream_text.js +36 -0
- package/dist/domain/check/check_source.js +18 -1
- package/dist/domain/check/decision_comments.js +38 -0
- package/dist/domain/decision_citations.js +62 -0
- package/dist/domain/edits/agents_file.js +9 -0
- package/dist/domain/ids.js +3 -1
- package/dist/domain/journal.js +6 -4
- package/dist/domain/land/merge_plan.js +7 -2
- package/dist/domain/line_diff.js +56 -0
- package/dist/domain/project/element_lookup.js +124 -0
- package/dist/domain/project/living_model.js +2 -1
- package/dist/domain/project/notes.js +74 -0
- package/dist/domain/skill_budget.js +36 -0
- package/dist/domain/status/next_step.js +27 -5
- package/dist/domain/status/pending_rethink.js +22 -0
- package/dist/domain/status/work_status.js +17 -2
- package/dist/domain/validation/decision_rules.js +76 -0
- package/dist/domain/validation/delta_rules.js +2 -0
- package/dist/domain/validation/future_rules.js +40 -0
- package/dist/domain/validation/living_rules.js +2 -0
- package/dist/domain/validation/validate_project.js +4 -0
- package/dist/domain/validation/vision_rules.js +41 -0
- package/dist/domain/versions/rule_versions.js +3 -0
- package/package.json +1 -1
- package/skills/architect/SKILL.md +2 -1
- package/skills/architect/feature.md +6 -4
- package/skills/architect/project.md +2 -2
- package/skills/behavioural-testing/SKILL.md +3 -2
- package/skills/behavioural-testing/fakes.md +3 -3
- package/skills/build/SKILL.md +6 -6
- package/skills/envision/SKILL.md +3 -2
- package/skills/envision/feature.md +3 -1
- package/skills/envision/intake.md +28 -0
- package/skills/envision/possibilities.md +13 -0
- package/skills/envision/project.md +5 -3
- package/skills/explore/SKILL.md +2 -2
- package/skills/formats/SKILL.md +13 -2
- package/skills/formats/reference/architecture.md +7 -3
- package/skills/formats/reference/durable-docs.md +4 -1
- package/skills/formats/reference/work-items.md +1 -1
- package/skills/formats/templates/vision.md +12 -0
- package/skills/grilling/SKILL.md +10 -0
- package/skills/hexagonal-design/languages/rust.md +2 -1
- package/skills/hexagonal-design/runtime.md +1 -0
- package/skills/land/SKILL.md +6 -2
- package/skills/plan/SKILL.md +13 -3
- package/skills/plan/tasks.md +3 -3
- package/skills/realign/SKILL.md +5 -3
- package/skills/realign/notes/0.0.10.md +131 -0
- package/skills/realign/notes/0.0.11.md +15 -0
- package/skills/realign/notes/0.0.2.md +1 -1
- package/skills/realign/notes/0.0.6.md +1 -1
- package/skills/realign/notes/0.0.8.md +1 -1
- package/skills/realign/notes/0.0.9.md +2 -2
- package/skills/realign/notes.md +2 -0
- package/skills/research/SKILL.md +3 -2
- package/skills/rethink/SKILL.md +7 -4
- package/skills/rethink/levels.md +15 -6
- package/skills/roadmap/SKILL.md +2 -1
- package/skills/roles/SKILL.md +4 -4
- package/skills/roles/auditor.md +1 -1
- package/skills/roles/code-reviewer.md +6 -5
- package/skills/roles/design-reviewer.md +4 -40
- package/skills/roles/design-rubric.md +40 -0
- package/skills/roles/plan-rubric.md +11 -0
- package/skills/roles/surveyor.md +3 -2
- package/skills/triage/SKILL.md +1 -1
- package/skills/verify/SKILL.md +6 -6
- package/src/adapters/inbound/cli/cli_context.ts +4 -0
- package/src/adapters/inbound/cli/commands/diff.ts +23 -0
- package/src/adapters/inbound/cli/commands/documents.ts +1 -1
- package/src/adapters/inbound/cli/commands/show.ts +66 -2
- package/src/adapters/inbound/cli/commands/status.ts +7 -3
- package/src/adapters/inbound/cli/commands.ts +9 -3
- package/src/adapters/inbound/cli/parse_command.ts +8 -0
- package/src/adapters/outbound/git_repository.ts +9 -0
- package/src/app/ports/repository.ts +4 -0
- package/src/app/use_cases/approve_artifact.ts +18 -10
- package/src/app/use_cases/check_code.ts +5 -3
- package/src/app/use_cases/journal_events.ts +6 -2
- package/src/app/use_cases/new_project_doc.ts +10 -2
- package/src/app/use_cases/run_review.ts +10 -2
- package/src/app/use_cases/show_elements.ts +18 -0
- package/src/app/use_cases/show_notes.ts +11 -0
- package/src/app/use_cases/show_scope.ts +15 -1
- package/src/app/use_cases/upstream_diff.ts +55 -0
- package/src/domain/approvals/rebase.ts +39 -0
- package/src/domain/approvals/upstream_text.ts +36 -0
- package/src/domain/check/check_source.ts +18 -1
- package/src/domain/check/decision_comments.ts +38 -0
- package/src/domain/decision_citations.ts +62 -0
- package/src/domain/edits/agents_file.ts +9 -0
- package/src/domain/ids.ts +3 -1
- package/src/domain/journal.ts +6 -4
- package/src/domain/land/merge_plan.ts +7 -2
- package/src/domain/line_diff.ts +56 -0
- package/src/domain/project/element_lookup.ts +124 -0
- package/src/domain/project/living_model.ts +2 -1
- package/src/domain/project/notes.ts +74 -0
- package/src/domain/skill_budget.ts +36 -0
- package/src/domain/status/next_step.ts +27 -5
- package/src/domain/status/pending_rethink.ts +22 -0
- package/src/domain/status/work_status.ts +17 -2
- package/src/domain/validation/decision_rules.ts +76 -0
- package/src/domain/validation/delta_rules.ts +2 -0
- package/src/domain/validation/future_rules.ts +40 -0
- package/src/domain/validation/living_rules.ts +2 -0
- package/src/domain/validation/validate_project.ts +4 -0
- package/src/domain/validation/vision_rules.ts +41 -0
- package/src/domain/versions/rule_versions.ts +3 -0
- 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:
|
|
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
|
-
|
|
135
|
-
|
|
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:
|
|
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
|
-
|
|
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`. */
|
package/templates/init/AGENTS.md
CHANGED
|
@@ -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.
|