vericify 1.0.1 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vericify",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local-first run intelligence and operations hub for agent systems.",
@@ -1,4 +1,7 @@
1
+ import { hashText } from "../core/util.js";
1
2
  import { listLocalStatePaths, loadLocalState } from "./local-state.js";
3
+ import { loadPeerAdapterStates } from "./peer-capture.js";
4
+ import { annotateRecords, mergeByKeyWithProvenance } from "./provenance.js";
2
5
  import {
3
6
  attachWorkspaceAdapter,
4
7
  detectWorkspaceAdapters,
@@ -14,9 +17,68 @@ export function listDefaultWorkspacePaths(workspaceRoot) {
14
17
 
15
18
  export function loadWorkspaceState(workspaceRoot) {
16
19
  const state = loadLocalState(workspaceRoot);
20
+ const adapterProfiles = detectWorkspaceAdapters(workspaceRoot, state.adapterAttachments);
21
+ const peer = loadPeerAdapterStates(workspaceRoot, adapterProfiles);
22
+
23
+ const handoffs = mergeByKeyWithProvenance(
24
+ state.handoffs,
25
+ annotateRecords(peer.handoffs, {
26
+ source: "peer",
27
+ sourceKind: "handoff",
28
+ sourcePath: peer.sourceRefs?.[0],
29
+ }),
30
+ (h) => h.handoff_id ?? hashText(JSON.stringify(h))
31
+ );
32
+ const todoNodes = mergeByKeyWithProvenance(
33
+ state.todoNodes,
34
+ annotateRecords(peer.todoNodes, {
35
+ source: "peer",
36
+ sourceKind: "todo_node",
37
+ sourcePath: peer.sourceRefs?.[0],
38
+ }),
39
+ (n) => n.id ?? hashText(JSON.stringify(n))
40
+ );
41
+ const ledgerEntries = mergeByKeyWithProvenance(
42
+ state.ledgerEntries,
43
+ annotateRecords(peer.ledgerEntries, {
44
+ source: "peer",
45
+ sourceKind: "run_ledger",
46
+ sourcePath: peer.sourceRefs?.[0],
47
+ }),
48
+ (e) => e.id ?? hashText(`${e.timestamp_utc}:${e.tool}:${e.message}`)
49
+ );
50
+ const statusEvents = mergeByKeyWithProvenance(
51
+ state.statusEvents,
52
+ annotateRecords(peer.statusEvents, {
53
+ source: "peer",
54
+ sourceKind: "status_event",
55
+ sourcePath: peer.sourceRefs?.[0],
56
+ }),
57
+ (e) => e.event_id ?? hashText(`${e.timestamp}:${e.source_module}:${e.event_type}:${e.status}`)
58
+ ).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
59
+ const processPosts = mergeByKeyWithProvenance(
60
+ state.processPosts,
61
+ annotateRecords(peer.processPosts, {
62
+ source: "peer",
63
+ sourceKind: "process_post",
64
+ sourcePath: peer.sourceRefs?.[0],
65
+ }),
66
+ (p) => p.process_post_id ?? hashText(`${p.timestamp}:${p.agent_id}:${p.summary}`)
67
+ ).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
68
+
17
69
  return {
18
70
  ...state,
19
- adapterProfiles: detectWorkspaceAdapters(workspaceRoot, state.adapterAttachments),
71
+ handoffs,
72
+ todoNodes,
73
+ todoOrder: [...new Set([...state.todoOrder, ...peer.todoOrder])],
74
+ ledgerEntries,
75
+ statusEvents,
76
+ processPosts,
77
+ adapterProfiles,
78
+ sourceRefs: {
79
+ ...state.sourceRefs,
80
+ peer: peer.sourceRefs,
81
+ },
20
82
  };
21
83
  }
22
84
 
@@ -2,18 +2,12 @@ import { hashText, unique } from "../core/util.js";
2
2
  import { readJson, readNdjson } from "../core/fs.js";
3
3
  import { listStorePaths, resolveStoreLayout } from "../store/paths.js";
4
4
  import { readAdapterAttachments } from "../store/adapter-attachments.js";
5
+ import { annotateRecords, mergeByKeyWithProvenance } from "./provenance.js";
5
6
 
6
7
  export function listLocalStatePaths(workspaceRoot) {
7
8
  return listStorePaths(workspaceRoot);
8
9
  }
9
10
 
10
- function mergeByKey(preferredItems, partnerItems, getKey) {
11
- const merged = new Map();
12
- for (const item of partnerItems) merged.set(getKey(item), item);
13
- for (const item of preferredItems) merged.set(getKey(item), item);
14
- return [...merged.values()];
15
- }
16
-
17
11
  function mergeOrderedIds(preferred, partner) {
18
12
  return unique([...preferred, ...partner]);
19
13
  }
@@ -36,29 +30,69 @@ export function loadLocalState(workspaceRoot) {
36
30
  statusEvents: readNdjson(paths.partner.statusEvents),
37
31
  };
38
32
 
39
- const handoffs = mergeByKey(
40
- Object.values(vericifyState.handoffRegistry.handoffs ?? {}),
41
- Object.values(partnerState.handoffRegistry.handoffs ?? {}),
33
+ const handoffs = mergeByKeyWithProvenance(
34
+ annotateRecords(Object.values(vericifyState.handoffRegistry.handoffs ?? {}), {
35
+ source: "native",
36
+ sourceKind: "handoff",
37
+ sourcePath: paths.vericify.handoffRegistry,
38
+ }),
39
+ annotateRecords(Object.values(partnerState.handoffRegistry.handoffs ?? {}), {
40
+ source: "partner",
41
+ sourceKind: "handoff",
42
+ sourcePath: paths.partner.handoffRegistry,
43
+ }),
42
44
  (handoff) => handoff.handoff_id ?? hashText(JSON.stringify(handoff))
43
45
  );
44
- const todoNodes = mergeByKey(
45
- Object.values(vericifyState.todoState.nodes ?? {}),
46
- Object.values(partnerState.todoState.nodes ?? {}),
46
+ const todoNodes = mergeByKeyWithProvenance(
47
+ annotateRecords(Object.values(vericifyState.todoState.nodes ?? {}), {
48
+ source: "native",
49
+ sourceKind: "todo_node",
50
+ sourcePath: paths.vericify.todoState,
51
+ }),
52
+ annotateRecords(Object.values(partnerState.todoState.nodes ?? {}), {
53
+ source: "partner",
54
+ sourceKind: "todo_node",
55
+ sourcePath: paths.partner.todoState,
56
+ }),
47
57
  (node) => node.id ?? hashText(JSON.stringify(node))
48
58
  );
49
- const ledgerEntries = mergeByKey(
50
- Array.isArray(vericifyState.runLedger.entries) ? vericifyState.runLedger.entries : [],
51
- Array.isArray(partnerState.runLedger.entries) ? partnerState.runLedger.entries : [],
59
+ const ledgerEntries = mergeByKeyWithProvenance(
60
+ annotateRecords(Array.isArray(vericifyState.runLedger.entries) ? vericifyState.runLedger.entries : [], {
61
+ source: "native",
62
+ sourceKind: "run_ledger",
63
+ sourcePath: paths.vericify.runLedger,
64
+ }),
65
+ annotateRecords(Array.isArray(partnerState.runLedger.entries) ? partnerState.runLedger.entries : [], {
66
+ source: "partner",
67
+ sourceKind: "run_ledger",
68
+ sourcePath: paths.partner.runLedger,
69
+ }),
52
70
  (entry) => entry.id ?? hashText(`${entry.timestamp_utc}:${entry.tool}:${entry.message}`)
53
71
  );
54
- const statusEvents = mergeByKey(
55
- vericifyState.statusEvents,
56
- partnerState.statusEvents,
72
+ const statusEvents = mergeByKeyWithProvenance(
73
+ annotateRecords(vericifyState.statusEvents, {
74
+ source: "native",
75
+ sourceKind: "status_event",
76
+ sourcePath: paths.vericify.statusEvents,
77
+ }),
78
+ annotateRecords(partnerState.statusEvents, {
79
+ source: "partner",
80
+ sourceKind: "status_event",
81
+ sourcePath: paths.partner.statusEvents,
82
+ }),
57
83
  (event) => event.event_id ?? hashText(`${event.timestamp}:${event.source_module}:${event.event_type}:${event.status}`)
58
84
  ).sort((left, right) => String(left.timestamp).localeCompare(String(right.timestamp)));
59
- const processPosts = mergeByKey(
60
- Array.isArray(vericifyState.processPosts.posts) ? vericifyState.processPosts.posts : [],
61
- Array.isArray(partnerState.processPosts.posts) ? partnerState.processPosts.posts : [],
85
+ const processPosts = mergeByKeyWithProvenance(
86
+ annotateRecords(Array.isArray(vericifyState.processPosts.posts) ? vericifyState.processPosts.posts : [], {
87
+ source: "native",
88
+ sourceKind: "process_post",
89
+ sourcePath: paths.vericify.processPosts,
90
+ }),
91
+ annotateRecords(Array.isArray(partnerState.processPosts.posts) ? partnerState.processPosts.posts : [], {
92
+ source: "partner",
93
+ sourceKind: "process_post",
94
+ sourcePath: paths.partner.processPosts,
95
+ }),
62
96
  (post) => post.process_post_id ?? hashText(`${post.timestamp}:${post.agent_id}:${post.summary}`)
63
97
  ).sort((left, right) => String(left.timestamp).localeCompare(String(right.timestamp)));
64
98
 
@@ -0,0 +1,285 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { readJson, readText } from "../core/fs.js";
4
+ import { hashText } from "../core/util.js";
5
+
6
+ function safeStatMtime(path) {
7
+ try {
8
+ return statSync(path).mtime.toISOString();
9
+ } catch {
10
+ return new Date().toISOString();
11
+ }
12
+ }
13
+
14
+ function peerId(adapterId, suffix) {
15
+ return `peer:${adapterId}:${hashText(suffix).slice(0, 10)}`;
16
+ }
17
+
18
+ function latestMtime(paths) {
19
+ let latest = 0;
20
+ for (const p of paths) {
21
+ try {
22
+ const t = statSync(p).mtime.getTime();
23
+ if (t > latest) latest = t;
24
+ } catch {
25
+ // ignore unreachable paths
26
+ }
27
+ }
28
+ return latest ? new Date(latest).toISOString() : new Date().toISOString();
29
+ }
30
+
31
+ function makeLedgerEntry(adapterId, label, timestamp, detectedPaths) {
32
+ return {
33
+ id: peerId(adapterId, `ledger:${timestamp}:${adapterId}`),
34
+ timestamp_utc: timestamp,
35
+ tool: adapterId,
36
+ category: "peer_observation",
37
+ message: `${label} workspace detected`,
38
+ metadata: {
39
+ adapter_id: adapterId,
40
+ source_adapter: adapterId,
41
+ detected_paths: detectedPaths,
42
+ },
43
+ };
44
+ }
45
+
46
+ function makeContextEvent(adapterId, filePath, content) {
47
+ return {
48
+ event_id: peerId(adapterId, `context:${filePath}`),
49
+ timestamp: safeStatMtime(filePath),
50
+ source_module: adapterId,
51
+ event_type: "CONTEXT_FILE",
52
+ status: "in_progress",
53
+ source_adapter: adapterId,
54
+ payload: {
55
+ summary: content.trim().slice(0, 120).replace(/\n+/g, " ") || `${basename(filePath)} present`,
56
+ file: basename(filePath),
57
+ },
58
+ };
59
+ }
60
+
61
+ function captureContextFile(adapterId, filePath, sourceRefs) {
62
+ if (!existsSync(filePath)) return null;
63
+ sourceRefs.push(filePath);
64
+ return makeContextEvent(adapterId, filePath, readText(filePath, ""));
65
+ }
66
+
67
+ function scanJsonFiles(dir) {
68
+ try {
69
+ return readdirSync(dir)
70
+ .filter((name) => name.endsWith(".json"))
71
+ .map((name) => join(dir, name));
72
+ } catch {
73
+ return [];
74
+ }
75
+ }
76
+
77
+ function passthroughJson(filePath) {
78
+ const empty = { handoffs: [], todoNodes: [], ledgerEntries: [], statusEvents: [], processPosts: [] };
79
+ try {
80
+ const data = readJson(filePath, null);
81
+ if (!data || typeof data !== "object") return empty;
82
+ return {
83
+ handoffs: data.handoffs && typeof data.handoffs === "object"
84
+ ? Object.values(data.handoffs).filter((v) => v?.handoff_id)
85
+ : [],
86
+ todoNodes: data.nodes && typeof data.nodes === "object"
87
+ ? Object.values(data.nodes).filter((v) => v?.id)
88
+ : [],
89
+ ledgerEntries: Array.isArray(data.entries) ? data.entries.filter((v) => v?.id) : [],
90
+ statusEvents: Array.isArray(data.events) ? data.events.filter((v) => v?.event_id) : [],
91
+ processPosts: Array.isArray(data.posts) ? data.posts.filter((v) => v?.process_post_id) : [],
92
+ };
93
+ } catch {
94
+ return empty;
95
+ }
96
+ }
97
+
98
+ function mergePassthrough(out, pt) {
99
+ out.handoffs.push(...pt.handoffs);
100
+ out.todoNodes.push(...pt.todoNodes);
101
+ out.ledgerEntries.push(...pt.ledgerEntries);
102
+ out.statusEvents.push(...pt.statusEvents);
103
+ out.processPosts.push(...pt.processPosts);
104
+ }
105
+
106
+ // ── Per-adapter capture ─────────────────────────────────────────────────────
107
+
108
+ function captureCodex(workspaceRoot, profile, out) {
109
+ const sourceRefs = [];
110
+ const events = [];
111
+ const ev = captureContextFile("codex", resolve(workspaceRoot, "AGENTS.md"), sourceRefs);
112
+ if (ev) events.push(ev);
113
+
114
+ const codexDir = resolve(workspaceRoot, ".codex");
115
+ for (const jf of scanJsonFiles(codexDir)) {
116
+ sourceRefs.push(jf);
117
+ mergePassthrough(out, passthroughJson(jf));
118
+ }
119
+
120
+ if (sourceRefs.length) {
121
+ out.ledgerEntries.push(makeLedgerEntry("codex", profile.label, latestMtime(sourceRefs), sourceRefs));
122
+ out.statusEvents.push(...events);
123
+ out.sourceRefs.push(...sourceRefs);
124
+ }
125
+ }
126
+
127
+ function captureClaudeCode(workspaceRoot, profile, out) {
128
+ const sourceRefs = [];
129
+ const events = [];
130
+ const ev = captureContextFile("claude-code", resolve(workspaceRoot, "CLAUDE.md"), sourceRefs);
131
+ if (ev) events.push(ev);
132
+
133
+ const claudeDir = resolve(workspaceRoot, ".claude");
134
+ const settingsFiles = [join(claudeDir, "settings.json"), join(claudeDir, "settings.local.json")];
135
+ for (const sf of settingsFiles) {
136
+ if (existsSync(sf)) sourceRefs.push(sf);
137
+ }
138
+ for (const jf of scanJsonFiles(claudeDir)) {
139
+ if (!sourceRefs.includes(jf)) sourceRefs.push(jf);
140
+ mergePassthrough(out, passthroughJson(jf));
141
+ }
142
+
143
+ if (sourceRefs.length) {
144
+ out.ledgerEntries.push(makeLedgerEntry("claude-code", profile.label, latestMtime(sourceRefs), sourceRefs));
145
+ out.statusEvents.push(...events);
146
+ out.sourceRefs.push(...sourceRefs);
147
+ }
148
+ }
149
+
150
+ function captureCursor(workspaceRoot, profile, out) {
151
+ const sourceRefs = [];
152
+ const events = [];
153
+ const ev = captureContextFile("cursor", resolve(workspaceRoot, ".cursorrules"), sourceRefs);
154
+ if (ev) events.push(ev);
155
+
156
+ const rulesDir = resolve(workspaceRoot, ".cursor/rules");
157
+ if (existsSync(rulesDir)) {
158
+ try {
159
+ const ruleFiles = readdirSync(rulesDir)
160
+ .filter((name) => /\.(md|txt|mdc)$/.test(name))
161
+ .map((name) => join(rulesDir, name));
162
+ for (const rf of ruleFiles) {
163
+ const rev = captureContextFile("cursor", rf, sourceRefs);
164
+ if (rev) events.push(rev);
165
+ }
166
+ } catch {
167
+ // ignore unreadable rules dir
168
+ }
169
+ }
170
+
171
+ if (sourceRefs.length) {
172
+ out.ledgerEntries.push(makeLedgerEntry("cursor", profile.label, latestMtime(sourceRefs), sourceRefs));
173
+ out.statusEvents.push(...events);
174
+ out.sourceRefs.push(...sourceRefs);
175
+ }
176
+ }
177
+
178
+ function captureVscodeCopilot(workspaceRoot, profile, out) {
179
+ const sourceRefs = [];
180
+ const events = [];
181
+ const ev = captureContextFile(
182
+ "vscode-copilot-chat",
183
+ resolve(workspaceRoot, ".github/copilot-instructions.md"),
184
+ sourceRefs
185
+ );
186
+ if (ev) events.push(ev);
187
+
188
+ const settings = resolve(workspaceRoot, ".vscode/settings.json");
189
+ if (existsSync(settings)) sourceRefs.push(settings);
190
+
191
+ if (sourceRefs.length) {
192
+ out.ledgerEntries.push(
193
+ makeLedgerEntry("vscode-copilot-chat", profile.label, latestMtime(sourceRefs), sourceRefs)
194
+ );
195
+ out.statusEvents.push(...events);
196
+ out.sourceRefs.push(...sourceRefs);
197
+ }
198
+ }
199
+
200
+ function captureAntigravity(workspaceRoot, profile, out) {
201
+ const sourceRefs = [];
202
+ const events = [];
203
+ const configJson = resolve(workspaceRoot, "antigravity.config.json");
204
+ if (existsSync(configJson)) {
205
+ sourceRefs.push(configJson);
206
+ mergePassthrough(out, passthroughJson(configJson));
207
+ }
208
+
209
+ const antigravityDir = resolve(workspaceRoot, ".antigravity");
210
+ for (const jf of scanJsonFiles(antigravityDir)) {
211
+ if (!sourceRefs.includes(jf)) sourceRefs.push(jf);
212
+ mergePassthrough(out, passthroughJson(jf));
213
+ }
214
+
215
+ if (sourceRefs.length) {
216
+ out.ledgerEntries.push(
217
+ makeLedgerEntry("antigravity", profile.label, latestMtime(sourceRefs), sourceRefs)
218
+ );
219
+ out.statusEvents.push(...events);
220
+ out.sourceRefs.push(...sourceRefs);
221
+ }
222
+ }
223
+
224
+ const CAPTURE_FNS = {
225
+ codex: captureCodex,
226
+ "claude-code": captureClaudeCode,
227
+ cursor: captureCursor,
228
+ "vscode-copilot-chat": captureVscodeCopilot,
229
+ antigravity: captureAntigravity,
230
+ };
231
+
232
+ // ── Public API ──────────────────────────────────────────────────────────────
233
+
234
+ export function capturePeerAdapterState(workspaceRoot, profile) {
235
+ const out = {
236
+ handoffs: [],
237
+ todoNodes: [],
238
+ todoOrder: [],
239
+ ledgerEntries: [],
240
+ statusEvents: [],
241
+ processPosts: [],
242
+ sourceRefs: [],
243
+ };
244
+
245
+ if (profile.detection_status !== "attached" && profile.detection_status !== "detected") {
246
+ return out;
247
+ }
248
+
249
+ const fn = CAPTURE_FNS[profile.adapter_id];
250
+ if (!fn) return out;
251
+
252
+ try {
253
+ fn(workspaceRoot, profile, out);
254
+ } catch {
255
+ // peer capture never throws
256
+ }
257
+
258
+ return out;
259
+ }
260
+
261
+ export function loadPeerAdapterStates(workspaceRoot, adapterProfiles) {
262
+ const merged = {
263
+ handoffs: [],
264
+ todoNodes: [],
265
+ todoOrder: [],
266
+ ledgerEntries: [],
267
+ statusEvents: [],
268
+ processPosts: [],
269
+ sourceRefs: [],
270
+ };
271
+
272
+ for (const profile of adapterProfiles) {
273
+ if (profile.category !== "peer") continue;
274
+ const captured = capturePeerAdapterState(workspaceRoot, profile);
275
+ merged.handoffs.push(...captured.handoffs);
276
+ merged.todoNodes.push(...captured.todoNodes);
277
+ merged.todoOrder.push(...captured.todoOrder);
278
+ merged.ledgerEntries.push(...captured.ledgerEntries);
279
+ merged.statusEvents.push(...captured.statusEvents);
280
+ merged.processPosts.push(...captured.processPosts);
281
+ merged.sourceRefs.push(...captured.sourceRefs);
282
+ }
283
+
284
+ return merged;
285
+ }
@@ -0,0 +1,70 @@
1
+ function sourceSignature(source) {
2
+ return `${source.source ?? "-"}|${source.source_kind ?? "-"}|${source.source_path ?? "-"}`;
3
+ }
4
+
5
+ function uniqueRecordSources(values) {
6
+ const seen = new Set();
7
+ const merged = [];
8
+ for (const value of values) {
9
+ if (!value) continue;
10
+ const normalized = {
11
+ source: value.source,
12
+ source_kind: value.source_kind,
13
+ source_path: value.source_path,
14
+ };
15
+ const signature = sourceSignature(normalized);
16
+ if (seen.has(signature)) continue;
17
+ seen.add(signature);
18
+ merged.push(normalized);
19
+ }
20
+ return merged;
21
+ }
22
+
23
+ function recordSourceList(record) {
24
+ const explicit = record.record_source
25
+ ? [{
26
+ source: record.record_source,
27
+ source_kind: record.record_source_kind,
28
+ source_path: record.record_source_path,
29
+ }]
30
+ : [];
31
+ return uniqueRecordSources([...explicit, ...(Array.isArray(record.record_sources) ? record.record_sources : [])]);
32
+ }
33
+
34
+ function mergeRecordMetadata(preferred, secondary) {
35
+ const recordSources = uniqueRecordSources([...recordSourceList(preferred), ...recordSourceList(secondary)]);
36
+ const primary = recordSources[0];
37
+ return {
38
+ ...secondary,
39
+ ...preferred,
40
+ record_source: preferred.record_source ?? secondary.record_source ?? primary?.source,
41
+ record_source_kind: preferred.record_source_kind ?? secondary.record_source_kind ?? primary?.source_kind,
42
+ record_source_path: preferred.record_source_path ?? secondary.record_source_path ?? primary?.source_path,
43
+ record_sources: recordSources,
44
+ };
45
+ }
46
+
47
+ export function annotateRecord(record, { source, sourceKind, sourcePath }) {
48
+ return mergeRecordMetadata({
49
+ ...record,
50
+ record_source: source,
51
+ record_source_kind: sourceKind,
52
+ record_source_path: sourcePath,
53
+ }, record);
54
+ }
55
+
56
+ export function annotateRecords(records, provenance) {
57
+ return records.map((record) => annotateRecord(record, provenance));
58
+ }
59
+
60
+ export function mergeByKeyWithProvenance(preferredItems, secondaryItems, getKey) {
61
+ const merged = new Map();
62
+ const upsert = (item) => {
63
+ const key = getKey(item);
64
+ const existing = merged.get(key);
65
+ merged.set(key, existing ? mergeRecordMetadata(item, existing) : item);
66
+ };
67
+ for (const item of secondaryItems) upsert(item);
68
+ for (const item of preferredItems) upsert(item);
69
+ return [...merged.values()];
70
+ }