ossclip 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/produce.ts CHANGED
@@ -15,7 +15,6 @@ import {
15
15
  applyOverrides,
16
16
  applyRepairs,
17
17
  assembleScenes,
18
- applyCaptionEdits,
19
18
  buildCaptionLines,
20
19
  buildCutlist,
21
20
  buildZoomPlan,
@@ -105,6 +104,8 @@ import {
105
104
  workdirBaseName,
106
105
  } from "./stranded-overrides";
107
106
  import { editHint } from "./interactive/edit-hint";
107
+ import { reconcileCaptionEdits } from "./caption-report";
108
+ import { overridesWriteLine, writeOverrideDoc } from "./overrides-write";
108
109
  import { recordedProduceArgs } from "./replay-argv";
109
110
  import { renderCover, renderProduction } from "@ossclip/renderer";
110
111
  import {
@@ -1595,7 +1596,8 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
1595
1596
  clipStarts: map.spans.map((s) => s.outIn),
1596
1597
  });
1597
1598
  // User splits (R16 §61) — after the fill so takes split like scenes, and
1598
- // before the final override pass so edits on the `id@ms` halves land. A
1599
+ // before the final override pass so edits on the `id@<split id>` halves land
1600
+ // (the suffix is the split's own minted id, §137, not its time). A
1599
1601
  // split whose ROOT was a graphic scene already happened once inside
1600
1602
  // `splitThenDropHidden` above (PLAN 2026-08-04 Task 1) — re-running it here
1601
1603
  // is a no-op for that scene (the split point sits exactly on the joint
@@ -1607,7 +1609,7 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
1607
1609
  }
1608
1610
  const { cues: mergedCues, orphans: rawOrphans } = applyOverrides(split, overrideDoc);
1609
1611
  // Halves of a TAKE the user deleted after splitting: a take id only exists
1610
- // once the fill above runs, so its `id@ms` half couldn't have been seen by
1612
+ // once the fill above runs, so its `id@<split id>` half couldn't have been seen by
1611
1613
  // `splitThenDropHidden` earlier (that pass only ever saw graphic scenes).
1612
1614
  // Scene halves were already caught above; this is a no-op for them. Same
1613
1615
  // order as the editor's live memo.
@@ -1931,22 +1933,36 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
1931
1933
  // caption output for zero visual reason (PLAN Task A4.4).
1932
1934
  breakpoints: graphicCues.flatMap((c) => [c.startSec, c.endSec]),
1933
1935
  });
1934
- // The user's retyped caption words (editor, PLAN 2026-07-29 Task 7 scope
1935
- // (a)). Guarded per word: a stale edit — the pipeline re-derived a
1936
- // different word at that position — is dropped LOUDLY, never applied to
1937
- // the wrong word and never silently forgotten.
1938
- const { lines: captionLines, dropped: staleCaptionEdits } = applyCaptionEdits(
1939
- baseCaptionLines,
1940
- overrideDoc.captions,
1941
- );
1942
- const liveCaptionEdits = Object.keys(overrideDoc.captions).length - staleCaptionEdits.length;
1943
- if (liveCaptionEdits > 0) console.log(`▸ ${liveCaptionEdits} caption word(s) retyped by the editor`);
1944
- for (const d of staleCaptionEdits) {
1945
- console.log(
1946
- ` ⚠ caption edit at word ${d.index} dropped: expected "${d.expected}" there, ` +
1947
- `the transcript now has "${d.found}"`,
1948
- );
1949
- }
1936
+ // §137 (Task 6 review, Critical 1): the caption half of a run — migrate the
1937
+ // doc's keys, apply what applies, and account for the rest — is one pure
1938
+ // pass in `caption-report.ts`, and this is its I/O.
1939
+ //
1940
+ // MIGRATION RUNS HERE, not only in the editor. The editor's is in memory and
1941
+ // `edits.load` leaves the doc CLEAN, so `onRender` (which saves only when
1942
+ // dirty) sends a user who opened an old project, saw every retype come back
1943
+ // on screen, and clicked Render straight into this function with the
1944
+ // untouched legacy doc — and `applyCaptionEdits` matched nothing, so the
1945
+ // render shipped without a single retype after showing a state strictly more
1946
+ // convincing than the truth. None of the reasoning that keeps this call out
1947
+ // of `edit.ts` applies: `buildCaptionLines` just stamped a real `srcStart`
1948
+ // on every word above (`captions.ts:128`), so there is nothing to backfill
1949
+ // and no repair rule to duplicate.
1950
+ //
1951
+ // THE MIGRATED DOC IS WRITTEN BACK — the decision, stated: through the one
1952
+ // sanctioned `overrides.json` write further down, with its `.bak` and atomic
1953
+ // rename, never a second write here. A legacy key's resolvability DECAYS (it
1954
+ // is found by the word it names, so the next re-plan that rewrites that word
1955
+ // loses it for good), and this is the only durable repair in the product —
1956
+ // the editor's evaporates, as Critical 1 showed. The edits it could NOT
1957
+ // place stay in the doc regardless (`captionEditsToKeep`): they are printed
1958
+ // by name below, and a run that cannot anchor one today is not permission to
1959
+ // delete it — the next run, against a different cut, may place it (final
1960
+ // review, Critical 1).
1961
+ const captionWork = reconcileCaptionEdits(overrideDoc, baseCaptionLines);
1962
+ overrideDoc = captionWork.doc;
1963
+ const captionLines = captionWork.lines;
1964
+ const captionKeysReanchored = captionWork.reanchored;
1965
+ for (const line of captionWork.log) console.log(line);
1950
1966
 
