@saptools/service-flow 0.1.12 → 0.1.13

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.13
4
+
5
+ - Added implementation-context fallback for local `cds.services.*` calls so helper packages can resolve model-package operations only when handler/dependency/registration evidence ties the caller repository to the target operation.
6
+ - Preserved same-repository local service resolution as the strongest path and added explicit evidence/reasons for implementation-context ownership, rejected candidates, and candidate-without-caller-ownership cases.
7
+ - Replaced fragile CAP DB query string extraction with AST traversal for chained `cds.run(SELECT/INSERT/UPDATE/DELETE...)` forms, including multiline `columns(...).where(...)` chains and `this.EntityName` targets.
8
+ - Kept genuinely dynamic DB queries terminal while exposing parser-warning evidence and rendering unknown targets as `Entity: unknown` instead of raw numeric call ids in table/Mermaid output.
9
+ - Further reduced symbol-call noise by filtering built-in collection/string methods, logger calls, global built-ins, third-party package property calls, and unindexed `this.container.method()` calls while retaining indexed local helpers and relative object-literal helper imports.
10
+ - Added doctor aggregate visibility for local service calls resolved by implementation context and calls left unresolved because candidates lack caller ownership.
11
+ - Updated README and technical notes to clarify generated constants remain low-level parser output rather than persisted graph facts.
12
+
3
13
  ## 0.1.12
4
14
 
5
15
  - Resolved same-repository local CAP service calls by qualified CDS name, simple service name, and service path, with explicit local transport and alias-chain evidence.
package/README.md CHANGED
@@ -152,11 +152,11 @@ target handler up to `--depth` instead of showing only calls in the first file.
152
152
 
153
153
  `service-flow trace` starts from the selected handler method symbol, renders outbound calls owned by that symbol, and follows conservative local helper-call facts. Supported helper edges include same-file functions, `this.method()` calls, and exactly mapped relative imports/exports that resolve to an indexed executable symbol. Calls from unrelated functions in the same source file are not included merely because the file path matches.
154
154
 
155
- Local CAP calls through `cds.services.<Service>.<operation>()`, bracket service lookups, and simple aliases are indexed as local operation calls. Linking stays within the same repository by default and matches the target operation by exact qualified CDS service name, exact simple service name, exact service path, or an unambiguous service-path suffix. Entity accessors such as `cds.services.db.entities(...)` are treated as entity metadata access, not operation calls.
155
+ Local CAP calls through `cds.services.<Service>.<operation>()`, bracket service lookups, and simple aliases are indexed as local operation calls. Linking first stays within the same repository and matches the target operation by exact qualified CDS service name, exact simple service name, exact service path, or an unambiguous service-path suffix. If no same-repository service exists, the linker can use implementation-context evidence to resolve model-package operations for helper packages: a resolved/ambiguous implementation candidate, registration package, or dependency/import edge must tie the caller repository to the model operation. Name-only global matches are preserved as unresolved candidate evidence rather than guessed links. Entity accessors such as `cds.services.db.entities(...)` are treated as entity metadata access, not operation calls.
156
156
 
157
157
  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.
158
158
  JSON output includes typed nodes for calls, operations, database entities,
159
- external destinations, and unresolved/dynamic candidates when edges exist.
159
+ 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.
160
160
 
