@afokapu/atdd-bun 0.10.106 → 0.10.108

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.
@@ -8,7 +8,7 @@
8
8
  // data (snake_case, plan/_trains/_interlockings/**); the e2e tests are Bun/TS under e2e/**.
9
9
 
10
10
  import { excludedPath } from "../../../lib/scan.mjs";
11
- import { directInterlockingExecutionProofs, entryDelegatedAction, provenJourneyAction } from "../../../lib/journey-chain.mjs";
11
+ import { directInterlockingExecutionProofs, entryDelegatedAction, provenJourneyAction, transparentCallbackAliases } from "../../../lib/journey-chain.mjs";
12
12
  import { existsSync, readFileSync, statSync, readdirSync, writeFileSync } from "node:fs";
13
13
  import { dirname, join, resolve, sep } from "node:path";
14
14
 
@@ -529,7 +529,11 @@ function firstRebinding(text, name, executeArgument, returns) {
529
529
  function resolutionBinding(after, resolution, executeArgument) {
530
530
  const statement = after.slice(enclosingEnd(after) + 1);
531
531
  const scope = statement.slice(0, enclosingEnd(statement));
532
- return { scope, bound: scope.slice(0, firstRebinding(scope, resolution, executeArgument, topLevelReturns(scope))) };
532
+ return {
533
+ scope,
534
+ offset: after.length - statement.length,
535
+ bound: scope.slice(0, firstRebinding(scope, resolution, executeArgument, topLevelReturns(scope))),
536
+ };
533
537
  }
534
538
 
535
539
  function trainExecutionForms(resolution) {
@@ -541,15 +545,20 @@ function trainExecutionForms(resolution) {
541
545
  return { legacy, constructed, declaration: `${constructed}${r}\\s*(?:,|\\))` };
542
546
  }
543
547
 
544
- function resolutionTrainExecution(after, resolution) {
548
+ // `aliases` are the transparent callback-return aliases of this resolution's execution (see
549
+ // transparentCallbackAliases); only one declared inside the resolution's binding names the execution.
550
+ function resolutionTrainExecution(after, resolution, aliases = [], base = 0) {
545
551
  const { legacy, constructed, declaration } = trainExecutionForms(resolution);
546
- const { scope, bound } = resolutionBinding(after, resolution, constructed);
552
+ const { scope, bound, offset } = resolutionBinding(after, resolution, constructed);
547
553
  const assignment = `\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?`;
548
554
  const assigned = new RegExp(`${assignment}${legacy}`).exec(after) ?? new RegExp(`${assignment}${declaration}`).exec(bound);
549
555
  const returns = `\\breturn\\s+(?:await\\s+)?`;
556
+ const first = base + offset;
557
+ const alias = aliases.find((candidate) => candidate.start >= first && candidate.start < first + bound.length);
550
558
  return {
551
559
  execution: assigned?.[1] ?? null,
552
- found: Boolean(assigned || new RegExp(legacy).test(after) || new RegExp(declaration).test(bound)),
560
+ alias: alias?.alias ?? null,
561
+ found: Boolean(assigned || alias || new RegExp(legacy).test(after) || new RegExp(declaration).test(bound)),
553
562
  returned: new RegExp(`${returns}${legacy}`).test(after) || returnsMatch(scope, declaration, bound.length) ||
554
563
  Boolean(assigned && new RegExp(`${returns}${escaped(assigned[1])}\\s*;?`).test(after)),
555
564
  };
@@ -566,12 +575,23 @@ export function productionExecutionProofs(text, file, croot) {
566
575
  const direct = new RegExp(`new\\s+${PROD_INTERLOCKING}\\b`).test(receiver);
567
576
  const named = [...receivers].some((name) => receiver.trim().endsWith(name));
568
577
  if (!direct && !named) continue;
569
- const proof = resolutionTrainExecution(text.slice((m.index ?? 0) + whole.length), resolution);
578
+ const base = (m.index ?? 0) + whole.length;
579
+ const proof = resolutionTrainExecution(text.slice(base), resolution,
580
+ file ? transparentCallbackAliases(text, file, resolution) : [], base);
570
581
  if (proof.found) proofs.push({ resolution, execution: proof.execution });
582
+ if (proof.alias && proof.alias !== proof.execution) proofs.push({ resolution, execution: proof.alias });
571
583
  }
572
584
  return proofs;
573
585
  }
574
586
 
587
+ // A member read of a binding: `.` or the optional-chain `?.`. Calls and writes are never matched here.
588
+ const MEMBER = "\\??\\.";
589
+
590
+ // Name of the const/let/var bound to `<execution>.trace` (or `<execution>?.trace`), or undefined.
591
+ function boundTraceName(text, execution) {
592
+ return new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?${escaped(execution)}${MEMBER}trace\\b`).exec(text)?.[1];
593
+ }
594
+
575
595
  export function assertsExpression(text, expression, expected = null) {
576
596
  const pat = new RegExp(`\\bexpect\\s*\\(\\s*(?:await\\s+)?${expression}\\s*\\)\\s*\\.(?:to(?:Be|BeDefined|Equal|StrictEqual|Contain|ContainEqual|MatchObject|Match|BeTruthy)|not\\.to(?:Be|BeDefined|Equal|StrictEqual|Contain|ContainEqual|MatchObject|Match))\\b([\\s\\S]{0,220})`);
577
597
  const m = pat.exec(text);
@@ -591,8 +611,8 @@ export function hasProductionExecutionProof(text, file, croot) {
591
611
  if (assertsExpression(text, `${escaped(resolution)}\\.(?:routeId|trainId|selectedTrainId)`) ||
592
612
  (execution && assertsExpression(text, escaped(execution)))) return true;
593
613
  if (!execution) return false;
594
- const trace = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?${escaped(execution)}\\.trace\\b`).exec(text)?.[1];
595
- return Boolean(trace && REQUIRED_TRACE_FIELDS.some(([field]) => assertsExpression(text, `${escaped(trace)}\\.${field}`)));
614
+ const trace = boundTraceName(text, execution);
615
+ return Boolean(trace && REQUIRED_TRACE_FIELDS.some(([field]) => assertsExpression(text, `${escaped(trace)}${MEMBER}${field}`)));
596
616
  });
597
617
  }
