videobook-engine 5.3.3 → 5.4.0

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.
Files changed (69) hide show
  1. package/README.md +6 -0
  2. package/dist/catalog-metadata.d.ts +1 -1
  3. package/dist/catalog-metadata.js +1 -1
  4. package/dist/domain.js +15 -5
  5. package/dist/domain.js.map +1 -1
  6. package/dist/engine-types.d.ts +140 -1
  7. package/dist/engine-types.d.ts.map +1 -1
  8. package/dist/engine-types.js.map +1 -1
  9. package/dist/engine.d.ts +21 -0
  10. package/dist/engine.d.ts.map +1 -1
  11. package/dist/engine.js +3 -0
  12. package/dist/engine.js.map +1 -1
  13. package/dist/fork.d.ts +3 -4
  14. package/dist/fork.d.ts.map +1 -1
  15. package/dist/fork.js +4 -4
  16. package/dist/fork.js.map +1 -1
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +4 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/merge-policy.d.ts.map +1 -1
  22. package/dist/merge-policy.js +2 -0
  23. package/dist/merge-policy.js.map +1 -1
  24. package/dist/migrate-tags-v24.d.ts +12 -0
  25. package/dist/migrate-tags-v24.d.ts.map +1 -0
  26. package/dist/migrate-tags-v24.js +40 -0
  27. package/dist/migrate-tags-v24.js.map +1 -0
  28. package/dist/mvp-contracts.d.ts +2 -2
  29. package/dist/mvp-contracts.js +1 -1
  30. package/dist/schema.d.ts +3 -2
  31. package/dist/schema.d.ts.map +1 -1
  32. package/dist/schema.js +59 -0
  33. package/dist/schema.js.map +1 -1
  34. package/dist/store.d.ts +6 -0
  35. package/dist/store.d.ts.map +1 -1
  36. package/dist/store.js +150 -54
  37. package/dist/store.js.map +1 -1
  38. package/dist/tag-merge.d.ts +4 -0
  39. package/dist/tag-merge.d.ts.map +1 -0
  40. package/dist/tag-merge.js +85 -0
  41. package/dist/tag-merge.js.map +1 -0
  42. package/dist/tag-queries.d.ts +30 -0
  43. package/dist/tag-queries.d.ts.map +1 -0
  44. package/dist/tag-queries.js +313 -0
  45. package/dist/tag-queries.js.map +1 -0
  46. package/dist/tag-rows.d.ts +36 -0
  47. package/dist/tag-rows.d.ts.map +1 -0
  48. package/dist/tag-rows.js +48 -0
  49. package/dist/tag-rows.js.map +1 -0
  50. package/dist/tag-transfer.d.ts +26 -0
  51. package/dist/tag-transfer.d.ts.map +1 -0
  52. package/dist/tag-transfer.js +341 -0
  53. package/dist/tag-transfer.js.map +1 -0
  54. package/dist/tag-validation.d.ts +15 -0
  55. package/dist/tag-validation.d.ts.map +1 -0
  56. package/dist/tag-validation.js +36 -0
  57. package/dist/tag-validation.js.map +1 -0
  58. package/dist/tag-values.d.ts +68 -0
  59. package/dist/tag-values.d.ts.map +1 -0
  60. package/dist/tag-values.js +107 -0
  61. package/dist/tag-values.js.map +1 -0
  62. package/dist/tags.d.ts +44 -0
  63. package/dist/tags.d.ts.map +1 -0
  64. package/dist/tags.js +488 -0
  65. package/dist/tags.js.map +1 -0
  66. package/docs/engine-layout.md +89 -1
  67. package/docs/release-evidence.md +18 -1
  68. package/fixtures/v5/contract-fixtures.json +3 -3
  69. package/package.json +5 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../src/tags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAGhB,oBAAoB,EAEpB,WAAW,EACX,wBAAwB,EACxB,MAAM,EACP,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,aAAa,EAA0B,MAAM,cAAc,CAAC;AAMrE,OAAO,KAAK,EAEV,gBAAgB,EAChB,QAAQ,EACT,MAAM,iBAAiB,CAAC;AASzB,iDAAiD;AACjD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAiCtC,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa;uBAQ7B,MAAM,KAAG,MAAM,CAAC,gBAAgB,EAAE,WAAW,CAAC;4BAGlD,SAAS,MAAM,EAAE,KAC7B,MAAM,CAAC,gBAAgB,EAAE,EAAE,WAAW,CAAC;sBAG5B,MAAM,OACb,QAAQ,KACZ,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;0BAGnC,MAAM,QACZ,SAAS,QAAQ,EAAE,KACxB,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;yBAGnC,MAAM,OACb,gBAAgB,KACpB,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;0BAGnC,MAAM,OACb,gBAAgB,KACpB,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;;+BAIjC,MAAM,KACjB,MAAM,CAAC,oBAAoB,GAAG,SAAS,EAAE,WAAW,CAAC;yBAG/C,wBAAwB,KAC9B,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;;;;;;;;aAlB/C,CAAC;;;EAsBR"}
