@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/cli.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ANALYZER_VERSION,
|
|
4
|
+
DEFAULT_EVENT_ENVIRONMENT_KEYS,
|
|
5
|
+
EVENT_ENVIRONMENT_KEY_CAP,
|
|
4
6
|
PreparedRepositorySnapshotError,
|
|
5
7
|
VERSION,
|
|
6
8
|
analyzePackagePublicSurface,
|
|
@@ -34,6 +36,7 @@ import {
|
|
|
34
36
|
loadPackageJsonSnapshot,
|
|
35
37
|
loadRepositorySourceContext,
|
|
36
38
|
migrate,
|
|
39
|
+
normalizeEventEnvironmentKeys,
|
|
37
40
|
normalizeODataOperationInvocationPath,
|
|
38
41
|
normalizePath,
|
|
39
42
|
parseCdsFile,
|
|
@@ -55,8 +58,9 @@ import {
|
|
|
55
58
|
sha256Text,
|
|
56
59
|
stableLocalValueReference,
|
|
57
60
|
symbolImportReference,
|
|
58
|
-
trace
|
|
59
|
-
|
|
61
|
+
trace,
|
|
62
|
+
validEventEnvironmentKey
|
|
63
|
+
} from "./chunk-32WOTGTS.js";
|
|
60
64
|
|
|
61
65
|
// src/cli.ts
|
|
62
66
|
import { Command, Option } from "commander";
|
|
@@ -81,10 +85,18 @@ var DEFAULT_DB_FILE = "service-flow.db";
|
|
|
81
85
|
import fs from "fs/promises";
|
|
82
86
|
import path from "path";
|
|
83
87
|
import { z } from "zod";
|
|
88
|
+
var environmentKeys = z.array(
|
|
89
|
+
z.string().refine(validEventEnvironmentKey)
|
|
90
|
+
).min(1).max(EVENT_ENVIRONMENT_KEY_CAP).transform(
|
|
91
|
+
normalizeEventEnvironmentKeys
|
|
92
|
+
);
|
|
84
93
|
var schema = z.object({
|
|
85
94
|
rootPath: z.string(),
|
|
86
95
|
dbPath: z.string(),
|
|
87
96
|
ignore: z.array(z.string()),
|
|
97
|
+
eventEnvironmentKeys: environmentKeys.default(
|
|
98
|
+
[...DEFAULT_EVENT_ENVIRONMENT_KEYS]
|
|
99
|
+
),
|
|
88
100
|
createdAt: z.string(),
|
|
89
101
|
updatedAt: z.string()
|
|
90
102
|
});
|
|
@@ -121,6 +133,7 @@ function createWorkspaceConfig(rootPath, dbPath, ignore = [...DEFAULT_IGNORES])
|
|
|
121
133
|
rootPath: root,
|
|
122
134
|
dbPath: path.resolve(dbPath ?? defaultDbPath(root)),
|
|
123
135
|
ignore,
|
|
136
|
+
eventEnvironmentKeys: [...DEFAULT_EVENT_ENVIRONMENT_KEYS],
|
|
124
137
|
createdAt: now,
|
|
125
138
|
updatedAt: now
|
|
126
139
|
};
|
|
@@ -2594,8 +2607,12 @@ function resetPackageEventCalls(db, workspaceId, targetRepoId, names, batch) {
|
|
|
2594
2607
|
const source = call.evidence.eventNameConstantSourceExpression;
|
|
2595
2608
|
if (typeof source !== "string" || source.length === 0)
|
|
2596
2609
|
throw new Error("invalid_current_package_event_constant_evidence");
|
|
2597
|
-
|
|
2598
|
-
|
|
2610
|
+
update.run(
|
|
2611
|
+
source,
|
|
2612
|
+
"event_name_constant_resolution_pending",
|
|
2613
|
+
pendingEventEvidence(call.evidence),
|
|
2614
|
+
call.id
|
|
2615
|
+
);
|
|
2599
2616
|
batch.affectedCallerRepoIds.add(call.repoId);
|
|
2600
2617
|
matched = true;
|
|
2601
2618
|
}
|
|
@@ -2696,7 +2713,7 @@ function invalidateEventSurfaceFacts(db, repoId, calls, nextEnvironmentJson) {
|
|
|
2696
2713
|
}
|
|
2697
2714
|
|
|
2698
2715
|
// src/indexer/repository-indexer.ts
|
|
2699
|
-
async function prepareRepositoryIndex(repo, force, instrumentation) {
|
|
2716
|
+
async function prepareRepositoryIndex(repo, force, instrumentation, eventEnvironmentKeys) {
|
|
2700
2717
|
const sourceFiles = await findSourceFiles(repo.absolute_path);
|
|
2701
2718
|
const packageSnapshot = await loadPackageJsonSnapshot(repo.absolute_path, {
|
|
2702
2719
|
strict: true,
|
|
@@ -2708,13 +2725,23 @@ async function prepareRepositoryIndex(repo, force, instrumentation) {
|
|
|
2708
2725
|
sourceFiles,
|
|
2709
2726
|
instrumentation
|
|
2710
2727
|
);
|
|
2728
|
+
const environmentDeclarations = collectEnvironmentDeclarations(
|
|
2729
|
+
sources,
|
|
2730
|
+
eventEnvironmentKeys
|
|
2731
|
+
);
|
|
2711
2732
|
const fingerprint = repositoryFingerprint(
|
|
2712
2733
|
sources,
|
|
2713
2734
|
packageFacts,
|
|
2714
|
-
packageSnapshot.rawText
|
|
2735
|
+
packageSnapshot.rawText,
|
|
2736
|
+
environmentDeclarations
|
|
2715
2737
|
);
|
|
2716
2738
|
if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
|
|
2717
|
-
const parsedFacts = await parseAllSourceFacts(
|
|
2739
|
+
const parsedFacts = await parseAllSourceFacts(
|
|
2740
|
+
repo.absolute_path,
|
|
2741
|
+
sources,
|
|
2742
|
+
environmentDeclarations,
|
|
2743
|
+
eventEnvironmentKeys
|
|
2744
|
+
);
|
|
2718
2745
|
const packageSurface = analyzeRepositoryPackageSurface(
|
|
2719
2746
|
packageFacts,
|
|
2720
2747
|
packageSnapshot.manifest,
|
|
@@ -2731,7 +2758,7 @@ async function prepareRepositoryIndex(repo, force, instrumentation) {
|
|
|
2731
2758
|
kind: await classifyRepository(repo.absolute_path, packageFacts),
|
|
2732
2759
|
parsed,
|
|
2733
2760
|
packagePublicSurface: packageSurface.surface,
|
|
2734
|
-
environmentDeclarations
|
|
2761
|
+
environmentDeclarations,
|
|
2735
2762
|
fileCount: sourceFiles.length,
|
|
2736
2763
|
diagnosticCount: parsed.handlers.filter((handler) => handler.hasHandlerDecorator && (handler.methods.length === 0 || handler.methods.some((method) => !handlerMethodIsExecutable(method)))).length,
|
|
2737
2764
|
skipped: false
|
|
@@ -2822,11 +2849,16 @@ function recordIndexFailure(db, repoId, error) {
|
|
|
2822
2849
|
)`).run(repoId);
|
|
2823
2850
|
db.prepare("INSERT INTO diagnostics(repo_id,severity,code,message) VALUES(?,?,?,?)").run(repoId, "error", "source_read_failed", `Index failed before publication; previous facts and fingerprint were preserved. ${message}`);
|
|
2824
2851
|
}
|
|
2825
|
-
async function parseAllSourceFacts(root, sources) {
|
|
2852
|
+
async function parseAllSourceFacts(root, sources, environmentDeclarations, eventEnvironmentKeys) {
|
|
2826
2853
|
const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], generatedConstants: [], fileRecords: [] };
|
|
2827
2854
|
for (const snapshot of sources.entries()) {
|
|
2828
2855
|
const file = snapshot.filePath;
|
|
2829
|
-
facts.fileRecords.push({
|
|
2856
|
+
facts.fileRecords.push({
|
|
2857
|
+
relativePath: normalizePath(file),
|
|
2858
|
+
extension: path5.extname(file),
|
|
2859
|
+
sha256: sourceFactHash(snapshot, environmentDeclarations),
|
|
2860
|
+
sizeBytes: snapshot.sizeBytes
|
|
2861
|
+
});
|
|
2830
2862
|
if (file.endsWith(".cds")) facts.services.push(...await parseCdsFile(root, file, sources));
|
|
2831
2863
|
if (/\.[jt]s$/.test(file)) {
|
|
2832
2864
|
const source = snapshot.sourceFile();
|
|
@@ -2837,7 +2869,12 @@ async function parseAllSourceFacts(root, sources) {
|
|
|
2837
2869
|
source,
|
|
2838
2870
|
file
|
|
2839
2871
|
),
|
|
2840
|
-
eventEnvironmentReferenceResolver: createEventEnvironmentReferenceResolver(
|
|
2872
|
+
eventEnvironmentReferenceResolver: createEventEnvironmentReferenceResolver(
|
|
2873
|
+
sources,
|
|
2874
|
+
source,
|
|
2875
|
+
file,
|
|
2876
|
+
eventEnvironmentKeys
|
|
2877
|
+
)
|
|
2841
2878
|
});
|
|
2842
2879
|
facts.handlers.push(...await parseDecorators(root, file, sources));
|
|
2843
2880
|
facts.registrations.push(...await parseHandlerRegistrations(root, file, sources));
|
|
@@ -2893,7 +2930,7 @@ function isDefaultTestFile(relativeFile) {
|
|
|
2893
2930
|
if (parts.some((part) => ["test", "tests", "__tests__"].includes(part))) return true;
|
|
2894
2931
|
return /\.(test|spec)\.[jt]s$/.test(parts.at(-1) ?? "");
|
|
2895
2932
|
}
|
|
2896
|
-
function repositoryFingerprint(sources, facts, packageJsonText) {
|
|
2933
|
+
function repositoryFingerprint(sources, facts, packageJsonText, environmentDeclarations) {
|
|
2897
2934
|
const normalizedFacts = {
|
|
2898
2935
|
analyzerVersion: ANALYZER_VERSION,
|
|
2899
2936
|
packageName: facts.packageName,
|
|
@@ -2902,13 +2939,33 @@ function repositoryFingerprint(sources, facts, packageJsonText) {
|
|
|
2902
2939
|
cdsRequires: [...facts.cdsRequires].sort((a, b) => a.alias.localeCompare(b.alias)),
|
|
2903
2940
|
scripts: Object.fromEntries(Object.entries(facts.scripts).sort()),
|
|
2904
2941
|
includeTests: false,
|
|
2942
|
+
eventEnvironmentKeys: environmentDeclarations.allowedKeys,
|
|
2905
2943
|
packageJsonHash: sha256Text(packageJsonText)
|
|
2906
2944
|
};
|
|
2907
2945
|
const entries = [`facts:${JSON.stringify(normalizedFacts)}`];
|
|
2908
2946
|
for (const snapshot of sources.entries())
|
|
2909
|
-
entries.push(
|
|
2947
|
+
entries.push(
|
|
2948
|
+
`${snapshot.filePath}:${sourceFactHash(snapshot, environmentDeclarations)}`
|
|
2949
|
+
);
|
|
2910
2950
|
return sha256Text(entries.join("\n"));
|
|
2911
2951
|
}
|
|
2952
|
+
function sourceFactHash(snapshot, environmentDeclarations) {
|
|
2953
|
+
if (!isEnvironmentFactInput(snapshot.filePath))
|
|
2954
|
+
return sha256Text(snapshot.text);
|
|
2955
|
+
const declarations = environmentDeclarations.declarations.filter(
|
|
2956
|
+
(item) => item.sourceFile === snapshot.filePath
|
|
2957
|
+
);
|
|
2958
|
+
return sha256Text(JSON.stringify({
|
|
2959
|
+
allowedKeys: environmentDeclarations.allowedKeys,
|
|
2960
|
+
declarations
|
|
2961
|
+
}));
|
|
2962
|
+
}
|
|
2963
|
+
function isEnvironmentFactInput(filePath) {
|
|
2964
|
+
const name = filePath.split("/").at(-1);
|
|
2965
|
+
return ["nodemon.json", ".env", "mta.yaml", "manifest.yml"].includes(
|
|
2966
|
+
name ?? ""
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2912
2969
|
|
|
2913
2970
|
// src/indexer/cds-extension-resolver.ts
|
|
2914
2971
|
function materializeCdsExtensionOperations(db, workspaceId, excludedRepoIds = /* @__PURE__ */ new Set()) {
|
|
@@ -3088,7 +3145,12 @@ async function indexWorkspace(db, workspaceId, options) {
|
|
|
3088
3145
|
rows: []
|
|
3089
3146
|
};
|
|
3090
3147
|
try {
|
|
3091
|
-
await prepareRepositories(
|
|
3148
|
+
await prepareRepositories(
|
|
3149
|
+
repos,
|
|
3150
|
+
options.force,
|
|
3151
|
+
state,
|
|
3152
|
+
options.eventEnvironmentKeys
|
|
3153
|
+
);
|
|
3092
3154
|
return publishPreparedWorkspaceRows(
|
|
3093
3155
|
db,
|
|
3094
3156
|
workspaceId,
|
|
@@ -3115,10 +3177,15 @@ function selectedRepositories(db, workspaceId, repoName) {
|
|
|
3115
3177
|
);
|
|
3116
3178
|
return repos;
|
|
3117
3179
|
}
|
|
3118
|
-
async function prepareRepositories(repos, force, state) {
|
|
3180
|
+
async function prepareRepositories(repos, force, state, eventEnvironmentKeys) {
|
|
3119
3181
|
for (const repo of repos) {
|
|
3120
3182
|
state.activeRepoId = repo.id;
|
|
3121
|
-
const result = await prepareRepositoryIndex(
|
|
3183
|
+
const result = await prepareRepositoryIndex(
|
|
3184
|
+
repo,
|
|
3185
|
+
force,
|
|
3186
|
+
void 0,
|
|
3187
|
+
eventEnvironmentKeys
|
|
3188
|
+
);
|
|
3122
3189
|
state.rows.push(result);
|
|
3123
3190
|
state.fileCount += result.fileCount;
|
|
3124
3191
|
state.diagnosticCount += result.diagnosticCount;
|
|
@@ -3470,6 +3537,16 @@ var eventNameReason = `COALESCE(
|
|
|
3470
3537
|
OR c.unresolved_reason GLOB 'event_name_*'
|
|
3471
3538
|
THEN c.unresolved_reason END
|
|
3472
3539
|
)`;
|
|
3540
|
+
var receiverReason = `CASE
|
|
3541
|
+
WHEN json_extract(c.evidence_json,
|
|
3542
|
+
'$.receiverClassification')='name_fallback'
|
|
3543
|
+
THEN COALESCE(json_extract(c.evidence_json,
|
|
3544
|
+
'$.receiverFallbackRefusedReason'),'name_fallback')
|
|
3545
|
+
WHEN json_extract(c.evidence_json,
|
|
3546
|
+
'$.receiverClassification')='unproven'
|
|
3547
|
+
THEN COALESCE(json_extract(c.evidence_json,
|
|
3548
|
+
'$.receiverUnresolvedReason'),'unproven')
|
|
3549
|
+
ELSE 'missing' END`;
|
|
3473
3550
|
function workspacePredicate(alias) {
|
|
3474
3551
|
return `(? IS NULL OR ${alias}.workspace_id=?)`;
|
|
3475
3552
|
}
|
|
@@ -3507,6 +3584,14 @@ function eventNameResolutionQuality(db, workspaceId) {
|
|
|
3507
3584
|
workspaceId
|
|
3508
3585
|
);
|
|
3509
3586
|
const unresolved = count2(aggregate?.unresolved);
|
|
3587
|
+
const reasonCount = count2(db.prepare(`SELECT COUNT(DISTINCT reason) count
|
|
3588
|
+
FROM (SELECT ${eventNameReason} reason
|
|
3589
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
3590
|
+
WHERE c.call_type='async_emit' AND ${workspacePredicate("r")}
|
|
3591
|
+
AND ${eventNameReason} IS NOT NULL)`).get(
|
|
3592
|
+
workspaceId,
|
|
3593
|
+
workspaceId
|
|
3594
|
+
)?.count);
|
|
3510
3595
|
return {
|
|
3511
3596
|
severity: unresolved > 0 ? "warning" : "info",
|
|
3512
3597
|
code: "strict_event_name_resolution_quality",
|
|
@@ -3514,6 +3599,9 @@ function eventNameResolutionQuality(db, workspaceId) {
|
|
|
3514
3599
|
publicationTotal: count2(aggregate?.total),
|
|
3515
3600
|
unresolvedPublicationCount: unresolved,
|
|
3516
3601
|
reasonBuckets: reasons,
|
|
3602
|
+
reasonBucketCount: reasonCount,
|
|
3603
|
+
shownReasonBucketCount: reasons.length,
|
|
3604
|
+
omittedReasonBucketCount: Math.max(0, reasonCount - reasons.length),
|
|
3517
3605
|
examples: unresolvedEventNameExamples(db, workspaceId),
|
|
3518
3606
|
exampleCount: unresolved
|
|
3519
3607
|
};
|
|
@@ -3639,8 +3727,12 @@ function unmatchedSubscriptionRows(db, workspaceId) {
|
|
|
3639
3727
|
);
|
|
3640
3728
|
}
|
|
3641
3729
|
function unmatchedSubscriptionQuality(db, workspaceId) {
|
|
3642
|
-
const row = db.prepare(`SELECT
|
|
3730
|
+
const row = db.prepare(`SELECT
|
|
3731
|
+
COUNT(DISTINCT json_extract(e.evidence_json,'$.subscribeCallId'))
|
|
3732
|
+
siteCount,
|
|
3643
3733
|
SUM(CASE
|
|
3734
|
+
WHEN json_extract(e.evidence_json,'$.materializedLoopEventName')
|
|
3735
|
+
IS NOT NULL THEN 1
|
|
3644
3736
|
WHEN json_extract(c.evidence_json,
|
|
3645
3737
|
'$.subscriptionLoopRegistrationStatus')='enumerated'
|
|
3646
3738
|
THEN CAST(json_extract(c.evidence_json,
|
|
@@ -3650,7 +3742,7 @@ function unmatchedSubscriptionQuality(db, workspaceId) {
|
|
|
3650
3742
|
ELSE 1 END) count,
|
|
3651
3743
|
SUM(CASE WHEN json_extract(c.evidence_json,
|
|
3652
3744
|
'$.subscriptionLoopRegistrationStatus')='unresolved'
|
|
3653
|
-
THEN 1 ELSE 0 END)
|
|
3745
|
+
THEN 1 ELSE 0 END) unknownMultiplicityEdgeCount
|
|
3654
3746
|
FROM graph_edges e LEFT JOIN outbound_calls c
|
|
3655
3747
|
ON c.id=CAST(json_extract(e.evidence_json,'$.subscribeCallId') AS INTEGER)
|
|
3656
3748
|
WHERE e.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
|
|
@@ -3663,19 +3755,37 @@ function unmatchedSubscriptionQuality(db, workspaceId) {
|
|
|
3663
3755
|
AND publication.to_id=e.from_id)`).get(workspaceId, workspaceId);
|
|
3664
3756
|
const total = count2(row?.count);
|
|
3665
3757
|
const siteCount = count2(row?.siteCount);
|
|
3758
|
+
const unknownSites = unknownMultiplicitySiteCount(db, workspaceId);
|
|
3666
3759
|
return {
|
|
3667
3760
|
severity: siteCount > 0 ? "warning" : "info",
|
|
3668
3761
|
code: "strict_event_subscription_without_publication_quality",
|
|
3669
3762
|
message: "Event subscriptions without a matching publication",
|
|
3670
3763
|
unmatchedSubscriptionCount: total,
|
|
3671
3764
|
unmatchedSubscriptionSiteCount: siteCount,
|
|
3672
|
-
unknownMultiplicitySiteCount:
|
|
3765
|
+
unknownMultiplicitySiteCount: unknownSites,
|
|
3673
3766
|
examples: unmatchedSubscriptionRows(db, workspaceId),
|
|
3674
3767
|
exampleCount: siteCount
|
|
3675
3768
|
};
|
|
3676
3769
|
}
|
|
3677
|
-
function
|
|
3678
|
-
const row = db.prepare(`SELECT COUNT(
|
|
3770
|
+
function unknownMultiplicitySiteCount(db, workspaceId) {
|
|
3771
|
+
const row = db.prepare(`SELECT COUNT(DISTINCT
|
|
3772
|
+
json_extract(e.evidence_json,'$.subscribeCallId')) count
|
|
3773
|
+
FROM graph_edges e LEFT JOIN outbound_calls c
|
|
3774
|
+
ON c.id=CAST(json_extract(e.evidence_json,'$.subscribeCallId') AS INTEGER)
|
|
3775
|
+
WHERE e.edge_type='EVENT_SUBSCRIPTION_HANDLED_BY'
|
|
3776
|
+
AND ${workspacePredicate("e")}
|
|
3777
|
+
AND json_extract(c.evidence_json,
|
|
3778
|
+
'$.subscriptionLoopRegistrationStatus')='unresolved'
|
|
3779
|
+
AND NOT EXISTS (SELECT 1 FROM graph_edges publication
|
|
3780
|
+
WHERE publication.workspace_id=e.workspace_id
|
|
3781
|
+
AND publication.generation=e.generation
|
|
3782
|
+
AND publication.edge_type='HANDLER_EMITS_EVENT'
|
|
3783
|
+
AND publication.to_kind='event'
|
|
3784
|
+
AND publication.to_id=e.from_id)`).get(workspaceId, workspaceId);
|
|
3785
|
+
return count2(row?.count);
|
|
3786
|
+
}
|
|
3787
|
+
function receiverProofAggregate(db, workspaceId) {
|
|
3788
|
+
return db.prepare(`SELECT COUNT(*) eventTotal,
|
|
3679
3789
|
SUM(CASE WHEN json_extract(c.evidence_json,
|
|
3680
3790
|
'$.receiverClassification')='cap_evidence' THEN 1 ELSE 0 END) proven,
|
|
3681
3791
|
SUM(CASE WHEN json_extract(c.evidence_json,
|
|
@@ -3686,13 +3796,9 @@ function receiverProofQuality(db, workspaceId) {
|
|
|
3686
3796
|
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
3687
3797
|
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
3688
3798
|
AND ${workspacePredicate("r")}`).get(workspaceId, workspaceId);
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
WHEN json_extract(c.evidence_json,
|
|
3693
|
-
'$.receiverClassification')='unproven'
|
|
3694
|
-
THEN COALESCE(c.unresolved_reason,'unproven')
|
|
3695
|
-
ELSE 'missing' END reason,COUNT(*) count
|
|
3799
|
+
}
|
|
3800
|
+
function receiverReasonBuckets(db, workspaceId) {
|
|
3801
|
+
return db.prepare(`SELECT ${receiverReason} reason,COUNT(*) count
|
|
3696
3802
|
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
3697
3803
|
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
3698
3804
|
AND ${workspacePredicate("r")}
|
|
@@ -3702,6 +3808,21 @@ function receiverProofQuality(db, workspaceId) {
|
|
|
3702
3808
|
workspaceId,
|
|
3703
3809
|
workspaceId
|
|
3704
3810
|
);
|
|
3811
|
+
}
|
|
3812
|
+
function receiverReasonBucketCount(db, workspaceId) {
|
|
3813
|
+
const row = db.prepare(`SELECT COUNT(DISTINCT reason) count
|
|
3814
|
+
FROM (SELECT ${receiverReason} reason
|
|
3815
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
3816
|
+
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
3817
|
+
AND ${workspacePredicate("r")}
|
|
3818
|
+
AND json_extract(c.evidence_json,'$.receiverClassification')
|
|
3819
|
+
<>'cap_evidence')`).get(workspaceId, workspaceId);
|
|
3820
|
+
return count2(row?.count);
|
|
3821
|
+
}
|
|
3822
|
+
function receiverProofQuality(db, workspaceId) {
|
|
3823
|
+
const row = receiverProofAggregate(db, workspaceId);
|
|
3824
|
+
const buckets = receiverReasonBuckets(db, workspaceId);
|
|
3825
|
+
const bucketCount = receiverReasonBucketCount(db, workspaceId);
|
|
3705
3826
|
const questionable = count2(row?.nameFallback) + count2(row?.unproven);
|
|
3706
3827
|
return {
|
|
3707
3828
|
severity: questionable > 0 ? "warning" : "info",
|
|
@@ -3713,6 +3834,9 @@ function receiverProofQuality(db, workspaceId) {
|
|
|
3713
3834
|
unproven: count2(row?.unproven),
|
|
3714
3835
|
questionable,
|
|
3715
3836
|
reasonBuckets: buckets,
|
|
3837
|
+
reasonBucketCount: bucketCount,
|
|
3838
|
+
shownReasonBucketCount: buckets.length,
|
|
3839
|
+
omittedReasonBucketCount: Math.max(0, bucketCount - buckets.length),
|
|
3716
3840
|
examples: receiverProofExamples(db, workspaceId),
|
|
3717
3841
|
exampleCount: questionable
|
|
3718
3842
|
};
|
|
@@ -3721,8 +3845,11 @@ function receiverProofExamples(db, workspaceId) {
|
|
|
3721
3845
|
return db.prepare(`SELECT r.name repositoryName,c.call_type callType,
|
|
3722
3846
|
c.source_file sourceFile,c.source_line sourceLine,
|
|
3723
3847
|
json_extract(c.evidence_json,'$.receiverClassification')
|
|
3724
|
-
|
|
3725
|
-
c.
|
|
3848
|
+
receiverClassification,
|
|
3849
|
+
COALESCE(json_extract(c.evidence_json,'$.receiverUnresolvedReason'),
|
|
3850
|
+
json_extract(c.evidence_json,'$.receiverFallbackRefusedReason')) reason,
|
|
3851
|
+
json_array_length(json_extract(c.evidence_json,
|
|
3852
|
+
'$.consideredBindingSites')) consideredBindingSiteCount
|
|
3726
3853
|
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
3727
3854
|
WHERE c.call_type IN ('async_emit','async_subscribe')
|
|
3728
3855
|
AND ${workspacePredicate("r")}
|
|
@@ -4645,6 +4772,15 @@ function renderCompactJson(value) {
|
|
|
4645
4772
|
`;
|
|
4646
4773
|
}
|
|
4647
4774
|
|
|
4775
|
+
// src/output/repository-inspection.ts
|
|
4776
|
+
function projectRepositoryInspection(repository) {
|
|
4777
|
+
return Object.fromEntries(
|
|
4778
|
+
Object.entries(repository).filter(
|
|
4779
|
+
([key]) => key !== "environment_declarations_json"
|
|
4780
|
+
)
|
|
4781
|
+
);
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4648
4784
|
// src/cli/clean.ts
|
|
4649
4785
|
import fs6 from "fs/promises";
|
|
4650
4786
|
import path6 from "path";
|
|
@@ -4758,7 +4894,7 @@ async function withWorkspace(workspace, fn) {
|
|
|
4758
4894
|
try {
|
|
4759
4895
|
const row = getWorkspace(db, config.rootPath);
|
|
4760
4896
|
const workspaceId = row?.id ?? upsertWorkspace(db, config.rootPath, config.dbPath);
|
|
4761
|
-
return await fn(db, workspaceId, config.rootPath);
|
|
4897
|
+
return await fn(db, workspaceId, config.rootPath, config);
|
|
4762
4898
|
} finally {
|
|
4763
4899
|
db.close();
|
|
4764
4900
|
}
|
|
@@ -4873,10 +5009,11 @@ function registerInitCommand(program) {
|
|
|
4873
5009
|
}
|
|
4874
5010
|
function registerIndexCommand(program) {
|
|
4875
5011
|
program.command("index").option("--workspace <path>").option("--repo <name>").option("--force").action(
|
|
4876
|
-
(opts) => void withWorkspace(opts.workspace, async (db, workspaceId) => {
|
|
5012
|
+
(opts) => void withWorkspace(opts.workspace, async (db, workspaceId, _root, config) => {
|
|
4877
5013
|
const r = await indexWorkspace(db, workspaceId, {
|
|
4878
5014
|
repo: opts.repo,
|
|
4879
|
-
force: Boolean(opts.force)
|
|
5015
|
+
force: Boolean(opts.force),
|
|
5016
|
+
eventEnvironmentKeys: config.eventEnvironmentKeys
|
|
4880
5017
|
});
|
|
4881
5018
|
const outcome = indexCommandOutcome(r);
|
|
4882
5019
|
writeStdout(outcome.stdout);
|
|
@@ -4891,7 +5028,7 @@ function registerLinkCommand(program) {
|
|
|
4891
5028
|
const upgradeWarnings = linkUpgradeWarnings(db, workspaceId);
|
|
4892
5029
|
writeStdout(
|
|
4893
5030
|
`${upgradeWarnings.length ? `Warnings: ${upgradeWarnings.map((item) => String(item.code)).join(", ")}. Run service-flow doctor --strict for remediation.
|
|
4894
|
-
` : ""}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
|
|
5031
|
+
` : ""}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
|
|
4895
5032
|
`
|
|
4896
5033
|
);
|
|
4897
5034
|
}).catch(fail)
|
|
@@ -4901,18 +5038,20 @@ function registerTraceCommand(program) {
|
|
|
4901
5038
|
program.command("trace").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--handler <name>").option("--depth <n>", "trace depth", "25").addOption(traceFormatOption()).option("--include-external").option("--include-db").option("--include-async").option("--implementation-repo <name>").option("--implementation-hint <scope>", "scoped implementation hint", collect, []).option("--var <key=value>", "dynamic variable", collect, []).option("--dynamic-mode <mode>", "strict|candidates|infer", "strict").option("--max-dynamic-candidates <n>", "maximum dynamic candidates to show", "5").action((opts) => void runTraceCommand(opts).catch(fail));
|
|
4902
5039
|
}
|
|
4903
5040
|
function listRepositoriesCommand(opts) {
|
|
4904
|
-
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) =>
|
|
4905
|
-
|
|
5041
|
+
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
5042
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
5043
|
+
writeStdout(renderJson(
|
|
4906
5044
|
listRepositories(db, workspaceId).map((repo) => ({
|
|
4907
5045
|
name: repo.name,
|
|
4908
5046
|
kind: repo.kind,
|
|
4909
5047
|
packageName: repo.package_name
|
|
4910
5048
|
}))
|
|
4911
|
-
)
|
|
4912
|
-
)
|
|
5049
|
+
));
|
|
5050
|
+
});
|
|
4913
5051
|
}
|
|
4914
5052
|
function listServicesCommand(opts) {
|
|
4915
5053
|
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
5054
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
4916
5055
|
const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
|
|
4917
5056
|
if (selection.diagnostic) {
|
|
4918
5057
|
writeStdout(renderJson([selection.diagnostic]));
|
|
@@ -4927,6 +5066,7 @@ function listServicesCommand(opts) {
|
|
|
4927
5066
|
}
|
|
4928
5067
|
function listOperationsCommand(opts) {
|
|
4929
5068
|
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
5069
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
4930
5070
|
const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
|
|
4931
5071
|
if (selection.diagnostic) {
|
|
4932
5072
|
writeStdout(renderJson([selection.diagnostic]));
|
|
@@ -4947,6 +5087,7 @@ function listOperationsCommand(opts) {
|
|
|
4947
5087
|
}
|
|
4948
5088
|
function listCallsCommand(opts) {
|
|
4949
5089
|
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
5090
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
4950
5091
|
const selection = opts.repo ? selectRepository(db, opts.repo, workspaceId) : {};
|
|
4951
5092
|
if (selection.diagnostic) {
|
|
4952
5093
|
writeStdout(renderJson([selection.diagnostic]));
|
|
@@ -4987,20 +5128,29 @@ function registerGraphCommand(program) {
|
|
|
4987
5128
|
}
|
|
4988
5129
|
function inspectRepositoryCommand(name, opts) {
|
|
4989
5130
|
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
5131
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
4990
5132
|
const selection = selectRepository(db, name, workspaceId);
|
|
4991
5133
|
writeStdout(renderJson(
|
|
4992
|
-
selection.repo
|
|
5134
|
+
selection.repo ? projectRepositoryInspection(selection.repo) : selection.diagnostic ?? { error: "repo not found" }
|
|
4993
5135
|
));
|
|
4994
5136
|
});
|
|
4995
5137
|
}
|
|
4996
5138
|
function inspectOperationCommand(selector, opts) {
|
|
4997
5139
|
return withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
5140
|
+
if (writeLifecycleBlock(db, workspaceId)) return;
|
|
4998
5141
|
const rows = db.prepare(
|
|
4999
5142
|
"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=?)"
|
|
5000
5143
|
).all(workspaceId, selector, selector);
|
|
5001
5144
|
writeStdout(renderJson(rows));
|
|
5002
5145
|
});
|
|
5003
5146
|
}
|
|
5147
|
+
function writeLifecycleBlock(db, workspaceId) {
|
|
5148
|
+
const diagnostic = factLifecycleDiagnostic(db, workspaceId);
|
|
5149
|
+
if (!diagnostic) return false;
|
|
5150
|
+
writeStdout(renderJson([diagnostic]));
|
|
5151
|
+
process.exitCode = 1;
|
|
5152
|
+
return true;
|
|
5153
|
+
}
|
|
5004
5154
|
function registerInspectCommands(program) {
|
|
5005
5155
|
const inspect = program.command("inspect");
|
|
5006
5156
|
inspect.command("repo").argument("<name>").option("--workspace <path>").action(
|