@konneal/engine 0.2.33 → 0.2.34

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.
@@ -12,6 +12,30 @@
12
12
  // answer depends on the declaration, not just the query) and is never
13
13
  // written into them — the caches stay context-clean by construction.
14
14
 
15
+ /** One act the declared entity's lifecycle machine offers at its current
16
+ * state, FOR THE DECLARING USER'S ROLE (the platform filters before
17
+ * publishing; the wire is advisory — the platform re-validates every
18
+ * proposed act authoritatively on the way back). */
19
+ export interface MachineAct {
20
+ /** the machine's own action id (the model's vocabulary) */
21
+ action: string;
22
+ /** the state the act lands the entity on */
23
+ to: string;
24
+ /** the declared guard (the caller-supplied computed input the
25
+ * transition requires), when the transition declares one */
26
+ guard?: string;
27
+ }
28
+
29
+ /** The affordance channel (TODO.ai-platform/08): the entity declaration's
30
+ * machine facet — the lifecycle machine's current state (the fact,
31
+ * published even when the role fires nothing) + the acts offered to the
32
+ * declaring user's role (EMPTY means "the user can read but not act").
33
+ * Only ever present on the entity kind. */
34
+ export interface MachineContext {
35
+ state: string;
36
+ acts: MachineAct[];
37
+ }
38
+
15
39
  export interface DeclaredContext {
16
40
  kind: "page" | "entity" | "document" | "account";
17
41
  /** display label ("this certificate R60/2021-A-EX1-26.01") — echoed
@@ -24,6 +48,8 @@ export interface DeclaredContext {
24
48
  * (urn:oiml:pub:r:60-1:2021) or the plain docidentifier */
25
49
  doc?: string;
26
50
  edition?: string;
51
+ /** the affordance channel's machine facet (entity kind only) */
52
+ machine?: MachineContext;
27
53
  }
28
54
 
29
55
  /** The account context's live-read echo (TODO.ai-platform/03): WHEN the
@@ -55,10 +81,47 @@ export interface AppliedContext {
55
81
  * chip grounded this answer in the model node itself — its constraint,
56
82
  * its provenance, its tests */
57
83
  model?: { node_id: string; kind: string; standard: string; clause?: string };
84
+ /** the affordance channel's echo (TODO.ai-platform/08): the declared
85
+ * machine's state + how many acts it offered the declaring user's role
86
+ * — the panel's honest context line reads THIS, never its own copy */
87
+ machine?: { state: string; offered: number };
58
88
  }
59
89
 
60
90
  export const NO_CONTEXT: AppliedContext = { kind: "none", scoped_to: null };
61
91
 
92
+ /** The affordance channel's bounds (TODO.ai-platform/08): the machine
93
+ * facet is a hint from the panel, so it parses bounded and degrades to
94
+ * undefined (no facet) on any malformation — never to an error. The
95
+ * state is the fact; the acts may be EMPTY (the role fires nothing at
96
+ * this state — the honest "read but not act"). */
97
+ const MACHINE_STATE_MAX = 60;
98
+ const MACHINE_ACT_MAX = 24;
99
+ const MACHINE_FIELD_MAX = 60;
100
+
101
+ export function parseMachine(v: any): MachineContext | undefined {
102
+ if (!v || typeof v !== "object") return undefined;
103
+ const state = typeof v.state === "string" ? v.state.trim().slice(0, MACHINE_STATE_MAX) : "";
104
+ if (!state) return undefined;
105
+ const acts: MachineAct[] = [];
106
+ for (const a of Array.isArray(v.acts) ? v.acts.slice(0, MACHINE_ACT_MAX) : []) {
107
+ const action = typeof a?.action === "string" ? a.action.trim().slice(0, MACHINE_FIELD_MAX) : "";
108
+ const to = typeof a?.to === "string" ? a.to.trim().slice(0, MACHINE_FIELD_MAX) : "";
109
+ if (!action || !to) continue;
110
+ const guard = typeof a?.guard === "string" && a.guard.trim() ? a.guard.trim().slice(0, MACHINE_FIELD_MAX) : undefined;
111
+ acts.push({ action, to, ...(guard ? { guard } : {}) });
112
+ }
113
+ return { state, acts };
114
+ }
115
+
116
+ /** The proposal bound (TODO.ai-platform/08): a draft act the offered set
117
+ * does not carry is never proposed. An absent facet is not a bound (the
118
+ * page publishes none); a present facet with an empty acts array bounds
119
+ * to NOTHING — the user can read but not act. */
120
+ export function machineOffers(machine: MachineContext | undefined, action: string): boolean {
121
+ if (!machine) return false;
122
+ return machine.acts.some((a) => a.action === action);
123
+ }
124
+
62
125
  /** Parse + bound the ask body's optional `context` field. Anything
63
126
  * malformed degrades to null (no context), never to a 400 — a context
64
127
  * the service can't parse is a context it must not apply. */