package/dist/tags.js ADDED
@@ -0,0 +1,488 @@
1
+ /**
2
+ * Asset tags: durable automatic and user-managed tagging.
3
+ *
4
+ * The engine owns state, normalization, transactions and provenance; the
5
+ * consumer owns inference, scheduling and vocabulary. Three rules shape
6
+ * everything here:
7
+ *
8
+ * 1. Semantic identity is (facet, canonical key). Two facets carrying the
9
+ * same word stay distinct assignments.
10
+ * 2. Manual intent outranks analysis. An automatic snapshot replaces only
11
+ * the automatic rows; it can never add, drop or relabel a manual row,
12
+ * and it can never clear a dismissal.
13
+ * 3. Automatic output is fenced to the content it described. A snapshot
14
+ * records the analyzed source hash and a per-artifact generation, so a
15
+ * late result cannot overwrite a newer one and tags for replaced
16
+ * content drop out of the effective set instead of lying.
17
+ */
18
+ import { ok } from "./engine-types.js";
19
+ import { resultOf, syncResultOf } from "./context.js";
20
+ import { EngineFault } from "./store.js";
21
+ import { createTagQueriesApi } from "./tag-queries.js";
22
+ import { automaticTagsMatch, compareTags, snapshotMatches } from "./tag-rows.js";
23
+ import { createTagTransferApi } from "./tag-transfer.js";
24
+ import { normalizeAutomaticTags } from "./tag-validation.js";
25
+ import { MANUAL_TAGS_PER_ARTIFACT_MAX, TAG_DISMISSALS_PER_ARTIFACT_MAX, normalizeTagIdentity, normalizeTagList, tagIdentity, } from "./tag-values.js";
26
+ /** Artifacts a single batched read may cover. */
27
+ export const TAG_READ_BATCH_MAX = 500;
28
+ const READ_CHUNK = 200;
29
+ export function createTagsApi(context) {
30
+ const queries = createTagQueriesApi(context);
31
+ const transfer = createTagTransferApi(context, (artifactId) => tagState(context, artifactId));
32
+ return {
33
+ ...queries,
34
+ ...transfer,
35
+ read: (artifactId) => syncResultOf(() => readTagState(context, artifactId)),
36
+ readMany: (artifactIds) => syncResultOf(() => readTagStates(context, artifactIds)),
37
+ add: (artifactId, tag) => addManualTags(context, artifactId, [tag]),
38
+ addMany: (artifactId, tags) => addManualTags(context, artifactId, tags),
39
+ remove: (artifactId, tag) => removeTag(context, artifactId, tag),
40
+ restore: (artifactId, tag) => restoreTag(context, artifactId, tag),
41
+ automatic: {
42
+ snapshot: (artifactId) => syncResultOf(() => readSnapshot(context, artifactId)),
43
+ replace: (input) => replaceAutomaticTags(context, input),
44
+ },
45
+ };
46
+ }
47
+ // ---------------------------------------------------------------- reads
48
+ function readTagState(context, artifactReference) {
49
+ const artifact = context.artifactRow(artifactReference);
50
+ return tagState(context, artifact.artifact_id);
51
+ }
52
+ function readTagStates(context, artifactReferences) {
53
+ if (artifactReferences.length > TAG_READ_BATCH_MAX) {
54
+ throw new Error(`Tag reads must cover at most ${TAG_READ_BATCH_MAX} artifacts; received ${artifactReferences.length}`);
55
+ }
56
+ const artifactIds = artifactReferences.map((reference) => context.artifactRow(reference).artifact_id);
57
+ const tags = chunkedRows(context, artifactIds, (placeholders) => `SELECT artifact_id, origin, facet, tag_key, label, entity_id, created_at
58
+ FROM artifact_tags
59
+ WHERE artifact_id IN (${placeholders})
60
+ ORDER BY artifact_id, facet, tag_key`);
61
+ const dismissals = chunkedRows(context, artifactIds, (placeholders) => `SELECT artifact_id, facet, tag_key, label, dismissed_at
62
+ FROM artifact_tag_dismissals
63
+ WHERE artifact_id IN (${placeholders})
64
+ ORDER BY artifact_id, facet, tag_key`);
65
+ const snapshots = chunkedRows(context, artifactIds, (placeholders) => `SELECT artifact_id, source_hash, generator, model, extractor_version,
66
+ tag_count, generation, analyzed_at
67
+ FROM artifact_tag_snapshots
68
+ WHERE artifact_id IN (${placeholders})`);
69
+ const snapshotByArtifact = new Map(snapshots.map((row) => [row.artifact_id, row]));
70
+ const liveHashes = artifactFileHashes(context, artifactIds);
71
+ return artifactIds.map((artifactId) => assembleState(artifactId, tags.filter((row) => row.artifact_id === artifactId), dismissals.filter((row) => row.artifact_id === artifactId), snapshotByArtifact.get(artifactId), liveHashes.get(artifactId) ?? new Set()));
72
+ }
73
+ function tagState(context, artifactId) {
74
+ const tags = context.store.db
75
+ .prepare(`SELECT artifact_id, origin, facet, tag_key, label, entity_id, created_at
76
+ FROM artifact_tags
77
+ WHERE artifact_id=?
78
+ ORDER BY facet, tag_key`)
79
+ .all(artifactId);
80
+ const dismissals = context.store.db
81
+ .prepare(`SELECT artifact_id, facet, tag_key, label, dismissed_at
82
+ FROM artifact_tag_dismissals
83
+ WHERE artifact_id=?
84
+ ORDER BY facet, tag_key`)
85
+ .all(artifactId);
86
+ const snapshot = snapshotRow(context, artifactId);
87
+ return assembleState(artifactId, tags, dismissals, snapshot, artifactFileHashes(context, [artifactId]).get(artifactId) ??
88
+ new Set());
89
+ }
90
+ /**
91
+ * Effective tags: every manual assignment, plus automatic assignments that
92
+ * are neither dismissed nor stale, deduplicated on identity with the
93
+ * manual display label and ownership preferred.
94
+ */
95
+ function assembleState(artifactId, tagRows, dismissalRows, snapshotRow, liveHashes) {
96
+ const dismissed = new Set(dismissalRows.map((row) => tagIdentity(row.facet, row.tag_key)));
97
+ const stale = snapshotRow !== undefined && !liveHashes.has(snapshotRow.source_hash);
98
+ const manual = tagRows
99
+ .filter((row) => row.origin === "manual")
100
+ .map((row) => assetTag(row));
101
+ const automatic = tagRows
102
+ .filter((row) => row.origin === "automatic")
103
+ .map((row) => ({
104
+ ...assetTag(row),
105
+ dismissed: dismissed.has(tagIdentity(row.facet, row.tag_key)),
106
+ stale,
107
+ }));
108
+ const effective = [...manual];
109
+ const claimed = new Set(manual.map((tag) => tagIdentity(tag.facet, tag.key)));
110
+ for (const tag of automatic) {
111
+ if (tag.dismissed || tag.stale)
112
+ continue;
113
+ const identity = tagIdentity(tag.facet, tag.key);
114
+ if (claimed.has(identity))
115
+ continue;
116
+ claimed.add(identity);
117
+ effective.push({
118
+ facet: tag.facet,
119
+ key: tag.key,
120
+ label: tag.label,
121
+ origin: tag.origin,
122
+ ...(tag.entityId ? { entityId: tag.entityId } : {}),
123
+ createdAt: tag.createdAt,
124
+ });
125
+ }
126
+ effective.sort(compareTags);
127
+ return {
128
+ artifactId,
129
+ effective,
130
+ manual,
131
+ automatic,
132
+ dismissed: dismissalRows.map((row) => ({
133
+ facet: row.facet,
134
+ key: row.tag_key,
135
+ label: row.label,
136
+ dismissedAt: row.dismissed_at,
137
+ })),
138
+ ...(snapshotRow
139
+ ? { snapshot: snapshotFromRow(snapshotRow, stale) }
140
+ : {}),
141
+ };
142
+ }
143
+ function assetTag(row) {
144
+ return {
145
+ facet: row.facet,
146
+ key: row.tag_key,
147
+ label: row.label,
148
+ origin: row.origin,
149
+ ...(row.entity_id ? { entityId: row.entity_id } : {}),
150
+ createdAt: row.created_at,
151
+ };
152
+ }
153
+ function readSnapshot(context, artifactReference) {
154
+ const artifact = context.artifactRow(artifactReference);
155
+ const row = snapshotRow(context, artifact.artifact_id);
156
+ if (!row)
157
+ return undefined;
158
+ const hashes = artifactFileHashes(context, [artifact.artifact_id]);
159
+ const live = hashes.get(artifact.artifact_id) ?? new Set();
160
+ return snapshotFromRow(row, !live.has(row.source_hash));
161
+ }
162
+ function snapshotRow(context, artifactId) {
163
+ return context.store.db
164
+ .prepare(`SELECT artifact_id, source_hash, generator, model, extractor_version,
165
+ tag_count, generation, analyzed_at
166
+ FROM artifact_tag_snapshots
167
+ WHERE artifact_id=?`)
168
+ .get(artifactId);
169
+ }
170
+ function snapshotFromRow(row, stale) {
171
+ return {
172
+ artifactId: row.artifact_id,
173
+ sourceHash: row.source_hash,
174
+ generator: row.generator,
175
+ ...(row.model === null ? {} : { model: row.model }),
176
+ extractorVersion: row.extractor_version,
177
+ tagCount: row.tag_count,
178
+ generation: row.generation,
179
+ analyzedAt: row.analyzed_at,
180
+ stale,
181
+ };
182
+ }
183
+ /**
184
+ * Current content hashes per artifact. An automatic snapshot is stale once
185
+ * the hash it analyzed is no longer one of the artifact's files, which is
186
+ * how replaced originals drop their automatic tags without deleting the
187
+ * provenance that explains them.
188
+ */
189
+ function artifactFileHashes(context, artifactIds) {
190
+ const rows = chunkedRows(context, artifactIds, (placeholders) => `SELECT artifact_id, object_hash FROM artifact_files
191
+ WHERE artifact_id IN (${placeholders})`);
192
+ const hashes = new Map();
193
+ for (const row of rows) {
194
+ const existing = hashes.get(row.artifact_id);
195
+ if (existing)
196
+ existing.add(row.object_hash);
197
+ else
198
+ hashes.set(row.artifact_id, new Set([row.object_hash]));
199
+ }
200
+ return hashes;
201
+ }
202
+ function chunkedRows(context, artifactIds, sql) {
203
+ const rows = [];
204
+ const uniqueIds = [...new Set(artifactIds)];
205
+ for (let index = 0; index < uniqueIds.length; index += READ_CHUNK) {
206
+ const chunk = uniqueIds.slice(index, index + READ_CHUNK);
207
+ if (chunk.length === 0)
208
+ continue;
209
+ const placeholders = chunk.map(() => "?").join(", ");
210
+ rows.push(...context.store.db
211
+ .prepare(sql(placeholders))
212
+ .all(...chunk));
213
+ }
214
+ return rows;
215
+ }
216
+ // --------------------------------------------------------------- writes
217
+ async function addManualTags(context, artifactReference, inputs) {
218
+ return resultOf(() => context.store.semanticOperation((commit) => {
219
+ const artifact = context.artifactRow(artifactReference);
220
+ const artifactId = artifact.artifact_id;
221
+ const tags = normalizeTagList(inputs);
222
+ if (tags.length === 0) {
223
+ throw new Error("At least one tag is required");
224
+ }
225
+ for (const tag of tags)
226
+ requireEntity(context, tag);
227
+ const existing = manualRowsByIdentity(context, artifactId);
228
+ const added = tags.filter((tag) => !existing.has(tagIdentity(tag.facet, tag.key)));
229
+ const total = existing.size + added.length;
230
+ if (total > MANUAL_TAGS_PER_ARTIFACT_MAX) {
231
+ throw new EngineFault({
232
+ code: "RESOURCE_EXHAUSTED",
233
+ message: `Artifact ${artifactId} would carry ${total} manual tags; the limit is ` +
234
+ `${MANUAL_TAGS_PER_ARTIFACT_MAX}`,
235
+ details: { artifactId, requested: total, limit: MANUAL_TAGS_PER_ARTIFACT_MAX },
236
+ });
237
+ }
238
+ const dismissed = dismissedIdentities(context, artifactId);
239
+ const changed = tags.filter((tag) => {
240
+ const identity = tagIdentity(tag.facet, tag.key);
241
+ if (dismissed.has(identity))
242
+ return true;
243
+ const row = existing.get(identity);
244
+ if (!row)
245
+ return true;
246
+ return row.label !== tag.label || (row.entity_id ?? undefined) !== tag.entityId;
247
+ });
248
+ if (changed.length === 0)
249
+ return ok(tagState(context, artifactId));
250
+ const mutation = commit({
251
+ operation: "add_artifact_tags",
252
+ tables: ["artifact_tags", "artifact_tag_dismissals"],
253
+ artifactId,
254
+ details: { artifactId, tags: changed.map(identityDetail) },
255
+ writeSet: changed.map((tag) => writeSetKey(artifactId, tag)),
256
+ }, (_operationId, now) => {
257
+ const insert = context.store.db.prepare(`INSERT INTO artifact_tags(
258
+ artifact_id, origin, facet, tag_key, label, entity_id, created_at
259
+ ) VALUES (?, 'manual', ?, ?, ?, ?, ?)
260
+ ON CONFLICT(artifact_id, origin, facet, tag_key) DO UPDATE SET
261
+ label=excluded.label,
262
+ entity_id=excluded.entity_id`);
263
+ const clearDismissal = context.store.db.prepare(`DELETE FROM artifact_tag_dismissals
264
+ WHERE artifact_id=? AND facet=? AND tag_key=?`);
265
+ for (const tag of changed) {
266
+ insert.run(artifactId, tag.facet, tag.key, tag.label, tag.entityId ?? null, now);
267
+ // An explicit add is explicit consent: it clears the durable
268
+ // suppression that a previous removal recorded.
269
+ clearDismissal.run(artifactId, tag.facet, tag.key);
270
+ }
271
+ });
272
+ return ok(tagState(context, artifactId), mutation.revision);
273
+ }));
274
+ }
275
+ async function removeTag(context, artifactReference, input) {
276
+ return resultOf(() => context.store.semanticOperation((commit) => {
277
+ const artifact = context.artifactRow(artifactReference);
278
+ const artifactId = artifact.artifact_id;
279
+ const identity = normalizeTagIdentity(input);
280
+ const manual = manualRowsByIdentity(context, artifactId).get(tagIdentity(identity.facet, identity.key));
281
+ const dismissal = context.store.db
282
+ .prepare(`SELECT artifact_id, facet, tag_key, label, dismissed_at
283
+ FROM artifact_tag_dismissals
284
+ WHERE artifact_id=? AND facet=? AND tag_key=?`)
285
+ .get(artifactId, identity.facet, identity.key);
286
+ if (!manual && dismissal)
287
+ return ok(tagState(context, artifactId));
288
+ if (!dismissal)
289
+ assertDismissalCapacity(context, artifactId);
290
+ // The dismissal keeps the label the user actually saw, so a restored
291
+ // suppression reads the same way it did when it was removed.
292
+ const label = manual?.label ?? automaticLabel(context, artifactId, identity) ??
293
+ identity.label;
294
+ const mutation = commit({
295
+ operation: "remove_artifact_tag",
296
+ tables: ["artifact_tags", "artifact_tag_dismissals"],
297
+ artifactId,
298
+ details: {
299
+ artifactId,
300
+ facet: identity.facet,
301
+ key: identity.key,
302
+ },
303
+ writeSet: [writeSetKey(artifactId, identity)],
304
+ }, (_operationId, now) => {
305
+ context.store.db
306
+ .prepare(`DELETE FROM artifact_tags
307
+ WHERE artifact_id=? AND origin='manual' AND facet=? AND tag_key=?`)
308
+ .run(artifactId, identity.facet, identity.key);
309
+ context.store.db
310
+ .prepare(`INSERT INTO artifact_tag_dismissals(
311
+ artifact_id, facet, tag_key, label, dismissed_at
312
+ ) VALUES (?, ?, ?, ?, ?)
313
+ ON CONFLICT(artifact_id, facet, tag_key) DO UPDATE SET
314
+ label=excluded.label`)
315
+ .run(artifactId, identity.facet, identity.key, label, now);
316
+ });
317
+ return ok(tagState(context, artifactId), mutation.revision);
318
+ }));
319
+ }
320
+ async function restoreTag(context, artifactReference, input) {
321
+ return resultOf(() => context.store.semanticOperation((commit) => {
322
+ const artifact = context.artifactRow(artifactReference);
323
+ const artifactId = artifact.artifact_id;
324
+ const identity = normalizeTagIdentity(input);
325
+ const present = context.store.db
326
+ .prepare(`SELECT 1 AS present FROM artifact_tag_dismissals
327
+ WHERE artifact_id=? AND facet=? AND tag_key=?`)
328
+ .get(artifactId, identity.facet, identity.key);
329
+ if (!present)
330
+ return ok(tagState(context, artifactId));
331
+ const mutation = commit({
332
+ operation: "restore_artifact_tag",
333
+ tables: ["artifact_tag_dismissals"],
334
+ artifactId,
335
+ details: { artifactId, facet: identity.facet, key: identity.key },
336
+ writeSet: [writeSetKey(artifactId, identity)],
337
+ }, () => {
338
+ context.store.db
339
+ .prepare(`DELETE FROM artifact_tag_dismissals
340
+ WHERE artifact_id=? AND facet=? AND tag_key=?`)
341
+ .run(artifactId, identity.facet, identity.key);
342
+ });
343
+ return ok(tagState(context, artifactId), mutation.revision);
344
+ }));
345
+ }
346
+ async function replaceAutomaticTags(context, input) {
347
+ return resultOf(() => context.store.semanticOperation((commit) => {
348
+ const artifact = context.artifactRow(input.artifactId);
349
+ const artifactId = artifact.artifact_id;
350
+ const { sourceHash, generator, extractorVersion, model, tags } = normalizeAutomaticTags(input);
351
+ if (input.expectedGeneration !== undefined &&
352
+ (!Number.isSafeInteger(input.expectedGeneration) || input.expectedGeneration < 0)) {
353
+ throw new Error("Automatic tag expectedGeneration must be a nonnegative integer");
354
+ }
355
+ for (const tag of tags)
356
+ requireEntity(context, tag);
357
+ const current = snapshotRow(context, artifactId);
358
+ const generation = current?.generation ?? 0;
359
+ if (input.expectedGeneration !== undefined &&
360
+ input.expectedGeneration !== generation) {
361
+ throw new EngineFault({
362
+ code: "STALE_REVISION",
363
+ message: `Automatic tags for ${artifactId} expected generation ` +
364
+ `${input.expectedGeneration} but the catalog is at ${generation}`,
365
+ details: {
366
+ artifactId,
367
+ expectedGeneration: input.expectedGeneration,
368
+ generation,
369
+ },
370
+ });
371
+ }
372
+ if (snapshotMatches(current, {
373
+ sourceHash,
374
+ generator,
375
+ ...(model ? { model } : {}),
376
+ extractorVersion,
377
+ }) &&
378
+ automaticTagsMatch(context.store.db, artifactId, tags)) {
379
+ // Re-running the same analyzer over the same bytes with the same
380
+ // answer is not history; it mints no commit and does not advance
381
+ // the fence.
382
+ return ok(tagState(context, artifactId));
383
+ }
384
+ const mutation = commit({
385
+ operation: "replace_automatic_artifact_tags",
386
+ tables: ["artifact_tags", "artifact_tag_snapshots"],
387
+ artifactId,
388
+ details: {
389
+ artifactId,
390
+ sourceHash,
391
+ generator,
392
+ extractorVersion,
393
+ generation: generation + 1,
394
+ tags: tags.map(identityDetail),
395
+ },
396
+ writeSet: [`artifact-tags-automatic:${artifactId}`],
397
+ }, (_operationId, now) => {
398
+ context.store.db
399
+ .prepare("DELETE FROM artifact_tags WHERE artifact_id=? AND origin='automatic'")
400
+ .run(artifactId);
401
+ const insert = context.store.db.prepare(`INSERT INTO artifact_tags(
402
+ artifact_id, origin, facet, tag_key, label, entity_id, created_at
403
+ ) VALUES (?, 'automatic', ?, ?, ?, ?, ?)`);
404
+ for (const tag of tags) {
405
+ insert.run(artifactId, tag.facet, tag.key, tag.label, tag.entityId ?? null, now);
406
+ }
407
+ context.store.db
408
+ .prepare(`INSERT INTO artifact_tag_snapshots(
409
+ artifact_id, source_hash, generator, model, extractor_version,
410
+ tag_count, generation, analyzed_at
411
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
412
+ ON CONFLICT(artifact_id) DO UPDATE SET
413
+ source_hash=excluded.source_hash,
414
+ generator=excluded.generator,
415
+ model=excluded.model,
416
+ extractor_version=excluded.extractor_version,
417
+ tag_count=excluded.tag_count,
418
+ generation=excluded.generation,
419
+ analyzed_at=excluded.analyzed_at`)
420
+ .run(artifactId, sourceHash, generator, model || null, extractorVersion, tags.length, generation + 1, now);
421
+ });
422
+ return ok(tagState(context, artifactId), mutation.revision);
423
+ }));
424
+ }
425
+ // --------------------------------------------------------------- helpers
426
+ function manualRowsByIdentity(context, artifactId) {
427
+ const rows = context.store.db
428
+ .prepare(`SELECT artifact_id, origin, facet, tag_key, label, entity_id, created_at
429
+ FROM artifact_tags
430
+ WHERE artifact_id=? AND origin='manual'`)
431
+ .all(artifactId);
432
+ return new Map(rows.map((row) => [tagIdentity(row.facet, row.tag_key), row]));
433
+ }
434
+ function dismissedIdentities(context, artifactId) {
435
+ const rows = context.store.db
436
+ .prepare("SELECT facet, tag_key FROM artifact_tag_dismissals WHERE artifact_id=?")
437
+ .all(artifactId);
438
+ return new Set(rows.map((row) => tagIdentity(row.facet, row.tag_key)));
439
+ }
440
+ function assertDismissalCapacity(context, artifactId) {
441
+ const row = context.store.db
442
+ .prepare("SELECT COUNT(*) AS total FROM artifact_tag_dismissals WHERE artifact_id=?")
443
+ .get(artifactId);
444
+ if (row.total >= TAG_DISMISSALS_PER_ARTIFACT_MAX) {
445
+ throw new EngineFault({
446
+ code: "RESOURCE_EXHAUSTED",
447
+ message: `Artifact ${artifactId} already carries ${row.total} dismissals; the limit is ` +
448
+ `${TAG_DISMISSALS_PER_ARTIFACT_MAX}`,
449
+ details: {
450
+ artifactId,
451
+ total: row.total,
452
+ limit: TAG_DISMISSALS_PER_ARTIFACT_MAX,
453
+ },
454
+ });
455
+ }
456
+ }
457
+ function automaticLabel(context, artifactId, identity) {
458
+ const row = context.store.db
459
+ .prepare(`SELECT label FROM artifact_tags
460
+ WHERE artifact_id=? AND origin='automatic' AND facet=? AND tag_key=?`)
461
+ .get(artifactId, identity.facet, identity.key);
462
+ return row?.label;
463
+ }
464
+ /**
465
+ * Entity references are carried, never invented: a tag may point at an
466
+ * entity the caller has already confirmed, and the reference must resolve.
467
+ */
468
+ function requireEntity(context, tag) {
469
+ if (!tag.entityId)
470
+ return;
471
+ const row = context.store.db
472
+ .prepare("SELECT 1 AS present FROM entities WHERE entity_id=?")
473
+ .get(tag.entityId);
474
+ if (!row) {
475
+ throw new EngineFault({
476
+ code: "NOT_FOUND",
477
+ message: `Tag entity does not exist: ${tag.entityId}`,
478
+ details: { entityId: tag.entityId },
479
+ });
480
+ }
481
+ }
482
+ function identityDetail(tag) {
483
+ return tagIdentity(tag.facet, tag.key);
484
+ }
485
+ function writeSetKey(artifactId, tag) {
486
+ return `artifact-tag:${artifactId}:${tagIdentity(tag.facet, tag.key)}`;
487
+ }
488
+ //# sourceMappingURL=tags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tags.js","sourceRoot":"","sources":["../src/tags.ts"],"names":[],"mappings":"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"}
@@ -105,7 +105,7 @@ Notation used below:
105
105
  - `→` names the referenced column.
