@esneiderbravo/speclaw 2.0.12 → 2.0.14
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 +1 -1
- package/dist/cli/commands/init.js +3 -2
- package/dist/cli/commands/lawbook.js +4 -1
- package/dist/cli/commands/query.js +2 -2
- package/dist/cli/commands/ship-on-stop.js +46 -0
- package/dist/cli/commands/ship.js +44 -0
- package/dist/cli/commands/update.js +25 -0
- package/dist/cli/index.js +6 -1
- package/dist/cli/lib/help.js +36 -4
- package/dist/modules/compass/db.js +47 -9
- package/dist/modules/compass/explore-rich.js +50 -9
- package/dist/modules/compass/extract.js +220 -0
- package/dist/modules/compass/find-output.js +172 -0
- package/dist/modules/compass/hybrid.js +149 -21
- package/dist/modules/compass/indexer.js +26 -2
- package/dist/modules/compass/query.js +48 -12
- package/dist/modules/compass/register.js +13 -7
- package/dist/modules/cortex/harness.js +17 -4
- package/dist/modules/cortex/status.js +1 -1
- package/dist/modules/foundation/assets/AGENTS.template.md +12 -11
- package/dist/modules/foundation/assets/CLAUDE.template.md +16 -12
- package/dist/modules/foundation/assets/docs/compass.template.md +11 -4
- package/dist/modules/foundation/deps.js +5 -3
- package/dist/modules/foundation/dialects/copilot.js +3 -5
- package/dist/modules/foundation/graph.js +5 -5
- package/dist/modules/foundation/hooks.js +30 -4
- package/dist/modules/foundation/laws.js +5 -0
- package/dist/modules/foundation/scaffold.js +3 -16
- package/dist/modules/lawbook/anchors.js +38 -1
- package/dist/modules/lawbook/assets/commands/cortex.md +1 -1
- package/dist/modules/lawbook/assets/rules/spec-reports-disciplines.md +6 -0
- package/dist/modules/lawbook/assets/rules/spec-tasks-mandatory-steps.md +6 -0
- package/dist/modules/lawbook/assets/skills/cortex/SKILL.md +17 -5
- package/dist/modules/lawbook/bugfix.js +12 -4
- package/dist/modules/lawbook/change-tool.js +7 -0
- package/dist/modules/lawbook/engine.js +3 -0
- package/dist/modules/lawbook/levels.js +35 -4
- package/dist/modules/lawbook/quick.js +17 -6
- package/dist/modules/lawbook/ship.js +325 -0
- package/dist/shared/agents.js +7 -1
- package/dist/shared/mcp.js +6 -4
- package/dist/shared/output-budget.js +36 -12
- package/package.json +1 -1
- package/dist/modules/foundation/assets/workflows/speclaw.yml +0 -26
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ gh attestation verify <tarball> --owner esneiderbravo
|
|
|
116
116
|
| Module | What it does |
|
|
117
117
|
| :-- | :-- |
|
|
118
118
|
| **Foundation** | The project's constitution: `LAWS.md` binding granular standards under `docs/standards/`, plus `CLAUDE.md` / `AGENTS.md`. **Enforced** via agent hooks (`speclaw check`), deterministic graph laws (`speclaw verify` / `laws verify`), multidialect compile (`laws compile`), and committed **`speclaw.lock`** digests + injection scan (`laws lock` / `accept` / `scan`). |
|
|
119
|
-
| **Compass** | Local code graph (tree-sitter → `node:sqlite`): hybrid find (FTS5 + vectors + RRF + PageRank), impact, affected-tests, hotspots, coupling, visualize. Schema **
|
|
119
|
+
| **Compass** | Local code graph (tree-sitter → `node:sqlite`): hybrid find (FTS5 + vectors + RRF + PageRank), impact, affected-tests, hotspots, coupling, visualize. Schema **12** (type-reference callers). Refreshed silently at every Claude Code session start once built. No LLM — lives in `.speclaw/` (gitignored). |
|
|
120
120
|
| **Lawbook** | Spec-driven workflow with **adaptive ceremony** (levels 0–3), EARS linting, requirement **coverage**, sealed **drift** anchors, bugfix + `lawbook_investigate`. Artifacts under `lawbook/`. |
|
|
121
121
|
| **Cortex** | **One brain. Many agents.** The multi-agent loop: durable `harness.json`, MCP tool `cortex` (`status`/`start`/`advance`/`rework`/`brief`), CLI `speclaw cortex`, skill `/lawbook/cortex`. Dispatches explorer → planner → implementer → reviewer → tester → archiver. |
|
|
122
122
|
| **Team** | Declare `team.owners` in `lawbook/config.yaml`; `speclaw owners --write` compiles a managed trailing block in `.github/CODEOWNERS` (GitHub: last match wins). Doctor checks the posture. CLI-only — no MCP tool. |
|
|
@@ -56,7 +56,7 @@ export async function runInit(flags) {
|
|
|
56
56
|
agents: () => clack.multiselect({
|
|
57
57
|
message: "Which agents do you use? (space to select)",
|
|
58
58
|
options: AGENTS.map((a) => ({ value: a.id, label: a.label })),
|
|
59
|
-
initialValues: ["
|
|
59
|
+
initialValues: ["agents"],
|
|
60
60
|
required: true,
|
|
61
61
|
}),
|
|
62
62
|
};
|
|
@@ -77,7 +77,8 @@ export async function runInit(flags) {
|
|
|
77
77
|
packs = answers.packs ?? [];
|
|
78
78
|
}
|
|
79
79
|
else {
|
|
80
|
-
|
|
80
|
+
// Default: one agent-facing folder, `.agents/`, linked into ai-specs/.
|
|
81
|
+
agents = list(flags.agents).length ? list(flags.agents) : ["agents"];
|
|
81
82
|
packs = list(flags.packs);
|
|
82
83
|
}
|
|
83
84
|
const unknownAgents = agents.filter((a) => !agentById(a));
|
|
@@ -126,7 +126,10 @@ export async function runSpec(flags) {
|
|
|
126
126
|
console.log(JSON.stringify(result, null, 2));
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
|
-
ui.ok(`bug change "${name}" scaffolded at ${result.dir}`
|
|
129
|
+
ui.ok(`bug change "${name}" scaffolded at ${result.dir} ` +
|
|
130
|
+
(level === undefined
|
|
131
|
+
? "(level unconfirmed — run `speclaw lawbook level set`)"
|
|
132
|
+
: `(level ${level})`));
|
|
130
133
|
return;
|
|
131
134
|
}
|
|
132
135
|
const name = req(change, "lawbook draft <name> [--level N] [--capability C] [--json] | --bug <name>");
|
|
@@ -36,7 +36,7 @@ export async function runQuery(cmd, flags) {
|
|
|
36
36
|
try {
|
|
37
37
|
switch (cmd) {
|
|
38
38
|
case "explore": {
|
|
39
|
-
const r = explore(cwd, need(args[0], "explore <node>"));
|
|
39
|
+
const r = explore(cwd, need(args[0], "explore <node>"), { includeRefs: true });
|
|
40
40
|
if (!r.found) {
|
|
41
41
|
ui.warn(r.message ?? "not found");
|
|
42
42
|
r.otherMatches?.forEach((m) => ui.info(`${m.name} (${m.kind}) ${m.file}:${m.line}`));
|
|
@@ -51,7 +51,7 @@ export async function runQuery(cmd, flags) {
|
|
|
51
51
|
ui.info(`+ ${r.unresolvedCallees.count} unresolved call(s): ${r.unresolvedCallees.sample.join(", ")}`);
|
|
52
52
|
}
|
|
53
53
|
ui.heading("Callers");
|
|
54
|
-
r.callers?.forEach((c) => ui.info(`${c.kind === "file" ? "(file) " : ""}${c.name} (${c.file}:${c.line})`));
|
|
54
|
+
r.callers?.forEach((c) => ui.info(`${c.kind === "file" ? "(file) " : ""}${c.name} (${c.file}:${c.line})${c.via ? ` via ${c.via}` : ""}`));
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
case "search":
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { shipOnStop } from "../../modules/lawbook/ship.js";
|
|
2
|
+
/**
|
|
3
|
+
* `speclaw ship-on-stop`: the Claude Code `Stop` hook. Ships the branch's
|
|
4
|
+
* change when the work changed since the last ship; prints nothing on stdout.
|
|
5
|
+
* A failing gate goes to stderr with exit 2 so the agent sees it and fixes it
|
|
6
|
+
* — once: when the hook already blocked this stop (`stop_hook_active`), it
|
|
7
|
+
* exits 0 so the agent can never loop. Any other failure exits 0.
|
|
8
|
+
*
|
|
9
|
+
* @param cwd - Project root (defaults to the process working directory).
|
|
10
|
+
*/
|
|
11
|
+
// Covers: req~ship-on-stop-hook~1
|
|
12
|
+
export async function runShipOnStop(cwd = process.cwd()) {
|
|
13
|
+
const active = await stopHookActive();
|
|
14
|
+
try {
|
|
15
|
+
const out = shipOnStop(cwd);
|
|
16
|
+
if (out.skipped === null && !out.result.gatesPassed && !active) {
|
|
17
|
+
const failed = out.result.gates[out.result.gates.length - 1];
|
|
18
|
+
process.stderr.write(`speclaw: gate failed — ${failed.command} (exit ${failed.exitCode}). Fix it before finishing.\n${failed.tail}\n`);
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// A hook must never break the session.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Whether this stop was already blocked by the hook once (`stop_hook_active`). */
|
|
27
|
+
async function stopHookActive() {
|
|
28
|
+
try {
|
|
29
|
+
const raw = process.stdin.isTTY ? "" : await readStdin();
|
|
30
|
+
return raw
|
|
31
|
+
? Boolean(JSON.parse(raw).stop_hook_active)
|
|
32
|
+
: false;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function readStdin() {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
let data = "";
|
|
41
|
+
process.stdin.setEncoding("utf8");
|
|
42
|
+
process.stdin.on("data", (c) => (data += c));
|
|
43
|
+
process.stdin.on("end", () => resolve(data));
|
|
44
|
+
setTimeout(() => resolve(data), 1000).unref();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { list } from "../lib/args.js";
|
|
2
|
+
import { ui } from "../lib/ui.js";
|
|
3
|
+
import { shipChange } from "../../modules/lawbook/ship.js";
|
|
4
|
+
/**
|
|
5
|
+
* Fast path for finished work (`speclaw ship <name>`): gates once, a report
|
|
6
|
+
* from their real output, and the archive at level 0.
|
|
7
|
+
*
|
|
8
|
+
* @param flags - `_[0]` is the change name; `--summary`, `--gate`, `--discipline`, `--no-archive`, `--json`.
|
|
9
|
+
*/
|
|
10
|
+
export async function runShip(flags) {
|
|
11
|
+
const name = flags._[0];
|
|
12
|
+
if (!name || typeof name !== "string") {
|
|
13
|
+
ui.err("Usage: speclaw ship <name> [--summary <text>] [--gate <cmd>] [--discipline <name>]");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const gates = list(flags.gate);
|
|
17
|
+
const result = shipChange(process.cwd(), name, {
|
|
18
|
+
summary: typeof flags.summary === "string" ? flags.summary : undefined,
|
|
19
|
+
gates: gates.length ? gates : undefined,
|
|
20
|
+
discipline: typeof flags.discipline === "string" ? flags.discipline : undefined,
|
|
21
|
+
noArchive: Boolean(flags["no-archive"]),
|
|
22
|
+
});
|
|
23
|
+
if (flags.json) {
|
|
24
|
+
console.log(JSON.stringify(result, null, 2));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
for (const g of result.gates) {
|
|
28
|
+
const t = `${(g.durationMs / 1000).toFixed(1)} s`;
|
|
29
|
+
if (g.exitCode === 0)
|
|
30
|
+
ui.ok(`${g.command} (${t})`);
|
|
31
|
+
else
|
|
32
|
+
ui.err(`${g.command} failed, exit ${g.exitCode} (${t})`);
|
|
33
|
+
}
|
|
34
|
+
ui.ok(`report ${ui.code(result.report)}`);
|
|
35
|
+
if (result.archivedTo)
|
|
36
|
+
ui.ok(`archived to ${result.archivedTo}`);
|
|
37
|
+
for (const n of result.next)
|
|
38
|
+
ui.warn(n);
|
|
39
|
+
const s = (ms) => `${(ms / 1000).toFixed(2)} s`;
|
|
40
|
+
ui.info(`speclaw overhead ${s(result.timings.overhead)} · gates ${s(result.timings.gates)} · total ${s(result.timings.total)}`);
|
|
41
|
+
}
|
|
42
|
+
if (!result.gatesPassed)
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
@@ -269,6 +269,31 @@ const MIGRATIONS = [
|
|
|
269
269
|
"up at the next session start or `compass_index`.\n" +
|
|
270
270
|
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
271
271
|
},
|
|
272
|
+
{
|
|
273
|
+
version: "2.0.13",
|
|
274
|
+
describe: "Compass schema 12 — type-reference callers, compact compass_find, unconfirmed bug drafts",
|
|
275
|
+
agentPrompt: "- Compass is now **schema 12**: the first `speclaw index` after upgrading migrates " +
|
|
276
|
+
"11→12 and re-extracts every file once (embeddings are reused). Where the project " +
|
|
277
|
+
'mentions "Schema **11**" or "Compass (schema 11)", change it to schema 12 ' +
|
|
278
|
+
"(11→12 forces a reindex; embeddings reused).\n" +
|
|
279
|
+
"- speclaw 2.0.12 or older (including an MCP entry pinned at `@2.0.12` or a stale " +
|
|
280
|
+
"global CLI) treats a schema-12 index as incompatible and rebuilds it from scratch. " +
|
|
281
|
+
"Pin every speclaw the project uses (global CLI, `npx`, CI, and the MCP entry) to " +
|
|
282
|
+
"2.0.13: run `speclaw update` so the pinned MCP entry moves to 2.0.13.\n" +
|
|
283
|
+
"- `compass_find` now returns one compact JSON document capped as a whole at " +
|
|
284
|
+
"`maxTokens` (default 1500): hits carry only `name`, `kind`, `file`, `line`; exact mode " +
|
|
285
|
+
"adds `found`, `terms`, and `nearest` when no symbol has the name; `focusIgnored` lists " +
|
|
286
|
+
"focus paths that are not indexed; `truncated: true` appears only when it trimmed. " +
|
|
287
|
+
"`speclaw search --json` keeps its shape. If the project's docs describe the find " +
|
|
288
|
+
"response fields, update them.\n" +
|
|
289
|
+
"- `compass_explore` and `speclaw explore` list type references (`ref` edges: " +
|
|
290
|
+
"annotations, `extends`/`implements`) among callers; every caller carries " +
|
|
291
|
+
'`via: "call" | "ref"`. Blast radius, affected tests, and ranking are unchanged.\n' +
|
|
292
|
+
"- Cortex: `pauseForQuestions` is accepted only from stage `planning` (an error " +
|
|
293
|
+
"elsewhere); `advance`/`rework` re-read the confirmed level; `speclaw lawbook draft " +
|
|
294
|
+
"--bug` without `--level` leaves the level unconfirmed (validate uses 3).\n" +
|
|
295
|
+
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
296
|
+
},
|
|
272
297
|
];
|
|
273
298
|
/**
|
|
274
299
|
* Bring the current project up to date without a full re-init. When the npm
|
package/dist/cli/index.js
CHANGED
|
@@ -28,6 +28,7 @@ const HEADER_COMMANDS = new Set([
|
|
|
28
28
|
"lawbook",
|
|
29
29
|
"cortex",
|
|
30
30
|
"quick",
|
|
31
|
+
"ship",
|
|
31
32
|
]);
|
|
32
33
|
/**
|
|
33
34
|
* Print the branded header once, ahead of a command's output, when it is a
|
|
@@ -124,6 +125,10 @@ async function dispatch(cmd, flags) {
|
|
|
124
125
|
return (await import("./commands/visualize.js")).runVisualize(flags);
|
|
125
126
|
case "quick":
|
|
126
127
|
return (await import("./commands/quick.js")).runQuick(flags);
|
|
128
|
+
case "ship":
|
|
129
|
+
return (await import("./commands/ship.js")).runShip(flags);
|
|
130
|
+
case "ship-on-stop":
|
|
131
|
+
return (await import("./commands/ship-on-stop.js")).runShipOnStop();
|
|
127
132
|
case "cortex":
|
|
128
133
|
return (await import("./commands/cortex.js")).runCortex(flags);
|
|
129
134
|
case "lawbook":
|
|
@@ -170,7 +175,7 @@ async function main() {
|
|
|
170
175
|
maybeHeader(cmd, flags);
|
|
171
176
|
await dispatch(cmd, flags);
|
|
172
177
|
// The SessionStart and edit hooks must stay silent: no update notice either.
|
|
173
|
-
if (cmd === "session-start" || cmd === "reindex-file")
|
|
178
|
+
if (cmd === "session-start" || cmd === "reindex-file" || cmd === "ship-on-stop")
|
|
174
179
|
return;
|
|
175
180
|
await maybeNotifyUpdate(cmd);
|
|
176
181
|
}
|
package/dist/cli/lib/help.js
CHANGED
|
@@ -24,6 +24,7 @@ Setup
|
|
|
24
24
|
Compass (code intelligence — the same surface agents use via MCP)
|
|
25
25
|
index (Re)build the local code graph (--force / --prune / --json)
|
|
26
26
|
session-start Silent, fail-safe refresh of an existing index (SessionStart hook)
|
|
27
|
+
ship-on-stop Ship the branch's change when work changed (Stop hook)
|
|
27
28
|
reindex-file [paths...] Silent re-index of edited files (PostToolUse hook; stdin JSON)
|
|
28
29
|
watch Keep the index fresh on file changes
|
|
29
30
|
explore <node> A node's source + callers/callees
|
|
@@ -41,6 +42,7 @@ Cortex (One brain. Many agents. — multi-agent loop)
|
|
|
41
42
|
cortex <op> status|start|advance|rework|brief — drive harness.json (--change)
|
|
42
43
|
|
|
43
44
|
Lawbook (spec-driven workflow)
|
|
45
|
+
ship <name> Fast path: gates once → report → archive (level 0)
|
|
44
46
|
quick <name> Scaffold a level-0 change (record.md + reports)
|
|
45
47
|
lawbook init Create the lawbook/ workspace
|
|
46
48
|
lawbook list Active/archived changes and capabilities
|
|
@@ -127,7 +129,7 @@ npx @esneiderbravo/speclaw@latest init so the scaffold matches the latest releas
|
|
|
127
129
|
|
|
128
130
|
Options
|
|
129
131
|
--yes, -y Accept the defaults without prompting
|
|
130
|
-
--agents <ids>
|
|
132
|
+
--agents <ids> Agents to configure (default: agents → only .agents/; also claude, cursor, codex, windsurf)
|
|
131
133
|
--packs <ids> Comma-separated tool packs to install
|
|
132
134
|
--project-name <name> Project name for the templates
|
|
133
135
|
--minimal Omit setup/lifecycle MCP tools
|
|
@@ -164,6 +166,15 @@ Options
|
|
|
164
166
|
usage: `Usage: speclaw watch
|
|
165
167
|
|
|
166
168
|
Keep the Compass index fresh: re-index changed files until interrupted (Ctrl-C).
|
|
169
|
+
`,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "ship-on-stop",
|
|
173
|
+
usage: `Usage: speclaw ship-on-stop
|
|
174
|
+
|
|
175
|
+
The Claude Code Stop hook. On a feature branch whose work changed since the
|
|
176
|
+
last ship, runs \`speclaw ship\` for the change named after the branch. Prints
|
|
177
|
+
nothing on stdout; a failing gate goes to stderr with exit 2 once.
|
|
167
178
|
`,
|
|
168
179
|
},
|
|
169
180
|
{
|
|
@@ -196,7 +207,9 @@ speclaw index, compass_index, watch). Prints nothing and always exits 0.
|
|
|
196
207
|
name: "explore",
|
|
197
208
|
usage: `Usage: speclaw explore <node> [options]
|
|
198
209
|
|
|
199
|
-
A symbol's source plus its callers and callees.
|
|
210
|
+
A symbol's source plus its callers and callees. Each caller carries
|
|
211
|
+
via: call (it calls the symbol) or ref (it names the type in an annotation
|
|
212
|
+
or an extends/implements clause).
|
|
200
213
|
|
|
201
214
|
Options
|
|
202
215
|
--json Print the result as JSON
|
|
@@ -307,6 +320,23 @@ Options
|
|
|
307
320
|
--depth <n> Neighborhood depth around <node>
|
|
308
321
|
--limit <n> Maximum nodes
|
|
309
322
|
--no-open Do not open the browser
|
|
323
|
+
`,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "ship",
|
|
327
|
+
usage: `Usage: speclaw ship <name> [options]
|
|
328
|
+
|
|
329
|
+
Fast path for finished work: scaffold a level-0 record if the change is new,
|
|
330
|
+
run the project's gates once, write reports/<discipline>.md from their real
|
|
331
|
+
output, and archive at level 0. Level 1+ stops after the evidence: review
|
|
332
|
+
happens on the PR. Ship never records a review verdict.
|
|
333
|
+
|
|
334
|
+
Options
|
|
335
|
+
--summary <text> What changed (default: the changed-file list)
|
|
336
|
+
--gate <cmd> Gate command (repeatable; default ship.gates or package.json)
|
|
337
|
+
--discipline <name> Report name (default ship.discipline or "change")
|
|
338
|
+
--no-archive Stop after the report
|
|
339
|
+
--json Print the result as JSON
|
|
310
340
|
`,
|
|
311
341
|
},
|
|
312
342
|
{
|
|
@@ -332,7 +362,8 @@ Options
|
|
|
332
362
|
--verdict <v> PASS or FAIL (advance from reviewing/testing)
|
|
333
363
|
--note <text> A history note
|
|
334
364
|
--question <text> One open question, commas included; repeat for more
|
|
335
|
-
--pause-questions
|
|
365
|
+
--pause-questions From planning only: pause for the --question list
|
|
366
|
+
(any other stage is rejected, exit 1)
|
|
336
367
|
--json Print the result as JSON
|
|
337
368
|
`,
|
|
338
369
|
},
|
|
@@ -344,7 +375,8 @@ Options
|
|
|
344
375
|
list Active/archived changes and capabilities
|
|
345
376
|
level <mode> [change] propose|set|promote|explain ceremony level (--level, --reason)
|
|
346
377
|
draft <name> Scaffold a feature change (--level N, --capability C)
|
|
347
|
-
draft --bug <name> Scaffold a bug change (bugfix.md + reports
|
|
378
|
+
draft --bug <name> Scaffold a bug change (bugfix.md + reports;
|
|
379
|
+
level unconfirmed unless --level N)
|
|
348
380
|
investigate Rank bug suspects (--symptom, --stack-trace, --symbol, --path)
|
|
349
381
|
validate <change> Validate a change's artifacts
|
|
350
382
|
sync <change> Promote delta specs to canonical
|
|
@@ -234,7 +234,7 @@ export function probeFts5Support() {
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
/** Schema version stamped into the `meta` table on first creation. */
|
|
237
|
-
export const SCHEMA_VERSION = "
|
|
237
|
+
export const SCHEMA_VERSION = "12";
|
|
238
238
|
/**
|
|
239
239
|
* `nodes.kind` of the synthetic per-file owner node. It owns the references that
|
|
240
240
|
* sit outside any definition (top-level imports, calls in callbacks) and is
|
|
@@ -253,7 +253,7 @@ function readSchemaVersion(db) {
|
|
|
253
253
|
}
|
|
254
254
|
/**
|
|
255
255
|
* Decide whether an existing database is from an incompatible schema and must be
|
|
256
|
-
* rebuilt. Schema 8→9, 9→10,
|
|
256
|
+
* rebuilt. Schema 8→9, 9→10, 10→11, and 11→12 are handled by migrators instead of a wipe.
|
|
257
257
|
*/
|
|
258
258
|
function isStale(db) {
|
|
259
259
|
const hasEdges = db
|
|
@@ -262,10 +262,8 @@ function isStale(db) {
|
|
|
262
262
|
if (!hasEdges)
|
|
263
263
|
return false;
|
|
264
264
|
const ver = readSchemaVersion(db);
|
|
265
|
-
if (ver === "8" || ver === "9" || ver === "10")
|
|
265
|
+
if (ver === "8" || ver === "9" || ver === "10" || ver === "11")
|
|
266
266
|
return false; // migrate in openDb
|
|
267
|
-
if (ver === "11" && !hasEdgeSpec(db))
|
|
268
|
-
return false; // pre-release 11: migrate in openDb
|
|
269
267
|
if (ver !== SCHEMA_VERSION)
|
|
270
268
|
return true;
|
|
271
269
|
const edgeCols = db.prepare("PRAGMA table_info(edges)").all().map((c) => c.name);
|
|
@@ -469,12 +467,49 @@ export function migrate10to11(db) {
|
|
|
469
467
|
throw new Error(`schema 10→11 migration failed — delete .speclaw/index.db to rebuild: ${err.message}`, { cause: err });
|
|
470
468
|
}
|
|
471
469
|
}
|
|
470
|
+
/** Reason recorded with the needs-reindex marker by the 11→12 migration. */
|
|
471
|
+
export const SCHEMA_12_REINDEX_REASON = "schema 12 adds type-reference (ref) edges for explore callers; reindex required";
|
|
472
|
+
/**
|
|
473
|
+
* Migrate schema 11 → 12: set the needs-reindex marker (its reason names
|
|
474
|
+
* schema 12, after any reason an earlier step of the same chain recorded) and
|
|
475
|
+
* stamp `"12"`, in one `BEGIN IMMEDIATE` transaction. No column changes
|
|
476
|
+
* (`edges.kind` is text); the next index run re-extracts every file so `ref`
|
|
477
|
+
* edges exist, while `embedding_cache` is untouched so unchanged symbols reuse
|
|
478
|
+
* their vectors.
|
|
479
|
+
*
|
|
480
|
+
* @param db - Open connection already at schema 11 (with `edges.spec`).
|
|
481
|
+
* @throws If any step fails; the transaction is rolled back and the stamp stays `"11"`.
|
|
482
|
+
*/
|
|
483
|
+
export function migrate11to12(db) {
|
|
484
|
+
// Covers: req~schema-ref-edges~1
|
|
485
|
+
db.exec("BEGIN IMMEDIATE");
|
|
486
|
+
try {
|
|
487
|
+
const pending = db.prepare("SELECT value FROM meta WHERE key = 'needs_reindex'").get();
|
|
488
|
+
const prior = db.prepare("SELECT value FROM meta WHERE key = 'reindex_reason'").get();
|
|
489
|
+
const reason = pending?.value === "1" && prior?.value && prior.value !== SCHEMA_12_REINDEX_REASON
|
|
490
|
+
? `${prior.value}; ${SCHEMA_12_REINDEX_REASON}`
|
|
491
|
+
: SCHEMA_12_REINDEX_REASON;
|
|
492
|
+
db.prepare("INSERT INTO meta(key, value) VALUES ('needs_reindex', '1') ON CONFLICT(key) DO UPDATE SET value = excluded.value").run();
|
|
493
|
+
db.prepare("INSERT INTO meta(key, value) VALUES ('reindex_reason', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(reason);
|
|
494
|
+
db.prepare("INSERT INTO meta(key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run("12");
|
|
495
|
+
db.exec("COMMIT");
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
try {
|
|
499
|
+
db.exec("ROLLBACK");
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
/* already rolled back */
|
|
503
|
+
}
|
|
504
|
+
throw new Error(`schema 11→12 migration failed — delete .speclaw/index.db to rebuild: ${err.message}`, { cause: err });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
472
507
|
/**
|
|
473
508
|
* Open (creating if needed) the index database at `<projectPath>/.speclaw/index.db`.
|
|
474
509
|
*
|
|
475
510
|
* Ensures the `.speclaw` directory exists, enables WAL journaling and foreign
|
|
476
|
-
* keys, and applies the schema. Schema 8→9, 9→10,
|
|
477
|
-
* and chain forward (embeddings preserved). Other incompatible schemas are wiped
|
|
511
|
+
* keys, and applies the schema. Schema 8→9, 9→10, 10→11, and 11→12 migrate in
|
|
512
|
+
* place and chain forward (embeddings preserved). Other incompatible schemas are wiped
|
|
478
513
|
* and rebuilt.
|
|
479
514
|
*
|
|
480
515
|
* @param projectPath - Absolute path to the project root.
|
|
@@ -494,13 +529,16 @@ export function openDb(projectPath) {
|
|
|
494
529
|
return null;
|
|
495
530
|
}
|
|
496
531
|
})();
|
|
497
|
-
if (ver === "8" || ver === "9" || ver === "10" ||
|
|
532
|
+
if (ver === "8" || ver === "9" || ver === "10" || ver === "11") {
|
|
498
533
|
try {
|
|
499
534
|
if (ver === "8")
|
|
500
535
|
migrate8to9(db, projectPath);
|
|
501
536
|
if (ver === "8" || ver === "9")
|
|
502
537
|
migrate9to10(db);
|
|
503
|
-
|
|
538
|
+
// A pre-release schema-11 index (no `edges.spec`) takes the 10→11 step too.
|
|
539
|
+
if (ver !== "11" || !hasEdgeSpec(db))
|
|
540
|
+
migrate10to11(db);
|
|
541
|
+
migrate11to12(db);
|
|
504
542
|
}
|
|
505
543
|
catch (err) {
|
|
506
544
|
db.close();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { explore, impact, trace } from "./query.js";
|
|
2
2
|
import { affectedTests } from "./affected.js";
|
|
3
|
-
import { FILE_NODE_KIND } from "./db.js";
|
|
3
|
+
import { FILE_NODE_KIND, indexExists, openDb } from "./db.js";
|
|
4
4
|
import { hotspots } from "./hotspots.js";
|
|
5
5
|
import { summarizeImpact } from "./impact-summary.js";
|
|
6
|
-
import { budgetExploreShape, applyTextBudget, } from "../../shared/output-budget.js";
|
|
6
|
+
import { budgetExploreShape, applyTextBudget, OUTPUT_BUDGET, } from "../../shared/output-budget.js";
|
|
7
7
|
const DEFAULT_INCLUDES = [
|
|
8
8
|
"source",
|
|
9
9
|
"callers",
|
|
@@ -28,7 +28,7 @@ export async function exploreRich(query) {
|
|
|
28
28
|
const degraded = [];
|
|
29
29
|
if (query.to) {
|
|
30
30
|
const pathResult = trace(query.projectPath, query.node, query.to, query.maxDepth ?? 8);
|
|
31
|
-
const base = explore(query.projectPath, query.node);
|
|
31
|
+
const base = explore(query.projectPath, query.node, { includeRefs: true });
|
|
32
32
|
const out = {
|
|
33
33
|
...base,
|
|
34
34
|
path: pathResult.path,
|
|
@@ -49,7 +49,7 @@ export async function exploreRich(query) {
|
|
|
49
49
|
budgetExploreShape(out, mode, truncated);
|
|
50
50
|
return out;
|
|
51
51
|
}
|
|
52
|
-
const base = explore(query.projectPath, query.node);
|
|
52
|
+
const base = explore(query.projectPath, query.node, { includeRefs: true });
|
|
53
53
|
const out = { ...base, truncated, degraded };
|
|
54
54
|
if (!includes.includes("source") && out.symbol)
|
|
55
55
|
out.symbol = withoutSource(out.symbol);
|
|
@@ -123,16 +123,57 @@ export async function exploreRich(query) {
|
|
|
123
123
|
delete out.degraded;
|
|
124
124
|
return out;
|
|
125
125
|
}
|
|
126
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Merge lexical and semantic search behind one surface (the MCP find path).
|
|
128
|
+
* The focus is resolved against the index first so dropped paths can be
|
|
129
|
+
* reported; exact mode keeps only symbols named exactly a query term and
|
|
130
|
+
* reports `found`, `terms`, and, when nothing matches, `nearest`.
|
|
131
|
+
*
|
|
132
|
+
* @param projectPath - Indexed project root.
|
|
133
|
+
* @param query - Identifier(s) or prose.
|
|
134
|
+
* @param mode - `exact` (name lookup) or `concept` (fuzzy, dense-heavy).
|
|
135
|
+
* @param limit - Seed limit (default 50).
|
|
136
|
+
* @param opts - Focus paths and the response token cap (default: brief ceiling).
|
|
137
|
+
*/
|
|
138
|
+
// Covers: req~find-exact-not-found~1, req~task-relative-ranking~1
|
|
127
139
|
export async function findSymbols(projectPath, query, mode, limit, opts) {
|
|
128
|
-
const { hybridSearch } = await import("./hybrid.js");
|
|
140
|
+
const { hybridSearch, resolveSearchFocus, exactTerms, nearestSymbols } = await import("./hybrid.js");
|
|
141
|
+
if (!indexExists(projectPath)) {
|
|
142
|
+
throw new Error("No index found. Build it first with the index_build tool (creates .speclaw/index.db).");
|
|
143
|
+
}
|
|
144
|
+
const db = openDb(projectPath);
|
|
145
|
+
let resolvedFocus;
|
|
146
|
+
try {
|
|
147
|
+
resolvedFocus = resolveSearchFocus(db, projectPath, opts?.focus);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
db.close();
|
|
151
|
+
}
|
|
152
|
+
const cap = opts?.maxTokens ?? OUTPUT_BUDGET.brief;
|
|
153
|
+
const terms = mode === "exact" ? exactTerms(query) : undefined;
|
|
154
|
+
const report = { ranked: 0, knnIds: [] };
|
|
129
155
|
const result = await hybridSearch(projectPath, query, {
|
|
130
156
|
mode,
|
|
131
|
-
|
|
132
|
-
maxTokens: opts?.maxTokens,
|
|
157
|
+
maxTokens: cap,
|
|
133
158
|
seedLimit: limit ?? 50,
|
|
159
|
+
resolvedFocus,
|
|
160
|
+
exactNames: terms,
|
|
161
|
+
report,
|
|
134
162
|
});
|
|
135
|
-
|
|
163
|
+
const out = {
|
|
164
|
+
...result,
|
|
165
|
+
mode,
|
|
166
|
+
focusIgnored: resolvedFocus.ignored,
|
|
167
|
+
cap,
|
|
168
|
+
capped: report.ranked > result.hits.length,
|
|
169
|
+
};
|
|
170
|
+
if (terms) {
|
|
171
|
+
out.terms = terms;
|
|
172
|
+
out.found = result.hits.length > 0;
|
|
173
|
+
if (!out.found)
|
|
174
|
+
out.nearest = nearestSymbols(projectPath, terms, report.knnIds);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
136
177
|
}
|
|
137
178
|
/** Serialize explore-rich with output budget applied. */
|
|
138
179
|
export function formatExploreRich(result, mode = "brief") {
|