Dogfooding `campaign` + `knowledge-refresh` against a throwaway ledger. Seven
defects, all found by running the commands as written and measuring, not by
reading them.
The headline pair only existed together. `knowledge-refresh` built
`STALE_AFTER="--stale-after 30"` and expanded it unquoted, trusting the shell to
split it in two. bash does; zsh — the macOS default, and what the Bash tool runs
here — does not. The CLI got one argv entry, matched no flag, and because it had
no unknown-flag branch, silently kept the 90-day default and reported "✓ All 14
register entries were re-verified within the last 90 days": a true-sounding
sentence about a threshold the user had just overridden. Fixing either half alone
leaves a silent wrong answer or a loud one; both are fixed, and a guard now
rejects any template that packs a flag and its value into one variable.
`knowledge-refresh` also read one register and wrote another: step 6 named an
unanchored `knowledge/best-practices.json` while the CLI reads
`${CLAUDE_PLUGIN_ROOT}/…`, which for an installed plugin is the cache. The
validation gate then ran the cached test against the cached register — green no
matter what was written. The two copies were byte-identical that day, which is
exactly why it was invisible.
`campaign` vouched for repos it could not read. `add /finnes/ikke` returned
`added` + exit 0; `refresh-tokens` then put the phantom in `swept[]` with a
0-token delta and left `skipped[]` empty, so the machine-wide bill claimed
coverage of three repos on a machine with two. Paths stay tracked — an unmounted
volume is a legitimate absence — but are reported as `addedUnverified`, and the
command names them.
Two class sweeps, both measured rather than assumed. `posture` was the single
scanner (1 of 14) whose fatal catch exited 1, which ux-rules defines as a normal
WARNING grade — a crash indistinguishable from a result. And all 13 payload
writers failed on a `--output-file` whose parent did not exist, which on a fresh
machine turned `campaign`'s first run into "the ledger may be corrupt"; they now
share `scanners/lib/write-output.mjs`.
Predicted breadth was too wide for the first time in five sessions: 6 of 8 CLIs
predicted to lack unknown-flag rejection, 4 measured. `drift` and `fix` already
reject them, via a construct the grep did not recognise — a grep matches an
implementation, the invariant is a behaviour. The sweep was rewritten to run each
CLI with a bogus flag and read the exit code.
Suite 1453 → 1469/0. Frozen snapshots untouched. `optimize-lens-cli` and
`token-hotspots-cli` share the unknown-flag defect and are deferred to the v5.14
argument-handling chunk with their positional-swallow arm; the count is recorded
in the guard rather than rounded down to zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NHWjN8EnoxSqRvMTLK2NE
115 lines
4.3 KiB
JavaScript
115 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* knowledge-refresh CLI — feeds the v5.7 `/config-audit knowledge-refresh` command
|
|
* (Chunk 3: the "living" half of the living knowledge base).
|
|
*
|
|
* This is the DETERMINISTIC half of the hybrid motor: it loads the best-practices
|
|
* register and classifies every entry as `fresh` or `stale` by the age of its
|
|
* `source.verified` stamp (via the pure `assessFreshness` core). It is READ-ONLY —
|
|
* it NEVER writes the register and NEVER touches the network. Candidate discovery
|
|
* (polling the CC changelog + Anthropic blog) and the human-approved writes live in
|
|
* the command layer (Verifiseringsplikt). `--dry-run` is implicit and the only mode;
|
|
* the flag is accepted for explicitness and echoed back.
|
|
*
|
|
* Naming: `-cli` suffix → NOT an orchestrated scanner (the scan-orchestrator only
|
|
* loads scanner modules), so the scanner count is unchanged and the snapshot suite
|
|
* stays byte-stable.
|
|
*
|
|
* Usage:
|
|
* node knowledge-refresh-cli.mjs [--output-file <path>] [--stale-after <N>]
|
|
* [--reference-date <YYYY-MM-DD>] [--dry-run]
|
|
*
|
|
* Exit codes: 0 = every entry fresh, 1 = one or more stale (advisory), 3 = error.
|
|
*/
|
|
|
|
import { resolve } from 'node:path';
|
|
import { writeOutputFile } from './lib/write-output.mjs';
|
|
import { loadRegister, REGISTER_PATH } from './lib/best-practices-register.mjs';
|
|
import { assessFreshness, STALE_AFTER_DAYS_DEFAULT } from './lib/knowledge-refresh.mjs';
|
|
|
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
|
/**
|
|
* Usage error. Throws rather than calling process.exit(): exit() discards
|
|
* unflushed stdout when stdout is a pipe. The top-level catch prints the same
|
|
* `Error: ` text and sets the same exit code 3, so callers see no difference.
|
|
*/
|
|
class CliUsageError extends Error {}
|
|
|
|
function fail(message) {
|
|
throw new CliUsageError(message);
|
|
}
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
let outputFile = null;
|
|
let staleAfterDays = STALE_AFTER_DAYS_DEFAULT;
|
|
let referenceDate = null; // null → today
|
|
let dryRun = false;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const a = args[i];
|
|
if (a === '--dry-run') dryRun = true;
|
|
else if (a === '--output-file' && args[i + 1]) outputFile = args[++i];
|
|
else if (a === '--stale-after' && args[i + 1] !== undefined) {
|
|
const n = Number.parseInt(args[++i], 10);
|
|
if (!Number.isInteger(n) || n < 0) fail('--stale-after must be a non-negative integer (days)');
|
|
staleAfterDays = n;
|
|
} else if (a === '--reference-date' && args[i + 1]) {
|
|
referenceDate = args[++i];
|
|
if (!DATE_RE.test(referenceDate)) fail('--reference-date must be YYYY-MM-DD');
|
|
}
|
|
// A flag we do not understand must fail loudly. Silently dropping it is how
|
|
// `--stale-after 30` — arriving as ONE argv entry from a shell that does not
|
|
// word-split — became "all 14 entries are fresh within 90 days": a confident
|
|
// answer to a question the caller did not ask.
|
|
else if (a.startsWith('--')) fail(`unknown flag "${a}"`);
|
|
}
|
|
|
|
// The clock is read here ONLY — the core takes an injected date and stays pure.
|
|
const ref = referenceDate || new Date();
|
|
|
|
let register;
|
|
try {
|
|
register = loadRegister();
|
|
} catch (err) {
|
|
fail(`could not load register at ${REGISTER_PATH}: ${err.message}`);
|
|
}
|
|
|
|
let assessment;
|
|
try {
|
|
assessment = assessFreshness(register, { referenceDate: ref, staleAfterDays });
|
|
} catch (err) {
|
|
fail(err.message);
|
|
}
|
|
|
|
const payload = {
|
|
status: 'ok',
|
|
registerPath: REGISTER_PATH,
|
|
version: register.version,
|
|
dryRun: true, // this CLI never writes; the flag is informational
|
|
requestedDryRun: dryRun,
|
|
referenceDate: assessment.referenceDate,
|
|
staleAfterDays: assessment.staleAfterDays,
|
|
counts: assessment.counts,
|
|
stale: assessment.stale,
|
|
fresh: assessment.fresh,
|
|
};
|
|
|
|
const json = JSON.stringify(payload, null, 2);
|
|
if (outputFile) await writeOutputFile(outputFile, json, 'utf-8');
|
|
else process.stdout.write(json + '\n');
|
|
|
|
process.exitCode = assessment.counts.stale > 0 ? 1 : 0;
|
|
}
|
|
|
|
const isDirectRun =
|
|
process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
|
|
if (isDirectRun) {
|
|
main().catch((err) => {
|
|
const prefix = err instanceof CliUsageError ? 'Error' : 'Fatal';
|
|
process.stderr.write(`${prefix}: ${err.message}\n`);
|
|
process.exitCode = 3;
|
|
});
|
|
}
|