@tangleai/context 0.21.1 → 0.25.0

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/src/recall.d.ts CHANGED
@@ -1,44 +1,70 @@
1
+ /**
2
+ * `recall` — the address scheme compaction writes, and the tool that
3
+ * reads it back.
4
+ *
5
+ * Compaction used to be a one-way door: a dropped tool round left the
6
+ * request as a 60-character excerpt and the rest of it was gone. The
7
+ * excerpt is now a *preview* of something that still exists, and this
8
+ * module owns both halves of that promise — the address a synopsis line
9
+ * carries, and the tool a model calls to follow it. They live together
10
+ * because they are one contract: a name written by `agent.ts` and read
11
+ * by a model has to be produced and parsed in exactly one place, or the
12
+ * two drift and a request ends up naming an address nothing answers.
13
+ *
14
+ * Three decisions:
15
+ *
16
+ * - **The name is content-addressed.** A round's slot name is derived
17
+ * from the round's own bytes, so compacting the same history twice
18
+ * writes the same slot instead of a second copy — idempotence falls
19
+ * out of the naming rather than being bolted on with a counter. The
20
+ * character length rides beside the 32-bit fingerprint. These can
21
+ * collide, so compaction compares exact bytes and refuses a conflicting
22
+ * address before dropping anything from the transcript.
23
+ * - **Recall is a tool, not a mechanism.** Nothing re-expands a dropped
24
+ * round automatically. Automatic re-expansion is a guess about which
25
+ * round matters, and a wrong guess spends the budget it was trying to
26
+ * save; the model already knows what it is looking for. It also keeps
27
+ * the machinery inspectable — a recall appears in `steps` like any
28
+ * other call.
29
+ * - **It never throws.** An unknown address answers `{ error }` with the
30
+ * index address to try instead, exactly as every other tool in this
31
+ * package answers a content-level problem.
32
+ */
33
+ /** The tool's name. A host that registers its own `recall` keeps it. */
34
+ export declare const RECALL_TOOL_NAME = "recall";
1
35
  /**
2
36
  * The slot name for one archived round. Content-addressed: same bytes,
3
37
  * same name, so a re-compaction overwrites rather than duplicates.
4
- * @param {string} text - the round's serialized wire messages
5
- * @returns {string}
38
+ * @param text - the round's serialized wire messages
6
39
  */
7
- export function roundSlotName(text: string): string;
40
+ export declare function roundSlotName(text: string): string;
8
41
  /**
9
42
  * The slot name for the index of one archived set. Content-addressed the
10
43
  * same way, over the listing itself — so a request that dropped the same
11
44
  * rounds names the same index.
12
- * @param {string} text - the index listing
13
- * @returns {string}
45
+ * @param text - the index listing
14
46
  */
15
- export function indexSlotName(text: string): string;
47
+ export declare function indexSlotName(text: string): string;
16
48
  /**
17
49
  * One address, written the one way it is ever written — the call a model
18
50
  * would make to follow it. Everything that names a slot in prose goes
19
51
  * through here, so `slotAddressesIn` has a single syntax to read and a
20
52
  * reader following an address is never following a second convention.
21
- * @param {string} name
22
- * @returns {string}
23
53
  */
24
- export function slotRef(name: string): string;
54
+ export declare function slotRef(name: string): string;
25
55
  /**
26
56
  * The address as it appears in a synopsis line. Short on purpose: it is
27
57
  * paid for out of the same character budget the rounds were cut to fit.
28
- * @param {string} name
29
- * @param {number} size - the archived round's size in characters
30
- * @returns {string}
58
+ * @param size - the archived round's size in characters
31
59
  */
32
- export function slotAddress(name: string, size: number): string;
60
+ export declare function slotAddress(name: string, size: number): string;
33
61
  /**
34
62
  * Every slot address named in a piece of text, in order, without
35
63
  * duplicates. The one reader of the address syntax — a probe, a test or
36
64
  * a host auditing what a request can still reach uses this rather than
37
65
  * writing the pattern a second time.
38
- * @param {string} text
39
- * @returns {string[]}
40
66
  */
