Skip to content

feat: add portable generation-record v1 contract - #138

Merged
IAnMove merged 2 commits into
mainfrom
feat/generation-record-contract
Sep 5, 2026
Merged

feat: add portable generation-record v1 contract#138
IAnMove merged 2 commits into
mainfrom
feat/generation-record-contract

Conversation

@IAnMove

@IAnMove IAnMove commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Resumen ejecutivo

Esta primera sección está pensada para project managers y revisores no
técnicos. Mantén el detalle técnico completo más abajo.

Qué cambia

Se añade un contrato portable de generación v1: un documento JSON tipado
que describe un intento de generar cualquier elemento (imagen, audio, vídeo,
3D, etc.). No sustituye los ficheros generados ni el manifiesto de asset
existente; lee y escribe sobre esas fuentes.

Para qué sirve

Hoy el estado de una generación está repartido entre el manifiesto de asset,
la cola durable, el ciclo de vida del job y la procedencia. Este contrato da
una identidad estable (generation_id / asset_id), estados legales,
cancelación, reanudación tras un reinicio y aislamiento por Workspace, sin
inventar un segundo almacén de medios.

Impacto para el usuario

Ninguno en esta PR. No hay cambios de UI visible ni de generación real. El
cableado a Launch, Activity y Library queda para un PR posterior porque
_launch_runtime.py está reservado por #135.

Riesgo

  • Bajo
  • Medio
  • Alto

Estado

  • En desarrollo
  • Listo para revisión
  • Bloqueado por CI o revisión
  • Requiere migración o acción manual

Summary

Adds GenerationRecord v1 as a portable read/write projection over the existing
asset-manifest v1 sidecar, generation provenance and job lifecycle. Persistence
uses atomic temp-file replace (the DurableGenerationQueue pattern). Public
status is the six-value enum planned | queued | running | completed | failed | cancelled. Asset-manifest prepared maps to planned; partial is not a
seventh status (completed + result.kind = "partial" when a filename exists,
otherwise failed).

Attempt identity follows policy (b): a retry mints a new generation_id,
increments retry_count on the new attempt, and links the parent in
lineage.parents. asset_id is reused only when the bytes are the same
artifact. Resume after a simulated process restart reloads the JSON and keeps
the last durable status; it never invents success.

The module does not import FastAPI, WanGP or launch. Titles/prompts are not
identity. Secrets in model configuration are redacted. workspace_id is a
collection ID and output_folder / location filenames are never host paths.
A record saved under workspace A cannot be listed or loaded as workspace B.

Overview

build_generation_record creates a canonical attempt. project_from_asset_manifest
and to_asset_manifest_patch keep the two contracts compatible.
GenerationRecordStore persists one JSON file per attempt under
{root}/{workspace_id}/{generation_id}.json. transition_status enforces legal
transitions (including running -> queued for multi-phase work).
request_cancel settles planned/queued immediately and leaves running records
in running with cancellation.requested until apply_cancel, matching
job-lifecycle acknowledgement.

UI ui/src/lib/generationRecord.ts mirrors the types and pure helpers. No
store, Activity or launch wiring.

Detailed changes

Backend

  • New app/services/generation_record.py: contract, projection, lifecycle,
    atomic persist/load/list, cross-workspace isolation, retry lineage.
  • No FastAPI routes, no WanGP, no _launch_runtime.py edits.

UI and Wizard

  • New ui/src/lib/generationRecord.ts types and pure helpers.
  • New ui/tests/generationRecord.test.ts.
  • No i18n or visible chrome in this slice.

Data, provenance and compatibility

  • Schema: docs/development/generation-record-v1.schema.json.
  • Narrative: docs/development/GENERATION_RECORD.md.
  • Additive cross-links in DOMAIN_MODEL_AND_ASSET_PROVENANCE.md.
  • Existing sidecars remain authoritative for published bytes.

Files and ownership

File Why
app/services/generation_record.py Portable contract and store
docs/development/generation-record-v1.schema.json JSON schema
docs/development/GENERATION_RECORD.md Identity policy, status map, resume
docs/development/DOMAIN_MODEL_AND_ASSET_PROVENANCE.md Additive cross-links only
tests/test_generation_record.py Schema, persist, resume, cancel, isolation, secrets, lineage
ui/src/lib/generationRecord.ts UI types and pure mappers
ui/tests/generationRecord.test.ts Matching UI contract tests

