@saptools/service-flow 0.1.50 → 0.1.52
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 +14 -0
- package/README.md +16 -3
- package/TECHNICAL-NOTE.md +10 -1
- package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +318 -510
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +59 -17
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +97 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +120 -22
- package/src/db/schema.ts +7 -2
- package/src/index.ts +1 -1
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +22 -2
- package/src/linker/service-resolver.ts +15 -4
- package/src/output/table-output.ts +56 -3
- package/src/parsers/cds-parser.ts +8 -2
- package/src/parsers/decorator-parser.ts +382 -48
- package/src/parsers/handler-registration-parser.ts +38 -21
- package/src/parsers/imported-wrapper-parser.ts +18 -5
- package/src/parsers/outbound-call-parser.ts +25 -8
- package/src/parsers/package-json-parser.ts +36 -11
- package/src/parsers/service-binding-parser-helpers.ts +8 -1
- package/src/parsers/service-binding-parser.ts +6 -3
- package/src/parsers/symbol-parser.ts +13 -3
- package/src/parsers/ts-project.ts +54 -0
- package/src/trace/000-dynamic-target-types.ts +84 -0
- package/src/trace/001-dynamic-identity.ts +280 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +82 -0
- package/src/trace/dynamic-branches.ts +45 -0
- package/src/trace/dynamic-targets.ts +654 -0
- package/src/trace/evidence.ts +391 -22
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +101 -94
- package/src/types.ts +39 -1
- package/dist/chunk-52OUS3MO.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
1
3
|
type CallType = 'remote_action' | 'remote_query' | 'remote_entity_read' | 'remote_entity_mutation' | 'remote_entity_delete' | 'remote_entity_media' | 'remote_entity_candidate' | 'local_db_query' | 'external_http' | 'async_emit' | 'async_subscribe' | 'local_service_call' | 'unknown';
|
|
2
4
|
interface DiscoveredRepository {
|
|
3
5
|
name: string;
|
|
@@ -55,17 +57,36 @@ interface HandlerClassFact {
|
|
|
55
57
|
sourceFile: string;
|
|
56
58
|
sourceLine: number;
|
|
57
59
|
methods: HandlerMethodFact[];
|
|
60
|
+
hasHandlerDecorator?: boolean;
|
|
61
|
+
classDecoratorNames?: string[];
|
|
62
|
+
observedDecoratorNames?: string[];
|
|
63
|
+
unsupportedDecoratorNames?: string[];
|
|
58
64
|
}
|
|
65
|
+
type HandlerMethodKind = 'operation' | 'entity_lifecycle' | 'event' | 'unsupported_lifecycle' | 'unsupported_decorator';
|
|
66
|
+
type HandlerLifecyclePhase = 'on' | 'before' | 'after';
|
|
67
|
+
type HandlerLifecycleEvent = 'CREATE' | 'READ' | 'UPDATE' | 'DELETE';
|
|
59
68
|
interface HandlerMethodFact {
|
|
60
69
|
methodName: string;
|
|
61
70
|
decoratorKind: string;
|
|
62
71
|
decoratorValue?: string;
|
|
63
72
|
decoratorRawExpression: string;
|
|
73
|
+
handlerKind?: HandlerMethodKind;
|
|
74
|
+
executable?: boolean;
|
|
75
|
+
lifecyclePhase?: HandlerLifecyclePhase;
|
|
76
|
+
lifecycleEvent?: HandlerLifecycleEvent;
|
|
64
77
|
decoratorResolution: {
|
|
65
78
|
rawExpression: string;
|
|
79
|
+
decoratorExpression?: string;
|
|
80
|
+
argumentExpression?: string;
|
|
81
|
+
resolvedDecoratorKind?: string;
|
|
82
|
+
decoratorImportSource?: string;
|
|
66
83
|
resolvedValue?: string;
|
|
67
|
-
resolutionKind: 'literal' | 'const_identifier' | 'enum_member' | 'const_object_property' | 'generated_constant_name' | 'unresolved';
|
|
84
|
+
resolutionKind: 'literal' | 'const_identifier' | 'enum_member' | 'const_object_property' | 'generated_constant_name' | 'lifecycle_implicit' | 'unresolved';
|
|
68
85
|
unresolvedReason?: string;
|
|
86
|
+
handlerKind?: HandlerMethodKind;
|
|
87
|
+
executable?: boolean;
|
|
88
|
+
lifecyclePhase?: HandlerLifecyclePhase;
|
|
89
|
+
lifecycleEvent?: HandlerLifecycleEvent;
|
|
69
90
|
};
|
|
70
91
|
sourceFile: string;
|
|
71
92
|
sourceLine: number;
|
|
@@ -135,6 +156,19 @@ interface ImplementationHint {
|
|
|
135
156
|
candidateFamily?: string;
|
|
136
157
|
implementationRepo: string;
|
|
137
158
|
}
|
|
159
|
+
type DynamicMode = 'strict' | 'candidates' | 'infer';
|
|
160
|
+
interface TraceOptions {
|
|
161
|
+
depth: number;
|
|
162
|
+
workspaceId?: number;
|
|
163
|
+
vars?: Record<string, string>;
|
|
164
|
+
includeExternal?: boolean;
|
|
165
|
+
includeDb?: boolean;
|
|
166
|
+
includeAsync?: boolean;
|
|
167
|
+
implementationRepo?: string;
|
|
168
|
+
implementationHints?: ImplementationHint[];
|
|
169
|
+
dynamicMode?: DynamicMode;
|
|
170
|
+
maxDynamicCandidates?: number;
|
|
171
|
+
}
|
|
138
172
|
interface TraceEdge {
|
|
139
173
|
step: number;
|
|
140
174
|
type: string;
|
|
@@ -153,17 +187,33 @@ interface TraceResult {
|
|
|
153
187
|
|
|
154
188
|
declare function discoverRepositories(rootPath: string, ignore: readonly string[]): Promise<DiscoveredRepository[]>;
|
|
155
189
|
|
|
156
|
-
|
|
190
|
+
interface ParsePackageJsonOptions {
|
|
191
|
+
strict?: boolean;
|
|
192
|
+
allowMissing?: boolean;
|
|
193
|
+
}
|
|
194
|
+
declare function parsePackageJson(repoPath: string, options?: ParsePackageJsonOptions): Promise<PackageFacts>;
|
|
195
|
+
|
|
196
|
+
interface SourceFileSnapshot {
|
|
197
|
+
repoPath: string;
|
|
198
|
+
filePath: string;
|
|
199
|
+
text: string;
|
|
200
|
+
sizeBytes: number;
|
|
201
|
+
sourceFile: () => ts.SourceFile;
|
|
202
|
+
}
|
|
203
|
+
interface RepositorySourceContext {
|
|
204
|
+
get: (filePath: string) => SourceFileSnapshot | undefined;
|
|
205
|
+
entries: () => SourceFileSnapshot[];
|
|
206
|
+
}
|
|
157
207
|
|
|
158
|
-
declare function parseCdsFile(repoPath: string, filePath: string): Promise<CdsServiceFact[]>;
|
|
208
|
+
declare function parseCdsFile(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<CdsServiceFact[]>;
|
|
159
209
|
|
|
160
|
-
declare function parseDecorators(repoPath: string, filePath: string): Promise<HandlerClassFact[]>;
|
|
210
|
+
declare function parseDecorators(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<HandlerClassFact[]>;
|
|
161
211
|
|
|
162
|
-
declare function parseHandlerRegistrations(repoPath: string, filePath: string): Promise<HandlerRegistrationFact[]>;
|
|
212
|
+
declare function parseHandlerRegistrations(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<HandlerRegistrationFact[]>;
|
|
163
213
|
|
|
164
|
-
declare function parseServiceBindings(repoPath: string, filePath: string): Promise<ServiceBindingFact[]>;
|
|
214
|
+
declare function parseServiceBindings(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<ServiceBindingFact[]>;
|
|
165
215
|
|
|
166
|
-
declare function parseOutboundCalls(repoPath: string, filePath: string): Promise<OutboundCallFact[]>;
|
|
216
|
+
declare function parseOutboundCalls(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<OutboundCallFact[]>;
|
|
167
217
|
|
|
168
218
|
declare function parseGeneratedConstants(repoPath: string, filePath: string): Promise<GeneratedConstantFact[]>;
|
|
169
219
|
|
|
@@ -213,19 +263,11 @@ declare function applyVariables(template: string | undefined, vars: Record<strin
|
|
|
213
263
|
declare function extractPlaceholders(template: string | undefined): string[];
|
|
214
264
|
declare function substituteVariables(template: string | undefined, vars: Record<string, string>): RuntimeSubstitution;
|
|
215
265
|
|
|
216
|
-
declare function trace(db: Db, start: TraceStart, options:
|
|
217
|
-
depth: number;
|
|
218
|
-
vars?: Record<string, string>;
|
|
219
|
-
includeExternal?: boolean;
|
|
220
|
-
includeDb?: boolean;
|
|
221
|
-
includeAsync?: boolean;
|
|
222
|
-
implementationRepo?: string;
|
|
223
|
-
implementationHints?: ImplementationHint[];
|
|
224
|
-
}): TraceResult;
|
|
266
|
+
declare function trace(db: Db, start: TraceStart, options: TraceOptions): TraceResult;
|
|
225
267
|
|
|
226
268
|
declare function parseImplementationHint(value: string): ImplementationHint;
|
|
227
269
|
|
|
228
270
|
declare function redactText(text: string): string;
|
|
229
271
|
declare function redactValue(value: unknown): unknown;
|
|
230
272
|
|
|
231
|
-
export { type ImplementationHint, type RuntimeSubstitution, applyVariables, discoverRepositories, extractPlaceholders, linkWorkspace, parseCdsFile, parseDecorators, parseGeneratedConstants, parseHandlerRegistrations, parseImplementationHint, parseOutboundCalls, parsePackageJson, parseServiceBindings, redactText, redactValue, substituteVariables, trace };
|
|
273
|
+
export { type DynamicMode, type ImplementationHint, type RuntimeSubstitution, type TraceOptions, applyVariables, discoverRepositories, extractPlaceholders, linkWorkspace, parseCdsFile, parseDecorators, parseGeneratedConstants, parseHandlerRegistrations, parseImplementationHint, parseOutboundCalls, parsePackageJson, parseServiceBindings, redactText, redactValue, substituteVariables, trace };
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { WorkspaceConfig } from '../config/workspace-config.js';
|
|
4
|
+
import { openDatabase } from '../db/connection.js';
|
|
5
|
+
import { getWorkspace, upsertWorkspace } from '../db/repositories.js';
|
|
6
|
+
import { claimIndexRun } from '../indexer/workspace-indexer.js';
|
|
7
|
+
import { errorMessage } from '../utils/diagnostics.js';
|
|
8
|
+
|
|
9
|
+
type CleanConfig = Pick<WorkspaceConfig, 'dbPath' | 'rootPath'>;
|
|
10
|
+
|
|
11
|
+
export async function cleanWorkspaceState(
|
|
12
|
+
config: CleanConfig,
|
|
13
|
+
dbOnly: boolean,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const dbDir = path.resolve(path.dirname(config.dbPath));
|
|
16
|
+
if (!dbOnly) await assertOwnedStateDirectory(dbDir, config.rootPath);
|
|
17
|
+
const runId = claimCleanWriter(config);
|
|
18
|
+
try {
|
|
19
|
+
if (dbOnly) await removeDatabaseFiles(config.dbPath);
|
|
20
|
+
else await fs.rm(dbDir, { recursive: true, force: true });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
await markCleanClaimFailed(config.dbPath, runId, error);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function claimCleanWriter(config: CleanConfig): number {
|
|
28
|
+
const db = openDatabase(config.dbPath);
|
|
29
|
+
try {
|
|
30
|
+
const workspaceId = getWorkspace(db, config.rootPath)?.id
|
|
31
|
+
?? upsertWorkspace(db, config.rootPath, config.dbPath);
|
|
32
|
+
return claimIndexRun(db, workspaceId, 0);
|
|
33
|
+
} finally {
|
|
34
|
+
db.close();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function assertOwnedStateDirectory(
|
|
39
|
+
dbDir: string,
|
|
40
|
+
rootPath: string,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const marker = path.join(dbDir, '.service-flow-state');
|
|
43
|
+
const dangerous = new Set([
|
|
44
|
+
path.parse(dbDir).root,
|
|
45
|
+
'/tmp',
|
|
46
|
+
process.env.HOME ? path.resolve(process.env.HOME) : '',
|
|
47
|
+
path.resolve(rootPath),
|
|
48
|
+
]);
|
|
49
|
+
const ownsState = await fs.stat(marker)
|
|
50
|
+
.then((stat) => stat.isFile())
|
|
51
|
+
.catch(() => false);
|
|
52
|
+
if (!ownsState || dangerous.has(dbDir))
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function removeDatabaseFiles(dbPath: string): Promise<void> {
|
|
59
|
+
for (const suffix of ['-wal', '-shm', '-journal'])
|
|
60
|
+
await fs.rm(`${dbPath}${suffix}`, { force: true });
|
|
61
|
+
await fs.rm(dbPath, { force: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function markCleanClaimFailed(
|
|
65
|
+
dbPath: string,
|
|
66
|
+
runId: number,
|
|
67
|
+
error: unknown,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const exists = await fs.stat(dbPath).then(() => true).catch(() => false);
|
|
70
|
+
if (!exists) return;
|
|
71
|
+
const db = openDatabase(dbPath);
|
|
72
|
+
try {
|
|
73
|
+
db.prepare(`UPDATE index_runs SET finished_at=?,status='failed',
|
|
74
|
+
error_message=? WHERE id=? AND status='running'`).run(
|
|
75
|
+
new Date().toISOString(),
|
|
76
|
+
`Clean failed after writer claim: ${errorMessage(error)}`,
|
|
77
|
+
runId,
|
|
78
|
+
);
|
|
79
|
+
} finally {
|
|
80
|
+
db.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
2
|
import { DEFAULT_IGNORES } from './config/defaults.js';
|
|
5
3
|
import {
|
|
6
4
|
createWorkspaceConfig,
|
|
7
5
|
loadWorkspaceConfig,
|
|
8
6
|
saveWorkspaceConfig,
|
|
9
7
|
} from './config/workspace-config.js';
|
|
10
|
-
import { openDatabase, openReadOnlyDatabase } from './db/connection.js';
|
|
8
|
+
import { openDatabase, openReadOnlyDatabase, type Db } from './db/connection.js';
|
|
11
9
|
import {
|
|
12
10
|
getWorkspace,
|
|
13
11
|
listRepositories,
|
|
14
|
-
|
|
12
|
+
reposByName,
|
|
13
|
+
type RepoRow,
|
|
15
14
|
upsertRepository,
|
|
16
15
|
upsertWorkspace,
|
|
17
16
|
} from './db/repositories.js';
|
|
@@ -22,13 +21,18 @@ import { indexWorkspace } from './indexer/workspace-indexer.js';
|
|
|
22
21
|
import { linkWorkspace } from './linker/cross-repo-linker.js';
|
|
23
22
|
import { doctorDiagnostics, linkUpgradeWarnings } from './cli/doctor.js';
|
|
24
23
|
import { trace } from './trace/trace-engine.js';
|
|
25
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
parseVars,
|
|
26
|
+
selectorRepoAmbiguousDiagnostic,
|
|
27
|
+
} from './trace/selectors.js';
|
|
26
28
|
import { parseImplementationHint } from './trace/implementation-hints.js';
|
|
27
29
|
import { renderTraceTable } from './output/table-output.js';
|
|
28
30
|
import { renderTraceJson, renderJson } from './output/json-output.js';
|
|
29
31
|
import { renderDoctorDiagnostics } from './output/doctor-output.js';
|
|
30
32
|
import { renderMermaid } from './output/mermaid-output.js';
|
|
31
33
|
import { VERSION } from './version.js';
|
|
34
|
+
import type { DynamicMode } from './types.js';
|
|
35
|
+
import { cleanWorkspaceState } from './cli/000-clean.js';
|
|
32
36
|
async function init(
|
|
33
37
|
workspace: string,
|
|
34
38
|
options: { db?: string; ignore?: string[] },
|
|
@@ -91,6 +95,30 @@ async function withReadOnlyWorkspace<T>(
|
|
|
91
95
|
db.close();
|
|
92
96
|
}
|
|
93
97
|
}
|
|
98
|
+
function selectRepository(db: Db, selector: string, workspaceId?: number): {
|
|
99
|
+
repo?: RepoRow;
|
|
100
|
+
diagnostic?: Record<string, unknown>;
|
|
101
|
+
} {
|
|
102
|
+
const candidates = reposByName(db, selector, workspaceId);
|
|
103
|
+
if (candidates.length === 1) return { repo: candidates[0] };
|
|
104
|
+
if (candidates.length === 0) return {
|
|
105
|
+
diagnostic: {
|
|
106
|
+
severity: 'warning',
|
|
107
|
+
code: 'selector_repo_not_found',
|
|
108
|
+
message: `Repository selector not found: ${selector}`,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
diagnostic: selectorRepoAmbiguousDiagnostic(
|
|
113
|
+
selector,
|
|
114
|
+
candidates.map((repo) => ({
|
|
115
|
+
id: repo.id,
|
|
116
|
+
name: repo.name,
|
|
117
|
+
packageName: repo.package_name ?? undefined,
|
|
118
|
+
})),
|
|
119
|
+
),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
94
122
|
export function createProgram(): Command {
|
|
95
123
|
const program = new Command();
|
|
96
124
|
program
|
|
@@ -155,6 +183,8 @@ export function createProgram(): Command {
|
|
|
155
183
|
.option('--implementation-repo <name>')
|
|
156
184
|
.option('--implementation-hint <scope>', 'scoped implementation hint', collect, [])
|
|
157
185
|
.option('--var <key=value>', 'dynamic variable', collect, [])
|
|
186
|
+
.option('--dynamic-mode <mode>', 'strict|candidates|infer', 'strict')
|
|
187
|
+
.option('--max-dynamic-candidates <n>', 'maximum dynamic candidates to show', '5')
|
|
158
188
|
.action(
|
|
159
189
|
(opts: {
|
|
160
190
|
workspace?: string;
|
|
@@ -171,8 +201,10 @@ export function createProgram(): Command {
|
|
|
171
201
|
implementationRepo?: string;
|
|
172
202
|
implementationHint: string[];
|
|
173
203
|
var: string[];
|
|
204
|
+
dynamicMode: string;
|
|
205
|
+
maxDynamicCandidates: string;
|
|
174
206
|
}) =>
|
|
175
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
207
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
176
208
|
const result = trace(
|
|
177
209
|
db,
|
|
178
210
|
{
|
|
@@ -184,12 +216,15 @@ export function createProgram(): Command {
|
|
|
184
216
|
},
|
|
185
217
|
{
|
|
186
218
|
depth: Number(opts.depth),
|
|
219
|
+
workspaceId,
|
|
187
220
|
vars: parseVars(opts.var),
|
|
188
221
|
includeExternal: Boolean(opts.includeExternal),
|
|
189
222
|
includeDb: Boolean(opts.includeDb),
|
|
190
223
|
includeAsync: Boolean(opts.includeAsync),
|
|
191
224
|
implementationRepo: opts.implementationRepo,
|
|
192
225
|
implementationHints: opts.implementationHint.map(parseImplementationHint),
|
|
226
|
+
dynamicMode: parseDynamicMode(opts.dynamicMode),
|
|
227
|
+
maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5),
|
|
193
228
|
},
|
|
194
229
|
);
|
|
195
230
|
process.stdout.write(
|
|
@@ -207,10 +242,10 @@ export function createProgram(): Command {
|
|
|
207
242
|
.option('--workspace <path>')
|
|
208
243
|
.action(
|
|
209
244
|
(opts: { workspace?: string }) =>
|
|
210
|
-
void withReadOnlyWorkspace(opts.workspace, (db) =>
|
|
245
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) =>
|
|
211
246
|
process.stdout.write(
|
|
212
247
|
renderJson(
|
|
213
|
-
listRepositories(db).map((r) => ({
|
|
248
|
+
listRepositories(db, workspaceId).map((r) => ({
|
|
214
249
|
name: r.name,
|
|
215
250
|
kind: r.kind,
|
|
216
251
|
packageName: r.package_name,
|
|
@@ -225,17 +260,20 @@ export function createProgram(): Command {
|
|
|
225
260
|
.option('--repo <name>')
|
|
226
261
|
.action(
|
|
227
262
|
(opts: { workspace?: string; repo?: string }) =>
|
|
228
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
263
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
264
|
+
const selection = opts.repo
|
|
265
|
+
? selectRepository(db, opts.repo, workspaceId)
|
|
266
|
+
: {};
|
|
267
|
+
if (selection.diagnostic) {
|
|
268
|
+
process.stdout.write(renderJson([selection.diagnostic]));
|
|
232
269
|
return;
|
|
233
270
|
}
|
|
271
|
+
const repo = selection.repo;
|
|
234
272
|
const rows = db
|
|
235
273
|
.prepare(
|
|
236
|
-
'SELECT r.name repo,s.service_path servicePath,s.qualified_name qualifiedName FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path',
|
|
274
|
+
'SELECT r.name repo,s.service_path servicePath,s.qualified_name qualifiedName FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path',
|
|
237
275
|
)
|
|
238
|
-
.all(repo?.id, repo?.id);
|
|
276
|
+
.all(workspaceId, repo?.id, repo?.id);
|
|
239
277
|
process.stdout.write(renderJson(rows));
|
|
240
278
|
}).catch(fail),
|
|
241
279
|
);
|
|
@@ -246,17 +284,20 @@ export function createProgram(): Command {
|
|
|
246
284
|
.option('--service <path>')
|
|
247
285
|
.action(
|
|
248
286
|
(opts: { workspace?: string; repo?: string; service?: string }) =>
|
|
249
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
287
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
288
|
+
const selection = opts.repo
|
|
289
|
+
? selectRepository(db, opts.repo, workspaceId)
|
|
290
|
+
: {};
|
|
291
|
+
if (selection.diagnostic) {
|
|
292
|
+
process.stdout.write(renderJson([selection.diagnostic]));
|
|
253
293
|
return;
|
|
254
294
|
}
|
|
295
|
+
const repo = selection.repo;
|
|
255
296
|
const rows = db
|
|
256
297
|
.prepare(
|
|
257
|
-
'SELECT r.name repo,s.service_path servicePath,o.operation_name operation,o.operation_path path FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)',
|
|
298
|
+
'SELECT r.name repo,s.service_path servicePath,o.operation_name operation,o.operation_path path 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 (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)',
|
|
258
299
|
)
|
|
259
|
-
.all(repo?.id, repo?.id, opts.service, opts.service);
|
|
300
|
+
.all(workspaceId, repo?.id, repo?.id, opts.service, opts.service);
|
|
260
301
|
process.stdout.write(renderJson(rows));
|
|
261
302
|
}).catch(fail),
|
|
262
303
|
);
|
|
@@ -267,17 +308,21 @@ export function createProgram(): Command {
|
|
|
267
308
|
.option('--operation <name>')
|
|
268
309
|
.action(
|
|
269
310
|
(opts: { workspace?: string; repo?: string; operation?: string }) =>
|
|
270
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
311
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
312
|
+
const selection = opts.repo
|
|
313
|
+
? selectRepository(db, opts.repo, workspaceId)
|
|
314
|
+
: {};
|
|
315
|
+
if (selection.diagnostic) {
|
|
316
|
+
process.stdout.write(renderJson([selection.diagnostic]));
|
|
274
317
|
return;
|
|
275
318
|
}
|
|
319
|
+
const repo = selection.repo;
|
|
276
320
|
const rows = db
|
|
277
321
|
.prepare(
|
|
278
|
-
'SELECT r.name repo,c.call_type type,c.operation_path_expr path,c.source_file file,c.source_line line FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) AND (? IS NULL OR c.operation_path_expr=? OR c.operation_path_expr=? OR c.payload_summary LIKE ?)',
|
|
322
|
+
'SELECT r.name repo,c.call_type type,c.operation_path_expr path,c.source_file file,c.source_line line FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE r.workspace_id=? AND (? IS NULL OR c.repo_id=?) AND (? IS NULL OR c.operation_path_expr=? OR c.operation_path_expr=? OR c.payload_summary LIKE ?)',
|
|
279
323
|
)
|
|
280
324
|
.all(
|
|
325
|
+
workspaceId,
|
|
281
326
|
repo?.id,
|
|
282
327
|
repo?.id,
|
|
283
328
|
opts.operation,
|
|
@@ -299,6 +344,8 @@ export function createProgram(): Command {
|
|
|
299
344
|
.option('--implementation-repo <name>')
|
|
300
345
|
.option('--implementation-hint <scope>', 'scoped implementation hint', collect, [])
|
|
301
346
|
.option('--var <key=value>', 'dynamic variable', collect, [])
|
|
347
|
+
.option('--dynamic-mode <mode>', 'strict|candidates|infer', 'strict')
|
|
348
|
+
.option('--max-dynamic-candidates <n>', 'maximum dynamic candidates to show', '5')
|
|
302
349
|
.action(
|
|
303
350
|
(opts: {
|
|
304
351
|
workspace?: string;
|
|
@@ -310,8 +357,10 @@ export function createProgram(): Command {
|
|
|
310
357
|
implementationRepo?: string;
|
|
311
358
|
implementationHint: string[];
|
|
312
359
|
var: string[];
|
|
360
|
+
dynamicMode: string;
|
|
361
|
+
maxDynamicCandidates: string;
|
|
313
362
|
}) =>
|
|
314
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
363
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
315
364
|
const result = trace(
|
|
316
365
|
db,
|
|
317
366
|
{
|
|
@@ -322,12 +371,15 @@ export function createProgram(): Command {
|
|
|
322
371
|
},
|
|
323
372
|
{
|
|
324
373
|
depth: 100,
|
|
374
|
+
workspaceId,
|
|
325
375
|
includeAsync: true,
|
|
326
376
|
includeDb: true,
|
|
327
377
|
includeExternal: true,
|
|
328
378
|
vars: parseVars(opts.var),
|
|
329
379
|
implementationRepo: opts.implementationRepo,
|
|
330
380
|
implementationHints: opts.implementationHint.map(parseImplementationHint),
|
|
381
|
+
dynamicMode: parseDynamicMode(opts.dynamicMode),
|
|
382
|
+
maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5),
|
|
331
383
|
},
|
|
332
384
|
);
|
|
333
385
|
process.stdout.write(
|
|
@@ -344,11 +396,12 @@ export function createProgram(): Command {
|
|
|
344
396
|
.option('--workspace <path>')
|
|
345
397
|
.action(
|
|
346
398
|
(name: string, opts: { workspace?: string }) =>
|
|
347
|
-
void withReadOnlyWorkspace(opts.workspace, (db) =>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
399
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
400
|
+
const selection = selectRepository(db, name, workspaceId);
|
|
401
|
+
process.stdout.write(renderJson(
|
|
402
|
+
selection.repo ?? selection.diagnostic ?? { error: 'repo not found' },
|
|
403
|
+
));
|
|
404
|
+
}).catch(fail),
|
|
352
405
|
);
|
|
353
406
|
inspect
|
|
354
407
|
.command('operation')
|
|
@@ -356,12 +409,12 @@ export function createProgram(): Command {
|
|
|
356
409
|
.option('--workspace <path>')
|
|
357
410
|
.action(
|
|
358
411
|
(selector: string, opts: { workspace?: string }) =>
|
|
359
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
412
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
360
413
|
const rows = db
|
|
361
414
|
.prepare(
|
|
362
|
-
'SELECT
|
|
415
|
+
'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=?)',
|
|
363
416
|
)
|
|
364
|
-
.all(selector, selector);
|
|
417
|
+
.all(workspaceId, selector, selector);
|
|
365
418
|
process.stdout.write(renderJson(rows));
|
|
366
419
|
}).catch(fail),
|
|
367
420
|
);
|
|
@@ -386,29 +439,7 @@ export function createProgram(): Command {
|
|
|
386
439
|
(opts: { workspace?: string; dbOnly?: boolean }) =>
|
|
387
440
|
void (async () => {
|
|
388
441
|
const config = await loadWorkspaceConfig(opts.workspace);
|
|
389
|
-
|
|
390
|
-
const workspaceRoot = path.resolve(config.rootPath);
|
|
391
|
-
await fs.rm(config.dbPath, { force: true });
|
|
392
|
-
if (!opts.dbOnly) {
|
|
393
|
-
const marker = path.join(dbDir, '.service-flow-state');
|
|
394
|
-
const dangerous = new Set([
|
|
395
|
-
path.parse(dbDir).root,
|
|
396
|
-
'/tmp',
|
|
397
|
-
process.env.HOME ? path.resolve(process.env.HOME) : '',
|
|
398
|
-
workspaceRoot,
|
|
399
|
-
]);
|
|
400
|
-
let ownsState: boolean;
|
|
401
|
-
try {
|
|
402
|
-
ownsState = (await fs.stat(marker)).isFile();
|
|
403
|
-
} catch {
|
|
404
|
-
ownsState = false;
|
|
405
|
-
}
|
|
406
|
-
if (!ownsState || dangerous.has(dbDir))
|
|
407
|
-
throw new Error(
|
|
408
|
-
`Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`,
|
|
409
|
-
);
|
|
410
|
-
await fs.rm(dbDir, { recursive: true, force: true });
|
|
411
|
-
}
|
|
442
|
+
await cleanWorkspaceState(config, Boolean(opts.dbOnly));
|
|
412
443
|
process.stdout.write('Cleaned service-flow state\n');
|
|
413
444
|
})().catch(fail),
|
|
414
445
|
);
|
|
@@ -418,6 +449,15 @@ function collect(value: string, previous: string[]): string[] {
|
|
|
418
449
|
previous.push(value);
|
|
419
450
|
return previous;
|
|
420
451
|
}
|
|
452
|
+
function parseDynamicMode(value: string | undefined): DynamicMode {
|
|
453
|
+
if (value === undefined || value === 'strict') return 'strict';
|
|
454
|
+
if (value === 'candidates' || value === 'infer') return value;
|
|
455
|
+
throw new Error(`Invalid --dynamic-mode ${value}; expected strict, candidates, or infer`);
|
|
456
|
+
}
|
|
457
|
+
function parsePositiveInteger(value: string | undefined, fallback: number): number {
|
|
458
|
+
const parsed = Number(value);
|
|
459
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
460
|
+
}
|
|
421
461
|
function fail(error: unknown): void {
|
|
422
462
|
process.stderr.write(
|
|
423
463
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
package/src/db/connection.ts
CHANGED
|
@@ -101,8 +101,8 @@ export function openDatabase(dbPath: string, options: OpenDatabaseOptions = {}):
|
|
|
101
101
|
},
|
|
102
102
|
transaction<T>(fn: () => T): T {
|
|
103
103
|
if (inTransaction) return fn();
|
|
104
|
-
inTransaction = true;
|
|
105
104
|
native.exec('BEGIN IMMEDIATE');
|
|
105
|
+
inTransaction = true;
|
|
106
106
|
try {
|
|
107
107
|
const result = fn();
|
|
108
108
|
native.exec('COMMIT');
|
package/src/db/migrations.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Db } from './connection.js';
|
|
2
|
-
import {
|
|
3
|
-
const CURRENT_SCHEMA_VERSION =
|
|
2
|
+
import { schemaIndexesSql, schemaTablesSql } from './schema.js';
|
|
3
|
+
const CURRENT_SCHEMA_VERSION = 11;
|
|
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 '{}'" },
|
|
@@ -57,6 +57,7 @@ const columns: Record<string, Array<{ name: string; ddl: string }>> = {
|
|
|
57
57
|
],
|
|
58
58
|
index_runs: [
|
|
59
59
|
{ name: 'error_message', ddl: 'ALTER TABLE index_runs ADD COLUMN error_message TEXT' },
|
|
60
|
+
{ name: 'owner_pid', ddl: 'ALTER TABLE index_runs ADD COLUMN owner_pid INTEGER' },
|
|
60
61
|
],
|
|
61
62
|
};
|
|
62
63
|
function hasColumn(db: Db, table: string, column: string): boolean {
|
|
@@ -81,8 +82,9 @@ export function migrate(db: Db): void {
|
|
|
81
82
|
db.transaction(() => {
|
|
82
83
|
const version = userVersion(db);
|
|
83
84
|
if (version > CURRENT_SCHEMA_VERSION) throw new Error(`Unsupported future service-flow schema version ${version}`);
|
|
84
|
-
db.exec(
|
|
85
|
+
db.exec(schemaTablesSql);
|
|
85
86
|
addMissingColumns(db);
|
|
87
|
+
db.exec(schemaIndexesSql);
|
|
86
88
|
normalizeLegacyStatus(db);
|
|
87
89
|
const violations = db.pragma('foreign_key_check');
|
|
88
90
|
if (violations.length > 0) throw new Error('SQLite foreign_key_check failed during migration');
|