@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,51 +0,0 @@
|
|
|
1
|
-
import { CruzeError } from "../../domain/cruze_error.ts";
|
|
2
|
-
import { resolveArtifact } from "../../domain/project/artifact_ref.ts";
|
|
3
|
-
import { scopeOf } from "../../domain/ids.ts";
|
|
4
|
-
import { notesFor, readNotes, type NotesFor } from "../../domain/project/notes.ts";
|
|
5
|
-
import { scopeView, type ScopeView } from "../../domain/project/scope_view.ts";
|
|
6
|
-
import type { WorkItem } from "../../domain/project/work_items.ts";
|
|
7
|
-
import { loadView, type ProjectDeps } from "../project_context.ts";
|
|
8
|
-
import { listEvents } from "./journal_events.ts";
|
|
9
|
-
|
|
10
|
-
export interface Disposition {
|
|
11
|
-
finding: string;
|
|
12
|
-
disposition: string;
|
|
13
|
-
reason: string;
|
|
14
|
-
review: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ScopeReport extends ScopeView {
|
|
18
|
-
/** The settled ledger of the item: the feature's dispositions and its changes', or a standalone change's. */
|
|
19
|
-
dispositions: Disposition[];
|
|
20
|
-
/** The external documents the design must comply with, from `references:` in `.cruze/config.yaml`. */
|
|
21
|
-
references: Array<{ path: string; use: string }>;
|
|
22
|
-
/** The notes about this work, and the parts of the user's input handed to it. */
|
|
23
|
-
notes: NotesFor;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The design a change builds, or a feature's whole delta, with the elements it cites and the
|
|
28
|
-
* item's settled ledger: what plan, build and the reviewers read instead of the whole living docs.
|
|
29
|
-
*/
|
|
30
|
-
export async function showScope(deps: ProjectDeps, ref: string): Promise<ScopeReport> {
|
|
31
|
-
const view = await loadView(deps.files);
|
|
32
|
-
const { item } = resolveArtifact(view, ref);
|
|
33
|
-
if (item === undefined) throw new CruzeError("not-work", `"${ref}" is not a feature or change; show takes a feature or change ID`);
|
|
34
|
-
const scope = scopeView(view, item);
|
|
35
|
-
const dispositions = (await listEvents(deps, "disposition", scope.feature ?? item.ref, true)).map((entry) => ({
|
|
36
|
-
finding: String(entry["finding"] ?? ""),
|
|
37
|
-
disposition: String(entry["disposition"] ?? ""),
|
|
38
|
-
reason: String(entry["reason"] ?? ""),
|
|
39
|
-
review: String(entry["review"] ?? ""),
|
|
40
|
-
}));
|
|
41
|
-
const feature = scope.feature === undefined ? item : (view.items.find((i) => i.ref === scope.feature) ?? item);
|
|
42
|
-
const contexts = [...scope.elements, ...scope.scenarios].flatMap((e) => scopeOf(e.id) ?? []);
|
|
43
|
-
const notes = notesFor(readNotes(view.snapshot), [...slugsOf(feature), ...contexts]);
|
|
44
|
-
return { ...scope, dispositions, references: view.config?.config?.references ?? [], notes };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** The names a note or a hand-off may use for a feature or standalone change: its roadmap slug, and its folder name without the date. */
|
|
48
|
-
export function slugsOf(item: WorkItem): string[] {
|
|
49
|
-
const roadmap = item.doc.frontmatter?.["roadmap"];
|
|
50
|
-
return [...new Set([...(typeof roadmap === "string" ? [roadmap] : []), item.folderName.replace(/^\d{4}-\d{2}-\d{2}-/, "")])];
|
|
51
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { evaluateApproval, type ApprovalState } from "../../domain/approvals/evaluate.ts";
|
|
2
|
-
import { rebaseNeeded } from "../../domain/approvals/rebase.ts";
|
|
3
|
-
import { upstreamLines, upstreamPath } from "../../domain/approvals/upstream_text.ts";
|
|
4
|
-
import { lineDiff } from "../../domain/line_diff.ts";
|
|
5
|
-
import { resolveArtifact } from "../../domain/project/artifact_ref.ts";
|
|
6
|
-
import { approvalFolder, approvalsPath } from "../../domain/project/project_view.ts";
|
|
7
|
-
import { loadView, type ProjectDeps } from "../project_context.ts";
|
|
8
|
-
|
|
9
|
-
export interface UpstreamChange {
|
|
10
|
-
/** The upstream element ID, or `doc:<path>` for a feature's design. */
|
|
11
|
-
key: string;
|
|
12
|
-
path?: string;
|
|
13
|
-
/** The approved text against the current text, when the approved text was found. */
|
|
14
|
-
diff?: string[];
|
|
15
|
-
note?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface UpstreamDiffReport {
|
|
19
|
-
artifact: string;
|
|
20
|
-
ref?: string;
|
|
21
|
-
state: ApprovalState;
|
|
22
|
-
changes: UpstreamChange[];
|
|
23
|
-
/** Elements the work's delta changes whose living text moved: approval needs --rebase for each. */
|
|
24
|
-
rebase: string[];
|
|
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: ProjectDeps, ref: string): Promise<UpstreamDiffReport> {
|
|
34
|
-
const view = await loadView(deps.files);
|
|
35
|
-
const { path, item } = resolveArtifact(view, ref);
|
|
36
|
-
const status = evaluateApproval(view, path);
|
|
37
|
-
const report: UpstreamDiffReport = { 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
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { CruzeError } from "../cruze_error.ts";
|
|
2
|
-
import type { JournalEntry } from "../journal.ts";
|
|
3
|
-
import type { WorkItem } from "../project/work_items.ts";
|
|
4
|
-
import type { ApprovalStatus } from "./evaluate.ts";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Why an approval was given without the user: a plan whose review closed, or work that a
|
|
8
|
-
* rethink left unchanged while something it cites moved. The user can still object, and
|
|
9
|
-
* editing the document makes the approval stale as usual.
|
|
10
|
-
*/
|
|
11
|
-
export type AgentBasis = "plan-review" | "rethink";
|
|
12
|
-
|
|
13
|
-
const byTime = (a: JournalEntry, b: JournalEntry): number => a.at.localeCompare(b.at);
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* A feature's change may be approved by the agent once its plan review has closed: a review
|
|
17
|
-
* recorded since the change was last approved, no blocker left open in its last round, and a
|
|
18
|
-
* disposition for every finding of its first round. The feature's design, and a standalone
|
|
19
|
-
* change's, stay the user's to approve.
|
|
20
|
-
*/
|
|
21
|
-
export function requirePlanReviewClosed(journal: JournalEntry[], item: WorkItem): void {
|
|
22
|
-
if (item.kind !== "change") {
|
|
23
|
-
throw new CruzeError("needs-user", `${item.ref} carries design, so the user approves it; only a feature's change can be approved after its plan review`);
|
|
24
|
-
}
|
|
25
|
-
const entries = journal.filter((e) => e["item"] === item.ref || e["artifact"] === item.path).sort(byTime);
|
|
26
|
-
const lastApproval = entries.filter((e) => e.event === "approve").at(-1);
|
|
27
|
-
const reviews = entries.filter((e) => e.event === "review" && e["review"] === "plan" && (lastApproval === undefined || e.at > lastApproval.at));
|
|
28
|
-
const last = reviews.at(-1);
|
|
29
|
-
if (last === undefined) {
|
|
30
|
-
const since = lastApproval === undefined ? "" : " since the change was last approved";
|
|
31
|
-
throw new CruzeError("plan-review-open", `no plan review is recorded for ${item.ref}${since}; run the plan review first`);
|
|
32
|
-
}
|
|
33
|
-
const blockers = Number(last["blockers"] ?? 0);
|
|
34
|
-
if (blockers > 0 && Number(last["round"]) === 1) {
|
|
35
|
-
throw new CruzeError("plan-review-open", `the plan review's round 1 found ${blockers} blocker(s); check their fixes in round 2`);
|
|
36
|
-
}
|
|
37
|
-
if (blockers > 0) {
|
|
38
|
-
throw new CruzeError("plan-review-open", `round 2 left ${blockers} blocker(s) open; the user decides them and approves the plan`);
|
|
39
|
-
}
|
|
40
|
-
const first = reviews.filter((e) => Number(e["round"]) === 1).at(-1) ?? last;
|
|
41
|
-
const findings = Number(first["blockers"] ?? 0) + Number(first["concerns"] ?? 0);
|
|
42
|
-
const decided = entries.filter((e) => e.event === "disposition" && e["review"] === "plan" && e.at > first.at).length;
|
|
43
|
-
if (decided < findings) {
|
|
44
|
-
throw new CruzeError("plan-review-open", `the plan review raised ${findings} finding(s) but ${decided} have a disposition; record one for each`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* A rethink may re-approve, for the user, a feature or change it didn't edit: one whose own
|
|
50
|
-
* design is unchanged and that is stale only because something it cites changed. A change
|
|
51
|
-
* with a task reopened since its approval goes back to the user.
|
|
52
|
-
*/
|
|
53
|
-
export function requireUnchangedSinceApproval(journal: JournalEntry[], item: WorkItem | undefined, path: string, status: ApprovalStatus): void {
|
|
54
|
-
if (item === undefined) {
|
|
55
|
-
throw new CruzeError("needs-user", `${path} is a project document, so the user re-approves it; a rethink re-stamps only features and changes`);
|
|
56
|
-
}
|
|
57
|
-
if (status.state === "edited") {
|
|
58
|
-
throw new CruzeError("needs-user", `${item.ref} itself changed since its approval, so the user re-approves it`);
|
|
59
|
-
}
|
|
60
|
-
if (status.state !== "upstream-changed") {
|
|
61
|
-
throw new CruzeError("not-stale", `${item.ref} is ${status.state}; only work whose upstream changed can be re-stamped`);
|
|
62
|
-
}
|
|
63
|
-
const approvedAt = journal.filter((e) => e.event === "approve" && e["artifact"] === path).sort(byTime).at(-1)?.at ?? "";
|
|
64
|
-
const reopened = journal.filter((e) => e.event === "reopen" && e["change"] === item.ref && e.at > approvedAt).map((e) => String(e["task"]));
|
|
65
|
-
if (reopened.length > 0) {
|
|
66
|
-
throw new CruzeError("needs-user", `${item.ref} had ${reopened.join(", ")} reopened since its approval, so the user re-approves it`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { parseMarkdown } from "../markdown.ts";
|
|
2
|
-
import { approvalFolder, type ProjectView } from "../project/project_view.ts";
|
|
3
|
-
import { currentUpstreamHash, fingerprint } from "./fingerprint.ts";
|
|
4
|
-
import { latestApproval, type ApprovalRecord } from "./records.ts";
|
|
5
|
-
|
|
6
|
-
export type ApprovalState = "approved" | "edited" | "upstream-changed" | "unapproved";
|
|
7
|
-
|
|
8
|
-
export interface ApprovalStatus {
|
|
9
|
-
artifact: string;
|
|
10
|
-
state: ApprovalState;
|
|
11
|
-
/** Upstream IDs or documents that changed or disappeared since approval. */
|
|
12
|
-
changed: string[];
|
|
13
|
-
record?: ApprovalRecord;
|
|
14
|
-
/** Set when an approval exists but the journal has no matching entry. */
|
|
15
|
-
unjournaled?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Computes an artifact's approval state from content, never from stored state:
|
|
20
|
-
* editing an upstream document is all it takes to make downstream work stale.
|
|
21
|
-
*/
|
|
22
|
-
export function evaluateApproval(view: ProjectView, artifact: string): ApprovalStatus {
|
|
23
|
-
const record = latestApproval(view.approvals.get(approvalFolder(view, artifact)) ?? { version: 1, approvals: [], merged: {} }, artifact);
|
|
24
|
-
if (record === undefined) return { artifact, state: "unapproved", changed: [] };
|
|
25
|
-
const journaled = view.journal.some((e) => e.event === "approve" && e.artifact === artifact && e.hash === record.hash);
|
|
26
|
-
if (!journaled) return { artifact, state: "unapproved", changed: [], record, unjournaled: true };
|
|
27
|
-
|
|
28
|
-
const text = view.snapshot.get(artifact);
|
|
29
|
-
if (text === undefined) return { artifact, state: "unapproved", changed: [], record };
|
|
30
|
-
const { hash } = fingerprint(view, artifact, parseMarkdown(text));
|
|
31
|
-
if (hash !== record.hash) return { artifact, state: "edited", changed: [], record };
|
|
32
|
-
const changed = Object.entries(record.upstream)
|
|
33
|
-
.filter(([key, pinned]) => currentUpstreamHash(view, key) !== pinned)
|
|
34
|
-
.map(([key]) => key);
|
|
35
|
-
return { artifact, state: changed.length > 0 ? "upstream-changed" : "approved", changed, record };
|
|
36
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { headingElements, rowElements } from "../elements.ts";
|
|
2
|
-
import { designHash, elementHash } from "../hashing.ts";
|
|
3
|
-
import { findIds } from "../ids.ts";
|
|
4
|
-
import type { MarkdownDoc } from "../markdown.ts";
|
|
5
|
-
import { PATHS } from "../project/layout.ts";
|
|
6
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
7
|
-
import { featureOf } from "../project/work_items.ts";
|
|
8
|
-
|
|
9
|
-
/** What an approval pins: the document's design hash and the hash of everything upstream it relies on. */
|
|
10
|
-
export interface Fingerprint {
|
|
11
|
-
hash: string;
|
|
12
|
-
upstream: Record<string, string>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Documents a person can approve. Specs, the glossary and ADRs change only through approved work. */
|
|
16
|
-
export function isApprovable(view: ProjectView, path: string): boolean {
|
|
17
|
-
return [PATHS.vision, PATHS.architecture, PATHS.roadmap].includes(path as never) || view.items.some((i) => i.path === path && !i.archived);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function fingerprint(view: ProjectView, path: string, doc: MarkdownDoc): Fingerprint {
|
|
21
|
-
const upstream: Record<string, string> = {};
|
|
22
|
-
const ownIds = new Set([...headingElements(doc), ...rowElements(doc)].filter((e) => e.op !== "ADDED").map((e) => e.id));
|
|
23
|
-
const isLivingDoc = !view.items.some((item) => item.path === path);
|
|
24
|
-
const designText = doc.lines.filter((_, i) => !doc.inManaged[i]).join("\n");
|
|
25
|
-
|
|
26
|
-
for (const id of findIds(designText)) {
|
|
27
|
-
if (isLivingDoc && ownIds.has(id)) continue;
|
|
28
|
-
const living = view.living.elements.get(id);
|
|
29
|
-
if (living !== undefined) upstream[id] = elementHash(living.doc, living.element);
|
|
30
|
-
}
|
|
31
|
-
const item = view.items.find((i) => i.path === path);
|
|
32
|
-
if (item !== undefined) {
|
|
33
|
-
// Work always depends on the dependency rules, cited or not.
|
|
34
|
-
for (const [id, living] of view.living.elements) {
|
|
35
|
-
if (id.startsWith("RULE-")) upstream[id] = elementHash(living.doc, living.element);
|
|
36
|
-
}
|
|
37
|
-
const feature = item.kind === "change" ? featureOf(view.items, item) : undefined;
|
|
38
|
-
if (feature !== undefined) upstream[`doc:${feature.path}`] = designHash(feature.doc);
|
|
39
|
-
}
|
|
40
|
-
return { hash: designHash(doc), upstream };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** The current hash of one upstream key, or undefined when it no longer exists. */
|
|
44
|
-
export function currentUpstreamHash(view: ProjectView, key: string): string | undefined {
|
|
45
|
-
if (key.startsWith("doc:")) {
|
|
46
|
-
const path = key.slice("doc:".length);
|
|
47
|
-
const item = view.items.find((i) => i.path === path);
|
|
48
|
-
return item === undefined ? undefined : designHash(item.doc);
|
|
49
|
-
}
|
|
50
|
-
const living = view.living.elements.get(key);
|
|
51
|
-
return living === undefined ? undefined : elementHash(living.doc, living.element);
|
|
52
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { elementHash } from "../hashing.ts";
|
|
2
|
-
import type { MarkdownDoc } from "../markdown.ts";
|
|
3
|
-
import { readDelta } from "../project/deltas.ts";
|
|
4
|
-
import { approvalFolder, type ProjectView } from "../project/project_view.ts";
|
|
5
|
-
import type { WorkItem } from "../project/work_items.ts";
|
|
6
|
-
import { latestApproval } from "./records.ts";
|
|
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: MarkdownDoc, merged: Record<string, string>): Set<string> {
|
|
15
|
-
const settled = new Set<string>();
|
|
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: ProjectView, item: WorkItem): string[] {
|
|
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
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/** What `approvals.json` holds. Only the CLI writes it. */
|
|
2
|
-
|
|
3
|
-
export interface ApprovalRecord {
|
|
4
|
-
/** Path of the approved document. */
|
|
5
|
-
artifact: string;
|
|
6
|
-
/** Design hash of the document when approved. */
|
|
7
|
-
hash: string;
|
|
8
|
-
/** Hash of each upstream element or document the approval depends on, by ID or `doc:<path>`. */
|
|
9
|
-
upstream: Record<string, string>;
|
|
10
|
-
approvedAt: string;
|
|
11
|
-
by: string;
|
|
12
|
-
/** Why the approval exists when no person gave it, such as a merge by `land`. */
|
|
13
|
-
basis?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ApprovalsFile {
|
|
17
|
-
version: 1;
|
|
18
|
-
approvals: ApprovalRecord[];
|
|
19
|
-
/** For a feature or standalone change: hash of each living element it merged, by ID. */
|
|
20
|
-
merged: Record<string, string>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function emptyApprovals(): ApprovalsFile {
|
|
24
|
-
return { version: 1, approvals: [], merged: {} };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function parseApprovals(text: string | undefined): { file: ApprovalsFile; problem?: string } {
|
|
28
|
-
if (text === undefined) return { file: emptyApprovals() };
|
|
29
|
-
try {
|
|
30
|
-
const raw = JSON.parse(text) as Partial<ApprovalsFile>;
|
|
31
|
-
if (raw.version !== 1 || !Array.isArray(raw.approvals)) return { file: emptyApprovals(), problem: "approvals.json has an unknown format" };
|
|
32
|
-
return { file: { version: 1, approvals: raw.approvals, merged: raw.merged ?? {} } };
|
|
33
|
-
} catch {
|
|
34
|
-
return { file: emptyApprovals(), problem: "approvals.json is not valid JSON" };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function serializeApprovals(file: ApprovalsFile): string {
|
|
39
|
-
return `${JSON.stringify(file, null, 2)}\n`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** The latest approval of an artifact, if any. */
|
|
43
|
-
export function latestApproval(file: ApprovalsFile, artifact: string): ApprovalRecord | undefined {
|
|
44
|
-
return file.approvals.filter((record) => record.artifact === artifact).at(-1);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Replaces the artifact's approval: one current approval per artifact; history lives in the journal. */
|
|
48
|
-
export function withApproval(file: ApprovalsFile, record: ApprovalRecord): ApprovalsFile {
|
|
49
|
-
return { ...file, approvals: [...file.approvals.filter((r) => r.artifact !== record.artifact), record] };
|
|
50
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { isStatusLine } from "../elements.ts";
|
|
2
|
-
import { designHash, elementHash } from "../hashing.ts";
|
|
3
|
-
import { parseMarkdown, type MarkdownDoc } from "../markdown.ts";
|
|
4
|
-
import { buildLivingModel } from "../project/living_model.ts";
|
|
5
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
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: ProjectView, key: string): string | undefined {
|
|
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: string, path: string, text: string, pinned?: string): string[] | undefined {
|
|
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: MarkdownDoc, start: number, end: number): string[] {
|
|
30
|
-
const lines: string[] = [];
|
|
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,23 +0,0 @@
|
|
|
1
|
-
import type { Kind } from "./ids.ts";
|
|
2
|
-
|
|
3
|
-
/** The `##` sections of architecture.md, in order, and the element kind each holds. */
|
|
4
|
-
export const ARCHITECTURE_SECTIONS: ReadonlyArray<{ title: string; kind: Kind | null }> = [
|
|
5
|
-
{ title: "Overview", kind: "VIEW" },
|
|
6
|
-
{ title: "Bounded contexts", kind: "CTX" },
|
|
7
|
-
{ title: "Domain model", kind: "ENT" },
|
|
8
|
-
{ title: "Use cases", kind: "UC" },
|
|
9
|
-
{ title: "Ports", kind: "PORT" },
|
|
10
|
-
{ title: "Adapters", kind: "ADP" },
|
|
11
|
-
{ title: "Flows", kind: "FLOW" },
|
|
12
|
-
{ title: "Modules", kind: "MOD" },
|
|
13
|
-
{ title: "Dependency rules", kind: "RULE" },
|
|
14
|
-
{ title: "Cross-cutting concerns", kind: "XC" },
|
|
15
|
-
{ title: "Decisions", kind: null },
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
export const ARCHITECTURE_KINDS: readonly Kind[] = ["VIEW", "CTX", "ENT", "UC", "PORT", "ADP", "FLOW", "MOD", "RULE", "XC"];
|
|
19
|
-
export const SPEC_KINDS: readonly Kind[] = ["REQ", "SCN"];
|
|
20
|
-
|
|
21
|
-
export function sectionForKind(kind: Kind): string | undefined {
|
|
22
|
-
return ARCHITECTURE_SECTIONS.find((section) => section.kind === kind)?.title;
|
|
23
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { findDecisionCitations, type DecisionCitation } from "../decision_citations.ts";
|
|
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: string, text: string): DecisionCitation[] {
|
|
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: string, lines: string[]): string[] {
|
|
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: string, marker: RegExp): string {
|
|
36
|
-
const match = marker.exec(line);
|
|
37
|
-
return match === null ? "" : line.slice(match.index);
|
|
38
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
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
|
-
export interface DecisionCitation {
|
|
9
|
-
/** 0-based line. */
|
|
10
|
-
line: number;
|
|
11
|
-
text: string;
|
|
12
|
-
numbers: number[];
|
|
13
|
-
/** The work item named with it, as in `events D17` or `D1 of 02-links`. */
|
|
14
|
-
qualifier?: string;
|
|
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: readonly string[], slugs: ReadonlySet<string>, skip: (line: number) => boolean = () => false): DecisionCitation[] {
|
|
26
|
-
const citations: DecisionCitation[] = [];
|
|
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: readonly string[]): Set<number> {
|
|
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: Iterable<string>): Set<string> {
|
|
55
|
-
const slugs = new Set<string>();
|
|
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,58 +0,0 @@
|
|
|
1
|
-
import { factsOf, statusOf } from "../elements.ts";
|
|
2
|
-
import { findSection, parseMarkdown } from "../markdown.ts";
|
|
3
|
-
import type { ProjectView } from "../project/project_view.ts";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The managed sections of a project's `AGENTS.md`: its commands, from `.cruze/config.yaml`,
|
|
7
|
-
* and its layout, from the built modules in `docs/architecture.md`.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const OPEN = "<!-- cruze:managed -->";
|
|
11
|
-
const CLOSE = "<!-- /cruze:managed -->";
|
|
12
|
-
|
|
13
|
-
/** Rewrites the managed blocks under `## Commands` and `## Layout`; other text is left alone. */
|
|
14
|
-
export function renderAgentsFile(text: string, view: ProjectView): string {
|
|
15
|
-
let next = text;
|
|
16
|
-
const commands = Object.entries(view.config?.config?.commands ?? {});
|
|
17
|
-
if (commands.length > 0) next = replaceBlock(next, "Commands", commands.map(([name, command]) => `- ${name}: \`${command}\``));
|
|
18
|
-
const modules = builtModules(view);
|
|
19
|
-
if (modules.length > 0) next = replaceBlock(next, "Layout", ["Modules, from `docs/architecture.md`:", "", ...modules]);
|
|
20
|
-
return next;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** The lines under `## Known pitfalls`, one per pitfall, which the feedback export carries back to Cruze. */
|
|
24
|
-
export function knownPitfalls(text: string): string[] {
|
|
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
|
-
|
|
31
|
-
/**
|
|
32
|
-
* The modules whose code exists: those built, and those a land built before a rethink set them
|
|
33
|
-
* back to `planned` for a change to rebuild, since their directories are still there.
|
|
34
|
-
*/
|
|
35
|
-
function builtModules(view: ProjectView): string[] {
|
|
36
|
-
const everBuilt = new Set(view.journal.filter((e) => e.event === "land").flatMap((e) => (Array.isArray(e["built"]) ? (e["built"] as unknown[]).map(String) : [])));
|
|
37
|
-
return [...view.living.elements.values()]
|
|
38
|
-
.filter(({ element, doc }) => element.kind === "MOD" && (statusOf(doc, element) === "built" || everBuilt.has(element.id)))
|
|
39
|
-
.map(({ element, doc }) => {
|
|
40
|
-
const facts = factsOf(doc, element);
|
|
41
|
-
const paths = (facts.get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => `\`${m[1] ?? ""}\``));
|
|
42
|
-
return `- ${paths.join(", ")} (${facts.get("Layer")?.[0] ?? "unknown"}): ${element.title}`;
|
|
43
|
-
})
|
|
44
|
-
.sort();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Replaces the body of the first managed block after `## <heading>`, before the next `## ` heading. */
|
|
48
|
-
function replaceBlock(text: string, heading: string, body: string[]): string {
|
|
49
|
-
const lines = text.split("\n");
|
|
50
|
-
const at = lines.findIndex((line) => line === `## ${heading}`);
|
|
51
|
-
if (at === -1) return text;
|
|
52
|
-
const sectionEnd = lines.findIndex((line, i) => i > at && line.startsWith("## "));
|
|
53
|
-
const end = sectionEnd === -1 ? lines.length : sectionEnd;
|
|
54
|
-
const open = lines.findIndex((line, i) => i > at && i < end && line.trim() === OPEN);
|
|
55
|
-
const close = lines.findIndex((line, i) => i > open && i < end && line.trim() === CLOSE);
|
|
56
|
-
if (open === -1 || close === -1) return text;
|
|
57
|
-
return [...lines.slice(0, open + 1), ...body, ...lines.slice(close)].join("\n");
|
|
58
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { parseMarkdown } from "../markdown.ts";
|
|
2
|
-
import { splitRow, tablesIn } from "../markdown_tables.ts";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Edits to the managed tables: the feature map in vision.md and the status table
|
|
6
|
-
* in roadmap.md. Rows are keyed by their first cell.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** Replaces or appends the row keyed by `cells[0]` in the table under a heading. */
|
|
10
|
-
export function upsertRow(text: string, heading: string, cells: string[]): string {
|
|
11
|
-
return editTable(text, heading, (rows) => {
|
|
12
|
-
const index = rows.findIndex((row) => row[0] === cells[0]);
|
|
13
|
-
if (index === -1) rows.push(cells);
|
|
14
|
-
else rows[index] = cells;
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function removeRow(text: string, heading: string, key: string): { text: string; removed: string[] | null } {
|
|
19
|
-
let removed: string[] | null = null;
|
|
20
|
-
const next = editTable(text, heading, (rows) => {
|
|
21
|
-
const index = rows.findIndex((row) => row[0] === key);
|
|
22
|
-
if (index !== -1) removed = rows.splice(index, 1)[0] ?? null;
|
|
23
|
-
});
|
|
24
|
-
return { text: next, removed };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function readRows(text: string, heading: string): string[][] {
|
|
28
|
-
const doc = parseMarkdown(text);
|
|
29
|
-
const at = doc.headings.find((h) => h.text === heading);
|
|
30
|
-
if (at === undefined) return [];
|
|
31
|
-
const end = doc.headings.find((h) => h.line > at.line && h.level <= at.level)?.line ?? doc.lines.length;
|
|
32
|
-
return tablesIn(doc.lines, doc.inFence, at.line + 1, end)[0]?.rows.map((row) => row.cells) ?? [];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function editTable(text: string, heading: string, edit: (rows: string[][]) => void): string {
|
|
36
|
-
const doc = parseMarkdown(text);
|
|
37
|
-
const at = doc.headings.find((h) => h.text === heading);
|
|
38
|
-
if (at === undefined) throw new Error(`No "${heading}" heading to edit`);
|
|
39
|
-
const end = doc.headings.find((h) => h.line > at.line && h.level <= at.level)?.line ?? doc.lines.length;
|
|
40
|
-
let first = -1;
|
|
41
|
-
let last = -1;
|
|
42
|
-
for (let i = at.line + 1; i < end; i++) {
|
|
43
|
-
if (/^\s*\|.*\|\s*$/.test(doc.lines[i] ?? "")) {
|
|
44
|
-
if (first === -1) first = i;
|
|
45
|
-
last = i;
|
|
46
|
-
} else if (first !== -1) break;
|
|
47
|
-
}
|
|
48
|
-
if (first === -1) throw new Error(`No table under "${heading}"`);
|
|
49
|
-
const header = splitRow(doc.lines[first] ?? "");
|
|
50
|
-
const rows = doc.lines.slice(first + 2, last + 1).map(splitRow);
|
|
51
|
-
edit(rows);
|
|
52
|
-
const rendered = [renderRow(header), renderRow(header.map(() => "---")), ...rows.map(renderRow)];
|
|
53
|
-
const lines = [...doc.lines];
|
|
54
|
-
lines.splice(first, last - first + 1, ...rendered);
|
|
55
|
-
return lines.join("\n");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function renderRow(cells: string[]): string {
|
|
59
|
-
return `| ${cells.join(" | ")} |`;
|
|
60
|
-
}
|