Deliberately untouched (owned by #135 or out of scope):
app/_launch_runtime.py, ui/src/stores/useStore.ts,
ui/src/features/agent/agentActions.ts,
ui/src/features/stories/StoryLabPanel.tsx,
ui/src/features/stories/musicModel.ts,
ui/src/features/agent/studioCapabilities.ts, app/routers/llm.py,
scripts/code_health_baseline.json.

Validation

  • python scripts/verify_clean_repo.py — PASS
  • python -m compileall -q app/services/generation_record.py — PASS
  • Focused Python tests: python -m pytest -q tests/test_generation_record.py14 passed
  • Architecture contracts: python scripts/architecture_contracts.py and pytest -q tests/test_architecture_contracts.py — PASS (new test does not read launch/useStore)
  • UI focused tests: cd ui && npx tsx --tsconfig tsconfig.app.json --import ./tests/setupI18n.ts --test tests/generationRecord.test.ts7 passed
  • cd ui && npm test && npm run lint -- --max-warnings=0 && npm run build — PASS via validate_local.sh
  • git diff --check — PASS
  • E2E/smoke: bash scripts/validate_local.sh simulated Playwright — 7 passed; no GPU/real media
  • Code-health equivalent of scripts/check_code_health_pr_base.sh (that script is not in the tree): python scripts/code_health.py --check --baseline <origin/main report>PASS. Production LOC +1,493 (budget ~2,000+); functions ≥15 +0; max complexity +0; new file generation_record.py is 1,077 lines (limit 1,200). WARN: production LOC increased by +1,493.

PYTHON=/home/ina/pinokio/api/Maestro-next.git/app/env/bin/python bash scripts/validate_local.sh completed with [local] complete (no GPU or external provider calls).

Code quality

  • Score: pending CI
  • Complexity trend: pending CI
  • Production LOC trend: pending CI
  • Regression versus main: pending CI
  • Ratchet: pending CI

Local ratchet versus origin/main passed as recorded above. The published
quality score remains the CI comment; this PR does not invent it.

CI and review

  • CI: pending
  • Cursor/Bugbot: pending
  • Human review: pending

Do not treat this PR as done while required CI or review is still running.

Coste de la tarea

  • Tests simulados: 0 tokens externos
  • Tests reales: N/A
  • Llamadas LLM externas: 0
  • Tokens de prompt: N/A
  • Tokens de respuesta: N/A
  • Tokens totales: N/A
  • Generaciones de imágenes/audio/vídeo: 0
  • Tiempo transcurrido: N/A
  • Proveedores/modelos: N/A

Notes and limitations

  • This is a library/contract slice. Nothing publishes generation records from
    Studio/Director yet.
  • Resume restores the last durable JSON status; it does not checkpoint GPU
    weights (same limitation as DurableGenerationQueue).
  • scripts/check_code_health_pr_base.sh is not present on this branch; the
    same comparison is implemented inside scripts/validate_local.sh.

Follow-up work

  • Wire generation records into _launch_runtime.py, Activity and the Library
    catalog in a later sequential PR. #135 currently owns _launch_runtime.py.
  • Extra info / inspector presentation of the record can follow once writers
    persist technical.generation_id on the sidecar via to_asset_manifest_patch.

Checklist

  • The executive summary is understandable without reading the code.
  • The detailed Summary/Overview has not been removed or shortened.
  • Tests and their actual results are recorded.
  • Generated assets, secrets and local-only files are not committed.
  • Required CI and Cursor/Bugbot review are complete, or the PR is clearly
    marked as waiting for them.

Note

Low Risk
Additive library, schema, and docs only; no API routes, launch runtime, or user-facing behavior changes until a follow-up wires writers.

Overview
Introduces Generation record v1 as a typed JSON contract for a single asset-generation attempt, layered on asset-manifest v1, provenance, and job lifecycle—without a second media store or Launch/Activity wiring in this PR.

Backend (generation_record.py) adds build/validate, six public statuses (planned through cancelled) with manifest mapping (preparedplanned; partial via result.kind), legal transitions, cancel/retry/resume (policy b: new generation_id per retry with parent lineage), bidirectional project_from_asset_manifest / to_asset_manifest_patch, secret redaction and portable paths, plus GenerationRecordStore with atomic persist and workspace-scoped load/list.

Docs add GENERATION_RECORD.md, generation-record-v1.schema.json, and a Generation noun in the domain model.

UI mirrors the contract in ui/src/lib/generationRecord.ts with matching pure helpers and tests. Python and UI tests cover schema, persistence, isolation, cancellation, and manifest projection.

Reviewed by Cursor Bugbot for commit 4aba32c. Configure here.

Project asset-manifest, provenance and job lifecycle into one typed
attempt record with durable JSON persistence, cancellation and resume.
Retries mint a new generation_id and keep asset_id only for the same
artifact. Launch/Activity/Library wiring is left to a later PR.
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

PR Review — Loreframe Studio

Risk: medium
Scope: 7 file(s); +2338/-0; 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

  • medium — Large pull request
    2338 additions / 0 deletions. Reviewers will have an easier time with smaller, focused PRs.
  • medium — Very large file change (app/services/generation_record.py)
    app/services/generation_record.py adds 1077 lines. Consider splitting the PR.
  • medium — Very large file change (ui/src/lib/generationRecord.ts)
    ui/src/lib/generationRecord.ts adds 436 lines. Consider splitting the PR.
  • 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.py, docs/development/GENERATION_RECORD.md, docs/development/generation-record-v1.schema.json, tests/test_generation_record.py, ui/src/lib/generationRecord.ts, ui/tests/generationRecord.test.ts
  • modified: docs/development/DOMAIN_MODEL_AND_ASSET_PROVENANCE.md

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 4, 2026

Copy link
Copy Markdown

Code health

Quality score: 49.5/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.3
File concentration 25% 54.7 +0.6
Oversized-file debt 20% 30.0 +0.5
Modularity 10% 61.9 -0.6

Change vs PR base: +0.3 points.

Metric Value
Production LOC 242,841
Production files 534
Test LOC 70,660
Functions measured 15,171
Functions complexity ≥ 15 784
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:4021 Async method 'startGeneration'
355 app/_launch_runtime.py:23472 _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:8563 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:1109 Function 'parseAction'
226 app/services/director_pipeline.py:6689 update_comic_preview
225 ui/src/features/agent/agentActions.ts:2766 Async function 'executeAgentActions'

Trend vs baseline

Metric Δ
Production LOC +1,513
Test LOC +482
Functions ≥ 15 +0
Maximum complexity +0

Warnings

  • production LOC increased by +1,513

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 5 potential issues.

Fix All in Cursor

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

  • ✅ Fixed: Cancel loses to multi-phase requeue
    • transition_status now settles a requested cancel on queued and running targets as well, so multi-phase requeue/start cannot continue after cancel.
  • ✅ Fixed: UI cancel overwrites finished records
    • applyCancel now leaves cancelled records unchanged and throws on completed/failed, matching the Python apply_cancel contract.
  • ✅ Fixed: Unknown manifest status becomes planned
    • mapAssetManifestStatus now maps unrecognized non-empty statuses to failed with invalid_status, same as the Python mapper.
  • ✅ Fixed: Retry helper reuses the same IDs
    • retryGeneration now mints unique generation_id and asset_id values with crypto.randomUUID so repeated retries no longer collide.
  • ✅ Fixed: Patch writes the wrong duration field
    • toAssetManifestPatch now writes timestamps.duration_ms as timing.total_ms so a later Python projection can recover duration.

Create PR

Or push these changes by commenting:

@cursor push e134a22599
Preview (e134a22599)
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
@@ -814,7 +814,9 @@
     """Apply one legal lifecycle transition. Cancellation already requested wins."""
     current = validate_generation_record(record)
     resolved = (_clean(target) or "").casefold()
-    if current["cancellation"]["requested"] and resolved in {"completed", "failed"}:
+    if current["cancellation"]["requested"] and resolved in {
+        "queued", "running", "completed", "failed",
+    }:
         return apply_cancel(current, reason=current["cancellation"].get("reason"), at=at)
     if not is_legal_transition(current["status"], resolved):
         raise GenerationRecordError(

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
@@ -213,7 +213,14 @@
   if (STATUS_SET.has(raw)) {
     return { status: raw as GenerationStatus, resultKind: null, error: null }
   }
-  return { status: 'planned', resultKind: null, error: null }
+  if (!raw) {
+    return { status: 'planned', resultKind: null, error: null }
+  }
+  return {
+    status: 'failed',
+    resultKind: null,
+    error: { code: 'invalid_status', message: `Unsupported status '${raw}'` },
+  }
 }
 
 export function mapGenerationStatusToManifest(
@@ -370,7 +377,13 @@
       parameters: record.model.configuration,
       inputs: parents,
     },
-    timing: record.timestamps,
+    timing: {
+      created_at: record.timestamps.created_at,
+      queued_at: record.timestamps.queued_at,
+      started_at: record.timestamps.started_at,
+      completed_at: record.timestamps.completed_at,
+      total_ms: record.timestamps.duration_ms,
+    },
     lineage: { parents, transformations: [] },
     technical: { generation_id: record.generation_id, result: record.result },
   }
