claudemd-cli 0.110.0 → 0.111.0
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 +16 -14
- package/bin/claudemd-lint.js +28 -8
- package/package.json +2 -2
- package/scripts/lib/argv.js +1 -1
- package/scripts/lib/lint.js +12 -3
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
claudemd plugs into the Claude Code hook system to **block commits, pushes, and bash commands** that violate AI-CODING-SPEC v8.1 — banned vocabulary in commit messages, `rm -rf $VAR` without variable validation, ship-on-red-CI, unread `MEMORY.md` entries during release flows, and more. The spec itself (`CLAUDE.md` + `CLAUDE-extended.md` + `CLAUDE-changelog.md` + `OPERATOR.md`) ships with the plugin and installs into `~/.claude/`, so the rules Claude Code reads at session start match the rules the hooks enforce. (`OPERATOR.md` is the human-only spec-maintenance handbook — Agent-loaded files are the CLAUDE trio.)
|
|
10
10
|
|
|
11
|
-
A standalone CLI (`npx claudemd-cli`) reuses the same `banned-vocab.patterns` source for git
|
|
11
|
+
A standalone CLI (`npx claudemd-cli`) reuses the same `banned-vocab.patterns` source for git commit-msg hooks, GitHub Actions, and other agents that don't run inside Claude Code.
|
|
12
12
|
|
|
13
13
|
> **Status & scope.** This is a single-maintainer tool that encodes one developer's working discipline — not a general-purpose product. It's exercised almost entirely on my own repositories, the spec is opinionated, and the defaults reflect my preferences rather than a consensus. If you install it, expect to fork `CLAUDE.md` and tune the rules to your own workflow. Treat it as a reference implementation of "spec-as-enforced-hooks," not a turnkey solution. Issues and PRs are welcome.
|
|
14
14
|
|
|
@@ -35,7 +35,7 @@ Then bootstrap the **current** session (skip the wait-for-next-session restart)
|
|
|
35
35
|
|
|
36
36
|
Fallback (no slash command — e.g. scripting outside CC): `node ~/.claude/plugins/cache/claudemd/claudemd/<version>/scripts/install.js`. Find `<version>` with `ls ~/.claude/plugins/cache/claudemd/claudemd/ | sort -V | tail -1`.
|
|
37
37
|
|
|
38
|
-
> ⚠️ **`~/.claude/CLAUDE.md` is shared real estate.** Claude Code reads this file as your user-global instructions across every project. If you've hand-written personal instructions there (`Always reply in 中文`, `My name is X`, etc.), install moves your existing files to `~/.claude/backup-<ISO>/` (last 5 kept automatically) before writing the spec. Since v0.5.3, install prints a `[claudemd] WARN: …` line to stderr when the existing file does not look like a claudemd spec — visible when you run `/claudemd-install` (or `install.js`) yourself. Since v0.75.0 the **SessionStart** bootstrap, where that stderr goes to `claudemd-bootstrap.log` instead, raises the same notice as an in-session banner naming the backup path (silence it with `DISABLE_USER_CONTENT_BANNER=1`). To bring your personal instructions back
|
|
38
|
+
> ⚠️ **`~/.claude/CLAUDE.md` is shared real estate.** Claude Code reads this file as your user-global instructions across every project. If you've hand-written personal instructions there (`Always reply in 中文`, `My name is X`, etc.), install moves your existing files to `~/.claude/backup-<ISO>/` (last 5 kept automatically) before writing the spec. Since v0.5.3, install prints a `[claudemd] WARN: …` line to stderr when the existing file does not look like a claudemd spec — visible when you run `/claudemd-install` (or `install.js`) yourself. Since v0.75.0 the **SessionStart** bootstrap, where that stderr goes to `claudemd-bootstrap.log` instead, raises the same notice as an in-session banner naming the backup path (silence it with `DISABLE_USER_CONTENT_BANNER=1`). To bring your personal instructions back, run `/claudemd-uninstall restore` and then `/plugin uninstall claudemd@claudemd` — while the plugin stays installed, the next session start installs the spec over the restored file again — or merge the two files by hand.
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
@@ -127,7 +127,7 @@ Per-hook timeout (3-5s in `hooks.json`); timeout = treated as exit 0 (pass) per
|
|
|
127
127
|
| `/claudemd-audit [N]` | Aggregate rule-hits over last N days (default 30). Top banned-vocab patterns, per-hook deny counts. Slash form takes a bare number (`/claudemd-audit 90`); direct script invocation takes `--days=N` (= form only). |
|
|
128
128
|
| `/claudemd-toggle <hook-name>` | Enable/disable a specific hook by toggling `DISABLE_*_HOOK` in `settings.json` env. |
|
|
129
129
|
| `/claudemd-doctor [--prune-backups=N]` | Health checks; optionally prune each backup namespace (`~/.claude/backup-*`, `spec-backup-*`, `handhook-backup-*`) to its N newest. Dirs the `backup-namespace-legacy` check reports (pre-0.68.3 spec backups sitting in the personal namespace) are skipped — neither deleted nor counted toward N — and echoed under `pruneSkippedLegacy`. v0.7.1+ also flags rule sections whose bypass:deny ratio > 50% (R-N6 §0.1 demotion candidates). |
|
|
130
|
-
| `/claudemd-rules [N]` | v0.8.0+ — audit `spec/hard-rules.json` manifest over last N days (default 30 — lowered from 90d in v0.13.1 after the 90d gate was structurally unreachable under typical log retention). Surfaces `demoteCandidates` (hook-enforced rules with 0 hits) and `staleReviews` (rules whose `last_demote_review` is null/old). |
|
|
130
|
+
| `/claudemd-rules [N]` | v0.8.0+ — audit `spec/hard-rules.json` manifest over last N days (default 30 — lowered from 90d in v0.13.1 after the 90d gate was structurally unreachable under typical log retention). Surfaces `demoteCandidates` (hook-enforced rules with 0 fired rows, `hits.fired`) and `staleReviews` (rules whose `last_demote_review` is null/old). |
|
|
131
131
|
| `/claudemd-sparkline [--days=A,B,C]` | v0.8.4+ R-N9 — per-`spec_section` cumulative counts of signal events across 3 windows (default 30/60/90d). Trend arrow compares per-period rate; `(newly active)` / `(silenced)` annotations flag activation/deactivation transitions. Markdown block suitable for CHANGELOG header pre-release. |
|
|
132
132
|
| `/claudemd-clean-residue [--apply]` | Dry-run-by-default cleanup across four scopes: `$TMPDIR` (`claudemd-sync-*` / `claudemd-memtags-hay-*` sentinels, historical `claudemd-(mockgh\|work).*` sandboxes), `~/.claude/tmp` tool-exhaust past the §EXT §7-EXT retention window, the seven ephemeral classes in `~/.claude/.claudemd-state`, and headless-probe dirs (`-tmp-*`, `-var-tmp`, every transcript `entrypoint: sdk-cli`) in `~/.claude/projects` past the same window, never an interactive session's or one holding a memory file. Exits 3 if `--apply` leaves targets behind. |
|
|
133
133
|
| `/claudemd-design-adopt [check\|remove]` | v0.24.0 — for a UI project, generate a thin, fact-based `DESIGN.md` from its real design-token sources (deterministic detector `scripts/design-detect.js`; evidence-gated rules menu; never invents values) and wire a sentinel block into project CLAUDE.md. `check` verifies pointers resolve; `remove` unwires. Always shows the diff and asks before writing. Manual/opt-in — no SessionStart hook, nothing auto-fires. |
|
|
@@ -138,7 +138,7 @@ Per-hook timeout (3-5s in `hooks.json`); timeout = treated as exit 0 (pass) per
|
|
|
138
138
|
|
|
139
139
|
## Standalone CLI
|
|
140
140
|
|
|
141
|
-
The same `banned-vocab.patterns` source the in-CC hook uses is also exposed as a Node CLI for **git
|
|
141
|
+
The same `banned-vocab.patterns` source the in-CC hook uses is also exposed as a Node CLI for **git commit-msg hooks, GitHub Actions, and other agents** (Codex, Cursor, OpenClaw) — anywhere outside the Claude Code process.
|
|
142
142
|
|
|
143
143
|
```bash
|
|
144
144
|
# After npm publish (operator-driven, not part of plugin install):
|
|
@@ -154,14 +154,14 @@ node bin/claudemd-lint.js audit ~/.claude/projects/<encoded>/<session>.jsonl
|
|
|
154
154
|
|
|
155
155
|
| Subcommand | Purpose |
|
|
156
156
|
|---|---|
|
|
157
|
-
| `lint <text>` / `--file` / `--stdin` | Scan commit-message text for §10-V banned vocab. Exit 0 clean / 1 hits. |
|
|
158
|
-
| `audit <jsonl-path>` | Scan all assistant-text turns in
|
|
157
|
+
| `lint <text>` / `--file` / `--stdin` | Scan commit-message text for §10-V banned vocab. Exit 0 clean / 1 hits / 2 bad arguments, including an empty or blank text argument (`--stdin` or `--file` with empty input still exits 0). |
|
|
158
|
+
| `audit <jsonl-path>` | Scan all assistant-text turns in one Claude Code transcript jsonl (more than one path exits 2; run it once per file). Skips `@ratio` patterns by default (chat prose has different baseline conventions); pass `--include-ratio` to include them. |
|
|
159
159
|
| `--json` | JSON output (machine-readable for CI). |
|
|
160
160
|
| `--commit-msg` / `--no-commit-msg` | (`lint`) Force git commit-message cleanup on/off. Auto-ON for `COMMIT_EDITMSG` / `MERGE_MSG` / `SQUASH_MSG` / `TAG_EDITMSG` / `NOTES_EDITMSG`. |
|
|
161
161
|
| `--comment-char <c>` | (`lint`) git `core.commentChar`. Default `#`. |
|
|
162
162
|
| `--version` / `--help` | Standard. |
|
|
163
163
|
|
|
164
|
-
**
|
|
164
|
+
**Commit-message hook example (`.git/hooks/commit-msg`)** — a `commit-msg` hook, not `pre-commit`: git hands `commit-msg` the message file as `$1` and gives `pre-commit` no arguments (an empty `lint ""` exits 2 rather than passing):
|
|
165
165
|
|
|
166
166
|
```bash
|
|
167
167
|
#!/usr/bin/env bash
|
|
@@ -385,15 +385,15 @@ Reversing the order is the orphan-state vector — `${CLAUDE_PLUGIN_ROOT}` and `
|
|
|
385
385
|
|
|
386
386
|
### Spec disposition
|
|
387
387
|
|
|
388
|
-
`/claudemd-uninstall` defaults to `keep` (leaves `~/.claude/CLAUDE*.md` in place).
|
|
388
|
+
`/claudemd-uninstall` defaults to `keep` (leaves `~/.claude/CLAUDE*.md` in place). A slash command takes no env vars, so pick `restore` by typing `/claudemd-uninstall restore`; any other argument (`/claudemd-uninstall delete`, say) gets a list of the options and a question, which you answer in the next message; the command runs `scripts/uninstall.js` with the env vars below (the same ones work when you run the script directly):
|
|
389
389
|
|
|
390
|
-
| Option | Env vars | Behavior |
|
|
391
|
-
|
|
392
|
-
| `keep` (default) | (none) | `~/.claude/CLAUDE*.md` left in place; settings.json hook entries cleared. |
|
|
393
|
-
| `restore` | `CLAUDEMD_SPEC_ACTION=restore` | Copies the most recent `~/.claude/backup-<ISO>/*.md` back to `~/.claude
|
|
394
|
-
| `delete` | `CLAUDEMD_SPEC_ACTION=delete CLAUDEMD_CONFIRM=1` | Hard-AUTH: removes all four spec files (`CLAUDE*.md` + `OPERATOR.md`) and every regular `*.md` file in `~/.claude/spec-modules/`. |
|
|
390
|
+
| Option | Type / ask for | Env vars the script reads | Behavior |
|
|
391
|
+
|---|---|---|---|
|
|
392
|
+
| `keep` (default) | `/claudemd-uninstall` | (none) | `~/.claude/CLAUDE*.md` left in place; settings.json hook entries cleared. |
|
|
393
|
+
| `restore` | `/claudemd-uninstall restore` | `CLAUDEMD_SPEC_ACTION=restore` | Copies the most recent `~/.claude/backup-<ISO>/*.md` back to `~/.claude/`, except over a home file that is not the spec and differs from the backup (listed in `kept`). Use this if your install-time stderr showed `[claudemd] WARN: existing ~/.claude/CLAUDE.md does not look like a claudemd spec` — your hand-written user-global instructions are sitting in the backup waiting to be brought back. Then run `/plugin uninstall claudemd@claudemd`: while the plugin stays installed, the next session start installs the spec over the restored file again. |
|
|
394
|
+
| `delete` | ask for a full removal | `CLAUDEMD_SPEC_ACTION=delete CLAUDEMD_CONFIRM=1` | Hard-AUTH: removes all four spec files (`CLAUDE*.md` + `OPERATOR.md`) and every regular `*.md` file in `~/.claude/spec-modules/`. After a default uninstall (no manifest left), only spec files whose first line is the `# AI-CODING-SPEC` H1 and only the module files claudemd generated (their `# AI-CODING-SPEC … — module: <name>` heading) go; the rest are listed in `kept`. |
|
|
395
395
|
|
|
396
|
-
`CLAUDEMD_PURGE=1`
|
|
396
|
+
Asking for a purge (`CLAUDEMD_PURGE=1`) also drops `~/.claude/.claudemd-state/` and your rule-hits log.
|
|
397
397
|
|
|
398
398
|
**What `--purge` deliberately leaves**: `~/.claude/settings.json.claudemd-backup-<ISO>` — the copy of your `settings.json` taken before install rewrote it, and `~/.claude/backup-<ISO>/` / `spec-backup-<ISO>/`. Purge removes claudemd's own state; it does not remove your data, and these files are the copies that exist so `restore` has something to restore from. Delete them by hand once you are sure you no longer want them.
|
|
399
399
|
|
|
@@ -409,6 +409,8 @@ CLAUDEMD_SPEC_ACTION=delete CLAUDEMD_CONFIRM=1 node ~/.claude/plugins/cache/clau
|
|
|
409
409
|
|
|
410
410
|
The slash command and the script are equivalent — the slash command just supplies `${CLAUDE_PLUGIN_ROOT}` for you.
|
|
411
411
|
|
|
412
|
+
Once the cache is gone too, restore your own `CLAUDE.md` by hand: copy `CLAUDE.md` from the newest `~/.claude/backup-<ISO>/` that holds one to `~/.claude/CLAUDE.md`.
|
|
413
|
+
|
|
412
414
|
---
|
|
413
415
|
|
|
414
416
|
## Troubleshooting
|
package/bin/claudemd-lint.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// claudemd-lint — CLI surface for §10-V banned-vocab + transcript scanning.
|
|
3
|
-
// Built for use OUTSIDE Claude Code: git
|
|
3
|
+
// Built for use OUTSIDE Claude Code: git commit-msg hooks, GitHub Actions,
|
|
4
4
|
// other agent integrations (Codex / Cursor / OpenClaw). Reuses the same
|
|
5
5
|
// pattern file (hooks/banned-vocab.patterns) the in-CC bash hooks read,
|
|
6
6
|
// so enforcement is consistent across surfaces.
|
|
@@ -62,7 +62,7 @@ Flags:
|
|
|
62
62
|
Notes:
|
|
63
63
|
A bare \`lint <arg>\` whose only positional is an existing regular file
|
|
64
64
|
is auto-treated as \`--file <arg>\` so \`claudemd-cli lint .git/COMMIT_EDITMSG\`
|
|
65
|
-
works as expected in
|
|
65
|
+
works as expected in a commit-msg hook. Pass --stdin or quote literal
|
|
66
66
|
text to opt out.
|
|
67
67
|
|
|
68
68
|
A commit-msg hook is handed the RAW message file, which still holds git's
|
|
@@ -276,7 +276,7 @@ function lintCmd(rawArgs) {
|
|
|
276
276
|
} else if (positional.length > 0) {
|
|
277
277
|
// Auto-detect: a bare single positional that is an existing regular file
|
|
278
278
|
// is overwhelmingly the user's intent (they're piping a commit-msg path
|
|
279
|
-
// from a git
|
|
279
|
+
// from a git commit-msg hook). Without this, `claudemd-cli lint message.txt`
|
|
280
280
|
// silently scans the LITERAL STRING "message.txt" → exits 0 even when
|
|
281
281
|
// the file contents would deny.
|
|
282
282
|
//
|
|
@@ -294,7 +294,7 @@ function lintCmd(rawArgs) {
|
|
|
294
294
|
// a single literal word, not a typo'd path. `lint message.txt` (no slash,
|
|
295
295
|
// missing file) stays text — it's ambiguous between "literal text" and
|
|
296
296
|
// "filename in cwd"; pre-existing-file behavior wins to preserve the
|
|
297
|
-
//
|
|
297
|
+
// commit-msg-hook ergonomic where `--file` is explicit.
|
|
298
298
|
if (positional.length === 1) {
|
|
299
299
|
const arg = positional[0];
|
|
300
300
|
// Path-shape heuristic: contains `/`, or is `.`/`..`. Whitespace disqualifies
|
|
@@ -325,6 +325,17 @@ function lintCmd(rawArgs) {
|
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
if (text === undefined) text = positional.join(' ');
|
|
328
|
+
// An empty positional is a missing one (R15). Its live shape is
|
|
329
|
+
// `lint "$1"` installed as a pre-commit hook, which git calls with no
|
|
330
|
+
// arguments: the scan of "" passed every commit unscanned.
|
|
331
|
+
if (sourcePath === null && text.trim() === '') {
|
|
332
|
+
process.stderr.write(
|
|
333
|
+
'lint: the text is empty, so there is nothing to scan. In a git hook this usually means it ' +
|
|
334
|
+
'is installed as pre-commit, which gets no arguments; use a commit-msg hook, which gets the ' +
|
|
335
|
+
'message file as $1.\n'
|
|
336
|
+
);
|
|
337
|
+
process.exit(2);
|
|
338
|
+
}
|
|
328
339
|
} else {
|
|
329
340
|
process.stderr.write('lint: text required (positional arg, --file PATH, or --stdin)\n');
|
|
330
341
|
process.exit(2);
|
|
@@ -349,7 +360,7 @@ function lintCmd(rawArgs) {
|
|
|
349
360
|
}
|
|
350
361
|
|
|
351
362
|
// Per-commit escape hatch — mirrors hooks/banned-vocab-check.sh:36. Without
|
|
352
|
-
// this, `claudemd-cli lint --file=.git/COMMIT_EDITMSG` in a git
|
|
363
|
+
// this, `claudemd-cli lint --file=.git/COMMIT_EDITMSG` in a git commit-msg
|
|
353
364
|
// hook silently disagreed with the in-CC bash hook: the same commit message
|
|
354
365
|
// with `[allow-banned-vocab]` would pass the bash gate (exit 0) but the CLI
|
|
355
366
|
// would still exit 1 and block the commit. Same input → different verdict =
|
|
@@ -376,7 +387,7 @@ function lintCmd(rawArgs) {
|
|
|
376
387
|
// rescans the tail — quadratic, 710ms on a 20k digit run, and this runs on
|
|
377
388
|
// the RAW file before scan(), so lint.js:150-157's sanitizer fix for the same
|
|
378
389
|
// family never covered it. A digit-dense lockfile or hash dump hung the
|
|
379
|
-
//
|
|
390
|
+
// commit-msg hook. 64 is far past any real anchor (`p99 580ms→140ms` is 15).
|
|
380
391
|
const HAS_NUMERIC_ARROW = /\d\S{0,64}\s*(?:→|->|=>)\s*\d/;
|
|
381
392
|
const HAS_BASELINE = /baseline/i;
|
|
382
393
|
const baselineExempt = HAS_NUMERIC_ARROW.test(text) || HAS_BASELINE.test(text);
|
|
@@ -399,6 +410,15 @@ function auditCmd(rawArgs) {
|
|
|
399
410
|
const positional = args.filter(a => !a.startsWith('--'));
|
|
400
411
|
const transcriptPath = positional[0];
|
|
401
412
|
|
|
413
|
+
// A shell glob (`audit dir/*.jsonl`) hands over several paths; scanning only
|
|
414
|
+
// the first let a hit in any later file pass as exit 0 (R15). Checked before
|
|
415
|
+
// the missing-path test so `audit '' b.jsonl` is not "required" (R18 L9).
|
|
416
|
+
if (positional.length > 1) {
|
|
417
|
+
process.stderr.write(
|
|
418
|
+
`audit: takes one <jsonl-path>, got ${positional.length} — run it once per transcript\n`
|
|
419
|
+
);
|
|
420
|
+
process.exit(2);
|
|
421
|
+
}
|
|
402
422
|
if (!transcriptPath) {
|
|
403
423
|
process.stderr.write('audit: <jsonl-path> required\n');
|
|
404
424
|
process.exit(2);
|
|
@@ -426,7 +446,7 @@ function auditCmd(rawArgs) {
|
|
|
426
446
|
// Mirrors the `lint --file` read path above (R11-11). The existsSync/statSync
|
|
427
447
|
// guards a few lines up both PASS on a mode-000 file, so an unreadable
|
|
428
448
|
// transcript reached this line and exited 1 with a V8 stack — colliding with
|
|
429
|
-
// the documented `1 = hits found`, which is what CI and
|
|
449
|
+
// the documented `1 = hits found`, which is what CI and commit-msg hooks gate on.
|
|
430
450
|
let jsonl;
|
|
431
451
|
try {
|
|
432
452
|
jsonl = fs.readFileSync(transcriptPath, 'utf8');
|
|
@@ -491,7 +511,7 @@ function auditCmd(rawArgs) {
|
|
|
491
511
|
if (integrity.badLines > 0) {
|
|
492
512
|
process.stderr.write(
|
|
493
513
|
`audit: warning: skipped ${integrity.badLines} of ${integrity.totalLines} non-empty line(s) ` +
|
|
494
|
-
`that did not parse as JSON — the turn count below covers only the rows that parsed.\n`
|
|
514
|
+
`that did not parse as JSON or are not transcript-shaped — the turn count below covers only the rows that parsed.\n`
|
|
495
515
|
);
|
|
496
516
|
}
|
|
497
517
|
// QA ISSUE-002 (option c): string-shape assistant rows are outside the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudemd-cli",
|
|
3
|
-
"version": "0.
|
|
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
|
|
3
|
+
"version": "0.111.0",
|
|
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 commit-msg hooks, GitHub Actions, and other agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claudemd-cli": "bin/claudemd-lint.js"
|
package/scripts/lib/argv.js
CHANGED
|
@@ -178,7 +178,7 @@ export function parseStrictOrExit(argv, spec = {}) {
|
|
|
178
178
|
// being converged. It is not merged into parseStrict because the two contracts
|
|
179
179
|
// genuinely differ and the difference is published: parseStrict rejects the
|
|
180
180
|
// `--key value` space form and positional arguments, both of which
|
|
181
|
-
// `claudemd-cli lint <path> --comment-char ';'` documents and users'
|
|
181
|
+
// `claudemd-cli lint <path> --comment-char ';'` documents and users' commit-msg
|
|
182
182
|
// hooks depend on.
|
|
183
183
|
//
|
|
184
184
|
// Strict-validate flag-shaped args + normalize `--key=value` → `--key value`
|
package/scripts/lib/lint.js
CHANGED
|
@@ -390,7 +390,7 @@ function templateComments(templateLines, c) {
|
|
|
390
390
|
// had them stripped before the scan, muting any §10-V violation inside. Same
|
|
391
391
|
// violation at 2 trailing `#` lines denied, at 3 it exited 0 — a silent miss
|
|
392
392
|
// that grew MORE likely the longer the commented block got, on the shipped
|
|
393
|
-
//
|
|
393
|
+
// commit-msg/CI entry point.
|
|
394
394
|
//
|
|
395
395
|
// That removal was shipped with the claim that no git shape needs the signal.
|
|
396
396
|
// The 0.68.3 pre-tag review refuted it: the six-shape table measured
|
|
@@ -463,10 +463,19 @@ export function parseTranscript(jsonlText, integrity = null) {
|
|
|
463
463
|
continue;
|
|
464
464
|
}
|
|
465
465
|
if (row.type !== 'assistant') continue;
|
|
466
|
-
const content = row.message?.content
|
|
466
|
+
const content = row.message?.content;
|
|
467
|
+
// Same defect one level down (R15): an object where the block array
|
|
468
|
+
// belongs threw "not iterable", a null block threw on `.type`. A string is
|
|
469
|
+
// a known shape, counted apart by countStringContentAssistantRows.
|
|
470
|
+
if (!Array.isArray(content)) {
|
|
471
|
+
if (content != null && typeof content !== 'string' && integrity) {
|
|
472
|
+
integrity.badLines = (integrity.badLines || 0) + 1;
|
|
473
|
+
}
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
467
476
|
const texts = [];
|
|
468
477
|
for (const b of content) {
|
|
469
|
-
if (b.type === 'text' && typeof b.text === 'string') texts.push(b.text);
|
|
478
|
+
if (b && b.type === 'text' && typeof b.text === 'string') texts.push(b.text);
|
|
470
479
|
}
|
|
471
480
|
if (texts.length === 0) continue;
|
|
472
481
|
turns.push({ turnIndex, line: i + 1, text: texts.join(' ') });
|