@printpractical/cruze 0.0.13 → 0.0.15
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 +61 -118
- package/dist/adapters/inbound/cli/cli_context.js +0 -27
- package/dist/adapters/inbound/cli/commands/code.js +5 -18
- package/dist/adapters/inbound/cli/commands/documents.js +11 -37
- package/dist/adapters/inbound/cli/commands/init.js +1 -1
- package/dist/adapters/inbound/cli/commands/install.js +1 -0
- package/dist/adapters/inbound/cli/commands/review.js +2 -9
- package/dist/adapters/inbound/cli/commands/status.js +16 -63
- package/dist/adapters/inbound/cli/commands.js +10 -37
- package/dist/adapters/inbound/cli/parse_command.js +0 -54
- package/dist/adapters/outbound/git_repository.js +2 -71
- package/dist/app/ports/repository.js +0 -12
- package/dist/app/project_context.js +15 -25
- package/dist/app/use_cases/approve_document.js +29 -0
- package/dist/app/use_cases/check_code.js +21 -56
- package/dist/app/use_cases/install_skills.js +7 -1
- package/dist/app/use_cases/new_document.js +25 -0
- package/dist/app/use_cases/project_status.js +43 -67
- package/dist/app/use_cases/run_review.js +11 -30
- package/dist/app/use_cases/validate_project.js +8 -5
- package/dist/domain/approvals.js +51 -0
- package/dist/domain/check/check_source.js +3 -32
- package/dist/domain/check/imports.js +3 -3
- package/dist/domain/check/rust_imports.js +25 -3
- package/dist/domain/config.js +8 -84
- package/dist/domain/hashing.js +16 -30
- package/dist/domain/ids.js +13 -27
- package/dist/domain/next_step.js +41 -0
- package/dist/domain/project/features.js +71 -0
- package/dist/domain/project/layout.js +11 -27
- package/dist/domain/project/project_docs.js +48 -0
- package/dist/domain/project/release.js +30 -0
- package/dist/domain/scaffold.js +0 -1
- package/dist/domain/skill_budget.js +4 -4
- package/dist/domain/trace.js +8 -25
- package/dist/domain/validation/document_rules.js +65 -0
- package/dist/domain/validation/release_rules.js +45 -0
- package/dist/domain/validation/review_rules.js +21 -0
- package/dist/domain/validation/validate_project.js +20 -27
- package/dist/domain/versions/cruze_version.js +3 -9
- package/dist/main.js +1 -1
- package/package.json +1 -1
- package/skills/asking/SKILL.md +51 -0
- package/skills/behavioural-testing/SKILL.md +14 -16
- package/skills/behavioural-testing/examples.md +1 -2
- package/skills/behavioural-testing/fakes.md +2 -2
- package/skills/build/SKILL.md +34 -38
- package/skills/dependency-approval/SKILL.md +34 -31
- package/skills/envision/SKILL.md +23 -18
- package/skills/envision/architecture.md +16 -0
- package/skills/envision/breakdown.md +14 -0
- package/skills/envision/possibilities.md +6 -7
- package/skills/fix/SKILL.md +34 -0
- package/skills/formats/SKILL.md +39 -87
- package/skills/formats/config.md +47 -0
- package/skills/formats/templates/architecture.md +18 -74
- package/skills/formats/templates/feature.md +18 -34
- package/skills/formats/templates/vision.md +15 -27
- package/skills/hexagonal-design/SKILL.md +11 -11
- package/skills/hexagonal-design/contracts.md +3 -3
- package/skills/hexagonal-design/fundamentals.md +1 -1
- package/skills/hexagonal-design/languages/cpp.md +1 -1
- package/skills/hexagonal-design/languages/rust.md +1 -1
- package/skills/hexagonal-design/languages/swift.md +2 -2
- package/skills/hexagonal-design/runtime.md +2 -2
- package/skills/research/SKILL.md +4 -4
- package/skills/research/evaluation.md +1 -1
- package/skills/review/SKILL.md +27 -0
- package/skills/roles/SKILL.md +8 -59
- package/skills/roles/code-reviewer.md +22 -32
- package/skills/roles/researcher.md +3 -3
- package/skills/shape/SKILL.md +37 -0
- package/skills/shape/self-check.md +12 -0
- package/src/adapters/inbound/cli/cli_context.ts +0 -27
- package/src/adapters/inbound/cli/commands/code.ts +5 -18
- package/src/adapters/inbound/cli/commands/documents.ts +11 -37
- package/src/adapters/inbound/cli/commands/init.ts +1 -1
- package/src/adapters/inbound/cli/commands/install.ts +1 -0
- package/src/adapters/inbound/cli/commands/review.ts +2 -9
- package/src/adapters/inbound/cli/commands/status.ts +16 -63
- package/src/adapters/inbound/cli/commands.ts +10 -37
- package/src/adapters/inbound/cli/parse_command.ts +0 -54
- package/src/adapters/outbound/git_repository.ts +2 -71
- package/src/app/ports/repository.ts +0 -12
- package/src/app/project_context.ts +15 -25
- package/src/app/use_cases/approve_document.ts +29 -0
- package/src/app/use_cases/check_code.ts +21 -56
- package/src/app/use_cases/install_skills.ts +7 -1
- package/src/app/use_cases/new_document.ts +25 -0
- package/src/app/use_cases/project_status.ts +49 -73
- package/src/app/use_cases/run_review.ts +11 -30
- package/src/app/use_cases/validate_project.ts +8 -5
- package/src/domain/approvals.ts +51 -0
- package/src/domain/check/check_source.ts +3 -32
- package/src/domain/check/imports.ts +3 -3
- package/src/domain/check/rust_imports.ts +25 -3
- package/src/domain/config.ts +9 -85
- package/src/domain/hashing.ts +16 -30
- package/src/domain/ids.ts +13 -27
- package/src/domain/next_step.ts +41 -0
- package/src/domain/project/features.ts +71 -0
- package/src/domain/project/layout.ts +11 -27
- package/src/domain/project/project_docs.ts +48 -0
- package/src/domain/project/release.ts +30 -0
- package/src/domain/scaffold.ts +0 -1
- package/src/domain/skill_budget.ts +4 -4
- package/src/domain/trace.ts +8 -25
- package/src/domain/validation/document_rules.ts +65 -0
- package/src/domain/validation/release_rules.ts +45 -0
- package/src/domain/validation/review_rules.ts +21 -0
- package/src/domain/validation/validate_project.ts +21 -28
- package/src/domain/versions/cruze_version.ts +3 -9
- package/src/main.ts +1 -1
- package/templates/init/AGENTS.md +4 -12
- package/templates/init/ci.yml +3 -3
- package/templates/init/config.yaml +8 -17
- package/templates/init/review/claude.yaml +2 -2
- package/dist/adapters/inbound/cli/argument_files.js +0 -29
- package/dist/adapters/inbound/cli/commands/diff.js +0 -23
- package/dist/adapters/inbound/cli/commands/journal.js +0 -32
- package/dist/adapters/inbound/cli/commands/progress.js +0 -55
- package/dist/adapters/inbound/cli/commands/realign.js +0 -28
- package/dist/adapters/inbound/cli/commands/show.js +0 -96
- package/dist/app/use_cases/abandon_work.js +0 -42
- package/dist/app/use_cases/approve_artifact.js +0 -197
- package/dist/app/use_cases/journal_events.js +0 -64
- package/dist/app/use_cases/land_change.js +0 -134
- package/dist/app/use_cases/new_project_doc.js +0 -48
- package/dist/app/use_cases/new_work_item.js +0 -75
- package/dist/app/use_cases/realign_done.js +0 -45
- package/dist/app/use_cases/realign_status.js +0 -106
- package/dist/app/use_cases/record_progress.js +0 -118
- package/dist/app/use_cases/show_elements.js +0 -18
- package/dist/app/use_cases/show_notes.js +0 -11
- package/dist/app/use_cases/show_scope.js +0 -51
- package/dist/app/use_cases/upstream_diff.js +0 -55
- package/dist/domain/approvals/agent_approval.js +0 -68
- package/dist/domain/approvals/evaluate.js +0 -36
- package/dist/domain/approvals/fingerprint.js +0 -52
- package/dist/domain/approvals/rebase.js +0 -39
- package/dist/domain/approvals/records.js +0 -50
- package/dist/domain/approvals/upstream_text.js +0 -36
- package/dist/domain/architecture_sections.js +0 -23
- package/dist/domain/check/decision_comments.js +0 -38
- package/dist/domain/decision_citations.js +0 -62
- package/dist/domain/edits/agents_file.js +0 -58
- package/dist/domain/edits/managed_tables.js +0 -60
- package/dist/domain/edits/progress_block.js +0 -33
- package/dist/domain/edits/status_lines.js +0 -52
- package/dist/domain/elements.js +0 -126
- package/dist/domain/journal.js +0 -109
- package/dist/domain/land/bookkeeping.js +0 -54
- package/dist/domain/land/merge_plan.js +0 -137
- package/dist/domain/land/text_ops.js +0 -66
- package/dist/domain/line_diff.js +0 -56
- package/dist/domain/project/artifact_ref.js +0 -42
- package/dist/domain/project/deltas.js +0 -49
- package/dist/domain/project/element_lookup.js +0 -124
- package/dist/domain/project/living_model.js +0 -55
- package/dist/domain/project/new_items.js +0 -48
- package/dist/domain/project/notes.js +0 -74
- package/dist/domain/project/plan_parts.js +0 -111
- package/dist/domain/project/progress.js +0 -43
- package/dist/domain/project/project_view.js +0 -72
- package/dist/domain/project/roadmap.js +0 -43
- package/dist/domain/project/scope_view.js +0 -134
- package/dist/domain/project/work_items.js +0 -68
- package/dist/domain/status/next_step.js +0 -181
- package/dist/domain/status/pending_rethink.js +0 -22
- package/dist/domain/status/verification.js +0 -29
- package/dist/domain/status/work_status.js +0 -125
- package/dist/domain/validation/decision_rules.js +0 -76
- package/dist/domain/validation/delta_rules.js +0 -81
- package/dist/domain/validation/element_rules.js +0 -104
- package/dist/domain/validation/future_rules.js +0 -40
- package/dist/domain/validation/living_rules.js +0 -99
- package/dist/domain/validation/plan_rules.js +0 -124
- package/dist/domain/validation/project_rules.js +0 -115
- package/dist/domain/validation/size_rules.js +0 -45
- package/dist/domain/validation/vision_rules.js +0 -41
- package/dist/domain/validation/work_item_rules.js +0 -175
- package/dist/domain/versions/realign_notes.js +0 -72
- package/dist/domain/versions/rule_versions.js +0 -29
- package/dist/domain/versions/scheduled_exceptions.js +0 -41
- package/skills/about/SKILL.md +0 -21
- package/skills/architect/SKILL.md +0 -49
- package/skills/architect/feature.md +0 -40
- package/skills/architect/project.md +0 -71
- package/skills/architect/tweak.md +0 -22
- package/skills/architect/walkthrough.md +0 -35
- package/skills/domain-language/SKILL.md +0 -37
- package/skills/envision/feature.md +0 -25
- package/skills/envision/intake.md +0 -28
- package/skills/envision/project.md +0 -24
- package/skills/explore/SKILL.md +0 -39
- package/skills/formats/reference/architecture.md +0 -84
- package/skills/formats/reference/config.md +0 -80
- package/skills/formats/reference/durable-docs.md +0 -74
- package/skills/formats/reference/specs.md +0 -65
- package/skills/formats/reference/work-items.md +0 -100
- package/skills/formats/templates/adr.md +0 -7
- package/skills/formats/templates/change.md +0 -29
- package/skills/formats/templates/glossary.md +0 -8
- package/skills/formats/templates/roadmap.md +0 -32
- package/skills/formats/templates/spec.md +0 -11
- package/skills/formats/templates/standalone-change.md +0 -46
- package/skills/grilling/SKILL.md +0 -90
- package/skills/grilling/illustrating.md +0 -38
- package/skills/improve/SKILL.md +0 -51
- package/skills/land/SKILL.md +0 -48
- package/skills/next/SKILL.md +0 -31
- package/skills/plan/SKILL.md +0 -62
- package/skills/plan/tasks.md +0 -35
- package/skills/plan/test-plan.md +0 -14
- package/skills/realign/SKILL.md +0 -37
- package/skills/realign/notes/0.0.10.md +0 -131
- package/skills/realign/notes/0.0.11.md +0 -15
- package/skills/realign/notes/0.0.12.md +0 -31
- package/skills/realign/notes/0.0.13.md +0 -11
- package/skills/realign/notes/0.0.2.md +0 -69
- package/skills/realign/notes/0.0.3.md +0 -11
- package/skills/realign/notes/0.0.4.md +0 -27
- package/skills/realign/notes/0.0.5.md +0 -38
- package/skills/realign/notes/0.0.6.md +0 -44
- package/skills/realign/notes/0.0.7.md +0 -11
- package/skills/realign/notes/0.0.8.md +0 -97
- package/skills/realign/notes/0.0.9.md +0 -59
- package/skills/realign/notes.md +0 -40
- package/skills/rethink/SKILL.md +0 -62
- package/skills/rethink/levels.md +0 -54
- package/skills/roadmap/SKILL.md +0 -54
- package/skills/roles/auditor.md +0 -35
- package/skills/roles/design-reviewer.md +0 -43
- package/skills/roles/design-rubric.md +0 -41
- package/skills/roles/plan-rubric.md +0 -11
- package/skills/roles/surveyor.md +0 -41
- package/skills/roles/verifier.md +0 -42
- package/skills/triage/SKILL.md +0 -37
- package/skills/verify/SKILL.md +0 -45
- package/skills/verify/manual-test.md +0 -23
- package/src/adapters/inbound/cli/argument_files.ts +0 -29
- package/src/adapters/inbound/cli/commands/diff.ts +0 -23
- package/src/adapters/inbound/cli/commands/journal.ts +0 -32
- package/src/adapters/inbound/cli/commands/progress.ts +0 -55
- package/src/adapters/inbound/cli/commands/realign.ts +0 -28
- package/src/adapters/inbound/cli/commands/show.ts +0 -96
- package/src/app/use_cases/abandon_work.ts +0 -42
- package/src/app/use_cases/approve_artifact.ts +0 -197
- package/src/app/use_cases/journal_events.ts +0 -64
- package/src/app/use_cases/land_change.ts +0 -134
- package/src/app/use_cases/new_project_doc.ts +0 -48
- package/src/app/use_cases/new_work_item.ts +0 -75
- package/src/app/use_cases/realign_done.ts +0 -45
- package/src/app/use_cases/realign_status.ts +0 -106
- package/src/app/use_cases/record_progress.ts +0 -118
- package/src/app/use_cases/show_elements.ts +0 -18
- package/src/app/use_cases/show_notes.ts +0 -11
- package/src/app/use_cases/show_scope.ts +0 -51
- package/src/app/use_cases/upstream_diff.ts +0 -55
- package/src/domain/approvals/agent_approval.ts +0 -68
- package/src/domain/approvals/evaluate.ts +0 -36
- package/src/domain/approvals/fingerprint.ts +0 -52
- package/src/domain/approvals/rebase.ts +0 -39
- package/src/domain/approvals/records.ts +0 -50
- package/src/domain/approvals/upstream_text.ts +0 -36
- package/src/domain/architecture_sections.ts +0 -23
- package/src/domain/check/decision_comments.ts +0 -38
- package/src/domain/decision_citations.ts +0 -62
- package/src/domain/edits/agents_file.ts +0 -58
- package/src/domain/edits/managed_tables.ts +0 -60
- package/src/domain/edits/progress_block.ts +0 -33
- package/src/domain/edits/status_lines.ts +0 -52
- package/src/domain/elements.ts +0 -126
- package/src/domain/journal.ts +0 -109
- package/src/domain/land/bookkeeping.ts +0 -54
- package/src/domain/land/merge_plan.ts +0 -137
- package/src/domain/land/text_ops.ts +0 -66
- package/src/domain/line_diff.ts +0 -56
- package/src/domain/project/artifact_ref.ts +0 -42
- package/src/domain/project/deltas.ts +0 -49
- package/src/domain/project/element_lookup.ts +0 -124
- package/src/domain/project/living_model.ts +0 -55
- package/src/domain/project/new_items.ts +0 -48
- package/src/domain/project/notes.ts +0 -74
- package/src/domain/project/plan_parts.ts +0 -111
- package/src/domain/project/progress.ts +0 -43
- package/src/domain/project/project_view.ts +0 -72
- package/src/domain/project/roadmap.ts +0 -43
- package/src/domain/project/scope_view.ts +0 -134
- package/src/domain/project/work_items.ts +0 -68
- package/src/domain/status/next_step.ts +0 -181
- package/src/domain/status/pending_rethink.ts +0 -22
- package/src/domain/status/verification.ts +0 -29
- package/src/domain/status/work_status.ts +0 -125
- package/src/domain/validation/decision_rules.ts +0 -76
- package/src/domain/validation/delta_rules.ts +0 -81
- package/src/domain/validation/element_rules.ts +0 -104
- package/src/domain/validation/future_rules.ts +0 -40
- package/src/domain/validation/living_rules.ts +0 -99
- package/src/domain/validation/plan_rules.ts +0 -124
- package/src/domain/validation/project_rules.ts +0 -115
- package/src/domain/validation/size_rules.ts +0 -45
- package/src/domain/validation/vision_rules.ts +0 -41
- package/src/domain/validation/work_item_rules.ts +0 -175
- package/src/domain/versions/realign_notes.ts +0 -72
- package/src/domain/versions/rule_versions.ts +0 -29
- package/src/domain/versions/scheduled_exceptions.ts +0 -41
- package/templates/init/gitattributes +0 -2
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { MANAGED_CLOSE, MANAGED_OPEN, parseMarkdown } from "../markdown.ts";
|
|
2
|
-
import { readTasks } from "../project/plan_parts.ts";
|
|
3
|
-
import { progressRange, readProgress, type Progress } from "../project/progress.ts";
|
|
4
|
-
|
|
5
|
-
/** Rewrites the managed `## Progress` block of a feature or change from a changed Progress. */
|
|
6
|
-
export function updateProgress(text: string, change: (progress: Progress) => void): string {
|
|
7
|
-
const doc = parseMarkdown(text);
|
|
8
|
-
const progress = readProgress(doc);
|
|
9
|
-
for (const task of readTasks(doc).tasks) if (!progress.tasks.has(task.number)) progress.tasks.set(task.number, null);
|
|
10
|
-
change(progress);
|
|
11
|
-
const block = [MANAGED_OPEN, "## Progress", ...renderProgress(progress), MANAGED_CLOSE];
|
|
12
|
-
const range = progressRange(doc);
|
|
13
|
-
const lines = [...doc.lines];
|
|
14
|
-
if (range === null) {
|
|
15
|
-
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
16
|
-
return [...lines, "", ...block, ""].join("\n");
|
|
17
|
-
}
|
|
18
|
-
lines.splice(range.open, range.end - range.open + 1, ...block);
|
|
19
|
-
return lines.join("\n");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function renderProgress(progress: Progress): string[] {
|
|
23
|
-
const lines: string[] = [];
|
|
24
|
-
if (progress.branch !== undefined) lines.push(`- Branch: ${progress.branch}`);
|
|
25
|
-
for (const [change, state] of progress.changes) lines.push(`- ${change}: ${state}`);
|
|
26
|
-
const ordered = [...progress.tasks].sort(([a], [b]) => Number(a.slice(1)) - Number(b.slice(1)));
|
|
27
|
-
for (const [task, commit] of ordered) {
|
|
28
|
-
lines.push(commit === null ? `- [ ] ${task}` : commit === "" ? `- [x] ${task}` : `- [x] ${task} (${commit})`);
|
|
29
|
-
}
|
|
30
|
-
for (const deviation of progress.deviations) lines.push(`- Deviation ${deviation}`);
|
|
31
|
-
if (progress.landed !== undefined) lines.push(`- Landed: ${progress.landed}`);
|
|
32
|
-
return lines;
|
|
33
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { headingElements, isStatusLine, statusOf, type Status } from "../elements.ts";
|
|
2
|
-
import { STATUS_KINDS } from "../ids.ts";
|
|
3
|
-
import { parseMarkdown, type MarkdownDoc } from "../markdown.ts";
|
|
4
|
-
|
|
5
|
-
/** Managed `- Status:` lines: only the CLI adds or changes them. */
|
|
6
|
-
|
|
7
|
-
/** Sets one element's status, adding the line after its facts when it has none. Returns the new text. */
|
|
8
|
-
export function setStatus(text: string, id: string, status: Status): string {
|
|
9
|
-
const doc = parseMarkdown(text);
|
|
10
|
-
const element = headingElements(doc).find((e) => e.id === id);
|
|
11
|
-
if (element === undefined) throw new Error(`${id} is not in this document`);
|
|
12
|
-
const lines = [...doc.lines];
|
|
13
|
-
const at = statusLineIndex(doc, element.start, element.end);
|
|
14
|
-
if (at !== null) lines[at] = `- Status: ${status}`;
|
|
15
|
-
else lines.splice(insertionPoint(doc, element.start, element.end), 0, `- Status: ${status}`);
|
|
16
|
-
return lines.join("\n");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Adds `- Status: planned` to every status-carrying element that has none. */
|
|
20
|
-
export function stampMissingStatuses(text: string): { text: string; stamped: string[] } {
|
|
21
|
-
let current = text;
|
|
22
|
-
const stamped: string[] = [];
|
|
23
|
-
for (const element of headingElements(parseMarkdown(text))) {
|
|
24
|
-
if (!STATUS_KINDS.includes(element.kind)) continue;
|
|
25
|
-
const doc = parseMarkdown(current);
|
|
26
|
-
const fresh = headingElements(doc).find((e) => e.id === element.id);
|
|
27
|
-
if (fresh === undefined || statusOf(doc, fresh) !== undefined) continue;
|
|
28
|
-
current = setStatus(current, element.id, "planned");
|
|
29
|
-
stamped.push(element.id);
|
|
30
|
-
}
|
|
31
|
-
return { text: current, stamped };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function statusLineIndex(doc: MarkdownDoc, start: number, end: number): number | null {
|
|
35
|
-
for (let i = start + 1; i < end; i++) {
|
|
36
|
-
if (!doc.inFence[i] && /^#{1,6} /.test(doc.lines[i] ?? "")) return null;
|
|
37
|
-
if (!doc.inFence[i] && isStatusLine(doc.lines[i] ?? "")) return i;
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** After the element's last top-level fact or step, before any diagram or nested heading. */
|
|
43
|
-
function insertionPoint(doc: MarkdownDoc, start: number, end: number): number {
|
|
44
|
-
let point = start + 1;
|
|
45
|
-
for (let i = start + 1; i < end; i++) {
|
|
46
|
-
const line = doc.lines[i] ?? "";
|
|
47
|
-
if (doc.inFence[i] || /^#{1,6} /.test(line)) break;
|
|
48
|
-
if (/^(- |\s{2,}- )/.test(line)) point = i + 1;
|
|
49
|
-
else if (line.trim() !== "" && point > start + 1) break;
|
|
50
|
-
}
|
|
51
|
-
return point;
|
|
52
|
-
}
|
package/src/domain/elements.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { ID_PATTERN, STATUS_KINDS, kindOf, type Kind } from "./ids.ts";
|
|
2
|
-
import { sectionEnd, type MarkdownDoc } from "./markdown.ts";
|
|
3
|
-
|
|
4
|
-
export type DeltaOp = "ADDED" | "MODIFIED" | "REMOVED";
|
|
5
|
-
export type Status = "planned" | "built";
|
|
6
|
-
|
|
7
|
-
/** One element: a heading `<ID>: <Title>` and its body, or a table row whose first cell is an ID. */
|
|
8
|
-
export interface Element {
|
|
9
|
-
id: string;
|
|
10
|
-
kind: Kind;
|
|
11
|
-
title: string;
|
|
12
|
-
form: "heading" | "row";
|
|
13
|
-
/** Set when the heading is a delta operation, such as `### ADDED PORT-x: Title`. */
|
|
14
|
-
op?: DeltaOp;
|
|
15
|
-
level: number;
|
|
16
|
-
/** Line range [start, end) in the document. */
|
|
17
|
-
start: number;
|
|
18
|
-
end: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** A delta heading that creates a spec file: `### ADDED CAPABILITY <name>: <Title>`. */
|
|
22
|
-
export interface CapabilityOp {
|
|
23
|
-
capability: string;
|
|
24
|
-
title: string;
|
|
25
|
-
start: number;
|
|
26
|
-
end: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const HEADING = new RegExp(`^(?:(ADDED|MODIFIED|REMOVED) )?(${ID_PATTERN}): (.+)$`);
|
|
30
|
-
const CAPABILITY = /^ADDED CAPABILITY ([a-z0-9]+(?:-[a-z0-9]+)*): (.+)$/;
|
|
31
|
-
const ROW = new RegExp(`^\\|\\s*(${ID_PATTERN})\\s*\\|(.*)\\|\\s*$`);
|
|
32
|
-
const STATUS_LINE = /^- Status: (planned|built)\s*$/;
|
|
33
|
-
|
|
34
|
-
/** Heading elements whose heading lies in [from, to). */
|
|
35
|
-
export function headingElements(doc: MarkdownDoc, from = 0, to = doc.lines.length): Element[] {
|
|
36
|
-
const elements: Element[] = [];
|
|
37
|
-
for (const heading of doc.headings) {
|
|
38
|
-
if (heading.line < from || heading.line >= to) continue;
|
|
39
|
-
const match = HEADING.exec(heading.text);
|
|
40
|
-
if (match?.[2] === undefined || match[3] === undefined) continue;
|
|
41
|
-
const element: Element = {
|
|
42
|
-
id: match[2],
|
|
43
|
-
kind: kindOf(match[2]),
|
|
44
|
-
title: match[3],
|
|
45
|
-
form: "heading",
|
|
46
|
-
level: heading.level,
|
|
47
|
-
start: heading.line,
|
|
48
|
-
end: Math.min(sectionEnd(doc, heading), to),
|
|
49
|
-
};
|
|
50
|
-
if (match[1] !== undefined) element.op = match[1] as DeltaOp;
|
|
51
|
-
elements.push(element);
|
|
52
|
-
}
|
|
53
|
-
return elements;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function capabilityOps(doc: MarkdownDoc, from = 0, to = doc.lines.length): CapabilityOp[] {
|
|
57
|
-
return doc.headings
|
|
58
|
-
.filter((h) => h.line >= from && h.line < to)
|
|
59
|
-
.flatMap((heading) => {
|
|
60
|
-
const match = CAPABILITY.exec(heading.text);
|
|
61
|
-
if (match?.[1] === undefined || match[2] === undefined) return [];
|
|
62
|
-
return [{ capability: match[1], title: match[2], start: heading.line, end: Math.min(sectionEnd(doc, heading), to) }];
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Row elements: table rows, outside code fences, whose first cell is an ID. */
|
|
67
|
-
export function rowElements(doc: MarkdownDoc): Element[] {
|
|
68
|
-
return doc.lines.flatMap((line, index) => {
|
|
69
|
-
if (doc.inFence[index]) return [];
|
|
70
|
-
const match = ROW.exec(line);
|
|
71
|
-
if (match?.[1] === undefined) return [];
|
|
72
|
-
const title = match[2]?.split("|")[0]?.trim() ?? "";
|
|
73
|
-
return [{ id: match[1], kind: kindOf(match[1]), title, form: "row" as const, level: 0, start: index, end: index + 1 }];
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function elementLines(doc: MarkdownDoc, element: { start: number; end: number }): string[] {
|
|
78
|
-
return doc.lines.slice(element.start, element.end);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function isStatusLine(line: string): boolean {
|
|
82
|
-
return STATUS_LINE.test(line);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** The element's own managed status: the first status line before any nested heading. */
|
|
86
|
-
/**
|
|
87
|
-
* Whether an element is still to be built: marked `planned`, or a kind that carries a status
|
|
88
|
-
* but not stamped yet, because the document holding it has never been approved.
|
|
89
|
-
*/
|
|
90
|
-
export function isUnbuilt(doc: MarkdownDoc, element: Element): boolean {
|
|
91
|
-
const status = statusOf(doc, element);
|
|
92
|
-
return status === "planned" || (status === undefined && STATUS_KINDS.includes(element.kind));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function statusOf(doc: MarkdownDoc, element: Element): Status | undefined {
|
|
96
|
-
for (let i = element.start + 1; i < element.end; i++) {
|
|
97
|
-
if (/^#{1,6} /.test(doc.lines[i] ?? "") && !doc.inFence[i]) return undefined;
|
|
98
|
-
const match = STATUS_LINE.exec(doc.lines[i] ?? "");
|
|
99
|
-
if (match?.[1] !== undefined) return match[1] as Status;
|
|
100
|
-
}
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* The element's top-level facts: `- Key: value` items and their indented sub-items.
|
|
106
|
-
* Nested elements' facts are excluded.
|
|
107
|
-
*/
|
|
108
|
-
export function factsOf(doc: MarkdownDoc, element: Element): Map<string, string[]> {
|
|
109
|
-
const facts = new Map<string, string[]>();
|
|
110
|
-
let current: string[] | null = null;
|
|
111
|
-
for (let i = element.start + 1; i < element.end; i++) {
|
|
112
|
-
const line = doc.lines[i] ?? "";
|
|
113
|
-
if (doc.inFence[i]) continue;
|
|
114
|
-
if (/^#{1,6} /.test(line)) break;
|
|
115
|
-
const fact = /^- ([A-Z][A-Za-z ]*): ?(.*)$/.exec(line);
|
|
116
|
-
if (fact?.[1] !== undefined) {
|
|
117
|
-
current = fact[2] === undefined || fact[2] === "" ? [] : [fact[2]];
|
|
118
|
-
facts.set(fact[1], current);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
const sub = /^\s{2,}- (.+)$/.exec(line);
|
|
122
|
-
if (sub?.[1] !== undefined && current !== null) current.push(sub[1]);
|
|
123
|
-
else if (line.trim() !== "") current = null;
|
|
124
|
-
}
|
|
125
|
-
return facts;
|
|
126
|
-
}
|
package/src/domain/journal.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/** The append-only journal: one JSON object per line, written only by the CLI. */
|
|
2
|
-
|
|
3
|
-
export interface JournalEntry {
|
|
4
|
-
at: string;
|
|
5
|
-
event: string;
|
|
6
|
-
by: string;
|
|
7
|
-
[field: string]: unknown;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Events other tools and skills record, with the fields each requires. */
|
|
11
|
-
export const RECORDED_EVENTS: Record<string, string[]> = {
|
|
12
|
-
rethink: ["level", "kind", "summary", "wrong", "found_by"],
|
|
13
|
-
disposition: ["finding", "disposition", "reason", "review"],
|
|
14
|
-
review: ["review", "round", "blockers", "concerns"],
|
|
15
|
-
override: ["gate", "reason"],
|
|
16
|
-
bug: ["summary", "cause"],
|
|
17
|
-
verification: ["result", "summary"],
|
|
18
|
-
checks: ["commit"],
|
|
19
|
-
lesson: ["summary", "instead", "step"],
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/** Fields an event may carry beyond its required ones, checked when present. A defect rethink must say which step missed it. */
|
|
23
|
-
const OPTIONAL_FIELDS: Record<string, string[]> = {
|
|
24
|
-
rethink: ["missed_by"],
|
|
25
|
-
review: ["nits", "checked_by"],
|
|
26
|
-
disposition: ["decided"],
|
|
27
|
-
verification: ["back_to", "commit"],
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/** The workflow steps and checks that find or miss a problem, so a retro can count them. */
|
|
31
|
-
export const STEPS = ["envision", "architect", "research", "design-review", "walkthrough", "plan", "plan-review", "build", "code-review", "verify", "land", "realign", "improve", "validate", "check", "user"];
|
|
32
|
-
|
|
33
|
-
export const FIELD_VALUES: Record<string, string[]> = {
|
|
34
|
-
level: ["task", "detail", "change", "feature", "architecture", "vision"],
|
|
35
|
-
kind: ["defect", "discovery"],
|
|
36
|
-
disposition: ["fixed", "waived", "deferred", "rejected", "rethink"],
|
|
37
|
-
decided: ["agent", "user"],
|
|
38
|
-
review: ["design", "plan", "code", "realign", "improve"],
|
|
39
|
-
result: ["accepted", "sent-back"],
|
|
40
|
-
back_to: ["build", "rethink"],
|
|
41
|
-
checked_by: ["agent", "reviewer"],
|
|
42
|
-
found_by: STEPS,
|
|
43
|
-
missed_by: STEPS,
|
|
44
|
-
step: STEPS,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/** A defect rethink must say which step missed it, and a sent-back verification where the change goes back to. */
|
|
48
|
-
function conditionallyRequired(event: string, fields: Record<string, string>): string[] {
|
|
49
|
-
if (event === "rethink" && fields["kind"] === "defect") return ["missed_by"];
|
|
50
|
-
if (event === "verification" && fields["result"] === "sent-back") return ["back_to"];
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const CONDITION_NOTES: Record<string, string> = { missed_by: " for a defect", back_to: " when the change is sent back" };
|
|
55
|
-
|
|
56
|
-
/** Fields recorded as numbers rather than text. */
|
|
57
|
-
const COUNT_FIELDS = ["round", "blockers", "concerns", "nits"];
|
|
58
|
-
|
|
59
|
-
export function journalPath(folder: string): string {
|
|
60
|
-
return `${folder}/journal.jsonl`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function parseJournal(text: string | undefined): { entries: JournalEntry[]; badLines: number[] } {
|
|
64
|
-
const entries: JournalEntry[] = [];
|
|
65
|
-
const badLines: number[] = [];
|
|
66
|
-
(text ?? "").split("\n").forEach((line, index) => {
|
|
67
|
-
if (line.trim() === "") return;
|
|
68
|
-
try {
|
|
69
|
-
const entry = JSON.parse(line) as JournalEntry;
|
|
70
|
-
if (typeof entry.event === "string" && typeof entry.at === "string") entries.push(entry);
|
|
71
|
-
else badLines.push(index + 1);
|
|
72
|
-
} catch {
|
|
73
|
-
badLines.push(index + 1);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
return { entries, badLines };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function serializeEntry(entry: JournalEntry): string {
|
|
80
|
-
return `${JSON.stringify(entry)}\n`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Problems with an event a caller asks to record; empty when it can be recorded. */
|
|
84
|
-
export function recordedEventProblems(event: string, fields: Record<string, string>): string[] {
|
|
85
|
-
const required = RECORDED_EVENTS[event];
|
|
86
|
-
if (required === undefined) return [`unknown event "${event}"; recordable events: ${Object.keys(RECORDED_EVENTS).join(", ")}`];
|
|
87
|
-
const needed = [...required, ...conditionallyRequired(event, fields)];
|
|
88
|
-
const problems = needed.filter((field) => (fields[field] ?? "").trim() === "").map((field) => `${event} needs ${field}${CONDITION_NOTES[field] ?? ""}`);
|
|
89
|
-
const known = [...required, ...(OPTIONAL_FIELDS[event] ?? [])];
|
|
90
|
-
for (const field of known) {
|
|
91
|
-
const value = fields[field];
|
|
92
|
-
if (value === undefined) continue;
|
|
93
|
-
const allowed = FIELD_VALUES[field];
|
|
94
|
-
if (allowed !== undefined && !allowed.includes(value)) problems.push(`${field} must be one of ${allowed.join(", ")}`);
|
|
95
|
-
if (COUNT_FIELDS.includes(field) && !/^\d+$/.test(value)) problems.push(`${field} must be a whole number`);
|
|
96
|
-
}
|
|
97
|
-
if ((event === "checks" || event === "verification") && fields["commit"] !== undefined && !/^[0-9a-f]{7,40}$/.test(fields["commit"])) {
|
|
98
|
-
problems.push("commit must be a commit hash, such as the output of git rev-parse --short HEAD");
|
|
99
|
-
}
|
|
100
|
-
if (event === "disposition" && fields["decided"] === "agent" && fields["disposition"] !== "fixed") {
|
|
101
|
-
problems.push("the agent decides only fixes; the user waives, defers or rejects a finding (decided=user)");
|
|
102
|
-
}
|
|
103
|
-
return problems;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** The fields as the journal stores them: counts become numbers. */
|
|
107
|
-
export function recordedFields(fields: Record<string, string>): Record<string, string | number> {
|
|
108
|
-
return Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, COUNT_FIELDS.includes(key) ? Number(value) : value]));
|
|
109
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { removeRow, upsertRow } from "../edits/managed_tables.ts";
|
|
2
|
-
import { updateProgress } from "../edits/progress_block.ts";
|
|
3
|
-
import { parseMarkdown } from "../markdown.ts";
|
|
4
|
-
import { PATHS } from "../project/layout.ts";
|
|
5
|
-
import { readChangesTable } from "../project/plan_parts.ts";
|
|
6
|
-
import { readProgress } from "../project/progress.ts";
|
|
7
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
8
|
-
import type { WorkItem } from "../project/work_items.ts";
|
|
9
|
-
|
|
10
|
-
export interface Bookkeeping {
|
|
11
|
-
/** Whether this land finishes the work: a standalone change, or a feature's last change. */
|
|
12
|
-
finished: boolean;
|
|
13
|
-
/** The folder to move into the archive when finished. */
|
|
14
|
-
archive?: { from: string; to: string };
|
|
15
|
-
release: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Records the land in progress blocks, the roadmap status and, when a feature finishes, the feature map. */
|
|
19
|
-
export function applyBookkeeping(view: ProjectView, change: WorkItem, source: WorkItem, landed: string, files: Map<string, string>): Bookkeeping {
|
|
20
|
-
const text = (path: string): string | undefined => files.get(path) ?? view.snapshot.get(path);
|
|
21
|
-
files.set(change.path, updateProgress(text(change.path) ?? "", (progress) => (progress.landed = landed)));
|
|
22
|
-
|
|
23
|
-
let finished = true;
|
|
24
|
-
if (source !== change) {
|
|
25
|
-
const featureText = updateProgress(text(source.path) ?? "", (progress) => progress.changes.set(change.folderName, "landed"));
|
|
26
|
-
files.set(source.path, featureText);
|
|
27
|
-
const states = readProgress(parseMarkdown(featureText)).changes;
|
|
28
|
-
finished = readChangesTable(source.doc).every((row) => states.get(row.change) === "landed");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const release = releaseName(text(PATHS.roadmap));
|
|
32
|
-
const slug = typeof source.doc.frontmatter?.["roadmap"] === "string" ? source.doc.frontmatter["roadmap"] : undefined;
|
|
33
|
-
const roadmap = text(PATHS.roadmap);
|
|
34
|
-
if (slug !== undefined && roadmap !== undefined && roadmap.includes("## Status")) {
|
|
35
|
-
files.set(PATHS.roadmap, upsertRow(roadmap, "Status", [slug, source.folderName, finished ? "landed" : "building"]));
|
|
36
|
-
}
|
|
37
|
-
const vision = text(PATHS.vision);
|
|
38
|
-
if (finished && source.kind === "feature" && slug !== undefined && vision !== undefined && vision.includes("### Implemented")) {
|
|
39
|
-
const { text: withoutFuture, removed } = removeRow(vision, "Future", slug);
|
|
40
|
-
const summary = removed?.[1] ?? String(source.doc.frontmatter?.["title"] ?? slug);
|
|
41
|
-
files.set(PATHS.vision, upsertRow(withoutFuture, "Implemented", [slug, release, summary]));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const result: Bookkeeping = { finished, release };
|
|
45
|
-
if (finished) result.archive = { from: source.folder, to: `${PATHS.archiveDir}/${source.folderName}` };
|
|
46
|
-
return result;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** The release in progress: the roadmap's `## Release` line up to its colon. */
|
|
50
|
-
export function releaseName(roadmap: string | undefined): string {
|
|
51
|
-
if (roadmap === undefined) return "unreleased";
|
|
52
|
-
const line = roadmap.split("## Release")[1]?.split("\n").find((l) => l.trim() !== "");
|
|
53
|
-
return line?.split(":")[0]?.trim() || "unreleased";
|
|
54
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { settledIds } from "../approvals/rebase.ts";
|
|
2
|
-
import { sectionForKind } from "../architecture_sections.ts";
|
|
3
|
-
import { CruzeError } from "../cruze_error.ts";
|
|
4
|
-
import { setStatus } from "../edits/status_lines.ts";
|
|
5
|
-
import { headingElements, statusOf, type Element } from "../elements.ts";
|
|
6
|
-
import { elementHash } from "../hashing.ts";
|
|
7
|
-
import { STATUS_KINDS, kindOf, scopeOf } from "../ids.ts";
|
|
8
|
-
import { findSection, parseMarkdown } from "../markdown.ts";
|
|
9
|
-
import { readDelta, type DeltaEntry } from "../project/deltas.ts";
|
|
10
|
-
import { PATHS, specPath } from "../project/layout.ts";
|
|
11
|
-
import { readScope } from "../project/plan_parts.ts";
|
|
12
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
13
|
-
import type { WorkItem } from "../project/work_items.ts";
|
|
14
|
-
import { REMOVED_MARK, capabilityKey } from "../validation/delta_rules.ts";
|
|
15
|
-
import { appendBlock, relevel, removeElement, replaceElement } from "./text_ops.ts";
|
|
16
|
-
|
|
17
|
-
export interface MergePlan {
|
|
18
|
-
/** New text of every living document the merge changes. */
|
|
19
|
-
files: Map<string, string>;
|
|
20
|
-
/** The work's merge record after this land: element ID to its merged hash. */
|
|
21
|
-
merged: Record<string, string>;
|
|
22
|
-
mergedIds: string[];
|
|
23
|
-
built: string[];
|
|
24
|
-
removed: string[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Merges a change's work into the living docs. The first change of a feature to land merges
|
|
29
|
-
* the whole delta; elements it doesn't build arrive as planned, and later changes flip what
|
|
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.
|
|
32
|
-
*/
|
|
33
|
-
export function planMerge(view: ProjectView, change: WorkItem, source: WorkItem): MergePlan {
|
|
34
|
-
const scope = readScope(change.doc);
|
|
35
|
-
const inScope = new Set([...(scope?.delivers ?? []), ...(scope?.builds ?? [])]);
|
|
36
|
-
const delta = readDelta(source.doc);
|
|
37
|
-
const merged = { ...(view.approvals.get(source.folder)?.merged ?? {}) };
|
|
38
|
-
const files = new Map<string, string>();
|
|
39
|
-
const text = (path: string): string => files.get(path) ?? view.snapshot.get(path) ?? "";
|
|
40
|
-
const plan: MergePlan = { files, merged, mergedIds: [], built: [], removed: [] };
|
|
41
|
-
const settled = settledIds(source.doc, merged);
|
|
42
|
-
|
|
43
|
-
for (const op of delta.capabilities) {
|
|
44
|
-
const key = capabilityKey(op.capability);
|
|
45
|
-
if (merged[key] !== undefined || view.snapshot.has(specPath(op.capability))) continue;
|
|
46
|
-
const body = source.doc.lines.slice(op.start + 1, op.end).join("\n").trim();
|
|
47
|
-
files.set(specPath(op.capability), `# ${op.title}\n\n${body}\n`);
|
|
48
|
-
merged[key] = "created";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
for (const entry of delta.entries) {
|
|
52
|
-
const { id } = entry.element;
|
|
53
|
-
const path = entry.section === "spec" ? specPath(scopeOf(id) ?? "") : PATHS.architecture;
|
|
54
|
-
if (entry.element.op === "REMOVED") {
|
|
55
|
-
if (scope?.removes.includes(id) === true) {
|
|
56
|
-
files.set(path, removeElement(text(path), id));
|
|
57
|
-
merged[id] = REMOVED_MARK;
|
|
58
|
-
plan.removed.push(id);
|
|
59
|
-
}
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
const kept = settled.has(id) && view.living.elements.has(id);
|
|
63
|
-
if (!kept) mergeEntry(view, source, entry, path, text, files, merged, plan);
|
|
64
|
-
for (const target of [entry.element, ...entry.scenarios].filter((e) => STATUS_KINDS.includes(e.kind))) {
|
|
65
|
-
if (kept && !inScope.has(target.id)) continue;
|
|
66
|
-
files.set(path, setStatus(text(path), target.id, statusAfterMerge(view, source, target, inScope)));
|
|
67
|
-
if (inScope.has(target.id)) plan.built.push(target.id);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
for (const id of [...inScope].filter((id) => !delta.ids.has(id) && STATUS_KINDS.includes(kindOf(id)))) {
|
|
72
|
-
const living = view.living.elements.get(id);
|
|
73
|
-
if (living === undefined) continue;
|
|
74
|
-
files.set(living.path, setStatus(text(living.path), id, "built"));
|
|
75
|
-
plan.built.push(id);
|
|
76
|
-
}
|
|
77
|
-
verifyMerged(source, delta.entries, text, plan);
|
|
78
|
-
return plan;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function mergeEntry(
|
|
82
|
-
view: ProjectView,
|
|
83
|
-
source: WorkItem,
|
|
84
|
-
entry: DeltaEntry,
|
|
85
|
-
path: string,
|
|
86
|
-
text: (path: string) => string,
|
|
87
|
-
files: Map<string, string>,
|
|
88
|
-
merged: Record<string, string>,
|
|
89
|
-
plan: MergePlan,
|
|
90
|
-
): void {
|
|
91
|
-
const { id } = entry.element;
|
|
92
|
-
const block = relevel(source.doc, entry.element.start, entry.element.end, entry.section === "spec" ? 2 : 3);
|
|
93
|
-
const deltaHash = elementHash(source.doc, entry.element);
|
|
94
|
-
const doc = parseMarkdown(text(path));
|
|
95
|
-
const living = headingElements(doc).find((e) => e.id === id);
|
|
96
|
-
const previous = merged[id];
|
|
97
|
-
if (living !== undefined) {
|
|
98
|
-
const livingHash = elementHash(doc, living);
|
|
99
|
-
if (previous !== undefined && previous !== REMOVED_MARK && livingHash !== previous) {
|
|
100
|
-
throw new CruzeError("merge-conflict", `${id} in ${path} changed since ${source.ref} merged it; rethink before landing`);
|
|
101
|
-
}
|
|
102
|
-
if (livingHash !== deltaHash) {
|
|
103
|
-
files.set(path, replaceElement(text(path), id, block));
|
|
104
|
-
plan.mergedIds.push(id);
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
if (previous !== undefined && previous !== REMOVED_MARK) {
|
|
108
|
-
throw new CruzeError("merge-conflict", `${id} was removed from ${path} after ${source.ref} merged it; rethink before landing`);
|
|
109
|
-
}
|
|
110
|
-
const section = entry.section === "spec" ? undefined : sectionForKind(entry.element.kind);
|
|
111
|
-
if (section !== undefined && findSection(doc, section) === null) throw new CruzeError("merge-conflict", `${path} has no "## ${section}" section for ${id}`);
|
|
112
|
-
files.set(path, appendBlock(text(path), block, section));
|
|
113
|
-
plan.mergedIds.push(id);
|
|
114
|
-
}
|
|
115
|
-
merged[id] = deltaHash;
|
|
116
|
-
for (const scenario of entry.scenarios) merged[scenario.id] = elementHash(source.doc, scenario);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Built when this change builds it, or when it was built and its text is unchanged; planned otherwise. */
|
|
120
|
-
function statusAfterMerge(view: ProjectView, source: WorkItem, target: Element, inScope: Set<string>): "planned" | "built" {
|
|
121
|
-
if (inScope.has(target.id)) return "built";
|
|
122
|
-
const before = view.living.elements.get(target.id);
|
|
123
|
-
if (before === undefined || statusOf(before.doc, before.element) !== "built") return "planned";
|
|
124
|
-
return elementHash(before.doc, before.element) === elementHash(source.doc, target) ? "built" : "planned";
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** After merging, every merged element must hash exactly as it did in the delta. */
|
|
128
|
-
function verifyMerged(source: WorkItem, entries: DeltaEntry[], text: (path: string) => string, plan: MergePlan): void {
|
|
129
|
-
for (const entry of entries.filter((e) => plan.mergedIds.includes(e.element.id))) {
|
|
130
|
-
const path = entry.section === "spec" ? specPath(scopeOf(entry.element.id) ?? "") : PATHS.architecture;
|
|
131
|
-
const doc = parseMarkdown(text(path));
|
|
132
|
-
const living = headingElements(doc).find((e) => e.id === entry.element.id);
|
|
133
|
-
if (living === undefined || elementHash(doc, living) !== elementHash(source.doc, entry.element)) {
|
|
134
|
-
throw new Error(`merge check failed: ${entry.element.id} in ${path} does not match the delta`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { headingElements, isStatusLine } from "../elements.ts";
|
|
2
|
-
import { parseMarkdown, type MarkdownDoc } from "../markdown.ts";
|
|
3
|
-
|
|
4
|
-
/** Line-level edits land makes to living documents. Every function takes and returns whole text. */
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* A delta block ready for a living document: headings shifted so the element's own heading
|
|
8
|
-
* sits at `level`, the operation prefix dropped, status lines removed, trailing blanks trimmed.
|
|
9
|
-
*/
|
|
10
|
-
export function relevel(doc: MarkdownDoc, start: number, end: number, level: number): string[] {
|
|
11
|
-
const own = /^(#{1,6}) /.exec(doc.lines[start] ?? "")?.[1]?.length ?? level;
|
|
12
|
-
const shift = level - own;
|
|
13
|
-
const out: string[] = [];
|
|
14
|
-
for (let i = start; i < end; i++) {
|
|
15
|
-
let line = doc.lines[i] ?? "";
|
|
16
|
-
if (!doc.inFence[i]) {
|
|
17
|
-
if (isStatusLine(line)) continue;
|
|
18
|
-
const heading = /^(#{1,6}) (.*)$/.exec(line);
|
|
19
|
-
if (heading?.[1] !== undefined && heading[2] !== undefined) {
|
|
20
|
-
const text = i === start ? heading[2].replace(/^(ADDED|MODIFIED|REMOVED) /, "") : heading[2];
|
|
21
|
-
line = `${"#".repeat(heading[1].length + shift)} ${text}`;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
out.push(line);
|
|
25
|
-
}
|
|
26
|
-
while (out.length > 0 && out[out.length - 1]?.trim() === "") out.pop();
|
|
27
|
-
return out;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Replaces an element's whole block, keeping one blank line before whatever follows. */
|
|
31
|
-
export function replaceElement(text: string, id: string, block: string[]): string {
|
|
32
|
-
const doc = parseMarkdown(text);
|
|
33
|
-
const element = headingElements(doc).find((e) => e.id === id);
|
|
34
|
-
if (element === undefined) throw new Error(`${id} is not in the document`);
|
|
35
|
-
const lines = [...doc.lines];
|
|
36
|
-
const follows = element.end < lines.length;
|
|
37
|
-
lines.splice(element.start, element.end - element.start, ...block, ...(follows ? [""] : []));
|
|
38
|
-
return lines.join("\n");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function removeElement(text: string, id: string): string {
|
|
42
|
-
const doc = parseMarkdown(text);
|
|
43
|
-
const element = headingElements(doc).find((e) => e.id === id);
|
|
44
|
-
if (element === undefined) return text;
|
|
45
|
-
const lines = [...doc.lines];
|
|
46
|
-
lines.splice(element.start, element.end - element.start);
|
|
47
|
-
return lines.join("\n");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Appends a block at the end of a `##` section, or at the end of the document when none is named. */
|
|
51
|
-
export function appendBlock(text: string, block: string[], section?: string): string {
|
|
52
|
-
const doc = parseMarkdown(text);
|
|
53
|
-
const lines = [...doc.lines];
|
|
54
|
-
let at = lines.length;
|
|
55
|
-
if (section !== undefined) {
|
|
56
|
-
const heading = doc.headings.find((h) => h.level === 2 && h.text === section);
|
|
57
|
-
if (heading === undefined) throw new Error(`the document has no "## ${section}" section`);
|
|
58
|
-
at = doc.headings.find((h) => h.line > heading.line && h.level <= 2)?.line ?? lines.length;
|
|
59
|
-
}
|
|
60
|
-
let insertAt = at;
|
|
61
|
-
while (insertAt > 0 && lines[insertAt - 1]?.trim() === "") insertAt--;
|
|
62
|
-
const trailing = at < lines.length ? [""] : [];
|
|
63
|
-
lines.splice(insertAt, at - insertAt, "", ...block, ...trailing);
|
|
64
|
-
const joined = lines.join("\n");
|
|
65
|
-
return joined.endsWith("\n") ? joined : `${joined}\n`;
|
|
66
|
-
}
|
package/src/domain/line_diff.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
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
|
-
}
|