@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,55 @@
|
|
|
1
|
+
import { evaluateApproval, } from "../../domain/approvals/evaluate.js";
|
|
2
|
+
import { rebaseNeeded } from "../../domain/approvals/rebase.js";
|
|
3
|
+
import { upstreamLines, upstreamPath } from "../../domain/approvals/upstream_text.js";
|
|
4
|
+
import { lineDiff } from "../../domain/line_diff.js";
|
|
5
|
+
import { resolveArtifact } from "../../domain/project/artifact_ref.js";
|
|
6
|
+
import { approvalFolder, approvalsPath } from "../../domain/project/project_view.js";
|
|
7
|
+
import { loadView, } from "../project_context.js";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What changed upstream of a document since its approval, element by element, so a person can
|
|
29
|
+
* judge a re-stamp from the text rather than the IDs. The approved text comes from the commit
|
|
30
|
+
* that recorded the approval. One project went to a rethink for two file-list merges that
|
|
31
|
+
* changed no behaviour, because status named only the IDs.
|
|
32
|
+
*/
|
|
33
|
+
export async function upstreamDiff(deps , ref ) {
|
|
34
|
+
const view = await loadView(deps.files);
|
|
35
|
+
const { path, item } = resolveArtifact(view, ref);
|
|
36
|
+
const status = evaluateApproval(view, path);
|
|
37
|
+
const report = { artifact: path, state: status.state, changes: [], rebase: item === undefined ? [] : rebaseNeeded(view, item) };
|
|
38
|
+
if (item !== undefined) report.ref = item.ref;
|
|
39
|
+
const record = status.record;
|
|
40
|
+
if (record === undefined || status.changed.length === 0) return report;
|
|
41
|
+
const commit = await deps.repository.firstCommitAdding(approvalsPath(approvalFolder(view, path)), record.approvedAt);
|
|
42
|
+
for (const key of status.changed) {
|
|
43
|
+
const where = upstreamPath(view, key);
|
|
44
|
+
if (where === undefined) {
|
|
45
|
+
report.changes.push({ key, note: "it no longer exists" });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const now = upstreamLines(key, where, view.snapshot.get(where) ?? "") ?? [];
|
|
49
|
+
const then = commit === null ? null : await deps.repository.fileAt(commit, where);
|
|
50
|
+
const approved = then === null ? undefined : upstreamLines(key, where, then, record.upstream[key]);
|
|
51
|
+
if (approved !== undefined) report.changes.push({ key, path: where, diff: lineDiff(approved, now) });
|
|
52
|
+
else report.changes.push({ key, path: where, note: commit === null ? "the approval isn't committed yet; compare with git diff" : `its approved text isn't in ${where} at ${commit}, the commit that recorded the approval` });
|
|
53
|
+
}
|
|
54
|
+
return report;
|
|
55
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { elementHash } from "../hashing.js";
|
|
2
|
+
|
|
3
|
+
import { readDelta } from "../project/deltas.js";
|
|
4
|
+
import { approvalFolder, } from "../project/project_view.js";
|
|
5
|
+
|
|
6
|
+
import { latestApproval } from "./records.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Delta elements a feature or standalone change merged and hasn't changed since. The merge did
|
|
10
|
+
* their job, so the living text is the one to keep: an edit there by a rethink, a realign or
|
|
11
|
+
* another land asks nothing of the delta. One project's delta went stale this way, and every
|
|
12
|
+
* later edit of an element its first change had merged forced a rebase of text with no work left in it.
|
|
13
|
+
*/
|
|
14
|
+
export function settledIds(doc , merged ) {
|
|
15
|
+
const settled = new Set ();
|
|
16
|
+
for (const entry of readDelta(doc).entries) {
|
|
17
|
+
for (const element of [entry.element, ...entry.scenarios]) {
|
|
18
|
+
if (merged[element.id] === elementHash(doc, element)) settled.add(element.id);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return settled;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Elements the work's delta still changes whose living text moved since its last approval:
|
|
26
|
+
* approving the delta as it stands would overwrite that edit. A change's own approval pins no delta.
|
|
27
|
+
*/
|
|
28
|
+
export function rebaseNeeded(view , item ) {
|
|
29
|
+
if (item.kind === "change") return [];
|
|
30
|
+
const file = view.approvals.get(approvalFolder(view, item.path));
|
|
31
|
+
const previous = file === undefined ? undefined : latestApproval(file, item.path);
|
|
32
|
+
if (previous === undefined) return [];
|
|
33
|
+
const settled = settledIds(item.doc, file?.merged ?? {});
|
|
34
|
+
return [...readDelta(item.doc).ids].filter((id) => {
|
|
35
|
+
const living = view.living.elements.get(id);
|
|
36
|
+
const before = previous.upstream[id];
|
|
37
|
+
return !settled.has(id) && living !== undefined && before !== undefined && before !== elementHash(living.doc, living.element);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isStatusLine } from "../elements.js";
|
|
2
|
+
import { designHash, elementHash } from "../hashing.js";
|
|
3
|
+
import { parseMarkdown, } from "../markdown.js";
|
|
4
|
+
import { buildLivingModel } from "../project/living_model.js";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** The document that holds an upstream key now: an element's living document, or the path of a `doc:` key. */
|
|
8
|
+
export function upstreamPath(view , key ) {
|
|
9
|
+
if (key.startsWith("doc:")) return view.snapshot.has(key.slice("doc:".length)) ? key.slice("doc:".length) : undefined;
|
|
10
|
+
return view.living.elements.get(key)?.path;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The design lines behind an upstream key in one version of the document at `path`, without the
|
|
15
|
+
* managed blocks and status lines no hash covers. With `pinned`, only when they hash as pinned.
|
|
16
|
+
*/
|
|
17
|
+
export function upstreamLines(key , path , text , pinned ) {
|
|
18
|
+
if (key.startsWith("doc:")) {
|
|
19
|
+
const doc = parseMarkdown(text);
|
|
20
|
+
if (pinned !== undefined && designHash(doc) !== pinned) return undefined;
|
|
21
|
+
return designLines(doc, 0, doc.lines.length);
|
|
22
|
+
}
|
|
23
|
+
const living = buildLivingModel(new Map([[path, text]])).elements.get(key);
|
|
24
|
+
if (living === undefined) return undefined;
|
|
25
|
+
if (pinned !== undefined && elementHash(living.doc, living.element) !== pinned) return undefined;
|
|
26
|
+
return designLines(living.doc, living.element.start, living.element.end);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function designLines(doc , start , end ) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
for (let i = start; i < end; i++) {
|
|
32
|
+
const line = doc.lines[i] ?? "";
|
|
33
|
+
if (!doc.inManaged[i] && (doc.inFence[i] || !isStatusLine(line))) lines.push(line);
|
|
34
|
+
}
|
|
35
|
+
return lines;
|
|
36
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
|
|
2
2
|
import { matchesAny } from "../glob.js";
|
|
3
3
|
import { budgetedText } from "./budget_text.js";
|
|
4
|
+
import { decisionCitationsInComments } from "./decision_comments.js";
|
|
4
5
|
import { importsOf, resolveImport } from "./imports.js";
|
|
5
6
|
import { countTypes } from "./type_count.js";
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
|
|
@@ -21,6 +22,8 @@ import { countTypes } from "./type_count.js";
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
|
|
26
|
+
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
|
|
@@ -67,10 +70,24 @@ export function checkSource(input ) {
|
|
|
67
70
|
if (modulePaths.length > 0 && !insideModuleMap(path, modulePaths)) {
|
|
68
71
|
findings.push({ path, rule: "outside-module-map", severity: "warning", message: "the file is outside every module in the architecture's module map" });
|
|
69
72
|
}
|
|
73
|
+
findings.push(...decisionFindings(path, text));
|
|
74
|
+
}
|
|
75
|
+
for (const [path, text] of input.tests ?? []) {
|
|
76
|
+
if (!sources.has(path) && (input.only === undefined || input.only.includes(path))) findings.push(...decisionFindings(path, text));
|
|
70
77
|
}
|
|
71
78
|
return findings;
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
function decisionFindings(path , text ) {
|
|
82
|
+
return decisionCitationsInComments(path, text).map((citation) => ({
|
|
83
|
+
path,
|
|
84
|
+
line: citation.line + 1,
|
|
85
|
+
rule: "decision-citation",
|
|
86
|
+
severity: "warning",
|
|
87
|
+
message: `the comment cites ${citation.text}, a decision number a reader of the code can't follow; say what was decided, or cite the element or ADR that holds it`,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
/** Whether a module's Path covers the file: a directory path ending in `/` covers everything under it, any other names one file. */
|
|
75
92
|
export function insideModuleMap(path , modulePaths ) {
|
|
76
93
|
return modulePaths.some((module) => (module.endsWith("/") ? path.startsWith(module) : path === module));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { findDecisionCitations, } from "../decision_citations.js";
|
|
2
|
+
|
|
3
|
+
const HASH_COMMENTS = new Set(["py", "rb", "sh", "bash", "zsh", "pl", "r", "ex", "exs", "jl", "nim", "cr", "ps1"]);
|
|
4
|
+
const DASH_COMMENTS = new Set(["sql", "lua", "hs", "elm", "ada"]);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Decisions a source file's comments cite by number, such as `(D22)` or `(events D23)`. Each work
|
|
8
|
+
* item numbers its decisions from D1 and is archived when it finishes, so a number in code points
|
|
9
|
+
* at nothing a reader can find.
|
|
10
|
+
*/
|
|
11
|
+
export function decisionCitationsInComments(path , text ) {
|
|
12
|
+
const comments = commentText(path, text.split("\n"));
|
|
13
|
+
return findDecisionCitations(comments, new Set());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Each line with everything but its comment blanked, so a citation's line number stays right. */
|
|
17
|
+
function commentText(path , lines ) {
|
|
18
|
+
const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
|
|
19
|
+
if (HASH_COMMENTS.has(extension)) return lines.map((line) => after(line, /(^|\s)#/));
|
|
20
|
+
if (DASH_COMMENTS.has(extension)) return lines.map((line) => after(line, /--/));
|
|
21
|
+
let inBlock = false;
|
|
22
|
+
return lines.map((line) => {
|
|
23
|
+
if (inBlock) {
|
|
24
|
+
if (line.includes("*/")) inBlock = false;
|
|
25
|
+
return line;
|
|
26
|
+
}
|
|
27
|
+
const open = line.indexOf("/*");
|
|
28
|
+
if (open !== -1 && !line.includes("*/", open + 2)) inBlock = true;
|
|
29
|
+
const slash = line.indexOf("//");
|
|
30
|
+
const start = [open, slash].filter((at) => at !== -1).sort((a, b) => a - b)[0];
|
|
31
|
+
return start === undefined ? "" : line.slice(start);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function after(line , marker ) {
|
|
36
|
+
const match = marker.exec(line);
|
|
37
|
+
return match === null ? "" : line.slice(match.index);
|
|
38
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settled decisions are numbered `D<n>` within the document that records them: the architecture's
|
|
3
|
+
* `## Decisions`, or one feature or change, which is archived when it finishes. A number cited
|
|
4
|
+
* anywhere else either dangles or silently means another document's decision.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** One citation of decisions by number, such as `D5`, `D5, D19` or `events D17`. */
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const SLUG = "[a-z0-9]+(?:-[a-z0-9]+)*";
|
|
18
|
+
const CITATION = new RegExp(`(?:(?<![\\w-])(${SLUG})\\s+)?(?<![\\w-])(D\\d+(?:\\s*(?:,|and)\\s*D\\d+)*)(?![\\w-])(?:\\s+of\\s+(${SLUG}))?`, "g");
|
|
19
|
+
const DEFINITION = /^\s*- D(\d+):/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Every citation of decisions by number in the lines given, skipping the `- D<n>:` that defines one
|
|
23
|
+
* and anything inside backticks. `slugs` are the work items a citation may name.
|
|
24
|
+
*/
|
|
25
|
+
export function findDecisionCitations(lines , slugs , skip = () => false) {
|
|
26
|
+
const citations = [];
|
|
27
|
+
lines.forEach((raw, line) => {
|
|
28
|
+
if (skip(line)) return;
|
|
29
|
+
const text = raw.replace(DEFINITION, (m) => " ".repeat(m.length)).replace(/`[^`]*`/g, (m) => " ".repeat(m.length));
|
|
30
|
+
for (const match of text.matchAll(CITATION)) {
|
|
31
|
+
const before = match[1] !== undefined && slugs.has(match[1]) ? match[1] : undefined;
|
|
32
|
+
const after = match[3] !== undefined && slugs.has(match[3]) ? match[3] : undefined;
|
|
33
|
+
const numbers = [...(match[2] ?? "").matchAll(/D(\d+)/g)].map((m) => Number(m[1]));
|
|
34
|
+
const qualifier = before ?? after;
|
|
35
|
+
const cited = qualifier === undefined ? (match[2] ?? "") : (match[0] ?? "").trim();
|
|
36
|
+
citations.push(qualifier === undefined ? { line, text: cited, numbers } : { line, text: cited, numbers, qualifier });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return citations;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The decisions a `## Decisions` section, or any lines, define as `- D<n>: ...`. */
|
|
43
|
+
export function definedDecisions(lines ) {
|
|
44
|
+
return new Set(lines.flatMap((line) => {
|
|
45
|
+
const match = DEFINITION.exec(line);
|
|
46
|
+
return match?.[1] === undefined ? [] : [Number(match[1])];
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The names a citation may give a work item: a feature's slug with and without its date, and a
|
|
52
|
+
* change's folder name, such as `events`, `2026-10-08-events` and `02-links`.
|
|
53
|
+
*/
|
|
54
|
+
export function workItemSlugs(folderNames ) {
|
|
55
|
+
const slugs = new Set ();
|
|
56
|
+
for (const name of folderNames) {
|
|
57
|
+
slugs.add(name);
|
|
58
|
+
const undated = name.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
59
|
+
if (undated !== "") slugs.add(undated);
|
|
60
|
+
}
|
|
61
|
+
return slugs;
|
|
62
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { factsOf, statusOf } from "../elements.js";
|
|
2
|
+
import { findSection, parseMarkdown } from "../markdown.js";
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -19,6 +20,14 @@ export function renderAgentsFile(text , view ) {
|
|
|
19
20
|
return next;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/** The lines under `## Known pitfalls`, one per pitfall, which the feedback export carries back to Cruze. */
|
|
24
|
+
export function knownPitfalls(text ) {
|
|
25
|
+
const doc = parseMarkdown(text);
|
|
26
|
+
const range = findSection(doc, "Known pitfalls");
|
|
27
|
+
if (range === null) return [];
|
|
28
|
+
return doc.lines.slice(range.start, range.end).filter((line) => line.startsWith("- ")).map((line) => line.slice(2).trim());
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
/**
|
|
23
32
|
* The modules whose code exists: those built, and those a land built before a rethink set them
|
|
24
33
|
* back to `planned` for a change to rebuild, since their directories are still there.
|
package/dist/domain/ids.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** Element IDs: `<KIND>-<name>` or `<KIND>-<scope>.<name>`, named and never reused. */
|
|
2
2
|
|
|
3
|
-
export const KINDS = ["GOAL", "CTX", "ENT", "UC", "PORT", "ADP", "FLOW", "MOD", "RULE", "VIEW", "XC", "REQ", "SCN", "ADR"] ;
|
|
3
|
+
export const KINDS = ["GOAL", "PRINCIPLE", "LATER", "CTX", "ENT", "UC", "PORT", "ADP", "FLOW", "MOD", "RULE", "VIEW", "XC", "REQ", "SCN", "ADR"] ;
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
/** Kinds of the vision's row elements: goals, the principles every design follows, and what must stay possible later. */
|
|
7
|
+
export const VISION_KINDS = ["GOAL", "PRINCIPLE", "LATER"];
|
|
6
8
|
/** Kinds that describe code, and so carry a managed planned/built status. */
|
|
7
9
|
export const CODE_KINDS = ["ENT", "UC", "PORT", "ADP", "FLOW", "MOD"];
|
|
8
10
|
/** Kinds whose elements carry a managed status: code kinds and scenarios. */
|
package/dist/domain/journal.js
CHANGED
|
@@ -16,6 +16,7 @@ export const RECORDED_EVENTS = {
|
|
|
16
16
|
bug: ["summary", "cause"],
|
|
17
17
|
verification: ["result", "summary"],
|
|
18
18
|
checks: ["commit"],
|
|
19
|
+
lesson: ["summary", "instead", "step"],
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
/** Fields an event may carry beyond its required ones, checked when present. A defect rethink must say which step missed it. */
|
|
@@ -32,7 +33,7 @@ export const STEPS = ["envision", "architect", "research", "design-review", "wal
|
|
|
32
33
|
export const FIELD_VALUES = {
|
|
33
34
|
level: ["task", "change", "feature", "architecture", "vision"],
|
|
34
35
|
kind: ["defect", "discovery"],
|
|
35
|
-
disposition: ["fixed", "waived", "deferred", "rejected"],
|
|
36
|
+
disposition: ["fixed", "waived", "deferred", "rejected", "rethink"],
|
|
36
37
|
decided: ["agent", "user"],
|
|
37
38
|
review: ["design", "plan", "code", "realign", "improve"],
|
|
38
39
|
result: ["accepted", "sent-back"],
|
|
@@ -40,6 +41,7 @@ export const FIELD_VALUES = {
|
|
|
40
41
|
checked_by: ["agent", "reviewer"],
|
|
41
42
|
found_by: STEPS,
|
|
42
43
|
missed_by: STEPS,
|
|
44
|
+
step: STEPS,
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
/** A defect rethink must say which step missed it, and a sent-back verification where the change goes back to. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { settledIds } from "../approvals/rebase.js";
|
|
1
2
|
import { sectionForKind } from "../architecture_sections.js";
|
|
2
3
|
import { CruzeError } from "../cruze_error.js";
|
|
3
4
|
import { setStatus } from "../edits/status_lines.js";
|
|
@@ -26,7 +27,8 @@ import { appendBlock, relevel, removeElement, replaceElement } from "./text_ops.
|
|
|
26
27
|
/**
|
|
27
28
|
* Merges a change's work into the living docs. The first change of a feature to land merges
|
|
28
29
|
* the whole delta; elements it doesn't build arrive as planned, and later changes flip what
|
|
29
|
-
* they build to built. Text is replaced only when nobody else changed it since this work merged it
|
|
30
|
+
* they build to built. Text is replaced only when nobody else changed it since this work merged it,
|
|
31
|
+
* and an element merged and unchanged in the delta since keeps its living text, whoever edited it.
|
|
30
32
|
*/
|
|
31
33
|
export function planMerge(view , change , source ) {
|
|
32
34
|
const scope = readScope(change.doc);
|
|
@@ -36,6 +38,7 @@ export function planMerge(view , change , source )
|
|
|
36
38
|
const files = new Map ();
|
|
37
39
|
const text = (path ) => files.get(path) ?? view.snapshot.get(path) ?? "";
|
|
38
40
|
const plan = { files, merged, mergedIds: [], built: [], removed: [] };
|
|
41
|
+
const settled = settledIds(source.doc, merged);
|
|
39
42
|
|
|
40
43
|
for (const op of delta.capabilities) {
|
|
41
44
|
const key = capabilityKey(op.capability);
|
|
@@ -56,8 +59,10 @@ export function planMerge(view , change , source )
|
|
|
56
59
|
}
|
|
57
60
|
continue;
|
|
58
61
|
}
|
|
59
|
-
|
|
62
|
+
const kept = settled.has(id) && view.living.elements.has(id);
|
|
63
|
+
if (!kept) mergeEntry(view, source, entry, path, text, files, merged, plan);
|
|
60
64
|
for (const target of [entry.element, ...entry.scenarios].filter((e) => STATUS_KINDS.includes(e.kind))) {
|
|
65
|
+
if (kept && !inScope.has(target.id)) continue;
|
|
61
66
|
files.set(path, setStatus(text(path), target.id, statusAfterMerge(view, source, target, inScope)));
|
|
62
67
|
if (inScope.has(target.id)) plan.built.push(target.id);
|
|
63
68
|
}
|
|
@@ -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 , after , context = 2) {
|
|
6
|
+
const ops = diffOps(before, after);
|
|
7
|
+
const near = new Set ();
|
|
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 = [];
|
|
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
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/** The edit script of a longest common subsequence: documents are small enough for the quadratic table. */
|
|
33
|
+
function diffOps(a , b ) {
|
|
34
|
+
const width = b.length + 1;
|
|
35
|
+
const table = new Int32Array((a.length + 1) * width);
|
|
36
|
+
const at = (i , j ) => 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 = [];
|
|
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, } from "../elements.js";
|
|
2
|
+
import { findIds, kindOf, scopeOf, } from "../ids.js";
|
|
3
|
+
|
|
4
|
+
import { readDelta } from "./deltas.js";
|
|
5
|
+
import { PATHS } from "./layout.js";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
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
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
const DOCS_INDEXED = (path ) => 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 , scope ) {
|
|
47
|
+
const inScope = (id ) => scope === undefined || scopeOf(id) === scope || id === `CTX-${scope}`;
|
|
48
|
+
const entries = [];
|
|
49
|
+
const byId = new Map ();
|
|
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 , ids ) {
|
|
75
|
+
const elements = [];
|
|
76
|
+
const missing = [];
|
|
77
|
+
for (const id of ids) {
|
|
78
|
+
const found = [];
|
|
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 , element , doc , source ) {
|
|
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 , element , path ) {
|
|
99
|
+
return { id: element.id, title: element.title, source: `${path}:${element.start + 1}`, body: elementLines(doc, element) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function inFlight(view ) {
|
|
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 ) {
|
|
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 , requirement ) {
|
|
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 ) {
|
|
121
|
+
const words = (path ) => (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, } from "../elements.js";
|
|
2
|
+
import { VISION_KINDS } from "../ids.js";
|
|
2
3
|
import { parseMarkdown, } from "../markdown.js";
|
|
3
4
|
import { PATHS, childFiles, } from "./layout.js";
|
|
4
5
|
|
|
@@ -49,6 +50,6 @@ function elementsOf(path , doc ) {
|
|
|
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.js";
|
|
2
|
+
import { tablesIn } from "../markdown_tables.js";
|
|
3
|
+
import { PATHS } from "./layout.js";
|
|
4
|
+
|
|
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"] ;
|
|
14
|
+
|
|
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"] ;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export function readNotes(snapshot ) {
|
|
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 , text ) {
|
|
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 ) =>
|
|
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
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
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 , targets ) {
|
|
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
|
+
}
|