@printpractical/cruze 0.0.3 → 0.0.5
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 +10 -10
- package/dist/adapters/inbound/cli/argument_files.js +29 -0
- package/dist/adapters/inbound/cli/cli_context.js +3 -0
- package/dist/adapters/inbound/cli/command_options.js +20 -0
- package/dist/adapters/inbound/cli/commands/documents.js +5 -1
- package/dist/adapters/inbound/cli/commands/journal.js +3 -2
- package/dist/adapters/inbound/cli/commands/progress.js +7 -3
- package/dist/adapters/inbound/cli/commands/review.js +3 -2
- package/dist/adapters/inbound/cli/commands/status.js +7 -1
- package/dist/adapters/inbound/cli/commands.js +36 -20
- package/dist/adapters/inbound/cli/parse_command.js +12 -2
- package/dist/adapters/inbound/cli/run_cli.js +9 -5
- package/dist/adapters/outbound/node_project_files.js +2 -1
- package/dist/app/use_cases/approve_artifact.js +26 -1
- package/dist/app/use_cases/journal_events.js +12 -4
- package/dist/app/use_cases/project_status.js +8 -2
- package/dist/app/use_cases/record_progress.js +17 -0
- package/dist/domain/approvals/agent_approval.js +68 -0
- package/dist/domain/config.js +30 -2
- package/dist/domain/journal.js +5 -0
- package/dist/domain/status/verification.js +3 -2
- package/dist/domain/status/work_status.js +1 -3
- package/dist/domain/validation/plan_rules.js +2 -0
- package/dist/domain/validation/size_rules.js +36 -0
- package/dist/domain/validation/work_item_rules.js +2 -0
- package/dist/domain/versions/rule_versions.js +3 -0
- package/package.json +1 -1
- package/skills/about/SKILL.md +1 -1
- package/skills/architect/feature.md +7 -2
- package/skills/envision/SKILL.md +1 -1
- package/skills/envision/feature.md +1 -0
- package/skills/formats/SKILL.md +1 -1
- package/skills/formats/reference/config.md +10 -0
- package/skills/formats/reference/durable-docs.md +1 -1
- package/skills/formats/reference/work-items.md +1 -1
- package/skills/land/SKILL.md +1 -0
- package/skills/plan/SKILL.md +3 -3
- package/skills/realign/notes/0.0.4.md +27 -0
- package/skills/realign/notes/0.0.5.md +38 -0
- package/skills/realign/notes.md +2 -0
- package/skills/rethink/SKILL.md +5 -1
- package/skills/rethink/levels.md +4 -4
- package/skills/roles/SKILL.md +17 -7
- package/skills/roles/code-reviewer.md +1 -1
- package/skills/roles/design-reviewer.md +2 -2
- package/skills/verify/SKILL.md +1 -1
- package/src/adapters/inbound/cli/argument_files.ts +29 -0
- package/src/adapters/inbound/cli/cli_context.ts +3 -0
- package/src/adapters/inbound/cli/command_options.ts +20 -0
- package/src/adapters/inbound/cli/commands/documents.ts +5 -1
- package/src/adapters/inbound/cli/commands/journal.ts +3 -2
- package/src/adapters/inbound/cli/commands/progress.ts +7 -3
- package/src/adapters/inbound/cli/commands/review.ts +3 -2
- package/src/adapters/inbound/cli/commands/status.ts +7 -1
- package/src/adapters/inbound/cli/commands.ts +36 -20
- package/src/adapters/inbound/cli/parse_command.ts +12 -2
- package/src/adapters/inbound/cli/run_cli.ts +9 -5
- package/src/adapters/outbound/node_project_files.ts +2 -1
- package/src/app/use_cases/approve_artifact.ts +26 -1
- package/src/app/use_cases/journal_events.ts +12 -4
- package/src/app/use_cases/project_status.ts +8 -2
- package/src/app/use_cases/record_progress.ts +17 -0
- package/src/domain/approvals/agent_approval.ts +68 -0
- package/src/domain/config.ts +30 -2
- package/src/domain/journal.ts +5 -0
- package/src/domain/status/verification.ts +3 -2
- package/src/domain/status/work_status.ts +1 -3
- package/src/domain/validation/plan_rules.ts +2 -0
- package/src/domain/validation/size_rules.ts +36 -0
- package/src/domain/validation/work_item_rules.ts +2 -0
- package/src/domain/versions/rule_versions.ts +3 -0
|
@@ -58,6 +58,23 @@ export async function addFutureFeature(deps: ProjectDeps, slug: string, summary:
|
|
|
58
58
|
return { feature: slug };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Replaces a feature's summary on the feature map, in the future list or the implemented list,
|
|
63
|
+
* when a decision changed what the feature delivers. Its goals or release stay as they are.
|
|
64
|
+
*/
|
|
65
|
+
export async function updateFeatureSummary(deps: ProjectDeps, slug: string, summary: string): Promise<{ feature: string; list: "Future" | "Implemented" }> {
|
|
66
|
+
if (summary.trim() === "") throw new CruzeError("missing-summary", "a feature needs a summary");
|
|
67
|
+
const vision = await requireVision(deps);
|
|
68
|
+
const future = readRows(vision, "Future").find((row) => row[0] === slug);
|
|
69
|
+
const implemented = readRows(vision, "Implemented").find((row) => row[0] === slug);
|
|
70
|
+
if (future === undefined && implemented === undefined) throw new CruzeError("not-found", `${slug} is not on the feature map`);
|
|
71
|
+
const list = future !== undefined ? "Future" : "Implemented";
|
|
72
|
+
const cells = future !== undefined ? [slug, summary.trim(), future[2] ?? ""] : [slug, implemented?.[1] ?? "", summary.trim()];
|
|
73
|
+
await deps.files.writeText(PATHS.vision, upsertRow(vision, list, cells));
|
|
74
|
+
await appendJournal(deps, ".cruze", "feature-map", { action: "update", feature: slug, summary: summary.trim() });
|
|
75
|
+
return { feature: slug, list };
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
export async function dropFutureFeature(deps: ProjectDeps, slug: string, reason: string): Promise<{ feature: string }> {
|
|
62
79
|
const vision = await requireVision(deps);
|
|
63
80
|
const { text, removed } = removeRow(vision, "Future", slug);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { CruzeError } from "../cruze_error.ts";
|
|
2
|
+
import type { JournalEntry } from "../journal.ts";
|
|
3
|
+
import type { WorkItem } from "../project/work_items.ts";
|
|
4
|
+
import type { ApprovalStatus } from "./evaluate.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Why an approval was given without the user: a plan whose review closed, or work that a
|
|
8
|
+
* rethink left unchanged while something it cites moved. The user can still object, and
|
|
9
|
+
* editing the document makes the approval stale as usual.
|
|
10
|
+
*/
|
|
11
|
+
export type AgentBasis = "plan-review" | "rethink";
|
|
12
|
+
|
|
13
|
+
const byTime = (a: JournalEntry, b: JournalEntry): number => a.at.localeCompare(b.at);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A feature's change may be approved by the agent once its plan review has closed: a review
|
|
17
|
+
* recorded since the change was last approved, no blocker left open in its last round, and a
|
|
18
|
+
* disposition for every finding of its first round. The feature's design, and a standalone
|
|
19
|
+
* change's, stay the user's to approve.
|
|
20
|
+
*/
|
|
21
|
+
export function requirePlanReviewClosed(journal: JournalEntry[], item: WorkItem): void {
|
|
22
|
+
if (item.kind !== "change") {
|
|
23
|
+
throw new CruzeError("needs-user", `${item.ref} carries design, so the user approves it; only a feature's change can be approved after its plan review`);
|
|
24
|
+
}
|
|
25
|
+
const entries = journal.filter((e) => e["item"] === item.ref || e["artifact"] === item.path).sort(byTime);
|
|
26
|
+
const lastApproval = entries.filter((e) => e.event === "approve").at(-1);
|
|
27
|
+
const reviews = entries.filter((e) => e.event === "review" && e["review"] === "plan" && (lastApproval === undefined || e.at > lastApproval.at));
|
|
28
|
+
const last = reviews.at(-1);
|
|
29
|
+
if (last === undefined) {
|
|
30
|
+
const since = lastApproval === undefined ? "" : " since the change was last approved";
|
|
31
|
+
throw new CruzeError("plan-review-open", `no plan review is recorded for ${item.ref}${since}; run the plan review first`);
|
|
32
|
+
}
|
|
33
|
+
const blockers = Number(last["blockers"] ?? 0);
|
|
34
|
+
if (blockers > 0 && Number(last["round"]) === 1) {
|
|
35
|
+
throw new CruzeError("plan-review-open", `the plan review's round 1 found ${blockers} blocker(s); check their fixes in round 2`);
|
|
36
|
+
}
|
|
37
|
+
if (blockers > 0) {
|
|
38
|
+
throw new CruzeError("plan-review-open", `round 2 left ${blockers} blocker(s) open; the user decides them and approves the plan`);
|
|
39
|
+
}
|
|
40
|
+
const first = reviews.filter((e) => Number(e["round"]) === 1).at(-1) ?? last;
|
|
41
|
+
const findings = Number(first["blockers"] ?? 0) + Number(first["concerns"] ?? 0);
|
|
42
|
+
const decided = entries.filter((e) => e.event === "disposition" && e["review"] === "plan" && e.at > first.at).length;
|
|
43
|
+
if (decided < findings) {
|
|
44
|
+
throw new CruzeError("plan-review-open", `the plan review raised ${findings} finding(s) but ${decided} have a disposition; record one for each`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A rethink may re-approve, for the user, a feature or change it didn't edit: one whose own
|
|
50
|
+
* design is unchanged and that is stale only because something it cites changed. A change
|
|
51
|
+
* with a task reopened since its approval goes back to the user.
|
|
52
|
+
*/
|
|
53
|
+
export function requireUnchangedSinceApproval(journal: JournalEntry[], item: WorkItem | undefined, path: string, status: ApprovalStatus): void {
|
|
54
|
+
if (item === undefined) {
|
|
55
|
+
throw new CruzeError("needs-user", `${path} is a project document, so the user re-approves it; a rethink re-stamps only features and changes`);
|
|
56
|
+
}
|
|
57
|
+
if (status.state === "edited") {
|
|
58
|
+
throw new CruzeError("needs-user", `${item.ref} itself changed since its approval, so the user re-approves it`);
|
|
59
|
+
}
|
|
60
|
+
if (status.state !== "upstream-changed") {
|
|
61
|
+
throw new CruzeError("not-stale", `${item.ref} is ${status.state}; only work whose upstream changed can be re-stamped`);
|
|
62
|
+
}
|
|
63
|
+
const approvedAt = journal.filter((e) => e.event === "approve" && e["artifact"] === path).sort(byTime).at(-1)?.at ?? "";
|
|
64
|
+
const reopened = journal.filter((e) => e.event === "reopen" && e["change"] === item.ref && e.at > approvedAt).map((e) => String(e["task"]));
|
|
65
|
+
if (reopened.length > 0) {
|
|
66
|
+
throw new CruzeError("needs-user", `${item.ref} had ${reopened.join(", ")} reopened since its approval, so the user re-approves it`);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/domain/config.ts
CHANGED
|
@@ -23,9 +23,25 @@ export interface CruzeConfig {
|
|
|
23
23
|
* How `cruze review` starts an agent in a fresh context: the program and its arguments.
|
|
24
24
|
* Without one, the calling agent runs the role's prompt in a helper of its own.
|
|
25
25
|
*/
|
|
26
|
-
review: { command?: string[] };
|
|
26
|
+
review: { command?: string[]; decide: ReviewDecide };
|
|
27
|
+
/** The size band of a change, so each one is worth its own plan, review and verify. */
|
|
28
|
+
changes: ChangeLimits;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/** Which review findings the user decides: only the exceptions, or every one. */
|
|
32
|
+
export type ReviewDecide = "exceptions" | "all";
|
|
33
|
+
|
|
34
|
+
export interface ChangeLimits {
|
|
35
|
+
/** A feature's change building fewer elements than this merges into a neighbour. */
|
|
36
|
+
minBuilds: number;
|
|
37
|
+
/** A change delivering more scenarios than this splits. */
|
|
38
|
+
maxScenarios: number;
|
|
39
|
+
/** A plan with more tasks than this splits. */
|
|
40
|
+
maxTasks: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_CHANGE_LIMITS: ChangeLimits = { minBuilds: 3, maxScenarios: 30, maxTasks: 25 };
|
|
44
|
+
|
|
29
45
|
export interface Layer {
|
|
30
46
|
name: string;
|
|
31
47
|
paths: string[];
|
|
@@ -97,6 +113,14 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
97
113
|
const review = asRecord(root["review"]) ?? {};
|
|
98
114
|
const reviewCommand = review["command"] === undefined ? undefined : stringList(review["command"], "review.command", problems);
|
|
99
115
|
if (reviewCommand?.length === 0) problems.push("review.command needs at least the program to run");
|
|
116
|
+
const decide = review["decide"] ?? "exceptions";
|
|
117
|
+
if (decide !== "exceptions" && decide !== "all") problems.push("review.decide must be exceptions or all");
|
|
118
|
+
const limits = asRecord(root["changes"]) ?? {};
|
|
119
|
+
const changes: ChangeLimits = {
|
|
120
|
+
minBuilds: positiveInt(limits["min_builds"], "changes.min_builds", problems) ?? DEFAULT_CHANGE_LIMITS.minBuilds,
|
|
121
|
+
maxScenarios: positiveInt(limits["max_scenarios"], "changes.max_scenarios", problems) ?? DEFAULT_CHANGE_LIMITS.maxScenarios,
|
|
122
|
+
maxTasks: positiveInt(limits["max_tasks"], "changes.max_tasks", problems) ?? DEFAULT_CHANGE_LIMITS.maxTasks,
|
|
123
|
+
};
|
|
100
124
|
|
|
101
125
|
const config: CruzeConfig = {
|
|
102
126
|
version: 1,
|
|
@@ -109,7 +133,11 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
109
133
|
check: { maxLines: maxLines ?? 250, maxTypes: maxTypes ?? 5, exceptions },
|
|
110
134
|
layers,
|
|
111
135
|
commands,
|
|
112
|
-
review:
|
|
136
|
+
review: {
|
|
137
|
+
...(reviewCommand === undefined || reviewCommand.length === 0 ? {} : { command: reviewCommand }),
|
|
138
|
+
decide: decide === "all" ? "all" : "exceptions",
|
|
139
|
+
},
|
|
140
|
+
changes,
|
|
113
141
|
};
|
|
114
142
|
return { config, problems };
|
|
115
143
|
}
|
package/src/domain/journal.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const RECORDED_EVENTS: Record<string, string[]> = {
|
|
|
21
21
|
const OPTIONAL_FIELDS: Record<string, string[]> = {
|
|
22
22
|
rethink: ["missed_by"],
|
|
23
23
|
review: ["nits"],
|
|
24
|
+
disposition: ["decided"],
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
/** The workflow steps and checks that find or miss a problem, so a retro can count them. */
|
|
@@ -30,6 +31,7 @@ export const FIELD_VALUES: Record<string, string[]> = {
|
|
|
30
31
|
level: ["task", "change", "feature", "architecture", "vision"],
|
|
31
32
|
kind: ["defect", "discovery"],
|
|
32
33
|
disposition: ["fixed", "waived", "deferred", "rejected"],
|
|
34
|
+
decided: ["agent", "user"],
|
|
33
35
|
review: ["design", "plan", "code", "realign"],
|
|
34
36
|
result: ["accepted", "sent-back"],
|
|
35
37
|
found_by: STEPS,
|
|
@@ -77,6 +79,9 @@ export function recordedEventProblems(event: string, fields: Record<string, stri
|
|
|
77
79
|
if (allowed !== undefined && !allowed.includes(value)) problems.push(`${field} must be one of ${allowed.join(", ")}`);
|
|
78
80
|
if (COUNT_FIELDS.includes(field) && !/^\d+$/.test(value)) problems.push(`${field} must be a whole number`);
|
|
79
81
|
}
|
|
82
|
+
if (event === "disposition" && fields["decided"] === "agent" && fields["disposition"] !== "fixed") {
|
|
83
|
+
problems.push("the agent decides only fixes; the user waives, defers or rejects a finding (decided=user)");
|
|
84
|
+
}
|
|
80
85
|
return problems;
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -4,13 +4,14 @@ import type { WorkItem } from "../project/work_items.ts";
|
|
|
4
4
|
/**
|
|
5
5
|
* Whether the user accepted a change at verify. The latest verification recorded for the change
|
|
6
6
|
* counts. A later approval keeps it only when the change's own design is unchanged, as when it is
|
|
7
|
-
* re-approved because something upstream moved; a changed plan means verifying again.
|
|
7
|
+
* re-approved because something upstream moved; a changed plan means verifying again. Land's
|
|
8
|
+
* re-stamps don't count, since they follow the merge; an approval the agent gave does.
|
|
8
9
|
*/
|
|
9
10
|
export function isVerified(journal: JournalEntry[], change: WorkItem): boolean {
|
|
10
11
|
const byTime = (a: JournalEntry, b: JournalEntry): number => a.at.localeCompare(b.at);
|
|
11
12
|
const verification = journal.filter((e) => e.event === "verification" && e["item"] === change.ref).sort(byTime).at(-1);
|
|
12
13
|
if (verification?.["result"] !== "accepted") return false;
|
|
13
|
-
const approvals = journal.filter((e) => e.event === "approve" && e["artifact"] === change.path && e["basis"]
|
|
14
|
+
const approvals = journal.filter((e) => e.event === "approve" && e["artifact"] === change.path && !String(e["basis"] ?? "").startsWith("land ")).sort(byTime);
|
|
14
15
|
const verifiedHash = approvals.filter((e) => e.at < verification.at).at(-1)?.["hash"];
|
|
15
16
|
return approvals.filter((e) => e.at > verification.at).every((e) => e["hash"] === verifiedHash);
|
|
16
17
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { evaluateApproval, type ApprovalStatus } from "../approvals/evaluate.ts";
|
|
2
|
-
import { findIds } from "../ids.ts";
|
|
3
2
|
import { PATHS } from "../project/layout.ts";
|
|
4
3
|
import { readDelta } from "../project/deltas.ts";
|
|
5
4
|
import { readScope } from "../project/plan_parts.ts";
|
|
@@ -89,13 +88,12 @@ export function buildGateReasons(view: ProjectView, branch: string | null): stri
|
|
|
89
88
|
});
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
/** The elements a change touches: its scope
|
|
91
|
+
/** The elements a change touches: its scope and its delta, not the elements it only cites, such as a test seam. */
|
|
93
92
|
export function touchedIds(view: ProjectView, change: WorkItem): Set<string> {
|
|
94
93
|
const feature = featureOf(view.items, change);
|
|
95
94
|
const scope = readScope(change.doc);
|
|
96
95
|
const ids = new Set([...(scope?.delivers ?? []), ...(scope?.builds ?? []), ...(scope?.removes ?? [])]);
|
|
97
96
|
const deltaDoc = feature?.doc ?? change.doc;
|
|
98
97
|
for (const id of readDelta(deltaDoc).ids) ids.add(id);
|
|
99
|
-
for (const id of findIds(change.doc.lines.join("\n"))) ids.add(id);
|
|
100
98
|
return new Set([...ids].filter((id) => !id.startsWith("RULE-")));
|
|
101
99
|
}
|
|
@@ -7,6 +7,7 @@ import type { Delta } from "../project/deltas.ts";
|
|
|
7
7
|
import { isPlanned, readScope, readTasks, readTestPlan, type Scope, type Task } from "../project/plan_parts.ts";
|
|
8
8
|
import type { ProjectView } from "../project/project_view.ts";
|
|
9
9
|
import { error, warning, type Problem } from "./problem.ts";
|
|
10
|
+
import { planSizeProblems } from "./size_rules.ts";
|
|
10
11
|
|
|
11
12
|
const TEST_KINDS = ["behaviour", "contract", "domain", "smoke"];
|
|
12
13
|
|
|
@@ -31,6 +32,7 @@ export function changePlanProblems(
|
|
|
31
32
|
problems.push(error(path, line, "task-format", "task lines read: - T<n>: `<owner>` in `<path>`[, `<path>`], proves <ID>[, <ID>]"));
|
|
32
33
|
}
|
|
33
34
|
if (tasks.length === 0 && malformed.length === 0) problems.push(error(path, undefined, "missing-tasks", "the change has no tasks"));
|
|
35
|
+
problems.push(...planSizeProblems(view, path, tasks.length));
|
|
34
36
|
|
|
35
37
|
const proved = new Set(tasks.flatMap((task) => task.proves));
|
|
36
38
|
const taskPaths = tasks.flatMap((task) => task.paths);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DEFAULT_CHANGE_LIMITS, type ChangeLimits } from "../config.ts";
|
|
2
|
+
import { kindOf } from "../ids.ts";
|
|
3
|
+
import type { ChangeRow } from "../project/plan_parts.ts";
|
|
4
|
+
import type { ProjectView } from "../project/project_view.ts";
|
|
5
|
+
import { warning, type Problem } from "./problem.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every change pays for its own plan review, code review, verifier run and land, so a change
|
|
9
|
+
* should carry enough work to be worth them, and no more than one review and one run can hold.
|
|
10
|
+
*/
|
|
11
|
+
export function changeLimits(view: ProjectView): ChangeLimits {
|
|
12
|
+
return view.config?.config?.changes ?? DEFAULT_CHANGE_LIMITS;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A feature's changes not yet landed that fall outside the size band. A feature of one change has nothing to merge into. */
|
|
16
|
+
export function changeRowSizeProblems(view: ProjectView, path: string, rows: ChangeRow[], landed: Set<string>): Problem[] {
|
|
17
|
+
const limits = changeLimits(view);
|
|
18
|
+
const problems: Problem[] = [];
|
|
19
|
+
for (const row of rows.filter((r) => !landed.has(r.change))) {
|
|
20
|
+
if (rows.length > 1 && row.builds.length < limits.minBuilds) {
|
|
21
|
+
problems.push(warning(path, row.line, "change-too-small", `${row.change} builds ${row.builds.length} element(s), under ${limits.minBuilds}; merge it into the change it continues, since each change pays for its own plan, review and verify`));
|
|
22
|
+
}
|
|
23
|
+
const scenarios = row.delivers.filter((id) => kindOf(id) === "SCN").length;
|
|
24
|
+
if (scenarios > limits.maxScenarios) {
|
|
25
|
+
problems.push(warning(path, row.line, "change-too-large", `${row.change} delivers ${scenarios} scenarios, over ${limits.maxScenarios}; split it, since one verifier run has to go through them all`));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return problems;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A plan with more tasks than one code review and one verify can hold. */
|
|
32
|
+
export function planSizeProblems(view: ProjectView, path: string, tasks: number): Problem[] {
|
|
33
|
+
const { maxTasks } = changeLimits(view);
|
|
34
|
+
if (tasks <= maxTasks) return [];
|
|
35
|
+
return [warning(path, undefined, "plan-too-large", `the plan has ${tasks} tasks, over ${maxTasks}; split the change through rethink at feature level`)];
|
|
36
|
+
}
|
|
@@ -11,6 +11,7 @@ import { deltaProblems } from "./delta_rules.ts";
|
|
|
11
11
|
import { malformedIdHeadings, lineOf } from "./living_rules.ts";
|
|
12
12
|
import { changePlanProblems } from "./plan_rules.ts";
|
|
13
13
|
import { error, warning, type Problem } from "./problem.ts";
|
|
14
|
+
import { changeRowSizeProblems } from "./size_rules.ts";
|
|
14
15
|
|
|
15
16
|
const REQUIRED_SECTIONS: Record<WorkItem["kind"], string[]> = {
|
|
16
17
|
feature: ["Intent", "Spec delta", "Architecture delta", "Changes"],
|
|
@@ -71,6 +72,7 @@ function featureScopeProblems(view: ProjectView, feature: WorkItem, delta: Delta
|
|
|
71
72
|
if (extra !== undefined) {
|
|
72
73
|
problems.push(warning(feature.path, extra.line, "feature-size", `the feature has ${rows.length} changes, over ${MAX_CHANGES}; split it into features a single design review can hold`));
|
|
73
74
|
}
|
|
75
|
+
problems.push(...changeRowSizeProblems(view, feature.path, rows, landedChanges));
|
|
74
76
|
for (const row of rows) {
|
|
75
77
|
if (!/^\d\d-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(row.change)) problems.push(error(feature.path, row.line, "change-name", `change "${row.change}" must be named NN-slug`));
|
|
76
78
|
if (names.indexOf(row.change) !== names.lastIndexOf(row.change)) problems.push(error(feature.path, row.line, "change-name", `change ${row.change} is listed twice`));
|
|
@@ -13,6 +13,9 @@ export const RULE_SINCE: Record<string, string> = {
|
|
|
13
13
|
"validate:outside-module-map": "0.0.2",
|
|
14
14
|
"validate:readme-overview": "0.0.2",
|
|
15
15
|
"validate:feature-size": "0.0.2",
|
|
16
|
+
"validate:change-too-small": "0.0.5",
|
|
17
|
+
"validate:change-too-large": "0.0.5",
|
|
18
|
+
"validate:plan-too-large": "0.0.5",
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
/** Whether a rule arrived after the version the project's code meets, so it waits for `realign`. */
|