@esneiderbravo/speclaw 0.3.10 → 0.3.12
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/dist/cli/commands/lawbook.js +43 -6
- package/dist/cli/commands/query.js +49 -2
- package/dist/cli/commands/quick.js +35 -0
- package/dist/cli/commands/update.js +18 -0
- package/dist/cli/index.js +17 -3
- package/dist/modules/compass/db.js +15 -2
- package/dist/modules/compass/extract.js +44 -0
- package/dist/modules/compass/git-history-cache.js +19 -2
- package/dist/modules/compass/hotspots.js +230 -0
- package/dist/modules/compass/indexer.js +2 -0
- package/dist/modules/compass/languages.js +39 -0
- package/dist/modules/compass/register.js +17 -0
- package/dist/modules/foundation/doctor.js +63 -0
- package/dist/modules/lawbook/assets/commands/archive.md +5 -6
- package/dist/modules/lawbook/assets/commands/draft.md +6 -7
- package/dist/modules/lawbook/assets/commands/quick.md +14 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +4 -3
- package/dist/modules/lawbook/assets/skills/draft/SKILL.md +1 -1
- package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +3 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +28 -25
- package/dist/modules/lawbook/assets/skills/quick/SKILL.md +11 -0
- package/dist/modules/lawbook/assets/skills/quick/steps/01-scaffold.md +6 -0
- package/dist/modules/lawbook/assets/skills/quick/steps/02-implement.md +7 -0
- package/dist/modules/lawbook/engine.js +115 -55
- package/dist/modules/lawbook/levels.js +421 -0
- package/dist/modules/lawbook/quick.js +86 -0
- package/dist/modules/lawbook/register.js +10 -0
- package/dist/shared/exposure.js +3 -0
- package/dist/shared/git-history.js +85 -5
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { coverageArchiveBlockers } from "./coverage.js";
|
|
4
4
|
import { sealCapability } from "./anchors.js";
|
|
5
|
+
import { artifactNeeds, confirmedLevel, countUncheckedTasks, gatherSignals, hasDisciplineReport, loadCeremonyConfig, proposeLevel, } from "./levels.js";
|
|
5
6
|
// speclaw's own spec-driven workflow engine. Inspired by OpenSpec's model
|
|
6
7
|
// (proposals, delta specs, changes, archive) but implemented from scratch and
|
|
7
8
|
// deliberately simpler: a change's specs/ holds the full intended spec for each
|
|
@@ -33,25 +34,33 @@ mandatory_task_steps:
|
|
|
33
34
|
- "Archive the change within the same PR (lawbook:archive)."
|
|
34
35
|
|
|
35
36
|
# A change is required for new behavior, endpoints, schema changes, or UI flows;
|
|
36
|
-
# one-line fixes
|
|
37
|
+
# one-line fixes may use ceremony level 0 (\`speclaw quick\`) instead of full artifacts.
|
|
38
|
+
|
|
39
|
+
# Ceremony levels (adaptive). Defaults match speclaw's built-in thresholds.
|
|
40
|
+
ceremony:
|
|
41
|
+
cuts: [3, 8, 15]
|
|
42
|
+
hotspotFloor: 0.7
|
|
37
43
|
`;
|
|
38
44
|
const README_MD = `# lawbook/ — the spec-driven workflow (speclaw)
|
|
39
45
|
|
|
40
46
|
This directory is managed by speclaw's **lawbook** module.
|
|
41
47
|
|
|
42
48
|
- \`specs/\` — the canonical specifications (the current source of truth).
|
|
43
|
-
- \`changes/<name>/\` — an in-flight change
|
|
44
|
-
|
|
49
|
+
- \`changes/<name>/\` — an in-flight change. Artifact volume follows the
|
|
50
|
+
confirmed ceremony level in \`change.json\` (0=quick … 3=full). Missing
|
|
51
|
+
\`change.json\` means level 3 (proposal, design, tasks, delta specs).
|
|
45
52
|
- \`changes/archive/\` — completed, archived changes.
|
|
46
|
-
- \`config.yaml\` — mandatory task steps and
|
|
53
|
+
- \`config.yaml\` — mandatory task steps, coverage, and optional ceremony cuts.
|
|
47
54
|
|
|
48
55
|
## Workflow
|
|
49
56
|
|
|
50
|
-
1. \`lawbook:
|
|
51
|
-
2. \`lawbook:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
1. \`lawbook:explore\` — think through an idea before or during a change.
|
|
58
|
+
2. \`lawbook:draft\` / \`speclaw quick\` — propose/confirm a ceremony level, then
|
|
59
|
+
scaffold only the artifacts that level requires.
|
|
60
|
+
3. \`lawbook:build\` — implement the tasks.
|
|
61
|
+
4. \`lawbook:sync\` — promote the change's delta specs into \`specs/\` (when the
|
|
62
|
+
level requires specs).
|
|
63
|
+
5. \`lawbook:archive\` — sync (if needed) + move the change to \`changes/archive/\`.
|
|
55
64
|
`;
|
|
56
65
|
/**
|
|
57
66
|
* Initialize the spec/ workspace, creating the specs/, changes/, and archive
|
|
@@ -146,18 +155,17 @@ function deltaSpecFiles(changeDir) {
|
|
|
146
155
|
return out;
|
|
147
156
|
}
|
|
148
157
|
/**
|
|
149
|
-
* Validate a change's artifacts
|
|
150
|
-
*
|
|
151
|
-
* header, and a "#### Scenario:" acceptance criterion.
|
|
158
|
+
* Validate a change's artifacts against its confirmed ceremony level
|
|
159
|
+
* (missing `change.json` ⇒ level 3 / full ceremony).
|
|
152
160
|
*
|
|
153
161
|
* @param projectPath - Absolute path to the project root.
|
|
154
162
|
* @param change - Change name (folder under lawbook/changes/).
|
|
155
|
-
* @
|
|
156
|
-
* for a missing change — it is reported as an issue with `valid: false`.
|
|
163
|
+
* @param remeasure - Optional targets to re-score scope growth (paths/symbols).
|
|
157
164
|
*/
|
|
158
|
-
export function specValidate(projectPath, change) {
|
|
165
|
+
export function specValidate(projectPath, change, remeasure) {
|
|
159
166
|
const changeDir = path.join(specRoot(projectPath), "changes", change);
|
|
160
167
|
const issues = [];
|
|
168
|
+
const warnings = [];
|
|
161
169
|
if (!fs.existsSync(changeDir)) {
|
|
162
170
|
return {
|
|
163
171
|
change,
|
|
@@ -167,18 +175,43 @@ export function specValidate(projectPath, change) {
|
|
|
167
175
|
deltaSpecs: [],
|
|
168
176
|
};
|
|
169
177
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
const level = confirmedLevel(projectPath, change);
|
|
179
|
+
const needs = artifactNeeds(level);
|
|
180
|
+
if (needs.record && !fs.existsSync(path.join(changeDir, "record.md"))) {
|
|
181
|
+
issues.push(`missing record.md (required at ceremony level ${level})`);
|
|
182
|
+
}
|
|
183
|
+
if (needs.proposal && !fs.existsSync(path.join(changeDir, "proposal.md"))) {
|
|
184
|
+
issues.push(`missing proposal.md (required at ceremony level ${level})`);
|
|
185
|
+
}
|
|
186
|
+
if (needs.design && !fs.existsSync(path.join(changeDir, "design.md"))) {
|
|
187
|
+
issues.push(`missing design.md (required at ceremony level ${level})`);
|
|
188
|
+
}
|
|
189
|
+
if (needs.designOptionalWithJustification && !fs.existsSync(path.join(changeDir, "design.md"))) {
|
|
190
|
+
const record = path.join(changeDir, "record.md");
|
|
191
|
+
const text = fs.existsSync(record) ? fs.readFileSync(record, "utf8") : "";
|
|
192
|
+
if (!/design\s*(omitted|skipped|n\/a)/i.test(text) && !/why.*design/i.test(text)) {
|
|
193
|
+
issues.push(`level ${level}: design.md omitted without justification in record.md`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (needs.tasksFile && !fs.existsSync(path.join(changeDir, "tasks.md"))) {
|
|
197
|
+
issues.push(`missing tasks.md (required at ceremony level ${level})`);
|
|
198
|
+
}
|
|
175
199
|
const deltas = deltaSpecFiles(changeDir);
|
|
176
|
-
if (deltas.length === 0)
|
|
177
|
-
issues.push(
|
|
200
|
+
if (needs.deltaSpecs && deltas.length === 0) {
|
|
201
|
+
issues.push(`no delta specs under specs/ (required at ceremony level ${level})`);
|
|
202
|
+
}
|
|
203
|
+
// Scope-growth: when remeasure targets provided (or change.json has prior signals
|
|
204
|
+
// with paths we cannot recover), only check if caller passes targets.
|
|
205
|
+
if (remeasure && (remeasure.paths.length > 0 || remeasure.symbols.length > 0)) {
|
|
206
|
+
const { thresholds } = loadCeremonyConfig(projectPath);
|
|
207
|
+
const measured = proposeLevel(gatherSignals(projectPath, remeasure, thresholds), thresholds);
|
|
208
|
+
if (measured.level !== null && measured.level >= level + 2) {
|
|
209
|
+
issues.push(`scope grew: measured level ${measured.level}, recorded ${level} — run promote or justify (${measured.rationale})`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
178
212
|
const root = specRoot(projectPath);
|
|
179
213
|
const changeSpecs = path.join(changeDir, "specs");
|
|
180
214
|
const capabilities = canonicalCapabilities(root);
|
|
181
|
-
const warnings = [];
|
|
182
215
|
for (const file of deltas) {
|
|
183
216
|
const rel = path.relative(changeDir, file);
|
|
184
217
|
const content = fs.readFileSync(file, "utf8");
|
|
@@ -191,7 +224,6 @@ export function specValidate(projectPath, change) {
|
|
|
191
224
|
if (!/^###\s+Requirement:/m.test(content)) {
|
|
192
225
|
issues.push(`${rel}: no "### Requirement:" header`);
|
|
193
226
|
}
|
|
194
|
-
// Advisory divergence checks against the canonical specs.
|
|
195
227
|
const relFromSpecs = path.relative(changeSpecs, file);
|
|
196
228
|
const capability = relFromSpecs.split(path.sep)[0];
|
|
197
229
|
const nearMatch = nearMatchCapability(capability, capabilities);
|
|
@@ -268,10 +300,9 @@ export function specSync(projectPath, change) {
|
|
|
268
300
|
* Deterministic completeness checks that gate archiving a change. Returns the
|
|
269
301
|
* blocking reasons; an empty array means the change may be archived.
|
|
270
302
|
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* the last spec edit). The reports/README.md scaffold does not count as a report.
|
|
303
|
+
* Gates respect the confirmed ceremony level (missing `change.json` ⇒ level 3).
|
|
304
|
+
* Every level still requires checked tasks and a discipline report. Delta-spec
|
|
305
|
+
* sync is required only when the level demands delta specs.
|
|
275
306
|
*
|
|
276
307
|
* @param projectPath - Absolute path to the project root.
|
|
277
308
|
* @param change - Change name (folder under lawbook/changes/).
|
|
@@ -283,33 +314,46 @@ export function specArchivePreconditions(projectPath, change) {
|
|
|
283
314
|
if (!fs.existsSync(changeDir))
|
|
284
315
|
return [`change "${change}" not found under lawbook/changes/`];
|
|
285
316
|
const blockers = [];
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
317
|
+
const level = confirmedLevel(projectPath, change);
|
|
318
|
+
const needs = artifactNeeds(level);
|
|
319
|
+
// 1. Every task must be checked (tasks.md, or checklist in record.md at level 0).
|
|
320
|
+
if (needs.tasksFile) {
|
|
321
|
+
const tasksPath = path.join(changeDir, "tasks.md");
|
|
322
|
+
if (!fs.existsSync(tasksPath)) {
|
|
323
|
+
blockers.push("missing tasks.md");
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
const unchecked = countUncheckedTasks(fs.readFileSync(tasksPath, "utf8"));
|
|
327
|
+
if (unchecked > 0)
|
|
328
|
+
blockers.push(`${unchecked} unchecked task(s) in tasks.md`);
|
|
329
|
+
}
|
|
290
330
|
}
|
|
291
|
-
else {
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
blockers.push(
|
|
331
|
+
else if (needs.record) {
|
|
332
|
+
const recordPath = path.join(changeDir, "record.md");
|
|
333
|
+
if (!fs.existsSync(recordPath)) {
|
|
334
|
+
blockers.push("missing record.md");
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
const unchecked = countUncheckedTasks(fs.readFileSync(recordPath, "utf8"));
|
|
338
|
+
if (unchecked > 0)
|
|
339
|
+
blockers.push(`${unchecked} unchecked task(s) in record.md`);
|
|
340
|
+
}
|
|
295
341
|
}
|
|
296
342
|
// 2. At least one discipline report must exist (README.md scaffold aside).
|
|
297
|
-
|
|
298
|
-
const reports = fs.existsSync(reportsDir)
|
|
299
|
-
? fs.readdirSync(reportsDir).filter((n) => n.endsWith(".md") && n.toLowerCase() !== "readme.md")
|
|
300
|
-
: [];
|
|
301
|
-
if (reports.length === 0) {
|
|
343
|
+
if (!hasDisciplineReport(changeDir)) {
|
|
302
344
|
blockers.push("no discipline report under reports/ (build must record what was tested)");
|
|
303
345
|
}
|
|
304
|
-
// 3. Delta specs must already be synced
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
346
|
+
// 3. Delta specs must already be synced — only when the level requires them.
|
|
347
|
+
if (needs.deltaSpecs) {
|
|
348
|
+
for (const file of deltaSpecFiles(changeDir)) {
|
|
349
|
+
const rel = path.relative(path.join(changeDir, "specs"), file);
|
|
350
|
+
const canonical = path.join(root, "specs", rel);
|
|
351
|
+
if (!fs.existsSync(canonical)) {
|
|
352
|
+
blockers.push(`spec not synced: lawbook/specs/${rel} missing (run sync first)`);
|
|
353
|
+
}
|
|
354
|
+
else if (fs.readFileSync(file, "utf8") !== fs.readFileSync(canonical, "utf8")) {
|
|
355
|
+
blockers.push(`spec not synced: lawbook/specs/${rel} differs from the delta (run sync first)`);
|
|
356
|
+
}
|
|
313
357
|
}
|
|
314
358
|
}
|
|
315
359
|
// 4. Opt-in coverage gate: only when the change's delta specs declare ids.
|
|
@@ -317,8 +361,8 @@ export function specArchivePreconditions(projectPath, change) {
|
|
|
317
361
|
return blockers;
|
|
318
362
|
}
|
|
319
363
|
/**
|
|
320
|
-
* Finalize a change: promote its delta specs (via {@link specSync})
|
|
321
|
-
* it to changes/archive/<date>-<name>/.
|
|
364
|
+
* Finalize a change: promote its delta specs (via {@link specSync}) when the
|
|
365
|
+
* ceremony level requires them, then move it to changes/archive/<date>-<name>/.
|
|
322
366
|
*
|
|
323
367
|
* @param projectPath - Absolute path to the project root.
|
|
324
368
|
* @param change - Change name (folder under lawbook/changes/).
|
|
@@ -336,7 +380,11 @@ export function specArchive(projectPath, change, date) {
|
|
|
336
380
|
if (blockers.length > 0) {
|
|
337
381
|
throw new Error(`cannot archive "${change}" — resolve first:\n${blockers.map((b) => ` - ${b}`).join("\n")}`);
|
|
338
382
|
}
|
|
339
|
-
const
|
|
383
|
+
const level = confirmedLevel(projectPath, change);
|
|
384
|
+
const sync = artifactNeeds(level).deltaSpecs
|
|
385
|
+
? specSync(projectPath, change)
|
|
386
|
+
: { change, promoted: [], created: [], updated: [] };
|
|
387
|
+
const { promoted, created, updated } = sync;
|
|
340
388
|
const seals = sealPromotedCapabilities(projectPath, change, [
|
|
341
389
|
...promoted,
|
|
342
390
|
...created,
|
|
@@ -391,7 +439,13 @@ function sealPromotedCapabilities(projectPath, change, promotedPaths) {
|
|
|
391
439
|
export function specList(projectPath) {
|
|
392
440
|
const root = specRoot(projectPath);
|
|
393
441
|
if (!fs.existsSync(root)) {
|
|
394
|
-
return {
|
|
442
|
+
return {
|
|
443
|
+
initialized: false,
|
|
444
|
+
activeChanges: [],
|
|
445
|
+
activeLevels: {},
|
|
446
|
+
archivedChanges: [],
|
|
447
|
+
capabilities: [],
|
|
448
|
+
};
|
|
395
449
|
}
|
|
396
450
|
const dirsIn = (rel) => {
|
|
397
451
|
const abs = path.join(root, rel);
|
|
@@ -402,9 +456,15 @@ export function specList(projectPath) {
|
|
|
402
456
|
.filter((e) => e.isDirectory() && e.name !== "archive")
|
|
403
457
|
.map((e) => e.name);
|
|
404
458
|
};
|
|
459
|
+
const activeChanges = dirsIn("changes");
|
|
460
|
+
const activeLevels = {};
|
|
461
|
+
for (const name of activeChanges) {
|
|
462
|
+
activeLevels[name] = confirmedLevel(projectPath, name);
|
|
463
|
+
}
|
|
405
464
|
return {
|
|
406
465
|
initialized: true,
|
|
407
|
-
activeChanges
|
|
466
|
+
activeChanges,
|
|
467
|
+
activeLevels,
|
|
408
468
|
archivedChanges: dirsIn("changes/archive"),
|
|
409
469
|
capabilities: dirsIn("specs"),
|
|
410
470
|
};
|