@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.
- 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 +3 -1
- 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 +2 -2
- 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.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 +1 -0
- package/skills/research/SKILL.md +3 -2
- package/skills/rethink/SKILL.md +5 -3
- package/skills/rethink/levels.md +6 -6
- package/skills/roadmap/SKILL.md +2 -1
- package/skills/roles/SKILL.md +3 -3
- package/skills/roles/auditor.md +1 -1
- package/skills/roles/code-reviewer.md +2 -2
- 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/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 +3 -1
- 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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A line diff of two texts as unified-diff lines: " " for a kept line, "- " removed, "+ " added.
|
|
3
|
+
* Only the changes and `context` kept lines around each are shown; "…" marks the lines left out.
|
|
4
|
+
*/
|
|
5
|
+
export function lineDiff(before: string[], after: string[], context = 2): string[] {
|
|
6
|
+
const ops = diffOps(before, after);
|
|
7
|
+
const near = new Set<number>();
|
|
8
|
+
ops.forEach((op, i) => {
|
|
9
|
+
if (op.kind === " ") return;
|
|
10
|
+
for (let j = Math.max(0, i - context); j <= Math.min(ops.length - 1, i + context); j++) near.add(j);
|
|
11
|
+
});
|
|
12
|
+
const lines: string[] = [];
|
|
13
|
+
let skipped = false;
|
|
14
|
+
ops.forEach((op, i) => {
|
|
15
|
+
if (!near.has(i)) {
|
|
16
|
+
skipped = true;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (skipped && lines.length > 0) lines.push("…");
|
|
20
|
+
skipped = false;
|
|
21
|
+
lines.push(`${op.kind} ${op.line}`);
|
|
22
|
+
});
|
|
23
|
+
if (skipped && lines.length > 0) lines.push("…");
|
|
24
|
+
return lines;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Op {
|
|
28
|
+
kind: " " | "-" | "+";
|
|
29
|
+
line: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The edit script of a longest common subsequence: documents are small enough for the quadratic table. */
|
|
33
|
+
function diffOps(a: string[], b: string[]): Op[] {
|
|
34
|
+
const width = b.length + 1;
|
|
35
|
+
const table = new Int32Array((a.length + 1) * width);
|
|
36
|
+
const at = (i: number, j: number): number => table[i * width + j] ?? 0;
|
|
37
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
38
|
+
for (let j = b.length - 1; j >= 0; j--) {
|
|
39
|
+
table[i * width + j] = a[i] === b[j] ? at(i + 1, j + 1) + 1 : Math.max(at(i + 1, j), at(i, j + 1));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const ops: Op[] = [];
|
|
43
|
+
let i = 0;
|
|
44
|
+
let j = 0;
|
|
45
|
+
while (i < a.length || j < b.length) {
|
|
46
|
+
if (i < a.length && j < b.length && a[i] === b[j]) {
|
|
47
|
+
ops.push({ kind: " ", line: a[i++] ?? "" });
|
|
48
|
+
j++;
|
|
49
|
+
} else if (j >= b.length || (i < a.length && at(i + 1, j) >= at(i, j + 1))) {
|
|
50
|
+
ops.push({ kind: "-", line: a[i++] ?? "" });
|
|
51
|
+
} else {
|
|
52
|
+
ops.push({ kind: "+", line: b[j++] ?? "" });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return ops;
|
|
56
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { elementLines, factsOf, statusOf, type DeltaOp, type Element, type Status } from "../elements.ts";
|
|
2
|
+
import { findIds, kindOf, scopeOf, type Kind } from "../ids.ts";
|
|
3
|
+
import type { MarkdownDoc } from "../markdown.ts";
|
|
4
|
+
import { readDelta } from "./deltas.ts";
|
|
5
|
+
import { PATHS } from "./layout.ts";
|
|
6
|
+
import type { ProjectView } from "./project_view.ts";
|
|
7
|
+
import type { ShownElement } from "./scope_view.ts";
|
|
8
|
+
import type { WorkItem } from "./work_items.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Elements one at a time, and an index of all of them, so a step that needs a few elements of a
|
|
12
|
+
* large architecture fetches those instead of reading the whole file. One project's architecture
|
|
13
|
+
* held 52k words, and a single architect run read all of it to find what it would extend.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface IndexEntry {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
kind: Kind;
|
|
20
|
+
/** The living document that holds it, or the work item that adds it. */
|
|
21
|
+
source: string;
|
|
22
|
+
status?: Status;
|
|
23
|
+
/** Where its code lives: the `Module` it names, or a module's `Path`. */
|
|
24
|
+
place?: string;
|
|
25
|
+
/** For a requirement, how many scenarios it holds. */
|
|
26
|
+
scenarios?: number;
|
|
27
|
+
/** Work in flight whose delta changes or removes it. */
|
|
28
|
+
changedBy: string[];
|
|
29
|
+
/** Set when only in-flight work defines it so far. */
|
|
30
|
+
addedBy?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ElementIndex {
|
|
34
|
+
/** The words in the living architecture and specs, so a step can judge whether to read them whole. */
|
|
35
|
+
words: { architecture: number; specs: number };
|
|
36
|
+
entries: IndexEntry[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DOCS_INDEXED = (path: string): boolean => path === PATHS.architecture || path.startsWith(`${PATHS.specsDir}/`);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Every element of the living architecture and specs, one entry each, with what in-flight work
|
|
43
|
+
* adds. Without a scope, scenarios are counted under their requirement; with a context or
|
|
44
|
+
* capability name, only its elements are listed, scenarios included.
|
|
45
|
+
*/
|
|
46
|
+
export function elementIndex(view: ProjectView, scope?: string): ElementIndex {
|
|
47
|
+
const inScope = (id: string): boolean => scope === undefined || scopeOf(id) === scope || id === `CTX-${scope}`;
|
|
48
|
+
const entries: IndexEntry[] = [];
|
|
49
|
+
const byId = new Map<string, IndexEntry>();
|
|
50
|
+
for (const living of view.living.elements.values()) {
|
|
51
|
+
if (!DOCS_INDEXED(living.path) || !inScope(living.id) || (scope === undefined && kindOf(living.id) === "SCN")) continue;
|
|
52
|
+
const entry = entryOf(living.id, living.element, living.doc, living.path);
|
|
53
|
+
if (kindOf(living.id) === "REQ") entry.scenarios = scenarioCount(view, living.id);
|
|
54
|
+
entries.push(entry);
|
|
55
|
+
byId.set(entry.id, entry);
|
|
56
|
+
}
|
|
57
|
+
for (const item of inFlight(view)) {
|
|
58
|
+
for (const element of deltaElements(item)) {
|
|
59
|
+
if (!inScope(element.element.id) || (scope === undefined && element.element.kind === "SCN")) continue;
|
|
60
|
+
const known = byId.get(element.element.id);
|
|
61
|
+
if (known !== undefined) {
|
|
62
|
+
known.changedBy.push(item.ref);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const added = { ...entryOf(element.element.id, element.element, item.doc, item.ref), addedBy: item.ref };
|
|
66
|
+
entries.push(added);
|
|
67
|
+
byId.set(added.id, added);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { words: livingWords(view), entries };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The bodies of the named elements: the living one, then each in-flight delta that changes it. */
|
|
74
|
+
export function lookupElements(view: ProjectView, ids: string[]): { elements: ShownElement[]; missing: string[] } {
|
|
75
|
+
const elements: ShownElement[] = [];
|
|
76
|
+
const missing: string[] = [];
|
|
77
|
+
for (const id of ids) {
|
|
78
|
+
const found: ShownElement[] = [];
|
|
79
|
+
const living = view.living.elements.get(id);
|
|
80
|
+
if (living !== undefined) found.push(shown(living.doc, living.element, living.path));
|
|
81
|
+
for (const item of inFlight(view)) {
|
|
82
|
+
const delta = deltaElements(item).find((d) => d.element.id === id);
|
|
83
|
+
if (delta !== undefined) found.push({ ...shown(item.doc, delta.element, item.path), ...(delta.op === undefined ? {} : { op: delta.op }) });
|
|
84
|
+
}
|
|
85
|
+
if (found.length === 0) missing.push(id);
|
|
86
|
+
elements.push(...found);
|
|
87
|
+
}
|
|
88
|
+
return { elements, missing };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function entryOf(id: string, element: Element, doc: MarkdownDoc, source: string): IndexEntry {
|
|
92
|
+
const facts = factsOf(doc, element);
|
|
93
|
+
const place = (facts.get("Module") ?? []).flatMap(findIds)[0] ?? (facts.get("Path") ?? [])[0];
|
|
94
|
+
const status = statusOf(doc, element);
|
|
95
|
+
return { id, title: element.title, kind: kindOf(id), source, changedBy: [], ...(status === undefined ? {} : { status }), ...(place === undefined ? {} : { place }) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function shown(doc: MarkdownDoc, element: Element, path: string): ShownElement {
|
|
99
|
+
return { id: element.id, title: element.title, source: `${path}:${element.start + 1}`, body: elementLines(doc, element) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function inFlight(view: ProjectView): WorkItem[] {
|
|
103
|
+
return view.items.filter((item) => !item.archived && item.kind !== "change");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Each element a work item's delta defines or changes, scenarios under a requirement included. */
|
|
107
|
+
function deltaElements(item: WorkItem): Array<{ element: Element; op?: DeltaOp }> {
|
|
108
|
+
return readDelta(item.doc).entries.flatMap((entry) => [
|
|
109
|
+
{ element: entry.element, ...(entry.element.op === undefined ? {} : { op: entry.element.op }) },
|
|
110
|
+
...entry.scenarios.map((scenario) => ({ element: scenario })),
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function scenarioCount(view: ProjectView, requirement: string): number {
|
|
115
|
+
const living = view.living.elements.get(requirement);
|
|
116
|
+
if (living === undefined) return 0;
|
|
117
|
+
return [...view.living.elements.values()].filter((e) => e.path === living.path && e.id.startsWith("SCN-") && e.element.start > living.element.start && e.element.start < living.element.end).length;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function livingWords(view: ProjectView): { architecture: number; specs: number } {
|
|
121
|
+
const words = (path: string): number => (view.snapshot.get(path) ?? "").split(/\s+/).filter((w) => w !== "").length;
|
|
122
|
+
const specs = [...view.living.docs.keys()].filter((path) => path.startsWith(`${PATHS.specsDir}/`));
|
|
123
|
+
return { architecture: words(PATHS.architecture), specs: specs.reduce((sum, path) => sum + words(path), 0) };
|
|
124
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { headingElements, rowElements, type Element } from "../elements.ts";
|
|
2
|
+
import { VISION_KINDS } from "../ids.ts";
|
|
2
3
|
import { parseMarkdown, type MarkdownDoc } from "../markdown.ts";
|
|
3
4
|
import { PATHS, childFiles, type Snapshot } from "./layout.ts";
|
|
4
5
|
|
|
@@ -49,6 +50,6 @@ function elementsOf(path: string, doc: MarkdownDoc): Element[] {
|
|
|
49
50
|
if (path.startsWith(`${PATHS.adrDir}/`)) {
|
|
50
51
|
return [{ id: adrId(path), kind: "ADR", title: doc.headings[0]?.text ?? "", form: "heading", level: 1, start: 0, end: doc.lines.length }];
|
|
51
52
|
}
|
|
52
|
-
if (path === PATHS.vision) return rowElements(doc).filter((element) => element.kind
|
|
53
|
+
if (path === PATHS.vision) return rowElements(doc).filter((element) => VISION_KINDS.includes(element.kind));
|
|
53
54
|
return headingElements(doc);
|
|
54
55
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { findSection, parseMarkdown } from "../markdown.ts";
|
|
2
|
+
import { tablesIn } from "../markdown_tables.ts";
|
|
3
|
+
import { PATHS } from "./layout.ts";
|
|
4
|
+
import type { Snapshot } from "./layout.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Notes under `.cruze/notes/`: what the user brought, what explore captured, the evidence research
|
|
8
|
+
* gathered, and the hand-off envision wrote from them. Each says what it is about, so a step reads
|
|
9
|
+
* the notes for its work rather than guessing from file names: one project held 21 notes, and a
|
|
10
|
+
* 9,500-word seed whose design sections no step was told to read.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const NOTE_KINDS = ["input", "intake", "explore", "research"] as const;
|
|
14
|
+
export type NoteKind = (typeof NOTE_KINDS)[number];
|
|
15
|
+
|
|
16
|
+
/** The step an intake row hands a part to, besides a feature on the map. */
|
|
17
|
+
export const HANDOFF_STEPS = ["vision", "architect", "roadmap", "user"] as const;
|
|
18
|
+
|
|
19
|
+
export interface HandoffRow {
|
|
20
|
+
/** The part of the input, by its heading or a short quote. */
|
|
21
|
+
part: string;
|
|
22
|
+
/** Where it is: a note path and heading, or "below" for text saved in the intake note. */
|
|
23
|
+
source: string;
|
|
24
|
+
/** A step from HANDOFF_STEPS, or the slug of the feature that takes it up. */
|
|
25
|
+
goesTo: string;
|
|
26
|
+
/** `open` until the step that takes it up settles it with the user: `settled` or `dropped`. */
|
|
27
|
+
status: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Note {
|
|
31
|
+
path: string;
|
|
32
|
+
title: string;
|
|
33
|
+
kind?: string;
|
|
34
|
+
/** `project`, feature slugs, or context and capability names. */
|
|
35
|
+
about: string[];
|
|
36
|
+
handoff: HandoffRow[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readNotes(snapshot: Snapshot): Note[] {
|
|
40
|
+
return [...snapshot.keys()]
|
|
41
|
+
.filter((path) => path.startsWith(`${PATHS.notesDir}/`) && path.endsWith(".md"))
|
|
42
|
+
.sort()
|
|
43
|
+
.map((path) => readNote(path, snapshot.get(path) ?? ""));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readNote(path: string, text: string): Note {
|
|
47
|
+
const doc = parseMarkdown(text);
|
|
48
|
+
const title = doc.headings.find((h) => h.level === 1)?.text ?? path.slice(path.lastIndexOf("/") + 1, -".md".length);
|
|
49
|
+
const firstSection = doc.headings.find((h) => h.level === 2)?.line ?? doc.lines.length;
|
|
50
|
+
const fact = (key: string): string | undefined =>
|
|
51
|
+
doc.lines.slice(0, firstSection).map((line) => new RegExp(`^- ${key}: (.+)$`).exec(line)?.[1]?.trim()).find((v) => v !== undefined);
|
|
52
|
+
const about = (fact("About") ?? "").split(",").map((a) => a.trim()).filter((a) => a !== "");
|
|
53
|
+
const kind = fact("Kind");
|
|
54
|
+
const section = findSection(doc, "Hand-off");
|
|
55
|
+
const rows = section === null ? [] : (tablesIn(doc.lines, doc.inFence, section.start, section.end)[0]?.rows ?? []);
|
|
56
|
+
const handoff = rows.map(({ cells }) => ({ part: cells[0] ?? "", source: cells[1] ?? "", goesTo: (cells[2] ?? "").replace(/`/g, ""), status: cells[3] ?? "open" }));
|
|
57
|
+
return { path, title, about, handoff, ...(kind === undefined ? {} : { kind }) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface NotesFor {
|
|
61
|
+
/** Notes about one of the targets, or about the whole project when it is a target. */
|
|
62
|
+
notes: Note[];
|
|
63
|
+
/** Intake rows handed to one of the targets, with the intake note that holds them. */
|
|
64
|
+
handoffs: Array<HandoffRow & { note: string }>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The notes and hand-off rows for a piece of work, named by its targets: `project`, a step, a feature slug or a context. */
|
|
68
|
+
export function notesFor(notes: Note[], targets: string[]): NotesFor {
|
|
69
|
+
const wanted = new Set(targets);
|
|
70
|
+
return {
|
|
71
|
+
notes: notes.filter((note) => note.about.some((a) => wanted.has(a))),
|
|
72
|
+
handoffs: notes.flatMap((note) => note.handoff.filter((row) => wanted.has(row.goesTo)).map((row) => ({ ...row, note: note.path }))),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SkillFiles } from "./skill_links.ts";
|
|
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: string): number {
|
|
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: SkillFiles[]): string[] {
|
|
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.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),
|