@atrib/trace 0.5.20 → 1.0.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/README.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @atrib/trace
2
2
 
3
+ **Legacy home.** The read-verb implementation moved to
4
+ [`@atrib/recall`](../atrib-recall/README.md) per the attest/recall rename
5
+ ([D164](../../DECISIONS.md#d164-attestrecall-verb-rename-and-primitive-surface-collapse)).
6
+ `trace` and `trace_forward` fold into `recall` with `shape: "walk"` and
7
+ `direction: "backward"` or `"forward"`. This package re-exports the same
8
+ surface and forwards the `atrib-trace` binary to `@atrib/recall`'s
9
+ handlers. Results are JSON-identical. The `trace` and `trace_forward`
10
+ tool names stay mounted as permanent aliases during the alias window,
11
+ alongside the new `recall` tool.
12
+
3
13
  MCP server exposing the `trace` tool for atrib's verifiable action layer. It walks a record's `informed_by` chain backward to surface the signed relationship path that led to it.
4
14
 
5
15
  Closes the consumer-side cognitive-loop primitive: recall returns raw records; trace returns the declared-relationship trace, so an agent asking "why did I do X?" can see "X was claimed to be informed by Y, which was claimed to be informed by Z" without manually walking `informed_by` hash-by-hash.
package/dist/index.d.ts CHANGED
@@ -1,79 +1,2 @@
1
- /**
2
- * atrib-trace MCP server: registers the `trace` tool that walks a record's
3
- * informed_by chain backward to surface the reasoning chain that led to it.
4
- *
5
- * Closes the consumer-side cognitive-loop primitive: recall returns raw
6
- * records; trace returns the declared-relationship path, so an agent asking
7
- * "why did I do X?" can see "X claimed Y as input, and Y claimed Z as input"
8
- * without manually walking informed_by hash-by-hash.
9
- *
10
- * Reads only the local mirror (~/.atrib/records/*.jsonl) for v1, same scope
11
- * as recall. v2 will fall back to log.atrib.dev/v1/lookup/<hash> for hashes
12
- * not in the local mirror. Forward-walk (records that reference THIS one)
13
- * is also a v2 concern; v1 is backward-only.
14
- */
15
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
16
- import { type TraceVisited } from './trace.js';
17
- interface CompactVisited {
18
- depth: number;
19
- record_hash: string;
20
- parent_hashes: string[];
21
- source: string | null;
22
- event_type: string | null;
23
- context_id: string | null;
24
- creator_key: string | null;
25
- timestamp: number | null;
26
- /** informed_by entries on this record. Empty if none or dangling. */
27
- next_informed_by: string[];
28
- /** Sub-set of next_informed_by that were resolved (present in mirror). */
29
- next_resolved: string[];
30
- /** Sub-set of next_informed_by that were dangling (not in mirror). */
31
- next_dangling: string[];
32
- /**
33
- * Semantic context from the local sidecar when present. Carries
34
- * tool name + brief content summary so the agent can reason about
35
- * the record's meaning without separately fetching it. Absent on
36
- * legacy bare-record entries that pre-date the sidecar.
37
- */
38
- sidecar_summary?: {
39
- tool_name?: string;
40
- span_kind?: string;
41
- span_name?: string;
42
- model_name?: string;
43
- prompt_version?: string;
44
- topics?: string[];
45
- intent?: string;
46
- rationale?: string;
47
- /**
48
- * First 200 chars of the record's human-readable content, derived
49
- * per event_type from the @atrib/mcp normative shapes (D086):
50
- * observation `what`, annotation `summary`, revision `new_position`.
51
- * For legacy records using non-normative names (`summary` on
52
- * observation), falls back to `summary`.
53
- */
54
- what?: string;
55
- /** Annotation `importance` field when present. */
56
- importance?: string;
57
- producer?: string;
58
- };
59
- /** Signed structural disclosures, surfaced when present on the record. */
60
- informed_by?: string[];
61
- tool_name?: string;
62
- args_hash?: string;
63
- result_hash?: string;
64
- /** D062 local mirror body, included only when include_content=true. */
65
- local_content?: unknown;
66
- local_producer?: string;
67
- }
68
- /** Pull a compact summary from the sidecar's content payload.
69
- * Exported for unit testing of the per-event_type content-shape handling. */
70
- export declare function summarizeSidecar(loadedRecord: {
71
- local?: import('./storage.js').SidecarPayload;
72
- } | undefined): CompactVisited['sidecar_summary'];
73
- export declare function compactVisited(v: TraceVisited, danglingSet: Set<string>, byHash: Map<string, import('./storage.js').IndexedRecord>, includeContent: boolean): CompactVisited;
74
- export declare function extractRecordHashFieldsFromMcpResult(result: unknown): string[];
75
- export interface AtribTraceServer {
76
- mcp: McpServer;
77
- }
78
- export declare function createAtribTraceServer(): Promise<AtribTraceServer>;
79
- export {};
1
+ export { compactVisited, createAtribTraceServer, extractRecordHashFieldsFromMcpResult, registerTraceTools, runTraceWalk, summarizeSidecar, TraceInput, } from '@atrib/recall';
2
+ export type { AtribTraceServer, TraceInputT } from '@atrib/recall';
package/dist/index.js CHANGED
@@ -1,306 +1,8 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- /**
3
- * atrib-trace MCP server: registers the `trace` tool that walks a record's
4
- * informed_by chain backward to surface the reasoning chain that led to it.
5
- *
6
- * Closes the consumer-side cognitive-loop primitive: recall returns raw
7
- * records; trace returns the declared-relationship path, so an agent asking
8
- * "why did I do X?" can see "X claimed Y as input, and Y claimed Z as input"
9
- * without manually walking informed_by hash-by-hash.
10
- *
11
- * Reads only the local mirror (~/.atrib/records/*.jsonl) for v1, same scope
12
- * as recall. v2 will fall back to log.atrib.dev/v1/lookup/<hash> for hashes
13
- * not in the local mirror. Forward-walk (records that reference THIS one)
14
- * is also a v2 concern; v1 is backward-only.
15
- */
16
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
17
- import { z } from 'zod';
18
- import { resolveEnvContextId, logReadPrimitiveCall } from '@atrib/mcp';
19
- import { loadAllRecords } from './storage.js';
20
- import { traceBackward, traceForward } from './trace.js';
21
- const SHA256_REF_PATTERN = /^sha256:[0-9a-f]{64}$/;
22
- const HEX_32_PATTERN = /^[0-9a-f]{32}$/;
23
- const TraceInput = z.object({
24
- record_hash: z
25
- .string()
26
- .regex(SHA256_REF_PATTERN)
27
- .describe("The 'sha256:<64-hex>' record_hash to start from. Walks backward via " +
28
- 'informed_by edges from this record.'),
29
- context_id: z
30
- .string()
31
- .regex(HEX_32_PATTERN)
32
- .optional()
33
- .describe('Optional 32-hex context_id scope. When set, edges crossing into a ' +
34
- 'different context_id surface as dangling rather than expanding the ' +
35
- 'walk. Defaults to process.env.ATRIB_CONTEXT_ID when valid; omit at ' +
36
- 'the env-var level to walk cross-context. Used by Inspect-style ' +
37
- "harnesses to keep each arm's trace inside its own context per D072."),
38
- depth: z
39
- .number()
40
- .int()
41
- .min(0)
42
- .max(10)
43
- .optional()
44
- .describe('Maximum hop count from the starting record. Use 0 to return only ' +
45
- 'the start record. Defaults to 3. Bounded ' +
46
- 'at 10 to keep responses tractable; deeper chains should be walked ' +
47
- 'in pieces by re-rooting at a returned upstream hash.'),
48
- max_nodes: z
49
- .number()
50
- .int()
51
- .min(1)
52
- .max(500)
53
- .optional()
54
- .describe('Hard cap on total visited records. Defaults to 200. Prevents pathological ' +
55
- 'fan-out (a record with hundreds of informed_by entries) from exploding ' +
56
- 'the response.'),
57
- compact: z
58
- .boolean()
59
- .optional()
60
- .describe('When true (the default), per-record output omits signature/content_id/' +
61
- 'spec_version/chain_root to keep the response small. Set false for full ' +
62
- 'record bytes (useful for re-verification).'),
63
- include_content: z
64
- .boolean()
65
- .optional()
66
- .describe('When true and compact=true, include the D062 local mirror body as ' +
67
- 'local_content and local_producer on each visited record. Defaults false ' +
68
- 'so trace stays cheap for ordinary causal walks.'),
69
- });
70
- /** Pull a compact summary from the sidecar's content payload.
71
- * Exported for unit testing of the per-event_type content-shape handling. */
72
- export function summarizeSidecar(loadedRecord) {
73
- if (!loadedRecord?.local)
74
- return undefined;
75
- const sc = loadedRecord.local;
76
- const out = {};
77
- if (sc.toolName)
78
- out.tool_name = sc.toolName;
79
- if (sc.producer)
80
- out.producer = sc.producer;
81
- const c = sc.content;
82
- if (!out.tool_name && c && typeof c['tool_name'] === 'string') {
83
- out.tool_name = c['tool_name'];
84
- }
85
- if (c && typeof c['span_kind'] === 'string')
86
- out.span_kind = c['span_kind'];
87
- if (c && typeof c['span_name'] === 'string')
88
- out.span_name = c['span_name'];
89
- if (c && typeof c['model_name'] === 'string')
90
- out.model_name = c['model_name'];
91
- if (c && typeof c['prompt_version'] === 'string')
92
- out.prompt_version = c['prompt_version'];
93
- if (c && typeof c['intent'] === 'string') {
94
- out.intent = c['intent'].length > 200 ? c['intent'].slice(0, 197) + '...' : c['intent'];
95
- }
96
- if (c && typeof c['rationale'] === 'string') {
97
- out.rationale =
98
- c['rationale'].length > 200 ? c['rationale'].slice(0, 197) + '...' : c['rationale'];
99
- }
100
- if (c && Array.isArray(c['topics'])) {
101
- out.topics = c['topics']
102
- .filter((t) => typeof t === 'string')
103
- .slice(0, 6);
104
- }
105
- // Per-event_type human-readable text. The `what` slot here is generic
106
- // (it's the human-scannable summary regardless of event_type); the
107
- // priority order pulls the normative D086 field for each shape, with
108
- // an explicit final fallback to `summary` for legacy records.
109
- // observation: content.what
110
- // annotation: content.summary
111
- // revision: content.new_position (D086-normative). Surface this
112
- // so trace consumers can read the agent's stance shift
113
- // without a separate recall call
114
- // legacy: content.summary (catches pre-D086 extractor records)
115
- if (c) {
116
- let primary;
117
- if (typeof c['what'] === 'string') {
118
- primary = c['what'];
119
- }
120
- else if (typeof c['new_position'] === 'string') {
121
- primary = c['new_position'];
122
- }
123
- else if (typeof c['summary'] === 'string') {
124
- primary = c['summary'];
125
- }
126
- else if (typeof c['prompt'] === 'string') {
127
- primary = c['prompt'];
128
- }
129
- else if (typeof c['output'] === 'string') {
130
- primary = c['output'];
131
- }
132
- if (primary) {
133
- out.what = primary.length > 200 ? primary.slice(0, 197) + '…' : primary;
134
- }
135
- }
136
- if (c && typeof c['importance'] === 'string')
137
- out.importance = c['importance'];
138
- return Object.keys(out).length === 0 ? undefined : out;
139
- }
140
- export function compactVisited(v, danglingSet, byHash, includeContent) {
141
- const indexed = byHash.get(v.record_hash);
142
- const record = v.record;
143
- const out = {
144
- depth: v.depth,
145
- record_hash: v.record_hash,
146
- parent_hashes: v.parent_hashes,
147
- source: v.source,
148
- event_type: v.record?.event_type ?? null,
149
- context_id: v.record?.context_id ?? null,
150
- creator_key: v.record?.creator_key ?? null,
151
- timestamp: v.record?.timestamp ?? null,
152
- next_informed_by: v.next_informed_by,
153
- next_resolved: v.next_informed_by.filter((h) => !danglingSet.has(h)),
154
- next_dangling: v.next_informed_by.filter((h) => danglingSet.has(h)),
155
- ...(summarizeSidecar(indexed) ? { sidecar_summary: summarizeSidecar(indexed) } : {}),
156
- };
157
- if (record) {
158
- const informedBy = record.informed_by;
159
- const toolName = record.tool_name;
160
- const argsHash = record.args_hash;
161
- const resultHash = record.result_hash;
162
- if (Array.isArray(informedBy) && informedBy.length > 0)
163
- out.informed_by = informedBy;
164
- if (toolName)
165
- out.tool_name = toolName;
166
- if (argsHash)
167
- out.args_hash = argsHash;
168
- if (resultHash)
169
- out.result_hash = resultHash;
170
- }
171
- if (includeContent && indexed?.local?.content !== undefined) {
172
- out.local_content = indexed.local.content;
173
- }
174
- if (includeContent && indexed?.local?.producer !== undefined) {
175
- out.local_producer = indexed.local.producer;
176
- }
177
- return out;
178
- }
179
- export function extractRecordHashFieldsFromMcpResult(result) {
180
- const seen = new Set();
181
- const pattern = /^sha256:[0-9a-f]{64}$/;
182
- const content = result?.content;
183
- const text = Array.isArray(content) &&
184
- typeof content[0]?.text === 'string'
185
- ? content[0].text
186
- : undefined;
187
- let root = result;
188
- if (text) {
189
- try {
190
- root = JSON.parse(text);
191
- }
192
- catch {
193
- root = result;
194
- }
195
- }
196
- walk(root);
197
- return Array.from(seen);
198
- function walk(node) {
199
- if (node === null || node === undefined)
200
- return;
201
- if (Array.isArray(node)) {
202
- for (const item of node)
203
- walk(item);
204
- return;
205
- }
206
- if (typeof node !== 'object')
207
- return;
208
- const obj = node;
209
- if (typeof obj.record_hash === 'string' && pattern.test(obj.record_hash)) {
210
- seen.add(obj.record_hash);
211
- }
212
- for (const value of Object.values(obj))
213
- walk(value);
214
- }
215
- }
216
- export async function createAtribTraceServer() {
217
- const mcp = new McpServer({
218
- name: 'atrib-trace',
219
- version: '0.1.0',
220
- });
221
- // Shared shape: build a compact payload from a TraceResult.
222
- function buildPayload(result, byHash, compact, includeContent) {
223
- const danglingSet = new Set(result.dangling);
224
- return compact
225
- ? {
226
- start_hash: result.start_hash,
227
- direction: result.direction,
228
- depth_requested: result.depth_requested,
229
- depth_reached: result.depth_reached,
230
- visited: result.visited.map((v) => compactVisited(v, danglingSet, byHash, includeContent)),
231
- dangling: result.dangling,
232
- truncated_by_depth: result.truncated_by_depth,
233
- truncated_by_cap: result.truncated_by_cap,
234
- warnings: result.warnings,
235
- }
236
- : { ...result };
237
- }
238
- mcp.registerTool('trace', {
239
- title: 'trace informed_by chain backward',
240
- description: "Walk a record's informed_by chain backward to surface the reasoning " +
241
- 'chain that led to it. Reads from the local signed-record mirror ' +
242
- '(~/.atrib/records/*.jsonl). Returns the records visited, parent links ' +
243
- 'so the caller can reconstruct the tree, and dangling hashes (records ' +
244
- 'referenced via informed_by but not present in the local mirror). ' +
245
- 'Sibling tool `trace_forward` walks the opposite direction (records ' +
246
- 'that cited THIS one via their informed_by).',
247
- inputSchema: TraceInput.shape,
248
- }, async (args) => logReadPrimitiveCall('trace', args, async () => {
249
- const depth = args.depth ?? 3;
250
- const maxNodes = args.max_nodes ?? 200;
251
- const compact = args.compact ?? true;
252
- const includeContent = args.include_content ?? false;
253
- // Env-var context_id default: when the caller omitted context_id,
254
- // fall back to @atrib/mcp's resolveEnvContextId (D078 + D083
255
- // precedence: ATRIB_CONTEXT_ID, then a registered harness env var
256
- // like CLAUDE_CODE_SESSION_ID). Explicit args.context_id always wins.
257
- const contextId = args.context_id ?? resolveEnvContextId();
258
- const { byHash } = loadAllRecords();
259
- const result = traceBackward(args.record_hash, depth, byHash, {
260
- maxNodes,
261
- ...(contextId ? { contextId } : {}),
262
- });
263
- return {
264
- content: [
265
- {
266
- type: 'text',
267
- text: JSON.stringify(buildPayload(result, byHash, compact, includeContent), null, 2),
268
- },
269
- ],
270
- };
271
- }, extractRecordHashFieldsFromMcpResult));
272
- // Sibling tool: forward walk (records that cited THIS one). Mirrors
273
- // the trace input schema + response shape exactly so callers can use
274
- // it interchangeably; only the direction of the walk differs.
275
- mcp.registerTool('trace_forward', {
276
- title: 'trace informed_by chain forward',
277
- description: 'Walk forward from a record_hash, surfacing the records that cited ' +
278
- 'it via their informed_by chain. The dual of `trace` (backward). ' +
279
- 'Answers "I made decision X, what did I do because of it?" Reads from ' +
280
- 'the local signed-record mirror (~/.atrib/records/*.jsonl). Returns ' +
281
- 'the records visited, parent links (which upstream record at the ' +
282
- 'prior level cited this one), and dangling hashes (rare; only if the ' +
283
- 'reverse index references a record missing from the mirror).',
284
- inputSchema: TraceInput.shape,
285
- }, async (args) => logReadPrimitiveCall('trace_forward', args, async () => {
286
- const depth = args.depth ?? 3;
287
- const maxNodes = args.max_nodes ?? 200;
288
- const compact = args.compact ?? true;
289
- const includeContent = args.include_content ?? false;
290
- const contextId = args.context_id ?? resolveEnvContextId();
291
- const { byHash } = loadAllRecords();
292
- const result = traceForward(args.record_hash, depth, byHash, {
293
- maxNodes,
294
- ...(contextId ? { contextId } : {}),
295
- });
296
- return {
297
- content: [
298
- {
299
- type: 'text',
300
- text: JSON.stringify(buildPayload(result, byHash, compact, includeContent), null, 2),
301
- },
302
- ],
303
- };
304
- }, extractRecordHashFieldsFromMcpResult));
305
- return { mcp };
306
- }
2
+ // @atrib/trace is the legacy home of the trace read primitive. The
3
+ // implementation lives in @atrib/recall per the attest/recall rename:
4
+ // trace and trace_forward fold into the `recall` verb as shape='walk'
5
+ // with a direction, and both legacy tool names stay mounted as permanent
6
+ // aliases over the same runner. This package re-exports the surface so
7
+ // existing imports keep working.
8
+ export { compactVisited, createAtribTraceServer, extractRecordHashFieldsFromMcpResult, registerTraceTools, runTraceWalk, summarizeSidecar, TraceInput, } from '@atrib/recall';
package/dist/main.js CHANGED
@@ -1,15 +1,13 @@
1
1
  #!/usr/bin/env node
2
- // SPDX-License-Identifier: Apache-2.0
3
- // atrib-trace standalone binary. Wires the McpServer to a stdio transport
4
- // so it can be launched as a subprocess by an MCP host (Claude Code,
5
- // Claude Desktop, etc.).
2
+ // atrib-trace standalone binary (forwarding shim). Serves the legacy
3
+ // atrib-trace server, which mounts `trace` + `trace_forward` plus the
4
+ // `recall` verb per the alias-window rule W1.
6
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import { createAtribTraceServer } from './index.js';
6
+ import { createAtribTraceServer } from '@atrib/recall';
8
7
  async function main() {
9
8
  const { mcp } = await createAtribTraceServer();
10
9
  const transport = new StdioServerTransport();
11
10
  await mcp.connect(transport);
12
- // Stays alive on the stdio transport until the host closes it.
13
11
  }
14
12
  main().catch((e) => {
15
13
  console.error('atrib-trace: fatal', e instanceof Error ? e.stack ?? e.message : String(e));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@atrib/trace",
3
- "version": "0.5.20",
4
- "description": "MCP server for atrib's verifiable action layer. Walks informed_by chains to show the signed relationship path.",
3
+ "version": "1.0.0",
4
+ "description": "Legacy home of atrib's trace read primitive. Superseded by @atrib/recall (the read verb, shape=walk with a direction); this package re-exports the same surface and forwards the atrib-trace binary.",
5
5
  "author": "atrib <hello@atrib.dev>",
6
6
  "keywords": [
7
7
  "atrib",
@@ -24,8 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@modelcontextprotocol/sdk": "^1.29.0",
27
- "zod": "^3.25.76",
28
- "@atrib/mcp": "0.20.0"
27
+ "@atrib/recall": "1.0.0"
29
28
  },
30
29
  "devDependencies": {
31
30
  "@types/node": "^25.9.3",
package/dist/storage.d.ts DELETED
@@ -1,36 +0,0 @@
1
- import { type AtribRecord, type OnRecordSidecar } from '@atrib/mcp';
2
- export interface IndexedRecord {
3
- record: AtribRecord;
4
- /** sha256:<64-hex> form, matching informed_by entries. */
5
- record_hash: string;
6
- /** Source file the record was read from (for debugging). */
7
- source: string;
8
- /**
9
- * Pre-sign payload context, when the producer wrote it to the local
10
- * mirror as a `_local` sidecar on the envelope. Carries semantic content
11
- * (toolName, args, result, content) the signed AtribRecord COMMITS TO
12
- * via content_id / args_hash / result_hash but does not itself contain.
13
- * Absent on legacy bare-record entries from older mirror writes.
14
- */
15
- local?: SidecarPayload;
16
- }
17
- /**
18
- * Combined sidecar shape, superset of every producer's local payload
19
- * (mcp-wrap writes toolName/args/result; atrib-emit writes content).
20
- */
21
- export interface SidecarPayload extends OnRecordSidecar {
22
- content?: unknown;
23
- producer?: string;
24
- }
25
- /**
26
- * Read every *.jsonl mirror in ATRIB_RECORDS_DIR and return an index keyed
27
- * by sha256:<64-hex> record_hash. Both shapes (bare AtribRecord and emit's
28
- * { record, proof, written_at } envelope) are supported.
29
- *
30
- * Returns the indexed map AND a flat list (newest-first by timestamp) for
31
- * callers that want recall-style enumeration without by-hash lookup.
32
- */
33
- export declare function loadAllRecords(dir?: string): {
34
- byHash: Map<string, IndexedRecord>;
35
- newestFirst: IndexedRecord[];
36
- };
package/dist/storage.js DELETED
@@ -1,92 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- /**
3
- * Local mirror reader for atrib-trace.
4
- *
5
- * Reads either one ATRIB_RECORD_FILE mirror or every *.jsonl mirror under
6
- * ~/.atrib/records/ and normalizes to bare AtribRecord shape regardless of
7
- * producer (wrapper-signed or emit envelope).
8
- *
9
- * Builds an in-memory index by record_hash so trace can walk informed_by
10
- * chains in O(1) per hop. The hash key is sha256:<64-hex> (the same form
11
- * informed_by entries use), so no transformation needed at lookup time.
12
- */
13
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
14
- import { homedir } from 'node:os';
15
- import { basename, join } from 'node:path';
16
- import { canonicalRecord, hexEncode, sha256, withDerivedLocalContent, } from '@atrib/mcp';
17
- const RECORDS_FILE = process.env.ATRIB_RECORD_FILE;
18
- const RECORDS_DIR = process.env.ATRIB_RECORDS_DIR ?? join(homedir(), '.atrib', 'records');
19
- /**
20
- * Read every *.jsonl mirror in ATRIB_RECORDS_DIR and return an index keyed
21
- * by sha256:<64-hex> record_hash. Both shapes (bare AtribRecord and emit's
22
- * { record, proof, written_at } envelope) are supported.
23
- *
24
- * Returns the indexed map AND a flat list (newest-first by timestamp) for
25
- * callers that want recall-style enumeration without by-hash lookup.
26
- */
27
- export function loadAllRecords(dir = RECORDS_DIR) {
28
- const byHash = new Map();
29
- const all = [];
30
- const paths = [];
31
- if (RECORDS_FILE) {
32
- paths.push({ path: RECORDS_FILE, source: basename(RECORDS_FILE) });
33
- }
34
- else {
35
- if (!existsSync(dir))
36
- return { byHash, newestFirst: [] };
37
- try {
38
- for (const fname of readdirSync(dir).filter((f) => f.endsWith('.jsonl'))) {
39
- paths.push({ path: join(dir, fname), source: fname });
40
- }
41
- }
42
- catch {
43
- return { byHash, newestFirst: [] };
44
- }
45
- }
46
- for (const { path, source } of paths) {
47
- try {
48
- const raw = readFileSync(path, 'utf8');
49
- for (const line of raw.split('\n')) {
50
- const trimmed = line.trim();
51
- if (!trimmed)
52
- continue;
53
- try {
54
- const parsed = JSON.parse(trimmed);
55
- // Normalize: envelope shape vs legacy bare record shape.
56
- const isEnvelope = 'record' in parsed &&
57
- parsed.record &&
58
- parsed.record.context_id !== undefined;
59
- const rec = isEnvelope
60
- ? parsed.record
61
- : parsed;
62
- if (!rec.context_id || !rec.signature || !rec.timestamp)
63
- continue;
64
- const hashHex = hexEncode(sha256(canonicalRecord(rec)));
65
- const indexed = {
66
- record: rec,
67
- record_hash: `sha256:${hashHex}`,
68
- source,
69
- };
70
- // Lift the `_local` sidecar onto the indexed record when present.
71
- // Legacy bare-record entries have no sidecar; that's OK, consumers
72
- // tolerate its absence.
73
- if (isEnvelope) {
74
- const sidecar = parsed._local;
75
- if (sidecar)
76
- indexed.local = withDerivedLocalContent(rec.event_type, sidecar);
77
- }
78
- byHash.set(indexed.record_hash, indexed);
79
- all.push(indexed);
80
- }
81
- catch {
82
- // Malformed line; skip.
83
- }
84
- }
85
- }
86
- catch {
87
- // File read failure; skip this file.
88
- }
89
- }
90
- all.sort((a, b) => b.record.timestamp - a.record.timestamp);
91
- return { byHash, newestFirst: all };
92
- }
package/dist/trace.d.ts DELETED
@@ -1,106 +0,0 @@
1
- /**
2
- * Bounded directional walk through informed_by chains.
3
- *
4
- * Two directions:
5
- * - backward (traceBackward): from a record, walk its informed_by chain
6
- * toward causal ancestors. Answers "what informed this?"
7
- * - forward (traceForward): from a record, walk the records that cite it
8
- * via their informed_by. Answers "what was informed by this?" — i.e.
9
- * what followed from this decision/observation.
10
- *
11
- * Each direction breadth-first walks edges until either (a) depth bound
12
- * reached, (b) no more unresolved hashes remain, or (c) safety cap on
13
- * total visited nodes hit.
14
- *
15
- * Each visited record is annotated with:
16
- * - depth , hop count from the starting record (0 = the start)
17
- * - parent_hashes, which records in the previous level referenced this one
18
- *
19
- * Records referenced via informed_by but NOT present in the local mirror
20
- * are surfaced as `dangling` entries: the verifier hasn't seen the upstream
21
- * locally. This is informational, not a failure, the upstream may live on
22
- * the public log but not in the agent's local mirror.
23
- *
24
- * Forward walk requires a full-mirror reverse index (built on each call
25
- * from the same IndexedRecord map; O(N) once, then O(out-degree) per hop).
26
- * For typical mirror sizes (~14K records) this is negligible.
27
- */
28
- import type { IndexedRecord } from './storage.js';
29
- import type { AtribRecord } from '@atrib/mcp';
30
- export type TraceDirection = 'backward' | 'forward';
31
- export interface TraceVisited {
32
- depth: number;
33
- record_hash: string;
34
- parent_hashes: string[];
35
- /** Empty when source mirror lacks this record (dangling). */
36
- record: AtribRecord | null;
37
- /** Source file the record was read from, or null if dangling. */
38
- source: string | null;
39
- /** Sub-set of informed_by entries that are themselves walkable next-hop. */
40
- next_informed_by: string[];
41
- }
42
- export interface TraceResult {
43
- start_hash: string;
44
- direction: TraceDirection;
45
- depth_requested: number;
46
- depth_reached: number;
47
- visited: TraceVisited[];
48
- dangling: string[];
49
- truncated_by_depth: boolean;
50
- truncated_by_cap: boolean;
51
- warnings: string[];
52
- }
53
- export interface TraceOptions {
54
- /** Hard cap on total visited nodes regardless of depth. Defaults to 200. */
55
- maxNodes?: number;
56
- /**
57
- * When set, scope the walk to records that share this context_id. Edges
58
- * crossing into a different context_id are treated as dangling: the
59
- * upstream is real but lies outside the requested scope. Used by
60
- * Inspect-style harnesses passing ATRIB_CONTEXT_ID to keep each arm's
61
- * trace inside its own context.
62
- */
63
- contextId?: string;
64
- }
65
- /**
66
- * Walk informed_by edges backward from the starting record.
67
- *
68
- * Behaviors:
69
- * - Cycle-safe: every record is visited at most once, even if multiple
70
- * parents reference it.
71
- * - Cap-safe: if maxNodes is hit mid-walk, returns partial result with
72
- * truncated_by_cap=true. Callers can re-run with a smaller depth or
73
- * start from a sub-tree if needed.
74
- * - Dangling-aware: informed_by entries pointing at records not in the
75
- * local mirror surface as `dangling`. Those entries do NOT advance the
76
- * walk (we have nothing to walk from).
77
- */
78
- export declare function traceBackward(startHash: string, depth: number, index: Map<string, IndexedRecord>, options?: TraceOptions): TraceResult;
79
- /**
80
- * Build a reverse informed_by index: for each record_hash R, the list of
81
- * record_hashes that cite R in their informed_by field. O(total
82
- * informed_by entries) build; O(1) lookup per parent during walk.
83
- *
84
- * Exported so callers (or tests) can pre-build the index when running
85
- * multiple traceForward calls against the same mirror.
86
- */
87
- export declare function buildReverseInformedByIndex(index: Map<string, IndexedRecord>): Map<string, string[]>;
88
- /**
89
- * Walk informed_by edges FORWARD from the starting record.
90
- *
91
- * Answers: "what records cited THIS record via informed_by?" — i.e. what
92
- * decisions/observations followed from this one in the causal substrate.
93
- * The dual of traceBackward. Useful for "I made decision X, what did I do
94
- * because of it?" lookups.
95
- *
96
- * Same behaviors as traceBackward:
97
- * - Cycle-safe via visited-set
98
- * - Cap-safe via maxNodes
99
- * - context_id-scoped (if specified): edges into a different context_id
100
- * are treated as dangling
101
- * - Dangling: walking forward to a record present in the reverse index
102
- * but missing from the mirror surfaces it as dangling (defensive; in
103
- * practice the same mirror that built the reverse index has the
104
- * record itself)
105
- */
106
- export declare function traceForward(startHash: string, depth: number, index: Map<string, IndexedRecord>, options?: TraceOptions): TraceResult;
package/dist/trace.js DELETED
@@ -1,274 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- /**
3
- * Walk informed_by edges backward from the starting record.
4
- *
5
- * Behaviors:
6
- * - Cycle-safe: every record is visited at most once, even if multiple
7
- * parents reference it.
8
- * - Cap-safe: if maxNodes is hit mid-walk, returns partial result with
9
- * truncated_by_cap=true. Callers can re-run with a smaller depth or
10
- * start from a sub-tree if needed.
11
- * - Dangling-aware: informed_by entries pointing at records not in the
12
- * local mirror surface as `dangling`. Those entries do NOT advance the
13
- * walk (we have nothing to walk from).
14
- */
15
- export function traceBackward(startHash, depth, index, options = {}) {
16
- const maxNodes = options.maxNodes ?? 200;
17
- const contextId = options.contextId;
18
- const warnings = [];
19
- const visited = new Map();
20
- const dangling = new Set();
21
- let truncatedByCap = false;
22
- let truncatedByDepth = false;
23
- let depthReached = 0;
24
- const startIdx = index.get(startHash);
25
- if (!startIdx) {
26
- return {
27
- start_hash: startHash,
28
- direction: 'backward',
29
- depth_requested: depth,
30
- depth_reached: 0,
31
- visited: [],
32
- dangling: [startHash],
33
- truncated_by_depth: false,
34
- truncated_by_cap: false,
35
- warnings: [`start_hash ${startHash} not in local mirror`],
36
- };
37
- }
38
- // context_id scope check: when the caller pinned a context_id, the start
39
- // record itself must already live within it. Reject explicitly rather than
40
- // silently returning an empty walk; the caller likely wants to know they
41
- // pointed trace at the wrong record for the requested scope.
42
- if (contextId && startIdx.record.context_id !== contextId) {
43
- return {
44
- start_hash: startHash,
45
- direction: 'backward',
46
- depth_requested: depth,
47
- depth_reached: 0,
48
- visited: [],
49
- dangling: [],
50
- truncated_by_depth: false,
51
- truncated_by_cap: false,
52
- warnings: [
53
- `start_hash ${startHash} lives in context_id ${startIdx.record.context_id} but trace was scoped to ${contextId} (set ATRIB_CONTEXT_ID to override or omit context_id to walk cross-context)`,
54
- ],
55
- };
56
- }
57
- const frontier = [{ hash: startHash, depth: 0, parent: null }];
58
- while (frontier.length > 0) {
59
- if (visited.size >= maxNodes) {
60
- truncatedByCap = true;
61
- break;
62
- }
63
- const current = frontier.shift();
64
- if (current.depth > depth) {
65
- truncatedByDepth = true;
66
- continue;
67
- }
68
- depthReached = Math.max(depthReached, current.depth);
69
- const idx = index.get(current.hash);
70
- if (!idx) {
71
- dangling.add(current.hash);
72
- // Tag the parent so caller can see which walkable record had a
73
- // dangling reference, but we don't visit further.
74
- continue;
75
- }
76
- // context_id scope filter: an upstream record that lives in a different
77
- // context_id is treated as dangling from this walk's perspective. The
78
- // record itself exists, but it sits outside the requested scope. This
79
- // keeps per-arm trace results clean when ATRIB_CONTEXT_ID is set per
80
- // D072 (cross-arm informed_by edges, if any leaked, do not pollute
81
- // the walk).
82
- if (contextId && idx.record.context_id !== contextId) {
83
- dangling.add(current.hash);
84
- continue;
85
- }
86
- let v = visited.get(current.hash);
87
- if (!v) {
88
- const informedByEntries = Array.isArray(idx.record.informed_by)
89
- ? idx.record.informed_by
90
- : [];
91
- v = {
92
- depth: current.depth,
93
- record_hash: current.hash,
94
- parent_hashes: [],
95
- record: idx.record,
96
- source: idx.source,
97
- next_informed_by: informedByEntries,
98
- };
99
- visited.set(current.hash, v);
100
- // Schedule informed_by entries for visit at depth+1.
101
- if (current.depth < depth) {
102
- for (const upstream of informedByEntries) {
103
- frontier.push({ hash: upstream, depth: current.depth + 1, parent: current.hash });
104
- }
105
- }
106
- else if (informedByEntries.length > 0) {
107
- truncatedByDepth = true;
108
- }
109
- }
110
- if (current.parent && !v.parent_hashes.includes(current.parent)) {
111
- v.parent_hashes.push(current.parent);
112
- }
113
- }
114
- return {
115
- start_hash: startHash,
116
- direction: 'backward',
117
- depth_requested: depth,
118
- depth_reached: depthReached,
119
- visited: Array.from(visited.values()),
120
- dangling: Array.from(dangling),
121
- truncated_by_depth: truncatedByDepth,
122
- truncated_by_cap: truncatedByCap,
123
- warnings,
124
- };
125
- }
126
- /**
127
- * Build a reverse informed_by index: for each record_hash R, the list of
128
- * record_hashes that cite R in their informed_by field. O(total
129
- * informed_by entries) build; O(1) lookup per parent during walk.
130
- *
131
- * Exported so callers (or tests) can pre-build the index when running
132
- * multiple traceForward calls against the same mirror.
133
- */
134
- export function buildReverseInformedByIndex(index) {
135
- const reverse = new Map();
136
- for (const { record, record_hash } of index.values()) {
137
- const ib = Array.isArray(record.informed_by) ? record.informed_by : [];
138
- for (const upstream of ib) {
139
- if (typeof upstream !== 'string')
140
- continue;
141
- const existing = reverse.get(upstream);
142
- if (existing)
143
- existing.push(record_hash);
144
- else
145
- reverse.set(upstream, [record_hash]);
146
- }
147
- }
148
- return reverse;
149
- }
150
- /**
151
- * Walk informed_by edges FORWARD from the starting record.
152
- *
153
- * Answers: "what records cited THIS record via informed_by?" — i.e. what
154
- * decisions/observations followed from this one in the causal substrate.
155
- * The dual of traceBackward. Useful for "I made decision X, what did I do
156
- * because of it?" lookups.
157
- *
158
- * Same behaviors as traceBackward:
159
- * - Cycle-safe via visited-set
160
- * - Cap-safe via maxNodes
161
- * - context_id-scoped (if specified): edges into a different context_id
162
- * are treated as dangling
163
- * - Dangling: walking forward to a record present in the reverse index
164
- * but missing from the mirror surfaces it as dangling (defensive; in
165
- * practice the same mirror that built the reverse index has the
166
- * record itself)
167
- */
168
- export function traceForward(startHash, depth, index, options = {}) {
169
- const maxNodes = options.maxNodes ?? 200;
170
- const contextId = options.contextId;
171
- const warnings = [];
172
- const visited = new Map();
173
- const dangling = new Set();
174
- let truncatedByCap = false;
175
- let truncatedByDepth = false;
176
- let depthReached = 0;
177
- const startIdx = index.get(startHash);
178
- if (!startIdx) {
179
- return {
180
- start_hash: startHash,
181
- direction: 'forward',
182
- depth_requested: depth,
183
- depth_reached: 0,
184
- visited: [],
185
- dangling: [startHash],
186
- truncated_by_depth: false,
187
- truncated_by_cap: false,
188
- warnings: [`start_hash ${startHash} not in local mirror`],
189
- };
190
- }
191
- if (contextId && startIdx.record.context_id !== contextId) {
192
- return {
193
- start_hash: startHash,
194
- direction: 'forward',
195
- depth_requested: depth,
196
- depth_reached: 0,
197
- visited: [],
198
- dangling: [],
199
- truncated_by_depth: false,
200
- truncated_by_cap: false,
201
- warnings: [
202
- `start_hash ${startHash} lives in context_id ${startIdx.record.context_id} but trace was scoped to ${contextId} (set ATRIB_CONTEXT_ID to override or omit context_id to walk cross-context)`,
203
- ],
204
- };
205
- }
206
- // Build the reverse index once per call. For Layer 1 corpus sizes
207
- // (~14K records) the O(N) build is negligible; the saved per-hop O(1)
208
- // child lookup is worth the linear scan.
209
- const reverse = buildReverseInformedByIndex(index);
210
- const frontier = [{ hash: startHash, depth: 0, parent: null }];
211
- while (frontier.length > 0) {
212
- if (visited.size >= maxNodes) {
213
- truncatedByCap = true;
214
- break;
215
- }
216
- const current = frontier.shift();
217
- if (current.depth > depth) {
218
- truncatedByDepth = true;
219
- continue;
220
- }
221
- depthReached = Math.max(depthReached, current.depth);
222
- const idx = index.get(current.hash);
223
- if (!idx) {
224
- // The reverse index pointed at a record not present in the mirror.
225
- // Defensive case (the reverse index was built FROM the same mirror,
226
- // so this branch is unreachable in practice). Treat as dangling.
227
- dangling.add(current.hash);
228
- continue;
229
- }
230
- if (contextId && idx.record.context_id !== contextId) {
231
- dangling.add(current.hash);
232
- continue;
233
- }
234
- let v = visited.get(current.hash);
235
- if (!v) {
236
- const children = reverse.get(current.hash) ?? [];
237
- v = {
238
- depth: current.depth,
239
- record_hash: current.hash,
240
- parent_hashes: [],
241
- record: idx.record,
242
- source: idx.source,
243
- // `next_informed_by` reused here as "edges out of this node in
244
- // the walk direction" — for forward walk that's the children
245
- // (records citing this one), not this record's own informed_by.
246
- // Field name kept for shape-compat with traceBackward.
247
- next_informed_by: children,
248
- };
249
- visited.set(current.hash, v);
250
- if (current.depth < depth) {
251
- for (const child of children) {
252
- frontier.push({ hash: child, depth: current.depth + 1, parent: current.hash });
253
- }
254
- }
255
- else if (children.length > 0) {
256
- truncatedByDepth = true;
257
- }
258
- }
259
- if (current.parent && !v.parent_hashes.includes(current.parent)) {
260
- v.parent_hashes.push(current.parent);
261
- }
262
- }
263
- return {
264
- start_hash: startHash,
265
- direction: 'forward',
266
- depth_requested: depth,
267
- depth_reached: depthReached,
268
- visited: Array.from(visited.values()),
269
- dangling: Array.from(dangling),
270
- truncated_by_depth: truncatedByDepth,
271
- truncated_by_cap: truncatedByCap,
272
- warnings,
273
- };
274
- }