@miadi/connector 0.1.12 → 0.1.14

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.
@@ -1,27 +1,70 @@
1
1
  import { steerAttempts, steeringGrantOf, checkGrant } from "./steer.js";
2
2
  const ISSUE_REF = /^([\w.-]+)\/([\w.-]+)#(\d+)$/;
3
3
  const SESSION_REF = /^session:([0-9a-f-]{36})$/;
4
- /** The record a mission reference resolves to, and how it was found. */
4
+ const MISSION_KINDS = new Set(["run_request", "work_proposal"]);
5
+ const CLOSING_ENDINGS = new Set(["exit", "complete_then_exit"]);
6
+ export const RELEASE_FRESHNESS_MS = 7 * 24 * 3600_000;
7
+ const CLOCK_SKEW_MS = 5 * 60_000;
8
+ /** A release receipt whose target the connector can check itself. */
9
+ const SELF_TARGET = /miadi-connector/;
10
+ /** The issue a mission implements, and how that is known. */
11
+ export function primaryIssueOf(record) {
12
+ const declared = record.body?.primary_issue;
13
+ if (typeof declared === "string" && ISSUE_REF.test(declared))
14
+ return { ref: declared, provenance: "declared" };
15
+ const first = record.refs.find((r) => ISSUE_REF.test(r));
16
+ return first ? { ref: first, provenance: "first_issue_ref" } : undefined;
17
+ }
18
+ const newestFirst = (a, b) => b.created_at.localeCompare(a.created_at);
19
+ function candidate(r, relation, provenance) {
20
+ const primary = primaryIssueOf(r);
21
+ return { id: r.id, kind: r.kind, created_at: r.created_at, relation, provenance, ...(primary ? { primary_issue: primary.ref } : {}) };
22
+ }
23
+ /** Whether a resolved reference may be acted on: an exact id, or a declared primary nobody else holds. */
24
+ export function authorizesActions(found) {
25
+ return found.resolution === "exact" || (found.resolution === "declared_primary" && !found.contested);
26
+ }
27
+ /**
28
+ * The record a mission reference resolves to, how it was found, and every other
29
+ * candidate. `record` is absent when the reference is ambiguous or only related.
30
+ * Expects the folded ledger (`current()`), so a superseding record never stands
31
+ * as a mission of its own.
32
+ */
5
33
  export function resolveMissionSubject(ref, records) {
6
34
  const exact = records.find((r) => r.id === ref);
7
35
  if (exact)
8
- return { record: exact, via: "record_id" };
9
- const session = SESSION_REF.exec(ref);
10
- if (session) {
11
- const citing = records
12
- .filter((r) => (r.kind === "run_request" || r.kind === "work_proposal") && r.refs.includes(ref))
13
- .sort((a, b) => b.created_at.localeCompare(a.created_at));
14
- if (citing.length)
15
- return { record: citing[0], via: "session_ref" };
16
- return null;
36
+ return { record: exact, via: "record_id", resolution: "exact", candidates: [] };
37
+ // A request retired as an earlier version of another (jgwill/Miadi#919) is not a mission of its own.
38
+ const missions = records.filter((r) => MISSION_KINDS.has(r.kind) && r.completion_receipt?.decision !== "superseded").sort(newestFirst);
39
+ if (SESSION_REF.test(ref)) {
40
+ const citing = missions.filter((r) => r.refs.includes(ref));
41
+ if (!citing.length)
42
+ return null;
43
+ if (citing.length === 1)
44
+ return { record: citing[0], via: "session_ref", resolution: "inferred_primary", candidates: [candidate(citing[0], "cited", "cited_ref")] };
45
+ return { via: "session_ref", resolution: "ambiguous_mission", candidates: citing.map((r) => candidate(r, "cited", "cited_ref")) };
17
46
  }
18
47
  if (ISSUE_REF.test(ref)) {
19
- const citing = records
20
- .filter((r) => (r.kind === "run_request" || r.kind === "work_proposal") && (r.refs.includes(ref) || r.evidence_refs.includes(ref)))
21
- .sort((a, b) => b.created_at.localeCompare(a.created_at));
22
- if (citing.length)
23
- return { record: citing[0], via: "issue_ref" };
24
- return null;
48
+ const citing = missions.filter((r) => r.refs.includes(ref) || r.evidence_refs.includes(ref));
49
+ if (!citing.length)
50
+ return null;
51
+ const cands = citing.map((r) => {
52
+ const primary = primaryIssueOf(r);
53
+ return primary?.ref === ref ? candidate(r, "primary", primary.provenance) : candidate(r, "related", "cited_ref");
54
+ });
55
+ const declared = cands.filter((c) => c.relation === "primary" && c.provenance === "declared");
56
+ const inferred = cands.filter((c) => c.relation === "primary" && c.provenance === "first_issue_ref");
57
+ const pick = (c) => citing.find((r) => r.id === c.id);
58
+ if (declared.length === 1) {
59
+ return { record: pick(declared[0]), via: "issue_ref", resolution: "declared_primary", ...(inferred.length ? { contested: true } : {}), candidates: cands };
60
+ }
61
+ if (declared.length > 1)
62
+ return { via: "issue_ref", resolution: "ambiguous_mission", candidates: cands };
63
+ if (inferred.length === 1)
64
+ return { record: pick(inferred[0]), via: "issue_ref", resolution: "inferred_primary", candidates: cands };
65
+ if (inferred.length > 1)
66
+ return { via: "issue_ref", resolution: "ambiguous_mission", candidates: cands };
67
+ return { via: "issue_ref", resolution: "related_only", candidates: cands };
25
68
  }
26
69
  return null;
27
70
  }
@@ -48,12 +91,108 @@ function nextAction(record, standing, queue) {
48
91
  : "The request is done. A mission closes when its session's ending is decided, not when this request completes.";
49
92
  }
50
93
  }
