@mengine/medeo-tool 1.2.1-alpha.8 → 1.2.1-alpha.9
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/README.md +25 -3
- package/dist/index.d.mts +51 -5
- package/dist/index.mjs +281 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -76,9 +76,31 @@ explicitly. Assets and media Entities are intentionally not one-to-one.
|
|
|
76
76
|
The production tool exposes native Clip/Marker editing plus visual placement,
|
|
77
77
|
voiceover, caption, and BGM helpers through its generated sandbox interface.
|
|
78
78
|
They preserve structural and anchor relations in the same plan. Timeline targets
|
|
79
|
-
are Entity IDs, never raw asset IDs or URLs. Asset import, media creation,
|
|
80
|
-
insertion belong in the **same entity plan**.
|
|
81
|
-
|
|
79
|
+
are Entity IDs, never raw asset IDs or URLs. Asset import, media creation, and
|
|
80
|
+
Clip insertion belong in the **same entity plan**.
|
|
81
|
+
|
|
82
|
+
Generation lineage is program-synced, not model-authored. After a confirmed
|
|
83
|
+
entity commit the tool resolves the plan's diff against host-supplied generation
|
|
84
|
+
facts and commits missing `generated` Relations between fact-matched media
|
|
85
|
+
Entities already present in the document (endpoint 0 output, endpoint 1 input;
|
|
86
|
+
lookup can use either endpoint). The diff covers created media Entities, edits
|
|
87
|
+
that re-point an existing physical-asset binding or Asset external key, and
|
|
88
|
+
creations; pairs already fact-resolvable before the plan stay untouched. Models
|
|
89
|
+
do not pass generation history through inputs — the host queries it with
|
|
90
|
+
`loadGenerationFacts(docId, assetIds)`, returning every known generation record
|
|
91
|
+
involving the given asset ids in either role. Each record must carry an explicit
|
|
92
|
+
`inputAssetIds` array: an explicit empty array declares text-only generation
|
|
93
|
+
with no lineage edge, while a missing or non-array field is a malformed record
|
|
94
|
+
that fails the whole query instead of being silently read as text-only.
|
|
95
|
+
One-sided facts are skipped without creating entities or blocking the commit —
|
|
96
|
+
lineage sync never backfills a missing source or output Entity; entity creation
|
|
97
|
+
stays a model decision inside the edit plan. Repeated commits are idempotent,
|
|
98
|
+
and deletions, rebindings, and revision conflicts are respected: a CAS-conflict
|
|
99
|
+
retry re-derives the trigger keys from the fresh bindings and re-queries the
|
|
100
|
+
newly scoped facts, so a backfillable edge is never misreported as current.
|
|
101
|
+
An empty result array means no known lineage; a rejection means the lineage
|
|
102
|
+
query failed and is reported as `generation_sync: {status:'failed'}` plus a
|
|
103
|
+
`generation_sync_failed` warning — never as synced state.
|
|
82
104
|
|
|
83
105
|
Entities own their facts: SequenceMarker owns source/target ranges, duration,
|
|
84
106
|
and time remapping; Clip owns volume; Track owns role/visibility. Cross-entity
|
package/dist/index.d.mts
CHANGED
|
@@ -209,6 +209,38 @@ type EditScriptResult = {
|
|
|
209
209
|
/** Run `script` against a forked document snapshot; always resolves (never rejects). */
|
|
210
210
|
declare function runEditScript(options: RunEditScriptOptions): Promise<EditScriptResult>;
|
|
211
211
|
//#endregion
|
|
212
|
+
//#region src/entity/generation-sync.d.ts
|
|
213
|
+
/** Factual generation lineage for recalled Memota assets, supplied by the host. */
|
|
214
|
+
interface AssetGenerationFact {
|
|
215
|
+
/** External asset id of the generation output (memota asset or speech result id). */
|
|
216
|
+
readonly outputAssetId: string;
|
|
217
|
+
/** Factual input asset ids; empty for text-only generation. */
|
|
218
|
+
readonly inputAssetIds: readonly string[];
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Host callback resolving lineage by external asset id. Implementations return
|
|
222
|
+
* every known generation record involving the given ids in either role; an
|
|
223
|
+
* empty array means no known lineage and a rejection means the lineage query
|
|
224
|
+
* failed. Entity and Relation semantics stay inside this package.
|
|
225
|
+
*/
|
|
226
|
+
type GenerationFactsLoader = (docId: string, assetIds: readonly string[]) => Promise<readonly AssetGenerationFact[]>;
|
|
227
|
+
/**
|
|
228
|
+
* Outcome of the post-commit lineage sync. `failed` is always also surfaced as
|
|
229
|
+
* a `generation_sync_failed` warning so an unavailable lineage query is never
|
|
230
|
+
* presented as synced state.
|
|
231
|
+
*/
|
|
232
|
+
interface GenerationSyncOutcome {
|
|
233
|
+
/**
|
|
234
|
+
* applied: new generated Relations were committed.
|
|
235
|
+
* current: the query succeeded and nothing was missing (no created asset,
|
|
236
|
+
* single side absent, text-only generation, or pair already linked).
|
|
237
|
+
* failed: the host query or the sync commit failed.
|
|
238
|
+
*/
|
|
239
|
+
readonly status: 'applied' | 'current' | 'failed';
|
|
240
|
+
readonly created_relation_ids?: readonly string[];
|
|
241
|
+
readonly message?: string;
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
212
244
|
//#region src/prompt.d.ts
|
|
213
245
|
declare const MEDEO_TOOL_DESCRIPTION: string;
|
|
214
246
|
//#endregion
|
|
@@ -245,7 +277,7 @@ declare const MEDEO_TOOL_PARAMETERS: {
|
|
|
245
277
|
};
|
|
246
278
|
readonly inputs: {
|
|
247
279
|
readonly type: 'object';
|
|
248
|
-
readonly description: 'Pre-materialized, side-effect-free values passed into the script, including recalled
|
|
280
|
+
readonly description: 'Pre-materialized, side-effect-free values passed into the script, including recalled asset facts. Generation history is never an input: the host queries lineage itself and syncs generated Relations after each commit. Generation and network IO must happen in the host before this call.';
|
|
249
281
|
};
|
|
250
282
|
readonly asset_facts: {
|
|
251
283
|
readonly type: 'array';
|
|
@@ -530,6 +562,15 @@ interface CreateMedeoToolOptions {
|
|
|
530
562
|
* snapshot, and tolerates a concurrent creator winning the race.
|
|
531
563
|
*/
|
|
532
564
|
loadInitialDraft?: (docId: string) => Promise<VideoDraft>;
|
|
565
|
+
/**
|
|
566
|
+
* Resolve factual generation lineage by external asset id after a confirmed
|
|
567
|
+
* entity commit. Return every known generation record involving the given
|
|
568
|
+
* ids in either role; an empty array means no known lineage and a rejection
|
|
569
|
+
* means the lineage query failed (surfaced as a warning, never as synced
|
|
570
|
+
* state). The package owns all Entity/Relation semantics: the host never
|
|
571
|
+
* names entities, relations, or endpoints.
|
|
572
|
+
*/
|
|
573
|
+
loadGenerationFacts?: GenerationFactsLoader;
|
|
533
574
|
fetchImpl?: typeof fetch;
|
|
534
575
|
/** @deprecated ManualSyncDoc has no SSE or reconnect loop. */
|
|
535
576
|
sseReconnectDelayMs?: number;
|
|
@@ -576,15 +617,20 @@ type MedeoToolInput = {
|
|
|
576
617
|
plan_id: string;
|
|
577
618
|
validation?: 'version' | 'preflight';
|
|
578
619
|
};
|
|
579
|
-
|
|
620
|
+
type MedeoToolWarning = {
|
|
580
621
|
kind: 'pull_failed';
|
|
581
622
|
message: string;
|
|
582
|
-
}
|
|
623
|
+
} | {
|
|
624
|
+
kind: 'generation_sync_failed';
|
|
625
|
+
message: string;
|
|
626
|
+
};
|
|
583
627
|
type EntityCommitResult = {
|
|
584
628
|
kind: 'committed';
|
|
585
629
|
ops_applied: number;
|
|
586
630
|
collaborated: false;
|
|
587
|
-
entity_revision: number;
|
|
631
|
+
entity_revision: number; /** Present only when the host supplies loadGenerationFacts. */
|
|
632
|
+
generation_sync?: GenerationSyncOutcome;
|
|
633
|
+
warnings?: MedeoToolWarning[];
|
|
588
634
|
} | {
|
|
589
635
|
kind: 'unconfirmed';
|
|
590
636
|
reason: 'push_failed';
|
|
@@ -682,5 +728,5 @@ type MedeoInitialDraft = VideoDraft;
|
|
|
682
728
|
*/
|
|
683
729
|
declare function createMedeoTool(options: CreateMedeoToolOptions): MedeoTool;
|
|
684
730
|
//#endregion
|
|
685
|
-
export { type AuthorableRelationKind, type ChangePlan, type CommitPlan, type CommitPlanOptions, type CommitPlanResult, type CompactProjectionOptions, type ConsoleShim, type CreateEntityInput, type CreateMedeoToolOptions, type DeleteEntityInput, type EditFacade, EditSandboxSession, type EditSandboxSessionOptions, type EditScriptResult, type EntityCommand, type EntityCommitResult, type EntityFacade, type EntityPlanState, type EntityStoreSnapshot, type ImportAssetInput, type JsonObject, type JsonPrimitive, type JsonValue, type KnownEntityKind, type KnownRelationKind, type LinkGeneratedRelationInput, type LinkRelationInput, MEDEO_TOOL_DESCRIPTION, MEDEO_TOOL_NAME, MEDEO_TOOL_PARAMETERS, type MedeoCommitResult, type MedeoInitialDraft, type MedeoModelContext, type MedeoModelContextInput, type MedeoTool, type MedeoToolInput, type MedeoToolOp, type MedeoToolResult, type RelationFacade, type RunEditScriptOptions, type SandboxCheckpoint, type SandboxEntity, type SandboxRelation, type TimelineClipDescriptor, type TimelineFacade, type TimelinePartDescriptor, type UnlinkRelationInput, type UpdateEntityInput, collectAffectedPartIds, commitPlan, createMedeoTool, renderCompactProjection, renderPreview, runEditScript };
|
|
731
|
+
export { type AssetGenerationFact, type AuthorableRelationKind, type ChangePlan, type CommitPlan, type CommitPlanOptions, type CommitPlanResult, type CompactProjectionOptions, type ConsoleShim, type CreateEntityInput, type CreateMedeoToolOptions, type DeleteEntityInput, type EditFacade, EditSandboxSession, type EditSandboxSessionOptions, type EditScriptResult, type EntityCommand, type EntityCommitResult, type EntityFacade, type EntityPlanState, type EntityStoreSnapshot, type GenerationFactsLoader, type GenerationSyncOutcome, type ImportAssetInput, type JsonObject, type JsonPrimitive, type JsonValue, type KnownEntityKind, type KnownRelationKind, type LinkGeneratedRelationInput, type LinkRelationInput, MEDEO_TOOL_DESCRIPTION, MEDEO_TOOL_NAME, MEDEO_TOOL_PARAMETERS, type MedeoCommitResult, type MedeoInitialDraft, type MedeoModelContext, type MedeoModelContextInput, type MedeoTool, type MedeoToolInput, type MedeoToolOp, type MedeoToolResult, type RelationFacade, type RunEditScriptOptions, type SandboxCheckpoint, type SandboxEntity, type SandboxRelation, type TimelineClipDescriptor, type TimelineFacade, type TimelinePartDescriptor, type UnlinkRelationInput, type UpdateEntityInput, collectAffectedPartIds, commitPlan, createMedeoTool, renderCompactProjection, renderPreview, runEditScript };
|
|
686
732
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -276,9 +276,9 @@ var EntityHttpClient = class {
|
|
|
276
276
|
}
|
|
277
277
|
};
|
|
278
278
|
function toSnapshot(value, expectedDocId) {
|
|
279
|
-
if (!isRecord$
|
|
279
|
+
if (!isRecord$2(value) || typeof value.doc_id !== "string" || !isNonNegativeInteger(value.revision)) throw new Error("invalid entity-state response envelope");
|
|
280
280
|
if (value.doc_id !== expectedDocId) throw new Error(`entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
281
|
-
if (!isRecord$
|
|
281
|
+
if (!isRecord$2(value.rows) || !Array.isArray(value.rows.entities) || !Array.isArray(value.rows.relations)) throw new Error("invalid entity-state response rows");
|
|
282
282
|
const response = value;
|
|
283
283
|
return {
|
|
284
284
|
revision: response.revision,
|
|
@@ -287,15 +287,15 @@ function toSnapshot(value, expectedDocId) {
|
|
|
287
287
|
};
|
|
288
288
|
}
|
|
289
289
|
function parseEntity(value) {
|
|
290
|
-
if (!isRecord$
|
|
290
|
+
if (!isRecord$2(value) || !isTrimmed(value.entity_id) || typeof value.entity_kind !== "string" || !entityKinds.has(value.entity_kind) || !isJsonObject(value.payload)) throw new Error("invalid Entity row in entity-state response");
|
|
291
291
|
return structuredClone(value);
|
|
292
292
|
}
|
|
293
293
|
function parseRelation(value) {
|
|
294
|
-
if (!isRecord$
|
|
294
|
+
if (!isRecord$2(value) || !isTrimmed(value.relation_id) || typeof value.relation_kind !== "string" || !relationKinds.has(value.relation_kind) || !isTrimmed(value.endpoint_0_entity_id) || !isTrimmed(value.endpoint_1_entity_id) || !isJsonObject(value.metadata) || !isJsonObject(value.trace)) throw new Error("invalid Relation row in entity-state response");
|
|
295
295
|
return structuredClone(value);
|
|
296
296
|
}
|
|
297
297
|
function isJsonObject(value) {
|
|
298
|
-
return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$
|
|
298
|
+
return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$2(value);
|
|
299
299
|
}
|
|
300
300
|
function isJsonValue(value, ancestors) {
|
|
301
301
|
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
@@ -308,7 +308,7 @@ function isJsonValue(value, ancestors) {
|
|
|
308
308
|
ancestors.delete(value);
|
|
309
309
|
return valid;
|
|
310
310
|
}
|
|
311
|
-
function isRecord$
|
|
311
|
+
function isRecord$2(value) {
|
|
312
312
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
313
313
|
}
|
|
314
314
|
function isTrimmed(value) {
|
|
@@ -327,6 +327,223 @@ async function safeReadJson(response) {
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
//#endregion
|
|
330
|
+
//#region src/entity/generation-sync.ts
|
|
331
|
+
/**
|
|
332
|
+
* External systems whose asset entities carry a factual Memota identity.
|
|
333
|
+
* Voice results use the speech system; every other medium uses `memota`.
|
|
334
|
+
*/
|
|
335
|
+
const ASSET_SYSTEMS = new Set(["memota", "memota-speech"]);
|
|
336
|
+
/** Entity kinds that may carry a generated Relation endpoint (DSL `GeneratedMedia`). */
|
|
337
|
+
const GENERATED_MEDIA_KINDS = new Set([
|
|
338
|
+
"video",
|
|
339
|
+
"image",
|
|
340
|
+
"audio",
|
|
341
|
+
"voice"
|
|
342
|
+
]);
|
|
343
|
+
/** Bounded CAS retry budget for the sync commit after a concurrent winner. */
|
|
344
|
+
const MAX_COMMIT_ATTEMPTS = 3;
|
|
345
|
+
/** Validate host-supplied facts; a malformed record fails the whole query. */
|
|
346
|
+
function parseGenerationFacts(value) {
|
|
347
|
+
if (!Array.isArray(value)) throw new Error("generation facts must be an array");
|
|
348
|
+
return value.map((item) => {
|
|
349
|
+
if (!isRecord$1(item)) throw new Error("each generation fact must be an object");
|
|
350
|
+
const { outputAssetId, inputAssetIds } = item;
|
|
351
|
+
if (typeof outputAssetId !== "string" || outputAssetId.length === 0 || outputAssetId.trim() !== outputAssetId) throw new Error("generation fact outputAssetId must be a non-empty trimmed string");
|
|
352
|
+
if (!Array.isArray(inputAssetIds)) throw new Error("generation fact inputAssetIds must be an array (explicit [] means text-only)");
|
|
353
|
+
const inputs = inputAssetIds;
|
|
354
|
+
for (const input of inputs) if (typeof input !== "string" || input.length === 0 || input.trim() !== input) throw new Error("generation fact inputAssetIds entries must be non-empty trimmed strings");
|
|
355
|
+
return {
|
|
356
|
+
outputAssetId,
|
|
357
|
+
inputAssetIds: [...inputs]
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function planGenerationScope(base, commands, state) {
|
|
362
|
+
const createdMediaIds = /* @__PURE__ */ new Set();
|
|
363
|
+
const keyChangedAssetIds = /* @__PURE__ */ new Set();
|
|
364
|
+
const bindingTouchedMediaIds = /* @__PURE__ */ new Set();
|
|
365
|
+
for (const command of commands) if (command.kind === "create-entity") {
|
|
366
|
+
if (command.entity.entity_kind === "asset") keyChangedAssetIds.add(command.entity.entity_id);
|
|
367
|
+
else if (GENERATED_MEDIA_KINDS.has(command.entity.entity_kind)) createdMediaIds.add(command.entity.entity_id);
|
|
368
|
+
} else if (command.kind === "update-entity") {
|
|
369
|
+
const baseEntity = base.entities.find((entity) => entity.entity_id === command.entity_id);
|
|
370
|
+
if (baseEntity?.entity_kind !== "asset") continue;
|
|
371
|
+
const before = assetKeyOf(baseEntity);
|
|
372
|
+
const after = assetKeyOf({
|
|
373
|
+
entity_id: command.entity_id,
|
|
374
|
+
entity_kind: "asset",
|
|
375
|
+
payload: command.payload
|
|
376
|
+
});
|
|
377
|
+
if (after !== void 0 && before !== after) keyChangedAssetIds.add(command.entity_id);
|
|
378
|
+
} else if (command.kind === "link-relation") {
|
|
379
|
+
if (command.relation.relation_kind === "physical-asset") bindingTouchedMediaIds.add(command.relation.endpoint_0_entity_id);
|
|
380
|
+
} else if (command.kind === "unlink-relation") {
|
|
381
|
+
const removed = base.relations.find((relation) => relation.relation_id === command.relation_id);
|
|
382
|
+
if (removed?.relation_kind === "physical-asset") bindingTouchedMediaIds.add(removed.endpoint_0_entity_id);
|
|
383
|
+
}
|
|
384
|
+
const scoped = new Set([...createdMediaIds, ...bindingTouchedMediaIds]);
|
|
385
|
+
const assetsById = new Map(state.entities.filter((entity) => entity.entity_kind === "asset").map((entity) => [entity.entity_id, entity]));
|
|
386
|
+
for (const relation of state.relations) {
|
|
387
|
+
if (relation.relation_kind !== "physical-asset") continue;
|
|
388
|
+
if (!keyChangedAssetIds.has(relation.endpoint_1_entity_id)) continue;
|
|
389
|
+
scoped.add(relation.endpoint_0_entity_id);
|
|
390
|
+
}
|
|
391
|
+
const queryKeys = /* @__PURE__ */ new Set();
|
|
392
|
+
for (const relation of state.relations) {
|
|
393
|
+
if (relation.relation_kind !== "physical-asset") continue;
|
|
394
|
+
if (!scoped.has(relation.endpoint_0_entity_id)) continue;
|
|
395
|
+
const asset = assetsById.get(relation.endpoint_1_entity_id);
|
|
396
|
+
const key = asset === void 0 ? void 0 : assetKeyOf(asset);
|
|
397
|
+
if (key !== void 0) queryKeys.add(key);
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
scopedMediaIds: scoped,
|
|
401
|
+
queryAssetKeys: [...queryKeys].sort()
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Ordered generated(output,input) Relations missing from `state` for the given
|
|
406
|
+
* factual records. Both endpoints must already exist and fact-match through
|
|
407
|
+
* physical-asset bindings, and the pair must involve a media Entity the plan
|
|
408
|
+
* newly fact-exposed (`scopedMediaIds`): lineage scopes to the commit's diff,
|
|
409
|
+
* so a pair the user deleted between untouched entities stays deleted. A pair
|
|
410
|
+
* the facts already resolved against the plan's base state is likewise skipped.
|
|
411
|
+
* One-sided facts, text-only records, self pairs, and already-linked pairs are
|
|
412
|
+
* skipped. Duplicate records collapse to one Relation.
|
|
413
|
+
*/
|
|
414
|
+
function planGeneratedRelations(input) {
|
|
415
|
+
const { state, facts } = input;
|
|
416
|
+
const scoped = input.scopedMediaIds;
|
|
417
|
+
const mediaByAssetKey = resolveMediaByAssetKey(state);
|
|
418
|
+
const baseResolvable = new Set(resolvablePairs(resolveMediaByAssetKey(input.baseState), facts));
|
|
419
|
+
const linkedPairs = new Set(state.relations.filter((relation) => relation.relation_kind === "generated").map((relation) => pairKey(relation.endpoint_0_entity_id, relation.endpoint_1_entity_id)));
|
|
420
|
+
const relations = [];
|
|
421
|
+
for (const fact of facts) for (const outputId of mediaByAssetKey.get(fact.outputAssetId) ?? []) for (const inputAssetId of fact.inputAssetIds) for (const inputId of mediaByAssetKey.get(inputAssetId) ?? []) {
|
|
422
|
+
if (outputId === inputId) continue;
|
|
423
|
+
if (!scoped.has(outputId) && !scoped.has(inputId)) continue;
|
|
424
|
+
const pair = pairKey(outputId, inputId);
|
|
425
|
+
if (linkedPairs.has(pair) || baseResolvable.has(pair)) continue;
|
|
426
|
+
linkedPairs.add(pair);
|
|
427
|
+
relations.push({
|
|
428
|
+
relation_id: input.newRelationId(),
|
|
429
|
+
relation_kind: "generated",
|
|
430
|
+
endpoint_0_entity_id: outputId,
|
|
431
|
+
endpoint_1_entity_id: inputId,
|
|
432
|
+
metadata: {},
|
|
433
|
+
trace: { synced_by: "generation-sync" }
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return relations;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Sync generation lineage after a confirmed entity commit. Any failure is
|
|
440
|
+
* returned as a `failed` outcome instead of thrown, so the already-durable
|
|
441
|
+
* commit result is never masked; a successful query that finds nothing is
|
|
442
|
+
* `current`. A revision conflict re-reads, re-derives the trigger keys from
|
|
443
|
+
* the fresh bindings, queries any newly scoped facts, re-plans, and retries
|
|
444
|
+
* within `MAX_COMMIT_ATTEMPTS` before reporting failure — a stale asset query
|
|
445
|
+
* must never let a backfillable edge be reported as `current`.
|
|
446
|
+
*/
|
|
447
|
+
async function syncGeneratedRelations(input) {
|
|
448
|
+
const { client, docId, baseState, entityCommands, loadFacts } = input;
|
|
449
|
+
let state;
|
|
450
|
+
let facts = [];
|
|
451
|
+
const queriedKeys = /* @__PURE__ */ new Set();
|
|
452
|
+
try {
|
|
453
|
+
state = await client.fetchState();
|
|
454
|
+
let scope = planGenerationScope(baseState, entityCommands, state);
|
|
455
|
+
for (let attempt = 1; attempt <= MAX_COMMIT_ATTEMPTS; attempt++) {
|
|
456
|
+
const unseenKeys = scope.queryAssetKeys.filter((key) => !queriedKeys.has(key));
|
|
457
|
+
for (const key of unseenKeys) queriedKeys.add(key);
|
|
458
|
+
if (unseenKeys.length > 0) facts = [...facts, ...parseGenerationFacts(await loadFacts(docId, unseenKeys))];
|
|
459
|
+
if (scope.queryAssetKeys.length === 0) return { status: "current" };
|
|
460
|
+
const relations = planGeneratedRelations({
|
|
461
|
+
baseState,
|
|
462
|
+
state,
|
|
463
|
+
scopedMediaIds: scope.scopedMediaIds,
|
|
464
|
+
facts,
|
|
465
|
+
newRelationId: mintRelationId
|
|
466
|
+
});
|
|
467
|
+
if (relations.length === 0) return { status: "current" };
|
|
468
|
+
try {
|
|
469
|
+
await client.commit(state.revision, {
|
|
470
|
+
...state,
|
|
471
|
+
relations: [...state.relations, ...relations]
|
|
472
|
+
});
|
|
473
|
+
return {
|
|
474
|
+
status: "applied",
|
|
475
|
+
created_relation_ids: relations.map((relation) => relation.relation_id)
|
|
476
|
+
};
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (!(error instanceof MengineEntityHttpRequestError && error.status === 409) || attempt === MAX_COMMIT_ATTEMPTS) return {
|
|
479
|
+
status: "failed",
|
|
480
|
+
message: `generation lineage sync commit failed: ${errorMessage(error)}`
|
|
481
|
+
};
|
|
482
|
+
state = await client.fetchState();
|
|
483
|
+
scope = planGenerationScope(baseState, entityCommands, state);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
status: "failed",
|
|
488
|
+
message: "generation lineage sync exhausted its retry budget"
|
|
489
|
+
};
|
|
490
|
+
} catch (error) {
|
|
491
|
+
return {
|
|
492
|
+
status: "failed",
|
|
493
|
+
message: `generation lineage query failed: ${errorMessage(error)}`
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function assetKeyOf(entity) {
|
|
498
|
+
if (entity.entity_kind !== "asset") return void 0;
|
|
499
|
+
const external = entity.payload?.external;
|
|
500
|
+
if (external == null || typeof external !== "object" || Array.isArray(external)) return void 0;
|
|
501
|
+
const { system, key } = external;
|
|
502
|
+
if (typeof system !== "string" || !ASSET_SYSTEMS.has(system)) return void 0;
|
|
503
|
+
if (typeof key !== "string" || key.length === 0 || key.trim() !== key) return void 0;
|
|
504
|
+
return key;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Media Entity ids fact-matched to each external asset key. Bindings follow
|
|
508
|
+
* the canonical physical-asset direction (endpoint 0 = sequence media,
|
|
509
|
+
* endpoint 1 = Asset) that the DSL spec enforces.
|
|
510
|
+
*/
|
|
511
|
+
function resolveMediaByAssetKey(state) {
|
|
512
|
+
const assetsById = new Map(state.entities.filter((entity) => entity.entity_kind === "asset").map((entity) => [entity.entity_id, entity]));
|
|
513
|
+
const mediaKinds = new Set([...GENERATED_MEDIA_KINDS]);
|
|
514
|
+
const mediaIds = new Set(state.entities.filter((entity) => mediaKinds.has(entity.entity_kind)).map((entity) => entity.entity_id));
|
|
515
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
516
|
+
for (const relation of state.relations) {
|
|
517
|
+
if (relation.relation_kind !== "physical-asset") continue;
|
|
518
|
+
if (!mediaIds.has(relation.endpoint_0_entity_id)) continue;
|
|
519
|
+
const asset = assetsById.get(relation.endpoint_1_entity_id);
|
|
520
|
+
const key = asset === void 0 ? void 0 : assetKeyOf(asset);
|
|
521
|
+
if (key === void 0) continue;
|
|
522
|
+
const media = resolved.get(key);
|
|
523
|
+
if (media === void 0) resolved.set(key, [relation.endpoint_0_entity_id]);
|
|
524
|
+
else if (!media.includes(relation.endpoint_0_entity_id)) media.push(relation.endpoint_0_entity_id);
|
|
525
|
+
}
|
|
526
|
+
return resolved;
|
|
527
|
+
}
|
|
528
|
+
function pairKey(endpoint0, endpoint1) {
|
|
529
|
+
return `${endpoint0}\u0000${endpoint1}`;
|
|
530
|
+
}
|
|
531
|
+
/** Pair keys the facts already resolve to under the given base bindings. */
|
|
532
|
+
function resolvablePairs(mediaByAssetKey, facts) {
|
|
533
|
+
const pairs = [];
|
|
534
|
+
for (const fact of facts) for (const outputId of mediaByAssetKey.get(fact.outputAssetId) ?? []) for (const inputAssetId of fact.inputAssetIds) for (const inputId of mediaByAssetKey.get(inputAssetId) ?? []) if (outputId !== inputId) pairs.push(pairKey(outputId, inputId));
|
|
535
|
+
return pairs;
|
|
536
|
+
}
|
|
537
|
+
function mintRelationId() {
|
|
538
|
+
return `relation_${randomUUID()}`;
|
|
539
|
+
}
|
|
540
|
+
function errorMessage(error) {
|
|
541
|
+
return error instanceof Error ? error.message : String(error);
|
|
542
|
+
}
|
|
543
|
+
function isRecord$1(value) {
|
|
544
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
545
|
+
}
|
|
546
|
+
//#endregion
|
|
330
547
|
//#region src/migration-input.ts
|
|
331
548
|
/** Validate host-recalled facts without accepting a caller-controlled snapshot or VV. */
|
|
332
549
|
function parseMigrationAssetFacts(value) {
|
|
@@ -951,20 +1168,20 @@ Edit the authoritative Medeo Entity/Relation graph through a deterministic, side
|
|
|
951
1168
|
Operations:
|
|
952
1169
|
- snapshot: return the Entity/Relation state summary and opaque base version.
|
|
953
1170
|
- migrate-legacy: explicitly migrate an existing legacy timeline using recalled asset_facts. MEngine reads the canonical document and version, verifies that editing facts are preserved, and commits migration alone. Then take a fresh snapshot before any edit; never pass a caller-created legacy snapshot or version.
|
|
954
|
-
- run-edit-script: inspect timeline.snapshot(), entities.*, and relations.*; edit.* operates existing Entity ids and creates the required Clip/SequenceMarker structural graph. Asset import, media Entity creation
|
|
1171
|
+
- run-edit-script: inspect timeline.snapshot(), entities.*, and relations.*; edit.* operates existing Entity ids and creates the required Clip/SequenceMarker structural graph. Asset import, media Entity creation and timeline edits belong in ONE plan. The sandbox has no network, storage or generation access. Pass recalled asset facts through inputs; generation history is not a script input — the host program queries it itself after each commit. A successful run returns preview, logs, base revision and plan_id.
|
|
955
1172
|
- commit-plan: commit the complete Entity/Relation plan through revision CAS. The server derives the read-only timeline projection in the same transaction. There is no separate writable timeline plan and no preflight replay into a legacy editor. A failed transport is unconfirmed, never committed; retry the same plan_id.
|
|
956
1173
|
|
|
957
1174
|
Default flow: snapshot → run-edit-script with auto_commit=false → inspect preview → commit-plan. Use auto_commit=true only for low-risk edits when the host does not need user confirmation. On version mismatch, rerun snapshot and the script; never try to patch a rejected journal by hand.
|
|
958
1175
|
|
|
959
|
-
Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts, reuse or create the appropriate media Entity and physical-asset Relation, then pass the media Entity id to edit.insertClip. A raw asset id or URL is not valid contentEntityId. Asset and media Entity identity are not one-to-one.
|
|
1176
|
+
Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts, reuse or create the appropriate media Entity and physical-asset Relation, then pass the media Entity id to edit.insertClip. A raw asset id or URL is not valid contentEntityId. Asset and media Entity identity are not one-to-one. Generation lineage is program-synced: after each successful commit the tool connects existing media Entities from host-recalled generation facts (endpoint 0 output, endpoint 1 input) that the host queries itself — do not pass generation history through inputs. Do not author generated Relations yourself, and never create an Entity merely to backfill or represent lineage; media Entities the edit itself legitimately needs (for example placing a recalled input asset) are still created normally. Text-only generation has no input and no lineage edge. relations.of(entityId) is endpoint-agnostic.
|
|
960
1177
|
`.trim();
|
|
961
1178
|
const MEDEO_TOOL_EXECUTION_RULES = `
|
|
962
1179
|
The host supplies the current document. Do not ask for, invent, or pass a document id.
|
|
963
1180
|
timeline.snapshot() returns the Entity/Relation graph with its revision, not a legacy VideoDraft. Inspect Timeline, Track, Clip, SequenceMarker and their relations to plan edits.
|
|
964
|
-
Generation lineage and Memota asset facts are host-provided through inputs. Never invent an asset id, Entity kind, or peer Entity id.
|
|
1181
|
+
Generation lineage is not a model input: the host program queries it via loadGenerationFacts and syncs generated Relations after each successful commit. Memota asset facts are host-provided through inputs. Never invent an asset id, Entity kind, or peer Entity id.
|
|
965
1182
|
Before importing an Asset, call entities.findByAssetId(assetId), inspect every match, and decide whether an existing Entity represents the intended logical asset. Multiple matches are valid; do not assume Asset↔media is one-to-one.
|
|
966
1183
|
For recalled video/audio/voice, create a bounded/native payload whose extent end comes from factual media duration/coordinates in inputs; never fabricate a duration. Image uses unbounded/constant semantics and has no invented end. If required facts are absent, do not create the media Entity yet.
|
|
967
|
-
For physical-asset authoring, use sequence media as endpoint_0_entity_id and Asset as endpoint_1_entity_id.
|
|
1184
|
+
For physical-asset authoring, use sequence media as endpoint_0_entity_id and Asset as endpoint_1_entity_id. Generated lineage Relations are not model-authored: the tool derives them from host generation facts after a successful commit, so do not call relations.linkGenerated to record provenance. relations.of remains endpoint-agnostic for lookup.
|
|
968
1185
|
The compatibility reader supports the existing four Track roles: video_clip (Image/Video), speech (Voice), caption (Caption), and bgm (Audio), one of each. Clip.volume is decibels (-60 to 20, 0 = original). Marker.sourceRange is the selected source interval; Marker.duration is effective display/playback duration. Coordinates and duration are whole milliseconds for this reader, not a global DSL restriction. Placement is exactly one of Clip.order, Marker.targetRange, or clip-anchor(child,host) plus Marker.anchorOffset. Use the native move/delete/voiceover helpers so placement and cascade decisions are in the same entity plan. Reading the graph never rebinds anchors or invents empty clips. Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'} and agrees with rounded source span divided by rate. Image remains unbounded/constant; an explicit linear rate scales its display window, not an invented media extent. Nonlinear speed and multiple visual overlay tracks are unsupported.
|
|
969
1186
|
Voice links to AudioScript through audio-script-render(output,script). Caption owns text/style and is placed through a Clip anchored to the Voice Clip; caption alignment/provenance agree with that Voice/AudioScript. BGM Audio keeps factual source duration and its Marker declares durationPolicy:'timeline'. External Asset identity and storageKey are distinct from the placed Clip identity. Never introduce a speech entity kind.
|
|
970
1187
|
Create only the known entity kinds. On an empty document, explicitly create Timeline and Track(role='video_clip') and connect timeline-track before inserting a Clip. If snapshot reports legacy migration is required, recall the listed asset facts and call migrate-legacy first. Missing facts, unsupported layouts, and version conflicts fail closed; never fall back to an old timeline method or raw update endpoint.
|
|
@@ -1064,7 +1281,7 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1064
1281
|
},
|
|
1065
1282
|
inputs: {
|
|
1066
1283
|
type: "object",
|
|
1067
|
-
description: "Pre-materialized, side-effect-free values passed into the script, including recalled
|
|
1284
|
+
description: "Pre-materialized, side-effect-free values passed into the script, including recalled asset facts. Generation history is never an input: the host queries lineage itself and syncs generated Relations after each commit. Generation and network IO must happen in the host before this call."
|
|
1068
1285
|
},
|
|
1069
1286
|
asset_facts: {
|
|
1070
1287
|
type: "array",
|
|
@@ -1569,11 +1786,12 @@ function createMedeoTool(options) {
|
|
|
1569
1786
|
...peerId !== void 0 ? { peerId } : {}
|
|
1570
1787
|
});
|
|
1571
1788
|
}
|
|
1572
|
-
function rememberPlan(docId, plan) {
|
|
1789
|
+
function rememberPlan(docId, plan, baseState) {
|
|
1573
1790
|
const planId = randomUUID();
|
|
1574
1791
|
plans.set(planId, {
|
|
1575
1792
|
docId,
|
|
1576
|
-
plan
|
|
1793
|
+
plan,
|
|
1794
|
+
...plan.plan_kind === "entities" ? { baseState: baseState && structuredClone(baseState) } : {}
|
|
1577
1795
|
});
|
|
1578
1796
|
while (plans.size > maxPlans) {
|
|
1579
1797
|
const protectedPlanIds = new Set([...pendingPushes.values()].map((pending) => pending.planId));
|
|
@@ -1588,7 +1806,7 @@ function createMedeoTool(options) {
|
|
|
1588
1806
|
const pending = pendingPushes.get(docId);
|
|
1589
1807
|
if (pending != null) throw new Error(`doc ${docId} has an unconfirmed push; retry plan_id ${pending.planId} before continuing`);
|
|
1590
1808
|
}
|
|
1591
|
-
function recordPushResult(docId, planId, plan, result) {
|
|
1809
|
+
function recordPushResult(docId, planId, plan, result, baseState) {
|
|
1592
1810
|
if (result.kind === "unconfirmed") {
|
|
1593
1811
|
pendingPushes.set(docId, plan.plan_kind === "timeline" ? {
|
|
1594
1812
|
kind: "timeline",
|
|
@@ -1598,7 +1816,8 @@ function createMedeoTool(options) {
|
|
|
1598
1816
|
} : {
|
|
1599
1817
|
kind: "entities",
|
|
1600
1818
|
planId,
|
|
1601
|
-
plan
|
|
1819
|
+
plan,
|
|
1820
|
+
...baseState !== void 0 ? { baseState } : {}
|
|
1602
1821
|
});
|
|
1603
1822
|
return;
|
|
1604
1823
|
}
|
|
@@ -1608,11 +1827,49 @@ function createMedeoTool(options) {
|
|
|
1608
1827
|
async function fetchEntityStateForSandbox(docId) {
|
|
1609
1828
|
return await getEntityClient(docId).fetchState();
|
|
1610
1829
|
}
|
|
1611
|
-
async function commitCachedPlan(docId, _doc, plan, validation) {
|
|
1830
|
+
async function commitCachedPlan(docId, _doc, plan, validation, baseState) {
|
|
1612
1831
|
if (plan.plan_kind === "timeline") throw new Error("Legacy timeline plans are not editable; use an Entity/Relation plan");
|
|
1613
1832
|
if (validation === "preflight") throw new Error("Entity plans use revision CAS; validation=preflight is not supported");
|
|
1614
1833
|
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1615
|
-
|
|
1834
|
+
const client = getEntityClient(docId);
|
|
1835
|
+
const preCommitState = baseState ?? (options.loadGenerationFacts !== void 0 ? await client.fetchState() : void 0);
|
|
1836
|
+
return await attachGenerationSync(docId, plan, await commitEntityPlan(client, plan), preCommitState);
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* After a confirmed entity commit, connect fact-matched generated Relations
|
|
1840
|
+
* from host-recalled lineage. The commit is already durable, so a sync
|
|
1841
|
+
* failure never fails the op; it is attached to the result and surfaced as a
|
|
1842
|
+
* warning instead. The plan's diff against `baseState` scopes the sync:
|
|
1843
|
+
* created media Entities, edits that re-point an existing physical-asset
|
|
1844
|
+
* binding or Asset external key, and creations — not untouched pairs.
|
|
1845
|
+
* One-sided facts are skipped silently inside the sync.
|
|
1846
|
+
*/
|
|
1847
|
+
async function attachGenerationSync(docId, plan, result, baseState) {
|
|
1848
|
+
if (result.kind !== "committed" || options.loadGenerationFacts === void 0 || baseState === void 0) return result;
|
|
1849
|
+
let outcome;
|
|
1850
|
+
try {
|
|
1851
|
+
outcome = await syncGeneratedRelations({
|
|
1852
|
+
client: getEntityClient(docId),
|
|
1853
|
+
docId,
|
|
1854
|
+
baseState,
|
|
1855
|
+
entityCommands: plan.entity_commands,
|
|
1856
|
+
loadFacts: options.loadGenerationFacts
|
|
1857
|
+
});
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
outcome = {
|
|
1860
|
+
status: "failed",
|
|
1861
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
const warnings = outcome.status === "failed" ? [{
|
|
1865
|
+
kind: "generation_sync_failed",
|
|
1866
|
+
message: outcome.message ?? "generation lineage sync failed"
|
|
1867
|
+
}] : void 0;
|
|
1868
|
+
return {
|
|
1869
|
+
...result,
|
|
1870
|
+
generation_sync: outcome,
|
|
1871
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1872
|
+
};
|
|
1616
1873
|
}
|
|
1617
1874
|
async function observePull(doc) {
|
|
1618
1875
|
const result = await doc.pull();
|
|
@@ -1745,7 +2002,7 @@ function createMedeoTool(options) {
|
|
|
1745
2002
|
...result.plan,
|
|
1746
2003
|
doc_id: input.doc_id
|
|
1747
2004
|
};
|
|
1748
|
-
const planId = rememberPlan(input.doc_id, plan);
|
|
2005
|
+
const planId = rememberPlan(input.doc_id, plan, entityState);
|
|
1749
2006
|
const base = {
|
|
1750
2007
|
ok: true,
|
|
1751
2008
|
op: "run-edit-script",
|
|
@@ -1762,8 +2019,8 @@ function createMedeoTool(options) {
|
|
|
1762
2019
|
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1763
2020
|
};
|
|
1764
2021
|
if (input.auto_commit !== true) return base;
|
|
1765
|
-
const commit = await commitCachedPlan(input.doc_id, doc, plan);
|
|
1766
|
-
recordPushResult(input.doc_id, planId, plan, commit);
|
|
2022
|
+
const commit = await commitCachedPlan(input.doc_id, doc, plan, void 0, entityState);
|
|
2023
|
+
recordPushResult(input.doc_id, planId, plan, commit, entityState);
|
|
1767
2024
|
const warnings = mergeWarnings(pull.warnings, commitWarnings(commit));
|
|
1768
2025
|
return {
|
|
1769
2026
|
...base,
|
|
@@ -1779,8 +2036,8 @@ function createMedeoTool(options) {
|
|
|
1779
2036
|
const pending = pendingPushes.get(input.doc_id);
|
|
1780
2037
|
if (pending != null) {
|
|
1781
2038
|
if (pending.planId !== input.plan_id) throw new Error(`doc ${input.doc_id} has an unconfirmed push for plan_id ${pending.planId}; retry it before ${input.plan_id}`);
|
|
1782
|
-
const result = pending.kind === "timeline" ? await retryPlanPush(doc, pending.opsApplied) : await commitCachedPlan(input.doc_id, doc, pending.plan, input.validation);
|
|
1783
|
-
recordPushResult(input.doc_id, input.plan_id, pending.plan, result);
|
|
2039
|
+
const result = pending.kind === "timeline" ? await retryPlanPush(doc, pending.opsApplied) : await commitCachedPlan(input.doc_id, doc, pending.plan, input.validation, pending.baseState);
|
|
2040
|
+
recordPushResult(input.doc_id, input.plan_id, pending.plan, result, pending.kind === "entities" ? pending.baseState : void 0);
|
|
1784
2041
|
const warnings = commitWarnings(result);
|
|
1785
2042
|
return {
|
|
1786
2043
|
ok: true,
|
|
@@ -1797,8 +2054,8 @@ function createMedeoTool(options) {
|
|
|
1797
2054
|
const cached = plans.get(input.plan_id);
|
|
1798
2055
|
if (cached == null || cached.docId !== input.doc_id) throw new Error(`plan_id ${input.plan_id} is not available for doc ${input.doc_id}`);
|
|
1799
2056
|
const pull = cached.plan.plan_kind === "timeline" ? await observePull(doc) : { collaborated: false };
|
|
1800
|
-
const result = await commitCachedPlan(input.doc_id, doc, cached.plan, input.validation);
|
|
1801
|
-
recordPushResult(input.doc_id, input.plan_id, cached.plan, result);
|
|
2057
|
+
const result = await commitCachedPlan(input.doc_id, doc, cached.plan, input.validation, cached.baseState);
|
|
2058
|
+
recordPushResult(input.doc_id, input.plan_id, cached.plan, result, cached.baseState);
|
|
1802
2059
|
const warnings = mergeWarnings(pull.warnings, commitWarnings(result));
|
|
1803
2060
|
return {
|
|
1804
2061
|
ok: true,
|