@saptools/service-flow 0.1.69 → 0.1.72

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 (144) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +9 -5
  3. package/TECHNICAL-NOTE.md +17 -0
  4. package/dist/{chunk-3N3B5KHV.js → chunk-Z6D433R5.js} +8766 -6546
  5. package/dist/chunk-Z6D433R5.js.map +1 -0
  6. package/dist/cli.js +703 -160
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +53 -14
  9. package/dist/index.js +2 -23
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/cli/doctor-event-quality.ts +371 -0
  13. package/src/cli/{002-doctor-lifecycle.ts → doctor-lifecycle.ts} +1 -1
  14. package/src/cli/{001-doctor-projection.ts → doctor-projection.ts} +1 -1
  15. package/src/cli/doctor.ts +9 -11
  16. package/src/cli.ts +6 -6
  17. package/src/db/{009-binding-fact-semantics.ts → binding-fact-semantics.ts} +2 -2
  18. package/src/db/{000-call-fact-repository.ts → call-fact-repository.ts} +12 -9
  19. package/src/db/{003-current-fact-semantics.ts → current-fact-semantics.ts} +8 -8
  20. package/src/db/event-fact-semantics.ts +347 -0
  21. package/src/db/event-surface-invalidation.ts +38 -0
  22. package/src/db/{002-fact-json-inventory.ts → fact-json-inventory.ts} +16 -0
  23. package/src/db/{001-fact-lifecycle.ts → fact-lifecycle.ts} +3 -3
  24. package/src/db/migrations.ts +12 -1
  25. package/src/db/{007-package-fact-semantics.ts → package-fact-semantics.ts} +6 -6
  26. package/src/db/{010-package-symbol-surface-semantics.ts → package-symbol-surface-semantics.ts} +2 -2
  27. package/src/db/{004-package-target-invalidation.ts → package-target-invalidation.ts} +80 -1
  28. package/src/db/{008-relative-fact-semantics.ts → relative-fact-semantics.ts} +3 -3
  29. package/src/db/{006-relative-symbol-resolution.ts → relative-symbol-resolution.ts} +1 -1
  30. package/src/db/repositories.ts +31 -5
  31. package/src/db/{005-schema-structure.ts → schema-structure.ts} +41 -1
  32. package/src/db/schema.ts +6 -2
  33. package/src/db/{011-symbol-call-semantics.ts → symbol-call-semantics.ts} +1 -1
  34. package/src/index.ts +3 -3
  35. package/src/indexer/repository-indexer.ts +50 -11
  36. package/src/indexer/workspace-indexer.ts +3 -3
  37. package/src/linker/{007-call-edge-insertion.ts → call-edge-insertion.ts} +2 -2
  38. package/src/linker/{002-call-evidence.ts → call-evidence.ts} +2 -2
  39. package/src/linker/cross-repo-linker.ts +30 -8
  40. package/src/linker/dynamic-edge-resolver.ts +1 -1
  41. package/src/linker/event-environment-link.ts +211 -0
  42. package/src/linker/event-shape-candidate-linker.ts +161 -0
  43. package/src/linker/{004-event-subscription-handler-linker.ts → event-subscription-handler-linker.ts} +124 -20
  44. package/src/linker/{006-event-template-link.ts → event-template-link.ts} +40 -6
  45. package/src/linker/external-http-target.ts +1 -1
  46. package/src/linker/helper-package-linker.ts +1 -1
  47. package/src/linker/{000-implementation-candidates.ts → implementation-candidates.ts} +1 -1
  48. package/src/linker/{001-implementation-evidence-projection.ts → implementation-evidence-projection.ts} +1 -1
  49. package/src/linker/odata-path-normalizer.ts +1 -1
  50. package/src/linker/{005-odata-path-structure.ts → odata-path-structure.ts} +1 -1
  51. package/src/linker/package-event-constant-resolver.ts +298 -0
  52. package/src/linker/{003-package-import-symbol-resolver.ts → package-import-symbol-resolver.ts} +3 -3
  53. package/src/linker/service-resolver.ts +2 -2
  54. package/src/output/{001-compact-json-output.ts → compact-json-output.ts} +1 -1
  55. package/src/output/table-output.ts +13 -1
  56. package/src/parsers/{006-binding-identity.ts → binding-identity.ts} +3 -3
  57. package/src/parsers/{011-binding-lexical-scope.ts → binding-lexical-scope.ts} +1 -1
  58. package/src/parsers/{021-binding-visibility.ts → binding-visibility.ts} +1 -1
  59. package/src/parsers/decorator-parser.ts +9 -53
  60. package/src/parsers/environment-declarations.ts +327 -0
  61. package/src/parsers/event-call-analysis.ts +242 -0
  62. package/src/parsers/event-environment-reference.ts +231 -0
  63. package/src/parsers/event-loop-registration.ts +132 -0
  64. package/src/parsers/event-name-import-resolution.ts +243 -0
  65. package/src/parsers/event-receiver-analysis.ts +404 -0
  66. package/src/parsers/{005-event-subscription-facts.ts → event-subscription-facts.ts} +6 -2
  67. package/src/parsers/generated-constants-parser.ts +80 -14
  68. package/src/parsers/{016-local-symbol-reference.ts → local-symbol-reference.ts} +2 -2
  69. package/src/parsers/operation-path-analysis.ts +1 -1
  70. package/src/parsers/{023-outbound-call-classifier.ts → outbound-call-classifier.ts} +30 -127
  71. package/src/parsers/outbound-call-parser.ts +15 -3
  72. package/src/parsers/{022-outbound-expression-analysis.ts → outbound-expression-analysis.ts} +4 -4
  73. package/src/parsers/{018-package-commonjs-syntax.ts → package-commonjs-syntax.ts} +1 -1
  74. package/src/parsers/{012-package-fact-contract.ts → package-fact-contract.ts} +3 -3
  75. package/src/parsers/{010-package-public-surface-analysis.ts → package-public-surface-analysis.ts} +2 -2
  76. package/src/parsers/{003-package-public-surface.ts → package-public-surface.ts} +3 -3
  77. package/src/parsers/{008-package-surface-publication.ts → package-surface-publication.ts} +1 -1
  78. package/src/parsers/{001-query-entity-resolution.ts → query-entity-resolution.ts} +1 -1
  79. package/src/parsers/{015-service-binding-collector.ts → service-binding-collector.ts} +3 -3
  80. package/src/parsers/{014-service-binding-helper-flow.ts → service-binding-helper-flow.ts} +2 -2
  81. package/src/parsers/service-binding-parser-helpers.ts +1 -1
  82. package/src/parsers/service-binding-parser.ts +2 -2
  83. package/src/parsers/{007-source-fact-reconciliation.ts → source-fact-reconciliation.ts} +3 -3
  84. package/src/parsers/{020-stable-local-value.ts → stable-local-value.ts} +32 -11
  85. package/src/parsers/string-constant-lookups.ts +358 -0
  86. package/src/parsers/{009-symbol-call-facts.ts → symbol-call-facts.ts} +3 -3
  87. package/src/parsers/{017-symbol-derived-contexts.ts → symbol-derived-contexts.ts} +2 -2
  88. package/src/parsers/symbol-parser.ts +6 -6
  89. package/src/trace/{021-compact-decision-normalization.ts → compact-decision-normalization.ts} +2 -2
  90. package/src/trace/{020-compact-field-projection.ts → compact-field-projection.ts} +3 -3
  91. package/src/trace/{024-compact-observation-decision.ts → compact-observation-decision.ts} +3 -3
  92. package/src/trace/{016-compact-projector.ts → compact-projector.ts} +4 -4
  93. package/src/trace/{018-compact-trace.ts → compact-trace.ts} +3 -3
  94. package/src/trace/{006-contextual-projection.ts → contextual-projection.ts} +1 -1
  95. package/src/trace/{008-contextual-runtime-state.ts → contextual-runtime-state.ts} +1 -1
  96. package/src/trace/{001-dynamic-identity.ts → dynamic-identity.ts} +2 -2
  97. package/src/trace/{003-dynamic-references.ts → dynamic-references.ts} +2 -2
  98. package/src/trace/dynamic-targets.ts +6 -6
  99. package/src/trace/{030-event-runtime-resolution.ts → event-runtime-resolution.ts} +26 -5
  100. package/src/trace/event-shape-candidate-trace.ts +172 -0
  101. package/src/trace/{011-event-subscriber-traversal.ts → event-subscriber-traversal.ts} +20 -8
  102. package/src/trace/evidence.ts +9 -2
  103. package/src/trace/implementation-hints.ts +2 -2
  104. package/src/trace/{005-implementation-selection.ts → implementation-selection.ts} +2 -2
  105. package/src/trace/{031-local-call-expansion.ts → local-call-expansion.ts} +3 -3
  106. package/src/trace/{009-selected-handler-provenance.ts → selected-handler-provenance.ts} +1 -1
  107. package/src/trace/selectors.ts +1 -1
  108. package/src/trace/{017-trace-context.ts → trace-context.ts} +2 -2
  109. package/src/trace/{015-trace-edge-recorder.ts → trace-edge-recorder.ts} +2 -2
  110. package/src/trace/{019-trace-edge-semantics.ts → trace-edge-semantics.ts} +3 -3
  111. package/src/trace/trace-engine.ts +10 -10
  112. package/src/trace/{022-trace-fact-preflight.ts → trace-fact-preflight.ts} +2 -2
  113. package/src/trace/{025-trace-implementation-scope.ts → trace-implementation-scope.ts} +1 -1
  114. package/src/trace/{028-trace-operation-execution.ts → trace-operation-execution.ts} +10 -10
  115. package/src/trace/{013-trace-root-scopes.ts → trace-root-scopes.ts} +3 -3
  116. package/src/trace/{027-trace-scope-execution.ts → trace-scope-execution.ts} +35 -43
  117. package/src/trace/{029-trace-start-implementation.ts → trace-start-implementation.ts} +7 -7
  118. package/src/trace/{026-trace-start-scope.ts → trace-start-scope.ts} +5 -5
  119. package/src/trace/{010-traversal-scope.ts → traversal-scope.ts} +1 -1
  120. package/src/types.ts +17 -1
  121. package/src/utils/event-skeleton.ts +207 -0
  122. package/src/version.ts +1 -1
  123. package/dist/chunk-3N3B5KHV.js.map +0 -1
  124. /package/src/cli/{000-clean.ts → clean.ts} +0 -0
  125. /package/src/cli/{003-doctor-package-resolution.ts → doctor-package-resolution.ts} +0 -0
  126. /package/src/cli/{001-index-summary.ts → index-summary.ts} +0 -0
  127. /package/src/db/{014-binding-helper-provenance.ts → binding-helper-provenance.ts} +0 -0
  128. /package/src/db/{012-binding-reference-proof.ts → binding-reference-proof.ts} +0 -0
  129. /package/src/db/{013-index-publication-failure.ts → index-publication-failure.ts} +0 -0
  130. /package/src/output/{000-stdout-policy.ts → stdout-policy.ts} +0 -0
  131. /package/src/parsers/{019-binding-assignment-targets.ts → binding-assignment-targets.ts} +0 -0
  132. /package/src/parsers/{000-direct-query-execution.ts → direct-query-execution.ts} +0 -0
  133. /package/src/parsers/{013-executable-body-eligibility.ts → executable-body-eligibility.ts} +0 -0
  134. /package/src/parsers/{004-fact-identity.ts → fact-identity.ts} +0 -0
  135. /package/src/parsers/{002-symbol-import-bindings.ts → symbol-import-bindings.ts} +0 -0
  136. /package/src/trace/{014-compact-contract.ts → compact-contract.ts} +0 -0
  137. /package/src/trace/{004-dynamic-candidate-sources.ts → dynamic-candidate-sources.ts} +0 -0
  138. /package/src/trace/{000-dynamic-target-types.ts → dynamic-target-types.ts} +0 -0
  139. /package/src/trace/{007-implementation-start-diagnostic.ts → implementation-start-diagnostic.ts} +0 -0
  140. /package/src/trace/{023-nested-event-scopes.ts → nested-event-scopes.ts} +0 -0
  141. /package/src/trace/{002-trace-diagnostics.ts → trace-diagnostics.ts} +0 -0
  142. /package/src/trace/{012-trace-graph-lookups.ts → trace-graph-lookups.ts} +0 -0
  143. /package/src/utils/{000-bounded-projection.ts → bounded-projection.ts} +0 -0
  144. /package/src/utils/{001-placeholders.ts → placeholders.ts} +0 -0
