@esneiderbravo/speclaw 2.0.4 → 2.0.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.
@@ -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
- ok = false;
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: `Managed symlink missing (expected → ${entry.target})`,
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
- ok = false;
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: `Managed symlink retargeted`,
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
- // `.cursor/rules/` mirrors regenerable `ai-specs/` (gitignored) — lock/CI must
110
- // not treat them as strict committed files; scan when present, never pin.
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
- /** True when a path is an IDE mirror of regenerable (typically gitignored) content. */
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.startsWith(".cursor/rules/") ||
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.4",
3
+ "version": "2.0.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },