@esneiderbravo/speclaw 0.1.1 → 0.1.3

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 (36) hide show
  1. package/README.md +12 -8
  2. package/dist/cli/commands/init.js +16 -26
  3. package/dist/cli/commands/{spec.js → lawbook.js} +5 -5
  4. package/dist/cli/commands/visualize.js +44 -0
  5. package/dist/cli/index.js +11 -8
  6. package/dist/modules/compass/register.js +10 -0
  7. package/dist/modules/compass/visualize.js +218 -0
  8. package/dist/modules/foundation/assets/AGENTS.template.md +4 -4
  9. package/dist/modules/foundation/assets/CLAUDE.template.md +3 -3
  10. package/dist/modules/foundation/assets/LAWS.template.md +3 -3
  11. package/dist/modules/foundation/assets/docs/standards/base-standards.template.md +10 -5
  12. package/dist/modules/foundation/assets/docs/standards/conventions.template.md +12 -9
  13. package/dist/modules/foundation/assets/docs/standards/{spec-workflow.template.md → lawbook.template.md} +8 -8
  14. package/dist/modules/foundation/assets/docs/standards/testing-standards.template.md +1 -1
  15. package/dist/modules/foundation/doctor.js +7 -7
  16. package/dist/modules/foundation/register.js +6 -8
  17. package/dist/modules/foundation/scaffold.js +2 -3
  18. package/dist/modules/{spec → lawbook}/assets/commands/archive.md +2 -2
  19. package/dist/modules/{spec → lawbook}/assets/commands/draft.md +4 -4
  20. package/dist/modules/{spec → lawbook}/assets/commands/explore.md +1 -1
  21. package/dist/modules/{spec → lawbook}/assets/commands/sync.md +2 -2
  22. package/dist/modules/{spec → lawbook}/assets/rules/spec-tasks-mandatory-steps.md +6 -6
  23. package/dist/modules/{spec → lawbook}/assets/skills/archive/SKILL.md +7 -7
  24. package/dist/modules/{spec → lawbook}/assets/skills/build/SKILL.md +3 -3
  25. package/dist/modules/{spec → lawbook}/assets/skills/draft/SKILL.md +8 -8
  26. package/dist/modules/{spec → lawbook}/assets/skills/explore/SKILL.md +2 -2
  27. package/dist/modules/lawbook/assets/skills/sync/SKILL.md +21 -0
  28. package/dist/modules/{spec → lawbook}/engine.js +17 -17
  29. package/dist/modules/{spec → lawbook}/register.js +12 -12
  30. package/dist/modules/tools/assets/packs/agents/backend-developer.md +1 -1
  31. package/dist/modules/tools/assets/packs/agents/frontend-developer.md +1 -1
  32. package/dist/modules/tools/register.js +1 -1
  33. package/dist/server.js +2 -2
  34. package/package.json +2 -2
  35. package/dist/modules/spec/assets/skills/sync/SKILL.md +0 -21
  36. /package/dist/modules/{spec → lawbook}/assets/commands/build.md +0 -0
package/README.md CHANGED
@@ -37,17 +37,21 @@ One command. No cloud, no LLM, no API keys — <b>everything runs on your machin
37
37
 
38
38
  ## ◆ Quick start
39
39
 
40
+ In your project root:
41
+
42
+ ```bash
43
+ npx @esneiderbravo/speclaw init
44
+ ```
45
+
40
46
  <p align="center">
41
47
  <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-quickstart.png" width="800" alt="npx @esneiderbravo/speclaw init">
42
48
  </p>
43
49
 
44
- <p align="center">Copy &amp; run in your project root: <code>npx @esneiderbravo/speclaw init</code></p>
45
-
46
50
  `init` will:
47
51
 
48
52
  1. **Ask which agents you use** (Claude Code, Cursor, Codex, …) — and configure
49
53
  only those. Add more later; nothing is forced on you.
50
- 2. Write the **foundation** (constitution + standards) and the **spec workflow**.
54
+ 2. Write the **foundation** (constitution + standards) and the **lawbook workflow**.
51
55
  3. **Index your code** with a live progress bar and a summary of what it found.
52
56
  4. Register the speclaw **MCP server** in each chosen agent's config.
53
57
  5. Print a prompt to paste into your agent so it fills the constitution with your
@@ -71,12 +75,12 @@ Works with `npm`, `pnpm` (`pnpm dlx @esneiderbravo/speclaw init`), and `yarn`.
71
75
 
72
76
  | Module | What it does |
73
77
  | :-- | :-- |
74
- | **Foundation** | The project's constitution: `LAWS.md` binding a set of granular standards under `docs/standards/` (base, architecture, backend, frontend, testing, documentation, conventions, spec-workflow), plus strict `CLAUDE.md` / `AGENTS.md` agent contracts — filled from your real codebase. |
78
+ | **Foundation** | The project's constitution: `LAWS.md` binding a set of granular standards under `docs/standards/` (base, architecture, backend, frontend, testing, documentation, conventions, lawbook), plus strict `CLAUDE.md` / `AGENTS.md` agent contracts — filled from your real codebase. |
75
79
  | **Compass** | speclaw's own local code graph. Parses your code (tree-sitter) into nodes + edges plus a local vector store, so an agent finds and understands code with a fraction of the tokens a grep/read loop would cost. No LLM, 100% local, lives in `.speclaw/` (gitignored). |