@@ -70,7 +133,9 @@ export function parseContext(body: any): DeclaredContext | null {
70
133
  const route = typeof c.route === "string" && c.route.trim() ? c.route.trim().slice(0, 200) : undefined;
71
134
  const doc = typeof c.doc === "string" && c.doc.trim() ? c.doc.trim().slice(0, 80) : undefined;
72
135
  const edition = typeof c.edition === "string" && /^\d{4}$/.test(c.edition.trim()) ? c.edition.trim() : undefined;
73
- return { kind: c.kind, label, ...(route ? { route } : {}), ...(doc ? { doc } : {}), ...(edition ? { edition } : {}) };
136
+ // the machine facet rides the entity kind only (the channel's contract)
137
+ const machine = c.kind === "entity" ? parseMachine(c.machine) : undefined;
138
+ return { kind: c.kind, label, ...(route ? { route } : {}), ...(doc ? { doc } : {}), ...(edition ? { edition } : {}), ...(machine ? { machine } : {}) };
74
139
  }
75
140
 
76
141
  import { refCodec, type DocScope } from "./codecs.ts";
@@ -132,6 +197,10 @@ export function appliedContext(
132
197
  scoped_to: scope ? scope.label : null,
133
198
  ...(note ? { note } : {}),
134
199
  ...(live ? { live } : {}),
200
+ // the affordance echo (TODO.ai-platform/08): the machine's state +
201
+ // the offered-act COUNT (the acts themselves are the platform's
202
+ // vocabulary, echoed back only bounded on the draft path)
203
+ ...(declared.machine ? { machine: { state: declared.machine.state, offered: declared.machine.acts.length } } : {}),
135
204
  };
136
205
  }
137
206
 
@@ -165,7 +234,14 @@ export function parseAppliedContext(v: any): AppliedContext | null {
165
234
  ...(typeof v.model.clause === "string" && v.model.clause.trim() ? { clause: v.model.clause.slice(0, 120) } : {}),
166
235
  }
167
236
  : undefined;
168
- return { kind: v.kind, ...(label ? { label } : {}), scoped_to: scoped, ...(note ? { note } : {}), ...(live ? { live } : {}), ...(model ? { model } : {}) };
237
+ // The machine echo round-trips bounded too (TODO.ai-platform/08): a
238
+ // resumed session keeps the "the machine offers N acts at this state"
239
+ // honesty.
240
+ const machine =
241
+ v.machine && typeof v.machine === "object" && typeof v.machine.state === "string" && typeof v.machine.offered === "number"
242
+ ? { state: v.machine.state.slice(0, MACHINE_STATE_MAX), offered: Math.min(Math.max(0, Math.floor(v.machine.offered)), MACHINE_ACT_MAX) }
243
+ : undefined;
244
+ return { kind: v.kind, ...(label ? { label } : {}), scoped_to: scoped, ...(note ? { note } : {}), ...(live ? { live } : {}), ...(model ? { model } : {}), ...(machine ? { machine } : {}) };
169
245
  }
170
246
 