598
618
 
@@ -617,13 +637,13 @@ export function traceHasProductionProvenance(text, file, croot) {
617
637
  return productionExecutionProofs(text, file, croot).some(({ execution, direct }) => {
618
638
  if (direct) return direct.traceBound && REQUIRED_TRACE_FIELDS.every(([field]) => direct.traceFields.includes(field));
619
639
  if (!execution) return false;
620
- const trace = new RegExp(`\\b(?:const|let|var)\\s+(${ident})(?:\\s*:\\s*[^=;\\n]+)?\\s*=\\s*(?:await\\s+)?${escaped(execution)}\\.trace\\b`).exec(text)?.[1];
640
+ const trace = boundTraceName(text, execution);
621
641
  return Boolean(trace);
622
642
  });
623
643
  }
624
644
 
625
645
  export function assertedTraceFields(text, traceName = "trace") {
626
- return REQUIRED_TRACE_FIELDS.filter(([field]) => !assertsExpression(text, `${escaped(traceName)}\\.${field}`)).map(([field]) => field);
646
+ return REQUIRED_TRACE_FIELDS.filter(([field]) => !assertsExpression(text, `${escaped(traceName)}${MEMBER}${field}`)).map(([field]) => field);
627
647
  }
628
648
 
629
649
  // A smoke path has a real Station Master call and observes that call's returned value; separately
package/integrity.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.10.106",
2
+ "version": "0.10.108",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "README.md": "8aefceaa24bee498f39081b2aaf4c0d5c54bc1ed9849c20e1d98d1427df56eab",
@@ -384,7 +384,7 @@
384
384
  "detectors/bun_interlocking_binding/fixtures/dirty_transcribed_runtime/server.ts": "d5bc34c5b1b2b0a511e0c4604ce6f9c77491dca9aee994be720540f3052babc1",
385
385
  "detectors/bun_interlocking_binding/fixtures/dirty_transcribed_runtime/src/trains/interlocking.ts": "9bcfdb4ecff6efd1f658b9fd587342615179924e25468c75daa07c2626d4d8ab",
386
386
  "detectors/bun_interlocking_binding/fixtures/dirty_transcribed_runtime/src/trains/runner.ts": "0e4b885e50f12cc2a8c8f6740bf83e7b2e7137a28e1e143586e9a8550ea5bd45",
387
- "detectors/bun_interlocking_coverage/_shared/interlocking.mjs": "e4de9503c31ed631e2e6a0bd5b21e78ee8ca1f1186fa93ff65106ff47695013e",
387
+ "detectors/bun_interlocking_coverage/_shared/interlocking.mjs": "f6a716c4ef612a0f1bf7b612587c4af94d8e6aaa89ecdb8acee7cc977609bf43",
388
388
  "detectors/bun_interlocking_coverage/atdd.implementation.yaml": "f160517a8138ba3ca05e3d130e6fbfb796e643376f25743fdef4fc210196525b",
