@nurix/etna 0.19.90 → 0.19.91

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "8d6f9b80d64d",
2
+ "fingerprint": "b52694cf6295",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.10.13",
4
+ "version": "3.10.14",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -15,5 +15,5 @@
15
15
  "docs/memory/main.md",
16
16
  ".mcp.json"
17
17
  ],
18
- "digest": "e58c5f75136f"
18
+ "digest": "98eb07166e9f"
19
19
  }
@@ -26,11 +26,13 @@ import {
26
26
  formatNeedsSessionRow,
27
27
  appendRows,
28
28
  resolveRows,
29
- unexpiredWaivers,
29
+ waiverCovers,
30
30
  stateDir,
31
31
  recordTouched,
32
32
  } from "./lib/state.mjs";
33
- import { frozenStatus } from "./lib/frozen.mjs";
33
+ import { readSafe, relIfInside, today, SOURCE_EXT, commandRoot } from "./lib/repo.mjs";
34
+ import { listPlans } from "./lib/plans.mjs";
35
+ import { frozenStatusAtHead } from "./lib/frozen.mjs";
34
36
  import { approvedPackages, rejectedPackages } from "./lib/approved.mjs";
35
37
  import {
36
38
  recordSession,
@@ -42,24 +44,6 @@ import {
42
44
  } from "./lib/claims.mjs";
43
45
  import { appendEvent } from "./lib/session-log.mjs";
44
46
 
45
- const today = () => new Date().toISOString().slice(0, 10);
46
-
47
- function readSafe(file) {
48
- try {
49
- return fs.readFileSync(file, "utf8");
50
- } catch (e) {
51
- return "";
52
- }
53
- }
54
-
55
- function readJsonSafe(file) {
56
- try {
57
- return JSON.parse(fs.readFileSync(file, "utf8"));
58
- } catch (e) {
59
- return null;
60
- }
61
- }
62
-
63
47
  // ---------------------------------------------------------------------------
64
48
  // SessionStart
65
49
  // ---------------------------------------------------------------------------
@@ -79,7 +63,12 @@ function detectPackageManager(projectDir) {
79
63
  }
80
64
 
81
65
  function harnessLine(projectDir) {
82
- const marker = readJsonSafe(path.join(projectDir, ".claude", "harness.json"));
66
+ let marker = null;
67
+ try {
68
+ marker = JSON.parse(readSafe(path.join(projectDir, ".claude", "harness.json")));
69
+ } catch (e) {
70
+ /* absent or unparseable — no line */
71
+ }
83
72
  if (!marker || !marker.version) return null;
84
73
  const structure = readSafe(path.join(projectDir, "docs", "structure.md"));
85
74
  const stamped = (/harness-version:\s*([\w.-]+)/.exec(frontmatter(structure) || structure) || [])[1];
@@ -104,26 +93,14 @@ function memoryLine(projectDir) {
104
93
  }
105
94
 
106
95
  function openPlanLines(projectDir) {
107
- const dir = path.join(projectDir, "docs", "history", "plans");
108
- let entries;
109
- try {
110
- entries = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
111
- } catch (e) {
112
- return [];
113
- }
114
- const lines = [];
115
- for (const file of entries) {
116
- const text = readSafe(path.join(dir, file));
117
- const status = (/^\*\*Status:\*\*\s*(.+)$/m.exec(text) || [])[1]?.trim();
118
- if (!status || /Complete|Abandoned/.test(status)) continue;
119
- const gates = (/^\*\*Gates:\*\*\s*(.+)$/m.exec(text) || [])[1]?.trim();
120
- const ledgerRows = (text.match(/^\|\s*\d+\s*\|/gm) || []).length;
121
- const parts = [`Open plan: ${file} (${status}`];
122
- if (gates) parts.push(` · Gates: ${gates}`);
123
- if (ledgerRows) parts.push(` · ledger: ${ledgerRows} rows`);
124
- lines.push(parts.join("") + ")");
125
- }
126
- return lines;
96
+ return listPlans(projectDir)
97
+ .filter((p) => !/Complete|Abandoned/.test(p.status))
98
+ .map((p) => {
99
+ const parts = [`Open plan: ${p.file} (${p.status}`];
100
+ if (p.gates) parts.push(` · Gates: ${p.gates}`);
101
+ if (p.ledgerRows) parts.push(` · ledger: ${p.ledgerRows} rows`);
102
+ return parts.join("") + ")";
103
+ });
127
104
  }
128
105
 
129
106
  // --- the re-verification sweep ---------------------------------------------------------------
@@ -190,6 +167,45 @@ const ROW_VERIFIERS = {
190
167
  if (srcTime === null) return true; // the source is gone; nothing implicates the doc any more
191
168
  return docTime >= srcTime;
192
169
  },
170
+
171
+ // "<rel> has N doc-lint finding(s)" — dead once a re-run comes back clean (or the doc is gone).
172
+ "doc-lint": (projectDir, body) => {
173
+ const m = /·\s+doc-lint\s+·\s+(\S+)\s+has/.exec(body);
174
+ if (!m) return false;
175
+ if (!fs.existsSync(path.join(projectDir, m[1]))) return true;
176
+ return docLint(projectDir, m[1]).length === 0;
177
+ },
178
+
179
+ // "<rel> is not linked from docs/README.md" — dead once the index names it (or it is gone).
180
+ "index-row-missing": (projectDir, body) => {
181
+ const m = /·\s+index-row-missing\s+·\s+(\S+)\s+is not linked/.exec(body);
182
+ if (!m) return false;
183
+ if (!fs.existsSync(path.join(projectDir, m[1]))) return true;
184
+ return readSafe(path.join(projectDir, "docs", "README.md")).includes(path.basename(m[1]));
185
+ },
186
+
187
+ // "<rel> declares `<name>` (package-rejected)" — dead once the manifest drops the package
188
+ // or the table stops rejecting it.
189
+ "package-rejected": (projectDir, body) => {
190
+ const m = /·\s+package-rejected\s+·\s+(\S+)\s+declares\s+`([^`]+)`/.exec(body);
191
+ if (!m) return false;
192
+ const [, rel, name] = m;
193
+ let manifest;
194
+ try {
195
+ manifest = JSON.parse(readSafe(path.join(projectDir, rel)));
196
+ } catch (e) {
197
+ return !fs.existsSync(path.join(projectDir, rel)); // gone ⇒ resolved; unparseable ⇒ standing
198
+ }
199
+ const declared = { ...manifest.dependencies, ...manifest.devDependencies };
200
+ if (!(name in declared)) return true;
201
+ return !rejectedPackages(projectDir).has(name);
202
+ },
203
+
204
+ // Retired detector kinds — the harness no longer raises these, so a standing row caches a
205
+ // condition nothing can re-derive. Swept unconditionally — but only machine-raised rows: a
206
+ // note: or waived: row whose free text merely mentions the phrase is a human's, not ours.
207
+ "doc-placement": (projectDir, body, ctx, row) => row.kind === "auto",
208
+ "boundary-scan-suggested": (projectDir, body, ctx, row) => row.kind === "auto",
193
209
  };
194
210
 
195
211
  // Delete every row — `auto:` or `waived:` — whose condition no longer holds. Returns the count.
@@ -200,7 +216,7 @@ export function sweepResolvedRows(projectDir, repoFiles) {
200
216
  for (const [kind, verify] of Object.entries(ROW_VERIFIERS)) {
201
217
  if (!row.body.includes(kind)) continue;
202
218
  try {
203
- return verify(projectDir, row.body, ctx);
219
+ return verify(projectDir, row.body, ctx, row);
204
220
  } catch (e) {
205
221
  return false; // never delete a row because a verifier threw
206
222
  }
@@ -215,56 +231,40 @@ function driftBanner(projectDir) {
215
231
  // Re-verify before counting: the walls must act on conditions that are live NOW, not on rows
216
232
  // that happen to still be written down.
217
233
  const swept = sweepResolvedRows(projectDir, repoFiles);
218
- const dead = deadGlobs(projectDir, repoFiles);
234
+ const docs = scanFeatureDocs(projectDir);
235
+ const dead = deadGlobs(projectDir, repoFiles, docs);
219
236
  const queueRows = readQueue(projectDir);
220
237
  const counts = countRows(queueRows);
221
- const waivers = unexpiredWaivers(queueRows, "dead-glob", today());
222
- const unwaivedDead = dead.filter((d) => !waivers.some((w) => w.body.includes(d.glob)));
223
-
224
- // A waiver becomes a wall the day it expires, so the queue says so a week ahead — the deferral
225
- // ending should be a date somebody saw coming, not a commit that suddenly blocks.
226
- const soon = new Date(Date.now() + 7 * 864e5).toISOString().slice(0, 10);
227
- const expiringSoon = queueRows
228
- .filter((r) => r.kind === "waived")
229
- .map((r) => {
230
- const d = (/expires=(\d{4}-\d{2}-\d{2})/.exec(r.body) || [])[1];
231
- if (!d || d < today() || d > soon) return null;
232
- // Drop the row's own raised-date prefix and its reason tail — the expiry and the finding are
233
- // what a reader is scanning for here.
234
- const finding = r.body.replace(/^\d{4}-\d{2}-\d{2}\s+·\s+/, "").split(" · reason:")[0];
235
- return `expires ${d} · ${finding.slice(0, 120)}`;
236
- })
237
- .filter(Boolean)
238
- .sort();
238
+ const unwaivedDead = dead.filter((d) => !waiverCovers(queueRows, "dead-glob", d.glob, today()));
239
239
 
240
240
  // A queued dead-glob row covered by an unexpired waiver is resolved history — drop it,
241
241
  // so waiving a finding class actually empties the rows it supersedes.
242
242
  resolveRows(projectDir, (row) => {
243
243
  if (row.kind !== "auto" || !row.body.includes("dead-glob")) return false;
244
244
  const m = /`([^`]+)`/.exec(row.body);
245
- return !!m && waivers.some((w) => w.body.includes(`\`${m[1]}\``));
245
+ return !!m && waiverCovers(queueRows, "dead-glob", m[1], today());
246
246
  });
247
247
 
248
- // Docs older than the code they own (doc mtime < newest owned-file mtime).
249
- const staleDocs = [];
250
- for (const entry of scanFeatureDocs(projectDir)) {
251
- let docTime;
252
- try {
253
- docTime = fs.statSync(path.join(projectDir, entry.doc)).mtimeMs;
254
- } catch (e) {
255
- continue;
256
- }
257
- const res = entry.owns.map((g) => globToRegExp(g));
258
- let newest = 0;
259
- for (const file of repoFiles) {
260
- if (!res.some((re) => re.test(file))) continue;
248
+ // Docs older than the code they own (doc mtime < newest owned-file mtime) — one stat per
249
+ // repo file, cached across docs, instead of docs × files stats.
250
+ const mtimes = new Map();
251
+ const mtimeOf = (rel) => {
252
+ if (!mtimes.has(rel)) {
261
253
  try {
262
- newest = Math.max(newest, fs.statSync(path.join(projectDir, file)).mtimeMs);
254
+ mtimes.set(rel, fs.statSync(path.join(projectDir, rel)).mtimeMs);
263
255
  } catch (e) {
264
- /* deleted between walk and stat */
256
+ mtimes.set(rel, null);
265
257
  }
266
258
  }
267
- if (newest > docTime) staleDocs.push(entry.doc);
259
+ return mtimes.get(rel);
260
+ };
261
+ const staleDocs = [];
262
+ for (const entry of docs) {
263
+ const docTime = mtimeOf(entry.doc);
264
+ if (docTime === null) continue;
265
+ const res = entry.owns.map((g) => globToRegExp(g));
266
+ const stale = repoFiles.some((file) => res.some((re) => re.test(file)) && (mtimeOf(file) ?? 0) > docTime);
267
+ if (stale) staleDocs.push(entry.doc);
268
268
  }
269
269
 
270
270
  const report = [
@@ -277,9 +277,6 @@ function driftBanner(projectDir) {
277
277
  ...staleDocs.map((d) => `- ${d}`),
278
278
  "",
279
279
  `## Bookkeeping queue (${counts.unresolved} unresolved: ${counts.auto} auto · ${counts.note} note · ${counts.needsSession} needs-session · ${counts.expiredWaived} expired-waived; ${counts.waived} waived total, ${swept} swept as resolved this session)`,
280
- ...(expiringSoon.length
281
- ? ["", `## Waivers expiring within 7 days (${expiringSoon.length})`, ...expiringSoon.map((w) => `- ${w}`)]
282
- : []),
283
280
  ].join("\n");
284
281
  try {
285
282
  fs.writeFileSync(path.join(stateDir(projectDir), "drift-report.md"), report + "\n");
@@ -301,16 +298,10 @@ function driftBanner(projectDir) {
301
298
 
302
299
  function sessionStart(projectDir, payload = {}) {
303
300
  // Stamp the session's start so gitless repos still have a "modified since" horizon for the
304
- // outcome detector's fallback scan (dirtyPaths). The legacy shared stamp stays for its reader;
305
- // the per-sid twin exists because two concurrent sessions clobber the shared one. Best-effort.
301
+ // outcome detector's fallback scan (dirtyPaths). Best-effort.
306
302
  const sid = payload.session_id;
307
- const stamp = new Date().toISOString() + "\n";
308
303
  try {
309
- fs.writeFileSync(path.join(stateDir(projectDir), "session-started"), stamp);
310
- if (sid) {
311
- const short = String(sid).replace(/[^a-zA-Z0-9-]/g, "").slice(0, 8);
312
- fs.writeFileSync(path.join(stateDir(projectDir), `session-started-${short}`), stamp);
313
- }
304
+ fs.writeFileSync(path.join(stateDir(projectDir), "session-started"), new Date().toISOString() + "\n");
314
305
  } catch (e) {
315
306
  /* the stamp is a convenience for the fallback; git repos never need it */
316
307
  }
@@ -350,6 +341,14 @@ function sessionStart(projectDir, payload = {}) {
350
341
  const memory = memoryLine(projectDir);
351
342
  if (memory) lines.push(memory);
352
343
  lines.push(...openPlanLines(projectDir).slice(0, 4));
344
+ // A gate that crashed or skipped a write left a trace here — surface the count so the
345
+ // failure log finally has its reader.
346
+ try {
347
+ const failures = readSafe(path.join(stateDir(projectDir), "hook-failures.log")).split("\n").filter(Boolean);
348
+ if (failures.length) lines.push(`Hook failures logged: ${failures.length} (.claude/state/hook-failures.log) — enforcement may have gaps.`);
349
+ } catch (e) {
350
+ /* the count is a convenience */
351
+ }
353
352
  try {
354
353
  const drift = driftBanner(projectDir);
355
354
  if (drift) lines.push(drift);
@@ -373,15 +372,6 @@ function sessionStart(projectDir, payload = {}) {
373
372
  // PostToolUse — the silent detector
374
373
  // ---------------------------------------------------------------------------
375
374
 
376
- const SOURCE_EXT = /\.(m?[jt]sx?|py|rs|go|rb|java|kt|swift|c|h|cpp|hpp|cs|sh|sql|css|scss|vue|svelte)$/;
377
-
378
- function relIfInside(projectDir, filePath) {
379
- if (!filePath) return null;
380
- const rel = path.relative(projectDir, path.resolve(projectDir, filePath));
381
- if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
382
- return rel.split(path.sep).join("/");
383
- }
384
-
385
375
  // Spawn the installed doc-lint over one file; returns finding lines (empty when absent/clean).
386
376
  function docLint(projectDir, rel) {
387
377
  const script = path.join(projectDir, ".claude", "skills", "documentation", "scripts", "doc-lint.mjs");
@@ -441,20 +431,6 @@ function detectorForFile(projectDir, rel, meta, rows) {
441
431
  );
442
432
  }
443
433
  }
444
- const segment = rel.split("/")[1];
445
- if (rel.startsWith("docs/") && segment && !segment.endsWith(".md")) {
446
- const structure = readSafe(path.join(projectDir, "docs", "structure.md"));
447
- if (structure && !structure.includes(`${segment}/`) && !structure.includes(`docs/${segment}`)) {
448
- rows.push(
449
- formatAutoRow({
450
- ...meta,
451
- kind: "doc-placement",
452
- description: `${rel} sits in a folder absent from docs/structure.md's authority map`,
453
- action: "move the doc per the add-a-doc table, or record the new folder in structure.md",
454
- })
455
- );
456
- }
457
- }
458
434
  if (rel.startsWith("docs/features/") && !readSafe(path.join(projectDir, "docs", "README.md")).includes(path.basename(rel))) {
459
435
  rows.push(
460
436
  formatAutoRow({
@@ -489,24 +465,14 @@ function detectorForFile(projectDir, rel, meta, rows) {
489
465
  })
490
466
  );
491
467
  }
492
- if (/(schema|init\.sql|migration|auth|webhook|\benv\b|wrangler)/i.test(rel)) {
493
- rows.push(
494
- formatAutoRow({
495
- ...meta,
496
- kind: "boundary-scan-suggested",
497
- description: `${rel} is contract-heavy`,
498
- action: "run the boundary skill scoped to the diff",
499
- })
500
- );
501
- }
502
468
  }
503
469
  }
504
470
 
505
471
  function detectRenames(projectDir, command, meta, rows) {
506
472
  if (!/\bgit mv\b|\bmv\b/.test(command || "")) return;
507
- const waivers = unexpiredWaivers(readQueue(projectDir), "dead-glob", today());
473
+ const queueRows = readQueue(projectDir);
508
474
  for (const { glob, doc } of deadGlobs(projectDir)) {
509
- if (waivers.some((w) => w.body.includes(`\`${glob}\``))) continue;
475
+ if (waiverCovers(queueRows, "dead-glob", glob, today())) continue;
510
476
  rows.push(
511
477
  formatAutoRow({
512
478
  ...meta,
@@ -612,7 +578,9 @@ function detectOutcomes(projectDir, meta, rows) {
612
578
  }
613
579
  }
614
580
  if (rel.startsWith("docs/history/")) {
615
- const frozen = frozenStatus(projectDir, rel);
581
+ // Frozen AT HEAD, not in the worktree: the completion flip makes a plan dirty+Complete in
582
+ // one edit, and the worktree predicate walled the very commit that ceremony requires.
583
+ const frozen = frozenStatusAtHead(projectDir, rel);
616
584
  if (frozen.frozen && !raised(`${rel} is a frozen record`)) {
617
585
  rows.push(
618
586
  formatAutoRow({
@@ -638,11 +606,10 @@ function postToolUse(projectDir, payload) {
638
606
  const rows = [];
639
607
  if (payload.tool_name === "Bash") {
640
608
  // A completed `git commit` releases the claim compliance took at PreToolUse — the `end` line
641
- // is what lets a waiting session verify the actor finished. Root resolution mirrors the
642
- // gate's: an explicit `cd` in the command wins, so cross-repo commits release where they claimed.
643
- if (/(?:^|&&|;|\|)\s*(?:sudo\s+)?git\s+commit\b/.test(input.command || "")) {
644
- const cdMatch = /(?:^|&&|;)\s*cd\s+"?([^"\s;&]+)"?/.exec(input.command || "");
645
- const root = cdMatch && path.isAbsolute(cdMatch[1]) && fs.existsSync(cdMatch[1]) ? cdMatch[1] : projectDir;
609
+ // is what lets a waiting session verify the actor finished. Root resolution is the gate's own
610
+ // `commandRoot`, shared, so a claim and its release can never resolve to different repos.
611
+ if (/(?:^|\n|&&|;|\|)\s*(?:sudo\s+)?git\s+commit\b/.test(input.command || "")) {
612
+ const root = commandRoot(projectDir, payload, input.command);
646
613
  try {
647
614
  releaseActivity(root, "commit", payload.session_id);
648
615
  appendEvent(root, payload.session_id, "coord", "commit-claim", "released");
@@ -3,15 +3,11 @@
3
3
  // judged mechanically: lockfile-only, harness-file-only, pure renames, changelog housekeeping,
4
4
  // or a bot author. Exit 1 when a non-exempt HEAD stages no entry.
5
5
 
6
- import { spawnSync } from "node:child_process";
7
- import path from "node:path";
6
+ import { changelogExemptFile } from "./lib/exempt.mjs";
7
+ import { git as gitAt } from "./lib/repo.mjs";
8
8
 
9
9
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
10
-
11
- function git(args) {
12
- const res = spawnSync("git", args, { cwd: projectDir, encoding: "utf8" });
13
- return res.status === 0 ? (res.stdout || "").trim() : null;
14
- }
10
+ const git = (args) => gitAt(projectDir, args);
15
11
 
16
12
  const nameStatus = git(["diff-tree", "--root", "--no-commit-id", "--name-status", "-r", "HEAD"]) || "";
17
13
  const entries = nameStatus
@@ -22,28 +18,11 @@ const entries = nameStatus
22
18
  return { status, file: files[files.length - 1] };
23
19
  });
24
20
 
25
- const LOCKFILES = /^(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?|Cargo\.lock)$/;
26
- const HARNESS_PREFIXES = [".claude/", ".agents/", ".codex/"];
27
- const HARNESS_FILES = [
28
- "AGENTS.md",
29
- "CLAUDE.md",
30
- ".mcp.json",
31
- ".vscode/settings.json",
32
- ".prettierignore",
33
- ".prettierrc.json",
34
- ];
35
-
36
- const exemptFile = (f) =>
37
- LOCKFILES.test(path.basename(f)) ||
38
- HARNESS_PREFIXES.some((p) => f.startsWith(p)) ||
39
- HARNESS_FILES.includes(f) ||
40
- f.startsWith("docs/history/changelog/") ||
41
- f === "docs/history/CHANGELOG.md";
42
21
 
43
22
  const author = git(["log", "-1", "--format=%an <%ae>"]) || "";
44
23
  const isBot = /\[bot\]|actions@github\.com/i.test(author);
45
24
  const allRenames = entries.length > 0 && entries.every((e) => e.status.startsWith("R"));
46
- const allExempt = entries.length === 0 || entries.every((e) => exemptFile(e.file));
25
+ const allExempt = entries.length === 0 || entries.every((e) => changelogExemptFile(e.file));
47
26
  const hasEntry = entries.some((e) => e.file.startsWith("docs/history/changelog/") && e.file.endsWith(".md"));
48
27
 
49
28
  if (isBot || allRenames || allExempt || hasEntry) {
@@ -8,18 +8,11 @@ import fs from "node:fs";
8
8
  import path from "node:path";
9
9
  import { frontmatter, field } from "./lib/ownership.mjs";
10
10
  import { undischargedRows } from "./lib/ledger.mjs";
11
+ import { readSafe } from "./lib/repo.mjs";
11
12
 
12
13
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
13
14
  const plansDir = path.join(projectDir, "docs", "history", "plans");
14
15
 
15
- function readSafe(file) {
16
- try {
17
- return fs.readFileSync(file, "utf8");
18
- } catch (e) {
19
- return "";
20
- }
21
- }
22
-
23
16
  let failures = 0;
24
17
  const fail = (msg) => {
25
18
  failures += 1;
@@ -6,14 +6,14 @@
6
6
  // leaves "most specific wins" unable to pick an owner
7
7
  //
8
8
  // Either is silenced by an unexpired `waived:` row in docs/operations/bookkeeping.md naming its
9
- // kind plus the offending glob (dead-glob) or the shared path (ownership-overlap) verbatim.
9
+ // kind plus the offending glob (dead-glob) or the shared path (ownership-overlap) in backticks.
10
10
  // Exit 1 on anything unwaived.
11
11
  //
12
12
  // A glob at strictly HIGHER specificity carving a sub-path out of a broader `**` is the designed
13
13
  // layering, not a conflict — only an exact tie is reported.
14
14
 
15
15
  import { deadGlobs, listRepoFiles, overlaps } from "./lib/ownership.mjs";
16
- import { readQueue, unexpiredWaivers } from "./lib/state.mjs";
16
+ import { readQueue, waiverCovers } from "./lib/state.mjs";
17
17
 
18
18
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
19
19
  const today = new Date().toISOString().slice(0, 10);
@@ -22,12 +22,10 @@ const files = listRepoFiles(projectDir);
22
22
  const queue = readQueue(projectDir);
23
23
 
24
24
  const dead = deadGlobs(projectDir, files);
25
- const deadWaivers = unexpiredWaivers(queue, "dead-glob", today);
26
- const unwaivedDead = dead.filter((d) => !deadWaivers.some((w) => w.body.includes(d.glob)));
25
+ const unwaivedDead = dead.filter((d) => !waiverCovers(queue, "dead-glob", d.glob, today));
27
26
 
28
27
  const conflicts = overlaps(projectDir, files);
29
- const overlapWaivers = unexpiredWaivers(queue, "ownership-overlap", today);
30
- const unwaivedOverlaps = conflicts.filter((c) => !overlapWaivers.some((w) => w.body.includes(c.example)));
28
+ const unwaivedOverlaps = conflicts.filter((c) => !waiverCovers(queue, "ownership-overlap", c.example, today));
31
29
 
32
30
  if (unwaivedDead.length === 0 && unwaivedOverlaps.length === 0) {
33
31
  console.log(
@@ -39,7 +37,7 @@ if (unwaivedDead.length === 0 && unwaivedOverlaps.length === 0) {
39
37
  if (unwaivedDead.length > 0) {
40
38
  console.error(`check-ownership: ${unwaivedDead.length} unwaived dead owns: glob(s):`);
41
39
  for (const d of unwaivedDead) console.error(` ${d.glob} → ${d.doc}`);
42
- console.error("Fix the owning doc's owns: frontmatter, or add a `waived:` dead-glob row with reason + expires=.");
40
+ console.error("Fix the owning doc's owns: frontmatter, or add a `waived:` dead-glob row naming the glob in backticks, with reason + expires=.");
43
41
  }
44
42
 
45
43
  if (unwaivedOverlaps.length > 0) {
@@ -50,7 +48,7 @@ if (unwaivedOverlaps.length > 0) {
50
48
  console.error(` ${c.b.glob} → ${c.b.doc}`);
51
49
  }
52
50
  console.error(
53
- "Narrow one doc's glob so a single owner wins, or add a `waived:` ownership-overlap row naming the shared path with reason + expires=.",
51
+ "Narrow one doc's glob so a single owner wins, or add a `waived:` ownership-overlap row naming the shared path in backticks with reason + expires=.",
54
52
  );
55
53
  }
56
54