From b823dddf543fb6d41d69a9b842bd0944359fd79e Mon Sep 17 00:00:00 2001 From: Rene Zander Date: Sun, 23 Aug 2026 12:13:11 +0000 Subject: [PATCH] fix(store): name the nested Timelines/ layout on >= 8.7 storage (#95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit diagnose already attached the redacted nested_evidence block and fixture already bundled Timelines/, both keyed off nestedTimelines.length. The next_action and the version support note were keyed off the layout value, which stays content-/info-primary above 8.7 by design — so a modern store got a report carrying nested evidence with no prose explaining why. Adds NESTED_TIMELINES_MODERN_ACTION, emitted when nested documents exist but the layout is not timelines-nested. It asserts nothing: #50's 7.x discard risk and #68's 8.5.0 survival observation both predate the 8.7 storage change and neither transfers across it. Write-time stays silent. CapCut creates Timelines/ routinely on modern builds (#60), so a per-write warning would fire for the majority on a hazard nobody has evidenced. --- src/index.ts | 4 ++ src/store.ts | 21 +++++++++ test/timelines-layout.test.mjs | 83 ++++++++++++++++++++++++++++++++-- 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index da6169b..1f3a5fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,6 +63,7 @@ import { diagnoseDraftStore, discoverDraftStore, editorProcesses, + NESTED_TIMELINES_MODERN_ACTION, nestedTimelinesAction, nestedTimelinesWriteWarning, planTimelineSync, @@ -3390,7 +3391,10 @@ function cmdVersion(draft: Draft, filePath: string, flags: Flags): void { // CapCut 7.x nested Timelines/ layout (issue #50): `version` answers "will a // write be honored?", and on this layout a root-mirror write may be // discarded by the app — name the layout alongside the write-guard notes. + // On >= 8.7 storage the layout value stays content-/info-primary by design, + // so the same question needs the claim-free note instead of silence. if (store.layout === "timelines-nested") v.support.notes.push(nestedTimelinesAction(store.version)); + else if (store.nestedTimelines.length > 0) v.support.notes.push(NESTED_TIMELINES_MODERN_ACTION); if (flags.human) { console.log(`App: ${v.app}${v.app_source !== "unknown" ? ` (${v.app_source})` : ""}`); console.log(`Version: ${v.app_version ?? "(unknown)"}`); diff --git a/src/store.ts b/src/store.ts index 128380f..8cb6198 100644 --- a/src/store.ts +++ b/src/store.ts @@ -271,6 +271,26 @@ export const NESTED_TIMELINES_ACTION = "Evidence for this layout is report-only — if you have such a project, contribute a bundle: " + "`capcut fixture --out `."; +/** + * The same structure on >= 8.7 storage, where `layout` deliberately stays at its + * content-/info-primary value so the 7.x claim never relabels a modern store. + * + * `diagnose` already attaches the redacted nested evidence on these stores + * (index.ts `cmdDiagnose`, gated on `nested_timelines.length`), and `fixture` + * already bundles it, but the human-readable action and the `version` note were + * gated on the layout value alone — so a >= 8.7 user got a report carrying + * Timelines/ evidence with no line of text saying why it was collected. This is + * that line, and it asserts nothing: neither #50's 7.x discard risk nor #68's + * 8.5.0 survival observation transfers across the 8.7 storage change. + */ +export const NESTED_TIMELINES_MODERN_ACTION = + "Timelines/ directory with a nested timeline document, on CapCut >= 8.7 storage. No discard risk is claimed " + + "here and none is ruled out: the 7.x report in issue #50 and the 8.5.0 open/close round trip in issue #68 both " + + "predate this storage generation, so what the app does with the nested document on >= 8.7 is unevidenced in " + + "either direction. Edit commands read and write the project-root files only. If this project opens in your app " + + "with a CLI edit intact — or without it — that is the artifact issue #50 has been blocked on: " + + "`capcut fixture --out `."; + /** * First app version reported to regenerate the nested mirror FROM the root file * rather than the other way round (issue #68). @@ -871,6 +891,7 @@ export function diagnoseDraftStore(input: string): DraftStoreReport { ); } if (store.layout === "timelines-nested") actions.push(nestedTimelinesAction(store.version)); + else if (store.nestedTimelines.length > 0) actions.push(NESTED_TIMELINES_MODERN_ACTION); if (running.length > 0) actions.push(`Close ${running.join(" / ")} before editing this managed draft.`); if (actions.length === 0) actions.push("Storage targets are readable and agree. A normal CLI write will synchronize them."); diff --git a/test/timelines-layout.test.mjs b/test/timelines-layout.test.mjs index b07e2ba..a2ec9e3 100644 --- a/test/timelines-layout.test.mjs +++ b/test/timelines-layout.test.mjs @@ -197,7 +197,7 @@ describe("CapCut 7.x nested Timelines/ layout (issue #50, detection-only guard)" assert.equal(readFileSync(f.rootPath, "utf-8"), rootBefore, "an explicit nested edit must not touch the root file"); }); - it("CapCut >= 8.7 stores keep their layout value and stay silent", () => { + it("CapCut >= 8.7 stores keep their layout value and write silently", () => { const f = nestedProject({ appVersion: "8.7.0", rootName: "draft_content.json" }); after(f.cleanup); @@ -206,13 +206,16 @@ describe("CapCut 7.x nested Timelines/ layout (issue #50, detection-only guard)" assert.equal(store.layout, "content-primary", "the 7.x claim must not relabel >= 8.7 storage"); assert.deepEqual(store.nestedTimelines, ["Timelines/project.json", `Timelines/${TIMELINE_ID}/draft_info.json`]); + // Writes stay silent: CapCut creates Timelines/ routinely on modern builds + // (issue #60), so a per-write warning would fire for the majority on a + // hazard nobody has evidenced. `diagnose` is where the note belongs. const r = spawnCli(["set-text", f.dir, "seg-1", "modern edit"]); assert.equal(r.status, 0, `stderr: ${r.stderr}`); - assert.ok(!/Timelines/.test(r.stderr), "no nested-layout warning on >= 8.7 storage"); + assert.ok(!/Timelines/.test(r.stderr), "no nested-layout warning on >= 8.7 writes"); const report = diagnoseDraftStore(f.dir); assert.ok( - !report.next_actions.some((a) => /discard/.test(a) && /Timelines\//.test(a)), - "diagnose must not carry the 7.x discard action on >= 8.7 storage", + !report.next_actions.some((a) => /may discard those edits/.test(a)), + "diagnose must not carry the 7.x discard claim on >= 8.7 storage", ); }); @@ -307,3 +310,75 @@ describe("nested Timelines/ guidance is version-gated (issue #68)", () => { assert.doesNotMatch(r.stderr, /may\s+be discarded the next time/, "8.5.0 must not get the 7.x risk claim"); }); }); + +// A >= 8.7 store keeps its content-/info-primary layout value by design, so both +// the layout-gated action and the layout-gated `version` note fell away — while +// `diagnose` still attached the redacted nested evidence and `fixture` still +// bundled it, both keyed off nested_timelines.length rather than the layout. +// The report was carrying Timelines/ evidence with no line of text saying why. +describe("nested Timelines/ on >= 8.7 storage is named, not asserted", () => { + it("diagnose names the layout claim-free, with the fixture CTA", () => { + const f = nestedProject({ appVersion: "9.2.8" }); + after(f.cleanup); + + const store = discoverDraftStore(f.dir); + assert.equal(store.layout, "info-primary", "the layout value stays as it was"); + assert.equal(store.modernStorage, true); + + const action = diagnoseDraftStore(f.dir).next_actions.find((a) => /Timelines\//.test(a)); + assert.ok(action, ">= 8.7 nested stores must no longer be silent"); + assert.match(action, />= 8\.7 storage/); + assert.match(action, /capcut fixture/); + assert.match(action, /issue #50/); + // Claim-free in both directions: neither report transfers across 8.7. + assert.doesNotMatch(action, /may discard those edits/, "the 7.x risk must not be asserted"); + assert.doesNotMatch(action, /should survive the next open/, "the 8.5.0 survival claim must not be asserted"); + }); + + it("version carries the same note alongside the write-guard notes", () => { + const f = nestedProject({ appVersion: "9.2.8" }); + after(f.cleanup); + + const r = spawnCli(["version", f.dir]); + assert.equal(r.status, 0, `stderr: ${r.stderr}`); + const note = r.json.support.notes.find((n) => /Timelines\//.test(n)); + assert.ok(note, `support.notes must name the layout; got: ${JSON.stringify(r.json.support.notes)}`); + assert.match(note, />= 8\.7 storage/); + }); + + it("the JSON report's nested evidence and the prose note now agree", () => { + const f = nestedProject({ appVersion: "9.2.8" }); + after(f.cleanup); + + const r = spawnCli(["diagnose", f.dir]); + assert.equal(r.status, 0, `stderr: ${r.stderr}`); + assert.deepEqual(r.json.nested_timelines, ["Timelines/project.json", `Timelines/${TIMELINE_ID}/draft_info.json`]); + assert.ok(r.json.nested_evidence, "evidence was already attached on >= 8.7 — that is the half that worked"); + assert.ok( + r.json.next_actions.some((a) => /Timelines\//.test(a)), + "and now a next_action explains why the evidence is there", + ); + }); + + it("a >= 8.7 store with no Timelines/ directory stays completely silent", () => { + const f = nestedProject({ appVersion: "9.2.8", omitTimelines: true }); + after(f.cleanup); + + const report = diagnoseDraftStore(f.dir); + assert.deepEqual(report.nested_timelines, []); + assert.ok(!report.next_actions.some((a) => /Timelines\//.test(a)), "no note without the structure"); + + const r = spawnCli(["version", f.dir]); + assert.equal(r.status, 0, `stderr: ${r.stderr}`); + assert.ok(!r.json.support.notes.some((n) => /Timelines\//.test(n))); + }); + + it("never emits both the layout action and the >= 8.7 note", () => { + for (const appVersion of ["7.9.0", "8.5.0", "8.7.0", "9.2.8"]) { + const f = nestedProject({ appVersion }); + after(f.cleanup); + const hits = diagnoseDraftStore(f.dir).next_actions.filter((a) => /Timelines\//.test(a)); + assert.equal(hits.length, 1, `${appVersion} must produce exactly one nested note, got ${hits.length}`); + } + }); +});