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
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
diagnoseDraftStore,
discoverDraftStore,
editorProcesses,
NESTED_TIMELINES_MODERN_ACTION,
nestedTimelinesAction,
nestedTimelinesWriteWarning,
planTimelineSync,
Expand Down Expand Up @@ -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)"}`);
Expand Down
21 changes: 21 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <project> --out <dir>`.";

/**
* 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 <project> --out <dir>`.";

/**
* First app version reported to regenerate the nested mirror FROM the root file
* rather than the other way round (issue #68).
Expand Down Expand Up @@ -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.");
Expand Down
83 changes: 79 additions & 4 deletions test/timelines-layout.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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",
);
});

Expand Down Expand Up @@ -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}`);
}
});
});