opencode-telos 1.0.1 → 1.0.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.
Files changed (41) hide show
  1. package/dist/code-intelligence/ast/cache.d.ts.map +1 -1
  2. package/dist/code-intelligence/ast/cache.js +8 -5
  3. package/dist/code-intelligence/ast/cache.js.map +1 -1
  4. package/dist/opencode/hooks.d.ts.map +1 -1
  5. package/dist/opencode/hooks.js +0 -6
  6. package/dist/opencode/hooks.js.map +1 -1
  7. package/dist/opencode/tools.d.ts.map +1 -1
  8. package/dist/opencode/tools.js +9 -8
  9. package/dist/opencode/tools.js.map +1 -1
  10. package/dist/sdd/cache/atomic.d.ts +3 -0
  11. package/dist/sdd/cache/atomic.d.ts.map +1 -0
  12. package/dist/sdd/cache/atomic.js +35 -0
  13. package/dist/sdd/cache/atomic.js.map +1 -0
  14. package/dist/sdd/cache/fingerprint.d.ts +18 -0
  15. package/dist/sdd/cache/fingerprint.d.ts.map +1 -0
  16. package/dist/sdd/cache/fingerprint.js +99 -0
  17. package/dist/sdd/cache/fingerprint.js.map +1 -0
  18. package/dist/sdd/cache/manager.d.ts +24 -10
  19. package/dist/sdd/cache/manager.d.ts.map +1 -1
  20. package/dist/sdd/cache/manager.js +184 -33
  21. package/dist/sdd/cache/manager.js.map +1 -1
  22. package/dist/sdd/cache/snapshot-store.d.ts +2 -1
  23. package/dist/sdd/cache/snapshot-store.d.ts.map +1 -1
  24. package/dist/sdd/cache/snapshot-store.js +13 -6
  25. package/dist/sdd/cache/snapshot-store.js.map +1 -1
  26. package/dist/sdd/persistence/sqlite.d.ts.map +1 -1
  27. package/dist/sdd/persistence/sqlite.js +66 -41
  28. package/dist/sdd/persistence/sqlite.js.map +1 -1
  29. package/dist/sdd/persistence/yaml.d.ts.map +1 -1
  30. package/dist/sdd/persistence/yaml.js +33 -25
  31. package/dist/sdd/persistence/yaml.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/code-intelligence/ast/cache.ts +11 -6
  34. package/src/opencode/hooks.ts +0 -5
  35. package/src/opencode/tools.ts +9 -8
  36. package/src/sdd/cache/atomic.ts +30 -0
  37. package/src/sdd/cache/fingerprint.ts +98 -0
  38. package/src/sdd/cache/manager.ts +182 -39
  39. package/src/sdd/cache/snapshot-store.ts +14 -7
  40. package/src/sdd/persistence/sqlite.ts +50 -24
  41. package/src/sdd/persistence/yaml.ts +40 -26
@@ -7,9 +7,11 @@
7
7
  * Solução G: Snapshot persistente do grafo.
8
8
  */
9
9
 
10
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"
10
+ import { existsSync, readFileSync, mkdirSync } from "fs"
11
11
  import { join, dirname } from "path"
12
12
  import type { KnowledgeGraph, AnyNode, Relationship } from "../domain/types.js"
13
+ import { atomicWriteFile } from "./atomic.js"
14
+ import { graphFingerprint } from "./fingerprint.js"
13
15
 
14
16
  const SNAPSHOT_FILE = ".sdd/graph-cache.json"
15
17
  const SNAPSHOT_MAX_AGE_MS = 60 * 60 * 1000 // 1 hour
@@ -31,6 +33,7 @@ interface SerializedSnapshot {
31
33
  graph: KnowledgeGraph
32
34
  indices: SerializedIndices
33
35
  graphHash: string
36
+ sourceSignature: string
34
37
  }
35
38
 
