@esneiderbravo/speclaw 0.3.5 → 0.3.8
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 +44 -15
- package/dist/cli/commands/coverage.js +49 -0
- package/dist/cli/commands/doctor.js +53 -21
- package/dist/cli/commands/telemetry.js +16 -0
- package/dist/cli/commands/update.js +21 -0
- package/dist/cli/index.js +16 -2
- package/dist/cli/lib/untrack.js +1 -0
- package/dist/modules/compass/db.js +20 -1
- package/dist/modules/compass/extract.js +59 -5
- package/dist/modules/compass/indexer.js +30 -1
- package/dist/modules/foundation/context-budget.js +16 -3
- package/dist/modules/foundation/doctor.js +626 -170
- package/dist/modules/foundation/graph.js +7 -4
- package/dist/modules/foundation/hooks.js +12 -0
- package/dist/modules/foundation/laws.js +1 -0
- package/dist/modules/foundation/register-core.js +108 -0
- package/dist/modules/foundation/register.js +18 -106
- package/dist/modules/lawbook/coverage.js +479 -0
- package/dist/modules/lawbook/engine.js +3 -0
- package/dist/modules/lawbook/register.js +13 -0
- package/dist/modules/lawbook/spec-items.js +168 -0
- package/dist/shared/exposure.js +1 -1
- package/dist/shared/install.js +1 -0
- package/dist/shared/redact.js +90 -0
- package/package.json +1 -1
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { underPaths } from "./verify-model.js";
|
|
2
2
|
/** Build the cross-file dependency graph, restricted to `paths` when given. */
|
|
3
|
-
function buildGraph(db, paths) {
|
|
3
|
+
function buildGraph(db, paths, edgeKinds) {
|
|
4
|
+
const kindFilter = edgeKinds && edgeKinds.length > 0
|
|
5
|
+
? ` AND e.kind IN (${edgeKinds.map(() => "?").join(", ")})`
|
|
6
|
+
: "";
|
|
4
7
|
const rows = db
|
|
5
8
|
.prepare(`SELECT DISTINCT sf.path AS src, df.path AS dst
|
|
6
9
|
FROM edges e
|
|
7
10
|
JOIN files sf ON sf.id = e.src_file_id
|
|
8
11
|
JOIN nodes dn ON dn.id = e.dst_node_id
|
|
9
12
|
JOIN files df ON df.id = dn.file_id
|
|
10
|
-
WHERE e.dst_node_id IS NOT NULL AND sf.path <> df.path`)
|
|
11
|
-
.all();
|
|
13
|
+
WHERE e.dst_node_id IS NOT NULL AND sf.path <> df.path${kindFilter}`)
|
|
14
|
+
.all(...(edgeKinds && edgeKinds.length > 0 ? edgeKinds : []));
|
|
12
15
|
const adj = new Map();
|
|
13
16
|
for (const { src, dst } of rows) {
|
|
14
17
|
if (!underPaths(src, paths) || !underPaths(dst, paths))
|
|
@@ -203,7 +206,7 @@ function reachableFindings(law, rule, adj) {
|
|
|
203
206
|
*/
|
|
204
207
|
export function runGraphLaw(db, law, paths) {
|
|
205
208
|
const rule = law.verification.rule;
|
|
206
|
-
const adj = buildGraph(db, paths);
|
|
209
|
+
const adj = buildGraph(db, paths, rule.edgeKinds);
|
|
207
210
|
const findings = [];
|
|
208
211
|
const wantReachable = rule.reachable === true && rule.from != null && rule.to != null;
|
|
209
212
|
const wantCircular = rule.circular === true || (!rule.circular && !wantReachable);
|
|
@@ -3,12 +3,24 @@ import path from "node:path";
|
|
|
3
3
|
import { agentById } from "../../shared/agents.js";
|
|
4
4
|
import { sha256 } from "../../shared/install.js";
|
|
5
5
|
import { globError, hasBackend } from "./laws.js";
|
|
6
|
+
/** Claude Code `${path}` templates — see https://code.claude.com/docs/en/hooks */
|
|
7
|
+
const SPECLAW_HOOK_INPUT = {
|
|
8
|
+
projectPath: "${cwd}",
|
|
9
|
+
event: "${hook_event_name}",
|
|
10
|
+
toolName: "${tool_name}",
|
|
11
|
+
payload: {
|
|
12
|
+
hook_event_name: "${hook_event_name}",
|
|
13
|
+
tool_name: "${tool_name}",
|
|
14
|
+
tool_input: { file_path: "${tool_input.file_path}" },
|
|
15
|
+
},
|
|
16
|
+
};
|
|
6
17
|
/** The speclaw hook object — its `{type, server}` pair is the merge identity. */
|
|
7
18
|
const SPECLAW_HOOK = {
|
|
8
19
|
type: "mcp_tool",
|
|
9
20
|
server: "speclaw",
|
|
10
21
|
tool: "speclaw_check",
|
|
11
22
|
timeout: 5,
|
|
23
|
+
input: SPECLAW_HOOK_INPUT,
|
|
12
24
|
};
|
|
13
25
|
/** Tool-name matcher for the file-mutating tools the `path` backend can evaluate. */
|
|
14
26
|
const MUTATION_MATCHER = "Write|Edit|MultiEdit|NotebookEdit";
|
|
@@ -22,6 +22,7 @@ const graphRuleSchema = z.object({
|
|
|
22
22
|
reachable: z.boolean().optional(),
|
|
23
23
|
from: z.string().optional(),
|
|
24
24
|
to: z.string().optional(),
|
|
25
|
+
edgeKinds: z.array(z.string()).optional(),
|
|
25
26
|
});
|
|
26
27
|
const verificationSchema = z.discriminatedUnion("kind", [
|
|
27
28
|
z.object({ kind: z.literal("path") }),
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool, text } from "../../shared/mcp.js";
|
|
3
|
+
import { shouldExpose } from "../../shared/exposure.js";
|
|
4
|
+
import { scaffold } from "./scaffold.js";
|
|
5
|
+
import { checkAction } from "./check.js";
|
|
6
|
+
import { verifyLaws } from "./verify.js";
|
|
7
|
+
import { loadPacks } from "../tools/packs.js";
|
|
8
|
+
import { AGENTS, configureAgent } from "../../shared/agents.js";
|
|
9
|
+
import { emptyReport } from "../../shared/install.js";
|
|
10
|
+
/** Human help text for init_project's questionnaire (not embedded in MCP schemas). */
|
|
11
|
+
const profileFieldHelp = {
|
|
12
|
+
project_name: "Short project name, e.g. the repo name",
|
|
13
|
+
project_description: "One-line description of what the project does",
|
|
14
|
+
organization: "Company/team name",
|
|
15
|
+
stack_summary: "e.g. 'Next.js 15 + TypeScript frontend, FastAPI + PostgreSQL backend'",
|
|
16
|
+
architecture: "e.g. 'hexagonal architecture with bounded contexts'",
|
|
17
|
+
test_commands: "Real commands, e.g. 'pytest backend/tests && npm run test'",
|
|
18
|
+
lint_commands: "Real commands, e.g. 'ruff check . && npm run lint && tsc --noEmit'",
|
|
19
|
+
branch_pattern: "e.g. 'feature/<ticket-id>-<slug>'",
|
|
20
|
+
commit_style: "e.g. 'conventional commits, imperative, English'",
|
|
21
|
+
custom_laws: "Extra markdown for LAWS.md — project-specific binding rules",
|
|
22
|
+
compass_hints: "Markdown bullets with real entrypoints for docs/compass.md",
|
|
23
|
+
base_standards_extra: "Extra cross-cutting rules for base-standards.md",
|
|
24
|
+
modules_table: "Markdown table of modules/bounded contexts",
|
|
25
|
+
layering_rules: "Layers and allowed dependencies for architecture.md",
|
|
26
|
+
backend_layers: "Backend layer table for backend-standards.md",
|
|
27
|
+
frontend_layers: "Frontend layer table for frontend-standards.md",
|
|
28
|
+
versioning_rules: "Versioning/release convention for conventions.md",
|
|
29
|
+
documentation_extra: "Repo-specific docstring notes for documentation.md",
|
|
30
|
+
};
|
|
31
|
+
/** Lean Zod shape for scaffold — no .describe() text (that cost rides in every request). */
|
|
32
|
+
const profileShape = {
|
|
33
|
+
project_name: z.string(),
|
|
34
|
+
project_description: z.string().optional(),
|
|
35
|
+
organization: z.string().optional(),
|
|
36
|
+
stack_summary: z.string().optional(),
|
|
37
|
+
architecture: z.string().optional(),
|
|
38
|
+
test_commands: z.string().optional(),
|
|
39
|
+
lint_commands: z.string().optional(),
|
|
40
|
+
branch_pattern: z.string().optional(),
|
|
41
|
+
commit_style: z.string().optional(),
|
|
42
|
+
custom_laws: z.string().optional(),
|
|
43
|
+
compass_hints: z.string().optional(),
|
|
44
|
+
base_standards_extra: z.string().optional(),
|
|
45
|
+
modules_table: z.string().optional(),
|
|
46
|
+
layering_rules: z.string().optional(),
|
|
47
|
+
backend_layers: z.string().optional(),
|
|
48
|
+
frontend_layers: z.string().optional(),
|
|
49
|
+
versioning_rules: z.string().optional(),
|
|
50
|
+
documentation_extra: z.string().optional(),
|
|
51
|
+
};
|
|
52
|
+
function makeAdd(server, minimal) {
|
|
53
|
+
return (name, description, inputSchema, handler) => {
|
|
54
|
+
if (!shouldExpose(name, minimal))
|
|
55
|
+
return;
|
|
56
|
+
defineTool(server, { name, description, inputSchema, handler });
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Foundation tools except `doctor`. Lives in a separate file so budget/doctor
|
|
61
|
+
* measurement can import it without forming a file-level SCC through
|
|
62
|
+
* `register.ts` → `doctor.ts` → `context-budget.ts`.
|
|
63
|
+
*/
|
|
64
|
+
export function registerFoundationCore(server, opts = {}) {
|
|
65
|
+
const add = makeAdd(server, Boolean(opts.minimal));
|
|
66
|
+
add("init_project", "Start here to initialize speclaw: returns the analysis questionnaire and packs.", { projectPath: z.string() }, async () => {
|
|
67
|
+
const packs = loadPacks();
|
|
68
|
+
return text({
|
|
69
|
+
instructions: [
|
|
70
|
+
"1. Analyze the repository at projectPath and fill in every profile field below with REAL values from the codebase (read package.json / pyproject.toml / CI configs / README — do not invent).",
|
|
71
|
+
"2. The foundation is a set of GRANULAR standards under docs/standards/ (base, architecture, backend, frontend, testing, conventions, lawbook), bound by LAWS.md and referenced from CLAUDE.md/AGENTS.md. Fill their structured fields from the real repo: modules_table and layering_rules (architecture), backend_layers, frontend_layers, versioning_rules, and any base_standards_extra. Omit a field only when that standard genuinely doesn't apply to this stack.",
|
|
72
|
+
"3. Suggest packs: add stack packs whose 'detect' hints match dependencies you found; offer the rest. Ask the user which packs to install (the lawbook workflow is always installed).",
|
|
73
|
+
"4. Infer the working language and the branch/commit/tracker conventions from the repo itself — the language already used in docstrings, commit messages, branch names, and PR/ticket bodies. Do NOT ask the user or assume English; match what the repo does, and set branch_pattern/commit_style accordingly. speclaw does not prescribe a ticket tool — leave tracker linkage to the team's own convention.",
|
|
74
|
+
"5. Draft any custom_laws (extra binding rules for LAWS.md) from conventions you observed that the standard set doesn't cover.",
|
|
75
|
+
"6. Call the 'scaffold' tool with { projectPath, profile, packs }.",
|
|
76
|
+
"7. Follow the nextSteps returned by scaffold: complete the HTML-comment sections still left in docs/standards/*, then run the lawbook_init and compass_index tools (both built into speclaw — no external installs).",
|
|
77
|
+
],
|
|
78
|
+
profileFields: profileFieldHelp,
|
|
79
|
+
packs,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
add("scaffold", "Write foundation, lawbook workflow, packs, IDE symlinks, and .mcp.json. Never overwrites.", {
|
|
83
|
+
projectPath: z.string(),
|
|
84
|
+
profile: z.object(profileShape),
|
|
85
|
+
packs: z.array(z.string()),
|
|
86
|
+
agents: z.array(z.string()).optional(),
|
|
87
|
+
}, async ({ projectPath, profile, packs, agents }) => text(scaffold(projectPath, profile, packs, agents ?? [])));
|
|
88
|
+
add("configure_agent", "Add one agent's IDE symlinks and MCP config to an already-scaffolded project.", {
|
|
89
|
+
projectPath: z.string(),
|
|
90
|
+
agent: z.enum(AGENTS.map((a) => a.id)),
|
|
91
|
+
}, async ({ projectPath, agent }) => {
|
|
92
|
+
const report = emptyReport();
|
|
93
|
+
configureAgent(projectPath, agent, report);
|
|
94
|
+
return text(report);
|
|
95
|
+
});
|
|
96
|
+
add("speclaw_check", "Invoked by speclaw's hooks to enforce laws — do not call directly.", {
|
|
97
|
+
projectPath: z.string(),
|
|
98
|
+
event: z.enum(["PreToolUse", "PostToolUse", "Stop", "InstructionsLoaded"]),
|
|
99
|
+
toolName: z.string().optional(),
|
|
100
|
+
payload: z.record(z.unknown()),
|
|
101
|
+
}, async ({ projectPath, event, toolName, payload }) => text(checkAction({ projectPath, event: event, toolName, payload })));
|
|
102
|
+
add("law_verify", "Verify deterministic deps/graph laws and return violations by file.", {
|
|
103
|
+
projectPath: z.string(),
|
|
104
|
+
paths: z.array(z.string()).optional(),
|
|
105
|
+
engines: z.array(z.enum(["deps", "graph"])).optional(),
|
|
106
|
+
lawIds: z.array(z.string()).optional(),
|
|
107
|
+
}, async ({ projectPath, paths, engines, lawIds }) => text(verifyLaws({ projectPath, paths, engines: engines, lawIds })));
|
|
108
|
+
}
|
|
@@ -1,114 +1,26 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineTool, text } from "../../shared/mcp.js";
|
|
3
3
|
import { shouldExpose } from "../../shared/exposure.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { loadPacks } from "../tools/packs.js";
|
|
9
|
-
import { AGENTS, configureAgent } from "../../shared/agents.js";
|
|
10
|
-
import { emptyReport } from "../../shared/install.js";
|
|
11
|
-
/** Human help text for init_project's questionnaire (not embedded in MCP schemas). */
|
|
12
|
-
const profileFieldHelp = {
|
|
13
|
-
project_name: "Short project name, e.g. the repo name",
|
|
14
|
-
project_description: "One-line description of what the project does",
|
|
15
|
-
organization: "Company/team name",
|
|
16
|
-
stack_summary: "e.g. 'Next.js 15 + TypeScript frontend, FastAPI + PostgreSQL backend'",
|
|
17
|
-
architecture: "e.g. 'hexagonal architecture with bounded contexts'",
|
|
18
|
-
test_commands: "Real commands, e.g. 'pytest backend/tests && npm run test'",
|
|
19
|
-
lint_commands: "Real commands, e.g. 'ruff check . && npm run lint && tsc --noEmit'",
|
|
20
|
-
branch_pattern: "e.g. 'feature/<ticket-id>-<slug>'",
|
|
21
|
-
commit_style: "e.g. 'conventional commits, imperative, English'",
|
|
22
|
-
custom_laws: "Extra markdown for LAWS.md — project-specific binding rules",
|
|
23
|
-
compass_hints: "Markdown bullets with real entrypoints for docs/compass.md",
|
|
24
|
-
base_standards_extra: "Extra cross-cutting rules for base-standards.md",
|
|
25
|
-
modules_table: "Markdown table of modules/bounded contexts",
|
|
26
|
-
layering_rules: "Layers and allowed dependencies for architecture.md",
|
|
27
|
-
backend_layers: "Backend layer table for backend-standards.md",
|
|
28
|
-
frontend_layers: "Frontend layer table for frontend-standards.md",
|
|
29
|
-
versioning_rules: "Versioning/release convention for conventions.md",
|
|
30
|
-
documentation_extra: "Repo-specific docstring notes for documentation.md",
|
|
31
|
-
};
|
|
32
|
-
/** Lean Zod shape for scaffold — no .describe() text (that cost rides in every request). */
|
|
33
|
-
const profileShape = {
|
|
34
|
-
project_name: z.string(),
|
|
35
|
-
project_description: z.string().optional(),
|
|
36
|
-
organization: z.string().optional(),
|
|
37
|
-
stack_summary: z.string().optional(),
|
|
38
|
-
architecture: z.string().optional(),
|
|
39
|
-
test_commands: z.string().optional(),
|
|
40
|
-
lint_commands: z.string().optional(),
|
|
41
|
-
branch_pattern: z.string().optional(),
|
|
42
|
-
commit_style: z.string().optional(),
|
|
43
|
-
custom_laws: z.string().optional(),
|
|
44
|
-
compass_hints: z.string().optional(),
|
|
45
|
-
base_standards_extra: z.string().optional(),
|
|
46
|
-
modules_table: z.string().optional(),
|
|
47
|
-
layering_rules: z.string().optional(),
|
|
48
|
-
backend_layers: z.string().optional(),
|
|
49
|
-
frontend_layers: z.string().optional(),
|
|
50
|
-
versioning_rules: z.string().optional(),
|
|
51
|
-
documentation_extra: z.string().optional(),
|
|
52
|
-
};
|
|
53
|
-
/** Register foundation MCP tools. */
|
|
4
|
+
import { registerFoundationCore } from "./register-core.js";
|
|
5
|
+
export { registerFoundationCore } from "./register-core.js";
|
|
6
|
+
const DOCTOR_DESCRIPTION = "Verify the speclaw install; returns a versioned DoctorReport (schemaVersion 1).";
|
|
7
|
+
/** Register foundation MCP tools (core + doctor). */
|
|
54
8
|
export function registerFoundation(server, opts = {}) {
|
|
9
|
+
registerFoundationCore(server, opts);
|
|
55
10
|
const minimal = Boolean(opts.minimal);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
return text({
|
|
64
|
-
instructions: [
|
|
65
|
-
"1. Analyze the repository at projectPath and fill in every profile field below with REAL values from the codebase (read package.json / pyproject.toml / CI configs / README — do not invent).",
|
|
66
|
-
"2. The foundation is a set of GRANULAR standards under docs/standards/ (base, architecture, backend, frontend, testing, conventions, lawbook), bound by LAWS.md and referenced from CLAUDE.md/AGENTS.md. Fill their structured fields from the real repo: modules_table and layering_rules (architecture), backend_layers, frontend_layers, versioning_rules, and any base_standards_extra. Omit a field only when that standard genuinely doesn't apply to this stack.",
|
|
67
|
-
"3. Suggest packs: add stack packs whose 'detect' hints match dependencies you found; offer the rest. Ask the user which packs to install (the lawbook workflow is always installed).",
|
|
68
|
-
"4. Infer the working language and the branch/commit/tracker conventions from the repo itself — the language already used in docstrings, commit messages, branch names, and PR/ticket bodies. Do NOT ask the user or assume English; match what the repo does, and set branch_pattern/commit_style accordingly. speclaw does not prescribe a ticket tool — leave tracker linkage to the team's own convention.",
|
|
69
|
-
"5. Draft any custom_laws (extra binding rules for LAWS.md) from conventions you observed that the standard set doesn't cover.",
|
|
70
|
-
"6. Call the 'scaffold' tool with { projectPath, profile, packs }.",
|
|
71
|
-
"7. Follow the nextSteps returned by scaffold: complete the HTML-comment sections still left in docs/standards/*, then run the lawbook_init and compass_index tools (both built into speclaw — no external installs).",
|
|
72
|
-
],
|
|
73
|
-
profileFields: profileFieldHelp,
|
|
74
|
-
packs,
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
add("scaffold", "Write foundation, lawbook workflow, packs, IDE symlinks, and .mcp.json. Never overwrites.", {
|
|
78
|
-
projectPath: z.string(),
|
|
79
|
-
profile: z.object(profileShape),
|
|
80
|
-
packs: z.array(z.string()),
|
|
81
|
-
agents: z.array(z.string()).optional(),
|
|
82
|
-
}, async ({ projectPath, profile, packs, agents }) => text(scaffold(projectPath, profile, packs, agents ?? [])));
|
|
83
|
-
add("configure_agent", "Add one agent's IDE symlinks and MCP config to an already-scaffolded project.", {
|
|
84
|
-
projectPath: z.string(),
|
|
85
|
-
agent: z.enum(AGENTS.map((a) => a.id)),
|
|
86
|
-
}, async ({ projectPath, agent }) => {
|
|
87
|
-
const report = emptyReport();
|
|
88
|
-
configureAgent(projectPath, agent, report);
|
|
11
|
+
if (!shouldExpose("doctor", minimal))
|
|
12
|
+
return;
|
|
13
|
+
const inputSchema = { projectPath: z.string() };
|
|
14
|
+
const handler = async ({ projectPath }) => {
|
|
15
|
+
// Lazy load: register.ts must stay out of the context-budget → doctor SCC.
|
|
16
|
+
const { doctor } = await import("./doctor.js");
|
|
17
|
+
const report = await doctor(projectPath, { redact: true });
|
|
89
18
|
return text(report);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}, async ({ projectPath, event, toolName, payload }) => text(checkAction({ projectPath, event: event, toolName, payload })));
|
|
97
|
-
add("law_verify", "Verify deterministic deps/graph laws and return violations by file.", {
|
|
98
|
-
projectPath: z.string(),
|
|
99
|
-
paths: z.array(z.string()).optional(),
|
|
100
|
-
engines: z.array(z.enum(["deps", "graph"])).optional(),
|
|
101
|
-
lawIds: z.array(z.string()).optional(),
|
|
102
|
-
}, async ({ projectPath, paths, engines, lawIds }) => text(verifyLaws({ projectPath, paths, engines: engines, lawIds })));
|
|
103
|
-
add("doctor", "Verify the speclaw install: foundation, symlinks, lawbook, Compass, and MCP wiring.", { projectPath: z.string() }, async ({ projectPath }) => {
|
|
104
|
-
const checks = doctor(projectPath);
|
|
105
|
-
const failed = checks.filter((c) => !c.ok);
|
|
106
|
-
return text({
|
|
107
|
-
healthy: failed.length === 0,
|
|
108
|
-
checks,
|
|
109
|
-
summary: failed.length === 0
|
|
110
|
-
? "Everything is within the law."
|
|
111
|
-
: `${failed.length} check(s) failed — see details.`,
|
|
112
|
-
});
|
|
19
|
+
};
|
|
20
|
+
defineTool(server, {
|
|
21
|
+
name: "doctor",
|
|
22
|
+
description: DOCTOR_DESCRIPTION,
|
|
23
|
+
inputSchema,
|
|
24
|
+
handler,
|
|
113
25
|
});
|
|
114
26
|
}
|