@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +67 -11
  3. package/TECHNICAL-NOTE.md +41 -1
  4. package/dist/{chunk-OONNRIDL.js → chunk-ZQABU7MR.js} +3764 -643
  5. package/dist/chunk-ZQABU7MR.js.map +1 -0
  6. package/dist/cli.js +158 -295
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +196 -1
  9. package/dist/index.js +7 -3
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/cli/002-doctor-lifecycle.ts +66 -0
  13. package/src/cli/doctor.ts +14 -27
  14. package/src/cli.ts +130 -102
  15. package/src/db/000-call-fact-repository.ts +537 -0
  16. package/src/db/001-fact-lifecycle.ts +111 -0
  17. package/src/db/migrations.ts +16 -1
  18. package/src/db/repositories.ts +1 -315
  19. package/src/db/schema.ts +4 -2
  20. package/src/index.ts +22 -0
  21. package/src/linker/004-event-subscription-handler-linker.ts +300 -0
  22. package/src/linker/cross-repo-linker.ts +23 -2
  23. package/src/output/001-compact-json-output.ts +6 -0
  24. package/src/parsers/imported-wrapper-parser.ts +4 -0
  25. package/src/parsers/outbound-call-parser.ts +11 -1
  26. package/src/parsers/symbol-parser.ts +7 -4
  27. package/src/trace/010-traversal-scope.ts +188 -0
  28. package/src/trace/011-event-subscriber-traversal.ts +366 -0
  29. package/src/trace/012-trace-graph-lookups.ts +74 -0
  30. package/src/trace/013-trace-root-scopes.ts +279 -0
  31. package/src/trace/014-compact-contract.ts +347 -0
  32. package/src/trace/015-trace-edge-recorder.ts +336 -0
  33. package/src/trace/016-compact-projector.ts +697 -0
  34. package/src/trace/017-trace-context.ts +378 -0
  35. package/src/trace/018-compact-trace.ts +87 -0
  36. package/src/trace/019-trace-edge-semantics.ts +328 -0
  37. package/src/trace/020-compact-field-projection.ts +387 -0
  38. package/src/trace/dynamic-branches.ts +35 -13
  39. package/src/trace/trace-engine.ts +271 -245
  40. package/src/types.ts +10 -0
  41. package/src/version.ts +1 -1
  42. 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
- } from "./chunk-OONNRIDL.js";
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":";;;;;;;;;;;;;;;;;;;;;;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":[]}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saptools/service-flow",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "description": "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -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 { ANALYZER_VERSION } from '../version.js';
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;