389
389
  "detectors/bun_interlocking_coverage/checks/_map.json": "0a218ea319cc6258af7bd721c2154cb9561fe67e80d80ef32d08fc77bc9403c3",
390
390
  "detectors/bun_interlocking_coverage/checks/interlocking_production_runner.mjs": "97224d46f0c4c3259f15e41f90bb7f9f5a4e651d4facd41f1e037cac919be500",
@@ -1033,7 +1033,7 @@
1033
1033
  "lib/frontend.mjs": "d6d9b29c3d7f2d162cda03bb34b20a3224a82887b6be3db7cab375510913c9a3",
1034
1034
  "lib/imports.mjs": "3db4979269488fa26f6e7aefded6ece9c646be6587a10e249821a5a208109962",
1035
1035
  "lib/interlocking.mjs": "d0803d080ac7d7477c51be70112df1e85213d339fd3c6b71f75f26145fa8accd",
1036
- "lib/journey-chain.mjs": "34b1efa073b17940fa398af2970c85bb2fac35e4b2608ce635cfb6c39e3adc53",
1036
+ "lib/journey-chain.mjs": "bede441e5d61ea950ed5b76bbb6357ef0d2057cb29053ed32df1faab33ad9ac1",
1037
1037
  "lib/scan.mjs": "b6973e411ecd14c548a055c871e01e948f002b772042136be8401f38d998e687",
1038
1038
  "planner-nodes/ENFORCEMENT_SCOPE.yaml": "47aa9c78a4bb1754c6b435f5e056038baa04a24cd48df2c205db2bc08a3a3cac",
1039
1039
  "planner-nodes/nodes/planner.acceptance.abstract-fields-required.convention.yaml": "bd565693f0ae861f0561ad2f7a91a8c55f9a209c3acd59c5750cfcffd96a85d8",
@@ -767,6 +767,113 @@ function awaitedCall(expression) {
767
767
  return value && ts.isAwaitExpression(value) ? unwrap(value.expression) : null;
768
768
  }
769
769
 
