Three separate defects on one path, all of them guidance — which CLAUDE.md treats as one
mechanism: "use the same command strings sec status prints, so the map and the suggestions stay
one vocabulary."
(a) sec --dry-run index prints three lines, two of them false
With 500 converted, unindexed documents in filing_document and the kb tables already built:
$ sec --dry-run index
-> Dry run — no data will be written
Would index 500 filing(s) into the knowledge base.
indexed 0 filing(s) · 0 sections
Next:
sec update documents there is nothing converted to index yet
Five hundred filings are converted and waiting — the line above says so.
Mechanism: 6d9ee18 made the dry-run branch return { indexed: 0, sections: 0, skipped, truncated }
(IndexFilingSectionsTask.ts:185-188), and the renderer keys its summary and its suggestion off
that zero (ask.ts:69-81):
console.log(`indexed ${out.indexed} filing(s) · ${out.sections} sections` + …);
if (out.indexed > 0 || out.skipped > 0) { suggest({ command: 'sec ask "..."' }); }
else { suggest({ command: "sec update documents", why: "there is nothing converted to index yet" }); }
Fix: put the count on the output — wouldIndex (or a dryRun: true discriminator alongside
work.length) — and have the renderer print and branch on it. The console.log inside the task
then goes away too, which is (c).
(b) db stats tells the operator to run a command that cannot fix what it is pointing at
b3c7552d appends the three knowledge-base tables to the report with rows: null, deliberately
"the same signal a registered-but-uncreated table reports" (kbTableStats.ts:29). But
src/cli/groups/db.ts:70 renders that signal as one fixed sentence:
console.log(`\n${missing} table(s) not counted (n/a) — run \`db setup\`?`);
and db setup does not create them — they are outside storageRegistry, built lazily by the
first command that opens the index:
$ sec db setup # exit 0
$ sec db stats | tail -5
kb_document n/a
kb_chunk n/a
kb_index n/a
3 table(s) not counted (n/a) — run `db setup`? # ← immediately after db setup succeeded
The command that fixes it is sec index. Fix: let a TableStat carry the command that creates
it, or split the footer into the registry rows (db setup) and the kb rows (sec index). This
matters more than it looks: it is the only n/a an operator can actually hit on a healthy database,
so it is the only time that footer is ever read.
(c) sec index ignores --json
The global flag is advertised as "Emit status and error output as machine-parseable JSON"
(GlobalOptions.ts:16) and status honours it — valid JSON, including nextSteps. index does
not, and under --dry-run it interleaves one genuine JSON status line with two plain-text lines on
the same stream:
$ sec --json --dry-run index
{"status":"info","message":"Dry run — no data will be written"}
Would index 0 filing(s) into the knowledge base.
indexed 0 filing(s) · 0 sections
--> INVALID JSON: Unexpected non-whitespace character after JSON at position 64 (line 2 column 1)
ask gets this right (ask.ts:143-146). index needs the same branch. Note the second line
comes from a console.log inside the task (IndexFilingSectionsTask.ts:186) — consistent
with the nine other tasks that print inside an isDryRun() branch, so this is a house pattern
rather than a one-off, and it is invisible to isJsonOutput() by construction. Returning the
count and letting the command render it fixes (a) and (c) together.
Verify
sec db setup && sec index # build the kb tables
# seed filing_document with a few converted, unindexed rows
sec --dry-run index # (a)
sec db setup && sec db stats | tail -5 # (b)
sec --json --dry-run index | node -e 'let s="";process.stdin.on("data",d=>s+=d)
.on("end",()=>{try{JSON.parse(s);console.log("VALID")}catch(e){console.log("INVALID:",e.message)}})' # (c)
Found during the 2026-09-14 review. Snapshot: workglow-dev/prd → analysis/grades/2026-09-14/sec-detailed.md §4.5.
Three separate defects on one path, all of them guidance — which CLAUDE.md treats as one
mechanism: "use the same command strings
sec statusprints, so the map and the suggestions stayone vocabulary."
(a)
sec --dry-run indexprints three lines, two of them falseWith 500 converted, unindexed documents in
filing_documentand the kb tables already built:Five hundred filings are converted and waiting — the line above says so.
Mechanism:
6d9ee18made the dry-run branch return{ indexed: 0, sections: 0, skipped, truncated }(
IndexFilingSectionsTask.ts:185-188), and the renderer keys its summary and its suggestion offthat zero (
ask.ts:69-81):Fix: put the count on the output —
wouldIndex(or adryRun: truediscriminator alongsidework.length) — and have the renderer print and branch on it. Theconsole.loginside the taskthen goes away too, which is (c).
(b)
db statstells the operator to run a command that cannot fix what it is pointing atb3c7552dappends the three knowledge-base tables to the report withrows: null, deliberately"the same signal a registered-but-uncreated table reports" (
kbTableStats.ts:29). Butsrc/cli/groups/db.ts:70renders that signal as one fixed sentence:and
db setupdoes not create them — they are outsidestorageRegistry, built lazily by thefirst command that opens the index:
The command that fixes it is
sec index. Fix: let aTableStatcarry the command that createsit, or split the footer into the registry rows (
db setup) and the kb rows (sec index). Thismatters more than it looks: it is the only n/a an operator can actually hit on a healthy database,
so it is the only time that footer is ever read.
(c)
sec indexignores--jsonThe global flag is advertised as "Emit status and error output as machine-parseable JSON"
(
GlobalOptions.ts:16) andstatushonours it — valid JSON, includingnextSteps.indexdoesnot, and under
--dry-runit interleaves one genuine JSON status line with two plain-text lines onthe same stream:
askgets this right (ask.ts:143-146).indexneeds the same branch. Note the second linecomes from a
console.loginside the task (IndexFilingSectionsTask.ts:186) — consistentwith the nine other tasks that print inside an
isDryRun()branch, so this is a house patternrather than a one-off, and it is invisible to
isJsonOutput()by construction. Returning thecount and letting the command render it fixes (a) and (c) together.
Verify
Found during the 2026-09-14 review. Snapshot:
workglow-dev/prd→analysis/grades/2026-09-14/sec-detailed.md§4.5.