36
39
  export class GraphSnapshotStore {
@@ -43,27 +46,28 @@ export class GraphSnapshotStore {
43
46
  /**
44
47
  * Save full graph + indices to disk.
45
48
  */
46
- save(graph: KnowledgeGraph, graphHash: string): void {
49
+ save(graph: KnowledgeGraph, graphHash: string, sourceSignature: string): void {
47
50
  const snapshot: SerializedSnapshot = {
48
- version: 2,
51
+ version: 3,
49
52
  timestamp: Date.now(),
50
53
  graph,
51
54
  indices: this.serializeIndices(graph),
52
55
  graphHash,
56
+ sourceSignature,
53
57
  }
54
58
 
55
59
  const path = join(this.projectDir, SNAPSHOT_FILE)
56
60
  const dir = dirname(path)
57
61
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
58
62
 
59
- writeFileSync(path, JSON.stringify(snapshot), "utf-8")
63
+ atomicWriteFile(path, JSON.stringify(snapshot))
60
64
  }
61
65
 
62
66
  /**
63
67
  * Load graph from snapshot if valid.
64
68
  * Returns null if snapshot is missing, stale, or corrupted.
65
69
  */
66
- load(): { graph: KnowledgeGraph; graphHash: string } | null {
70
+ load(): { graph: KnowledgeGraph; graphHash: string; sourceSignature: string } | null {
67
71
  const path = join(this.projectDir, SNAPSHOT_FILE)
68
72
  if (!existsSync(path)) return null
69
73
 
@@ -72,17 +76,19 @@ export class GraphSnapshotStore {
72
76
  const snapshot: SerializedSnapshot = JSON.parse(raw)
73
77
 
74
78
  // Validate version
75
- if (snapshot.version !== 2) return null
79
+ if (snapshot.version !== 3) return null
76
80
 
77
81
  // Validate age
78
82
  if (Date.now() - snapshot.timestamp > SNAPSHOT_MAX_AGE_MS) return null
79
83
 
80
84
  // Validate graph structure
81
85
  if (!snapshot.graph || !Array.isArray(snapshot.graph.nodes)) return null
86
+ if (!snapshot.sourceSignature || snapshot.graphHash !== graphFingerprint(snapshot.graph)) return null
82
87
 
83
88
  return {
84
89
  graph: snapshot.graph,
85
90
  graphHash: snapshot.graphHash || "",
91
+ sourceSignature: snapshot.sourceSignature,
86
92
  }
87
93
  } catch {
88
94
  return null
@@ -99,7 +105,8 @@ export class GraphSnapshotStore {
99
105
  try {
100
106
  const raw = readFileSync(path, "utf-8")
101
107
  const snapshot: SerializedSnapshot = JSON.parse(raw)
102
- return snapshot.version === 2 && (Date.now() - snapshot.timestamp) < SNAPSHOT_MAX_AGE_MS
108
+ return snapshot.version === 3 && Boolean(snapshot.sourceSignature) &&
109
+ (Date.now() - snapshot.timestamp) < SNAPSHOT_MAX_AGE_MS
103
110
  } catch {
104
111
  return false
105
112
  }
@@ -10,6 +10,7 @@ import { GraphIndices } from "../graph/index.js"
10
10
  import { ensureDir } from "./yaml.js"
11
11
  import { join } from "path"
12
12
  import { getCacheManager } from "../cache/manager.js"
13
+ import { fileSignature } from "../cache/fingerprint.js"
13
14
  import type { GraphRepository } from "./repository.js"
14
15
  import { recordLegitimateSave, validateGraphIntegrity } from "../graph/integrity-guard.js"
15
16
 
@@ -30,7 +31,7 @@ export class SqliteGraphRepository {
30
31
  private baseDir: string
31
32
  private dbPath: string
32
33
  private db: any
33
- private static cache: Map<string, { graph: KnowledgeGraph; indices: GraphIndices }> = new Map()
34
+ private static cache: Map<string, { graph: KnowledgeGraph; indices: GraphIndices; sourceSignature: string }> = new Map()
34
35
 
35
36
  constructor(projectDir: string) {
36
37
  this.baseDir = join(projectDir, ".sdd")
@@ -106,18 +107,34 @@ export class SqliteGraphRepository {
106
107
  }
107
108
 
108
109
  loadGraph(): KnowledgeGraph {
110
+ const cacheMgr = getCacheManager(require("path").dirname(this.baseDir))
111
+ const sourcePaths = [this.dbPath, `${this.dbPath}-wal`, `${this.dbPath}-shm`]
112
+ const currentSourceSignature = fileSignature(sourcePaths)
113
+
109
114
  // Check cache with revalidation
110
115
  const cached = SqliteGraphRepository.cache.get(this.dbPath)
111
116
  if (cached) {
112
- // Revalidation: check if db was modified externally
113
- const cacheMgr = getCacheManager(require("path").dirname(this.baseDir))
114
- if (!cacheMgr.checkCrossProcessInvalidation(this.dbPath)) {
117
+ // Revalidation: check content signature and durable invalidation events.
118
+ if (cached.sourceSignature === currentSourceSignature && !cacheMgr.checkCrossProcessInvalidation(this.dbPath)) {
115
119
  return structuredClone(cached.graph)
116
120
  }
117
121
  // DB was modified externally, invalidate cache
118
122
  SqliteGraphRepository.cache.delete(this.dbPath)
119
123
  }
120
124
 
125
+ const snapshot = cacheMgr.loadGraphSnapshot()
126
+ if (snapshot?.sourceSignature === currentSourceSignature) {
127
+ const graph = snapshot.graph
128
+ const indices = GraphIndices.from(graph)
129
+ SqliteGraphRepository.cache.set(this.dbPath, {
130
+ graph: structuredClone(graph),
131
+ indices,
132
+ sourceSignature: currentSourceSignature,
133
+ })
134
+ cacheMgr.initGraphHash(graph.nodes.length, graph.relationships.length)
135
+ return structuredClone(graph)
136
+ }
137
+
121
138
  const db = this.getDb()
122
139
 
123
140
  // Load metadata
@@ -168,7 +185,7 @@ export class SqliteGraphRepository {
168
185
 
169
186
  // Build indices and cache
170
187
  const indices = GraphIndices.from(graph)
171
- SqliteGraphRepository.cache.set(this.dbPath, { graph, indices })
188
+ SqliteGraphRepository.cache.set(this.dbPath, { graph, indices, sourceSignature: currentSourceSignature })
172
189
 
173
190
  // Anti-bypass: check for out-of-band modifications
174
191
  const projectDir = require("path").dirname(this.baseDir)
@@ -184,21 +201,26 @@ export class SqliteGraphRepository {
184
201
 
185
202
  saveGraph(graph: KnowledgeGraph): void {
186
203
  const db = this.getDb()
204
+ const cacheMgr = getCacheManager(require("path").dirname(this.baseDir))
205
+ if (!cacheMgr.acquireWriteLock()) {
206
+ throw new Error("Another process is writing the SDD graph; retry the mutation")
207
+ }
187
208
 
188
209
  // Use transaction for atomicity
189
- db.exec("BEGIN TRANSACTION")
190
-
191
210
  try {
192
- // Differential persistence: update only changed rows and delete only
193
- // rows that disappeared. This keeps SQLite useful for large graphs.
194
- const upsertMeta = db.prepare(
195
- "INSERT INTO graph_metadata (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
196
- )
197
- upsertMeta.run("project_id", graph.project_id)
198
- upsertMeta.run("version", graph.version)
199
- upsertMeta.run("created_at", graph.metadata.created_at)
200
- upsertMeta.run("updated_at", graph.metadata.updated_at)
201
- upsertMeta.run("sdd_version", graph.metadata.sdd_version)
211
+ db.exec("BEGIN TRANSACTION")
212
+
213
+ try {
214
+ // Differential persistence: update only changed rows and delete only
215
+ // rows that disappeared. This keeps SQLite useful for large graphs.
216
+ const upsertMeta = db.prepare(
217
+ "INSERT INTO graph_metadata (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
218
+ )
219
+ upsertMeta.run("project_id", graph.project_id)
220
+ upsertMeta.run("version", graph.version)
221
+ upsertMeta.run("created_at", graph.metadata.created_at)
222
+ upsertMeta.run("updated_at", graph.metadata.updated_at)
223
+ upsertMeta.run("sdd_version", graph.metadata.sdd_version)
202
224
 
203
225
  // Save nodes in batches for performance
204
226
  const upsertNode = db.prepare(`
@@ -258,10 +280,13 @@ export class SqliteGraphRepository {
258
280
  const deleteRelationship = db.prepare("DELETE FROM relationships WHERE id = ?")
259
281
  for (const row of existingRelationshipIds) if (!relationshipIds.has(row.id)) deleteRelationship.run(row.id)
260
282
 
261
- db.exec("COMMIT")
262
- } catch (e) {
263
- db.exec("ROLLBACK")
264
- throw e
283
+ db.exec("COMMIT")
284
+ } catch (e) {
285
+ db.exec("ROLLBACK")
286
+ throw e
287
+ }
288
+ } finally {
289
+ cacheMgr.releaseWriteLock()
265
290
  }
266
291
 
267
292
  // Compare against the immutable cached snapshot before replacing it.
@@ -270,7 +295,9 @@ export class SqliteGraphRepository {
270
295
  const snapshot = structuredClone(graph) as KnowledgeGraph
271
296
  const dirty = computeDirtyState(cached?.graph || null, snapshot)
272
297
  const indices = GraphIndices.from(snapshot)
273
- SqliteGraphRepository.cache.set(this.dbPath, { graph: snapshot, indices })
298
+ const sourcePaths = [this.dbPath, `${this.dbPath}-wal`, `${this.dbPath}-shm`]
299
+ const sourceSignature = fileSignature(sourcePaths)
300
+ SqliteGraphRepository.cache.set(this.dbPath, { graph: snapshot, indices, sourceSignature })
274
301
  /*
275
302
  if (cached) {
276
303
  const oldGraph = cached.graph
@@ -313,7 +340,6 @@ export class SqliteGraphRepository {
313
340
  */
314
341
 
315
342
  // Update incremental hash
316
- const cacheMgr = getCacheManager(require("path").dirname(this.baseDir))
317
343
  cacheMgr.initGraphHash(graph.nodes.length, graph.relationships.length)
318
344
 
319
345
  // A: Granular invalidation — only invalidate caches for changed types
@@ -326,7 +352,7 @@ export class SqliteGraphRepository {
326
352
  }
327
353
 
328
354
  // G: Save graph snapshot to disk for cross-session restore
329
- cacheMgr.saveGraphSnapshot(graph)
355
+ cacheMgr.saveGraphSnapshot(graph, sourceSignature)
330
356
 
331
357
  // Anti-bypass: record legitimate save for integrity tracking
332
358
  const projectDir = require("path").dirname(this.baseDir)
@@ -1,10 +1,12 @@
1
1
  import * as yaml from "js-yaml"
2
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs"
2
+ import { readFileSync, existsSync, mkdirSync, readdirSync } from "fs"
3
3
  import { join, dirname } from "path"
4
4
  import type { KnowledgeGraph, AnyNode, NodeType, NodeStatus, Relationship } from "../domain/types.js"
5
5
  import { GraphIndices } from "../graph/index.js"
6
6
  import type { GraphRepository } from "./repository.js"
7
7
  import { getCacheManager } from "../cache/manager.js"
8
+ import { atomicWriteFile } from "../cache/atomic.js"
9
+ import { fileSignature } from "../cache/fingerprint.js"
8
10
  import { recordLegitimateSave, validateGraphIntegrity, isGraphTampered } from "../graph/integrity-guard.js"
9
11
 
10
12
  export function readYaml<T>(filePath: string): T {
@@ -21,7 +23,7 @@ export function writeYaml(filePath: string, data: unknown): void {
21
23
  quotingType: '"',
22
24
  forceQuotes: false,
23
25
  })
24
- writeFileSync(filePath, content, "utf-8")
26
+ atomicWriteFile(filePath, content)
25
27
  }
26
28
 
27
29
  export function readJson<T>(filePath: string): T {
@@ -32,7 +34,7 @@ export function readJson<T>(filePath: string): T {
32
34
  export function writeJson(filePath: string, data: unknown): void {
33
35
  const dir = dirname(filePath)
34
36
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
35
- writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8")
37
+ atomicWriteFile(filePath, JSON.stringify(data, null, 2))
36
38
  }
37
39
 
38
40
  export function ensureDir(dirPath: string): void {
@@ -73,6 +75,7 @@ interface GraphCache {
73
75
  graph: KnowledgeGraph
74
76
  indices: GraphIndices
75
77
  lastModified: number
78
+ sourceSignature: string
76
79
  }
77
80
 
78
81
  // ── Repository ───────────────────────────────────────────────────────
@@ -110,29 +113,36 @@ export class YamlGraphRepository implements GraphRepository {
110
113
  throw new Error("SDD not initialized. Run sdd.initialize first.")
111
114
  }
112
115
 
116
+ const cacheMgr = getCacheManager(require("path").dirname(this.baseDir))
117
+ const currentSourceSignature = fileSignature([this.graphPath])
118
+ const externallyInvalidated = cacheMgr.checkCrossProcessInvalidation(this.graphPath)
119
+
113
120
  // Check cache with revalidation
114
121
  const cached = YamlGraphRepository.cache.get(this.graphPath)
115
122
  if (cached) {
116
- // Revalidation: check if file was modified externally
117
- const currentMtime = (() => { try { return require("fs").statSync(this.graphPath).mtimeMs } catch { return 0 } })()
118
- if (currentMtime <= cached.lastModified) {
119
- // Cache is valid — also check incremental hash
120
- const cacheMgr = getCacheManager(require("path").dirname(this.baseDir))
121
- if (cacheMgr.isGraphHashValid(cached.indices.totalNodes + ":" + cached.indices.totalRelationships)) {
122
- // Never expose the cached snapshot itself: callers intentionally
123
- // mutate the graph before saveGraph(), and leaking this reference
124
- // makes old/new comparisons impossible.
125
- return structuredClone(cached.graph)
126
- }
123
+ if (!externallyInvalidated && cached.sourceSignature === currentSourceSignature) {
124
+ // Never expose the cached snapshot itself: callers intentionally
125
+ // mutate the graph before saveGraph(), and leaking this reference
126
+ // makes old/new comparisons impossible.
127
+ return structuredClone(cached.graph)
127
128
  }
128
129
  // File was modified externally or hash mismatch, invalidate cache
129
130
  YamlGraphRepository.cache.delete(this.graphPath)
130
131
  }
132
+ if (externallyInvalidated) YamlGraphRepository.cache.delete(this.graphPath)
131
133
 
132
- // Cross-process check
133
- const cacheMgr = getCacheManager(require("path").dirname(this.baseDir))
134
- if (cacheMgr.checkCrossProcessInvalidation(this.graphPath)) {
135
- YamlGraphRepository.cache.delete(this.graphPath)
134
+ const snapshot = cacheMgr.loadGraphSnapshot()
135
+ if (snapshot?.sourceSignature === currentSourceSignature) {
136
+ const graph = snapshot.graph
137
+ const indices = GraphIndices.from(graph)
138
+ YamlGraphRepository.cache.set(this.graphPath, {
139
+ graph: structuredClone(graph),
140
+ indices,
141
+ lastModified: Date.now(),
142
+ sourceSignature: currentSourceSignature,
143
+ })
144
+ cacheMgr.initGraphHash(graph.nodes.length, graph.relationships.length)
145
+ return structuredClone(graph)
136
146
  }
137
147
 
138
148
  const graph = readYaml<KnowledgeGraph>(this.graphPath)
@@ -143,9 +153,10 @@ export class YamlGraphRepository implements GraphRepository {
143
153
  graph,
144
154
  indices,
145
155
  lastModified: stat,
156
+ sourceSignature: currentSourceSignature,
146
157
  })
147
158
 
148
- // Initialize incremental hash
159
+ // Initialize the legacy mutation counter used by compatibility APIs.
149
160
  cacheMgr.initGraphHash(graph.nodes.length, graph.relationships.length)
150
161
 
151
162
  // Anti-bypass: check for out-of-band modifications
@@ -174,7 +185,9 @@ export class YamlGraphRepository implements GraphRepository {
174
185
 
175
186
  // Acquire cross-process lock
176
187
  const cacheMgr = getCacheManager(require("path").dirname(this.baseDir))
177
- cacheMgr.acquireWriteLock()
188
+ if (!cacheMgr.acquireWriteLock()) {
189
+ throw new Error("Another process is writing the SDD graph; retry the mutation")
190
+ }
178
191
 
179
192
  try {
180
193
  writeYaml(this.graphPath, graph)
@@ -199,11 +212,12 @@ export class YamlGraphRepository implements GraphRepository {
199
212
  // readonly totals/search entries after additions and removals. YAML is
200
213
  // limited to small graphs; large graphs migrate to SQLite.
201
214
  const indices = GraphIndices.from(snapshot)
202
- YamlGraphRepository.cache.set(this.graphPath, {
203
- graph: snapshot,
204
- indices,
205
- lastModified: Date.now(),
206
- })
215
+ YamlGraphRepository.cache.set(this.graphPath, {
216
+ graph: snapshot,
217
+ indices,
218
+ lastModified: Date.now(),
219
+ sourceSignature: fileSignature([this.graphPath]),
220
+ })
207
221
  /*
208
222
  if (cached) {
209
223
  // Compute dirty nodes incrementally
@@ -273,7 +287,7 @@ export class YamlGraphRepository implements GraphRepository {
273
287
  }
274
288
 
275
289
  // G: Save graph snapshot to disk for cross-session restore
276
- cacheMgr.saveGraphSnapshot(graph)
290
+ cacheMgr.saveGraphSnapshot(graph, fileSignature([this.graphPath]))
277
291
  }
278
292
 
279
293
  getStorageType(): "yaml" {