41
- export function slotAddressesIn(text: string): string[];
67
+ export declare function slotAddressesIn(text: string): string[];
42
68
  /**
43
69
  * The `recall` tool definition, bound to a ledger.
44
70
  *
@@ -52,15 +78,12 @@ export function slotAddressesIn(text: string): string[];
52
78
  * into the transcript, so a slot bigger than that is cut there, once, by
53
79
  * the same rule as any other oversized result.
54
80
  *
55
- * @param {{ readSlot: (name: string) => Promise<any> }} ledger
56
- * @param {{ index?: string }} [options] - `index` is the address of the
81
+ * @param [options] - `index` is the address of the
57
82
  * listing to point at when a name is unknown; it travels in the
58
83
  * rejection because a model that mistyped an address needs the place
59
84
  * the real ones are written down, not just a refusal.
60
- * @returns {{ name: string, description: string, inputSchema: any,
61
- * execute: (input: any) => Promise<any> }}
62
85
  */
63
- export function createRecallTool(ledger: {
86
+ export declare function createRecallTool(ledger: {
64
87
  readSlot: (name: string) => Promise<any>;
65
88
  }, options?: {
66
89
  index?: string;
@@ -70,5 +93,3 @@ export function createRecallTool(ledger: {
70
93
  inputSchema: any;
71
94
  execute: (input: any) => Promise<any>;
72
95
  };
73
- /** The tool's name. A host that registers its own `recall` keeps it. */
74
- export const RECALL_TOOL_NAME: "recall";
package/src/recall.js CHANGED
@@ -1,4 +1,3 @@
1
- //@ts-check
2
1
  /**
3
2
  * `recall` — the address scheme compaction writes, and the tool that
4
3
  * reads it back.
@@ -8,7 +7,7 @@
8
7
  * excerpt is now a *preview* of something that still exists, and this
9
8
  * module owns both halves of that promise — the address a synopsis line
10
9
  * carries, and the tool a model calls to follow it. They live together
11
- * because they are one contract: a name written by `agent.js` and read
10
+ * because they are one contract: a name written by `agent.ts` and read
12
11
  * by a model has to be produced and parsed in exactly one place, or the
13
12
  * two drift and a request ends up naming an address nothing answers.
14
13
  *
@@ -31,77 +30,60 @@
31
30
  * index address to try instead, exactly as every other tool in this
32
31
  * package answers a content-level problem.
33
32
  */
34
-
35
33
  import { hashContent } from '@jarenjs/core/string';
36
-
37
34
  /** The tool's name. A host that registers its own `recall` keeps it. */
38
35
  export const RECALL_TOOL_NAME = 'recall';
39
-
40
36
  /** Address prefixes: one archived round, and the index that lists them. */
41
37
  const ROUND_PREFIX = 'r-';
42
38
  const INDEX_PREFIX = 'rx-';
43
-
44
39
  /**
45
40
  * The slot name for one archived round. Content-addressed: same bytes,
46
41
  * same name, so a re-compaction overwrites rather than duplicates.
47
- * @param {string} text - the round's serialized wire messages
48
- * @returns {string}
42
+ * @param text - the round's serialized wire messages
49
43
  */
50
44
  export function roundSlotName(text) {
51
- return `${ROUND_PREFIX}${hashContent(text)}-${text.length}`;
45
+ return `${ROUND_PREFIX}${hashContent(text)}-${text.length}`;
52
46
  }
53
-
54
47
  /**
55
48
  * The slot name for the index of one archived set. Content-addressed the
56
49
  * same way, over the listing itself — so a request that dropped the same
57
50
  * rounds names the same index.
58
- * @param {string} text - the index listing
59
- * @returns {string}
51
+ * @param text - the index listing
60
52
  */
61
53
  export function indexSlotName(text) {
62
- return `${INDEX_PREFIX}${hashContent(text)}-${text.length}`;
54
+ return `${INDEX_PREFIX}${hashContent(text)}-${text.length}`;
63
55
  }
64
-
65
56
  /**
66
57
  * One address, written the one way it is ever written — the call a model
67
58
  * would make to follow it. Everything that names a slot in prose goes
68
59
  * through here, so `slotAddressesIn` has a single syntax to read and a
69
60
  * reader following an address is never following a second convention.
70
- * @param {string} name
71
- * @returns {string}
72
61
  */
73
62
  export function slotRef(name) {
74
- return `recall("${name}")`;
63
+ return `recall("${name}")`;
75
64
  }
76
-
77
65
  /**
78
66
  * The address as it appears in a synopsis line. Short on purpose: it is
79
67
  * paid for out of the same character budget the rounds were cut to fit.
80
- * @param {string} name
81
- * @param {number} size - the archived round's size in characters
82
- * @returns {string}
68
+ * @param size - the archived round's size in characters
83
69
  */
84
70
  export function slotAddress(name, size) {
85
- return `[${slotRef(name)} · ${size}B]`;
71
+ return `[${slotRef(name)} · ${size}B]`;
86
72
  }
87
-
88
73
  /**
89
74
  * Every slot address named in a piece of text, in order, without
90
75
  * duplicates. The one reader of the address syntax — a probe, a test or
91
76
  * a host auditing what a request can still reach uses this rather than
92
77
  * writing the pattern a second time.
93
- * @param {string} text
94
- * @returns {string[]}
95
78
  */
96
79
  export function slotAddressesIn(text) {
97
- /** @type {string[]} */
98
- const names = [];
99
- for (const match of String(text ?? '').matchAll(/recall\("([^"\s]+)"\)/g)) {
100
- if (!names.includes(match[1])) names.push(match[1]);
101
- }
102
- return names;
80
+ const names = [];
81
+ for (const match of String(text ?? '').matchAll(/recall\("([^"\s]+)"\)/g)) {
82
+ if (!names.includes(match[1]))
83
+ names.push(match[1]);
84
+ }
85
+ return names;
103
86
  }
104
-
105
87
  /**
106
88
  * The `recall` tool definition, bound to a ledger.
107
89
  *
@@ -115,46 +97,44 @@ export function slotAddressesIn(text) {
115
97
  * into the transcript, so a slot bigger than that is cut there, once, by
116
98
  * the same rule as any other oversized result.
117
99
  *
118
- * @param {{ readSlot: (name: string) => Promise<any> }} ledger
119
- * @param {{ index?: string }} [options] - `index` is the address of the
100
+ * @param [options] - `index` is the address of the
120
101
  * listing to point at when a name is unknown; it travels in the
121
102
  * rejection because a model that mistyped an address needs the place
122
103
  * the real ones are written down, not just a refusal.
123
- * @returns {{ name: string, description: string, inputSchema: any,
124
- * execute: (input: any) => Promise<any> }}
125
104
  */
126
105
  export function createRecallTool(ledger, options = {}) {
127
- return {
128
- name: RECALL_TOOL_NAME,
129
- description: 'Fetch an archived conversation round in full by its address. Earlier rounds'
130
- + ' that did not fit the history budget were archived, not deleted: each synopsis line'
131
- + ' carries its address as recall("name"), and the index address listed in the synopsis'
132
- + ' header returns every address there is.',
133
- inputSchema: {
134
- type: 'object',
135
- properties: {
136
- slot: {
137
- type: 'string',
138
- minLength: 1,
139
- description: 'The address from a synopsis line, e.g. the name inside recall("…").',
106
+ return {
107
+ name: RECALL_TOOL_NAME,
108
+ description: 'Fetch an archived conversation round in full by its address. Earlier rounds'
109
+ + ' that did not fit the history budget were archived, not deleted: each synopsis line'
110
+ + ' carries its address as recall("name"), and the index address listed in the synopsis'
111
+ + ' header returns every address there is.',
112
+ inputSchema: {
113
+ type: 'object',
114
+ properties: {
115
+ slot: {
116
+ type: 'string',
117
+ minLength: 1,
118
+ description: 'The address from a synopsis line, e.g. the name inside recall("…").',
119
+ },
120
+ },
121
+ required: ['slot'],
122
+ additionalProperties: false,
140
123
  },
141
- },
142
- required: ['slot'],
143
- additionalProperties: false,
144
- },
145
- execute: async ({ slot }) => {
146
- const content = await ledger.readSlot(slot);
147
- if (content?.status === 'evicted') return { error: `slot '${slot}' was evicted`, ...content };
148
- if (content === undefined || content === null) {
149
- return {
150
- error: `unknown slot '${slot}'`,
151
- ...(options.index === undefined ? {} : { index: options.index }),
152
- hint: options.index === undefined
153
- ? 'use an address exactly as it appears inside recall("…") in the synopsis'
154
- : `${slotRef(options.index)} lists every address that exists`,
155
- };
156
- }
157
- return { slot, size: String(content).length, content };
158
- },
159
- };
124
+ execute: async ({ slot }) => {
125
+ const content = await ledger.readSlot(slot);
126
+ if (content?.status === 'evicted')
127
+ return { error: `slot '${slot}' was evicted`, ...content };
128
+ if (content === undefined || content === null) {
129
+ return {
130
+ error: `unknown slot '${slot}'`,
131
+ ...(options.index === undefined ? {} : { index: options.index }),
132
+ hint: options.index === undefined
133
+ ? 'use an address exactly as it appears inside recall("…") in the synopsis'
134
+ : `${slotRef(options.index)} lists every address that exists`,
135
+ };
136
+ }
137
+ return { slot, size: String(content).length, content };
138
+ },
139
+ };
160
140
  }
@@ -1,24 +1,24 @@
1
+ /** Serialized UTF-8 bytes, including JSON escaping and punctuation. */
2
+ export declare const jsonBytes: (value: any) => number;
1
3
  /**
2
4
  * Exact whole-map accounting. Braces belong to overhead; each member owns its
3
5
  * leading comma. Archive metadata, contents, reports and tombstones stay visible.
4
- * @param {Record<string, any>} records
5
6
  */
6
- export function ledgerFootprint(records: Record<string, any>): {
7
+ export declare function ledgerFootprint(records: Record<string, any>): {
7
8
  bytes: any;
8
9
  overhead: number;
9
- classes: {};
10
+ classes: Record<string, any>;
10
11
  };
11
12
  /** Lossless checkpoint: repeated note/evidence pairs share a dictionary entry. */
12
- export function checkpointProgress(goal: any): {
13
+ export declare function checkpointProgress(goal: any): {
13
14
  version: number;
14
15
  sources: any[];
15
16
  records: any[];
16
17
  };
17
18
  /** Validate exact source coverage and evidence, including prior checkpoints. */
18
- export function validateCheckpoint(checkpoint: any, goal: any): {
19
+ export declare function validateCheckpoint(checkpoint: any, goal: any): {
19
20
  valid: boolean;
20
21
  errors: any[];
21
22
  };
22
23
  /** Complete goal context. No excerpting or silent clipping of evidence. */
23
- export function goalPrompt(goal: any): string;
24
- export function jsonBytes(value: any): number;
24
+ export declare function goalPrompt(goal: any): string;
package/src/retention.js CHANGED
@@ -1,91 +1,108 @@
1
- //@ts-check
2
1
  import { JarenValidator } from '@jarenjs/validate';
3
- import { GOAL_SCHEMA } from './schemas/ledger.js';
2
+ import { GOAL_SCHEMA } from "./schemas/ledger.js";
4
3
  import { checkOutcome } from '@jarenjs/core/check';
5
4
  const checkpointCheck = new JarenValidator({ collectErrors: true, skipErrors: false, unknownFormats: 'ignore' }).compile(GOAL_SCHEMA.properties.checkpoint);
6
-
7
5
  /** Serialized UTF-8 bytes, including JSON escaping and punctuation. */
8
6
  export const jsonBytes = (value) => new TextEncoder().encode(JSON.stringify(value)).length;
9
-
10
7
  /**
11
8
  * Exact whole-map accounting. Braces belong to overhead; each member owns its
12
9
  * leading comma. Archive metadata, contents, reports and tombstones stay visible.
13
- * @param {Record<string, any>} records
14
10
  */
15
11
  export function ledgerFootprint(records) {
16
- const classes = {};
17
- Object.keys(records).sort().forEach((key, index) => {
18
- let kind = 'other';
19
- if (key.startsWith('ai/snap/')) kind = 'snapshots';
20
- else if (key.startsWith('ai/counters/')) kind = 'counters';
21
- else if (key.startsWith('ai/state/memory/')) kind = 'memories';
22
- else if (key.startsWith('ai/state/skill/')) kind = 'skills';
23
- else if (key.startsWith('ai/state/goal/')) kind = 'goals';
24
- else if (key.startsWith('ai/state/evicted/') || key.startsWith('ai/state/retention/')) kind = 'retention';
25
- else if (key.startsWith('ai/state/slot/')) kind = records[key]?.kind?.startsWith('agent-round') ? 'archives' : 'slots';
26
- else if (key.startsWith('ai/state/slot-content/')) {
27
- const name = key.slice('ai/state/slot-content/'.length);
28
- kind = records[`ai/state/slot/${name}`]?.kind?.startsWith('agent-round') ? 'archives' : 'slots';
29
- }
30
- const entry = classes[kind] ??= { bytes: 0, items: 0 };
31
- entry.bytes += jsonBytes(key) + 1 + jsonBytes(records[key]) + (index ? 1 : 0);
32
- entry.items++;
33
- });
34
- return { bytes: 2 + Object.values(classes).reduce((n, entry) => n + entry.bytes, 0), overhead: 2, classes };
12
+ const classes = {};
13
+ Object.keys(records).sort().forEach((key, index) => {
14
+ let kind = 'other';
15
+ if (key.startsWith('ai/snap/'))
16
+ kind = 'snapshots';
17
+ else if (key.startsWith('ai/counters/'))
18
+ kind = 'counters';
19
+ else if (key.startsWith('ai/state/memory/'))
20
+ kind = 'memories';
21
+ else if (key.startsWith('ai/state/skill/'))
22
+ kind = 'skills';
23
+ else if (key.startsWith('ai/state/goal/'))
24
+ kind = 'goals';
25
+ else if (key.startsWith('ai/state/evicted/') || key.startsWith('ai/state/retention/'))
26
+ kind = 'retention';
27
+ else if (key.startsWith('ai/state/slot/'))
28
+ kind = records[key]?.kind?.startsWith('agent-round') ? 'archives' : 'slots';
29
+ else if (key.startsWith('ai/state/slot-content/')) {
30
+ const name = key.slice('ai/state/slot-content/'.length);
31
+ kind = records[`ai/state/slot/${name}`]?.kind?.startsWith('agent-round') ? 'archives' : 'slots';
32
+ }
33
+ const entry = classes[kind] ??= { bytes: 0, items: 0 };
34
+ entry.bytes += jsonBytes(key) + 1 + jsonBytes(records[key]) + (index ? 1 : 0);
35
+ entry.items++;
36
+ });
37
+ return { bytes: 2 + Object.values(classes).reduce((n, entry) => n + entry.bytes, 0), overhead: 2, classes };
35
38
  }
36
-
37
39
  /** Lossless checkpoint: repeated note/evidence pairs share a dictionary entry. */
38
40
  export function checkpointProgress(goal) {
39
- const records = [], sources = [];
40
- const add = (source, record) => {
41
- if (typeof source?.id !== 'string' || source.id === '' || !record) throw new TypeError('checkpoint needs identified source entries');
42
- let index = records.findIndex((held) => held.note === record.note && held.evidence === record.evidence);
43
- if (index < 0) { index = records.length; records.push({ note: record.note, evidence: record.evidence }); }
44
- sources.push({ id: source.id, at: source.at, record: index });
45
- };
46
- for (const source of goal.checkpoint?.sources ?? []) add(source, goal.checkpoint.records[source.record]);
47
- for (const entry of goal.progress) add(entry, entry);
48
- return { version: 1, sources, records };
41
+ const records = [], sources = [];
42
+ const add = (source, record) => {
43
+ if (typeof source?.id !== 'string' || source.id === '' || !record)
44
+ throw new TypeError('checkpoint needs identified source entries');
45
+ let index = records.findIndex((held) => held.note === record.note && held.evidence === record.evidence);
46
+ if (index < 0) {
47
+ index = records.length;
48
+ records.push({ note: record.note, evidence: record.evidence });
49
+ }
50
+ sources.push({ id: source.id, at: source.at, record: index });
51
+ };
52
+ for (const source of goal.checkpoint?.sources ?? [])
53
+ add(source, goal.checkpoint.records[source.record]);
54
+ for (const entry of goal.progress)
55
+ add(entry, entry);
56
+ return { version: 1, sources, records };
49
57
  }
50
-
51
58
  /** Validate exact source coverage and evidence, including prior checkpoints. */
52
59
  export function validateCheckpoint(checkpoint, goal) {
53
- const shape = checkOutcome(checkpointCheck(checkpoint));
54
- if (!shape.valid) return { valid: false, errors: shape.errors.map((error) => ({ ...error,
55
- code: 'GOAL_CHECKPOINT', docPath: `/checkpoint${error.instancePath ?? ''}` })) };
56
- let expected;
57
- try { expected = checkpointProgress(goal); }
58
- catch { return { valid: false, errors: [{ code: 'GOAL_CHECKPOINT', docPath: '/goal', message: 'invalid checkpoint source goal' }] }; }
59
- const errors = [];
60
- const add = (path, message) => errors.push({ code: 'GOAL_CHECKPOINT', docPath: path, instancePath: path, message });
61
- if (checkpoint?.version !== 1 || !Array.isArray(checkpoint?.sources) || !Array.isArray(checkpoint?.records))
62
- add('/checkpoint', 'expected a versioned checkpoint with sources and records');
63
- else {
64
- const sources = new Map(expected.sources.map((source) => [source.id, source]));
65
- const seen = new Set();
66
- checkpoint.sources.forEach((source, index) => {
67
- const held = sources.get(source?.id), record = checkpoint.records[source?.record];
68
- if (!held || seen.has(source.id) || source.at !== held.at || !Number.isSafeInteger(source.record)
69
- || (record?.note !== expected.records[held.record].note || record?.evidence !== expected.records[held.record].evidence))
70
- add(`/checkpoint/sources/${index}`, 'source is invented, duplicated or changes its note/evidence');
71
- seen.add(source?.id);
72
- });
73
- if (seen.size !== sources.size || [...sources.keys()].some((id) => !seen.has(id)))
74
- add('/checkpoint/sources', 'every retired source id must remain covered');
75
- if (checkpoint.records.some((_, index) => !checkpoint.sources.some((source) => source.record === index)))
76
- add('/checkpoint/records', 'unreferenced checkpoint record');
77
- }
78
- return { valid: errors.length === 0, errors };
60
+ const shape = checkOutcome(checkpointCheck(checkpoint));
61
+ if (!shape.valid)
62
+ return {
63
+ valid: false, errors: shape.errors.map((error) => ({
64
+ ...error,
65
+ code: 'GOAL_CHECKPOINT', docPath: `/checkpoint${error.instancePath ?? ''}`
66
+ }))
67
+ };
68
+ let expected;
69
+ try {
70
+ expected = checkpointProgress(goal);
71
+ }
72
+ catch {
73
+ return { valid: false, errors: [{ code: 'GOAL_CHECKPOINT', docPath: '/goal', message: 'invalid checkpoint source goal' }] };
74
+ }
75
+ const errors = [];
76
+ const add = (path, message) => errors.push({ code: 'GOAL_CHECKPOINT', docPath: path, instancePath: path, message });
77
+ if (checkpoint?.version !== 1 || !Array.isArray(checkpoint?.sources) || !Array.isArray(checkpoint?.records))
78
+ add('/checkpoint', 'expected a versioned checkpoint with sources and records');
79
+ else {
80
+ const sources = new Map(expected.sources.map((source) => [source.id, source]));
81
+ const seen = new Set();
82
+ checkpoint.sources.forEach((source, index) => {
83
+ const held = sources.get(source?.id), record = checkpoint.records[source?.record];
84
+ if (!held || seen.has(source.id) || source.at !== held.at || !Number.isSafeInteger(source.record)
85
+ || (record?.note !== expected.records[held.record].note || record?.evidence !== expected.records[held.record].evidence))
86
+ add(`/checkpoint/sources/${index}`, 'source is invented, duplicated or changes its note/evidence');
87
+ seen.add(source?.id);
88
+ });
89
+ if (seen.size !== sources.size || [...sources.keys()].some((id) => !seen.has(id)))
90
+ add('/checkpoint/sources', 'every retired source id must remain covered');
91
+ if (checkpoint.records.some((_, index) => !checkpoint.sources.some((source) => source.record === index)))
92
+ add('/checkpoint/records', 'unreferenced checkpoint record');
93
+ }
94
+ return { valid: errors.length === 0, errors };
79
95
  }
80
-
81
96
  /** Complete goal context. No excerpting or silent clipping of evidence. */
82
97
  export function goalPrompt(goal) {
83
- const progress = Array.isArray(goal.progress) ? goal.progress : [];
84
- const lines = ['## Your objective (persistent, across sessions)', goal.objective];
85
- if (goal.checkpoint) lines.push('', 'Validated checkpoint — this work is DONE, do not repeat it:', JSON.stringify(goal.checkpoint));
86
- if (progress.length) {
87
- lines.push('', `Progress recorded so far (${progress.length} entr${progress.length === 1 ? 'y' : 'ies'}) — this work is DONE, do not repeat it:`);
88
- for (const entry of progress) lines.push(`- [${entry.at}] ${entry.note} (evidence: ${entry.evidence})`);
89
- }
90
- return lines.join('\n');
98
+ const progress = Array.isArray(goal.progress) ? goal.progress : [];
99
+ const lines = ['## Your objective (persistent, across sessions)', goal.objective];
100
+ if (goal.checkpoint)
101
+ lines.push('', 'Validated checkpoint — this work is DONE, do not repeat it:', JSON.stringify(goal.checkpoint));
102
+ if (progress.length) {
103
+ lines.push('', `Progress recorded so far (${progress.length} entr${progress.length === 1 ? 'y' : 'ies'}) — this work is DONE, do not repeat it:`);
104
+ for (const entry of progress)
105
+ lines.push(`- [${entry.at}] ${entry.note} (evidence: ${entry.evidence})`);
106
+ }
107
+ return lines.join('\n');
91
108
  }
@@ -1,30 +1,31 @@
1
1
  /** An admitted artifact describes identity and location; validation never fetches it. */
2
- export const ARTIFACT_SCHEMA: {
2
+ export declare const ARTIFACT_SCHEMA: {
3
3
  type: "object";
4
4
  properties: Record<string, object>;
5
5
  required: string[];
6
6
  additionalProperties: false;
7
7
  };
8
8
  /** Evidence selects content from one admitted artifact. */
9
- export const EVIDENCE_SCHEMA: {
9
+ export declare const EVIDENCE_SCHEMA: {
10
10
  type: "object";
11
11
  properties: Record<string, object>;
12
12
  required: string[];
13
13
  additionalProperties: false;
14
14
  };
15
15
  /** Status is an explicit author assertion, not an entailment or authority score. */
16
- export const CLAIM_SCHEMA: {
16
+ export declare const CLAIM_SCHEMA: {
17
+ type: "object";
18
+ properties: Record<string, object>;
19
+ required: string[];
20
+ additionalProperties: false;
21
+ };
22
+ /** Versioned referential envelope; visible ids are the evidence admitted to this view. */
23
+ export declare const CLAIM_EVIDENCE_SCHEMA: {
17
24
  type: "object";
18
25
  properties: Record<string, object>;
19
26
  required: string[];
20
27
  additionalProperties: false;
21
28
  };
22
- export namespace CLAIM_EVIDENCE_SCHEMA {
23
- let type: "object";
24
- let properties: Record<string, object>;
25
- let required: string[];
26
- let additionalProperties: false;
27
- }
28
29
  export type ArtifactRecord = {
29
30
  id: string;
30
31
  kind: string;
@@ -42,7 +43,7 @@ export type ClaimRecord = {
42
43
  id: string;
43
44
  text: string;
44
45
  critical: boolean;
45
- status: "supported" | "unresolved";
46
+ status: 'supported' | 'unresolved';
46
47
  evidence: string[];
47
48
  };
48
49
  export type ClaimEvidenceEnvelope = {
@@ -1,32 +1,25 @@
1
- //@ts-check
2
1
  const ID = { type: 'string', minLength: 1 };
3
2
  /**
4
- * @param {Record<string, object>} properties
5
- * @param {string[]} required
6
- * @returns {{ type: 'object', properties: Record<string, object>, required: string[], additionalProperties: false }}
7
3
  */
8
4
  const record = (properties, required) => ({ type: 'object', properties, required, additionalProperties: false });
9
-
10
5
  /** An admitted artifact describes identity and location; validation never fetches it. */
11
- export const ARTIFACT_SCHEMA = record({ id: ID, kind: ID, locator: ID, digest: ID,
12
- metadata: { type: 'object' } }, ['id', 'kind']);
6
+ export const ARTIFACT_SCHEMA = record({
7
+ id: ID, kind: ID, locator: ID, digest: ID,
8
+ metadata: { type: 'object' }
9
+ }, ['id', 'kind']);
13
10
  /** Evidence selects content from one admitted artifact. */
14
11
  export const EVIDENCE_SCHEMA = record({ id: ID, artifact: ID, selector: ID, quote: ID }, ['id', 'artifact']);
15
12
  /** Status is an explicit author assertion, not an entailment or authority score. */
16
- export const CLAIM_SCHEMA = record({ id: ID, text: ID, critical: { type: 'boolean' },
17
- status: { enum: ['supported', 'unresolved'] },
18
- evidence: { type: 'array', items: ID, uniqueItems: true } }, ['id', 'text', 'critical', 'status', 'evidence']);
13
+ export const CLAIM_SCHEMA = record({
14
+ id: ID, text: ID, critical: { type: 'boolean' },
15
+ status: { enum: ['supported', 'unresolved'] },
16
+ evidence: { type: 'array', items: ID, uniqueItems: true }
17
+ }, ['id', 'text', 'critical', 'status', 'evidence']);
19
18
  /** Versioned referential envelope; visible ids are the evidence admitted to this view. */
20
19
  export const CLAIM_EVIDENCE_SCHEMA = {
21
- ...record({ version: { const: 1 }, artifacts: { type: 'array', items: ARTIFACT_SCHEMA },
22
- evidence: { type: 'array', items: EVIDENCE_SCHEMA }, claims: { type: 'array', items: CLAIM_SCHEMA },
23
- visibleEvidence: { type: 'array', items: ID, uniqueItems: true } },
24
- ['version', 'artifacts', 'evidence', 'claims', 'visibleEvidence']),
20
+ ...record({
21
+ version: { const: 1 }, artifacts: { type: 'array', items: ARTIFACT_SCHEMA },
22
+ evidence: { type: 'array', items: EVIDENCE_SCHEMA }, claims: { type: 'array', items: CLAIM_SCHEMA },
23
+ visibleEvidence: { type: 'array', items: ID, uniqueItems: true }
24
+ }, ['version', 'artifacts', 'evidence', 'claims', 'visibleEvidence']),
25
25
  };
26
-
27
- /**
28
- * @typedef {{ id: string, kind: string, locator?: string, digest?: string, metadata?: Record<string, any> }} ArtifactRecord
29
- * @typedef {{ id: string, artifact: string, selector?: string, quote?: string }} EvidenceRecord
30
- * @typedef {{ id: string, text: string, critical: boolean, status: 'supported'|'unresolved', evidence: string[] }} ClaimRecord
31
- * @typedef {{ version: 1, artifacts: ArtifactRecord[], evidence: EvidenceRecord[], claims: ClaimRecord[], visibleEvidence: string[] }} ClaimEvidenceEnvelope
32
- */