@saptools/service-flow 0.1.54 โ†’ 0.1.55

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.55
4
+
5
+ - Indexed directly awaited CAP query-builder statements as local database calls, preserving exact builder-root evidence, fluent statement offsets, dynamic-entity warnings, method ownership, and the existing single `cds.run(...)` fact.
6
+ - Made normal CLI stdout pipeline-safe: an early downstream pipe close stops further output without an unhandled `EPIPE` stack trace, while unrelated stream failures remain non-zero diagnostics.
7
+
3
8
  ## 0.1.54
4
9
 
5
10
  - Made contextual dynamic trace diagnostics runtime-current after `--var` substitution while retaining labelled pre-substitution binding state and distinct structural blockers.
package/README.md CHANGED
@@ -156,6 +156,10 @@ service-flow link --workspace /path/to/workspace --force
156
156
  | `--workspace <path>` | Workspace to link |
157
157
  | `--force` | Accepted for workflow symmetry; linking always rebuilds graph edges |
158
158
 
159
+ ### Pipeline-safe output
160
+
161
+ Normal command output is safe to pipe to a Unix consumer that intentionally stops reading early. A closed stdout pipe stops further output without an unhandled `EPIPE` stack trace; unrelated stdout failures still use the normal stderr diagnostic and non-zero exit outcome. Complete JSON, table, and Mermaid output bytes are unchanged.
162
+
159
163
  ### ๐Ÿงต `service-flow trace`
160
164
 
161
165
  Trace one starting point and render table, JSON, or Mermaid output. Trace now
@@ -170,7 +174,7 @@ Local CAP calls through `cds.services.<Service>.<operation>()`, bracket service
170
174
 
171
175
  Conservative local symbol traversal intentionally excludes decorators, built-ins such as `JSON.parse`, collection methods, third-party APIs, and arbitrary property chains unless the callee can plausibly resolve to an indexed local symbol. Named export lists such as `export { loadTemplate as publicLoadTemplate }` are indexed with the public exported name so relative imports can resolve. One-level object-literal helpers are indexed as symbols named like `cacheHelper.getConfiguration`; nested object literals are not yet expanded beyond the first helper level. `parseGeneratedConstants` remains a public low-level parser export for callers that need it, but generated constants are not persisted as graph facts in this patch; linking uses the deterministic decorator normalizer described above.
172
176
  JSON output includes typed nodes for calls, operations, database entities,
173
- external destinations, and unresolved/dynamic candidates when edges exist. Chained CAP DB queries inside `cds.run(...)` are parsed with TypeScript AST evidence for `SELECT`, `INSERT`, `UPDATE`, and `DELETE` forms. When the query target is genuinely dynamic, graph status remains terminal and JSON retains `parserWarning` evidence, while table and Mermaid render the target as `Entity: unknown` rather than a numeric call id.
177
+ external destinations, and unresolved/dynamic candidates when edges exist. Chained CAP DB queries inside `cds.run(...)` and directly awaited supported builders are parsed with TypeScript AST evidence for `SELECT`, `INSERT`, `UPSERT`, `UPDATE`, and `DELETE` forms. Direct builders must have a recognized CAP root; unrelated methods named `from`, `where`, or `set` are not promoted to database facts. When the query target is genuinely dynamic, graph status remains terminal and JSON retains `parserWarning` evidence, while table and Mermaid render the target as `Entity: unknown` rather than a numeric call id.
174
178
 
175
179
  ```bash
176
180
  service-flow trace --workspace /path/to/workspace --repo facade-service --operation doWork
@@ -270,9 +274,10 @@ External HTTP facts use semantic terminal nodes instead of outbound-call row ids
270
274
  a concrete client. Trace evidence includes the caller variable, returned
271
275
  property, imported helper, source file, exported symbol, placeholders, and
272
276
  transaction alias steps.
273
- - `SELECT.one.from(Entity)`, `SELECT.from(Entity)`, `INSERT.into(Entity)`,
274
- `UPDATE(Entity)`, and `DELETE.from(Entity)` are indexed as local database
275
- query entities when statically knowable.
277
+ - Direct awaited `SELECT.one.from(Entity)`, `SELECT.from(Entity)`,
278
+ `INSERT.into(Entity)`, `UPSERT.into(Entity)`, `UPDATE(Entity)`, and
279
+ `DELETE.from(Entity)` statements, plus equivalent `cds.run(...)` queries,
280
+ are indexed as local database query entities when statically knowable.
276
281
  - `doctor` reports silent quality problems such as services without operations,
277
282
  handler repositories without CDS service facts, and an empty search index.
278
283
 
@@ -336,7 +341,7 @@ service-flow clean --workspace /path/to/workspace
336
341
  | Handler facts | `cds-routing-handlers` decorators, handler classes/methods, server registrations |
337
342
  | Service bindings | `cds.connect.to("alias")`, aliases from `package.json#cds.requires`, destination/service path expressions |
338
343
  | Outbound calls | `remote.send({ method, path })`, `remote.send({ query })`, `cds.services.Service.operation()`, service wrapper calls |
