@saptools/service-flow 0.1.50 → 0.1.52
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/CHANGELOG.md +14 -0
- package/README.md +16 -3
- package/TECHNICAL-NOTE.md +10 -1
- package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +318 -510
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +59 -17
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +97 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +120 -22
- package/src/db/schema.ts +7 -2
- package/src/index.ts +1 -1
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +22 -2
- package/src/linker/service-resolver.ts +15 -4
- package/src/output/table-output.ts +56 -3
- package/src/parsers/cds-parser.ts +8 -2
- package/src/parsers/decorator-parser.ts +382 -48
- package/src/parsers/handler-registration-parser.ts +38 -21
- package/src/parsers/imported-wrapper-parser.ts +18 -5
- package/src/parsers/outbound-call-parser.ts +25 -8
- package/src/parsers/package-json-parser.ts +36 -11
- package/src/parsers/service-binding-parser-helpers.ts +8 -1
- package/src/parsers/service-binding-parser.ts +6 -3
- package/src/parsers/symbol-parser.ts +13 -3
- package/src/parsers/ts-project.ts +54 -0
- package/src/trace/000-dynamic-target-types.ts +84 -0
- package/src/trace/001-dynamic-identity.ts +280 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +82 -0
- package/src/trace/dynamic-branches.ts +45 -0
- package/src/trace/dynamic-targets.ts +654 -0
- package/src/trace/evidence.ts +391 -22
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +101 -94
- package/src/types.ts +39 -1
- package/dist/chunk-52OUS3MO.js.map +0 -1
package/src/db/repositories.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
CdsRequire,
|
|
4
4
|
CdsServiceFact,
|
|
5
5
|
HandlerClassFact,
|
|
6
|
+
HandlerMethodFact,
|
|
6
7
|
HandlerRegistrationFact,
|
|
7
8
|
OutboundCallFact,
|
|
8
9
|
ServiceBindingFact,
|
|
@@ -85,15 +86,29 @@ export function upsertRepository(
|
|
|
85
86
|
.get(workspaceId, r.absolutePath)?.id,
|
|
86
87
|
);
|
|
87
88
|
}
|
|
88
|
-
export function listRepositories(db: Db): RepoRow[] {
|
|
89
|
+
export function listRepositories(db: Db, workspaceId?: number): RepoRow[] {
|
|
89
90
|
return db
|
|
90
|
-
.prepare('SELECT * FROM repositories ORDER BY name')
|
|
91
|
-
.all() as unknown as RepoRow[];
|
|
91
|
+
.prepare('SELECT * FROM repositories WHERE (? IS NULL OR workspace_id=?) ORDER BY name,absolute_path,id')
|
|
92
|
+
.all(workspaceId, workspaceId) as unknown as RepoRow[];
|
|
92
93
|
}
|
|
93
|
-
export function repoByName(
|
|
94
|
+
export function repoByName(
|
|
95
|
+
db: Db,
|
|
96
|
+
name: string,
|
|
97
|
+
workspaceId?: number,
|
|
98
|
+
): RepoRow | undefined {
|
|
99
|
+
const matches = reposByName(db, name, workspaceId);
|
|
100
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
101
|
+
}
|
|
102
|
+
export function reposByName(
|
|
103
|
+
db: Db,
|
|
104
|
+
name: string,
|
|
105
|
+
workspaceId?: number,
|
|
106
|
+
): RepoRow[] {
|
|
94
107
|
return db
|
|
95
|
-
.prepare(
|
|
96
|
-
|
|
108
|
+
.prepare(`SELECT * FROM repositories
|
|
109
|
+
WHERE (? IS NULL OR workspace_id=?) AND (name=? OR package_name=?)
|
|
110
|
+
ORDER BY name,absolute_path,id`)
|
|
111
|
+
.all(workspaceId, workspaceId, name, name) as unknown as RepoRow[];
|
|
97
112
|
}
|
|
98
113
|
export function clearRepoFacts(db: Db, repoId: number): void {
|
|
99
114
|
for (const t of [
|
|
@@ -188,21 +203,7 @@ export function insertHandler(
|
|
|
188
203
|
repoId: number,
|
|
189
204
|
h: HandlerClassFact,
|
|
190
205
|
): number {
|
|
191
|
-
const sid =
|
|
192
|
-
db
|
|
193
|
-
.prepare(
|
|
194
|
-
'INSERT INTO symbols(repo_id,kind,name,qualified_name,exported,start_line,end_line) VALUES(?,?,?,?,?,?,?) RETURNING id',
|
|
195
|
-
)
|
|
196
|
-
.get(
|
|
197
|
-
repoId,
|
|
198
|
-
'class',
|
|
199
|
-
h.className,
|
|
200
|
-
h.className,
|
|
201
|
-
1,
|
|
202
|
-
h.sourceLine,
|
|
203
|
-
h.sourceLine,
|
|
204
|
-
)?.id,
|
|
205
|
-
);
|
|
206
|
+
const sid = insertHandlerClassSymbol(db, repoId, h);
|
|
206
207
|
const hid = Number(
|
|
207
208
|
db
|
|
208
209
|
.prepare(
|
|
@@ -220,12 +221,109 @@ export function insertHandler(
|
|
|
220
221
|
m.decoratorKind,
|
|
221
222
|
m.decoratorValue,
|
|
222
223
|
m.decoratorRawExpression,
|
|
223
|
-
JSON.stringify(m
|
|
224
|
+
JSON.stringify(canonicalHandlerMethodResolution(m)),
|
|
224
225
|
m.sourceFile,
|
|
225
226
|
m.sourceLine,
|
|
226
227
|
);
|
|
228
|
+
insertHandlerIndexDiagnostic(db, repoId, h);
|
|
227
229
|
return hid;
|
|
228
230
|
}
|
|
231
|
+
function insertHandlerClassSymbol(
|
|
232
|
+
db: Db,
|
|
233
|
+
repoId: number,
|
|
234
|
+
h: HandlerClassFact,
|
|
235
|
+
): number {
|
|
236
|
+
const classEvidence = {
|
|
237
|
+
hasHandlerDecorator: h.hasHandlerDecorator ?? false,
|
|
238
|
+
classDecoratorNames: h.classDecoratorNames ?? [],
|
|
239
|
+
observedDecoratorNames: h.observedDecoratorNames ?? [],
|
|
240
|
+
unsupportedDecoratorNames: h.unsupportedDecoratorNames ?? [],
|
|
241
|
+
unsupportedMethods: h.methods
|
|
242
|
+
.filter((method) => !handlerMethodIsExecutable(method))
|
|
243
|
+
.map((method) => ({
|
|
244
|
+
methodName: method.methodName,
|
|
245
|
+
decoratorKind: method.decoratorKind,
|
|
246
|
+
sourceFile: method.sourceFile,
|
|
247
|
+
sourceLine: method.sourceLine,
|
|
248
|
+
reason: method.decoratorResolution.unresolvedReason,
|
|
249
|
+
})),
|
|
250
|
+
};
|
|
251
|
+
return Number(
|
|
252
|
+
db
|
|
253
|
+
.prepare(
|
|
254
|
+
'INSERT INTO symbols(repo_id,kind,name,qualified_name,exported,start_line,end_line,source_file,evidence_json) VALUES(?,?,?,?,?,?,?,?,?) RETURNING id',
|
|
255
|
+
)
|
|
256
|
+
.get(
|
|
257
|
+
repoId,
|
|
258
|
+
'class',
|
|
259
|
+
h.className,
|
|
260
|
+
h.className,
|
|
261
|
+
1,
|
|
262
|
+
h.sourceLine,
|
|
263
|
+
h.sourceLine,
|
|
264
|
+
h.sourceFile,
|
|
265
|
+
JSON.stringify(classEvidence),
|
|
266
|
+
)?.id,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
function insertHandlerIndexDiagnostic(
|
|
270
|
+
db: Db,
|
|
271
|
+
repoId: number,
|
|
272
|
+
h: HandlerClassFact,
|
|
273
|
+
): void {
|
|
274
|
+
if (!h.hasHandlerDecorator) return;
|
|
275
|
+
const hasExecutable = h.methods.some(handlerMethodIsExecutable);
|
|
276
|
+
const unsupported = h.methods.filter((method) =>
|
|
277
|
+
!handlerMethodIsExecutable(method));
|
|
278
|
+
if (hasExecutable && unsupported.length === 0) return;
|
|
279
|
+
const code = hasExecutable
|
|
280
|
+
? 'handler_decorators_not_indexed'
|
|
281
|
+
: 'handler_methods_not_indexed';
|
|
282
|
+
const names = unsupported.map((method) => method.decoratorKind).sort();
|
|
283
|
+
const detail = names.length > 0
|
|
284
|
+
? ` Unsupported decorators: ${[...new Set(names)].join(', ')}.`
|
|
285
|
+
: '';
|
|
286
|
+
db.prepare(
|
|
287
|
+
'INSERT INTO diagnostics(repo_id,severity,code,message,source_file,source_line) VALUES(?,?,?,?,?,?)',
|
|
288
|
+
).run(
|
|
289
|
+
repoId,
|
|
290
|
+
'warning',
|
|
291
|
+
code,
|
|
292
|
+
hasExecutable
|
|
293
|
+
? `Handler class ${h.className} contains methods that were not indexed.${detail}`
|
|
294
|
+
: `Handler class ${h.className} has no indexed executable methods; use a supported CAP handler decorator and re-index.${detail}`,
|
|
295
|
+
h.sourceFile,
|
|
296
|
+
h.sourceLine,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
export function canonicalHandlerMethodResolution(
|
|
300
|
+
method: HandlerMethodFact,
|
|
301
|
+
): HandlerMethodFact['decoratorResolution'] {
|
|
302
|
+
const handlerKind = method.handlerKind
|
|
303
|
+
?? method.decoratorResolution.handlerKind
|
|
304
|
+
?? legacyHandlerKind(method.decoratorKind);
|
|
305
|
+
const executable = method.executable
|
|
306
|
+
?? method.decoratorResolution.executable
|
|
307
|
+
?? (handlerKind === 'operation' || handlerKind === 'event'
|
|
308
|
+
|| handlerKind === 'entity_lifecycle');
|
|
309
|
+
return {
|
|
310
|
+
...method.decoratorResolution,
|
|
311
|
+
handlerKind,
|
|
312
|
+
executable,
|
|
313
|
+
lifecyclePhase: method.lifecyclePhase
|
|
314
|
+
?? method.decoratorResolution.lifecyclePhase,
|
|
315
|
+
lifecycleEvent: method.lifecycleEvent
|
|
316
|
+
?? method.decoratorResolution.lifecycleEvent,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
export function handlerMethodIsExecutable(method: HandlerMethodFact): boolean {
|
|
320
|
+
return canonicalHandlerMethodResolution(method).executable === true;
|
|
321
|
+
}
|
|
322
|
+
function legacyHandlerKind(kind: string): HandlerMethodFact['handlerKind'] {
|
|
323
|
+
if (kind === 'Event') return 'event';
|
|
324
|
+
if (['Action', 'Func', 'On'].includes(kind)) return 'operation';
|
|
325
|
+
return 'unsupported_decorator';
|
|
326
|
+
}
|
|
229
327
|
export function insertRegistrations(
|
|
230
328
|
db: Db,
|
|
231
329
|
repoId: number,
|
package/src/db/schema.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const schemaTablesSql = `
|
|
2
2
|
CREATE TABLE IF NOT EXISTS workspaces (id INTEGER PRIMARY KEY, root_path TEXT UNIQUE NOT NULL, db_path TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
3
3
|
CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, fact_generation INTEGER NOT NULL DEFAULT 0, graph_generation INTEGER NOT NULL DEFAULT 0, graph_stale_reason TEXT, graph_stale_at TEXT, fact_analyzer_version TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
4
4
|
CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, relative_path TEXT NOT NULL, extension TEXT NOT NULL, sha256 TEXT NOT NULL, size_bytes INTEGER NOT NULL, last_indexed_at TEXT NOT NULL, UNIQUE(repo_id, relative_path), FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
@@ -13,8 +13,11 @@ CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INT
|
|
|
13
13
|
CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT, local_service_name TEXT, local_service_lookup TEXT, alias_chain_json TEXT, evidence_json TEXT, external_target_kind TEXT, external_target_id TEXT, external_target_label TEXT, external_target_dynamic INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
|
|
14
14
|
CREATE TABLE IF NOT EXISTS symbol_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, caller_symbol_id INTEGER NOT NULL, callee_symbol_id INTEGER, callee_expression TEXT NOT NULL, import_source TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, status TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, unresolved_reason TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(caller_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE, FOREIGN KEY(callee_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
|
|
15
15
|
CREATE TABLE IF NOT EXISTS graph_edges (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, edge_type TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'unresolved', from_kind TEXT NOT NULL, from_id TEXT NOT NULL, to_kind TEXT NOT NULL, to_id TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, is_dynamic INTEGER NOT NULL, unresolved_reason TEXT, generation INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
16
|
-
CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL, error_message TEXT, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
16
|
+
CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL, error_message TEXT, owner_pid INTEGER, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
17
17
|
CREATE TABLE IF NOT EXISTS diagnostics (id INTEGER PRIMARY KEY, repo_id INTEGER, file_id INTEGER, severity TEXT NOT NULL, code TEXT NOT NULL, message TEXT NOT NULL, source_file TEXT, source_line INTEGER, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE SET NULL);
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
export const schemaIndexesSql = `
|
|
18
21
|
CREATE INDEX IF NOT EXISTS idx_repo_name ON repositories(name);
|
|
19
22
|
CREATE INDEX IF NOT EXISTS idx_service_path ON cds_services(service_path);
|
|
20
23
|
CREATE INDEX IF NOT EXISTS idx_extension_import ON cds_services(extension_module_specifier, extension_imported_symbol, is_extend);
|
|
@@ -24,3 +27,5 @@ CREATE INDEX IF NOT EXISTS idx_calls_repo ON outbound_calls(repo_id, call_type);
|
|
|
24
27
|
CREATE INDEX IF NOT EXISTS idx_symbol_calls_caller ON symbol_calls(repo_id, caller_symbol_id);
|
|
25
28
|
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, repo);
|
|
26
29
|
`;
|
|
30
|
+
|
|
31
|
+
export const schemaSql = `${schemaTablesSql}\n${schemaIndexesSql}`;
|
package/src/index.ts
CHANGED
|
@@ -11,5 +11,5 @@ export { applyVariables, extractPlaceholders, substituteVariables } from './link
|
|
|
11
11
|
export type { RuntimeSubstitution } from './linker/dynamic-edge-resolver.js';
|
|
12
12
|
export { trace } from './trace/trace-engine.js';
|
|
13
13
|
export { parseImplementationHint } from './trace/implementation-hints.js';
|
|
14
|
-
export type { ImplementationHint } from './types.js';
|
|
14
|
+
export type { DynamicMode, ImplementationHint, TraceOptions } from './types.js';
|
|
15
15
|
export { redactValue, redactText } from './utils/redaction.js';
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
insertCalls,
|
|
8
8
|
insertExecutableSymbols,
|
|
9
9
|
insertHandler,
|
|
10
|
+
handlerMethodIsExecutable,
|
|
10
11
|
insertRegistrations,
|
|
11
12
|
insertSymbolCalls,
|
|
12
13
|
insertRequires,
|
|
@@ -19,14 +20,20 @@ import { parseDecorators } from '../parsers/decorator-parser.js';
|
|
|
19
20
|
import { parseHandlerRegistrations } from '../parsers/handler-registration-parser.js';
|
|
20
21
|
import { parseOutboundCalls } from '../parsers/outbound-call-parser.js';
|
|
21
22
|
import { parseExecutableSymbols } from '../parsers/symbol-parser.js';
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
loadPackageJsonSnapshot,
|
|
25
|
+
} from '../parsers/package-json-parser.js';
|
|
23
26
|
import { parseServiceBindings } from '../parsers/service-binding-parser.js';
|
|
24
|
-
import { sha256File } from '../utils/hashing.js';
|
|
25
27
|
import { normalizePath } from '../utils/path-utils.js';
|
|
26
28
|
import { errorMessage } from '../utils/diagnostics.js';
|
|
27
29
|
import { sha256Text } from '../utils/hashing.js';
|
|
28
30
|
import { ANALYZER_VERSION } from '../version.js';
|
|
29
|
-
import
|
|
31
|
+
import {
|
|
32
|
+
loadRepositorySourceContext,
|
|
33
|
+
type RepositorySourceContext,
|
|
34
|
+
type SourceContextInstrumentation,
|
|
35
|
+
} from '../parsers/ts-project.js';
|
|
36
|
+
import type { CdsServiceFact, HandlerClassFact, HandlerRegistrationFact, OutboundCallFact, PackageFacts, ServiceBindingFact, ExecutableSymbolFact, SymbolCallFact } from '../types.js';
|
|
30
37
|
export interface IndexRepoResult {
|
|
31
38
|
fileCount: number;
|
|
32
39
|
diagnosticCount: number;
|
|
@@ -44,7 +51,7 @@ interface ParsedFacts {
|
|
|
44
51
|
}
|
|
45
52
|
export interface PreparedRepositoryIndex extends IndexRepoResult {
|
|
46
53
|
repo: RepoRow;
|
|
47
|
-
packageFacts?:
|
|
54
|
+
packageFacts?: PackageFacts;
|
|
48
55
|
fingerprint?: string;
|
|
49
56
|
kind?: string;
|
|
50
57
|
parsed?: ParsedFacts;
|
|
@@ -53,9 +60,10 @@ export async function indexRepository(
|
|
|
53
60
|
db: Db,
|
|
54
61
|
repo: RepoRow,
|
|
55
62
|
force: boolean,
|
|
63
|
+
instrumentation?: SourceContextInstrumentation,
|
|
56
64
|
): Promise<IndexRepoResult> {
|
|
57
65
|
try {
|
|
58
|
-
const prepared = await prepareRepositoryIndex(repo, force);
|
|
66
|
+
const prepared = await prepareRepositoryIndex(repo, force, instrumentation);
|
|
59
67
|
if (!prepared.skipped) db.transaction(() => publishPreparedRepositoryIndex(db, prepared));
|
|
60
68
|
return { fileCount: prepared.fileCount, diagnosticCount: prepared.diagnosticCount, skipped: prepared.skipped };
|
|
61
69
|
} catch (error) {
|
|
@@ -63,19 +71,36 @@ export async function indexRepository(
|
|
|
63
71
|
return { fileCount: 0, diagnosticCount: 1, skipped: false };
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
|
-
export async function prepareRepositoryIndex(
|
|
74
|
+
export async function prepareRepositoryIndex(
|
|
75
|
+
repo: RepoRow,
|
|
76
|
+
force: boolean,
|
|
77
|
+
instrumentation?: SourceContextInstrumentation,
|
|
78
|
+
): Promise<PreparedRepositoryIndex> {
|
|
67
79
|
const sourceFiles = await findSourceFiles(repo.absolute_path);
|
|
68
|
-
const
|
|
69
|
-
|
|
80
|
+
const packageSnapshot = await loadPackageJsonSnapshot(repo.absolute_path, {
|
|
81
|
+
strict: true,
|
|
82
|
+
allowMissing: repo.package_name === null,
|
|
83
|
+
});
|
|
84
|
+
const packageFacts = packageSnapshot.facts;
|
|
85
|
+
const sources = await loadRepositorySourceContext(
|
|
86
|
+
repo.absolute_path, sourceFiles, instrumentation,
|
|
87
|
+
);
|
|
88
|
+
const fingerprint = repositoryFingerprint(
|
|
89
|
+
sources, packageFacts, packageSnapshot.rawText,
|
|
90
|
+
);
|
|
70
91
|
if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
|
|
92
|
+
const parsed = await parseAllSourceFacts(repo.absolute_path, sources);
|
|
71
93
|
return {
|
|
72
94
|
repo,
|
|
73
95
|
packageFacts,
|
|
74
96
|
fingerprint,
|
|
75
97
|
kind: await classifyRepository(repo.absolute_path, packageFacts),
|
|
76
|
-
parsed
|
|
98
|
+
parsed,
|
|
77
99
|
fileCount: sourceFiles.length,
|
|
78
|
-
diagnosticCount:
|
|
100
|
+
diagnosticCount: parsed.handlers.filter((handler) =>
|
|
101
|
+
handler.hasHandlerDecorator
|
|
102
|
+
&& (handler.methods.length === 0
|
|
103
|
+
|| handler.methods.some((method) => !handlerMethodIsExecutable(method)))).length,
|
|
79
104
|
skipped: false,
|
|
80
105
|
};
|
|
81
106
|
}
|
|
@@ -104,21 +129,23 @@ export function recordIndexFailure(db: Db, repoId: number, error: unknown): void
|
|
|
104
129
|
db.prepare("DELETE FROM diagnostics WHERE repo_id=? AND code IN ('index_failed_snapshot_preserved','source_read_failed')").run(repoId);
|
|
105
130
|
db.prepare('INSERT INTO diagnostics(repo_id,severity,code,message) VALUES(?,?,?,?)').run(repoId, 'error', 'source_read_failed', `Index failed before publication; previous facts and fingerprint were preserved. ${message}`);
|
|
106
131
|
}
|
|
107
|
-
async function parseAllSourceFacts(
|
|
132
|
+
async function parseAllSourceFacts(
|
|
133
|
+
root: string,
|
|
134
|
+
sources: RepositorySourceContext,
|
|
135
|
+
): Promise<ParsedFacts> {
|
|
108
136
|
const facts: ParsedFacts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], fileRecords: [] };
|
|
109
|
-
for (const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (file.endsWith('.cds')) facts.services.push(...(await parseCdsFile(root, file)));
|
|
137
|
+
for (const snapshot of sources.entries()) {
|
|
138
|
+
const file = snapshot.filePath;
|
|
139
|
+
facts.fileRecords.push({ relativePath: normalizePath(file), extension: path.extname(file), sha256: sha256Text(snapshot.text), sizeBytes: snapshot.sizeBytes });
|
|
140
|
+
if (file.endsWith('.cds')) facts.services.push(...(await parseCdsFile(root, file, sources)));
|
|
114
141
|
if (/\.[jt]s$/.test(file)) {
|
|
115
|
-
facts.handlers.push(...(await parseDecorators(root, file)));
|
|
116
|
-
facts.registrations.push(...(await parseHandlerRegistrations(root, file)));
|
|
117
|
-
facts.bindings.push(...(await parseServiceBindings(root, file)));
|
|
118
|
-
const symbolFacts = await parseExecutableSymbols(root, file);
|
|
142
|
+
facts.handlers.push(...(await parseDecorators(root, file, sources)));
|
|
143
|
+
facts.registrations.push(...(await parseHandlerRegistrations(root, file, sources)));
|
|
144
|
+
facts.bindings.push(...(await parseServiceBindings(root, file, sources)));
|
|
145
|
+
const symbolFacts = await parseExecutableSymbols(root, file, sources);
|
|
119
146
|
facts.symbols.push(...symbolFacts.symbols);
|
|
120
147
|
facts.symbolCalls.push(...symbolFacts.calls);
|
|
121
|
-
facts.calls.push(...(await parseOutboundCalls(root, file)));
|
|
148
|
+
facts.calls.push(...(await parseOutboundCalls(root, file, sources)));
|
|
122
149
|
}
|
|
123
150
|
}
|
|
124
151
|
return facts;
|
|
@@ -126,7 +153,7 @@ async function parseAllSourceFacts(root: string, files: string[]): Promise<Parse
|
|
|
126
153
|
async function findSourceFiles(root: string): Promise<string[]> {
|
|
127
154
|
const out: string[] = [];
|
|
128
155
|
async function walk(dir: string, prefix = ''): Promise<void> {
|
|
129
|
-
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
156
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
130
157
|
for (const e of entries) {
|
|
131
158
|
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
132
159
|
if (e.isDirectory()) {
|
|
@@ -142,8 +169,11 @@ function isDefaultTestFile(relativeFile: string): boolean {
|
|
|
142
169
|
if (parts.some((part) => ['test', 'tests', '__tests__'].includes(part))) return true;
|
|
143
170
|
return /\.(test|spec)\.[jt]s$/.test(parts.at(-1) ?? '');
|
|
144
171
|
}
|
|
145
|
-
|
|
146
|
-
|
|
172
|
+
function repositoryFingerprint(
|
|
173
|
+
sources: RepositorySourceContext,
|
|
174
|
+
facts: PackageFacts,
|
|
175
|
+
packageJsonText: string,
|
|
176
|
+
): string {
|
|
147
177
|
const normalizedFacts = {
|
|
148
178
|
analyzerVersion: ANALYZER_VERSION,
|
|
149
179
|
packageName: facts.packageName,
|
|
@@ -152,12 +182,10 @@ async function repositoryFingerprint(root: string, files: string[], facts: Await
|
|
|
152
182
|
cdsRequires: [...facts.cdsRequires].sort((a, b) => a.alias.localeCompare(b.alias)),
|
|
153
183
|
scripts: Object.fromEntries(Object.entries(facts.scripts).sort()),
|
|
154
184
|
includeTests: false,
|
|
155
|
-
packageJsonHash: sha256Text(
|
|
185
|
+
packageJsonHash: sha256Text(packageJsonText),
|
|
156
186
|
};
|
|
157
187
|
const entries: string[] = [`facts:${JSON.stringify(normalizedFacts)}`];
|
|
158
|
-
for (const
|
|
159
|
-
|
|
160
|
-
entries.push(`${file}:${sha256Text(content)}`);
|
|
161
|
-
}
|
|
188
|
+
for (const snapshot of sources.entries())
|
|
189
|
+
entries.push(`${snapshot.filePath}:${sha256Text(snapshot.text)}`);
|
|
162
190
|
return sha256Text(entries.join('\n'));
|
|
163
191
|
}
|
|
@@ -1,16 +1,94 @@
|
|
|
1
1
|
import type { Db } from '../db/connection.js';
|
|
2
|
-
import { listRepositories,
|
|
2
|
+
import { listRepositories, reposByName } from '../db/repositories.js';
|
|
3
3
|
import { errorMessage } from '../utils/diagnostics.js';
|
|
4
4
|
import { prepareRepositoryIndex, publishPreparedRepositoryIndex, recordIndexFailure, type PreparedRepositoryIndex } from './repository-indexer.js';
|
|
5
5
|
import { materializeCdsExtensionOperations } from './cds-extension-resolver.js';
|
|
6
|
+
// Ownerless rows predate PID coordination; this matches doctor's stale-run threshold without taking over a recent legacy writer.
|
|
7
|
+
const LEGACY_OWNER_RECOVERY_MS = 60 * 60 * 1_000;
|
|
8
|
+
type RunningIndexRow = Record<string, unknown>;
|
|
9
|
+
function ownerPid(value: unknown): number | undefined {
|
|
10
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0
|
|
11
|
+
? value
|
|
12
|
+
: undefined;
|
|
13
|
+
}
|
|
14
|
+
function processIsAlive(pid: number): boolean {
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
// Only ESRCH proves that ownership ended; permission and platform errors must fail closed.
|
|
20
|
+
const ownerIsMissing = typeof error === 'object'
|
|
21
|
+
&& error !== null
|
|
22
|
+
&& 'code' in error
|
|
23
|
+
&& error.code === 'ESRCH';
|
|
24
|
+
return !ownerIsMissing;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isRecoverableRun(row: RunningIndexRow, now: number): boolean {
|
|
28
|
+
const pid = ownerPid(row.ownerPid);
|
|
29
|
+
if (pid !== undefined) return !processIsAlive(pid);
|
|
30
|
+
if (typeof row.startedAt !== 'string') return false;
|
|
31
|
+
const startedAt = Date.parse(row.startedAt);
|
|
32
|
+
return Number.isFinite(startedAt) && now - startedAt >= LEGACY_OWNER_RECOVERY_MS;
|
|
33
|
+
}
|
|
34
|
+
function recoveredOwnerMessage(row: RunningIndexRow): string {
|
|
35
|
+
const pid = ownerPid(row.ownerPid);
|
|
36
|
+
return pid === undefined
|
|
37
|
+
? 'Recovered stale legacy index writer without owner process metadata.'
|
|
38
|
+
: `Recovered stale index writer because owner process ${pid} is no longer running.`;
|
|
39
|
+
}
|
|
40
|
+
export function claimIndexRun(
|
|
41
|
+
db: Db,
|
|
42
|
+
workspaceId: number,
|
|
43
|
+
repoCount: number,
|
|
44
|
+
): number {
|
|
45
|
+
// The short write transaction serializes claims without holding a SQLite writer lock during source preparation.
|
|
46
|
+
try {
|
|
47
|
+
return db.transaction(() => {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const rows = db
|
|
50
|
+
.prepare("SELECT id,workspace_id workspaceId,owner_pid ownerPid,started_at startedAt FROM index_runs WHERE status='running' ORDER BY id")
|
|
51
|
+
.all();
|
|
52
|
+
const active = rows.find((row) => !isRecoverableRun(row, now));
|
|
53
|
+
if (active) {
|
|
54
|
+
const pid = ownerPid(active.ownerPid);
|
|
55
|
+
const owner = pid === undefined ? 'an unknown owner' : `process ${pid}`;
|
|
56
|
+
throw new Error(`index_writer_active: this database is already being indexed for workspace ${String(active.workspaceId ?? 'unknown')} by ${owner}; wait for that writer to finish.`);
|
|
57
|
+
}
|
|
58
|
+
const finish = db.prepare(
|
|
59
|
+
"UPDATE index_runs SET finished_at=?,status='failed',error_message=? WHERE id=?",
|
|
60
|
+
);
|
|
61
|
+
for (const row of rows)
|
|
62
|
+
finish.run(new Date(now).toISOString(), recoveredOwnerMessage(row), row.id);
|
|
63
|
+
const inserted = db
|
|
64
|
+
.prepare('INSERT INTO index_runs(workspace_id,started_at,status,repo_count,file_count,diagnostic_count,owner_pid) VALUES(?,?,?,?,?,?,?) RETURNING id')
|
|
65
|
+
.get(workspaceId, new Date(now).toISOString(), 'running', repoCount, 0, 0, process.pid);
|
|
66
|
+
const runId = Number(inserted?.id);
|
|
67
|
+
if (!Number.isSafeInteger(runId)) throw new Error('index_writer_claim_failed: SQLite did not return an index run identifier.');
|
|
68
|
+
return runId;
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (/\b(?:locked|busy)\b/i.test(errorMessage(error)))
|
|
72
|
+
throw new Error(
|
|
73
|
+
'index_writer_coordination_failed: SQLite remained busy beyond the bounded writer-claim interval; wait for the active publication to finish.',
|
|
74
|
+
{ cause: error },
|
|
75
|
+
);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
6
79
|
export async function indexWorkspace(
|
|
7
80
|
db: Db,
|
|
8
81
|
workspaceId: number,
|
|
9
82
|
options: { repo?: string; force: boolean; injectDerivedMaterializationFailure?: boolean },
|
|
10
83
|
): Promise<{ repoCount: number; indexedCount: number; skippedCount: number; fileCount: number; diagnosticCount: number }> {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
84
|
+
const repos = options.repo
|
|
85
|
+
? reposByName(db, options.repo, workspaceId)
|
|
86
|
+
: listRepositories(db, workspaceId);
|
|
87
|
+
if (options.repo && repos.length === 0)
|
|
88
|
+
throw new Error(`selector_repo_not_found: no indexed repository matched ${options.repo}.`);
|
|
89
|
+
if (options.repo && repos.length > 1)
|
|
90
|
+
throw new Error(`selector_repo_ambiguous: repository selector ${options.repo} matched ${repos.length} repositories; use a unique repository name.`);
|
|
91
|
+
const runId = claimIndexRun(db, workspaceId, repos.length);
|
|
14
92
|
let fileCount = 0;
|
|
15
93
|
let diagnosticCount = 0;
|
|
16
94
|
let skippedCount = 0;
|
|
@@ -32,12 +110,12 @@ export async function indexWorkspace(
|
|
|
32
110
|
}
|
|
33
111
|
if (options.injectDerivedMaterializationFailure) throw new Error('Injected derived materialization failure');
|
|
34
112
|
materializeCdsExtensionOperations(db, workspaceId);
|
|
35
|
-
db.prepare(
|
|
113
|
+
db.prepare("UPDATE index_runs SET finished_at=?, status='success', file_count=?, diagnostic_count=? WHERE id=?").run(new Date().toISOString(), fileCount, diagnosticCount, runId);
|
|
36
114
|
});
|
|
37
115
|
return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
|
|
38
116
|
} catch (error) {
|
|
39
|
-
if (activeRepoId && preparedRows.length < repos.length) recordIndexFailure(db, activeRepoId, error);
|
|
40
117
|
db.prepare("UPDATE index_runs SET finished_at=?, status='failed', file_count=?, diagnostic_count=?, error_message=? WHERE id=?").run(new Date().toISOString(), fileCount, diagnosticCount + 1, errorMessage(error), runId);
|
|
118
|
+
if (activeRepoId && preparedRows.length < repos.length) recordIndexFailure(db, activeRepoId, error);
|
|
41
119
|
throw error;
|
|
42
120
|
}
|
|
43
121
|
}
|
|
@@ -224,7 +224,15 @@ function compactCandidateScores(candidates: unknown[]): Array<Record<string, unk
|
|
|
224
224
|
return candidates.flatMap((candidate): Array<Record<string, unknown>> => {
|
|
225
225
|
const row = objectValue(candidate);
|
|
226
226
|
if (!row) return [];
|
|
227
|
-
return [{
|
|
227
|
+
return [{
|
|
228
|
+
repo: row.repoName,
|
|
229
|
+
servicePath: row.servicePath,
|
|
230
|
+
operationPath: row.operationPath,
|
|
231
|
+
score: row.score,
|
|
232
|
+
reasons: Array.isArray(row.reasons)
|
|
233
|
+
? row.reasons.filter((reason): reason is string => typeof reason === 'string')
|
|
234
|
+
: ['operation_path_match'],
|
|
235
|
+
}];
|
|
228
236
|
});
|
|
229
237
|
}
|
|
230
238
|
|
|
@@ -445,7 +453,7 @@ function implementationCandidates(db: Db, workspaceId: number, operation: Record
|
|
|
445
453
|
CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
|
|
446
454
|
CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id AND localService.service_path=?) THEN 1 ELSE 0 END localServicePathMatch,
|
|
447
455
|
CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id) THEN 1 ELSE 0 END applicationHasLocalServices,
|
|
448
|
-
CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON ((localReg.handler_class_id IS NOT NULL AND localClass.id=localReg.handler_class_id) OR (localReg.handler_class_id IS NULL AND localClass.class_name=localReg.class_name AND localClass.repo_id=localReg.repo_id)) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=? OR localMethod.decorator_raw_expression LIKE ?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
|
|
456
|
+
CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON ((localReg.handler_class_id IS NOT NULL AND localClass.id=localReg.handler_class_id) OR (localReg.handler_class_id IS NULL AND localClass.class_name=localReg.class_name AND localClass.repo_id=localReg.repo_id)) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND COALESCE(json_extract(localMethod.decorator_resolution_json,'$.handlerKind'),CASE WHEN localMethod.decorator_kind='Event' THEN 'event' WHEN localMethod.decorator_kind IN ('Action','Func','On') THEN 'operation' ELSE 'unsupported' END)='operation' AND COALESCE(json_extract(localMethod.decorator_resolution_json,'$.executable'),CASE WHEN localMethod.decorator_kind IN ('Action','Func','On') THEN 1 ELSE 0 END)=1 AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=? OR localMethod.decorator_raw_expression LIKE ?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
|
|
449
457
|
CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END appDependsOnModel,
|
|
450
458
|
CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=CAST(handlerRepo.id AS TEXT)) THEN 1 ELSE 0 END appDependsOnHandler,
|
|
451
459
|
CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(handlerRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END handlerDependsOnModel
|
|
@@ -477,6 +485,13 @@ function implementationCandidates(db: Db, workspaceId: number, operation: Record
|
|
|
477
485
|
)
|
|
478
486
|
JOIN repositories appRepo ON appRepo.id=hr.repo_id
|
|
479
487
|
WHERE appRepo.workspace_id=?
|
|
488
|
+
AND COALESCE(json_extract(hm.decorator_resolution_json,'$.handlerKind'),
|
|
489
|
+
CASE WHEN hm.decorator_kind='Event' THEN 'event'
|
|
490
|
+
WHEN hm.decorator_kind IN ('Action','Func','On') THEN 'operation'
|
|
491
|
+
ELSE 'unsupported' END)='operation'
|
|
492
|
+
AND COALESCE(json_extract(hm.decorator_resolution_json,'$.executable'),
|
|
493
|
+
CASE WHEN hm.decorator_kind IN ('Action','Func','On')
|
|
494
|
+
THEN 1 ELSE 0 END)=1
|
|
480
495
|
AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=? OR hm.decorator_raw_expression LIKE ?)`).all(
|
|
481
496
|
operation.modelRepoId,
|
|
482
497
|
operation.modelRepo,
|
|
@@ -605,6 +620,11 @@ function candidateEvidence(candidate: ImplementationCandidate, rank: number): Re
|
|
|
605
620
|
};
|
|
606
621
|
}
|
|
607
622
|
function implementationMethodSignal(row: Record<string, unknown>, operation: Record<string, unknown>): { matches: boolean; contradicted: boolean; acceptedReasons: string[]; rejectedReasons: string[] } {
|
|
623
|
+
const resolution = objectJson(row.decoratorResolutionJson) ?? {};
|
|
624
|
+
if (resolution.handlerKind && resolution.handlerKind !== 'operation')
|
|
625
|
+
return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ['non_operation_handler_kind'] };
|
|
626
|
+
if (resolution.executable === false)
|
|
627
|
+
return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ['handler_method_not_executable'] };
|
|
608
628
|
const op = normalizedOperationName(String(operation.operationPath ?? operation.operationName ?? ''));
|
|
609
629
|
const decorator = normalizeDecoratorOperationSignal(typeof row.decoratorValue === 'string' ? row.decoratorValue : undefined, typeof row.decoratorRawExpression === 'string' ? row.decoratorRawExpression : undefined, op);
|
|
610
630
|
if (decorator.status === 'resolved' && decorator.operationName === op) return { matches: true, contradicted: false, acceptedReasons: ['decorator targets operation'], rejectedReasons: [] };
|
|
@@ -26,9 +26,9 @@ function rows(
|
|
|
26
26
|
workspaceId?: number,
|
|
27
27
|
): OperationTarget[] {
|
|
28
28
|
const names = operationLookupNames(operationPath);
|
|
29
|
-
|
|
29
|
+
const result = db
|
|
30
30
|
.prepare(
|
|
31
|
-
`SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score
|
|
31
|
+
`SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score
|
|
32
32
|
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
33
33
|
WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path IN (?,?) OR o.operation_name IN (?,?)) ORDER BY r.name,s.service_path,o.operation_name`,
|
|
34
34
|
)
|
|
@@ -39,7 +39,12 @@ function rows(
|
|
|
39
39
|
names.simplePath,
|
|
40
40
|
names.name,
|
|
41
41
|
names.simpleName,
|
|
42
|
-
) as
|
|
42
|
+
) as Array<Omit<OperationTarget, 'reasons'>>;
|
|
43
|
+
return result.map((row) => ({
|
|
44
|
+
...row,
|
|
45
|
+
score: Number(row.score ?? 0),
|
|
46
|
+
reasons: [],
|
|
47
|
+
}));
|
|
43
48
|
}
|
|
44
49
|
function operationLookupNames(operationPath: string): { path: string; simplePath: string; name: string; simpleName: string } {
|
|
45
50
|
const name = operationPath.replace(/^\//, '');
|
|
@@ -70,7 +75,13 @@ export function resolveOperation(
|
|
|
70
75
|
if (missing.length > 0)
|
|
71
76
|
return {
|
|
72
77
|
status: 'dynamic',
|
|
73
|
-
candidates: signals.operationPath
|
|
78
|
+
candidates: signals.operationPath
|
|
79
|
+
? rows(db, signals.operationPath, workspaceId).map((candidate) => ({
|
|
80
|
+
...candidate,
|
|
81
|
+
score: 0.2,
|
|
82
|
+
reasons: ['operation_path_match'],
|
|
83
|
+
}))
|
|
84
|
+
: [],
|
|
74
85
|
reasons: [...new Set(missing)].map((name) => `missing_variable:${name}`),
|
|
75
86
|
};
|
|
76
87
|
if (!signals.operationPath)
|