@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.
- package/README.md +12 -8
- package/dist/cli/commands/init.js +16 -26
- package/dist/cli/commands/{spec.js → lawbook.js} +5 -5
- package/dist/cli/commands/visualize.js +44 -0
- package/dist/cli/index.js +11 -8
- package/dist/modules/compass/register.js +10 -0
- package/dist/modules/compass/visualize.js +218 -0
- package/dist/modules/foundation/assets/AGENTS.template.md +4 -4
- package/dist/modules/foundation/assets/CLAUDE.template.md +3 -3
- package/dist/modules/foundation/assets/LAWS.template.md +3 -3
- package/dist/modules/foundation/assets/docs/standards/base-standards.template.md +10 -5
- package/dist/modules/foundation/assets/docs/standards/conventions.template.md +12 -9
- package/dist/modules/foundation/assets/docs/standards/{spec-workflow.template.md → lawbook.template.md} +8 -8
- package/dist/modules/foundation/assets/docs/standards/testing-standards.template.md +1 -1
- package/dist/modules/foundation/doctor.js +7 -7
- package/dist/modules/foundation/register.js +6 -8
- package/dist/modules/foundation/scaffold.js +2 -3
- package/dist/modules/{spec → lawbook}/assets/commands/archive.md +2 -2
- package/dist/modules/{spec → lawbook}/assets/commands/draft.md +4 -4
- package/dist/modules/{spec → lawbook}/assets/commands/explore.md +1 -1
- package/dist/modules/{spec → lawbook}/assets/commands/sync.md +2 -2
- package/dist/modules/{spec → lawbook}/assets/rules/spec-tasks-mandatory-steps.md +6 -6
- package/dist/modules/{spec → lawbook}/assets/skills/archive/SKILL.md +7 -7
- package/dist/modules/{spec → lawbook}/assets/skills/build/SKILL.md +3 -3
- package/dist/modules/{spec → lawbook}/assets/skills/draft/SKILL.md +8 -8
- package/dist/modules/{spec → lawbook}/assets/skills/explore/SKILL.md +2 -2
- package/dist/modules/lawbook/assets/skills/sync/SKILL.md +21 -0
- package/dist/modules/{spec → lawbook}/engine.js +17 -17
- package/dist/modules/{spec → lawbook}/register.js +12 -12
- package/dist/modules/tools/assets/packs/agents/backend-developer.md +1 -1
- package/dist/modules/tools/assets/packs/agents/frontend-developer.md +1 -1
- package/dist/modules/tools/register.js +1 -1
- package/dist/server.js +2 -2
- package/package.json +2 -2
- package/dist/modules/spec/assets/skills/sync/SKILL.md +0 -21
- /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 & 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 **
|
|
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,
|
|
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
|
-
| **
|
|
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
|
|
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
|
|
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 & 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 (
|
|
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/
|
|
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
|
|
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`, `--
|
|
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(`
|
|
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(`
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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/
|
|
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 ? "
|
|
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
|
|
27
|
-
ui.heading("
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 "
|
|
65
|
-
return (await import("./commands/
|
|
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
|
|
22
|
+
3. **Follow the lawbook workflow** for every non-trivial change; archive
|
|
23
23
|
within the same PR. Rules:
|
|
24
|
-
[`docs/standards/
|
|
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/
|
|
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
|
-
| `
|
|
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/
|
|
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/
|
|
43
|
-
the workflow skills live in `ai-specs/skills/` and the `/
|
|
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
|
-
|
|
|
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
|
|
34
|
-
reviewed change (see the
|
|
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 (
|
|
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
|
|
12
|
-
|
|
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
|
|
29
|
-
|
|
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
|
|