@miadi/connector 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/approve.d.ts +62 -9
- package/dist/approve.js +368 -33
- package/dist/config.d.ts +12 -0
- package/dist/config.js +11 -0
- package/dist/decisions.d.ts +4 -2
- package/dist/decisions.js +54 -18
- package/dist/mission-status.js +3 -1
- package/dist/model-preflight.d.ts +9 -2
- package/dist/model-preflight.js +23 -1
- package/dist/provider-policy.d.ts +94 -0
- package/dist/provider-policy.js +150 -0
- package/dist/providers.d.ts +7 -3
- package/dist/providers.js +13 -7
- package/dist/queue-state.js +12 -1
- package/dist/reading/adapters.d.ts +18 -0
- package/dist/reading/adapters.js +90 -0
- package/dist/reading/contract.d.ts +153 -0
- package/dist/reading/contract.js +79 -0
- package/dist/reading/markdown.d.ts +47 -0
- package/dist/reading/markdown.js +52 -0
- package/dist/reading/privacy.d.ts +7 -0
- package/dist/reading/privacy.js +14 -0
- package/dist/reading/workflow.d.ts +93 -0
- package/dist/reading/workflow.js +325 -0
- package/dist/records.d.ts +1 -1
- package/dist/records.js +4 -0
- package/dist/request-state.d.ts +3 -1
- package/dist/request-state.js +8 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +6 -2
- package/dist/store.d.ts +2 -0
- package/dist/store.js +18 -1
- package/dist/tools.js +204 -30
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/providers.js
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* The connector never assumes a provider works because a task text names one:
|
|
5
5
|
* it reports what is installed on this host, what sign-in evidence exists (the
|
|
6
6
|
* presence of the provider's own config directory — never its contents), and
|
|
7
|
-
* applies an explicit preference order. Claude Code
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* applies an explicit preference order. Codex and Claude Code are never selected
|
|
8
|
+
* silently: each requires the request to name it, and Codex also needs the
|
|
9
|
+
* requester's own choice for that mission (provider-policy.ts, jgwill/Miadi#926).
|
|
10
|
+
* When nothing safe is configured the answer is `provider_unavailable` and the
|
|
11
|
+
* mission is left unstarted.
|
|
10
12
|
*
|
|
11
13
|
* No bearer token is read, logged or returned here. Version probes run the
|
|
12
14
|
* provider's own `--version` with a short timeout.
|
|
@@ -15,9 +17,13 @@ import { execFileSync } from "node:child_process";
|
|
|
15
17
|
import { existsSync } from "node:fs";
|
|
16
18
|
import { homedir } from "node:os";
|
|
17
19
|
import { join } from "node:path";
|
|
18
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Preference order when a run request names no provider (jgwill/Miadi#893). Codex is never
|
|
22
|
+
* picked by default: a request names it, and it runs only on the requester's own choice for
|
|
23
|
+
* that mission or their own policy (provider-policy.ts, jgwill/Miadi#926).
|
|
24
|
+
*/
|
|
19
25
|
export const PROVIDERS = [
|
|
20
|
-
{ id: "codex", cli: "codex", name: "Codex CLI", selectable_by_default:
|
|
26
|
+
{ id: "codex", cli: "codex", name: "Codex CLI", selectable_by_default: false, auth_evidence: [".codex"] },
|
|
21
27
|
{ id: "pi", cli: "pi", name: "Pi coding agent", selectable_by_default: true, auth_evidence: [".pi", ".config/pi"] },
|
|
22
28
|
{ id: "copilot", cli: "copilot", name: "GitHub Copilot CLI", selectable_by_default: true, auth_evidence: [".copilot"] },
|
|
23
29
|
{ id: "claude", cli: "claude", name: "Claude Code", selectable_by_default: false, auth_evidence: [".claude"] },
|
|
@@ -59,8 +65,8 @@ export function discoverProviders(probe = {}, specs = PROVIDERS) {
|
|
|
59
65
|
/**
|
|
60
66
|
* Resolve the provider a run will ask the intake for. A requested provider must
|
|
61
67
|
* be known and installed; with no request the first installed provider in the
|
|
62
|
-
* preference order that may be selected by default wins
|
|
63
|
-
* by
|
|
68
|
+
* preference order that may be selected by default wins. Codex and Claude Code
|
|
69
|
+
* only ever by name; whether a named Codex may run is provider-policy.ts's answer.
|
|
64
70
|
*/
|
|
65
71
|
export function resolveProvider(requested, found) {
|
|
66
72
|
const installed = found.filter((p) => p.installed);
|
package/dist/queue-state.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Shared, read-only factory queue evidence for every request-state surface. */
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
export function factoryDir(env = process.env) {
|
|
5
5
|
// The factory's data folder, beside the connector's own /srv/miadi/connector: never a home directory (jgwill/Miadi#911).
|
|
@@ -18,6 +18,17 @@ export function readQueueState(record, dir = factoryDir()) {
|
|
|
18
18
|
return { state: "delivered", file };
|
|
19
19
|
if (existsSync(join(queue, "cancelled", file)))
|
|
20
20
|
return { state: "cancelled", file };
|
|
21
|
+
if (existsSync(join(queue, "refused", file))) {
|
|
22
|
+
// The factory runner would not launch its provider (jgwill/Miadi#926): the reason it wrote beside the brief.
|
|
23
|
+
let why = "";
|
|
24
|
+
try {
|
|
25
|
+
why = readFileSync(join(queue, "refused", `${file}.why`), "utf8").trim().slice(0, 500);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* no reason written */
|
|
29
|
+
}
|
|
30
|
+
return { state: "refused", file, note: `The factory runner refused to launch it${why ? `: ${why}` : "."}` };
|
|
31
|
+
}
|
|
21
32
|
if (existsSync(join(queue, "stale", file)))
|
|
22
33
|
return { state: "stale", file, note: "Its seat did not take it in time; the intake moved it aside." };
|
|
23
34
|
return { state: record.approval_state === "approved" ? "missing" : "not_queued", file };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReadingScope } from "./contract.js";
|
|
2
|
+
import type { ReadingAdapters, ReviewBase } from "./workflow.js";
|
|
3
|
+
export declare function reviewAdapter(baseUrl: string, token: string | undefined, fetchImpl?: typeof fetch): {
|
|
4
|
+
lookup(video: string): Promise<ReviewBase | null>;
|
|
5
|
+
readExact: (id: string, version: number) => Promise<ReviewBase>;
|
|
6
|
+
atomicImport: boolean;
|
|
7
|
+
/** Future v0 contract; enable only from an authenticated live handshake, never a caller flag. */
|
|
8
|
+
discoverAtomicImport(): Promise<boolean>;
|
|
9
|
+
importExact(input: Parameters<ReadingAdapters["importExact"]>[0]): Promise<{
|
|
10
|
+
id: any;
|
|
11
|
+
version: any;
|
|
12
|
+
}>;
|
|
13
|
+
storeTranscript(id: string, text: string): Promise<any>;
|
|
14
|
+
};
|
|
15
|
+
/** Public literature GETs only: allowlisted HTTPS hosts, no credentials, redirects or private IPs. */
|
|
16
|
+
export declare function literatureVerifier(hosts: string[], fetchImpl?: typeof fetch): ReadingAdapters["verifySource"];
|
|
17
|
+
/** Human-readable factory task; its scope is data in the approved record, never shell interpolation. */
|
|
18
|
+
export declare function readingTask(scope: ReadingScope): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** The real service adapter deliberately refuses the legacy unconditional import endpoint. */
|
|
2
|
+
import { ReviewClient, reviewIdOf } from "../reviews.js";
|
|
3
|
+
import { block, canonicalVideo } from "./contract.js";
|
|
4
|
+
export function reviewAdapter(baseUrl, token, fetchImpl = fetch) {
|
|
5
|
+
const client = new ReviewClient(baseUrl, token, fetchImpl);
|
|
6
|
+
const request = async (path, init = {}) => {
|
|
7
|
+
const res = await fetchImpl(`${baseUrl}${path}`, { ...init, redirect: "error", signal: AbortSignal.timeout(30000), headers: { accept: "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}), ...init.headers } });
|
|
8
|
+
if (res.status === 401 || res.status === 403)
|
|
9
|
+
block("blocked_permission", "Refresh the review service credential in its configured environment; do not paste it into chat.");
|
|
10
|
+
if (res.status === 409 || res.status === 412)
|
|
11
|
+
block("blocked_permission", "The review changed after staging. Rebase and obtain approval for a new candidate.");
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
block("failed_retryable", `Review service answered ${res.status}; retry the same operation key after recovery.`);
|
|
14
|
+
return res.json();
|
|
15
|
+
};
|
|
16
|
+
const readExact = async (id, version) => {
|
|
17
|
+
if (reviewIdOf(id) !== id)
|
|
18
|
+
block("blocked_permission", "Review service returned an invalid review id.");
|
|
19
|
+
const body = await client.get(id, version);
|
|
20
|
+
if (!body || body.version?.version !== version)
|
|
21
|
+
block("failed_retryable", "Pinned review version is unavailable; fallback-to-latest is refused.");
|
|
22
|
+
const raw = await fetchImpl(`${baseUrl}/review/${id}/raw?v=${version}`, { redirect: "error", signal: AbortSignal.timeout(30000) });
|
|
23
|
+
if (!raw.ok)
|
|
24
|
+
block("failed_retryable", "Pinned raw Markdown could not be read.");
|
|
25
|
+
const markdown = await raw.text();
|
|
26
|
+
if (markdown !== body.version.markdown)
|
|
27
|
+
block("failed_retryable", "Raw and JSON Markdown differ; inspect the service before importing.");
|
|
28
|
+
return { id, version, markdown, video_id: canonicalVideo(body.video_id) };
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
async lookup(video) {
|
|
32
|
+
if (!token)
|
|
33
|
+
block("blocked_permission", "Configure the review search token on the host before generating a potentially duplicate V1.");
|
|
34
|
+
const response = await request(`/api/reviews/search?${new URLSearchParams({ q: canonicalVideo(video), limit: "100" })}`);
|
|
35
|
+
const rows = response.reviews ?? response.results;
|
|
36
|
+
if (!Array.isArray(rows))
|
|
37
|
+
block("failed_retryable", "Review search returned malformed data; do not infer that no review exists.");
|
|
38
|
+
const matches = rows.filter(r => r.video_id === video);
|
|
39
|
+
if (matches.length > 1)
|
|
40
|
+
block("blocked_permission", "Multiple reviews exist for this video; the author must select the base.");
|
|
41
|
+
if (!matches.length)
|
|
42
|
+
return null;
|
|
43
|
+
if (!matches[0].latest_version)
|
|
44
|
+
block("failed_retryable", "Existing review is pending generation; no duplicate V1 will be generated.");
|
|
45
|
+
return readExact(matches[0].id, matches[0].latest_version);
|
|
46
|
+
},
|
|
47
|
+
readExact,
|
|
48
|
+
atomicImport: false,
|
|
49
|
+
/** Future v0 contract; enable only from an authenticated live handshake, never a caller flag. */
|
|
50
|
+
async discoverAtomicImport() {
|
|
51
|
+
const res = await fetchImpl(`${baseUrl}/api/reviews/capabilities`, { redirect: "error", signal: AbortSignal.timeout(15000), headers: token ? { authorization: `Bearer ${token}` } : {} });
|
|
52
|
+
if (!res.ok)
|
|
53
|
+
return false;
|
|
54
|
+
const c = await res.json();
|
|
55
|
+
return c.conditional_import === "sha256-cas-v1" && c.idempotency === "durable-transactional-v1" && c.publication === "explicit";
|
|
56
|
+
},
|
|
57
|
+
async importExact(input) {
|
|
58
|
+
// Kept on the real /import path, with a negotiated extension. Never send a legacy write.
|
|
59
|
+
if (!await this.discoverAtomicImport())
|
|
60
|
+
block("blocked_permission", "Atomic import is not implemented by the review service. Apply the v0 prompt first.");
|
|
61
|
+
const result = await request("/api/reviews/import", { method: "POST", headers: { "content-type": "application/json", "idempotency-key": input.idempotency_key }, body: JSON.stringify({ url: `https://youtu.be/${input.video_id}`, markdown: input.markdown, review_id: input.review_id, expected_version: input.expected_version, expected_sha256: input.expected_sha256, sha256: input.sha256, public_release: true, contract: "sha256-cas-v1" }) });
|
|
62
|
+
return { id: result.id, version: result.version };
|
|
63
|
+
},
|
|
64
|
+
async storeTranscript(id, text) {
|
|
65
|
+
// No generation-on-empty, trimming, or overwrite. Connector source-part remains canonical.
|
|
66
|
+
if (!text.trim() || reviewIdOf(id) !== id)
|
|
67
|
+
block("blocked_transcript", "An existing review id and nonempty exact transcript are required.");
|
|
68
|
+
return request(`/api/reviews/${id}/transcript`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ transcript: text, overwrite: false }) });
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Public literature GETs only: allowlisted HTTPS hosts, no credentials, redirects or private IPs. */
|
|
73
|
+
export function literatureVerifier(hosts, fetchImpl = fetch) {
|
|
74
|
+
return async (source) => {
|
|
75
|
+
const u = new URL(source.url);
|
|
76
|
+
if (u.protocol !== "https:" || u.username || u.password || u.port || !hosts.includes(u.hostname) || /^[\d.]+$/.test(u.hostname) || u.hostname.includes(":"))
|
|
77
|
+
block("blocked_citations", "Literature source is outside the host's public-source allowlist.");
|
|
78
|
+
const res = await fetchImpl(u, { redirect: "error", signal: AbortSignal.timeout(15000), headers: { accept: "text/plain, text/html, application/json" } });
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
block("blocked_citations", "Public source could not be retrieved; no citation was marked verified.");
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
if (Buffer.byteLength(text) > 2 * 1024 * 1024)
|
|
83
|
+
block("blocked_citations", "Public source exceeds the bounded verification size.");
|
|
84
|
+
return { url: source.url, text: text.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " "), fetched_at: new Date().toISOString() };
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Human-readable factory task; its scope is data in the approved record, never shell interpolation. */
|
|
88
|
+
export function readingTask(scope) {
|
|
89
|
+
return `Run the bounded transcript-first reading worker for THIS approved connector record:\nnode packages/connector/scripts/reading.mjs run <this-run-request-id>\nThe typed reading grant in this record is the only processing authority. Read packages/connector/scripts/READING.md.\nReturn stage receipts and staged V1/V2/V3 with hashes, insertion diff and provenance. Stop at editorial approval. No public import, Deep Diver, Chronicle, paid fallback or media upload beyond the grant.\nKind: ${scope.kind}; language: ${scope.language}; total provider ceiling USD ${scope.max_cost_usd}. Missing capabilities are explicit blockers, never permission to generate from memory.`;
|
|
90
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** Bounded reading contract. Product state lives in the connector ledger; providers stay injected. */
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export declare const hash: (value: string) => string;
|
|
4
|
+
export declare const readingScope: z.ZodObject<{
|
|
5
|
+
source_intent: z.ZodString;
|
|
6
|
+
revision_of: z.ZodOptional<z.ZodString>;
|
|
7
|
+
kind: z.ZodDefault<z.ZodEnum<{
|
|
8
|
+
"research-inquiry": "research-inquiry";
|
|
9
|
+
"ui-screenwalk": "ui-screenwalk";
|
|
10
|
+
}>>;
|
|
11
|
+
language: z.ZodString;
|
|
12
|
+
rights: z.ZodString;
|
|
13
|
+
providers: z.ZodArray<z.ZodString>;
|
|
14
|
+
max_cost_usd: z.ZodDefault<z.ZodNumber>;
|
|
15
|
+
budget_approval_ref: z.ZodOptional<z.ZodString>;
|
|
16
|
+
allow_media_access: z.ZodDefault<z.ZodBoolean>;
|
|
17
|
+
allow_external_processing: z.ZodDefault<z.ZodBoolean>;
|
|
18
|
+
max_field_words: z.ZodDefault<z.ZodNumber>;
|
|
19
|
+
max_fields: z.ZodDefault<z.ZodNumber>;
|
|
20
|
+
}, z.core.$strict>;
|
|
21
|
+
export type ReadingScope = z.infer<typeof readingScope>;
|
|
22
|
+
export declare const sourcePart: z.ZodObject<{
|
|
23
|
+
video_id: z.ZodString;
|
|
24
|
+
kind: z.ZodEnum<{
|
|
25
|
+
captions: "captions";
|
|
26
|
+
verbatim_transcript: "verbatim_transcript";
|
|
27
|
+
}>;
|
|
28
|
+
channel: z.ZodEnum<{
|
|
29
|
+
video: "video";
|
|
30
|
+
commentary: "commentary";
|
|
31
|
+
}>;
|
|
32
|
+
text: z.ZodString;
|
|
33
|
+
normalized_text: z.ZodOptional<z.ZodString>;
|
|
34
|
+
language: z.ZodString;
|
|
35
|
+
locator: z.ZodString;
|
|
36
|
+
provider: z.ZodString;
|
|
37
|
+
caption_kind: z.ZodEnum<{
|
|
38
|
+
manual: "manual";
|
|
39
|
+
automatic: "automatic";
|
|
40
|
+
supplied: "supplied";
|
|
41
|
+
local_stt: "local_stt";
|
|
42
|
+
}>;
|
|
43
|
+
fetched_at: z.ZodString;
|
|
44
|
+
rights: z.ZodString;
|
|
45
|
+
timestamp_granularity: z.ZodEnum<{
|
|
46
|
+
none: "none";
|
|
47
|
+
word: "word";
|
|
48
|
+
segment: "segment";
|
|
49
|
+
turn: "turn";
|
|
50
|
+
}>;
|
|
51
|
+
alignment_status: z.ZodEnum<{
|
|
52
|
+
unknown: "unknown";
|
|
53
|
+
verified: "verified";
|
|
54
|
+
}>;
|
|
55
|
+
speaker_status: z.ZodEnum<{
|
|
56
|
+
unknown: "unknown";
|
|
57
|
+
verified: "verified";
|
|
58
|
+
}>;
|
|
59
|
+
segments: z.ZodArray<z.ZodObject<{
|
|
60
|
+
start_ms: z.ZodNumber;
|
|
61
|
+
end_ms: z.ZodNumber;
|
|
62
|
+
text: z.ZodString;
|
|
63
|
+
speaker: z.ZodOptional<z.ZodString>;
|
|
64
|
+
}, z.core.$strict>>;
|
|
65
|
+
}, z.core.$strict>;
|
|
66
|
+
export type SourcePart = z.infer<typeof sourcePart>;
|
|
67
|
+
export interface Provider {
|
|
68
|
+
id: string;
|
|
69
|
+
available: boolean;
|
|
70
|
+
external: boolean;
|
|
71
|
+
media: boolean;
|
|
72
|
+
max_cost_usd: number;
|
|
73
|
+
method: string;
|
|
74
|
+
}
|
|
75
|
+
export declare const evidence: z.ZodObject<{
|
|
76
|
+
category: z.ZodEnum<{
|
|
77
|
+
transcript_supported: "transcript_supported";
|
|
78
|
+
visually_evidenced: "visually_evidenced";
|
|
79
|
+
inference: "inference";
|
|
80
|
+
open_question: "open_question";
|
|
81
|
+
}>;
|
|
82
|
+
claim: z.ZodString;
|
|
83
|
+
at_ms: z.ZodOptional<z.ZodNumber>;
|
|
84
|
+
visual_receipt: z.ZodOptional<z.ZodString>;
|
|
85
|
+
}, z.core.$strict>;
|
|
86
|
+
export declare const baseReview: z.ZodObject<{
|
|
87
|
+
markdown: z.ZodString;
|
|
88
|
+
claims: z.ZodArray<z.ZodObject<{
|
|
89
|
+
category: z.ZodEnum<{
|
|
90
|
+
transcript_supported: "transcript_supported";
|
|
91
|
+
visually_evidenced: "visually_evidenced";
|
|
92
|
+
inference: "inference";
|
|
93
|
+
open_question: "open_question";
|
|
94
|
+
}>;
|
|
95
|
+
claim: z.ZodString;
|
|
96
|
+
at_ms: z.ZodOptional<z.ZodNumber>;
|
|
97
|
+
visual_receipt: z.ZodOptional<z.ZodString>;
|
|
98
|
+
}, z.core.$strict>>;
|
|
99
|
+
model: z.ZodString;
|
|
100
|
+
}, z.core.$strict>;
|
|
101
|
+
export declare const survey: z.ZodObject<{
|
|
102
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
103
|
+
name: z.ZodString;
|
|
104
|
+
body: z.ZodString;
|
|
105
|
+
at_ms: z.ZodNumber;
|
|
106
|
+
question: z.ZodString;
|
|
107
|
+
limitation: z.ZodString;
|
|
108
|
+
source_ids: z.ZodArray<z.ZodString>;
|
|
109
|
+
}, z.core.$strict>>;
|
|
110
|
+
sources: z.ZodArray<z.ZodObject<{
|
|
111
|
+
id: z.ZodString;
|
|
112
|
+
title: z.ZodString;
|
|
113
|
+
url: z.ZodString;
|
|
114
|
+
excerpt: z.ZodString;
|
|
115
|
+
claim: z.ZodString;
|
|
116
|
+
}, z.core.$strict>>;
|
|
117
|
+
model: z.ZodString;
|
|
118
|
+
}, z.core.$strict>;
|
|
119
|
+
export declare const reflection: z.ZodObject<{
|
|
120
|
+
dialogue: z.ZodArray<z.ZodObject<{
|
|
121
|
+
voice: z.ZodEnum<{
|
|
122
|
+
Ava: "Ava";
|
|
123
|
+
Mia: "Mia";
|
|
124
|
+
}>;
|
|
125
|
+
text: z.ZodString;
|
|
126
|
+
}, z.core.$strict>>;
|
|
127
|
+
synthesis: z.ZodString;
|
|
128
|
+
methods: z.ZodArray<z.ZodObject<{
|
|
129
|
+
question: z.ZodString;
|
|
130
|
+
method: z.ZodString;
|
|
131
|
+
evidence: z.ZodString;
|
|
132
|
+
contradiction: z.ZodString;
|
|
133
|
+
}, z.core.$strict>>;
|
|
134
|
+
hypothesis: z.ZodString;
|
|
135
|
+
model: z.ZodString;
|
|
136
|
+
}, z.core.$strict>;
|
|
137
|
+
export declare const suppliedDialogue: z.ZodObject<{
|
|
138
|
+
text: z.ZodString;
|
|
139
|
+
author: z.ZodString;
|
|
140
|
+
tool: z.ZodString;
|
|
141
|
+
supplied_at: z.ZodString;
|
|
142
|
+
}, z.core.$strict>;
|
|
143
|
+
export type BaseReview = z.infer<typeof baseReview>;
|
|
144
|
+
export type Survey = z.infer<typeof survey>;
|
|
145
|
+
export type Reflection = z.infer<typeof reflection>;
|
|
146
|
+
export declare class ReadingBlocked extends Error {
|
|
147
|
+
readonly status: string;
|
|
148
|
+
readonly next_action: string;
|
|
149
|
+
constructor(status: string, next_action: string);
|
|
150
|
+
}
|
|
151
|
+
export declare function block(status: string, next: string): never;
|
|
152
|
+
/** Recognise only real YouTube hosts and exact ids; never use URL-derived text as code. */
|
|
153
|
+
export declare function canonicalVideo(input: string): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Bounded reading contract. Product state lives in the connector ledger; providers stay injected. */
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
export const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
5
|
+
export const readingScope = z.object({
|
|
6
|
+
source_intent: z.string().regex(/^source_intent:[\da-f-]{36}$/i),
|
|
7
|
+
revision_of: z.string().regex(/^reading:[a-f\d]{32}$/).optional(),
|
|
8
|
+
kind: z.enum(["research-inquiry", "ui-screenwalk"]).default("research-inquiry"),
|
|
9
|
+
language: z.string().min(2).max(40),
|
|
10
|
+
rights: z.string().min(1).max(1000),
|
|
11
|
+
providers: z.array(z.string().min(1).max(100)).min(1).max(20),
|
|
12
|
+
max_cost_usd: z.number().finite().min(0).max(100).default(0),
|
|
13
|
+
budget_approval_ref: z.string().min(1).optional(),
|
|
14
|
+
allow_media_access: z.boolean().default(false),
|
|
15
|
+
allow_external_processing: z.boolean().default(false),
|
|
16
|
+
max_field_words: z.number().int().min(20).max(500).default(55),
|
|
17
|
+
max_fields: z.number().int().min(1).max(12).default(6),
|
|
18
|
+
}).strict();
|
|
19
|
+
export const sourcePart = z.object({
|
|
20
|
+
video_id: z.string().regex(/^[\w-]{11}$/),
|
|
21
|
+
kind: z.enum(["captions", "verbatim_transcript"]),
|
|
22
|
+
channel: z.enum(["video", "commentary"]),
|
|
23
|
+
text: z.string().min(1).max(800000),
|
|
24
|
+
normalized_text: z.string().max(800000).optional(),
|
|
25
|
+
language: z.string().min(2),
|
|
26
|
+
locator: z.string().min(1),
|
|
27
|
+
provider: z.string().min(1),
|
|
28
|
+
caption_kind: z.enum(["manual", "automatic", "supplied", "local_stt"]),
|
|
29
|
+
fetched_at: z.string().datetime(),
|
|
30
|
+
rights: z.string().min(1),
|
|
31
|
+
timestamp_granularity: z.enum(["word", "segment", "turn", "none"]),
|
|
32
|
+
alignment_status: z.enum(["unknown", "verified"]),
|
|
33
|
+
speaker_status: z.enum(["unknown", "verified"]),
|
|
34
|
+
segments: z.array(z.object({ start_ms: z.number().int().min(0), end_ms: z.number().int().min(0), text: z.string(), speaker: z.string().optional() }).strict()).max(50000),
|
|
35
|
+
}).strict();
|
|
36
|
+
export const evidence = z.object({
|
|
37
|
+
category: z.enum(["transcript_supported", "visually_evidenced", "inference", "open_question"]),
|
|
38
|
+
claim: z.string().min(1),
|
|
39
|
+
at_ms: z.number().int().min(0).optional(),
|
|
40
|
+
visual_receipt: z.string().min(1).optional(),
|
|
41
|
+
}).strict();
|
|
42
|
+
export const baseReview = z.object({ markdown: z.string().min(1).max(400000), claims: z.array(evidence).min(1), model: z.string().min(1) }).strict();
|
|
43
|
+
export const survey = z.object({
|
|
44
|
+
fields: z.array(z.object({ name: z.string().min(1), body: z.string().min(1), at_ms: z.number().int().min(0), question: z.string().min(1), limitation: z.string().min(1), source_ids: z.array(z.string()).min(1) }).strict()).min(1),
|
|
45
|
+
sources: z.array(z.object({ id: z.string().regex(/^[A-Za-z0-9_-]{1,40}$/), title: z.string().trim().min(1), url: z.string().url(), excerpt: z.string().min(20).max(1000).refine(s => s.trim().length >= 20, "A verifiable excerpt needs meaningful text"), claim: z.string().trim().min(1) }).strict()).min(1),
|
|
46
|
+
model: z.string().min(1),
|
|
47
|
+
}).strict();
|
|
48
|
+
export const reflection = z.object({
|
|
49
|
+
dialogue: z.array(z.object({ voice: z.enum(["Ava", "Mia"]), text: z.string().min(1) }).strict()).min(2),
|
|
50
|
+
synthesis: z.string().min(40),
|
|
51
|
+
methods: z.array(z.object({ question: z.string().min(1), method: z.string().min(1), evidence: z.string().min(1), contradiction: z.string().min(1) }).strict()).min(1),
|
|
52
|
+
hypothesis: z.string().min(1), model: z.string().min(1),
|
|
53
|
+
}).strict();
|
|
54
|
+
export const suppliedDialogue = z.object({ text: z.string().min(1).max(200000), author: z.string().min(1), tool: z.string().min(1), supplied_at: z.string().datetime() }).strict();
|
|
55
|
+
export class ReadingBlocked extends Error {
|
|
56
|
+
status;
|
|
57
|
+
next_action;
|
|
58
|
+
constructor(status, next_action) {
|
|
59
|
+
super(status);
|
|
60
|
+
this.status = status;
|
|
61
|
+
this.next_action = next_action;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function block(status, next) { throw new ReadingBlocked(status, next); }
|
|
65
|
+
/** Recognise only real YouTube hosts and exact ids; never use URL-derived text as code. */
|
|
66
|
+
export function canonicalVideo(input) {
|
|
67
|
+
if (/^[\w-]{11}$/.test(input))
|
|
68
|
+
return input;
|
|
69
|
+
try {
|
|
70
|
+
const u = new URL(input);
|
|
71
|
+
if (u.protocol !== "https:")
|
|
72
|
+
throw new Error();
|
|
73
|
+
const id = u.hostname === "youtu.be" ? u.pathname.slice(1) : ["youtube.com", "www.youtube.com", "m.youtube.com"].includes(u.hostname) ? u.searchParams.get("v") ?? u.pathname.match(/^\/(?:shorts|embed)\/([\w-]+)$/)?.[1] : undefined;
|
|
74
|
+
if (id && /^[\w-]{11}$/.test(id))
|
|
75
|
+
return id;
|
|
76
|
+
}
|
|
77
|
+
catch { /* fail closed */ }
|
|
78
|
+
return block("blocked_permission", "Supply an exact YouTube video id or supported HTTPS URL.");
|
|
79
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type Reflection, type Survey } from "./contract.js";
|
|
2
|
+
/** UTF-16 string offsets outside code fences, retaining CRLF. No Markdown reserialization. */
|
|
3
|
+
export declare function headings(markdown: string): {
|
|
4
|
+
name: string;
|
|
5
|
+
at: number;
|
|
6
|
+
}[];
|
|
7
|
+
export declare function insert(base: string, at: number, addition: string): {
|
|
8
|
+
markdown: string;
|
|
9
|
+
diff: {
|
|
10
|
+
operation: string;
|
|
11
|
+
at: number;
|
|
12
|
+
addition: string;
|
|
13
|
+
base_sha256: string;
|
|
14
|
+
sha256: string;
|
|
15
|
+
bytes: number;
|
|
16
|
+
retained: boolean;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export declare function academic(base: string, s: Survey): {
|
|
20
|
+
markdown: string;
|
|
21
|
+
diffs: {
|
|
22
|
+
operation: string;
|
|
23
|
+
at: number;
|
|
24
|
+
addition: string;
|
|
25
|
+
base_sha256: string;
|
|
26
|
+
sha256: string;
|
|
27
|
+
bytes: number;
|
|
28
|
+
retained: boolean;
|
|
29
|
+
}[];
|
|
30
|
+
};
|
|
31
|
+
export declare function reflective(base: string, r: Reflection, companion?: {
|
|
32
|
+
text: string;
|
|
33
|
+
author: string;
|
|
34
|
+
tool: string;
|
|
35
|
+
supplied_at: string;
|
|
36
|
+
}): {
|
|
37
|
+
markdown: string;
|
|
38
|
+
diff: {
|
|
39
|
+
operation: string;
|
|
40
|
+
at: number;
|
|
41
|
+
addition: string;
|
|
42
|
+
base_sha256: string;
|
|
43
|
+
sha256: string;
|
|
44
|
+
bytes: number;
|
|
45
|
+
retained: boolean;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { block, hash } from "./contract.js";
|
|
2
|
+
/** UTF-16 string offsets outside code fences, retaining CRLF. No Markdown reserialization. */
|
|
3
|
+
export function headings(markdown) {
|
|
4
|
+
let offset = 0, fence = null, fenceLength = 0;
|
|
5
|
+
const out = [];
|
|
6
|
+
for (const line of markdown.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
|
|
7
|
+
const f = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
8
|
+
if (f) {
|
|
9
|
+
if (!fence) {
|
|
10
|
+
fence = f[1][0];
|
|
11
|
+
fenceLength = f[1].length;
|
|
12
|
+
}
|
|
13
|
+
else if (fence === f[1][0] && f[1].length >= fenceLength)
|
|
14
|
+
fence = null;
|
|
15
|
+
}
|
|
16
|
+
else if (!fence) {
|
|
17
|
+
const h = line.match(/^## ([^\r\n]+?)\s*#*\s*$/);
|
|
18
|
+
if (h)
|
|
19
|
+
out.push({ name: h[1].trim(), at: offset });
|
|
20
|
+
}
|
|
21
|
+
offset += line.length;
|
|
22
|
+
}
|
|
23
|
+
if (fence)
|
|
24
|
+
block("blocked_citations", "The base contains an unclosed Markdown fence. Ask the author to approve its repair before enrichment.");
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
export function insert(base, at, addition) {
|
|
28
|
+
const markdown = base.slice(0, at) + addition + base.slice(at);
|
|
29
|
+
return { markdown, diff: { operation: "insert", at, addition, base_sha256: hash(base), sha256: hash(markdown), bytes: Buffer.byteLength(markdown), retained: true } };
|
|
30
|
+
}
|
|
31
|
+
export function academic(base, s) {
|
|
32
|
+
const hs = headings(base), summary = hs.findIndex(h => h.name.toLowerCase() === "summary");
|
|
33
|
+
if (summary < 0 || hs.filter(h => h.name.toLowerCase() === "summary").length !== 1 || hs.some(h => /^(academic fields|academic field survey)$/i.test(h.name)))
|
|
34
|
+
block("blocked_citations", "A unique Summary and no existing Academic Fields are required; resolve the base with the author.");
|
|
35
|
+
const at = hs[summary + 1]?.at ?? base.length;
|
|
36
|
+
const fields = "\n\n## Academic Fields\n\n" + s.fields.map(f => `### ${f.name}\n\n${f.body}\n\nSource anchor: ${f.at_ms} ms (transcript). External literature: ${f.source_ids.map(id => `[${id}]`).join(", ")}.\n\nOpen question: ${f.question}\n\nLimit: ${f.limitation}\n`).join("\n") + "\n";
|
|
37
|
+
const first = insert(base, at, fields);
|
|
38
|
+
const sources = "\n\n## Reading Sources\n\n" + s.sources.map(x => `- [${x.id}] ${x.title}. ${x.url}\n Retrieved excerpt checked; relevance to the inquiry is an agent interpretation. This receipt does not claim a full reading.\n`).join("\n");
|
|
39
|
+
const second = insert(first.markdown, first.markdown.length, sources);
|
|
40
|
+
return { markdown: second.markdown, diffs: [first.diff, second.diff] };
|
|
41
|
+
}
|
|
42
|
+
export function reflective(base, r, companion) {
|
|
43
|
+
let added = `\n\n## Reflective Methodology — AI-generated Ava–Mia Dialogue\n\nGenerated by ${r.model}; fictional agent voices reflecting on this survey. These words were not spoken in the recording or supplied by the author.\n\n`;
|
|
44
|
+
added += r.dialogue.map(d => `**${d.voice} (AI-generated):** ${d.text}\n`).join("\n");
|
|
45
|
+
added += `\n### Cross-field synthesis (interpretation)\n\n${r.synthesis}\n\n### Proposed methods and possible evidence\n\n`;
|
|
46
|
+
added += r.methods.map(m => `- Question: ${m.question}\n Method (proposed): ${m.method}\n Possible evidence: ${m.evidence}\n Contradiction to test: ${m.contradiction}\n`).join("\n");
|
|
47
|
+
added += `\nHypothesis (untested): ${r.hypothesis}\n\n`;
|
|
48
|
+
if (companion)
|
|
49
|
+
added += `## Author-supplied Companion Dialogue\n\nSeparate contribution; supplied by ${companion.author}, originating tool ${companion.tool}, ${companion.supplied_at}. Alignment with video unknown. Exact supplied text follows.\n\n${companion.text}\n\n`;
|
|
50
|
+
const at = headings(base).find(h => h.name === "Reading Sources")?.at ?? base.length;
|
|
51
|
+
return insert(base, at, added);
|
|
52
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ConnectorRecord } from "../records.js";
|
|
2
|
+
/** Processing approval never grants other readers access to drafts or completion text. */
|
|
3
|
+
export declare function readingOwner(record: ConnectorRecord, records: ConnectorRecord[]): string | undefined;
|
|
4
|
+
export declare function canReadReading(record: ConnectorRecord, records: ConnectorRecord[], viewer: {
|
|
5
|
+
id: string;
|
|
6
|
+
api_tier?: string | null;
|
|
7
|
+
}): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Processing approval never grants other readers access to drafts or completion text. */
|
|
2
|
+
export function readingOwner(record, records) {
|
|
3
|
+
if (record.body.reading || record.body.reading_import)
|
|
4
|
+
return record.actor.person_id;
|
|
5
|
+
if (record.kind === "session_completion_proposal") {
|
|
6
|
+
const parent = records.find(r => r.kind === "run_request" && (r.body.reading || r.body.reading_import) && (r.id === record.body.mission || record.refs.includes(r.id)));
|
|
7
|
+
return parent?.actor.person_id;
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
export function canReadReading(record, records, viewer) {
|
|
12
|
+
const owner = readingOwner(record, records);
|
|
13
|
+
return !owner || viewer.api_tier === "write" || owner === viewer.id;
|
|
14
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type Actor, type ConnectorRecord } from "../records.js";
|
|
2
|
+
import { Ledger } from "../store.js";
|
|
3
|
+
import { type Provider, type ReadingScope, type SourcePart, type BaseReview, type Survey, type Reflection } from "./contract.js";
|
|
4
|
+
export interface ReviewBase {
|
|
5
|
+
id: string;
|
|
6
|
+
version: number;
|
|
7
|
+
markdown: string;
|
|
8
|
+
video_id: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StageContext {
|
|
11
|
+
workflow_id: string;
|
|
12
|
+
video_id: string;
|
|
13
|
+
scope: ReadingScope;
|
|
14
|
+
idempotency_key: string;
|
|
15
|
+
transcript: SourcePart;
|
|
16
|
+
source_part_id: string;
|
|
17
|
+
base?: BaseReview;
|
|
18
|
+
survey?: Survey;
|
|
19
|
+
}
|
|
20
|
+
export interface ReadingAdapters {
|
|
21
|
+
/** Provider execution belongs to the runtime/shim. Every operation must replay a key without charging twice. */
|
|
22
|
+
providers: Record<"transcript" | "v1" | "academic" | "reflective", Provider & {
|
|
23
|
+
idempotent: true;
|
|
24
|
+
}>;
|
|
25
|
+
lookup(video: string): Promise<ReviewBase | null>;
|
|
26
|
+
acquire(context: Omit<StageContext, "transcript" | "source_part_id">): Promise<SourcePart | null>;
|
|
27
|
+
generateV1(context: StageContext): Promise<BaseReview>;
|
|
28
|
+
generateSurvey(context: StageContext): Promise<Survey>;
|
|
29
|
+
generateReflection(context: StageContext): Promise<Reflection>;
|
|
30
|
+
/** Read primary sources through a host-controlled allowlist; no source-directed credential or media forwarding. */
|
|
31
|
+
verifySource(source: Survey["sources"][number]): Promise<{
|
|
32
|
+
url: string;
|
|
33
|
+
text: string;
|
|
34
|
+
fetched_at: string;
|
|
35
|
+
}>;
|
|
36
|
+
/** True only after a real capability handshake for atomic compare-and-swap + durable idempotency. */
|
|
37
|
+
atomicImport: boolean;
|
|
38
|
+
importExact(input: {
|
|
39
|
+
video_id: string;
|
|
40
|
+
review_id: string | null;
|
|
41
|
+
expected_version: number;
|
|
42
|
+
expected_sha256: string;
|
|
43
|
+
markdown: string;
|
|
44
|
+
sha256: string;
|
|
45
|
+
idempotency_key: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
id: string;
|
|
48
|
+
version: number;
|
|
49
|
+
}>;
|
|
50
|
+
readExact(id: string, version: number): Promise<ReviewBase>;
|
|
51
|
+
}
|
|
52
|
+
type Receipt = {
|
|
53
|
+
stage: string;
|
|
54
|
+
status: string;
|
|
55
|
+
attempt: number;
|
|
56
|
+
started_at: string;
|
|
57
|
+
completed_at: string;
|
|
58
|
+
workflow_id: string;
|
|
59
|
+
request_id: string;
|
|
60
|
+
video_id: string;
|
|
61
|
+
actor: Actor;
|
|
62
|
+
consent_scope: ReadingScope;
|
|
63
|
+
approval_state: string;
|
|
64
|
+
artifact_refs: string[];
|
|
65
|
+
sha256: string;
|
|
66
|
+
previous_sha256: string | null;
|
|
67
|
+
artifact: any;
|
|
68
|
+
next_action: string;
|
|
69
|
+
};
|
|
70
|
+
export type ReadingResult = {
|
|
71
|
+
workflow_id: string;
|
|
72
|
+
status: string;
|
|
73
|
+
next_action: string;
|
|
74
|
+
receipts: Receipt[];
|
|
75
|
+
};
|
|
76
|
+
/** One workflow per person and video; two concurrent releases cannot spend twice. */
|
|
77
|
+
export declare function workflowId(actor: string, video: string, revisionOf?: string): string;
|
|
78
|
+
export declare class ReadingWorkflow {
|
|
79
|
+
readonly stateDir: string;
|
|
80
|
+
readonly adapters: ReadingAdapters;
|
|
81
|
+
readonly ledger: Ledger<ConnectorRecord>;
|
|
82
|
+
constructor(stateDir: string, adapters: ReadingAdapters);
|
|
83
|
+
receipts(id: string): Receipt[];
|
|
84
|
+
private approved;
|
|
85
|
+
private source;
|
|
86
|
+
private locked;
|
|
87
|
+
private record;
|
|
88
|
+
private done;
|
|
89
|
+
private provider;
|
|
90
|
+
run(requestId: string, companion?: unknown): Promise<ReadingResult>;
|
|
91
|
+
importApproved(requestId: string): Promise<ReadingResult>;
|
|
92
|
+
}
|
|
93
|
+
export {};
|