1951
1967
  // Micro zoom punches (FINDINGS §15) reversing at real phrase breaks (§18).
1952
1968
  // Breaths are source-time; TimeMap has no span mapper, so both ends go
@@ -2185,23 +2201,32 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
2185
2201
  // next run's `priorMap` then sees drift and re-anchors again, the same
2186
2202
  // recovery path finding 3 already has to support — never a false "nothing
2187
2203
  // changed" that quietly corrupts positions.
2188
- if (cutResult.changed) {
2189
- // Keep a `.bak` of whatever was on disk first — the same safety net
2190
- // `saveConfigPatch` keeps for a config file it's about to replace —
2191
- // before overwriting the user's own data. Atomic write via tmp+rename,
2192
- // matching the edit server's own `PUT /overrides` handler: the producer
2193
- // or a live editor session may read this file at any moment, and a
2194
- // half-written document would be worse than a stale one.
2195
- try {
2196
- const raw = await readFile(overridesPath, "utf8");
2197
- await writeFile(`${overridesPath}.bak`, raw);
2198
- } catch {
2199
- // Nothing on disk to back up (first cut ever applied here) — fine.
2200
- }
2201
- const tmp = `${overridesPath}.tmp`;
2202
- await writeFile(tmp, JSON.stringify(overrideDoc, null, 2));
2203
- await rename(tmp, overridesPath);
2204
- console.log("▸ overrides.json re-anchored to the new cut and saved (previous copy kept as .bak)");
2204
+ // `cutResult.changed` OR a §137 caption-key migration that actually MOVED an
2205
+ // edit — the second is why this is no longer a bare cut check. Both are
2206
+ // re-anchorings of the user's doc to something the pipeline just recomputed,
2207
+ // and both go through the one sanctioned write; a separate write for the
2208
+ // migration would be a SECOND sanctioned write, which the comment above
2209
+ // exists to prevent.
2210
+ //
2211
+ // "ACTUALLY MOVED" is load-bearing and was not there at first (final review,
2212
+ // Critical 2). Gated on "the migration reported something" instead, this
2213
+ // fires on a run that repaired NOTHING — and since a caption migration is
2214
+ // independent of the cut, it fires on runs where the pre-§137 gate wrote
2215
+ // nothing at all.
2216
+ //
2217
+ // THEY DO NOT SHARE THE `.bak`, THOUGH, and that is the rest of the same
2218
+ // finding (final review round 2). `refreshBackup: cutResult.changed`, never
2219
+ // the gate: on the field workdir the caption migration re-anchors THREE
2220
+ // edits, so the gate legitimately fires while `cutResult.changed` is false —
2221
+ // and an unconditional refresh would then copy the already-damaged
2222
+ // `overrides.json` over `overrides.json.bak`, which is that user's only
2223
+ // pre-cut save and the only artefact their deleted split half can still be
2224
+ // recovered from (`legacySplitId`). Repairing the captions would destroy the
2225
+ // evidence for the split. `writeOverrideDoc` carries the full argument for
2226
+ // why a caption-only write has nothing worth backing up.
2227
+ if (cutResult.changed || captionKeysReanchored) {
2228
+ await writeOverrideDoc(overridesPath, overrideDoc, { refreshBackup: cutResult.changed });
2229
+ console.log(overridesWriteLine(cutResult.changed));
2205
2230
  }
2206
2231
 
2207
2232
  if (!opts.render) {
package/src/program.ts CHANGED
@@ -7,6 +7,21 @@ import { CleanupLevelSchema, SceneComponentIdSchema } from "@ossclip/core";
7
7
  import { STUDIO_ENTRY } from "@ossclip/renderer";
8
8
  import { loadEnvFiles } from "./env";
9
9
  import { produce } from "./produce";
10
+ // The one interactive import that is STATIC rather than `await import()`: the
11
+ // `resetInputSource()` run boundary in `buildProgram` has to run synchronously
12
+ // while the program is being built, and `buildProgram` cannot await. The graph
13
+ // already loads @ossclip/renderer and @ossclip/scenes eagerly through
14
+ // produce.ts, so clack riding along costs ~14ms on a ~320ms startup — cheap
15
+ // enough not to trade for a racy fire-and-forget import (§136).
16
+ //
17
+ // Consequence worth stating for whoever edits ask-input.ts next: this module,
18
+ // and everything it imports (picker.ts, prompts.ts → @clack/prompts,
19
+ // suggest-inputs.ts), is now EAGER on every ossclip invocation — `--version`
20
+ // and `doctor` included. A heavy dependency added there is no longer free.
21
+ // Removing this line to "restore laziness" deletes the run boundary with it:
22
+ // `input_source` would then report the PREVIOUS run's branch, and the only
23
+ // test that notices is the buildProgram case in telemetry.test.ts.
24
+ import { inputSourceUsed, resetInputSource } from "./interactive/ask-input";
10
25
  import { setReplayArgv } from "./replay-argv";
11
26
  import {
12
27
  bootstrapTelemetry,
@@ -38,6 +53,19 @@ const envFiles = loadEnvFiles();
38
53
  export function buildProgram(): Command {
39
54
  const program = new Command();
40
55
 
56
+ // §136: the input-source telemetry is module state, so something has to say
57
+ // when a RUN begins. Not the produce action — every wizard route re-enters
58
+ // that same action through `program.parseAsync` (§129), so a reset there
59
+ // fires on the re-entered parse, AFTER `askInput` recorded the branch, and
60
+ // `input_source` would read "argv" for every wizard run: the feature would
61
+ // measure exactly nothing. Nor is the process the boundary — a batch or REPL
62
+ // driver runs produce more than once, and the second run would report the
63
+ // first one's branch. The PROGRAM CONSTRUCTION is the boundary: commander 12
64
+ // keeps option state across parseAsync calls (see the bare-`produce` refusal
65
+ // below), so any batch has to rebuild the program per run anyway, which makes
66
+ // one reset per `buildProgram` exactly one per run in both cases.
67
+ resetInputSource();
68
+
41
69
  // Before dispatch, so the one-time first-run notice precedes any command's
42
70
  // own output. Inert in this repo's tests by construction: while POSTHOG_KEY
43
71
  // is the placeholder, bootstrap touches no disk and sends nothing (FINDINGS
@@ -433,6 +461,10 @@ export function buildProgram(): Command {
433
461
  render: opts.render !== false,
434
462
  source_duration_bucket: durationBucket(result.sourceDurationSec),
435
463
  scenes: result.sceneCount,
464
+ // Which branch of the input prompt was used — a branch name, never
465
+ // the path itself (§136). The picker exists because typing a path
466
+ // blocked non-technical users; this is how we find out if it helped.
467
+ input_source: inputSourceUsed(),
436
468
  });
437
469
  if (!telemetry.disabled) {
438
470
  telemetry.state.produceCount += 1;
@@ -1 +0,0 @@
1
- .ossclip-scroll-list{scrollbar-width:thin;scrollbar-color:#3a3a44 transparent}.ossclip-scroll-list::-webkit-scrollbar{width:8px}.ossclip-scroll-list::-webkit-scrollbar-track{background:transparent}.ossclip-scroll-list::-webkit-scrollbar-thumb{background:#3a3a44;border-radius:4px}