@esneiderbravo/speclaw 0.3.10 → 0.3.12
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/dist/cli/commands/lawbook.js +43 -6
- package/dist/cli/commands/query.js +49 -2
- package/dist/cli/commands/quick.js +35 -0
- package/dist/cli/commands/update.js +18 -0
- package/dist/cli/index.js +17 -3
- package/dist/modules/compass/db.js +15 -2
- package/dist/modules/compass/extract.js +44 -0
- package/dist/modules/compass/git-history-cache.js +19 -2
- package/dist/modules/compass/hotspots.js +230 -0
- package/dist/modules/compass/indexer.js +2 -0
- package/dist/modules/compass/languages.js +39 -0
- package/dist/modules/compass/register.js +17 -0
- package/dist/modules/foundation/doctor.js +63 -0
- package/dist/modules/lawbook/assets/commands/archive.md +5 -6
- package/dist/modules/lawbook/assets/commands/draft.md +6 -7
- package/dist/modules/lawbook/assets/commands/quick.md +14 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +4 -3
- package/dist/modules/lawbook/assets/skills/draft/SKILL.md +1 -1
- package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +3 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +28 -25
- package/dist/modules/lawbook/assets/skills/quick/SKILL.md +11 -0
- package/dist/modules/lawbook/assets/skills/quick/steps/01-scaffold.md +6 -0
- package/dist/modules/lawbook/assets/skills/quick/steps/02-implement.md +7 -0
- package/dist/modules/lawbook/engine.js +115 -55
- package/dist/modules/lawbook/levels.js +421 -0
- package/dist/modules/lawbook/quick.js +86 -0
- package/dist/modules/lawbook/register.js +10 -0
- package/dist/shared/exposure.js +3 -0
- package/dist/shared/git-history.js +85 -5
- package/package.json +1 -1
|
@@ -214,6 +214,7 @@ export async function buildIndex(projectPath, onProgress) {
|
|
|
214
214
|
const delCoverage = db.prepare("DELETE FROM coverage_links WHERE file_path = ?");
|
|
215
215
|
const insNode = db.prepare(`INSERT INTO nodes(file_id, name, kind, start_line, end_line, start_byte, end_byte, parent_id, signature, body_hash, norm_hash)
|
|
216
216
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
217
|
+
const insMetrics = db.prepare(`INSERT INTO node_metrics(node_id, loc, max_nesting, branches) VALUES (?, ?, ?, ?)`);
|
|
217
218
|
const insEdge = db.prepare(`INSERT INTO edges(src_node_id, src_file_id, dst_name, kind, line) VALUES (?, ?, ?, ?, ?)`);
|
|
218
219
|
const insCoverage = db.prepare(`INSERT OR REPLACE INTO coverage_links(
|
|
219
220
|
artifact_type, name, revision, kind, file_path, line, node_id, source_type, origin
|
|
@@ -265,6 +266,7 @@ export async function buildIndex(projectPath, onProgress) {
|
|
|
265
266
|
const parentId = s.parentIndex !== null ? nodeIds[s.parentIndex] : null;
|
|
266
267
|
const id = Number(insNode.run(fileId, s.name, s.kind, s.startLine, s.endLine, s.startByte, s.endByte, parentId, s.signature, s.bodyHash, s.normHash).lastInsertRowid);
|
|
267
268
|
nodeIds.push(id);
|
|
269
|
+
insMetrics.run(id, s.loc, s.maxNesting, s.branches);
|
|
268
270
|
// embed the node from its name + signature (cheap, meaningful text)
|
|
269
271
|
const vec = await embedder.embed(`${s.kind} ${s.name} ${s.signature ?? ""}`);
|
|
270
272
|
insEmbed.run(id, embedder.dim, embedder.id, toBlob(vec));
|
|
@@ -17,6 +17,19 @@ export const LANGUAGES = [
|
|
|
17
17
|
callNode: "call",
|
|
18
18
|
callField: "function",
|
|
19
19
|
importNodes: ["import_statement", "import_from_statement"],
|
|
20
|
+
nestingNodes: ["block", "suite"],
|
|
21
|
+
branchNodes: [
|
|
22
|
+
"if_statement",
|
|
23
|
+
"elif_clause",
|
|
24
|
+
"for_statement",
|
|
25
|
+
"while_statement",
|
|
26
|
+
"match_statement",
|
|
27
|
+
"case_clause",
|
|
28
|
+
"conditional_expression",
|
|
29
|
+
"except_clause",
|
|
30
|
+
"with_statement",
|
|
31
|
+
"boolean_operator",
|
|
32
|
+
],
|
|
20
33
|
},
|
|
21
34
|
{
|
|
22
35
|
id: "javascript",
|
|
@@ -30,6 +43,19 @@ export const LANGUAGES = [
|
|
|
30
43
|
callNode: "call_expression",
|
|
31
44
|
callField: "function",
|
|
32
45
|
importNodes: ["import_statement"],
|
|
46
|
+
nestingNodes: ["statement_block", "class_body"],
|
|
47
|
+
branchNodes: [
|
|
48
|
+
"if_statement",
|
|
49
|
+
"else_clause",
|
|
50
|
+
"for_statement",
|
|
51
|
+
"for_in_statement",
|
|
52
|
+
"while_statement",
|
|
53
|
+
"do_statement",
|
|
54
|
+
"switch_case",
|
|
55
|
+
"switch_default",
|
|
56
|
+
"catch_clause",
|
|
57
|
+
"ternary_expression",
|
|
58
|
+
],
|
|
33
59
|
},
|
|
34
60
|
{
|
|
35
61
|
id: "typescript",
|
|
@@ -46,6 +72,19 @@ export const LANGUAGES = [
|
|
|
46
72
|
callNode: "call_expression",
|
|
47
73
|
callField: "function",
|
|
48
74
|
importNodes: ["import_statement"],
|
|
75
|
+
nestingNodes: ["statement_block", "class_body"],
|
|
76
|
+
branchNodes: [
|
|
77
|
+
"if_statement",
|
|
78
|
+
"else_clause",
|
|
79
|
+
"for_statement",
|
|
80
|
+
"for_in_statement",
|
|
81
|
+
"while_statement",
|
|
82
|
+
"do_statement",
|
|
83
|
+
"switch_case",
|
|
84
|
+
"switch_default",
|
|
85
|
+
"catch_clause",
|
|
86
|
+
"ternary_expression",
|
|
87
|
+
],
|
|
49
88
|
},
|
|
50
89
|
];
|
|
51
90
|
const BY_EXT = new Map();
|
|
@@ -4,6 +4,7 @@ import { shouldExpose } from "../../shared/exposure.js";
|
|
|
4
4
|
import { buildIndex } from "./indexer.js";
|
|
5
5
|
import { explore, search, recall, impact, trace } from "./query.js";
|
|
6
6
|
import { affectedTests } from "./affected.js";
|
|
7
|
+
import { hotspots, coupling } from "./hotspots.js";
|
|
7
8
|
import { startWatch, stopWatch, watchStatus } from "./watcher.js";
|
|
8
9
|
import { visualize } from "./visualize.js";
|
|
9
10
|
// ─── Compass: speclaw's own code-intelligence engine (no external deps) ───
|
|
@@ -55,6 +56,22 @@ export function registerCompass(server, opts = {}) {
|
|
|
55
56
|
fromDiff: z.string().optional(),
|
|
56
57
|
maxDepth: z.number().int().min(1).max(12).optional(),
|
|
57
58
|
}, async ({ projectPath, files, symbols, fromDiff, maxDepth }) => text(affectedTests(projectPath, { files, symbols, fromDiff, maxDepth })));
|
|
59
|
+
add("compass_hotspots", "Rank files by recent churn and AST complexity; two axes, no magic score.", {
|
|
60
|
+
projectPath: z.string(),
|
|
61
|
+
days: z.number().int().min(1).max(3650).optional(),
|
|
62
|
+
since: z.string().optional(),
|
|
63
|
+
sortBy: z.enum(["churn", "complexity", "combined"]).optional(),
|
|
64
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
65
|
+
}, async ({ projectPath, days, since, sortBy, limit }) => text(hotspots(projectPath, { days, since, sortBy, limit })));
|
|
66
|
+
add("compass_coupling", "Files that co-change with a target; strength, graph edge, and test-pair facts.", {
|
|
67
|
+
projectPath: z.string(),
|
|
68
|
+
file: z.string(),
|
|
69
|
+
days: z.number().int().min(1).max(3650).optional(),
|
|
70
|
+
since: z.string().optional(),
|
|
71
|
+
minShared: z.number().int().min(1).optional(),
|
|
72
|
+
maxFilesPerCommit: z.number().int().min(2).optional(),
|
|
73
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
74
|
+
}, async ({ projectPath, file, days, since, minShared, maxFilesPerCommit, limit }) => text(coupling(projectPath, file, { days, since, minShared, maxFilesPerCommit, limit })));
|
|
58
75
|
add("compass_trace", "Find a call path between two symbols within a depth limit.", {
|
|
59
76
|
projectPath: z.string(),
|
|
60
77
|
from: z.string(),
|
|
@@ -8,6 +8,7 @@ import { pkgName, pkgVersion } from "../../shared/version.js";
|
|
|
8
8
|
import { indexExists, openDb } from "../compass/db.js";
|
|
9
9
|
import { specList } from "../lawbook/engine.js";
|
|
10
10
|
import { doctorDriftCheck } from "../lawbook/drift.js";
|
|
11
|
+
import { loadCeremonyConfig } from "../lawbook/levels.js";
|
|
11
12
|
import { globError, hasBackend, hasBatchBackend, readLawManifest } from "./laws.js";
|
|
12
13
|
import { redactValue } from "../../shared/redact.js";
|
|
13
14
|
const STATUS_RANK = {
|
|
@@ -467,6 +468,67 @@ function specsOrphansCheck(projectPath) {
|
|
|
467
468
|
remedy: `speclaw lawbook archive ${active[0]}`,
|
|
468
469
|
};
|
|
469
470
|
}
|
|
471
|
+
/** Ceremony config validity + archived level histogram. */
|
|
472
|
+
function ceremonyChecks(projectPath) {
|
|
473
|
+
const out = [];
|
|
474
|
+
const { invalidCuts } = loadCeremonyConfig(projectPath);
|
|
475
|
+
if (invalidCuts) {
|
|
476
|
+
out.push({
|
|
477
|
+
id: "cfg.ceremony.cuts",
|
|
478
|
+
title: "ceremony thresholds",
|
|
479
|
+
status: "warn",
|
|
480
|
+
detail: "invalid ceremony.cuts — using built-in defaults [3, 8, 15]",
|
|
481
|
+
remedy: "fix cuts in lawbook/config.yaml so they are strictly increasing, or remove the block",
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
out.push({
|
|
486
|
+
id: "cfg.ceremony.cuts",
|
|
487
|
+
title: "ceremony thresholds",
|
|
488
|
+
status: "ok",
|
|
489
|
+
detail: "ceremony cuts valid (or using defaults)",
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
const archiveRoot = path.join(projectPath, "lawbook", "changes", "archive");
|
|
493
|
+
const counts = { "0": 0, "1": 0, "2": 0, "3": 0, missing: 0 };
|
|
494
|
+
if (fs.existsSync(archiveRoot)) {
|
|
495
|
+
for (const name of fs.readdirSync(archiveRoot)) {
|
|
496
|
+
const dir = path.join(archiveRoot, name);
|
|
497
|
+
if (!fs.statSync(dir).isDirectory())
|
|
498
|
+
continue;
|
|
499
|
+
// archived folder is YYYY-MM-DD-name; change.json lives inside
|
|
500
|
+
const rec = (() => {
|
|
501
|
+
try {
|
|
502
|
+
const p = path.join(dir, "change.json");
|
|
503
|
+
if (!fs.existsSync(p))
|
|
504
|
+
return null;
|
|
505
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
})();
|
|
511
|
+
if (!rec || rec.confirmedLevel === undefined) {
|
|
512
|
+
counts.missing += 1;
|
|
513
|
+
counts["3"] += 1;
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
counts[String(rec.confirmedLevel)] = (counts[String(rec.confirmedLevel)] ?? 0) + 1;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const total = (counts["0"] ?? 0) + (counts["1"] ?? 0) + (counts["2"] ?? 0) + (counts["3"] ?? 0);
|
|
521
|
+
out.push({
|
|
522
|
+
id: "cfg.ceremony.levels",
|
|
523
|
+
title: "ceremony level distribution",
|
|
524
|
+
status: "ok",
|
|
525
|
+
value: JSON.stringify(counts),
|
|
526
|
+
detail: total === 0
|
|
527
|
+
? "no archived changes"
|
|
528
|
+
: `archived levels: 0=${counts["0"]}, 1=${counts["1"]}, 2=${counts["2"]}, 3=${counts["3"]} (missing change.json=${counts.missing})`,
|
|
529
|
+
});
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
470
532
|
function configurationChecks(projectPath, initialised) {
|
|
471
533
|
if (!initialised) {
|
|
472
534
|
const ids = [
|
|
@@ -559,6 +621,7 @@ export async function doctor(projectPath, opts = {}) {
|
|
|
559
621
|
configuration.push(await budgetCheck(projectPath));
|
|
560
622
|
configuration.push(freshnessCheck(projectPath));
|
|
561
623
|
configuration.push(specsOrphansCheck(projectPath));
|
|
624
|
+
configuration.push(...ceremonyChecks(projectPath));
|
|
562
625
|
{
|
|
563
626
|
const d = doctorDriftCheck(projectPath);
|
|
564
627
|
addCheck(configuration, {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Finalize a completed change — sync specs
|
|
2
|
+
description: Finalize a completed change — sync specs when needed, then archive it.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
Archive the completed change: $ARGUMENTS
|
|
6
6
|
|
|
7
|
-
Follow the `archive` skill: confirm every task
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
`lawbook/changes/archive/`. Never move the folder by hand.
|
|
7
|
+
Follow the `archive` skill: confirm every task (or level-0 checklist) is done
|
|
8
|
+
and gates are green, reconcile if the level has delta specs, run
|
|
9
|
+
`lawbook_validate`, then `lawbook_archive` with today's date (YYYY-MM-DD). Sync
|
|
10
|
+
runs only when the ceremony level requires specs. Never move the folder by hand.
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Draft a new spec-driven change
|
|
2
|
+
description: Draft a new spec-driven change at the confirmed ceremony level before coding.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
Draft a new change under `lawbook/changes/<name>/` for: $ARGUMENTS
|
|
6
6
|
|
|
7
|
-
Follow the `draft` skill: ensure `lawbook/` exists (`lawbook_init`), investigate
|
|
8
|
-
|
|
9
|
-
`
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
running `lawbook_validate` and fixing every issue.
|
|
7
|
+
Follow the `draft` skill: ensure `lawbook/` exists (`lawbook_init`), investigate
|
|
8
|
+
with Compass, propose a ceremony level (`lawbook_level` mode `propose`) and
|
|
9
|
+
**confirm** it with the human (`set`), then scaffold only the artifacts that
|
|
10
|
+
level requires. For true one-liners use `speclaw quick` / the `quick` skill
|
|
11
|
+
instead. Finish by running `lawbook_validate` and fixing every issue.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Scaffold a level-0 lawbook change (record.md + reports) for a tiny fix.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scaffold a **ceremony level 0** change named `$ARGUMENTS` with `speclaw quick`
|
|
6
|
+
(or the equivalent `scaffoldQuick` path). Do **not** invent `proposal.md` /
|
|
7
|
+
`design.md` / delta specs for a true one-liner.
|
|
8
|
+
|
|
9
|
+
1. Ensure `lawbook/` exists (`lawbook_init` if needed).
|
|
10
|
+
2. Prefer passing `--path` / `--symbol` so the proposal rationale is measured.
|
|
11
|
+
3. Confirm with the human if the measured proposal is higher than 0 — promote
|
|
12
|
+
(`lawbook_level` mode `promote`) instead of staying at quick.
|
|
13
|
+
4. Implement, check the checklist in `record.md`, write a discipline report
|
|
14
|
+
under `reports/`, then archive (no sync required at level 0).
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Validate and sync
|
|
2
2
|
|
|
3
|
-
Run `lawbook_validate
|
|
4
|
-
`
|
|
5
|
-
unless the canonical specs already match
|
|
3
|
+
Run `lawbook_validate`. If the confirmed ceremony level requires delta specs
|
|
4
|
+
(levels 1–3), run `lawbook_sync` to promote them into `lawbook/specs/` —
|
|
5
|
+
`lawbook_archive` refuses unless the canonical specs already match. At **level
|
|
6
|
+
0**, skip sync (there are no deltas).
|
|
6
7
|
|
|
7
8
|
Next: read `steps/04-archive.md` and do only what it says.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: draft
|
|
3
|
-
description: Draft a new spec-driven change —
|
|
3
|
+
description: Draft a new spec-driven change — propose a ceremony level, then write only the artifacts that level needs — before writing any code. Use when the user wants to start, plan, or propose a new feature, fix, or refactor: "draft a change for X", "propose X", "let's plan X", "spec out X", "new change". Part of speclaw's lawbook module (draft → build → sync → archive).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# draft — Draft a new change
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
- Clarify what the user wants (feature / fix / refactor) and confirm scope.
|
|
7
7
|
- Use `compass_explore` and `compass_recall` (speclaw's code index) BEFORE
|
|
8
8
|
grep/read to locate the real code the change touches and its blast radius.
|
|
9
|
+
- **Propose a ceremony level** with `lawbook_level` (mode `propose`) using the
|
|
10
|
+
paths/symbols you found; **confirm with the human** (mode `set`) before
|
|
11
|
+
writing artifacts. For an obvious one-liner, offer `speclaw quick` instead.
|
|
9
12
|
- Read the governing standards in `docs/standards/` (architecture, backend,
|
|
10
13
|
frontend, testing) so the change complies with the project's law.
|
|
11
14
|
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
# Write the artifacts
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
Artifact volume follows the **confirmed ceremony level** in `change.json`
|
|
4
|
+
(propose + confirm with `lawbook_level` / the human **before** scaffolding).
|
|
5
|
+
Missing `change.json` means level 3.
|
|
6
|
+
|
|
7
|
+
Create under `lawbook/changes/<name>/` only what the level needs:
|
|
8
|
+
|
|
9
|
+
- **Level 0** — prefer `speclaw quick` / the `quick` skill: `record.md`
|
|
10
|
+
(inline checklist) + `reports/` + `change.json`. No proposal/design/deltas.
|
|
11
|
+
- **Level 1** — `record.md`, `tasks.md`, ≥1 delta under
|
|
12
|
+
`specs/<capability>/spec.md`, `reports/`, `change.json`.
|
|
13
|
+
- **Level 2** — `proposal.md`, `tasks.md`, delta specs, `reports/`;
|
|
14
|
+
`design.md` optional only with justification in `record.md`.
|
|
15
|
+
- **Level 3** — `proposal.md`, `design.md`, `tasks.md`, delta specs, `reports/`.
|
|
16
|
+
|
|
17
|
+
For every level that needs delta specs:
|
|
18
|
+
|
|
19
|
+
- **specs/<capability>/spec.md** — the delta for each affected capability.
|
|
20
|
+
`sync` promotes by overwriting the whole canonical file, so the delta must
|
|
21
|
+
carry the capability's **full** intended spec. When updating an existing
|
|
22
|
+
capability, **start from** `lawbook/specs/<capability>/spec.md`. Use normative
|
|
13
23
|
language and testable scenarios:
|
|
14
24
|
```markdown
|
|
15
25
|
# <Capability>
|
|
@@ -22,20 +32,13 @@ Create under `lawbook/changes/<name>/`:
|
|
|
22
32
|
- When <action>
|
|
23
33
|
- Then <observable outcome>
|
|
24
34
|
```
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
open set (`backend.md`, `frontend.md`, `api.md`, `database.md`, `infra.md`,
|
|
34
|
-
`security.md`, … — and `api.md` is required when the change touches any API
|
|
35
|
-
surface) that `build` will fill, following the required report structure
|
|
36
|
-
(header · gates table · tests added · spec-scenario coverage · pre-existing
|
|
37
|
-
failures · pending manual · verdict — see the `build` skill's discipline-reports
|
|
38
|
-
step). Every change ships this folder; archive is blocked until it holds at
|
|
39
|
-
least one discipline report.
|
|
35
|
+
|
|
36
|
+
- **tasks.md** (levels 1–3) — ordered, checkable steps. MUST include the
|
|
37
|
+
mandatory steps from `lawbook/config.yaml` (feature branch first; tests;
|
|
38
|
+
manual verification by the agent; discipline reports; docs; archive in PR).
|
|
39
|
+
- **reports/** — always scaffold with `reports/README.md` naming expected
|
|
40
|
+
disciplines (`backend.md`, `frontend.md`, `api.md`, … — `api.md` when an API
|
|
41
|
+
surface is touched). Archive is blocked until at least one discipline report
|
|
42
|
+
exists.
|
|
40
43
|
|
|
41
44
|
Next: read `steps/05-validate.md` and do only what it says.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: quick
|
|
3
|
+
description: Scaffold a level-0 lawbook change (record.md + reports) for a tiny fix. Use when the user wants a one-line fix, typo, or docs-only tweak without full ceremony — "quick change", "speclaw quick", "level 0", "skip proposal".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# quick — Level-0 change
|
|
7
|
+
|
|
8
|
+
Create `lawbook/changes/<name>/` with `record.md`, `change.json` (confirmed
|
|
9
|
+
level 0), and `reports/`. No proposal, design, or delta specs.
|
|
10
|
+
|
|
11
|
+
Read `steps/01-scaffold.md` and do only what it says.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Implement and evidence
|
|
2
|
+
|
|
3
|
+
Make the fix, tick every `- [ ]` in `record.md`, and write at least one
|
|
4
|
+
discipline report under `reports/`. Archive with `lawbook_archive` (no sync at
|
|
5
|
+
level 0). Promote via `lawbook_level` if scope grew.
|
|
6
|
+
|
|
7
|
+
No further steps — workflow complete.
|