106
106
  - Defaults and checks are shown inline.
107
107
 
108
- There are 34 allowlisted semantic tables.
108
+ There are 36 allowlisted semantic tables.
109
109
 
110
110
  ### Catalog, artifacts, and content
111
111
 
@@ -136,6 +136,89 @@ There are 34 allowlisted semantic tables.
136
136
  | `runs` | `run_id TEXT`<br>`notebook_id TEXT`<br>`status TEXT`<br>`started_at INTEGER`<br>`completed_at INTEGER`<br>`cell_order_json TEXT`<br>`outputs_json TEXT`<br>`error TEXT?` | `run_id PK`; notebook `FK → notebooks ON DELETE CASCADE`; `status CHECK IN (completed, failed, aborted)`. Terminal, versioned notebook execution records. |
137
137
  | `generations` | `generation_id TEXT`<br>`notebook_id TEXT`<br>`cell_id TEXT`<br>`output_cell_id TEXT?`<br>`run_id TEXT?`<br>`status TEXT`<br>`tool TEXT`<br>`provider TEXT?`<br>`model TEXT?`<br>`prompt TEXT?`<br>`resolved_prompt TEXT?`<br>`provider_artifact_id TEXT?`<br>`output_artifact_id TEXT?`<br>`error TEXT?`<br>`created_at INTEGER`<br>`updated_at INTEGER` | `generation_id PK`; composite cell FK with cascade; `status CHECK IN (dispatched, awaiting_provider, completed, failed)`. One row per generation attempt; every transition is its own attributed semantic commit, so `dolt_history_generations` is the per-attempt timeline. |
