@esneiderbravo/speclaw 2.0.4 → 2.0.6
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 +13 -13
- package/dist/cli/lib/ui.js +17 -13
- package/dist/modules/compass/visualize.js +15 -12
- package/dist/modules/foundation/integrity.js +20 -7
- package/dist/modules/foundation/lock.js +24 -7
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
<br/>
|
|
6
6
|
|
|
7
|
-
<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw"><img src="https://img.shields.io/npm/v/@esneiderbravo/speclaw?color=
|
|
8
|
-
<a href="https://github.com/esneiderbravo/speclaw/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/esneiderbravo/speclaw/ci.yml?branch=main&label=CI&labelColor=
|
|
9
|
-
<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw?activeTab=versions"><img src="https://img.shields.io/badge/provenance-SLSA-
|
|
10
|
-
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-
|
|
11
|
-
<img src="https://img.shields.io/badge/node-%E2%89%A522.16-
|
|
12
|
-
<img src="https://img.shields.io/badge/v2.0-
|
|
7
|
+
<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw"><img src="https://img.shields.io/npm/v/@esneiderbravo/speclaw?color=00707f&labelColor=131313&style=flat-square&label=npm" alt="npm"></a>
|
|
8
|
+
<a href="https://github.com/esneiderbravo/speclaw/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/esneiderbravo/speclaw/ci.yml?branch=main&label=CI&labelColor=131313&style=flat-square&color=00707f" alt="CI"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw?activeTab=versions"><img src="https://img.shields.io/badge/provenance-SLSA-00707f?labelColor=131313&style=flat-square" alt="npm provenance"></a>
|
|
10
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-00707f?labelColor=131313&style=flat-square" alt="MIT"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A522.16-00707f?labelColor=131313&style=flat-square" alt="Node >= 22.16">
|
|
12
|
+
<img src="https://img.shields.io/badge/v2.0-00707f?labelColor=131313&style=flat-square" alt="v2.0">
|
|
13
13
|
|
|
14
14
|
<br/><br/>
|
|
15
15
|
|
|
@@ -24,12 +24,12 @@ those laws hold in the editor, in CI, and in the PR.
|
|
|
24
24
|
One command. No cloud, no LLM, no API keys — <b>everything runs on your machine.</b>
|
|
25
25
|
</p>
|
|
26
26
|
|
|
27
|
-
<img src="https://img.shields.io/badge/100%25_local-
|
|
28
|
-
<img src="https://img.shields.io/badge/no_LLM_·_no_cloud-
|
|
29
|
-
<img src="https://img.shields.io/badge/CLI_+_MCP-
|
|
30
|
-
<img src="https://img.shields.io/badge/9_canonical_tools-
|
|
31
|
-
<img src="https://img.shields.io/badge/Cortex-One_brain._Many_agents.-
|
|
32
|
-
<img src="https://img.shields.io/badge/any_agent-
|
|
27
|
+
<img src="https://img.shields.io/badge/100%25_local-00707f?labelColor=131313&style=flat-square" alt="100% local">
|
|
28
|
+
<img src="https://img.shields.io/badge/no_LLM_·_no_cloud-00707f?labelColor=131313&style=flat-square" alt="no LLM">
|
|
29
|
+
<img src="https://img.shields.io/badge/CLI_+_MCP-00707f?labelColor=131313&style=flat-square" alt="CLI + MCP">
|
|
30
|
+
<img src="https://img.shields.io/badge/9_canonical_tools-00707f?labelColor=131313&style=flat-square" alt="9 tools">
|
|
31
|
+
<img src="https://img.shields.io/badge/Cortex-One_brain._Many_agents.-00707f?labelColor=131313&style=flat-square" alt="Cortex">
|
|
32
|
+
<img src="https://img.shields.io/badge/any_agent-00707f?labelColor=131313&style=flat-square" alt="any agent">
|
|
33
33
|
|
|
34
34
|
</div>
|
|
35
35
|
|
|
@@ -107,7 +107,7 @@ gh attestation verify <tarball> --owner esneiderbravo
|
|
|
107
107
|
<img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-init.png" width="800" alt="speclaw init — terminal output">
|
|
108
108
|
</p>
|
|
109
109
|
|
|
110
|
-
<p align="center"><i>
|
|
110
|
+
<p align="center"><i>Cyan steps, green checks, a live progress bar — themed with the speclaw palette.</i></p>
|
|
111
111
|
|
|
112
112
|
<br/>
|
|
113
113
|
|
package/dist/cli/lib/ui.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
// Brand-themed terminal UI. Colors come from the speclaw
|
|
2
|
-
// = the "law"
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Brand-themed terminal UI. Colors come from the speclaw site's ink theme
|
|
2
|
+
// (`app/tokens.css` in speclaw-site): signal cyan #00E3FD = the "law" accent,
|
|
3
|
+
// ink #F4F4F3 primary text, ink-faint #999EA3 secondary text, deny #FF5C47 for
|
|
4
|
+
// errors, and green/amber re-tuned for success/warning. Every color holds at
|
|
5
|
+
// least 4.5:1 (WCAG 2.x) against the ink paper #131313. They render as 24-bit
|
|
6
|
+
// truecolor ANSI — no dependency needed — and auto-disable when the output is
|
|
7
|
+
// not a TTY or NO_COLOR is set.
|
|
5
8
|
import { pkgVersion } from "../../shared/version.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
// Covers: req~brand-terminal-palette~1
|
|
10
|
+
export const PALETTE = Object.freeze({
|
|
11
|
+
cyan: [0, 227, 253], // #00E3FD — signal: the accent / "law"
|
|
12
|
+
cyanDim: [0, 227, 253], // #00E3FD — signal (the site has no mid cyan for text)
|
|
13
|
+
cream: [244, 244, 243], // #F4F4F3 — ink: primary text
|
|
14
|
+
muted: [153, 158, 163], // #999EA3 — ink-faint: secondary text
|
|
15
|
+
green: [62, 207, 122], // #3ECF7A — success (re-tuned)
|
|
16
|
+
amber: [245, 183, 61], // #F5B73D — warning (re-tuned)
|
|
17
|
+
red: [255, 92, 71], // #FF5C47 — deny: errors
|
|
18
|
+
});
|
|
15
19
|
const colorOn = (Boolean(process.stdout.isTTY) || process.env.FORCE_COLOR === "1") && !process.env.NO_COLOR;
|
|
16
20
|
// Whether the terminal reliably renders the unicode box/block glyphs the brand
|
|
17
21
|
// output uses. Non-Windows terminals are assumed capable; a Windows console is
|
|
@@ -88,11 +88,14 @@ export function visualize(projectPath, opts = {}) {
|
|
|
88
88
|
/**
|
|
89
89
|
* Render the graph as a self-contained, offline HTML page: the data is embedded
|
|
90
90
|
* as JSON and drawn by an inline canvas force-directed renderer (no CDN, no
|
|
91
|
-
* dependencies).
|
|
91
|
+
* dependencies). Colors are the site's ink tokens (`app/tokens.css` in
|
|
92
|
+
* speclaw-site): paper #131313, sheet #1f2022, rule #303236, ink #f4f4f3,
|
|
93
|
+
* ink-faint #999ea3, signal #00e3fd. No retired-palette color may return here.
|
|
92
94
|
*
|
|
93
95
|
* @param data - The nodes/links/totals to embed.
|
|
94
96
|
* @returns A complete HTML document as a string.
|
|
95
97
|
*/
|
|
98
|
+
// Covers: req~brand-viewer-palette~1
|
|
96
99
|
export function renderHtml(data) {
|
|
97
100
|
const payload = JSON.stringify(data);
|
|
98
101
|
return `<!doctype html>
|
|
@@ -100,20 +103,20 @@ export function renderHtml(data) {
|
|
|
100
103
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
101
104
|
<title>speclaw · Compass graph</title>
|
|
102
105
|
<style>
|
|
103
|
-
:root { --cy:#
|
|
106
|
+
:root { --cy:#00e3fd; --cr:#f4f4f3; --mu:#999ea3; --bg:#131313; --sh:#1f2022; --ru:#303236; }
|
|
104
107
|
* { box-sizing:border-box; }
|
|
105
108
|
html,body { margin:0; height:100%; background:var(--bg); color:var(--cr);
|
|
106
|
-
font-family:'
|
|
109
|
+
font-family:'Chivo Mono','SFMono-Regular',Menlo,monospace; overflow:hidden; }
|
|
107
110
|
canvas { display:block; cursor:grab; }
|
|
108
111
|
canvas:active { cursor:grabbing; }
|
|
109
112
|
.panel { position:fixed; top:16px; left:16px; padding:12px 14px; border-radius:10px;
|
|
110
|
-
background:rgba(
|
|
113
|
+
background:rgba(31,32,34,.82); border:1px solid var(--ru); font-size:12px; line-height:1.5; }
|
|
111
114
|
.panel b { color:var(--cr); } .panel .sub { color:var(--mu); }
|
|
112
115
|
.legend { position:fixed; bottom:16px; left:16px; font-size:11px; color:var(--mu);
|
|
113
|
-
background:rgba(
|
|
116
|
+
background:rgba(31,32,34,.82); border:1px solid var(--ru); border-radius:10px; padding:10px 12px; }
|
|
114
117
|
.legend span { display:inline-flex; align-items:center; margin-right:12px; }
|
|
115
118
|
.legend i { width:9px; height:9px; border-radius:50%; display:inline-block; margin-right:5px; }
|
|
116
|
-
.tip { position:fixed; padding:6px 9px; border-radius:7px; background
|
|
119
|
+
.tip { position:fixed; padding:6px 9px; border-radius:7px; background:var(--sh); border:1px solid var(--cy);
|
|
117
120
|
color:var(--cr); font-size:12px; pointer-events:none; opacity:0; transition:opacity .1s; white-space:nowrap; }
|
|
118
121
|
.brand { color:var(--cy); font-weight:700; }
|
|
119
122
|
</style></head><body>
|
|
@@ -127,9 +130,9 @@ export function renderHtml(data) {
|
|
|
127
130
|
<div class="tip" id="tip"></div>
|
|
128
131
|
<script>
|
|
129
132
|
const DATA = ${payload};
|
|
130
|
-
const KIND_COLORS = { function:'#
|
|
131
|
-
interface:'#
|
|
132
|
-
const colorOf = k => KIND_COLORS[k] || '#
|
|
133
|
+
const KIND_COLORS = { function:'#00e3fd', method:'#3ecf7a', class:'#f5b73d',
|
|
134
|
+
interface:'#babdc1', type:'#babdc1', enum:'#babdc1' };
|
|
135
|
+
const colorOf = k => KIND_COLORS[k] || '#7c8083';
|
|
133
136
|
|
|
134
137
|
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
|
|
135
138
|
const tip = document.getElementById('tip');
|
|
@@ -179,7 +182,7 @@ function draw(){
|
|
|
179
182
|
ctx.lineWidth = 1;
|
|
180
183
|
for(const l of L){ const a=toScreen(N[l.s]), b=toScreen(N[l.t]);
|
|
181
184
|
const on = hover>=0 && (l.s===hover||l.t===hover);
|
|
182
|
-
ctx.strokeStyle = on ? 'rgba(
|
|
185
|
+
ctx.strokeStyle = on ? 'rgba(0,227,253,.7)' : 'rgba(124,128,131,.16)';
|
|
183
186
|
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
|
|
184
187
|
}
|
|
185
188
|
for(let i=0;i<N.length;i++){ const n=N[i], p=toScreen(n);
|
|
@@ -187,7 +190,7 @@ function draw(){
|
|
|
187
190
|
ctx.globalAlpha = dim ? 0.28 : 1;
|
|
188
191
|
ctx.beginPath(); ctx.arc(p.x,p.y,n.r*Math.sqrt(scale),0,7); ctx.fillStyle=colorOf(n.kind); ctx.fill();
|
|
189
192
|
if((n.r>7 || i===hover || (hoverSet&&hoverSet.has(i))) && scale>0.5){
|
|
190
|
-
ctx.globalAlpha = dim?0.4:0.9; ctx.fillStyle='#
|
|
193
|
+
ctx.globalAlpha = dim?0.4:0.9; ctx.fillStyle='#f4f4f3'; ctx.font="11px 'Chivo Mono','SFMono-Regular',Menlo,monospace";
|
|
191
194
|
ctx.fillText(n.name, p.x+n.r+3, p.y+3);
|
|
192
195
|
}
|
|
193
196
|
}
|
|
@@ -208,7 +211,7 @@ addEventListener('mousemove', e=>{
|
|
|
208
211
|
lastX=e.clientX; lastY=e.clientY;
|
|
209
212
|
const i=pick(e.clientX,e.clientY); hover=i;
|
|
210
213
|
if(i>=0){ const n=N[i]; tip.style.opacity=1; tip.style.left=(e.clientX+12)+'px'; tip.style.top=(e.clientY+12)+'px';
|
|
211
|
-
tip.innerHTML='<b>'+n.name+'</b> <span style="color:#
|
|
214
|
+
tip.innerHTML='<b>'+n.name+'</b> <span style="color:#999ea3">'+n.kind+' · '+n.file+':'+n.line+'</span>'; }
|
|
212
215
|
else tip.style.opacity=0;
|
|
213
216
|
});
|
|
214
217
|
addEventListener('mouseup', ()=>{ dragging=null; panning=false; });
|
|
@@ -9,7 +9,9 @@ import { digestText, discoverIntegrityPaths, integrityPolicy, isRegenerableIdeMi
|
|
|
9
9
|
import { loadScanSuppressions, scanPaths } from "./scan.js";
|
|
10
10
|
/**
|
|
11
11
|
* Verify digests and/or scan rule files. Missing lockfile is soft (ok=true)
|
|
12
|
-
* with guidance to run `speclaw laws lock`.
|
|
12
|
+
* with guidance to run `speclaw laws lock`. Lock entries for regenerable IDE
|
|
13
|
+
* mirrors (files or the `.claude/rules/speclaw` symlink) that are missing or
|
|
14
|
+
* retargeted only warn; other managed symlinks fail strictly.
|
|
13
15
|
*/
|
|
14
16
|
export function verifyIntegrity(opts) {
|
|
15
17
|
const projectPath = opts.projectPath;
|
|
@@ -174,6 +176,11 @@ export function verifyIntegrity(opts) {
|
|
|
174
176
|
catch {
|
|
175
177
|
actual = null;
|
|
176
178
|
}
|
|
179
|
+
// Entries for regenerable mirror links come from locks written before
|
|
180
|
+
// they stopped being pinned; the link targets gitignored `ai-specs/` and
|
|
181
|
+
// is absent or dangling on clean clones, so it warns only and never
|
|
182
|
+
// fails CI.
|
|
183
|
+
const regenerable = isRegenerableIdeMirror(rel);
|
|
177
184
|
if (actual === null) {
|
|
178
185
|
symlinks.push({
|
|
179
186
|
path: rel,
|
|
@@ -181,13 +188,16 @@ export function verifyIntegrity(opts) {
|
|
|
181
188
|
actualTarget: null,
|
|
182
189
|
status: "missing",
|
|
183
190
|
});
|
|
184
|
-
|
|
191
|
+
if (!regenerable)
|
|
192
|
+
ok = false;
|
|
185
193
|
verifyFindings.push({
|
|
186
194
|
lawId: "integrity~symlink~1",
|
|
187
|
-
severity: "error",
|
|
195
|
+
severity: regenerable ? "warn" : "error",
|
|
188
196
|
engine: "integrity",
|
|
189
197
|
file: rel,
|
|
190
|
-
message:
|
|
198
|
+
message: regenerable
|
|
199
|
+
? "Regenerable IDE mirror symlink missing — run `speclaw update` or `speclaw laws lock` to drop the stale entry"
|
|
200
|
+
: `Managed symlink missing (expected → ${entry.target})`,
|
|
191
201
|
});
|
|
192
202
|
}
|
|
193
203
|
else if (normalizeLink(actual) !== normalizeLink(entry.target)) {
|
|
@@ -197,13 +207,16 @@ export function verifyIntegrity(opts) {
|
|
|
197
207
|
actualTarget: actual,
|
|
198
208
|
status: "mismatch",
|
|
199
209
|
});
|
|
200
|
-
|
|
210
|
+
if (!regenerable)
|
|
211
|
+
ok = false;
|
|
201
212
|
verifyFindings.push({
|
|
202
213
|
lawId: "integrity~symlink~1",
|
|
203
|
-
severity: "error",
|
|
214
|
+
severity: regenerable ? "warn" : "error",
|
|
204
215
|
engine: "integrity",
|
|
205
216
|
file: rel,
|
|
206
|
-
message:
|
|
217
|
+
message: regenerable
|
|
218
|
+
? "Regenerable IDE mirror symlink retargeted — run `speclaw update` or `speclaw laws lock` to drop the stale entry"
|
|
219
|
+
: `Managed symlink retargeted`,
|
|
207
220
|
detail: `expected ${entry.target} found ${actual}`,
|
|
208
221
|
});
|
|
209
222
|
}
|
|
@@ -106,13 +106,13 @@ export function buildLock(opts) {
|
|
|
106
106
|
/** Integrity severity policy for a project-relative path. */
|
|
107
107
|
export function integrityPolicy(relPath) {
|
|
108
108
|
const n = relPath.split("\\").join("/");
|
|
109
|
-
//
|
|
110
|
-
//
|
|
109
|
+
// Regenerable IDE mirrors of gitignored `ai-specs/` (`.cursor/rules/`, the
|
|
110
|
+
// `.claude/rules/speclaw` link) fall through to scan-only: lock/CI must not
|
|
111
|
+
// treat them as strict committed files; scan when present, never pin.
|
|
111
112
|
if (n === "AGENTS.md" ||
|
|
112
113
|
n === "CLAUDE.md" ||
|
|
113
114
|
n.startsWith(".github/instructions/") ||
|
|
114
|
-
n === ".coderabbit.yaml"
|
|
115
|
-
n === ".claude/rules/speclaw") {
|
|
115
|
+
n === ".coderabbit.yaml") {
|
|
116
116
|
return "strict";
|
|
117
117
|
}
|
|
118
118
|
if (n === "LAWS.md" || n === "docs/compass.md" || n.startsWith("docs/standards/")) {
|
|
@@ -120,10 +120,22 @@ export function integrityPolicy(relPath) {
|
|
|
120
120
|
}
|
|
121
121
|
return "scan-only";
|
|
122
122
|
}
|
|
123
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* True when a path is an IDE mirror of regenerable (typically gitignored) content.
|
|
125
|
+
*
|
|
126
|
+
* Covers the `.claude/rules/speclaw` symlink as well as mirror files: the link
|
|
127
|
+
* targets gitignored `ai-specs/rules`, so on a clean clone it is absent (when
|
|
128
|
+
* the project ignores it) or dangling, and must never be pinned in, or fail
|
|
129
|
+
* verify against, the lock.
|
|
130
|
+
*
|
|
131
|
+
* @param relPath - Project-relative path (file or symlink).
|
|
132
|
+
* @returns Whether lock/verify should treat the path as regenerable.
|
|
133
|
+
*/
|
|
124
134
|
export function isRegenerableIdeMirror(relPath) {
|
|
125
135
|
const n = relPath.split("\\").join("/");
|
|
126
|
-
return (n
|
|
136
|
+
return (n === ".claude/rules/speclaw" ||
|
|
137
|
+
n.startsWith(".claude/rules/speclaw/") ||
|
|
138
|
+
n.startsWith(".cursor/rules/") ||
|
|
127
139
|
n.startsWith(".cursor/skills/") ||
|
|
128
140
|
n.startsWith(".cursor/commands/") ||
|
|
129
141
|
n.startsWith(".claude/skills/") ||
|
|
@@ -226,6 +238,8 @@ export function prepareIntegrityText(relPath, raw) {
|
|
|
226
238
|
* Snapshot digests for discovered files with ownership policy.
|
|
227
239
|
* For `.coderabbit.yaml`, digests only the speclaw delimited block when present.
|
|
228
240
|
* For `docs/compass.md`, digests with the regenerable map body stripped.
|
|
241
|
+
* Regenerable IDE mirror symlinks ({@link isRegenerableIdeMirror}) are never
|
|
242
|
+
* pinned, so the lock is identical with or without the local link.
|
|
229
243
|
*/
|
|
230
244
|
export function snapshotLockEntries(projectPath) {
|
|
231
245
|
const { files: paths, symlinks } = discoverIntegrityPaths(projectPath);
|
|
@@ -239,8 +253,11 @@ export function snapshotLockEntries(projectPath) {
|
|
|
239
253
|
files[rel] = { digest: digestText(raw), ownership };
|
|
240
254
|
}
|
|
241
255
|
const symlinkMap = {};
|
|
242
|
-
for (const s of symlinks)
|
|
256
|
+
for (const s of symlinks) {
|
|
257
|
+
if (isRegenerableIdeMirror(s.path))
|
|
258
|
+
continue;
|
|
243
259
|
symlinkMap[s.path] = { target: s.target };
|
|
260
|
+
}
|
|
244
261
|
return { files, symlinks: symlinkMap };
|
|
245
262
|
}
|
|
246
263
|
/** Extract a speclaw-marked region from coderabbit yaml if present. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esneiderbravo/speclaw",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -59,6 +59,8 @@
|
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@eslint/js": "^10.0.1",
|
|
62
|
+
"@fontsource/chivo": "^5.3.0",
|
|
63
|
+
"@fontsource/chivo-mono": "^5.3.0",
|
|
62
64
|
"@resvg/resvg-js": "^2.6.2",
|
|
63
65
|
"@types/node": "^22.0.0",
|
|
64
66
|
"eslint": "^10.8.0",
|
|
@@ -67,6 +69,7 @@
|
|
|
67
69
|
"globals": "^17.9.0",
|
|
68
70
|
"prettier": "^3.9.6",
|
|
69
71
|
"typescript": "^5.6.0",
|
|
70
|
-
"typescript-eslint": "^8.65.0"
|
|
72
|
+
"typescript-eslint": "^8.65.0",
|
|
73
|
+
"wawoff2": "^2.0.1"
|
|
71
74
|
}
|
|
72
75
|
}
|