@gentbajko/slopify 3.16.0 → 3.18.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.
- package/dist/edge/http/actions.js +2 -2
- package/dist/edge/http/actor.js +10 -0
- package/dist/edge/http/app.js +21 -0
- package/dist/edge/http/checkpoints.js +14 -6
- package/dist/edge/http/external-reviews.js +95 -0
- package/dist/edge/http/projects.js +36 -18
- package/dist/edge/http/reviews.js +53 -18
- package/dist/edge/http/revisions.js +5 -1
- package/dist/edge/http/settings.js +9 -1
- package/dist/edge/http/starts.js +41 -0
- package/dist/edge/http/studio.js +5 -130
- package/dist/edge/mcp/attribution.js +38 -0
- package/dist/edge/mcp/call-route.js +203 -0
- package/dist/edge/mcp/excluded-routes.js +95 -0
- package/dist/edge/mcp/file-tools.js +141 -0
- package/dist/edge/mcp/http.js +79 -0
- package/dist/edge/mcp/instructions.js +17 -0
- package/dist/edge/mcp/library-tools.js +143 -0
- package/dist/edge/mcp/remake-tools.js +90 -0
- package/dist/edge/mcp/review-tools.js +64 -0
- package/dist/edge/mcp/route-tool.js +61 -0
- package/dist/edge/mcp/server.js +48 -0
- package/dist/edge/mcp/state-shapes.js +60 -0
- package/dist/edge/mcp/tools-channels.js +153 -0
- package/dist/edge/mcp/tools-library.js +151 -0
- package/dist/edge/mcp/tools-planning.js +137 -0
- package/dist/edge/mcp/tools-projects.js +167 -0
- package/dist/edge/mcp/tools-publishing.js +148 -0
- package/dist/edge/mcp/tools-runs.js +94 -0
- package/dist/edge/mcp/tools-schedules.js +143 -0
- package/dist/edge/mcp/tools-setup.js +234 -0
- package/dist/edge/mcp/tools-storage.js +99 -0
- package/dist/edge/mcp/tools.js +21 -0
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/kernel/db/migrations/0055-external-reviews.sql +56 -0
- package/dist/kernel/db/migrations/0056-agent-actions.sql +20 -0
- package/dist/kernel/db/migrations/0057-agent-action-once.sql +5 -0
- package/dist/kernel/db/migrations/0058-revision-work-admission.sql +3 -0
- package/dist/kernel/db/migrations/0059-external-review-canceled.sql +29 -0
- package/dist/kernel/db/migrations/0060-reservation-work.sql +4 -0
- package/dist/kernel/db/migrations/0061-project-control-counts.sql +4 -0
- package/dist/kernel/db/migrations/0062-told-failures.sql +8 -0
- package/dist/kernel/pipeline.js +6 -0
- package/dist/kernel/runner/graph.js +37 -2
- package/dist/kernel/runner/index.js +6 -3
- package/dist/main.js +29 -13
- package/dist/patch-notes/3.17.0.md +10 -0
- package/dist/patch-notes/3.18.0.md +13 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/admission/admitted-work.js +53 -0
- package/dist/slices/admission/heads.js +5 -0
- package/dist/slices/admission/repo.js +47 -29
- package/dist/slices/admission/schema.js +1 -0
- package/dist/slices/admission/stage-writes.js +21 -0
- package/dist/slices/admission/start.js +2 -1
- package/dist/slices/admission/state-words.js +110 -0
- package/dist/slices/agents/attention-work.js +233 -0
- package/dist/slices/agents/attention.js +168 -0
- package/dist/slices/agents/connection.js +15 -0
- package/dist/slices/agents/model.js +29 -0
- package/dist/slices/agents/repo.js +66 -0
- package/dist/slices/cancel/index.js +4 -1
- package/dist/slices/control/index.js +2 -1
- package/dist/slices/episodes/summarize.js +2 -1
- package/dist/slices/estimate/index.js +6 -2
- package/dist/slices/images/run.js +2 -1
- package/dist/slices/library/transfer.js +69 -0
- package/dist/slices/narration/run.js +2 -1
- package/dist/slices/notifications/address.js +40 -0
- package/dist/slices/notifications/notifier.js +43 -10
- package/dist/slices/notifications/rules.js +111 -45
- package/dist/slices/notifications/server.js +14 -0
- package/dist/slices/notifications/settings.js +1 -1
- package/dist/slices/notifications/subject.js +30 -0
- package/dist/slices/notifications/told.js +33 -0
- package/dist/slices/play-drafts/readiness.js +9 -2
- package/dist/slices/play-drafts/schema.js +2 -1
- package/dist/slices/rebuild/narration-words.js +76 -0
- package/dist/slices/rebuild/recipe-model.js +1 -0
- package/dist/slices/rebuild/recipe-reviews.js +44 -23
- package/dist/slices/rebuild/recovery-repo.js +0 -20
- package/dist/slices/rebuild/recovery-selection.js +14 -3
- package/dist/slices/rebuild/recovery.js +5 -2
- package/dist/slices/rebuild/remake-outdated.js +27 -0
- package/dist/slices/rebuild/repo.js +7 -2
- package/dist/slices/rebuild/review-redo.js +12 -0
- package/dist/slices/rebuild/runtime-agent-review.js +121 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +26 -11
- package/dist/slices/rebuild/runtime-review.js +12 -2
- package/dist/slices/rebuild/runtime-run.js +11 -2
- package/dist/slices/rebuild/runtime-store.js +32 -5
- package/dist/slices/reruns/index.js +2 -1
- package/dist/slices/research/run.js +2 -1
- package/dist/slices/reviews/external-model.js +21 -0
- package/dist/slices/reviews/external-release.js +38 -0
- package/dist/slices/reviews/external-repo.js +109 -0
- package/dist/slices/reviews/external-rows.js +68 -0
- package/dist/slices/reviews/external-waiting.js +75 -0
- package/dist/slices/reviews/holds.js +144 -0
- package/dist/slices/reviews/model.js +14 -0
- package/dist/slices/reviews/repo.js +27 -10
- package/dist/slices/reviews/rules.js +5 -3
- package/dist/slices/reviews/verdict.js +18 -8
- package/dist/slices/revisions/repo.js +3 -0
- package/dist/slices/revisions/schema.js +1 -0
- package/dist/slices/schedules/agenda.js +47 -17
- package/dist/slices/schedules/prepare.js +6 -0
- package/dist/slices/schedules/schema.js +13 -1
- package/dist/slices/storage/consequences.js +27 -0
- package/dist/slices/storage/portable.js +3 -0
- package/dist/slices/storage/project-files.js +66 -0
- package/dist/slices/studio/settings.js +0 -35
- package/dist/slices/studio/stats.js +2 -34
- package/dist/slices/subtitles/prepare.js +2 -1
- package/dist/slices/uploads/repo.js +12 -6
- package/dist/slices/uploads/rules.js +5 -0
- package/dist/slices/video/write-export.js +1 -1
- package/dist/slices/video/write-subtitles.js +1 -1
- package/dist/tutorials/Agents-MCP.md +168 -0
- package/dist/tutorials/Calendar.md +2 -1
- package/dist/tutorials/First-Launch-and-Welcome.md +1 -0
- package/dist/tutorials/Home-and-Projects.md +16 -9
- package/dist/tutorials/Home.md +1 -0
- package/dist/tutorials/Notifications.md +25 -6
- package/dist/tutorials/Project-Page.md +8 -2
- package/dist/tutorials/Publishing-to-YouTube.md +1 -0
- package/dist/tutorials/Recovery-and-Retries.md +17 -3
- package/dist/tutorials/Reviews-and-Checkpoints.md +22 -5
- package/dist/tutorials/Schedules.md +1 -1
- package/dist/tutorials/Settings-Reference.md +15 -0
- package/dist/tutorials/Troubleshooting.md +3 -1
- package/dist/tutorials/Your-First-Short.md +1 -0
- package/dist/tutorials/_Sidebar.md +1 -0
- package/dist/web/assets/{index-VbpUYLAp.css → index-BKbZIxAq.css} +1 -1
- package/dist/web/assets/index-Drth_MfQ.js +325 -0
- package/dist/web/assets/{pdf-C6KhxeKQ.js → pdf-CQFUyijy.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/slices/studio/performance.js +0 -83
- package/dist/slices/studio/report-stats.js +0 -39
- package/dist/slices/studio/report.js +0 -172
- package/dist/web/assets/index-Dvz8bsiD.js +0 -325
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { liveProject, oneProject, projectTrashed } from "../admission/repo.js";
|
|
3
|
+
import { externalReviewOf } from "./external-rows.js";
|
|
4
|
+
// A review the project still wants: its step is reserved by the project's current version with
|
|
5
|
+
// the same fingerprint. A rerun or an edit that replaced the item leaves it unwanted.
|
|
6
|
+
export function wantedBy(table) {
|
|
7
|
+
return `EXISTS (SELECT 1 FROM revision_work_reservations r
|
|
8
|
+
JOIN project_heads h ON h.project_id=r.project_id AND h.revision_id=r.revision_id
|
|
9
|
+
WHERE r.project_id=${table}.project_id AND r.work_id=${table}.work_id
|
|
10
|
+
AND r.work_key=${table}.work_key AND r.fingerprint=${table}.review_fingerprint)`;
|
|
11
|
+
}
|
|
12
|
+
// Nothing of a canceled run goes ahead on its own: the review's work, or its own step, is
|
|
13
|
+
// canceled. Another step canceled is not enough: retrying one step (`recoverProject`) admits
|
|
14
|
+
// it again while the rest stay canceled, and its reviews wait for their verdict.
|
|
15
|
+
export function liveWork(table) {
|
|
16
|
+
return `EXISTS (SELECT 1 FROM revision_work w WHERE w.id=${table}.work_id AND NOT ${canceledStep("w")})`;
|
|
17
|
+
}
|
|
18
|
+
// The step reads canceled by its stage too, whatever the work row says: a restart before
|
|
19
|
+
// canceled work was kept canceled (`recoverWork`) held it pending.
|
|
20
|
+
function canceledStep(work) {
|
|
21
|
+
return `(${work}.state='canceled' OR EXISTS (SELECT 1 FROM stages s WHERE s.id=${work}.stage_id AND s.state='canceled'))`;
|
|
22
|
+
}
|
|
23
|
+
// Where a review stands, in the one order get_external_review and a refused verdict both say
|
|
24
|
+
// it: a verdict already kept, its run canceled (Cancel closed it, or its step reads canceled;
|
|
25
|
+
// no new one waits until the step is taken up and asks again, unless its item was made again
|
|
26
|
+
// and the new review waits: superseded), its item replaced or its step gone (`superseded`, or
|
|
27
|
+
// `replaced-pending` while no new one waits yet), then its project in the Trash - said last,
|
|
28
|
+
// as only then would Restore bring it back. It waits only when none holds, exactly when
|
|
29
|
+
// `stillWaiting` lists it.
|
|
30
|
+
export function reviewStanding(db, review) {
|
|
31
|
+
if (review.state === "answered" || review.state === "auto-passed")
|
|
32
|
+
return review.state;
|
|
33
|
+
if (review.state === "canceled" ||
|
|
34
|
+
db
|
|
35
|
+
.prepare(`SELECT 1 FROM revision_work w WHERE w.id=? AND ${canceledStep("w")}`)
|
|
36
|
+
.get(review.workId) !== undefined)
|
|
37
|
+
return replacementWaits(db, review) ? "superseded" : "canceled";
|
|
38
|
+
const wanted = review.state === "waiting" &&
|
|
39
|
+
db
|
|
40
|
+
.prepare(`SELECT 1 FROM external_reviews e WHERE e.project_id=? AND e.id=? AND ${wantedBy("e")} AND ${liveWork("e")}`)
|
|
41
|
+
.get(review.projectId, review.id) !== undefined;
|
|
42
|
+
if (!wanted)
|
|
43
|
+
return replacementWaits(db, review) ? "superseded" : "replaced-pending";
|
|
44
|
+
return projectTrashed(db, review.projectId) ? "trashed" : "waiting";
|
|
45
|
+
}
|
|
46
|
+
// Whether another review of the same item waits in its place, the project in the Trash or not:
|
|
47
|
+
// either way the old one was replaced.
|
|
48
|
+
function replacementWaits(db, review) {
|
|
49
|
+
return (db
|
|
50
|
+
.prepare(`SELECT 1 FROM external_reviews e WHERE ${wantedAndLive} AND e.project_id=? AND e.item_key=? AND e.id!=?`)
|
|
51
|
+
.get(review.projectId, review.itemKey, review.id) !== undefined);
|
|
52
|
+
}
|
|
53
|
+
// A review `e` marked waiting, still wanted and of a run that isn't canceled.
|
|
54
|
+
const wantedAndLive = `e.state='waiting' AND ${wantedBy("e")} AND ${liveWork("e")}`;
|
|
55
|
+
// A review `e` that waits: the same, of a project not in the Trash.
|
|
56
|
+
const stillWaiting = `${wantedAndLive} AND ${liveProject("e", "project_id")}`;
|
|
57
|
+
// Waiting, still wanted by the current version, of a run that isn't canceled, oldest first.
|
|
58
|
+
export function waitingExternalReviews(db, projectId) {
|
|
59
|
+
return waitingExternalReviewRows(db, projectId).map(externalReviewOf);
|
|
60
|
+
}
|
|
61
|
+
// The same rows unread, for a caller that skips one it can't read instead of failing them all.
|
|
62
|
+
export function waitingExternalReviewRows(db, projectId) {
|
|
63
|
+
const one = oneProject("e", projectId);
|
|
64
|
+
return db
|
|
65
|
+
.prepare(`SELECT e.* FROM external_reviews e WHERE ${stillWaiting}${one.sql} ORDER BY e.waiting_since, e.rowid`)
|
|
66
|
+
.all(...one.params);
|
|
67
|
+
}
|
|
68
|
+
// The projects with a review waiting for an agent, as `waitingExternalReviewRows` finds them.
|
|
69
|
+
export function projectsWaitingOnAgents(db, projectId) {
|
|
70
|
+
const one = oneProject("e", projectId);
|
|
71
|
+
const rows = db
|
|
72
|
+
.prepare(`SELECT DISTINCT e.project_id FROM external_reviews e WHERE ${stillWaiting}${one.sql}`)
|
|
73
|
+
.all(...one.params);
|
|
74
|
+
return new Set(rows.map((row) => z.string().parse(row.project_id)));
|
|
75
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { derive, satisfied, deps as stageDeps, } from "../../kernel/runner/graph.js";
|
|
3
|
+
import { pendingAdmitted } from "../admission/admitted-work.js";
|
|
4
|
+
import { liveProject, oneProject, stageStandingsByProject } from "../admission/repo.js";
|
|
5
|
+
import { checkpointStageSchema } from "../checkpoints/schema.js";
|
|
6
|
+
import { heldForItsDay } from "../schedules/prepare.js";
|
|
7
|
+
import { uploadedAndSettled } from "../uploads/repo.js";
|
|
8
|
+
import { projectsWaitingOnAgents } from "./external-waiting.js";
|
|
9
|
+
const gateRow = z.object({
|
|
10
|
+
project_id: z.string(),
|
|
11
|
+
checkpoint_id: z.string(),
|
|
12
|
+
stage: checkpointStageSchema,
|
|
13
|
+
revision_id: z.string(),
|
|
14
|
+
fingerprint: z.string(),
|
|
15
|
+
created_at: z.string(),
|
|
16
|
+
own: z.number(),
|
|
17
|
+
});
|
|
18
|
+
const projectRow = z.object({ project_id: z.string() });
|
|
19
|
+
// Every project's holds, or one project's alone when `projectId` is given. A caller that has
|
|
20
|
+
// already read the stage standings (`stageStandingsByProject`) or the uploaded projects that
|
|
21
|
+
// read Done (`uploadedAndSettled`) passes them; one that lists what it can read passes `skip`,
|
|
22
|
+
// given each checkpoint row that can't be read instead of failing them all.
|
|
23
|
+
export function projectHolds(db, scope = {}) {
|
|
24
|
+
const { projectId, skip } = scope;
|
|
25
|
+
const gatesOf = oneProject("c", projectId);
|
|
26
|
+
const gateSql = `SELECT c.project_id, c.checkpoint_id, c.stage, c.revision_id, c.fingerprint, c.created_at, (c.stage='video' AND ${heldForItsDay("c")}) AS own
|
|
27
|
+
FROM review_checkpoints c
|
|
28
|
+
JOIN project_heads h ON h.project_id=c.project_id AND h.revision_id=c.revision_id
|
|
29
|
+
WHERE c.state IN ('held','pending-review') AND c.approved_at IS NULL AND ${liveProject("c", "project_id")}${gatesOf.sql}`;
|
|
30
|
+
const gates = db
|
|
31
|
+
.prepare(gateSql)
|
|
32
|
+
.all(...gatesOf.params)
|
|
33
|
+
.flatMap((row) => {
|
|
34
|
+
if (skip === undefined)
|
|
35
|
+
return [gateRow.parse(row)];
|
|
36
|
+
const gate = gateRow.safeParse(row);
|
|
37
|
+
if (gate.success)
|
|
38
|
+
return [gate.data];
|
|
39
|
+
skip(row, gate.error);
|
|
40
|
+
return [];
|
|
41
|
+
});
|
|
42
|
+
const standings = scope.standings ?? stageStandingsByProject(db, projectId);
|
|
43
|
+
// A checkpoint holds the run once the stages its step needs are made; before that the run
|
|
44
|
+
// is held by whatever those stages wait on.
|
|
45
|
+
const reached = (project, stage) => stageDeps[stage].every((dependency) => {
|
|
46
|
+
const standing = standings.get(project)?.find((made) => made.kind === dependency);
|
|
47
|
+
return standing === undefined || satisfied(standing.state);
|
|
48
|
+
});
|
|
49
|
+
const theirs = new Set();
|
|
50
|
+
const reachedTheirs = new Set();
|
|
51
|
+
const reachedOwn = new Set();
|
|
52
|
+
const holding = new Map();
|
|
53
|
+
for (const gate of gates) {
|
|
54
|
+
const own = gate.own === 1;
|
|
55
|
+
if (!own)
|
|
56
|
+
theirs.add(gate.project_id);
|
|
57
|
+
if (!reached(gate.project_id, gate.stage))
|
|
58
|
+
continue;
|
|
59
|
+
(own ? reachedOwn : reachedTheirs).add(gate.project_id);
|
|
60
|
+
const hold = {
|
|
61
|
+
checkpointId: gate.checkpoint_id,
|
|
62
|
+
stage: gate.stage,
|
|
63
|
+
waitsFor: own ? "day" : "person",
|
|
64
|
+
revisionId: gate.revision_id,
|
|
65
|
+
fingerprint: gate.fingerprint,
|
|
66
|
+
since: gate.created_at,
|
|
67
|
+
};
|
|
68
|
+
holding.set(gate.project_id, [...(holding.get(gate.project_id) ?? []), hold]);
|
|
69
|
+
}
|
|
70
|
+
const headsOf = oneProject("h", projectId);
|
|
71
|
+
const scheduled = db
|
|
72
|
+
.prepare(`SELECT h.project_id FROM project_heads h WHERE ${heldForItsDay("h")}${headsOf.sql}`)
|
|
73
|
+
.all(...headsOf.params)
|
|
74
|
+
.map((row) => projectRow.parse(row).project_id);
|
|
75
|
+
const agents = projectsWaitingOnAgents(db, projectId);
|
|
76
|
+
const runs = heldRuns(db, projectId);
|
|
77
|
+
// An uploaded project with nothing admitted reads Done (`edge/http/projects.ts`).
|
|
78
|
+
const settled = scope.settled ?? uploadedAndSettled(db, projectId);
|
|
79
|
+
const waitingForAgent = new Set([...agents].filter((id) => !reachedTheirs.has(id) && runs.get(id)?.held !== true));
|
|
80
|
+
const stopped = new Set([...runs]
|
|
81
|
+
.filter(([, run]) => run.stuck)
|
|
82
|
+
.map(([id]) => id)
|
|
83
|
+
.filter((id) => !reachedTheirs.has(id) &&
|
|
84
|
+
!waitingForAgent.has(id) &&
|
|
85
|
+
// Held at its day's hold, it goes on by itself on its day, though a failed step's
|
|
86
|
+
// admission holds its video's step back until then; a step a restart held still
|
|
87
|
+
// waits for Continue the run.
|
|
88
|
+
(!reachedOwn.has(id) || runs.get(id)?.held === true) &&
|
|
89
|
+
!settled.has(id) &&
|
|
90
|
+
derive(standings.get(id) ?? []) === "pending"));
|
|
91
|
+
return {
|
|
92
|
+
prepared: new Set(scheduled.filter((id) => !theirs.has(id) && !stopped.has(id))),
|
|
93
|
+
waitingForAgent,
|
|
94
|
+
stopped,
|
|
95
|
+
// Only Continue the run moves a stopped run on: the one hold it can have reached, its
|
|
96
|
+
// day's, holds it again once it has.
|
|
97
|
+
holding: new Map([...holding].filter(([id]) => !stopped.has(id))),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// An admitted step of `h`'s project still to run (`pendingAdmitted`), held other than one
|
|
101
|
+
// parked on an agent's review: a restart (`recoverWork`) or a pause held it, and only Continue
|
|
102
|
+
// the run starts it.
|
|
103
|
+
function heldBesideReviews(h) {
|
|
104
|
+
return `EXISTS (SELECT 1 FROM revision_work w
|
|
105
|
+
WHERE w.project_id=${h}.project_id AND w.dispatch_state='held' AND ${pendingAdmitted("w")}
|
|
106
|
+
AND NOT EXISTS (SELECT 1 FROM external_reviews e WHERE e.work_id=w.id AND e.state='waiting'))`;
|
|
107
|
+
}
|
|
108
|
+
const runRow = z.object({ project_id: z.string(), held: z.number(), stuck: z.number() });
|
|
109
|
+
// Whether `h` is still: no step's call in flight, not paused and not waiting its turn in a
|
|
110
|
+
// batch. A running stage needs no clause here: it makes the run's state running (`derive`).
|
|
111
|
+
function still(h) {
|
|
112
|
+
return `NOT EXISTS(SELECT 1 FROM revision_work w WHERE w.project_id=${h}.project_id AND w.state='running')
|
|
113
|
+
AND NOT EXISTS(SELECT 1 FROM project_controls c WHERE c.project_id=${h}.project_id AND c.paused=1)
|
|
114
|
+
AND NOT EXISTS(SELECT 1 FROM project_queue q WHERE q.project_id=${h}.project_id AND q.state='queued')`;
|
|
115
|
+
}
|
|
116
|
+
// No admitted work of `h`'s current version that can start: a saved rerun or edit is admitted
|
|
117
|
+
// by no one until Resume, and work whose admission has a failed sibling never starts.
|
|
118
|
+
function nothingAdmitted(h) {
|
|
119
|
+
return `NOT EXISTS(SELECT 1 FROM revision_work w
|
|
120
|
+
JOIN revision_work_reservations r ON r.work_id=w.id AND r.revision_id=${h}.revision_id
|
|
121
|
+
WHERE w.state='pending' AND w.dispatch_state='allowed' AND NOT EXISTS(
|
|
122
|
+
SELECT 1 FROM revision_work f WHERE f.admission_id=w.admission_id AND f.state IN ('failed','canceled')))`;
|
|
123
|
+
}
|
|
124
|
+
// Each project out of the Trash with a step held beside the reviews (`held`), and whether its
|
|
125
|
+
// run is stuck: it has unfinished stages, is still, and nothing will move it on but Continue
|
|
126
|
+
// the run, because nothing admitted can start or because a step is held (a step still allowed
|
|
127
|
+
// may wait on it). Each reading is made once per project (MATERIALIZED), not once per use.
|
|
128
|
+
function heldRuns(db, projectId) {
|
|
129
|
+
const one = oneProject("h", projectId);
|
|
130
|
+
const rows = db
|
|
131
|
+
.prepare(`WITH runs AS MATERIALIZED (
|
|
132
|
+
SELECT h.project_id, ${heldBesideReviews("h")} AS held,
|
|
133
|
+
EXISTS(SELECT 1 FROM stages s WHERE s.project_id=h.project_id AND s.state NOT IN ('done','provided','skipped'))
|
|
134
|
+
AND ${still("h")} AS idle,
|
|
135
|
+
${nothingAdmitted("h")} AS unadmitted
|
|
136
|
+
FROM project_heads h WHERE ${liveProject("h", "project_id")}${one.sql})
|
|
137
|
+
SELECT project_id, held, idle AND (held OR unadmitted) AS stuck FROM runs
|
|
138
|
+
WHERE held OR (idle AND unadmitted)`)
|
|
139
|
+
.all(...one.params);
|
|
140
|
+
return new Map(rows.map((row) => {
|
|
141
|
+
const run = runRow.parse(row);
|
|
142
|
+
return [run.project_id, { held: run.held === 1, stuck: run.stuck === 1 }];
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
@@ -6,6 +6,15 @@ export const reviewStages = ["article", "images", "narration", "thumbnail", "sho
|
|
|
6
6
|
// Off is the same as the stage having no entry, which is what every project saved before
|
|
7
7
|
// reviews existed has.
|
|
8
8
|
export const reviewModes = ["off", "flag", "redo"];
|
|
9
|
+
// Who answers the reviews: a text model Slopify calls, or an external agent through MCP.
|
|
10
|
+
export const reviewers = ["model", "agent"];
|
|
11
|
+
// Who gave a verdict: the reviewer model, an external agent, the person (through the verdict
|
|
12
|
+
// route), or nobody when the review passed at its deadline.
|
|
13
|
+
export const reviewAnswerers = ["model", "agent", "person", "nobody"];
|
|
14
|
+
export const reviewerLabels = {
|
|
15
|
+
model: "Model",
|
|
16
|
+
agent: "External agent (MCP)",
|
|
17
|
+
};
|
|
9
18
|
// ceiling: the owner's numbers. Two redos is what a person would try before looking at it
|
|
10
19
|
// themselves; five is already six paid images for one slot.
|
|
11
20
|
export const defaultReviewRetries = 2;
|
|
@@ -35,6 +44,9 @@ export function reviewModeOf(settings, stage) {
|
|
|
35
44
|
export function reviewRetriesOf(settings) {
|
|
36
45
|
return settings?.retries ?? defaultReviewRetries;
|
|
37
46
|
}
|
|
47
|
+
export function reviewedByAgent(settings) {
|
|
48
|
+
return settings?.reviewer === "agent";
|
|
49
|
+
}
|
|
38
50
|
export function reviewedStages(settings) {
|
|
39
51
|
return reviewStages.filter((stage) => reviewModeOf(settings, stage) !== "off");
|
|
40
52
|
}
|
|
@@ -56,6 +68,7 @@ export function reviewSettingsForm(settings) {
|
|
|
56
68
|
provider: settings.provider,
|
|
57
69
|
model: settings.model,
|
|
58
70
|
...(settings.thinking === undefined ? {} : { thinking: settings.thinking }),
|
|
71
|
+
...(settings.reviewer === "agent" ? { reviewer: "agent" } : {}),
|
|
59
72
|
retries: settings.retries === undefined ? "" : String(settings.retries),
|
|
60
73
|
stages: Object.fromEntries(reviewStages.flatMap((stage) => {
|
|
61
74
|
const picked = settings.stages[stage];
|
|
@@ -86,6 +99,7 @@ export function reviewSettingsFromForm(form, makes) {
|
|
|
86
99
|
provider: form.provider,
|
|
87
100
|
model: form.model,
|
|
88
101
|
...(form.thinking === undefined ? {} : { thinking: form.thinking }),
|
|
102
|
+
...(form.reviewer === "agent" ? { reviewer: "agent" } : {}),
|
|
89
103
|
...(retries === undefined || !valid ? {} : { retries }),
|
|
90
104
|
stages,
|
|
91
105
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { actors } from "../agents/model.js";
|
|
3
|
+
import { reviewAnswerers, reviewStages } from "./model.js";
|
|
3
4
|
const rowSchema = z.object({
|
|
4
5
|
id: z.string(),
|
|
5
6
|
project_id: z.string(),
|
|
@@ -10,12 +11,14 @@ const rowSchema = z.object({
|
|
|
10
11
|
review_fingerprint: z.string(),
|
|
11
12
|
passed: z.number(),
|
|
12
13
|
reasons: z.string(),
|
|
13
|
-
outcome: z.enum(["passed", "flagged", "redo"]),
|
|
14
|
+
outcome: z.enum(["passed", "flagged", "redo", "auto-passed"]),
|
|
14
15
|
attempt: z.number().int(),
|
|
15
16
|
action: z.enum(["overruled", "redone"]).nullable(),
|
|
16
17
|
action_at: z.string().nullable(),
|
|
17
18
|
redo_state: z.enum(["pending", "started", "failed"]).nullable(),
|
|
18
19
|
redo_error: z.string().nullable(),
|
|
20
|
+
answered_by: z.enum(reviewAnswerers),
|
|
21
|
+
action_by: z.enum(actors).nullable(),
|
|
19
22
|
created_at: z.string(),
|
|
20
23
|
});
|
|
21
24
|
function recordOf(value) {
|
|
@@ -36,18 +39,21 @@ function recordOf(value) {
|
|
|
36
39
|
actionAt: row.action_at,
|
|
37
40
|
redoState: row.redo_state,
|
|
38
41
|
redoError: row.redo_error,
|
|
42
|
+
answeredBy: row.answered_by,
|
|
43
|
+
actionBy: row.action_by,
|
|
39
44
|
createdAt: row.created_at,
|
|
40
45
|
};
|
|
41
46
|
}
|
|
42
47
|
// One verdict per review step: the same review asked again (a retried step whose answer was
|
|
43
48
|
// saved but not published) replaces the earlier answer rather than adding a second one.
|
|
44
49
|
export function saveVerdict(db, record) {
|
|
45
|
-
db.prepare(`INSERT INTO review_verdicts(id,project_id,revision_id,item_key,stage,item_fingerprint,review_fingerprint,passed,reasons,outcome,attempt,action,action_at,redo_state,redo_error,created_at)
|
|
46
|
-
VALUES (
|
|
50
|
+
db.prepare(`INSERT INTO review_verdicts(id,project_id,revision_id,item_key,stage,item_fingerprint,review_fingerprint,passed,reasons,outcome,attempt,action,action_at,redo_state,redo_error,answered_by,action_by,created_at)
|
|
51
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
47
52
|
ON CONFLICT(project_id,review_fingerprint) DO UPDATE SET revision_id=excluded.revision_id,
|
|
48
53
|
item_fingerprint=excluded.item_fingerprint,passed=excluded.passed,reasons=excluded.reasons,
|
|
49
|
-
outcome=excluded.outcome,attempt=excluded.attempt,action=NULL,action_at=NULL,
|
|
50
|
-
redo_state=excluded.redo_state,redo_error=NULL,
|
|
54
|
+
outcome=excluded.outcome,attempt=excluded.attempt,action=NULL,action_at=NULL,action_by=NULL,
|
|
55
|
+
redo_state=excluded.redo_state,redo_error=NULL,answered_by=excluded.answered_by,
|
|
56
|
+
created_at=excluded.created_at`).run(record.id, record.projectId, record.revisionId, record.itemKey, record.stage, record.itemFingerprint, record.reviewFingerprint, record.passed ? 1 : 0, JSON.stringify(record.reasons), record.outcome, record.attempt, record.action, record.actionAt, record.redoState, record.redoError, record.answeredBy, record.actionBy, record.createdAt);
|
|
51
57
|
const saved = verdictByReview(db, record.projectId, record.reviewFingerprint);
|
|
52
58
|
if (saved === undefined)
|
|
53
59
|
throw new Error("Slopify hit an internal error (a review's verdict could not be saved). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
@@ -93,10 +99,13 @@ export function setRedoState(db, id, state, error = null) {
|
|
|
93
99
|
// A passed verdict has nothing to overrule. Overrule also calls off an automatic redo that is
|
|
94
100
|
// still waiting to start (what depends on the item then goes ahead with it as it is), but a
|
|
95
101
|
// redo already under way owns the item until it lands.
|
|
96
|
-
export function actOnVerdict(db, projectId, id, action, at) {
|
|
102
|
+
export function actOnVerdict(db, projectId, id, action, at, by = "person") {
|
|
97
103
|
const record = verdictById(db, projectId, id);
|
|
98
104
|
if (record === undefined)
|
|
99
105
|
return { ok: false, reason: "not-found" };
|
|
106
|
+
// The first decision stands: a second person or agent is told it was already made.
|
|
107
|
+
if (record.action !== null)
|
|
108
|
+
return { ok: false, reason: "decided", value: record };
|
|
100
109
|
if (action === "overruled" && record.passed)
|
|
101
110
|
return { ok: false, reason: "conflict" };
|
|
102
111
|
if (record.redoState === "started")
|
|
@@ -105,13 +114,21 @@ export function actOnVerdict(db, projectId, id, action, at) {
|
|
|
105
114
|
if (action !== "overruled")
|
|
106
115
|
return { ok: false, reason: "conflict" };
|
|
107
116
|
const called = db
|
|
108
|
-
.prepare("UPDATE review_verdicts SET action=?, action_at=?, redo_state=NULL WHERE id=? AND redo_state='pending'")
|
|
109
|
-
.run(action, at, id);
|
|
117
|
+
.prepare("UPDATE review_verdicts SET action=?, action_at=?, action_by=?, redo_state=NULL WHERE id=? AND redo_state='pending' AND action IS NULL")
|
|
118
|
+
.run(action, at, by, id);
|
|
110
119
|
if (Number(called.changes) !== 1)
|
|
111
120
|
return { ok: false, reason: "conflict" };
|
|
112
121
|
}
|
|
113
122
|
else {
|
|
114
|
-
|
|
123
|
+
const changed = db
|
|
124
|
+
.prepare("UPDATE review_verdicts SET action=?, action_at=?, action_by=? WHERE id=? AND action IS NULL")
|
|
125
|
+
.run(action, at, by, id);
|
|
126
|
+
if (Number(changed.changes) !== 1) {
|
|
127
|
+
const now = verdictById(db, projectId, id);
|
|
128
|
+
return now === undefined
|
|
129
|
+
? { ok: false, reason: "not-found" }
|
|
130
|
+
: { ok: false, reason: "decided", value: now };
|
|
131
|
+
}
|
|
115
132
|
}
|
|
116
133
|
const updated = verdictById(db, projectId, id);
|
|
117
134
|
return updated === undefined ? { ok: false, reason: "not-found" } : { ok: true, value: updated };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { imageReviewers, reviewModeOf, reviewRetriesMax, reviewRetriesMin, reviewStageLabels, reviewStages, visionStages, } from "./model.js";
|
|
1
|
+
import { imageReviewers, reviewedByAgent, reviewModeOf, reviewRetriesMax, reviewRetriesMin, reviewStageLabels, reviewStages, visionStages, } from "./model.js";
|
|
2
2
|
// A stage is reviewed only while it makes something: a provided article, uploaded images or
|
|
3
3
|
// narration Off leave nothing for a reviewer to look at, so a review left on there asks for
|
|
4
4
|
// nothing and holds nothing up.
|
|
@@ -39,7 +39,8 @@ export function reviewFields(draft) {
|
|
|
39
39
|
if (active.length === 0 || reviews === undefined)
|
|
40
40
|
return [];
|
|
41
41
|
const fields = [];
|
|
42
|
-
|
|
42
|
+
const byAgent = reviewedByAgent(reviews);
|
|
43
|
+
if (!byAgent && (reviews.provider.trim() === "" || reviews.model.trim() === ""))
|
|
43
44
|
fields.push({
|
|
44
45
|
field: "reviews.provider",
|
|
45
46
|
message: "Automatic reviews need a reviewer model. Choose one in the Reviews section, or set every review to Off.",
|
|
@@ -52,7 +53,8 @@ export function reviewFields(draft) {
|
|
|
52
53
|
message: `Enter a number of redos between ${reviewRetriesMin} and ${reviewRetriesMax} in the Reviews section.`,
|
|
53
54
|
});
|
|
54
55
|
const pictures = active.filter((stage) => visionStages.includes(stage));
|
|
55
|
-
if (
|
|
56
|
+
if (!byAgent &&
|
|
57
|
+
pictures.length > 0 &&
|
|
56
58
|
reviews.provider.trim() !== "" &&
|
|
57
59
|
!reviewerSeesImages(reviews.provider))
|
|
58
60
|
fields.push({
|
|
@@ -3,15 +3,28 @@ import { z } from "zod";
|
|
|
3
3
|
// at least one reason, since a reason is what the person reads beside it.
|
|
4
4
|
export const reviewReasonMax = 500;
|
|
5
5
|
export const reviewReasonsMax = 10;
|
|
6
|
-
const
|
|
6
|
+
export const verdictAnswerSchema = z
|
|
7
7
|
.object({
|
|
8
8
|
verdict: z.enum(["pass", "fail"]),
|
|
9
|
-
reasons: z
|
|
9
|
+
reasons: z
|
|
10
|
+
.array(z
|
|
11
|
+
.string()
|
|
12
|
+
.trim()
|
|
13
|
+
.min(1, "a reason can't be empty")
|
|
14
|
+
.max(reviewReasonMax, `a reason is at most ${String(reviewReasonMax)} characters`))
|
|
15
|
+
.max(reviewReasonsMax, `at most ${String(reviewReasonsMax)} reasons`),
|
|
10
16
|
})
|
|
11
17
|
.strict()
|
|
12
18
|
.refine((answer) => answer.verdict === "pass" || answer.reasons.length > 0, {
|
|
13
19
|
message: "a failed verdict names no reason",
|
|
14
20
|
});
|
|
21
|
+
// What was wrong with a verdict, where it was: " (reasons.0): a reason can't be empty". The
|
|
22
|
+
// reviewer's refused answer and an agent's refused verdict say it the same way.
|
|
23
|
+
export function verdictIssue(issues, otherwise) {
|
|
24
|
+
const issue = issues[0];
|
|
25
|
+
const where = issue === undefined || issue.path.length === 0 ? "" : ` (${issue.path.map(String).join(".")})`;
|
|
26
|
+
return `${where}: ${issue?.message ?? otherwise}`;
|
|
27
|
+
}
|
|
15
28
|
const answerRules = 'Answer with one JSON object and nothing else: {"verdict":"pass","reasons":[]} when the item is fine, or {"verdict":"fail","reasons":["..."]} with one short sentence per problem you found (at most 10). Name only real problems you can point to.';
|
|
16
29
|
export function reviewMessages(material) {
|
|
17
30
|
const attached = material.images.length === 0
|
|
@@ -55,15 +68,12 @@ export function parseVerdict(text) {
|
|
|
55
68
|
reason: `The reviewer's answer was not the JSON verdict Slopify asked for. ${retryHint}`,
|
|
56
69
|
};
|
|
57
70
|
}
|
|
58
|
-
const parsed =
|
|
59
|
-
if (!parsed.success)
|
|
60
|
-
const issue = parsed.error.issues[0];
|
|
61
|
-
const where = issue === undefined || issue.path.length === 0 ? "" : ` (${issue.path.join(".")})`;
|
|
71
|
+
const parsed = verdictAnswerSchema.safeParse(value);
|
|
72
|
+
if (!parsed.success)
|
|
62
73
|
return {
|
|
63
74
|
ok: false,
|
|
64
|
-
reason: `The reviewer's verdict was malformed${
|
|
75
|
+
reason: `The reviewer's verdict was malformed${verdictIssue(parsed.error.issues, "it did not match the verdict format")}. ${retryHint}`,
|
|
65
76
|
};
|
|
66
|
-
}
|
|
67
77
|
return {
|
|
68
78
|
ok: true,
|
|
69
79
|
value: { passed: parsed.data.verdict === "pass", reasons: parsed.data.reasons },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { agentRevisionIds } from "../agents/repo.js";
|
|
2
3
|
import { projectRevisionSchema } from "./schema.js";
|
|
3
4
|
export { insertManifestOutput, insertManifestPiece, outputsForRevision, piecesForRevision, selectOutputRecord, selectPieceRecord, } from "./manifest-repo.js";
|
|
4
5
|
const revisionRow = z.object({
|
|
@@ -37,6 +38,7 @@ export function currentRevisionId(db, projectId) {
|
|
|
37
38
|
}
|
|
38
39
|
export function listRevisionHistory(db, projectId) {
|
|
39
40
|
const head = currentRevisionId(db, projectId);
|
|
41
|
+
const byAgent = agentRevisionIds(db, projectId);
|
|
40
42
|
return db
|
|
41
43
|
.prepare("SELECT * FROM project_revisions WHERE project_id=? ORDER BY created_at DESC,id DESC")
|
|
42
44
|
.all(projectId)
|
|
@@ -49,6 +51,7 @@ export function listRevisionHistory(db, projectId) {
|
|
|
49
51
|
title: revision.config.title,
|
|
50
52
|
createdAt: revision.createdAt,
|
|
51
53
|
current: revision.id === head,
|
|
54
|
+
...(byAgent.has(revision.id) ? { byAgent: true } : {}),
|
|
52
55
|
};
|
|
53
56
|
});
|
|
54
57
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { derive } from "../../kernel/runner/graph.js";
|
|
2
|
+
import { derive, progressOf } from "../../kernel/runner/graph.js";
|
|
3
3
|
import { liveProject, stageStandingsByProject } from "../admission/repo.js";
|
|
4
|
+
import { readsByWaitBesideFailure } from "../admission/state-words.js";
|
|
4
5
|
import { queueEntries } from "../batch/index.js";
|
|
5
6
|
import { sampleIds } from "../onboarding/model.js";
|
|
6
7
|
import { readSampleRecord } from "../onboarding/state.js";
|
|
8
|
+
import { projectHolds } from "../reviews/holds.js";
|
|
7
9
|
import { limitWaitsByProject, listingWait } from "../run-cost/panel.js";
|
|
8
|
-
import { uploadedProjects } from "../uploads/repo.js";
|
|
10
|
+
import { setAsideProjects, uploadedAndSettled, uploadedProjects } from "../uploads/repo.js";
|
|
11
|
+
import { settledStatus } from "../uploads/rules.js";
|
|
9
12
|
import { nextOccurrence } from "./calendar.js";
|
|
10
13
|
import { preparedProject } from "./prepare.js";
|
|
11
14
|
import { scheduleRows } from "./repo.js";
|
|
@@ -109,21 +112,22 @@ const projectRow = z.object({
|
|
|
109
112
|
finished_at: z.string().nullable(),
|
|
110
113
|
});
|
|
111
114
|
// Projects the person has something to decide on in the current revision: a review
|
|
112
|
-
// checkpoint holding the run
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
+
// checkpoint holding the run for them (`reviews/holds.ts`: reached, and not a Before Video hold
|
|
116
|
+
// its schedule added, which waits for its day), or an automatic review that failed an item and
|
|
117
|
+
// waits for Overrule or Redo (its latest verdict, with no redo under way), as needs_attention
|
|
118
|
+
// reads them.
|
|
119
|
+
function awaitingReview(db, holds) {
|
|
120
|
+
const checkpoints = [...holds.holding]
|
|
121
|
+
.filter(([, held]) => held.some((hold) => hold.waitsFor === "person"))
|
|
122
|
+
.map(([id]) => id);
|
|
115
123
|
const ids = db
|
|
116
|
-
.prepare(`SELECT
|
|
117
|
-
JOIN project_heads h ON h.project_id=c.project_id AND h.revision_id=c.revision_id
|
|
118
|
-
WHERE c.state IN ('held','pending-review') AND c.approved_at IS NULL
|
|
119
|
-
UNION
|
|
120
|
-
SELECT v.project_id AS id FROM review_verdicts v
|
|
124
|
+
.prepare(`SELECT v.project_id AS id FROM review_verdicts v
|
|
121
125
|
JOIN project_heads h ON h.project_id=v.project_id AND h.revision_id=v.revision_id
|
|
122
126
|
WHERE v.passed=0 AND v.action IS NULL AND (v.redo_state IS NULL OR v.redo_state='failed')
|
|
123
127
|
AND NOT EXISTS (SELECT 1 FROM review_verdicts n WHERE n.project_id=v.project_id
|
|
124
128
|
AND n.item_key=v.item_key AND (n.created_at>v.created_at OR (n.created_at=v.created_at AND n.rowid>v.rowid)))`)
|
|
125
129
|
.all();
|
|
126
|
-
return new Set(ids.map((row) => z.string().parse(row.id)));
|
|
130
|
+
return new Set([...checkpoints, ...ids.map((row) => z.string().parse(row.id))]);
|
|
127
131
|
}
|
|
128
132
|
// Whether a finished project's settings make a video: projects stored before sources were
|
|
129
133
|
// recorded made one.
|
|
@@ -137,13 +141,20 @@ function sampleProjectIds(db) {
|
|
|
137
141
|
}));
|
|
138
142
|
}
|
|
139
143
|
// What a project on the calendar asks of the person, if anything. The same readings Home's
|
|
140
|
-
// Needs you uses: a failed
|
|
141
|
-
|
|
144
|
+
// Needs you uses: a failed run, a paused run, one a restart stopped (Continue the run), or one
|
|
145
|
+
// waiting for a review. A step failed beside a wait reads by that wait and is listed apart
|
|
146
|
+
// (`stepFailed`). One Kept as is asks nothing, nor does an uploaded one that reads Done
|
|
147
|
+
// (`uploadedAndSettled`).
|
|
148
|
+
function needsOf(state, ask) {
|
|
142
149
|
if (state === "failed")
|
|
143
150
|
return "failed";
|
|
144
151
|
if (state === "paused")
|
|
145
152
|
return "paused";
|
|
146
|
-
if (
|
|
153
|
+
if (ask.aside || ask.settled)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (ask.stopped)
|
|
156
|
+
return "stopped";
|
|
157
|
+
if (ask.reviewing && state !== "canceled")
|
|
147
158
|
return "review";
|
|
148
159
|
return undefined;
|
|
149
160
|
}
|
|
@@ -151,10 +162,13 @@ function needsOf(state, reviewing) {
|
|
|
151
162
|
// the range, with what each needs from the person and whether its video is ready to upload.
|
|
152
163
|
function projectsIn(deps, from, to) {
|
|
153
164
|
const standings = stageStandingsByProject(deps.db);
|
|
154
|
-
const
|
|
165
|
+
const settled = uploadedAndSettled(deps.db);
|
|
166
|
+
const holds = projectHolds(deps.db, { standings, settled });
|
|
167
|
+
const reviewing = awaitingReview(deps.db, holds);
|
|
155
168
|
const uploads = uploadedProjects(deps.db);
|
|
156
169
|
const samples = sampleProjectIds(deps.db);
|
|
157
170
|
const waits = limitWaitsByProject(deps.db);
|
|
171
|
+
const aside = setAsideProjects(deps.db);
|
|
158
172
|
const fromSchedule = new Map();
|
|
159
173
|
for (const row of deps.db
|
|
160
174
|
.prepare(`SELECT DISTINCT admitted.value AS project_id, schedule_runs.schedule_id
|
|
@@ -180,14 +194,22 @@ function projectsIn(deps, from, to) {
|
|
|
180
194
|
.all(to.toISOString(), from.toISOString())
|
|
181
195
|
.flatMap((row) => {
|
|
182
196
|
const project = projectRow.parse(row);
|
|
183
|
-
const state = derive(standings.get(project.id) ?? [], project.paused === 1);
|
|
197
|
+
const state = settledStatus(derive(standings.get(project.id) ?? [], project.paused === 1), settled.has(project.id));
|
|
184
198
|
const terminal = state === "done" || state === "partial" || state === "failed" || state === "canceled";
|
|
185
199
|
if (terminal) {
|
|
186
200
|
const ended = project.finished_at ?? project.created_at;
|
|
187
201
|
if (ended < from.toISOString() || ended >= to.toISOString())
|
|
188
202
|
return [];
|
|
189
203
|
}
|
|
190
|
-
|
|
204
|
+
// One Kept as is waits for nobody. A run parked on an agent's review holds no checkpoint
|
|
205
|
+
// of the person's, but an item already flagged still waits for their Overrule or Redo.
|
|
206
|
+
const waitingForAgent = holds.waitingForAgent.has(project.id);
|
|
207
|
+
const needs = needsOf(state, {
|
|
208
|
+
stopped: holds.stopped.has(project.id),
|
|
209
|
+
reviewing: reviewing.has(project.id),
|
|
210
|
+
aside: aside.has(project.id),
|
|
211
|
+
settled: settled.has(project.id),
|
|
212
|
+
});
|
|
191
213
|
const ready = (state === "done" || state === "partial") &&
|
|
192
214
|
makesVideo(project.video_source) &&
|
|
193
215
|
!uploads.has(project.id) &&
|
|
@@ -201,8 +223,16 @@ function projectsIn(deps, from, to) {
|
|
|
201
223
|
createdAt: project.created_at,
|
|
202
224
|
finishedAt: terminal ? (project.finished_at ?? project.created_at) : null,
|
|
203
225
|
scheduleId: fromSchedule.get(project.id) ?? null,
|
|
226
|
+
progress: progressOf(standings.get(project.id) ?? []),
|
|
204
227
|
...(needs === undefined ? {} : { needs }),
|
|
205
228
|
...(ready ? { readyToUpload: true } : {}),
|
|
229
|
+
...(aside.has(project.id) ? { setAside: true } : {}),
|
|
230
|
+
...(holds.prepared.has(project.id) ? { prepared: true } : {}),
|
|
231
|
+
...(waitingForAgent ? { waitingForAgent: true } : {}),
|
|
232
|
+
...(holds.stopped.has(project.id) ? { stopped: true } : {}),
|
|
233
|
+
...(readsByWaitBesideFailure(state, standings.get(project.id) ?? [])
|
|
234
|
+
? { stepFailed: true }
|
|
235
|
+
: {}),
|
|
206
236
|
...(waiting === undefined ? {} : { limitWaits: waiting.map(listingWait) }),
|
|
207
237
|
},
|
|
208
238
|
];
|
|
@@ -154,3 +154,9 @@ export function preparedTopics(deps, scheduleId) {
|
|
|
154
154
|
return found === undefined ? [] : [{ title, topic: topic.title, projectId: found.projectId }];
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
|
+
// A project a schedule prepared ahead whose Before Video hold is the schedule's own: that hold
|
|
158
|
+
// waits for its day, not for anyone (`reviews/holds.ts` reads it with the project's other
|
|
159
|
+
// checkpoints). The day deletes the row (`continuePrepared`).
|
|
160
|
+
export function heldForItsDay(alias) {
|
|
161
|
+
return `EXISTS (SELECT 1 FROM prepared_videos p WHERE p.project_id=${alias}.project_id AND p.added_checkpoint=1)`;
|
|
162
|
+
}
|
|
@@ -5,7 +5,7 @@ import { cadenceSchema, validTimeZone } from "./calendar.js";
|
|
|
5
5
|
const id = z.uuid();
|
|
6
6
|
// What a project on the calendar waits on the person for: a failed or paused run, or a review
|
|
7
7
|
// (a checkpoint holding the run, or an automatic review's failed item).
|
|
8
|
-
export const calendarNeeds = ["failed", "paused", "review"];
|
|
8
|
+
export const calendarNeeds = ["failed", "paused", "stopped", "review"];
|
|
9
9
|
const keywordName = z
|
|
10
10
|
.string()
|
|
11
11
|
.min(1)
|
|
@@ -242,10 +242,22 @@ export const calendarProjectSchema = z
|
|
|
242
242
|
createdAt: z.string(),
|
|
243
243
|
finishedAt: z.string().nullable(),
|
|
244
244
|
scheduleId: z.string().nullable(),
|
|
245
|
+
// The run's share done (`progressOf`), which tells a held run from a queued one. Older
|
|
246
|
+
// servers leave it out.
|
|
247
|
+
progress: z.number().min(0).max(1).default(0),
|
|
245
248
|
// What the project waits on the person for; left out when nothing.
|
|
246
249
|
needs: z.enum(calendarNeeds).optional(),
|
|
247
250
|
// Finished, makes a video, not marked uploaded and not a bundled sample.
|
|
248
251
|
readyToUpload: z.literal(true).optional(),
|
|
252
|
+
// What a held run waits for when it isn't the person (`reviews/holds.ts`), and a run
|
|
253
|
+
// Kept as is, as the Projects list says them.
|
|
254
|
+
setAside: z.literal(true).optional(),
|
|
255
|
+
prepared: z.literal(true).optional(),
|
|
256
|
+
waitingForAgent: z.literal(true).optional(),
|
|
257
|
+
stopped: z.literal(true).optional(),
|
|
258
|
+
// A step failed while the run still reads by a wait (`readsByWaitBesideFailure`): listed as a
|
|
259
|
+
// failed item beside what it waits for, as on Home. Older servers leave it out.
|
|
260
|
+
stepFailed: z.literal(true).optional(),
|
|
249
261
|
// Stages waiting for a CLI plan's limits to reset.
|
|
250
262
|
limitWaits: z
|
|
251
263
|
.array(z
|