161
161
  ```bash
162
162
  service-flow trace --workspace /path/to/workspace --repo facade-service --operation doWork
@@ -287,7 +287,7 @@ service-flow clean --workspace /path/to/workspace
287
287
  | Local data access | `cds.run(SELECT...)` and local entity query evidence |
288
288
  | Async channels | Event Mesh-style `emit`, `publish`, and `on` facts |
289
289
  | External calls | Cloud SDK-style HTTP/destination calls and external edge evidence |
290
- | Generated constants | constants used to resolve service paths, operation paths, and thin helper wrappers |
290
+ | Generated constants | low-level `parseGeneratedConstants` parser output for integrations; not persisted as first-class graph facts in this patch |
291
291
 
292
292
  ---
293
293
 
package/TECHNICAL-NOTE.md CHANGED
@@ -1,4 +1,4 @@
1
- # Service Flow 0.1.8 Resolution Notes
1
+ # Service Flow Resolution Notes
2
2
 
3
3
  - Imported helper bindings: TypeScript imports are resolved for relative modules. When a caller assigns `const client = await connectToService()`, the analyzer follows the imported symbol to an exported helper that returns `cds.connect.to(...)` and persists caller-variable evidence plus the helper source/export chain.
4
4
  - Candidate ranking: operation-path matches start as weak candidates. A resolved operation edge requires a strong signal such as exact service path, CDS alias/destination context, or explicit dynamic variable overrides. Otherwise candidates are preserved in edge evidence as ambiguous or unresolved.
@@ -6,6 +6,15 @@
6
6
  - Trace cycle safety: trace queues carry repository IDs, visited scope keys are independent of depth, graph edge IDs are emitted once, and revisiting an already-seen downstream operation scope creates a cycle marker instead of recursive expansion.
7
7
  - SQLite reliability: the package uses a persistent SQLite connection per opened database, bound parameters, transactions, WAL, busy timeouts, read-only openings for query commands, and connection-local foreign-key enforcement. Native driver loading failures produce an actionable startup error before output rendering.
8
8
 
9
+ ## 0.1.13 audit follow-up notes
10
+
11
+ - Local CAP service calls keep same-repository service ownership as the strongest resolution path. When the caller repository does not own the CDS service model, the linker searches workspace operations by service identity and operation name/path, then requires caller ownership evidence from implementation edges, ambiguous implementation candidates, registration packages, or resolved dependency/import edges before resolving. Candidate operations without caller evidence are retained with `local_service_candidate_without_caller_ownership` rather than guessed.
12
+ - Trace traversal can use local-call context to choose the caller repository's handler from an otherwise ambiguous global implementation edge. This is scoped to the local call and does not rewrite the global `OPERATION_IMPLEMENTED_BY_HANDLER` ambiguity.
13
+ - CAP DB entity extraction for local `cds.run(...)` calls uses TypeScript AST traversal across chained query expressions, including `SELECT.one.from(Entity).columns(...).where(...)`, `INSERT.into(Entity)`, `UPDATE(Entity)`, and `DELETE.from(Entity)`. Dynamic or unknowable query targets remain terminal graph edges with parser-warning evidence.
14
+ - Human output never labels an unknown DB query target with the raw call id. Table and Mermaid output use `Entity: unknown`; JSON keeps the parser warning and call id evidence for machines.
15
+ - Symbol-call indexing is intentionally conservative for property accesses. Built-in collection/string calls, global built-ins, logger calls, third-party package properties, and unindexed `this.someContainer.method()` calls are filtered unless indexed local helper or relative import evidence makes the edge actionable.
16
+ - Generated constants remain low-level parser output through `parseGeneratedConstants`. They are not persisted as graph facts; implementation linking only uses deterministic decorator normalization for common generated action/function names.
17
+
9
18
  ## 0.1.4 trace-correctness additions
10
19
 
11
20
  - Helper exports are normalized through a public-to-local export map, so `export { helper }` and `export { helper as publicHelper }` both resolve to the local declaration that contains the `cds.connect.to(...)` call.
@@ -555,22 +555,39 @@ function matchingParen(text, open) {
555
555
  }
556
556
  return -1;
557
557
  }
558
- function argumentForCall(expr, marker) {
559
- const idx = expr.indexOf(marker);
560
- if (idx < 0) return void 0;
561
- const open = expr.indexOf("(", idx + marker.length);
562
- if (open < 0) return void 0;
563
- const close = matchingParen(expr, open);
564
- return close > open ? expr.slice(open + 1, close).trim() : void 0;
565
- }
566
- function entityFromArg(arg) {
567
- if (!arg) return void 0;
568
- const first = arg.split(",")[0]?.trim();
569
- if (!first) return void 0;
570
- return stripQuotes(first).replace(/^this\./, "");
558
+ function entityFromExpression(expr) {
559
+ if (!expr) return void 0;
560
+ if (ts4.isIdentifier(expr) || ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
561
+ if (ts4.isPropertyAccessExpression(expr) && expr.expression.kind === ts4.SyntaxKind.ThisKeyword) return expr.name.text;
562
+ return void 0;
563
+ }
564
+ function expressionName(expr) {
565
+ if (ts4.isIdentifier(expr)) return expr.text;
566
+ if (ts4.isPropertyAccessExpression(expr)) return `${expressionName(expr.expression)}.${expr.name.text}`;
567
+ return expr.getText();
568
+ }
569
+ function queryEntityFromAst(expr) {
570
+ if (ts4.isParenthesizedExpression(expr) || ts4.isAwaitExpression(expr)) return queryEntityFromAst(expr.expression);
571
+ if (ts4.isCallExpression(expr)) {
572
+ const name = expressionName(expr.expression);
573
+ if (name === "cds.run") return queryEntityFromAst(expr.arguments[0]);
574
+ if (name === "SELECT.one.from" || name === "SELECT.from" || name === "INSERT.into" || name === "DELETE.from") return entityFromExpression(expr.arguments[0]);
575
+ if (name === "UPDATE") return entityFromExpression(expr.arguments[0]);
576
+ const receiver = ts4.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
577
+ if (receiver) return queryEntityFromAst(receiver);
578
+ }
579
+ return void 0;
571
580
  }
572
581
  function extractQueryEntity(expr) {
573
- return entityFromArg(argumentForCall(expr, "SELECT.one.from")) ?? entityFromArg(argumentForCall(expr, "SELECT.from")) ?? entityFromArg(argumentForCall(expr, "INSERT.into")) ?? entityFromArg(argumentForCall(expr, "UPDATE")) ?? entityFromArg(argumentForCall(expr, "DELETE.from"));
582
+ const source = ts4.createSourceFile("query.ts", `const __query = (${expr});`, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
583
+ let found;
584
+ const visit = (node) => {
585
+ if (found) return;
586
+ if (ts4.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression);
587
+ ts4.forEachChild(node, visit);
588
+ };
589
+ visit(source);
590
+ return found;
574
591
  }
575
592
  async function parseOutboundCalls(repoPath, filePath) {
576
593
  const text = await fs6.readFile(path7.join(repoPath, filePath), "utf8");
@@ -1074,7 +1091,7 @@ function substituteVariables(template, vars) {
1074
1091
  // src/linker/service-resolver.ts
1075
1092
  function rows(db, operationPath, workspaceId) {
1076
1093
  return db.prepare(
1077
- `SELECT o.id operationId,r.name repoName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
1094
+ `SELECT o.id operationId,r.id repoId,r.name repoName,r.package_name packageName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
1078
1095
  FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
1079
1096
  WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path=? OR o.operation_name=?) ORDER BY r.name,s.service_path,o.operation_name`