339
- | Local data access | `cds.run(SELECT...)` and local entity query evidence |
344
+ | Local data access | direct awaited CAP query builders, `cds.run(SELECT...)`, and local entity query evidence |
340
345
  | Async channels | Event Mesh-style `emit`, `publish`, and `on` facts |
341
346
  | External calls | Cloud SDK-style HTTP/destination calls and external edge evidence |
342
347
  | Generated constants | low-level `parseGeneratedConstants` parser output for integrations; not persisted as first-class graph facts in this patch |
package/TECHNICAL-NOTE.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Service Flow Resolution Notes
2
2
 
3
+ ## 0.1.55 direct query-builder and stdout notes
4
+
5
+ - Direct, awaited CAP builders now create one `local_db_query` fact when their AST root is a supported `SELECT`, `INSERT`, `UPSERT`, `UPDATE`, or `DELETE` form. The parser follows fluent continuations and transparent TypeScript wrappers, records the direct dispatch marker plus root and statement offsets, and keeps ordinary member-name lookalikes out of database facts. A builder nested in `cds.run(...)` remains one wrapper-dispatched fact.
6
+ - Static entities retain high confidence. Dynamic entity expressions retain the existing terminal unknown-entity behavior and parser warning; neither form evaluates application expressions. Fact insertion continues to use the logical statement location, so the narrowest executable symbol owns lifecycle and operation-method queries independently.
7
+ - CLI command output uses one shared stdout policy. `EPIPE` from an early-closing Unix consumer marks output complete and suppresses later writes. Other stdout errors are reported through the existing failure path, so valid full JSON, table, and Mermaid bytes are unchanged and genuine failures still exit non-zero.
8
+
3
9
  ## 0.1.54 runtime-current diagnostics and selected handler notes
4
10
 
5
11
  - Contextual service-binding resolution now emits typed local trace state instead of passing a message string as control flow. `dynamic_missing`, `ambiguous_binding`, `ambiguous_operation`, `no_matching_operation`, and other conservative blockers remain distinguishable; `contextualPreSubstitutionState` preserves the historical attempt without changing a persisted graph row.
@@ -3045,19 +3045,105 @@ function variableInitializers(source) {
3045
3045
  }
3046
3046
  return initializers;
3047
3047
  }
3048
+ function unwrapQueryExpression(expr) {
3049
+ if (ts8.isParenthesizedExpression(expr) || ts8.isAwaitExpression(expr) || ts8.isAsExpression(expr) || ts8.isTypeAssertionExpression(expr) || ts8.isNonNullExpression(expr) || ts8.isSatisfiesExpression(expr))
3050
+ return unwrapQueryExpression(expr.expression);
3051
+ return expr;
3052
+ }
3048
3053
  function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
3049
- if (ts8.isParenthesizedExpression(expr) || ts8.isAwaitExpression(expr)) return queryEntityFromAst(expr.expression, initializers);
3050
- if (ts8.isIdentifier(expr) && initializers.has(expr.text)) return queryEntityFromAst(initializers.get(expr.text), initializers);
3051
- if (ts8.isCallExpression(expr)) {
3052
- const name = expressionName(expr.expression);
3053
- if (name === "cds.run") return queryEntityFromAst(expr.arguments[0], initializers);
3054
- if (["SELECT.one.from", "SELECT.from", "SELECT.one", "INSERT.into", "UPSERT.into", "DELETE.from", "UPDATE.entity"].includes(name)) return entityFromExpression(expr.arguments[0]);
3055
- if (name === "UPDATE") return entityFromExpression(expr.arguments[0]);
3056
- const receiver = ts8.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
3054
+ const unwrapped = unwrapQueryExpression(expr);
3055
+ if (ts8.isIdentifier(unwrapped) && initializers.has(unwrapped.text)) return queryEntityFromAst(initializers.get(unwrapped.text), initializers);
3056
+ if (ts8.isCallExpression(unwrapped)) {
3057
+ const name = expressionName(unwrapped.expression);
3058
+ if (name === "cds.run") return queryEntityFromAst(unwrapped.arguments[0], initializers);
3059
+ if (capQueryBuilderRoots.has(name)) return entityFromExpression(unwrapped.arguments[0]);
3060
+ const receiver = ts8.isPropertyAccessExpression(unwrapped.expression) ? unwrapped.expression.expression : void 0;
3057
3061
  if (receiver) return queryEntityFromAst(receiver, initializers);
3058
3062
  }
3059
3063
  return void 0;
3060
3064
  }
