@tangleai/context 0.21.1

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.
@@ -0,0 +1,357 @@
1
+ //@ts-check
2
+ /**
3
+ * The ledger's four record schemas, as plain JSON Schema documents
4
+ * compiled by `JarenValidator` when a ledger is constructed. A malformed
5
+ * write is rejected at the boundary exactly the way a malformed tool call
6
+ * is — the suite validating its own durable state with its own validator.
7
+ *
8
+ * They live in one module rather than four files because they are small,
9
+ * they share a vocabulary (`at` is an RFC 3339 timestamp everywhere, ids
10
+ * are non-empty strings everywhere), and one file is where a reader looks
11
+ * for "what may a ledger hold".
12
+ *
13
+ * Why four kinds and not one bag: they differ in every dimension that
14
+ * matters. Lifetime — a goal is superseded, a memory accumulates, a slot
15
+ * is garbage. Retrieval — a goal is always in context, memories are
16
+ * recalled by relevance, slots are addressed by name. Write rule — a
17
+ * memory or skill may be proposed by a model (and must therefore carry
18
+ * evidence), a slot is written by the harness and never proposed.
19
+ */
20
+
21
+ import { CLAIM_EVIDENCE_SCHEMA } from './evidence.js';
22
+
23
+ /**
24
+ * A timestamp property: RFC 3339, the suite's only date representation.
25
+ *
26
+ * Both keywords earn their place. `format` is the right metadata and a
27
+ * host that injects a validator with `@jarenjs/formats` registered gets
28
+ * real format checking from it — but `format` is annotation-only by
29
+ * default, so on its own it would enforce NOTHING here, and this package
30
+ * may not depend on `@jarenjs/formats` to find that out. The `pattern`
31
+ * is the zero-dependency enforcement, and it is not pedantry: recency
32
+ * ordering compares these strings lexicographically, so a junk `at`
33
+ * would not be rejected, it would quietly sort wrong.
34
+ *
35
+ * The lexicographic comparison is exact only for a consistent offset.
36
+ * The ledger's own clock emits `Z`; a host mixing offsets gets ordering
37
+ * that is off by the difference, which is why the pattern documents the
38
+ * offset rather than banning it.
39
+ */
40
+ const AT = {
41
+ type: 'string',
42
+ format: 'date-time',
43
+ pattern: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$',
44
+ };
45
+
46
+ /** A non-empty identifier. */
47
+ const ID = { type: 'string', minLength: 1 };
48
+
49
+ /**
50
+ * A stored embedding: a plain array of numbers, never a typed array. A
51
+ * `Float32Array` does not survive the storage boundary — JSON serializes
52
+ * it as a dense object — so the ledger's form is what JSON keeps. What
53
+ * the schema cannot say (every component finite, the length equal to the
54
+ * identity's `dims`) the ledger checks with `isVector` from
55
+ * `@jarenjs/core/vector` in the same gate.
56
+ */
57
+ const EMBEDDING = { type: 'array', items: { type: 'number' }, minItems: 1 };
58
+
59
+ /**
60
+ * A vector's identity: which model produced it, at what width. Vectors
61
+ * from two models are pairwise meaningless and compare into plausible
62
+ * garbage, so an embedding never travels without this and ranked recall
63
+ * refuses to mix two.
64
+ */
65
+ const EMBEDDED_BY = {
66
+ type: 'object',
67
+ properties: {
68
+ model: { type: 'string', minLength: 1 },
69
+ dims: { type: 'integer', minimum: 1 },
70
+ },
71
+ required: ['model', 'dims'],
72
+ additionalProperties: false,
73
+ };
74
+
75
+ /**
76
+ * `embedding` and `embeddedBy` are both-or-neither, and nothing requires
77
+ * them: an un-embedded record is exactly as valid as it ever was. The
78
+ * schemas declare no `$schema`, so they compile under the validator's
79
+ * draft-07 default, where "if this member is present, that one is
80
+ * required" is spelled `dependencies` (the `dependentRequired` of
81
+ * 2019-09 and later says the same thing).
82
+ */
83
+ const EMBEDDING_PAIR = { embedding: ['embeddedBy'], embeddedBy: ['embedding'] };
84
+
85
+ /**
86
+ * One active objective. Singular by construction: a second `setGoal`
87
+ * supersedes this one and archives it, so "what am I doing" has exactly
88
+ * one answer at any moment.
89
+ *
90
+ * `progress` is storage only in this order — an append-only record of
91
+ * what happened, with the evidence for it. Nothing here interprets a
92
+ * progress entry or decides when a goal is done.
93
+ */
94
+ export const GOAL_SCHEMA = {
95
+ $id: 'https://jarenjs.github.io/schemas/ai/ledger-goal.json',
96
+ type: 'object',
97
+ properties: {
98
+ objective: { type: 'string', minLength: 1 },
99
+ createdAt: AT,
100
+ status: { enum: ['active', 'done', 'abandoned', 'superseded'] },
101
+ progress: {
102
+ type: 'array',
103
+ items: {
104
+ type: 'object',
105
+ properties: {
106
+ at: AT,
107
+ id: ID,
108
+ note: { type: 'string', minLength: 1 },
109
+ // an unevidenced progress note is a claim, not a record
110
+ evidence: { type: 'string', minLength: 1 },
111
+ },
112
+ required: ['at', 'note', 'evidence'],
113
+ additionalProperties: false,
114
+ },
115
+ },
116
+ checkpoint: {
117
+ type: 'object',
118
+ properties: {
119
+ version: { const: 1 },
120
+ records: { type: 'array', items: { type: 'object', properties: {
121
+ note: ID, evidence: ID,
122
+ }, required: ['note', 'evidence'], additionalProperties: false } },
123
+ sources: { type: 'array', items: { type: 'object', properties: {
124
+ id: ID, at: AT, record: { type: 'integer', minimum: 0 },
125
+ }, required: ['id', 'at', 'record'], additionalProperties: false } },
126
+ },
127
+ required: ['version', 'records', 'sources'], additionalProperties: false,
128
+ },
129
+ retention: { type: 'object', properties: {
130
+ version: { const: 1 }, reason: { const: 'goal-budget' },
131
+ retired: { type: 'array', items: ID, uniqueItems: true },
132
+ }, required: ['version', 'reason', 'retired'], additionalProperties: false },
133
+ },
134
+ required: ['objective', 'createdAt', 'status', 'progress'],
135
+ additionalProperties: false,
136
+ };
137
+
138
+ /**
139
+ * A fact worth carrying past this context window.
140
+ *
141
+ * `evidence` is REQUIRED, and that is the load-bearing part: a memory
142
+ * without evidence is not a memory, it is a guess, and a ledger full of
143
+ * guesses is worse than an empty one. It is also what makes a
144
+ * model-proposed refinement auditable — the reviewer of a patch can ask
145
+ * "on what basis" and get an answer from the record itself.
146
+ *
147
+ * `embedding` + `embeddedBy` are optional and travel together: a memory
148
+ * that carries them can be recalled by meaning through the embedder
149
+ * seam; one that does not is recalled by tag and recency exactly as
150
+ * before, and ranked recall reports it as skipped rather than scoring it.
151
+ */
152
+ export const MEMORY_SCHEMA = {
153
+ $id: 'https://jarenjs.github.io/schemas/ai/ledger-memory.json',
154
+ type: 'object',
155
+ properties: {
156
+ id: ID,
157
+ text: { type: 'string', minLength: 1 },
158
+ evidence: { anyOf: [{ type: 'string', minLength: 1 }, CLAIM_EVIDENCE_SCHEMA] },
159
+ tags: { type: 'array', items: { type: 'string', minLength: 1 } },
160
+ at: AT,
161
+ // OPTIONAL, as a pair: the vector `recall({ near })` ranks by, and
162
+ // the identity that makes it comparable
163
+ embedding: EMBEDDING,
164
+ embeddedBy: EMBEDDED_BY,
165
+ },
166
+ required: ['id', 'text', 'evidence', 'tags', 'at'],
167
+ dependencies: EMBEDDING_PAIR,
168
+ additionalProperties: false,
169
+ };
170
+
171
+ /**
172
+ * A reusable recipe: when it applies, what to do, and which tools it
173
+ * needs. Retrieved like a memory and composed into a system prompt by
174
+ * whatever drives the agent — the ledger stores it and nothing more.
175
+ */
176
+ export const PROGRAM_SKILL_SCHEMA = {
177
+ type: 'object',
178
+ properties: {
179
+ version: { const: 1 },
180
+ question: { type: 'string', minLength: 1 },
181
+ fingerprint: { type: 'string', pattern: '^[a-f0-9]{64}$' },
182
+ environmentId: { type: 'string', minLength: 1 },
183
+ schemaVersion: { type: 'string', minLength: 1 },
184
+ document: { type: 'object' },
185
+ evidence: { type: 'string', minLength: 1 },
186
+ checked: { const: true },
187
+ },
188
+ required: ['version', 'question', 'fingerprint', 'environmentId', 'schemaVersion', 'document', 'evidence', 'checked'],
189
+ additionalProperties: false,
190
+ };
191
+
192
+ export const SKILL_SCHEMA = {
193
+ $id: 'https://jarenjs.github.io/schemas/ai/ledger-skill.json',
194
+ type: 'object',
195
+ properties: {
196
+ id: ID,
197
+ name: { type: 'string', minLength: 1 },
198
+ when: { type: 'string', minLength: 1 },
199
+ instructions: { type: 'string', minLength: 1 },
200
+ program: PROGRAM_SKILL_SCHEMA,
201
+ tools: { type: 'array', items: { type: 'string', minLength: 1 } },
202
+ at: AT,
203
+ // the same optional pair as a memory; the text a skill is embedded
204
+ // from is its name, when and instructions together
205
+ embedding: EMBEDDING,
206
+ embeddedBy: EMBEDDED_BY,
207
+ },
208
+ required: ['id', 'name', 'when', 'instructions', 'tools', 'at'],
209
+ dependencies: EMBEDDING_PAIR,
210
+ additionalProperties: false,
211
+ };
212
+
213
+ /**
214
+ * An addressable blob's METADATA. The content lives under a separate
215
+ * storage key and never travels with the metadata — that separation is
216
+ * the whole point: a root request may carry `{ name, kind, size, excerpt }`
217
+ * for a hundred slots without carrying one slot's content.
218
+ */
219
+ export const SLOT_SCHEMA = {
220
+ $id: 'https://jarenjs.github.io/schemas/ai/ledger-slot.json',
221
+ type: 'object',
222
+ properties: {
223
+ name: ID,
224
+ kind: { type: 'string', minLength: 1 },
225
+ size: { type: 'integer', minimum: 0 },
226
+ excerpt: { type: 'string' },
227
+ at: AT,
228
+ // OPTIONAL, and optional on purpose: `size` is what a slot costs and
229
+ // is always known, while `count` is what it CONTAINS — lines,
230
+ // records, pieces — which only the writer knows and only sometimes.
231
+ // A root view listing a hundred slots is far more useful with "1 240
232
+ // lines" beside a size, and a slot whose writer could not say is
233
+ // better off saying nothing than guessing.
234
+ count: { type: 'integer', minimum: 0 },
235
+ pinned: { type: 'boolean' },
236
+ },
237
+ required: ['name', 'kind', 'size', 'excerpt', 'at'],
238
+ additionalProperties: false,
239
+ };
240
+
241
+ /** Every ledger schema by kind — what `createLedger` compiles at construction. */
242
+ export const LEDGER_SCHEMAS = {
243
+ goal: GOAL_SCHEMA,
244
+ memory: MEMORY_SCHEMA,
245
+ skill: SKILL_SCHEMA,
246
+ slot: SLOT_SCHEMA,
247
+ };
248
+
249
+ /*
250
+ * The record shapes, as named types beside the schemas that enforce them.
251
+ *
252
+ * These exist because a strictly-typed consumer (the first one was the
253
+ * tangleai rebuild, 2026-08-24) otherwise hand-writes its own copy of
254
+ * "what addMemory returns" and the copy drifts. The schema stays the
255
+ * runtime contract; the typedef is the same statement made to the
256
+ * compiler, and the two live in one file so a change to one is a diff
257
+ * touching the other's neighbourhood.
258
+ */
259
+
260
+ /**
261
+ * One evidenced progress entry on the active goal.
262
+ * @typedef {object} LedgerProgressEntry
263
+ * @property {string} [id]
264
+ * @property {string} at RFC 3339
265
+ * @property {string} note
266
+ * @property {string} evidence
267
+ */
268
+
269
+ /**
270
+ * The active (or archived) objective — see {@link GOAL_SCHEMA}.
271
+ * @typedef {object} LedgerGoal
272
+ * @property {string} objective
273
+ * @property {string} createdAt RFC 3339
274
+ * @property {'active'|'done'|'abandoned'|'superseded'} status
275
+ * @property {LedgerProgressEntry[]} progress
276
+ * @property {{ version: 1, records: { note: string, evidence: string }[], sources: { id: string, at: string, record: number }[] }} [checkpoint]
277
+ * @property {{ version: 1, reason: 'goal-budget', retired: string[] }} [retention]
278
+ */
279
+
280
+ /**
281
+ * A vector's identity — see {@link EMBEDDED_BY}. What `recall({ near })`
282
+ * compares against the query embedder's `{ model, dims }` before any
283
+ * arithmetic happens.
284
+ * @typedef {object} LedgerEmbeddedBy
285
+ * @property {string} model
286
+ * @property {number} dims
287
+ */
288
+
289
+ /**
290
+ * The vector pair, both-or-neither — the schemas' `dependencies` rule
291
+ * (see {@link EMBEDDING_PAIR}) stated in the type: a record either
292
+ * carries `embedding` AND `embeddedBy`, or neither. `embedding` is the
293
+ * vector `recall({ near })` ranks by — plain numbers, never a typed
294
+ * array — and `embeddedBy` its identity. Narrow on either member and
295
+ * the other follows: `if (record.embedding) record.embeddedBy.dims`.
296
+ * An orphan vector does not type, just as the ledger refuses it.
297
+ * @typedef {{ embedding?: undefined, embeddedBy?: undefined }
298
+ * | { embedding: number[], embeddedBy: LedgerEmbeddedBy }} LedgerEmbeddingPair
299
+ */
300
+
301
+ /**
302
+ * A stored memory's own members — see {@link MEMORY_SCHEMA}; the
303
+ * record is these plus {@link LedgerEmbeddingPair}.
304
+ * @typedef {object} LedgerMemoryFields
305
+ * @property {string} id
306
+ * @property {string} text
307
+ * @property {string | import('./evidence.js').ClaimEvidenceEnvelope} evidence
308
+ * @property {string[]} tags
309
+ * @property {string} at RFC 3339
310
+ */
311
+
312
+ /**
313
+ * A stored memory — see {@link MEMORY_SCHEMA}.
314
+ * @typedef {LedgerMemoryFields & LedgerEmbeddingPair} LedgerMemory
315
+ */
316
+
317
+ /**
318
+ * A stored skill's own members — see {@link SKILL_SCHEMA}; the record
319
+ * is these plus {@link LedgerEmbeddingPair}.
320
+ * @typedef {object} LedgerSkillFields
321
+ * @property {string} id
322
+ * @property {string} name
323
+ * @property {string} when
324
+ * @property {string} instructions
325
+ * @property {string[]} tools
326
+ * @property {string} at RFC 3339
327
+ * @property {{ version: 1, question: string, fingerprint: string, environmentId: string,
328
+ * schemaVersion: string, document: any, evidence: string, checked: true }} [program]
329
+ */
330
+
331
+ /**
332
+ * A stored skill — see {@link SKILL_SCHEMA}.
333
+ * @typedef {LedgerSkillFields & LedgerEmbeddingPair} LedgerSkill
334
+ */
335
+
336
+ /**
337
+ * A slot's metadata — see {@link SLOT_SCHEMA}. Never the content.
338
+ * @typedef {object} LedgerSlot
339
+ * @property {string} name
340
+ * @property {string} kind
341
+ * @property {number} size
342
+ * @property {string} excerpt
343
+ * @property {string} at RFC 3339
344
+ * @property {number} [count]
345
+ * @property {boolean} [pinned]
346
+ */
347
+
348
+ /**
349
+ * The one rejection shape every schema-guarded write answers with —
350
+ * produced by `invalidInput` in `check.js`, named here because this is
351
+ * where a consumer of the ledger's API goes looking for "what comes
352
+ * back when a write is refused".
353
+ * @typedef {object} LedgerRejection
354
+ * @property {string} error
355
+ * @property {any[]} errors
356
+ * @property {any} inputSchema
357
+ */
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Path-discriminated RFC 6902 operations. Provider decoding uses this same
3
+ * schema in non-strict mode; local validation remains authoritative.
4
+ * @param {{ maxOps?: number }} [options]
5
+ * @returns {any}
6
+ */
7
+ export function refinementPatchSchema(options?: {
8
+ maxOps?: number;
9
+ }): any;
10
+ /**
11
+ * Where a refinement may write. Anchored, and read as: append or address
12
+ * one memory, append or address one skill, append one progress entry.
13
+ *
14
+ * `-` is RFC 6902's "end of array" and is how an append is written.
15
+ * There is deliberately no way to address INSIDE a record (`/memories/0/text`):
16
+ * a memory is revised by replacing it whole, with fresh evidence, so
17
+ * that every stored record was validated as a whole exactly once.
18
+ */
19
+ export const REFINEMENT_PATH_PATTERN: "^(/memories/(-|[0-9]+)|/skills/(-|[0-9]+)|/goal/progress/-)$";
20
+ /** The default cap on operations per refinement. */
21
+ export const DEFAULT_MAX_OPS: 6;
22
+ export namespace MEMORY_PROPOSAL_SCHEMA {
23
+ let type: string;
24
+ namespace properties {
25
+ namespace text {
26
+ let type_1: string;
27
+ export { type_1 as type };
28
+ export let minLength: number;
29
+ export let description: string;
30
+ }
31
+ namespace evidence {
32
+ let anyOf: ({
33
+ type: "object";
34
+ properties: Record<string, object>;
35
+ required: string[];
36
+ additionalProperties: false;
37
+ } | {
38
+ type: string;
39
+ minLength: number;
40
+ })[];
41
+ }
42
+ namespace tags {
43
+ let type_2: string;
44
+ export { type_2 as type };
45
+ export namespace items {
46
+ let type_3: string;
47
+ export { type_3 as type };
48
+ let minLength_1: number;
49
+ export { minLength_1 as minLength };
50
+ }
51
+ }
52
+ }
53
+ let required: string[];
54
+ let additionalProperties: boolean;
55
+ }
56
+ export namespace SKILL_PROPOSAL_SCHEMA {
57
+ let type_4: string;
58
+ export { type_4 as type };
59
+ export namespace properties_1 {
60
+ namespace name {
61
+ let type_5: string;
62
+ export { type_5 as type };
63
+ let minLength_2: number;
64
+ export { minLength_2 as minLength };
65
+ }
66
+ namespace when {
67
+ let type_6: string;
68
+ export { type_6 as type };
69
+ let minLength_3: number;
70
+ export { minLength_3 as minLength };
71
+ let description_1: string;
72
+ export { description_1 as description };
73
+ }
74
+ namespace instructions {
75
+ let type_7: string;
76
+ export { type_7 as type };
77
+ let minLength_4: number;
78
+ export { minLength_4 as minLength };
79
+ let description_2: string;
80
+ export { description_2 as description };
81
+ }
82
+ namespace tools {
83
+ let type_8: string;
84
+ export { type_8 as type };
85
+ export namespace items_1 {
86
+ let type_9: string;
87
+ export { type_9 as type };
88
+ let minLength_5: number;
89
+ export { minLength_5 as minLength };
90
+ }
91
+ export { items_1 as items };
92
+ }
93
+ }
94
+ export { properties_1 as properties };
95
+ let required_1: string[];
96
+ export { required_1 as required };
97
+ let additionalProperties_1: boolean;
98
+ export { additionalProperties_1 as additionalProperties };
99
+ }
100
+ export namespace PROGRESS_PROPOSAL_SCHEMA {
101
+ let type_10: string;
102
+ export { type_10 as type };
103
+ export namespace properties_2 {
104
+ export namespace note {
105
+ let type_11: string;
106
+ export { type_11 as type };
107
+ let minLength_6: number;
108
+ export { minLength_6 as minLength };
109
+ let description_3: string;
110
+ export { description_3 as description };
111
+ }
112
+ export namespace evidence_1 {
113
+ let type_12: string;
114
+ export { type_12 as type };
115
+ let minLength_7: number;
116
+ export { minLength_7 as minLength };
117
+ let description_4: string;
118
+ export { description_4 as description };
119
+ }
120
+ export { evidence_1 as evidence };
121
+ }
122
+ export { properties_2 as properties };
123
+ let required_2: string[];
124
+ export { required_2 as required };
125
+ let additionalProperties_2: boolean;
126
+ export { additionalProperties_2 as additionalProperties };
127
+ }
128
+ /** The full schema used for handwritten and generated proposals alike. */
129
+ export const REFINEMENT_PATCH_SCHEMA: any;
@@ -0,0 +1,135 @@
1
+ //@ts-check
2
+ /**
3
+ * The refinement patch: a deliberately small subset of RFC 6902.
4
+ *
5
+ * A model that has finished a run is asked what it learned. The unsafe
6
+ * answer is a rewritten prompt or a restated set of memories — a
7
+ * rewrite is unreviewable, unbounded, and a model asked to restate what
8
+ * it remembers will drift it. The safe answer is a PATCH: small enough
9
+ * to read, addressed enough to audit, and reversible with a snapshot.
10
+ *
11
+ * Three restrictions, and each one exists because the alternative fails
12
+ * on the tier this package targets:
13
+ *
14
+ * - **`path` is a pattern, not a free string.** It matches exactly the
15
+ * supplemental subtree — append or replace a memory, append or
16
+ * replace a skill, append one progress entry — and nothing else. The
17
+ * base system prompt is not merely undocumented as a target, it is
18
+ * unaddressable: it is not in the document a refinement is applied
19
+ * to, and no path that could reach it matches this pattern. That is
20
+ * D5 asserted rather than described.
21
+ * - **`op` is three verbs.** `move`, `copy` and `test` are legal RFC
22
+ * 6902 and useless here; every one of them is another shape a small
23
+ * model can get subtly wrong, and none of them expresses anything
24
+ * `add`/`replace`/`remove` cannot.
25
+ * - **The number of operations is capped.** A refinement is meant to be
26
+ * a few evidence-backed updates. A patch of forty operations is a
27
+ * rewrite wearing a patch's clothes, and it is also the shape that
28
+ * makes a repair round useless — the model cannot tell which of forty
29
+ * operations the error came from.
30
+ *
31
+ * `value` carries no `id` and no `at` on purpose. Identity and time are
32
+ * the ledger's to mint: a model that could choose an id could overwrite
33
+ * a record it never read, and a model that could choose a timestamp
34
+ * could put a memory in front of one that came later — the recency
35
+ * ordering everything downstream depends on is not the model's to set.
36
+ */
37
+
38
+ import { CLAIM_EVIDENCE_SCHEMA } from './evidence.js';
39
+
40
+ /**
41
+ * Where a refinement may write. Anchored, and read as: append or address
42
+ * one memory, append or address one skill, append one progress entry.
43
+ *
44
+ * `-` is RFC 6902's "end of array" and is how an append is written.
45
+ * There is deliberately no way to address INSIDE a record (`/memories/0/text`):
46
+ * a memory is revised by replacing it whole, with fresh evidence, so
47
+ * that every stored record was validated as a whole exactly once.
48
+ */
49
+ export const REFINEMENT_PATH_PATTERN =
50
+ '^(/memories/(-|[0-9]+)|/skills/(-|[0-9]+)|/goal/progress/-)$';
51
+
52
+ /** The default cap on operations per refinement. */
53
+ export const DEFAULT_MAX_OPS = 6;
54
+
55
+ /**
56
+ * A proposed memory. `evidence` is required here AND by the ledger's own
57
+ * schema — the same requirement twice, on purpose: this copy is what
58
+ * constrains decoding (a model generating against it tends to write the
59
+ * evidence rather than be corrected into it), and the ledger's copy is
60
+ * what makes the rule true even for a patch that never went near a
61
+ * model. It is the mechanism that stops a refinement laundering a
62
+ * hallucination into durable state.
63
+ */
64
+ export const MEMORY_PROPOSAL_SCHEMA = {
65
+ type: 'object',
66
+ properties: {
67
+ text: { type: 'string', minLength: 1, description: 'The fact worth carrying, in one sentence.' },
68
+ evidence: { anyOf: [{ type: 'string', minLength: 1 }, CLAIM_EVIDENCE_SCHEMA] },
69
+ tags: { type: 'array', items: { type: 'string', minLength: 1 } },
70
+ },
71
+ required: ['text', 'evidence'],
72
+ additionalProperties: false,
73
+ };
74
+
75
+ /** A proposed skill: when it applies, what to do, which tools it needs. */
76
+ export const SKILL_PROPOSAL_SCHEMA = {
77
+ type: 'object',
78
+ properties: {
79
+ name: { type: 'string', minLength: 1 },
80
+ when: { type: 'string', minLength: 1, description: 'The situation this recipe applies to.' },
81
+ instructions: { type: 'string', minLength: 1, description: 'The steps, concretely.' },
82
+ tools: { type: 'array', items: { type: 'string', minLength: 1 } },
83
+ },
84
+ required: ['name', 'when', 'instructions'],
85
+ additionalProperties: false,
86
+ };
87
+
88
+ /** A proposed progress entry against the active goal. */
89
+ export const PROGRESS_PROPOSAL_SCHEMA = {
90
+ type: 'object',
91
+ properties: {
92
+ note: { type: 'string', minLength: 1, description: 'What was done or established.' },
93
+ evidence: { type: 'string', minLength: 1, description: 'What in the run establishes it.' },
94
+ },
95
+ required: ['note', 'evidence'],
96
+ additionalProperties: false,
97
+ };
98
+
99
+ /**
100
+ * Path-discriminated RFC 6902 operations. Provider decoding uses this same
101
+ * schema in non-strict mode; local validation remains authoritative.
102
+ * @param {{ maxOps?: number }} [options]
103
+ * @returns {any}
104
+ */
105
+ export function refinementPatchSchema(options = {}) {
106
+ const maxOps = options.maxOps ?? DEFAULT_MAX_OPS;
107
+ if (!Number.isSafeInteger(maxOps) || maxOps < 0)
108
+ throw new TypeError('maxOps must be a non-negative safe integer');
109
+ const branch = (op, pattern, value) => ({
110
+ type: 'object',
111
+ properties: { op: { enum: op }, path: { type: 'string', pattern },
112
+ ...(value ? { value } : {}) },
113
+ required: value ? ['op', 'path', 'value'] : ['op', 'path'],
114
+ additionalProperties: false,
115
+ });
116
+ const index = '(0|[1-9][0-9]*)';
117
+ return {
118
+ $id: 'https://jarenjs.github.io/schemas/ai/refinement-patch.json',
119
+ title: 'Refinement patch',
120
+ type: 'array', maxItems: maxOps,
121
+ $defs: { memory: MEMORY_PROPOSAL_SCHEMA, skill: SKILL_PROPOSAL_SCHEMA,
122
+ progress: PROGRESS_PROPOSAL_SCHEMA },
123
+ items: { oneOf: [
124
+ ...[['memories', 'memory'], ['skills', 'skill']].flatMap(([path, kind]) => [
125
+ branch(['add'], `^/${path}/(-|${index})$`, { $ref: `#/$defs/${kind}` }),
126
+ branch(['replace'], `^/${path}/${index}$`, { $ref: `#/$defs/${kind}` }),
127
+ branch(['remove'], `^/${path}/${index}$`, null),
128
+ ]),
129
+ branch(['add'], '^/goal/progress/-$', { $ref: '#/$defs/progress' }),
130
+ ] },
131
+ };
132
+ }
133
+
134
+ /** The full schema used for handwritten and generated proposals alike. */
135
+ export const REFINEMENT_PATCH_SCHEMA = refinementPatchSchema();