@miadi/connector 0.1.19 → 0.1.21

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 (43) hide show
  1. package/README.md +40 -5
  2. package/dist/approve.d.ts +16 -2
  3. package/dist/approve.js +185 -16
  4. package/dist/bin.js +3 -0
  5. package/dist/mission-status.d.ts +13 -1
  6. package/dist/mission-status.js +13 -5
  7. package/dist/model-preflight.d.ts +9 -2
  8. package/dist/model-preflight.js +23 -1
  9. package/dist/provider-policy.d.ts +94 -0
  10. package/dist/provider-policy.js +150 -0
  11. package/dist/providers.d.ts +7 -3
  12. package/dist/providers.js +13 -7
  13. package/dist/queue-state.js +12 -1
  14. package/dist/reading/adapters.d.ts +18 -0
  15. package/dist/reading/adapters.js +90 -0
  16. package/dist/reading/contract.d.ts +153 -0
  17. package/dist/reading/contract.js +79 -0
  18. package/dist/reading/markdown.d.ts +47 -0
  19. package/dist/reading/markdown.js +52 -0
  20. package/dist/reading/privacy.d.ts +7 -0
  21. package/dist/reading/privacy.js +14 -0
  22. package/dist/reading/workflow.d.ts +93 -0
  23. package/dist/reading/workflow.js +325 -0
  24. package/dist/records.d.ts +1 -1
  25. package/dist/records.js +6 -0
  26. package/dist/request-state.js +9 -1
  27. package/dist/research-digest.d.ts +30 -0
  28. package/dist/research-digest.js +104 -0
  29. package/dist/research-shelf.d.ts +173 -0
  30. package/dist/research-shelf.js +297 -0
  31. package/dist/runtime-steer.d.ts +182 -0
  32. package/dist/runtime-steer.js +543 -0
  33. package/dist/server.d.ts +14 -0
  34. package/dist/server.js +4 -12
  35. package/dist/steer.d.ts +21 -7
  36. package/dist/steer.js +115 -10
  37. package/dist/store.d.ts +2 -0
  38. package/dist/store.js +18 -1
  39. package/dist/tools.d.ts +29 -1
  40. package/dist/tools.js +435 -50
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/package.json +13 -1
@@ -0,0 +1,325 @@
1
+ import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, fsyncSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { z } from "zod";
5
+ import { current } from "../records.js";
6
+ import { Ledger } from "../store.js";
7
+ import { contentHash } from "../revisions.js";
8
+ import { academic, reflective, insert } from "./markdown.js";
9
+ import { baseReview, block, canonicalVideo, hash, ReadingBlocked, readingScope, sourcePart, suppliedDialogue, survey, reflection } from "./contract.js";
10
+ /** One workflow per person and video; two concurrent releases cannot spend twice. */
11
+ export function workflowId(actor, video, revisionOf) { return `reading:${hash(`${actor}:${video}${revisionOf ? `:${revisionOf}` : ""}`).slice(0, 32)}`; }
12
+ export class ReadingWorkflow {
13
+ stateDir;
14
+ adapters;
15
+ ledger;
16
+ constructor(stateDir, adapters) {
17
+ this.stateDir = stateDir;
18
+ this.adapters = adapters;
19
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
20
+ this.ledger = new Ledger(join(stateDir, "ledger.jsonl"));
21
+ }
22
+ receipts(id) {
23
+ return this.ledger.all().filter(r => r.kind === "observation" && r.body.reading && r.body.reading.workflow_id === id).map(r => r.body.reading);
24
+ }
25
+ approved(requestId) {
26
+ const r = current(this.ledger.all()).find(x => x.id === requestId);
27
+ if (!r || r.kind !== "run_request" || r.approval_state !== "approved")
28
+ block("blocked_permission", "Approve the bounded reading request through the connector decision door.");
29
+ if (r.completion_receipt?.instructions)
30
+ block("blocked_permission", "Approval carries changes: reconcile them in a revised typed scope before execution.");
31
+ return r;
32
+ }
33
+ source(request) {
34
+ const scope = readingScope.parse(request.body.reading);
35
+ if (scope.max_cost_usd > 0 && !scope.budget_approval_ref)
36
+ block("blocked_permission", "A nonzero provider budget needs a separate budget approval reference.");
37
+ if (scope.max_cost_usd > 0) {
38
+ const b = this.approved(scope.budget_approval_ref);
39
+ const parsed = z.object({ source_intent: z.string(), max_cost_usd: z.number().finite().min(0), providers: z.array(z.string()).min(1) }).strict().safeParse(b.body.reading_budget);
40
+ if (!parsed.success || parsed.data.source_intent !== scope.source_intent || parsed.data.max_cost_usd < scope.max_cost_usd || scope.providers.some(p => !parsed.data.providers.includes(p)) || b.actor.person_id !== request.actor.person_id)
41
+ block("blocked_permission", "The separate approved budget must name this source, person, providers and finite ceiling.");
42
+ }
43
+ const source = current(this.ledger.all()).find(r => r.id === scope.source_intent);
44
+ if (!source || source.kind !== "source_intent" || source.actor.person_id !== request.actor.person_id || source.state !== "ready" || source.body.readiness !== "released_for_review")
45
+ block("withheld", "The source owner must explicitly release this source; registration or uploading does not start work.");
46
+ const video = canonicalVideo(String(source.body.video_id));
47
+ if (scope.revision_of) {
48
+ const previous = this.done(scope.revision_of, "registration");
49
+ const priorRequest = current(this.ledger.all()).find(r => r.kind === "run_request" && r.body.workflow_id === scope.revision_of && r.actor.person_id === request.actor.person_id && r.approval_state === "approved" && r.body.reading?.source_intent === scope.source_intent);
50
+ if ((!previous && !priorRequest) || (previous && (previous.actor.person_id !== request.actor.person_id || previous.video_id !== video)) || this.done(scope.revision_of, "import"))
51
+ block("blocked_permission", "A revision must name this author's unimported prior workflow for the same video.");
52
+ }
53
+ return { scope, source, video, id: workflowId(request.actor.person_id, video, scope.revision_of) };
54
+ }
55
+ async locked(id, fn) {
56
+ const root = join(this.stateDir, "reading-locks");
57
+ mkdirSync(root, { recursive: true, mode: 0o700 });
58
+ const file = join(root, hash(id));
59
+ // Serialize every acquisition/recovery, including the dead-owner check and replacement.
60
+ // A crash in this tiny critical section fails closed; it is never stolen on a timer.
61
+ const acquiring = `${file}.acquiring`;
62
+ try {
63
+ mkdirSync(acquiring, { mode: 0o700 });
64
+ }
65
+ catch {
66
+ return block("failed_retryable", `Acquisition is in progress. If its worker crashed, inspect and recover ${acquiring} before retrying.`);
67
+ }
68
+ try {
69
+ try {
70
+ mkdirSync(file, { mode: 0o700 });
71
+ }
72
+ catch (e) {
73
+ if (e.code !== "EEXIST")
74
+ throw e;
75
+ // No time-based stealing: a slow provider must never become a second writer.
76
+ let pid;
77
+ try {
78
+ pid = Number(readFileSync(join(file, "pid"), "utf8"));
79
+ }
80
+ catch {
81
+ return block("failed_retryable", "Workflow lock is initializing; retry, or inspect its owner after a crash.");
82
+ }
83
+ if (!Number.isSafeInteger(pid) || pid <= 0)
84
+ block("failed_retryable", "Inspect the invalid workflow lock owner.");
85
+ try {
86
+ process.kill(pid, 0);
87
+ return block("failed_retryable", "Another worker holds this video; read its receipts.");
88
+ }
89
+ catch (e) {
90
+ if (e.code !== "ESRCH")
91
+ throw e;
92
+ }
93
+ // The acquisition mutex prevents two recoverers from moving a new active lock.
94
+ const stale = `${file}.dead-${randomUUID()}`;
95
+ try {
96
+ renameSync(file, stale);
97
+ }
98
+ catch {
99
+ return block("failed_retryable", "Another worker recovered this lock; retry.");
100
+ }
101
+ rmSync(stale, { recursive: true });
102
+ mkdirSync(file, { mode: 0o700 });
103
+ }
104
+ writeFileSync(join(file, "pid"), String(process.pid), { mode: 0o600 });
105
+ }
106
+ finally {
107
+ rmSync(acquiring, { recursive: true });
108
+ }
109
+ try {
110
+ return await fn();
111
+ }
112
+ finally {
113
+ rmSync(file, { recursive: true });
114
+ }
115
+ }
116
+ record(request, scope, id, video, stage, status, artifact, next_action = "") {
117
+ const previous = this.receipts(id), at = new Date().toISOString();
118
+ const starts = previous.filter(r => r.stage === stage && r.status === "started");
119
+ const attempt = status === "started" ? starts.length + 1 : starts.length || 1;
120
+ const started = status === "started" ? at : starts.at(-1)?.started_at ?? at;
121
+ const receipt = { stage, status, attempt, started_at: started, completed_at: at, workflow_id: id, request_id: request.id, video_id: video, actor: request.actor, consent_scope: scope, approval_state: "approved_bounded_processing", artifact_refs: [], sha256: hash(JSON.stringify(artifact)), previous_sha256: previous.at(-1)?.sha256 ?? null, artifact, next_action };
122
+ const record = { id: `observation:${randomUUID()}`, kind: "observation", actor: request.actor, created_at: at, circle: "continuity", claim_category: "observed", refs: [request.id, `https://youtu.be/${video}`], evidence_refs: [], body: { reading: receipt } };
123
+ receipt.artifact_refs = [record.id];
124
+ this.ledger.appendDurable(record);
125
+ // Persist a new ledger directory entry too, before a downstream call.
126
+ const fd = openSync(this.stateDir, "r");
127
+ try {
128
+ fsyncSync(fd);
129
+ }
130
+ finally {
131
+ closeSync(fd);
132
+ }
133
+ return receipt;
134
+ }
135
+ done(id, stage) { return this.receipts(id).find(r => r.stage === stage && r.status === "completed"); }
136
+ provider(scope, id, stage) {
137
+ const p = this.adapters.providers[stage];
138
+ if (!p || !p.available)
139
+ block(stage === "transcript" ? "blocked_transcript" : "blocked_permission", `Configure and verify an available ${stage} provider; no fallback was called.`);
140
+ if (!p.idempotent || !scope.providers.includes(p.id) || (p.external && !scope.allow_external_processing) || (p.media && !scope.allow_media_access))
141
+ block("blocked_permission", `Authorize provider ${p.id}, its media/external access and durable retry contract first.`);
142
+ if (!Number.isFinite(p.max_cost_usd) || p.max_cost_usd < 0)
143
+ block("blocked_permission", "Provider must declare a finite maximum cost.");
144
+ const reservations = this.receipts(id).filter(r => r.status === "reserved");
145
+ const spent = reservations.reduce((s, r) => s + r.artifact.max_cost_usd, 0);
146
+ if (!reservations.some(r => r.stage === stage) && spent + p.max_cost_usd > scope.max_cost_usd)
147
+ block("blocked_permission", `The ${stage} provider exceeds the approved total budget; request a separate budget.`);
148
+ const old = reservations.find(r => r.stage === stage);
149
+ if (old && (old.artifact.id !== p.id || old.artifact.method !== p.method || old.artifact.max_cost_usd !== p.max_cost_usd))
150
+ block("blocked_permission", "Provider settings changed after reservation; reconcile the prior operation before retry.");
151
+ return p;
152
+ }
153
+ async run(requestId, companion) {
154
+ const request = this.approved(requestId), { scope, video, id } = this.source(request);
155
+ const approvedCompanion = request.body.reading_companion;
156
+ if (companion !== undefined && hash(JSON.stringify(companion)) !== hash(JSON.stringify(approvedCompanion ?? null)))
157
+ block("blocked_permission", "The companion text must be part of the author's approved reading request.");
158
+ companion = approvedCompanion;
159
+ return this.locked(workflowId(request.actor.person_id, video), async () => {
160
+ let stage = "registration";
161
+ const result = (status, next_action) => ({ workflow_id: id, status, next_action, receipts: this.receipts(id) });
162
+ try {
163
+ const registration = this.done(id, "registration");
164
+ if (registration && registration.artifact.request_id !== requestId)
165
+ return result("duplicate_video", "This video already has a bounded workflow. Resume its original request.");
166
+ if (registration && registration.artifact.request_hash !== contentHash(request))
167
+ return result(registration.artifact.request_id === requestId ? "blocked_permission" : "duplicate_video", "This video already has a bounded workflow. Resume its original request; a changed grant cannot overwrite its receipts.");
168
+ if (!registration)
169
+ this.record(request, scope, id, video, stage, "completed", { request_hash: contentHash(request), request_id: requestId, video_id: video, kind: scope.kind });
170
+ let context = { workflow_id: id, video_id: video, scope, idempotency_key: "", transcript: undefined, source_part_id: "" };
171
+ const execute = async (name, fn, validate) => {
172
+ stage = name;
173
+ const done = this.done(id, name);
174
+ if (done)
175
+ return done.artifact;
176
+ // Source release is rechecked at every new stage.
177
+ this.source(this.approved(requestId));
178
+ const p = this.provider(scope, id, name);
179
+ if (!this.receipts(id).some(r => r.stage === name && r.status === "reserved"))
180
+ this.record(request, scope, id, video, name, "reserved", p);
181
+ this.record(request, scope, id, video, name, "started", { provider: p.id, method: p.method });
182
+ const output = validate(await fn({ ...context, idempotency_key: `${id}:${name}` }));
183
+ return this.record(request, scope, id, video, name, "completed", output).artifact;
184
+ };
185
+ const oldTranscript = scope.revision_of ? this.done(scope.revision_of, "transcript") : undefined;
186
+ if (!this.done(id, "transcript") && oldTranscript && oldTranscript.artifact.language === scope.language && oldTranscript.artifact.rights === scope.rights && scope.providers.includes(oldTranscript.artifact.provider)) {
187
+ this.record(request, scope, id, video, "transcript", "completed", { ...oldTranscript.artifact, reused_from: oldTranscript.artifact_refs });
188
+ }
189
+ const transcript = await execute("transcript", c => this.adapters.acquire(c), x => {
190
+ if (x === null)
191
+ block("blocked_transcript", "No authorized captions or working local transcript. Supply exact transcript/caption bytes with provenance and retry; no paid fallback ran.");
192
+ const part = sourcePart.parse(x);
193
+ if (!part.text.trim() || part.video_id !== video || part.channel !== "video" || part.language !== scope.language || part.provider !== this.adapters.providers.transcript.id || part.rights !== scope.rights || /\b(summary|key points)\b/i.test(part.kind))
194
+ block("blocked_transcript", "Video transcript provenance, language, rights or channel does not match this grant.");
195
+ if (part.segments.some((s, i) => s.end_ms < s.start_ms || (i > 0 && s.start_ms < part.segments[i - 1].start_ms)))
196
+ block("blocked_transcript", "Transcript has invalid or unordered timecodes.");
197
+ if (part.timestamp_granularity !== "none" && !part.segments.length)
198
+ block("blocked_transcript", "Claimed timestamp granularity needs actual segments.");
199
+ return { ...part, sha256: hash(part.text), source_part_id: `source_part:${hash(`${video}:${part.provider}:${scope.language}:${hash(part.text)}`)}`, stored_at: new Date().toISOString(), normalized_sha256: part.normalized_text ? hash(part.normalized_text) : null };
200
+ });
201
+ context = { ...context, transcript, source_part_id: transcript.source_part_id };
202
+ stage = "lookup";
203
+ if (!this.done(id, "lookup")) {
204
+ const base = await this.adapters.lookup(video);
205
+ if (base && (base.video_id !== video || !Number.isSafeInteger(base.version) || base.version < 1 || !base.markdown))
206
+ block("blocked_permission", "Review lookup returned an invalid or ambiguous base; resolve it before generating.");
207
+ this.record(request, scope, id, video, "lookup", "completed", { base });
208
+ }
209
+ const base = this.done(id, "lookup").artifact.base;
210
+ const validateBase = (x) => {
211
+ const b = baseReview.parse(x);
212
+ for (const c of b.claims) {
213
+ if (c.category === "transcript_supported" && (c.at_ms === undefined || !transcript.segments.some((s) => c.at_ms >= s.start_ms && c.at_ms <= s.end_ms)))
214
+ block("blocked_transcript", "A source claim lacks an archived transcript timecode.");
215
+ if (c.category === "visually_evidenced" && !c.visual_receipt)
216
+ block("blocked_transcript", "A visual claim requires a separately inspected visual evidence receipt.");
217
+ }
218
+ return b;
219
+ };
220
+ const priorV1 = scope.revision_of && !base && transcript.reused_from ? this.done(scope.revision_of, "v1") : undefined;
221
+ if (priorV1 && !this.done(id, "v1"))
222
+ this.record(request, scope, id, video, "v1", "completed", { ...priorV1.artifact, reused_from: priorV1.artifact_refs });
223
+ const v1 = base ? { markdown: base.markdown, claims: [{ category: "inference", claim: "Existing review reused verbatim; its claims have not been independently verified." }], model: "existing_review" } : await execute("v1", c => this.adapters.generateV1(c), validateBase);
224
+ if (base && !this.done(id, "v1"))
225
+ this.record(request, scope, id, video, "v1", "completed", { ...v1, reused: true, review_id: base.id, version: base.version });
226
+ context.base = v1;
227
+ const academicResult = await execute("academic", c => this.adapters.generateSurvey(c), asyncIdentity);
228
+ // Verification is its own resumable stage, before any V2 is accepted.
229
+ stage = "citations";
230
+ this.source(this.approved(requestId));
231
+ let checked = this.done(id, "citations")?.artifact;
232
+ if (!checked) {
233
+ const parsed = survey.parse(academicResult);
234
+ if (parsed.fields.length > scope.max_fields || new Set(parsed.sources.map(s => s.id)).size !== parsed.sources.length)
235
+ block("blocked_citations", "Field limit exceeded or duplicate source identifiers.");
236
+ const verified = [];
237
+ const normalize = (s) => s.replace(/\s+/g, " ").trim().toLowerCase();
238
+ for (const source of parsed.sources) {
239
+ const proof = await this.adapters.verifySource(source);
240
+ if (proof.url !== source.url || !Number.isFinite(Date.parse(proof.fetched_at)) || !normalize(proof.text) || !normalize(source.excerpt) || !normalize(proof.text).includes(normalize(source.excerpt)))
241
+ block("blocked_citations", `Source ${source.id} could not be verified against retrieved text. Correct or remove it, then request a new bounded candidate.`);
242
+ verified.push({ id: source.id, url: source.url, fetched_at: proof.fetched_at, sha256: hash(proof.text), excerpt_sha256: hash(source.excerpt), claim: source.claim, status: "retrieved_excerpt", full_reading: false });
243
+ }
244
+ for (const field of parsed.fields) {
245
+ if (field.body.trim().split(/\s+/).length > scope.max_field_words || !transcript.segments.some((s) => field.at_ms >= s.start_ms && field.at_ms <= s.end_ms) || field.source_ids.some(id => !verified.some(v => v.id === id)))
246
+ block("blocked_citations", "Field length, timestamp or citation does not meet the authorized survey contract.");
247
+ }
248
+ checked = this.record(request, scope, id, video, stage, "completed", { survey: parsed, verified }).artifact;
249
+ }
250
+ context.survey = checked.survey;
251
+ stage = "v2";
252
+ this.source(this.approved(requestId));
253
+ const v2 = this.done(id, "v2")?.artifact ?? this.record(request, scope, id, video, stage, "completed", academic(v1.markdown, checked.survey)).artifact;
254
+ const reflectionResult = await execute("reflective", c => this.adapters.generateReflection(c), x => {
255
+ const r = reflection.parse(x);
256
+ if (new Set(r.dialogue.map(d => d.voice)).size !== 2)
257
+ block("blocked_citations", "Reflective stage must contain both labelled generated voices.");
258
+ return r;
259
+ });
260
+ stage = "v3";
261
+ this.source(this.approved(requestId));
262
+ const supplied = companion === undefined ? undefined : suppliedDialogue.parse(companion);
263
+ const prior = this.done(id, "v3");
264
+ if (prior && hash(JSON.stringify(supplied ?? null)) !== prior.artifact.companion_sha256)
265
+ block("blocked_permission", "A later companion is a new attributed contribution; the immutable candidate cannot be overwritten.");
266
+ if (!prior) {
267
+ const v3 = reflective(v2.markdown, reflectionResult, supplied);
268
+ const manifest = { workflow_id: id, video_id: video, source_part_id: transcript.source_part_id, transcript_sha256: transcript.sha256, transcript_receipt: this.done(id, "transcript").artifact_refs, language: transcript.language, rights: transcript.rights, locator: transcript.locator, provider: transcript.provider, fetched_at: transcript.fetched_at, timestamp_granularity: transcript.timestamp_granularity, alignment_status: transcript.alignment_status, speaker_status: transcript.speaker_status, claim_categories: v1.claims, sources: checked.verified, base, companion: supplied ?? null, gates: ["editorial_import", "publication", "deep_diver", "chronicle", "cultural_reuse"] };
269
+ const manifestText = `\n\n## Processing Provenance\n\nTranscript archived before generation. Source transcript supports spoken content; visual evidence is separate. Unknown speakers and channel synchronization remain uncertain.\n\n\`\`\`json\n${JSON.stringify(manifest, null, 2)}\n\`\`\`\n`;
270
+ const final = insert(v3.markdown, v3.markdown.length, manifestText);
271
+ this.record(request, scope, id, video, stage, "completed", { markdown: final.markdown, sha256: hash(final.markdown), bytes: Buffer.byteLength(final.markdown), diffs: [...v2.diffs, v3.diff, final.diff], manifest, companion_sha256: hash(JSON.stringify(supplied ?? null)), expected_version: base?.version ?? 0, expected_sha256: hash(base?.markdown ?? ""), review_id: base?.id ?? null });
272
+ }
273
+ const imported = this.done(id, "import");
274
+ return imported ? result("imported", "Public review import was read back exactly. Deep Diver, Chronicle and further publication/reuse remain separate gates.") : result("awaiting_editorial_decision", "Inspect V1/V2/V3, the insertion diff and manifest. Approve the exact candidate hash and base in a separate editorial request; import is a public release.");
275
+ }
276
+ catch (e) {
277
+ const blocked = e instanceof ReadingBlocked ? e : new ReadingBlocked(stage === "transcript" ? "blocked_transcript" : stage === "academic" || stage === "citations" ? "blocked_citations" : "failed_retryable", "Stage failed or returned malformed data. Inspect the adapter; retry the same request and idempotency key after repair.");
278
+ this.record(request, scope, id, video, stage, blocked.status, { error_type: e instanceof Error ? e.name : "unknown" }, blocked.next_action);
279
+ return result(blocked.status, blocked.next_action);
280
+ }
281
+ });
282
+ }
283
+ async importApproved(requestId) {
284
+ const request = this.approved(requestId), approval = request.body.reading_import;
285
+ if (!approval || typeof approval.workflow_id !== "string" || !/^reading:[a-f\d]{32}$/.test(approval.workflow_id))
286
+ block("blocked_permission", "An editorial request must name one staged workflow and exact candidate/base hashes.");
287
+ const id = approval.workflow_id;
288
+ const stagedOwner = this.done(id, "registration");
289
+ if (!stagedOwner)
290
+ block("blocked_permission", "Workflow is unavailable.");
291
+ return this.locked(workflowId(stagedOwner.actor.person_id, stagedOwner.video_id), async () => {
292
+ const staged = this.done(id, "v3"), registration = this.done(id, "registration");
293
+ if (!staged || !registration || staged.actor.person_id !== request.actor.person_id)
294
+ block("blocked_permission", "Only the source owner can request this candidate's editorial import.");
295
+ const candidate = staged.artifact, scope = staged.consent_scope, video = staged.video_id;
296
+ // Recheck the original bounded grant and current source release before sharing anything.
297
+ this.source(this.approved(registration.artifact.request_id));
298
+ if (approval.sha256 !== candidate.sha256 || approval.expected_version !== candidate.expected_version || approval.expected_sha256 !== candidate.expected_sha256 || approval.public_release !== true)
299
+ block("blocked_permission", "Approve the exact candidate and base, explicitly including public release.");
300
+ if (this.done(id, "import"))
301
+ return { workflow_id: id, status: "imported", next_action: "Already imported and verified; nothing was written again.", receipts: this.receipts(id) };
302
+ if (!this.adapters.atomicImport)
303
+ block("blocked_permission", "Review service lacks verified atomic conditional import and idempotency. Apply the v0 owner prompt; never fall back to the legacy import API.");
304
+ const intent = this.done(id, "import_intent");
305
+ // Persist the approved exact write ahead of the remote call; a lost response replays the same key.
306
+ if (!intent)
307
+ this.record(request, scope, id, video, "import_intent", "completed", { sha256: candidate.sha256, approval: request.id });
308
+ this.record(request, scope, id, video, "import", "started", { candidate_sha256: candidate.sha256 });
309
+ try {
310
+ const receipt = await this.adapters.importExact({ video_id: video, review_id: candidate.review_id, expected_version: candidate.expected_version, expected_sha256: candidate.expected_sha256, markdown: candidate.markdown, sha256: candidate.sha256, idempotency_key: `${id}:import:${candidate.sha256}` });
311
+ const back = await this.adapters.readExact(receipt.id, receipt.version);
312
+ if (back.id !== receipt.id || (candidate.review_id && back.id !== candidate.review_id) || back.video_id !== video || back.version !== candidate.expected_version + 1 || hash(back.markdown) !== candidate.sha256)
313
+ block("failed_retryable", "Import read-back mismatch; reconcile the idempotency receipt before any further write.");
314
+ this.record(request, scope, id, video, "import", "completed", { ...receipt, sha256: hash(back.markdown), bytes: Buffer.byteLength(back.markdown), editorial_approval: request.id, publication: "explicitly_approved", deep_diver: "not_requested", chronicle: "not_requested" });
315
+ }
316
+ catch (e) {
317
+ const b = e instanceof ReadingBlocked ? e : new ReadingBlocked("failed_retryable", "Remote import outcome is uncertain. Replay the same durable operation key; never send a legacy write.");
318
+ this.record(request, scope, id, video, "import", b.status, { candidate_sha256: candidate.sha256 }, b.next_action);
319
+ throw b;
320
+ }
321
+ return { workflow_id: id, status: "imported", next_action: "Exact public import verified. Deep Diver and Chronicle require separate author decisions.", receipts: this.receipts(id) };
322
+ });
323
+ }
324
+ }
325
+ const asyncIdentity = (x) => survey.parse(x);
package/dist/records.d.ts CHANGED
@@ -36,7 +36,7 @@ export declare const DOMAINS: readonly ["creative", "ceremonial", "engineering"]
36
36
  /** #815 "claim category": what kind of statement a record makes. */
