claudemd-cli 0.26.0 → 0.26.2

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/CHANGELOG.md CHANGED
@@ -8,6 +8,24 @@ All notable changes to the `claudemd` plugin. This changelog tracks plugin artif
8
8
  - **Canonical spec version source**: `spec/CLAUDE.md` top-line title (`# AI-CODING-SPEC vX.Y.Z — Core`) + `spec/CLAUDE-changelog.md` top `##` entry.
9
9
  - **Plugin semver vs spec semver** are independent: plugin patch (0.2.0 → 0.2.1) may ship when spec is unchanged (this release); plugin minor (0.1.9 → 0.2.0) ships when spec minor updates (v0.2.0 shipped spec v6.10.0).
10
10
 
11
+ ## [0.26.2] - 2026-07-07
12
+
13
+ **Patch — dogfood QA hardening: three false-positive / data-loss fixes surfaced by an end-to-end usage pass.** All `fix:` (restore intended behavior); spec unchanged (stays v6.14.1). No new user-facing features.
14
+
15
+ - **statusLine multi-supersede data loss** (`scripts/lib/statusline.js`): `/claudemd-statusline adopt --supersede=A` then `--supersede=B` on a composite host overwrote the single `{superseded:<prov>}` restore record, so `remove()` restored only B — A was permanently lost from the host registry. The record is now a `{superseded:[…]}` list: `adopt()` appends (dedup by id), `remove()` restores ALL entries in reverse order (front-insert) so they regain their original relative order. Legacy singular records written by ≤0.26.1 are still restored (`readSupersededList()` normalizes both shapes), so an upgrade taken mid-supersede loses nothing.
16
+ - **banned-vocab `-am` false positive** (`hooks/banned-vocab-check.sh`): the commit-message extractor matched `-m`/`--message` but not combined short-flag blocks, so `git commit -am "…"` (one of the most common forms) fell through to the whole-command fallback — a banned word in a *chained* segment (`git commit -am "clean fix" && npm run comprehensive-x`) falsely denied a clean-message commit that the identical `-m` form passed. The extract + strip regexes now use `-[[:alpha:]]*m` (matches `-m`/`-am`/`-vam`; a strict superset — bare `-m` still matches with zero leading alphas).
17
+ - **§10-V identifier/path FP in the standalone CLI + transcript scan** (`scripts/lib/lint.js`, `bin/claudemd-lint.js`, `hooks/transcript-vocab-scan.sh`): the v0.23.19 Path 2 sanitizer was never ported to two siblings. `claudemd-cli lint` (used in git pre-commit hooks / CI) flagged commit messages naming a file/branch/backtick-identifier that embeds a high-fire word (`refactor comprehensive-parser.js` → exit 1 → **blocked commit**); `transcript-vocab-scan` inflated §10-V advisory telemetry the same way. Added `stripIdentifiers()` (new `lib/lint.js` export) + a `scan({sanitize})` opt-in wired into the CLI `lint`/`audit` paths, and the mirror awk+sed in `transcript-vocab-scan.sh`. Rule set: fenced blocks → inline backtick spans → slashed-path runs → bare `name.ext` files (lowercase-extension only, so decimals/versions like `3.5x`/`v6.14` survive and a baseline-less ratio claim is not swallowed — no false negative). Bare hyphenated identifiers with no extension/slash (`robust-retry`) remain a residual FP, the same `\b`-boundary limitation Path 2 has.
18
+ - **Tests (+8 → 611 Node tests; +5 shell hook tests)**: `statusline-adopt.test.js` multi-supersede + legacy-singular restore; `lint.test.js` `stripIdentifiers` + `scan({sanitize})` FP-fix + FN-guards; `banned-vocab.test.sh` 41 → 44 (`-am`/`-vam` isolation); `transcript-vocab-scan.test.sh` 12 → 14 (identifier-only clean / bare-prose-beside-id still fires).
19
+
20
+ ## [0.26.1] - 2026-07-06
21
+
22
+ **Patch — post-release review hardening of v0.26.0 statusLine coexistence.** Three fixes from an independent post-ship review (`superpowers:requesting-code-review`); no user-visible behavior change on the normal path. Spec unchanged (stays v6.14.1).
23
+
24
+ - **Durability fix** (`scripts/lib/statusline-hosts.js`): the guest-registry write now writes the durable `~/.claude` mirror **first** and no longer swallows its errors. Previously the volatile `~/.cache` primary was written first and unguarded while the durable mirror — the backstop code-graph self-heals the primary from — was best-effort; a silently-dropped mirror write could diverge from the primary and, after a `~/.cache` eviction, drop claudemd's segment. A failed durable write now surfaces instead of hiding.
25
+ - **Observability** (`scripts/lib/statusline.js`, `scripts/statusline-adopt.js`): `adopt --supersede=<id>` for an id not in the host registry no longer silently no-ops — `adopt()` returns `supersedeMissed:<id>` and the CLI warns, so a stale / TOCTOU id is visible instead of passing as a successful supersede.
26
+ - **Single source of truth for the supersede heuristic** (`scripts/lib/statusline.js`, `commands/claudemd-statusline.md`): `detect()` now surfaces the tested `manualPsCandidates()` predicate as a `psCandidates` array; the command reads that field instead of re-deriving the hand-made-PS1 heuristic in prose, removing drift between the tested predicate and the one that runs.
27
+ - **Tests (+5 → 603 Node tests)**: durable-mirror-first-no-swallow (`statusline-hosts.test.js`), `psCandidates` on host + null off-host and `supersedeMissed` (`statusline-adopt.test.js`), CLI supersede-missed warning (`statusline-cli.test.js`).
28
+
11
29
  ## [0.26.0] - 2026-07-06
