Skip to content

fix: generation-record authority as optional-workspace projection with CAS - #142

Merged
IAnMove merged 2 commits into
mainfrom
fix/generation-record-authority
Sep 5, 2026
Merged

fix: generation-record authority as optional-workspace projection with CAS#142
IAnMove merged 2 commits into
mainfrom
fix/generation-record-authority

Conversation

@IAnMove

@IAnMove IAnMove commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Problem

Generation-record v1 (#138) still treated workspace_id as a required collection (and could copy output_folder into it), merged empty lineage lists as wipes, treated a re-read of running as a live worker, and persisted with only an in-process RLock + atomic rename. That is not enough for two writers, and it is not a projection contract.

Final behavior

  • Authority is projection (AUTHORITY = "projection"). Bytes stay in asset-manifest v1; this JSON is a durable attempt projection with CAS, not a second catalog or scheduler.
  • Identity graph: command → workflow? → run? → generation attempt → task? → asset* (run_id is a correlation, not a new Run store).
  • workspace_id is an optional collection (string | null). output_folder is the physical folder. Unscoped records are not members of a collection named like the folder.
  • merge_generation_record / mergeGenerationRecord: empty or missing lineage lists do not wipe existing parents/derivatives/transformations. Identity cannot change.
  • to_asset_manifest_patch omits empty lineage arrays and preserves prompt_original / prompt_effective plus queue_ms / inference_ms / total_ms.
  • resume of queued/running without worker_alive=True sets reconciliation.needed + interrupted and never invents completed.
  • Persist increments revision and rejects stale expected revisions. Load does not mutate the file.

Scope

  • app/services/generation_record.py (contract)
  • app/services/generation_record_io.py (CAS persist, merge, resume, store)
  • ui/src/lib/generationRecord.ts + tests
  • schema, docs/development/GENERATION_RECORD.md, fase3.md

Not in this PR: _launch_runtime.py, useStore.ts, agentActions.ts, StoryLabPanel, producer wiring, file moves (F3.9).

Tests

  • pytest tests/test_generation_record.py — 18 passed (merge, CAS two writers, unscoped store, original/effective prompts, resume interrupted)
  • pytest tests/test_architecture_contracts.py
  • TS generationRecord.test.ts — 10 passed
  • bash scripts/validate_local.sh — OK (E2E 7/7)
  • bash scripts/check_code_health_pr_base.sh vs d4263ce6 — passed, +38 hotspot lines (budget 75), score 49.8 +0.0

Risks / limits

  • Unscoped files live under _physical/<output_folder>/ in the JSON store. That bucket name is a store detail, not a workspace id.
  • Resume marks reconciliation in memory; callers still have to persist it. No producer is wired.
  • Real media generation was not run.

Base

origin/main d4263ce6. Independent of #141 (lyrics). Do not merge automatically. Fase 4 waits for this merge.


Note

Medium Risk
Contract and persistence semantics change (nullable workspace, CAS revisions, merge rules); impact is limited today because only tests call the store, but future writers must adopt the new revision and resume behavior.

Overview
This PR reframes generation-record v1 as a projection over asset-manifest (AUTHORITY = "projection"), splits contract (generation_record.py) from CAS persistence (generation_record_io.py), and tightens the schema so writers cannot treat the JSON store as a second catalog.

Workspace & location: workspace_id is now optional (null = unscoped); output_folder is required and is never copied into workspace_id. Unscoped records persist under _physical/<output_folder>/, and load/list enforce exact collection membership (including null).

Richer metadata & patches: Records carry separate prompt_original / prompt_effective, lineage.transformations, timestamps.queue_ms / inference_ms, revision, and reconciliation. Manifest projection and to_asset_manifest_patch round-trip these fields and omit empty lineage (and origin.workspace_id when unscoped) so patches do not wipe sidecar lineage.

Concurrency & recovery: persist_generation_record increments revision and rejects stale writers; merge_generation_record unions non-empty lineage without wiping on empty lists. resume (default worker_alive=False) sets reconciliation.needed + interrupted for queued/running without inventing completed.

Python, JSON schema, GENERATION_RECORD.md, and ui/src/lib/generationRecord.ts (+ tests) stay aligned. No launch/runtime producer wiring in this PR.

Reviewed by Cursor Bugbot for commit a6adbbe. Configure here.

…h CAS

Keep GenerationRecord as a projection, not a second store. workspace_id is
an optional collection (never copied from output_folder). Merge does not
wipe lineage with empty lists. Resume of running/queued marks interrupted
reconciliation instead of inventing success. Persist rejects stale revisions.
@IAnMove

IAnMove commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

cursor review

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

PR Review — Loreframe Studio

Risk: low
Scope: 8 file(s); +952/-241; React UI, backend services, docs

Automated review from scripts/analyze_pr.py. This is a heuristic pass (no LLM) so humans still own the merge decision.

Findings

  • low — UI changed — rebuild before merge
    Run cd ui && npm run build (CI already does this). Pinokio Update rebuilds for end users; keep ui/dist untracked.

Changed files

  • added: app/services/generation_record_io.py
  • modified: app/services/generation_record.py, docs/development/GENERATION_RECORD.md, docs/development/generation-record-v1.schema.json, fase3.md, tests/test_generation_record.py, ui/src/lib/generationRecord.ts, ui/tests/generationRecord.test.ts

CONTRIBUTING checklist

  • python scripts/verify_clean_repo.py
  • python -m compileall -q app/services app/launch.py scripts
  • cd ui && npm run build if the UI changed
  • No weights, CivitAI sidecars, or generated guides
  • Stays local-first (no required accounts / telemetry)

Posted by the repo PR review workflow. Re-runs on each push to the PR.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

Code health

Quality score: 49.8/100

Higher is better. The score is a trend dashboard; the independent ratchet below remains the CI gate.

Component Weight Current Change
Cyclomatic health 45% 52.5 -0.1
File concentration 25% 55.4 +0.2
Oversized-file debt 20% 30.7 +0.2
Modularity 10% 62.3 +0.1

Change vs PR base: +0.0 points.

Metric Value
Production LOC 245,101
Production files 540
Test LOC 72,047
Functions measured 15,323
Functions complexity ≥ 15 792
Maximum complexity 667

Markdown, JSON catalogs and tests are out of this table. Only app/ runtime + ui/src TS/JS count.

Most complex functions

Complexity Where
667 app/wgp.py:7164 generate_video
374 ui/src/stores/useStore.ts:4024 Async method 'startGeneration'
355 app/_launch_runtime.py:23508 _run_generation
308 app/wgp.py:12281 generate_video_tab
271 ui/src/components/Sidebar/SceneAnimatorPanel.tsx:474 Function 'SceneAnimatorPanel'
266 ui/src/stores/useStore.ts:8566 Async method 'loadSettingsFromOutput'
258 app/services/director/planners/short_film.py:3433 ShortFilmPlanner._plan_story_driven
248 app/services/director_pipeline.py:13735 _run_video_generation
245 app/services/director_pipeline.py:7860 _run_pipeline
243 ui/src/features/agent/agentActions.ts:1128 Function 'parseAction'
226 app/services/director_pipeline.py:6689 update_comic_preview
225 ui/src/features/agent/agentActions.ts:2796 Async function 'executeAgentActions'

Trend vs baseline

Metric Δ
Production LOC +541
Test LOC +206
Functions ≥ 15 +4
Maximum complexity +0

Warnings

  • production LOC increased by +541
  • functions at complexity >= 15 increased by +4
  • hotspot app/services/generation_record.py increased by +39 lines

Ratchet passed.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Patch drops transformation artifact ids
    • to_asset_manifest_patch and toAssetManifestPatch now map transformations through the same artifact-ref helper as parents, so write-back keeps id instead of asset_id.
  • ✅ Fixed: Duration fallback uses inference span
    • _timestamps_block now derives missing duration_ms from created_at to completed_at, matching _stamp_transition and asset-manifest total_ms wall time.
  • ✅ Fixed: TS merge desyncs prompt fields
    • mergeGenerationRecord now promotes prompt_full into prompt_effective and rebuilds prompt_display, matching Python merge_generation_record.

Create PR

Or push these changes by commenting:

@cursor push 0044c4dca3
Preview (0044c4dca3)
diff --git a/app/services/generation_record.py b/app/services/generation_record.py
--- a/app/services/generation_record.py
+++ b/app/services/generation_record.py
@@ -380,7 +380,7 @@
     queue_ms = _optional_ms(raw.get("queue_ms"))
     inference_ms = _optional_ms(raw.get("inference_ms"))
     if duration is None:
-        duration = _milliseconds(started_at or created_at, completed_at)
+        duration = _milliseconds(created_at, completed_at)
     if queue_ms is None:
         queue_ms = _milliseconds(queued_at or created_at, started_at)
     if inference_ms is None:
@@ -927,8 +927,11 @@
     if parents:
         lineage_patch["parents"] = parents
     transformations = [
-        dict(item) for item in value["lineage"].get("transformations") or []
-        if isinstance(item, Mapping) and item
+        item for item in (
+            _artifact_parent(raw)
+            for raw in value["lineage"].get("transformations") or []
+            if isinstance(raw, Mapping)
+        ) if item
     ]
     if transformations:
         lineage_patch["transformations"] = transformations

diff --git a/ui/src/lib/generationRecord.ts b/ui/src/lib/generationRecord.ts
--- a/ui/src/lib/generationRecord.ts
+++ b/ui/src/lib/generationRecord.ts
@@ -281,6 +281,11 @@
   return value.map(lineageRef).filter((item): item is GenerationLineageRef => item != null)
 }
 