37
37
  export declare const CLAIM_CATEGORIES: readonly ["observed", "hypothesis", "desired_future", "fiction", "verified_implementation"];
38
38
  export type ClaimCategory = (typeof CLAIM_CATEGORIES)[number];
39
- export declare const RECORD_KINDS: readonly ["observation", "inquiry", "link", "work_proposal", "run_request", "handoff", "source_intent", "review_operation_request", "deep_diver_request", "review_episode_link_request", "calendar_event_draft", "session_completion_proposal", "research_artifact", "inquiry_episode_request", "session_steer_request"];
39
+ export declare const RECORD_KINDS: readonly ["observation", "inquiry", "link", "work_proposal", "run_request", "handoff", "source_intent", "review_operation_request", "deep_diver_request", "review_episode_link_request", "calendar_event_draft", "session_completion_proposal", "research_artifact", "inquiry_episode_request", "session_steer_request", "steering_revocation", "provider_choice", "provider_policy"];
40
40
  export type RecordKind = (typeof RECORD_KINDS)[number];
41
41
  /** #816 handoff states. */
42
42
  export declare const HANDOFF_STATES: readonly ["pending", "blocked", "ready", "in_progress", "completed", "withdrawn"];
package/dist/records.js CHANGED
@@ -61,6 +61,11 @@ export const RECORD_KINDS = [
61
61
  "research_artifact",
62
62
  "inquiry_episode_request",
63
63
  "session_steer_request",
64
+ // A person stops a run's steering, or one steer still waiting at the runtime (jgwill/Miadi#930).
65
+ "steering_revocation",
66
+ // A requester's own per-mission provider choice and provider policy, made on the connector's pages (jgwill/Miadi#926).
67
+ "provider_choice",
68
+ "provider_policy",
64
69
  ];
