@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.
Files changed (30) hide show
  1. package/dist/cli/commands/lawbook.js +43 -6
  2. package/dist/cli/commands/query.js +49 -2
  3. package/dist/cli/commands/quick.js +35 -0
  4. package/dist/cli/commands/update.js +18 -0
  5. package/dist/cli/index.js +17 -3
  6. package/dist/modules/compass/db.js +15 -2
  7. package/dist/modules/compass/extract.js +44 -0
  8. package/dist/modules/compass/git-history-cache.js +19 -2
  9. package/dist/modules/compass/hotspots.js +230 -0
  10. package/dist/modules/compass/indexer.js +2 -0
  11. package/dist/modules/compass/languages.js +39 -0
  12. package/dist/modules/compass/register.js +17 -0
  13. package/dist/modules/foundation/doctor.js +63 -0
  14. package/dist/modules/lawbook/assets/commands/archive.md +5 -6
  15. package/dist/modules/lawbook/assets/commands/draft.md +6 -7
  16. package/dist/modules/lawbook/assets/commands/quick.md +14 -0
  17. package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +4 -3
  18. package/dist/modules/lawbook/assets/skills/draft/SKILL.md +1 -1
  19. package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +3 -0
  20. package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +28 -25
  21. package/dist/modules/lawbook/assets/skills/quick/SKILL.md +11 -0
  22. package/dist/modules/lawbook/assets/skills/quick/steps/01-scaffold.md +6 -0
  23. package/dist/modules/lawbook/assets/skills/quick/steps/02-implement.md +7 -0
  24. package/dist/modules/lawbook/engine.js +115 -55
  25. package/dist/modules/lawbook/levels.js +421 -0
  26. package/dist/modules/lawbook/quick.js +86 -0
  27. package/dist/modules/lawbook/register.js +10 -0
  28. package/dist/shared/exposure.js +3 -0
  29. package/dist/shared/git-history.js +85 -5
  30. 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 into canonical, then archive it.
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 is done and gates are green, run
8
- the reconciliation review (recommend a sync with short insights if the code
9
- drifted past the contracts), run `lawbook_validate`, then `lawbook_archive` with
10
- today's date (YYYY-MM-DD). It syncs the specs and moves the change to
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 (proposal, delta specs, tasks) before coding.
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 the
8
- code with `compass_explore`/`compass_recall`, read the governing
9
- `docs/standards/`, then write `proposal.md`, `specs/<capability>/spec.md`
10
- (normative `SHALL`/`MUST` + `#### Scenario:`), `design.md`, and
11
- `tasks.md` (with the mandatory steps from `lawbook/config.yaml`). Finish by
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`, then `lawbook_sync` to promote the delta specs into
4
- `lawbook/specs/`. This is required, not optional: `lawbook_archive` refuses
5
- unless the canonical specs already match the delta specs.
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 — proposal, delta specs, and tasks — 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).
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
- Create under `lawbook/changes/<name>/`:
4
-
5
- - **proposal.md** the why, the what, non-goals, and whether migrations are
6
- needed. Reference the team's tracker ticket if there is one.
7
- - **specs/<capability>/spec.md** the delta spec for each affected capability.
8
- `sync` promotes this by overwriting the whole canonical file, so the delta must
9
- carry the capability's **full** intended spec. When you are updating an existing
10
- capability, **start from the current `lawbook/specs/<capability>/spec.md`** and
11
- edit on top of it, so its existing requirements are carried forward — do not
12
- author it from scratch, or promotion will silently drop them. Use normative
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
- - **design.md** — always: approach, alternatives weighed, and the trade-offs
26
- behind the decision. For a small change, keep it short but write it.
27
- - **tasks.md** ordered, checkable steps. MUST include the mandatory steps
28
- from `lawbook/config.yaml` (feature branch first; tests reviewed and run;
29
- manual verification executed by the agent; discipline reports produced; docs
30
- updated; archive within the PR).
31
- - **reports/** create the folder with a short `reports/README.md` naming the
32
- discipline reports the change will need — one per discipline it touches, from an
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,6 @@
1
+ # Scaffold the level-0 change
2
+
3
+ Run `speclaw quick <name>` (or the library scaffold) so `record.md`,
4
+ `change.json`, and `reports/` exist. Pass `--path` / `--symbol` when known.
5
+
6
+ Next: read `steps/02-implement.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.