@printpractical/cruze 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -13
- package/dist/adapters/inbound/cli/cli_context.js +4 -0
- package/dist/adapters/inbound/cli/commands/diff.js +23 -0
- package/dist/adapters/inbound/cli/commands/documents.js +1 -1
- package/dist/adapters/inbound/cli/commands/show.js +66 -2
- package/dist/adapters/inbound/cli/commands/status.js +7 -3
- package/dist/adapters/inbound/cli/commands.js +9 -3
- package/dist/adapters/inbound/cli/parse_command.js +8 -0
- package/dist/adapters/outbound/git_repository.js +9 -0
- package/dist/app/ports/repository.js +4 -0
- package/dist/app/use_cases/approve_artifact.js +18 -10
- package/dist/app/use_cases/check_code.js +5 -3
- package/dist/app/use_cases/journal_events.js +6 -2
- package/dist/app/use_cases/new_project_doc.js +10 -2
- package/dist/app/use_cases/run_review.js +10 -2
- package/dist/app/use_cases/show_elements.js +18 -0
- package/dist/app/use_cases/show_notes.js +11 -0
- package/dist/app/use_cases/show_scope.js +15 -1
- package/dist/app/use_cases/upstream_diff.js +55 -0
- package/dist/domain/approvals/rebase.js +39 -0
- package/dist/domain/approvals/upstream_text.js +36 -0
- package/dist/domain/check/check_source.js +18 -1
- package/dist/domain/check/decision_comments.js +38 -0
- package/dist/domain/decision_citations.js +62 -0
- package/dist/domain/edits/agents_file.js +9 -0
- package/dist/domain/ids.js +3 -1
- package/dist/domain/journal.js +3 -1
- package/dist/domain/land/merge_plan.js +7 -2
- package/dist/domain/line_diff.js +56 -0
- package/dist/domain/project/element_lookup.js +124 -0
- package/dist/domain/project/living_model.js +2 -1
- package/dist/domain/project/notes.js +74 -0
- package/dist/domain/skill_budget.js +36 -0
- package/dist/domain/status/next_step.js +27 -5
- package/dist/domain/status/pending_rethink.js +22 -0
- package/dist/domain/status/work_status.js +17 -2
- package/dist/domain/validation/decision_rules.js +76 -0
- package/dist/domain/validation/delta_rules.js +2 -0
- package/dist/domain/validation/future_rules.js +40 -0
- package/dist/domain/validation/living_rules.js +2 -0
- package/dist/domain/validation/validate_project.js +4 -0
- package/dist/domain/validation/vision_rules.js +41 -0
- package/dist/domain/versions/rule_versions.js +3 -0
- package/package.json +1 -1
- package/skills/architect/SKILL.md +2 -1
- package/skills/architect/feature.md +6 -4
- package/skills/architect/project.md +2 -2
- package/skills/behavioural-testing/SKILL.md +3 -2
- package/skills/behavioural-testing/fakes.md +3 -3
- package/skills/build/SKILL.md +2 -2
- package/skills/envision/SKILL.md +3 -2
- package/skills/envision/feature.md +3 -1
- package/skills/envision/intake.md +28 -0
- package/skills/envision/possibilities.md +13 -0
- package/skills/envision/project.md +5 -3
- package/skills/explore/SKILL.md +2 -2
- package/skills/formats/SKILL.md +13 -2
- package/skills/formats/reference/architecture.md +7 -3
- package/skills/formats/reference/durable-docs.md +4 -1
- package/skills/formats/reference/work-items.md +1 -1
- package/skills/formats/templates/vision.md +12 -0
- package/skills/grilling/SKILL.md +10 -0
- package/skills/hexagonal-design/languages/rust.md +2 -1
- package/skills/hexagonal-design/runtime.md +1 -0
- package/skills/land/SKILL.md +6 -2
- package/skills/plan/SKILL.md +13 -3
- package/skills/plan/tasks.md +3 -3
- package/skills/realign/SKILL.md +5 -3
- package/skills/realign/notes/0.0.10.md +131 -0
- package/skills/realign/notes/0.0.2.md +1 -1
- package/skills/realign/notes/0.0.6.md +1 -1
- package/skills/realign/notes/0.0.8.md +1 -1
- package/skills/realign/notes/0.0.9.md +2 -2
- package/skills/realign/notes.md +1 -0
- package/skills/research/SKILL.md +3 -2
- package/skills/rethink/SKILL.md +5 -3
- package/skills/rethink/levels.md +6 -6
- package/skills/roadmap/SKILL.md +2 -1
- package/skills/roles/SKILL.md +3 -3
- package/skills/roles/auditor.md +1 -1
- package/skills/roles/code-reviewer.md +2 -2
- package/skills/roles/design-reviewer.md +4 -40
- package/skills/roles/design-rubric.md +40 -0
- package/skills/roles/plan-rubric.md +11 -0
- package/skills/roles/surveyor.md +3 -2
- package/skills/triage/SKILL.md +1 -1
- package/src/adapters/inbound/cli/cli_context.ts +4 -0
- package/src/adapters/inbound/cli/commands/diff.ts +23 -0
- package/src/adapters/inbound/cli/commands/documents.ts +1 -1
- package/src/adapters/inbound/cli/commands/show.ts +66 -2
- package/src/adapters/inbound/cli/commands/status.ts +7 -3
- package/src/adapters/inbound/cli/commands.ts +9 -3
- package/src/adapters/inbound/cli/parse_command.ts +8 -0
- package/src/adapters/outbound/git_repository.ts +9 -0
- package/src/app/ports/repository.ts +4 -0
- package/src/app/use_cases/approve_artifact.ts +18 -10
- package/src/app/use_cases/check_code.ts +5 -3
- package/src/app/use_cases/journal_events.ts +6 -2
- package/src/app/use_cases/new_project_doc.ts +10 -2
- package/src/app/use_cases/run_review.ts +10 -2
- package/src/app/use_cases/show_elements.ts +18 -0
- package/src/app/use_cases/show_notes.ts +11 -0
- package/src/app/use_cases/show_scope.ts +15 -1
- package/src/app/use_cases/upstream_diff.ts +55 -0
- package/src/domain/approvals/rebase.ts +39 -0
- package/src/domain/approvals/upstream_text.ts +36 -0
- package/src/domain/check/check_source.ts +18 -1
- package/src/domain/check/decision_comments.ts +38 -0
- package/src/domain/decision_citations.ts +62 -0
- package/src/domain/edits/agents_file.ts +9 -0
- package/src/domain/ids.ts +3 -1
- package/src/domain/journal.ts +3 -1
- package/src/domain/land/merge_plan.ts +7 -2
- package/src/domain/line_diff.ts +56 -0
- package/src/domain/project/element_lookup.ts +124 -0
- package/src/domain/project/living_model.ts +2 -1
- package/src/domain/project/notes.ts +74 -0
- package/src/domain/skill_budget.ts +36 -0
- package/src/domain/status/next_step.ts +27 -5
- package/src/domain/status/pending_rethink.ts +22 -0
- package/src/domain/status/work_status.ts +17 -2
- package/src/domain/validation/decision_rules.ts +76 -0
- package/src/domain/validation/delta_rules.ts +2 -0
- package/src/domain/validation/future_rules.ts +40 -0
- package/src/domain/validation/living_rules.ts +2 -0
- package/src/domain/validation/validate_project.ts +4 -0
- package/src/domain/validation/vision_rules.ts +41 -0
- package/src/domain/versions/rule_versions.ts +3 -0
- package/templates/init/AGENTS.md +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ It works with any agent that supports the [Agent Skills](https://agentskills.io)
|
|
|
10
10
|
|
|
11
11
|
## Status
|
|
12
12
|
|
|
13
|
-
Version 0.0.
|
|
13
|
+
Version 0.0.10 is the tenth preview. Intent now survives from the first idea to every design: the vision holds the principles every design follows and the capabilities that must stay possible, envision takes in any input and hands each part to the step that takes it up, and the design reviewer checks every design against them. Steps read less, through an element index and lookup, one rubric per review and a word budget on the skills. Early development. The CLI is complete for the V1 lifecycle. The document formats ship as the `cruze-formats` skill, the standards an agent designs and codes to ship as knowledge skills, and the whole workflow, from explore to land, ships as workflow skills. Claude Code is the agent tested so far.
|
|
14
14
|
|
|
15
15
|
## Why
|
|
16
16
|
|
|
@@ -35,7 +35,7 @@ The skills run `cruze` commands, so the agent needs `cruze` on its path. Install
|
|
|
35
35
|
From a GitHub Release, without npm. Each release attaches the packed package:
|
|
36
36
|
|
|
37
37
|
```sh
|
|
38
|
-
npm install -g https://github.com/PrintPractical/cruze/releases/download/v0.0.
|
|
38
|
+
npm install -g https://github.com/PrintPractical/cruze/releases/download/v0.0.10/printpractical-cruze-0.0.10.tgz
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
From npm, once a version is published there:
|
|
@@ -47,7 +47,7 @@ npm install -g @printpractical/cruze
|
|
|
47
47
|
From the git repository, at a tag or `#main`. This needs git and access to the repository. npm 11 needs `--install-links` for a global git install, and the first `cruze` command builds the CLI, which takes a moment:
|
|
48
48
|
|
|
49
49
|
```sh
|
|
50
|
-
npm install -g --install-links github:PrintPractical/cruze#v0.0.
|
|
50
|
+
npm install -g --install-links github:PrintPractical/cruze#v0.0.10
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
### 2. Set up a repository
|
|
@@ -61,7 +61,7 @@ cruze init
|
|
|
61
61
|
When you didn't install from npm, tell `init` where Cruze came from, so the CI it writes runs the same source instead of the npm package:
|
|
62
62
|
|
|
63
63
|
```sh
|
|
64
|
-
cruze init --package https://github.com/PrintPractical/cruze/releases/download/v0.0.
|
|
64
|
+
cruze init --package https://github.com/PrintPractical/cruze/releases/download/v0.0.10/printpractical-cruze-0.0.10.tgz
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
`init` asks for the project name, and which coding agent you use: `claude` for Claude Code, or `other` for any agent that reads `AGENTS.md` and `.agents/skills/`, such as Codex or OpenCode. It doesn't ask when you pass `--agent claude` or `--agent other`, or when the repository already has `CLAUDE.md` or `.claude/`. With `--yes`, or without a terminal, it sets up Claude Code. Then it creates:
|
|
@@ -118,25 +118,29 @@ Skills call these at fixed points, and CI runs `check` and `trace` on every push
|
|
|
118
118
|
|
|
119
119
|
| Command | What it does |
|
|
120
120
|
| --- | --- |
|
|
121
|
-
| `cruze validate` | Checks every document against the formats: IDs, elements, deltas, scope rules, task lines, test plans, roadmap and config, and warns when a change falls outside the size band in `changes:` |
|
|
121
|
+
| `cruze validate` | Checks every document against the formats: IDs, elements, deltas, scope rules, task lines, test plans, roadmap and config, and warns when a change falls outside the size band in `changes:` or a document cites a decision recorded elsewhere |
|
|
122
122
|
| `cruze approve <doc>` | Records an approval as a fingerprint: the document's design hash plus the hash of every upstream element it cites. `--replan` sets built elements a rethink changed back to planned; `--rebase` accepts living text a feature must now build on. `--by-agent` approves a change for you once its plan review has closed, and `--restamp-unchanged` re-stamps work a rethink didn't change; both record that the agent gave the approval |
|
|
123
|
-
| `cruze next` | Names the step to run next on this branch, and why, from computed status |
|
|
123
|
+
| `cruze next` | Names the step to run next on this branch, and why, from computed status: for stale work, whether a re-stamp or a rebase is due |
|
|
124
124
|
| `cruze show <ref>` | Prints the design a change builds, or a feature's delta: each element's full body, the delivered scenarios, what they cite with where to find it, the settled decisions and the item's dispositions, so a step reads its scope instead of the whole architecture |
|
|
125
|
-
| `cruze
|
|
126
|
-
| `cruze
|
|
125
|
+
| `cruze show <ID>...` | Prints the chosen elements' bodies, with each in-flight delta's version beside the living one |
|
|
126
|
+
| `cruze show --notes [<target>]` | Lists every note with its kind and what it is about, and the parts of the user's input envision handed to each step or feature; with a target, such as `architect` or a feature slug, only those for it |
|
|
127
|
+
| `cruze show --index [<context>]` | Lists every element of the architecture and specs on one line, with its status, module and the work in flight that changes it, or one context or capability in full, and gives the size of the living docs |
|
|
128
|
+
| `cruze status` | Computes each document's state (approved, edited, upstream changed or unapproved) from content, never from stored state, and counts the findings waiting on a rethink |
|
|
129
|
+
| `cruze diff <ref>` | Shows what changed upstream of a document since its approval, each element's approved text against its current text, and whether the work's delta needs a rebase |
|
|
130
|
+
| `cruze status --gate build` | Passes only when this branch's change, its feature and the architecture are approved and current, and no finding on them waits on a rethink |
|
|
127
131
|
| `cruze status --overlap` | Lists changes on other branches that touch the same elements as this branch's change |
|
|
128
|
-
| `cruze new <kind>` | Creates a project document (`vision`, `glossary`, `architecture`, `roadmap`) or work (`feature`, `change`, `adr`, `note`) from its template, with a dated ID that is never reused |
|
|
132
|
+
| `cruze new <kind>` | Creates a project document (`vision`, `glossary`, `architecture`, `roadmap`) or work (`feature`, `change`, `adr`, `note`) from its template, with a dated ID that is never reused; a note says its kind and what it is about (`--kind`, `--about`) |
|
|
129
133
|
| `cruze task done <T#>` | Ticks a task with its commit; `cruze task reopen` unticks one a rethink changed, and `cruze task deviation` records a small departure from the plan |
|
|
130
134
|
| `cruze features <add\|update\|drop>` | Edits the feature map: adds or drops a future feature, or corrects a summary |
|
|
131
135
|
| `cruze roadmap prune` | Clears landed items from the roadmap's status when a release closes |
|
|
132
136
|
| `cruze trace` | Fails when a delivered scenario has no test carrying its ID; `--all` checks every built scenario |
|
|
133
|
-
| `cruze check` | Enforces the layer rules and the file budgets; `--ci` also fails on
|
|
137
|
+
| `cruze check` | Enforces the layer rules and the file budgets, and warns on decision numbers in code comments; `--ci` also fails on warnings |
|
|
134
138
|
| `cruze review <role>` | Runs a role (design reviewer, code reviewer, verifier, researcher or auditor) in a fresh agent context, through the command in `.cruze/config.yaml`. When no command is set, or its program isn't installed, it prints the role's prompt for the agent to run in a helper of its own |
|
|
135
139
|
| `cruze land` | Merges a verified change into the living docs, re-stamps the approvals the merge would make stale, fills the Commands and Layout sections of `AGENTS.md`, and archives finished work |
|
|
136
140
|
| `cruze realign status` | Lists what the installed Cruze expects of existing code since its `standards:` version, with what the CLI rules find now; `--full` covers every release |
|
|
137
141
|
| `cruze realign done` | Moves `standards:` up once every finding is fixed, waived or scheduled in a change named with `--change` |
|
|
138
142
|
| `cruze abandon <ref>` | Archives a feature or standalone change that stops for good, with its reason, and puts a feature back on the future list |
|
|
139
|
-
| `cruze journal add <event>` | Records a rethink, review round, disposition, verification, override or
|
|
143
|
+
| `cruze journal add <event>` | Records a rethink, review round, disposition, verification, override, bug or lesson; `cruze journal list` reads the work in flight's entries, or landed work's too with `--all`; `cruze feedback export` bundles the journal for improving Cruze itself |
|
|
140
144
|
|
|
141
145
|
Stepping back is editing: change an upstream document and every approval that cites a changed element shows as stale, with the element named. Re-approving is the rewind; there are no phases to reset.
|
|
142
146
|
|
|
@@ -147,7 +151,7 @@ Stepping back is editing: change an upstream document and every approval that ci
|
|
|
147
151
|
| Skill | What it holds |
|
|
148
152
|
| --- | --- |
|
|
149
153
|
| `cruze-explore` | Optional thinking partner before any step: options and trade-offs, captured as a note only when you ask |
|
|
150
|
-
| `cruze-envision` | The what: the project's vision, goals and feature map, or one feature's intent and requirements with scenarios |
|
|
154
|
+
| `cruze-envision` | The what: the project's vision, goals, principles, what must stay possible later and feature map, asking about the capabilities the product may grow into, or one feature's intent and requirements with scenarios |
|
|
151
155
|
| `cruze-architect` | The how: the architecture, a feature's architecture delta and its changes, or a tweak; ends with a design review and a walkthrough for your approval |
|
|
152
156
|
| `cruze-roadmap` | Orders the release into phases with blocking edges and goal coverage, and closes a release |
|
|
153
157
|
| `cruze-plan` | Turns a change into a test plan and ordered tasks with file targets, reviewed in a fresh context and approved once the review closes |
|
|
@@ -170,7 +174,7 @@ Stepping back is editing: change an upstream document and every approval that ci
|
|
|
170
174
|
|
|
171
175
|
## Reporting back
|
|
172
176
|
|
|
173
|
-
Cruze improves from what goes wrong in real projects. Every rethink, review finding, bug and override is recorded in the project's journal. To send them back
|
|
177
|
+
Cruze improves from what goes wrong in real projects. Every rethink, review finding, bug and override is recorded in the project's journal, along with each repeated lesson about Cruze's own steps. To send them back, with the `## Known pitfalls` of `AGENTS.md`:
|
|
174
178
|
|
|
175
179
|
```sh
|
|
176
180
|
cruze feedback export --out cruze-feedback.json
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { upstreamDiff, } from "../../../../app/use_cases/upstream_diff.js";
|
|
2
|
+
import { requireArgs, } from "../cli_context.js";
|
|
3
|
+
|
|
4
|
+
export async function runDiff(context , args ) {
|
|
5
|
+
requireArgs(args, ["ref"]);
|
|
6
|
+
const report = await upstreamDiff(context, args[0] ?? "");
|
|
7
|
+
return { json: report, human: render(report) };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function render(report ) {
|
|
11
|
+
const lines = [`${report.artifact}: ${report.state}`];
|
|
12
|
+
if (report.changes.length === 0) lines.push("Nothing it cites changed since its approval.");
|
|
13
|
+
for (const change of report.changes) {
|
|
14
|
+
lines.push("", `${change.key}${change.path === undefined ? "" : ` in ${change.path}`}:`);
|
|
15
|
+
lines.push(...(change.diff ?? [change.note ?? ""]).map((line) => ` ${line}`));
|
|
16
|
+
}
|
|
17
|
+
if (report.rebase.length > 0) {
|
|
18
|
+
lines.push("", `Its delta changes ${report.rebase.join(", ")}: rewrite the delta to keep the new text, then approve with ${report.rebase.map((id) => `--rebase ${id}`).join(" ")}.`);
|
|
19
|
+
} else if (report.ref !== undefined && report.state === "upstream-changed") {
|
|
20
|
+
lines.push("", `No rebase needed. When the work still holds against this, re-stamp it with cruze approve ${report.ref} --restamp-unchanged.`);
|
|
21
|
+
}
|
|
22
|
+
return lines.join("\n");
|
|
23
|
+
}
|
|
@@ -34,7 +34,7 @@ export async function runNew(context , args , options
|
|
|
34
34
|
}
|
|
35
35
|
requireArgs(args, ["kind", "slug"]);
|
|
36
36
|
if (args[0] === "note") {
|
|
37
|
-
const report = await createNote(context, args[1] ?? "", requireOption(options.title, "--title"));
|
|
37
|
+
const report = await createNote(context, args[1] ?? "", requireOption(options.title, "--title"), requireOption(options.kind, "--kind"), requireOption(options.about, "--about"));
|
|
38
38
|
return { json: report, human: `Created ${report.path}` };
|
|
39
39
|
}
|
|
40
40
|
const kind = args[0] ;
|
|
@@ -1,13 +1,54 @@
|
|
|
1
|
+
import { showElements, showIndex } from "../../../../app/use_cases/show_elements.js";
|
|
2
|
+
import { showNotes } from "../../../../app/use_cases/show_notes.js";
|
|
3
|
+
|
|
1
4
|
import { showScope, } from "../../../../app/use_cases/show_scope.js";
|
|
5
|
+
import { isId } from "../../../../domain/ids.js";
|
|
6
|
+
|
|
2
7
|
|
|
3
|
-
import { requireArgs,
|
|
8
|
+
import { UsageError, requireArgs, } from "../cli_context.js";
|
|
4
9
|
|
|
5
|
-
export async function runShow(context , args ) {
|
|
10
|
+
export async function runShow(context , args , options ) {
|
|
11
|
+
if (options.notes) {
|
|
12
|
+
if (args.length > 1 || options.index) throw new UsageError("--notes takes at most one target: project, a step, a feature slug or a context");
|
|
13
|
+
const report = await showNotes(context, args[0]);
|
|
14
|
+
return { json: report, human: "target" in report ? renderNotesFor(report).join("\n").trimEnd() : renderNotes(report.notes) };
|
|
15
|
+
}
|
|
16
|
+
if (options.index) {
|
|
17
|
+
if (args.length > 1) throw new UsageError("--index takes at most one context or capability");
|
|
18
|
+
const index = await showIndex(context, args[0]);
|
|
19
|
+
return { json: index, human: renderIndex(index) };
|
|
20
|
+
}
|
|
6
21
|
requireArgs(args, ["ref"]);
|
|
22
|
+
if (args.every(isId)) {
|
|
23
|
+
const report = await showElements(context, args);
|
|
24
|
+
return { json: report, human: blocks(report.elements).join("\n").trimEnd() };
|
|
25
|
+
}
|
|
26
|
+
if (args.length > 1) throw new UsageError("show takes one feature or change, or one or more element IDs");
|
|
7
27
|
const report = await showScope(context, args[0] ?? "");
|
|
8
28
|
return { json: report, human: render(report) };
|
|
9
29
|
}
|
|
10
30
|
|
|
31
|
+
/** The index as a list an agent scans for what exists, before fetching bodies with `cruze show <ID>`. */
|
|
32
|
+
function renderIndex(index ) {
|
|
33
|
+
const { architecture, specs } = index.words;
|
|
34
|
+
const lines = [`# Elements`, "", `docs/architecture.md holds ${architecture} words, and docs/specs/ ${specs}. Read an element's body with \`cruze show <ID>\`.`];
|
|
35
|
+
let source = "";
|
|
36
|
+
for (const entry of [...index.entries].sort((a, b) => Number(a.addedBy !== undefined) - Number(b.addedBy !== undefined))) {
|
|
37
|
+
const heading = entry.addedBy === undefined ? entry.source : "In flight, not yet merged";
|
|
38
|
+
if (heading !== source) lines.push("", `## ${heading}`, "");
|
|
39
|
+
source = heading;
|
|
40
|
+
const facts = [
|
|
41
|
+
entry.status,
|
|
42
|
+
entry.place,
|
|
43
|
+
entry.scenarios === undefined ? undefined : `${entry.scenarios} scenarios`,
|
|
44
|
+
entry.addedBy === undefined ? undefined : `added by ${entry.addedBy}`,
|
|
45
|
+
entry.changedBy.length === 0 ? undefined : `changed by ${entry.changedBy.join(", ")}`,
|
|
46
|
+
].filter((f) => f !== undefined);
|
|
47
|
+
lines.push(`- ${entry.id}: ${entry.title}${facts.length === 0 ? "" : ` (${facts.join("; ")})`}`);
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
11
52
|
/** The scope as a document an agent reads in place of the living docs. */
|
|
12
53
|
function render(report ) {
|
|
13
54
|
const lines = [`# ${report.ref}${report.feature === undefined ? "" : ` (feature ${report.feature})`}`, ""];
|
|
@@ -22,6 +63,7 @@ function render(report ) {
|
|
|
22
63
|
lines.push("## Dispositions", "");
|
|
23
64
|
lines.push(...(report.dispositions.length === 0 ? ["None."] : report.dispositions.map((d) => `- [${d.disposition}, ${d.review}] ${d.finding}: ${d.reason}`)), "");
|
|
24
65
|
if (report.references.length > 0) lines.push("## References", "", ...report.references.map((r) => `- \`${r.path}\`: ${r.use}`), "");
|
|
66
|
+
lines.push(...renderNotesFor(report.notes));
|
|
25
67
|
if (report.missing.length > 0) lines.push("## Missing", "", ...report.missing.map((id) => `- ${id} is defined nowhere`), "");
|
|
26
68
|
return lines.join("\n").trimEnd();
|
|
27
69
|
}
|
|
@@ -30,3 +72,25 @@ function blocks(elements ) {
|
|
|
30
72
|
if (elements.length === 0) return ["None.", ""];
|
|
31
73
|
return elements.flatMap((element) => [`<!-- ${element.source} -->`, ...element.body.join("\n").trimEnd().split("\n"), ""]);
|
|
32
74
|
}
|
|
75
|
+
|
|
76
|
+
/** The notes about a piece of work and the parts of the user's input handed to it; nothing when there are none. */
|
|
77
|
+
function renderNotesFor(found ) {
|
|
78
|
+
if (found.notes.length === 0 && found.handoffs.length === 0) return [];
|
|
79
|
+
return [
|
|
80
|
+
"## Notes",
|
|
81
|
+
"",
|
|
82
|
+
...found.notes.map((n) => `- \`${n.path}\`: ${n.title}${n.kind === undefined ? "" : ` (${n.kind})`}`),
|
|
83
|
+
...found.handoffs.map((h) => `- ${h.part}, from ${h.source} (${h.status}, handed off in \`${h.note}\`)`),
|
|
84
|
+
"",
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderNotes(notes ) {
|
|
89
|
+
if (notes.length === 0) return "No notes.";
|
|
90
|
+
return notes
|
|
91
|
+
.flatMap((n) => [
|
|
92
|
+
`- \`${n.path}\`: ${n.title} (${n.kind ?? "no kind"}; about ${n.about.length === 0 ? "nothing named" : n.about.join(", ")})`,
|
|
93
|
+
...n.handoff.map((h) => ` - ${h.part} goes to ${h.goesTo} (${h.status})`),
|
|
94
|
+
])
|
|
95
|
+
.join("\n");
|
|
96
|
+
}
|
|
@@ -25,13 +25,13 @@ export async function runStatus(context , _args , options
|
|
|
25
25
|
const lines = [`Branch: ${report.branch ?? "(none)"}${report.active === undefined ? "" : `, building ${report.active}`}`];
|
|
26
26
|
for (const doc of report.documents) lines.push(describe(doc.artifact, doc));
|
|
27
27
|
for (const feature of report.features) {
|
|
28
|
-
lines.push(describe(`feature ${feature.ref}`, feature.approval));
|
|
28
|
+
lines.push(`${describe(`feature ${feature.ref}`, feature.approval)}${pendingNote(feature.rethinkPending)}`);
|
|
29
29
|
for (const change of feature.changes) {
|
|
30
30
|
const name = change.ref.split("/")[1] ?? change.ref;
|
|
31
|
-
lines.push(change.landed !== undefined ? ` ${name}: landed ${change.landed}` : ` ${describe(name, change.approval)}, tasks ${change.tasksDone}/${change.tasksTotal}`);
|
|
31
|
+
lines.push(change.landed !== undefined ? ` ${name}: landed ${change.landed}` : ` ${describe(name, change.approval)}, tasks ${change.tasksDone}/${change.tasksTotal}${pendingNote(change.rethinkPending)}`);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
for (const change of report.standalone) lines.push(`${describe(`change ${change.ref}`, change.approval)}, tasks ${change.tasksDone}/${change.tasksTotal}`);
|
|
34
|
+
for (const change of report.standalone) lines.push(`${describe(`change ${change.ref}`, change.approval)}, tasks ${change.tasksDone}/${change.tasksTotal}${pendingNote(change.rethinkPending)}`);
|
|
35
35
|
const json = {
|
|
36
36
|
...report,
|
|
37
37
|
documents: report.documents.map(compact),
|
|
@@ -48,6 +48,10 @@ function compact(status )
|
|
|
48
48
|
return { ...rest, approvedAt: record.approvedAt, by: record.by, ...(record.basis === undefined ? {} : { basis: record.basis }) };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function pendingNote(findings ) {
|
|
52
|
+
return findings === undefined ? "" : `, ${findings.length} finding(s) waiting on a rethink`;
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
/** Every step starts a new session: one session that ran plan, build, verify and land reached 745k tokens of context. */
|
|
52
56
|
const FRESH_SESSION = "Run it in a new session, so its context starts empty.";
|
|
53
57
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { runCheck, runLand, runTrace } from "./commands/code.js";
|
|
3
|
+
import { runDiff } from "./commands/diff.js";
|
|
3
4
|
import { runApprove, runNew, runValidate } from "./commands/documents.js";
|
|
4
5
|
import { runInit } from "./commands/init.js";
|
|
5
6
|
import { runInstall } from "./commands/install.js";
|
|
@@ -23,7 +24,12 @@ export const COMMANDS = {
|
|
|
23
24
|
validate: { handler: runValidate, options: [], usage: "validate check every document against the formats" },
|
|
24
25
|
status: { handler: runStatus, options: ["gate", "override", "overlap", "change"], usage: "status [--gate build [--override <reason>]] [--overlap [--change <ref>]]" },
|
|
25
26
|
next: { handler: runNext, options: [], usage: "next the step to run next on this branch, and why" },
|
|
26
|
-
|
|
27
|
+
diff: { handler: runDiff, options: [], usage: "diff <ref> what changed upstream of a document since its approval, and whether its delta needs a rebase" },
|
|
28
|
+
show: {
|
|
29
|
+
handler: runShow,
|
|
30
|
+
options: ["index", "notes"],
|
|
31
|
+
usage: "show <ref> | show <ID>... | show --index [<context>] | show --notes [<target>] a change's design, chosen elements, one line per element, or the notes and hand-offs for a target",
|
|
32
|
+
},
|
|
27
33
|
approve: {
|
|
28
34
|
handler: runApprove,
|
|
29
35
|
options: ["replan", "rebase", "by-agent", "restamp-unchanged"],
|
|
@@ -31,8 +37,8 @@ export const COMMANDS = {
|
|
|
31
37
|
},
|
|
32
38
|
new: {
|
|
33
39
|
handler: runNew,
|
|
34
|
-
options: { vision: [], glossary: [], architecture: [], roadmap: [], feature: ["title", "roadmap"], change: ["title", "feature", "roadmap"], adr: ["title"], note: ["title"] },
|
|
35
|
-
usage: "new <vision|glossary|architecture|roadmap> | new <feature|change|adr
|
|
40
|
+
options: { vision: [], glossary: [], architecture: [], roadmap: [], feature: ["title", "roadmap"], change: ["title", "feature", "roadmap"], adr: ["title"], note: ["title", "kind", "about"] },
|
|
41
|
+
usage: "new <vision|glossary|architecture|roadmap> | new <feature|change|adr> <slug> --title <title> [--feature <ref>] [--roadmap <slug>] | new note <slug> --title <title> --kind <input|intake|explore|research> --about <subjects>",
|
|
36
42
|
},
|
|
37
43
|
task: { handler: runTask, options: { done: ["change", "commit"], deviation: ["change"], reopen: ["change"] }, usage: "task <done|deviation|reopen> <T#> [text] [--change <ref>] [--commit <sha>]" },
|
|
38
44
|
features: { handler: runFeatures, options: { add: ["summary", "goals"], update: ["summary"], drop: ["reason"] }, usage: "features <add|update|drop> <slug> [--summary <text>] [--goals <ids>] [--reason <text>]" },
|
|
@@ -54,6 +54,10 @@ export function parseCommand(argv ) {
|
|
|
54
54
|
blockers: { type: "string" },
|
|
55
55
|
"by-agent": { type: "boolean", default: false },
|
|
56
56
|
"restamp-unchanged": { type: "boolean", default: false },
|
|
57
|
+
index: { type: "boolean", default: false },
|
|
58
|
+
notes: { type: "boolean", default: false },
|
|
59
|
+
kind: { type: "string" },
|
|
60
|
+
about: { type: "string" },
|
|
57
61
|
},
|
|
58
62
|
});
|
|
59
63
|
} catch (error) {
|
|
@@ -78,6 +82,8 @@ export function parseCommand(argv ) {
|
|
|
78
82
|
rebase: values.rebase,
|
|
79
83
|
byAgent: values["by-agent"],
|
|
80
84
|
restampUnchanged: values["restamp-unchanged"],
|
|
85
|
+
index: values.index,
|
|
86
|
+
notes: values.notes,
|
|
81
87
|
...optional("name", values.name),
|
|
82
88
|
...optional("title", values.title),
|
|
83
89
|
...optional("feature", values.feature),
|
|
@@ -96,6 +102,8 @@ export function parseCommand(argv ) {
|
|
|
96
102
|
...optional("package", values.package),
|
|
97
103
|
...optional("base", values.base),
|
|
98
104
|
...optional("blockers", values.blockers),
|
|
105
|
+
...optional("kind", values.kind),
|
|
106
|
+
...optional("about", values.about),
|
|
99
107
|
};
|
|
100
108
|
const given = [...new Set(tokens.flatMap((token) => (token.kind === "option" ? [token.name] : [])))];
|
|
101
109
|
return { words: positionals, options, given, help: values.help, version: values.version };
|
|
@@ -45,6 +45,15 @@ export class GitRepository {
|
|
|
45
45
|
return new Map(paths.flatMap((path, i) => (blobs[i] === undefined ? [] : [[path, blobs[i]] ])));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
async firstCommitAdding(path , text ) {
|
|
49
|
+
const commits = await this.git("log", "--reverse", "--format=%H", `-S${text}`, "--", path);
|
|
50
|
+
return commits?.split("\n").find((c) => c !== "") ?? null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async fileAt(commit , path ) {
|
|
54
|
+
return (await this.catFiles([`${commit}:${path}`]))[0] ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
/** The contents of each object, in order, through one `git cat-file --batch`; undefined for one that is missing. */
|
|
49
58
|
catFiles(objects ) {
|
|
50
59
|
return new Promise((resolve) => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { requirePlanReviewClosed, requireUnchangedSinceApproval, } from "../../domain/approvals/agent_approval.js";
|
|
2
2
|
import { evaluateApproval } from "../../domain/approvals/evaluate.js";
|
|
3
3
|
import { fingerprint, isApprovable } from "../../domain/approvals/fingerprint.js";
|
|
4
|
-
import {
|
|
4
|
+
import { rebaseNeeded } from "../../domain/approvals/rebase.js";
|
|
5
|
+
import { serializeApprovals, withApproval } from "../../domain/approvals/records.js";
|
|
5
6
|
import { CruzeError } from "../../domain/cruze_error.js";
|
|
6
7
|
import { updateProgress } from "../../domain/edits/progress_block.js";
|
|
7
8
|
import { setStatus, stampMissingStatuses } from "../../domain/edits/status_lines.js";
|
|
@@ -14,7 +15,7 @@ import { PATHS } from "../../domain/project/layout.js";
|
|
|
14
15
|
import { isPlanned, readChangesTable } from "../../domain/project/plan_parts.js";
|
|
15
16
|
import { readProgress } from "../../domain/project/progress.js";
|
|
16
17
|
import { approvalFolder, approvalsPath, } from "../../domain/project/project_view.js";
|
|
17
|
-
import { featureOf
|
|
18
|
+
import { featureOf, } from "../../domain/project/work_items.js";
|
|
18
19
|
|
|
19
20
|
import { SIZE_RULES } from "../../domain/validation/size_rules.js";
|
|
20
21
|
import { validateProject } from "../../domain/validation/validate_project.js";
|
|
@@ -96,13 +97,14 @@ export async function approveArtifact(deps , ref , options
|
|
|
96
97
|
throw new CruzeError("invalid", `${path} has ${errors.length} problem(s) to fix before approval:\n${listing}`);
|
|
97
98
|
}
|
|
98
99
|
requireSizesAccepted(problems, path);
|
|
100
|
+
if (item !== undefined) requireNoDecisionCitations(problems, path);
|
|
99
101
|
|
|
100
102
|
const { hash, upstream } = fingerprint(view, path, parseMarkdown(text));
|
|
101
103
|
const folder = approvalFolder(view, path);
|
|
102
104
|
const current = view.approvals.get(folder) ?? { version: 1, approvals: [], merged: {} };
|
|
103
105
|
const rebased = options.rebase ?? [];
|
|
104
106
|
const deltaIds = item === undefined || item.kind === "change" ? new Set () : readDelta(item.doc).ids;
|
|
105
|
-
|
|
107
|
+
if (item !== undefined) requireRebased(view, item, rebased);
|
|
106
108
|
const merged = { ...current.merged };
|
|
107
109
|
for (const id of rebased) {
|
|
108
110
|
const living = view.living.elements.get(id);
|
|
@@ -143,13 +145,8 @@ export async function approveArtifact(deps , ref , options
|
|
|
143
145
|
* a rethink or another land, the delta would overwrite that edit, so approval waits until the
|
|
144
146
|
* delta is reconciled and the element is named with --rebase.
|
|
145
147
|
*/
|
|
146
|
-
function requireRebased(view ,
|
|
147
|
-
|
|
148
|
-
const moved = [...deltaIds].filter((id) => {
|
|
149
|
-
const living = view.living.elements.get(id);
|
|
150
|
-
const before = previous.upstream[id];
|
|
151
|
-
return living !== undefined && before !== undefined && before !== elementHash(living.doc, living.element) && !rebased.includes(id);
|
|
152
|
-
});
|
|
148
|
+
function requireRebased(view , item , rebased ) {
|
|
149
|
+
const moved = rebaseNeeded(view, item).filter((id) => !rebased.includes(id));
|
|
153
150
|
if (moved.length > 0) {
|
|
154
151
|
throw new CruzeError(
|
|
155
152
|
"rebase-required",
|
|
@@ -172,6 +169,17 @@ function requireSizesAccepted(problems , path ) {
|
|
|
172
169
|
);
|
|
173
170
|
}
|
|
174
171
|
|
|
172
|
+
/**
|
|
173
|
+
* A delta that cites decisions by number would merge them into the living docs, where they dangle once
|
|
174
|
+
* the work is archived. Existing docs only warn, so approval of new or redesigned work is where it stops.
|
|
175
|
+
*/
|
|
176
|
+
function requireNoDecisionCitations(problems , path ) {
|
|
177
|
+
const standing = problems.filter((p) => p.rule === "decision-citation");
|
|
178
|
+
if (standing.length === 0) return;
|
|
179
|
+
const listing = standing.map((p) => ` ${p.path}:${p.line ?? ""} ${p.message}`).join("\n");
|
|
180
|
+
throw new CruzeError("decision-cited", `${path} cites decisions by number in its delta, which merges into the living docs:\n${listing}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
175
183
|
function notWork(path ) {
|
|
176
184
|
throw new CruzeError("needs-user", `${path} is a project document, so the user approves it`);
|
|
177
185
|
}
|
|
@@ -27,16 +27,18 @@ export async function checkCode(deps , options
|
|
|
27
27
|
const view = await loadView(deps.files);
|
|
28
28
|
const config = requireConfig(view);
|
|
29
29
|
const sources = await readMatching(deps.files, config.source);
|
|
30
|
+
const tests = await readMatching(deps.files, config.tests);
|
|
30
31
|
for (const file of options.files ?? []) {
|
|
31
|
-
if (!sources.has(file)) {
|
|
32
|
+
if (!sources.has(file) && !tests.has(file)) {
|
|
32
33
|
const text = await deps.files.readText(file);
|
|
33
34
|
if (text === undefined) throw new CruzeError("not-found", `${file} does not exist`);
|
|
34
35
|
if (matchesAny(file, config.source)) sources.set(file, text);
|
|
36
|
+
else if (matchesAny(file, config.tests)) tests.set(file, text);
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
|
-
const only = options.files?.filter((file) => sources.has(file));
|
|
39
|
+
const only = options.files?.filter((file) => sources.has(file) || tests.has(file));
|
|
38
40
|
const standards = config.standards ?? FIRST_RELEASE;
|
|
39
|
-
const findings = checkSource({ config, sources, modulePaths: modulePaths(view), ...(only === undefined ? {} : { only }) }).map((f) =>
|
|
41
|
+
const findings = checkSource({ config, sources, tests, modulePaths: modulePaths(view), ...(only === undefined ? {} : { only }) }).map((f) =>
|
|
40
42
|
isPending("check", f.rule, standards) ? { ...f, pending: true } : f,
|
|
41
43
|
);
|
|
42
44
|
const failing = findings.filter((f) => f.pending !== true && (f.severity === "error" || options.ci));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CruzeError } from "../../domain/cruze_error.js";
|
|
2
|
+
import { knownPitfalls } from "../../domain/edits/agents_file.js";
|
|
2
3
|
import { journalPath, parseJournal, recordedEventProblems, recordedFields, } from "../../domain/journal.js";
|
|
3
4
|
import { resolveArtifact } from "../../domain/project/artifact_ref.js";
|
|
4
5
|
import { appendJournal, loadView, } from "../project_context.js";
|
|
@@ -40,10 +41,12 @@ export async function listEvents(deps , event , item
|
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
|
|
45
|
+
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
/**
|
|
46
|
-
* Bundles the journal for the framework feedback loop. By default the project name and
|
|
49
|
+
* Bundles the journal, and the project's known pitfalls, for the framework feedback loop. By default the project name and
|
|
47
50
|
* the approver's identity are redacted, since the bundle may leave the project.
|
|
48
51
|
*/
|
|
49
52
|
export async function exportFeedback(deps , redact ) {
|
|
@@ -56,5 +59,6 @@ export async function exportFeedback(deps , redact )
|
|
|
56
59
|
return value;
|
|
57
60
|
};
|
|
58
61
|
const entries = redact ? view.journal.map((entry) => ({ ...(scrub(entry) ), by: "<redacted>" })) : view.journal;
|
|
59
|
-
|
|
62
|
+
const pitfalls = knownPitfalls((await deps.files.readText("AGENTS.md")) ?? "").map((line) => (redact ? (scrub(line) ) : line));
|
|
63
|
+
return { format: "cruze-feedback/1", cruze: deps.bundle.version, exported_at: deps.clock.now().toISOString(), redacted: redact, entries, pitfalls };
|
|
60
64
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CruzeError } from "../../domain/cruze_error.js";
|
|
2
2
|
import { PATHS } from "../../domain/project/layout.js";
|
|
3
|
+
import { NOTE_KINDS } from "../../domain/project/notes.js";
|
|
3
4
|
import { requireSlug } from "../../domain/project/new_items.js";
|
|
4
5
|
import { renderTemplate } from "../../domain/scaffold.js";
|
|
5
6
|
import { appendJournal, loadView, } from "../project_context.js";
|
|
@@ -27,14 +28,21 @@ export async function createProjectDoc(deps , kind )
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/** Creates a dated note in `.cruze/notes/`, such as a captured explore session or research evidence. */
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* A dated note, saying what kind it is and what it is about, so `cruze show` finds it for the work it
|
|
33
|
+
* informs: `project`, feature slugs, or context and capability names.
|
|
34
|
+
*/
|
|
35
|
+
export async function createNote(deps , slug , title , kind , about ) {
|
|
31
36
|
requireSlug(slug);
|
|
32
37
|
if (title.trim() === "") throw new CruzeError("missing-title", "a title is required");
|
|
38
|
+
if (!(NOTE_KINDS ).includes(kind)) throw new CruzeError("note-kind", `a note's kind is one of ${NOTE_KINDS.join(", ")}`);
|
|
39
|
+
const subjects = about.split(",").map((a) => a.trim()).filter((a) => a !== "");
|
|
40
|
+
if (subjects.length === 0) throw new CruzeError("note-about", "name what the note is about: project, feature slugs, or contexts, comma-separated");
|
|
33
41
|
const view = await loadView(deps.files);
|
|
34
42
|
const date = deps.clock.today();
|
|
35
43
|
const base = `${PATHS.notesDir}/${date}-${slug}`;
|
|
36
44
|
let path = `${base}.md`;
|
|
37
45
|
for (let n = 2; view.snapshot.has(path); n++) path = `${base}-${n}.md`;
|
|
38
|
-
await deps.files.writeText(path, `# ${title}\n\n- Date: ${date}\n\n`);
|
|
46
|
+
await deps.files.writeText(path, `# ${title}\n\n- Date: ${date}\n- Kind: ${kind}\n- About: ${subjects.join(", ")}\n\n`);
|
|
39
47
|
return { kind: "note", path };
|
|
40
48
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CruzeError } from "../../domain/cruze_error.js";
|
|
2
2
|
import { resolveArtifact } from "../../domain/project/artifact_ref.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isPlanned } from "../../domain/project/plan_parts.js";
|
|
4
|
+
import { featureOf, } from "../../domain/project/work_items.js";
|
|
4
5
|
|
|
5
6
|
import { loadView, } from "../project_context.js";
|
|
6
7
|
|
|
@@ -44,7 +45,7 @@ export async function runReview(deps , req
|
|
|
44
45
|
context.push(`Under review: ${item?.ref ?? path} (\`${path}\`)`);
|
|
45
46
|
const feature = item === undefined ? undefined : featureOf(view.items, item);
|
|
46
47
|
if (feature !== undefined) context.push(`Its feature: \`${feature.path}\``);
|
|
47
|
-
if (role === "design-reviewer" && item !== undefined
|
|
48
|
+
if (role === "design-reviewer" && item !== undefined) context.push(rubricLine(item));
|
|
48
49
|
} else {
|
|
49
50
|
context.push("Under review: the project documents the role lists for project scope");
|
|
50
51
|
}
|
|
@@ -83,3 +84,10 @@ async function bundledRole(deps , role ) {
|
|
|
83
84
|
if (file === undefined) throw new CruzeError("role-missing", `the ${role} role is not installed; run cruze install`);
|
|
84
85
|
return file.text;
|
|
85
86
|
}
|
|
87
|
+
|
|
88
|
+
/** A design reviewer reads one rubric: the plan rubric for a planned change, the design rubric otherwise. */
|
|
89
|
+
function rubricLine(item ) {
|
|
90
|
+
return item.kind !== "feature" && isPlanned(item.doc)
|
|
91
|
+
? "This is a change's plan: review it against `.agents/skills/cruze-roles/plan-rubric.md`."
|
|
92
|
+
: "This is a design: review it against `.agents/skills/cruze-roles/design-rubric.md`.";
|
|
93
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CruzeError } from "../../domain/cruze_error.js";
|
|
2
|
+
import { elementIndex, lookupElements, } from "../../domain/project/element_lookup.js";
|
|
3
|
+
|
|
4
|
+
import { loadView, } from "../project_context.js";
|
|
5
|
+
|
|
6
|
+
/** The bodies of the named elements, living and in each in-flight delta that changes them. */
|
|
7
|
+
export async function showElements(deps , ids ) {
|
|
8
|
+
const { elements, missing } = lookupElements(await loadView(deps.files), ids);
|
|
9
|
+
if (missing.length > 0) throw new CruzeError("unknown-id", `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} defined nowhere in the living docs or the work in flight; cruze show --index lists every element`);
|
|
10
|
+
return { elements };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** One line per element of the living architecture and specs, with what work in flight adds or changes. */
|
|
14
|
+
export async function showIndex(deps , scope ) {
|
|
15
|
+
const index = elementIndex(await loadView(deps.files), scope);
|
|
16
|
+
if (scope !== undefined && index.entries.length === 0) throw new CruzeError("unknown-scope", `no element belongs to "${scope}"; name a context or capability, such as the part of an ID before its dot`);
|
|
17
|
+
return scope === undefined ? index : { ...index, scope };
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { notesFor, readNotes, } from "../../domain/project/notes.js";
|
|
2
|
+
import { loadView, } from "../project_context.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every note with what it is about and the hand-off it holds, or, given a target such as
|
|
6
|
+
* `project`, `architect` or a feature slug, the notes and hand-off rows for it.
|
|
7
|
+
*/
|
|
8
|
+
export async function showNotes(deps , target ) {
|
|
9
|
+
const notes = readNotes((await loadView(deps.files)).snapshot);
|
|
10
|
+
return target === undefined ? { notes } : { target, ...notesFor(notes, [target]) };
|
|
11
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { CruzeError } from "../../domain/cruze_error.js";
|
|
2
2
|
import { resolveArtifact } from "../../domain/project/artifact_ref.js";
|
|
3
|
+
import { scopeOf } from "../../domain/ids.js";
|
|
4
|
+
import { notesFor, readNotes, } from "../../domain/project/notes.js";
|
|
3
5
|
import { scopeView, } from "../../domain/project/scope_view.js";
|
|
6
|
+
|
|
4
7
|
import { loadView, } from "../project_context.js";
|
|
5
8
|
import { listEvents } from "./journal_events.js";
|
|
6
9
|
|
|
@@ -16,6 +19,8 @@ import { listEvents } from "./journal_events.js";
|
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
|
|
22
|
+
|
|
23
|
+
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
/**
|
|
@@ -33,5 +38,14 @@ export async function showScope(deps , ref )
|
|
|
33
38
|
reason: String(entry["reason"] ?? ""),
|
|
34
39
|
review: String(entry["review"] ?? ""),
|
|
35
40
|
}));
|
|
36
|
-
|
|
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 ) {
|
|
49
|
+
const roadmap = item.doc.frontmatter?.["roadmap"];
|
|
50
|
+
return [...new Set([...(typeof roadmap === "string" ? [roadmap] : []), item.folderName.replace(/^\d{4}-\d{2}-\d{2}-/, "")])];
|
|
37
51
|
}
|