@psnext/lscg 0.1.5 → 0.1.6

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.
@@ -0,0 +1,184 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { parseMetadata, parsePoint } from './row-decoders.js';
3
+ const EXPLORE_KINDS = ['contains', 'defines', 'calls', 'imports', 'exports'];
4
+ export function selectExploreAggregates(db, repositoryId, search) {
5
+ const predicates = ['f.repository_id = ?'];
6
+ const params = [repositoryId];
7
+ if (search?.trim()) {
8
+ predicates.push('(lower(f.path) LIKE ? OR lower(f.path) = ?)');
9
+ const normalized = search.trim().toLocaleLowerCase();
10
+ params.push(`%${normalized}%`, normalized);
11
+ }
12
+ const rows = db.prepare(`
13
+ SELECT f.id, f.path, f.language, f.repository_id AS repositoryId,
14
+ (SELECT count(*) FROM nodes n WHERE n.repository_id = f.repository_id AND n.file_id = f.id) AS childCount
15
+ FROM files f
16
+ WHERE ${predicates.join(' AND ')}
17
+ ORDER BY lower(f.path), f.id
18
+ `).all(...params);
19
+ return rows.map((row) => ({ ...row, id: aggregateId(repositoryId, row.id), childCount: Number(row.childCount) }));
20
+ }
21
+ export function selectExploreAggregateEdges(db, repositoryId, kinds = EXPLORE_KINDS) {
22
+ const safeKinds = kinds.filter((kind) => EXPLORE_KINDS.includes(kind));
23
+ if (safeKinds.length === 0)
24
+ return [];
25
+ const placeholders = safeKinds.map(() => '?').join(', ');
26
+ const rows = db.prepare(`
27
+ SELECT MIN(e.id) AS id, e.kind,
28
+ 'aggregate:' || e.repository_id || ':' || sf.id AS sourceAggregateId,
29
+ 'aggregate:' || e.repository_id || ':' || tf.id AS targetAggregateId,
30
+ AVG(e.confidence) AS confidence, count(*) AS count
31
+ FROM edges e
32
+ JOIN nodes s ON s.id = e.source_id AND s.repository_id = e.repository_id
33
+ JOIN nodes t ON t.id = e.target_id AND t.repository_id = e.repository_id
34
+ LEFT JOIN files sf ON sf.id = s.file_id
35
+ LEFT JOIN files tf ON tf.id = t.file_id
36
+ WHERE e.repository_id = ? AND e.kind IN (${placeholders}) AND sf.id IS NOT NULL AND tf.id IS NOT NULL AND sf.id <> tf.id
37
+ GROUP BY sf.id, tf.id, e.kind
38
+ ORDER BY sourceAggregateId, targetAggregateId, e.kind
39
+ `).all(repositoryId, ...safeKinds);
40
+ return rows.map((row) => ({
41
+ id: String(row.id), sourceId: String(row.sourceAggregateId), targetId: String(row.targetAggregateId),
42
+ sourceAggregateId: String(row.sourceAggregateId), targetAggregateId: String(row.targetAggregateId),
43
+ kind: row.kind, confidence: Number(row.confidence ?? 0), count: Number(row.count ?? 0), metadata: {}
44
+ }));
45
+ }
46
+ export function selectExploreSourcesForSearch(db, repositoryId, term, limit = 200) {
47
+ const normalized = term.trim().toLocaleLowerCase();
48
+ if (!normalized)
49
+ return [];
50
+ const rows = db.prepare(`
51
+ SELECT n.id, n.kind, n.type, n.name, n.file_id AS fileId,
52
+ n.start_point AS startPoint, n.end_point AS endPoint, n.metadata_json AS metadataJson, f.path AS path
53
+ FROM nodes n LEFT JOIN files f ON f.id = n.file_id
54
+ WHERE n.repository_id = ? AND (
55
+ lower(COALESCE(n.name, '')) = ? OR
56
+ lower(COALESCE(f.path, '') || ':' || COALESCE(n.name, '')) = ? OR
57
+ lower(COALESCE(n.name, '')) LIKE ? OR
58
+ lower(COALESCE(n.name, '')) LIKE ?
59
+ )
60
+ ORDER BY CASE
61
+ WHEN lower(COALESCE(f.path, '') || ':' || COALESCE(n.name, '')) = ? THEN 0
62
+ WHEN lower(COALESCE(n.name, '')) = ? THEN 1
63
+ WHEN lower(COALESCE(n.name, '')) LIKE ? THEN 2 ELSE 3 END,
64
+ lower(COALESCE(f.path, '')), lower(COALESCE(n.name, '')), n.id
65
+ LIMIT ?
66
+ `).all(repositoryId, normalized, normalized, `${normalized}%`, `%${normalized}%`, normalized, normalized, `${normalized}%`, Math.max(1, Math.min(1000, limit)));
67
+ return rows.map(sourceFromRow);
68
+ }
69
+ export function selectExploreCandidates(db, repositoryId, term, { kind, file, fileAnchor = false, limit = 20 } = {}) {
70
+ const normalized = term.trim().toLocaleLowerCase();
71
+ if (!normalized)
72
+ return [];
73
+ const safeLimit = Math.max(1, Math.min(100, Math.trunc(limit)));
74
+ const explicitFiles = db.prepare('SELECT id, path FROM files WHERE repository_id = ? AND path = ? ORDER BY id LIMIT ?').all(repositoryId, term.trim(), safeLimit);
75
+ if (fileAnchor && explicitFiles.length > 0)
76
+ return explicitFiles.map((row, index) => {
77
+ const fileId = String(row.id);
78
+ const filePath = String(row.path);
79
+ return { id: aggregateId(repositoryId, fileId), kind: 'file', type: 'file', name: filePath, fileId, path: filePath, startPoint: { row: 0, column: 0 }, endPoint: { row: 0, column: 0 }, metadata: {}, rank: index, match: 'path', qualifiedName: filePath, aggregateId: aggregateId(repositoryId, fileId) };
80
+ });
81
+ const stages = [
82
+ { match: 'exact', predicate: 'lower(COALESCE(n.name, \'\')) = ?', params: [normalized] },
83
+ { match: 'qualified', predicate: "lower(COALESCE(f.path, '') || ':' || COALESCE(n.name, '')) = ?", params: [normalized] },
84
+ { match: 'prefix', predicate: 'lower(COALESCE(n.name, \'\')) LIKE ?', params: [`${normalized}%`] },
85
+ { match: 'substring', predicate: 'lower(COALESCE(n.name, \'\')) LIKE ?', params: [`%${normalized}%`] }
86
+ ];
87
+ const result = [];
88
+ const seen = new Set();
89
+ for (const stage of stages) {
90
+ const predicates = ['n.repository_id = ?', stage.predicate];
91
+ const params = [repositoryId, ...stage.params];
92
+ if (kind) {
93
+ predicates.push('n.kind = ?');
94
+ params.push(kind);
95
+ }
96
+ if (file) {
97
+ predicates.push('f.path = ?');
98
+ params.push(file);
99
+ }
100
+ const rows = db.prepare(`
101
+ SELECT n.id, n.kind, n.type, n.name, n.file_id AS fileId,
102
+ n.start_point AS startPoint, n.end_point AS endPoint, n.metadata_json AS metadataJson, f.path AS path
103
+ FROM nodes n LEFT JOIN files f ON f.id = n.file_id
104
+ WHERE ${predicates.join(' AND ')}
105
+ ORDER BY lower(COALESCE(f.path, '')), lower(COALESCE(n.name, '')), n.id LIMIT ?
106
+ `).all(...params, safeLimit);
107
+ for (const row of rows) {
108
+ const source = sourceFromRow(row);
109
+ if (seen.has(source.id))
110
+ continue;
111
+ seen.add(source.id);
112
+ result.push({ ...source, rank: result.length, match: stage.match, qualifiedName: qualifiedName(source), aggregateId: source.fileId ? aggregateId(repositoryId, source.fileId) : null });
113
+ }
114
+ if (result.length > 0)
115
+ break;
116
+ }
117
+ // A path is an explicit file identity and resolves to its aggregate.
118
+ for (const row of explicitFiles) {
119
+ const fileId = String(row.id);
120
+ const id = aggregateId(repositoryId, fileId);
121
+ if (seen.has(id))
122
+ continue;
123
+ result.push({ id, kind: 'file', type: 'file', name: String(row.path), fileId, path: String(row.path), startPoint: { row: 0, column: 0 }, endPoint: { row: 0, column: 0 }, metadata: {}, rank: result.length, match: 'path', qualifiedName: String(row.path), aggregateId: id });
124
+ }
125
+ return result.slice(0, safeLimit);
126
+ }
127
+ export function selectExploreNode(db, repositoryId, id) {
128
+ const row = db.prepare(`
129
+ SELECT n.id, n.kind, n.type, n.name, n.file_id AS fileId,
130
+ n.start_point AS startPoint, n.end_point AS endPoint, n.metadata_json AS metadataJson, f.path AS path
131
+ FROM nodes n LEFT JOIN files f ON f.id = n.file_id WHERE n.repository_id = ? AND n.id = ?
132
+ `).get(repositoryId, id);
133
+ return row ? sourceFromRow(row) : null;
134
+ }
135
+ export function selectExploreTraversalEdges(db, repositoryId, frontier, direction, kinds, limit) {
136
+ if (frontier.length === 0 || limit <= 0)
137
+ return [];
138
+ const safeKinds = kinds.filter((kind) => EXPLORE_KINDS.includes(kind));
139
+ if (safeKinds.length === 0)
140
+ return [];
141
+ const nodePlaceholders = frontier.map(() => '?').join(', ');
142
+ const kindPlaceholders = safeKinds.map(() => '?').join(', ');
143
+ const adjacency = direction === 'incoming'
144
+ ? `(e.target_id IN (${nodePlaceholders}))`
145
+ : direction === 'outgoing'
146
+ ? `(e.source_id IN (${nodePlaceholders}))`
147
+ : `(e.source_id IN (${nodePlaceholders}) OR e.target_id IN (${nodePlaceholders}))`;
148
+ const nodeParams = direction === 'both' ? [...frontier, ...frontier] : [...frontier];
149
+ const rows = db.prepare(`
150
+ SELECT e.id, e.source_id AS sourceId, e.target_id AS targetId, e.kind, e.confidence, e.metadata_json AS metadataJson
151
+ FROM edges e
152
+ WHERE e.repository_id = ? AND e.kind IN (${kindPlaceholders}) AND ${adjacency}
153
+ ORDER BY e.kind, e.id
154
+ LIMIT ?
155
+ `).all(repositoryId, ...safeKinds, ...nodeParams, Math.max(1, limit));
156
+ return rows.map((row) => ({ id: String(row.id), sourceId: String(row.sourceId), targetId: String(row.targetId), kind: row.kind, confidence: Number(row.confidence ?? 0), metadata: parseMetadata(row.metadataJson) }));
157
+ }
158
+ export function selectExploreNodeRows(db, repositoryId, ids) {
159
+ if (ids.length === 0)
160
+ return [];
161
+ const placeholders = ids.map(() => '?').join(', ');
162
+ const rows = db.prepare(`
163
+ SELECT n.id, n.kind, n.type, n.name, n.file_id AS fileId,
164
+ n.start_point AS startPoint, n.end_point AS endPoint, n.metadata_json AS metadataJson, f.path AS path
165
+ FROM nodes n LEFT JOIN files f ON f.id = n.file_id
166
+ WHERE n.repository_id = ? AND n.id IN (${placeholders})
167
+ `).all(repositoryId, ...ids);
168
+ const byId = new Map(rows.map((row) => [String(row.id), sourceFromRow(row)]));
169
+ return ids.flatMap((id) => byId.get(id) ? [byId.get(id)] : []);
170
+ }
171
+ export function aggregateId(repositoryId, fileId) {
172
+ return `aggregate:${repositoryId}:${fileId}`;
173
+ }
174
+ function sourceFromRow(row) {
175
+ return {
176
+ id: String(row.id), kind: row.kind, type: String(row.type), name: row.name == null ? null : String(row.name),
177
+ fileId: row.fileId == null ? null : String(row.fileId), path: row.path == null ? null : String(row.path),
178
+ startPoint: parsePoint(row.startPoint), endPoint: parsePoint(row.endPoint), metadata: parseMetadata(row.metadataJson)
179
+ };
180
+ }
181
+ function qualifiedName(source) {
182
+ return source.name && source.path ? `${source.path}:${source.name}` : source.name;
183
+ }
184
+ //# sourceMappingURL=explore-queries.js.map
@@ -1,21 +1,27 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import type { AttributionApplyResult, AttributionOutcome, FileRecord, GraphEdge, GraphNode, RepositoryRecord } from '../types.js';
3
3
  export declare function upsertRepository(db: DatabaseSync, repository: RepositoryRecord): void;
