@esneiderbravo/speclaw 0.3.12 → 0.4.0
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 +2 -2
- package/dist/cli/commands/lawbook.js +42 -1
- package/dist/cli/commands/query.js +20 -0
- package/dist/cli/commands/update.js +10 -0
- package/dist/cli/index.js +8 -0
- package/dist/modules/compass/diff-context.js +134 -0
- package/dist/modules/compass/explore-rich.js +129 -0
- package/dist/modules/compass/impact-summary.js +33 -0
- package/dist/modules/compass/register.js +164 -74
- package/dist/modules/foundation/context-budget.js +1 -14
- package/dist/modules/foundation/doctor.js +77 -0
- package/dist/modules/foundation/register-core.js +57 -88
- package/dist/modules/foundation/register.js +1 -21
- package/dist/modules/foundation/setup-tool.js +96 -0
- package/dist/modules/lawbook/assets/commands/archive.md +1 -1
- package/dist/modules/lawbook/assets/commands/draft.md +1 -1
- package/dist/modules/lawbook/assets/commands/explore.md +1 -1
- package/dist/modules/lawbook/assets/commands/investigate.md +7 -0
- package/dist/modules/lawbook/assets/commands/sync.md +2 -2
- package/dist/modules/lawbook/assets/rules/spec-reports-disciplines.md +8 -0
- package/dist/modules/lawbook/assets/skills/archive/SKILL.md +1 -1
- package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +3 -3
- package/dist/modules/lawbook/assets/skills/archive/steps/04-archive.md +1 -1
- package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +1 -1
- package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +1 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/05-validate.md +1 -1
- package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +1 -1
- package/dist/modules/lawbook/assets/skills/investigate/SKILL.md +10 -0
- package/dist/modules/lawbook/assets/skills/investigate/steps/01-investigate.md +7 -0
- package/dist/modules/lawbook/assets/skills/investigate/steps/02-hand-off.md +6 -0
- package/dist/modules/lawbook/assets/skills/quick/steps/02-implement.md +1 -1
- package/dist/modules/lawbook/assets/skills/sync/SKILL.md +1 -1
- package/dist/modules/lawbook/assets/skills/sync/steps/03-validate.md +1 -1
- package/dist/modules/lawbook/assets/skills/sync/steps/04-promote.md +1 -1
- package/dist/modules/lawbook/bugfix.js +195 -0
- package/dist/modules/lawbook/change-tool.js +90 -0
- package/dist/modules/lawbook/engine.js +70 -7
- package/dist/modules/lawbook/investigate.js +358 -0
- package/dist/modules/lawbook/levels.js +49 -2
- package/dist/modules/lawbook/register.js +102 -52
- package/dist/modules/lawbook/stack-parse.js +135 -0
- package/dist/modules/tools/register.js +4 -26
- package/dist/shared/deprecation.js +99 -0
- package/dist/shared/exposure.js +5 -19
- package/dist/shared/git.js +25 -0
- package/dist/shared/mcp.js +29 -3
- package/dist/shared/output-budget.js +68 -0
- package/dist/shared/tool-catalog.js +49 -0
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { loadPacks } from "../tools/packs.js";
|
|
3
|
+
import { AGENTS, configureAgent } from "../../shared/agents.js";
|
|
4
|
+
import { emptyReport } from "../../shared/install.js";
|
|
5
|
+
import { refreshAgents } from "../../shared/agents.js";
|
|
6
|
+
import { installPack } from "../tools/packs.js";
|
|
7
|
+
/** Human help text for init questionnaire (not embedded in MCP schemas). */
|
|
8
|
+
const profileFieldHelp = {
|
|
9
|
+
project_name: "Short project name, e.g. the repo name",
|
|
10
|
+
project_description: "One-line description of what the project does",
|
|
11
|
+
organization: "Company/team name",
|
|
12
|
+
stack_summary: "e.g. 'Next.js 15 + TypeScript frontend, FastAPI + PostgreSQL backend'",
|
|
13
|
+
architecture: "e.g. 'hexagonal architecture with bounded contexts'",
|
|
14
|
+
test_commands: "Real commands, e.g. 'pytest backend/tests && npm run test'",
|
|
15
|
+
lint_commands: "Real commands, e.g. 'ruff check . && npm run lint && tsc --noEmit'",
|
|
16
|
+
branch_pattern: "e.g. 'feature/<ticket-id>-<slug>'",
|
|
17
|
+
commit_style: "e.g. 'conventional commits, imperative, English'",
|
|
18
|
+
custom_laws: "Extra markdown for LAWS.md — project-specific binding rules",
|
|
19
|
+
compass_hints: "Markdown bullets with real entrypoints for docs/compass.md",
|
|
20
|
+
base_standards_extra: "Extra cross-cutting rules for base-standards.md",
|
|
21
|
+
modules_table: "Markdown table of modules/bounded contexts",
|
|
22
|
+
layering_rules: "Layers and allowed dependencies for architecture.md",
|
|
23
|
+
backend_layers: "Backend layer table for backend-standards.md",
|
|
24
|
+
frontend_layers: "Frontend layer table for frontend-standards.md",
|
|
25
|
+
versioning_rules: "Versioning/release convention for conventions.md",
|
|
26
|
+
documentation_extra: "Repo-specific docstring notes for documentation.md",
|
|
27
|
+
};
|
|
28
|
+
const profileShape = {
|
|
29
|
+
project_name: z.string(),
|
|
30
|
+
project_description: z.string().optional(),
|
|
31
|
+
organization: z.string().optional(),
|
|
32
|
+
stack_summary: z.string().optional(),
|
|
33
|
+
architecture: z.string().optional(),
|
|
34
|
+
test_commands: z.string().optional(),
|
|
35
|
+
lint_commands: z.string().optional(),
|
|
36
|
+
branch_pattern: z.string().optional(),
|
|
37
|
+
commit_style: z.string().optional(),
|
|
38
|
+
custom_laws: z.string().optional(),
|
|
39
|
+
compass_hints: z.string().optional(),
|
|
40
|
+
base_standards_extra: z.string().optional(),
|
|
41
|
+
modules_table: z.string().optional(),
|
|
42
|
+
layering_rules: z.string().optional(),
|
|
43
|
+
backend_layers: z.string().optional(),
|
|
44
|
+
frontend_layers: z.string().optional(),
|
|
45
|
+
versioning_rules: z.string().optional(),
|
|
46
|
+
documentation_extra: z.string().optional(),
|
|
47
|
+
};
|
|
48
|
+
export const setupActions = ["init", "configure-agent", "add-pack", "list-packs"];
|
|
49
|
+
export const speclawSetupSchema = {
|
|
50
|
+
projectPath: z.string(),
|
|
51
|
+
action: z.enum(setupActions),
|
|
52
|
+
agent: z.enum(AGENTS.map((a) => a.id)).optional(),
|
|
53
|
+
pack: z.string().optional(),
|
|
54
|
+
vars: z.record(z.string()).optional(),
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Dispatch `speclaw_setup` by action. Scaffold is CLI-only — not exposed here.
|
|
58
|
+
*
|
|
59
|
+
* @param args - Setup action and parameters.
|
|
60
|
+
*/
|
|
61
|
+
export function handleSpeclawSetup(args) {
|
|
62
|
+
switch (args.action) {
|
|
63
|
+
case "init":
|
|
64
|
+
return {
|
|
65
|
+
instructions: [
|
|
66
|
+
"1. Analyze the repository at projectPath and fill profile fields from the real codebase.",
|
|
67
|
+
"2. Call speclaw_setup with action configure-agent / add-pack as needed.",
|
|
68
|
+
"3. Run lawbook_change action init and compass_index when scaffold completes via CLI if needed.",
|
|
69
|
+
],
|
|
70
|
+
profileFields: profileFieldHelp,
|
|
71
|
+
packs: loadPacks(),
|
|
72
|
+
note: "Full scaffold runs via CLI: speclaw init — not MCP.",
|
|
73
|
+
};
|
|
74
|
+
case "configure-agent": {
|
|
75
|
+
if (!args.agent)
|
|
76
|
+
throw new Error(`speclaw_setup: action 'configure-agent' requires 'agent'`);
|
|
77
|
+
const report = emptyReport();
|
|
78
|
+
configureAgent(args.projectPath, args.agent, report);
|
|
79
|
+
return report;
|
|
80
|
+
}
|
|
81
|
+
case "list-packs":
|
|
82
|
+
return loadPacks();
|
|
83
|
+
case "add-pack": {
|
|
84
|
+
if (!args.pack)
|
|
85
|
+
throw new Error(`speclaw_setup: action 'add-pack' requires 'pack'`);
|
|
86
|
+
const report = emptyReport();
|
|
87
|
+
installPack(args.projectPath, args.pack, args.vars ?? {}, report);
|
|
88
|
+
refreshAgents(args.projectPath, report);
|
|
89
|
+
return report;
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
throw new Error(`speclaw_setup: unknown action '${String(args.action)}'`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Zod profile shape for CLI scaffold (not in MCP schema). */
|
|
96
|
+
export { profileShape as setupProfileShape };
|
|
@@ -6,5 +6,5 @@ Archive the completed change: $ARGUMENTS
|
|
|
6
6
|
|
|
7
7
|
Follow the `archive` skill: confirm every task (or level-0 checklist) is done
|
|
8
8
|
and gates are green, reconcile if the level has delta specs, run
|
|
9
|
-
`
|
|
9
|
+
`lawbook_change` (action: validate), then `lawbook_change` (action: archive) with today's date (YYYY-MM-DD). Sync
|
|
10
10
|
runs only when the ceremony level requires specs. Never move the folder by hand.
|
|
@@ -8,4 +8,4 @@ Follow the `draft` skill: ensure `lawbook/` exists (`lawbook_init`), investigate
|
|
|
8
8
|
with Compass, propose a ceremony level (`lawbook_level` mode `propose`) and
|
|
9
9
|
**confirm** it with the human (`set`), then scaffold only the artifacts that
|
|
10
10
|
level requires. For true one-liners use `speclaw quick` / the `quick` skill
|
|
11
|
-
instead. Finish by running `
|
|
11
|
+
instead. Finish by running `lawbook_change` (action: validate) and fixing every issue.
|
|
@@ -4,7 +4,7 @@ description: Enter explore mode — a thinking partner before or during a change
|
|
|
4
4
|
|
|
5
5
|
Explore: $ARGUMENTS
|
|
6
6
|
|
|
7
|
-
Follow the `explore` skill: use `
|
|
7
|
+
Follow the `explore` skill: use `compass_find` (mode: concept)/`compass_explore` to understand
|
|
8
8
|
the code, ask sharp questions, check the relevant `docs/standards/`, weigh
|
|
9
9
|
approaches with trade-offs, and give a recommendation. Write nothing to
|
|
10
10
|
`lawbook/`; when the direction is clear, offer to `draft` the change.
|
|
@@ -5,6 +5,6 @@ description: Promote a change's delta specs into the canonical specs, without ar
|
|
|
5
5
|
Sync the change's specs into canonical: $ARGUMENTS
|
|
6
6
|
|
|
7
7
|
Follow the `sync` skill: reconcile the delta specs against what was actually
|
|
8
|
-
built (branch diff + code graph), validate the change (`
|
|
9
|
-
run `
|
|
8
|
+
built (branch diff + code graph), validate the change (`lawbook_change` (action: validate)), then
|
|
9
|
+
run `lawbook_change` (action: sync) to promote each delta spec into `lawbook/specs/`. Report what
|
|
10
10
|
you reconciled and what was promoted; leave the change active.
|
|
@@ -76,3 +76,11 @@ touched — and therefore which reports are owed, including `api.md` for any
|
|
|
76
76
|
API-touching change — is the agent's responsibility to judge and satisfy before
|
|
77
77
|
archiving; the engine gate counts files but cannot infer the set of concerns a
|
|
78
78
|
change exercised.
|
|
79
|
+
|
|
80
|
+
## 5. Bug changes must show the regression test failing first
|
|
81
|
+
|
|
82
|
+
When `changeType` is **bug**, the discipline report MUST include the output of
|
|
83
|
+
the regression test **failing before the fix** (or document why instrumentation
|
|
84
|
+
substitutes for a red-green cycle when reproduction is `unreproducible:`). A test
|
|
85
|
+
that only passes after the fix — with no evidence it ever failed — does not
|
|
86
|
+
satisfy the bug gate.
|
|
@@ -9,7 +9,7 @@ Close out a completed change: its delta specs become canonical and the change
|
|
|
9
9
|
folder moves to `lawbook/changes/archive/`. This is part of the PR that
|
|
10
10
|
implements the change, not a post-merge chore.
|
|
11
11
|
|
|
12
|
-
`
|
|
12
|
+
`lawbook_change` (action: archive) is **gated** — the engine refuses to archive (and reports the
|
|
13
13
|
reason) while any task is unchecked, while `reports/` holds no discipline report,
|
|
14
14
|
or while the delta specs are not yet synced into the canonical specs. So archive
|
|
15
15
|
is the last step of a completed change: reconcile, sync, then archive.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Validate and sync
|
|
2
2
|
|
|
3
|
-
Run `
|
|
4
|
-
(levels 1–3), run `
|
|
5
|
-
`
|
|
3
|
+
Run `lawbook_change` (action: validate). If the confirmed ceremony level requires delta specs
|
|
4
|
+
(levels 1–3), run `lawbook_change` (action: sync) to promote them into `lawbook/specs/` —
|
|
5
|
+
`lawbook_change` (action: archive) refuses unless the canonical specs already match. At **level
|
|
6
6
|
0**, skip sync (there are no deltas).
|
|
7
7
|
|
|
8
8
|
Next: read `steps/04-archive.md` and do only what it says.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Archive
|
|
2
2
|
|
|
3
|
-
Run the `
|
|
3
|
+
Run the `lawbook_change` (action: archive) tool with the change name and today's date
|
|
4
4
|
(`YYYY-MM-DD`). It re-checks the gate deterministically and, if it passes,
|
|
5
5
|
moves `lawbook/changes/<name>/` to `lawbook/changes/archive/<date>-<name>/`.
|
|
6
6
|
If it refuses, resolve the reported blockers (unchecked tasks, missing
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
code — it is incremental (unchanged files are skipped by hash), so this is
|
|
5
5
|
cheap and guarantees your decisions rest on the current graph, not a stale one.
|
|
6
6
|
- Clarify what the user wants (feature / fix / refactor) and confirm scope.
|
|
7
|
-
- Use `compass_explore` and `
|
|
7
|
+
- Use `compass_explore` and `compass_find` (mode: concept) (speclaw's code index) BEFORE
|
|
8
8
|
grep/read to locate the real code the change touches and its blast radius.
|
|
9
9
|
- **Propose a ceremony level** with `lawbook_level` (mode `propose`) using the
|
|
10
10
|
paths/symbols you found; **confirm with the human** (mode `set`) before
|
|
@@ -13,6 +13,7 @@ Create under `lawbook/changes/<name>/` only what the level needs:
|
|
|
13
13
|
- **Level 2** — `proposal.md`, `tasks.md`, delta specs, `reports/`;
|
|
14
14
|
`design.md` optional only with justification in `record.md`.
|
|
15
15
|
- **Level 3** — `proposal.md`, `design.md`, `tasks.md`, delta specs, `reports/`.
|
|
16
|
+
- **Bug (`draft --bug`)** — `bugfix.md` instead of proposal/design; see the investigate skill for RCA first.
|
|
16
17
|
|
|
17
18
|
For every level that needs delta specs:
|
|
18
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Validate
|
|
2
2
|
|
|
3
|
-
Run the `
|
|
3
|
+
Run the `lawbook_change` (action: validate) tool for the change and fix every issue it reports
|
|
4
4
|
(missing artifacts, non-normative specs, missing scenarios) before handing off
|
|
5
5
|
to implementation. Read its advisory **warnings** too: a near-duplicate
|
|
6
6
|
capability name usually means you should reuse the existing capability's exact
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- **Refresh the index first.** Run `compass_index` before investigating — it is
|
|
4
4
|
incremental (unchanged files skipped by hash), so it is cheap and keeps your
|
|
5
5
|
reasoning on the current graph rather than a stale one.
|
|
6
|
-
- **Understand the code first.** Use `
|
|
6
|
+
- **Understand the code first.** Use `compass_find` (mode: concept) to find relevant code by
|
|
7
7
|
meaning and `compass_explore` to read a symbol's source plus its callers and
|
|
8
8
|
callees — before grep/read.
|
|
9
9
|
- **Ask sharp questions** to surface hidden assumptions, constraints, and edge
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: investigate
|
|
3
|
+
description: Forensic bug triage — rank suspects via lawbook_investigate before draft --bug. Use with a stack trace or symptom when starting RCA.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# investigate — Bug RCA from the graph
|
|
7
|
+
|
|
8
|
+
Use when work is **"this is broken"**, not **"build X"**.
|
|
9
|
+
|
|
10
|
+
Read `steps/01-investigate.md` and do only what it says.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Hand off to draft
|
|
2
|
+
|
|
3
|
+
- **`compass_impact`** on the confirmed root cause for blast radius.
|
|
4
|
+
- **`speclaw lawbook draft --bug <name>`** — pre-seed only; fill repro, fix, regression test, prevention.
|
|
5
|
+
|
|
6
|
+
Treat the ranking as **evidence**, not a verdict. No further steps remain — investigate workflow complete.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Implement and evidence
|
|
2
2
|
|
|
3
3
|
Make the fix, tick every `- [ ]` in `record.md`, and write at least one
|
|
4
|
-
discipline report under `reports/`. Archive with `
|
|
4
|
+
discipline report under `reports/`. Archive with `lawbook_change` (action: archive) (no sync at
|
|
5
5
|
level 0). Promote via `lawbook_level` if scope grew.
|
|
6
6
|
|
|
7
7
|
No further steps — workflow complete.
|
|
@@ -9,7 +9,7 @@ Update the project's canonical specifications (`lawbook/specs/`) with a change's
|
|
|
9
9
|
delta specs, without archiving the change. Use this when the specs should
|
|
10
10
|
become the source of truth but the change isn't finished (e.g. multi-PR work).
|
|
11
11
|
|
|
12
|
-
`
|
|
12
|
+
`lawbook_change` (action: sync) is a deterministic copy — it is blind to the code. So before
|
|
13
13
|
promoting, YOU reconcile the delta specs against what was actually built, so the
|
|
14
14
|
specs that become canonical describe reality, not just the original draft.
|
|
15
15
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Promote
|
|
2
2
|
|
|
3
|
-
Run the `
|
|
3
|
+
Run the `lawbook_change` (action: sync) tool for the change. It copies each
|
|
4
4
|
`lawbook/changes/<name>/specs/<capability>/spec.md` over the canonical
|
|
5
5
|
`lawbook/specs/<capability>/spec.md` and reports what it promoted, flagging
|
|
6
6
|
each as **created** (new capability) or **updated** (overwrote an existing
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { gatherSignals, loadCeremonyConfig, proposeLevel, setCeremonyLevel, writeCeremonyRecord, } from "./levels.js";
|
|
4
|
+
export const BUGFIX_HEADINGS = [
|
|
5
|
+
"1. Observed symptom",
|
|
6
|
+
"2. Minimal reproduction",
|
|
7
|
+
"3. Root cause",
|
|
8
|
+
"4. Blast radius",
|
|
9
|
+
"5. Proposed fix",
|
|
10
|
+
"6. Regression test",
|
|
11
|
+
"7. Prevention",
|
|
12
|
+
];
|
|
13
|
+
/** Parsed section bodies keyed by heading label. */
|
|
14
|
+
export function parseBugfixSections(content) {
|
|
15
|
+
const out = new Map();
|
|
16
|
+
for (const part of content.split(/^##\s+/m).slice(1)) {
|
|
17
|
+
const nl = part.indexOf("\n");
|
|
18
|
+
if (nl < 0) {
|
|
19
|
+
out.set(part.trim(), "");
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
out.set(part.slice(0, nl).trim(), part.slice(nl + 1).trim());
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function sectionBody(sections, key) {
|
|
27
|
+
for (const [h, b] of sections) {
|
|
28
|
+
if (h.toLowerCase().startsWith(key.toLowerCase()))
|
|
29
|
+
return b;
|
|
30
|
+
}
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
function isFilled(body) {
|
|
34
|
+
const t = body.trim();
|
|
35
|
+
if (!t)
|
|
36
|
+
return false;
|
|
37
|
+
if (/^n\/a\s*:/i.test(t))
|
|
38
|
+
return true;
|
|
39
|
+
return t.length > 2;
|
|
40
|
+
}
|
|
41
|
+
/** True when prevention says a canonical requirement was missing. */
|
|
42
|
+
export function preventionRequiresDelta(content) {
|
|
43
|
+
const prev = sectionBody(parseBugfixSections(content), "7. Prevention");
|
|
44
|
+
if (!prev)
|
|
45
|
+
return false;
|
|
46
|
+
return (/\b(requirement|spec)\b.*\b(miss|missing|absent|incomplete|add|update)\b/i.test(prev) ||
|
|
47
|
+
/\bfaltaba\b/i.test(prev) ||
|
|
48
|
+
/\bmissing requirement\b/i.test(prev));
|
|
49
|
+
}
|
|
50
|
+
/** Infer archive resolution from bugfix.md content. */
|
|
51
|
+
export function inferBugResolution(content) {
|
|
52
|
+
if (/\bnot-a-bug\b/i.test(content) || /resolution:\s*not-a-bug/i.test(content)) {
|
|
53
|
+
return "not-a-bug";
|
|
54
|
+
}
|
|
55
|
+
const repro = sectionBody(parseBugfixSections(content), "2. Minimal reproduction");
|
|
56
|
+
if (/unreproducible\s*:/i.test(repro))
|
|
57
|
+
return "mitigated";
|
|
58
|
+
return "fixed";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Validate bugfix.md sections for a ceremony level.
|
|
62
|
+
*
|
|
63
|
+
* @returns Human-readable issues (empty when valid).
|
|
64
|
+
*/
|
|
65
|
+
export function validateBugfixContent(level, content) {
|
|
66
|
+
const issues = [];
|
|
67
|
+
const sections = parseBugfixSections(content);
|
|
68
|
+
for (const h of BUGFIX_HEADINGS) {
|
|
69
|
+
if (!sections.has(h) && ![...sections.keys()].some((k) => k.startsWith(h.split(".")[0]))) {
|
|
70
|
+
issues.push(`bugfix.md missing heading "## ${h}"`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const repro = sectionBody(sections, "2. Minimal reproduction");
|
|
74
|
+
if (!isFilled(repro) && !/unreproducible\s*:/i.test(repro)) {
|
|
75
|
+
issues.push("bugfix.md §2 requires reproduction steps or an `unreproducible:` block");
|
|
76
|
+
}
|
|
77
|
+
const requiredAt0 = [
|
|
78
|
+
"1. Observed symptom",
|
|
79
|
+
"2. Minimal reproduction",
|
|
80
|
+
"3. Root cause",
|
|
81
|
+
"5. Proposed fix",
|
|
82
|
+
"6. Regression test",
|
|
83
|
+
];
|
|
84
|
+
const optionalAt0 = ["4. Blast radius", "7. Prevention"];
|
|
85
|
+
const allRequired = level >= 1 ? [...BUGFIX_HEADINGS] : requiredAt0;
|
|
86
|
+
for (const key of allRequired) {
|
|
87
|
+
const body = sectionBody(sections, key);
|
|
88
|
+
if (!isFilled(body) && !/unreproducible\s*:/i.test(body)) {
|
|
89
|
+
issues.push(`bugfix.md §${key.split(".")[0]} (${key}) is empty`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (level === 0) {
|
|
93
|
+
for (const key of optionalAt0) {
|
|
94
|
+
const body = sectionBody(sections, key);
|
|
95
|
+
if (body && !isFilled(body) && !/^n\/a\s*:/i.test(body)) {
|
|
96
|
+
issues.push(`bugfix.md §${key.split(".")[0]} must be filled or start with \`n/a:\``);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const prevention = sectionBody(sections, "7. Prevention");
|
|
101
|
+
const resolution = inferBugResolution(content);
|
|
102
|
+
if (resolution === "not-a-bug" && !isFilled(prevention)) {
|
|
103
|
+
issues.push("not-a-bug resolution requires a prevention entry (usually a spec clarity fix)");
|
|
104
|
+
}
|
|
105
|
+
else if (level >= 1 && !isFilled(prevention)) {
|
|
106
|
+
issues.push("bugfix.md §7 Prevention must be answered (law, spec gap, or explicit none with reason)");
|
|
107
|
+
}
|
|
108
|
+
const regression = sectionBody(sections, "6. Regression test");
|
|
109
|
+
const mitigated = /unreproducible\s*:/i.test(repro);
|
|
110
|
+
if (!mitigated && !isFilled(regression)) {
|
|
111
|
+
issues.push("bugfix.md §6 Regression test is required unless reproduction is unreproducible");
|
|
112
|
+
}
|
|
113
|
+
if (mitigated && !isFilled(regression) && !/\binstrument/i.test(content)) {
|
|
114
|
+
issues.push("unreproducible bugs require instrumentation (§6 or explicit instrumentation reference)");
|
|
115
|
+
}
|
|
116
|
+
return issues;
|
|
117
|
+
}
|
|
118
|
+
function bugfixTemplate(name, level, seed) {
|
|
119
|
+
const symptom = seed?.inputSymptom ??
|
|
120
|
+
"<What you see: error message, wrong value, screenshot reference. Do not interpret yet.>";
|
|
121
|
+
const root = seed?.suspects?.[0] != null
|
|
122
|
+
? `${seed.suspects[0].name} (${seed.suspects[0].file}:${seed.suspects[0].startLine}) **(candidate — verify)**`
|
|
123
|
+
: "<symbol (file:line) — must resolve against the graph>";
|
|
124
|
+
const blast = seed?.blastRadiusSummary ??
|
|
125
|
+
"<Run compass_impact on the confirmed root cause; list modules and call sites.>";
|
|
126
|
+
return `# Bugfix: ${name}
|
|
127
|
+
|
|
128
|
+
**Level:** ${level} · **Type:** bug · **Severity:** normal
|
|
129
|
+
|
|
130
|
+
## 1. Observed symptom
|
|
131
|
+
${symptom}
|
|
132
|
+
|
|
133
|
+
## 2. Minimal reproduction
|
|
134
|
+
<!-- Steps to reproduce, or \`unreproducible: <reason and what was tried>\` -->
|
|
135
|
+
|
|
136
|
+
## 3. Root cause
|
|
137
|
+
${root}
|
|
138
|
+
|
|
139
|
+
## 4. Blast radius
|
|
140
|
+
${blast}
|
|
141
|
+
|
|
142
|
+
## 5. Proposed fix
|
|
143
|
+
<!-- The change; note discarded alternatives if any. -->
|
|
144
|
+
|
|
145
|
+
## 6. Regression test
|
|
146
|
+
<!-- test/path.test.ts::case name — must fail BEFORE the fix -->
|
|
147
|
+
|
|
148
|
+
## 7. Prevention
|
|
149
|
+
<!-- New law (executable-laws block), missing spec requirement, or "none: <reason>" -->
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Scaffold a bug change: `bugfix.md`, `change.json`, and `reports/`.
|
|
154
|
+
*/
|
|
155
|
+
export function scaffoldBugfix(projectPath, name, opts = {}) {
|
|
156
|
+
const changeDir = path.join(projectPath, "lawbook", "changes", name);
|
|
157
|
+
if (fs.existsSync(changeDir)) {
|
|
158
|
+
throw new Error(`change "${name}" already exists under lawbook/changes/`);
|
|
159
|
+
}
|
|
160
|
+
const targets = opts.targets ?? { paths: [], symbols: [] };
|
|
161
|
+
const { thresholds } = loadCeremonyConfig(projectPath);
|
|
162
|
+
const signals = gatherSignals(projectPath, targets, thresholds);
|
|
163
|
+
const proposal = proposeLevel(signals, thresholds);
|
|
164
|
+
const level = opts.level ?? (proposal.level !== null && proposal.level <= 1 ? proposal.level : 1);
|
|
165
|
+
fs.mkdirSync(path.join(changeDir, "reports"), { recursive: true });
|
|
166
|
+
fs.writeFileSync(path.join(changeDir, "bugfix.md"), bugfixTemplate(name, level, opts.seed));
|
|
167
|
+
fs.writeFileSync(path.join(changeDir, "reports", "README.md"), `# Reports — ${name}\n\nBug reports MUST include the regression test **failing before the fix**.\n`);
|
|
168
|
+
if (level >= 1) {
|
|
169
|
+
fs.writeFileSync(path.join(changeDir, "tasks.md"), `- [ ] Reproduce and confirm root cause\n- [ ] Implement fix\n- [ ] Add regression test (red before, green after)\n- [ ] Complete prevention §7\n- [ ] Write discipline report under reports/\n`);
|
|
170
|
+
}
|
|
171
|
+
if (level >= 2) {
|
|
172
|
+
fs.writeFileSync(path.join(changeDir, "design.md"), `# Design — ${name}\n\n## Approach\n\n(structural bugfix — document the fix architecture)\n`);
|
|
173
|
+
}
|
|
174
|
+
const record = setCeremonyLevel(projectPath, name, {
|
|
175
|
+
proposal,
|
|
176
|
+
level,
|
|
177
|
+
confirmedBy: "human",
|
|
178
|
+
});
|
|
179
|
+
const updated = { ...record, changeType: "bug" };
|
|
180
|
+
writeCeremonyRecord(projectPath, name, updated);
|
|
181
|
+
return { change: name, proposal, record: updated, dir: changeDir };
|
|
182
|
+
}
|
|
183
|
+
/** Read change type from an archived folder path. */
|
|
184
|
+
export function readChangeTypeFromDir(changeDir) {
|
|
185
|
+
const p = path.join(changeDir, "change.json");
|
|
186
|
+
if (!fs.existsSync(p))
|
|
187
|
+
return "feature";
|
|
188
|
+
try {
|
|
189
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
190
|
+
return raw.changeType === "bug" ? "bug" : "feature";
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return "feature";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { specInit, specValidate, specSync, specArchive, specList } from "./engine.js";
|
|
3
|
+
import { handleLevel } from "./quick.js";
|
|
4
|
+
import { buildCoverageReport, loadCoverageConfig, renderCoverageAgent } from "./coverage.js";
|
|
5
|
+
import { buildDriftReport, renderDriftAgent } from "./drift.js";
|
|
6
|
+
export const lawbookChangeActions = [
|
|
7
|
+
"init",
|
|
8
|
+
"list",
|
|
9
|
+
"validate",
|
|
10
|
+
"sync",
|
|
11
|
+
"archive",
|
|
12
|
+
"level",
|
|
13
|
+
"coverage",
|
|
14
|
+
"drift",
|
|
15
|
+
];
|
|
16
|
+
export const lawbookChangeSchema = {
|
|
17
|
+
projectPath: z.string(),
|
|
18
|
+
action: z.enum(lawbookChangeActions),
|
|
19
|
+
change: z.string().optional(),
|
|
20
|
+
date: z
|
|
21
|
+
.string()
|
|
22
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
23
|
+
.optional(),
|
|
24
|
+
mode: z.enum(["propose", "set", "promote", "explain"]).optional(),
|
|
25
|
+
paths: z.array(z.string()).optional(),
|
|
26
|
+
symbols: z.array(z.string()).optional(),
|
|
27
|
+
level: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]).optional(),
|
|
28
|
+
reason: z.string().optional(),
|
|
29
|
+
onlyDefects: z.boolean().optional(),
|
|
30
|
+
json: z.boolean().optional(),
|
|
31
|
+
capability: z.string().optional(),
|
|
32
|
+
includeReverse: z.boolean().optional(),
|
|
33
|
+
maxItems: z.number().int().min(1).max(50).optional(),
|
|
34
|
+
};
|
|
35
|
+
function requireField(args, field) {
|
|
36
|
+
const v = args[field];
|
|
37
|
+
if (typeof v === "string" && v.length > 0)
|
|
38
|
+
return v;
|
|
39
|
+
throw new Error(`lawbook_change: action '${args.action}' requires '${String(field)}'`);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Dispatch `lawbook_change` by action.
|
|
43
|
+
*
|
|
44
|
+
* @param args - Unified lawbook lifecycle arguments.
|
|
45
|
+
*/
|
|
46
|
+
export function handleLawbookChange(args) {
|
|
47
|
+
switch (args.action) {
|
|
48
|
+
case "init":
|
|
49
|
+
return specInit(args.projectPath);
|
|
50
|
+
case "list":
|
|
51
|
+
return specList(args.projectPath);
|
|
52
|
+
case "validate":
|
|
53
|
+
return specValidate(args.projectPath, requireField(args, "change"));
|
|
54
|
+
case "sync":
|
|
55
|
+
return specSync(args.projectPath, requireField(args, "change"));
|
|
56
|
+
case "archive":
|
|
57
|
+
return specArchive(args.projectPath, requireField(args, "change"), requireField(args, "date"));
|
|
58
|
+
case "level":
|
|
59
|
+
if (!args.mode)
|
|
60
|
+
throw new Error(`lawbook_change: action 'level' requires 'mode'`);
|
|
61
|
+
return handleLevel({
|
|
62
|
+
projectPath: args.projectPath,
|
|
63
|
+
mode: args.mode,
|
|
64
|
+
change: args.change,
|
|
65
|
+
paths: args.paths,
|
|
66
|
+
symbols: args.symbols,
|
|
67
|
+
level: args.level,
|
|
68
|
+
reason: args.reason,
|
|
69
|
+
});
|
|
70
|
+
case "coverage": {
|
|
71
|
+
const cfg = loadCoverageConfig(args.projectPath);
|
|
72
|
+
const report = buildCoverageReport(args.projectPath, { change: args.change, cfg });
|
|
73
|
+
if (args.json)
|
|
74
|
+
return report;
|
|
75
|
+
return renderCoverageAgent(report, args.onlyDefects !== false);
|
|
76
|
+
}
|
|
77
|
+
case "drift": {
|
|
78
|
+
const report = buildDriftReport(args.projectPath, {
|
|
79
|
+
capability: args.capability,
|
|
80
|
+
reverse: args.includeReverse === true,
|
|
81
|
+
failOn: "semantic",
|
|
82
|
+
});
|
|
83
|
+
if (args.json)
|
|
84
|
+
return report;
|
|
85
|
+
return renderDriftAgent(report, args.maxItems ?? 10);
|
|
86
|
+
}
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`lawbook_change: unknown action '${String(args.action)}'`);
|
|
89
|
+
}
|
|
90
|
+
}
|