171
247
  /** The prompt note the declared context contributes (rides the
@@ -185,9 +261,18 @@ export function contextNote(declared: DeclaredContext | null, scope: DocScope |
185
261
  return `Context note: the user is viewing ${declared.label || "a page"}${declared.route ? ` (${declared.route})` : ""} in the ${P().publisher.product_name} platform. The passages come from the general corpus; frame procedural guidance for that page when relevant.`;
186
262
  }
187
263
  if (declared.kind === "entity") {
264
+ // The affordance channel's sentence (TODO.ai-platform/08): the
265
+ // machine's state is a fact the answer may use; the offered acts are
266
+ // the proposal bound — an act the set does not offer is never
267
+ // proposed, and an empty set is the honest "read but not act".
268
+ const machineSentence = declared.machine
269
+ ? declared.machine.acts.length
270
+ ? ` The entity's lifecycle machine is at ${declared.machine.state}; the acts it offers this user's role there are exactly: ${declared.machine.acts.map((a) => a.action).join(", ")}. Never propose an act outside that set.`
271
+ : ` The entity's lifecycle machine is at ${declared.machine.state}, and it offers this user's role NO acts there — the user can read but not act; say so when they ask for an act, and propose nothing.`
272
+ : "";
188
273
  return scope
189
- ? `Context note: the user is asking about ${declared.label || "an entity"} — the passages are scoped to ${scope.label}, the publication that governs it. You do NOT have the entity's own data; answer what the publication requires and say when the question needs the record itself.`
190
- : `Context note: the user is asking about ${declared.label || "an entity"}. You do NOT have the entity's own data; answer from the corpus passages and say when the question needs the record itself.`;
274
+ ? `Context note: the user is asking about ${declared.label || "an entity"} — the passages are scoped to ${scope.label}, the publication that governs it. You do NOT have the entity's own data; answer what the publication requires and say when the question needs the record itself.${machineSentence}`
275
+ : `Context note: the user is asking about ${declared.label || "an entity"}. You do NOT have the entity's own data; answer from the corpus passages and say when the question needs the record itself.${machineSentence}`;
191
276
  }
192
277
  return scope
193
278
  ? `Context note: the user scoped this question to ${scope.label} — the passages come from that publication. If they cannot answer the question, say so instead of drawing on other documents.`
@@ -231,6 +231,16 @@ const norm = (s: string) =>
231
231
  /** The document-name normalization: whitespace-free ("R 60" ≡ "R60"). */
232
232
  const docNorm = (s: string) => norm(s).replace(/\s+/g, "");
233
233
 