4
- export declare function replaceFileGraph(db: DatabaseSync, { repository, file, nodes, edges }: {
4
+ export declare function replaceFileGraph(db: DatabaseSync, { repository, file, nodes, edges, enrichment }: {
5
5
  repository: RepositoryRecord;
6
6
  file: FileRecord;
7
7
  nodes: GraphNode[];
8
8
  edges: GraphEdge[];
9
+ enrichment?: {
10
+ pluginName: string;
11
+ historyFingerprint: string | null;
12
+ };
9
13
  }): void;
10
14
  /**
11
15
  * Applies optional attribution only when it still matches the structural file
12
16
  * hash. Failed outcomes never mutate existing attribution.
13
17
  */
14
- export declare function applyFileAttribution(db: DatabaseSync, { repository, fileId, sourceHash, outcome }: {
18
+ export declare function applyFileAttribution(db: DatabaseSync, { repository, fileId, sourceHash, outcome, pluginName, historyFingerprint }: {
15
19
  repository: RepositoryRecord;
16
20
  fileId: string;
17
21
  sourceHash: string;
18
22
  outcome: AttributionOutcome;
23
+ pluginName?: string;
24
+ historyFingerprint?: string | null;
19
25
  }): AttributionApplyResult;
20
26
  export declare function reconcileDeletedFiles(db: DatabaseSync, repositoryId: string, discoveredPaths: string[]): void;
21
27
  export declare function recordFileScanFailure(db: DatabaseSync, repositoryId: string, filePath: string, diagnostic: string): void;
@@ -10,7 +10,7 @@ export function upsertRepository(db, repository) {
10
10
  updated_at = datetime('now')
11
11
  `).run(repository.id, repository.root, repository.name);
12
12
  }
13
- export function replaceFileGraph(db, { repository, file, nodes, edges }) {
13
+ export function replaceFileGraph(db, { repository, file, nodes, edges, enrichment }) {
14
14
  upsertRepository(db, repository);
15
15
  db.exec('BEGIN IMMEDIATE');
16
16
  try {
@@ -42,14 +42,15 @@ export function replaceFileGraph(db, { repository, file, nodes, edges }) {
42
42
  }
43
43
  const insertEdge = db.prepare(`
44
44
  INSERT INTO edges(
45
- id, repository_id, file_id, source_id, target_id, kind, confidence, metadata_json
46
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
45
+ id, repository_id, file_id, source_id, target_id, kind, type, confidence, metadata_json
46
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
47
47
  `);
48
48
  for (const edge of edges) {
49
- insertEdge.run(edge.id, repository.id, file.id, edge.sourceId, edge.targetId, edge.kind, edge.confidence ?? 1, JSON.stringify(edge.metadata ?? {}));
49
+ insertEdge.run(edge.id, repository.id, file.id, edge.sourceId, edge.targetId, edge.kind, edge.type ?? edge.kind, edge.confidence ?? 1, JSON.stringify(edge.metadata ?? {}));
50
50
  }
51
51
  restorePriorAttribution(db, file.id, new Set(nodes.map((node) => node.id)), priorAttribution);
52
- queueFileEnrichment(db, repository.id, file.id, file.hash);
52
+ if (enrichment)
53
+ queueFileEnrichment(db, repository.id, file.id, file.hash, enrichment.pluginName, enrichment.historyFingerprint);
53
54
  db.exec('COMMIT');
54
55
  }
55
56
  catch (error) {
@@ -61,27 +62,34 @@ export function replaceFileGraph(db, { repository, file, nodes, edges }) {
61
62
  * Applies optional attribution only when it still matches the structural file
62
63
  * hash. Failed outcomes never mutate existing attribution.
63
64
  */
64
- export function applyFileAttribution(db, { repository, fileId, sourceHash, outcome }) {
65
+ export function applyFileAttribution(db, { repository, fileId, sourceHash, outcome, pluginName = 'git-attribution-plugin', historyFingerprint = null }) {
65
66
  db.exec('BEGIN IMMEDIATE');
66
67
  try {
67
68
  const current = db.prepare(`
68
- SELECT f.hash AS sourceHash, e.source_hash AS enrichmentHash
69
- FROM files f LEFT JOIN file_enrichment_state e ON e.file_id = f.id
70
- WHERE f.id = ? AND f.repository_id = ?
71
- `).get(fileId, repository.id);
72
- if (current?.sourceHash !== sourceHash || current.enrichmentHash !== sourceHash) {
69
+ SELECT f.hash AS sourceHash, e.source_hash AS enrichmentHash,
70
+ e.plugin_name AS pluginName, e.history_fingerprint AS historyFingerprint
71
+ FROM files f LEFT JOIN file_enrichment_state e ON e.file_id = f.id
72
+ WHERE f.id = ? AND f.repository_id = ?
73
+ `).get(fileId, repository.id);
74
+ if (current?.sourceHash === sourceHash && current.enrichmentHash === undefined) {
75
+ queueFileEnrichment(db, repository.id, fileId, sourceHash, pluginName, historyFingerprint);
76
+ }
77
+ const refreshed = current?.enrichmentHash === undefined
78
+ ? { ...current, enrichmentHash: sourceHash, pluginName, historyFingerprint }
79
+ : current;
80
+ if (refreshed?.sourceHash !== sourceHash || refreshed.enrichmentHash !== sourceHash || refreshed.pluginName !== pluginName || refreshed.historyFingerprint !== historyFingerprint) {
73
81
  db.exec('ROLLBACK');
74
82
  return 'stale';
75
83
  }
76
84
  if (outcome.status === 'complete') {
77
85
  replaceAttribution(db, repository, fileId, outcome.attribution);
78
- setFileEnrichmentState(db, repository.id, fileId, 'complete', null);
86
+ setFileEnrichmentState(db, repository.id, fileId, 'complete', 'complete', null);
79
87
  }
80
88
  else if (outcome.status === 'unavailable') {
81
- setFileEnrichmentState(db, repository.id, fileId, 'complete', null);
89
+ setFileEnrichmentState(db, repository.id, fileId, 'complete', 'unavailable', `unavailable:${outcome.reason}`);
82
90
  }
83
91
  else {
84
- setFileEnrichmentState(db, repository.id, fileId, 'failed', outcome.diagnostic);
92
+ setFileEnrichmentState(db, repository.id, fileId, 'failed', 'failed', outcome.diagnostic);
85
93
  }
86
94
  db.exec('COMMIT');
87
95
  return 'applied';
@@ -136,49 +144,63 @@ export function selectFileInventory(db, repositoryId) {
136
144
  function captureFileAttribution(db, fileId) {
137
145
  return {
138
146
  lastModified: db.prepare(`
139
- SELECT id AS nodeId, last_modified_user_id AS userId
147
+ SELECT id AS nodeId, last_modified_user_id AS userId,
148
+ kind || ':' || type || ':' || COALESCE(name, '') AS key
140
149
  FROM nodes
141
150
  WHERE file_id = ? AND last_modified_user_id IS NOT NULL
142
151
  `).all(fileId),
143
152
  edges: db.prepare(`
144
- SELECT id, source_id AS sourceId, target_id AS targetId, confidence, metadata_json AS metadataJson
145
- FROM edges
146
- WHERE file_id = ? AND kind = 'attributed_to'
153
+ SELECT e.id, e.source_id AS sourceId,
154
+ n.kind || ':' || n.type || ':' || COALESCE(n.name, '') AS sourceKey,
155
+ e.target_id AS targetId, e.confidence, e.metadata_json AS metadataJson
156
+ FROM edges e JOIN nodes n ON n.id = e.source_id
157
+ WHERE e.file_id = ? AND e.kind = 'attributed_to'
147
158
  `).all(fileId)
148
159
  };
149
160
  }
150
161
  function restorePriorAttribution(db, fileId, currentNodeIds, prior) {
162
+ const currentByKey = new Map(db.prepare(`
163
+ SELECT id, kind || ':' || type || ':' || COALESCE(name, '') AS key
164
+ FROM nodes WHERE file_id = ?
165
+ `).all(fileId).map((row) => [row.key, row.id]));
166
+ const mappedNodeId = (nodeId, key) => currentNodeIds.has(nodeId) ? nodeId : currentByKey.get(key);
151
167
  const updateLastModified = db.prepare('UPDATE nodes SET last_modified_user_id = ? WHERE id = ? AND file_id = ?');
152
168
  for (const entry of prior.lastModified) {
153
- if (currentNodeIds.has(entry.nodeId))
154
- updateLastModified.run(entry.userId, entry.nodeId, fileId);
169
+ const currentId = mappedNodeId(entry.nodeId, entry.key);
170
+ if (currentId)
171
+ updateLastModified.run(entry.userId, currentId, fileId);
155
172
  }
156
173
  const insertEdge = db.prepare(`
157
- INSERT OR IGNORE INTO edges(id, repository_id, file_id, source_id, target_id, kind, confidence, metadata_json)
158
- SELECT ?, repository_id, ?, ?, ?, 'attributed_to', ?, ?
174
+ INSERT OR IGNORE INTO edges(id, repository_id, file_id, source_id, target_id, kind, type, confidence, metadata_json)
175
+ SELECT ?, repository_id, ?, ?, ?, 'attributed_to', 'attributed_to', ?, ?
159
176
  FROM files WHERE id = ?
160
177
  `);
161
178
  for (const edge of prior.edges) {
162
- if (currentNodeIds.has(edge.sourceId)) {
163
- insertEdge.run(edge.id, fileId, edge.sourceId, edge.targetId, edge.confidence, edge.metadataJson, fileId);
179
+ const currentSourceId = mappedNodeId(edge.sourceId, edge.sourceKey);
180
+ if (currentSourceId) {
181
+ const edgeId = currentSourceId === edge.sourceId ? edge.id : hashParts([fileId, currentSourceId, edge.targetId, 'attributed_to']);
182
+ insertEdge.run(edgeId, fileId, currentSourceId, edge.targetId, edge.confidence, edge.metadataJson, fileId);
164
183
  }
165
184
  }
166
185
  }
167
- function queueFileEnrichment(db, repositoryId, fileId, sourceHash) {
186
+ function queueFileEnrichment(db, repositoryId, fileId, sourceHash, pluginName, historyFingerprint) {
168
187
  db.prepare(`
169
188
  INSERT INTO file_enrichment_state(
170
- repository_id, file_id, source_hash, state, diagnostic, queued_at, updated_at, completed_at, failed_at
171
- ) VALUES (?, ?, ?, 'pending', NULL, datetime('now'), datetime('now'), NULL, NULL)
189
+ repository_id, file_id, plugin_name, source_hash, history_fingerprint, state, outcome, diagnostic, queued_at, updated_at, completed_at, failed_at
190
+ ) VALUES (?, ?, ?, ?, ?, 'pending', NULL, NULL, datetime('now'), datetime('now'), NULL, NULL)
172
191
  ON CONFLICT(file_id) DO UPDATE SET
173
192
  repository_id = excluded.repository_id,
193
+ plugin_name = excluded.plugin_name,
174
194
  source_hash = excluded.source_hash,
195
+ history_fingerprint = excluded.history_fingerprint,
175
196
  state = 'pending',
197
+ outcome = NULL,
176
198
  diagnostic = NULL,
177
199
  queued_at = datetime('now'),
178
200
  updated_at = datetime('now'),
179
201
  completed_at = NULL,
180
202
  failed_at = NULL
181
- `).run(repositoryId, fileId, sourceHash);
203
+ `).run(repositoryId, fileId, pluginName, sourceHash, historyFingerprint);
182
204
  }
183
205
  function replaceAttribution(db, repository, fileId, attribution) {
184
206
  upsertContributorNodes(db, repository, attribution.contributorEmails);
@@ -186,8 +208,8 @@ function replaceAttribution(db, repository, fileId, attribution) {
186
208
  db.prepare('UPDATE nodes SET last_modified_user_id = NULL WHERE file_id = ?').run(fileId);
187
209
  const updateLastModified = db.prepare('UPDATE nodes SET last_modified_user_id = ? WHERE id = ? AND file_id = ?');
188
210
  const insertEdge = db.prepare(`
189
- INSERT INTO edges(id, repository_id, file_id, source_id, target_id, kind, confidence, metadata_json)
190
- VALUES (?, ?, ?, ?, ?, 'attributed_to', 1, ?)
211
+ INSERT INTO edges(id, repository_id, file_id, source_id, target_id, kind, type, confidence, metadata_json)
212
+ VALUES (?, ?, ?, ?, ?, 'attributed_to', 'attributed_to', 1, ?)
191
213
  `);
192
214
  for (const entry of attribution.nodeAttributions) {
193
215
  const lastModifiedUserId = resolveUserId(repository, entry.lastModifiedEmail);
@@ -201,14 +223,14 @@ function replaceAttribution(db, repository, fileId, attribution) {
201
223
  }
202
224
  }
203
225
  }
204
- function setFileEnrichmentState(db, repositoryId, fileId, state, diagnostic) {
226
+ function setFileEnrichmentState(db, repositoryId, fileId, state, outcome, diagnostic) {
205
227
  db.prepare(`
206
228
  UPDATE file_enrichment_state
207
- SET state = ?, diagnostic = ?, updated_at = datetime('now'),
229
+ SET state = ?, outcome = ?, diagnostic = ?, updated_at = datetime('now'),
208
230
  completed_at = CASE WHEN ? = 'complete' THEN datetime('now') ELSE NULL END,
209
231
  failed_at = CASE WHEN ? = 'failed' THEN datetime('now') ELSE NULL END
210
232
  WHERE repository_id = ? AND file_id = ?
211
- `).run(state, diagnostic, state, state, repositoryId, fileId);
233
+ `).run(state, outcome, diagnostic, state, state, repositoryId, fileId);
212
234
  }
213
235
  function upsertContributorNodes(db, repository, contributorEmails) {
214
236
  const insertNode = db.prepare(`
@@ -13,8 +13,8 @@ export function reconcilePluginEdges(db, repository, edges, fileIdByPath, active
13
13
  `).run(repository.id, plugin);
14
14
  }
15
15
  const insert = db.prepare(`
16
- INSERT OR REPLACE INTO edges(id, repository_id, file_id, source_id, target_id, kind, confidence, metadata_json)
17
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
16
+ INSERT OR REPLACE INTO edges(id, repository_id, file_id, source_id, target_id, kind, type, confidence, metadata_json)
17
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
18
18
  `);
19
19
  for (const edge of edges) {
20
20
  const plugin = typeof edge.metadata.plugin === 'string' ? edge.metadata.plugin : undefined;
@@ -29,7 +29,7 @@ export function reconcilePluginEdges(db, repository, edges, fileIdByPath, active
29
29
  const requiredEndpoints = edge.sourceId === edge.targetId ? 1 : 2;
30
30
  if (endpoints.count !== requiredEndpoints)
31
31
  continue;
32
- insert.run(edge.id, repository.id, fileId, edge.sourceId, edge.targetId, edge.kind, edge.confidence ?? 1, JSON.stringify(edge.metadata ?? {}));
32
+ insert.run(edge.id, repository.id, fileId, edge.sourceId, edge.targetId, edge.kind, edge.type ?? edge.kind, edge.confidence ?? 1, JSON.stringify(edge.metadata ?? {}));
33
33
  inserted += 1;
34
34
  }
35
35
  db.exec('COMMIT');
@@ -19,12 +19,14 @@ export declare function selectAllNodeTextRows(db: DatabaseSync, repositoryId: st
19
19
  export declare function selectAllNodes(db: DatabaseSync, repositoryId: string, { kind }?: {
20
20
  kind?: string | undefined;
21
21
  }): GraphNodeRow[];
22
- export declare function selectEdges(db: DatabaseSync, repositoryId: string, { kind, limit }?: {
22
+ export declare function selectEdges(db: DatabaseSync, repositoryId: string, { kind, type, limit }?: {
23
23
  kind?: string | undefined;
24
+ type?: string | undefined;
24
25
  limit?: number | undefined;
25
26
  }): GraphEdgeRow[];
26
- export declare function selectAllEdges(db: DatabaseSync, repositoryId: string, { kind }?: {
27
+ export declare function selectAllEdges(db: DatabaseSync, repositoryId: string, { kind, type }?: {
27
28
  kind?: string | undefined;
29
+ type?: string | undefined;
28
30
  }): GraphEdgeRow[];
29
31
  export declare function clampLimit(limit: number | undefined, max?: number): number;
30
32
  //# sourceMappingURL=queries.d.ts.map
@@ -13,8 +13,9 @@ export function selectFreshnessReport(db, repositoryId) {
13
13
  }
14
14
  export function selectFileEnrichmentState(db, repositoryId, fileId) {
15
15
  const row = db.prepare(`
16
- SELECT repository_id AS repositoryId, file_id AS fileId, source_hash AS sourceHash,
17
- state, diagnostic, queued_at AS queuedAt, updated_at AS updatedAt,
16
+ SELECT repository_id AS repositoryId, file_id AS fileId, plugin_name AS pluginName,
17
+ source_hash AS sourceHash, history_fingerprint AS historyFingerprint,
18
+ state, outcome, diagnostic, queued_at AS queuedAt, updated_at AS updatedAt,
18
19
  completed_at AS completedAt, failed_at AS failedAt
19
20
  FROM file_enrichment_state
20
21
  WHERE repository_id = ? AND file_id = ?
@@ -26,8 +27,9 @@ export function selectFileEnrichmentStates(db, repositoryId, states = ['pending'
26
27
  return [];
27
28
  const placeholders = states.map(() => '?').join(', ');
28
29
  return db.prepare(`
29
- SELECT repository_id AS repositoryId, file_id AS fileId, source_hash AS sourceHash,
30
- state, diagnostic, queued_at AS queuedAt, updated_at AS updatedAt,
30
+ SELECT repository_id AS repositoryId, file_id AS fileId, plugin_name AS pluginName,
31
+ source_hash AS sourceHash, history_fingerprint AS historyFingerprint,
32
+ state, outcome, diagnostic, queued_at AS queuedAt, updated_at AS updatedAt,
31
33
  completed_at AS completedAt, failed_at AS failedAt
32
34
  FROM file_enrichment_state
33
35
  WHERE repository_id = ? AND state IN (${placeholders})
@@ -39,8 +41,9 @@ export function selectEnrichmentWork(db, repositoryId, states = ['pending', 'fai
39
41
  return [];
40
42
  const placeholders = states.map(() => '?').join(', ');
41
43
  return db.prepare(`
42
- SELECT e.repository_id AS repositoryId, e.file_id AS fileId, e.source_hash AS sourceHash,
43
- e.state, e.diagnostic, e.queued_at AS queuedAt, e.updated_at AS updatedAt,
44
+ SELECT e.repository_id AS repositoryId, e.file_id AS fileId, e.plugin_name AS pluginName,
45
+ e.source_hash AS sourceHash, e.history_fingerprint AS historyFingerprint,
46
+ e.state, e.outcome, e.diagnostic, e.queued_at AS queuedAt, e.updated_at AS updatedAt,
44
47
  e.completed_at AS completedAt, e.failed_at AS failedAt, f.path, f.language
45
48
  FROM file_enrichment_state e
46
49
  JOIN files f ON f.id = e.file_id
@@ -51,11 +54,14 @@ export function selectEnrichmentWork(db, repositoryId, states = ['pending', 'fai
51
54
  export function selectEnrichmentSummary(db, repositoryId) {
52
55
  const states = selectFileEnrichmentStates(db, repositoryId, ['pending', 'complete', 'failed']);
53
56
  const failedEntries = states.filter((entry) => entry.state === 'failed');
57
+ const unavailable = states.filter((entry) => entry.outcome === 'unavailable');
54
58
  return {
55
- state: failedEntries.length > 0 ? 'failed' : states.some((entry) => entry.state === 'pending') ? 'pending' : 'complete',
59
+ state: states.length === 0 ? 'disabled' : failedEntries.length > 0 ? 'failed' : states.some((entry) => entry.state === 'pending') ? 'pending' : 'complete',
56
60
  pending: states.filter((entry) => entry.state === 'pending').length,
57
- complete: states.filter((entry) => entry.state === 'complete').length,
61
+ complete: states.filter((entry) => entry.state === 'complete' && entry.outcome !== 'unavailable').length,
58
62
  failed: failedEntries.length,
63
+ unavailable: unavailable.length,
64
+ notApplicable: unavailable.filter((entry) => entry.diagnostic === 'unavailable:not_applicable').length,
59
65
  diagnostics: failedEntries.flatMap((entry) => entry.diagnostic ? [entry.diagnostic] : [])
60
66
  };
61
67
  }
@@ -153,40 +159,43 @@ export function selectAllNodes(db, repositoryId, { kind } = {}) {
153
159
  ORDER BY kind, name IS NULL, name, id
154
160
  `).all(repositoryId);
155
161
  }
156
- export function selectEdges(db, repositoryId, { kind, limit = 50 } = {}) {
162
+ export function selectEdges(db, repositoryId, { kind, type, limit = 50 } = {}) {
157
163
  const safeLimit = clampLimit(limit);
164
+ const where = ['repository_id = ?'];
165
+ const parameters = [repositoryId];
158
166
  if (kind) {
159
- return db.prepare(`
160
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
161
- FROM edges
162
- WHERE repository_id = ? AND kind = ?
163
- ORDER BY kind, id
164
- LIMIT ?
165
- `).all(repositoryId, kind, safeLimit);
167
+ where.push('kind = ?');
168
+ parameters.push(kind);
169
+ }
170
+ if (type) {
171
+ where.push('type = ?');
172
+ parameters.push(type);
166
173
  }
167
174
  return db.prepare(`
168
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
175
+ SELECT id, kind, type, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
169
176
  FROM edges
170
- WHERE repository_id = ?
171
- ORDER BY kind, id
177
+ WHERE ${where.join(' AND ')}
178
+ ORDER BY kind, type, id
172
179
  LIMIT ?
173
- `).all(repositoryId, safeLimit);
180
+ `).all(...parameters, safeLimit);
174
181
  }
175
- export function selectAllEdges(db, repositoryId, { kind } = {}) {
182
+ export function selectAllEdges(db, repositoryId, { kind, type } = {}) {
183
+ const where = ['repository_id = ?'];
184
+ const parameters = [repositoryId];
176
185
  if (kind) {
177
- return db.prepare(`
178
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
179
- FROM edges
180
- WHERE repository_id = ? AND kind = ?
181
- ORDER BY kind, id
182
- `).all(repositoryId, kind);
186
+ where.push('kind = ?');
187
+ parameters.push(kind);
188
+ }
189
+ if (type) {
190
+ where.push('type = ?');
191
+ parameters.push(type);
183
192
  }
184
193
  return db.prepare(`
185
- SELECT id, kind, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
194
+ SELECT id, kind, type, source_id AS sourceId, target_id AS targetId, file_id AS fileId, confidence, metadata_json AS metadataJson
186
195
  FROM edges
187
- WHERE repository_id = ?
188
- ORDER BY kind, id
189
- `).all(repositoryId);
196
+ WHERE ${where.join(' AND ')}
197
+ ORDER BY kind, type, id
198
+ `).all(...parameters);
190
199
  }
191
200
  export function clampLimit(limit, max = 500) {
192
201
  const value = Number(limit) || 50;
@@ -1,4 +1,4 @@
1
- export declare const SCHEMA_VERSION = 7;
2
- export declare const CREATE_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS schema_migrations (\n version INTEGER PRIMARY KEY,\n applied_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS repositories (\n id TEXT PRIMARY KEY,\n root TEXT NOT NULL,\n name TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS files (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n language TEXT NOT NULL,\n hash TEXT NOT NULL,\n size INTEGER NOT NULL,\n mtime_ms INTEGER NOT NULL,\n scanned_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(repository_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n type TEXT NOT NULL,\n name TEXT,\n start_byte INTEGER NOT NULL,\n end_byte INTEGER NOT NULL,\n start_point TEXT NOT NULL,\n end_point TEXT NOT NULL,\n source_hash TEXT NOT NULL,\n parser TEXT NOT NULL,\n parser_version TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n last_modified_user_id TEXT\n);\n\nCREATE TABLE IF NOT EXISTS edges (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n source_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n target_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n confidence REAL NOT NULL DEFAULT 1.0,\n metadata_json TEXT NOT NULL DEFAULT '{}'\n);\n\nCREATE TABLE IF NOT EXISTS overlays (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n subject_id TEXT NOT NULL,\n subject_kind TEXT NOT NULL,\n operation TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_files_repo_path ON files(repository_id, path);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_kind ON nodes(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_name ON nodes(repository_id, name);\nCREATE INDEX IF NOT EXISTS idx_edges_repo_kind ON edges(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);\nCREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);\n\nCREATE TABLE IF NOT EXISTS plugin_contributions (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n plugin_name TEXT NOT NULL,\n generation INTEGER NOT NULL,\n nodes_json TEXT NOT NULL,\n edges_json TEXT NOT NULL,\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY(repository_id, plugin_name)\n);\nCREATE INDEX IF NOT EXISTS idx_plugin_contributions_repo ON plugin_contributions(repository_id);\n\nCREATE TABLE IF NOT EXISTS file_scan_failures (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n diagnostic TEXT NOT NULL,\n failed_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY(repository_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS repository_scan_state (\n repository_id TEXT PRIMARY KEY REFERENCES repositories(id) ON DELETE CASCADE,\n state TEXT NOT NULL CHECK(state IN ('fresh', 'degraded', 'stale')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS file_enrichment_state (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,\n source_hash TEXT NOT NULL,\n state TEXT NOT NULL CHECK(state IN ('pending', 'complete', 'failed')),\n diagnostic TEXT,\n queued_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n completed_at TEXT,\n failed_at TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_file_enrichment_state_repo_state ON file_enrichment_state(repository_id, state);\n\nCREATE TABLE IF NOT EXISTS repository_manifest_inventory (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n kind TEXT NOT NULL CHECK(kind IN ('maven-pom', 'gradle-groovy', 'gradle-kotlin')),\n module_identity TEXT NOT NULL,\n status TEXT NOT NULL CHECK(status IN ('observed', 'read_failed', 'traversal_incomplete', 'confirmed_deleted')),\n size INTEGER NOT NULL,\n mtime_ms INTEGER NOT NULL,\n source_hash TEXT,\n generation INTEGER NOT NULL,\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY(repository_id, path)\n);\nCREATE INDEX IF NOT EXISTS idx_repository_manifest_inventory_repo_status\n ON repository_manifest_inventory(repository_id, status);\n";
1
+ export declare const SCHEMA_VERSION = 9;
2
+ export declare const CREATE_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS schema_migrations (\n version INTEGER PRIMARY KEY,\n applied_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS repositories (\n id TEXT PRIMARY KEY,\n root TEXT NOT NULL,\n name TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS files (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n language TEXT NOT NULL,\n hash TEXT NOT NULL,\n size INTEGER NOT NULL,\n mtime_ms INTEGER NOT NULL,\n scanned_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(repository_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n type TEXT NOT NULL,\n name TEXT,\n start_byte INTEGER NOT NULL,\n end_byte INTEGER NOT NULL,\n start_point TEXT NOT NULL,\n end_point TEXT NOT NULL,\n source_hash TEXT NOT NULL,\n parser TEXT NOT NULL,\n parser_version TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n last_modified_user_id TEXT\n);\n\nCREATE TABLE IF NOT EXISTS edges (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n source_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n target_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n type TEXT NOT NULL,\n confidence REAL NOT NULL DEFAULT 1.0,\n metadata_json TEXT NOT NULL DEFAULT '{}'\n);\n\nCREATE TABLE IF NOT EXISTS overlays (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n subject_id TEXT NOT NULL,\n subject_kind TEXT NOT NULL,\n operation TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_files_repo_path ON files(repository_id, path);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_kind ON nodes(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_nodes_repo_name ON nodes(repository_id, name);\nCREATE INDEX IF NOT EXISTS idx_edges_repo_kind ON edges(repository_id, kind);\nCREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);\nCREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);\n\nCREATE TABLE IF NOT EXISTS plugin_contributions (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n plugin_name TEXT NOT NULL,\n generation INTEGER NOT NULL,\n nodes_json TEXT NOT NULL,\n edges_json TEXT NOT NULL,\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY(repository_id, plugin_name)\n);\nCREATE INDEX IF NOT EXISTS idx_plugin_contributions_repo ON plugin_contributions(repository_id);\n\nCREATE TABLE IF NOT EXISTS file_scan_failures (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n diagnostic TEXT NOT NULL,\n failed_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY(repository_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS repository_scan_state (\n repository_id TEXT PRIMARY KEY REFERENCES repositories(id) ON DELETE CASCADE,\n state TEXT NOT NULL CHECK(state IN ('fresh', 'degraded', 'stale')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS file_enrichment_state (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,\n plugin_name TEXT NOT NULL DEFAULT 'git-attribution-plugin',\n source_hash TEXT NOT NULL,\n history_fingerprint TEXT,\n state TEXT NOT NULL CHECK(state IN ('pending', 'complete', 'failed')),\n outcome TEXT CHECK(outcome IN ('complete', 'unavailable', 'failed')),\n diagnostic TEXT,\n queued_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n completed_at TEXT,\n failed_at TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_file_enrichment_state_repo_state ON file_enrichment_state(repository_id, state);\n\nCREATE TABLE IF NOT EXISTS repository_manifest_inventory (\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n path TEXT NOT NULL,\n kind TEXT NOT NULL CHECK(kind IN ('maven-pom', 'gradle-groovy', 'gradle-kotlin')),\n module_identity TEXT NOT NULL,\n status TEXT NOT NULL CHECK(status IN ('observed', 'read_failed', 'traversal_incomplete', 'confirmed_deleted')),\n size INTEGER NOT NULL,\n mtime_ms INTEGER NOT NULL,\n source_hash TEXT,\n generation INTEGER NOT NULL,\n updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY(repository_id, path)\n);\nCREATE INDEX IF NOT EXISTS idx_repository_manifest_inventory_repo_status\n ON repository_manifest_inventory(repository_id, status);\n";
3
3
  export declare const CREATE_NODES_TABLE_SQL = "\nCREATE TABLE nodes_new (\n id TEXT PRIMARY KEY,\n repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,\n file_id TEXT REFERENCES files(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n type TEXT NOT NULL,\n name TEXT,\n start_byte INTEGER NOT NULL,\n end_byte INTEGER NOT NULL,\n start_point TEXT NOT NULL,\n end_point TEXT NOT NULL,\n source_hash TEXT NOT NULL,\n parser TEXT NOT NULL,\n parser_version TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n last_modified_user_id TEXT\n);\n";
4
4
  //# sourceMappingURL=schema.d.ts.map
@@ -1,4 +1,4 @@
1
- export const SCHEMA_VERSION = 7;
1
+ export const SCHEMA_VERSION = 9;
2
2
  export const CREATE_SCHEMA_SQL = `
3
3
  CREATE TABLE IF NOT EXISTS schema_migrations (
4
4
  version INTEGER PRIMARY KEY,
@@ -50,6 +50,7 @@ CREATE TABLE IF NOT EXISTS edges (
50
50
  source_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
51
51
  target_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
52
52
  kind TEXT NOT NULL,
53
+ type TEXT NOT NULL,
53
54
  confidence REAL NOT NULL DEFAULT 1.0,
54
55
  metadata_json TEXT NOT NULL DEFAULT '{}'
55
56
  );
@@ -99,8 +100,11 @@ CREATE TABLE IF NOT EXISTS repository_scan_state (
99
100
  CREATE TABLE IF NOT EXISTS file_enrichment_state (
100
101
  repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
101
102
  file_id TEXT PRIMARY KEY REFERENCES files(id) ON DELETE CASCADE,
103
+ plugin_name TEXT NOT NULL DEFAULT 'git-attribution-plugin',
102
104
  source_hash TEXT NOT NULL,
105
+ history_fingerprint TEXT,
103
106
  state TEXT NOT NULL CHECK(state IN ('pending', 'complete', 'failed')),
107
+ outcome TEXT CHECK(outcome IN ('complete', 'unavailable', 'failed')),
104
108
  diagnostic TEXT,
105
109
  queued_at TEXT NOT NULL DEFAULT (datetime('now')),
106
110
  updated_at TEXT NOT NULL DEFAULT (datetime('now')),