@printpractical/cruze 0.0.13 → 0.0.14
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/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/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 +4 -4
- package/dist/domain/approvals.js +51 -0
- package/dist/domain/check/check_source.js +2 -31
- 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 +70 -0
- package/dist/domain/validation/release_rules.js +45 -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 +21 -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/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/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 +4 -4
- package/src/domain/approvals.ts +51 -0
- package/src/domain/check/check_source.ts +2 -31
- 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 +70 -0
- package/src/domain/validation/release_rules.ts +45 -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,42 +0,0 @@
|
|
|
1
|
-
import { CruzeError } from "../cruze_error.ts";
|
|
2
|
-
import { PATHS } from "./layout.ts";
|
|
3
|
-
import type { ProjectView } from "./project_view.ts";
|
|
4
|
-
import { findWorkItem, type WorkItem } from "./work_items.ts";
|
|
5
|
-
|
|
6
|
-
const NAMED: Record<string, string> = { vision: PATHS.vision, architecture: PATHS.architecture, roadmap: PATHS.roadmap };
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Resolves what a person or skill names on the command line: `vision`, `architecture`,
|
|
10
|
-
* `roadmap`, a work item ref such as `2026-09-25-open-console/01-local-serial`, the same ref
|
|
11
|
-
* without its date (`open-console/local-serial`) when that names one active item, or a path.
|
|
12
|
-
*/
|
|
13
|
-
export function resolveArtifact(view: ProjectView, ref: string): { path: string; item?: WorkItem } {
|
|
14
|
-
const named = NAMED[ref];
|
|
15
|
-
if (named !== undefined) {
|
|
16
|
-
if (!view.snapshot.has(named)) throw new CruzeError("not-found", `${named} does not exist yet`);
|
|
17
|
-
return { path: named };
|
|
18
|
-
}
|
|
19
|
-
const item = findWorkItem(view.items, ref.replace(/\/$/, "")) ?? bySlug(view.items, ref.replace(/\/$/, ""));
|
|
20
|
-
if (item !== undefined) return { path: item.path, item };
|
|
21
|
-
if (view.snapshot.has(ref)) return { path: ref };
|
|
22
|
-
throw new CruzeError("not-found", `nothing called "${ref}"; use vision, architecture, roadmap, a feature or change ID, or a path`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** An active item named by its slug, without the date or the change number. */
|
|
26
|
-
function bySlug(items: WorkItem[], ref: string): WorkItem | undefined {
|
|
27
|
-
const [head = "", change] = ref.split("/");
|
|
28
|
-
const undated = (name: string): string => name.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
29
|
-
const matches = items.filter((i) => !i.archived && i.kind !== "change" && undated(i.folderName) === head);
|
|
30
|
-
if (matches.length > 1) throw new CruzeError("ambiguous", `"${head}" names ${matches.map((m) => m.ref).join(" and ")}; use the full ID`);
|
|
31
|
-
const base = matches[0];
|
|
32
|
-
if (base === undefined || change === undefined) return base;
|
|
33
|
-
return items.find((i) => i.featureRef === base.ref && (i.folderName === change || i.folderName.replace(/^\d\d-/, "") === change));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Resolves a change: a feature's change or a standalone change, never archived. */
|
|
37
|
-
export function resolveChange(view: ProjectView, ref: string): WorkItem {
|
|
38
|
-
const { item } = resolveArtifact(view, ref);
|
|
39
|
-
if (item === undefined || item.kind === "feature") throw new CruzeError("not-a-change", `"${ref}" is not a change`);
|
|
40
|
-
if (item.archived) throw new CruzeError("archived", `${item.ref} has already landed`);
|
|
41
|
-
return item;
|
|
42
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { capabilityOps, headingElements, type CapabilityOp, type Element } from "../elements.ts";
|
|
2
|
-
import { findSection, type MarkdownDoc } from "../markdown.ts";
|
|
3
|
-
|
|
4
|
-
/** One delta operation on a living element, with its nested scenarios when it is a requirement. */
|
|
5
|
-
export interface DeltaEntry {
|
|
6
|
-
element: Element;
|
|
7
|
-
section: "spec" | "architecture";
|
|
8
|
-
/** Scenario elements nested under a requirement operation. */
|
|
9
|
-
scenarios: Element[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface Delta {
|
|
13
|
-
entries: DeltaEntry[];
|
|
14
|
-
capabilities: CapabilityOp[];
|
|
15
|
-
/** Every ID a delta defines or changes, including nested scenarios. */
|
|
16
|
-
ids: Set<string>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function readDelta(doc: MarkdownDoc): Delta {
|
|
20
|
-
const entries: DeltaEntry[] = [];
|
|
21
|
-
const capabilities: CapabilityOp[] = [];
|
|
22
|
-
for (const [title, section] of [["Spec delta", "spec"], ["Architecture delta", "architecture"]] as const) {
|
|
23
|
-
const range = findSection(doc, title);
|
|
24
|
-
if (range === null) continue;
|
|
25
|
-
capabilities.push(...capabilityOps(doc, range.start, range.end));
|
|
26
|
-
const elements = headingElements(doc, range.start, range.end);
|
|
27
|
-
for (const element of elements.filter((e) => e.op !== undefined)) {
|
|
28
|
-
const scenarios = elements.filter((e) => e.op === undefined && e.start > element.start && e.start < element.end);
|
|
29
|
-
entries.push({ element, section, scenarios });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
const ids = new Set(entries.flatMap((entry) => [entry.element.id, ...entry.scenarios.map((s) => s.id)]));
|
|
33
|
-
return { entries, capabilities, ids };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Scenario elements that sit under no requirement operation: always a format error. */
|
|
37
|
-
export function orphanDeltaElements(doc: MarkdownDoc): Element[] {
|
|
38
|
-
const orphans: Element[] = [];
|
|
39
|
-
for (const title of ["Spec delta", "Architecture delta"]) {
|
|
40
|
-
const range = findSection(doc, title);
|
|
41
|
-
if (range === null) continue;
|
|
42
|
-
const elements = headingElements(doc, range.start, range.end);
|
|
43
|
-
const ops = elements.filter((e) => e.op !== undefined);
|
|
44
|
-
for (const element of elements.filter((e) => e.op === undefined)) {
|
|
45
|
-
if (!ops.some((op) => element.start > op.start && element.start < op.end)) orphans.push(element);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return orphans;
|
|
49
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { elementLines, factsOf, statusOf, type DeltaOp, type Element, type Status } from "../elements.ts";
|
|
2
|
-
import { findIds, kindOf, scopeOf, type Kind } from "../ids.ts";
|
|
3
|
-
import type { MarkdownDoc } from "../markdown.ts";
|
|
4
|
-
import { readDelta } from "./deltas.ts";
|
|
5
|
-
import { PATHS } from "./layout.ts";
|
|
6
|
-
import type { ProjectView } from "./project_view.ts";
|
|
7
|
-
import type { ShownElement } from "./scope_view.ts";
|
|
8
|
-
import type { WorkItem } from "./work_items.ts";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Elements one at a time, and an index of all of them, so a step that needs a few elements of a
|
|
12
|
-
* large architecture fetches those instead of reading the whole file. One project's architecture
|
|
13
|
-
* held 52k words, and a single architect run read all of it to find what it would extend.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export interface IndexEntry {
|
|
17
|
-
id: string;
|
|
18
|
-
title: string;
|
|
19
|
-
kind: Kind;
|
|
20
|
-
/** The living document that holds it, or the work item that adds it. */
|
|
21
|
-
source: string;
|
|
22
|
-
status?: Status;
|
|
23
|
-
/** Where its code lives: the `Module` it names, or a module's `Path`. */
|
|
24
|
-
place?: string;
|
|
25
|
-
/** For a requirement, how many scenarios it holds. */
|
|
26
|
-
scenarios?: number;
|
|
27
|
-
/** Work in flight whose delta changes or removes it. */
|
|
28
|
-
changedBy: string[];
|
|
29
|
-
/** Set when only in-flight work defines it so far. */
|
|
30
|
-
addedBy?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ElementIndex {
|
|
34
|
-
/** The words in the living architecture and specs, so a step can judge whether to read them whole. */
|
|
35
|
-
words: { architecture: number; specs: number };
|
|
36
|
-
entries: IndexEntry[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const DOCS_INDEXED = (path: string): boolean => path === PATHS.architecture || path.startsWith(`${PATHS.specsDir}/`);
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Every element of the living architecture and specs, one entry each, with what in-flight work
|
|
43
|
-
* adds. Without a scope, scenarios are counted under their requirement; with a context or
|
|
44
|
-
* capability name, only its elements are listed, scenarios included.
|
|
45
|
-
*/
|
|
46
|
-
export function elementIndex(view: ProjectView, scope?: string): ElementIndex {
|
|
47
|
-
const inScope = (id: string): boolean => scope === undefined || scopeOf(id) === scope || id === `CTX-${scope}`;
|
|
48
|
-
const entries: IndexEntry[] = [];
|
|
49
|
-
const byId = new Map<string, IndexEntry>();
|
|
50
|
-
for (const living of view.living.elements.values()) {
|
|
51
|
-
if (!DOCS_INDEXED(living.path) || !inScope(living.id) || (scope === undefined && kindOf(living.id) === "SCN")) continue;
|
|
52
|
-
const entry = entryOf(living.id, living.element, living.doc, living.path);
|
|
53
|
-
if (kindOf(living.id) === "REQ") entry.scenarios = scenarioCount(view, living.id);
|
|
54
|
-
entries.push(entry);
|
|
55
|
-
byId.set(entry.id, entry);
|
|
56
|
-
}
|
|
57
|
-
for (const item of inFlight(view)) {
|
|
58
|
-
for (const element of deltaElements(item)) {
|
|
59
|
-
if (!inScope(element.element.id) || (scope === undefined && element.element.kind === "SCN")) continue;
|
|
60
|
-
const known = byId.get(element.element.id);
|
|
61
|
-
if (known !== undefined) {
|
|
62
|
-
known.changedBy.push(item.ref);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const added = { ...entryOf(element.element.id, element.element, item.doc, item.ref), addedBy: item.ref };
|
|
66
|
-
entries.push(added);
|
|
67
|
-
byId.set(added.id, added);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return { words: livingWords(view), entries };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** The bodies of the named elements: the living one, then each in-flight delta that changes it. */
|
|
74
|
-
export function lookupElements(view: ProjectView, ids: string[]): { elements: ShownElement[]; missing: string[] } {
|
|
75
|
-
const elements: ShownElement[] = [];
|
|
76
|
-
const missing: string[] = [];
|
|
77
|
-
for (const id of ids) {
|
|
78
|
-
const found: ShownElement[] = [];
|
|
79
|
-
const living = view.living.elements.get(id);
|
|
80
|
-
if (living !== undefined) found.push(shown(living.doc, living.element, living.path));
|
|
81
|
-
for (const item of inFlight(view)) {
|
|
82
|
-
const delta = deltaElements(item).find((d) => d.element.id === id);
|
|
83
|
-
if (delta !== undefined) found.push({ ...shown(item.doc, delta.element, item.path), ...(delta.op === undefined ? {} : { op: delta.op }) });
|
|
84
|
-
}
|
|
85
|
-
if (found.length === 0) missing.push(id);
|
|
86
|
-
elements.push(...found);
|
|
87
|
-
}
|
|
88
|
-
return { elements, missing };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function entryOf(id: string, element: Element, doc: MarkdownDoc, source: string): IndexEntry {
|
|
92
|
-
const facts = factsOf(doc, element);
|
|
93
|
-
const place = (facts.get("Module") ?? []).flatMap(findIds)[0] ?? (facts.get("Path") ?? [])[0];
|
|
94
|
-
const status = statusOf(doc, element);
|
|
95
|
-
return { id, title: element.title, kind: kindOf(id), source, changedBy: [], ...(status === undefined ? {} : { status }), ...(place === undefined ? {} : { place }) };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function shown(doc: MarkdownDoc, element: Element, path: string): ShownElement {
|
|
99
|
-
return { id: element.id, title: element.title, source: `${path}:${element.start + 1}`, body: elementLines(doc, element) };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function inFlight(view: ProjectView): WorkItem[] {
|
|
103
|
-
return view.items.filter((item) => !item.archived && item.kind !== "change");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Each element a work item's delta defines or changes, scenarios under a requirement included. */
|
|
107
|
-
function deltaElements(item: WorkItem): Array<{ element: Element; op?: DeltaOp }> {
|
|
108
|
-
return readDelta(item.doc).entries.flatMap((entry) => [
|
|
109
|
-
{ element: entry.element, ...(entry.element.op === undefined ? {} : { op: entry.element.op }) },
|
|
110
|
-
...entry.scenarios.map((scenario) => ({ element: scenario })),
|
|
111
|
-
]);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function scenarioCount(view: ProjectView, requirement: string): number {
|
|
115
|
-
const living = view.living.elements.get(requirement);
|
|
116
|
-
if (living === undefined) return 0;
|
|
117
|
-
return [...view.living.elements.values()].filter((e) => e.path === living.path && e.id.startsWith("SCN-") && e.element.start > living.element.start && e.element.start < living.element.end).length;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function livingWords(view: ProjectView): { architecture: number; specs: number } {
|
|
121
|
-
const words = (path: string): number => (view.snapshot.get(path) ?? "").split(/\s+/).filter((w) => w !== "").length;
|
|
122
|
-
const specs = [...view.living.docs.keys()].filter((path) => path.startsWith(`${PATHS.specsDir}/`));
|
|
123
|
-
return { architecture: words(PATHS.architecture), specs: specs.reduce((sum, path) => sum + words(path), 0) };
|
|
124
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { headingElements, rowElements, type Element } from "../elements.ts";
|
|
2
|
-
import { VISION_KINDS } from "../ids.ts";
|
|
3
|
-
import { parseMarkdown, type MarkdownDoc } from "../markdown.ts";
|
|
4
|
-
import { PATHS, childFiles, type Snapshot } from "./layout.ts";
|
|
5
|
-
|
|
6
|
-
/** An element of the living docs, with the document it lives in. */
|
|
7
|
-
export interface LivingElement {
|
|
8
|
-
id: string;
|
|
9
|
-
path: string;
|
|
10
|
-
doc: MarkdownDoc;
|
|
11
|
-
element: Element;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** The living docs: vision, architecture, specs and ADRs, indexed by element ID. */
|
|
15
|
-
export interface LivingModel {
|
|
16
|
-
docs: Map<string, MarkdownDoc>;
|
|
17
|
-
elements: Map<string, LivingElement>;
|
|
18
|
-
/** IDs defined more than once, with every place they appear. */
|
|
19
|
-
duplicates: Array<{ id: string; places: string[] }>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function livingDocPaths(snapshot: Snapshot): string[] {
|
|
23
|
-
const fixed = [PATHS.vision, PATHS.architecture].filter((path) => snapshot.has(path));
|
|
24
|
-
return [...fixed, ...childFiles(snapshot, PATHS.specsDir, ".md"), ...childFiles(snapshot, PATHS.adrDir, ".md")];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function buildLivingModel(snapshot: Snapshot): LivingModel {
|
|
28
|
-
const docs = new Map<string, MarkdownDoc>();
|
|
29
|
-
const elements = new Map<string, LivingElement>();
|
|
30
|
-
const places = new Map<string, string[]>();
|
|
31
|
-
|
|
32
|
-
for (const path of livingDocPaths(snapshot)) {
|
|
33
|
-
const doc = parseMarkdown(snapshot.get(path) ?? "");
|
|
34
|
-
docs.set(path, doc);
|
|
35
|
-
for (const element of elementsOf(path, doc)) {
|
|
36
|
-
places.set(element.id, [...(places.get(element.id) ?? []), `${path}:${element.start + 1}`]);
|
|
37
|
-
if (!elements.has(element.id)) elements.set(element.id, { id: element.id, path, doc, element });
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
const duplicates = [...places].filter(([, at]) => at.length > 1).map(([id, at]) => ({ id, places: at }));
|
|
41
|
-
return { docs, elements, duplicates };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The ID an ADR file defines: `ADR-<file stem>`. */
|
|
45
|
-
export function adrId(path: string): string {
|
|
46
|
-
return `ADR-${path.slice(path.lastIndexOf("/") + 1, -".md".length)}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function elementsOf(path: string, doc: MarkdownDoc): Element[] {
|
|
50
|
-
if (path.startsWith(`${PATHS.adrDir}/`)) {
|
|
51
|
-
return [{ id: adrId(path), kind: "ADR", title: doc.headings[0]?.text ?? "", form: "heading", level: 1, start: 0, end: doc.lines.length }];
|
|
52
|
-
}
|
|
53
|
-
if (path === PATHS.vision) return rowElements(doc).filter((element) => VISION_KINDS.includes(element.kind));
|
|
54
|
-
return headingElements(doc);
|
|
55
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { CruzeError } from "../cruze_error.ts";
|
|
2
|
-
import { readChangesTable } from "./plan_parts.ts";
|
|
3
|
-
import type { ProjectView } from "./project_view.ts";
|
|
4
|
-
import type { WorkItem } from "./work_items.ts";
|
|
5
|
-
|
|
6
|
-
const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
-
|
|
8
|
-
export function requireSlug(slug: string): void {
|
|
9
|
-
if (!SLUG.test(slug)) throw new CruzeError("invalid-slug", `"${slug}" must be lowercase words joined by single hyphens`);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A new feature or standalone change ID: the date plus the slug, with `-2`, `-3`… added
|
|
14
|
-
* on a clash with any live or archived work. IDs are never reused.
|
|
15
|
-
*/
|
|
16
|
-
export function allocateId(view: ProjectView, date: string, slug: string): string {
|
|
17
|
-
requireSlug(slug);
|
|
18
|
-
const taken = new Set(view.items.filter((item) => item.kind !== "change").map((item) => item.folderName));
|
|
19
|
-
const base = `${date}-${slug}`;
|
|
20
|
-
if (!taken.has(base)) return base;
|
|
21
|
-
for (let n = 2; ; n++) if (!taken.has(`${base}-${n}`)) return `${base}-${n}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The change folder name for a feature's change: the row of the feature's Changes table it
|
|
26
|
-
* names, by full name (`01-local-serial`) or slug (`local-serial`).
|
|
27
|
-
*/
|
|
28
|
-
export function changeRow(view: ProjectView, feature: WorkItem, name: string): ReturnType<typeof readChangesTable>[number] {
|
|
29
|
-
const rows = readChangesTable(feature.doc);
|
|
30
|
-
const row = rows.find((r) => r.change === name) ?? rows.find((r) => r.change.replace(/^\d\d-/, "") === name);
|
|
31
|
-
if (row === undefined) {
|
|
32
|
-
throw new CruzeError("change-not-listed", `${feature.ref} has no change "${name}" in its Changes table; add the row first`);
|
|
33
|
-
}
|
|
34
|
-
if (view.items.some((item) => item.featureRef === feature.ref && item.folderName === row.change)) {
|
|
35
|
-
throw new CruzeError("exists", `${feature.ref}/${row.change} already exists`);
|
|
36
|
-
}
|
|
37
|
-
return row;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Fills the template's frontmatter, title and scope lines for a new document. */
|
|
41
|
-
export function fillTemplate(template: string, values: { roadmap?: string; delivers?: string[]; builds?: string[] }): string {
|
|
42
|
-
let text = template;
|
|
43
|
-
if (values.roadmap !== undefined) text = text.replace(/^roadmap: .*$/m, `roadmap: ${values.roadmap}`);
|
|
44
|
-
else text = text.replace(/^roadmap: <.*\n/m, "");
|
|
45
|
-
if (values.delivers !== undefined) text = text.replace(/^- Delivers: .*$/m, `- Delivers: ${values.delivers.join(", ")}`);
|
|
46
|
-
if (values.builds !== undefined) text = text.replace(/^- Builds: .*$/m, `- Builds: ${values.builds.join(", ")}`);
|
|
47
|
-
return text;
|
|
48
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { findSection, parseMarkdown } from "../markdown.ts";
|
|
2
|
-
import { tablesIn } from "../markdown_tables.ts";
|
|
3
|
-
import { PATHS } from "./layout.ts";
|
|
4
|
-
import type { Snapshot } from "./layout.ts";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Notes under `.cruze/notes/`: what the user brought, what explore captured, the evidence research
|
|
8
|
-
* gathered, and the hand-off envision wrote from them. Each says what it is about, so a step reads
|
|
9
|
-
* the notes for its work rather than guessing from file names: one project held 21 notes, and a
|
|
10
|
-
* 9,500-word seed whose design sections no step was told to read.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export const NOTE_KINDS = ["input", "intake", "explore", "research"] as const;
|
|
14
|
-
export type NoteKind = (typeof NOTE_KINDS)[number];
|
|
15
|
-
|
|
16
|
-
/** The step an intake row hands a part to, besides a feature on the map. */
|
|
17
|
-
export const HANDOFF_STEPS = ["vision", "architect", "roadmap", "user"] as const;
|
|
18
|
-
|
|
19
|
-
export interface HandoffRow {
|
|
20
|
-
/** The part of the input, by its heading or a short quote. */
|
|
21
|
-
part: string;
|
|
22
|
-
/** Where it is: a note path and heading, or "below" for text saved in the intake note. */
|
|
23
|
-
source: string;
|
|
24
|
-
/** A step from HANDOFF_STEPS, or the slug of the feature that takes it up. */
|
|
25
|
-
goesTo: string;
|
|
26
|
-
/** `open` until the step that takes it up settles it with the user: `settled` or `dropped`. */
|
|
27
|
-
status: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface Note {
|
|
31
|
-
path: string;
|
|
32
|
-
title: string;
|
|
33
|
-
kind?: string;
|
|
34
|
-
/** `project`, feature slugs, or context and capability names. */
|
|
35
|
-
about: string[];
|
|
36
|
-
handoff: HandoffRow[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function readNotes(snapshot: Snapshot): Note[] {
|
|
40
|
-
return [...snapshot.keys()]
|
|
41
|
-
.filter((path) => path.startsWith(`${PATHS.notesDir}/`) && path.endsWith(".md"))
|
|
42
|
-
.sort()
|
|
43
|
-
.map((path) => readNote(path, snapshot.get(path) ?? ""));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function readNote(path: string, text: string): Note {
|
|
47
|
-
const doc = parseMarkdown(text);
|
|
48
|
-
const title = doc.headings.find((h) => h.level === 1)?.text ?? path.slice(path.lastIndexOf("/") + 1, -".md".length);
|
|
49
|
-
const firstSection = doc.headings.find((h) => h.level === 2)?.line ?? doc.lines.length;
|
|
50
|
-
const fact = (key: string): string | undefined =>
|
|
51
|
-
doc.lines.slice(0, firstSection).map((line) => new RegExp(`^- ${key}: (.+)$`).exec(line)?.[1]?.trim()).find((v) => v !== undefined);
|
|
52
|
-
const about = (fact("About") ?? "").split(",").map((a) => a.trim()).filter((a) => a !== "");
|
|
53
|
-
const kind = fact("Kind");
|
|
54
|
-
const section = findSection(doc, "Hand-off");
|
|
55
|
-
const rows = section === null ? [] : (tablesIn(doc.lines, doc.inFence, section.start, section.end)[0]?.rows ?? []);
|
|
56
|
-
const handoff = rows.map(({ cells }) => ({ part: cells[0] ?? "", source: cells[1] ?? "", goesTo: (cells[2] ?? "").replace(/`/g, ""), status: cells[3] ?? "open" }));
|
|
57
|
-
return { path, title, about, handoff, ...(kind === undefined ? {} : { kind }) };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface NotesFor {
|
|
61
|
-
/** Notes about one of the targets, or about the whole project when it is a target. */
|
|
62
|
-
notes: Note[];
|
|
63
|
-
/** Intake rows handed to one of the targets, with the intake note that holds them. */
|
|
64
|
-
handoffs: Array<HandoffRow & { note: string }>;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The notes and hand-off rows for a piece of work, named by its targets: `project`, a step, a feature slug or a context. */
|
|
68
|
-
export function notesFor(notes: Note[], targets: string[]): NotesFor {
|
|
69
|
-
const wanted = new Set(targets);
|
|
70
|
-
return {
|
|
71
|
-
notes: notes.filter((note) => note.about.some((a) => wanted.has(a))),
|
|
72
|
-
handoffs: notes.flatMap((note) => note.handoff.filter((row) => wanted.has(row.goesTo)).map((row) => ({ ...row, note: note.path }))),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { findIds } from "../ids.ts";
|
|
2
|
-
import { findSection, type MarkdownDoc } from "../markdown.ts";
|
|
3
|
-
import { tablesIn } from "../markdown_tables.ts";
|
|
4
|
-
|
|
5
|
-
/** The parts of feature.md and change.md that describe what gets built and how it is proved. */
|
|
6
|
-
|
|
7
|
-
export interface Scope {
|
|
8
|
-
delivers: string[];
|
|
9
|
-
builds: string[];
|
|
10
|
-
removes: string[];
|
|
11
|
-
line: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface Task {
|
|
15
|
-
number: string;
|
|
16
|
-
owner: string;
|
|
17
|
-
paths: string[];
|
|
18
|
-
proves: string[];
|
|
19
|
-
line: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface TestPlanRow {
|
|
23
|
-
subject: string;
|
|
24
|
-
seam: string;
|
|
25
|
-
file: string;
|
|
26
|
-
kind: string;
|
|
27
|
-
line: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ChangeRow {
|
|
31
|
-
change: string;
|
|
32
|
-
delivers: string[];
|
|
33
|
-
builds: string[];
|
|
34
|
-
removes: string[];
|
|
35
|
-
dependsOn: string[];
|
|
36
|
-
line: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const TASK = /^- (T\d+): `([^`]+)`[^`]*? in ((?:`[^`]+`(?:, )?)+), proves (.+)$/;
|
|
40
|
-
|
|
41
|
-
export function readScope(doc: MarkdownDoc): Scope | null {
|
|
42
|
-
const section = findSection(doc, "Scope");
|
|
43
|
-
if (section === null) return null;
|
|
44
|
-
const scope: Scope = { delivers: [], builds: [], removes: [], line: section.heading.line };
|
|
45
|
-
for (let i = section.start; i < section.end; i++) {
|
|
46
|
-
const match = /^- (Delivers|Builds|Removes): (.*)$/.exec(doc.lines[i] ?? "");
|
|
47
|
-
if (match?.[1] === undefined || match[2] === undefined) continue;
|
|
48
|
-
const key = match[1].toLowerCase() as "delivers" | "builds" | "removes";
|
|
49
|
-
scope[key] = findIds(match[2]);
|
|
50
|
-
}
|
|
51
|
-
return scope;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Task lines, and the lines under `## Tasks` that don't parse as tasks. */
|
|
55
|
-
export function readTasks(doc: MarkdownDoc): { tasks: Task[]; malformed: number[] } {
|
|
56
|
-
const section = findSection(doc, "Tasks");
|
|
57
|
-
const tasks: Task[] = [];
|
|
58
|
-
const malformed: number[] = [];
|
|
59
|
-
if (section === null) return { tasks, malformed };
|
|
60
|
-
for (let i = section.start; i < section.end; i++) {
|
|
61
|
-
const line = doc.lines[i] ?? "";
|
|
62
|
-
if (!line.startsWith("- ")) continue;
|
|
63
|
-
const match = TASK.exec(line);
|
|
64
|
-
if (match?.[1] === undefined || match[2] === undefined || match[3] === undefined || match[4] === undefined) {
|
|
65
|
-
malformed.push(i);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const paths = [...match[3].matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "");
|
|
69
|
-
tasks.push({ number: match[1], owner: match[2], paths, proves: findIds(match[4]), line: i });
|
|
70
|
-
}
|
|
71
|
-
return { tasks, malformed };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Whether a change has been planned. A standalone change is designed first, with an
|
|
76
|
-
* empty test plan and no tasks; plan fills both.
|
|
77
|
-
*/
|
|
78
|
-
export function isPlanned(doc: MarkdownDoc): boolean {
|
|
79
|
-
const { tasks, malformed } = readTasks(doc);
|
|
80
|
-
return readTestPlan(doc).length > 0 || tasks.length > 0 || malformed.length > 0;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function readTestPlan(doc: MarkdownDoc): TestPlanRow[] {
|
|
84
|
-
const section = findSection(doc, "Test plan");
|
|
85
|
-
if (section === null) return [];
|
|
86
|
-
const table = tablesIn(doc.lines, doc.inFence, section.start, section.end)[0];
|
|
87
|
-
return (table?.rows ?? []).map(({ cells, line }) => ({
|
|
88
|
-
subject: cells[0] ?? "",
|
|
89
|
-
seam: cells[1] ?? "",
|
|
90
|
-
file: (cells[2] ?? "").replace(/`/g, ""),
|
|
91
|
-
kind: cells[3] ?? "",
|
|
92
|
-
line,
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function readChangesTable(doc: MarkdownDoc): ChangeRow[] {
|
|
97
|
-
const section = findSection(doc, "Changes");
|
|
98
|
-
if (section === null) return [];
|
|
99
|
-
const table = tablesIn(doc.lines, doc.inFence, section.start, section.end)[0];
|
|
100
|
-
if (table === undefined) return [];
|
|
101
|
-
const column = (name: string): number => table.header.findIndex((cell) => cell.toLowerCase() === name);
|
|
102
|
-
const cell = (cells: string[], name: string): string => cells[column(name)] ?? "";
|
|
103
|
-
return table.rows.map(({ cells, line }) => ({
|
|
104
|
-
change: cells[0] ?? "",
|
|
105
|
-
delivers: findIds(cell(cells, "delivers")),
|
|
106
|
-
builds: findIds(cell(cells, "builds")),
|
|
107
|
-
removes: findIds(cell(cells, "removes")),
|
|
108
|
-
dependsOn: cell(cells, "depends on").split(",").map((s) => s.trim()).filter((s) => s !== ""),
|
|
109
|
-
line,
|
|
110
|
-
}));
|
|
111
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { MANAGED_CLOSE, MANAGED_OPEN, type MarkdownDoc } from "../markdown.ts";
|
|
2
|
-
|
|
3
|
-
/** The managed `## Progress` block of a feature or change. */
|
|
4
|
-
export interface Progress {
|
|
5
|
-
branch?: string;
|
|
6
|
-
/** Task number to the commit that completed it, or null while open. */
|
|
7
|
-
tasks: Map<string, string | null>;
|
|
8
|
-
deviations: string[];
|
|
9
|
-
landed?: string;
|
|
10
|
-
/** For a feature: each change's state line. */
|
|
11
|
-
changes: Map<string, string>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function readProgress(doc: MarkdownDoc): Progress {
|
|
15
|
-
const progress: Progress = { tasks: new Map(), deviations: [], changes: new Map() };
|
|
16
|
-
const range = progressRange(doc);
|
|
17
|
-
if (range === null) return progress;
|
|
18
|
-
for (let i = range.start; i < range.end; i++) {
|
|
19
|
-
const line = doc.lines[i] ?? "";
|
|
20
|
-
let match = /^- Branch: (.+)$/.exec(line);
|
|
21
|
-
if (match?.[1] !== undefined) progress.branch = match[1].trim();
|
|
22
|
-
match = /^- \[([ x])\] (T\d+)(?: \(([0-9a-f]+)\))?/.exec(line);
|
|
23
|
-
if (match?.[2] !== undefined) progress.tasks.set(match[2], match[1] === "x" ? (match[3] ?? "") : null);
|
|
24
|
-
match = /^- Deviation (.+)$/.exec(line);
|
|
25
|
-
if (match?.[1] !== undefined) progress.deviations.push(match[1]);
|
|
26
|
-
match = /^- Landed: (.+)$/.exec(line);
|
|
27
|
-
if (match?.[1] !== undefined) progress.landed = match[1];
|
|
28
|
-
match = /^- (\d\d-[a-z0-9-]+): (.+)$/.exec(line);
|
|
29
|
-
if (match?.[1] !== undefined && match[2] !== undefined) progress.changes.set(match[1], match[2]);
|
|
30
|
-
}
|
|
31
|
-
return progress;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Lines [start, end) inside the managed block that holds `## Progress`, or null. */
|
|
35
|
-
export function progressRange(doc: MarkdownDoc): { open: number; start: number; end: number } | null {
|
|
36
|
-
const heading = doc.headings.find((h) => h.level === 2 && h.text === "Progress");
|
|
37
|
-
if (heading === undefined || !doc.inManaged[heading.line]) return null;
|
|
38
|
-
let open = heading.line;
|
|
39
|
-
while (open > 0 && doc.lines[open]?.trim() !== MANAGED_OPEN) open--;
|
|
40
|
-
let end = heading.line;
|
|
41
|
-
while (end < doc.lines.length && doc.lines[end]?.trim() !== MANAGED_CLOSE) end++;
|
|
42
|
-
return { open, start: heading.line + 1, end };
|
|
43
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { parseApprovals, type ApprovalsFile } from "../approvals/records.ts";
|
|
2
|
-
import { parseConfig, type ConfigResult } from "../config.ts";
|
|
3
|
-
import { journalPath, parseJournal, type JournalEntry } from "../journal.ts";
|
|
4
|
-
import { PATHS, type Snapshot } from "./layout.ts";
|
|
5
|
-
import { buildLivingModel, type LivingModel } from "./living_model.ts";
|
|
6
|
-
import { readDelta } from "./deltas.ts";
|
|
7
|
-
import { listWorkItems, type WorkItem } from "./work_items.ts";
|
|
8
|
-
|
|
9
|
-
/** Everything the rules need, loaded once from a snapshot. */
|
|
10
|
-
export interface ProjectView {
|
|
11
|
-
snapshot: Snapshot;
|
|
12
|
-
living: LivingModel;
|
|
13
|
-
items: WorkItem[];
|
|
14
|
-
config: ConfigResult | null;
|
|
15
|
-
/** Approvals by the folder (or `.cruze` for project documents) that holds them. */
|
|
16
|
-
approvals: Map<string, ApprovalsFile>;
|
|
17
|
-
/** Problems reading approvals files, by path. */
|
|
18
|
-
approvalProblems: Map<string, string>;
|
|
19
|
-
/** IDs retired by archived work: never to be reused. */
|
|
20
|
-
retired: Set<string>;
|
|
21
|
-
/** Every journal entry in the project, oldest first. */
|
|
22
|
-
journal: JournalEntry[];
|
|
23
|
-
/** Journal lines that are not valid entries, by path. */
|
|
24
|
-
journalProblems: Map<string, number[]>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function buildProjectView(snapshot: Snapshot): ProjectView {
|
|
28
|
-
const items = listWorkItems(snapshot);
|
|
29
|
-
const approvals = new Map<string, ApprovalsFile>();
|
|
30
|
-
const approvalProblems = new Map<string, string>();
|
|
31
|
-
for (const folder of [".cruze", ...items.map((item) => item.folder)]) {
|
|
32
|
-
const path = approvalsPath(folder);
|
|
33
|
-
const { file, problem } = parseApprovals(snapshot.get(path));
|
|
34
|
-
approvals.set(folder, file);
|
|
35
|
-
if (problem !== undefined) approvalProblems.set(path, problem);
|
|
36
|
-
}
|
|
37
|
-
const retired = new Set(
|
|
38
|
-
items
|
|
39
|
-
.filter((item) => item.archived)
|
|
40
|
-
.flatMap((item) => readDelta(item.doc).entries.filter((e) => e.element.op === "REMOVED").map((e) => e.element.id)),
|
|
41
|
-
);
|
|
42
|
-
const journal: JournalEntry[] = [];
|
|
43
|
-
const journalProblems = new Map<string, number[]>();
|
|
44
|
-
for (const folder of [".cruze", ...items.map((item) => item.folder)]) {
|
|
45
|
-
const path = journalPath(folder);
|
|
46
|
-
const { entries, badLines } = parseJournal(snapshot.get(path));
|
|
47
|
-
journal.push(...entries);
|
|
48
|
-
if (badLines.length > 0) journalProblems.set(path, badLines);
|
|
49
|
-
}
|
|
50
|
-
journal.sort((a, b) => a.at.localeCompare(b.at));
|
|
51
|
-
const configText = snapshot.get(PATHS.config);
|
|
52
|
-
return {
|
|
53
|
-
snapshot,
|
|
54
|
-
living: buildLivingModel(snapshot),
|
|
55
|
-
items,
|
|
56
|
-
config: configText === undefined ? null : parseConfig(configText),
|
|
57
|
-
approvals,
|
|
58
|
-
approvalProblems,
|
|
59
|
-
retired,
|
|
60
|
-
journal,
|
|
61
|
-
journalProblems,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function approvalsPath(folder: string): string {
|
|
66
|
-
return `${folder}/approvals.json`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** The folder that holds an artifact's approval: the work item's folder, or `.cruze` for project documents. */
|
|
70
|
-
export function approvalFolder(view: ProjectView, artifactPath: string): string {
|
|
71
|
-
return view.items.find((item) => item.path === artifactPath)?.folder ?? ".cruze";
|
|
72
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { readRows } from "../edits/managed_tables.ts";
|
|
2
|
-
import { parseMarkdown } from "../markdown.ts";
|
|
3
|
-
import { tablesIn } from "../markdown_tables.ts";
|
|
4
|
-
|
|
5
|
-
/** The items of `docs/roadmap.md`, as the cruze-formats skill defines them. */
|
|
6
|
-
|
|
7
|
-
export interface RoadmapItem {
|
|
8
|
-
slug: string;
|
|
9
|
-
kind: string;
|
|
10
|
-
goals: string;
|
|
11
|
-
blockedBy: string[];
|
|
12
|
-
phase: number;
|
|
13
|
-
line: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface RoadmapStatusRow {
|
|
17
|
-
slug: string;
|
|
18
|
-
id: string;
|
|
19
|
-
state: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Every item in the roadmap's phases, in the order they appear. */
|
|
23
|
-
export function readRoadmapItems(text: string): RoadmapItem[] {
|
|
24
|
-
const doc = parseMarkdown(text);
|
|
25
|
-
const items: RoadmapItem[] = [];
|
|
26
|
-
for (const phase of doc.headings.filter((h) => h.level === 3 && /^Phase \d+: /.test(h.text))) {
|
|
27
|
-
const number = Number(/^Phase (\d+)/.exec(phase.text)?.[1] ?? 0);
|
|
28
|
-
const end = doc.headings.find((h) => h.line > phase.line && h.level <= 3)?.line ?? doc.lines.length;
|
|
29
|
-
for (const table of tablesIn(doc.lines, doc.inFence, phase.line + 1, end)) {
|
|
30
|
-
for (const { cells, line } of table.rows) {
|
|
31
|
-
const [slug = "", kind = "", goals = "", blocked = ""] = cells;
|
|
32
|
-
const blockedBy = blocked.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
33
|
-
items.push({ slug, kind, goals, blockedBy, phase: number, line });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return items;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** The managed `## Status` rows: each started item's folder ID and state. */
|
|
41
|
-
export function readRoadmapStatus(text: string): RoadmapStatusRow[] {
|
|
42
|
-
return readRows(text, "Status").map(([slug = "", id = "", state = ""]) => ({ slug, id, state }));
|
|
43
|
-
}
|