claudemd-cli 0.9.19 → 0.9.21
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 +40 -0
- package/bin/claudemd-lint.js +31 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,46 @@ 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.9.21] - 2026-05-10
|
|
12
|
+
|
|
13
|
+
**Hotfix — `claudemd-cli lint <path>` silent-text-scan when path looks like a path but doesn't resolve to a regular file (v0.9.14 family residual).** Spec v6.11.7 unchanged. The v0.9.14 fix added auto-`--file` for the case where the positional was an existing regular file. The other branches — missing path, directory, dead symlink — still fell through to scanning the literal positional string. Three failure shapes surfaced during the dogfood session that produced v0.9.18–v0.9.20:
|
|
14
|
+
- `claudemd lint /tmp/missing-msg.txt` → exit 0, "OK no hits" (silent success masking a CI misconfiguration).
|
|
15
|
+
- `claudemd lint /tmp` (existing dir) → exit 0, "OK no hits" (scans the literal string `/tmp`).
|
|
16
|
+
- `claudemd lint /tmp/significantly-improved.txt` (missing) → **exit 1 with banned-vocab hit on the basename** — falsely accuses the user of writing "significantly" in a commit message they didn't even compose.
|
|
17
|
+
|
|
18
|
+
Same silent-fall-through family the v0.9.14 fix targeted; the original fix was scoped only to the "file exists" branch. The third shape is the worst — a deny that points at text the user never wrote.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `[fix]` **`bin/claudemd-lint.js#lintCmd` rejects path-shape positionals that don't resolve to a regular file.** Pre-fix: `try { stat(); if isFile read } catch { /* fall through */ }` then `text = positional.join(' ')`. Post-fix: when the positional contains `/` (or is `.` / `..`), a stat miss exits 2 with `file not found`, and a stat-as-non-file exits 2 with `is not a regular file`. Non-path-shape positionals (single word, no slash) keep the v0.9.14 text fallback — `lint significantly` MUST stay a text scan, and `lint message.txt` (no slash, missing) stays as text to preserve the pre-commit-hook ergonomic where `--file` is explicit.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `[test]` **4 new cases in `tests/scripts/lint-cli.test.js`** (267 → 271 total): `lint /path/missing.txt` exits 2 with file-not-found, `lint <directory>` exits 2 with not-a-regular-file, `lint /path/with-banned-word-in-name (missing)` exits 2 (the false-positive-deny shape — anchors that the path string is NEVER scanned), `lint message.txt` (single word, no slash) stays as text scan exit 0 (anchor that the fix doesn't regress non-path-shape literals).
|
|
27
|
+
|
|
28
|
+
### Why no L3 / pre-ship-review chain
|
|
29
|
+
|
|
30
|
+
`fix:` per §2 hard-upgrade exclusion — restores the v0.9.14 fix's documented intent (path-shape input that doesn't resolve to a file should NOT silently scan the path string). L2 ceiling. Diff: 1 file changed in `bin/`, 1 file in `tests/scripts/`, ~25 LOC added. Pattern continuity: same antipattern family as v0.9.14/v0.9.15/v0.9.16/v0.9.17/v0.9.18 — "input parser silently falls through on unexpected shape, exits 0 looking like success." The lint-argv gate (v0.9.19) covers the JS .find/.includes/.indexOf shapes; this is a different shape (path-resolution branch missing) that lives in the lintCmd auto-detect logic, not in flag parsing — so the gate doesn't catch it. No new gate added: this branch is now covered by the four new regression tests, which is sufficient for a one-shot logic branch (the gate exists for the recurring `args.find` family, not every branch in every CLI).
|
|
31
|
+
|
|
32
|
+
## [0.9.20] - 2026-05-10
|
|
33
|
+
|
|
34
|
+
**Patch — `hard-rules-audit` demoteCandidates produced false-positive recommendations when log span < requested window.** Spec v6.11.7 unchanged. Surfaced in the same dogfood session that produced v0.9.18/v0.9.19: running `/claudemd-rules` (default 90-day window) against a `~/.claude/logs/claudemd.jsonl` that only spans 17 days reported `§11-memory-read` as a demote candidate — but that rule had been silently no-op'd in projects with `_` in the cwd path until v0.9.15 fixed it. The data couldn't see the rule firing because the rule itself was broken; "0 hits in window" wasn't a coldness signal. §0.1 HARD specifies "0 hits in 90d" — running the calculation on 17 days of data violates the spec. Same root applies to any rule fixed/added more recently than the log start.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- `[fix]` **`scripts/hard-rules-audit.js` suppresses `demoteCandidates` when `insufficientData`.** Pre-fix: `demoteCandidates` was computed as `hookEnforced.filter(r => r.hits.total === 0)` regardless of whether the log reached back the requested window. Post-fix: when `logSpanDays < days`, `demoteCandidates` is forced to `[]` and the would-have-been list is preserved under a new `demoteSuppressed` field (`{ reason, wouldHaveBeen }`) so the operator sees what's *potentially* cold without auto-acting on a false signal.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `[feat]` **`scripts/lib/rule-hits-parse.js#logFirstTs(path)`** — returns earliest ts in the rule-hits log (ms-since-epoch) or `null` for missing/empty/all-malformed files. Skips rows with non-finite timestamps. Used by `hardRulesAudit` to compute `logSpanDays`; future consumers (sparkline, audit) can adopt for the same insufficient-data check.
|
|
43
|
+
- `[feat]` **`hardRulesAudit` output: 4 new fields** — `logSpanDays` (operator transparency, surfaced even when sufficient), `insufficientData` (boolean), `demoteSuppressed` (`{reason, wouldHaveBeen}` when insufficient, `null` otherwise), and `demoteCandidates: []` when insufficient (compatibility: existing consumers reading the array get an empty array instead of false candidates).
|
|
44
|
+
- `[test]` **5 new cases in `tests/scripts/hard-rules-audit.test.js`** + **8 new cases in new `tests/scripts/rule-hits-parse.test.js` (257 → 270)**: insufficient-data suppresses + surfaces `demoteSuppressed`, sufficient-span preserves existing demote behavior, log-span boundary case (35d > 30d window), `logSpanDays` always reflects actual log reach not the window, `logFirstTs` returns `null` on missing/empty/all-malformed and earliest ts otherwise, skips non-finite ts rows.
|
|
45
|
+
- `[test]` **`demoteCandidates list hook-rules with zero hits` test updated** to write a 31-day-old sentinel row before asserting `§8-rm-rf-var` appears as candidate. Pre-fix, the test passed against an empty log because there was no insufficient-data check. Post-fix, an empty log triggers `insufficientData=true` and `demoteCandidates=[]`, so the test would have asserted on suppressed candidates and failed. Documented in the new "sufficient log span" test name.
|
|
46
|
+
|
|
47
|
+
### Why no L3 / pre-ship-review chain
|
|
48
|
+
|
|
49
|
+
`fix:` per §2 hard-upgrade exclusion — restores §0.1 HARD's documented intent (90d window means 90d of data, not "whatever the log has"). L2 ceiling. Diff: 1 lib helper added (~15 LOC), 1 script branch added (~15 LOC), 2 test files (+13 cases). Notable: this is the same family as v0.9.13's `session-summary` window-calculation bug — "report computes correct math against the data it has, but the data itself doesn't span the window the report claims to cover." Sparkline's `(newly active)` annotation is the same UX issue — not in this patch (would require updating the existing `newly active` test fixture to span 90 days, larger churn) — flagged for a follow-up patch.
|
|
50
|
+
|
|
11
51
|
## [0.9.19] - 2026-05-10
|
|
12
52
|
|
|
13
53
|
**Patch — repo-wide CI guard for the argv-shape silent-fallback antipattern (§13.2 Tier 1).** Spec v6.11.7 unchanged. v0.9.14 → v0.9.18 was five consecutive patches chasing the same antipattern in five different files because each fix-set was scoped to whatever the repro session tripped on. The CHANGELOG entry for v0.9.18 stated "the next exploratory-testing session WILL find a 6th hole"; this patch makes that prediction grep-enforceable instead of trusting the next session not to slip.
|
package/bin/claudemd-lint.js
CHANGED
|
@@ -164,13 +164,41 @@ function lintCmd(rawArgs) {
|
|
|
164
164
|
// from a git pre-commit hook). Without this, `claudemd lint message.txt`
|
|
165
165
|
// silently scans the LITERAL STRING "message.txt" → exits 0 even when
|
|
166
166
|
// the file contents would deny.
|
|
167
|
+
//
|
|
168
|
+
// v0.9.21 — close the v0.9.14 silent-fall-through residual: when the
|
|
169
|
+
// positional looks like a PATH (contains '/' or is '.' / '..') AND the
|
|
170
|
+
// path doesn't resolve to a regular file, error out instead of scanning
|
|
171
|
+
// the literal string. Pre-fix, `lint /tmp/missing.txt` scanned the path
|
|
172
|
+
// string (exit 0); `lint /tmp` (existing dir) scanned '/tmp' (exit 0);
|
|
173
|
+
// `lint /tmp/significantly-improved.txt` (missing) matched "significantly"
|
|
174
|
+
// in the basename and falsely exited 1. Same silent-success family the
|
|
175
|
+
// v0.9.14 fix targeted; the fix was scoped only to the "file exists" branch.
|
|
176
|
+
//
|
|
177
|
+
// Non-path-shape positionals (single word, no slash) keep the v0.9.14
|
|
178
|
+
// text fallback — `lint significantly` MUST stay a text scan because it's
|
|
179
|
+
// a single literal word, not a typo'd path. `lint message.txt` (no slash,
|
|
180
|
+
// missing file) stays text — it's ambiguous between "literal text" and
|
|
181
|
+
// "filename in cwd"; pre-existing-file behavior wins to preserve the
|
|
182
|
+
// pre-commit-hook ergonomic where `--file` is explicit.
|
|
167
183
|
if (positional.length === 1) {
|
|
184
|
+
const arg = positional[0];
|
|
185
|
+
const looksLikePath = arg.includes('/') || arg === '.' || arg === '..';
|
|
168
186
|
try {
|
|
169
|
-
const st = fs.statSync(
|
|
187
|
+
const st = fs.statSync(arg);
|
|
170
188
|
if (st.isFile()) {
|
|
171
|
-
text = fs.readFileSync(
|
|
189
|
+
text = fs.readFileSync(arg, 'utf8');
|
|
190
|
+
} else if (looksLikePath) {
|
|
191
|
+
process.stderr.write(`lint: '${arg}' is not a regular file (use --file PATH for explicit file scan or quote literal text)\n`);
|
|
192
|
+
process.exit(2);
|
|
172
193
|
}
|
|
173
|
-
|
|
194
|
+
// Non-path-shape + non-file (e.g. a symlink loop, fifo) → fall through to text scan.
|
|
195
|
+
} catch (e) {
|
|
196
|
+
if (looksLikePath) {
|
|
197
|
+
process.stderr.write(`lint: file not found: ${arg}\n`);
|
|
198
|
+
process.exit(2);
|
|
199
|
+
}
|
|
200
|
+
// Non-path-shape miss → fall through to text scan.
|
|
201
|
+
}
|
|
174
202
|
}
|
|
175
203
|
if (text === undefined) text = positional.join(' ');
|
|
176
204
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudemd-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.21",
|
|
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": {
|