opencode-telos 1.0.0 → 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.
- package/dist/code-intelligence/ast/cache.d.ts.map +1 -1
- package/dist/code-intelligence/ast/cache.js +8 -5
- package/dist/code-intelligence/ast/cache.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/opencode/hooks.d.ts +1 -1
- package/dist/opencode/hooks.d.ts.map +1 -1
- package/dist/opencode/hooks.js +34 -40
- package/dist/opencode/hooks.js.map +1 -1
- package/dist/opencode/tools.d.ts.map +1 -1
- package/dist/opencode/tools.js +9 -8
- package/dist/opencode/tools.js.map +1 -1
- package/dist/sdd/cache/atomic.d.ts +3 -0
- package/dist/sdd/cache/atomic.d.ts.map +1 -0
- package/dist/sdd/cache/atomic.js +35 -0
- package/dist/sdd/cache/atomic.js.map +1 -0
- package/dist/sdd/cache/fingerprint.d.ts +18 -0
- package/dist/sdd/cache/fingerprint.d.ts.map +1 -0
- package/dist/sdd/cache/fingerprint.js +99 -0
- package/dist/sdd/cache/fingerprint.js.map +1 -0
- package/dist/sdd/cache/manager.d.ts +24 -10
- package/dist/sdd/cache/manager.d.ts.map +1 -1
- package/dist/sdd/cache/manager.js +184 -33
- package/dist/sdd/cache/manager.js.map +1 -1
- package/dist/sdd/cache/snapshot-store.d.ts +2 -1
- package/dist/sdd/cache/snapshot-store.d.ts.map +1 -1
- package/dist/sdd/cache/snapshot-store.js +13 -6
- package/dist/sdd/cache/snapshot-store.js.map +1 -1
- package/dist/sdd/persistence/sqlite.d.ts.map +1 -1
- package/dist/sdd/persistence/sqlite.js +66 -41
- package/dist/sdd/persistence/sqlite.js.map +1 -1
- package/dist/sdd/persistence/yaml.d.ts.map +1 -1
- package/dist/sdd/persistence/yaml.js +33 -25
- package/dist/sdd/persistence/yaml.js.map +1 -1
- package/package.json +1 -1
- package/src/code-intelligence/ast/cache.ts +11 -6
- package/src/index.ts +4 -1
- package/src/opencode/hooks.ts +34 -39
- package/src/opencode/tools.ts +9 -8
- package/src/sdd/cache/atomic.ts +30 -0
- package/src/sdd/cache/fingerprint.ts +98 -0
- package/src/sdd/cache/manager.ts +182 -39
- package/src/sdd/cache/snapshot-store.ts +14 -7
- package/src/sdd/persistence/sqlite.ts +50 -24
- package/src/sdd/persistence/yaml.ts +40 -26
package/src/opencode/hooks.ts
CHANGED
|
@@ -171,18 +171,18 @@ export function detectShellFileWrites(command: string): string[] {
|
|
|
171
171
|
return [...new Set(files)]
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
export function createSddHooks(): Hooks {
|
|
174
|
+
export function createSddHooks(projectDir: string): Hooks {
|
|
175
175
|
let systemInjected = false
|
|
176
176
|
|
|
177
177
|
return {
|
|
178
178
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
179
179
|
if (systemInjected) return
|
|
180
|
-
if (!isSddEnabled(
|
|
180
|
+
if (!isSddEnabled(projectDir)) return
|
|
181
181
|
|
|
182
182
|
// Run pending migrations on first load
|
|
183
|
-
if (hasPendingMigrations(
|
|
183
|
+
if (hasPendingMigrations(projectDir)) {
|
|
184
184
|
try {
|
|
185
|
-
const migrationResults = runMigrations(
|
|
185
|
+
const migrationResults = runMigrations(projectDir)
|
|
186
186
|
const successful = migrationResults.filter(r => r.success)
|
|
187
187
|
if (successful.length > 0) {
|
|
188
188
|
output.system.push(`## 🔄 opencode-telos Migrations: ${successful.length} fix(es) applied`)
|
|
@@ -193,10 +193,10 @@ export function createSddHooks(): Hooks {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
const repo = createRepository(
|
|
196
|
+
const repo = createRepository(projectDir)
|
|
197
197
|
if (repo.isInitialized()) {
|
|
198
198
|
// Restore persistent cache from disk (cross-session)
|
|
199
|
-
const cacheMgr = getCacheManager(
|
|
199
|
+
const cacheMgr = getCacheManager(projectDir)
|
|
200
200
|
cacheMgr.restoreFromPersistentCache()
|
|
201
201
|
|
|
202
202
|
output.system.push(SDD_CORE_SYSTEM_PROMPT)
|
|
@@ -204,7 +204,7 @@ export function createSddHooks(): Hooks {
|
|
|
204
204
|
// Tool Registry: injeta tools relevantes para o estado atual do grafo
|
|
205
205
|
try {
|
|
206
206
|
const { getGraphSnapshot } = await import("./router/graph-state-snapshot.js")
|
|
207
|
-
const snapshot = getGraphSnapshot(
|
|
207
|
+
const snapshot = getGraphSnapshot(projectDir)
|
|
208
208
|
const { formatGraphState } = await import("./router/graph-state-snapshot.js")
|
|
209
209
|
output.system.push(formatGraphState(snapshot))
|
|
210
210
|
} catch {}
|
|
@@ -233,17 +233,12 @@ export function createSddHooks(): Hooks {
|
|
|
233
233
|
output.system.push(`## Pending Changes: ${pending.length} change(s) awaiting action`)
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
// G: Save graph snapshot to disk for fast cross-session restore
|
|
237
|
-
try {
|
|
238
|
-
const cacheMgrForSnapshot = getCacheManager(process.cwd())
|
|
239
|
-
cacheMgrForSnapshot.saveGraphSnapshot(graph)
|
|
240
|
-
} catch {}
|
|
241
236
|
} catch {
|
|
242
237
|
// Handoff is optional, don't fail if it can't be generated
|
|
243
238
|
}
|
|
244
239
|
// Persist cache to disk for cross-session reuse
|
|
245
240
|
try {
|
|
246
|
-
const cacheMgr = getCacheManager(
|
|
241
|
+
const cacheMgr = getCacheManager(projectDir)
|
|
247
242
|
cacheMgr.persistToDisk()
|
|
248
243
|
} catch {}
|
|
249
244
|
systemInjected = true
|
|
@@ -260,14 +255,14 @@ export function createSddHooks(): Hooks {
|
|
|
260
255
|
|
|
261
256
|
// Detect /sdd commands
|
|
262
257
|
if (text === "/sdd on") {
|
|
263
|
-
const state = setToggleState(
|
|
258
|
+
const state = setToggleState(projectDir, true)
|
|
264
259
|
systemInjected = false
|
|
265
260
|
part.text = `✅ SDD enforcement **enabled** at ${state.changed_at}.\n\nSpec-Driven Development is now active. All code changes will go through the SDD workflow.`
|
|
266
261
|
return
|
|
267
262
|
}
|
|
268
263
|
|
|
269
264
|
if (text === "/sdd off") {
|
|
270
|
-
const state = setToggleState(
|
|
265
|
+
const state = setToggleState(projectDir, false)
|
|
271
266
|
systemInjected = false
|
|
272
267
|
resetWorkflowState()
|
|
273
268
|
part.text = `⏸️ SDD enforcement **disabled** at ${state.changed_at}.\n\nYou can now make code changes freely without SDD workflow. Use \`/sdd on\` to re-enable.`
|
|
@@ -275,7 +270,7 @@ export function createSddHooks(): Hooks {
|
|
|
275
270
|
}
|
|
276
271
|
|
|
277
272
|
if (text === "/sdd status") {
|
|
278
|
-
const state = getToggleState(
|
|
273
|
+
const state = getToggleState(projectDir)
|
|
279
274
|
const status = state.enabled ? "🟢 ON" : "🔴 OFF"
|
|
280
275
|
part.text = `SDD Status: ${status}\nLast changed: ${state.changed_at}\n\nCommands: \`/sdd on\`, \`/sdd off\`, \`/sdd status\`, \`/sdd cache reset\``
|
|
281
276
|
return
|
|
@@ -283,7 +278,7 @@ export function createSddHooks(): Hooks {
|
|
|
283
278
|
|
|
284
279
|
// H: /sdd cache reset — full cache reset without killing the process
|
|
285
280
|
if (text === "/sdd cache reset") {
|
|
286
|
-
const cacheMgr = getCacheManager(
|
|
281
|
+
const cacheMgr = getCacheManager(projectDir)
|
|
287
282
|
const result = cacheMgr.fullReset()
|
|
288
283
|
const lines = ["## 🧹 Cache Reset Complete"]
|
|
289
284
|
lines.push(`- Memory cache: ${result.cleared.memory ? "✅ cleared" : "⏭️ skipped"}`)
|
|
@@ -297,7 +292,7 @@ export function createSddHooks(): Hooks {
|
|
|
297
292
|
}
|
|
298
293
|
|
|
299
294
|
// Semantic nudge — replaces regex-based pattern detection
|
|
300
|
-
if (isSddEnabled(
|
|
295
|
+
if (isSddEnabled(projectDir)) {
|
|
301
296
|
const nudge = formatNudgeInput(text)
|
|
302
297
|
if (nudge) {
|
|
303
298
|
part.text += `\n\n${nudge}`
|
|
@@ -308,14 +303,14 @@ export function createSddHooks(): Hooks {
|
|
|
308
303
|
|
|
309
304
|
"tool.execute.before": async (input, output) => {
|
|
310
305
|
// Skip enforcement if SDD is disabled
|
|
311
|
-
if (!isSddEnabled(
|
|
306
|
+
if (!isSddEnabled(projectDir)) return
|
|
312
307
|
|
|
313
308
|
// Enforce workflow context for SDD graph mutation tools
|
|
314
309
|
if (input.tool.startsWith("sdd.")) {
|
|
315
310
|
const access = checkToolAccess(input.tool)
|
|
316
311
|
if (!access.allowed) {
|
|
317
312
|
addAuditEntry(
|
|
318
|
-
|
|
313
|
+
projectDir,
|
|
319
314
|
process.env.USER || "current",
|
|
320
315
|
input.tool,
|
|
321
316
|
"graph",
|
|
@@ -333,7 +328,7 @@ export function createSddHooks(): Hooks {
|
|
|
333
328
|
// ENFORCEMENT: Block shell writes to .sdd/ directory
|
|
334
329
|
if (command.includes(".sdd/") && (/[>]|writeFile|open\(['"].*['"],\s*['"]w/.test(command))) {
|
|
335
330
|
addAuditEntry(
|
|
336
|
-
|
|
331
|
+
projectDir,
|
|
337
332
|
process.env.USER || "current",
|
|
338
333
|
"shell_command",
|
|
339
334
|
".sdd/",
|
|
@@ -353,7 +348,7 @@ export function createSddHooks(): Hooks {
|
|
|
353
348
|
|
|
354
349
|
const detectedFiles = detectShellFileWrites(command)
|
|
355
350
|
if (detectedFiles.length > 0) {
|
|
356
|
-
const repo = createRepository(
|
|
351
|
+
const repo = createRepository(projectDir)
|
|
357
352
|
if (!repo.isInitialized()) return
|
|
358
353
|
|
|
359
354
|
const graph = repo.loadGraph()
|
|
@@ -364,7 +359,7 @@ export function createSddHooks(): Hooks {
|
|
|
364
359
|
|
|
365
360
|
if (!hasSpecNodes && graph.nodes.length > 0) {
|
|
366
361
|
addAuditEntry(
|
|
367
|
-
|
|
362
|
+
projectDir,
|
|
368
363
|
process.env.USER || "current",
|
|
369
364
|
"shell_command",
|
|
370
365
|
detectedFiles.join(", "),
|
|
@@ -399,7 +394,7 @@ export function createSddHooks(): Hooks {
|
|
|
399
394
|
|
|
400
395
|
if (approvedChanges.length === 0) {
|
|
401
396
|
addAuditEntry(
|
|
402
|
-
|
|
397
|
+
projectDir,
|
|
403
398
|
process.env.USER || "current",
|
|
404
399
|
"shell_command",
|
|
405
400
|
detectedFiles.join(", "),
|
|
@@ -429,7 +424,7 @@ export function createSddHooks(): Hooks {
|
|
|
429
424
|
const shellWorkflow = getWorkflowState()
|
|
430
425
|
if (shellWorkflow.enforced && !shellWorkflow.specUpdated) {
|
|
431
426
|
addAuditEntry(
|
|
432
|
-
|
|
427
|
+
projectDir,
|
|
433
428
|
process.env.USER || "current",
|
|
434
429
|
"shell_command",
|
|
435
430
|
detectedFiles.join(", "),
|
|
@@ -456,10 +451,10 @@ export function createSddHooks(): Hooks {
|
|
|
456
451
|
|
|
457
452
|
// Approved — create snapshots
|
|
458
453
|
for (const change of approvedChanges) {
|
|
459
|
-
createSnapshot(graph, change.id,
|
|
454
|
+
createSnapshot(graph, change.id, projectDir)
|
|
460
455
|
}
|
|
461
456
|
addAuditEntry(
|
|
462
|
-
|
|
457
|
+
projectDir,
|
|
463
458
|
process.env.USER || "current",
|
|
464
459
|
"shell_command",
|
|
465
460
|
detectedFiles.join(", "),
|
|
@@ -479,7 +474,7 @@ export function createSddHooks(): Hooks {
|
|
|
479
474
|
// The SDD graph can ONLY be modified through sdd.* tools
|
|
480
475
|
if (filePath.includes(".sdd/")) {
|
|
481
476
|
addAuditEntry(
|
|
482
|
-
|
|
477
|
+
projectDir,
|
|
483
478
|
process.env.USER || "current",
|
|
484
479
|
input.tool,
|
|
485
480
|
filePath,
|
|
@@ -508,7 +503,7 @@ export function createSddHooks(): Hooks {
|
|
|
508
503
|
if (!isSourceFile) return
|
|
509
504
|
|
|
510
505
|
// Check if SDD is initialized
|
|
511
|
-
const repo = createRepository(
|
|
506
|
+
const repo = createRepository(projectDir)
|
|
512
507
|
if (!repo.isInitialized()) return
|
|
513
508
|
|
|
514
509
|
const graph = repo.loadGraph()
|
|
@@ -519,7 +514,7 @@ export function createSddHooks(): Hooks {
|
|
|
519
514
|
|
|
520
515
|
if (!hasSpecNodes && graph.nodes.length > 0) {
|
|
521
516
|
addAuditEntry(
|
|
522
|
-
|
|
517
|
+
projectDir,
|
|
523
518
|
process.env.USER || "current",
|
|
524
519
|
"write_file",
|
|
525
520
|
filePath,
|
|
@@ -543,12 +538,12 @@ export function createSddHooks(): Hooks {
|
|
|
543
538
|
}
|
|
544
539
|
|
|
545
540
|
// Check permissions before allowing changes
|
|
546
|
-
const userRole = getUserRoleWithAuth(
|
|
547
|
-
const hasPermission = checkPermission(userRole, "create_change",
|
|
541
|
+
const userRole = getUserRoleWithAuth(projectDir, process.env.USER || "current")
|
|
542
|
+
const hasPermission = checkPermission(userRole, "create_change", projectDir)
|
|
548
543
|
|
|
549
544
|
if (!hasPermission) {
|
|
550
545
|
addAuditEntry(
|
|
551
|
-
|
|
546
|
+
projectDir,
|
|
552
547
|
process.env.USER || "current",
|
|
553
548
|
"write_file",
|
|
554
549
|
filePath,
|
|
@@ -577,7 +572,7 @@ export function createSddHooks(): Hooks {
|
|
|
577
572
|
const workflow = getWorkflowState()
|
|
578
573
|
if (workflow.enforced && !workflow.specUpdated) {
|
|
579
574
|
addAuditEntry(
|
|
580
|
-
|
|
575
|
+
projectDir,
|
|
581
576
|
process.env.USER || "current",
|
|
582
577
|
"write_file",
|
|
583
578
|
filePath,
|
|
@@ -604,10 +599,10 @@ export function createSddHooks(): Hooks {
|
|
|
604
599
|
|
|
605
600
|
// Create snapshot before approving change
|
|
606
601
|
for (const change of approvedChanges) {
|
|
607
|
-
createSnapshot(graph, change.id,
|
|
602
|
+
createSnapshot(graph, change.id, projectDir)
|
|
608
603
|
}
|
|
609
604
|
addAuditEntry(
|
|
610
|
-
|
|
605
|
+
projectDir,
|
|
611
606
|
process.env.USER || "current",
|
|
612
607
|
"write_file",
|
|
613
608
|
filePath,
|
|
@@ -619,7 +614,7 @@ export function createSddHooks(): Hooks {
|
|
|
619
614
|
|
|
620
615
|
// File not covered by any approved Change → BLOCK the write
|
|
621
616
|
addAuditEntry(
|
|
622
|
-
|
|
617
|
+
projectDir,
|
|
623
618
|
process.env.USER || "current",
|
|
624
619
|
"write_file",
|
|
625
620
|
filePath,
|
|
@@ -689,7 +684,7 @@ export function createSddHooks(): Hooks {
|
|
|
689
684
|
},
|
|
690
685
|
|
|
691
686
|
"tool.definition": async (input, output) => {
|
|
692
|
-
if (!isSddEnabled(
|
|
687
|
+
if (!isSddEnabled(projectDir)) return
|
|
693
688
|
|
|
694
689
|
// Inject SDD enforcement warning into run_terminal_command description
|
|
695
690
|
if (input.toolID === "run_terminal_command") {
|
|
@@ -719,7 +714,7 @@ export function createSddHooks(): Hooks {
|
|
|
719
714
|
dispose: async () => {
|
|
720
715
|
// E: Persist cache and release resources on session end
|
|
721
716
|
try {
|
|
722
|
-
const cacheMgr = getCacheManager(
|
|
717
|
+
const cacheMgr = getCacheManager(projectDir)
|
|
723
718
|
cacheMgr.persistToDisk()
|
|
724
719
|
cacheMgr.releaseWriteLock()
|
|
725
720
|
} catch {}
|
package/src/opencode/tools.ts
CHANGED
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
createDriftWhitelistTool,
|
|
72
72
|
} from "./router/tools-composite.js"
|
|
73
73
|
import { createWorkflowTools } from "./workflows/tools-workflow.js"
|
|
74
|
+
import { graphFingerprint } from "../sdd/cache/fingerprint.js"
|
|
74
75
|
|
|
75
76
|
// ── Validation Coverage Index (singleton per session) ──────────────
|
|
76
77
|
const validationIndex = new ValidationIndex()
|
|
@@ -95,7 +96,7 @@ function getCachedToolResponse(directory: string, toolName: string, args: Record
|
|
|
95
96
|
const repo = getRepo(directory)
|
|
96
97
|
if (!repo.isInitialized()) return null
|
|
97
98
|
const graph = repo.loadGraph()
|
|
98
|
-
return cacheMgr.getToolResponse(toolName, args, graph
|
|
99
|
+
return cacheMgr.getToolResponse(toolName, args, graphFingerprint(graph))
|
|
99
100
|
} catch {
|
|
100
101
|
return null
|
|
101
102
|
}
|
|
@@ -110,7 +111,7 @@ function setCachedToolResponse(directory: string, toolName: string, args: Record
|
|
|
110
111
|
const repo = getRepo(directory)
|
|
111
112
|
if (!repo.isInitialized()) return
|
|
112
113
|
const graph = repo.loadGraph()
|
|
113
|
-
cacheMgr.setToolResponse(toolName, args, response, graph
|
|
114
|
+
cacheMgr.setToolResponse(toolName, args, response, graphFingerprint(graph))
|
|
114
115
|
} catch {}
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -698,7 +699,7 @@ export function createSddTools(): Record<string, ToolDefinition> {
|
|
|
698
699
|
|
|
699
700
|
// Check analysis cache
|
|
700
701
|
const cacheMgr = getCacheManager(ctx.directory)
|
|
701
|
-
|
|
702
|
+
const cached = cacheMgr.getAnalysisResult("validate", graphFingerprint(graph))
|
|
702
703
|
if (cached) {
|
|
703
704
|
recordTelemetry(ctx.directory, {
|
|
704
705
|
name: "graph_validation",
|
|
@@ -727,7 +728,7 @@ export function createSddTools(): Record<string, ToolDefinition> {
|
|
|
727
728
|
}
|
|
728
729
|
|
|
729
730
|
// Cache the result
|
|
730
|
-
|
|
731
|
+
cacheMgr.setAnalysisResult("validate", formatted, graphFingerprint(graph))
|
|
731
732
|
recordTelemetry(ctx.directory, {
|
|
732
733
|
name: "graph_validation",
|
|
733
734
|
duration_ms: Date.now() - startedAt,
|
|
@@ -751,14 +752,14 @@ export function createSddTools(): Record<string, ToolDefinition> {
|
|
|
751
752
|
|
|
752
753
|
// Check analysis cache
|
|
753
754
|
const cacheMgr = getCacheManager(ctx.directory)
|
|
754
|
-
|
|
755
|
+
const cached = cacheMgr.getAnalysisResult("drift", graphFingerprint(graph))
|
|
755
756
|
if (cached) return cached as string
|
|
756
757
|
|
|
757
758
|
const result = detectDrift(graph, ctx.directory)
|
|
758
759
|
const formatted = formatDriftReport(result)
|
|
759
760
|
|
|
760
761
|
// Cache the result
|
|
761
|
-
|
|
762
|
+
cacheMgr.setAnalysisResult("drift", formatted, graphFingerprint(graph))
|
|
762
763
|
|
|
763
764
|
return formatted
|
|
764
765
|
},
|
|
@@ -1867,14 +1868,14 @@ export function createSddTools(): Record<string, ToolDefinition> {
|
|
|
1867
1868
|
|
|
1868
1869
|
// Check analysis cache
|
|
1869
1870
|
const cacheMgr = getCacheManager(ctx.directory)
|
|
1870
|
-
|
|
1871
|
+
const cached = cacheMgr.getAnalysisResult("quality", graphFingerprint(graph))
|
|
1871
1872
|
if (cached) return cached as string
|
|
1872
1873
|
|
|
1873
1874
|
const report = calculateQualityScore(graph, ctx.directory)
|
|
1874
1875
|
const formatted = formatQualityReport(report)
|
|
1875
1876
|
|
|
1876
1877
|
// Cache the result
|
|
1877
|
-
|
|
1878
|
+
cacheMgr.setAnalysisResult("quality", formatted, graphFingerprint(graph))
|
|
1878
1879
|
|
|
1879
1880
|
return formatted
|
|
1880
1881
|
},
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, renameSync, writeFileSync } from "fs"
|
|
2
|
+
import { dirname } from "path"
|
|
3
|
+
|
|
4
|
+
/** Write a cache artifact so readers see either the old complete file or the new complete file. */
|
|
5
|
+
export function atomicWriteFile(filePath: string, content: string): void {
|
|
6
|
+
const directory = dirname(filePath)
|
|
7
|
+
if (!existsSync(directory)) mkdirSync(directory, { recursive: true })
|
|
8
|
+
|
|
9
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}`
|
|
10
|
+
let descriptor: number | undefined
|
|
11
|
+
try {
|
|
12
|
+
descriptor = openSync(temporaryPath, "w", 0o600)
|
|
13
|
+
writeFileSync(descriptor, content, "utf-8")
|
|
14
|
+
fsyncSync(descriptor)
|
|
15
|
+
closeSync(descriptor)
|
|
16
|
+
descriptor = undefined
|
|
17
|
+
renameSync(temporaryPath, filePath)
|
|
18
|
+
} finally {
|
|
19
|
+
if (descriptor !== undefined) {
|
|
20
|
+
try { closeSync(descriptor) } catch {}
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
// A failed rename must not leave a misleading cache artifact behind.
|
|
24
|
+
if (existsSync(temporaryPath)) {
|
|
25
|
+
const { unlinkSync } = require("fs")
|
|
26
|
+
unlinkSync(temporaryPath)
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHash } from "crypto"
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs"
|
|
3
|
+
import { join, relative } from "path"
|
|
4
|
+
import type { KnowledgeGraph } from "../domain/types.js"
|
|
5
|
+
|
|
6
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
7
|
+
".c", ".cc", ".cpp", ".cs", ".go", ".h", ".hpp", ".java", ".js", ".jsx",
|
|
8
|
+
".json", ".kt", ".mjs", ".mts", ".py", ".rb", ".rs", ".svelte", ".swift",
|
|
9
|
+
".ts", ".tsx", ".vue", ".yaml", ".yml",
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
13
|
+
".git", ".sdd", ".opencode", "node_modules", "dist", "build", "coverage",
|
|
14
|
+
".cache", ".next", ".turbo", "target", "vendor",
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
export function stableSerialize(value: unknown): string {
|
|
18
|
+
if (value === null) return "null"
|
|
19
|
+
if (value === undefined) return "undefined"
|
|
20
|
+
if (typeof value !== "object") return JSON.stringify(value)
|
|
21
|
+
if (Array.isArray(value)) return `[${value.map(stableSerialize).join(",")}]`
|
|
22
|
+
|
|
23
|
+
const record = value as Record<string, unknown>
|
|
24
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key])}`).join(",")}}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sha256(value: string | Uint8Array): string {
|
|
28
|
+
return createHash("sha256").update(value).digest("hex")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Deterministic content fingerprint for the complete graph, independent of YAML/SQLite formatting. */
|
|
32
|
+
export function graphFingerprint(graph: KnowledgeGraph): string {
|
|
33
|
+
return sha256(stableSerialize({
|
|
34
|
+
version: graph.version,
|
|
35
|
+
project_id: graph.project_id,
|
|
36
|
+
metadata: graph.metadata,
|
|
37
|
+
nodes: [...graph.nodes].sort((a, b) => a.id.localeCompare(b.id)),
|
|
38
|
+
relationships: [...graph.relationships].sort((a, b) => a.id.localeCompare(b.id)),
|
|
39
|
+
}))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function fileContentFingerprint(filePath: string): string {
|
|
43
|
+
try {
|
|
44
|
+
return sha256(readFileSync(filePath))
|
|
45
|
+
} catch {
|
|
46
|
+
return sha256(`missing:${filePath}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Fast identity for external-file detection; includes all files when several files are supplied. */
|
|
51
|
+
export function fileSignature(filePaths: string[]): string {
|
|
52
|
+
const parts = filePaths.map((filePath) => {
|
|
53
|
+
try {
|
|
54
|
+
const stat = statSync(filePath)
|
|
55
|
+
return `${filePath}:${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`
|
|
56
|
+
} catch {
|
|
57
|
+
return `${filePath}:missing`
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
return sha256(parts.sort().join("\n"))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function configFingerprint(projectDir: string): string {
|
|
64
|
+
const path = join(projectDir, ".sdd", "config.json")
|
|
65
|
+
return existsSync(path) ? fileContentFingerprint(path) : sha256("missing-config")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sourceFiles(projectDir: string): string[] {
|
|
69
|
+
const files: string[] = []
|
|
70
|
+
const visit = (directory: string): void => {
|
|
71
|
+
let entries
|
|
72
|
+
try { entries = readdirSync(directory, { withFileTypes: true }) } catch { return }
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (entry.isSymbolicLink()) continue
|
|
75
|
+
const path = join(directory, entry.name)
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) visit(path)
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
const extension = entry.name.slice(entry.name.lastIndexOf(".")).toLowerCase()
|
|
81
|
+
if (SOURCE_EXTENSIONS.has(extension)) files.push(path)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
visit(projectDir)
|
|
85
|
+
return files.sort()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Content fingerprint for code-dependent analyses. The caller can cache this by fileSignature. */
|
|
89
|
+
export function sourceFingerprint(
|
|
90
|
+
projectDir: string,
|
|
91
|
+
previous?: { fingerprint: string; signature: string } | null,
|
|
92
|
+
): { fingerprint: string; signature: string } {
|
|
93
|
+
const files = sourceFiles(projectDir)
|
|
94
|
+
const signature = fileSignature(files)
|
|
95
|
+
if (previous?.signature === signature) return previous
|
|
96
|
+
const content = files.map((path) => `${relative(projectDir, path)}:${fileContentFingerprint(path)}`).join("\n")
|
|
97
|
+
return { fingerprint: sha256(content), signature }
|
|
98
|
+
}
|