@@ -3,25 +3,25 @@ import {
3
3
  selectCallOwner,
4
4
  type OwnerCandidate,
5
5
  type OwnerSelection,
6
- } from '../parsers/004-fact-identity.js';
6
+ } from '../parsers/fact-identity.js';
7
7
  import type { Db, Statement } from './connection.js';
8
8
  import {
9
9
  resolveRelativeSymbolCall,
10
- } from './006-relative-symbol-resolution.js';
10
+ } from './relative-symbol-resolution.js';
11
11
  import {
12
12
  parsePackageImportReference,
13
- } from '../parsers/012-package-fact-contract.js';
13
+ } from '../parsers/package-fact-contract.js';
14
14
  import {
15
15
  resolvedBindingReferenceProofValid,
16
16
  type BindingProofCall,
17
17
  type BindingProofTarget,
18
- } from './012-binding-reference-proof.js';
18
+ } from './binding-reference-proof.js';
19
19
  import {
20
20
  preparedCallSnapshotError,
21
- } from './013-index-publication-failure.js';
21
+ } from './index-publication-failure.js';
22
22
  import {
23
23
  hasSingleHopHelperReturn,
24
- } from './014-binding-helper-provenance.js';
24
+ } from './binding-helper-provenance.js';
25
25
 
26
26
  export function insertSymbolCalls(db: Db, repoId: number, rows: SymbolCallFact[]): void {
27
27
  const insertStmt = db.prepare('INSERT INTO symbol_calls(repo_id,caller_symbol_id,callee_symbol_id,callee_expression,import_source,source_file,source_line,call_site_start_offset,call_site_end_offset,call_role,status,confidence,evidence_json,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)');
@@ -400,11 +400,12 @@ export function insertCalls(
400
400
  function outboundCallInsertStatement(db: Db): Statement {
401
401
  return db.prepare(`INSERT INTO outbound_calls(
402
402
  repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,
403
- event_name_expr,payload_summary,source_file,source_line,call_site_start_offset,
403
+ event_name_expr,event_skeleton_signature,event_skeleton_json,
404
+ payload_summary,source_file,source_line,call_site_start_offset,
404
405
  call_site_end_offset,confidence,unresolved_reason,local_service_name,
405
406
  local_service_lookup,alias_chain_json,evidence_json,external_target_kind,
406
407
  external_target_id,external_target_label,external_target_dynamic,service_binding_id
407
- ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
408
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
408
409
  }
409
410
 
410
411
  function insertOutboundCall(
@@ -424,7 +425,9 @@ function insertOutboundCall(
424
425
  stmt.run(
425
426
  repoId, sourceSymbolId,
426
427
  call.callType, call.method, call.operationPathExpr, call.queryEntity,
427
- call.eventNameExpr, call.payloadSummary, call.sourceFile, call.sourceLine,
428
+ call.eventNameExpr, call.eventSkeleton?.signature ?? null,
429
+ call.eventSkeleton ? JSON.stringify(call.eventSkeleton) : null,
430
+ call.payloadSummary, call.sourceFile, call.sourceLine,
428
431
  call.callSiteStartOffset, call.callSiteEndOffset, call.confidence,
429
432
  call.unresolvedReason,
430
433
  call.localServiceName, call.localServiceLookup,
@@ -3,16 +3,15 @@ import type { CallType, RepoKind } from '../types.js';
3
3
  import { ANALYZER_VERSION } from '../version.js';
4
4
  import {
5
5
  invalidPackageFactCategories,
6
- } from './007-package-fact-semantics.js';
6
+ } from './package-fact-semantics.js';
7
7
  import {
8
8
  invalidRelativeFactCategories,
9
- } from './008-relative-fact-semantics.js';
9
+ } from './relative-fact-semantics.js';
10
10
  import {
11
11
  invalidBindingFactCategories,
12
- } from './009-binding-fact-semantics.js';
13
- import {
14
- invalidSymbolFactCategories,
15
- } from './011-symbol-call-semantics.js';
12
+ } from './binding-fact-semantics.js';
13
+ import { invalidSymbolFactCategories } from './symbol-call-semantics.js';
14
+ import { invalidEventFactCategories } from './event-fact-semantics.js';
16
15
 
17
16
  export type PackageFactPhase = 'pre_package' | 'terminal';
18
17
 
@@ -262,7 +261,7 @@ function eventNameCategories(
262
261
  db: Db,
263
262
  workspaceId?: number,
264
263
  ): FactSemanticCategoryCount[] {
265
- const invalid = count(db, `SELECT COUNT(*) count
264
+ const invalidName = count(db, `SELECT COUNT(*) count
266
265
  FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
267
266
  WHERE ${currentRepositoryPredicate()}
268
267
  AND fact.call_type IN ('async_emit','async_subscribe')
@@ -278,7 +277,7 @@ function eventNameCategories(
278
277
  fact.call_site_end_offset HAVING COUNT(*)<>1
279
278
  )`, workspaceId);
280
279
  return [
281
- ...category('event_name_invalid', invalid),
280
+ ...category('event_name_invalid', invalidName),
282
281
  ...category('async_subscription_site_duplicate', duplicate),
283
282
  ];
284
283
  }
@@ -686,6 +685,7 @@ export function invalidFactSemanticCategories(
686
685
  ...callSpanCategories(db, workspaceId),
687
686
  ...duplicateSymbolCallCategories(db, workspaceId),
688
687
  ...eventNameCategories(db, workspaceId),
688
+ ...invalidEventFactCategories(db, workspaceId, phase),
689
689
  ...ownerCategories(db, workspaceId),
690
690
  ...bindingCategories(db, workspaceId),
691
691
  ...invalidBindingFactCategories(db, workspaceId),
@@ -0,0 +1,347 @@
1
+ import type { Db } from './connection.js';
2
+ import { ANALYZER_VERSION } from '../version.js';
3
+ import {
4
+ EVENT_ENVIRONMENT_KEY_ALLOWLIST,
5
+ parseEnvironmentDeclarationsFact,
6
+ } from '../parsers/environment-declarations.js';
7
+ import type {
8
+ EventEnvironmentReference,
9
+ } from '../parsers/event-environment-reference.js';
10
+ import { parseEventSkeletonFact } from '../utils/event-skeleton.js';
11
+ import { parsePackageImportReference } from
12
+ '../parsers/package-fact-contract.js';
13
+ import {
14
+ expectedPackageEventConstantResolution,
15
+ type PackageEventConstantResolution,
16
+ } from '../linker/package-event-constant-resolver.js';
17
+
18
+ export interface EventFactSemanticCategoryCount {
19
+ category: string;
20
+ count: number;
21
+ }
22
+
23
+ interface EventFactRow extends Record<string, unknown> {
24
+ workspaceId?: number;
25
+ eventName?: string;
26
+ skeletonSignature?: string | null;
27
+ skeletonJson?: string | null;
28
+ unresolvedReason?: string | null;
29
+ evidenceJson?: string;
30
+ }
31
+
32
+ const receiverReasons = new Set([
33
+ 'event_receiver_unproven_binding',
34
+ 'event_receiver_unproven_propagation',
35
+ 'event_receiver_not_cap_client',
36
+ ]);
37
+ const constantReasons = new Set([
38
+ 'event_name_constant_container_ambiguous',
39
+ 'event_name_constant_member_not_string',
40
+ 'event_name_constant_container_mutable',
41
+ 'event_name_constant_container_not_exported',
42
+ 'event_name_constant_resolution_pending',
43
+ ]);
44
+ const environmentKeys = new Set<string>(EVENT_ENVIRONMENT_KEY_ALLOWLIST);
45
+
46
+ function category(
47
+ name: string,
48
+ count: number,
49
+ ): EventFactSemanticCategoryCount[] {
50
+ return count > 0 ? [{ category: name, count }] : [];
51
+ }
52
+
53
+ function record(value: unknown): Record<string, unknown> | undefined {
54
+ return value && typeof value === 'object' && !Array.isArray(value)
55
+ ? value as Record<string, unknown> : undefined;
56
+ }
57
+
58
+ function jsonRecord(value: unknown): Record<string, unknown> | undefined {
59
+ if (typeof value !== 'string') return undefined;
60
+ try {
61
+ return record(JSON.parse(value) as unknown);
62
+ } catch {
63
+ return undefined;
64
+ }
65
+ }
66
+
67
+ function currentEventRows(
68
+ db: Db,
69
+ workspaceId?: number,
70
+ ): EventFactRow[] {
71
+ return db.prepare(`SELECT fact.event_name_expr eventName,
72
+ r.workspace_id workspaceId,
73
+ fact.event_skeleton_signature skeletonSignature,
74
+ fact.event_skeleton_json skeletonJson,
75
+ fact.unresolved_reason unresolvedReason,
76
+ fact.evidence_json evidenceJson
77
+ FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
78
+ WHERE r.fact_analyzer_version=?
79
+ AND (? IS NULL OR r.workspace_id=?)
80
+ AND fact.call_type IN ('async_emit','async_subscribe')`).all(
81
+ ANALYZER_VERSION, workspaceId, workspaceId,
82
+ ) as EventFactRow[];
83
+ }
84
+
85
+ function integerField(
86
+ value: Record<string, unknown>,
87
+ key: string,
88
+ ): number | undefined {
89
+ const item = value[key];
90
+ return Number.isInteger(item) && Number(item) >= 0
91
+ ? Number(item) : undefined;
92
+ }
93
+
94
+ function packagePendingValid(
95
+ row: EventFactRow,
96
+ evidence: Record<string, unknown>,
97
+ ): boolean {
98
+ const source = evidence.eventNameConstantSourceExpression;
99
+ return evidence.eventNameUnresolvedReason
100
+ === 'event_name_constant_resolution_pending'
101
+ && evidence.eventNamePackageConstantResolution === undefined
102
+ && typeof source === 'string' && source.length > 0
103
+ && row.eventName === source;
104
+ }
105
+
106
+ function resolutionFieldsValid(
107
+ value: Record<string, unknown>,
108
+ expected: PackageEventConstantResolution,
109
+ ): boolean {
110
+ return value.status === expected.status
111
+ && value.reason === expected.reason
112
+ && integerField(value, 'candidateCount') === expected.candidateCount
113
+ && integerField(value, 'eligibleCandidateCount')
114
+ === expected.eligibleCandidateCount
115
+ && integerField(value, 'selectedCandidateCount')
116
+ === (expected.status === 'resolved' ? 1 : 0)
117
+ && value.candidateSetComplete === expected.complete
118
+ && value.resolvedModulePath === expected.modulePath
119
+ && value.targetRepositoryId === expected.targetRepoId;
120
+ }
121
+
122
+ function packageTerminalValid(
123
+ db: Db,
124
+ row: EventFactRow,
125
+ evidence: Record<string, unknown>,
126
+ ): boolean {
127
+ const binding = parsePackageImportReference(
128
+ evidence.eventNameConstantImportBinding,
129
+ );
130
+ const workspaceId = row.workspaceId;
131
+ const resolution = record(evidence.eventNamePackageConstantResolution);
132
+ if (!binding || typeof workspaceId !== 'number' || !resolution)
133
+ return false;
134
+ const expected = expectedPackageEventConstantResolution(
135
+ db, workspaceId, binding,
136
+ );
137
+ const source = evidence.eventNameConstantSourceExpression;
138
+ const expectedName = expected.value ?? source;
139
+ const expectedReason = expected.status === 'resolved'
140
+ ? undefined : expected.reason;
141
+ if (row.eventName !== expectedName
142
+ || evidence.eventNameUnresolvedReason !== expectedReason
143
+ || !resolutionFieldsValid(resolution, expected)) return false;
144
+ if (expected.status !== 'resolved') return true;
145
+ const constant = record(evidence.eventNameConstant);
146
+ return constant?.sourceKind === 'package_static_string'
147
+ && constant.sourceFile === expected.sourceFile
148
+ && constant.sourceLine === expected.sourceLine;
149
+ }
150
+
151
+ function packageConstantValid(
152
+ db: Db,
153
+ row: EventFactRow,
154
+ evidence: Record<string, unknown>,
155
+ phase: 'pre_package' | 'terminal',
156
+ ): boolean {
157
+ if (evidence.eventNameConstantImportBinding === undefined) return true;
158
+ const binding = parsePackageImportReference(
159
+ evidence.eventNameConstantImportBinding,
160
+ );
161
+ const source = evidence.eventNameConstantSourceExpression;
162
+ if (!binding || typeof source !== 'string' || source.length === 0)
163
+ return false;
164
+ if (packagePendingValid(row, evidence))
165
+ return phase === 'pre_package';
166
+ return packageTerminalValid(db, row, evidence);
167
+ }
168
+
169
+ function receiverEvidenceValid(
170
+ row: EventFactRow,
171
+ evidence: Record<string, unknown>,
172
+ ): boolean {
173
+ const classification = evidence.receiverClassification;
174
+ const proof = evidence.receiverProof;
175
+ if (!['cap_evidence', 'name_fallback', 'unproven'].includes(
176
+ String(classification),
177
+ ) || typeof proof !== 'string' || proof.length === 0
178
+ || !Array.isArray(evidence.consideredBindingSites)
179
+ || evidence.consideredBindingSites.length > 8) return false;
180
+ if (classification === 'name_fallback')
181
+ return proof === 'compatibility_name_fallback';
182
+ if (classification !== 'unproven') return true;
183
+ return typeof row.unresolvedReason === 'string'
184
+ && receiverReasons.has(row.unresolvedReason);
185
+ }
186
+
187
+ function environmentBindingValid(
188
+ binding: EventEnvironmentReference,
189
+ sourceKeys: readonly string[],
190
+ ): boolean {
191
+ if (!['resolved', 'refused'].includes(binding.status)
192
+ || !sourceKeys.includes(binding.sourceKey)
193
+ || !Array.isArray(binding.transforms)
194
+ || !binding.transforms.every((item) =>
195
+ item === 'toUpperCase' || item === 'toLowerCase')) return false;
196
+ if (binding.status === 'refused')
197
+ return typeof binding.reason === 'string' && binding.reason.length > 0;
198
+ return typeof binding.environmentKey === 'string'
199
+ && environmentKeys.has(binding.environmentKey)
200
+ && typeof binding.sourceFile === 'string' && binding.sourceFile.length > 0
201
+ && Number.isInteger(binding.startOffset)
202
+ && Number(binding.startOffset) >= 0
203
+ && Number.isInteger(binding.endOffset)
204
+ && Number(binding.endOffset) > Number(binding.startOffset);
205
+ }
206
+
207
+ function skeletonValid(
208
+ row: EventFactRow,
209
+ evidence: Record<string, unknown>,
210
+ ): boolean {
211
+ const reason = evidence.eventNameUnresolvedReason;
212
+ if (reason !== 'dynamic_event_name_identifier')
213
+ return row.skeletonSignature == null && row.skeletonJson == null;
214
+ const skeleton = parseEventSkeletonFact(row.skeletonJson);
215
+ if (!skeleton || skeleton.status !== 'complete'
216
+ || row.skeletonSignature !== skeleton.signature
217
+ || !skeleton.environmentBindings.every((binding) =>
218
+ environmentBindingValid(binding, skeleton.sourceKeys))) return false;
219
+ const keys = evidence.eventNamePlaceholderKeys;
220
+ return Array.isArray(keys)
221
+ && JSON.stringify(keys) === JSON.stringify(skeleton.sourceKeys);
222
+ }
223
+
224
+ function eventReasonValid(
225
+ row: EventFactRow,
226
+ evidence: Record<string, unknown>,
227
+ ): boolean {
228
+ const eventReason = evidence.eventNameUnresolvedReason;
229
+ if (receiverReasons.has(String(row.unresolvedReason)))
230
+ return eventReason === undefined
231
+ || eventReason === 'dynamic_event_name_identifier'
232
+ || eventReason === 'dynamic_event_name_unsupported_expression'
233
+ || constantReasons.has(String(eventReason));
234
+ if (eventReason === undefined) return row.unresolvedReason == null;
235
+ if (eventReason === 'dynamic_event_name_identifier'
236
+ || eventReason === 'dynamic_event_name_unsupported_expression'
237
+ || constantReasons.has(String(eventReason)))
238
+ return row.unresolvedReason === eventReason;
239
+ return false;
240
+ }
241
+
242
+ function invalidEventRows(
243
+ db: Db,
244
+ workspaceId?: number,
245
+ phase: 'pre_package' | 'terminal' = 'pre_package',
246
+ ): number {
247
+ return currentEventRows(db, workspaceId).filter((row) => {
248
+ if (typeof row.eventName !== 'string' || row.eventName.length === 0)
249
+ return false;
250
+ const evidence = jsonRecord(row.evidenceJson);
251
+ return !evidence
252
+ || !receiverEvidenceValid(row, evidence)
253
+ || !eventReasonValid(row, evidence)
254
+ || !packageConstantValid(db, row, evidence, phase)
255
+ || !skeletonValid(row, evidence);
256
+ }).length;
257
+ }
258
+
259
+ function invalidEnvironmentRepositories(
260
+ db: Db,
261
+ workspaceId?: number,
262
+ ): number {
263
+ const rows = db.prepare(`SELECT environment_declarations_json value
264
+ FROM repositories WHERE fact_analyzer_version=?
265
+ AND (? IS NULL OR workspace_id=?)`).all(
266
+ ANALYZER_VERSION, workspaceId, workspaceId,
267
+ );
268
+ return rows.filter((row) =>
269
+ !parseEnvironmentDeclarationsFact(row.value)).length;
270
+ }
271
+
272
+ function generatedConstantInvalid(row: Record<string, unknown>): boolean {
273
+ const kind = String(row.constantKind);
274
+ const container = row.containerName;
275
+ const member = row.memberName;
276
+ const name = row.name;
277
+ const memberShape = kind === 'const_identifier'
278
+ ? container == null && member == null
279
+ : typeof container === 'string' && container.length > 0
280
+ && typeof member === 'string' && member.length > 0
281
+ && name === `${container}.${member}`;
282
+ const resolved = row.resolutionStatus === 'resolved';
283
+ const refused = row.resolutionStatus === 'refused';
284
+ return !['const_identifier', 'enum_member', 'const_object_property']
285
+ .includes(kind)
286
+ || typeof name !== 'string' || name.length === 0
287
+ || (!resolved && !refused)
288
+ || (resolved && (typeof row.value !== 'string'
289
+ || row.value.length === 0 || row.unresolvedReason != null))
290
+ || (refused && (row.value != null
291
+ || !constantReasons.has(String(row.unresolvedReason))))
292
+ || typeof row.sourceFile !== 'string' || row.sourceFile.length === 0
293
+ || !Number.isInteger(row.sourceLine) || Number(row.sourceLine) < 1
294
+ || ![0, 1].includes(Number(row.exported))
295
+ || ![0, 1].includes(Number(row.stable))
296
+ || !validConstantOffsets(row) || !memberShape;
297
+ }
298
+
299
+ function validConstantOffsets(row: Record<string, unknown>): boolean {
300
+ const start = row.declarationStartOffset;
301
+ const end = row.declarationEndOffset;
302
+ const valueStart = row.valueStartOffset;
303
+ const valueEnd = row.valueEndOffset;
304
+ return Number.isInteger(start) && Number(start) >= 0
305
+ && Number.isInteger(end) && Number(end) > Number(start)
306
+ && Number.isInteger(valueStart) && Number(valueStart) >= Number(start)
307
+ && Number.isInteger(valueEnd) && Number(valueEnd) > Number(valueStart)
308
+ && Number(valueEnd) <= Number(end);
309
+ }
310
+
311
+ function invalidGeneratedConstants(
312
+ db: Db,
313
+ workspaceId?: number,
314
+ ): number {
315
+ const rows = db.prepare(`SELECT fact.source_file sourceFile,
316
+ fact.source_line sourceLine,fact.name,fact.container_name containerName,
317
+ fact.member_name memberName,fact.value,
318
+ fact.constant_kind constantKind,fact.exported,fact.stable,
319
+ fact.resolution_status resolutionStatus,
320
+ fact.unresolved_reason unresolvedReason,
321
+ fact.declaration_start_offset declarationStartOffset,
322
+ fact.declaration_end_offset declarationEndOffset,
323
+ fact.value_start_offset valueStartOffset,
324
+ fact.value_end_offset valueEndOffset
325
+ FROM generated_constants fact
326
+ JOIN repositories r ON r.id=fact.repo_id
327
+ WHERE r.fact_analyzer_version=?
328
+ AND (? IS NULL OR r.workspace_id=?)`).all(
329
+ ANALYZER_VERSION, workspaceId, workspaceId,
330
+ );
331
+ return rows.filter(generatedConstantInvalid).length;
332
+ }
333
+
334
+ export function invalidEventFactCategories(
335
+ db: Db,
336
+ workspaceId?: number,
337
+ phase: 'pre_package' | 'terminal' = 'pre_package',
338
+ ): EventFactSemanticCategoryCount[] {
339
+ return [
340
+ ...category('event_fact_semantics_invalid',
341
+ invalidEventRows(db, workspaceId, phase)),
342
+ ...category('repository_environment_declarations_invalid',
343
+ invalidEnvironmentRepositories(db, workspaceId)),
344
+ ...category('generated_constant_fact_invalid',
345
+ invalidGeneratedConstants(db, workspaceId)),
346
+ ];
347
+ }
@@ -0,0 +1,38 @@
1
+ import type { Db } from './connection.js';
2
+ import type { OutboundCallFact } from '../types.js';
3
+
4
+ function priorEventFactCount(db: Db, repoId: number): number {
5
+ const row = db.prepare(`SELECT COUNT(*) count FROM outbound_calls
6
+ WHERE repo_id=? AND call_type IN ('async_emit','async_subscribe')`).get(
7
+ repoId,
8
+ );
9
+ return Number(row?.count ?? 0);
10
+ }
11
+
12
+ function repositoryEnvironment(
13
+ db: Db,
14
+ repoId: number,
15
+ ): string | null {
16
+ const row = db.prepare(`SELECT environment_declarations_json value
17
+ FROM repositories WHERE id=?`).get(repoId);
18
+ return typeof row?.value === 'string' ? row.value : null;
19
+ }
20
+
21
+ export function invalidateEventSurfaceFacts(
22
+ db: Db,
23
+ repoId: number,
24
+ calls: readonly OutboundCallFact[],
25
+ nextEnvironmentJson: string,
26
+ ): void {
27
+ const hasNewEvents = calls.some((call) =>
28
+ call.callType === 'async_emit' || call.callType === 'async_subscribe');
29
+ const environmentChanged = repositoryEnvironment(db, repoId)
30
+ !== nextEnvironmentJson;
31
+ if (!hasNewEvents && priorEventFactCount(db, repoId) === 0
32
+ && !environmentChanged) return;
33
+ db.prepare(`UPDATE repositories SET
34
+ graph_stale_reason='event_surface_facts_changed',
35
+ graph_stale_at=datetime('now')
36
+ WHERE workspace_id=(SELECT workspace_id FROM repositories WHERE id=?)`)
37
+ .run(repoId);
38
+ }
@@ -36,6 +36,14 @@ export const LINK_FACT_JSON_INVENTORY: readonly FactJsonInventoryItem[] = [
36
36
  consumers: ['package-import-symbol-resolver'],
37
37
  repositoryJoin: '',
38
38
  },
39
+ {
40
+ table: 'repositories',
41
+ column: 'environment_declarations_json',
42
+ nullable: false,
43
+ shape: 'object',
44
+ consumers: ['event-environment-resolver'],
45
+ repositoryJoin: '',
46
+ },
39
47
  {
40
48
  table: 'handler_methods',
41
49
  column: 'decorator_resolution_json',
@@ -77,6 +85,14 @@ export const LINK_FACT_JSON_INVENTORY: readonly FactJsonInventoryItem[] = [
77
85
  consumers: ['cross-repository-linker', 'trace-engine'],
78
86
  repositoryJoin: directRepositoryJoin,
79
87
  },
88
+ {
89
+ table: 'outbound_calls',
90
+ column: 'event_skeleton_json',
91
+ nullable: true,
92
+ shape: 'object',
93
+ consumers: ['event-shape-linker', 'event-runtime-resolver'],
94
+ repositoryJoin: directRepositoryJoin,
95
+ },
80
96
  {
81
97
  table: 'service_bindings',
82
98
  column: 'placeholders_json',
@@ -3,16 +3,16 @@ import { CURRENT_SCHEMA_VERSION, schemaVersion } from './migrations.js';
3
3
  import {
4
4
  invalidFactJsonCategories,
5
5
  type FactJsonCategoryCount,
6
- } from './002-fact-json-inventory.js';
6
+ } from './fact-json-inventory.js';
7
7
  import {
8
8
  invalidFactSemanticCategories,
9
9
  type FactSemanticCategoryCount,
10
10
  type PackageFactPhase,
11
- } from './003-current-fact-semantics.js';
11
+ } from './current-fact-semantics.js';
12
12
  import {
13
13
  invalidSchemaStructureCategories,
14
14
  type SchemaStructureCategoryCount,
15
- } from './005-schema-structure.js';
15
+ } from './schema-structure.js';
16
16
  import { ANALYZER_VERSION } from '../version.js';
17
17
 
18
18
  export type FactLifecycleCode =
@@ -1,6 +1,6 @@
1
1
  import type { Db } from './connection.js';
2
2
  import { schemaIndexesSql, schemaTablesSql } from './schema.js';
3
- export const CURRENT_SCHEMA_VERSION = 13;
3
+ export const CURRENT_SCHEMA_VERSION = 14;
4
4
  const columns: Record<string, Array<{ name: string; ddl: string }>> = {
5
5
  handler_methods: [
6
6
  { name: 'decorator_resolution_json', ddl: "ALTER TABLE handler_methods ADD COLUMN decorator_resolution_json TEXT NOT NULL DEFAULT '{}'" },
@@ -20,6 +20,7 @@ const columns: Record<string, Array<{ name: string; ddl: string }>> = {
20
20
  { name: 'graph_stale_at', ddl: 'ALTER TABLE repositories ADD COLUMN graph_stale_at TEXT' },
21
21
  { name: 'fact_analyzer_version', ddl: "ALTER TABLE repositories ADD COLUMN fact_analyzer_version TEXT DEFAULT 'legacy'" },
22
22
  { name: 'package_public_surface_json', ddl: 'ALTER TABLE repositories ADD COLUMN package_public_surface_json TEXT' },
23
+ { name: 'environment_declarations_json', ddl: 'ALTER TABLE repositories ADD COLUMN environment_declarations_json TEXT' },
23
24
  ],
24
25
  graph_edges: [
25
26
  { name: 'status', ddl: "ALTER TABLE graph_edges ADD COLUMN status TEXT NOT NULL DEFAULT 'unresolved'" },
@@ -60,6 +61,8 @@ const columns: Record<string, Array<{ name: string; ddl: string }>> = {
60
61
  { name: 'external_target_dynamic', ddl: 'ALTER TABLE outbound_calls ADD COLUMN external_target_dynamic INTEGER NOT NULL DEFAULT 0' },
61
62
  { name: 'call_site_start_offset', ddl: 'ALTER TABLE outbound_calls ADD COLUMN call_site_start_offset INTEGER' },
62
63
  { name: 'call_site_end_offset', ddl: 'ALTER TABLE outbound_calls ADD COLUMN call_site_end_offset INTEGER' },
64
+ { name: 'event_skeleton_signature', ddl: 'ALTER TABLE outbound_calls ADD COLUMN event_skeleton_signature TEXT' },
65
+ { name: 'event_skeleton_json', ddl: 'ALTER TABLE outbound_calls ADD COLUMN event_skeleton_json TEXT' },
63
66
  ],
64
67
  symbol_calls: [
65
68
  { name: 'call_site_start_offset', ddl: 'ALTER TABLE symbol_calls ADD COLUMN call_site_start_offset INTEGER' },
@@ -104,6 +107,13 @@ function markFactProvenanceMigrationStale(db: Db, priorVersion: number): void {
104
107
  graph_stale_at=COALESCE(graph_stale_at,datetime('now'))
105
108
  WHERE index_status='indexed' OR last_indexed_at IS NOT NULL`).run();
106
109
  }
110
+ function markEventSurfaceMigrationStale(db: Db, priorVersion: number): void {
111
+ if (priorVersion >= 14) return;
112
+ db.prepare(`UPDATE repositories
113
+ SET graph_stale_reason='schema_v14_event_surface_requires_reindex',
114
+ graph_stale_at=COALESCE(graph_stale_at,datetime('now'))
115
+ WHERE index_status='indexed' OR last_indexed_at IS NOT NULL`).run();
116
+ }
107
117
  export function migrate(db: Db): void {
108
118
  db.transaction(() => {
109
119
  const version = userVersion(db);
@@ -114,6 +124,7 @@ export function migrate(db: Db): void {
114
124
  normalizeLegacyStatus(db, version);
115
125
  markCallSiteMigrationStale(db, version);
116
126
  markFactProvenanceMigrationStale(db, version);
127
+ markEventSurfaceMigrationStale(db, version);
117
128
  const violations = db.pragma('foreign_key_check');
118
129
  if (violations.length > 0) throw new Error('SQLite foreign_key_check failed during migration');
119
130
  db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
@@ -4,21 +4,21 @@ import type {
4
4
  PackagePublicScope,
5
5
  PackagePublicSurfaceFact,
6
6
  PublicSurfaceTarget,
7
- } from '../parsers/003-package-public-surface.js';
7
+ } from '../parsers/package-public-surface.js';
8
8
  import {
9
9
  parsePackageImportReference,
10
10
  parsePackagePublicSurfaceFact,
11
- } from '../parsers/012-package-fact-contract.js';
11
+ } from '../parsers/package-fact-contract.js';
12
12
  import type { SymbolImportReference } from
13
- '../parsers/002-symbol-import-bindings.js';
14
- import type { PackageFactPhase } from './003-current-fact-semantics.js';
13
+ '../parsers/symbol-import-bindings.js';
14
+ import type { PackageFactPhase } from './current-fact-semantics.js';
15
15
  import {
16
16
  expectedPackageImportResolutions,
17
17
  type PackageCallResolution,
18
- } from '../linker/003-package-import-symbol-resolver.js';
18
+ } from '../linker/package-import-symbol-resolver.js';
19
19
  import {
20
20
  invalidPackageSymbolSurfaceCount,
21
- } from './010-package-symbol-surface-semantics.js';
21
+ } from './package-symbol-surface-semantics.js';
22
22
 
23
23
  export interface PackageFactCategoryCount {
24
24
  category: string;
@@ -6,10 +6,10 @@ import {
6
6
  type PackagePublicScope,
7
7
  type PackagePublicSurfaceFact,
8
8
  type PublicSurfaceTarget,
9
- } from '../parsers/003-package-public-surface.js';
9
+ } from '../parsers/package-public-surface.js';
10
10
  import {
11
11
  parsePackagePublicSurfaceFact,
12
- } from '../parsers/012-package-fact-contract.js';
12
+ } from '../parsers/package-fact-contract.js';
13
13
  import type { Db } from './connection.js';
14
14
 
15
15
  interface Exposure {