Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@
Notable changes to `@framesieve/cli`. Pre-1.0 the CLI surface may
change between minor versions.

## 0.2.1

- `--mode <previous|reference>` selects the diff comparison baseline
(core 0.4.0's reference mode), and `--persist <ms>` sets
`policy.referencePersistMs`. `--sweep referencePersistMs=a:b:step`
calibrates the reference-mode persistence window against a recording,
so `fsieve replay <dir> --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

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
20 changes: 18 additions & 2 deletions packages/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ Options:
--sweep <param=a:b:step> replay once per value of <param>;
params: downsampleFactor, luminanceThreshold,
blockChangeRatio, minChangedBlocks, windowSize,
debounceMs, minIntervalMs, maxSilenceMs
debounceMs, minIntervalMs, maxSilenceMs,
referencePersistMs
--algorithm <name> diff algorithm: downsample | pixel | edge
--mode <name> diff comparison: previous | reference
--downsample <n> diff.downsampleFactor
--luminance <n> diff.luminanceThreshold
--grid <cols>x<rows> blocks grid, e.g. 16x9
Expand All @@ -32,6 +34,7 @@ Options:
--debounce <ms> policy.debounceMs
--min-interval <ms> policy.minIntervalMs
--max-silence <ms> policy.maxSilenceMs
--persist <ms> policy.referencePersistMs (reference mode)
-h, --help show this help
`;

Expand All @@ -43,7 +46,8 @@ type SweepParam =
| "windowSize"
| "debounceMs"
| "minIntervalMs"
| "maxSilenceMs";
| "maxSilenceMs"
| "referencePersistMs";

const SWEEP_PARAMS: readonly SweepParam[] = [
"downsampleFactor",
Expand All @@ -54,6 +58,7 @@ const SWEEP_PARAMS: readonly SweepParam[] = [
"debounceMs",
"minIntervalMs",
"maxSilenceMs",
"referencePersistMs",
];

class UsageError extends Error {}
Expand Down Expand Up @@ -86,6 +91,7 @@ function withParam(
case "debounceMs":
case "minIntervalMs":
case "maxSilenceMs":
case "referencePersistMs":
return { ...options, policy: { ...options.policy, [param]: value } };
}
}
Expand Down Expand Up @@ -144,6 +150,13 @@ function buildOptions(values: Record<string, unknown>): 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));
Expand All @@ -156,6 +169,7 @@ function buildOptions(values: Record<string, unknown>): FrameGateOptions {
num("debounce", "debounceMs");
num("min-interval", "minIntervalMs");
num("max-silence", "maxSilenceMs");
num("persist", "referencePersistMs");

const grid = str("grid");
if (grid !== undefined) {
Expand Down Expand Up @@ -214,6 +228,7 @@ export async function run(argv: string[]): Promise<number> {
json: { type: "boolean" },
sweep: { type: "string" },
algorithm: { type: "string" },
mode: { type: "string" },
downsample: { type: "string" },
luminance: { type: "string" },
grid: { type: "string" },
Expand All @@ -224,6 +239,7 @@ export async function run(argv: string[]): Promise<number> {
debounce: { type: "string" },
"min-interval": { type: "string" },
"max-silence": { type: "string" },
persist: { type: "string" },
help: { type: "boolean", short: "h" },
},
});
Expand Down
47 changes: 47 additions & 0 deletions packages/cli/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
Loading