diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 24cc4f5..c837cd8 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -3,11 +3,20 @@ Notable changes to `@framesieve/cli`. Pre-1.0 the CLI surface may change between minor versions. +## 0.2.1 + +- `--mode ` selects the diff comparison baseline + (core 0.4.0's reference mode), and `--persist ` sets + `policy.referencePersistMs`. `--sweep referencePersistMs=a:b:step` + calibrates the reference-mode persistence window against a recording, + so `fsieve replay --mode reference --sweep referencePersistMs=0:4000:1000` + shows the emit knee. No behavior change to existing flags. + ## 0.2.0 - Requires `framesieve` `^0.4.0` and `@framesieve/adapters` `^0.3.0` - (core adds the opt-in reference diff mode). `fsieve replay --sweep` - can vary `diff.mode` like any other gate option; no new CLI flags. + (core adds the opt-in reference diff mode). No CLI surface change in + this version. ## 0.1.1 diff --git a/packages/cli/package.json b/packages/cli/package.json index adff617..b1231e1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@framesieve/cli", - "version": "0.2.0", + "version": "0.2.1", "description": "fsieve command: replay recordings and sweep gate parameters offline.", "license": "Apache-2.0", "type": "module", diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index 667bbd2..f04fc37 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -20,8 +20,10 @@ Options: --sweep replay once per value of ; params: downsampleFactor, luminanceThreshold, blockChangeRatio, minChangedBlocks, windowSize, - debounceMs, minIntervalMs, maxSilenceMs + debounceMs, minIntervalMs, maxSilenceMs, + referencePersistMs --algorithm diff algorithm: downsample | pixel | edge + --mode diff comparison: previous | reference --downsample diff.downsampleFactor --luminance diff.luminanceThreshold --grid x blocks grid, e.g. 16x9 @@ -32,6 +34,7 @@ Options: --debounce policy.debounceMs --min-interval policy.minIntervalMs --max-silence policy.maxSilenceMs + --persist policy.referencePersistMs (reference mode) -h, --help show this help `; @@ -43,7 +46,8 @@ type SweepParam = | "windowSize" | "debounceMs" | "minIntervalMs" - | "maxSilenceMs"; + | "maxSilenceMs" + | "referencePersistMs"; const SWEEP_PARAMS: readonly SweepParam[] = [ "downsampleFactor", @@ -54,6 +58,7 @@ const SWEEP_PARAMS: readonly SweepParam[] = [ "debounceMs", "minIntervalMs", "maxSilenceMs", + "referencePersistMs", ]; class UsageError extends Error {} @@ -86,6 +91,7 @@ function withParam( case "debounceMs": case "minIntervalMs": case "maxSilenceMs": + case "referencePersistMs": return { ...options, policy: { ...options.policy, [param]: value } }; } } @@ -144,6 +150,13 @@ function buildOptions(values: Record): FrameGateOptions { } options = { ...options, diff: { ...options.diff, algorithm } }; } + const mode = str("mode"); + if (mode !== undefined) { + if (mode !== "previous" && mode !== "reference") { + throw new UsageError(`--mode expects previous or reference`); + } + options = { ...options, diff: { ...options.diff, mode } }; + } const num = (flag: string, param: SweepParam): void => { const v = str(flag); if (v !== undefined) options = withParam(options, param, parseNumber(`--${flag}`, v)); @@ -156,6 +169,7 @@ function buildOptions(values: Record): FrameGateOptions { num("debounce", "debounceMs"); num("min-interval", "minIntervalMs"); num("max-silence", "maxSilenceMs"); + num("persist", "referencePersistMs"); const grid = str("grid"); if (grid !== undefined) { @@ -214,6 +228,7 @@ export async function run(argv: string[]): Promise { json: { type: "boolean" }, sweep: { type: "string" }, algorithm: { type: "string" }, + mode: { type: "string" }, downsample: { type: "string" }, luminance: { type: "string" }, grid: { type: "string" }, @@ -224,6 +239,7 @@ export async function run(argv: string[]): Promise { debounce: { type: "string" }, "min-interval": { type: "string" }, "max-silence": { type: "string" }, + persist: { type: "string" }, help: { type: "boolean", short: "h" }, }, }); diff --git a/packages/cli/test/cli.test.ts b/packages/cli/test/cli.test.ts index b2e03a0..d48a4c0 100644 --- a/packages/cli/test/cli.test.ts +++ b/packages/cli/test/cli.test.ts @@ -138,10 +138,57 @@ test("sweeping blockChangeRatio handles fractional steps without float drift", ( assert.deepEqual(values, ["0.1", "0.15", "0.2", "0.25", "0.3", "0.35", "0.4"]); }); +test("--mode reference matches the library's reference-mode decisions", async () => { + const { stdout, status } = fsieve("replay", recordingDir, "--json", "--mode", "reference"); + assert.equal(status, 0); + const fromCli = stdout.trim().split("\n").map(parseDecisionLine); + const fromLib = await replay(recordingDir, { diff: { mode: "reference" } }); + assert.deepEqual(fromCli, canonical(fromLib.decisions)); +}); + +test("--persist sets referencePersistMs, matching the library", async () => { + const { stdout, status } = fsieve( + "replay", + recordingDir, + "--json", + "--mode", + "reference", + "--persist", + "1000", + ); + assert.equal(status, 0); + const fromCli = stdout.trim().split("\n").map(parseDecisionLine); + const fromLib = await replay(recordingDir, { + diff: { mode: "reference" }, + policy: { referencePersistMs: 1000 }, + }); + assert.deepEqual(fromCli, canonical(fromLib.decisions)); +}); + +test("--sweep referencePersistMs prints one row per value", () => { + const { stdout, status } = fsieve( + "replay", + recordingDir, + "--mode", + "reference", + "--sweep", + "referencePersistMs=0:2000:1000", + ); + assert.equal(status, 0); + const rows = stdout + .trim() + .split("\n") + .filter((l) => /^ {2}\d/.test(l)); + assert.equal(rows.length, 3); // 0, 1000, 2000 + assert.match(rows[0]!, /^ {2}0\b/); + assert.match(rows[2]!, /^ {2}2000\b/); +}); + test("usage errors exit nonzero", () => { assert.equal(fsieve().status, 1); assert.equal(fsieve("replay").status, 1); assert.equal(fsieve("replay", recordingDir, "--sweep", "bogus=1:2:1").status, 1); + assert.equal(fsieve("replay", recordingDir, "--mode", "sideways").status, 1); assert.equal(fsieve("replay", join(tmpdir(), "does-not-exist-xyz")).status, 1); });