770
+ // Every `return` of `fn` (catch and `finally` bodies included, nested functions excluded), as expressions.
771
+ function everyReturn(fn) {
772
+ if (!fn.body) return null;
773
+ if (!ts.isBlock(fn.body)) return [fn.body];
774
+ const returns = [];
775
+ walk(fn.body, (node) => { if (ts.isReturnStatement(node)) returns.push(node.expression ?? null); }, false);
776
+ return returns;
777
+ }
778
+
779
+ // True when `fn` has live returns and EVERY return (live or not, catch/finally included) satisfies `accept`.
780
+ function alwaysReturns(fn, accept) {
781
+ const all = everyReturn(fn), live = liveReturns(fn);
782
+ return Boolean(all?.length && live?.length && all.every(accept));
783
+ }
784
+
785
+ // True when `node` assigns to or increments/decrements the binding `symbol`.
786
+ function writesSymbol(ctx, node, symbol) {
787
+ const target = ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
788
+ node.operatorToken.kind <= ts.SyntaxKind.LastAssignment ? node.left
789
+ : (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
790
+ (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) ? node.operand : null;
791
+ return Boolean(target && ts.isIdentifier(unwrap(target)) && symbolOf(ctx, unwrap(target)) === symbol);
792
+ }
793
+
794
+ // The function bound to `identifier`: a declaration, or a `const` arrow/function expression.
795
+ function localFunction(ctx, identifier) {
796
+ const declaration = declarationOf(ctx, identifier);
797
+ if (!declaration) return null;
798
+ if (ts.isFunctionDeclaration(declaration) && declaration.body) return declaration;
799
+ const initializer = constInitializer(ctx, identifier);
800
+ const value = initializer && unwrap(initializer);
801
+ return value && (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) ? value : null;
802
+ }
803
+
804
+ // A wrapper whose every return, on every path, IS the unchanged result of its callback parameter:
805
+ // `return work()` / `return await work()`. Anything that replaces, conditions, copies or discards the
806
+ // callback's value (spread, ternary, comma, a second return value, a returning catch) is not transparent.
807
+ function transparentWrapper(ctx, fn) {
808
+ const param = fn.parameters[0];
809
+ if (!param || !ts.isIdentifier(param.name) || param.dotDotDotToken || fn.asteriskToken) return false;
810
+ const symbol = symbolOf(ctx, param.name);
811
+ if (!symbol) return false;
812
+ const isWork = (expression) => {
813
+ const call = expression && unwrap(expression);
814
+ return Boolean(call && ts.isCallExpression(call) && call.arguments.length === 0 &&
815
+ ts.isIdentifier(unwrap(call.expression)) && symbolOf(ctx, unwrap(call.expression)) === symbol);
816
+ };
817
+ if (!alwaysReturns(fn, isWork)) return false;
818
+ let rebound = false;
819
+ walk(fn.body, (node) => { rebound ||= writesSymbol(ctx, node, symbol); });
820
+ return !rebound;
821
+ }
822
+
823
+ // `const <alias> = await <wrapper>(<callback>)` where the local wrapper is transparent and every return of
824
+ // the zero-parameter callback IS the production execution of `resolution` (the call itself, or a `const`
825
+ // bound to it), so `<alias>` IS that execution. Returns [{ alias, start }] in source order.
826
+ // Production forms: `new TrainRunner(<resolution>.trainId|selectedTrainId).execute(...)` and
827
+ // `new TrainRunner(<resolution>.trainPath, ...).execute(<resolution>, ...)`.
828
+ export function transparentCallbackAliases(text, file, resolution) {
829
+ if (!text || !file || !resolution) return [];
830
+ const ctx = analyze(file, text);
831
+ const aliases = [];
832
+ const isResolutionMember = (node, names) => {
833
+ node = node && unwrap(node);
834
+ return Boolean(node && ts.isPropertyAccessExpression(node) && names.includes(node.name.text) &&
835
+ ts.isIdentifier(node.expression) && node.expression.text === resolution && declarationOf(ctx, node.expression));
836
+ };
837
+ const isExecution = (node) => {
838
+ const call = unwrap(node);
839
+ if (!call || !ts.isCallExpression(call) || !ts.isPropertyAccessExpression(call.expression) ||
840
+ call.expression.name.text !== "execute") return false;
841
+ const construction = unwrap(call.expression.expression);
842
+ if (!construction || !ts.isNewExpression(construction) || !ts.isIdentifier(construction.expression) ||
843
+ construction.expression.text !== "TrainRunner" || !construction.arguments?.length) return false;
844
+ const [first] = construction.arguments;
845
+ if (isResolutionMember(first, ["trainId", "selectedTrainId"])) return construction.arguments.length === 1;
846
+ const executed = call.arguments[0] && unwrap(call.arguments[0]);
847
+ return isResolutionMember(first, ["trainPath"]) && Boolean(executed && ts.isIdentifier(executed) &&
848
+ executed.text === resolution && declarationOf(ctx, executed));
849
+ };
850
+ const isIdentity = (expression, seen = new Set()) => {
851
+ const node = expression && unwrap(expression);
852
+ if (!node) return false;
853
+ if (ts.isCallExpression(node)) return isExecution(node);
854
+ if (!ts.isIdentifier(node)) return false;
855
+ const initializer = constInitializer(ctx, node);
856
+ if (!initializer || seen.has(initializer)) return false;
857
+ seen.add(initializer);
858
+ return isIdentity(initializer, seen);
859
+ };
860
+ walk(ctx.source, (node) => {
861
+ if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || !node.initializer ||
862
+ !(ts.isVariableDeclarationList(node.parent) && node.parent.flags & ts.NodeFlags.Const) ||
863
+ dead(node, ctx.source) || insideCatch(node, ctx.source)) return;
864
+ const awaited = awaitedCall(node.initializer);
865
+ if (!awaited || !ts.isCallExpression(awaited) || !ts.isIdentifier(awaited.expression) || awaited.arguments.length !== 1) return;
866
+ const callback = unwrap(awaited.arguments[0]);
867
+ if (!callback || !(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) ||
868
+ callback.parameters.length > 0 || callback.asteriskToken) return;
869
+ const wrapper = localFunction(ctx, awaited.expression);
870
+ if (!wrapper || !transparentWrapper(ctx, wrapper)) return;
871
+ if (!alwaysReturns(callback, (expression) => isIdentity(expression))) return;
872
+ aliases.push({ alias: node.name.text, start: node.getStart(ctx.source) });
873
+ });
874
+ return aliases;
875
+ }
876
+
770
877
  function expectIdentifier(ctx, node) {
771
878
  if (!ts.isIdentifier(node) || node.text !== "expect") return false;
772
879
  const declaration = declarationOf(ctx, node);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.10.106",
3
+ "version": "0.10.108",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
@@ -35,7 +35,7 @@
35
35
  "bun": ">=1.3.0"
36
36
  },
37
37
  "dependencies": {
38
- "@afokapu/atdd-flow": "0.3.50",
38
+ "@afokapu/atdd-flow": "0.3.82",
39
39
  "ajv": "^8.17.1",
40
40
  "ajv-formats": "^3.0.1",
41
41
  "typescript": "6.0.3"