@esneiderbravo/speclaw 0.3.3 → 0.3.5
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 +54 -1
- package/dist/cli/commands/budget.js +36 -0
- package/dist/cli/commands/doctor.js +11 -1
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/update.js +30 -3
- package/dist/cli/commands/verify.js +118 -0
- package/dist/cli/index.js +14 -3
- package/dist/modules/compass/indexer.js +9 -0
- package/dist/modules/compass/map.js +114 -0
- package/dist/modules/compass/register.js +30 -64
- package/dist/modules/foundation/assets/docs/compass.template.md +3 -0
- package/dist/modules/foundation/assets/laws/laws-manifest.json +49 -0
- package/dist/modules/foundation/assets/workflows/speclaw.yml +26 -0
- package/dist/modules/foundation/ci.js +41 -0
- package/dist/modules/foundation/context-budget.js +45 -0
- package/dist/modules/foundation/deps.js +1 -1
- package/dist/modules/foundation/doctor.js +9 -0
- package/dist/modules/foundation/graph.js +1 -1
- package/dist/modules/foundation/laws.js +28 -0
- package/dist/modules/foundation/register.js +71 -120
- package/dist/modules/foundation/report-md.js +33 -0
- package/dist/modules/foundation/sarif.js +96 -0
- package/dist/modules/foundation/scaffold.js +32 -12
- package/dist/modules/foundation/verify-model.js +14 -0
- package/dist/modules/foundation/verify.js +16 -17
- package/dist/modules/lawbook/assets/skills/archive/SKILL.md +1 -31
- package/dist/modules/lawbook/assets/skills/archive/steps/01-confirm-done.md +7 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/02-reconcile.md +15 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +7 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/04-archive.md +9 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/05-report.md +7 -0
- package/dist/modules/lawbook/assets/skills/build/SKILL.md +2 -100
- package/dist/modules/lawbook/assets/skills/build/steps/01-load-change.md +7 -0
- package/dist/modules/lawbook/assets/skills/build/steps/02-branch.md +6 -0
- package/dist/modules/lawbook/assets/skills/build/steps/03-implement.md +11 -0
- package/dist/modules/lawbook/assets/skills/build/steps/04-quality-gates.md +10 -0
- package/dist/modules/lawbook/assets/skills/build/steps/05-manual-verification.md +22 -0
- package/dist/modules/lawbook/assets/skills/build/steps/06-discipline-reports.md +44 -0
- package/dist/modules/lawbook/assets/skills/build/steps/07-hand-off.md +9 -0
- package/dist/modules/lawbook/assets/skills/draft/SKILL.md +3 -80
- package/dist/modules/lawbook/assets/skills/draft/steps/01-ensure-workspace.md +5 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +12 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/03-name-capabilities.md +14 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +41 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/05-validate.md +11 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/06-hand-off.md +5 -0
- package/dist/modules/lawbook/assets/skills/explore/SKILL.md +6 -22
- package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +16 -0
- package/dist/modules/lawbook/assets/skills/explore/steps/02-summarize.md +7 -0
- package/dist/modules/lawbook/assets/skills/sync/SKILL.md +1 -30
- package/dist/modules/lawbook/assets/skills/sync/steps/01-confirm.md +5 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/02-reconcile.md +16 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/03-validate.md +6 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/04-promote.md +10 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/05-report.md +7 -0
- package/dist/modules/lawbook/register.js +17 -36
- package/dist/modules/tools/register.js +15 -15
- package/dist/server.js +13 -7
- package/dist/shared/budget.js +159 -0
- package/dist/shared/exposure.js +110 -0
- package/dist/shared/git.js +49 -0
- package/dist/shared/manifest.js +11 -2
- package/dist/shared/mcp.js +33 -0
- package/dist/shared/schema-tokens.js +86 -0
- package/dist/shared/tokens.js +41 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ too (also `pnpm dlx` / `yarn dlx`) — but installing globally means you can run
|
|
|
82
82
|
|
|
83
83
|
| Module | What it does |
|
|
84
84
|
| :-- | :-- |
|
|
85
|
-
| **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. It also **enforces** them: blocking laws compile into agent hooks that deny a forbidden edit at the keystroke (`speclaw check` / `speclaw_check`), and architectural laws are verified deterministically against the Compass graph — dependency rules (`deps`) and cycles (`graph`) — via `speclaw laws verify` / `law_verify
|
|
85
|
+
| **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. It also **enforces** them: blocking laws compile into agent hooks that deny a forbidden edit at the keystroke (`speclaw check` / `speclaw_check`), and architectural laws are verified deterministically against the Compass graph — dependency rules (`deps`) and cycles (`graph`) — via `speclaw verify` (CI orchestrator: exit codes, SARIF, markdown) and `speclaw laws verify` / `law_verify`. Each law is reported as passed, failed, skipped, or unknown (an unresolved reference is *unknown*, never a silent pass). |
|
|
86
86
|
| **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). |
|
|
87
87
|
| **Lawbook** | speclaw's own spec-driven workflow: `draft → build → sync → archive` (and `explore`), backed by `lawbook_*` engine tools. No external CLI. |
|
|
88
88
|
| **Tools** | Opt-in packs of skills and subagents (currently the dev-agents) that agents use for specific tasks. |
|
|
@@ -91,6 +91,30 @@ Compass is inspired by [CodeGraph](https://github.com/colbymchenry/codegraph) an
|
|
|
91
91
|
|
|
92
92
|
<br/>
|
|
93
93
|
|
|
94
|
+
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> Context cost
|
|
95
|
+
|
|
96
|
+
speclaw publishes and **gates** its own always-on context cost. Measured with a
|
|
97
|
+
deterministic offline estimator (`speclaw/estimate-v1`, about ±8% vs Anthropic's
|
|
98
|
+
tokenizer on this corpus — not a BPE dependency):
|
|
99
|
+
|
|
100
|
+
| | Tokens |
|
|
101
|
+
| :-- | --: |
|
|
102
|
+
| **speclaw budget (always-on)** | **~11.7k** (budget ceiling **13.0k**) |
|
|
103
|
+
| Spec Kit commands alone | ~18.6k ([spec-kit#1401](https://github.com/github/spec-kit/issues/1401)) |
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
speclaw budget # human table
|
|
107
|
+
speclaw budget --json # machine-readable; used by the suite gate
|
|
108
|
+
speclaw init --minimal # omit setup/lifecycle MCP tools from registration
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Raising a number in committed `token-budget.json` is a reviewable PR. Optional
|
|
112
|
+
calibration (never CI): `npm run budget:calibrate` with `ANTHROPIC_API_KEY`.
|
|
113
|
+
MCP servers cannot mark tools `defer_loading` — savings come from shorter
|
|
114
|
+
definitions, omitted registration (`--minimal`), and JIT skill steps.
|
|
115
|
+
|
|
116
|
+
<br/>
|
|
117
|
+
|
|
94
118
|
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> The spec-driven workflow (Lawbook)
|
|
95
119
|
|
|
96
120
|
Lawbook is speclaw's answer to the biggest risk with AI agents: **code that
|
|
@@ -210,6 +234,35 @@ context. Agents without hooks (Cursor, Codex) enforce the same laws in CI via
|
|
|
210
234
|
|
|
211
235
|
<br/>
|
|
212
236
|
|
|
237
|
+
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> Verify in CI
|
|
238
|
+
|
|
239
|
+
`speclaw verify` evaluates your `deps` and `graph` laws against the local Compass
|
|
240
|
+
index. It is deterministic: **no model, no API key, no network.**
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
speclaw verify --ci --sarif speclaw.sarif --json speclaw.json
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
| Exit | Meaning |
|
|
247
|
+
| :-- | :-- |
|
|
248
|
+
| **0** | No findings at or above `--fail-on` (default `error`) |
|
|
249
|
+
| **1** | At least one finding at or above `--fail-on` |
|
|
250
|
+
| **2** | Usage error (unknown `--fail-on` / `--format`) |
|
|
251
|
+
| **3** | Environment (shallow clone under `--ci`, or an unwritable `--sarif`/`--json` path) |
|
|
252
|
+
| **4** | At least one law was skipped, and `--strict-engines` was set |
|
|
253
|
+
|
|
254
|
+
On GitHub:
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
- uses: esneiderbravo/speclaw@v1
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`init` / `update` write `.github/workflows/speclaw.yml` only when that path is
|
|
261
|
+
missing — they never overwrite your CI. Make the check required in branch
|
|
262
|
+
protection yourself; speclaw does not.
|
|
263
|
+
|
|
264
|
+
<br/>
|
|
265
|
+
|
|
213
266
|
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> Staying up to date
|
|
214
267
|
|
|
215
268
|
speclaw checks for new releases in the background (at most once a day) and nudges
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ui } from "../lib/ui.js";
|
|
2
|
+
import { formatBudgetTable } from "../../shared/budget.js";
|
|
3
|
+
import { loadDeclaredBudget } from "../../shared/exposure.js";
|
|
4
|
+
import { measureInstallBudget } from "../../modules/foundation/context-budget.js";
|
|
5
|
+
/** Print the context-budget table or JSON. */
|
|
6
|
+
export async function runBudget(flags) {
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const minimal = flags.minimal === true ? true : flags.minimal === false ? false : undefined;
|
|
9
|
+
const measurement = measureInstallBudget(cwd, minimal);
|
|
10
|
+
const declared = loadDeclaredBudget();
|
|
11
|
+
if (flags.json) {
|
|
12
|
+
console.log(JSON.stringify({
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
profile: measurement.profile,
|
|
15
|
+
tools: measurement.tools,
|
|
16
|
+
skillsAndCommands: measurement.skillsAndCommands,
|
|
17
|
+
alwaysOnInstructions: measurement.alwaysOnInstructions,
|
|
18
|
+
pathScoped: measurement.pathScoped,
|
|
19
|
+
total: measurement.total,
|
|
20
|
+
toolCount: measurement.toolCount,
|
|
21
|
+
declared: {
|
|
22
|
+
tools: declared.surfaces.tools,
|
|
23
|
+
skillsAndCommands: declared.surfaces.skillsAndCommands,
|
|
24
|
+
alwaysOnInstructions: declared.surfaces.alwaysOnInstructions,
|
|
25
|
+
total: measurement.profile === "minimal" ? declared.minimal.total : declared.total,
|
|
26
|
+
},
|
|
27
|
+
details: measurement.details,
|
|
28
|
+
}, null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
ui.heading("speclaw budget");
|
|
32
|
+
console.log(formatBudgetTable(measurement, declared));
|
|
33
|
+
ui.plain();
|
|
34
|
+
ui.info(`profile: ${measurement.profile} · tools registered: ${measurement.toolCount}`);
|
|
35
|
+
ui.info("Spec Kit (commands only), for comparison: ~18,600 — github/spec-kit#1401");
|
|
36
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { doctor } from "../../modules/foundation/doctor.js";
|
|
2
|
+
import { measureInstallBudget } from "../../modules/foundation/context-budget.js";
|
|
2
3
|
import { ui } from "../lib/ui.js";
|
|
3
4
|
/** Verify the installation, printing each check and exiting non-zero on failure. */
|
|
4
5
|
export async function runDoctor(_flags) {
|
|
5
|
-
const
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
const checks = doctor(cwd);
|
|
6
8
|
ui.heading("speclaw doctor");
|
|
7
9
|
for (const c of checks) {
|
|
8
10
|
if (c.ok)
|
|
@@ -10,6 +12,14 @@ export async function runDoctor(_flags) {
|
|
|
10
12
|
else
|
|
11
13
|
ui.err(`${c.name} — ${c.detail}`);
|
|
12
14
|
}
|
|
15
|
+
try {
|
|
16
|
+
const m = measureInstallBudget(cwd);
|
|
17
|
+
ui.ok(`context budget — profile ${m.profile} · ${m.toolCount} tools · ~${m.total} always-on tokens ` +
|
|
18
|
+
`(A ${m.tools} · B ${m.skillsAndCommands} · C ${m.alwaysOnInstructions}; D path-scoped ${m.pathScoped} not in total)`);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
ui.warn(`context budget — could not measure: ${err.message}`);
|
|
22
|
+
}
|
|
13
23
|
const failed = checks.filter((c) => !c.ok).length;
|
|
14
24
|
ui.plain();
|
|
15
25
|
if (failed === 0)
|
|
@@ -87,7 +87,9 @@ export async function runInit(flags) {
|
|
|
87
87
|
if (reinit) {
|
|
88
88
|
ui.info("speclaw is already set up here — your existing files are kept; only missing pieces are added.");
|
|
89
89
|
}
|
|
90
|
-
const report = scaffold(cwd, profile, packs, agents
|
|
90
|
+
const report = scaffold(cwd, profile, packs, agents, {
|
|
91
|
+
minimal: Boolean(flags.minimal),
|
|
92
|
+
});
|
|
91
93
|
ui.ok(`Foundation ${c.muted("— LAWS.md + 8 standards + CLAUDE.md/AGENTS.md")}`);
|
|
92
94
|
ui.ok(`Lawbook workflow ${c.muted("— draft · build · sync · archive · explore")}`);
|
|
93
95
|
for (const p of packs)
|
|
@@ -60,6 +60,25 @@ const MIGRATIONS = [
|
|
|
60
60
|
"in docs/standards/testing-standards.md (the 'Manual & end-to-end verification' section).\n" +
|
|
61
61
|
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
version: "0.3.4",
|
|
65
|
+
describe: "CI verify workflow (if missing) and seed-law merge",
|
|
66
|
+
agentPrompt: "- If you want pull requests gated on speclaw, add the `speclaw` GitHub check as a " +
|
|
67
|
+
"required status check in branch protection. speclaw never enables that itself.\n" +
|
|
68
|
+
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
version: "0.3.5",
|
|
72
|
+
describe: "Context budget: compact map markers and budget-aware agent contracts",
|
|
73
|
+
agentPrompt: "- In docs/compass.md, ensure the markers `<!-- speclaw:map:start -->` and " +
|
|
74
|
+
"`<!-- speclaw:map:end -->` exist (usually just above 'Project-specific starting points') " +
|
|
75
|
+
"so `compass_index` can regenerate the compact project map between them. Do not hand-edit " +
|
|
76
|
+
"the content between the markers.\n" +
|
|
77
|
+
"- Optionally note in CLAUDE.md / AGENTS.md that `speclaw budget` reports always-on context " +
|
|
78
|
+
"cost and that `speclaw init --minimal` / `SPECLAW_MINIMAL=1` omit setup MCP tools " +
|
|
79
|
+
"(no server-side defer_loading).\n" +
|
|
80
|
+
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
81
|
+
},
|
|
63
82
|
];
|
|
64
83
|
/**
|
|
65
84
|
* Update speclaw and bring the current project up to date without a full re-init:
|
|
@@ -101,7 +120,12 @@ export async function runUpdate(flags) {
|
|
|
101
120
|
// Re-exec the NEWLY installed binary so migrations run with the new assets
|
|
102
121
|
// and any new feature steps — not this (now-stale) process. Carry --backup
|
|
103
122
|
// through so the refresh honors it after the upgrade.
|
|
104
|
-
const reArgs = [
|
|
123
|
+
const reArgs = [
|
|
124
|
+
"update",
|
|
125
|
+
"--migrate-only",
|
|
126
|
+
...(backup ? ["--backup"] : []),
|
|
127
|
+
...(flags.minimal ? ["--minimal"] : []),
|
|
128
|
+
];
|
|
105
129
|
const re = spawnSync("speclaw", reArgs, {
|
|
106
130
|
stdio: "inherit",
|
|
107
131
|
shell: winShell,
|
|
@@ -118,7 +142,7 @@ export async function runUpdate(flags) {
|
|
|
118
142
|
return;
|
|
119
143
|
}
|
|
120
144
|
}
|
|
121
|
-
applyProjectMigrations(cwd, backup);
|
|
145
|
+
applyProjectMigrations(cwd, backup, flags.minimal ? true : undefined);
|
|
122
146
|
}
|
|
123
147
|
/**
|
|
124
148
|
* Additively apply the current version's content and feature steps to a project.
|
|
@@ -128,8 +152,10 @@ export async function runUpdate(flags) {
|
|
|
128
152
|
* @param backup - When true, a locally edited managed file is copied to
|
|
129
153
|
* `<file>.bak` before it is refreshed; the default overwrites it in place
|
|
130
154
|
* (recoverable from git) and only reports the overwrite.
|
|
155
|
+
* @param minimal - When true, persist minimal exposure; when undefined, keep
|
|
156
|
+
* the manifest's existing value.
|
|
131
157
|
*/
|
|
132
|
-
function applyProjectMigrations(cwd, backup) {
|
|
158
|
+
function applyProjectMigrations(cwd, backup, minimal) {
|
|
133
159
|
const initialized = fs.existsSync(path.join(cwd, "ai-specs")) || fs.existsSync(path.join(cwd, "LAWS.md"));
|
|
134
160
|
if (!initialized) {
|
|
135
161
|
ui.step("Project");
|
|
@@ -147,6 +173,7 @@ function applyProjectMigrations(cwd, backup) {
|
|
|
147
173
|
const report = scaffold(cwd, { project_name: detectProjectName(cwd) }, packs, agents, {
|
|
148
174
|
refreshManaged: true,
|
|
149
175
|
backup,
|
|
176
|
+
...(minimal !== undefined ? { minimal } : {}),
|
|
150
177
|
});
|
|
151
178
|
const changed = report.written.filter((w) => !w.includes(".gitignore"));
|
|
152
179
|
if (changed.length) {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { list } from "../lib/args.js";
|
|
3
|
+
import { ui, c } from "../lib/ui.js";
|
|
4
|
+
import { isShallowRepo } from "../../shared/git-history.js";
|
|
5
|
+
import { pkgVersion } from "../../shared/version.js";
|
|
6
|
+
import { parseFailOn, verifyExitCode } from "../../modules/foundation/ci.js";
|
|
7
|
+
import { toMarkdown } from "../../modules/foundation/report-md.js";
|
|
8
|
+
import { toSarif } from "../../modules/foundation/sarif.js";
|
|
9
|
+
import { loadManifestForVerify } from "../../modules/foundation/laws.js";
|
|
10
|
+
import { verifyLaws } from "../../modules/foundation/verify.js";
|
|
11
|
+
const FORMATS = new Set(["text", "json", "sarif", "markdown"]);
|
|
12
|
+
/**
|
|
13
|
+
* `speclaw verify` — the CI orchestrator over {@link verifyLaws}. Formats and
|
|
14
|
+
* exit codes live here; graph evaluation stays in `verify.ts`. `speclaw check`
|
|
15
|
+
* (hooks) and `speclaw laws verify` (the thin batch twin) are unchanged.
|
|
16
|
+
*
|
|
17
|
+
* @param flags - Parsed CLI flags.
|
|
18
|
+
*/
|
|
19
|
+
export async function runVerify(flags) {
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
const ci = Boolean(flags.ci);
|
|
22
|
+
if (ci)
|
|
23
|
+
process.env.NO_COLOR = "1";
|
|
24
|
+
const failOn = parseFailOn(flags["fail-on"]);
|
|
25
|
+
if (failOn === null) {
|
|
26
|
+
ui.err(`--fail-on must be error, warn, or info.`);
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
const format = flags.format === undefined || flags.format === true ? "text" : String(flags.format);
|
|
30
|
+
if (!FORMATS.has(format)) {
|
|
31
|
+
ui.err(`--format must be text, json, sarif, or markdown.`);
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
const strict = Boolean(flags["strict-engines"]);
|
|
35
|
+
const engines = list(flags.engine).filter((e) => e === "deps" || e === "graph");
|
|
36
|
+
const paths = list(flags.path);
|
|
37
|
+
if (ci && isShallowRepo(cwd)) {
|
|
38
|
+
ui.err("Shallow clone — speclaw cannot see the merge base. Check out with fetch-depth: 0.");
|
|
39
|
+
process.exit(3);
|
|
40
|
+
}
|
|
41
|
+
const manifest = loadManifestForVerify(cwd);
|
|
42
|
+
const report = verifyLaws({
|
|
43
|
+
projectPath: cwd,
|
|
44
|
+
paths: paths.length ? paths : undefined,
|
|
45
|
+
engines: engines.length ? engines : undefined,
|
|
46
|
+
lawIds: list(flags.law).length ? list(flags.law) : undefined,
|
|
47
|
+
});
|
|
48
|
+
const sarifPath = typeof flags.sarif === "string" ? flags.sarif : undefined;
|
|
49
|
+
const jsonPath = typeof flags.json === "string" ? flags.json : undefined;
|
|
50
|
+
if (sarifPath) {
|
|
51
|
+
if (!writeOut(sarifPath, JSON.stringify(toSarif(report, sarifCtx(manifest.laws)), null, 2))) {
|
|
52
|
+
process.exit(3);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (jsonPath) {
|
|
56
|
+
if (!writeOut(jsonPath, JSON.stringify(report, null, 2)))
|
|
57
|
+
process.exit(3);
|
|
58
|
+
}
|
|
59
|
+
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
60
|
+
if (summaryFile) {
|
|
61
|
+
try {
|
|
62
|
+
fs.appendFileSync(summaryFile, toMarkdown(report));
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
ui.warn(`Could not write $GITHUB_STEP_SUMMARY: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
printReport(report, format, flags.json === true && !jsonPath);
|
|
69
|
+
process.exit(verifyExitCode(report, { failOn, strictEngines: strict }));
|
|
70
|
+
}
|
|
71
|
+
function sarifCtx(laws) {
|
|
72
|
+
return { speclawVersion: pkgVersion(), laws };
|
|
73
|
+
}
|
|
74
|
+
function writeOut(file, body) {
|
|
75
|
+
try {
|
|
76
|
+
fs.writeFileSync(file, body.endsWith("\n") ? body : body + "\n");
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
ui.err(`Cannot write ${file}: ${err.message}`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function printReport(report, format, jsonStdout) {
|
|
85
|
+
if (jsonStdout || format === "json") {
|
|
86
|
+
console.log(JSON.stringify(report, null, 2));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (format === "sarif") {
|
|
90
|
+
// Rules need the loaded laws; re-read via the same fallback the run used.
|
|
91
|
+
const laws = loadManifestForVerify(process.cwd()).laws;
|
|
92
|
+
console.log(JSON.stringify(toSarif(report, sarifCtx(laws)), null, 2));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (format === "markdown") {
|
|
96
|
+
process.stdout.write(toMarkdown(report));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const { summary } = report;
|
|
100
|
+
ui.heading("speclaw verify");
|
|
101
|
+
ui.info(`${summary.passed} passed · ${c.red(String(summary.failed))} failed · ` +
|
|
102
|
+
`${summary.skipped} skipped · ${summary.unknown} unknown ` +
|
|
103
|
+
`(${report.elapsedMs.toFixed(1)} ms)`);
|
|
104
|
+
if (summary.evaluated === 0 && summary.skipped === 0) {
|
|
105
|
+
ui.warn("0 batch laws evaluated — verify is not checking anything.");
|
|
106
|
+
}
|
|
107
|
+
for (const f of report.findings) {
|
|
108
|
+
const at = f.line ? `${f.file}:${f.line}` : f.file;
|
|
109
|
+
ui.warn(`${c.cream(f.lawId)} — ${at}${f.detail ? ` ${f.detail}` : ""}`);
|
|
110
|
+
}
|
|
111
|
+
for (const u of report.unknown)
|
|
112
|
+
ui.plain(` ? ${c.cream(u.lawId)} — ${u.detail}`);
|
|
113
|
+
for (const s of report.skipped) {
|
|
114
|
+
ui.plain(` – ${c.cream(s.lawId)} — skipped: ${s.reason}${s.detail ? ` (${s.detail})` : ""}`);
|
|
115
|
+
}
|
|
116
|
+
if (report.findings.length === 0 && summary.evaluated > 0)
|
|
117
|
+
ui.ok("No violations.");
|
|
118
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -11,7 +11,9 @@ Install globally so the command is always available:
|
|
|
11
11
|
|
|
12
12
|
Setup
|
|
13
13
|
init Interactive setup: pick agents, scaffold, index, get the prompt
|
|
14
|
+
(--minimal omits setup/lifecycle MCP tools)
|
|
14
15
|
update Upgrade speclaw and apply only what's new (no re-init)
|
|
16
|
+
(--minimal persists minimal exposure; omit to keep prior)
|
|
15
17
|
agent list Show which agents are configured
|
|
16
18
|
agent add <id> Configure another agent later (symlinks + MCP)
|
|
17
19
|
|
|
@@ -34,8 +36,10 @@ Lawbook (spec-driven workflow)
|
|
|
34
36
|
|
|
35
37
|
Other
|
|
36
38
|
doctor Verify the installation
|
|
39
|
+
budget Measure always-on context cost (tools, skills, instructions)
|
|
37
40
|
check Evaluate an action against the laws (hooks call this; --dry-run to preview)
|
|
38
41
|
laws verify Verify the deterministic dependency/graph laws against the index
|
|
42
|
+
verify Verify laws for CI: exit codes, --sarif, --json, --strict-engines
|
|
39
43
|
mcp Start the MCP server (used by your agent's config)
|
|
40
44
|
help Show this help
|
|
41
45
|
--version Print the installed speclaw version
|
|
@@ -54,6 +58,7 @@ const HEADER_COMMANDS = new Set([
|
|
|
54
58
|
"update",
|
|
55
59
|
"agent",
|
|
56
60
|
"doctor",
|
|
61
|
+
"budget",
|
|
57
62
|
"index",
|
|
58
63
|
"watch",
|
|
59
64
|
"lawbook",
|
|
@@ -63,13 +68,15 @@ const HEADER_COMMANDS = new Set([
|
|
|
63
68
|
* header-eligible command AND stdout is an interactive terminal (so pipes,
|
|
64
69
|
* redirection, and CI stay clean — mirroring the color gate in `ui.ts`). A
|
|
65
70
|
* forced-color signal counts as interactive so the header is exercisable in a
|
|
66
|
-
* child process.
|
|
71
|
+
* child process. `budget --json` is machine-consumed and suppresses the header.
|
|
67
72
|
*/
|
|
68
|
-
function maybeHeader(cmd) {
|
|
73
|
+
function maybeHeader(cmd, flags) {
|
|
69
74
|
if (!process.stdout.isTTY && process.env.FORCE_COLOR !== "1")
|
|
70
75
|
return;
|
|
71
76
|
if (!HEADER_COMMANDS.has(cmd))
|
|
72
77
|
return;
|
|
78
|
+
if (cmd === "budget" && flags.json)
|
|
79
|
+
return;
|
|
73
80
|
header();
|
|
74
81
|
}
|
|
75
82
|
/** Run the handler for a single command. Returns when the command completes. */
|
|
@@ -112,10 +119,14 @@ async function dispatch(cmd, flags) {
|
|
|
112
119
|
return (await import("./commands/lawbook.js")).runSpec(flags);
|
|
113
120
|
case "doctor":
|
|
114
121
|
return (await import("./commands/doctor.js")).runDoctor(flags);
|
|
122
|
+
case "budget":
|
|
123
|
+
return (await import("./commands/budget.js")).runBudget(flags);
|
|
115
124
|
case "check":
|
|
116
125
|
return (await import("./commands/check.js")).runCheck(flags);
|
|
117
126
|
case "laws":
|
|
118
127
|
return (await import("./commands/laws.js")).runLaws(flags);
|
|
128
|
+
case "verify":
|
|
129
|
+
return (await import("./commands/verify.js")).runVerify(flags);
|
|
119
130
|
default:
|
|
120
131
|
ui.err(`Unknown command: ${cmd}`);
|
|
121
132
|
console.log(HELP);
|
|
@@ -126,7 +137,7 @@ async function dispatch(cmd, flags) {
|
|
|
126
137
|
async function main() {
|
|
127
138
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
128
139
|
const flags = parseFlags(rest);
|
|
129
|
-
maybeHeader(cmd);
|
|
140
|
+
maybeHeader(cmd, flags);
|
|
130
141
|
await dispatch(cmd, flags);
|
|
131
142
|
await maybeNotifyUpdate(cmd);
|
|
132
143
|
}
|
|
@@ -9,6 +9,7 @@ const SKIP_DIRS = new Set([
|
|
|
9
9
|
".git",
|
|
10
10
|
"node_modules",
|
|
11
11
|
"dist",
|
|
12
|
+
"dist-test",
|
|
12
13
|
"build",
|
|
13
14
|
".next",
|
|
14
15
|
"out",
|
|
@@ -173,5 +174,13 @@ export async function buildIndex(projectPath, onProgress) {
|
|
|
173
174
|
finally {
|
|
174
175
|
db.close();
|
|
175
176
|
}
|
|
177
|
+
// Compact map in committed docs/compass.md (between markers) — zero tool-call cost.
|
|
178
|
+
try {
|
|
179
|
+
const { writeCompactMap } = await import("./map.js");
|
|
180
|
+
writeCompactMap(projectPath);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Map generation must never fail an index run.
|
|
184
|
+
}
|
|
176
185
|
return stats;
|
|
177
186
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { openDb } from "./db.js";
|
|
4
|
+
import { estimateTokens } from "../../shared/tokens.js";
|
|
5
|
+
import { loadDeclaredBudget } from "../../shared/exposure.js";
|
|
6
|
+
export const MAP_START = "<!-- speclaw:map:start -->";
|
|
7
|
+
export const MAP_END = "<!-- speclaw:map:end -->";
|
|
8
|
+
/**
|
|
9
|
+
* Build a compact project map (hubs + top-level layout) from the Compass DB.
|
|
10
|
+
* Truncates to the declared map token budget.
|
|
11
|
+
*
|
|
12
|
+
* @param projectPath - Project root with `.speclaw/index.db`.
|
|
13
|
+
* @returns Map body text (no markers), or null if the index is missing/empty.
|
|
14
|
+
*/
|
|
15
|
+
export function generateCompactMap(projectPath) {
|
|
16
|
+
const dbPath = path.join(projectPath, ".speclaw", "index.db");
|
|
17
|
+
if (!fs.existsSync(dbPath))
|
|
18
|
+
return null;
|
|
19
|
+
const db = openDb(projectPath);
|
|
20
|
+
try {
|
|
21
|
+
const fileCount = db.prepare("SELECT COUNT(*) AS c FROM files").get().c;
|
|
22
|
+
const nodeCount = db.prepare("SELECT COUNT(*) AS c FROM nodes").get().c;
|
|
23
|
+
if (fileCount === 0)
|
|
24
|
+
return null;
|
|
25
|
+
const hubs = db
|
|
26
|
+
.prepare(`SELECT n.name AS name, COUNT(*) AS fan_in
|
|
27
|
+
FROM edges e
|
|
28
|
+
JOIN nodes n ON n.id = e.dst_node_id
|
|
29
|
+
WHERE e.kind = 'call' AND e.dst_node_id IS NOT NULL
|
|
30
|
+
GROUP BY n.id
|
|
31
|
+
ORDER BY fan_in DESC
|
|
32
|
+
LIMIT 12`)
|
|
33
|
+
.all();
|
|
34
|
+
const topDirs = db
|
|
35
|
+
.prepare(`SELECT CASE
|
|
36
|
+
WHEN instr(path, '/') > 0 THEN substr(path, 1, instr(path, '/') - 1)
|
|
37
|
+
ELSE path
|
|
38
|
+
END AS top, COUNT(*) AS c
|
|
39
|
+
FROM files
|
|
40
|
+
GROUP BY top
|
|
41
|
+
ORDER BY c DESC
|
|
42
|
+
LIMIT 8`)
|
|
43
|
+
.all();
|
|
44
|
+
const hubLine = hubs.length === 0
|
|
45
|
+
? "hubs: (none yet)"
|
|
46
|
+
: `hubs: ${hubs.map((h) => `${h.name} ${h.fan_in}`).join(" · ")}`;
|
|
47
|
+
const dirLine = topDirs.map((d) => `${d.top}/ (${d.c})`).join(" ");
|
|
48
|
+
let body = [
|
|
49
|
+
`speclaw · ${fileCount} files · ${nodeCount} nodes`,
|
|
50
|
+
dirLine,
|
|
51
|
+
hubLine,
|
|
52
|
+
"entry: src/server.ts (mcp) · src/cli/index.ts (bin)",
|
|
53
|
+
].join("\n");
|
|
54
|
+
const cap = loadDeclaredBudget().map;
|
|
55
|
+
let omitted = false;
|
|
56
|
+
while (estimateTokens(body) > cap && hubs.length > 3) {
|
|
57
|
+
hubs.pop();
|
|
58
|
+
omitted = true;
|
|
59
|
+
const shorter = hubs.length === 0
|
|
60
|
+
? "hubs: (truncated)"
|
|
61
|
+
: `hubs: ${hubs.map((h) => `${h.name} ${h.fan_in}`).join(" · ")}`;
|
|
62
|
+
body = [
|
|
63
|
+
`speclaw · ${fileCount} files · ${nodeCount} nodes`,
|
|
64
|
+
dirLine,
|
|
65
|
+
shorter,
|
|
66
|
+
"entry: src/server.ts (mcp) · src/cli/index.ts (bin)",
|
|
67
|
+
omitted ? "(entries omitted to fit map budget)" : "",
|
|
68
|
+
]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
72
|
+
if (estimateTokens(body) > cap) {
|
|
73
|
+
body = [
|
|
74
|
+
`speclaw · ${fileCount} files · ${nodeCount} nodes`,
|
|
75
|
+
"entry: src/server.ts (mcp) · src/cli/index.ts (bin)",
|
|
76
|
+
"(entries omitted to fit map budget)",
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
return body;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
db.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Replace content between map markers in `docs/compass.md`. Preserves content
|
|
87
|
+
* outside markers. Does nothing (with reason) when markers are missing or the
|
|
88
|
+
* map cannot be generated.
|
|
89
|
+
*
|
|
90
|
+
* @param projectPath - Project root.
|
|
91
|
+
*/
|
|
92
|
+
export function writeCompactMap(projectPath) {
|
|
93
|
+
const compassPath = path.join(projectPath, "docs", "compass.md");
|
|
94
|
+
if (!fs.existsSync(compassPath)) {
|
|
95
|
+
return { written: false, reason: "docs/compass.md missing" };
|
|
96
|
+
}
|
|
97
|
+
const original = fs.readFileSync(compassPath, "utf8");
|
|
98
|
+
const start = original.indexOf(MAP_START);
|
|
99
|
+
const end = original.indexOf(MAP_END);
|
|
100
|
+
if (start < 0 || end < 0 || end < start) {
|
|
101
|
+
return { written: false, reason: "map markers missing — not regenerating" };
|
|
102
|
+
}
|
|
103
|
+
const map = generateCompactMap(projectPath);
|
|
104
|
+
if (!map) {
|
|
105
|
+
// Leave markers but clear body so we don't leave a stale map.
|
|
106
|
+
const next = original.slice(0, start + MAP_START.length) + "\n" + original.slice(end);
|
|
107
|
+
if (next !== original)
|
|
108
|
+
fs.writeFileSync(compassPath, next);
|
|
109
|
+
return { written: false, reason: "no index or empty graph — map omitted" };
|
|
110
|
+
}
|
|
111
|
+
const next = original.slice(0, start + MAP_START.length) + "\n" + map + "\n" + original.slice(end);
|
|
112
|
+
fs.writeFileSync(compassPath, next);
|
|
113
|
+
return { written: true };
|
|
114
|
+
}
|