@atrib/trace 0.5.0 → 0.5.2
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 +10 -7
- package/dist/index.d.ts +11 -0
- package/dist/index.js +74 -10
- package/dist/storage.js +21 -13
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -11,13 +11,14 @@ Two bidirectional walk tools share the same input + response shape; only the wal
|
|
|
11
11
|
```
|
|
12
12
|
mcp__atrib-trace__trace({ // BACKWARD — what informed this?
|
|
13
13
|
record_hash: "sha256:<64-hex>", // start
|
|
14
|
-
depth?: number, // hop cap (default 3, max 10)
|
|
14
|
+
depth?: number, // hop cap (0=start only, default 3, max 10)
|
|
15
15
|
max_nodes?: number, // safety cap (default 200, max 500)
|
|
16
|
-
compact?: boolean
|
|
16
|
+
compact?: boolean, // omit signature/content_id bytes (default true)
|
|
17
|
+
include_content?: boolean // include D062 local_content (default false)
|
|
17
18
|
})
|
|
18
19
|
|
|
19
20
|
mcp__atrib-trace__trace_forward({ // FORWARD — what was informed by this?
|
|
20
|
-
record_hash, depth?, max_nodes?, compact?
|
|
21
|
+
record_hash, depth?, max_nodes?, compact?, include_content? // same schema
|
|
21
22
|
})
|
|
22
23
|
|
|
23
24
|
→ {
|
|
@@ -28,7 +29,9 @@ mcp__atrib-trace__trace_forward({ // FORWARD — what was informed by this?
|
|
|
28
29
|
depth, record_hash, parent_hashes, source,
|
|
29
30
|
event_type, context_id, creator_key, timestamp,
|
|
30
31
|
next_informed_by, next_resolved, next_dangling,
|
|
31
|
-
|
|
32
|
+
informed_by?, tool_name?, args_hash?, result_hash?,
|
|
33
|
+
sidecar_summary?: { tool_name?, topics?, what?, importance?, producer? },
|
|
34
|
+
local_content?, local_producer? // only when include_content=true
|
|
32
35
|
}
|
|
33
36
|
],
|
|
34
37
|
dangling: string[],
|
|
@@ -43,12 +46,12 @@ mcp__atrib-trace__trace_forward({ // FORWARD — what was informed by this?
|
|
|
43
46
|
|
|
44
47
|
## Reads
|
|
45
48
|
|
|
46
|
-
Every `*.jsonl` mirror under `~/.atrib/records/` (override via `ATRIB_RECORDS_DIR`)
|
|
49
|
+
Every `*.jsonl` mirror under `~/.atrib/records/` (override via `ATRIB_RECORDS_DIR`) or one explicit mirror file via `ATRIB_RECORD_FILE`. Tolerates both producer envelope shapes:
|
|
47
50
|
|
|
48
51
|
- Bare `AtribRecord` per line (legacy / wrapper convention pre-sidecar)
|
|
49
52
|
- `{ record, _local?, written_at }` envelope (current shape)
|
|
50
53
|
|
|
51
|
-
When the envelope carries an optional `_local` sidecar (per the local-mirror sidecar pattern shipped in `@atrib/mcp` v0.2.x), trace surfaces a compact `sidecar_summary` per record: `tool_name`, `topics`, first ~200 chars of `what`/`summary`, and `importance` for annotations. Without the sidecar (legacy entries), the per-record output still includes the cryptographic evidence (event_type, hashes, creator_key, timestamp), just without
|
|
54
|
+
When the envelope carries an optional `_local` sidecar (per the local-mirror sidecar pattern shipped in `@atrib/mcp` v0.2.x), trace surfaces a compact `sidecar_summary` per record: `tool_name`, `topics`, first ~200 chars of `what`/`summary`, and `importance` for annotations. When `include_content=true`, trace also returns the full [D062](../../DECISIONS.md#d062-local-mirror-sidecar--two-tier-private-local--public-canonical-persistence) local mirror body as `local_content` plus `local_producer`; this is intended for harness-mediated causal replay where the agent needs rich expected/actual outcome context. Without the sidecar (legacy entries), the per-record output still includes the cryptographic evidence (event_type, hashes, creator_key, timestamp), just without semantic context.
|
|
52
55
|
|
|
53
56
|
## Behaviors
|
|
54
57
|
|
|
@@ -58,7 +61,7 @@ When the envelope carries an optional `_local` sidecar (per the local-mirror sid
|
|
|
58
61
|
- **Dangling-aware**: `informed_by` entries pointing at records not in the local mirror surface in `dangling` and do NOT advance the walk.
|
|
59
62
|
- **Local-only**: reads only the local mirror. A future ship will fall back to `log.atrib.dev/v1/lookup/<hash>` for hashes not in the local mirror.
|
|
60
63
|
- **Bidirectional**: `trace` walks `informed_by` backward; `trace_forward` walks forward via a reverse index built per call from the same mirror (O(N) build, O(out-degree) per hop).
|
|
61
|
-
- **Instrumented (per [D084](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d084-read-primitive-instrumentation-for-empirical-loop-closure-measurement) Surface 6)**: every call writes a per-invocation jsonl entry to `~/.atrib/state/read-primitives/calls.jsonl` for the unified loop-closure analyzer. Silent-failure per [§5.8](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#58-degradation-contract); instrumentation never blocks the trace result. `ATRIB_READ_PRIMITIVES_LOG` overrides the default path for tests.
|
|
64
|
+
- **Instrumented (per [D084](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d084-read-primitive-instrumentation-for-empirical-loop-closure-measurement) Surface 6)**: every call writes a per-invocation jsonl entry to `~/.atrib/state/read-primitives/calls.jsonl` for the unified loop-closure analyzer. Instrumentation samples only `record_hash` fields, not `args_hash`, `result_hash`, or hashes embedded in `local_content`. Silent-failure per [§5.8](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#58-degradation-contract); instrumentation never blocks the trace result. `ATRIB_READ_PRIMITIVES_LOG` overrides the default path for tests.
|
|
62
65
|
|
|
63
66
|
## Wire-up
|
|
64
67
|
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* is also a v2 concern; v1 is backward-only.
|
|
14
14
|
*/
|
|
15
15
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
|
+
import { type TraceVisited } from './trace.js';
|
|
16
17
|
interface CompactVisited {
|
|
17
18
|
depth: number;
|
|
18
19
|
record_hash: string;
|
|
@@ -49,12 +50,22 @@ interface CompactVisited {
|
|
|
49
50
|
importance?: string;
|
|
50
51
|
producer?: string;
|
|
51
52
|
};
|
|
53
|
+
/** Signed structural disclosures, surfaced when present on the record. */
|
|
54
|
+
informed_by?: string[];
|
|
55
|
+
tool_name?: string;
|
|
56
|
+
args_hash?: string;
|
|
57
|
+
result_hash?: string;
|
|
58
|
+
/** D062 local mirror body, included only when include_content=true. */
|
|
59
|
+
local_content?: unknown;
|
|
60
|
+
local_producer?: string;
|
|
52
61
|
}
|
|
53
62
|
/** Pull a compact summary from the sidecar's content payload.
|
|
54
63
|
* Exported for unit testing of the per-event_type content-shape handling. */
|
|
55
64
|
export declare function summarizeSidecar(loadedRecord: {
|
|
56
65
|
local?: import('./storage.js').SidecarPayload;
|
|
57
66
|
} | undefined): CompactVisited['sidecar_summary'];
|
|
67
|
+
export declare function compactVisited(v: TraceVisited, danglingSet: Set<string>, byHash: Map<string, import('./storage.js').IndexedRecord>, includeContent: boolean): CompactVisited;
|
|
68
|
+
export declare function extractRecordHashFieldsFromMcpResult(result: unknown): string[];
|
|
58
69
|
export interface AtribTraceServer {
|
|
59
70
|
mcp: McpServer;
|
|
60
71
|
}
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
17
|
import { z } from 'zod';
|
|
18
|
-
import { resolveEnvContextId, logReadPrimitiveCall,
|
|
18
|
+
import { resolveEnvContextId, logReadPrimitiveCall, } from '@atrib/mcp';
|
|
19
19
|
import { loadAllRecords } from './storage.js';
|
|
20
20
|
import { traceBackward, traceForward } from './trace.js';
|
|
21
21
|
const SHA256_REF_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
@@ -28,7 +28,8 @@ const TraceInput = z.object({
|
|
|
28
28
|
'walk. Defaults to process.env.ATRIB_CONTEXT_ID when valid; omit at ' +
|
|
29
29
|
'the env-var level to walk cross-context. Used by Inspect-style ' +
|
|
30
30
|
'harnesses to keep each arm\'s trace inside its own context per D072.'),
|
|
31
|
-
depth: z.number().int().min(
|
|
31
|
+
depth: z.number().int().min(0).max(10).optional().describe('Maximum hop count from the starting record. Use 0 to return only ' +
|
|
32
|
+
'the start record. Defaults to 3. Bounded ' +
|
|
32
33
|
'at 10 to keep responses tractable; deeper chains should be walked ' +
|
|
33
34
|
'in pieces by re-rooting at a returned upstream hash.'),
|
|
34
35
|
max_nodes: z.number().int().min(1).max(500).optional().describe('Hard cap on total visited records. Defaults to 200. Prevents pathological ' +
|
|
@@ -37,6 +38,9 @@ const TraceInput = z.object({
|
|
|
37
38
|
compact: z.boolean().optional().describe('When true (the default), per-record output omits signature/content_id/' +
|
|
38
39
|
'spec_version/chain_root to keep the response small. Set false for full ' +
|
|
39
40
|
'record bytes (useful for re-verification).'),
|
|
41
|
+
include_content: z.boolean().optional().describe('When true and compact=true, include the D062 local mirror body as ' +
|
|
42
|
+
'local_content and local_producer on each visited record. Defaults false ' +
|
|
43
|
+
'so trace stays cheap for ordinary causal walks.'),
|
|
40
44
|
});
|
|
41
45
|
/** Pull a compact summary from the sidecar's content payload.
|
|
42
46
|
* Exported for unit testing of the per-event_type content-shape handling. */
|
|
@@ -82,9 +86,10 @@ export function summarizeSidecar(loadedRecord) {
|
|
|
82
86
|
out.importance = c['importance'];
|
|
83
87
|
return Object.keys(out).length === 0 ? undefined : out;
|
|
84
88
|
}
|
|
85
|
-
function compactVisited(v, danglingSet, byHash) {
|
|
89
|
+
export function compactVisited(v, danglingSet, byHash, includeContent) {
|
|
86
90
|
const indexed = byHash.get(v.record_hash);
|
|
87
|
-
|
|
91
|
+
const record = v.record;
|
|
92
|
+
const out = {
|
|
88
93
|
depth: v.depth,
|
|
89
94
|
record_hash: v.record_hash,
|
|
90
95
|
parent_hashes: v.parent_hashes,
|
|
@@ -98,6 +103,63 @@ function compactVisited(v, danglingSet, byHash) {
|
|
|
98
103
|
next_dangling: v.next_informed_by.filter((h) => danglingSet.has(h)),
|
|
99
104
|
...(summarizeSidecar(indexed) ? { sidecar_summary: summarizeSidecar(indexed) } : {}),
|
|
100
105
|
};
|
|
106
|
+
if (record) {
|
|
107
|
+
const informedBy = record.informed_by;
|
|
108
|
+
const toolName = record.tool_name;
|
|
109
|
+
const argsHash = record.args_hash;
|
|
110
|
+
const resultHash = record.result_hash;
|
|
111
|
+
if (Array.isArray(informedBy) && informedBy.length > 0)
|
|
112
|
+
out.informed_by = informedBy;
|
|
113
|
+
if (toolName)
|
|
114
|
+
out.tool_name = toolName;
|
|
115
|
+
if (argsHash)
|
|
116
|
+
out.args_hash = argsHash;
|
|
117
|
+
if (resultHash)
|
|
118
|
+
out.result_hash = resultHash;
|
|
119
|
+
}
|
|
120
|
+
if (includeContent && indexed?.local?.content !== undefined) {
|
|
121
|
+
out.local_content = indexed.local.content;
|
|
122
|
+
}
|
|
123
|
+
if (includeContent && indexed?.local?.producer !== undefined) {
|
|
124
|
+
out.local_producer = indexed.local.producer;
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
export function extractRecordHashFieldsFromMcpResult(result) {
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
const pattern = /^sha256:[0-9a-f]{64}$/;
|
|
131
|
+
const content = result?.content;
|
|
132
|
+
const text = Array.isArray(content) && typeof content[0]?.text === 'string'
|
|
133
|
+
? (content[0].text)
|
|
134
|
+
: undefined;
|
|
135
|
+
let root = result;
|
|
136
|
+
if (text) {
|
|
137
|
+
try {
|
|
138
|
+
root = JSON.parse(text);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
root = result;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
walk(root);
|
|
145
|
+
return Array.from(seen);
|
|
146
|
+
function walk(node) {
|
|
147
|
+
if (node === null || node === undefined)
|
|
148
|
+
return;
|
|
149
|
+
if (Array.isArray(node)) {
|
|
150
|
+
for (const item of node)
|
|
151
|
+
walk(item);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (typeof node !== 'object')
|
|
155
|
+
return;
|
|
156
|
+
const obj = node;
|
|
157
|
+
if (typeof obj.record_hash === 'string' && pattern.test(obj.record_hash)) {
|
|
158
|
+
seen.add(obj.record_hash);
|
|
159
|
+
}
|
|
160
|
+
for (const value of Object.values(obj))
|
|
161
|
+
walk(value);
|
|
162
|
+
}
|
|
101
163
|
}
|
|
102
164
|
export async function createAtribTraceServer() {
|
|
103
165
|
const mcp = new McpServer({
|
|
@@ -105,7 +167,7 @@ export async function createAtribTraceServer() {
|
|
|
105
167
|
version: '0.1.0',
|
|
106
168
|
});
|
|
107
169
|
// Shared shape: build a compact payload from a TraceResult.
|
|
108
|
-
function buildPayload(result, byHash, compact) {
|
|
170
|
+
function buildPayload(result, byHash, compact, includeContent) {
|
|
109
171
|
const danglingSet = new Set(result.dangling);
|
|
110
172
|
return compact
|
|
111
173
|
? {
|
|
@@ -113,7 +175,7 @@ export async function createAtribTraceServer() {
|
|
|
113
175
|
direction: result.direction,
|
|
114
176
|
depth_requested: result.depth_requested,
|
|
115
177
|
depth_reached: result.depth_reached,
|
|
116
|
-
visited: result.visited.map((v) => compactVisited(v, danglingSet, byHash)),
|
|
178
|
+
visited: result.visited.map((v) => compactVisited(v, danglingSet, byHash, includeContent)),
|
|
117
179
|
dangling: result.dangling,
|
|
118
180
|
truncated_by_depth: result.truncated_by_depth,
|
|
119
181
|
truncated_by_cap: result.truncated_by_cap,
|
|
@@ -135,6 +197,7 @@ export async function createAtribTraceServer() {
|
|
|
135
197
|
const depth = args.depth ?? 3;
|
|
136
198
|
const maxNodes = args.max_nodes ?? 200;
|
|
137
199
|
const compact = args.compact ?? true;
|
|
200
|
+
const includeContent = args.include_content ?? false;
|
|
138
201
|
// Env-var context_id default: when the caller omitted context_id,
|
|
139
202
|
// fall back to @atrib/mcp's resolveEnvContextId (D078 + D083
|
|
140
203
|
// precedence: ATRIB_CONTEXT_ID, then a registered harness env var
|
|
@@ -146,9 +209,9 @@ export async function createAtribTraceServer() {
|
|
|
146
209
|
...(contextId ? { contextId } : {}),
|
|
147
210
|
});
|
|
148
211
|
return {
|
|
149
|
-
content: [{ type: 'text', text: JSON.stringify(buildPayload(result, byHash, compact), null, 2) }],
|
|
212
|
+
content: [{ type: 'text', text: JSON.stringify(buildPayload(result, byHash, compact, includeContent), null, 2) }],
|
|
150
213
|
};
|
|
151
|
-
},
|
|
214
|
+
}, extractRecordHashFieldsFromMcpResult));
|
|
152
215
|
// Sibling tool: forward walk (records that cited THIS one). Mirrors
|
|
153
216
|
// the trace input schema + response shape exactly so callers can use
|
|
154
217
|
// it interchangeably; only the direction of the walk differs.
|
|
@@ -166,6 +229,7 @@ export async function createAtribTraceServer() {
|
|
|
166
229
|
const depth = args.depth ?? 3;
|
|
167
230
|
const maxNodes = args.max_nodes ?? 200;
|
|
168
231
|
const compact = args.compact ?? true;
|
|
232
|
+
const includeContent = args.include_content ?? false;
|
|
169
233
|
const contextId = args.context_id ?? resolveEnvContextId();
|
|
170
234
|
const { byHash } = loadAllRecords();
|
|
171
235
|
const result = traceForward(args.record_hash, depth, byHash, {
|
|
@@ -173,8 +237,8 @@ export async function createAtribTraceServer() {
|
|
|
173
237
|
...(contextId ? { contextId } : {}),
|
|
174
238
|
});
|
|
175
239
|
return {
|
|
176
|
-
content: [{ type: 'text', text: JSON.stringify(buildPayload(result, byHash, compact), null, 2) }],
|
|
240
|
+
content: [{ type: 'text', text: JSON.stringify(buildPayload(result, byHash, compact, includeContent), null, 2) }],
|
|
177
241
|
};
|
|
178
|
-
},
|
|
242
|
+
}, extractRecordHashFieldsFromMcpResult));
|
|
179
243
|
return { mcp };
|
|
180
244
|
}
|
package/dist/storage.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Local mirror reader for atrib-trace.
|
|
4
4
|
*
|
|
5
|
-
* Reads every *.jsonl mirror under
|
|
6
|
-
*
|
|
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).
|
|
7
8
|
*
|
|
8
9
|
* Builds an in-memory index by record_hash so trace can walk informed_by
|
|
9
10
|
* chains in O(1) per hop. The hash key is sha256:<64-hex> (the same form
|
|
@@ -11,8 +12,9 @@
|
|
|
11
12
|
*/
|
|
12
13
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
13
14
|
import { homedir } from 'node:os';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
+
import { basename, join } from 'node:path';
|
|
15
16
|
import { canonicalRecord, hexEncode, sha256 } from '@atrib/mcp';
|
|
17
|
+
const RECORDS_FILE = process.env.ATRIB_RECORD_FILE;
|
|
16
18
|
const RECORDS_DIR = process.env.ATRIB_RECORDS_DIR ?? join(homedir(), '.atrib', 'records');
|
|
17
19
|
/**
|
|
18
20
|
* Read every *.jsonl mirror in ATRIB_RECORDS_DIR and return an index keyed
|
|
@@ -25,17 +27,23 @@ const RECORDS_DIR = process.env.ATRIB_RECORDS_DIR ?? join(homedir(), '.atrib', '
|
|
|
25
27
|
export function loadAllRecords(dir = RECORDS_DIR) {
|
|
26
28
|
const byHash = new Map();
|
|
27
29
|
const all = [];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
|
|
30
|
+
const paths = [];
|
|
31
|
+
if (RECORDS_FILE) {
|
|
32
|
+
paths.push({ path: RECORDS_FILE, source: basename(RECORDS_FILE) });
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
}
|
|
36
45
|
}
|
|
37
|
-
for (const
|
|
38
|
-
const path = join(dir, fname);
|
|
46
|
+
for (const { path, source } of paths) {
|
|
39
47
|
try {
|
|
40
48
|
const raw = readFileSync(path, 'utf8');
|
|
41
49
|
for (const line of raw.split('\n')) {
|
|
@@ -57,7 +65,7 @@ export function loadAllRecords(dir = RECORDS_DIR) {
|
|
|
57
65
|
const indexed = {
|
|
58
66
|
record: rec,
|
|
59
67
|
record_hash: `sha256:${hashHex}`,
|
|
60
|
-
source
|
|
68
|
+
source,
|
|
61
69
|
};
|
|
62
70
|
// Lift the `_local` sidecar onto the indexed record when present.
|
|
63
71
|
// Legacy bare-record entries have no sidecar; that's OK, consumers
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atrib/trace",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "MCP server for atrib. Walks informed_by chains backward from a record_hash to surface the reasoning chain that produced it.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
13
13
|
"zod": "^3.25.76",
|
|
14
|
-
"@atrib/mcp": "0.
|
|
14
|
+
"@atrib/mcp": "0.13.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@types/node": "^25.
|
|
18
|
-
"tsx": "^4.22.
|
|
17
|
+
"@types/node": "^25.9.1",
|
|
18
|
+
"tsx": "^4.22.3",
|
|
19
19
|
"typescript": "^6.0.3",
|
|
20
|
-
"vitest": "^4.1.
|
|
20
|
+
"vitest": "^4.1.7"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"dist"
|