1080
1097
  ).all(
@@ -1098,11 +1115,21 @@ function resolveOperation(db, signals, workspaceId) {
1098
1115
  candidates: [],
1099
1116
  reasons: ["missing_operation_path"]
1100
1117
  };
1101
- const candidates = rows(db, signals.operationPath, workspaceId).filter((c) => matchesLocalRepo(db, c.operationId, signals.repoId)).map((c) => ({
1118
+ const allCandidates = rows(db, signals.operationPath, workspaceId).map((c) => ({
1102
1119
  ...c,
1103
1120
  score: 0.2,
1104
1121
  reasons: ["operation_path_match"]
1105
1122
  }));
1123
+ let candidates = allCandidates.filter((c) => matchesLocalRepo(db, c.operationId, signals.repoId));
1124
+ if (candidates.length === 0 && signals.repoId !== void 0 && signals.serviceName) {
1125
+ candidates = implementationContextCandidates(db, allCandidates, signals.repoId, signals.serviceName);
1126
+ if (candidates.length === 0)
1127
+ return {
1128
+ status: "unresolved",
1129
+ candidates: allCandidates.filter((c) => serviceMatches(c, signals.serviceName)),
1130
+ reasons: allCandidates.some((c) => serviceMatches(c, signals.serviceName)) ? ["local_service_candidate_without_caller_ownership"] : ["no_operation_candidates"]
1131
+ };
1132
+ }
1106
1133
  if (candidates.length === 0)
1107
1134
  return {
1108
1135
  status: "unresolved",
@@ -1173,6 +1200,34 @@ function resolveOperation(db, signals, workspaceId) {
1173
1200
  reasons: ["candidate_score_below_resolution_threshold"]
1174
1201
  };
1175
1202
  }
1203
+ function serviceMatches(candidate, serviceName) {
1204
+ if (!serviceName) return false;
1205
+ const simple = serviceName.split(".").at(-1) ?? serviceName;
1206
+ return candidate.qualifiedName === serviceName || candidate.serviceName === serviceName || candidate.serviceName === simple || candidate.servicePath === serviceName || candidate.servicePath === `/${serviceName}` || candidate.servicePath === `/${simple}` || candidate.servicePath.endsWith(`/${simple}`);
1207
+ }
1208
+ function implementationContextCandidates(db, candidates, callerRepoId, serviceName) {
1209
+ const matching = candidates.filter((candidate) => serviceMatches(candidate, serviceName));
1210
+ const owned = matching.map((candidate) => ownershipReason(db, candidate, callerRepoId)).filter((item) => Boolean(item));
1211
+ if (owned.length === 0) return [];
1212
+ const direct = owned.filter((item) => item.reason !== "caller_depends_on_model_package");
1213
+ const chosen = direct.length > 0 ? direct : owned.length === 1 ? owned : [];
1214
+ return chosen.map((item) => ({ ...item.candidate, score: 0.95, reasons: [...item.candidate.reasons, "implementation_context_caller_ownership", item.reason] }));
1215
+ }
1216
+ function ownershipReason(db, candidate, callerRepoId) {
1217
+ const edge = db.prepare("SELECT status,evidence_json,to_id FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND from_kind='operation' AND from_id=? ORDER BY CASE status WHEN 'resolved' THEN 0 WHEN 'ambiguous' THEN 1 ELSE 2 END LIMIT 1").get(String(candidate.operationId));
1218
+ if (edge?.status === "resolved") {
1219
+ const row = db.prepare("SELECT hc.repo_id repoId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id WHERE hm.id=?").get(edge.to_id);
1220
+ if (row?.repoId === callerRepoId) return { candidate, reason: "resolved_implementation_handler_repo_matches_caller" };
1221
+ }
1222
+ if (edge?.evidence_json) {
1223
+ const evidence = JSON.parse(edge.evidence_json);
1224
+ const hit = evidence.candidates?.find((item) => item.accepted && (Number(item.handlerPackage?.id) === callerRepoId || Number(item.applicationPackage?.id) === callerRepoId));
1225
+ if (hit) return { candidate, reason: edge.status === "ambiguous" ? "ambiguous_implementation_candidate_repo_matches_caller" : "registration_package_matches_caller" };
1226
+ }
1227
+ const dep = db.prepare("SELECT 1 FROM graph_edges WHERE edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND status='resolved' AND from_kind='repo' AND from_id=? AND to_id=?").get(String(callerRepoId), String(candidate.repoId));
1228
+ if (dep) return { candidate, reason: "caller_depends_on_model_package" };
1229
+ return void 0;
1230
+ }
1176
1231
  function matchesLocalRepo(db, operationId, repoId) {
1177
1232
  if (repoId === void 0) return true;
1178
1233
  const row = db.prepare("SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?").get(operationId);
@@ -1227,8 +1282,8 @@ function linkWorkspace(db, workspaceId, vars = {}) {
1227
1282
  const generation = nextGraphGeneration(db, workspaceId);
1228
1283
  db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
1229
1284
  const deps = linkHelperPackages(db, workspaceId, generation);
1230
- const callSummary = linkCalls(db, workspaceId, vars, generation);
1231
1285
  const impl = linkImplementations(db, workspaceId, generation);
1286
+ const callSummary = linkCalls(db, workspaceId, vars, generation);
1232
1287
  db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
1233
1288
  return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount };
1234
1289
  });
@@ -1633,6 +1688,17 @@ function implementationScope(db, operationId) {
1633
1688
  const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(edge.to_id);
1634
1689
  return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
1635
1690
  }
1691
+ function contextImplementationMethodId(edge, callerRepoId) {
1692
+ if (!edge || edge.status !== "ambiguous" || callerRepoId === void 0) return void 0;
1693
+ const evidence = JSON.parse(String(edge.evidence_json || "{}"));
1694
+ const candidate = evidence.candidates?.find((item) => item.accepted && (Number(item.handlerPackage?.id) === callerRepoId || Number(item.applicationPackage?.id) === callerRepoId));
1695
+ return candidate?.methodId === void 0 ? void 0 : String(candidate.methodId);
1696
+ }
1697
+ function handlerScope(db, methodId) {
1698
+ const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(methodId);
1699
+ if (!row) return void 0;
1700
+ return { repoId: row.repoId, files: new Set(row.sourceFile ? [row.sourceFile] : []), symbolId: row.symbolId };
1701
+ }
1636
1702
  function includeCall(type, options) {
1637
1703
  if (!options.includeDb && type === "local_db_query") return false;
1638
1704
  if (!options.includeExternal && type === "external_http") return false;
@@ -1716,6 +1782,7 @@ function edgeTarget(row, evidence) {
1716
1782
  const operationPath = typeof evidence.operationPath === "string" ? evidence.operationPath : void 0;
1717
1783
  const targetOperation = typeof evidence.targetOperation === "string" ? evidence.targetOperation : void 0;
1718
1784
  const targetRepo = typeof evidence.targetRepo === "string" ? evidence.targetRepo : "";
1785
+ if (row.edge_type === "HANDLER_RUNS_DB_QUERY") return /^\d+$/.test(row.to_id) ? "Entity: unknown" : `Entity: ${row.to_id}`;
1719
1786
  return servicePath && operationPath ? `${servicePath}${operationPath}` : targetOperation ? `${targetRepo}:${targetOperation}` : row.to_id;
1720
1787
  }
1721
1788
  function trace(db, start, options) {
@@ -1827,9 +1894,11 @@ function trace(db, start, options) {
1827
1894
  });
1828
1895
  if (effectiveRow.to_kind === "operation") {
1829
1896
  const implementation = implementationScope(db, effectiveRow.to_id);
1897
+ const contextMethodId = contextImplementationMethodId(implementation.edge, current.repoId);
1898
+ const contextNode = contextMethodId ? handlerMethodNode(db, contextMethodId) : void 0;
1830
1899
  if (implementation.edge) {
1831
1900
  const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
1832
- const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) : void 0;
1901
+ const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) : contextNode;
1833
1902
  const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
1834
1903
  if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
1835
1904
  edges.push({
@@ -1843,10 +1912,11 @@ function trace(db, start, options) {
1843
1912
  });
1844
1913
  }
1845
1914
  if (current.depth >= maxDepth) continue;
1846
- const files = implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id);
1847
- const symbolIds = implementation.symbolId ? /* @__PURE__ */ new Set([implementation.symbolId]) : void 0;
1848
- if (implementation.edge?.status === "resolved" && files.size > 0) {
1849
- const targetRepoId = implementation.repoId ?? db.prepare(
1915
+ const contextScope = contextMethodId ? handlerScope(db, contextMethodId) : void 0;
1916
+ const files = contextScope?.files ?? (implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id));
1917
+ const symbolIds = contextScope?.symbolId ? /* @__PURE__ */ new Set([contextScope.symbolId]) : implementation.symbolId ? /* @__PURE__ */ new Set([implementation.symbolId]) : void 0;
1918
+ if ((implementation.edge?.status === "resolved" || contextScope) && files.size > 0) {
1919
+ const targetRepoId = contextScope?.repoId ?? implementation.repoId ?? db.prepare(
1850
1920
  "SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
1851
1921
  ).get(effectiveRow.to_id)?.repoId;
1852
1922
  const nextKey = `${targetRepoId ?? "*"}:${[...symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...files].sort().join(",")}`;
@@ -1893,4 +1963,4 @@ export {
1893
1963
  linkWorkspace,
1894
1964
  trace
1895
1965
  };
1896
- //# sourceMappingURL=chunk-MXYYARUP.js.map
1966
+ //# sourceMappingURL=chunk-Q7W7TE3W.js.map