12
30
 
13
31
  **Minor — statusLine multi-provider coexistence.** When a composite host (code-graph) owns the `statusLine` slot, `/claudemd-statusline` now registers claudemd as a *guest* provider in the host's registry so both segments render (`claudemd | code-graph`), instead of clobbering the slot. Empty-slot behavior is unchanged. Spec content unchanged (v6.14.1).
@@ -252,7 +252,7 @@ function lintCmd(rawArgs) {
252
252
  const HAS_BASELINE = /baseline/i;
253
253
  const baselineExempt = HAS_NUMERIC_ARROW.test(text) || HAS_BASELINE.test(text);
254
254
 
255
- const hits = scan(text, { excludeRatio: baselineExempt });
255
+ const hits = scan(text, { excludeRatio: baselineExempt, sanitize: true });
256
256
  if (json) {
257
257
  process.stdout.write(formatJSON({ scope: 'lint', text, hits }) + '\n');
258
258
  } else {
@@ -332,7 +332,7 @@ function auditCmd(rawArgs) {
332
332
  const patterns = readPatterns();
333
333
  const annotated = turns.map(t => ({
334
334
  ...t,
335
- hits: scan(t.text, { excludeRatio: !includeRatio, patterns }),
335
+ hits: scan(t.text, { excludeRatio: !includeRatio, patterns, sanitize: true }),
336
336
  }));
337
337
  const flaggedCount = annotated.reduce((n, t) => n + (t.hits.length > 0 ? 1 : 0), 0);
338
338
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudemd-cli",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
4
  "description": "Standalone CLI for §10-V banned-vocab + transcript scanning. Companion to the claudemd Claude Code plugin (github.com/sdsrss/claudemd) for use in git pre-commit hooks, GitHub Actions, and other agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -80,8 +80,47 @@ function posixClassesToJs(regex) {
80
80
  return out;
81
81
  }
82
82
 
83
- export function scan(text, { excludeRatio = false, patterns } = {}) {
83
+ // stripIdentifiers remove code / identifier / path regions before §10-V
84
+ // matching so a filename, branch, or backtick span quoting a high-fire word is
85
+ // not read as a value claim. `\b` treats '-', '/', '.' as word boundaries, so
86
+ // `\bcomprehensive\b` fires INSIDE `comprehensive-parser.js` or a branch name
87
+ // `docs/comprehensive-audit`. Mirrors hooks/banned-vocab-check.sh's v0.23.19
88
+ // Path 2 sanitizer (fenced blocks → inline backtick spans → slashed-path runs)
89
+ // and adds a bare dotted-file token strip, because the CLI's primary input —
90
+ // commit messages — commonly names bare files (`refactor comprehensive-parser.js`)
91
+ // without backticks or a leading path. Token classes are ASCII-only so 中文
92
+ // prose and bare-word claims (the real violations) stay intact and still match.
93
+ export function stripIdentifiers(text) {
94
+ if (!text) return text;
95
+ // 1. Fenced code blocks: line-based fence toggle, mirroring the bash awk
96
+ // `/^[[:space:]]*```/{f=!f; next} !f` — drop the ``` marker lines AND the
97
+ // body between them. An unterminated fence drops to EOF (in_fence stays on).
98
+ const kept = [];
99
+ let inFence = false;
100
+ for (const line of text.split('\n')) {
101
+ if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
102
+ if (!inFence) kept.push(line);
103
+ }
104
+ return kept.join('\n')
105
+ // 2. Inline backtick spans.
106
+ .replace(/`[^`]*`/g, ' ')
107
+ // 3. Slashed-path runs (branch names, file paths, URLs) — Path 2's rule.
108
+ .replace(/[A-Za-z0-9._@~-]*\/[A-Za-z0-9._/@~-]*/g, ' ')
109
+ // 4. Bare dotted-file tokens (foo.js, comprehensive-parser.ts) — CLI
110
+ // extension. The extension must start with a LOWERCASE letter, which
111
+ // (a) excludes decimals / versions ("3.5x", "v6.14") whose ".5x"/".14"
112
+ // could otherwise swallow a baseline-less ratio claim → false negative,
113
+ // and (b) excludes sentence-boundary typos ("comprehensive.Next", capital
114
+ // after the dot) so a real claim isn't stripped. Only true `name.ext`
115
+ // identifiers with a lowercase extension are removed.
116
+ .replace(/[A-Za-z0-9_-]+\.[a-z][a-z0-9]*/g, ' ');
117
+ }
118
+
119
+ export function scan(text, { excludeRatio = false, patterns, sanitize = false } = {}) {
84
120
  if (!text) return [];
121
+ // Sanitize identifier/path regions when asked (CLI lint/audit opt in). Match
122
+ // against the stripped text; the caller keeps the original for display.
123
+ const scanText = sanitize ? stripIdentifiers(text) : text;
85
124
  const pats = patterns || readPatterns();
86
125
  const hits = [];
87
126
  for (const p of pats) {
@@ -92,7 +131,7 @@ export function scan(text, { excludeRatio = false, patterns } = {}) {
92
131
  } catch {
93
132
  continue; // bad regex — skip (fail-open)
94
133
  }
95
- const m = text.match(re);
134
+ const m = scanText.match(re);
96
135
  if (m) hits.push({ match: m[0], regex: p.regex, reason: p.reason, isRatio: p.isRatio });
97
136
  }
98
137
  return hits;