234
+ /** The traceability primitive, shared with the api_call act's body guard
235
+ * (apicalls.ts): a value traces when it appears in the user's own
236
+ * messages (normalized). The proposing model never gets the benefit of
237
+ * the doubt — the user's words are the only source. */
238
+ export function valueTracesToUser(value: unknown, userTurns: string[]): boolean {
239
+ if (typeof value !== "string" || !value.trim()) return false;
240
+ const haystack = norm(userTurns.join("\n"));
241
+ return haystack.includes(norm(value));
242
+ }
243
+
234
244
  export interface GuardedFields {
235
245
  standard?: string;
236
246
  family_designation?: string;
@@ -48,12 +48,13 @@ export interface Env {
48
48
  // Domain modules import `portModelRunner(env)` etc. — never env.AI
49
49
  // directly; the purity lint enforces it. Zero runtime change: the
50
50
  // adapters forward to the same bindings the call sites used before.
51
- import { cfModelRunner, cfVectorIndex, cfKv, cfBlobs, cfRuntime } from "./ports/cloudflare/adapters.ts";
51
+ import { cfModelRunner, cfVectorIndex, cfKv, cfBlobs, cfRuntime, cfStore } from "./ports/cloudflare/adapters.ts";
52
52
  import type { ModelRunner } from "./ports/model.ts";
53
53
  import type { VectorIndex } from "./ports/vector.ts";
54
54
  import type { Kv } from "./ports/kv.ts";
55
55
  import type { Blobs } from "./ports/blobs.ts";
56
56
  import type { Runtime } from "./ports/runtime.ts";
57
+ import type { StoreQuery } from "./ports/store.ts";
57
58
 
58
59
  export function portModelRunner(env: Env): ModelRunner {
59
60
  return cfModelRunner(env.AI);
@@ -76,6 +77,9 @@ export function portKv(env: Env): Kv {
76
77
  export function portBlobs(env: Env): Blobs {
77
78
  return cfBlobs(env.UNIT_ASSETS);
78
79
  }
80
+ export function portStore(env: Env): StoreQuery {
81
+ return cfStore(env.DB);
82
+ }
79
83
  /** Does the deployment bind this lane index? Presence wiring stays in
80
84
  * the ports layer so domain stages never touch raw bindings. */
81
85
  export function hasLane(env: Env, which: "glossary"): boolean {
@@ -167,6 +167,14 @@ export function cfBlobs(bucket: unknown): Blobs {
167
167
  };
168
168
  }
169
169
 
170
+ // The transitional relational port (ports/store.ts): D1 IS the
171
+ // adapter — the port narrows to semantic repositories when the
172
+ // repository extraction completes.
173
+ import type { StoreQuery } from "../store.ts";
174
+ export function cfStore(db: unknown): StoreQuery {
175
+ return db as StoreQuery;
176
+ }
177
+
170
178
  export function cfRuntime(ctx: { waitUntil(p: Promise<unknown>): void } | undefined): Runtime {
171
179
  return {
172
180
  defer(fn) {
@@ -56,8 +56,14 @@ export function entitlementScope(keys: Set<string>): Set<string> | null {
56
56
  /** Validate + intersect. Returns { error } when the request explicitly
57
57
  * disables every dataset (a user error, not a scope). The corpora a
58
58
  * dataset searches travel WITH the declaration (profile datasets.yaml,
59
- * `corpora:`) — the engine maps no publisher names. */
60
- export function resolveRequestScope(body: any, member: unknown): RequestScope | { error: "empty-datasets" } {
59
+ * `corpora:`) — the engine maps no publisher names.
60
+ *
61
+ * `opts.keyWithEntitlements` (the key-tier licensed federation): an
62
+ * API-key caller whose request carries a VALIDATED entitlement set is
63
+ * admitted to the session-gated (federated) datasets — the key's
64
+ * entitlement set is the licensed scope the internal lane honors; the
65
+ * internal worker re-validates the key server-side before it answers. */
66
+ export function resolveRequestScope(body: any, member: unknown, opts?: { keyWithEntitlements?: boolean }): RequestScope | { error: "empty-datasets" } {
61
67
  const allIds = DATASETS().map((d) => d.id);
62
68
  const requested = Array.isArray(body?.datasets)
63
69
  ? (body.datasets as unknown[]).filter((x): x is string => typeof x === "string" && allIds.includes(x))
@@ -65,7 +71,7 @@ export function resolveRequestScope(body: any, member: unknown): RequestScope |
65
71
  if (requested !== null && requested.length === 0) return { error: "empty-datasets" };
66
72
  const permittedIds = allIds.filter((id) => {
67
73
  const d = DATASETS().find((x) => x.id === id)!;
68
- return d.session ? datasetAllowed(d, member) : true;
74
+ return d.session ? datasetAllowed(d, member) || !!opts?.keyWithEntitlements : true;
69
75
  });
70
76
  // requested ∩ permitted — an explicit request NEVER widens past the
71
77
  // session's permissions (a member without ai-preview asking
@@ -1,12 +0,0 @@
1
- import {
2
- handleAsk
3
- } from "./chunk-WSLADX7O.js";
4
- import "./chunk-NHHQAGBW.js";
5
- import "./chunk-2RKYO3OC.js";
6
- import "./chunk-A3QHHUN5.js";
7
- import "./chunk-JGKSUSF5.js";
8
- import "./chunk-OMXAE27N.js";
9
- import "./chunk-3FYJM7LH.js";
10
- export {
11
- handleAsk
12
- };
@@ -1,12 +0,0 @@
1
- import {
2
- handleSearch
3
- } from "./chunk-5LSEBH3J.js";
4
- import "./chunk-NHHQAGBW.js";
5
- import "./chunk-2RKYO3OC.js";
6
- import "./chunk-A3QHHUN5.js";
7
- import "./chunk-JGKSUSF5.js";
8
- import "./chunk-OMXAE27N.js";
9
- import "./chunk-3FYJM7LH.js";
10
- export {
11
- handleSearch
12
- };