@saptools/service-flow 0.1.72 → 0.1.73
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 +8 -0
- package/README.md +19 -6
- package/TECHNICAL-NOTE.md +11 -2
- package/dist/{chunk-Z6D433R5.js → chunk-32WOTGTS.js} +6714 -6161
- package/dist/chunk-32WOTGTS.js.map +1 -0
- package/dist/cli.js +189 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/doctor-event-quality.ts +90 -15
- package/src/cli.ts +35 -13
- package/src/config/workspace-config.ts +15 -0
- package/src/db/current-fact-semantics.ts +2 -26
- package/src/db/event-fact-semantics.ts +260 -61
- package/src/db/event-site-semantics.ts +62 -0
- package/src/db/fact-lifecycle.ts +70 -12
- package/src/db/migrations.ts +4 -1
- package/src/db/package-target-invalidation.ts +4 -3
- package/src/db/schema.ts +1 -1
- package/src/indexer/repository-indexer.ts +50 -6
- package/src/indexer/workspace-indexer.ts +13 -3
- package/src/linker/cross-repo-linker.ts +4 -2
- package/src/linker/event-shape-candidate-linker.ts +63 -20
- package/src/linker/event-subscription-handler-linker.ts +121 -30
- package/src/linker/package-event-constant-resolver.ts +22 -18
- package/src/output/repository-inspection.ts +11 -0
- package/src/parsers/environment-declarations.ts +104 -27
- package/src/parsers/event-call-analysis.ts +163 -35
- package/src/parsers/event-environment-reference.ts +18 -7
- package/src/parsers/event-receiver-analysis.ts +17 -27
- package/src/parsers/outbound-call-classifier.ts +1 -1
- package/src/parsers/stable-local-value.ts +12 -1
- package/src/parsers/string-constant-lookups.ts +78 -28
- package/src/trace/edge-target.ts +65 -0
- package/src/trace/event-subscriber-traversal.ts +71 -1
- package/src/trace/evidence.ts +0 -28
- package/src/trace/trace-scope-execution.ts +1 -1
- package/src/types.ts +3 -1
- package/src/version.ts +1 -1
- package/dist/chunk-Z6D433R5.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -242,7 +242,7 @@ interface GeneratedConstantFact {
|
|
|
242
242
|
exported: boolean;
|
|
243
243
|
stable: boolean;
|
|
244
244
|
resolutionStatus: 'resolved' | 'refused';
|
|
245
|
-
unresolvedReason?: 'event_name_constant_member_not_string' | 'event_name_constant_container_mutable';
|
|
245
|
+
unresolvedReason?: 'event_name_constant_member_not_string' | 'event_name_constant_container_mutable' | 'event_name_constant_container_unsafe_reference' | 'event_name_constant_container_unsupported_shape';
|
|
246
246
|
declarationStartOffset: number;
|
|
247
247
|
declarationEndOffset: number;
|
|
248
248
|
valueStartOffset: number;
|
|
@@ -353,6 +353,7 @@ interface LinkWorkspaceResult {
|
|
|
353
353
|
subscriptionHandlerUnresolvedCount: number;
|
|
354
354
|
subscriptionHandlerMissingAssociationCount: number;
|
|
355
355
|
eventShapeCandidateCount: number;
|
|
356
|
+
eventShapeCandidateOmittedCount: number;
|
|
356
357
|
}
|
|
357
358
|
declare function linkWorkspace(db: Db, workspaceId: number, vars?: Record<string, string>): LinkWorkspaceResult;
|
|
358
359
|
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -8,6 +8,16 @@ const eventNameReason = `COALESCE(
|
|
|
8
8
|
OR c.unresolved_reason GLOB 'event_name_*'
|
|
9
9
|
THEN c.unresolved_reason END
|
|
10
10
|
)`;
|
|
11
|
+
const receiverReason = `CASE
|
|
12
|
+
WHEN json_extract(c.evidence_json,
|
|
13
|
+
'$.receiverClassification')='name_fallback'
|
|
14
|
+
THEN COALESCE(json_extract(c.evidence_json,
|
|
15
|
+
'$.receiverFallbackRefusedReason'),'name_fallback')
|
|
16
|
+
WHEN json_extract(c.evidence_json,
|
|
17
|
+
'$.receiverClassification')='unproven'
|
|
18
|
+
THEN COALESCE(json_extract(c.evidence_json,
|
|
19
|
+
'$.receiverUnresolvedReason'),'unproven')
|
|
20
|
+
ELSE 'missing' END`;
|
|
11
21
|
|
|
12
22
|
function workspacePredicate(alias: string): string {
|
|
13
23
|
return `(? IS NULL OR ${alias}.workspace_id=?)`;
|
|
@@ -52,6 +62,13 @@ function eventNameResolutionQuality(
|
|
|
52
62
|
workspaceId, workspaceId,
|
|
53
63
|
);
|
|
54
64
|
const unresolved = count(aggregate?.unresolved);
|
|
65
|
+
const reasonCount = count(db.prepare(`SELECT COUNT(DISTINCT reason) count
|
|
66
|
+
FROM (SELECT ${eventNameReason} reason
|
|
67
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
68
|
+
WHERE c.call_type='async_emit' AND ${workspacePredicate('r')}
|
|
69
|
+
AND ${eventNameReason} IS NOT NULL)`).get(
|
|
70
|
+
workspaceId, workspaceId,
|
|
71
|
+
)?.count);
|
|
55
72
|
return {
|
|
56
73
|
severity: unresolved > 0 ? 'warning' : 'info',
|
|
57
74
|
code: 'strict_event_name_resolution_quality',
|
|
@@ -59,6 +76,9 @@ function eventNameResolutionQuality(
|
|
|
59
76
|
publicationTotal: count(aggregate?.total),
|
|
60
77
|
unresolvedPublicationCount: unresolved,
|
|
61
78
|
reasonBuckets: reasons,
|
|
79
|
+
reasonBucketCount: reasonCount,
|
|
80
|
+
shownReasonBucketCount: reasons.length,
|
|
81
|
+
omittedReasonBucketCount: Math.max(0, reasonCount - reasons.length),
|
|
62
82
|
examples: unresolvedEventNameExamples(db, workspaceId),
|
|
63
83
|
exampleCount: unresolved,
|
|
64
84
|
};
|
|
@@ -204,8 +224,12 @@ function unmatchedSubscriptionQuality(
|
|
|
204
224
|
db: Db,
|
|
205
225
|
workspaceId?: number,
|
|
206
226
|
): Diagnostic {
|
|
207
|
-
const row = db.prepare(`SELECT
|
|
227
|
+
const row = db.prepare(`SELECT
|
|
228
|
+
COUNT(DISTINCT json_extract(e.evidence_json,'$.subscribeCallId'))
|
|
229
|
+
siteCount,
|
|
208
230
|
SUM(CASE
|
|
231
|
+
WHEN json_extract(e.evidence_json,'$.materializedLoopEventName')
|
|
232
|
+
IS NOT NULL THEN 1
|
|
209
233
|
WHEN json_extract(c.evidence_json,
|
|
210
234
|
'$.subscriptionLoopRegistrationStatus')='enumerated'
|
|
211
235
|
THEN CAST(json_extract(c.evidence_json,
|
|
@@ -215,7 +239,7 @@ function unmatchedSubscriptionQuality(
|
|
|
215
239
|
ELSE 1 END) count,
|
|
216
240
|
SUM(CASE WHEN json_extract(c.evidence_json,
|
|
217
241
|
'$.subscriptionLoopRegistrationStatus')='unresolved'
|
|
218
|
-
THEN 1 ELSE 0 END)
|
|
242
|
+
THEN 1 ELSE 0 END) unknownMultiplicityEdgeCount
|
|
219
243
|
FROM graph_edges e LEFT JOIN outbound_calls c
|
|
220
244
|
ON c.id=CAST(json_extract(e.evidence_json,'$.subscribeCallId') AS INTEGER)
|
|
221
245
|
WHERE e.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
|
|
@@ -228,23 +252,45 @@ function unmatchedSubscriptionQuality(
|
|
|
228
252
|
AND publication.to_id=e.from_id)`).get(workspaceId, workspaceId);
|
|
229
253
|
const total = count(row?.count);
|
|
230
254
|
const siteCount = count(row?.siteCount);
|
|
255
|
+
const unknownSites = unknownMultiplicitySiteCount(db, workspaceId);
|
|
231
256
|
return {
|
|
232
257
|
severity: siteCount > 0 ? 'warning' : 'info',
|
|
233
258
|
code: 'strict_event_subscription_without_publication_quality',
|
|
234
259
|
message: 'Event subscriptions without a matching publication',
|
|
235
260
|
unmatchedSubscriptionCount: total,
|
|
236
261
|
unmatchedSubscriptionSiteCount: siteCount,
|
|
237
|
-
unknownMultiplicitySiteCount:
|
|
262
|
+
unknownMultiplicitySiteCount: unknownSites,
|
|
238
263
|
examples: unmatchedSubscriptionRows(db, workspaceId),
|
|
239
264
|
exampleCount: siteCount,
|
|
240
265
|
};
|
|
241
266
|
}
|
|
242
267
|
|
|
243
|
-
function
|
|
268
|
+
function unknownMultiplicitySiteCount(
|
|
244
269
|
db: Db,
|
|
245
270
|
workspaceId?: number,
|
|
246
|
-
):
|
|
247
|
-
const row = db.prepare(`SELECT COUNT(
|
|
271
|
+
): number {
|
|
272
|
+
const row = db.prepare(`SELECT COUNT(DISTINCT
|
|
273
|
+
json_extract(e.evidence_json,'$.subscribeCallId')) count
|
|
274
|
+
FROM graph_edges e LEFT JOIN outbound_calls c
|
|
275
|
+
ON c.id=CAST(json_extract(e.evidence_json,'$.subscribeCallId') AS INTEGER)
|
|
276
|
+
WHERE e.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
|
|
277
|
+
AND ${workspacePredicate('e')}
|
|
278
|
+
AND json_extract(c.evidence_json,
|
|
279
|
+
'$.subscriptionLoopRegistrationStatus')='unresolved'
|
|
280
|
+
AND NOT EXISTS (SELECT 1 FROM graph_edges publication
|
|
281
|
+
WHERE publication.workspace_id=e.workspace_id
|
|
282
|
+
AND publication.generation=e.generation
|
|
283
|
+
AND publication.edge_type='HANDLER_EMITS_EVENT'
|
|
284
|
+
AND publication.to_kind='event'
|
|
285
|
+
AND publication.to_id=e.from_id)`).get(workspaceId, workspaceId);
|
|
286
|
+
return count(row?.count);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function receiverProofAggregate(
|
|
290
|
+
db: Db,
|
|
291
|
+
workspaceId?: number,
|
|
292
|
+
): Record<string, unknown> | undefined {
|
|
293
|
+
return db.prepare(`SELECT COUNT(*) eventTotal,
|
|
248
294
|
SUM(CASE WHEN json_extract(c.evidence_json,
|
|
249
295
|
'$.receiverClassification')='cap_evidence' THEN 1 ELSE 0 END) proven,
|
|
250
296
|
SUM(CASE WHEN json_extract(c.evidence_json,
|
|
@@ -255,13 +301,13 @@ function receiverProofQuality(
|
|
|
255
301
|
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
256
302
|
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
257
303
|
AND ${workspacePredicate('r')}`).get(workspaceId, workspaceId);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function receiverReasonBuckets(
|
|
307
|
+
db: Db,
|
|
308
|
+
workspaceId?: number,
|
|
309
|
+
): Array<Record<string, unknown>> {
|
|
310
|
+
return db.prepare(`SELECT ${receiverReason} reason,COUNT(*) count
|
|
265
311
|
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
266
312
|
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
267
313
|
AND ${workspacePredicate('r')}
|
|
@@ -270,6 +316,29 @@ function receiverProofQuality(
|
|
|
270
316
|
GROUP BY reason ORDER BY count DESC,reason COLLATE BINARY LIMIT 16`).all(
|
|
271
317
|
workspaceId, workspaceId,
|
|
272
318
|
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function receiverReasonBucketCount(
|
|
322
|
+
db: Db,
|
|
323
|
+
workspaceId?: number,
|
|
324
|
+
): number {
|
|
325
|
+
const row = db.prepare(`SELECT COUNT(DISTINCT reason) count
|
|
326
|
+
FROM (SELECT ${receiverReason} reason
|
|
327
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
328
|
+
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
329
|
+
AND ${workspacePredicate('r')}
|
|
330
|
+
AND json_extract(c.evidence_json,'$.receiverClassification')
|
|
331
|
+
<>'cap_evidence')`).get(workspaceId, workspaceId);
|
|
332
|
+
return count(row?.count);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function receiverProofQuality(
|
|
336
|
+
db: Db,
|
|
337
|
+
workspaceId?: number,
|
|
338
|
+
): Diagnostic {
|
|
339
|
+
const row = receiverProofAggregate(db, workspaceId);
|
|
340
|
+
const buckets = receiverReasonBuckets(db, workspaceId);
|
|
341
|
+
const bucketCount = receiverReasonBucketCount(db, workspaceId);
|
|
273
342
|
const questionable = count(row?.nameFallback) + count(row?.unproven);
|
|
274
343
|
return {
|
|
275
344
|
severity: questionable > 0 ? 'warning' : 'info',
|
|
@@ -281,6 +350,9 @@ function receiverProofQuality(
|
|
|
281
350
|
unproven: count(row?.unproven),
|
|
282
351
|
questionable,
|
|
283
352
|
reasonBuckets: buckets,
|
|
353
|
+
reasonBucketCount: bucketCount,
|
|
354
|
+
shownReasonBucketCount: buckets.length,
|
|
355
|
+
omittedReasonBucketCount: Math.max(0, bucketCount - buckets.length),
|
|
284
356
|
examples: receiverProofExamples(db, workspaceId),
|
|
285
357
|
exampleCount: questionable,
|
|
286
358
|
};
|
|
@@ -293,8 +365,11 @@ function receiverProofExamples(
|
|
|
293
365
|
return db.prepare(`SELECT r.name repositoryName,c.call_type callType,
|
|
294
366
|
c.source_file sourceFile,c.source_line sourceLine,
|
|
295
367
|
json_extract(c.evidence_json,'$.receiverClassification')
|
|
296
|
-
|
|
297
|
-
c.
|
|
368
|
+
receiverClassification,
|
|
369
|
+
COALESCE(json_extract(c.evidence_json,'$.receiverUnresolvedReason'),
|
|
370
|
+
json_extract(c.evidence_json,'$.receiverFallbackRefusedReason')) reason,
|
|
371
|
+
json_array_length(json_extract(c.evidence_json,
|
|
372
|
+
'$.consideredBindingSites')) consideredBindingSiteCount
|
|
298
373
|
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
299
374
|
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
300
375
|
AND ${workspacePredicate('r')}
|
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createWorkspaceConfig,
|
|
5
5
|
loadWorkspaceConfig,
|
|
6
6
|
saveWorkspaceConfig,
|
|
7
|
+
type WorkspaceConfig,
|
|
7
8
|
} from './config/workspace-config.js';
|
|
8
9
|
import { openDatabase, openReadOnlyDatabase, type Db } from './db/connection.js';
|
|
9
10
|
import {
|
|
@@ -20,6 +21,7 @@ import { classifyRepository } from './discovery/classify-repository.js';
|
|
|
20
21
|
import { indexWorkspace } from './indexer/workspace-indexer.js';
|
|
21
22
|
import { linkWorkspace } from './linker/cross-repo-linker.js';
|
|
22
23
|
import { doctorDiagnostics, linkUpgradeWarnings } from './cli/doctor.js';
|
|
24
|
+
import { factLifecycleDiagnostic } from './db/fact-lifecycle.js';
|
|
23
25
|
import { trace } from './trace/trace-engine.js';
|
|
24
26
|
import { compactTrace } from './trace/compact-trace.js';
|
|
25
27
|
import {
|
|
@@ -33,6 +35,9 @@ import { renderDoctorDiagnostics } from './output/doctor-output.js';
|
|
|
33
35
|
import { renderMermaid } from './output/mermaid-output.js';
|
|
34
36
|
import { createStdoutWriter } from './output/stdout-policy.js';
|
|
35
37
|
import { renderCompactJson } from './output/compact-json-output.js';
|
|
38
|
+
import {
|
|
39
|
+
projectRepositoryInspection,
|
|
40
|
+
} from './output/repository-inspection.js';
|
|
36
41
|
import { VERSION } from './version.js';
|
|
37
42
|
import type {
|
|
38
43
|
DynamicMode,
|
|
@@ -122,6 +127,7 @@ async function withWorkspace<T>(
|
|
|
122
127
|
db: ReturnType<typeof openDatabase>,
|
|
123
128
|
workspaceId: number,
|
|
124
129
|
rootPath: string,
|
|
130
|
+
config: WorkspaceConfig,
|
|
125
131
|
) => Promise<T> | T,
|
|
126
132
|
): Promise<T> {
|
|
127
133
|
const config = await loadWorkspaceConfig(workspace);
|
|
@@ -130,7 +136,7 @@ async function withWorkspace<T>(
|
|
|
130
136
|
const row = getWorkspace(db, config.rootPath);
|
|
131
137
|
const workspaceId =
|
|
132
138
|
row?.id ?? upsertWorkspace(db, config.rootPath, config.dbPath);
|
|
133
|
-
return await fn(db, workspaceId, config.rootPath);
|
|
139
|
+
return await fn(db, workspaceId, config.rootPath, config);
|
|
134
140
|
} finally {
|
|
135
141
|
db.close();
|
|
136
142
|
}
|
|
@@ -275,10 +281,11 @@ function registerIndexCommand(program: Command): void {
|
|
|
275
281
|
.option('--force')
|
|
276
282
|
.action(
|
|
277
283
|
(opts: { workspace?: string; repo?: string; force?: boolean }) =>
|
|
278
|
-
void withWorkspace(opts.workspace, async (db, workspaceId) => {
|
|
284
|
+
void withWorkspace(opts.workspace, async (db, workspaceId, _root, config) => {
|
|
279
285
|
const r = await indexWorkspace(db, workspaceId, {
|
|
280
286
|
repo: opts.repo,
|
|
281
287
|
force: Boolean(opts.force),
|
|
288
|
+
eventEnvironmentKeys: config.eventEnvironmentKeys,
|
|
282
289
|
});
|
|
283
290
|
const outcome = indexCommandOutcome(r);
|
|
284
291
|
writeStdout(outcome.stdout);
|
|
@@ -298,7 +305,7 @@ function registerLinkCommand(program: Command): void {
|
|
|
298
305
|
const r = linkWorkspace(db, workspaceId);
|
|
299
306
|
const upgradeWarnings = linkUpgradeWarnings(db, workspaceId);
|
|
300
307
|
writeStdout(
|
|
301
|
-
`${upgradeWarnings.length ? `Warnings: ${upgradeWarnings.map((item) => String(item.code)).join(', ')}. Run service-flow doctor --strict for remediation.\n` : ''}Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved, ${r.subscriptionHandlerResolvedCount} subscription handlers resolved, ${r.subscriptionHandlerAmbiguousCount} subscription handlers ambiguous, ${r.subscriptionHandlerUnresolvedCount} subscription handlers unresolved, ${r.subscriptionHandlerMissingAssociationCount} subscription handler associations missing, ${r.eventShapeCandidateCount} event shape candidates\n`,
|
|
308
|
+
`${upgradeWarnings.length ? `Warnings: ${upgradeWarnings.map((item) => String(item.code)).join(', ')}. Run service-flow doctor --strict for remediation.\n` : ''}Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved, ${r.subscriptionHandlerResolvedCount} subscription handlers resolved, ${r.subscriptionHandlerAmbiguousCount} subscription handlers ambiguous, ${r.subscriptionHandlerUnresolvedCount} subscription handlers unresolved, ${r.subscriptionHandlerMissingAssociationCount} subscription handler associations missing, ${r.eventShapeCandidateCount} event shape candidates, ${r.eventShapeCandidateOmittedCount} event shape candidates omitted by the link cap\n`,
|
|
302
309
|
);
|
|
303
310
|
}).catch(fail),
|
|
304
311
|
);
|
|
@@ -329,22 +336,23 @@ function registerTraceCommand(program: Command): void {
|
|
|
329
336
|
function listRepositoriesCommand(
|
|
330
337
|
opts: { workspace?: string },
|
|
331
338
|
): Promise<void> {
|
|
332
|
-
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) =>
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
),
|
|
339
|
+
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
340
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
341
|
+
writeStdout(renderJson(
|
|
342
|
+
listRepositories(db, workspaceId).map((repo) => ({
|
|
343
|
+
name: repo.name,
|
|
344
|
+
kind: repo.kind,
|
|
345
|
+
packageName: repo.package_name,
|
|
346
|
+
})),
|
|
341
347
|
));
|
|
348
|
+
});
|
|
342
349
|
}
|
|
343
350
|
|
|
344
351
|
function listServicesCommand(
|
|
345
352
|
opts: { workspace?: string; repo?: string },
|
|
346
353
|
): Promise<void> {
|
|
347
354
|
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
355
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
348
356
|
const selection = opts.repo
|
|
349
357
|
? selectRepository(db, opts.repo, workspaceId) : {};
|
|
350
358
|
if (selection.diagnostic) {
|
|
@@ -363,6 +371,7 @@ function listOperationsCommand(
|
|
|
363
371
|
opts: { workspace?: string; repo?: string; service?: string },
|
|
364
372
|
): Promise<void> {
|
|
365
373
|
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
374
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
366
375
|
const selection = opts.repo
|
|
367
376
|
? selectRepository(db, opts.repo, workspaceId) : {};
|
|
368
377
|
if (selection.diagnostic) {
|
|
@@ -383,6 +392,7 @@ function listCallsCommand(
|
|
|
383
392
|
opts: { workspace?: string; repo?: string; operation?: string },
|
|
384
393
|
): Promise<void> {
|
|
385
394
|
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
395
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
386
396
|
const selection = opts.repo
|
|
387
397
|
? selectRepository(db, opts.repo, workspaceId) : {};
|
|
388
398
|
if (selection.diagnostic) {
|
|
@@ -460,9 +470,12 @@ function inspectRepositoryCommand(
|
|
|
460
470
|
opts: { workspace?: string },
|
|
461
471
|
): Promise<void> {
|
|
462
472
|
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
473
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
463
474
|
const selection = selectRepository(db, name, workspaceId);
|
|
464
475
|
writeStdout(renderJson(
|
|
465
|
-
selection.repo
|
|
476
|
+
selection.repo
|
|
477
|
+
? projectRepositoryInspection(selection.repo)
|
|
478
|
+
: selection.diagnostic ?? { error: 'repo not found' },
|
|
466
479
|
));
|
|
467
480
|
});
|
|
468
481
|
}
|
|
@@ -472,6 +485,7 @@ function inspectOperationCommand(
|
|
|
472
485
|
opts: { workspace?: string },
|
|
473
486
|
): Promise<void> {
|
|
474
487
|
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
488
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
475
489
|
const rows = db.prepare(
|
|
476
490
|
'SELECT o.* FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (o.operation_name=? OR o.operation_path=?)',
|
|
477
491
|
).all(workspaceId, selector, selector);
|
|
@@ -479,6 +493,14 @@ function inspectOperationCommand(
|
|
|
479
493
|
});
|
|
480
494
|
}
|
|
481
495
|
|
|
496
|
+
function writeLifecycleBlock(db: Db, workspaceId: number): boolean {
|
|
497
|
+
const diagnostic = factLifecycleDiagnostic(db, workspaceId);
|
|
498
|
+
if (!diagnostic) return false;
|
|
499
|
+
writeStdout(renderJson([diagnostic]));
|
|
500
|
+
process.exitCode = 1;
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
482
504
|
function registerInspectCommands(program: Command): void {
|
|
483
505
|
const inspect = program.command('inspect');
|
|
484
506
|
inspect
|
|
@@ -7,10 +7,24 @@ import {
|
|
|
7
7
|
DEFAULT_DB_FILE,
|
|
8
8
|
DEFAULT_IGNORES,
|
|
9
9
|
} from './defaults.js';
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_EVENT_ENVIRONMENT_KEYS,
|
|
12
|
+
EVENT_ENVIRONMENT_KEY_CAP,
|
|
13
|
+
normalizeEventEnvironmentKeys,
|
|
14
|
+
validEventEnvironmentKey,
|
|
15
|
+
} from '../parsers/environment-declarations.js';
|
|
16
|
+
const environmentKeys = z.array(
|
|
17
|
+
z.string().refine(validEventEnvironmentKey),
|
|
18
|
+
).min(1).max(EVENT_ENVIRONMENT_KEY_CAP).transform(
|
|
19
|
+
normalizeEventEnvironmentKeys,
|
|
20
|
+
);
|
|
10
21
|
const schema = z.object({
|
|
11
22
|
rootPath: z.string(),
|
|
12
23
|
dbPath: z.string(),
|
|
13
24
|
ignore: z.array(z.string()),
|
|
25
|
+
eventEnvironmentKeys: environmentKeys.default(
|
|
26
|
+
[...DEFAULT_EVENT_ENVIRONMENT_KEYS],
|
|
27
|
+
),
|
|
14
28
|
createdAt: z.string(),
|
|
15
29
|
updatedAt: z.string(),
|
|
16
30
|
});
|
|
@@ -55,6 +69,7 @@ export function createWorkspaceConfig(
|
|
|
55
69
|
rootPath: root,
|
|
56
70
|
dbPath: path.resolve(dbPath ?? defaultDbPath(root)),
|
|
57
71
|
ignore,
|
|
72
|
+
eventEnvironmentKeys: [...DEFAULT_EVENT_ENVIRONMENT_KEYS],
|
|
58
73
|
createdAt: now,
|
|
59
74
|
updatedAt: now,
|
|
60
75
|
};
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from './binding-fact-semantics.js';
|
|
13
13
|
import { invalidSymbolFactCategories } from './symbol-call-semantics.js';
|
|
14
14
|
import { invalidEventFactCategories } from './event-fact-semantics.js';
|
|
15
|
+
import { eventSiteCategories } from './event-site-semantics.js';
|
|
15
16
|
|
|
16
17
|
export type PackageFactPhase = 'pre_package' | 'terminal';
|
|
17
18
|
|
|
@@ -257,31 +258,6 @@ function duplicateSymbolCallCategories(
|
|
|
257
258
|
return category('symbol_call_site_duplicate', duplicate);
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
function eventNameCategories(
|
|
261
|
-
db: Db,
|
|
262
|
-
workspaceId?: number,
|
|
263
|
-
): FactSemanticCategoryCount[] {
|
|
264
|
-
const invalidName = count(db, `SELECT COUNT(*) count
|
|
265
|
-
FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
|
|
266
|
-
WHERE ${currentRepositoryPredicate()}
|
|
267
|
-
AND fact.call_type IN ('async_emit','async_subscribe')
|
|
268
|
-
AND (typeof(fact.event_name_expr)<>'text'
|
|
269
|
-
OR length(fact.event_name_expr)=0)`, workspaceId);
|
|
270
|
-
const duplicate = count(db, `SELECT COUNT(*) count FROM (
|
|
271
|
-
SELECT fact.repo_id,fact.source_file,fact.call_site_start_offset,
|
|
272
|
-
fact.call_site_end_offset,COUNT(*) duplicate_count
|
|
273
|
-
FROM outbound_calls fact JOIN repositories r ON r.id=fact.repo_id
|
|
274
|
-
WHERE ${currentRepositoryPredicate()}
|
|
275
|
-
AND fact.call_type='async_subscribe'
|
|
276
|
-
GROUP BY fact.repo_id,fact.source_file,fact.call_site_start_offset,
|
|
277
|
-
fact.call_site_end_offset HAVING COUNT(*)<>1
|
|
278
|
-
)`, workspaceId);
|
|
279
|
-
return [
|
|
280
|
-
...category('event_name_invalid', invalidName),
|
|
281
|
-
...category('async_subscription_site_duplicate', duplicate),
|
|
282
|
-
];
|
|
283
|
-
}
|
|
284
|
-
|
|
285
261
|
function outboundOwnerCount(db: Db, workspaceId?: number): number {
|
|
286
262
|
return count(db, `WITH eligible AS (
|
|
287
263
|
SELECT fact.id fact_id,s.id symbol_id,
|
|
@@ -684,7 +660,7 @@ export function invalidFactSemanticCategories(
|
|
|
684
660
|
...operationCategories(db, workspaceId),
|
|
685
661
|
...callSpanCategories(db, workspaceId),
|
|
686
662
|
...duplicateSymbolCallCategories(db, workspaceId),
|
|
687
|
-
...
|
|
663
|
+
...eventSiteCategories(db, workspaceId),
|
|
688
664
|
...invalidEventFactCategories(db, workspaceId, phase),
|
|
689
665
|
...ownerCategories(db, workspaceId),
|
|
690
666
|
...bindingCategories(db, workspaceId),
|