65
70
  /** #816 handoff states. */
66
71
  export const HANDOFF_STATES = ["pending", "blocked", "ready", "in_progress", "completed", "withdrawn"];
@@ -98,6 +103,7 @@ export function current(records) {
98
103
  approval_state: r.approval_state ?? base.approval_state,
99
104
  next_action: r.next_action ?? base.next_action,
100
105
  completion_receipt: r.completion_receipt ?? base.completion_receipt,
106
+ ...(base.kind === "source_intent" && r.kind === "source_intent" && r.body.source_readiness_transition === true ? { body: { ...base.body, readiness: r.body.readiness } } : {}),
101
107
  history: [...(base.history ?? []), r.id],
102
108
  });
103
109
  continue;
@@ -29,7 +29,12 @@ const OUTCOME_WORDS = {
29
29
  marked_ready: "marked ready for Deep Diver",
30
30
  decided: "recorded",
31
31
  nothing_to_run: "nothing to run",
32
- accepted: "accepted by the session",
32
+ accepted: "typed into an idle session, which turned busy; its delivery says whether the model read it",
33
+ queued_at_runtime: "handed to the session's inbox, on its way to the model",
34
+ delivered_to_agent_context: "delivered: the model has it",
35
+ acknowledged: "delivered, and the agent said so",
36
+ behavior_adjusted: "delivered, and the agent's next action shows it",
37
+ expired: "expired before the model read it",
33
38
  submitted: "typed into the session, acceptance unconfirmed",
34
39
  failed: "failed",
35
40
  };