138
138
 
139
+ ### Asset tags
140
+
141
+ Flat, book-local tags on artifacts. Semantic identity is the pair
142
+ (`facet`, `tag_key`) — never the display label alone — so the same word in
143
+ two facets stays two assignments. Manual and automatic assignments share
144
+ one table keyed by `origin`: replacing an automatic snapshot deletes and
145
+ reinserts only the `automatic` rows, so it can never add, drop or relabel
146
+ a manual row. Dismissals are a separate durable record of user intent that
147
+ outlives any snapshot, which is what stops re-analysis from resurrecting a
148
+ tag the user removed.
149
+
150
+ | Table | Columns | Keys, constraints, and purpose |
151
+ | --- | --- | --- |
152
+ | `artifact_tags` | `artifact_id TEXT`<br>`origin TEXT`<br>`facet TEXT`<br>`tag_key TEXT`<br>`label TEXT`<br>`entity_id TEXT?`<br>`created_at INTEGER` | `(artifact_id, origin, facet, tag_key) PK`; `artifact_id FK → artifacts.artifact_id ON DELETE CASCADE`; `entity_id FK → entities.entity_id ON DELETE RESTRICT`; `origin CHECK IN (manual, automatic)`; `facet CHECK IN (people, places, editing, custom)`. `tag_key` is the NFKC-normalized, whitespace-collapsed, lowercased key; `label` is the display form. A tag may carry an already-confirmed entity reference, and holding one keeps that entity alive (`engine.entities.delete` returns `IN_USE`). |
153
+ | `artifact_tag_dismissals` | `artifact_id TEXT`<br>`facet TEXT`<br>`tag_key TEXT`<br>`label TEXT`<br>`dismissed_at INTEGER` | `(artifact_id, facet, tag_key) PK`; `artifact_id FK → artifacts.artifact_id ON DELETE CASCADE`. One row per suppressed identity. Suppression is applied when tags are read, so the automatic row keeps the provenance that explains it while staying invisible. Adding the identity manually deletes the row; `engine.tags.restore` deletes it without claiming ownership. |
154
+ | `artifact_tag_snapshots` | `artifact_id TEXT`<br>`source_hash TEXT`<br>`generator TEXT`<br>`model TEXT?`<br>`extractor_version TEXT`<br>`tag_count INTEGER`<br>`generation INTEGER`<br>`analyzed_at INTEGER` | `artifact_id PK`; `artifact_id FK → artifacts.artifact_id ON DELETE CASCADE`; `tag_count CHECK >= 0`; `generation CHECK > 0`. The last successful automatic analysis. A row with `tag_count` 0 is a successful empty result; no row at all means no successful analysis. `extractor_version` includes the editorial tag-policy version so a policy change can drive selective refresh. `generation` is the per-artifact fence: a caller may pass `expectedGeneration` and a mismatch is refused with `STALE_REVISION`. |
155
+
156
+ `source_hash` is a content fingerprint, deliberately **not** an `objects`
157
+ foreign key and not a first-class `object_hash` column: tags neither pin
158
+ bytes against GC nor block forgetting. An automatic snapshot is stale once
159
+ that hash is no longer one of the artifact's `artifact_files` rows, and
160
+ stale automatic tags drop out of the effective set instead of describing
161
+ content that is gone.
162
+
163
+ Effective tags are every manual assignment plus every automatic assignment
164
+ that is neither dismissed nor stale, deduplicated on identity with the
165
+ manual label and ownership preferred. Documented limits: 64 Unicode code
166
+ points per label, 100 manual tags per artifact, 12 automatic tags per
167
+ snapshot, 500 dismissals per artifact. Exceeding one is refused with
168
+ `RESOURCE_EXHAUSTED` rather than silently dropping user intent.
169
+
170
+ Automatic analysis uses the `people`, `places`, and `editing` facets;
171
+ `custom` is reserved for manual tags. Writes check generation fences,
172
+ capacity, and no-op decisions inside the same serialized SQL transaction
173
+ as their mutation. Imports apply the same automatic metadata and size
174
+ validation, and count duplicate dismissals only once. The browser-safe
175
+ `videobook-engine/tag-values` subpath exposes normalization and limits.
176
+
177
+ `engine.tags.query` filters artifacts on that same effective set: `all`
178
+ (every identity), `any` (at least one), and `kinds`, with keyset paging and
179
+ a total that counts every match rather than the current page.
180
+ `engine.tags.candidates` returns the unpaged matching IDs so the
181
+ application can compose a tag filter with temporal or semantic search
182
+ before either applies a limit. `engine.tags.facets` counts distinct
183
+ matching artifacts per identity, and `engine.tags.suggest` completes known
184
+ keys by prefix and facet; both prefer the manual display label. The
185
+ effective-tag rule is written once as SQL in `src/tag-queries.ts` and once
186
+ in TypeScript in `src/tags.ts`, and `tests/tag-queries.test.ts` asserts the
187
+ two agree.
188
+
189
+ Tag rows are ordinary semantic rows, so history needs no special case:
190
+ `history.restore` reloads them from their `dolt_at_*` projections like
191
+ every other table, and an explicit restore may therefore rewind manual
192
+ edits that routine re-analysis never can. Restoring a revision recorded
193
+ under an older schema stays refused by the existing version guard.
194
+ `engine.tags.readAtRevision` reports the state a revision recorded; a
195
+ revision written before schema 25 has no tag state at all and yields an
196
+ empty snapshot rather than an error.
197
+
198
+ `engine.tags.export` produces a portable snapshot — manual tags,
199
+ dismissals, and automatic evidence only while it is still valid — and
200
+ `engine.tags.import` applies one to another artifact, in the same catalog
201
+ or a different engine. Import never trusts the source: labels are
202
+ re-normalized into destination-local identities, an entity reference
203
+ survives only when that entity exists in the destination, and automatic
204
+ evidence transfers only when the destination artifact carries the exact
205
+ analyzed hash. A hash-identical duplicate therefore inherits automatic
206
+ tags; a modified or generated derivative inherits only manual intent, with
207
+ `skippedAutomatic` saying so.
208
+ Re-importing unchanged, valid evidence is a no-op and leaves
209
+ `skippedAutomatic` false.
210
+
211
+ Merges treat an automatic snapshot and its tag rows as one unit. Competing
212
+ answers for the same artifact conflict even when their metadata agrees;
213
+ the engine never combines two automatic sets into an invented analysis.
214
+ Merges also reject combined manual or dismissal sets that exceed the
215
+ per-artifact limits, before changing the accepted catalog.
216
+
217
+ Query cost is fixed, not proportional to catalog size: `query` is three
218
+ statements (page, total, page tags), `candidates` one, and `facets` two
219
+ (counts, then preferred labels). `tests/tag-queries.test.ts` measures those
220
+ counts against a catalog ten times larger and asserts they do not move.
221
+
139
222
  ### Sequence timeline and media editing state
