@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 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, { maxNodes });
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.2.4",
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.1"
14
+ "@atrib/mcp": "0.6.2"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/node": "^22.19.17",