@jamesaphoenix/tx-core 0.4.4 → 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/dist/db.d.ts +4 -9
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +6 -80
- package/dist/db.js.map +1 -1
- package/dist/errors.d.ts +67 -10
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +44 -10
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +12 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -28
- package/dist/index.js.map +1 -1
- package/dist/layer.d.ts +22 -10
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +97 -39
- package/dist/layer.js.map +1 -1
- package/dist/mappers/anchor.d.ts +28 -0
- package/dist/mappers/anchor.d.ts.map +1 -0
- package/dist/mappers/anchor.js +105 -0
- package/dist/mappers/anchor.js.map +1 -0
- package/dist/mappers/candidate.d.ts +25 -0
- package/dist/mappers/candidate.d.ts.map +1 -0
- package/dist/mappers/candidate.js +83 -0
- package/dist/mappers/candidate.js.map +1 -0
- package/dist/mappers/doc.d.ts +2 -4
- package/dist/mappers/doc.d.ts.map +1 -1
- package/dist/mappers/doc.js +7 -4
- package/dist/mappers/doc.js.map +1 -1
- package/dist/mappers/edge.d.ts +19 -0
- package/dist/mappers/edge.d.ts.map +1 -0
- package/dist/mappers/edge.js +81 -0
- package/dist/mappers/edge.js.map +1 -0
- package/dist/mappers/index.d.ts +7 -3
- package/dist/mappers/index.d.ts.map +1 -1
- package/dist/mappers/index.js +14 -6
- package/dist/mappers/index.js.map +1 -1
- package/dist/mappers/message.d.ts +15 -0
- package/dist/mappers/message.d.ts.map +1 -0
- package/dist/mappers/message.js +58 -0
- package/dist/mappers/message.js.map +1 -0
- package/dist/repo/anchor-repo.d.ts +52 -0
- package/dist/repo/anchor-repo.d.ts.map +1 -0
- package/dist/repo/anchor-repo.js +245 -0
- package/dist/repo/anchor-repo.js.map +1 -0
- package/dist/repo/candidate-repo.d.ts +16 -0
- package/dist/repo/candidate-repo.d.ts.map +1 -0
- package/dist/repo/candidate-repo.js +164 -0
- package/dist/repo/candidate-repo.js.map +1 -0
- package/dist/repo/compaction-repo.d.ts +41 -0
- package/dist/repo/compaction-repo.d.ts.map +1 -0
- package/dist/repo/compaction-repo.js +84 -0
- package/dist/repo/compaction-repo.js.map +1 -0
- package/dist/repo/doc-repo.d.ts +68 -51
- package/dist/repo/doc-repo.d.ts.map +1 -1
- package/dist/repo/doc-repo.js +120 -54
- package/dist/repo/doc-repo.js.map +1 -1
- package/dist/repo/edge-repo.d.ts +26 -0
- package/dist/repo/edge-repo.d.ts.map +1 -0
- package/dist/repo/edge-repo.js +258 -0
- package/dist/repo/edge-repo.js.map +1 -0
- package/dist/repo/index.d.ts +8 -3
- package/dist/repo/index.d.ts.map +1 -1
- package/dist/repo/index.js +7 -2
- package/dist/repo/index.js.map +1 -1
- package/dist/repo/message-repo.d.ts +55 -0
- package/dist/repo/message-repo.d.ts.map +1 -0
- package/dist/repo/message-repo.js +132 -0
- package/dist/repo/message-repo.js.map +1 -0
- package/dist/services/agent-service.d.ts +18 -23
- package/dist/services/agent-service.d.ts.map +1 -1
- package/dist/services/agent-service.js +9 -0
- package/dist/services/agent-service.js.map +1 -1
- package/dist/services/anchor-service.d.ts +147 -0
- package/dist/services/anchor-service.d.ts.map +1 -0
- package/dist/services/anchor-service.js +540 -0
- package/dist/services/anchor-service.js.map +1 -0
- package/dist/services/anchor-verification.d.ts +102 -0
- package/dist/services/anchor-verification.d.ts.map +1 -0
- package/dist/services/anchor-verification.js +817 -0
- package/dist/services/anchor-verification.js.map +1 -0
- package/dist/services/ast-grep-service.d.ts +58 -0
- package/dist/services/ast-grep-service.d.ts.map +1 -0
- package/dist/services/ast-grep-service.js +427 -0
- package/dist/services/ast-grep-service.js.map +1 -0
- package/dist/services/attempt-service.d.ts.map +1 -1
- package/dist/services/attempt-service.js +4 -1
- package/dist/services/attempt-service.js.map +1 -1
- package/dist/services/auto-sync-service.d.ts.map +1 -1
- package/dist/services/auto-sync-service.js +7 -7
- package/dist/services/auto-sync-service.js.map +1 -1
- package/dist/services/candidate-extractor-service.d.ts +44 -0
- package/dist/services/candidate-extractor-service.d.ts.map +1 -0
- package/dist/services/candidate-extractor-service.js +175 -0
- package/dist/services/candidate-extractor-service.js.map +1 -0
- package/dist/services/claim-service.d.ts.map +1 -1
- package/dist/services/claim-service.js +0 -8
- package/dist/services/claim-service.js.map +1 -1
- package/dist/services/compaction-service.d.ts +105 -0
- package/dist/services/compaction-service.d.ts.map +1 -0
- package/dist/services/compaction-service.js +281 -0
- package/dist/services/compaction-service.js.map +1 -0
- package/dist/services/cycle-scan-service.d.ts +1 -5
- package/dist/services/cycle-scan-service.d.ts.map +1 -1
- package/dist/services/cycle-scan-service.js +49 -19
- package/dist/services/cycle-scan-service.js.map +1 -1
- package/dist/services/daemon-service.d.ts +2 -8
- package/dist/services/daemon-service.d.ts.map +1 -1
- package/dist/services/daemon-service.js +21 -35
- package/dist/services/daemon-service.js.map +1 -1
- package/dist/services/doc-service.d.ts +25 -32
- package/dist/services/doc-service.d.ts.map +1 -1
- package/dist/services/doc-service.js +206 -190
- package/dist/services/doc-service.js.map +1 -1
- package/dist/services/edge-service.d.ts +78 -0
- package/dist/services/edge-service.d.ts.map +1 -0
- package/dist/services/edge-service.js +158 -0
- package/dist/services/edge-service.js.map +1 -0
- package/dist/services/embedding-service.d.ts +2 -2
- package/dist/services/embedding-service.d.ts.map +1 -1
- package/dist/services/embedding-service.js +7 -13
- package/dist/services/embedding-service.js.map +1 -1
- package/dist/services/feedback-tracker.d.ts +64 -0
- package/dist/services/feedback-tracker.d.ts.map +1 -0
- package/dist/services/feedback-tracker.js +110 -0
- package/dist/services/feedback-tracker.js.map +1 -0
- package/dist/services/file-watcher-service.d.ts.map +1 -1
- package/dist/services/file-watcher-service.js +1 -3
- package/dist/services/file-watcher-service.js.map +1 -1
- package/dist/services/graph-expansion.d.ts +158 -0
- package/dist/services/graph-expansion.d.ts.map +1 -0
- package/dist/services/graph-expansion.js +487 -0
- package/dist/services/graph-expansion.js.map +1 -0
- package/dist/services/index.d.ts +19 -8
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +18 -7
- package/dist/services/index.js.map +1 -1
- package/dist/services/learning-service.d.ts.map +1 -1
- package/dist/services/learning-service.js +22 -14
- package/dist/services/learning-service.js.map +1 -1
- package/dist/services/llm-service.d.ts +61 -39
- package/dist/services/llm-service.d.ts.map +1 -1
- package/dist/services/llm-service.js +199 -113
- package/dist/services/llm-service.js.map +1 -1
- package/dist/services/message-service.d.ts +57 -0
- package/dist/services/message-service.d.ts.map +1 -0
- package/dist/services/message-service.js +78 -0
- package/dist/services/message-service.js.map +1 -0
- package/dist/services/orchestrator-service.d.ts.map +1 -1
- package/dist/services/orchestrator-service.js +19 -20
- package/dist/services/orchestrator-service.js.map +1 -1
- package/dist/services/promotion-service.d.ts +67 -0
- package/dist/services/promotion-service.d.ts.map +1 -0
- package/dist/services/promotion-service.js +151 -0
- package/dist/services/promotion-service.js.map +1 -0
- package/dist/services/query-expansion-service.d.ts +7 -22
- package/dist/services/query-expansion-service.d.ts.map +1 -1
- package/dist/services/query-expansion-service.js +41 -75
- package/dist/services/query-expansion-service.js.map +1 -1
- package/dist/services/retriever-service.d.ts +8 -5
- package/dist/services/retriever-service.d.ts.map +1 -1
- package/dist/services/retriever-service.js +150 -15
- package/dist/services/retriever-service.js.map +1 -1
- package/dist/services/swarm-verification.d.ts +104 -0
- package/dist/services/swarm-verification.d.ts.map +1 -0
- package/dist/services/swarm-verification.js +406 -0
- package/dist/services/swarm-verification.js.map +1 -0
- package/dist/services/sync-service.d.ts.map +1 -1
- package/dist/services/sync-service.js +8 -9
- package/dist/services/sync-service.js.map +1 -1
- package/dist/services/task-service.d.ts.map +1 -1
- package/dist/services/task-service.js +3 -8
- package/dist/services/task-service.js.map +1 -1
- package/dist/services/tracing-service.js +6 -6
- package/dist/services/tracing-service.js.map +1 -1
- package/dist/services/transcript-adapter.d.ts.map +1 -1
- package/dist/services/transcript-adapter.js +1 -1
- package/dist/services/transcript-adapter.js.map +1 -1
- package/dist/services/worker-process.d.ts.map +1 -1
- package/dist/services/worker-process.js +8 -30
- package/dist/services/worker-process.js.map +1 -1
- package/dist/utils/doc-hash.d.ts +0 -4
- package/dist/utils/doc-hash.d.ts.map +1 -1
- package/dist/utils/doc-hash.js.map +1 -1
- package/dist/utils/doc-renderer.d.ts +31 -26
- package/dist/utils/doc-renderer.d.ts.map +1 -1
- package/dist/utils/doc-renderer.js +0 -7
- package/dist/utils/doc-renderer.js.map +1 -1
- package/dist/utils/llm-json.d.ts +17 -0
- package/dist/utils/llm-json.d.ts.map +1 -0
- package/dist/utils/llm-json.js +51 -0
- package/dist/utils/llm-json.js.map +1 -0
- package/dist/utils/toml-config.d.ts +10 -16
- package/dist/utils/toml-config.d.ts.map +1 -1
- package/dist/utils/toml-config.js +3 -1
- package/dist/utils/toml-config.js.map +1 -1
- package/dist/worker/run-worker.d.ts.map +1 -1
- package/dist/worker/run-worker.js +2 -7
- package/dist/worker/run-worker.js.map +1 -1
- package/package.json +9 -8
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnchorVerificationService - Periodic anchor verification for PRD-017
|
|
3
|
+
*
|
|
4
|
+
* Verifies anchors by checking actual file system state:
|
|
5
|
+
* - glob: Check if glob pattern still matches files
|
|
6
|
+
* - hash: Compute content hash and compare
|
|
7
|
+
* - symbol: Grep-based check for symbol presence
|
|
8
|
+
* - line_range: Verify file exists and has enough lines
|
|
9
|
+
*
|
|
10
|
+
* Updates anchor status (valid/drifted/invalid) and logs all changes
|
|
11
|
+
* to invalidation_log via AnchorRepository.
|
|
12
|
+
*
|
|
13
|
+
* @see docs/prd/PRD-017-invalidation-maintenance.md
|
|
14
|
+
* @see docs/design/DD-017-invalidation-maintenance.md
|
|
15
|
+
*/
|
|
16
|
+
import { Config, Context, Effect, Layer } from "effect";
|
|
17
|
+
import * as fs from "node:fs/promises";
|
|
18
|
+
import * as crypto from "node:crypto";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { AnchorRepository } from "../repo/anchor-repo.js";
|
|
21
|
+
import { matchesGlob } from "../utils/glob.js";
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Service Definition
|
|
24
|
+
// =============================================================================
|
|
25
|
+
export class AnchorVerificationService extends Context.Tag("AnchorVerificationService")() {
|
|
26
|
+
}
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Configuration
|
|
29
|
+
// =============================================================================
|
|
30
|
+
/** Default anchor cache TTL in seconds (1 hour) */
|
|
31
|
+
export const DEFAULT_ANCHOR_CACHE_TTL = 3600;
|
|
32
|
+
/**
|
|
33
|
+
* Get the anchor cache TTL from TX_ANCHOR_CACHE_TTL env var.
|
|
34
|
+
* Used by lazy verification to determine staleness.
|
|
35
|
+
*
|
|
36
|
+
* @returns Effect yielding TTL in seconds (default: 3600)
|
|
37
|
+
*/
|
|
38
|
+
export const getAnchorTTL = () => Config.number("TX_ANCHOR_CACHE_TTL").pipe(Config.withDefault(DEFAULT_ANCHOR_CACHE_TTL), Effect.catchAll(() => Effect.succeed(DEFAULT_ANCHOR_CACHE_TTL)));
|
|
39
|
+
/**
|
|
40
|
+
* Check if an anchor is stale based on its verified_at timestamp and TTL.
|
|
41
|
+
* An anchor is stale if:
|
|
42
|
+
* - verified_at is null (never verified), or
|
|
43
|
+
* - verified_at is older than (now - TTL)
|
|
44
|
+
*
|
|
45
|
+
* @param anchor - The anchor to check
|
|
46
|
+
* @param ttlSeconds - TTL in seconds (from getAnchorTTL)
|
|
47
|
+
* @returns true if anchor is stale and needs verification
|
|
48
|
+
*/
|
|
49
|
+
export const isStale = (anchor, ttlSeconds) => {
|
|
50
|
+
if (!anchor.verifiedAt) {
|
|
51
|
+
return true; // Never verified = stale
|
|
52
|
+
}
|
|
53
|
+
const verifiedAtMs = anchor.verifiedAt.getTime();
|
|
54
|
+
const ttlMs = ttlSeconds * 1000;
|
|
55
|
+
const cutoffMs = Date.now() - ttlMs;
|
|
56
|
+
return verifiedAtMs < cutoffMs;
|
|
57
|
+
};
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// Utility Functions
|
|
60
|
+
// =============================================================================
|
|
61
|
+
/** Compute SHA256 hash of content */
|
|
62
|
+
const computeContentHash = (content) => crypto.createHash("sha256").update(content).digest("hex");
|
|
63
|
+
/** Check if file exists */
|
|
64
|
+
const fileExists = (filePath) => Effect.tryPromise({
|
|
65
|
+
try: async () => {
|
|
66
|
+
await fs.access(filePath);
|
|
67
|
+
return true;
|
|
68
|
+
},
|
|
69
|
+
catch: () => false
|
|
70
|
+
}).pipe(Effect.catchAll(() => Effect.succeed(false)));
|
|
71
|
+
/** Read file content */
|
|
72
|
+
const readFile = (filePath) => Effect.tryPromise({
|
|
73
|
+
try: async () => {
|
|
74
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
75
|
+
return content;
|
|
76
|
+
},
|
|
77
|
+
catch: () => null
|
|
78
|
+
}).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
79
|
+
/** Read specific line range from file */
|
|
80
|
+
const readLineRange = (filePath, lineStart, lineEnd) => Effect.gen(function* () {
|
|
81
|
+
// Validate line numbers are positive (1-indexed)
|
|
82
|
+
if (lineStart < 1 || lineEnd < 1)
|
|
83
|
+
return null;
|
|
84
|
+
// Validate range is valid (end >= start)
|
|
85
|
+
if (lineEnd < lineStart)
|
|
86
|
+
return null;
|
|
87
|
+
const content = yield* readFile(filePath);
|
|
88
|
+
if (!content)
|
|
89
|
+
return null;
|
|
90
|
+
const lines = content.split("\n");
|
|
91
|
+
if (lineStart > lines.length)
|
|
92
|
+
return null;
|
|
93
|
+
const end = Math.min(lineEnd, lines.length);
|
|
94
|
+
return lines.slice(lineStart - 1, end).join("\n");
|
|
95
|
+
});
|
|
96
|
+
/** Count lines in file */
|
|
97
|
+
const countLines = (filePath) => Effect.gen(function* () {
|
|
98
|
+
const content = yield* readFile(filePath);
|
|
99
|
+
if (!content)
|
|
100
|
+
return 0;
|
|
101
|
+
return content.split("\n").length;
|
|
102
|
+
});
|
|
103
|
+
/** Escape regex special characters in a string */
|
|
104
|
+
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
105
|
+
/** Check if a symbol exists in file (grep-based) */
|
|
106
|
+
const symbolExistsInFile = (filePath, symbolName) => Effect.gen(function* () {
|
|
107
|
+
const content = yield* readFile(filePath);
|
|
108
|
+
if (!content)
|
|
109
|
+
return false;
|
|
110
|
+
// Extract just the symbol name from FQName (e.g., "file.ts::ClassName" -> "ClassName")
|
|
111
|
+
// Uses lastIndexOf+slice instead of split+pop to avoid:
|
|
112
|
+
// - Array allocation for a single value extraction
|
|
113
|
+
// - The ?? fallback not catching empty strings (pop() returns "" for "file.ts::", and "" ?? x === "")
|
|
114
|
+
const lastSep = symbolName.lastIndexOf("::");
|
|
115
|
+
const simpleName = lastSep >= 0
|
|
116
|
+
? symbolName.slice(lastSep + 2) || symbolName
|
|
117
|
+
: symbolName;
|
|
118
|
+
// If symbol name is empty or whitespace-only, symbol is not found
|
|
119
|
+
// This prevents false positives where regex would match any declaration
|
|
120
|
+
if (!simpleName || simpleName.trim().length === 0) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
// Escape regex special characters to prevent injection and false matches
|
|
124
|
+
const escapedName = escapeRegex(simpleName.trim());
|
|
125
|
+
// Check for common patterns: function, class, const, export, interface, type
|
|
126
|
+
// Use explicit boundaries (?<![...]) and (?![...]) instead of \b because
|
|
127
|
+
// \b doesn't work with $ (not a word char), causing symbols like $store to fail
|
|
128
|
+
const idStart = `(?<![a-zA-Z0-9_$])`; // negative lookbehind for identifier chars
|
|
129
|
+
const idEnd = `(?![a-zA-Z0-9_$])`; // negative lookahead for identifier chars
|
|
130
|
+
const patterns = [
|
|
131
|
+
new RegExp(`\\b(function|const|let|var)\\s+${escapedName}${idEnd}`),
|
|
132
|
+
new RegExp(`\\bclass\\s+${escapedName}${idEnd}`),
|
|
133
|
+
new RegExp(`\\binterface\\s+${escapedName}${idEnd}`),
|
|
134
|
+
new RegExp(`\\btype\\s+${escapedName}${idEnd}`),
|
|
135
|
+
new RegExp(`\\bexport\\s+(default\\s+)?(function|class|const|let|var|interface|type)\\s+${escapedName}${idEnd}`),
|
|
136
|
+
new RegExp(`\\bexport\\s+\\{[^}]*${idStart}${escapedName}${idEnd}[^}]*\\}`) // export { Symbol }
|
|
137
|
+
];
|
|
138
|
+
return patterns.some(p => p.test(content));
|
|
139
|
+
});
|
|
140
|
+
// =============================================================================
|
|
141
|
+
// Self-Healing Utilities (PRD-017)
|
|
142
|
+
// =============================================================================
|
|
143
|
+
/** Default threshold for self-healing (80% similarity) */
|
|
144
|
+
const SELF_HEAL_THRESHOLD = 0.8;
|
|
145
|
+
/** Maximum content preview length */
|
|
146
|
+
const MAX_PREVIEW_LENGTH = 500;
|
|
147
|
+
/**
|
|
148
|
+
* Tokenize text for Jaccard similarity computation.
|
|
149
|
+
* Extracts words/identifiers, lowercased, filtering short tokens.
|
|
150
|
+
*/
|
|
151
|
+
const tokenize = (text) => {
|
|
152
|
+
return new Set(text
|
|
153
|
+
.toLowerCase()
|
|
154
|
+
.replace(/[^\w\s]/g, " ")
|
|
155
|
+
.split(/\s+/)
|
|
156
|
+
.filter(t => t.length > 2));
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Compute Jaccard similarity between two sets of tokens.
|
|
160
|
+
* Returns value between 0 (completely different) and 1 (identical).
|
|
161
|
+
*/
|
|
162
|
+
const jaccardSimilarity = (a, b) => {
|
|
163
|
+
if (a.size === 0 && b.size === 0)
|
|
164
|
+
return 1;
|
|
165
|
+
if (a.size === 0 || b.size === 0)
|
|
166
|
+
return 0;
|
|
167
|
+
const intersection = new Set([...a].filter(x => b.has(x)));
|
|
168
|
+
const union = new Set([...a, ...b]);
|
|
169
|
+
return union.size === 0 ? 0 : intersection.size / union.size;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Compute Jaccard similarity between two text strings.
|
|
173
|
+
*/
|
|
174
|
+
const computeSimilarity = (oldContent, newContent) => {
|
|
175
|
+
const oldTokens = tokenize(oldContent);
|
|
176
|
+
const newTokens = tokenize(newContent);
|
|
177
|
+
return jaccardSimilarity(oldTokens, newTokens);
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Create a content preview (truncated content for storage/comparison).
|
|
181
|
+
*/
|
|
182
|
+
const createContentPreview = (content) => {
|
|
183
|
+
if (content.length <= MAX_PREVIEW_LENGTH)
|
|
184
|
+
return content;
|
|
185
|
+
return content.slice(0, MAX_PREVIEW_LENGTH);
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Try to self-heal a drifted anchor by comparing content similarity.
|
|
189
|
+
* If the new content is similar enough (>= 0.8 Jaccard), update the anchor
|
|
190
|
+
* with the new hash and preview.
|
|
191
|
+
*/
|
|
192
|
+
const trySelfHeal = (anchor, newContent, newHash, anchorRepo) => Effect.gen(function* () {
|
|
193
|
+
// If no content preview stored, we can't compare - can't self-heal
|
|
194
|
+
if (!anchor.contentPreview) {
|
|
195
|
+
return { healed: false, similarity: 0 };
|
|
196
|
+
}
|
|
197
|
+
// Compute similarity between old preview and new content
|
|
198
|
+
const similarity = computeSimilarity(anchor.contentPreview, newContent);
|
|
199
|
+
// If similarity is above threshold, self-heal
|
|
200
|
+
if (similarity >= SELF_HEAL_THRESHOLD) {
|
|
201
|
+
const newPreview = createContentPreview(newContent);
|
|
202
|
+
// Update anchor with new hash and preview
|
|
203
|
+
yield* anchorRepo.update(anchor.id, {
|
|
204
|
+
contentHash: newHash,
|
|
205
|
+
contentPreview: newPreview,
|
|
206
|
+
verifiedAt: new Date()
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
healed: true,
|
|
210
|
+
similarity,
|
|
211
|
+
newHash,
|
|
212
|
+
newPreview
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// Similarity too low - can't self-heal
|
|
216
|
+
return { healed: false, similarity };
|
|
217
|
+
});
|
|
218
|
+
// =============================================================================
|
|
219
|
+
// Service Implementation
|
|
220
|
+
// =============================================================================
|
|
221
|
+
export const AnchorVerificationServiceLive = Layer.effect(AnchorVerificationService, Effect.gen(function* () {
|
|
222
|
+
const anchorRepo = yield* AnchorRepository;
|
|
223
|
+
/**
|
|
224
|
+
* Verify a single anchor against the actual file system.
|
|
225
|
+
*/
|
|
226
|
+
const verifyAnchor = (anchor, detectedBy, baseDir) => Effect.gen(function* () {
|
|
227
|
+
const oldStatus = anchor.status;
|
|
228
|
+
// Skip pinned anchors
|
|
229
|
+
if (anchor.pinned) {
|
|
230
|
+
return {
|
|
231
|
+
anchorId: anchor.id,
|
|
232
|
+
previousStatus: oldStatus,
|
|
233
|
+
newStatus: oldStatus,
|
|
234
|
+
action: "unchanged"
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// Resolve full file path and validate it stays within baseDir
|
|
238
|
+
const resolvedBase = path.resolve(baseDir);
|
|
239
|
+
const fullPath = path.resolve(resolvedBase, anchor.filePath);
|
|
240
|
+
if (!fullPath.startsWith(resolvedBase + path.sep) && fullPath !== resolvedBase) {
|
|
241
|
+
yield* anchorRepo.updateStatus(anchor.id, "invalid");
|
|
242
|
+
yield* anchorRepo.logInvalidation({
|
|
243
|
+
anchorId: anchor.id,
|
|
244
|
+
oldStatus,
|
|
245
|
+
newStatus: "invalid",
|
|
246
|
+
reason: "path_traversal_rejected",
|
|
247
|
+
detectedBy
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
anchorId: anchor.id,
|
|
251
|
+
previousStatus: oldStatus,
|
|
252
|
+
newStatus: "invalid",
|
|
253
|
+
action: "invalidated",
|
|
254
|
+
reason: "path_traversal_rejected"
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// Step 1: Check if file exists
|
|
258
|
+
const exists = yield* fileExists(fullPath);
|
|
259
|
+
if (!exists) {
|
|
260
|
+
// File deleted - mark as invalid
|
|
261
|
+
yield* anchorRepo.updateStatus(anchor.id, "invalid");
|
|
262
|
+
yield* anchorRepo.logInvalidation({
|
|
263
|
+
anchorId: anchor.id,
|
|
264
|
+
oldStatus,
|
|
265
|
+
newStatus: "invalid",
|
|
266
|
+
reason: "file_deleted",
|
|
267
|
+
detectedBy
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
anchorId: anchor.id,
|
|
271
|
+
previousStatus: oldStatus,
|
|
272
|
+
newStatus: "invalid",
|
|
273
|
+
action: "invalidated",
|
|
274
|
+
reason: "file_deleted"
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// Step 2: Type-specific verification
|
|
278
|
+
switch (anchor.anchorType) {
|
|
279
|
+
case "glob": {
|
|
280
|
+
// For glob anchors, the file exists and matches - should be valid
|
|
281
|
+
// If previously drifted/invalid, recover to valid
|
|
282
|
+
if (oldStatus !== "valid") {
|
|
283
|
+
yield* anchorRepo.updateStatus(anchor.id, "valid");
|
|
284
|
+
yield* anchorRepo.logInvalidation({
|
|
285
|
+
anchorId: anchor.id,
|
|
286
|
+
oldStatus,
|
|
287
|
+
newStatus: "valid",
|
|
288
|
+
reason: "recovered",
|
|
289
|
+
detectedBy
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
anchorId: anchor.id,
|
|
293
|
+
previousStatus: oldStatus,
|
|
294
|
+
newStatus: "valid",
|
|
295
|
+
action: "self_healed",
|
|
296
|
+
reason: "file_restored"
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
yield* anchorRepo.updateVerifiedAt(anchor.id);
|
|
300
|
+
return {
|
|
301
|
+
anchorId: anchor.id,
|
|
302
|
+
previousStatus: oldStatus,
|
|
303
|
+
newStatus: oldStatus,
|
|
304
|
+
action: "unchanged"
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
case "hash": {
|
|
308
|
+
// Check content hash
|
|
309
|
+
// Use explicit null checks: 0 is an invalid line number, not "no line range"
|
|
310
|
+
if (anchor.lineStart == null || anchor.lineEnd == null) {
|
|
311
|
+
// No line range specified, read whole file
|
|
312
|
+
const content = yield* readFile(fullPath);
|
|
313
|
+
if (!content) {
|
|
314
|
+
yield* anchorRepo.updateStatus(anchor.id, "invalid");
|
|
315
|
+
yield* anchorRepo.logInvalidation({
|
|
316
|
+
anchorId: anchor.id,
|
|
317
|
+
oldStatus,
|
|
318
|
+
newStatus: "invalid",
|
|
319
|
+
reason: "content_read_failed",
|
|
320
|
+
detectedBy
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
anchorId: anchor.id,
|
|
324
|
+
previousStatus: oldStatus,
|
|
325
|
+
newStatus: "invalid",
|
|
326
|
+
action: "invalidated",
|
|
327
|
+
reason: "content_read_failed"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const newHash = computeContentHash(content);
|
|
331
|
+
// If no stored hash, initialize it with current content (can't verify without baseline)
|
|
332
|
+
if (!anchor.contentHash) {
|
|
333
|
+
const newPreview = createContentPreview(content);
|
|
334
|
+
yield* anchorRepo.update(anchor.id, {
|
|
335
|
+
contentHash: newHash,
|
|
336
|
+
contentPreview: newPreview,
|
|
337
|
+
verifiedAt: new Date()
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
anchorId: anchor.id,
|
|
341
|
+
previousStatus: oldStatus,
|
|
342
|
+
newStatus: oldStatus,
|
|
343
|
+
action: "unchanged"
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (newHash === anchor.contentHash) {
|
|
347
|
+
// Hash matches - if previously drifted/invalid, recover to valid
|
|
348
|
+
if (oldStatus !== "valid") {
|
|
349
|
+
yield* anchorRepo.updateStatus(anchor.id, "valid");
|
|
350
|
+
yield* anchorRepo.logInvalidation({
|
|
351
|
+
anchorId: anchor.id,
|
|
352
|
+
oldStatus,
|
|
353
|
+
newStatus: "valid",
|
|
354
|
+
reason: "recovered",
|
|
355
|
+
detectedBy
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
anchorId: anchor.id,
|
|
359
|
+
previousStatus: oldStatus,
|
|
360
|
+
newStatus: "valid",
|
|
361
|
+
action: "self_healed",
|
|
362
|
+
reason: "content_restored"
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
yield* anchorRepo.updateVerifiedAt(anchor.id);
|
|
366
|
+
return {
|
|
367
|
+
anchorId: anchor.id,
|
|
368
|
+
previousStatus: oldStatus,
|
|
369
|
+
newStatus: oldStatus,
|
|
370
|
+
action: "unchanged"
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
// Hash mismatch - try self-healing if we have content preview
|
|
374
|
+
const healResult = yield* trySelfHeal(anchor, content, newHash, anchorRepo);
|
|
375
|
+
if (healResult.healed) {
|
|
376
|
+
// Successfully self-healed - update status to valid
|
|
377
|
+
yield* anchorRepo.updateStatus(anchor.id, "valid");
|
|
378
|
+
yield* anchorRepo.logInvalidation({
|
|
379
|
+
anchorId: anchor.id,
|
|
380
|
+
oldStatus,
|
|
381
|
+
newStatus: "valid",
|
|
382
|
+
reason: "self_healed",
|
|
383
|
+
detectedBy,
|
|
384
|
+
oldContentHash: anchor.contentHash,
|
|
385
|
+
newContentHash: newHash,
|
|
386
|
+
similarityScore: healResult.similarity
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
anchorId: anchor.id,
|
|
390
|
+
previousStatus: oldStatus,
|
|
391
|
+
newStatus: "valid",
|
|
392
|
+
action: "self_healed",
|
|
393
|
+
reason: "content_similar",
|
|
394
|
+
similarity: healResult.similarity,
|
|
395
|
+
oldContentHash: anchor.contentHash,
|
|
396
|
+
newContentHash: newHash
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
// Could not self-heal - mark as drifted
|
|
400
|
+
yield* anchorRepo.updateStatus(anchor.id, "drifted");
|
|
401
|
+
yield* anchorRepo.logInvalidation({
|
|
402
|
+
anchorId: anchor.id,
|
|
403
|
+
oldStatus,
|
|
404
|
+
newStatus: "drifted",
|
|
405
|
+
reason: "hash_mismatch",
|
|
406
|
+
detectedBy,
|
|
407
|
+
oldContentHash: anchor.contentHash,
|
|
408
|
+
newContentHash: newHash,
|
|
409
|
+
similarityScore: healResult.similarity
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
anchorId: anchor.id,
|
|
413
|
+
previousStatus: oldStatus,
|
|
414
|
+
newStatus: "drifted",
|
|
415
|
+
action: "drifted",
|
|
416
|
+
reason: "hash_mismatch",
|
|
417
|
+
similarity: healResult.similarity,
|
|
418
|
+
oldContentHash: anchor.contentHash,
|
|
419
|
+
newContentHash: newHash
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
// Read line range
|
|
423
|
+
const content = yield* readLineRange(fullPath, anchor.lineStart, anchor.lineEnd);
|
|
424
|
+
if (!content) {
|
|
425
|
+
yield* anchorRepo.updateStatus(anchor.id, "invalid");
|
|
426
|
+
yield* anchorRepo.logInvalidation({
|
|
427
|
+
anchorId: anchor.id,
|
|
428
|
+
oldStatus,
|
|
429
|
+
newStatus: "invalid",
|
|
430
|
+
reason: "line_range_invalid",
|
|
431
|
+
detectedBy
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
anchorId: anchor.id,
|
|
435
|
+
previousStatus: oldStatus,
|
|
436
|
+
newStatus: "invalid",
|
|
437
|
+
action: "invalidated",
|
|
438
|
+
reason: "line_range_invalid"
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const newHash = computeContentHash(content);
|
|
442
|
+
// If no stored hash, initialize it with current content (can't verify without baseline)
|
|
443
|
+
if (!anchor.contentHash) {
|
|
444
|
+
const newPreview = createContentPreview(content);
|
|
445
|
+
yield* anchorRepo.update(anchor.id, {
|
|
446
|
+
contentHash: newHash,
|
|
447
|
+
contentPreview: newPreview,
|
|
448
|
+
verifiedAt: new Date()
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
anchorId: anchor.id,
|
|
452
|
+
previousStatus: oldStatus,
|
|
453
|
+
newStatus: oldStatus,
|
|
454
|
+
action: "unchanged"
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (newHash === anchor.contentHash) {
|
|
458
|
+
// Hash matches - if previously drifted/invalid, recover to valid
|
|
459
|
+
if (oldStatus !== "valid") {
|
|
460
|
+
yield* anchorRepo.updateStatus(anchor.id, "valid");
|
|
461
|
+
yield* anchorRepo.logInvalidation({
|
|
462
|
+
anchorId: anchor.id,
|
|
463
|
+
oldStatus,
|
|
464
|
+
newStatus: "valid",
|
|
465
|
+
reason: "recovered",
|
|
466
|
+
detectedBy
|
|
467
|
+
});
|
|
468
|
+
return {
|
|
469
|
+
anchorId: anchor.id,
|
|
470
|
+
previousStatus: oldStatus,
|
|
471
|
+
newStatus: "valid",
|
|
472
|
+
action: "self_healed",
|
|
473
|
+
reason: "content_restored"
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
yield* anchorRepo.updateVerifiedAt(anchor.id);
|
|
477
|
+
return {
|
|
478
|
+
anchorId: anchor.id,
|
|
479
|
+
previousStatus: oldStatus,
|
|
480
|
+
newStatus: oldStatus,
|
|
481
|
+
action: "unchanged"
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
// Hash mismatch - try self-healing if we have content preview
|
|
485
|
+
const healResult = yield* trySelfHeal(anchor, content, newHash, anchorRepo);
|
|
486
|
+
if (healResult.healed) {
|
|
487
|
+
// Successfully self-healed - update status to valid
|
|
488
|
+
yield* anchorRepo.updateStatus(anchor.id, "valid");
|
|
489
|
+
yield* anchorRepo.logInvalidation({
|
|
490
|
+
anchorId: anchor.id,
|
|
491
|
+
oldStatus,
|
|
492
|
+
newStatus: "valid",
|
|
493
|
+
reason: "self_healed",
|
|
494
|
+
detectedBy,
|
|
495
|
+
oldContentHash: anchor.contentHash,
|
|
496
|
+
newContentHash: newHash,
|
|
497
|
+
similarityScore: healResult.similarity
|
|
498
|
+
});
|
|
499
|
+
return {
|
|
500
|
+
anchorId: anchor.id,
|
|
501
|
+
previousStatus: oldStatus,
|
|
502
|
+
newStatus: "valid",
|
|
503
|
+
action: "self_healed",
|
|
504
|
+
reason: "content_similar",
|
|
505
|
+
similarity: healResult.similarity,
|
|
506
|
+
oldContentHash: anchor.contentHash,
|
|
507
|
+
newContentHash: newHash
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
// Could not self-heal - mark as drifted
|
|
511
|
+
yield* anchorRepo.updateStatus(anchor.id, "drifted");
|
|
512
|
+
yield* anchorRepo.logInvalidation({
|
|
513
|
+
anchorId: anchor.id,
|
|
514
|
+
oldStatus,
|
|
515
|
+
newStatus: "drifted",
|
|
516
|
+
reason: "hash_mismatch",
|
|
517
|
+
detectedBy,
|
|
518
|
+
oldContentHash: anchor.contentHash,
|
|
519
|
+
newContentHash: newHash,
|
|
520
|
+
similarityScore: healResult.similarity
|
|
521
|
+
});
|
|
522
|
+
return {
|
|
523
|
+
anchorId: anchor.id,
|
|
524
|
+
previousStatus: oldStatus,
|
|
525
|
+
newStatus: "drifted",
|
|
526
|
+
action: "drifted",
|
|
527
|
+
reason: "hash_mismatch",
|
|
528
|
+
similarity: healResult.similarity,
|
|
529
|
+
oldContentHash: anchor.contentHash,
|
|
530
|
+
newContentHash: newHash
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
case "symbol": {
|
|
534
|
+
// Check if symbol exists in file
|
|
535
|
+
const symbolName = anchor.symbolFqname ?? anchor.anchorValue;
|
|
536
|
+
// If symbol name is empty or invalid, mark as invalid immediately
|
|
537
|
+
// This prevents false positives from regex matching any declaration
|
|
538
|
+
if (!symbolName || symbolName.trim().length === 0) {
|
|
539
|
+
yield* anchorRepo.updateStatus(anchor.id, "invalid");
|
|
540
|
+
yield* anchorRepo.logInvalidation({
|
|
541
|
+
anchorId: anchor.id,
|
|
542
|
+
oldStatus,
|
|
543
|
+
newStatus: "invalid",
|
|
544
|
+
reason: "symbol_name_invalid",
|
|
545
|
+
detectedBy
|
|
546
|
+
});
|
|
547
|
+
return {
|
|
548
|
+
anchorId: anchor.id,
|
|
549
|
+
previousStatus: oldStatus,
|
|
550
|
+
newStatus: "invalid",
|
|
551
|
+
action: "invalidated",
|
|
552
|
+
reason: "symbol_name_invalid"
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
const symbolExists = yield* symbolExistsInFile(fullPath, symbolName);
|
|
556
|
+
if (symbolExists) {
|
|
557
|
+
// Symbol found - if previously drifted/invalid, recover to valid
|
|
558
|
+
if (oldStatus !== "valid") {
|
|
559
|
+
yield* anchorRepo.updateStatus(anchor.id, "valid");
|
|
560
|
+
yield* anchorRepo.logInvalidation({
|
|
561
|
+
anchorId: anchor.id,
|
|
562
|
+
oldStatus,
|
|
563
|
+
newStatus: "valid",
|
|
564
|
+
reason: "recovered",
|
|
565
|
+
detectedBy
|
|
566
|
+
});
|
|
567
|
+
return {
|
|
568
|
+
anchorId: anchor.id,
|
|
569
|
+
previousStatus: oldStatus,
|
|
570
|
+
newStatus: "valid",
|
|
571
|
+
action: "self_healed",
|
|
572
|
+
reason: "symbol_restored"
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
yield* anchorRepo.updateVerifiedAt(anchor.id);
|
|
576
|
+
return {
|
|
577
|
+
anchorId: anchor.id,
|
|
578
|
+
previousStatus: oldStatus,
|
|
579
|
+
newStatus: oldStatus,
|
|
580
|
+
action: "unchanged"
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
// Symbol not found - mark as invalid
|
|
584
|
+
yield* anchorRepo.updateStatus(anchor.id, "invalid");
|
|
585
|
+
yield* anchorRepo.logInvalidation({
|
|
586
|
+
anchorId: anchor.id,
|
|
587
|
+
oldStatus,
|
|
588
|
+
newStatus: "invalid",
|
|
589
|
+
reason: "symbol_missing",
|
|
590
|
+
detectedBy
|
|
591
|
+
});
|
|
592
|
+
return {
|
|
593
|
+
anchorId: anchor.id,
|
|
594
|
+
previousStatus: oldStatus,
|
|
595
|
+
newStatus: "invalid",
|
|
596
|
+
action: "invalidated",
|
|
597
|
+
reason: "symbol_missing"
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
case "line_range": {
|
|
601
|
+
// Check if file has enough lines
|
|
602
|
+
const lineCount = yield* countLines(fullPath);
|
|
603
|
+
const requiredLines = anchor.lineEnd ?? anchor.lineStart ?? 1;
|
|
604
|
+
if (lineCount >= requiredLines) {
|
|
605
|
+
// Line count sufficient - if previously drifted/invalid, recover to valid
|
|
606
|
+
if (oldStatus !== "valid") {
|
|
607
|
+
yield* anchorRepo.updateStatus(anchor.id, "valid");
|
|
608
|
+
yield* anchorRepo.logInvalidation({
|
|
609
|
+
anchorId: anchor.id,
|
|
610
|
+
oldStatus,
|
|
611
|
+
newStatus: "valid",
|
|
612
|
+
reason: "recovered",
|
|
613
|
+
detectedBy
|
|
614
|
+
});
|
|
615
|
+
return {
|
|
616
|
+
anchorId: anchor.id,
|
|
617
|
+
previousStatus: oldStatus,
|
|
618
|
+
newStatus: "valid",
|
|
619
|
+
action: "self_healed",
|
|
620
|
+
reason: "line_count_restored"
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
yield* anchorRepo.updateVerifiedAt(anchor.id);
|
|
624
|
+
return {
|
|
625
|
+
anchorId: anchor.id,
|
|
626
|
+
previousStatus: oldStatus,
|
|
627
|
+
newStatus: oldStatus,
|
|
628
|
+
action: "unchanged"
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
// Not enough lines - mark as drifted (file changed but still exists)
|
|
632
|
+
yield* anchorRepo.updateStatus(anchor.id, "drifted");
|
|
633
|
+
yield* anchorRepo.logInvalidation({
|
|
634
|
+
anchorId: anchor.id,
|
|
635
|
+
oldStatus,
|
|
636
|
+
newStatus: "drifted",
|
|
637
|
+
reason: `line_count_insufficient (have ${lineCount}, need ${requiredLines})`,
|
|
638
|
+
detectedBy
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
anchorId: anchor.id,
|
|
642
|
+
previousStatus: oldStatus,
|
|
643
|
+
newStatus: "drifted",
|
|
644
|
+
action: "drifted",
|
|
645
|
+
reason: `line_count_insufficient`
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
default:
|
|
649
|
+
// Unknown anchor type - keep as is
|
|
650
|
+
yield* anchorRepo.updateVerifiedAt(anchor.id);
|
|
651
|
+
return {
|
|
652
|
+
anchorId: anchor.id,
|
|
653
|
+
previousStatus: oldStatus,
|
|
654
|
+
newStatus: oldStatus,
|
|
655
|
+
action: "unchanged"
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
/**
|
|
660
|
+
* Aggregate verification results into summary
|
|
661
|
+
*/
|
|
662
|
+
const aggregateResults = (results, failedAnchors, startTime) => {
|
|
663
|
+
let unchanged = 0;
|
|
664
|
+
let selfHealed = 0;
|
|
665
|
+
let drifted = 0;
|
|
666
|
+
let invalid = 0;
|
|
667
|
+
for (const result of results) {
|
|
668
|
+
switch (result.action) {
|
|
669
|
+
case "unchanged":
|
|
670
|
+
unchanged++;
|
|
671
|
+
break;
|
|
672
|
+
case "self_healed":
|
|
673
|
+
selfHealed++;
|
|
674
|
+
break;
|
|
675
|
+
case "drifted":
|
|
676
|
+
drifted++;
|
|
677
|
+
break;
|
|
678
|
+
case "invalidated":
|
|
679
|
+
invalid++;
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
total: results.length + failedAnchors.length,
|
|
685
|
+
unchanged,
|
|
686
|
+
selfHealed,
|
|
687
|
+
drifted,
|
|
688
|
+
invalid,
|
|
689
|
+
errors: failedAnchors.length,
|
|
690
|
+
duration: Date.now() - startTime,
|
|
691
|
+
failedAnchors
|
|
692
|
+
};
|
|
693
|
+
};
|
|
694
|
+
return {
|
|
695
|
+
verify: (anchorId, options = {}) => Effect.gen(function* () {
|
|
696
|
+
const anchor = yield* anchorRepo.findById(anchorId);
|
|
697
|
+
if (!anchor) {
|
|
698
|
+
// Return unchanged result for non-existent anchor
|
|
699
|
+
return {
|
|
700
|
+
anchorId,
|
|
701
|
+
previousStatus: "valid",
|
|
702
|
+
newStatus: "valid",
|
|
703
|
+
action: "unchanged",
|
|
704
|
+
reason: "anchor_not_found"
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
const detectedBy = options.detectedBy ?? "lazy";
|
|
708
|
+
const baseDir = options.baseDir ?? process.cwd();
|
|
709
|
+
return yield* verifyAnchor(anchor, detectedBy, baseDir);
|
|
710
|
+
}),
|
|
711
|
+
verifyAll: (options = {}) => Effect.gen(function* () {
|
|
712
|
+
const startTime = Date.now();
|
|
713
|
+
const detectedBy = options.detectedBy ?? "periodic";
|
|
714
|
+
const baseDir = options.baseDir ?? process.cwd();
|
|
715
|
+
const skipPinned = options.skipPinned ?? true;
|
|
716
|
+
const anchors = yield* anchorRepo.findAll(100_000);
|
|
717
|
+
const results = [];
|
|
718
|
+
const failedAnchors = [];
|
|
719
|
+
for (const anchor of anchors) {
|
|
720
|
+
if (skipPinned && anchor.pinned) {
|
|
721
|
+
results.push({
|
|
722
|
+
anchorId: anchor.id,
|
|
723
|
+
previousStatus: anchor.status,
|
|
724
|
+
newStatus: anchor.status,
|
|
725
|
+
action: "unchanged"
|
|
726
|
+
});
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const result = yield* verifyAnchor(anchor, detectedBy, baseDir).pipe(Effect.catchAll((error) => {
|
|
730
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
731
|
+
console.error(`[AnchorVerification] Failed to verify anchor ${anchor.id} (${anchor.filePath}): ${errorMessage}`);
|
|
732
|
+
failedAnchors.push({
|
|
733
|
+
anchorId: anchor.id,
|
|
734
|
+
filePath: anchor.filePath,
|
|
735
|
+
error: errorMessage
|
|
736
|
+
});
|
|
737
|
+
return Effect.succeed(null);
|
|
738
|
+
}));
|
|
739
|
+
if (result) {
|
|
740
|
+
results.push(result);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return aggregateResults(results, failedAnchors, startTime);
|
|
744
|
+
}),
|
|
745
|
+
verifyFile: (filePath, options = {}) => Effect.gen(function* () {
|
|
746
|
+
const startTime = Date.now();
|
|
747
|
+
const detectedBy = options.detectedBy ?? "manual";
|
|
748
|
+
const baseDir = options.baseDir ?? process.cwd();
|
|
749
|
+
const skipPinned = options.skipPinned ?? true;
|
|
750
|
+
const anchors = yield* anchorRepo.findByFilePath(filePath);
|
|
751
|
+
const results = [];
|
|
752
|
+
const failedAnchors = [];
|
|
753
|
+
for (const anchor of anchors) {
|
|
754
|
+
if (skipPinned && anchor.pinned) {
|
|
755
|
+
results.push({
|
|
756
|
+
anchorId: anchor.id,
|
|
757
|
+
previousStatus: anchor.status,
|
|
758
|
+
newStatus: anchor.status,
|
|
759
|
+
action: "unchanged"
|
|
760
|
+
});
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const result = yield* verifyAnchor(anchor, detectedBy, baseDir).pipe(Effect.catchAll((error) => {
|
|
764
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
765
|
+
console.error(`[AnchorVerification] Failed to verify anchor ${anchor.id} (${anchor.filePath}): ${errorMessage}`);
|
|
766
|
+
failedAnchors.push({
|
|
767
|
+
anchorId: anchor.id,
|
|
768
|
+
filePath: anchor.filePath,
|
|
769
|
+
error: errorMessage
|
|
770
|
+
});
|
|
771
|
+
return Effect.succeed(null);
|
|
772
|
+
}));
|
|
773
|
+
if (result) {
|
|
774
|
+
results.push(result);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return aggregateResults(results, failedAnchors, startTime);
|
|
778
|
+
}),
|
|
779
|
+
verifyGlob: (globPattern, options = {}) => Effect.gen(function* () {
|
|
780
|
+
const startTime = Date.now();
|
|
781
|
+
const detectedBy = options.detectedBy ?? "manual";
|
|
782
|
+
const baseDir = options.baseDir ?? process.cwd();
|
|
783
|
+
const skipPinned = options.skipPinned ?? true;
|
|
784
|
+
// Get all anchors and filter by glob pattern (explicit high limit for batch verification)
|
|
785
|
+
const allAnchors = yield* anchorRepo.findAll(100_000);
|
|
786
|
+
const matchingAnchors = allAnchors.filter(a => matchesGlob(a.filePath, globPattern));
|
|
787
|
+
const results = [];
|
|
788
|
+
const failedAnchors = [];
|
|
789
|
+
for (const anchor of matchingAnchors) {
|
|
790
|
+
if (skipPinned && anchor.pinned) {
|
|
791
|
+
results.push({
|
|
792
|
+
anchorId: anchor.id,
|
|
793
|
+
previousStatus: anchor.status,
|
|
794
|
+
newStatus: anchor.status,
|
|
795
|
+
action: "unchanged"
|
|
796
|
+
});
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
const result = yield* verifyAnchor(anchor, detectedBy, baseDir).pipe(Effect.catchAll((error) => {
|
|
800
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
801
|
+
console.error(`[AnchorVerification] Failed to verify anchor ${anchor.id} (${anchor.filePath}): ${errorMessage}`);
|
|
802
|
+
failedAnchors.push({
|
|
803
|
+
anchorId: anchor.id,
|
|
804
|
+
filePath: anchor.filePath,
|
|
805
|
+
error: errorMessage
|
|
806
|
+
});
|
|
807
|
+
return Effect.succeed(null);
|
|
808
|
+
}));
|
|
809
|
+
if (result) {
|
|
810
|
+
results.push(result);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return aggregateResults(results, failedAnchors, startTime);
|
|
814
|
+
})
|
|
815
|
+
};
|
|
816
|
+
}));
|
|
817
|
+
//# sourceMappingURL=anchor-verification.js.map
|