76
- | **Spec** | speclaw's own spec-driven workflow: `draft → build → sync → archive` (and `explore`), backed by `spec_*` engine tools. No external CLI. |
80
+ | **Lawbook** | speclaw's own spec-driven workflow: `draft → build → sync → archive` (and `explore`), backed by `lawbook_*` engine tools. No external CLI. |
77
81
  | **Tools** | Opt-in packs of skills and subagents (currently the dev-agents) that agents use for specific tasks. |
78
82
 
79
- Compass is inspired by [CodeGraph](https://github.com/colbymchenry/codegraph) and the Spec module by [OpenSpec](https://github.com/Fission-AI/openspec) — both MIT. speclaw reimplements the ideas as its own code and gives full credit; see [ATTRIBUTION.md](ATTRIBUTION.md).
83
+ Compass is inspired by [CodeGraph](https://github.com/colbymchenry/codegraph) and the Lawbook module by [OpenSpec](https://github.com/Fission-AI/openspec) — both MIT. speclaw reimplements the ideas as its own code and gives full credit; see [ATTRIBUTION.md](ATTRIBUTION.md).
80
84
 
81
85
  <br/>
82
86
 
@@ -85,7 +89,7 @@ Compass is inspired by [CodeGraph](https://github.com/colbymchenry/codegraph) an
85
89
  speclaw meets you where you are. Everything works through the **CLI** — so no one
86
90
  is blocked by MCP setup — and the same capabilities are exposed as **MCP tools**
87
91
  for a smoother, integrated experience once configured. An agent without MCP can
88
- still use Compass and the spec engine by calling the CLI from its shell.
92
+ still use Compass and the lawbook engine by calling the CLI from its shell.
89
93
 
90
94
  <p align="center"><b>CLI</b> — the installer &amp; operator, runs anywhere <code>node</code> does</p>
91
95
  <p align="center"><img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-cli.png" width="800" alt="speclaw CLI commands"></p>
@@ -110,7 +114,7 @@ still use Compass and the spec engine by calling the CLI from its shell.
110
114
  > of AI coding agents isn't lack of capability — it's working without the project's
111
115
  > tacit knowledge: the rules the team actually lives by. speclaw makes that
112
116
  > knowledge explicit, executable, and binding, and gives agents a local map
113
- > (Compass) and a disciplined workflow (Spec) to act on it — without burning tokens.
117
+ > (Compass) and a disciplined workflow (Lawbook) to act on it — without burning tokens.
114
118
 
115
119
  <br/>
116
120
 
@@ -2,12 +2,12 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import * as clack from "@clack/prompts";
4
4
  import { scaffold } from "../../modules/foundation/scaffold.js";
5
- import { specInit } from "../../modules/spec/engine.js";
5
+ import { specInit } from "../../modules/lawbook/engine.js";
6
6
  import { buildIndex } from "../../modules/compass/indexer.js";
7
7
  import { AGENTS, agentById } from "../../shared/agents.js";
8
8
  import { loadPacks } from "../../modules/tools/packs.js";
9
9
  import { list } from "../lib/args.js";
10
- import { ui, c, banner, renderProgress, clearProgress, box } from "../lib/ui.js";
10
+ import { ui, c, banner, renderProgress, clearProgress } from "../lib/ui.js";
11
11
  const PACK_LABELS = {
12
12
  agents: "dev-agents (backend · frontend · product)",
13
13
  };
@@ -26,8 +26,7 @@ function detectProjectName(cwd) {
26
26
  * Interactive setup: pick agents and packs, scaffold, index, and print the handoff prompt.
27
27
  *
28
28
  * @param flags - Parsed flags; runs interactively on a TTY unless `--agents`, `--yes`, or
29
- * `-y` is set. Honors `--project-name`, `--ticket-prefix`, `--team-language`, `--packs`,
30
- * and `--no-index`.
29
+ * `-y` is set. Honors `--project-name`, `--packs`, and `--no-index`.
31
30
  */
32
31
  export async function runInit(flags) {
33
32
  const cwd = process.cwd();
@@ -35,8 +34,6 @@ export async function runInit(flags) {
35
34
  const interactive = Boolean(process.stdin.isTTY) && !flags.agents && !flags.yes && !flags.y;
36
35
  let agents;
37
36
  let packs;
38
- let ticketPrefix = flags["ticket-prefix"];
39
- let teamLanguage = flags["team-language"];
40
37
  banner();
41
38
  if (interactive) {
42
39
  const answers = await clack.group({
@@ -56,13 +53,9 @@ export async function runInit(flags) {
56
53
  initialValues: ["agents"],
57
54
  required: false,
58
55
  }),
59
- ticketPrefix: () => clack.text({ message: "Ticket prefix (optional, e.g. FAR)", defaultValue: "" }),
60
- teamLanguage: () => clack.text({ message: "Team communication language", defaultValue: "English" }),
61
56
  }, { onCancel: () => process.exit(1) });
62
57
  agents = answers.agents;
63
58
  packs = answers.packs;
64
- ticketPrefix = answers.ticketPrefix || undefined;
65
- teamLanguage = answers.teamLanguage || undefined;
66
59
  }
67
60
  else {
68
61
  agents = list(flags.agents).length ? list(flags.agents) : ["claude"];
@@ -73,20 +66,16 @@ export async function runInit(flags) {
73
66
  ui.err(`Unknown agent(s): ${unknownAgents.join(", ")}. Known: ${AGENTS.map((a) => a.id).join(", ")}`);
74
67
  process.exit(1);
75
68
  }
76
- const profile = {
77
- project_name: projectName,
78
- ...(ticketPrefix ? { ticket_prefix: ticketPrefix } : {}),
79
- ...(teamLanguage ? { team_language: teamLanguage } : {}),
80
- };
69
+ const profile = { project_name: projectName };
81
70
  // 1. Content + chosen agents, with a check per piece installed
82
71
  ui.step(`Setting up ${c.bold(c.cyan(projectName))}`);
83
72
  scaffold(cwd, profile, packs, agents);
84
73
  ui.ok(`Foundation ${c.muted("— LAWS.md + 8 standards + CLAUDE.md/AGENTS.md")}`);
85
- ui.ok(`Spec workflow ${c.muted("— draft · build · sync · archive · explore")}`);
74
+ ui.ok(`Lawbook workflow ${c.muted("— draft · build · sync · archive · explore")}`);
86
75
  for (const p of packs)
87
76
  ui.ok(`${PACK_LABELS[p] ?? p + " pack"}`);
88
77
  specInit(cwd);
89
- ui.ok(`Spec workspace ${c.muted("— spec/")}`);
78
+ ui.ok(`Lawbook workspace ${c.muted("— lawbook/")}`);
90
79
  ui.step("Configuring agents");
91
80
  for (const id of agents)
92
81
  ui.ok(`${agentById(id).label} ${c.muted("— symlinks + MCP")}`);
@@ -100,19 +89,20 @@ export async function runInit(flags) {
100
89
  c.bold(c.cream(String(stats.edges))) + c.muted(" edges · ") +
101
90
  c.bold(c.cream(String(stats.embeddings))) + c.muted(" embeddings"));
102
91
  }
103
- // 3. Handoff prompt for the chosen agent
92
+ // 3. Handoff prompt for the chosen agent — printed as a single flush-left
93
+ // line so it copy-pastes cleanly (no borders, no wrapping artifacts).
104
94
  const primary = agentById(agents[0]);
105
95
  ui.step("You're set — one last step");
106
- ui.info(`Paste this into ${c.cyan(primary.label)} to build your constitution:`);
107
96
  ui.plain();
108
- box([
109
- "Complete speclaw's foundation: analyze this repo and fill",
110
- "LAWS.md and docs/standards/* with the real architecture,",
111
- "quality gates and conventions. Use init_project to start.",
112
- ], "prompt");
97
+ ui.info(`Copy this and paste it into ${c.cyan(primary.label)}:`);
113
98
  ui.plain();
114
- ui.info(`The dev-agents read those standards for your stack filling them well`);
115
- ui.info(`makes them stack-aware.`);
99
+ console.log(c.cream("Complete speclaw's foundation: analyze this repo and fill LAWS.md and " +
100
+ "docs/standards/* with its real architecture, quality gates and conventions. " +
101
+ "Infer the working language and conventions from the repo itself — docstrings, " +
102
+ "commit messages, branch names, PR and ticket language — don't assume. " +
103
+ "Start with init_project."));
104
+ ui.plain();
105
+ ui.info(`The dev-agents read those standards for your stack — filling them well makes them stack-aware.`);
116
106
  ui.plain();
117
107
  ui.info(`Add an agent: ${ui.code("speclaw agent add cursor")}`);
118
108
  ui.info(`Refresh index: ${ui.code("speclaw index")}`);
@@ -1,4 +1,4 @@
1
- import { specInit, specValidate, specSync, specArchive, specList } from "../../modules/spec/engine.js";
1
+ import { specInit, specValidate, specSync, specArchive, specList } from "../../modules/lawbook/engine.js";
2
2
  import { ui } from "../lib/ui.js";
3
3
  function today() {
4
4
  // The MCP path passes the date in; the CLL runs on a real machine, so read it here.
@@ -17,14 +17,14 @@ export async function runSpec(flags) {
17
17
  switch (sub) {
18
18
  case "init": {
19
19
  const r = specInit(cwd);
20
- ui.ok(r.alreadyExisted ? "spec/ already present" : `spec/ created (${r.created.length} entries)`);
20
+ ui.ok(r.alreadyExisted ? "lawbook/ already present" : `lawbook/ created (${r.created.length} entries)`);
21
21
  return;
22
22
  }
23
23
  case "list": {
24
24
  const r = specList(cwd);
25
25
  if (!r.initialized)
26
- return ui.warn("No spec/ — run `speclaw spec init`.");
27
- ui.heading("Spec workspace");
26
+ return ui.warn("No lawbook/ — run `speclaw lawbook init`.");
27
+ ui.heading("Lawbook workspace");
28
28
  ui.info(`active changes: ${r.activeChanges.join(", ") || "none"}`);
29
29
  ui.info(`archived: ${r.archivedChanges.join(", ") || "none"}`);
30
30
  ui.info(`capabilities: ${r.capabilities.join(", ") || "none"}`);
@@ -52,7 +52,7 @@ export async function runSpec(flags) {
52
52
  return;
53
53
  }
54
54
  default:
55
- ui.err("Usage: speclaw spec <init|list|validate|sync|archive> [change]");
55
+ ui.err("Usage: speclaw lawbook <init|list|validate|sync|archive> [change]");
56
56
  process.exit(1);
57
57
  }
58
58
  }
@@ -0,0 +1,44 @@
1
+ import { spawn } from "node:child_process";
2
+ import { visualize } from "../../modules/compass/visualize.js";
3
+ import { ui, c } from "../lib/ui.js";
4
+ /** Open a file in the OS default application (browser for .html). */
5
+ function openInBrowser(file) {
6
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
7
+ try {
8
+ spawn(cmd, [file], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
9
+ }
10
+ catch {
11
+ /* best-effort */
12
+ }
13
+ }
14
+ /**
15
+ * Build an interactive HTML visualization of the Compass graph into
16
+ * .speclaw/graph.html and open it.
17
+ *
18
+ * @param flags - `_[0]` optionally focuses on a node; `--depth`, `--limit`, `--no-open`.
19
+ * @throws Exits the process with code 1 if the index is missing or generation fails.
20
+ */
21
+ export async function runVisualize(flags) {
22
+ const cwd = process.cwd();
23
+ const focus = flags._[0];
24
+ try {
25
+ const r = visualize(cwd, {
26
+ focus,
27
+ depth: flags.depth ? Number(flags.depth) : undefined,
28
+ limit: flags.limit ? Number(flags.limit) : undefined,
29
+ });
30
+ ui.step("Compass graph");
31
+ ui.ok(c.bold(c.cream(String(r.shown))) + c.muted(" nodes · ") +
32
+ c.bold(c.cream(String(r.links))) + c.muted(" edges") +
33
+ (focus ? c.muted(` · focused on ${focus}`) : c.muted(` · top of ${r.total}`)));
34
+ ui.info(`→ ${ui.code(".speclaw/graph.html")}`);
35
+ if (!flags["no-open"]) {
36
+ openInBrowser(r.path);
37
+ ui.info("Opening in your browser…");
38
+ }
39
+ }
40
+ catch (err) {
41
+ ui.err(err.message);
42
+ process.exit(1);
43
+ }
44
+ }
package/dist/cli/index.js CHANGED
@@ -18,13 +18,14 @@ Compass (code intelligence — the same surface agents use via MCP)
18
18
  recall "<query>" Find code by meaning (semantic)
19
19
  impact <node> Blast radius: everything that (transitively) calls it
20
20
  trace <from> <to> A call path between two nodes
21
+ visualize [node] Interactive HTML graph → .speclaw/graph.html
21
22
 
22
- Spec workflow
23
- spec init Create the spec/ workspace
24
- spec list Active/archived changes and capabilities
25
- spec validate <change> Validate a change's artifacts
26
- spec sync <change> Promote delta specs to canonical
27
- spec archive <change> Finalize and archive a change
23
+ Lawbook (spec-driven workflow)
24
+ lawbook init Create the lawbook/ workspace
25
+ lawbook list Active/archived changes and capabilities
26
+ lawbook validate <c> Validate a change's artifacts
27
+ lawbook sync <c> Promote delta specs to canonical
28
+ lawbook archive <c> Finalize and archive a change
28
29
 
29
30
  Other
30
31
  doctor Verify the installation
@@ -61,8 +62,10 @@ async function main() {
61
62
  case "impact":
62
63
  case "trace":
63
64
  return (await import("./commands/query.js")).runQuery(cmd, flags);
64
- case "spec":
65
- return (await import("./commands/spec.js")).runSpec(flags);
65
+ case "visualize":
66
+ return (await import("./commands/visualize.js")).runVisualize(flags);
67
+ case "lawbook":
68
+ return (await import("./commands/lawbook.js")).runSpec(flags);
66
69
  case "doctor":
67
70
  return (await import("./commands/doctor.js")).runDoctor(flags);
68
71
  default:
@@ -3,6 +3,7 @@ import { text } from "../../shared/mcp.js";
3
3
  import { buildIndex } from "./indexer.js";
4
4
  import { explore, search, recall, impact, trace } from "./query.js";
5
5
  import { startWatch, stopWatch, watchStatus } from "./watcher.js";
6
+ import { visualize } from "./visualize.js";
6
7
  // ─── Compass: speclaw's own code-intelligence engine (no external deps) ───
7
8
  // A local graph of the codebase (nodes = definitions, edges = calls/imports)
8
9
  // plus a local vector store for semantic recall. Lives in .speclaw/ (gitignored).
@@ -57,6 +58,15 @@ export function registerCompass(server) {
57
58
  maxDepth: z.number().optional().describe("Max hops to search (default 8)"),
58
59
  },
59
60
  }, async ({ projectPath, from, to, maxDepth }) => text(trace(projectPath, from, to, maxDepth ?? 8)));
61
+ server.registerTool("compass_visualize", {
62
+ description: "Generate an interactive, offline HTML visualization of the code graph into .speclaw/graph.html (gitignored). Nodes are definitions, edges are calls; drag/zoom/hover to explore. Pass a node to focus on its neighborhood, else the most-connected nodes are shown. Requires compass_index.",
63
+ inputSchema: {
64
+ projectPath: z.string().describe("Absolute path to the project"),
65
+ node: z.string().optional().describe("Focus on this node's neighborhood (optional)"),
66
+ depth: z.number().optional().describe("BFS depth around the focus node (default 2)"),
67
+ limit: z.number().optional().describe("Max nodes for the whole-graph view (default 300)"),
68
+ },
69
+ }, async ({ projectPath, node, depth, limit }) => text(visualize(projectPath, { focus: node, depth, limit })));
60
70
  server.registerTool("compass_watch", {
61
71
  description: "Keep the Compass index fresh automatically: start/stop a file watcher that incrementally re-indexes on change (debounced). action=start|stop|status. Optional — the index is also refreshed on demand by compass_index.",
62
72
  inputSchema: {
@@ -0,0 +1,218 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { openDb, indexExists } from "./db.js";
4
+ /**
5
+ * Build the graph payload from the Compass index: nodes (definitions) and the
6
+ * resolved call links between them. With a focus node, returns its neighborhood
7
+ * (BFS both directions); otherwise the most-connected nodes up to `limit`.
8
+ *
9
+ * @throws If no index exists yet (run `compass_index` first).
10
+ */
11
+ export function graphData(projectPath, opts = {}) {
12
+ if (!indexExists(projectPath)) {
13
+ throw new Error("No index found. Run the compass_index tool (or `speclaw index`) first.");
14
+ }
15
+ const { focus, depth = 2, limit = 300 } = opts;
16
+ const db = openDb(projectPath);
17
+ try {
18
+ const allNodes = db
19
+ .prepare(`SELECT n.id, n.name, n.kind, f.path AS file, n.start_line AS line
20
+ FROM nodes n JOIN files f ON f.id = n.file_id`)
21
+ .all();
22
+ const allEdges = db
23
+ .prepare(`SELECT src_node_id AS s, dst_node_id AS t FROM edges
24
+ WHERE kind = 'call' AND dst_node_id IS NOT NULL AND src_node_id IS NOT NULL`)
25
+ .all();
26
+ // adjacency (undirected) + degree
27
+ const adj = new Map();
28
+ const deg = new Map();
29
+ for (const e of allEdges) {
30
+ if (e.s === e.t)
31
+ continue;
32
+ (adj.get(e.s) ?? adj.set(e.s, new Set()).get(e.s)).add(e.t);
33
+ (adj.get(e.t) ?? adj.set(e.t, new Set()).get(e.t)).add(e.s);
34
+ deg.set(e.s, (deg.get(e.s) ?? 0) + 1);
35
+ deg.set(e.t, (deg.get(e.t) ?? 0) + 1);
36
+ }
37
+ let included;
38
+ if (focus) {
39
+ const starts = allNodes.filter((n) => n.name === focus).map((n) => n.id);
40
+ included = new Set(starts);
41
+ let frontier = starts;
42
+ for (let d = 0; d < depth && frontier.length; d++) {
43
+ const next = [];
44
+ for (const id of frontier) {
45
+ for (const nb of adj.get(id) ?? []) {
46
+ if (!included.has(nb)) {
47
+ included.add(nb);
48
+ next.push(nb);
49
+ }
50
+ }
51
+ }
52
+ frontier = next;
53
+ }
54
+ }
55
+ else {
56
+ included = new Set([...allNodes]
57
+ .sort((a, b) => (deg.get(b.id) ?? 0) - (deg.get(a.id) ?? 0))
58
+ .slice(0, limit)
59
+ .map((n) => n.id));
60
+ }
61
+ const nodes = allNodes
62
+ .filter((n) => included.has(n.id))
63
+ .map((n) => ({ ...n, deg: deg.get(n.id) ?? 0 }));
64
+ const links = allEdges.filter((e) => included.has(e.s) && included.has(e.t));
65
+ return { nodes, links, total: allNodes.length, focus: focus ?? null };
66
+ }
67
+ finally {
68
+ db.close();
69
+ }
70
+ }
71
+ /**
72
+ * Build the graph and write it as an interactive HTML page to
73
+ * `<projectPath>/.speclaw/graph.html` (the gitignored index directory).
74
+ *
75
+ * @param projectPath - Absolute path to the project root.
76
+ * @param opts - Focus/depth/limit controlling which nodes are included.
77
+ * @returns The output path plus the shown/link/total counts for reporting.
78
+ * @throws If no Compass index exists yet.
79
+ */
80
+ export function visualize(projectPath, opts = {}) {
81
+ const data = graphData(projectPath, opts);
82
+ const dir = path.join(projectPath, ".speclaw");
83
+ fs.mkdirSync(dir, { recursive: true });
84
+ const out = path.join(dir, "graph.html");
85
+ fs.writeFileSync(out, renderHtml(data));
86
+ return { path: out, shown: data.nodes.length, links: data.links.length, total: data.total };
87
+ }
88
+ /**
89
+ * Render the graph as a self-contained, offline HTML page: the data is embedded
90
+ * as JSON and drawn by an inline canvas force-directed renderer (no CDN, no
91
+ * dependencies).
92
+ *
93
+ * @param data - The nodes/links/totals to embed.
94
+ * @returns A complete HTML document as a string.
95
+ */
96
+ export function renderHtml(data) {
97
+ const payload = JSON.stringify(data);
98
+ return `<!doctype html>
99
+ <html lang="en"><head>
100
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
101
+ <title>speclaw · Compass graph</title>
102
+ <style>
103
+ :root { --cy:#17C1C1; --cr:#F4F1EA; --mu:#6E7B80; --bg:#0B0F10; }
104
+ * { box-sizing:border-box; }
105
+ html,body { margin:0; height:100%; background:var(--bg); color:var(--cr);
106
+ font-family:'SF Mono','JetBrains Mono',Menlo,Consolas,monospace; overflow:hidden; }
107
+ canvas { display:block; cursor:grab; }
108
+ canvas:active { cursor:grabbing; }
109
+ .panel { position:fixed; top:16px; left:16px; padding:12px 14px; border-radius:10px;
110
+ background:rgba(12,17,19,.82); border:1px solid #232A2D; font-size:12px; line-height:1.5; }
111
+ .panel b { color:var(--cr); } .panel .sub { color:var(--mu); }
112
+ .legend { position:fixed; bottom:16px; left:16px; font-size:11px; color:var(--mu);
113
+ background:rgba(12,17,19,.82); border:1px solid #232A2D; border-radius:10px; padding:10px 12px; }
114
+ .legend span { display:inline-flex; align-items:center; margin-right:12px; }
115
+ .legend i { width:9px; height:9px; border-radius:50%; display:inline-block; margin-right:5px; }
116
+ .tip { position:fixed; padding:6px 9px; border-radius:7px; background:#0E1517; border:1px solid #17C1C1;
117
+ color:var(--cr); font-size:12px; pointer-events:none; opacity:0; transition:opacity .1s; white-space:nowrap; }
118
+ .brand { color:var(--cy); font-weight:700; }
119
+ </style></head><body>
120
+ <canvas id="c"></canvas>
121
+ <div class="panel">
122
+ <div><span class="brand">speclaw</span> · Compass graph</div>
123
+ <div class="sub" id="meta"></div>
124
+ <div class="sub">drag to pan · wheel to zoom · drag a node · hover to inspect</div>
125
+ </div>
126
+ <div class="legend" id="legend"></div>
127
+ <div class="tip" id="tip"></div>
128
+ <script>
129
+ const DATA = ${payload};
130
+ const KIND_COLORS = { function:'#17C1C1', method:'#3FB950', class:'#E3B341',
131
+ interface:'#8B989E', type:'#8B989E', enum:'#8B989E' };
132
+ const colorOf = k => KIND_COLORS[k] || '#6E7B80';
133
+
134
+ const cv = document.getElementById('c'), ctx = cv.getContext('2d');
135
+ const tip = document.getElementById('tip');
136
+ document.getElementById('meta').textContent =
137
+ DATA.nodes.length + ' nodes · ' + DATA.links.length + ' edges' +
138
+ (DATA.focus ? ' · focus: ' + DATA.focus : ' · top ' + DATA.nodes.length + ' of ' + DATA.total);
139
+ {
140
+ const kinds = [...new Set(DATA.nodes.map(n=>n.kind))];
141
+ document.getElementById('legend').innerHTML = kinds.map(k =>
142
+ '<span><i style="background:'+colorOf(k)+'"></i>'+k+'</span>').join('');
143
+ }
144
+
145
+ const idx = new Map(DATA.nodes.map((n,i)=>[n.id,i]));
146
+ const N = DATA.nodes.map(n => ({...n, x:(Math.random()-.5)*800, y:(Math.random()-.5)*600, vx:0, vy:0,
147
+ r: 4 + Math.min(10, Math.sqrt(n.deg||0)*2) }));
148
+ const L = DATA.links.map(l => ({s:idx.get(l.s), t:idx.get(l.t)})).filter(l=>l.s!=null&&l.t!=null);
149
+ const neigh = N.map(()=>new Set());
150
+ L.forEach(l => { neigh[l.s].add(l.t); neigh[l.t].add(l.s); });
151
+
152
+ let scale=1, ox=0, oy=0, W=0, H=0, DPR=Math.min(2,devicePixelRatio||1);
153
+ function resize(){ W=innerWidth; H=innerHeight; cv.width=W*DPR; cv.height=H*DPR; cv.style.width=W+'px'; cv.style.height=H+'px'; ctx.setTransform(DPR,0,0,DPR,0,0); }
154
+ addEventListener('resize', resize); resize(); ox=W/2; oy=H/2;
155
+
156
+ // force simulation
157
+ function tick(){
158
+ for(let i=0;i<N.length;i++){ const a=N[i];
159
+ for(let j=i+1;j<N.length;j++){ const b=N[j];
160
+ let dx=a.x-b.x, dy=a.y-b.y, d2=dx*dx+dy*dy||0.01, d=Math.sqrt(d2);
161
+ const f=Math.min(2200/d2, 40); const ux=dx/d, uy=dy/d;
162
+ a.vx+=ux*f; a.vy+=uy*f; b.vx-=ux*f; b.vy-=uy*f;
163
+ }
164
+ a.vx += -a.x*0.0016; a.vy += -a.y*0.0016; // gravity to center
165
+ }
166
+ for(const l of L){ const a=N[l.s], b=N[l.t];
167
+ let dx=b.x-a.x, dy=b.y-a.y, d=Math.sqrt(dx*dx+dy*dy)||0.01;
168
+ const f=(d-90)*0.02, ux=dx/d, uy=dy/d;
169
+ a.vx+=ux*f; a.vy+=uy*f; b.vx-=ux*f; b.vy-=uy*f;
170
+ }
171
+ for(const n of N){ if(n===dragging) continue; n.vx*=0.82; n.vy*=0.82; n.x+=n.vx; n.y+=n.vy; }
172
+ }
173
+ function toScreen(n){ return { x: n.x*scale+ox, y: n.y*scale+oy }; }
174
+
175
+ let hover=-1;
176
+ function draw(){
177
+ ctx.clearRect(0,0,W,H);
178
+ const hoverSet = hover>=0 ? neigh[hover] : null;
179
+ ctx.lineWidth = 1;
180
+ for(const l of L){ const a=toScreen(N[l.s]), b=toScreen(N[l.t]);
181
+ const on = hover>=0 && (l.s===hover||l.t===hover);
182
+ ctx.strokeStyle = on ? 'rgba(23,193,193,.7)' : 'rgba(110,123,128,.16)';
183
+ ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
184
+ }
185
+ for(let i=0;i<N.length;i++){ const n=N[i], p=toScreen(n);
186
+ const dim = hover>=0 && i!==hover && !(hoverSet&&hoverSet.has(i));
187
+ ctx.globalAlpha = dim ? 0.28 : 1;
188
+ ctx.beginPath(); ctx.arc(p.x,p.y,n.r*Math.sqrt(scale),0,7); ctx.fillStyle=colorOf(n.kind); ctx.fill();
189
+ if((n.r>7 || i===hover || (hoverSet&&hoverSet.has(i))) && scale>0.5){
190
+ ctx.globalAlpha = dim?0.4:0.9; ctx.fillStyle='#F4F1EA'; ctx.font='11px monospace';
191
+ ctx.fillText(n.name, p.x+n.r+3, p.y+3);
192
+ }
193
+ }
194
+ ctx.globalAlpha=1;
195
+ }
196
+ function loop(){ tick(); draw(); requestAnimationFrame(loop); } loop();
197
+
198
+ // interaction
199
+ let dragging=null, panning=false, lastX=0, lastY=0;
200
+ function pick(mx,my){ let best=-1, bd=1e9;
201
+ for(let i=0;i<N.length;i++){ const p=toScreen(N[i]); const d=Math.hypot(p.x-mx,p.y-my);
202
+ if(d < N[i].r*Math.sqrt(scale)+6 && d<bd){ bd=d; best=i; } } return best; }
203
+ cv.addEventListener('mousedown', e=>{ const i=pick(e.clientX,e.clientY);
204
+ if(i>=0) dragging=N[i]; else { panning=true; } lastX=e.clientX; lastY=e.clientY; });
205
+ addEventListener('mousemove', e=>{
206
+ if(dragging){ dragging.x += (e.clientX-lastX)/scale; dragging.y += (e.clientY-lastY)/scale; dragging.vx=dragging.vy=0; }
207
+ else if(panning){ ox+=e.clientX-lastX; oy+=e.clientY-lastY; }
208
+ lastX=e.clientX; lastY=e.clientY;
209
+ const i=pick(e.clientX,e.clientY); hover=i;
210
+ if(i>=0){ const n=N[i]; tip.style.opacity=1; tip.style.left=(e.clientX+12)+'px'; tip.style.top=(e.clientY+12)+'px';
211
+ tip.innerHTML='<b>'+n.name+'</b> <span style="color:#6E7B80">'+n.kind+' · '+n.file+':'+n.line+'</span>'; }
212
+ else tip.style.opacity=0;
213
+ });
214
+ addEventListener('mouseup', ()=>{ dragging=null; panning=false; });
215
+ cv.addEventListener('wheel', e=>{ e.preventDefault(); const f=e.deltaY<0?1.1:0.9;
216
+ const mx=e.clientX, my=e.clientY; ox=mx-(mx-ox)*f; oy=my-(my-oy)*f; scale*=f; }, {passive:false});
217
+ </script></body></html>`;
218
+ }
@@ -19,9 +19,9 @@ Claude-specific notes: [`CLAUDE.md`](CLAUDE.md). The law: [`LAWS.md`](LAWS.md).
19
19
  2. **Use Compass before file scanning** — the `compass_explore` /
20
20
  `compass_search` / `compass_recall` tools (run `compass_index` first if the
21
21
  graph is missing). Cheat sheet: [`docs/compass.md`](docs/compass.md).
22
- 3. **Follow the spec workflow** for every non-trivial change; archive
22
+ 3. **Follow the lawbook workflow** for every non-trivial change; archive
23
23
  within the same PR. Rules:
24
- [`docs/standards/spec-workflow.md`](docs/standards/spec-workflow.md).
24
+ [`docs/standards/lawbook.md`](docs/standards/lawbook.md).
25
25
  4. **Run the quality gates yourself** before declaring anything done — see
26
26
  [`docs/standards/testing-standards.md`](docs/standards/testing-standards.md):
27
27
  - Tests: `{{test_commands}}`
@@ -45,7 +45,7 @@ Claude-specific notes: [`CLAUDE.md`](CLAUDE.md). The law: [`LAWS.md`](LAWS.md).
45
45
  | [`docs/standards/testing-standards.md`](docs/standards/testing-standards.md) | Quality gates, testing rules |
46
46
  | [`docs/standards/documentation.md`](docs/standards/documentation.md) | Docstring/API-comment convention per language |
47
47
  | [`docs/standards/conventions.md`](docs/standards/conventions.md) | Branches, PRs, tracker, versioning |
48
- | [`docs/standards/spec-workflow.md`](docs/standards/spec-workflow.md) | Spec-driven workflow, archiving |
48
+ | [`docs/standards/lawbook.md`](docs/standards/lawbook.md) | Spec-driven workflow, archiving |
49
49
  | [`docs/compass.md`](docs/compass.md) | Compass usage |
50
50
 
51
51
  ## Directory map for agents
@@ -57,5 +57,5 @@ Claude-specific notes: [`CLAUDE.md`](CLAUDE.md). The law: [`LAWS.md`](LAWS.md).
57
57
  | `AGENTS.md` / `CLAUDE.md` | Agent entry points (this contract) |
58
58
  | `ai-specs/` | Canonical skills, commands, rules, agents |
59
59
  | `.claude/` `.cursor/` `.codex/` `.agents/` | IDE mirrors (symlinks into `ai-specs/`) |
60
- | `spec/` | Spec-driven workflow: specs, changes, archive |
60
+ | `lawbook/` | Spec-driven workflow: specs, changes, archive |
61
61
  | `.mcp.json` | MCP wiring (speclaw) |
@@ -21,7 +21,7 @@ governs the area you're touching **before** touching it:
21
21
  | Tests / gates | [`docs/standards/testing-standards.md`](docs/standards/testing-standards.md) |
22
22
  | Docstrings / API comments | [`docs/standards/documentation.md`](docs/standards/documentation.md) |
23
23
  | Branches / PRs / tickets | [`docs/standards/conventions.md`](docs/standards/conventions.md) |
24
- | Any non-trivial change | [`docs/standards/spec-workflow.md`](docs/standards/spec-workflow.md) |
24
+ | Any non-trivial change | [`docs/standards/lawbook.md`](docs/standards/lawbook.md) |
25
25
 
26
26
  When any instruction conflicts with a standard, **the standard wins** — and if
27
27
  you believe it is wrong, propose an amendment via a spec change; never silently
@@ -39,8 +39,8 @@ only when the graph doesn't cover what you need.
39
39
 
40
40
  No non-trivial change lands without a spec change (propose → implement →
41
41
  verify → archive). The rules are in
42
- [`docs/standards/spec-workflow.md`](docs/standards/spec-workflow.md);
43
- the workflow skills live in `ai-specs/skills/` and the `/spec` commands wrap
42
+ [`docs/standards/lawbook.md`](docs/standards/lawbook.md);
43
+ the workflow skills live in `ai-specs/skills/` and the `/lawbook` commands wrap
44
44
  them. A change is not done until it is archived — archiving belongs in the PR.
45
45
 
46
46
  ## Rule 3 — Quality gates are non-negotiable
@@ -21,7 +21,7 @@
21
21
  | Testing | [`docs/standards/testing-standards.md`](docs/standards/testing-standards.md) | Quality gates, what must be tested, verification |
22
22
  | Documentation | [`docs/standards/documentation.md`](docs/standards/documentation.md) | Docstring/API-comment convention per language |
23
23
  | Conventions | [`docs/standards/conventions.md`](docs/standards/conventions.md) | Branches, PRs, tracker, versioning |
24
- | Spec | [`docs/standards/spec-workflow.md`](docs/standards/spec-workflow.md) | Spec-driven workflow, mandatory task steps, archiving |
24
+ | Lawbook | [`docs/standards/lawbook.md`](docs/standards/lawbook.md) | Spec-driven workflow, mandatory task steps, archiving |
25
25
  | Compass | [`docs/compass.md`](docs/compass.md) | Using the code knowledge graph before grep |
26
26
 
27
27
  ## Binding rules
@@ -30,8 +30,8 @@
30
30
  the map. Agents open the standard that governs the code they're changing.
31
31
  2. **The standards are enforced, not advisory.** A violation is a blocking
32
32
  finding in review.
33
- 3. **Amendments go through the spec workflow.** A standard is changed like code — via a
34
- reviewed change (see the spec-workflow law). An agent may propose an amendment;
33
+ 3. **Amendments go through the lawbook workflow.** A standard is changed like code — via a
34
+ reviewed change (see the lawbook law). An agent may propose an amendment;
35
35
  it may never silently ignore a standard.
36
36
  4. **Entry points reference the law.** [`CLAUDE.md`](CLAUDE.md) and
37
37
  [`AGENTS.md`](AGENTS.md) point every agent here first.
@@ -5,11 +5,16 @@ of layer or language. This is a law of the project — see [`../../LAWS.md`](../
5
5
 
6
6
  ## Languages
7
7
 
8
+ The working language is **inferred from this repo's own conventions** — the
9
+ language already used in docstrings, commit messages, branch names, and PR/ticket
10
+ bodies. Match what the repo does; do not impose a language it doesn't use.
11
+
8
12
  - **Code, identifiers, comments, docstrings, commit messages, PR titles/bodies,
9
- and technical docs**: the repo's artifact language (default English).
13
+ and technical docs**: the repo's artifact language (English unless the repo
14
+ clearly uses another).
10
15
  - **User-facing product copy**: as the product requires.
11
- - **Agent ↔ human communication** (review comments, thread replies, ticket
12
- bodies): the team's working language ({{team_language}}). Technical terms
16
+ - **Agent ↔ human communication** (review comments, thread replies): the same
17
+ language the team already uses in the repo's tickets and PRs. Technical terms
13
18
  stay in English within that prose — don't force-translate them.
14
19
 
15
20
  ## Commits & branches
@@ -25,8 +30,8 @@ of layer or language. This is a law of the project — see [`../../LAWS.md`](../
25
30
  edge cases, "why"). They never narrate history, restate the next line, or
26
31
  address the reviewer.
27
32
  - **Never** put ticket IDs, ticket text, or changelog narration
28
- ("added for {{ticket_prefix}}-123", "fixed as part of…") in code or
29
- docstrings. Traceability lives in the branch name, PR, and git history.
33
+ ("added for TICKET-123", "fixed as part of…") in code or docstrings.
34
+ Traceability lives in the branch name, PR, and git history.
30
35
 
31
36
  ## Dependencies
32
37