@saptools/service-flow 0.1.65 → 0.1.67
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 +16 -0
- package/README.md +67 -11
- package/TECHNICAL-NOTE.md +41 -1
- package/dist/{chunk-OONNRIDL.js → chunk-ZQABU7MR.js} +3764 -643
- package/dist/chunk-ZQABU7MR.js.map +1 -0
- package/dist/cli.js +158 -295
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +196 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/002-doctor-lifecycle.ts +66 -0
- package/src/cli/doctor.ts +14 -27
- package/src/cli.ts +130 -102
- package/src/db/000-call-fact-repository.ts +537 -0
- package/src/db/001-fact-lifecycle.ts +111 -0
- package/src/db/migrations.ts +16 -1
- package/src/db/repositories.ts +1 -315
- package/src/db/schema.ts +4 -2
- package/src/index.ts +22 -0
- package/src/linker/004-event-subscription-handler-linker.ts +300 -0
- package/src/linker/cross-repo-linker.ts +23 -2
- package/src/output/001-compact-json-output.ts +6 -0
- package/src/parsers/imported-wrapper-parser.ts +4 -0
- package/src/parsers/outbound-call-parser.ts +11 -1
- package/src/parsers/symbol-parser.ts +7 -4
- package/src/trace/010-traversal-scope.ts +188 -0
- package/src/trace/011-event-subscriber-traversal.ts +366 -0
- package/src/trace/012-trace-graph-lookups.ts +74 -0
- package/src/trace/013-trace-root-scopes.ts +279 -0
- package/src/trace/014-compact-contract.ts +347 -0
- package/src/trace/015-trace-edge-recorder.ts +336 -0
- package/src/trace/016-compact-projector.ts +697 -0
- package/src/trace/017-trace-context.ts +378 -0
- package/src/trace/018-compact-trace.ts +87 -0
- package/src/trace/019-trace-edge-semantics.ts +328 -0
- package/src/trace/020-compact-field-projection.ts +387 -0
- package/src/trace/dynamic-branches.ts +35 -13
- package/src/trace/trace-engine.ts +271 -245
- package/src/types.ts +10 -0
- package/src/version.ts +1 -1
- package/dist/chunk-OONNRIDL.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -125,6 +125,8 @@ interface OutboundCallFact {
|
|
|
125
125
|
payloadSummary?: string;
|
|
126
126
|
sourceFile: string;
|
|
127
127
|
sourceLine: number;
|
|
128
|
+
callSiteStartOffset?: number;
|
|
129
|
+
callSiteEndOffset?: number;
|
|
128
130
|
confidence: number;
|
|
129
131
|
unresolvedReason?: string;
|
|
130
132
|
evidence?: Record<string, unknown>;
|
|
@@ -248,6 +250,10 @@ interface LinkWorkspaceResult {
|
|
|
248
250
|
implementationResolvedCount: number;
|
|
249
251
|
implementationAmbiguousCount: number;
|
|
250
252
|
implementationUnresolvedCount: number;
|
|
253
|
+
subscriptionHandlerResolvedCount: number;
|
|
254
|
+
subscriptionHandlerAmbiguousCount: number;
|
|
255
|
+
subscriptionHandlerUnresolvedCount: number;
|
|
256
|
+
subscriptionHandlerMissingAssociationCount: number;
|
|
251
257
|
}
|
|
252
258
|
declare function linkWorkspace(db: Db, workspaceId: number, vars?: Record<string, string>): LinkWorkspaceResult;
|
|
253
259
|
|
|
@@ -263,11 +269,200 @@ declare function applyVariables(template: string | undefined, vars: Record<strin
|
|
|
263
269
|
declare function extractPlaceholders(template: string | undefined): string[];
|
|
264
270
|
declare function substituteVariables(template: string | undefined, vars: Record<string, string>): RuntimeSubstitution;
|
|
265
271
|
|
|
272
|
+
type CompactStatus = 'resolved' | 'terminal' | 'inferred' | 'dynamic' | 'ambiguous' | 'unresolved' | 'cycle';
|
|
273
|
+
interface CompactSourceContext {
|
|
274
|
+
schemaVersion: number;
|
|
275
|
+
analyzerVersion: string;
|
|
276
|
+
graphGeneration: number;
|
|
277
|
+
}
|
|
278
|
+
interface CompactHintV1 {
|
|
279
|
+
servicePath: string | null;
|
|
280
|
+
operationPath: string | null;
|
|
281
|
+
packageName: string | null;
|
|
282
|
+
repositoryName: string | null;
|
|
283
|
+
candidateFamily: string | null;
|
|
284
|
+
implementationRepo: string | null;
|
|
285
|
+
}
|
|
286
|
+
interface CompactStartV1 {
|
|
287
|
+
repo: string | null;
|
|
288
|
+
servicePath: string | null;
|
|
289
|
+
operation: string | null;
|
|
290
|
+
operationPath: string | null;
|
|
291
|
+
handler: string | null;
|
|
292
|
+
}
|
|
293
|
+
interface CompactQueryV1 {
|
|
294
|
+
depth: number;
|
|
295
|
+
includeAsync: boolean;
|
|
296
|
+
includeDb: boolean;
|
|
297
|
+
includeExternal: boolean;
|
|
298
|
+
dynamicMode: DynamicMode;
|
|
299
|
+
maxDynamicCandidates: number;
|
|
300
|
+
suppliedVariableNames: string[];
|
|
301
|
+
runtimeValuesOmitted: true;
|
|
302
|
+
implementationRepo: string | null;
|
|
303
|
+
implementationHints: CompactHintV1[];
|
|
304
|
+
}
|
|
305
|
+
interface CompactReferenceGroupV1 {
|
|
306
|
+
values: Array<number | string>;
|
|
307
|
+
total: number;
|
|
308
|
+
shown: number;
|
|
309
|
+
omitted: number;
|
|
310
|
+
}
|
|
311
|
+
interface CompactReferencesV1 {
|
|
312
|
+
graphEdgeIds?: CompactReferenceGroupV1;
|
|
313
|
+
outboundCallIds?: CompactReferenceGroupV1;
|
|
314
|
+
subscribeCallIds?: CompactReferenceGroupV1;
|
|
315
|
+
symbolCallIds?: CompactReferenceGroupV1;
|
|
316
|
+
operationIds?: CompactReferenceGroupV1;
|
|
317
|
+
symbolIds?: CompactReferenceGroupV1;
|
|
318
|
+
handlerMethodIds?: CompactReferenceGroupV1;
|
|
319
|
+
}
|
|
320
|
+
interface CompactDecisionV1 {
|
|
321
|
+
effectiveResolutionStatus?: string;
|
|
322
|
+
effectiveTarget?: string;
|
|
323
|
+
persistedResolutionStatus?: string;
|
|
324
|
+
persistedTarget?: string;
|
|
325
|
+
missingVariableNames?: string[];
|
|
326
|
+
missingVariableCount?: number;
|
|
327
|
+
shownMissingVariableCount?: number;
|
|
328
|
+
omittedMissingVariableCount?: number;
|
|
329
|
+
dynamicMode?: DynamicMode;
|
|
330
|
+
candidateCount?: number;
|
|
331
|
+
viableCandidateCount?: number;
|
|
332
|
+
rejectedCandidateCount?: number;
|
|
333
|
+
omittedCandidateCount?: number;
|
|
334
|
+
implementationStrategy?: string;
|
|
335
|
+
implementationGuided?: boolean;
|
|
336
|
+
implementationContextual?: boolean;
|
|
337
|
+
eventMatchStrategy?: string;
|
|
338
|
+
dispatchCertainty?: string;
|
|
339
|
+
eventSubscriptionCount?: number;
|
|
340
|
+
associationStatus?: string;
|
|
341
|
+
associationBasis?: string;
|
|
342
|
+
eventScope?: string;
|
|
343
|
+
callRole?: string;
|
|
344
|
+
factOrigin?: string;
|
|
345
|
+
roleSiteMatchCount?: number;
|
|
346
|
+
bodyExpansion?: string;
|
|
347
|
+
reasonCode?: string;
|
|
348
|
+
remediationHint?: string;
|
|
349
|
+
omittedRemediationHintCount?: number;
|
|
350
|
+
}
|
|
351
|
+
interface CompactEdgeDetailsV1 {
|
|
352
|
+
decision: CompactDecisionV1;
|
|
353
|
+
refs: CompactReferencesV1;
|
|
354
|
+
}
|
|
355
|
+
interface CompactDiagnosticDetailsV1 {
|
|
356
|
+
reasonCode?: string;
|
|
357
|
+
missingVariableNames?: string[];
|
|
358
|
+
missingVariableCount?: number;
|
|
359
|
+
shownMissingVariableCount?: number;
|
|
360
|
+
omittedMissingVariableCount?: number;
|
|
361
|
+
candidateCount?: number;
|
|
362
|
+
viableCandidateCount?: number;
|
|
363
|
+
rejectedCandidateCount?: number;
|
|
364
|
+
remediationHint?: string;
|
|
365
|
+
omittedHintCount?: number;
|
|
366
|
+
}
|
|
367
|
+
type CompactNodeRowV1 = [
|
|
368
|
+
id: string,
|
|
369
|
+
kind: string,
|
|
370
|
+
label: string,
|
|
371
|
+
repo: number | null,
|
|
372
|
+
file: number | null,
|
|
373
|
+
line: number | null
|
|
374
|
+
];
|
|
375
|
+
type CompactEdgeRowV1 = [
|
|
376
|
+
id: string,
|
|
377
|
+
traceOrdinals: number[],
|
|
378
|
+
step: number,
|
|
379
|
+
type: string,
|
|
380
|
+
from: string,
|
|
381
|
+
to: string,
|
|
382
|
+
status: CompactStatus,
|
|
383
|
+
confidence: number,
|
|
384
|
+
count: number,
|
|
385
|
+
details: CompactEdgeDetailsV1 | null
|
|
386
|
+
];
|
|
387
|
+
type CompactDiagnosticRowV1 = [
|
|
388
|
+
fullDiagnosticIndex: number,
|
|
389
|
+
severity: 'error' | 'warning' | 'info',
|
|
390
|
+
code: string,
|
|
391
|
+
message: string,
|
|
392
|
+
file: number | null,
|
|
393
|
+
line: number | null,
|
|
394
|
+
details: CompactDiagnosticDetailsV1 | null
|
|
395
|
+
];
|
|
396
|
+
interface CompactStatusCountsV1 {
|
|
397
|
+
resolved: number;
|
|
398
|
+
terminal: number;
|
|
399
|
+
inferred: number;
|
|
400
|
+
dynamic: number;
|
|
401
|
+
ambiguous: number;
|
|
402
|
+
unresolved: number;
|
|
403
|
+
cycle: number;
|
|
404
|
+
}
|
|
405
|
+
interface CompactGraphV1 {
|
|
406
|
+
schema: 'service-flow/compact-graph@1';
|
|
407
|
+
start: CompactStartV1;
|
|
408
|
+
query: CompactQueryV1;
|
|
409
|
+
source: CompactSourceContext;
|
|
410
|
+
summary: {
|
|
411
|
+
completeness: 'complete' | 'partial' | 'blocked';
|
|
412
|
+
fullTraceNodes: number;
|
|
413
|
+
fullTraceEdges: number;
|
|
414
|
+
fullTraceDiagnostics: number;
|
|
415
|
+
nodes: number;
|
|
416
|
+
edges: number;
|
|
417
|
+
collapsedEdges: number;
|
|
418
|
+
statusCounts: CompactStatusCountsV1;
|
|
419
|
+
projection: {
|
|
420
|
+
evidence: 'summary-only';
|
|
421
|
+
syntheticEndpoints: number;
|
|
422
|
+
omittedUnreferencedFullNodes: number;
|
|
423
|
+
};
|
|
424
|
+
};
|
|
425
|
+
repos: string[];
|
|
426
|
+
files: string[];
|
|
427
|
+
nodeColumns: ['id', 'kind', 'label', 'repo', 'file', 'line'];
|
|
428
|
+
nodes: CompactNodeRowV1[];
|
|
429
|
+
edgeColumns: [
|
|
430
|
+
'id',
|
|
431
|
+
'traceOrdinals',
|
|
432
|
+
'step',
|
|
433
|
+
'type',
|
|
434
|
+
'from',
|
|
435
|
+
'to',
|
|
436
|
+
'status',
|
|
437
|
+
'confidence',
|
|
438
|
+
'count',
|
|
439
|
+
'details'
|
|
440
|
+
];
|
|
441
|
+
edges: CompactEdgeRowV1[];
|
|
442
|
+
diagnosticColumns: [
|
|
443
|
+
'fullDiagnosticIndex',
|
|
444
|
+
'severity',
|
|
445
|
+
'code',
|
|
446
|
+
'message',
|
|
447
|
+
'file',
|
|
448
|
+
'line',
|
|
449
|
+
'details'
|
|
450
|
+
];
|
|
451
|
+
diagnostics: CompactDiagnosticRowV1[];
|
|
452
|
+
}
|
|
453
|
+
|
|
266
454
|
declare function trace(db: Db, start: TraceStart, options: TraceOptions): TraceResult;
|
|
267
455
|
|
|
456
|
+
interface CompactTraceExecution {
|
|
457
|
+
trace: TraceResult;
|
|
458
|
+
compact: CompactGraphV1;
|
|
459
|
+
}
|
|
460
|
+
declare function compactTrace(db: Db, start: TraceStart, options: TraceOptions): CompactGraphV1;
|
|
461
|
+
declare function traceAndCompact(db: Db, start: TraceStart, options: TraceOptions): CompactTraceExecution;
|
|
462
|
+
|
|
268
463
|
declare function parseImplementationHint(value: string): ImplementationHint;
|
|
269
464
|
|
|
270
465
|
declare function redactText(text: string): string;
|
|
271
466
|
declare function redactValue(value: unknown): unknown;
|
|
272
467
|
|
|
273
|
-
export { type DynamicMode, type ImplementationHint, type RuntimeSubstitution, type TraceOptions, applyVariables, discoverRepositories, extractPlaceholders, linkWorkspace, parseCdsFile, parseDecorators, parseGeneratedConstants, parseHandlerRegistrations, parseImplementationHint, parseOutboundCalls, parsePackageJson, parseServiceBindings, redactText, redactValue, substituteVariables, trace };
|
|
468
|
+
export { type CompactDecisionV1, type CompactDiagnosticDetailsV1, type CompactDiagnosticRowV1, type CompactEdgeDetailsV1, type CompactEdgeRowV1, type CompactGraphV1, type CompactHintV1, type CompactNodeRowV1, type CompactQueryV1, type CompactReferenceGroupV1, type CompactReferencesV1, type CompactSourceContext, type CompactStartV1, type CompactStatus, type CompactStatusCountsV1, type CompactTraceExecution, type DynamicMode, type ImplementationHint, type RuntimeSubstitution, type TraceOptions, applyVariables, compactTrace, discoverRepositories, extractPlaceholders, linkWorkspace, parseCdsFile, parseDecorators, parseGeneratedConstants, parseHandlerRegistrations, parseImplementationHint, parseOutboundCalls, parsePackageJson, parseServiceBindings, redactText, redactValue, substituteVariables, trace, traceAndCompact };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
applyVariables,
|
|
4
|
+
compactTrace,
|
|
4
5
|
discoverRepositories,
|
|
5
6
|
extractPlaceholders,
|
|
6
7
|
linkWorkspace,
|
|
@@ -16,8 +17,9 @@ import {
|
|
|
16
17
|
redactValue,
|
|
17
18
|
stripQuotes,
|
|
18
19
|
substituteVariables,
|
|
19
|
-
trace
|
|
20
|
-
|
|
20
|
+
trace,
|
|
21
|
+
traceAndCompact
|
|
22
|
+
} from "./chunk-ZQABU7MR.js";
|
|
21
23
|
|
|
22
24
|
// src/parsers/generated-constants-parser.ts
|
|
23
25
|
import fs from "fs/promises";
|
|
@@ -40,6 +42,7 @@ async function parseGeneratedConstants(repoPath, filePath) {
|
|
|
40
42
|
}
|
|
41
43
|
export {
|
|
42
44
|
applyVariables,
|
|
45
|
+
compactTrace,
|
|
43
46
|
discoverRepositories,
|
|
44
47
|
extractPlaceholders,
|
|
45
48
|
linkWorkspace,
|
|
@@ -54,6 +57,7 @@ export {
|
|
|
54
57
|
redactText,
|
|
55
58
|
redactValue,
|
|
56
59
|
substituteVariables,
|
|
57
|
-
trace
|
|
60
|
+
trace,
|
|
61
|
+
traceAndCompact
|
|
58
62
|
};
|
|
59
63
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/parsers/generated-constants-parser.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { GeneratedConstantFact } from '../types.js';\nimport { normalizePath, stripQuotes } from '../utils/path-utils.js';\nfunction lineOf(text: string, idx: number): number {\n return text.slice(0, idx).split('\\n').length;\n}\nexport async function parseGeneratedConstants(\n repoPath: string,\n filePath: string\n): Promise<GeneratedConstantFact[]> {\n const text = await fs.readFile(path.join(repoPath, filePath), 'utf8');\n return [\n ...text.matchAll(\n /(?:export\\s+)?(?:const|static\\s+readonly)\\s+(\\w+)\\s*=\\s*(['\"])([^'\"]+)\\2/g\n )\n ].map((m) => ({\n name: m[1] ?? 'constant',\n value: stripQuotes(m[3] ?? ''),\n sourceFile: normalizePath(filePath),\n sourceLine: lineOf(text, m.index ?? 0)\n }));\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/parsers/generated-constants-parser.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { GeneratedConstantFact } from '../types.js';\nimport { normalizePath, stripQuotes } from '../utils/path-utils.js';\nfunction lineOf(text: string, idx: number): number {\n return text.slice(0, idx).split('\\n').length;\n}\nexport async function parseGeneratedConstants(\n repoPath: string,\n filePath: string\n): Promise<GeneratedConstantFact[]> {\n const text = await fs.readFile(path.join(repoPath, filePath), 'utf8');\n return [\n ...text.matchAll(\n /(?:export\\s+)?(?:const|static\\s+readonly)\\s+(\\w+)\\s*=\\s*(['\"])([^'\"]+)\\2/g\n )\n ].map((m) => ({\n name: m[1] ?? 'constant',\n value: stripQuotes(m[3] ?? ''),\n sourceFile: normalizePath(filePath),\n sourceLine: lineOf(text, m.index ?? 0)\n }));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AAGjB,SAAS,OAAO,MAAc,KAAqB;AACjD,SAAO,KAAK,MAAM,GAAG,GAAG,EAAE,MAAM,IAAI,EAAE;AACxC;AACA,eAAsB,wBACpB,UACA,UACkC;AAClC,QAAM,OAAO,MAAM,GAAG,SAAS,KAAK,KAAK,UAAU,QAAQ,GAAG,MAAM;AACpE,SAAO;AAAA,IACL,GAAG,KAAK;AAAA,MACN;AAAA,IACF;AAAA,EACF,EAAE,IAAI,CAAC,OAAO;AAAA,IACZ,MAAM,EAAE,CAAC,KAAK;AAAA,IACd,OAAO,YAAY,EAAE,CAAC,KAAK,EAAE;AAAA,IAC7B,YAAY,cAAc,QAAQ;AAAA,IAClC,YAAY,OAAO,MAAM,EAAE,SAAS,CAAC;AAAA,EACvC,EAAE;AACJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import { factLifecycleDiagnostic } from '../db/001-fact-lifecycle.js';
|
|
3
|
+
import { ANALYZER_VERSION } from '../version.js';
|
|
4
|
+
|
|
5
|
+
type Diagnostic = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
export function linkUpgradeWarnings(
|
|
8
|
+
db: Db,
|
|
9
|
+
workspaceId?: number,
|
|
10
|
+
): Diagnostic[] {
|
|
11
|
+
const lifecycle = factLifecycleDiagnostic(db, workspaceId);
|
|
12
|
+
if (lifecycle) return [lifecycle];
|
|
13
|
+
return [
|
|
14
|
+
...schemaDriftDiagnostics(db, true, workspaceId),
|
|
15
|
+
...analyzerVersionDiagnostics(db, true, workspaceId),
|
|
16
|
+
].filter((item) => [
|
|
17
|
+
'schema_legacy_columns_present',
|
|
18
|
+
'external_target_columns_missing_data',
|
|
19
|
+
'reindex_required_after_upgrade',
|
|
20
|
+
'reindex_required_after_analyzer_upgrade',
|
|
21
|
+
].includes(String(item.code)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function schemaDriftDiagnostics(
|
|
25
|
+
db: Db,
|
|
26
|
+
strict: boolean,
|
|
27
|
+
workspaceId?: number,
|
|
28
|
+
): Diagnostic[] {
|
|
29
|
+
if (!strict) return [];
|
|
30
|
+
const columns = db.prepare('PRAGMA table_info(symbols)').all() as Array<{ name?: string }>;
|
|
31
|
+
const legacy = columns.filter((row) => [
|
|
32
|
+
'external_target_kind', 'external_target_id', 'external_target_label',
|
|
33
|
+
'external_target_dynamic',
|
|
34
|
+
].includes(String(row.name))).map((row) => row.name);
|
|
35
|
+
const missing = db.prepare(`SELECT c.id id,c.source_file sourceFile,
|
|
36
|
+
c.source_line sourceLine FROM outbound_calls c
|
|
37
|
+
JOIN repositories r ON r.id=c.repo_id
|
|
38
|
+
WHERE c.call_type='external_http'
|
|
39
|
+
AND (? IS NULL OR r.workspace_id=?)
|
|
40
|
+
AND (c.external_target_id IS NULL OR c.external_target_label IS NULL
|
|
41
|
+
OR c.external_target_kind IS NULL) LIMIT 20`).all(
|
|
42
|
+
workspaceId, workspaceId,
|
|
43
|
+
) as Diagnostic[];
|
|
44
|
+
const out: Diagnostic[] = [];
|
|
45
|
+
if (legacy.length > 0) out.push({ severity: 'warning', code: 'schema_legacy_columns_present', message: 'Legacy external-target columns are present on symbols; run service-flow clean --db-only, then init/index/link to rebuild with the current schema.', scope: 'workspace', affectedColumns: legacy, remediation: 'service-flow clean --db-only && service-flow init <workspace> && service-flow index && service-flow link' });
|
|
46
|
+
if (missing.length > 0) out.push({ severity: 'warning', code: 'external_target_columns_missing_data', message: 'External HTTP calls are missing queryable external target metadata; reindex is required after upgrade.', scope: 'workspace', affectedRows: missing, remediation: 'service-flow index --force && service-flow link' });
|
|
47
|
+
if (legacy.length > 0 || missing.length > 0) out.push({ severity: 'warning', code: 'reindex_required_after_upgrade', message: 'This database cannot be made equivalent to a fresh index by relink alone.', scope: 'workspace', remediation: 'Rebuild or force reindex the workspace, then run service-flow doctor --strict.' });
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function analyzerVersionDiagnostics(
|
|
52
|
+
db: Db,
|
|
53
|
+
strict: boolean,
|
|
54
|
+
workspaceId?: number,
|
|
55
|
+
): Diagnostic[] {
|
|
56
|
+
if (!strict) return [];
|
|
57
|
+
const rows = db.prepare(`SELECT name,
|
|
58
|
+
COALESCE(fact_analyzer_version,'legacy') factAnalyzerVersion
|
|
59
|
+
FROM repositories WHERE index_status='indexed'
|
|
60
|
+
AND (? IS NULL OR workspace_id=?)
|
|
61
|
+
AND COALESCE(fact_analyzer_version,'legacy')<>?`).all(
|
|
62
|
+
workspaceId, workspaceId, ANALYZER_VERSION,
|
|
63
|
+
) as Diagnostic[];
|
|
64
|
+
if (rows.length === 0) return [];
|
|
65
|
+
return [{ severity: 'warning', code: 'reindex_required_after_analyzer_upgrade', message: 'Repository facts were produced by an older or unknown analyzer; run service-flow index --force before relink to apply current parser semantics.', scope: 'workspace', affectedRepositoryCount: rows.length, currentAnalyzerVersion: ANALYZER_VERSION, repositories: rows, remediation: 'service-flow index --force && service-flow link' }];
|
|
66
|
+
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -2,27 +2,33 @@ import type { Db } from '../db/connection.js';
|
|
|
2
2
|
import { classifyODataPathIntent, normalizeODataOperationInvocationPath } from '../linker/odata-path-normalizer.js';
|
|
3
3
|
import { implementationHintSuggestions } from '../trace/implementation-hints.js';
|
|
4
4
|
import { boundDoctorDiagnostics } from './001-doctor-projection.js';
|
|
5
|
-
import {
|
|
5
|
+
import { factLifecycleDiagnostic } from '../db/001-fact-lifecycle.js';
|
|
6
|
+
import {
|
|
7
|
+
analyzerVersionDiagnostics,
|
|
8
|
+
schemaDriftDiagnostics,
|
|
9
|
+
} from './002-doctor-lifecycle.js';
|
|
10
|
+
export { linkUpgradeWarnings } from './002-doctor-lifecycle.js';
|
|
6
11
|
|
|
7
12
|
type Diagnostic = Record<string, unknown>;
|
|
8
13
|
interface DoctorOptions {
|
|
9
14
|
detail?: boolean;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export function linkUpgradeWarnings(db: Db): Diagnostic[] {
|
|
13
|
-
return [...schemaDriftDiagnostics(db, true), ...analyzerVersionDiagnostics(db, true)]
|
|
14
|
-
.filter((item) => ['schema_legacy_columns_present', 'external_target_columns_missing_data', 'reindex_required_after_upgrade', 'reindex_required_after_analyzer_upgrade'].includes(String(item.code)));
|
|
15
|
+
workspaceId?: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function doctorDiagnostics(db: Db, strict: boolean, options: DoctorOptions = {}): Diagnostic[] {
|
|
19
|
+
const lifecycle = factLifecycleDiagnostic(db, options.workspaceId);
|
|
20
|
+
if (lifecycle?.code === 'schema_upgrade_required'
|
|
21
|
+
|| lifecycle?.code === 'unsupported_future_schema')
|
|
22
|
+
return boundDoctorDiagnostics([lifecycle]);
|
|
18
23
|
const diagnostics = db.prepare('SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id').all() as Diagnostic[];
|
|
19
24
|
return boundDoctorDiagnostics([
|
|
25
|
+
...(lifecycle ? [lifecycle] : []),
|
|
20
26
|
...diagnostics,
|
|
21
27
|
...healthDiagnostics(db, strict),
|
|
22
28
|
...remoteTargetWithoutImplementationDiagnostics(db, strict, Boolean(options.detail)),
|
|
23
29
|
...localServiceDiagnostics(db, strict),
|
|
24
|
-
...schemaDriftDiagnostics(db, strict),
|
|
25
|
-
...analyzerVersionDiagnostics(db, strict),
|
|
30
|
+
...schemaDriftDiagnostics(db, strict, options.workspaceId),
|
|
31
|
+
...analyzerVersionDiagnostics(db, strict, options.workspaceId),
|
|
26
32
|
...parserQualityDiagnostics(db, strict, options),
|
|
27
33
|
]);
|
|
28
34
|
}
|
|
@@ -105,25 +111,6 @@ function remoteTargetWithoutImplementationExamples(db: Db, servicePath: string,
|
|
|
105
111
|
ORDER BY r.name,c.source_file,c.source_line`).all(servicePath, operationPath) as Diagnostic[];
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
function schemaDriftDiagnostics(db: Db, strict: boolean): Diagnostic[] {
|
|
109
|
-
if (!strict) return [];
|
|
110
|
-
const columns = db.prepare('PRAGMA table_info(symbols)').all() as Array<{ name?: string }>;
|
|
111
|
-
const legacy = columns.filter((row) => ['external_target_kind', 'external_target_id', 'external_target_label', 'external_target_dynamic'].includes(String(row.name))).map((row) => row.name);
|
|
112
|
-
const missing = db.prepare("SELECT id id,source_file sourceFile,source_line sourceLine FROM outbound_calls WHERE call_type='external_http' AND (external_target_id IS NULL OR external_target_label IS NULL OR external_target_kind IS NULL) LIMIT 20").all() as Diagnostic[];
|
|
113
|
-
const out: Diagnostic[] = [];
|
|
114
|
-
if (legacy.length > 0) out.push({ severity: 'warning', code: 'schema_legacy_columns_present', message: 'Legacy external-target columns are present on symbols; run service-flow clean --db-only, then init/index/link to rebuild with the current schema.', scope: 'workspace', affectedColumns: legacy, remediation: 'service-flow clean --db-only && service-flow init <workspace> && service-flow index && service-flow link' });
|
|
115
|
-
if (missing.length > 0) out.push({ severity: 'warning', code: 'external_target_columns_missing_data', message: 'External HTTP calls are missing queryable external target metadata; reindex is required after upgrade.', scope: 'workspace', affectedRows: missing, remediation: 'service-flow index --force && service-flow link' });
|
|
116
|
-
if (legacy.length > 0 || missing.length > 0) out.push({ severity: 'warning', code: 'reindex_required_after_upgrade', message: 'This database cannot be made equivalent to a fresh index by relink alone.', scope: 'workspace', remediation: 'Rebuild or force reindex the workspace, then run service-flow doctor --strict.' });
|
|
117
|
-
return out;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function analyzerVersionDiagnostics(db: Db, strict: boolean): Diagnostic[] {
|
|
121
|
-
if (!strict) return [];
|
|
122
|
-
const rows = db.prepare("SELECT name,COALESCE(fact_analyzer_version,'legacy') factAnalyzerVersion FROM repositories WHERE index_status='indexed' AND COALESCE(fact_analyzer_version,'legacy')<>?").all(ANALYZER_VERSION) as Diagnostic[];
|
|
123
|
-
if (rows.length === 0) return [];
|
|
124
|
-
return [{ severity: 'warning', code: 'reindex_required_after_analyzer_upgrade', message: 'Repository facts were produced by an older or unknown analyzer; run service-flow index --force before relink to apply current parser semantics.', scope: 'workspace', affectedRepositoryCount: rows.length, currentAnalyzerVersion: ANALYZER_VERSION, repositories: rows, remediation: 'service-flow index --force && service-flow link' }];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
114
|
function localServiceDiagnostics(db: Db, strict: boolean): Diagnostic[] {
|
|
128
115
|
const rows = db.prepare("SELECT e.status status,e.unresolved_reason reason,e.evidence_json evidenceJson FROM graph_edges e JOIN outbound_calls c ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER) WHERE c.call_type='local_service_call'").all() as Array<{ status?: string; reason?: string | null; evidenceJson?: string }>;
|
|
129
116
|
const implementationContext = rows.filter((row) => row.status === 'resolved' && String(row.evidenceJson ?? '').includes('implementation_context_caller_ownership')).length;
|