3065
+ var capQueryBuilderRoots = /* @__PURE__ */ new Set([
3066
+ "SELECT.from",
3067
+ "SELECT.one.from",
3068
+ "SELECT.one",
3069
+ "INSERT.into",
3070
+ "UPSERT.into",
3071
+ "UPDATE.entity",
3072
+ "UPDATE",
3073
+ "DELETE.from"
3074
+ ]);
3075
+ function wrapperParent(node) {
3076
+ const parent = node.parent;
3077
+ if ((ts8.isParenthesizedExpression(parent) || ts8.isAsExpression(parent) || ts8.isTypeAssertionExpression(parent) || ts8.isNonNullExpression(parent) || ts8.isSatisfiesExpression(parent)) && parent.expression === node)
3078
+ return parent;
3079
+ return void 0;
3080
+ }
3081
+ function fluentCallParent(node) {
3082
+ const property = node.parent;
3083
+ if (!ts8.isPropertyAccessExpression(property) || property.expression !== node) return void 0;
3084
+ const call = property.parent;
3085
+ return ts8.isCallExpression(call) && call.expression === property ? call : void 0;
3086
+ }
3087
+ function queryBuilderRoot(expr) {
3088
+ const unwrapped = unwrapQueryExpression(expr);
3089
+ if (!ts8.isCallExpression(unwrapped)) return void 0;
3090
+ if (capQueryBuilderRoots.has(expressionName(unwrapped.expression))) return unwrapped;
3091
+ return ts8.isPropertyAccessExpression(unwrapped.expression) ? queryBuilderRoot(unwrapped.expression.expression) : void 0;
3092
+ }
3093
+ function outerFluentQueryCall(root) {
3094
+ let current = root;
3095
+ let outer = root;
3096
+ while (true) {
3097
+ const wrapper = wrapperParent(current);
3098
+ if (wrapper) {
3099
+ current = wrapper;
3100
+ continue;
3101
+ }
3102
+ const next = fluentCallParent(current);
3103
+ if (!next) return outer;
3104
+ outer = next;
3105
+ current = next;
3106
+ }
3107
+ }
3108
+ function directQueryBuilderStatement(node) {
3109
+ const root = queryBuilderRoot(node);
3110
+ if (!root) return void 0;
3111
+ const logicalCall = outerFluentQueryCall(root);
3112
+ if (logicalCall !== node) return void 0;
3113
+ let current = logicalCall;
3114
+ while (true) {
3115
+ const wrapper = wrapperParent(current);
3116
+ if (wrapper) {
3117
+ current = wrapper;
3118
+ continue;
3119
+ }
3120
+ const parent = current.parent;
3121
+ return ts8.isAwaitExpression(parent) && parent.expression === current ? { root, logicalCall, awaitExpression: parent } : void 0;
3122
+ }
3123
+ }
3124
+ function queryBuilderEvidence(source, statement) {
3125
+ return {
3126
+ classifier: "cap_query_builder_direct",
3127
+ queryDispatch: "direct_query_builder",
3128
+ queryRoot: expressionName(statement.root.expression),
3129
+ queryRootStartOffset: statement.root.getStart(source),
3130
+ queryRootEndOffset: statement.root.getEnd(),
3131
+ queryStatementStartOffset: statement.awaitExpression.getStart(source),
3132
+ queryStatementEndOffset: statement.awaitExpression.getEnd()
3133
+ };
3134
+ }
3135
+ function queryRunEvidence(source, argument) {
3136
+ const root = argument ? queryBuilderRoot(argument) : void 0;
3137
+ return {
3138
+ classifier: "cap_query_run_wrapper",
3139
+ queryDispatch: "cds_run_wrapper",
3140
+ ...root ? {
3141
+ queryRoot: expressionName(root.expression),
3142
+ queryRootStartOffset: root.getStart(source),
3143
+ queryRootEndOffset: root.getEnd()
3144
+ } : {}
3145
+ };
3146
+ }
3061
3147
  function extractQueryEntity(expr) {
3062
3148
  const source = ts8.createSourceFile("query.ts", `const __query = (${expr});`, ts8.ScriptTarget.Latest, true, ts8.ScriptKind.TS);
3063
3149
  const initializers = variableInitializers(source);
@@ -3410,11 +3496,16 @@ function classifyOutboundCallsInSource(source, filePath) {
3410
3496
  }
3411
3497
  const expr = node.expression;
3412
3498
  const exprText = expr.getText(source);
3499
+ const directQuery = directQueryBuilderStatement(node);
3413
3500
  if (exprText === "cds.run") {
3414
3501
  const arg = node.arguments[0];
3415
3502
  const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
3416
3503
  const payload = arg?.getText(source) ?? "";
3417
- add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) });
3504
+ add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) }, queryRunEvidence(source, arg));
3505
+ } else if (directQuery) {
3506
+ const entity = queryEntityFromAst(directQuery.logicalCall, initializers);
3507
+ const payload = directQuery.logicalCall.getText(source);
3508
+ add(directQuery.logicalCall, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) }, queryBuilderEvidence(source, directQuery));
3418
3509
  } else if (ts8.isPropertyAccessExpression(expr) && expr.name.text === "send" && (ts8.isIdentifier(expr.expression) || ts8.isPropertyAccessExpression(expr.expression))) {
3419
3510
  const objectArg = node.arguments[0];
3420
3511
  if (objectArg && ts8.isObjectLiteralExpression(objectArg)) {
@@ -8250,4 +8341,4 @@ export {
8250
8341
  selectorRepoAmbiguousDiagnostic,
8251
8342
  trace
8252
8343
  };
8253
- //# sourceMappingURL=chunk-ERIZHM5C.js.map
8344
+ //# sourceMappingURL=chunk-GXYVIHJ5.js.map