From 71e3622640df5c709a657de2e4b13ec4bcf80f65 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Wed, 19 Aug 2026 21:47:31 +0000 Subject: [PATCH] fix(skills): harden TasteForge multimodal contract --- skills/tasteforge-video/SKILL.md | 33 ++++++--- tests/ci/tasteforge-video-skill.test.js | 74 ++++++++++++++++++- .../reject-continue-without-anchor.json | 19 +++++ .../reject-dry-run-false.json | 20 +++++ 4 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 tests/fixtures/tasteforge-video/reject-continue-without-anchor.json create mode 100644 tests/fixtures/tasteforge-video/reject-dry-run-false.json diff --git a/skills/tasteforge-video/SKILL.md b/skills/tasteforge-video/SKILL.md index 458c8b073..42c9d2572 100644 --- a/skills/tasteforge-video/SKILL.md +++ b/skills/tasteforge-video/SKILL.md @@ -1,6 +1,6 @@ --- name: tasteforge-video -description: Use when a user wants a taste interview for video work, to distill a visual aesthetic into structured, reusable constraints (a style pack), to validate or audit a style pack, to apply a pack's measured cadence and look to local footage, or to export an editable EDL/FCPXML cut. Also for auditing generated-media provenance and for deciding what is local-deterministic versus provider generation. All ECC-side operations are offline and deterministic; provider (Fal) generation fails closed here. +description: Use for file-driven multimodal image, video, and 3D-asset discovery; taste interviews; distill or apply workflows; style-pack validation; editable EDL/FCPXML export; provenance audits; and offline planning that must fail closed before provider generation. metadata: origin: ECC --- @@ -29,8 +29,12 @@ explains it and does not vendor or duplicate its code. - The user asks for a **generated-media provenance audit** — where did this pack, spec, or cut come from; what was measured locally versus generated by a provider; what was dry-run. +- The user asks to **discover or plan file-driven multimodal image, video, or + 3D-asset outputs** from local reference files, including separate manifests, + subject-anchored CV effects, or Resolve effect recipes. - The user mentions TasteForge, style packs, flashethereal, taste distillation, - cadence/rhythm planning, or a taste interview for video. + cadence/rhythm planning, multimodal discovery, distill/apply workflows, or a + taste interview for video. ## Local Deterministic Operations vs Provider Generation @@ -70,8 +74,8 @@ dry-run/dry_run semantics — say "dry-run spec" or "deterministic plan", never canonical local GitHub checkout root (never a hard-coded machine path); package directory `tasteforge/`. - CLI: `python3 -m tasteforge ` — `provenance`, `inspect`, `validate`, - `interview`, `distill`, `apply`, `export`. `--live` flags exit with code 2 - and refuse. + `interview`, `distill`, `apply`, `export`, `multimodal`. `--live` flags exit + with code 2 and refuse. - Schemas are the contract: taste profile, pack manifest, grade, cadence, spec, timeline events, application reports (`provider` is enum-locked to `"none"`; `dry_run` to `true`). @@ -125,16 +129,23 @@ genres distinct rather than blending them into one generic aesthetic: The command measures local references with ffprobe/ffmpeg and emits one style spec per genre, separate image, video, and 3D-asset manifests, provenance, and a Resolve effect recipe. The effect schedule must be seeded aperiodic. CV -effects require a real subject anchor with fail-closed track-loss behavior. -Every effect carries placement constraints that preserve faces and readable -type and prevent decorative corner meshes from replacing full-frame 3D work. +effects require a real subject anchor whose exact lost-track policy is +`disable_effect_until_track_recovers`; `continue_without_anchor` and every +other policy fail closed. Every effect carries placement constraints that +preserve faces and readable type and prevent decorative corner meshes from +replacing full-frame 3D work. The returned receipt is the bundle boundary. It binds every emitted evidence artifact by relative path, byte size, SHA-256, genre, modality, -`provider_execution: false`, and exact reference/time provenance. Manifests and -requests also require `provider_calls: 0`, `provider_execution: false`, -`submit: false`, and disabled provider-call mode. Whole-file evidence uses an -explicit whole-file time basis and never invents timestamps. +`provider_execution:false`, and exact reference/time provenance. The receipt +itself requires `provider_calls:0`, `provider_execution:false`, and +`dry_run:true`. Every modality manifest and every nested request must contain +all four exact fail-closed fields: `provider_calls:0`, +`provider_execution:false`, `dry_run:true`, and `submit:false`; each request +also requires `provider_call_mode:"disabled"`. A missing field is a rejection, +not a default, and `dry_run:false` must be rejected before output is written. +Whole-file evidence uses an explicit whole-file time basis and never invents +timestamps. Always run bundle validation after creation. A missing image, video, or 3D-asset manifest must fail closed. Genericized or duplicate genres, periodic schedules, diff --git a/tests/ci/tasteforge-video-skill.test.js b/tests/ci/tasteforge-video-skill.test.js index 1155e358a..b758f545e 100644 --- a/tests/ci/tasteforge-video-skill.test.js +++ b/tests/ci/tasteforge-video-skill.test.js @@ -20,6 +20,37 @@ function readJson(relativePath) { return JSON.parse(read(relativePath)); } +function assertExactDryRunBoundary(payload, label) { + assert.strictEqual(payload.provider_calls, 0, `${label} must require provider_calls:0`); + assert.strictEqual(payload.provider_execution, false, `${label} must require provider_execution:false`); + assert.strictEqual(payload.dry_run, true, `${label} must require dry_run:true`); + assert.strictEqual(payload.submit, false, `${label} must require submit:false`); +} + +function validateRejectedContractFixture(fixture) { + if (fixture.kind === "manifest") { + assertExactDryRunBoundary(fixture.payload, "manifest"); + for (const request of fixture.payload.requests || []) { + assertExactDryRunBoundary(request, "request"); + assert.strictEqual(request.provider_call_mode, "disabled"); + } + return; + } + if (fixture.kind === "effect_recipe") { + for (const event of fixture.payload.events || []) { + if (event.requires_subject_anchor) { + assert.strictEqual( + event.subject_anchor?.lost_policy, + "disable_effect_until_track_recovers", + "anchored CV effects must disable_effect_until_track_recovers" + ); + } + } + return; + } + assert.fail(`unknown fixture kind: ${fixture.kind}`); +} + const tests = []; function test(name, fn) { tests.push([name, fn]); } @@ -37,6 +68,15 @@ test("has valid discoverable frontmatter and trigger phrases", () => { /export EDL\/FCPXML|export .*EDL.*FCPXML/i, /audit .*generated-media provenance|provenance audit/i, ]) assert.match(skill, trigger); + + const description = skill.match(/^description: ([^\n]+)$/m)?.[1] || ""; + for (const discoveryTerm of ["multimodal", "image", "video", "3D", "file-driven", "distill", "apply"]) { + assert.match(description, new RegExp(discoveryTerm, "i"), `frontmatter misses ${discoveryTerm}`); + } + const triggers = skill.match(/## When to Use\n([\s\S]*?)\n## /)?.[1] || ""; + for (const discoveryTerm of ["multimodal", "image", "video", "3D", "file-driven", "distill", "apply"]) { + assert.match(triggers, new RegExp(discoveryTerm, "i"), `triggers miss ${discoveryTerm}`); + } }); test("distinguishes local deterministic operations from provider generation", () => { @@ -97,11 +137,29 @@ test("defines the fail-closed file-driven multimodal contract", () => { /genre.*modality/is, /exact reference\/time provenance/i, /provider_calls:\s*0/i, + /dry_run:\s*true/i, + /submit:\s*false/i, + /disable_effect_until_track_recovers/i, ]) assert.match(skill, phrase); assert.match(skill, /missing.*manifest.*fail closed/is); assert.match(skill, /tamper.*fail closed/is); }); +test("executable fixtures reject dry_run:false and continue_without_anchor", () => { + for (const fixtureName of [ + "reject-dry-run-false.json", + "reject-continue-without-anchor.json", + ]) { + const fixture = readJson(`tests/fixtures/tasteforge-video/${fixtureName}`); + assert.strictEqual(fixture.expected, "reject"); + assert.throws( + () => validateRejectedContractFixture(fixture), + undefined, + `${fixtureName} was not rejected` + ); + } +}); + test("ships through the opt-in media-generation install module and npm package", () => { const modules = readJson("manifests/install-modules.json").modules; const module = modules.find((candidate) => candidate.id === "media-generation"); @@ -166,8 +224,10 @@ test("passes the curated skill validator", () => { // ECC_TEST_NPM_PACK=1 (release/CI verification); the default suite relies on // the files-array assertions above. test("ships inside the real npm tarball (opt-in)", () => { - if (process.env.ECC_TEST_NPM_PACK !== "1") return; - const result = spawnSync("npm", ["pack", "--dry-run"], { + if (process.env.ECC_TEST_NPM_PACK !== "1") { + return { skipped: "set ECC_TEST_NPM_PACK=1 to run real npm pack inclusion" }; + } + const result = spawnSync("npm", ["pack", "--dry-run", "--ignore-scripts"], { cwd: REPO_ROOT, encoding: "utf8", }); @@ -180,10 +240,16 @@ test("ships inside the real npm tarball (opt-in)", () => { }); let failed = 0; +let skipped = 0; console.log("\n=== Testing TasteForge video skill ===\n"); for (const [name, fn] of tests) { try { - fn(); + const result = fn(); + if (result?.skipped) { + skipped += 1; + console.log(` - SKIP ${name}: ${result.skipped}`); + continue; + } console.log(` ✓ ${name}`); } catch (error) { failed += 1; @@ -192,4 +258,4 @@ for (const [name, fn] of tests) { } } if (failed) process.exit(1); -console.log(`\n${tests.length - failed}/${tests.length} passed`); +console.log(`\n${tests.length - failed - skipped}/${tests.length} passed, ${skipped} skipped`); diff --git a/tests/fixtures/tasteforge-video/reject-continue-without-anchor.json b/tests/fixtures/tasteforge-video/reject-continue-without-anchor.json new file mode 100644 index 000000000..303d373c2 --- /dev/null +++ b/tests/fixtures/tasteforge-video/reject-continue-without-anchor.json @@ -0,0 +1,19 @@ +{ + "kind": "effect_recipe", + "expected": "reject", + "payload": { + "events": [ + { + "effect": "cv_subject_glitch", + "requires_subject_anchor": true, + "subject_anchor": { + "mode": "object_track", + "target": "primary_subject", + "source_ref_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "evidence_time": 0, + "lost_policy": "continue_without_anchor" + } + } + ] + } +} diff --git a/tests/fixtures/tasteforge-video/reject-dry-run-false.json b/tests/fixtures/tasteforge-video/reject-dry-run-false.json new file mode 100644 index 000000000..0c91bca27 --- /dev/null +++ b/tests/fixtures/tasteforge-video/reject-dry-run-false.json @@ -0,0 +1,20 @@ +{ + "kind": "manifest", + "expected": "reject", + "payload": { + "modality": "video", + "provider_calls": 0, + "provider_execution": false, + "dry_run": true, + "submit": false, + "requests": [ + { + "provider_calls": 0, + "provider_execution": false, + "dry_run": false, + "submit": false, + "provider_call_mode": "disabled" + } + ] + } +}