@@ -381,8 +394,8 @@
   'asset_id' | 'generation_id' | 'retry_count' | 'lineage' | 'status' | 'workspace_id'
 > {
   return {
-    generation_id: `gen_retry_${record.generation_id}`,
-    asset_id: sameArtifact ? record.asset_id : `asset_retry_${record.asset_id}`,
+    generation_id: `gen_${crypto.randomUUID().replace(/-/g, '')}`,
+    asset_id: sameArtifact ? record.asset_id : `asset_${crypto.randomUUID().replace(/-/g, '')}`,
     retry_count: record.retry_count + 1,
     status: 'planned',
     workspace_id: record.workspace_id,
@@ -409,6 +422,12 @@
 }
 
 export function applyCancel(record: GenerationRecord): Pick<GenerationRecord, 'status' | 'cancellation'> {
+  if (record.status === 'cancelled') {
+    return { status: record.status, cancellation: record.cancellation }
+  }
+  if (record.status === 'completed' || record.status === 'failed') {
+    throw new Error('Cannot cancel a finished generation')
+  }
   return {
     status: 'cancelled',
     cancellation: { requested: true, at: record.cancellation.at, reason: record.cancellation.reason },

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

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

Reviewed by Cursor Bugbot for commit 4aba32c. Configure here.

if not is_legal_transition(current["status"], resolved):
raise GenerationRecordError(
f"Illegal generation transition {current['status']!r} -> {resolved!r}",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cancel loses to multi-phase requeue

Medium Severity

transition_status only turns a requested cancel into cancelled when the target is completed or failed. A running record with cancellation.requested can still move to queued and back to running, so multi-phase work continues after the user cancelled.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4aba32c. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in b66ea0a. transition_status now applies cancel for any non-cancelled target once cancellation.requested is set, including queued/running requeue. Covered by test_cancellation_before_and_during_running.

status: 'cancelled',
cancellation: { requested: true, at: record.cancellation.at, reason: record.cancellation.reason },
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

UI cancel overwrites finished records

Medium Severity

applyCancel always returns cancelled, including when the record is already completed or failed. The Python helper rejects that, and the contract says terminal states do not transition, so the UI mapper can relabel finished work as cancelled.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4aba32c. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in b66ea0a. applyCancel now leaves completed, failed, and cancelled records unchanged, matching the Python helper.

return { status: raw as GenerationStatus, resultKind: null, error: null }
}
return { status: 'planned', resultKind: null, error: null }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unknown manifest status becomes planned

Medium Severity

mapAssetManifestStatus maps any unrecognized execution.status to planned with no error. The Python mapper treats the same input as failed with invalid_status, so the UI can present a corrupt or unsupported sidecar as a generation that has not started.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4aba32c. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in b66ea0a. Unrecognized manifest statuses now map to failed with invalid_status, same as Python map_manifest_status.

derivatives: [],
},
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Retry helper reuses the same IDs

Medium Severity

retryGeneration builds generation_id as gen_retry_${parent} and a new asset_id as asset_retry_${parent}. A second retry of the same parent mints the same IDs, which breaks policy (b) that every attempt gets a new generation_id.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4aba32c. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in b66ea0a. retryGeneration mints unique IDs via crypto.randomUUID (with a timestamp fallback) instead of gen_retry_${parent}.

timing: record.timestamps,
lineage: { parents, transformations: [] },
technical: { generation_id: record.generation_id, result: record.result },
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Patch writes the wrong duration field

Medium Severity

toAssetManifestPatch copies record.timestamps onto timing, which keeps duration_ms. Asset-manifest timing uses total_ms, and the Python patch maps that field correctly. Applying the UI patch therefore drops duration for any later Python projection.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4aba32c. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in b66ea0a. toAssetManifestPatch now writes timing.total_ms from timestamps.duration_ms.

A requested cancel now wins over requeue. UI cancel leaves completed/failed
records alone, retries mint unique IDs, unknown manifest statuses fail, and
the asset-manifest patch writes total_ms.
@IAnMove
IAnMove merged commit 3f14b0e into main Sep 5, 2026
4 checks passed
@IAnMove
IAnMove deleted the feat/generation-record-contract 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