+function artifactRef(item: GenerationLineageRef): { id: string; kind: string; uri?: string } | null {
+  if (!item.asset_id) return null
+  return { id: item.asset_id, kind: item.kind || 'other', ...(item.uri ? { uri: item.uri } : {}) }
+}
+
 function manifestPromptPair(prompts: JsonMap): { original: string; effective: string } {
   const original = firstText(prompts.original) || ''
   const effective = firstText(prompts.effective) || ''
@@ -382,9 +387,10 @@
 
 export function toAssetManifestPatch(record: GenerationRecord): JsonMap {
   const filename = record.location.filename
-  const parents = record.lineage.parents.flatMap(item => (
-    item.asset_id ? [{ id: item.asset_id, kind: item.kind || 'other', ...(item.uri ? { uri: item.uri } : {}) }] : []
-  ))
+  const parents = record.lineage.parents.flatMap(item => {
+    const ref = artifactRef(item)
+    return ref ? [ref] : []
+  })
   const patch: JsonMap = {
     asset: { id: record.asset_id, filename, uri: record.location.uri || filename },
     origin: {
@@ -421,7 +427,10 @@
     },
     technical: { generation_id: record.generation_id, result: record.result },
   }
-  const transformations = record.lineage.transformations || []
+  const transformations = (record.lineage.transformations || []).flatMap(item => {
+    const ref = artifactRef(item)
+    return ref ? [ref] : []
+  })
   const lineage: JsonMap = {}
   if (parents.length) lineage.parents = parents
   if (transformations.length) lineage.transformations = transformations
@@ -436,11 +445,30 @@
     : null
   const next: GenerationRecord = { ...base }
   for (const [key, value] of Object.entries(incoming)) {
-    if (key === 'generation_id' || key === 'asset_id' || key === 'workspace_id' || key === 'schema' || key === 'schema_version' || key === 'lineage') {
+    if (
+      key === 'generation_id' || key === 'asset_id' || key === 'workspace_id'
+      || key === 'schema' || key === 'schema_version' || key === 'lineage'
+      || key === 'prompt_full' || key === 'prompt_original'
+      || key === 'prompt_effective' || key === 'prompt_display'
+    ) {
       continue
     }
     ;(next as unknown as JsonMap)[key] = value
   }
+  if (incoming.prompt_original != null) {
+    next.prompt_original = String(incoming.prompt_original)
+  }
+  if (incoming.prompt_effective != null) {
+    next.prompt_effective = String(incoming.prompt_effective)
+  } else if (incoming.prompt_full != null) {
+    next.prompt_effective = String(incoming.prompt_full)
+  }
+  if (incoming.prompt_full != null) {
+    next.prompt_full = String(incoming.prompt_full)
+  }
+  const display = next.prompt_effective || next.prompt_original || next.prompt_full
+  next.prompt_full = display
+  next.prompt_display = truncatePromptDisplay(display)
   if (lineagePatch) {
     const lineage = {
       parents: [...base.lineage.parents],

You can send follow-ups to the cloud agent here.

Comment thread app/services/generation_record.py
Comment thread app/services/generation_record.py
Comment thread ui/src/lib/generationRecord.ts
Map transformations to asset-manifest artifact refs (id), derive duration_ms
from created_at as total wall time, and keep TS prompt_full/effective/display
in sync on merge.
@IAnMove

IAnMove commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a6adbbe. Configure here.

@IAnMove
IAnMove merged commit 8b010a6 into main Sep 5, 2026
5 checks passed
@IAnMove
IAnMove deleted the fix/generation-record-authority branch September 5, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant