@atrib/recall 0.3.1 → 0.5.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 +68 -7
- package/dist/aggregations.d.ts +114 -0
- package/dist/aggregations.js +267 -0
- package/dist/aggregations.js.map +1 -0
- package/dist/graph.d.ts +63 -0
- package/dist/graph.js +217 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +136 -4
- package/dist/index.js +526 -36
- package/dist/index.js.map +1 -1
- package/dist/scoring.d.ts +109 -0
- package/dist/scoring.js +126 -0
- package/dist/scoring.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,27 +2,85 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server for atrib. Lets agents query their own provable past from the local signed-record mirror with per-record signature verification.
|
|
4
4
|
|
|
5
|
-
The consumer-side counterpart to `@atrib/emit`: emit produces signed records, recall reads them back and exposes them to the agent through
|
|
5
|
+
The consumer-side counterpart to `@atrib/emit`: emit produces signed records, recall reads them back and exposes them to the agent through five MCP tools. Each returned record carries a `signature_verified` boolean so a poorly-written agent treats tampered records as such.
|
|
6
6
|
|
|
7
7
|
## Tool surface
|
|
8
8
|
|
|
9
|
+
Five MCP tools cover the cognitive surface of the local mirror.
|
|
10
|
+
|
|
11
|
+
### `recall_my_attribution_history`
|
|
12
|
+
|
|
13
|
+
The base filter-rank-page tool over the local mirror.
|
|
14
|
+
|
|
9
15
|
```typescript
|
|
10
16
|
mcp__atrib-recall__recall_my_attribution_history({
|
|
11
17
|
// All optional
|
|
12
18
|
context_id?: string, // 32-hex. Filter to records signed under this trace.
|
|
13
|
-
event_type?: 'tool_call' | 'transaction'
|
|
14
|
-
|
|
19
|
+
event_type?: 'tool_call' | 'transaction' | 'annotation' | 'revision',
|
|
20
|
+
// Filter to a single event kind. Short-form names are normalized
|
|
21
|
+
// to the URI form.
|
|
22
|
+
content_id?: string, // sha256:... exact match on §1.2.2 content_id.
|
|
23
|
+
tool_name?: string, // §8.2 disclosed tool name; records without disclosure excluded.
|
|
24
|
+
args_hash?: string, // sha256:... §8.3 args_hash exact match.
|
|
15
25
|
limit?: number, // Default 25, max 200.
|
|
16
26
|
offset?: number, // For pagination. Note pagination_caveat in the response.
|
|
17
|
-
compact?: boolean, // Default true
|
|
27
|
+
compact?: boolean, // Default true - omits signature/content_id/chain_root/spec_version
|
|
18
28
|
// fields. Set false for full record bytes (re-verification).
|
|
19
|
-
include_unverified?: boolean, // Default false
|
|
29
|
+
include_unverified?: boolean, // Default false - drops records whose signature didn't verify.
|
|
20
30
|
// Set true ONLY when consuming the verbose mode AND explicitly
|
|
21
31
|
// checking signature_verified per record.
|
|
32
|
+
|
|
33
|
+
// Annotation- and revision-driven filters. Records with no incident
|
|
34
|
+
// annotation are excluded when min_importance or topic_tags is set;
|
|
35
|
+
// records that have a revision pointing at them surface superseded_by
|
|
36
|
+
// by default and are hidden when include_revised=true.
|
|
37
|
+
min_importance?: 'critical' | 'high' | 'medium' | 'low' | 'noise',
|
|
38
|
+
topic_tags?: string[], // OR-match against annotation topic_tags.
|
|
39
|
+
include_revised?: boolean, // True hides records superseded by a D059 revision.
|
|
40
|
+
min_signers?: number, // Distinct-signer threshold; 1 for non-transaction records.
|
|
41
|
+
|
|
42
|
+
// Ranking.
|
|
43
|
+
rank_by?: 'timestamp' | 'relevance' | 'causal_distance',
|
|
44
|
+
// 'timestamp' (default): newest first.
|
|
45
|
+
// 'relevance': Park et al. weighted-sum scoring (recency +
|
|
46
|
+
// annotation-derived importance + BM25 against rank_anchor).
|
|
47
|
+
// 'causal_distance': BFS shortest-path from rank_anchor over
|
|
48
|
+
// the local derived graph (CHAIN_PRECEDES, INFORMED_BY,
|
|
49
|
+
// ANNOTATES, REVISES).
|
|
50
|
+
rank_anchor?: string, // record_hash for causal_distance, free-form query for relevance.
|
|
51
|
+
|
|
52
|
+
// Response shape.
|
|
53
|
+
toc?: boolean, // Default false. True returns the ~40-80-token-per-entry
|
|
54
|
+
// table-of-contents shape (record_hash, tool_name, summary,
|
|
55
|
+
// importance, topic_tags, timestamp, superseded_by) suitable
|
|
56
|
+
// for SessionStart auto-injected scaffolds.
|
|
22
57
|
})
|
|
23
58
|
```
|
|
24
59
|
|
|
25
|
-
Returns
|
|
60
|
+
Returns `{ total, returned, filtered_out_by_verification, record_files, record_file, log_origin, pagination_caveat, records }`. Each record carries `annotations` (when annotation records point at it) and `superseded_by` (when revision records point at it).
|
|
61
|
+
|
|
62
|
+
### Sibling tools
|
|
63
|
+
|
|
64
|
+
- `mcp__atrib-recall__recall_walk({ from_record_hash, edge_types?, depth? })` - walks the local derived graph from `from_record_hash` up to `depth` hops (default 3), returning each reachable record_hash + weighted distance. Edge types: CHAIN_PRECEDES (weight 1), INFORMED_BY (weight 1), ANNOTATES (weight 2), REVISES (weight 2). SESSION_PRECEDES, SESSION_PARALLEL, CONVERGES_ON, CROSS_SESSION, and PROVENANCE_OF are deferred to subsequent releases.
|
|
65
|
+
|
|
66
|
+
- `mcp__atrib-recall__recall_annotations({ record_hash })` - returns the aggregated annotation summary (max_importance, union of topics, latest summary) for the target record. Returns `annotations: null` when no annotation points at the record.
|
|
67
|
+
|
|
68
|
+
- `mcp__atrib-recall__recall_revisions({ record_hash })` - returns the forward revision chain for the target record. Each entry's revises field points at the prior entry; the chain follows the first-by-timestamp revision at each step. Sibling fan-out (parallel revisions of the same target) requires calling `recall_my_attribution_history` with event_type=revision and inspecting `content.revises` manually.
|
|
69
|
+
|
|
70
|
+
- `mcp__atrib-recall__recall_by_content({ query, k? })` - BM25 free-form retrieval over each record's annotation summary + topic_tags, then reranked by Park et al. weighted-sum scoring (recency + importance + relevance). Default k=10, max 50. Records with no annotation contribute no relevance signal (will only surface via the recency + importance fallback). Layer 2 (sqlite-vec sidecar, separate ship) extends with embedding similarity over the same indexed text.
|
|
71
|
+
|
|
72
|
+
### Tunable weights
|
|
73
|
+
|
|
74
|
+
The Park et al. ranking weights and recency time constant are environment-tunable for per-axis sensitivity studies:
|
|
75
|
+
|
|
76
|
+
| Env var | Default | Role |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `ATRIB_RECALL_ALPHA` | 0.3 | Recency component weight |
|
|
79
|
+
| `ATRIB_RECALL_BETA` | 0.3 | Importance component weight |
|
|
80
|
+
| `ATRIB_RECALL_GAMMA` | 0.4 | Relevance (BM25) component weight |
|
|
81
|
+
| `ATRIB_RECALL_TAU_DAYS` | 7 | Exponential-decay time constant for recency |
|
|
82
|
+
|
|
83
|
+
The implementation does not enforce that alpha + beta + gamma sum to 1.0; the operator-facing defaults do.
|
|
26
84
|
|
|
27
85
|
## Trust scope
|
|
28
86
|
|
|
@@ -32,9 +90,12 @@ Signature verification is local-only. A passing `signature_verified` proves the
|
|
|
32
90
|
|
|
33
91
|
| Env var | Required | Purpose |
|
|
34
92
|
|---|---|---|
|
|
35
|
-
| `ATRIB_RECORD_FILE` | optional | Path to
|
|
93
|
+
| `ATRIB_RECORD_FILE` | optional | Path to a single signed-record jsonl mirror to read. When set, overrides directory scanning. Back-compat with pre-0.4.0 callers that pinned a specific producer's mirror. No default. |
|
|
94
|
+
| `ATRIB_MIRROR_DIR` | optional | Directory to scan; recall reads every `*.jsonl` inside. Default: `~/.atrib/records/` (the spec [§5.9](../../atrib-spec.md#59-local-mirror-conventions) well-known per-agent mirror namespace). When unset, this is the path used. |
|
|
36
95
|
| `ATRIB_LOG_ORIGIN` | optional | Origin used in human-readable response messages. Default: `log.atrib.dev` |
|
|
37
96
|
|
|
97
|
+
**Mirror discovery priority** (per spec [§5.9](../../atrib-spec.md#59-local-mirror-conventions)): if `ATRIB_RECORD_FILE` is set, recall reads that single file. Otherwise recall scans `ATRIB_MIRROR_DIR` and merges every `*.jsonl` inside. The directory-scan default unifies recall across producers without recall having to know per-producer naming conventions; any producer that follows the spec convention just shows up.
|
|
98
|
+
|
|
38
99
|
## Installation in an MCP host
|
|
39
100
|
|
|
40
101
|
```jsonc
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { AtribRecord } from '@atrib/mcp';
|
|
2
|
+
import type { ImportanceLabel } from './index.js';
|
|
3
|
+
/**
|
|
4
|
+
* A mirror record paired with its D062 sidecar content (when present) and
|
|
5
|
+
* its content-addressable record_hash (computed at load time). The
|
|
6
|
+
* record_hash form is `sha256:<64-hex>` per spec §2.3 — matches what the
|
|
7
|
+
* log entries commit to and what `informed_by` / `content.annotates` /
|
|
8
|
+
* `content.revises` reference.
|
|
9
|
+
*
|
|
10
|
+
* `content` is the deserialized `_local.content` from a D062 envelope mirror
|
|
11
|
+
* line. Producers writing bare AtribRecord lines (legacy / non-envelope)
|
|
12
|
+
* yield `content: undefined` here; annotation aggregation simply skips
|
|
13
|
+
* those records (the §8.1 posture: no body disclosed). Code that wants to
|
|
14
|
+
* read annotation importance / topics / summary MUST handle the undefined
|
|
15
|
+
* case.
|
|
16
|
+
*/
|
|
17
|
+
export type LoadedRecord = {
|
|
18
|
+
record: AtribRecord;
|
|
19
|
+
record_hash: string;
|
|
20
|
+
content?: unknown;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Compute the spec §2.3 record_hash for an AtribRecord: sha256 over the
|
|
24
|
+
* JCS-canonical serialization including the signature field. Matches the
|
|
25
|
+
* pattern in `packages/openinference/test/informed-by.test.ts`
|
|
26
|
+
* and the in-tree atrib-span-processor build output. Kept local to
|
|
27
|
+
* atrib-recall for now — when a third caller appears, lift to `@atrib/mcp`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function computeRecordHash(record: AtribRecord): string;
|
|
30
|
+
/**
|
|
31
|
+
* Load a single jsonl mirror file as LoadedRecord[]. Each line's
|
|
32
|
+
* record_hash is computed once at load; subsequent lookups are O(1).
|
|
33
|
+
* Malformed lines are silently skipped (same contract as `loadRecords`).
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadLoaded(path: string): LoadedRecord[];
|
|
36
|
+
/**
|
|
37
|
+
* Mirror-discovery variant of `loadLoaded` that scans a directory of
|
|
38
|
+
* `*.jsonl` files. See loadRecordsFromDir in index.ts for the bare
|
|
39
|
+
* AtribRecord equivalent. Files that don't exist or aren't readable are silently
|
|
40
|
+
* skipped (a file rotated mid-scan shouldn't error the whole call).
|
|
41
|
+
* Returns the union of loaded entries plus the list of files scanned.
|
|
42
|
+
*/
|
|
43
|
+
export declare function loadLoadedFromDir(dir: string): {
|
|
44
|
+
loaded: LoadedRecord[];
|
|
45
|
+
files: string[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* LoadedRecord variant of `discoverRecords` in index.ts. Same priority
|
|
49
|
+
* order: explicit `recordFile` arg > ATRIB_RECORD_FILE env > ATRIB_MIRROR_DIR
|
|
50
|
+
* scan. Re-evaluates env vars on each call so test harnesses that mutate
|
|
51
|
+
* process.env per-test get the value they set.
|
|
52
|
+
*/
|
|
53
|
+
export declare function discoverLoaded(recordFile?: string): {
|
|
54
|
+
loaded: LoadedRecord[];
|
|
55
|
+
files: string[];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Per-record annotation summary: max importance across all annotations
|
|
59
|
+
* pointing at the record (or undefined if none), the union of all
|
|
60
|
+
* topic_tags arrays carried by those annotations, and the most-recent
|
|
61
|
+
* summary string. "Most recent" here means the last annotation by
|
|
62
|
+
* timestamp; ties (rare in practice) resolve to the last array index
|
|
63
|
+
* (mirror order).
|
|
64
|
+
*
|
|
65
|
+
* Matches the AnnotationSummary type declared in `index.ts`. Kept as a
|
|
66
|
+
* separate exported type so future callers (e.g. the public
|
|
67
|
+
* recall_annotations handler) can return this shape without depending
|
|
68
|
+
* on the recall server's internal types.
|
|
69
|
+
*/
|
|
70
|
+
export type AnnotationSummary = {
|
|
71
|
+
max_importance?: ImportanceLabel;
|
|
72
|
+
topics?: string[];
|
|
73
|
+
summary?: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Walk loaded records, identify D058 annotation records, and bin them by
|
|
77
|
+
* `content.annotates` target. Returns Map<target_record_hash,
|
|
78
|
+
* AnnotationSummary>. Records with no annotations pointing at them
|
|
79
|
+
* receive no entry (callers should default to undefined).
|
|
80
|
+
*
|
|
81
|
+
* Annotation records WITHOUT a `_local.content` sidecar (legacy bare
|
|
82
|
+
* mirrors that didn't preserve content) are skipped entirely; the §8.1
|
|
83
|
+
* privacy posture means the annotation's importance / topics / summary
|
|
84
|
+
* are not knowable from the public AtribRecord alone.
|
|
85
|
+
*
|
|
86
|
+
* Spec references:
|
|
87
|
+
* - D058: annotation event_type byte 0x05, URI form annotation
|
|
88
|
+
* - §8.3: salted-commitment posture (body lives in _local; log has only content_id)
|
|
89
|
+
* - §1.2.4: event_type URI form (required for annotation records)
|
|
90
|
+
*/
|
|
91
|
+
export declare function aggregateAnnotationsByRecord(loaded: LoadedRecord[]): Map<string, AnnotationSummary>;
|
|
92
|
+
/**
|
|
93
|
+
* Walk loaded records, identify D059 revision records, and bin them by
|
|
94
|
+
* `content.revises` target. Returns Map<target_record_hash,
|
|
95
|
+
* revision_record_hashes[]>. The value array contains the record_hashes
|
|
96
|
+
* of every revision pointing at the target (immediate revisions only;
|
|
97
|
+
* chain traversal is the caller's responsibility — the recall_revisions
|
|
98
|
+
* handler walks the chain by recursing on each revision's own hash).
|
|
99
|
+
*
|
|
100
|
+
* The returned value array is ordered by revision timestamp ascending so
|
|
101
|
+
* the caller sees revisions in the order they were issued. Ties resolve
|
|
102
|
+
* to mirror-iteration order.
|
|
103
|
+
*
|
|
104
|
+
* Revision records WITHOUT a `_local.content` sidecar are skipped per the
|
|
105
|
+
* §8.1 bare-record posture — without content, the `revises` target is
|
|
106
|
+
* unknowable. Revision records WITH content but no `revises` field are
|
|
107
|
+
* also skipped (the revision is malformed).
|
|
108
|
+
*
|
|
109
|
+
* Spec references:
|
|
110
|
+
* - D059: revision event_type byte 0x06, URI form revision
|
|
111
|
+
* - §8.3: salted-commitment posture (body lives in _local; log has only content_id)
|
|
112
|
+
* - §1.2.4: event_type URI form (required for revision records)
|
|
113
|
+
*/
|
|
114
|
+
export declare function aggregateRevisionsByRecord(loaded: LoadedRecord[]): Map<string, string[]>;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Layer 1 aggregation helpers for the recall semantic surface.
|
|
4
|
+
*
|
|
5
|
+
* Annotation records (D058, event_type = EVENT_TYPE_ANNOTATION_URI) carry
|
|
6
|
+
* their importance + topic_tags + summary in `content.*`, with the target
|
|
7
|
+
* record_hash in `content.annotates`. Per the §8.3 privacy posture the
|
|
8
|
+
* public log carries only content_id (kind hash); the actual content body
|
|
9
|
+
* lives in the local mirror's D062 envelope at `_local.content`. To compute
|
|
10
|
+
* "what annotations does record X have?" the recall server must walk the
|
|
11
|
+
* mirror, recover `_local.content` per envelope, and bin annotations by
|
|
12
|
+
* their `content.annotates` target.
|
|
13
|
+
*
|
|
14
|
+
* This module isolates that walk + bin step plus the record_hash helper
|
|
15
|
+
* needed to key the result map. The existing `loadRecords` / `recall` paths
|
|
16
|
+
* in `index.ts` deal in bare AtribRecord[] and stay unchanged; the Layer 1
|
|
17
|
+
* filter / ranking wire-up will compose `loadLoaded` ->
|
|
18
|
+
* `aggregateAnnotationsByRecord` -> filter alongside the existing recall
|
|
19
|
+
* flow.
|
|
20
|
+
*
|
|
21
|
+
* Revision aggregation (D059) is a near-identical shape that will land
|
|
22
|
+
* alongside; the structure here is designed to extend.
|
|
23
|
+
*/
|
|
24
|
+
import { canonicalRecord, sha256, hexEncode, EVENT_TYPE_ANNOTATION_URI, EVENT_TYPE_REVISION_URI, } from '@atrib/mcp';
|
|
25
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { IMPORTANCE_NUMERIC } from './index.js';
|
|
28
|
+
/**
|
|
29
|
+
* Compute the spec §2.3 record_hash for an AtribRecord: sha256 over the
|
|
30
|
+
* JCS-canonical serialization including the signature field. Matches the
|
|
31
|
+
* pattern in `packages/openinference/test/informed-by.test.ts`
|
|
32
|
+
* and the in-tree atrib-span-processor build output. Kept local to
|
|
33
|
+
* atrib-recall for now — when a third caller appears, lift to `@atrib/mcp`.
|
|
34
|
+
*/
|
|
35
|
+
export function computeRecordHash(record) {
|
|
36
|
+
return `sha256:${hexEncode(sha256(canonicalRecord(record)))}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pull the inner AtribRecord and the D062 `_local.content` out of one
|
|
40
|
+
* parsed mirror line. Returns null when the line is neither shape or is
|
|
41
|
+
* missing the required AtribRecord fields. The shape contract mirrors
|
|
42
|
+
* `extractRecord` in `index.ts` exactly; the only addition is the optional
|
|
43
|
+
* `content` field carried back. Kept as a separate function (rather than
|
|
44
|
+
* augmenting `extractRecord`) so the existing recall flow's signature
|
|
45
|
+
* stays AtribRecord[].
|
|
46
|
+
*/
|
|
47
|
+
function extractLoaded(parsed) {
|
|
48
|
+
if (!parsed || typeof parsed !== 'object')
|
|
49
|
+
return null;
|
|
50
|
+
const obj = parsed;
|
|
51
|
+
const envelopeRecord = (typeof obj.record === 'object' && obj.record !== null)
|
|
52
|
+
? obj.record
|
|
53
|
+
: null;
|
|
54
|
+
const candidate = envelopeRecord ?? obj;
|
|
55
|
+
if (typeof candidate.spec_version !== 'string' ||
|
|
56
|
+
typeof candidate.event_type !== 'string' ||
|
|
57
|
+
typeof candidate.context_id !== 'string' ||
|
|
58
|
+
typeof candidate.creator_key !== 'string' ||
|
|
59
|
+
typeof candidate.chain_root !== 'string' ||
|
|
60
|
+
typeof candidate.signature !== 'string') {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const record = candidate;
|
|
64
|
+
if (envelopeRecord !== null) {
|
|
65
|
+
const local = obj._local ?? undefined;
|
|
66
|
+
if (local && 'content' in local) {
|
|
67
|
+
return { record, content: local.content };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { record };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Load a single jsonl mirror file as LoadedRecord[]. Each line's
|
|
74
|
+
* record_hash is computed once at load; subsequent lookups are O(1).
|
|
75
|
+
* Malformed lines are silently skipped (same contract as `loadRecords`).
|
|
76
|
+
*/
|
|
77
|
+
export function loadLoaded(path) {
|
|
78
|
+
if (!existsSync(path))
|
|
79
|
+
return [];
|
|
80
|
+
const out = [];
|
|
81
|
+
const raw = readFileSync(path, 'utf8');
|
|
82
|
+
for (const line of raw.split('\n')) {
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
if (!trimmed)
|
|
85
|
+
continue;
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(trimmed);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const extracted = extractLoaded(parsed);
|
|
94
|
+
if (!extracted)
|
|
95
|
+
continue;
|
|
96
|
+
out.push({
|
|
97
|
+
record: extracted.record,
|
|
98
|
+
record_hash: computeRecordHash(extracted.record),
|
|
99
|
+
content: extracted.content,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Mirror-discovery variant of `loadLoaded` that scans a directory of
|
|
106
|
+
* `*.jsonl` files. See loadRecordsFromDir in index.ts for the bare
|
|
107
|
+
* AtribRecord equivalent. Files that don't exist or aren't readable are silently
|
|
108
|
+
* skipped (a file rotated mid-scan shouldn't error the whole call).
|
|
109
|
+
* Returns the union of loaded entries plus the list of files scanned.
|
|
110
|
+
*/
|
|
111
|
+
export function loadLoadedFromDir(dir) {
|
|
112
|
+
if (!existsSync(dir))
|
|
113
|
+
return { loaded: [], files: [] };
|
|
114
|
+
let entries = [];
|
|
115
|
+
try {
|
|
116
|
+
entries = readdirSync(dir).filter((name) => name.endsWith('.jsonl'));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { loaded: [], files: [] };
|
|
120
|
+
}
|
|
121
|
+
const loaded = [];
|
|
122
|
+
const files = [];
|
|
123
|
+
for (const name of entries) {
|
|
124
|
+
const full = join(dir, name);
|
|
125
|
+
try {
|
|
126
|
+
const stat = statSync(full);
|
|
127
|
+
if (!stat.isFile())
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const partial = loadLoaded(full);
|
|
134
|
+
files.push(full);
|
|
135
|
+
if (partial.length > 0)
|
|
136
|
+
loaded.push(...partial);
|
|
137
|
+
}
|
|
138
|
+
return { loaded, files };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* LoadedRecord variant of `discoverRecords` in index.ts. Same priority
|
|
142
|
+
* order: explicit `recordFile` arg > ATRIB_RECORD_FILE env > ATRIB_MIRROR_DIR
|
|
143
|
+
* scan. Re-evaluates env vars on each call so test harnesses that mutate
|
|
144
|
+
* process.env per-test get the value they set.
|
|
145
|
+
*/
|
|
146
|
+
export function discoverLoaded(recordFile) {
|
|
147
|
+
const envFile = process.env.ATRIB_RECORD_FILE;
|
|
148
|
+
const envDir = process.env.ATRIB_MIRROR_DIR ??
|
|
149
|
+
join(process.env.HOME ?? '', '.atrib', 'records');
|
|
150
|
+
const explicit = recordFile ?? envFile;
|
|
151
|
+
if (explicit) {
|
|
152
|
+
return { loaded: loadLoaded(explicit), files: [explicit] };
|
|
153
|
+
}
|
|
154
|
+
return loadLoadedFromDir(envDir);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Walk loaded records, identify D058 annotation records, and bin them by
|
|
158
|
+
* `content.annotates` target. Returns Map<target_record_hash,
|
|
159
|
+
* AnnotationSummary>. Records with no annotations pointing at them
|
|
160
|
+
* receive no entry (callers should default to undefined).
|
|
161
|
+
*
|
|
162
|
+
* Annotation records WITHOUT a `_local.content` sidecar (legacy bare
|
|
163
|
+
* mirrors that didn't preserve content) are skipped entirely; the §8.1
|
|
164
|
+
* privacy posture means the annotation's importance / topics / summary
|
|
165
|
+
* are not knowable from the public AtribRecord alone.
|
|
166
|
+
*
|
|
167
|
+
* Spec references:
|
|
168
|
+
* - D058: annotation event_type byte 0x05, URI form annotation
|
|
169
|
+
* - §8.3: salted-commitment posture (body lives in _local; log has only content_id)
|
|
170
|
+
* - §1.2.4: event_type URI form (required for annotation records)
|
|
171
|
+
*/
|
|
172
|
+
export function aggregateAnnotationsByRecord(loaded) {
|
|
173
|
+
const bins = new Map();
|
|
174
|
+
for (const lr of loaded) {
|
|
175
|
+
if (lr.record.event_type !== EVENT_TYPE_ANNOTATION_URI)
|
|
176
|
+
continue;
|
|
177
|
+
if (lr.content === undefined || lr.content === null)
|
|
178
|
+
continue;
|
|
179
|
+
if (typeof lr.content !== 'object')
|
|
180
|
+
continue;
|
|
181
|
+
const c = lr.content;
|
|
182
|
+
if (typeof c.annotates !== 'string' || c.annotates.length === 0)
|
|
183
|
+
continue;
|
|
184
|
+
const target = c.annotates;
|
|
185
|
+
const bin = bins.get(target) ?? {
|
|
186
|
+
importances: [],
|
|
187
|
+
topics: new Set(),
|
|
188
|
+
summary_ts: -Infinity,
|
|
189
|
+
};
|
|
190
|
+
if (typeof c.importance === 'string' && c.importance in IMPORTANCE_NUMERIC) {
|
|
191
|
+
bin.importances.push(c.importance);
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(c.topic_tags)) {
|
|
194
|
+
for (const t of c.topic_tags) {
|
|
195
|
+
if (typeof t === 'string' && t.length > 0)
|
|
196
|
+
bin.topics.add(t);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (typeof c.summary === 'string' && lr.record.timestamp >= bin.summary_ts) {
|
|
200
|
+
bin.summary = c.summary;
|
|
201
|
+
bin.summary_ts = lr.record.timestamp;
|
|
202
|
+
}
|
|
203
|
+
bins.set(target, bin);
|
|
204
|
+
}
|
|
205
|
+
const out = new Map();
|
|
206
|
+
for (const [target, bin] of bins) {
|
|
207
|
+
const max_importance = bin.importances.length === 0
|
|
208
|
+
? undefined
|
|
209
|
+
: bin.importances.reduce((a, b) => IMPORTANCE_NUMERIC[a] >= IMPORTANCE_NUMERIC[b] ? a : b);
|
|
210
|
+
const summary = {};
|
|
211
|
+
if (max_importance !== undefined)
|
|
212
|
+
summary.max_importance = max_importance;
|
|
213
|
+
if (bin.topics.size > 0)
|
|
214
|
+
summary.topics = [...bin.topics].sort();
|
|
215
|
+
if (bin.summary !== undefined)
|
|
216
|
+
summary.summary = bin.summary;
|
|
217
|
+
out.set(target, summary);
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Walk loaded records, identify D059 revision records, and bin them by
|
|
223
|
+
* `content.revises` target. Returns Map<target_record_hash,
|
|
224
|
+
* revision_record_hashes[]>. The value array contains the record_hashes
|
|
225
|
+
* of every revision pointing at the target (immediate revisions only;
|
|
226
|
+
* chain traversal is the caller's responsibility — the recall_revisions
|
|
227
|
+
* handler walks the chain by recursing on each revision's own hash).
|
|
228
|
+
*
|
|
229
|
+
* The returned value array is ordered by revision timestamp ascending so
|
|
230
|
+
* the caller sees revisions in the order they were issued. Ties resolve
|
|
231
|
+
* to mirror-iteration order.
|
|
232
|
+
*
|
|
233
|
+
* Revision records WITHOUT a `_local.content` sidecar are skipped per the
|
|
234
|
+
* §8.1 bare-record posture — without content, the `revises` target is
|
|
235
|
+
* unknowable. Revision records WITH content but no `revises` field are
|
|
236
|
+
* also skipped (the revision is malformed).
|
|
237
|
+
*
|
|
238
|
+
* Spec references:
|
|
239
|
+
* - D059: revision event_type byte 0x06, URI form revision
|
|
240
|
+
* - §8.3: salted-commitment posture (body lives in _local; log has only content_id)
|
|
241
|
+
* - §1.2.4: event_type URI form (required for revision records)
|
|
242
|
+
*/
|
|
243
|
+
export function aggregateRevisionsByRecord(loaded) {
|
|
244
|
+
const bins = new Map();
|
|
245
|
+
for (const lr of loaded) {
|
|
246
|
+
if (lr.record.event_type !== EVENT_TYPE_REVISION_URI)
|
|
247
|
+
continue;
|
|
248
|
+
if (lr.content === undefined || lr.content === null)
|
|
249
|
+
continue;
|
|
250
|
+
if (typeof lr.content !== 'object')
|
|
251
|
+
continue;
|
|
252
|
+
const c = lr.content;
|
|
253
|
+
if (typeof c.revises !== 'string' || c.revises.length === 0)
|
|
254
|
+
continue;
|
|
255
|
+
const target = c.revises;
|
|
256
|
+
const list = bins.get(target) ?? [];
|
|
257
|
+
list.push({ hash: lr.record_hash, ts: lr.record.timestamp });
|
|
258
|
+
bins.set(target, list);
|
|
259
|
+
}
|
|
260
|
+
const out = new Map();
|
|
261
|
+
for (const [target, entries] of bins) {
|
|
262
|
+
entries.sort((a, b) => a.ts - b.ts);
|
|
263
|
+
out.set(target, entries.map((e) => e.hash));
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=aggregations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregations.js","sourceRoot":"","sources":["../src/aggregations.ts"],"names":[],"mappings":"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"}
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { LoadedRecord } from './aggregations.js';
|
|
2
|
+
export type EdgeType = 'CHAIN_PRECEDES' | 'INFORMED_BY' | 'ANNOTATES' | 'REVISES';
|
|
3
|
+
/**
|
|
4
|
+
* Layer 1 BFS edge weights. Direct causal links (CHAIN_PRECEDES,
|
|
5
|
+
* INFORMED_BY) are weight 1; annotation/revision relationships are
|
|
6
|
+
* weight 2. The recall_walk handler accepts an edge_types filter that
|
|
7
|
+
* intersects with these four; weights apply only when an edge type
|
|
8
|
+
* passes the filter.
|
|
9
|
+
*/
|
|
10
|
+
export declare const EDGE_WEIGHTS: Record<EdgeType, number>;
|
|
11
|
+
export type GraphEdge = {
|
|
12
|
+
type: EdgeType;
|
|
13
|
+
target: string;
|
|
14
|
+
weight: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Undirected adjacency. For each record_hash, the value is the list of
|
|
18
|
+
* edges out of that record_hash. CHAIN_PRECEDES and INFORMED_BY edges
|
|
19
|
+
* are emitted in both directions (the BFS is for "how close is X to
|
|
20
|
+
* anchor", which is symmetric); ANNOTATES and REVISES likewise.
|
|
21
|
+
*/
|
|
22
|
+
export type LocalGraph = Map<string, GraphEdge[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Build the local Layer 1 graph from loaded records. Returns adjacency
|
|
25
|
+
* map keyed by record_hash. Records present in the loaded set but with
|
|
26
|
+
* no incident edges still appear as map keys with empty arrays — this
|
|
27
|
+
* keeps the BFS path consistent (graph.has(anchor) is still true even
|
|
28
|
+
* when the anchor has no neighbors).
|
|
29
|
+
*
|
|
30
|
+
* Time complexity: O(N) for the chain index pass + O(E) for edge
|
|
31
|
+
* emission. N = loaded.length, E = sum of informed_by entries + chain
|
|
32
|
+
* links + annotation+revision edges.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildLocalGraph(loaded: LoadedRecord[]): LocalGraph;
|
|
35
|
+
/**
|
|
36
|
+
* BFS-shortest-path distances from `start` over the local graph.
|
|
37
|
+
* Returns a map record_hash -> distance (weighted). Records with no path
|
|
38
|
+
* to `start` are omitted (rather than mapped to Infinity) so callers
|
|
39
|
+
* iterate only reachable nodes.
|
|
40
|
+
*
|
|
41
|
+
* The traversal honors `edgeTypes` when provided: only edges whose type
|
|
42
|
+
* is in the set are followed. When edgeTypes is undefined or empty,
|
|
43
|
+
* ALL edge types are followed.
|
|
44
|
+
*
|
|
45
|
+
* `maxDepth` caps the traversal at that hop-count (NOT cumulative
|
|
46
|
+
* weight). Set to Infinity (the default) to traverse the full reachable
|
|
47
|
+
* subgraph. Useful for recall_walk's depth parameter.
|
|
48
|
+
*
|
|
49
|
+
* Since edge weights differ (1 or 2), the traversal uses Dijkstra (with
|
|
50
|
+
* a simple O(V^2) min-extraction since the candidate sets are small at
|
|
51
|
+
* Layer 1). This produces correct shortest paths in weighted graphs.
|
|
52
|
+
*/
|
|
53
|
+
export declare function shortestDistances(graph: LocalGraph, start: string, edgeTypes?: Set<EdgeType>, maxHops?: number): Map<string, number>;
|
|
54
|
+
/**
|
|
55
|
+
* Walk the graph from `start`, returning every reachable record_hash
|
|
56
|
+
* within `maxHops` hops, filtered to the requested edge_types. Result
|
|
57
|
+
* is sorted by ascending distance from start. Used by the recall_walk
|
|
58
|
+
* MCP tool to surface the local causal neighborhood of an anchor.
|
|
59
|
+
*/
|
|
60
|
+
export declare function walkFrom(graph: LocalGraph, start: string, edgeTypes?: Set<EdgeType>, maxHops?: number): Array<{
|
|
61
|
+
record_hash: string;
|
|
62
|
+
distance: number;
|
|
63
|
+
}>;
|