@@ -154,6 +159,9 @@ function projectCore(record, ctx = {}) {
154
159
  if (ctx.queue?.state === "cancelled" || ctx.queue?.state === "stale") {
155
160
  outstanding.push(`The factory intake set the brief aside (${ctx.queue.state}), so nothing is running for it.`);
156
161
  }
162
+ if (ctx.queue?.state === "refused") {
163
+ outstanding.push(`${ctx.queue.note ?? "The factory runner refused to launch it."} Nothing is running for it.`);
164
+ }
157
165
  const lines = [...decisionLines(decision), outcomeWord ? `Outcome: ${outcomeWord}.` : "Outcome: queued for the factory intake."];
158
166
  if (delivered)
159
167
  lines.push("Delivered to an agent session.");
@@ -0,0 +1,30 @@
1
+ /**
2
+ * What a shelf report is, read from its own Markdown, for a shelf a person can look through: its
3
+ * title, its opening, how long it is, what is inside, how many sources it cites. Everything here is
4
+ * computed from the file; nothing is summarised or invented. jgwill/Miadi#879.
5
+ */
6
+ export interface ReportDigest {
7
+ title: string;
8
+ /** The document names itself; otherwise the title is its folder's name. */
9
+ titled: boolean;
10
+ /** The report's first paragraph of prose, without Markdown marks. */
11
+ excerpt?: string;
12
+ words: number;
13
+ minutes: number;
14
+ /** Its `##` and `###` headings, in order. */
15
+ headings: string[];
16
+ sections: number;
17
+ /** Distinct web addresses it cites. */
18
+ sources: number;
19
+ }
20
+ /** `jacqueline-m-quinless-…-261029-0e1f…` → `jacqueline m quinless …`: the date and uuid a folder name ends in are not its name. */
21
+ export declare function folderTitle(name: string): string;
22
+ export declare function digestReport(markdown: string, folder: string): ReportDigest;
23
+ /**
24
+ * The text as the page shows it. A stray fence would turn the rest of a report into one code box, so
25
+ * a fence with no close in reach is left out of the display. The file itself is never changed.
26
+ */
27
+ export declare function readableMarkdown(markdown: string): {
28
+ markdown: string;
29
+ repaired: boolean;
30
+ };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * What a shelf report is, read from its own Markdown, for a shelf a person can look through: its
3
+ * title, its opening, how long it is, what is inside, how many sources it cites. Everything here is
4
+ * computed from the file; nothing is summarised or invented. jgwill/Miadi#879.
5
+ */
6
+ const WORDS_PER_MINUTE = 230;
7
+ const MAX_HEADINGS = 6;
8
+ const MAX_EXCERPT = 280;
9
+ /** A stray fence must not swallow a report: a block longer than this is not treated as code. */
10
+ const MAX_FENCE_LINES = 60;
11
+ const MAX_TITLE = 100;
12
+ /** `jacqueline-m-quinless-…-261029-0e1f…` → `jacqueline m quinless …`: the date and uuid a folder name ends in are not its name. */
13
+ export function folderTitle(name) {
14
+ const bare = name
15
+ .replace(/^ep\d+[-_]/i, "")
16
+ .replace(/-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "")
17
+ .replace(/-\d{6,10}(-[0-9a-f]{6,})?$/i, "")
18
+ .replace(/[-_]+/g, " ")
19
+ .trim();
20
+ return bare ? bare[0].toUpperCase() + bare.slice(1) : name;
21
+ }
22
+ function plain(text) {
23
+ return text
24
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, "")
25
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
26
+ .replace(/[`*_~]+/g, "")
27
+ .replace(/<[^>]+>/g, "")
28
+ .replace(/\s+/g, " ")
29
+ .trim();
30
+ }
31
+ /** The lines outside fenced code, with a fence left alone when no close comes within reach. */
32
+ function outsideFences(lines) {
33
+ const out = [];
34
+ for (let i = 0; i < lines.length; i++) {
35
+ const open = /^(```|~~~)/.exec(lines[i]);
36
+ if (!open) {
37
+ out.push(lines[i]);
38
+ continue;
39
+ }
40
+ const close = lines.slice(i + 1, i + 1 + MAX_FENCE_LINES).findIndex((l) => l.trimEnd() === open[1]);
41
+ if (close < 0)
42
+ continue;
43
+ i += close + 1;
44
+ }
45
+ return out;
46
+ }
47
+ /** A heading that is a prompt, a command or a chat fragment is not the document's name. */
48
+ const nameable = (t) => t.length > 0 && t.length <= MAX_TITLE && !/^@/.test(t) && !/\s--[a-z]/.test(t) && !/^(hey|hi|ok|okay|so|please|can you|could you)\b/i.test(t);
49
+ /** How a document was made is not what it is about. */
50
+ const PROVENANCE = /^(parent vessel|inquiry vessel|author role|mission|seed for|for the author's review|rendered from|status:|date:|woven to|generated)\b|\/(srv|workspace|a\/src|home)\/|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
51
+ export function digestReport(markdown, folder) {
52
+ const clean = outsideFences(markdown.replace(/^---\n[\s\S]*?\n---\n/, "").split("\n"));
53
+ const body = clean.join("\n");
54
+ const h1 = clean.find((l) => /^#\s+\S/.test(l));
55
+ const h1Text = h1 ? plain(h1.replace(/^#\s+/, "")) : "";
56
+ const headings = clean.filter((l) => /^#{2,3}\s+\S/.test(l)).map((l) => plain(l.replace(/^#+\s+/, "")));
57
+ let excerpt;
58
+ for (const block of body.split(/\n\s*\n/)) {
59
+ const t = block.trim();
60
+ if (!t || /^(#|\||>|[-*+]\s|\d+\.\s|!\[|<|---|===)/.test(t))
61
+ continue;
62
+ const p = plain(t);
63
+ if (p.split(" ").length < 8 || PROVENANCE.test(p))
64
+ continue;
65
+ excerpt = p.length > MAX_EXCERPT ? `${p.slice(0, MAX_EXCERPT).replace(/\s+\S*$/, "")}…` : p;
66
+ break;
67
+ }
68
+ const words = plain(body.replace(/^#+\s+/gm, "")).split(" ").filter(Boolean).length;
69
+ const titled = nameable(h1Text);
70
+ return {
71
+ title: titled ? h1Text : folderTitle(folder),
72
+ titled,
73
+ ...(excerpt ? { excerpt } : {}),
74
+ words,
75
+ minutes: Math.max(1, Math.round(words / WORDS_PER_MINUTE)),
76
+ headings: headings.slice(0, MAX_HEADINGS),
77
+ sections: headings.length,
78
+ sources: new Set(markdown.match(/https?:\/\/[^\s)\]>"']+/g) ?? []).size,
79
+ };
80
+ }
81
+ /**
82
+ * The text as the page shows it. A stray fence would turn the rest of a report into one code box, so
83
+ * a fence with no close in reach is left out of the display. The file itself is never changed.
84
+ */
85
+ export function readableMarkdown(markdown) {
86
+ const lines = markdown.split("\n");
87
+ const out = [];
88
+ let repaired = false;
89
+ for (let i = 0; i < lines.length; i++) {
90
+ const open = /^(```|~~~)/.exec(lines[i]);
91
+ if (!open) {
92
+ out.push(lines[i]);
93
+ continue;
94
+ }
95
+ const close = lines.slice(i + 1, i + 1 + MAX_FENCE_LINES).findIndex((l) => l.trimEnd() === open[1]);
96
+ if (close < 0) {
97
+ repaired = true;
98
+ continue;
99
+ }
100
+ out.push(...lines.slice(i, i + close + 2));
101
+ i += close + 1;
102
+ }
103
+ return { markdown: out.join("\n"), repaired };
104
+ }