@de-otio/trellis 0.11.0 → 0.12.1
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/dist/env.d.ts +168 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +155 -0
- package/dist/env.js.map +1 -1
- package/dist/lambda/media-completion-worker.d.ts +175 -0
- package/dist/lambda/media-completion-worker.d.ts.map +1 -0
- package/dist/lambda/media-completion-worker.js +373 -0
- package/dist/lambda/media-completion-worker.js.map +1 -0
- package/dist/lambda/media-processing-worker.d.ts +172 -1
- package/dist/lambda/media-processing-worker.d.ts.map +1 -1
- package/dist/lambda/media-processing-worker.js +343 -49
- package/dist/lambda/media-processing-worker.js.map +1 -1
- package/dist/lib/exif-stripper.d.ts +37 -22
- package/dist/lib/exif-stripper.d.ts.map +1 -1
- package/dist/lib/exif-stripper.js +101 -41
- package/dist/lib/exif-stripper.js.map +1 -1
- package/dist/lib/media/cas-keys.d.ts +63 -0
- package/dist/lib/media/cas-keys.d.ts.map +1 -0
- package/dist/lib/media/cas-keys.js +102 -0
- package/dist/lib/media/cas-keys.js.map +1 -0
- package/dist/lib/media/classify-worker-error.d.ts +48 -0
- package/dist/lib/media/classify-worker-error.d.ts.map +1 -0
- package/dist/lib/media/classify-worker-error.js +319 -0
- package/dist/lib/media/classify-worker-error.js.map +1 -0
- package/dist/lib/media/dedupe-key.d.ts +29 -0
- package/dist/lib/media/dedupe-key.d.ts.map +1 -0
- package/dist/lib/media/dedupe-key.js +49 -0
- package/dist/lib/media/dedupe-key.js.map +1 -0
- package/dist/lib/media/duration-cap.d.ts +30 -0
- package/dist/lib/media/duration-cap.d.ts.map +1 -0
- package/dist/lib/media/duration-cap.js +37 -0
- package/dist/lib/media/duration-cap.js.map +1 -0
- package/dist/lib/media/ffmpeg-args.d.ts +83 -0
- package/dist/lib/media/ffmpeg-args.d.ts.map +1 -0
- package/dist/lib/media/ffmpeg-args.js +119 -0
- package/dist/lib/media/ffmpeg-args.js.map +1 -0
- package/dist/lib/media/media-ports.d.ts +126 -0
- package/dist/lib/media/media-ports.d.ts.map +1 -0
- package/dist/lib/media/media-ports.js +129 -0
- package/dist/lib/media/media-ports.js.map +1 -0
- package/dist/lib/media/media-upsert.d.ts +55 -0
- package/dist/lib/media/media-upsert.d.ts.map +1 -0
- package/dist/lib/media/media-upsert.js +38 -0
- package/dist/lib/media/media-upsert.js.map +1 -0
- package/dist/lib/media/moderation-provider.d.ts +111 -0
- package/dist/lib/media/moderation-provider.d.ts.map +1 -0
- package/dist/lib/media/moderation-provider.js +130 -0
- package/dist/lib/media/moderation-provider.js.map +1 -0
- package/dist/lib/media/moderation-resolved-payload.d.ts +48 -0
- package/dist/lib/media/moderation-resolved-payload.d.ts.map +1 -0
- package/dist/lib/media/moderation-resolved-payload.js +37 -0
- package/dist/lib/media/moderation-resolved-payload.js.map +1 -0
- package/dist/lib/media/moderation-status.d.ts +98 -0
- package/dist/lib/media/moderation-status.d.ts.map +1 -0
- package/dist/lib/media/moderation-status.js +122 -0
- package/dist/lib/media/moderation-status.js.map +1 -0
- package/dist/lib/media/processing-types.d.ts +45 -0
- package/dist/lib/media/processing-types.d.ts.map +1 -0
- package/dist/lib/media/processing-types.js +9 -0
- package/dist/lib/media/processing-types.js.map +1 -0
- package/dist/lib/media/promote-decision.d.ts +64 -0
- package/dist/lib/media/promote-decision.d.ts.map +1 -0
- package/dist/lib/media/promote-decision.js +76 -0
- package/dist/lib/media/promote-decision.js.map +1 -0
- package/dist/lib/media/quota-check.d.ts +22 -0
- package/dist/lib/media/quota-check.d.ts.map +1 -0
- package/dist/lib/media/quota-check.js +42 -0
- package/dist/lib/media/quota-check.js.map +1 -0
- package/dist/lib/media/quota-types.d.ts +15 -0
- package/dist/lib/media/quota-types.d.ts.map +1 -0
- package/dist/lib/media/quota-types.js +9 -0
- package/dist/lib/media/quota-types.js.map +1 -0
- package/dist/lib/media/route-upload.d.ts +58 -0
- package/dist/lib/media/route-upload.d.ts.map +1 -0
- package/dist/lib/media/route-upload.js +80 -0
- package/dist/lib/media/route-upload.js.map +1 -0
- package/dist/lib/media/serve-gate.d.ts +51 -0
- package/dist/lib/media/serve-gate.d.ts.map +1 -0
- package/dist/lib/media/serve-gate.js +68 -0
- package/dist/lib/media/serve-gate.js.map +1 -0
- package/dist/lib/media/tenant-resolution.d.ts +42 -0
- package/dist/lib/media/tenant-resolution.d.ts.map +1 -0
- package/dist/lib/media/tenant-resolution.js +45 -0
- package/dist/lib/media/tenant-resolution.js.map +1 -0
- package/dist/lib/media/text-moderation.d.ts +28 -0
- package/dist/lib/media/text-moderation.d.ts.map +1 -0
- package/dist/lib/media/text-moderation.js +62 -0
- package/dist/lib/media/text-moderation.js.map +1 -0
- package/dist/lib/media/track-verdict.d.ts +45 -0
- package/dist/lib/media/track-verdict.d.ts.map +1 -0
- package/dist/lib/media/track-verdict.js +52 -0
- package/dist/lib/media/track-verdict.js.map +1 -0
- package/dist/lib/media/transcript-moderation.d.ts +47 -0
- package/dist/lib/media/transcript-moderation.d.ts.map +1 -0
- package/dist/lib/media/transcript-moderation.js +70 -0
- package/dist/lib/media/transcript-moderation.js.map +1 -0
- package/dist/lib/media-handler.d.ts.map +1 -1
- package/dist/lib/media-handler.js +15 -9
- package/dist/lib/media-handler.js.map +1 -1
- package/dist/lib/post-handler.d.ts.map +1 -1
- package/dist/lib/post-handler.js +4 -1
- package/dist/lib/post-handler.js.map +1 -1
- package/dist/lib/route-helpers.d.ts.map +1 -1
- package/dist/lib/route-helpers.js +9 -1
- package/dist/lib/route-helpers.js.map +1 -1
- package/dist/lib/routes/media.d.ts +21 -0
- package/dist/lib/routes/media.d.ts.map +1 -1
- package/dist/lib/routes/media.js +584 -483
- package/dist/lib/routes/media.js.map +1 -1
- package/dist/lib/services/image-normalizer.d.ts +64 -6
- package/dist/lib/services/image-normalizer.d.ts.map +1 -1
- package/dist/lib/services/image-normalizer.js +88 -6
- package/dist/lib/services/image-normalizer.js.map +1 -1
- package/dist/lib/services/media-upload-service.d.ts +2 -2
- package/dist/lib/services/media-upload-service.d.ts.map +1 -1
- package/dist/lib/services/media-upload-service.js +22 -21
- package/dist/lib/services/media-upload-service.js.map +1 -1
- package/dist/lib/tenant-scope.d.ts.map +1 -1
- package/dist/lib/tenant-scope.js +16 -1
- package/dist/lib/tenant-scope.js.map +1 -1
- package/package.json +2 -1
- package/prisma/migrations/20260625000000_media_tenant_scope_and_moderation_status/migration.sql +49 -0
- package/prisma/migrations/20260625000001_p0b_moderation_jobs/migration.sql +73 -0
- package/prisma/schema.prisma +95 -17
- package/src/lambda/media-completion-worker.ts +567 -0
- package/src/lambda/media-processing-worker.ts +508 -59
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// CONTRACT: stable — coordinate changes.
|
|
2
|
+
//
|
|
3
|
+
// The MediaModerationProvider capability seam — THE interface every media
|
|
4
|
+
// moderation backend binds to, mirroring the RealtimeTransport seam
|
|
5
|
+
// (apps/api/src/lib/realtime/). Core ships the interface plus a fail-closed
|
|
6
|
+
// Null provider and a test-only Mock; the consuming app (Skybber) injects the
|
|
7
|
+
// concrete cloud adapter at startup. Core imports no cloud SDK.
|
|
8
|
+
//
|
|
9
|
+
// Binding rules for every provider implementor:
|
|
10
|
+
// - A verdict carries a 3-value `decision` (approved | review | quarantine).
|
|
11
|
+
// `rejected` is a lifecycle state reached only by human/CSAM action, never a
|
|
12
|
+
// provider decision (see ModerationStatus state machine).
|
|
13
|
+
// - Absence of signal, an internal fault, or any uncertainty MUST fail closed
|
|
14
|
+
// to `review`. A provider must NEVER manufacture `approved` from doubt.
|
|
15
|
+
// - This interface ships in the PUBLIC npm tarball: NO thresholds, secrets, or
|
|
16
|
+
// real-category vocabulary live here. Labels carry opaque category tokens;
|
|
17
|
+
// the operative numeric thresholds are injected via Env (config), not code.
|
|
18
|
+
// - CSAM detection is deliberately NOT on this interface — it is a separate,
|
|
19
|
+
// statutory provider with preserve-and-report duties.
|
|
20
|
+
// - Refs are opaque (key + bucket handle), never raw image/video bytes.
|
|
21
|
+
const NULL_PROVIDER_NAME = "null";
|
|
22
|
+
const NULL_PROVIDER_WARNING = "[NullModerationProvider] No moderation backend injected — failing closed to" +
|
|
23
|
+
' decision="review". Media will NOT auto-approve. Inject a real provider in' +
|
|
24
|
+
" any non-dev environment.";
|
|
25
|
+
/**
|
|
26
|
+
* A verdict that fails closed: every call resolves to `review` with no labels.
|
|
27
|
+
* Nothing this provider returns can ever auto-approve media. Used as the safe
|
|
28
|
+
* default before a concrete provider is injected (dev only — see the startup
|
|
29
|
+
* guard below).
|
|
30
|
+
*/
|
|
31
|
+
export class NullModerationProvider {
|
|
32
|
+
warn;
|
|
33
|
+
constructor(warn = (msg, data) => console.warn(msg, data)) {
|
|
34
|
+
this.warn = warn;
|
|
35
|
+
}
|
|
36
|
+
failClosed() {
|
|
37
|
+
this.warn(NULL_PROVIDER_WARNING);
|
|
38
|
+
return { decision: "review", labels: [], provider: NULL_PROVIDER_NAME };
|
|
39
|
+
}
|
|
40
|
+
async moderateImage(_input) {
|
|
41
|
+
return this.failClosed();
|
|
42
|
+
}
|
|
43
|
+
async startVideoModeration(_input) {
|
|
44
|
+
// Even starting a job warns: there is no backend to do the work.
|
|
45
|
+
this.warn(NULL_PROVIDER_WARNING);
|
|
46
|
+
return { jobId: `${NULL_PROVIDER_NAME}-noop` };
|
|
47
|
+
}
|
|
48
|
+
async getVideoModeration(_jobId) {
|
|
49
|
+
return this.failClosed();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Returns true for the fail-closed Null provider. The startup guard uses this to
|
|
54
|
+
* reject Null outside dev.
|
|
55
|
+
*/
|
|
56
|
+
export function isNullModerationProvider(provider) {
|
|
57
|
+
return provider instanceof NullModerationProvider;
|
|
58
|
+
}
|
|
59
|
+
const MOCK_PROVIDER_NAME = "mock";
|
|
60
|
+
const MOCK_DEFAULT_VERDICT = {
|
|
61
|
+
decision: "review",
|
|
62
|
+
labels: [],
|
|
63
|
+
provider: MOCK_PROVIDER_NAME,
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* A test seam: returns canned verdicts on demand. Default is the fail-closed
|
|
67
|
+
* `review`. Labels use ONLY abstract category tokens (`category_a`,
|
|
68
|
+
* `category_b`); no real-category strings, no real imagery ever.
|
|
69
|
+
*/
|
|
70
|
+
export class MockModerationProvider {
|
|
71
|
+
imageVerdict;
|
|
72
|
+
videoVerdict;
|
|
73
|
+
jobIdSeq = 0;
|
|
74
|
+
constructor(canned = {}) {
|
|
75
|
+
this.imageVerdict = canned.image ?? MOCK_DEFAULT_VERDICT;
|
|
76
|
+
this.videoVerdict = canned.video ?? MOCK_DEFAULT_VERDICT;
|
|
77
|
+
}
|
|
78
|
+
/** Program the verdict returned by `moderateImage`. */
|
|
79
|
+
setImageVerdict(verdict) {
|
|
80
|
+
this.imageVerdict = verdict;
|
|
81
|
+
}
|
|
82
|
+
/** Program the verdict returned by `getVideoModeration`. */
|
|
83
|
+
setVideoVerdict(verdict) {
|
|
84
|
+
this.videoVerdict = verdict;
|
|
85
|
+
}
|
|
86
|
+
async moderateImage(_input) {
|
|
87
|
+
return this.imageVerdict;
|
|
88
|
+
}
|
|
89
|
+
async startVideoModeration(_input) {
|
|
90
|
+
this.jobIdSeq += 1;
|
|
91
|
+
return { jobId: `${MOCK_PROVIDER_NAME}-job-${this.jobIdSeq}` };
|
|
92
|
+
}
|
|
93
|
+
async getVideoModeration(_jobId) {
|
|
94
|
+
return this.videoVerdict;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Abstract category tokens for Mock verdicts — never real-category strings. */
|
|
98
|
+
export const MOCK_CATEGORY_A = "category_a";
|
|
99
|
+
export const MOCK_CATEGORY_B = "category_b";
|
|
100
|
+
/**
|
|
101
|
+
* Error raised by the startup guard when the fail-closed Null provider would run
|
|
102
|
+
* outside dev. Carrying a distinct type lets the wiring fail loudly and lets
|
|
103
|
+
* tests assert on it.
|
|
104
|
+
*/
|
|
105
|
+
export class NullProviderInProductionError extends Error {
|
|
106
|
+
constructor(environment) {
|
|
107
|
+
super(`NullModerationProvider must not run in environment "${environment}" — a` +
|
|
108
|
+
" real moderation provider must be injected outside dev. Refusing to" +
|
|
109
|
+
" start: an un-moderated, fail-closed backend in production silently" +
|
|
110
|
+
" sends all media to review with no path to approval.");
|
|
111
|
+
this.name = "NullProviderInProductionError";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Startup guard for the seam wiring. Validates that a non-Null provider is
|
|
116
|
+
* injected whenever `environment !== "dev"`, and throws loudly otherwise.
|
|
117
|
+
* Returns the provider unchanged when the check passes, so it can wrap the
|
|
118
|
+
* injection site directly:
|
|
119
|
+
*
|
|
120
|
+
* const provider = assertModerationProviderAllowed(injected, env.ENVIRONMENT);
|
|
121
|
+
*
|
|
122
|
+
* Fail loud, never silently run Null in prod.
|
|
123
|
+
*/
|
|
124
|
+
export function assertModerationProviderAllowed(provider, environment) {
|
|
125
|
+
if (environment !== "dev" && isNullModerationProvider(provider)) {
|
|
126
|
+
throw new NullProviderInProductionError(environment);
|
|
127
|
+
}
|
|
128
|
+
return provider;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=moderation-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moderation-provider.js","sourceRoot":"","sources":["../../../src/lib/media/moderation-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-oracle resolved payload for media moderation.
|
|
3
|
+
*
|
|
4
|
+
* When the moderation pipeline settles, downstream consumers (CDN gate,
|
|
5
|
+
* client notifications) need exactly one bit of information: is this
|
|
6
|
+
* media object ready to serve? They must NOT receive the verdict, labels,
|
|
7
|
+
* confidence score, or any other signal that lets a caller infer why a
|
|
8
|
+
* piece of content was held back — that would publish operational
|
|
9
|
+
* thresholds and give bad actors a tuning signal.
|
|
10
|
+
*
|
|
11
|
+
* This module enforces the anti-oracle contract at the type level: the
|
|
12
|
+
* payload type has exactly two fields and the constructor function accepts
|
|
13
|
+
* a full ModerationStatus but discards everything except the binary
|
|
14
|
+
* ready/not-ready distinction.
|
|
15
|
+
*
|
|
16
|
+
* Pure functional core: no I/O, no clock, no randomness. Deterministic
|
|
17
|
+
* in => deterministic out.
|
|
18
|
+
*/
|
|
19
|
+
import type { ModerationStatus } from "./moderation-status.js";
|
|
20
|
+
/**
|
|
21
|
+
* The only view of a moderation outcome that leaves the moderation domain.
|
|
22
|
+
*
|
|
23
|
+
* Invariants enforced by the type (structural, not just convention):
|
|
24
|
+
* - Exactly two keys: `mediaId` and `status`. No third key is possible
|
|
25
|
+
* without a type error — there is no `decision`, `labels`, `confidence`,
|
|
26
|
+
* `reason`, or per-track field.
|
|
27
|
+
* - `status` is a binary flag: `"ready"` or `"not-ready"`. The caller
|
|
28
|
+
* cannot distinguish PENDING from REVIEW from QUARANTINED from REJECTED —
|
|
29
|
+
* all four collapse to `"not-ready"`.
|
|
30
|
+
*/
|
|
31
|
+
export interface ModerationResolvedPayload {
|
|
32
|
+
readonly mediaId: string;
|
|
33
|
+
readonly status: "ready" | "not-ready";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Construct the anti-oracle resolved payload from a full ModerationStatus.
|
|
37
|
+
*
|
|
38
|
+
* `"ready"` if and only if `status === "APPROVED"`. Every other status —
|
|
39
|
+
* `PENDING`, `REVIEW`, `QUARANTINED`, `REJECTED` — maps to `"not-ready"`.
|
|
40
|
+
* The asymmetry is intentional and fail-closed: uncertainty never yields
|
|
41
|
+
* `"ready"`.
|
|
42
|
+
*
|
|
43
|
+
* @param mediaId - The stable identifier of the media object.
|
|
44
|
+
* @param status - The settled ModerationStatus (sourced from Env.media /
|
|
45
|
+
* the moderation state machine; never hard-coded here).
|
|
46
|
+
*/
|
|
47
|
+
export declare function moderationResolvedPayload(mediaId: string, status: ModerationStatus): ModerationResolvedPayload;
|
|
48
|
+
//# sourceMappingURL=moderation-resolved-payload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moderation-resolved-payload.d.ts","sourceRoot":"","sources":["../../../src/lib/media/moderation-resolved-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,WAAW,CAAC;CACxC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,gBAAgB,GACvB,yBAAyB,CAK3B"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-oracle resolved payload for media moderation.
|
|
3
|
+
*
|
|
4
|
+
* When the moderation pipeline settles, downstream consumers (CDN gate,
|
|
5
|
+
* client notifications) need exactly one bit of information: is this
|
|
6
|
+
* media object ready to serve? They must NOT receive the verdict, labels,
|
|
7
|
+
* confidence score, or any other signal that lets a caller infer why a
|
|
8
|
+
* piece of content was held back — that would publish operational
|
|
9
|
+
* thresholds and give bad actors a tuning signal.
|
|
10
|
+
*
|
|
11
|
+
* This module enforces the anti-oracle contract at the type level: the
|
|
12
|
+
* payload type has exactly two fields and the constructor function accepts
|
|
13
|
+
* a full ModerationStatus but discards everything except the binary
|
|
14
|
+
* ready/not-ready distinction.
|
|
15
|
+
*
|
|
16
|
+
* Pure functional core: no I/O, no clock, no randomness. Deterministic
|
|
17
|
+
* in => deterministic out.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Construct the anti-oracle resolved payload from a full ModerationStatus.
|
|
21
|
+
*
|
|
22
|
+
* `"ready"` if and only if `status === "APPROVED"`. Every other status —
|
|
23
|
+
* `PENDING`, `REVIEW`, `QUARANTINED`, `REJECTED` — maps to `"not-ready"`.
|
|
24
|
+
* The asymmetry is intentional and fail-closed: uncertainty never yields
|
|
25
|
+
* `"ready"`.
|
|
26
|
+
*
|
|
27
|
+
* @param mediaId - The stable identifier of the media object.
|
|
28
|
+
* @param status - The settled ModerationStatus (sourced from Env.media /
|
|
29
|
+
* the moderation state machine; never hard-coded here).
|
|
30
|
+
*/
|
|
31
|
+
export function moderationResolvedPayload(mediaId, status) {
|
|
32
|
+
return {
|
|
33
|
+
mediaId,
|
|
34
|
+
status: status === "APPROVED" ? "ready" : "not-ready",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=moderation-resolved-payload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moderation-resolved-payload.js","sourceRoot":"","sources":["../../../src/lib/media/moderation-resolved-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAoBH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,yBAAyB,CACvC,OAAe,EACf,MAAwB;IAExB,OAAO;QACL,OAAO;QACP,MAAM,EAAE,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW;KACtD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media moderation lifecycle: status union, classifier-decision union, and the
|
|
3
|
+
* pure `nextStatus()` state machine.
|
|
4
|
+
*
|
|
5
|
+
* This module is the **hand-written source of truth** for the moderation
|
|
6
|
+
* lifecycle states. It deliberately has **zero dependency on the
|
|
7
|
+
* Prisma-generated client** so the serve gate and key-adoption code can compile
|
|
8
|
+
* in worktrees that have not regenerated the client. The Prisma
|
|
9
|
+
* `enum ModerationStatus` mirrors {@link ModerationStatus} exactly; the
|
|
10
|
+
* imperative shell maps `Prisma.MediaFile.moderationStatus -> this union` at the
|
|
11
|
+
* I/O boundary.
|
|
12
|
+
*
|
|
13
|
+
* Pure functional core: no I/O, no clock, no Prisma import. Exhaustively
|
|
14
|
+
* property-tested.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The lifecycle state of a media object's moderation, persisted as
|
|
18
|
+
* `MediaFile.moderationStatus`. Hand-written source of truth — the Prisma enum
|
|
19
|
+
* must match this union member-for-member.
|
|
20
|
+
*
|
|
21
|
+
* - `PENDING` — born here; nothing serves until a verdict moves it forward.
|
|
22
|
+
* - `APPROVED` — the only state that serves bytes (the gate is APPROVED-only).
|
|
23
|
+
* - `REVIEW` — classifier was uncertain; awaiting a human moderator.
|
|
24
|
+
* - `QUARANTINED` — classifier flagged it; awaiting a human moderator.
|
|
25
|
+
* - `REJECTED` — terminal; never serves.
|
|
26
|
+
*/
|
|
27
|
+
export type ModerationStatus = "PENDING" | "APPROVED" | "REVIEW" | "QUARANTINED" | "REJECTED";
|
|
28
|
+
/**
|
|
29
|
+
* The 3-value classifier verdict (the `decision` field of a moderation
|
|
30
|
+
* provider's result). This is intentionally **not** 4-value: `rejected` is a
|
|
31
|
+
* lifecycle *status* a human (or the CSAM provider) produces, never a classifier
|
|
32
|
+
* decision.
|
|
33
|
+
*/
|
|
34
|
+
export type ModerationDecision = "approved" | "review" | "quarantine";
|
|
35
|
+
/**
|
|
36
|
+
* Events that drive a `moderationStatus` transition.
|
|
37
|
+
*
|
|
38
|
+
* - `decision` — the classifier/worker verdict on a `PENDING` object.
|
|
39
|
+
* - `human` — a human moderator resolving a `REVIEW`/`QUARANTINED` object.
|
|
40
|
+
* - `csam` — a confirmed hit from the separate (statutory) CSAM provider;
|
|
41
|
+
* drives `-> REJECTED` from *any* state, with the preserve-and-
|
|
42
|
+
* report duty handled by the shell (a human checkpoint).
|
|
43
|
+
*/
|
|
44
|
+
export type ModerationEvent = {
|
|
45
|
+
readonly kind: "decision";
|
|
46
|
+
readonly decision: ModerationDecision;
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: "human";
|
|
49
|
+
readonly action: "approve" | "reject";
|
|
50
|
+
} | {
|
|
51
|
+
readonly kind: "csam";
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* A transition that the state machine refuses. Returned (never thrown) so the
|
|
55
|
+
* machine stays a pure total function and callers must handle the illegal case
|
|
56
|
+
* explicitly — an illegal transition must **never** silently no-op into
|
|
57
|
+
* `APPROVED`.
|
|
58
|
+
*/
|
|
59
|
+
export type IllegalTransition = {
|
|
60
|
+
readonly ok: false;
|
|
61
|
+
readonly reason: "illegal-transition";
|
|
62
|
+
readonly from: ModerationStatus;
|
|
63
|
+
readonly event: ModerationEvent;
|
|
64
|
+
};
|
|
65
|
+
/** A successful transition to a next status. */
|
|
66
|
+
export type TransitionResult = {
|
|
67
|
+
readonly ok: true;
|
|
68
|
+
readonly status: ModerationStatus;
|
|
69
|
+
} | IllegalTransition;
|
|
70
|
+
/**
|
|
71
|
+
* The pure moderation state machine.
|
|
72
|
+
*
|
|
73
|
+
* Transitions (everything else is an {@link IllegalTransition}):
|
|
74
|
+
*
|
|
75
|
+
* ```
|
|
76
|
+
* PENDING --decision approved--> APPROVED
|
|
77
|
+
* PENDING --decision review--> REVIEW
|
|
78
|
+
* PENDING --decision quarantine--> QUARANTINED
|
|
79
|
+
* REVIEW|QUARANTINED --human approve--> APPROVED
|
|
80
|
+
* REVIEW|QUARANTINED --human reject--> REJECTED
|
|
81
|
+
* (any) --csam--> REJECTED (terminal; from any state)
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* Invariants (property-tested):
|
|
85
|
+
* - `APPROVED` and `REJECTED` are absorbing under non-CSAM events.
|
|
86
|
+
* - A CSAM event drives any state to `REJECTED`.
|
|
87
|
+
* - `QUARANTINED`/`REJECTED` never reach `APPROVED` without a human approve.
|
|
88
|
+
* - An illegal transition is reported, never coerced to `APPROVED`.
|
|
89
|
+
*
|
|
90
|
+
* @param current the current persisted status
|
|
91
|
+
* @param event the driving event
|
|
92
|
+
*/
|
|
93
|
+
export declare function nextStatus(current: ModerationStatus, event: ModerationEvent): TransitionResult;
|
|
94
|
+
/** All lifecycle states, for exhaustive iteration in tests and the shell. */
|
|
95
|
+
export declare const ALL_MODERATION_STATUSES: readonly ModerationStatus[];
|
|
96
|
+
/** All classifier decisions, for exhaustive iteration in tests and the shell. */
|
|
97
|
+
export declare const ALL_MODERATION_DECISIONS: readonly ModerationDecision[];
|
|
98
|
+
//# sourceMappingURL=moderation-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moderation-status.d.ts","sourceRoot":"","sources":["../../../src/lib/media/moderation-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,UAAU,GACV,QAAQ,GACR,aAAa,GACb,UAAU,CAAC;AAEf;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEtE;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAA;CAAE,GACjE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CACjC,CAAC;AAEF,gDAAgD;AAChD,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,GACxD,iBAAiB,CAAC;AAwBtB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,eAAe,GACrB,gBAAgB,CAoClB;AA4BD,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,EAAE,SAAS,gBAAgB,EAExC,CAAC;AAExB,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,EAAE,SAAS,kBAAkB,EAIxD,CAAC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media moderation lifecycle: status union, classifier-decision union, and the
|
|
3
|
+
* pure `nextStatus()` state machine.
|
|
4
|
+
*
|
|
5
|
+
* This module is the **hand-written source of truth** for the moderation
|
|
6
|
+
* lifecycle states. It deliberately has **zero dependency on the
|
|
7
|
+
* Prisma-generated client** so the serve gate and key-adoption code can compile
|
|
8
|
+
* in worktrees that have not regenerated the client. The Prisma
|
|
9
|
+
* `enum ModerationStatus` mirrors {@link ModerationStatus} exactly; the
|
|
10
|
+
* imperative shell maps `Prisma.MediaFile.moderationStatus -> this union` at the
|
|
11
|
+
* I/O boundary.
|
|
12
|
+
*
|
|
13
|
+
* Pure functional core: no I/O, no clock, no Prisma import. Exhaustively
|
|
14
|
+
* property-tested.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Map a classifier decision to the status it drives a `PENDING` object into.
|
|
18
|
+
*
|
|
19
|
+
* Fail-closed: any value that is not one of the three known decisions resolves
|
|
20
|
+
* to `REVIEW` (never `APPROVED`). This is the only place the union is widened —
|
|
21
|
+
* callers at the I/O boundary may hand us an unexpected provider string, and we
|
|
22
|
+
* must degrade to human review, not to serving.
|
|
23
|
+
*/
|
|
24
|
+
function statusForDecision(decision) {
|
|
25
|
+
switch (decision) {
|
|
26
|
+
case "approved":
|
|
27
|
+
return "APPROVED";
|
|
28
|
+
case "review":
|
|
29
|
+
return "REVIEW";
|
|
30
|
+
case "quarantine":
|
|
31
|
+
return "QUARANTINED";
|
|
32
|
+
default:
|
|
33
|
+
// Unknown/unexpected decision => fail closed to human review.
|
|
34
|
+
return "REVIEW";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The pure moderation state machine.
|
|
39
|
+
*
|
|
40
|
+
* Transitions (everything else is an {@link IllegalTransition}):
|
|
41
|
+
*
|
|
42
|
+
* ```
|
|
43
|
+
* PENDING --decision approved--> APPROVED
|
|
44
|
+
* PENDING --decision review--> REVIEW
|
|
45
|
+
* PENDING --decision quarantine--> QUARANTINED
|
|
46
|
+
* REVIEW|QUARANTINED --human approve--> APPROVED
|
|
47
|
+
* REVIEW|QUARANTINED --human reject--> REJECTED
|
|
48
|
+
* (any) --csam--> REJECTED (terminal; from any state)
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* Invariants (property-tested):
|
|
52
|
+
* - `APPROVED` and `REJECTED` are absorbing under non-CSAM events.
|
|
53
|
+
* - A CSAM event drives any state to `REJECTED`.
|
|
54
|
+
* - `QUARANTINED`/`REJECTED` never reach `APPROVED` without a human approve.
|
|
55
|
+
* - An illegal transition is reported, never coerced to `APPROVED`.
|
|
56
|
+
*
|
|
57
|
+
* @param current the current persisted status
|
|
58
|
+
* @param event the driving event
|
|
59
|
+
*/
|
|
60
|
+
export function nextStatus(current, event) {
|
|
61
|
+
// CSAM is terminal from any state — checked first so it overrides every
|
|
62
|
+
// absorbing/illegal rule below.
|
|
63
|
+
if (event.kind === "csam") {
|
|
64
|
+
return { ok: true, status: "REJECTED" };
|
|
65
|
+
}
|
|
66
|
+
switch (current) {
|
|
67
|
+
case "PENDING": {
|
|
68
|
+
// Only the classifier acts on a freshly-uploaded object.
|
|
69
|
+
if (event.kind === "decision") {
|
|
70
|
+
return { ok: true, status: statusForDecision(event.decision) };
|
|
71
|
+
}
|
|
72
|
+
return illegal(current, event);
|
|
73
|
+
}
|
|
74
|
+
case "REVIEW":
|
|
75
|
+
case "QUARANTINED": {
|
|
76
|
+
// Only a human moderator resolves these.
|
|
77
|
+
if (event.kind === "human") {
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
status: event.action === "approve" ? "APPROVED" : "REJECTED",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return illegal(current, event);
|
|
84
|
+
}
|
|
85
|
+
case "APPROVED":
|
|
86
|
+
case "REJECTED":
|
|
87
|
+
// Absorbing under any non-CSAM event.
|
|
88
|
+
return illegal(current, event);
|
|
89
|
+
default:
|
|
90
|
+
return illegal(current, event);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function illegal(from, event) {
|
|
94
|
+
return { ok: false, reason: "illegal-transition", from, event };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Compile-time exhaustiveness guard for {@link ModerationStatus}.
|
|
98
|
+
*
|
|
99
|
+
* Keyed by the union, so adding a member to `ModerationStatus` without adding it
|
|
100
|
+
* here is a TYPE error (TS2741 "missing key"), and a stale key that is no longer
|
|
101
|
+
* a union member is a TYPE error too. This makes it impossible for
|
|
102
|
+
* {@link ALL_MODERATION_STATUSES} below to silently drift from the union — a
|
|
103
|
+
* drift that would let a new, un-enumerated status bypass the exhaustive
|
|
104
|
+
* anti-oracle property tests (which iterate this array). Pure type-level, no
|
|
105
|
+
* behavior.
|
|
106
|
+
*/
|
|
107
|
+
const MODERATION_STATUS_MEMBERS = {
|
|
108
|
+
PENDING: true,
|
|
109
|
+
APPROVED: true,
|
|
110
|
+
REVIEW: true,
|
|
111
|
+
QUARANTINED: true,
|
|
112
|
+
REJECTED: true,
|
|
113
|
+
};
|
|
114
|
+
/** All lifecycle states, for exhaustive iteration in tests and the shell. */
|
|
115
|
+
export const ALL_MODERATION_STATUSES = Object.keys(MODERATION_STATUS_MEMBERS);
|
|
116
|
+
/** All classifier decisions, for exhaustive iteration in tests and the shell. */
|
|
117
|
+
export const ALL_MODERATION_DECISIONS = [
|
|
118
|
+
"approved",
|
|
119
|
+
"review",
|
|
120
|
+
"quarantine",
|
|
121
|
+
];
|
|
122
|
+
//# sourceMappingURL=moderation-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moderation-status.js","sourceRoot":"","sources":["../../../src/lib/media/moderation-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA4DH;;;;;;;GAOG;AACH,SAAS,iBAAiB,CAAC,QAA4B;IACrD,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,UAAU;YACb,OAAO,UAAU,CAAC;QACpB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAC;QAClB,KAAK,YAAY;YACf,OAAO,aAAa,CAAC;QACvB;YACE,8DAA8D;YAC9D,OAAO,QAAQ,CAAC;IACpB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,UAAU,CACxB,OAAyB,EACzB,KAAsB;IAEtB,wEAAwE;IACxE,gCAAgC;IAChC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC1C,CAAC;IAED,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,yDAAyD;YACzD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBAC9B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjE,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC;QAED,KAAK,QAAQ,CAAC;QACd,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,yCAAyC;YACzC,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC3B,OAAO;oBACL,EAAE,EAAE,IAAI;oBACR,MAAM,EAAE,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU;iBAC7D,CAAC;YACJ,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC;QAED,KAAK,UAAU,CAAC;QAChB,KAAK,UAAU;YACb,sCAAsC;YACtC,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAEjC;YACE,OAAO,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CACd,IAAsB,EACtB,KAAsB;IAEtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAClE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,yBAAyB,GAAG;IAChC,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,IAAI;IACd,MAAM,EAAE,IAAI;IACZ,WAAW,EAAE,IAAI;IACjB,QAAQ,EAAE,IAAI;CACmC,CAAC;AAEpD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,uBAAuB,GAAgC,MAAM,CAAC,IAAI,CAC7E,yBAAyB,CACJ,CAAC;AAExB,iFAAiF;AACjF,MAAM,CAAC,MAAM,wBAAwB,GAAkC;IACrE,UAAU;IACV,QAAQ;IACR,YAAY;CACJ,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Track } from "./track-verdict.js";
|
|
2
|
+
/**
|
|
3
|
+
* A unit of work the processing worker dequeues for one uploaded object.
|
|
4
|
+
*
|
|
5
|
+
* - `tenantId` — owning tenant (CUID; validated by ./cas-keys.ts before use).
|
|
6
|
+
* - `uploadId` — the upload session id (CUID).
|
|
7
|
+
* - `key` — the staging/pending storage key the bytes currently live at
|
|
8
|
+
* (built ONLY via ./cas-keys.ts `pendingKey`).
|
|
9
|
+
*/
|
|
10
|
+
export interface ProcessingJob {
|
|
11
|
+
readonly tenantId: string;
|
|
12
|
+
readonly uploadId: string;
|
|
13
|
+
readonly key: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The outcome of processing one object.
|
|
17
|
+
*
|
|
18
|
+
* - `casKey` — the canonical content-addressed key the cleaned bytes were
|
|
19
|
+
* stored at (built via ./cas-keys.ts `casKey`).
|
|
20
|
+
* - `contentHash` — the 64-char lowercase SHA-256 of the cleaned bytes (the CAS
|
|
21
|
+
* address; validated by ./cas-keys.ts `validateContentHash`).
|
|
22
|
+
* - `visualJobId` — async moderation job handle for the VISUAL track, when a
|
|
23
|
+
* visual track was started; absent for audio-only objects.
|
|
24
|
+
* - `audioJobId` — async transcription/moderation handle for the AUDIO track,
|
|
25
|
+
* when an audio track was started; absent for objects with no
|
|
26
|
+
* audio.
|
|
27
|
+
*/
|
|
28
|
+
export interface ProcessingResult {
|
|
29
|
+
readonly casKey: string;
|
|
30
|
+
readonly contentHash: string;
|
|
31
|
+
readonly visualJobId?: string;
|
|
32
|
+
readonly audioJobId?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Input to the per-track dedupe key the worker uses to idempotently fan out /
|
|
36
|
+
* fan in async track jobs without double-processing. Keyed by the content hash
|
|
37
|
+
* (so identical bytes share work), the provider/transcription job id, and which
|
|
38
|
+
* track it is — so the VISUAL and AUDIO tracks of the same bytes never collide.
|
|
39
|
+
*/
|
|
40
|
+
export interface DedupeKeyInput {
|
|
41
|
+
readonly contentHash: string;
|
|
42
|
+
readonly jobId: string;
|
|
43
|
+
readonly track: Track;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=processing-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processing-types.d.ts","sourceRoot":"","sources":["../../../src/lib/media/processing-types.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// CONTRACT: stable — coordinate changes. Shared P0b processing value types.
|
|
2
|
+
//
|
|
3
|
+
// Pure data contracts that thread a media object through the processing
|
|
4
|
+
// pipeline: the job the worker dequeues, the result it produces, and the input
|
|
5
|
+
// to the per-track dedupe key. NO behavior here — just the shapes the shell and
|
|
6
|
+
// the functional-core units agree on. Ships in the PUBLIC npm tarball: no
|
|
7
|
+
// thresholds, secrets, or real-category vocabulary.
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=processing-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processing-types.js","sourceRoot":"","sources":["../../../src/lib/media/processing-types.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,wEAAwE;AACxE,+EAA+E;AAC/E,gFAAgF;AAChF,0EAA0E;AAC1E,oDAAoD"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type TrackOutcome } from "./track-verdict.js";
|
|
2
|
+
import { type ModerationDecision, type ModerationStatus, type TransitionResult } from "./moderation-status.js";
|
|
3
|
+
/**
|
|
4
|
+
* Everything {@link decidePromotion} needs to decide the worker's action.
|
|
5
|
+
*
|
|
6
|
+
* - `visual` / `audio` — the per-track outcomes (see {@link TrackOutcome}).
|
|
7
|
+
* - `currentStatus` — the object's persisted moderation status *before* this
|
|
8
|
+
* decision is applied. Used to detect replay / illegal
|
|
9
|
+
* transitions (terminal states absorb).
|
|
10
|
+
* - `casObjectPresent` — whether the content-addressed object actually exists in
|
|
11
|
+
* durable storage. Approval alone must not serve bytes
|
|
12
|
+
* that are not there; promotion is gated on presence.
|
|
13
|
+
*/
|
|
14
|
+
export interface PromotionInput {
|
|
15
|
+
readonly visual: TrackOutcome;
|
|
16
|
+
readonly audio: TrackOutcome;
|
|
17
|
+
readonly currentStatus: ModerationStatus;
|
|
18
|
+
readonly casObjectPresent: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The worker's decided action. All side effects are reported as booleans; the
|
|
22
|
+
* shell performs them in the fixed order promote -> persist -> emit.
|
|
23
|
+
*
|
|
24
|
+
* - `combined` — the object-level decision from {@link combineTrackVerdicts}.
|
|
25
|
+
* - `transition` — the result of running `combined` through the state
|
|
26
|
+
* machine from `currentStatus`. `ok:false` means the
|
|
27
|
+
* transition is illegal (e.g. replay on a terminal
|
|
28
|
+
* status) and the action is a no-op.
|
|
29
|
+
* - `shouldPromote` — adopt/publish the CAS object so it can serve. True
|
|
30
|
+
* IFF the transition is legal AND lands on APPROVED AND
|
|
31
|
+
* the CAS object is present.
|
|
32
|
+
* - `shouldPersistStatus` — write the new status. True IFF the transition is legal.
|
|
33
|
+
* - `shouldEmitResolved` — emit the "resolved" event. True IFF the transition is legal.
|
|
34
|
+
*/
|
|
35
|
+
export interface PromotionAction {
|
|
36
|
+
readonly combined: ModerationDecision;
|
|
37
|
+
readonly transition: TransitionResult;
|
|
38
|
+
readonly shouldPromote: boolean;
|
|
39
|
+
readonly shouldPersistStatus: boolean;
|
|
40
|
+
readonly shouldEmitResolved: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Decide the worker's action for a media object whose tracks have both resolved.
|
|
44
|
+
*
|
|
45
|
+
* Logic (total):
|
|
46
|
+
* 1. `combined = combineTrackVerdicts(visual, audio)` — fail-closed track join.
|
|
47
|
+
* 2. `transition = nextStatus(currentStatus, { kind: "decision", decision: combined })`.
|
|
48
|
+
* 3. If `transition.ok === false` (illegal — e.g. replay on a terminal status):
|
|
49
|
+
* idempotent NO-OP — `shouldPromote`/`shouldPersistStatus`/`shouldEmitResolved`
|
|
50
|
+
* are all `false`.
|
|
51
|
+
* 4. If `transition.ok === true`:
|
|
52
|
+
* - `shouldPersistStatus = true`
|
|
53
|
+
* - `shouldEmitResolved = true`
|
|
54
|
+
* - `shouldPromote = transition.status === "APPROVED" && casObjectPresent`
|
|
55
|
+
*
|
|
56
|
+
* Safety invariants (property-tested):
|
|
57
|
+
* - `shouldPromote === true` ⇒ BOTH tracks were decided-and-approved AND the
|
|
58
|
+
* resulting status is APPROVED. Promotion never happens from doubt.
|
|
59
|
+
* - APPROVED is never reached unless both tracks are decided-approved.
|
|
60
|
+
* - A replay/illegal transition on a terminal status yields an all-false no-op.
|
|
61
|
+
* - `casObjectPresent === false` ⇒ `shouldPromote === false`, even at APPROVED.
|
|
62
|
+
*/
|
|
63
|
+
export declare function decidePromotion(input: PromotionInput): PromotionAction;
|
|
64
|
+
//# sourceMappingURL=promote-decision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promote-decision.d.ts","sourceRoot":"","sources":["../../../src/lib/media/promote-decision.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAC;AAEhC;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;CACtC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,CAiCtE"}
|