94
+ const str = (v) => (typeof v === "string" ? v.trim() : "");
95
+ /** name@version from "npm:@scope/name@1.2.3" or "@scope/name@1.2.3". */
96
+ function artifactVersion(artifact) {
97
+ const m = /^(?:npm:)?((?:@[\w.-]+\/)?[\w.-]+)@(\d[\w.+-]*)$/.exec(artifact.trim());
98
+ return m ? { pkg: m[1], version: m[2] } : undefined;
99
+ }
100
+ /** Records that carry evidence about this mission: itself, then any record citing its id, newest first. */
101
+ function evidenceRecords(record, records) {
102
+ const citing = records.filter((r) => r.id !== record.id && (r.refs.includes(record.id) || r.evidence_refs.includes(record.id))).sort(newestFirst);
103
+ return [record, ...citing];
104
+ }
105
+ function releaseTruth(record, records, deps, now) {
106
+ let claimed = false;
107
+ for (const r of evidenceRecords(record, records)) {
108
+ const receipt = (r.completion_receipt ?? {});
109
+ const rel = (receipt.release ?? r.body?.release);
110
+ if (rel && typeof rel === "object") {
111
+ const ev = { artifact: str(rel.artifact), target: str(rel.target), verified_at: str(rel.verified_at), verification: str(rel.verification) };
112
+ const at = Date.parse(ev.verified_at);
113
+ if (!ev.artifact || !ev.target || !ev.verification || Number.isNaN(at) || at > now.getTime() + CLOCK_SKEW_MS) {
114
+ claimed = true;
115
+ continue;
116
+ }
117
+ const approver = str((record.completion_receipt ?? {}).decided_by?.person_id);
118
+ const evidence = { ...ev, record: r.id, by: r.actor.name, by_approver: Boolean(approver) && r.actor.person_id === approver };
119
+ const av = artifactVersion(ev.artifact);
120
+ const self = av && SELF_TARGET.test(ev.target) ? deps.liveVersionOf?.(av.pkg) : undefined;
121
+ if (av && self !== undefined) {
122
+ evidence.live_version = self;
123
+ if (self !== av.version)
124
+ return { released: "live_mismatch", release_evidence: evidence };
125
+ }
126
+ if (now.getTime() - at > (deps.releaseFreshnessMs ?? RELEASE_FRESHNESS_MS))
127
+ return { released: "stale", release_evidence: evidence };
128
+ return { released: self !== undefined ? "verified" : "receipt_recorded", release_evidence: evidence };
129
+ }
130
+ if (r.id === record.id && (receipt.published || receipt.version))
131
+ claimed = true;
132
+ }
133
+ return { released: claimed ? "claimed_unverified" : "no_evidence" };
134
+ }
135
+ /**
136
+ * The approved ending that closes this mission: it names the mission in its own `mission` field (one ending,
137
+ * one mission; a citation in refs is not enough) and, when the mission names sessions, ends one of them.
138
+ */
139
+ function closureTruth(record, records, missionSessions) {
140
+ const endings = records
141
+ .filter((r) => {
142
+ const b = (r.body ?? {});
143
+ if (r.kind !== "session_completion_proposal" || r.approval_state !== "approved" || b.mission !== record.id)
144
+ return false;
145
+ if (!CLOSING_ENDINGS.has(str(b.ending)))
146
+ return false;
147
+ return missionSessions.size === 0 || missionSessions.has(str(b.session_id));
148
+ })
149
+ .sort(newestFirst);
150
+ const e = endings[0];
151
+ if (!e)
152
+ return undefined;
153
+ const receipt = (e.completion_receipt ?? {});
154
+ const sessionId = str(e.body.session_id);
155
+ return {
156
+ record: e.id,
157
+ ending: str(e.body.ending),
158
+ session_id: sessionId,
159
+ session_bound: missionSessions.has(sessionId),
160
+ ...(str(receipt.decided_by?.name) ? { decided_by: str(receipt.decided_by.name) } : {}),
161
+ ...(str(receipt.decided_at) ? { decided_at: str(receipt.decided_at) } : {}),
162
+ };
163
+ }
51
164
  export async function missionStatus(ref, records, deps) {
52
165
  const notes = [];
53
166
  const found = resolveMissionSubject(ref, records);
54
167
  if (!found)
55
168
  return { ref, found: false, notes: ["No mission record resolves from this reference."] };
169
+ if (!found.record) {
170
+ const ambiguous = found.resolution === "ambiguous_mission";
171
+ return {
172
+ ref,
173
+ found: false,
174
+ via: found.via,
175
+ identity: { resolution: found.resolution, authorizes_actions: false, candidates: found.candidates },
176
+ next_authorized_action: "Name the mission by its record id. Nothing is steered, released or closed against this reference.",
177
+ notes: [
178
+ ambiguous
179
+ ? "Several requests hold this reference as their primary mission; none is chosen for you, the newest included."
180
+ : "Requests cite this reference only as related; none implements it, so it names no mission.",
181
+ ],
182
+ };
183
+ }
56
184
  const { record, via } = found;
185
+ const primary = primaryIssueOf(record);
186
+ const identity = {
187
+ resolution: found.resolution,
188
+ ...(found.contested ? { contested: true } : {}),
189
+ authorizes_actions: authorizesActions(found),
190
+ ...(primary ? { primary_issue: primary } : {}),
191
+ candidates: found.candidates,
192
+ };
193
+ if (found.resolution === "inferred_primary") {
194
+ notes.push(`Resolved by inference (${via === "session_ref" ? "the only request citing this session" : "the first issue in the request's refs"}). Name ${record.id} before any steer, release or closure.`);
195
+ }
57
196
  const standing = deps.projectOf(record) ?? undefined;
58
197
  const queue = deps.queueOf?.(record);
59
198
  const now = deps.now?.() ?? new Date();
@@ -64,7 +203,6 @@ export async function missionStatus(ref, records, deps) {
64
203
  const named = String(record.body.session ?? "") || undefined;
65
204
  const seen = new Set();
66
205
  const addSession = (session, association) => {
67
- const key = `${association}:${session}`;
68
206
  if (seen.has(session))
69
207
  return;
70
208
  seen.add(session);
@@ -74,7 +212,6 @@ export async function missionStatus(ref, records, deps) {
74
212
  live: live ? (live.has(session) ? "running" : "absent") : "unknown",
75
213
  ...(deps.paneBusy && live?.has(session) ? { activity: deps.paneBusy(session) } : {}),
76
214
  });
77
- void key;
78
215
  };
79
216
  if (typeof briefTo === "string" && briefTo)
80
217
  addSession(briefTo.replace(/^new:/, ""), "brief_target");
@@ -91,14 +228,22 @@ export async function missionStatus(ref, records, deps) {
91
228
  notes.push("Live session state was not joined (the viewer's tier or the host does not allow it).");
92
229
  const issues = [];
93
230
  if (deps.issueOf) {
94
- for (const r of [...record.refs, ...record.evidence_refs]) {
231
+ for (const r of [...new Set([...record.refs, ...record.evidence_refs])]) {
95
232
  const m = ISSUE_REF.exec(r);
96
233
  if (!m)
97
234
  continue;
98
- const issue = await deps
99
- .issueOf(m[1], m[2], m[3])
100
- .catch(() => ({ state: "unavailable" }));
101
- issues.push({ ref: r, state: issue.state, ...(issue.title ? { title: issue.title } : {}), ...(issue.merged !== undefined ? { merged: issue.merged } : {}) });
235
+ const issue = await deps.issueOf(m[1], m[2], m[3]).catch(() => ({ state: "unverified", access: "unreachable" }));
236
+ issues.push({
237
+ ref: r,
238
+ relation: primary?.ref === r ? "primary" : "related",
239
+ state: issue.state,
240
+ ...(issue.title ? { title: issue.title } : {}),
241
+ ...(issue.merged !== undefined ? { merged: issue.merged } : {}),
242
+ ...(issue.access ? { access: issue.access } : {}),
243
+ });
244
+ }
245
+ if (issues.some((i) => i.state === "unverified")) {
246
+ notes.push("Some GitHub references could not be read by the connector; they are unverified, not missing.");
102
247
  }
103
248
  }
104
249
  const grant = steeringGrantOf(record);
@@ -108,22 +253,47 @@ export async function missionStatus(ref, records, deps) {
108
253
  const receipt = (record.completion_receipt ?? {});
109
254
  const anyLive = sessions.some((s) => s.live === "running");
110
255
  const prs = issues.filter((i) => i.merged !== undefined);
256
+ const unverifiedRefs = issues.some((i) => i.state === "unverified");
257
+ const release = releaseTruth(record, records, deps, now);
258
+ const missionSessions = new Set(sessions.map((x) => x.session.replace(/^session:/, "")));
259
+ const closure = closureTruth(record, records, missionSessions);
260
+ const choice = standing?.decision?.choice ?? (str(receipt.decision) || undefined);
261
+ const instructions = standing?.decision?.instructions ?? (str(receipt.instructions) || undefined);
111
262
  const stageTruth = {
112
263
  approved: record.approval_state === "approved",
264
+ approval: {
265
+ state: record.approval_state ?? "not_required",
266
+ ...(choice ? { choice } : {}),
267
+ ...(instructions ? { instructions } : {}),
268
+ outstanding: standing?.outstanding ?? [],
269
+ },
113
270
  queued: queue?.state === "queued" || queue?.state === "delivered",
114
271
  delivered: queue?.state === "delivered",
115
272
  running: anyLive ? "evidence" : "no_evidence",
116
- pr_merged: prs.length ? (prs.every((p) => p.merged) ? "merged" : "open_or_absent") : "no_pr",
117
- released: receipt.published || receipt.version ? "evidence" : "no_evidence",
118
- closed: standing?.stage === "completed" && standing?.standing === "done",
273
+ request_completed: record.state === "completed",
274
+ pr_merged: unverifiedRefs ? "unverified" : prs.length ? (prs.every((p) => p.merged) ? "merged" : "open_or_absent") : "no_pr",
275
+ released: release.released,
276
+ ...(release.release_evidence ? { release_evidence: release.release_evidence } : {}),
277
+ closed: Boolean(closure),
278
+ ...(closure ? { closure_evidence: closure } : {}),
119
279
  };
120
280
  if (stageTruth.running === "no_evidence" && record.approval_state === "approved") {
121
281
  notes.push("No bound session shows live; an idle or absent pane is not proof of completion or failure.");
122
282
  }
283
+ if (stageTruth.request_completed && !stageTruth.closed) {
284
+ notes.push("The request completed; the mission stays open until an approved session ending cites it.");
285
+ }
286
+ if (stageTruth.pr_merged === "merged" && stageTruth.released !== "verified" && stageTruth.released !== "receipt_recorded") {
287
+ notes.push("Merged is not released: no fresh production check is recorded for this mission.");
288
+ }
289
+ if (found.contested) {
290
+ notes.push("This request declares the issue, and an earlier request holds it by inference; neither is acted on by issue. Name the record id.");
291
+ }
123
292
  return {
124
293
  ref,
125
294
  found: true,
126
295
  via,
296
+ identity,
127
297
  subject: {
128
298
  id: record.id,
129
299
  kind: record.kind,
@@ -138,7 +308,9 @@ export async function missionStatus(ref, records, deps) {
138
308
  issues,
139
309
  steering: { grant: grantState, attempts_used: attempts, ...(grant?.max_attempts !== undefined ? { max_attempts: grant.max_attempts } : {}) },
140
310
  stage_truth: stageTruth,
141
- next_authorized_action: nextAction(record, standing ?? null, queue),
311
+ next_authorized_action: identity.authorizes_actions
312
+ ? nextAction(record, standing ?? null, queue)
313
+ : `Not authorizing (${found.contested ? "contested" : "inferred"}): call mission_status with ${record.id} before acting. ${nextAction(record, standing ?? null, queue)}`,
142
314
  notes,
143
315
  };
144
316
  }
@@ -0,0 +1,144 @@
1
+ import { type JevConfig, type ProviderAvailability, type ProviderId } from "./providers.js";
2
+ export declare const PREFLIGHT_CONTRACT = "miadi.mission_model_preflight";
3
+ export declare const PREFLIGHT_VERSION = 1;
4
+ /** Below this, a Jev choice is not acted on (the ≥0.8 practice of jgwill/Miadi#770). */
5
+ export declare const JEV_CONFIDENCE_FLOOR = 0.8;
6
+ export declare const JEV_MODEL = "jev-latest";
7
+ export declare const TASK_TYPES: readonly ["file_relevance", "architecture_refactor", "implementation", "review"];
8
+ export type TaskType = (typeof TASK_TYPES)[number];
9
+ export declare const CRITICALITIES: readonly ["normal", "security_critical"];
10
+ export type Criticality = (typeof CRITICALITIES)[number];
11
+ export declare const REVIEW_TIERS: readonly ["standard", "stronger_reviewer", "human"];
12
+ export type ReviewTier = (typeof REVIEW_TIERS)[number];
13
+ export interface ModelCandidate {
14
+ /** provider, or provider:model when a model is known; the option Jev is asked to choose. */
15
+ option: string;
16
+ provider: ProviderId;
17
+ /** A concrete model id when the host shows one, else null. */
18
+ model_id: string | null;
19
+ /** configured_default: the provider's own config names it; it is not proof of what a launch will use. */
20
+ model_id_source: "configured_default" | "fixture" | "unknown";
21
+ /** What is known of the model's strength, with its source; absent on a live host, where nothing is measured yet. */
22
+ capability_note?: string;
23
+ installed: boolean;
24
+ version?: string;
25
+ /** Config-directory evidence only: never a verified sign-in. */
26
+ auth: "config_dir_only" | "none";
27
+ quota: "unknown";
28
+ eligible: boolean;
29
+ policy_reason: string;
30
+ }
31
+ export interface LaunchEvidence {
32
+ provider: ProviderId;
33
+ model?: string;
34
+ /** What shows it (a process line, a pane header); reported by the caller, not checked by the connector. */
35
+ evidence: string;
36
+ }
37
+ export type PreflightState = "recommended" | "abstained" | "provider_mismatch";
38
+ export type DeciderSource = "jev" | "static_fallback" | "synthetic_fixture";
39
+ export type RouterState = "answered" | "not_requested" | "router_unconfigured" | "router_unavailable" | "invalid_answer";
40
+ export interface ModelPreflight {
41
+ contract: typeof PREFLIGHT_CONTRACT;
42
+ version: typeof PREFLIGHT_VERSION;
43
+ state: PreflightState;
44
+ mission: {
45
+ id: string;
46
+ resolution: string;
47
+ authorizes_actions: boolean;
48
+ primary_issue?: string;
49
+ };
50
+ /** sha256 of the decision state (what Jev sees or would see), for audit and replay. */
51
+ input_hash: string;
52
+ task: {
53
+ type: TaskType;
54
+ criticality: Criticality;
55
+ };
56
+ approved_provider: ProviderId | null;
57
+ /** approved: the request was approved with this provider. requested: it is not approved (yet); the provider is only asked for. */
58
+ provider_basis: "approved" | "requested";
59
+ candidates: ModelCandidate[];
60
+ decider: {
61
+ source: DeciderSource;
62
+ router: RouterState;
63
+ model?: string;
64
+ calibration: "uncalibrated" | "not_applicable";
65
+ confidence_floor?: number;
66
+ derivation: string;
67
+ detail?: string;
68
+ };
69
+ recommendation: {
70
+ provider?: ProviderId;
71
+ model_id?: string | null;
72
+ confidence: number | null;
73
+ probabilities?: Record<string, number>;
74
+ review_tier: ReviewTier;
75
+ explanation: string;
76
+ abstain_reasons: string[];
77
+ };
78
+ evidence: {
79
+ latency_ms?: number;
80
+ jev_billing?: Record<string, string>;
81
+ model_cost: "unknown";
82
+ quota: "unknown";
83
+ };
84
+ launch: {
85
+ approved_provider: ProviderId | null;
86
+ actual: "unverified" | (LaunchEvidence & {
87
+ source: "reported";
88
+ });
89
+ mismatch: boolean | null;
90
+ };
91
+ policy: {
92
+ deterministic: true;
93
+ authorizes: false;
94
+ note: string;
95
+ };
96
+ activation_plan?: string[];
97
+ }
98
+ /** What a decider returns for one request: answers by question id, and what the call cost. */
99
+ export interface DeciderAnswer {
100
+ answers: Record<string, unknown>;
101
+ billing?: Record<string, string>;
102
+ latency_ms?: number;
103
+ }
104
+ export interface JevAdapter {
105
+ source: "jev" | "synthetic_fixture";
106
+ model: string;
107
+ decide(request: JevRequest): Promise<DeciderAnswer>;
108
+ }
109
+ export interface JevRequest {
110
+ state: Record<string, unknown>;
111
+ model: string;
112
+ questions: Record<string, {
113
+ type: "choice";
114
+ instructions: string;
115
+ criteria: Record<string, string>;
116
+ }>;
117
+ }
118
+ /** The model a provider's own config names, when it names one. Only that key is read. */
119
+ export declare function configuredModel(provider: ProviderId, home?: string): string | null;
120
+ export declare function candidatesFor(found: ProviderAvailability[], approved: ProviderId | null, modelOf?: (p: ProviderId) => string | null, basis?: "approved" | "requested"): ModelCandidate[];
121
+ /** The least review a task gets, whatever any decider says. Jev may raise it, never lower it. */
122
+ export declare function reviewFloor(task: TaskType, criticality: Criticality): ReviewTier;
123
+ export declare function decisionState(task: TaskType, criticality: Criticality, candidates: ModelCandidate[]): Record<string, unknown>;
124
+ export declare function hashState(state: unknown): string;
125
+ export declare function buildJevRequest(state: Record<string, unknown>, candidates: ModelCandidate[], model?: string): JevRequest;
126
+ /** A choice answer against its question: the failures, empty when it holds. */
127
+ export declare function checkChoice(criteria: Record<string, string>, answer: any): string[];
128
+ /** One call to Jev, never resent: a lost answer may already have been charged. */
129
+ export declare function jevAdapter(config: JevConfig, fetchImpl?: typeof fetch, timeoutMs?: number): JevAdapter;
130
+ export interface PreflightInput {
131
+ mission: ModelPreflight["mission"];
132
+ approved_provider: ProviderId | null;
133
+ provider_basis?: "approved" | "requested";
134
+ task: TaskType;
135
+ criticality: Criticality;
136
+ candidates: ModelCandidate[];
137
+ launch?: LaunchEvidence;
138
+ /** The decider to consult, when the caller asked for one; absent means static fallback only. */
139
+ adapter?: JevAdapter;
140
+ /** Why no adapter: not_requested or router_unconfigured. */
141
+ routerWhenAbsent?: "not_requested" | "router_unconfigured";
142
+ }
143
+ export declare const ACTIVATION_PLAN: string[];
144
+ export declare function modelPreflight(input: PreflightInput): Promise<ModelPreflight>;