@atrib/trace 0.2.4 → 0.3.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/dist/index.js +18 -1
- package/dist/trace.d.ts +8 -0
- package/dist/trace.js +30 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -18,9 +18,15 @@ import { z } from 'zod';
|
|
|
18
18
|
import { loadAllRecords } from './storage.js';
|
|
19
19
|
import { traceBackward } from './trace.js';
|
|
20
20
|
const SHA256_REF_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
21
|
+
const HEX_32_PATTERN = /^[0-9a-f]{32}$/;
|
|
21
22
|
const TraceInput = z.object({
|
|
22
23
|
record_hash: z.string().regex(SHA256_REF_PATTERN).describe("The 'sha256:<64-hex>' record_hash to start from. Walks backward via " +
|
|
23
24
|
'informed_by edges from this record.'),
|
|
25
|
+
context_id: z.string().regex(HEX_32_PATTERN).optional().describe('Optional 32-hex context_id scope. When set, edges crossing into a ' +
|
|
26
|
+
'different context_id surface as dangling rather than expanding the ' +
|
|
27
|
+
'walk. Defaults to process.env.ATRIB_CONTEXT_ID when valid; omit at ' +
|
|
28
|
+
'the env-var level to walk cross-context. Used by Inspect-style ' +
|
|
29
|
+
'harnesses to keep each arm\'s trace inside its own context per D072.'),
|
|
24
30
|
depth: z.number().int().min(1).max(10).optional().describe('Maximum hop count from the starting record. Defaults to 3. Bounded ' +
|
|
25
31
|
'at 10 to keep responses tractable; deeper chains should be walked ' +
|
|
26
32
|
'in pieces by re-rooting at a returned upstream hash.'),
|
|
@@ -91,8 +97,19 @@ export async function createAtribTraceServer() {
|
|
|
91
97
|
const depth = args.depth ?? 3;
|
|
92
98
|
const maxNodes = args.max_nodes ?? 200;
|
|
93
99
|
const compact = args.compact ?? true;
|
|
100
|
+
// ATRIB_CONTEXT_ID env-var default: when the caller omitted
|
|
101
|
+
// context_id, fall back to the env var if it carries a valid
|
|
102
|
+
// 32-hex value. Lets Inspect-style harnesses scope the trace walk
|
|
103
|
+
// to a per-arm context_id via the MCP env block (per D072's
|
|
104
|
+
// per-arm isolation contract). Explicit args.context_id always wins.
|
|
105
|
+
const envContextId = process.env['ATRIB_CONTEXT_ID'];
|
|
106
|
+
const contextId = args.context_id ??
|
|
107
|
+
(envContextId && HEX_32_PATTERN.test(envContextId) ? envContextId : undefined);
|
|
94
108
|
const { byHash } = loadAllRecords();
|
|
95
|
-
const result = traceBackward(args.record_hash, depth, byHash, {
|
|
109
|
+
const result = traceBackward(args.record_hash, depth, byHash, {
|
|
110
|
+
maxNodes,
|
|
111
|
+
...(contextId ? { contextId } : {}),
|
|
112
|
+
});
|
|
96
113
|
const danglingSet = new Set(result.dangling);
|
|
97
114
|
const payload = compact
|
|
98
115
|
? {
|
package/dist/trace.d.ts
CHANGED
|
@@ -47,6 +47,14 @@ export interface TraceResult {
|
|
|
47
47
|
export interface TraceOptions {
|
|
48
48
|
/** Hard cap on total visited nodes regardless of depth. Defaults to 200. */
|
|
49
49
|
maxNodes?: number;
|
|
50
|
+
/**
|
|
51
|
+
* When set, scope the walk to records that share this context_id. Edges
|
|
52
|
+
* crossing into a different context_id are treated as dangling: the
|
|
53
|
+
* upstream is real but lies outside the requested scope. Used by
|
|
54
|
+
* Inspect-style harnesses passing ATRIB_CONTEXT_ID to keep each arm's
|
|
55
|
+
* trace inside its own context.
|
|
56
|
+
*/
|
|
57
|
+
contextId?: string;
|
|
50
58
|
}
|
|
51
59
|
/**
|
|
52
60
|
* Walk informed_by edges backward from the starting record.
|
package/dist/trace.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export function traceBackward(startHash, depth, index, options = {}) {
|
|
16
16
|
const maxNodes = options.maxNodes ?? 200;
|
|
17
|
+
const contextId = options.contextId;
|
|
17
18
|
const warnings = [];
|
|
18
19
|
const visited = new Map();
|
|
19
20
|
const dangling = new Set();
|
|
@@ -34,6 +35,25 @@ export function traceBackward(startHash, depth, index, options = {}) {
|
|
|
34
35
|
warnings: [`start_hash ${startHash} not in local mirror`],
|
|
35
36
|
};
|
|
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
|
+
}
|
|
37
57
|
const frontier = [{ hash: startHash, depth: 0, parent: null }];
|
|
38
58
|
while (frontier.length > 0) {
|
|
39
59
|
if (visited.size >= maxNodes) {
|
|
@@ -53,6 +73,16 @@ export function traceBackward(startHash, depth, index, options = {}) {
|
|
|
53
73
|
// dangling reference, but we don't visit further.
|
|
54
74
|
continue;
|
|
55
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
|
+
}
|
|
56
86
|
let v = visited.get(current.hash);
|
|
57
87
|
if (!v) {
|
|
58
88
|
const informedByEntries = Array.isArray(idx.record.informed_by)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atrib/trace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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,7 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
13
13
|
"zod": "^3.25.76",
|
|
14
|
-
"@atrib/mcp": "0.6.
|
|
14
|
+
"@atrib/mcp": "0.6.2"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/node": "^22.19.17",
|