140
223
 
141
224
  Sequences are the single timeline model. `sequences`, `sequence_tracks`,
@@ -361,6 +444,9 @@ schema additionally defines every index below.
361
444
  | `prompt_entries_lookup` | `prompt_entries(surface, created_at, prompt_id)` |
362
445
  | `messages_created` | `messages(created_at, message_id)` |
363
446
  | `generations_cell` | `generations(notebook_id, cell_id, created_at)` |
447
+ | `artifact_tags_identity` | `artifact_tags(facet, tag_key, artifact_id)` |
448
+ | `artifact_tags_entity` | `artifact_tags(entity_id)` |
449
+ | `artifact_tag_dismissals_identity` | `artifact_tag_dismissals(facet, tag_key, artifact_id)` |
364
450
 
365
451
  ## Local-only runtime schema
366
452
 
@@ -728,6 +814,8 @@ must reproduce a render.
728
814
  | Content-addressed object layout, remote keys, deletion | [`src/cas.ts`](../src/cas.ts) |
729
815
  | Object publication, deletion, GC, and catalog backup | [`src/storage.ts`](../src/storage.ts) |
730
816
  | Artifact mappings and workspace materialization | [`src/files.ts`](../src/files.ts) |
817
+ | Asset tag state, precedence, and snapshots | [`src/tags.ts`](../src/tags.ts), [`src/tag-values.ts`](../src/tag-values.ts) |
818
+ | Asset tag schema upgrade (24 to 25) | [`src/migrate-tags-v24.ts`](../src/migrate-tags-v24.ts) |
731
819
  | Sequence reads/structure and transactional edits | [`src/sequences.ts`](../src/sequences.ts), [`src/edits.ts`](../src/edits.ts) |
732
820
  | Public manifest, job, status, and similarity types | [`src/engine-types.ts`](../src/engine-types.ts) |
733
821
  | Entity/notebook graph types | [`src/notebook/types.ts`](../src/notebook/types.ts) |