From d7035641630611552963aeb833edcebe32967a12 Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Sat, 1 Aug 2026 16:04:01 +0400 Subject: [PATCH 1/2] fix(index): bind shared consumer discriminators --- docs/core-reset/removal-manifest.yml | 104 +- docs/core-reset/scorecard.md | 6 +- docs/designs/2026-07-19-core-reset.md | 15 + docs/roadmap.md | 4 +- src/adapters/typescript/execution.ts | 2690 ++++++++--------- src/domain/index/build-state.ts | 2 +- src/domain/index/model.ts | 704 ++--- src/domain/query/index-status.ts | 807 +++-- ...anonical-index-execution-hardening.test.ts | 85 +- ...index-execution-review-regressions.test.ts | 2491 +++++++++++++++ tests/unit/canonical-index-execution.test.ts | 88 + tests/unit/core-reset-governance.test.ts | 95 +- .../query-index-execution-validation.test.ts | 79 +- tests/unit/update-index.test.ts | 39 + 14 files changed, 4912 insertions(+), 2297 deletions(-) diff --git a/docs/core-reset/removal-manifest.yml b/docs/core-reset/removal-manifest.yml index a9e6c8d0..b6411b53 100644 --- a/docs/core-reset/removal-manifest.yml +++ b/docs/core-reset/removal-manifest.yml @@ -26,23 +26,23 @@ review: release_amendment: 'Historical release receipt: @lubab/madar@0.40.0-beta.3 was published under npm dist-tag next and GitHub prerelease v0.40.0-beta.3 from exact protected-next commit ece7d0d02643ecec08bd91aa904a4514aa845f42. Issue #625 and PR #626 subsequently completed the generic evidence-skeleton repair on protected next at b6562b715133304bd46e537b6f39008bc1e02095. Issue #627 then published @lubab/madar@0.40.0-beta.4 under npm dist-tag next and the matching GitHub prerelease from exact protected-next commit 9043320cfa08370e5cdd3911bfb9283005aa9912 and tree f51d6e75e3b806dec6caf9ff0be43fc2ab5713fc. npm latest remains 0.32.0. Stable 0.40.0, MCP Registry publication, comparative claims, and main remain outside this release. Issue #632 authorizes no publication, release, Registry metadata, tag, or main action.' current: - updated_at: 2026-07-31 + updated_at: 2026-08-01 completed_phase: retrieval-regression-625 active_phase: semantic-execution-index-632 ready_phase: null base_commit: 9043320cfa08370e5cdd3911bfb9283005aa9912 completed_phase_commit: b6562b715133304bd46e537b6f39008bc1e02095 production_typescript_files: 44 - production_typescript_loc: 15934 - production_loc_added: 3667 - production_loc_removed: 187 - production_loc_net: 3480 + production_typescript_loc: 15719 + production_loc_added: 3462 + production_loc_removed: 197 + production_loc_net: 3265 npm_files: 102 - npm_packed_bytes: 145254 - npm_unpacked_bytes: 638736 - npm_shasum: 9f0c66e663f703afbb9a5e68f6037f9e211cba58 - npm_integrity: sha512-3yYpFxnym0r9DF66IfS8w1MI01DMLU+hX6uQi6aQoBQvbeu6jHn8j059N6ml3MwMx3wvlj41Y3yAMIX3D2N2Aw== - npm_artifact_sha256: b49bf7a1eae2b230da4d0b7a778f6112e1bc9d986c083573bc3bd7296a99c670 + npm_packed_bytes: 149453 + npm_unpacked_bytes: 639867 + npm_shasum: 0d7e3f067d09d6db953ffc34356d2c1221cc7d08 + npm_integrity: sha512-ZC5vSq9MhdEzCmeddBn0LgTNshnN/AtHlwNhxEkeMS0eMqtyqu0haYif64lZBcI2/KS/2nLM/ksVmpjyc/BLaw== + npm_artifact_sha256: fb1aa735fc8d3eb57c5771e9bf59b20c542afa9b96d43975a737e964d40743e6 measurement_state: source_and_package_exact snapshot_scope: semantic_execution_index_632_candidate release_candidate: @@ -2269,30 +2269,44 @@ items: candidate: source_measurement: production_typescript_files: 44 - production_typescript_loc: 15934 - added: 3667 - removed: 187 - net: 3480 - diff_sha256: e712d06a2c43cc0223c2c4219691ddc860f2f05d26c689b19b8ade2d180c99a6 - execution_source_sha256: cf43e183f9d001764f4fdd22b073bef8854f9d949fef5793905291b870d3404b + production_typescript_loc: 15719 + added: 3462 + removed: 197 + net: 3265 + diff_sha256: 910d0e1835e54af5e7a36af0a7ffa612977fb9240e9985d0f1368532d4ca3a43 + execution_source_sha256: 192f0eb505615dd5f62b340496b4515b2df8e8509240e960517f2a86fa09e034 package_measurement: files: 102 - packed_bytes: 145254 - unpacked_bytes: 638736 - shasum: 9f0c66e663f703afbb9a5e68f6037f9e211cba58 - integrity: sha512-3yYpFxnym0r9DF66IfS8w1MI01DMLU+hX6uQi6aQoBQvbeu6jHn8j059N6ml3MwMx3wvlj41Y3yAMIX3D2N2Aw== - artifact_sha256: b49bf7a1eae2b230da4d0b7a778f6112e1bc9d986c083573bc3bd7296a99c670 + packed_bytes: 149453 + unpacked_bytes: 639867 + shasum: 0d7e3f067d09d6db953ffc34356d2c1221cc7d08 + integrity: sha512-ZC5vSq9MhdEzCmeddBn0LgTNshnN/AtHlwNhxEkeMS0eMqtyqu0haYif64lZBcI2/KS/2nLM/ksVmpjyc/BLaw== + artifact_sha256: fb1aa735fc8d3eb57c5771e9bf59b20c542afa9b96d43975a737e964d40743e6 local_verification: - focused_tests_passed: 196 - last_pre_cache_full_tests_passed: 722 - candidate_full_suite_status: all_761_unique_tests_passed_but_single_process_full_coverage_blocked_by_local_fork_worker_start_pressure - candidate_full_suite_local_attempts: 6 - default_fork_completed_test_files: 79 - default_fork_completed_tests_passed: 707 - default_fork_unstarted_file: tests/unit/retrieve-context.test.ts - unstarted_file_isolated_tests_passed: 54 - unique_tests_passed_across_default_fork_runs: 761 - local_independent_review: no_blocker + focused_tests_passed: 247 + candidate_full_suite_status: passed + candidate_full_suite_local_attempts: 1 + candidate_full_suite_files_passed: 80 + candidate_full_suite_tests_passed: 785 + coverage_statements_percent: 85.85 + coverage_statements_covered: 7715 + coverage_statements_total: 8986 + coverage_branches_percent: 79.46 + coverage_branches_covered: 6959 + coverage_branches_total: 8757 + coverage_functions_percent: 92.37 + coverage_functions_covered: 1369 + coverage_functions_total: 1482 + coverage_lines_percent: 89.22 + coverage_lines_covered: 6481 + coverage_lines_total: 7264 + local_independent_review: pending_corrective_review + pre_reopen_merge_commit: e7bd30ce384cf743dbda3e8ee7f15b171a0ea649 + post_merge_acceptance_audit: failed_missing_exact_payload_and_discriminant_binding + corrective_real_graph_acceptance: passed + frozen_govalidate_corpus_files: 6889 + frozen_govalidate_corpus_bytes: 177796450 + frozen_govalidate_corpus_sha256: 3fa9a0a3edc13cf1439d572292601fff43c43a94f7a50948349f9a69123fa7a7 graph_nodes: 12313 graph_edges: 32717 exact_queue_channels: 6 @@ -2301,8 +2315,8 @@ items: exact_channel_edges: 42 publishes_to_edges: 35 consumed_by_edges: 7 - graph_artifact_bytes: 60267088 - graph_artifact_size_ratio: 1.2291718874663748 + graph_artifact_bytes: 60271172 + graph_artifact_size_ratio: 1.2292551823152718 beta4_indexing_trials_seconds: - 20.32 - 21.33 @@ -2311,18 +2325,18 @@ items: - 22.74 beta4_indexing_median_seconds: 22.16 candidate_indexing_trials_seconds: - - 14.83 - - 12.99 - - 13.05 - - 14.16 - - 13.17 - candidate_indexing_median_seconds: 13.17 - indexing_median_ratio: 0.5943140794223827 + - 13.71 + - 13.49 + - 13.41 + - 13.55 + - 13.45 + candidate_indexing_median_seconds: 13.49 + indexing_median_ratio: 0.608754512635379 warm_retrieval_samples: 100 - warm_retrieval_median_ms: 156.7398119999998 - warm_retrieval_p95_ms: 177.5851354499996 - warm_retrieval_max_ms: 183.07708300000013 - deterministic_graph_sha256: 77270a6f0330a3ce85fbc42b90e7a3e99f8bf37776f6e65f5da8aad1bad3caaf + warm_retrieval_median_ms: 213.32806250000067 + warm_retrieval_p95_ms: 238.83405145000143 + warm_retrieval_max_ms: 256.85883300000205 + deterministic_graph_sha256: 569af2dcd681c4db48124a47bceac7036a94b344f2a2f88e8cb35f6120711610 broad_retrieval_files: 10 broad_retrieval_snippets: 10 broad_retrieval_serialized_tokens: 3669 @@ -2351,7 +2365,7 @@ items: registry_metadata_publication: forbidden tag: forbidden main_target: forbidden - notes: 'Issue #632 extends the canonical index with authenticated ordered body facts, exact shared queue/job/event channels, bounded two-hop wrapper substitution, concurrency groups, and receiver/type-proven persistence. It is active from exact protected next commit 9043320cfa08370e5cdd3911bfb9283005aa9912 and tree f51d6e75e3b806dec6caf9ff0be43fc2ab5713fc. First PR head 9fe3c2448958c6b8cead2452758077fef093cf4e passed all six hosted jobs but independent review blocked twelve semantic-proof classes; later corrective heads c977de03ecba7958d03966df728abed9f1b36ff7 and f4ae64402d89ccf639bf698687b3767678ab708c were also stopped and not merged. The new local candidate retains the prior corrections and generically closes the f4ae false-proof classes: stale mutable Map, injected Queue and typed EventEmitter identities; dead-tail reachability; computed and nested secret taint; wrapper persistence multiplicity; exact switch-fallthrough path evidence; and mutated Promise inputs. It also validates dense persistence ordinals and gives only the 8,193-call synthetic boundary test a 60-second timeout without changing the production bound. The corrected topology contains 42 typed channel edges rather than the stale 51 count because nine outer-callsite projections were duplicate paths through dispatchWave or checkAndDispatchNext; all unique wrapper-owner producers, six queue channels, seven consumers, and call links remain. Exact local source, package, 196 focused assertions, real-corpus graph-size, channel-topology, broad and beta.4 retrieval compatibility, indexing-median, warm-retrieval-p95, and deterministic-graph receipts are recorded above. The latest default-fork coverage attempt passed 79 files / 707 tests before the busy local host failed to start the final retrieve-context worker; that exact file then passed 54/54 alone under default forks, proving all 761 unique tests while leaving the one-process full-suite gate honestly pending for clean exact-head CI. Independent review found no blocker on the frozen implementation and corrected governance diff. Exact corrected-head CI, CodeRabbit completion, zero-thread, merge, and publication receipts remain absent until those gates actually pass.' + notes: 'Issue #632 extends the canonical index with authenticated ordered body facts, exact shared queue/job/event channels, bounded two-hop wrapper substitution, concurrency groups, and receiver/type-proven persistence. It began from exact protected next commit 9043320cfa08370e5cdd3911bfb9283005aa9912 and tree f51d6e75e3b806dec6caf9ff0be43fc2ab5713fc. First PR head 9fe3c2448958c6b8cead2452758077fef093cf4e passed all six hosted jobs but independent review blocked twelve semantic-proof classes; later corrective heads c977de03ecba7958d03966df728abed9f1b36ff7 and f4ae64402d89ccf639bf698687b3767678ab708c were also stopped. PR #633 subsequently passed its gates and merged as e7bd30ce384cf743dbda3e8ee7f15b171a0ea649, but the required post-merge real-GoValidate acceptance audit found that wrapper producers did not authenticate the exact Queue.add payload position and switch consumers did not bind their parameter/property discriminant to typed case values. The stop was recorded on #632 and #577, #632 was reopened, and #630 remains dependency-frozen. This corrective candidate uses existing compact index structures to record exact dispatch_payload_argument metadata, a parameter/property discriminant path, and typed case arms; it supports direct selectors and safe immutable same-owner destructuring while reassigned, defaulted, rest, computed, dynamic, duplicate, and accessor-backed data or discriminator properties, including destructured aliases and shorthand, fail closed. The corrected real GoValidate graph proves ResearchAgentService.dispatchDbSync and AssemblyService.dispatchDbSync publish argument 2 into the shared queue channel, while DbSyncWorker.process consumes parameter 0 data.trigger with typed section_complete, assembly_complete, and status_change arms. Exact local source and package measurements remain below the unchanged +3,500-net and 102/165,000/640,000 ceilings; 247 focused assertions and 80 files / 785 full-suite tests pass with 85.85% statements (7,715/8,986), 79.46% branches (6,959/8,757), 92.37% functions (1,369/1,482), and 89.22% lines (6,481/7,264); five indexing trials pass at 13.49-second median / 0.608754512635379 baseline ratio, 100 warm retrieval samples pass at 238.83405145000143 ms p95, and the corrected 60,271,172-byte graph passes. Independent-review, exact-head CI, CodeRabbit, zero-thread, and merge receipts remain pending. No retrieval-v2 work or publication is authorized.' exit_gate: Every retained fact and exact channel edge is deterministic, source-authenticated and mutation-sensitive; false persistence/channel matches remain absent; source, graph-size, indexing, warm-retrieval, package, full-test, exact-head CI, independent-review, and zero-thread gates pass without a v2 result cutover, new dependency, publication, tag, release, Registry metadata, or main target. - id: obligation-driven-retrieval-630 diff --git a/docs/core-reset/scorecard.md b/docs/core-reset/scorecard.md index 0cbba8ca..b5c5f356 100644 --- a/docs/core-reset/scorecard.md +++ b/docs/core-reset/scorecard.md @@ -43,7 +43,7 @@ The schema-validated, share-safe receipt was recorded at tooling checkout `250a6 | Retrieval regression #618 | **Passed** | Restore grounded natural-flow retrieval in one call or at most one bounded recovery without repository-specific rules, graph/index changes, dependencies, or fallback engines | [#618](https://github.com/mohanagy/madar/issues/618) completed through [PR #620](https://github.com/mohanagy/madar/pull/620), merged at `eaa1a8781eda28dad5395d6da378a2cc40bf81fe`; all six exact-head CI jobs, two independent no-blocker reviews, and zero review threads passed | | Retrieval regression #622 | **Passed** | Stabilize equivalent end-to-end report-flow prompts and expose honest asynchronous handoff targets within the unchanged retrieval and package ceilings | [#622](https://github.com/mohanagy/madar/issues/622) completed through [PR #623](https://github.com/mohanagy/madar/pull/623), merged at `6416dbc02cefb3bd79157cf440e420b30dda8cf0`; [six-job CI](https://github.com/mohanagy/madar/actions/runs/30452883659), two exact-head no-blocker reviews, CodeRabbit PASS, and zero unresolved threads | | Retrieval regression #625 | **Passed** | Replace phrase-gated recovery with a generic bounded, graph-coherent evidence skeleton/forest without exceeding the inherited package ceilings | [#625](https://github.com/mohanagy/madar/issues/625) completed through [PR #626](https://github.com/mohanagy/madar/pull/626), merged at `b6562b715133304bd46e537b6f39008bc1e02095`; [six-job CI](https://github.com/mohanagy/madar/actions/runs/30533140531), independent exact-head review, CodeRabbit PASS, and zero unresolved threads | -| Semantic execution index #632 | **In progress** | Authenticated ordered body facts, exact async channels and receiver/type-proven persistence pass every source, graph, indexing, latency, package, CI, review and zero-thread gate | [#632](https://github.com/mohanagy/madar/issues/632); protected base `9043320cfa08370e5cdd3911bfb9283005aa9912`; three PR heads were stopped and not merged; the new local correction is qualified, while exact-head CI/review/merge remain pending | +| Semantic execution index #632 | **Reopened — corrective work in progress** | Authenticated ordered body facts, exact async channels and receiver/type-proven persistence pass every source, graph, indexing, latency, package, CI, review and zero-thread gate | [#632](https://github.com/mohanagy/madar/issues/632); PR #633 merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, then the mandatory real-GoValidate audit exposed missing exact payload/discriminant binding; the correction is locally proven while fresh full-suite/review/CI/merge gates remain pending | | Obligation-driven retrieval #630 | **Pending** | Return a complete authenticated workflow dossier or exact missing obligations within unchanged budgets | [#630](https://github.com/mohanagy/madar/issues/630); blocked on #632 | | No-fallback qualification #631 | **Pending** | Installed exact-head package matches or beats the strongest frozen baseline and requires zero repository-tool fallback | [#631](https://github.com/mohanagy/madar/issues/631); blocked on #632 and #630 | | External validation | **Deferred** | Activation, retention, and paid-intent evidence remains required for later stable claims, not this beta | No external-validation claim in `0.40.0-beta.4` | @@ -203,7 +203,9 @@ The following contract facts are historical. Issues #610 and #612, together with - Delivery is blocked above four new production files, 3,500 net new production lines, 1.5x the beta.4 GoValidate graph size, 1.25x the beta.4 same-machine indexing median, or warm retrieval p95 greater than or equal to 500 ms. Package ceilings remain 102 files / 165,000 packed / 640,000 unpacked bytes. - The package whitelist may remove only `examples/why-madar.md` and `CHANGELOG.md`; the repository files remain present, and version, scripts, dependencies, package lock, publication and public surface cannot change. - #632 owns authenticated ordered body facts, exact queue/job/event channels, bounded two-hop wrapper substitution, concurrency groups, and receiver/type-proven persistence. Retrieval-result v2, obligation planning, response dossier generation, comparator claims, provider activity, npm publication, GitHub Release, Registry metadata, tags, and `main` are outside this phase. -- First PR head `9fe3c2448958c6b8cead2452758077fef093cf4e` passed all six hosted jobs but was independently blocked on twelve semantic-proof classes; corrective heads `c977de03ecba7958d03966df728abed9f1b36ff7` and `f4ae64402d89ccf639bf698687b3767678ab708c` were also stopped. None was merged. The new local correction retains their fixes and closes the nine f4ae false-proof classes plus computed-key alias confidentiality and dense persistence-ordinal integrity with dedicated regressions. It measures 44 production files / 15,934 LOC at `+3,667/-187/net +3,480`; 102 package files / 145,254 packed / 638,736 unpacked bytes; and a 60,267,088-byte real GoValidate graph, ratio `1.2291718874663748`, containing 12,313 nodes / 32,717 edges / six exact queue channels / 42 typed channel edges. The 51-to-42 change removes only nine duplicate outer-callsite projections through `dispatchWave` and `checkAndDispatchNext`; every unique producer, channel, consumer, and call link remains. Five indexing trials have 13.17-second median, ratio `0.5943140794223827`; 100 warm retrieval samples have 156.7398119999998 ms median / 177.5851354499996 ms p95 / 183.07708300000013 ms maximum. Repeated graph SHA-256 is `77270a6f0330a3ce85fbc42b90e7a3e99f8bf37776f6e65f5da8aad1bad3caaf`; beta.4 v1 retrieval remains byte-identical at 15,294 bytes and SHA-256 `87b4ef75473834708b20f1d2580b31470a710d797d7bdf55eee1d0876827a173`. +- Earlier stopped heads remain immutable history. PR #633 later passed its gates and merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, but the mandatory post-merge real-GoValidate audit exposed a narrower acceptance gap: wrapper publishers did not authenticate the exact `Queue.add` payload argument and switch consumers did not bind a parameter/property selector to typed case values. #632 was reopened, the stop was recorded on #632 and RFC #577, and #630 remains frozen. +- The new correction records `dispatch_payload_argument`, parameter/property discriminant paths, and typed case arms using existing compact index structures. Direct selectors and safe immutable same-owner destructuring pass; reassigned, defaulted, rest, computed, dynamic, duplicate, and accessor-backed `data` or discriminator properties—including destructured aliases and shorthand—fail closed. The frozen GoValidate graph now proves argument 2 for both `ResearchAgentService.dispatchDbSync` and `AssemblyService.dispatchDbSync`, and parameter 0 `data.trigger` with typed `section_complete`, `assembly_complete`, and `status_change` cases in `DbSyncWorker.process`. +- The correction measures 44 production files / 15,719 LOC at `+3,462/-197/net +3,265`, below the unchanged net `+3,500` ceiling; 102 package files / 149,453 packed / 639,867 unpacked bytes; and a 60,271,172-byte real GoValidate graph, ratio `1.2292551823152718`, with 12,313 nodes / 32,717 edges / six exact queue channels / 42 typed channel edges. Its graph SHA-256 is `569af2dcd681c4db48124a47bceac7036a94b344f2a2f88e8cb35f6120711610`. Focused verification passes 247/247; the coverage suite passes 80 files / 785 tests at 85.85% statements (7,715/8,986), 79.46% branches (6,959/8,757), 92.37% functions (1,369/1,482), and 89.22% lines (6,481/7,264). The frozen corpus is attested as 6,889 files / 177,796,450 bytes / SHA-256 `3fa9a0a3edc13cf1439d572292601fff43c43a94f7a50948349f9a69123fa7a7`; five fresh indexing trials pass at 13.49-second median / `0.608754512635379` baseline ratio, and 100 warm retrieval samples pass at 238.83405145000143 ms p95. Independent-review, exact-head CI, CodeRabbit, zero-thread, and merge receipts remain pending. - These are local corrective-candidate measurements, not a final receipt. Corrected-head commit/tree, all-six CI, independent no-blocker review, CodeRabbit completion, zero unresolved threads, merge commit, and publication remain open until those exact gates pass. ### Successors #630 and #631 (pending) diff --git a/docs/designs/2026-07-19-core-reset.md b/docs/designs/2026-07-19-core-reset.md index 3d7bba58..51b9c425 100644 --- a/docs/designs/2026-07-19-core-reset.md +++ b/docs/designs/2026-07-19-core-reset.md @@ -471,6 +471,21 @@ The CLI remains narrow and lazy-loaded: These six names are an allowlist, not a quota. Existing names are preserved only when their meaning remains valid; removed names do not survive as aliases. +## 2026-07-31 semantic execution correction + +Issue #632 is the prerequisite index layer for obligation-driven retrieval. It authenticates operations inside function bodies, their numeric order and control context, shared queue/job/event channels, bounded wrapper substitution, concurrency groups, and receiver/type-proven persistence. Retrieval planning and the v2 response dossier remain owned by #630 and cannot begin while #632 is open. + +PR #633 merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, then the required real-GoValidate audit exposed an incomplete producer/consumer join. A wrapper publisher proved the queue channel but not which outer argument became the channel payload; a switch consumer proved cases but not the parameter/property selector consumed by those cases. #632 was reopened and #630 was dependency-stopped. + +The correction adds no schema version, compatibility alias, dependency, repository-specific map, or retrieval semantic. It reuses compact authenticated index values to record: + +- the exact outer dispatch payload argument on `publishes_to`; +- a closed parameter-plus-property discriminant path for a switch; +- typed case values, preserving the distinction between values such as string `"1"` and number `1`; and +- an evidence range that includes safe destructuring aliases, so later mutation invalidates the proof. + +Direct selectors and immutable same-owner object destructuring are accepted. Reassignment, defaults, rest bindings, computed properties, dynamic cases, and duplicate typed cases fail closed. Accessor-backed `data` or discriminator properties—including destructured aliases and shorthand—also fail closed. On the frozen GoValidate corpus the corrected graph joins both db-sync publishers at argument 2 to the consumer selector parameter 0 `data.trigger`, including typed `section_complete`, `assembly_complete`, and `status_change` arms. The coverage suite passes 80 files / 785 tests at 85.85% statements (7,715/8,986), 79.46% branches (6,959/8,757), 92.37% functions (1,369/1,482), and 89.22% lines (6,481/7,264). #632 completes only after the corrective exact head passes every source, package, graph-size, performance, full-suite, independent-review, CI, and zero-thread gate. + ## Migration and compatibility - `main` and `0.32.x` are maintenance-only while the RFC is active. diff --git a/docs/roadmap.md b/docs/roadmap.md index c304a079..d81636f5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -167,7 +167,9 @@ The published npm artifact is 102 files / 159,937 packed / 639,875 unpacked byte [#632](https://github.com/mohanagy/madar/issues/632) starts from exact protected `next` commit `9043320cfa08370e5cdd3911bfb9283005aa9912` and tree `f51d6e75e3b806dec6caf9ff0be43fc2ab5713fc`, and its PR target is `next`. It adds compact authenticated body facts, numeric order and control, exact queue/job/event channel topology, bounded two-hop wrapper substitution, concurrency groups, and receiver/type-proven persistence. It does not cut retrieval output to v2. -The active delivery limits are no more than four new production files, no more than 3,500 net new production lines, no more than 1.5x the beta.4 GoValidate graph size, indexing median no slower than 1.25x beta.4 on the same machine, warm retrieval p95 strictly below 500 ms, and the unchanged package ceilings of 102 files / 165,000 packed / 640,000 unpacked bytes. No dependency, provider activity, publication, GitHub Release, Registry metadata, tag, or `main` action is authorized. Three superseded PR heads remain stopped and unmerged. The current local correction is within every source, package, graph, indexing, warm-retrieval and deterministic-output ceiling; its exact measurements are recorded in the removal manifest. Exact-head CI, independent review, CodeRabbit completion, zero unresolved threads, merge, and publication receipts remain pending and must not be inferred from local work. +The active delivery limits are no more than four new production files, no more than 3,500 net new production lines, no more than 1.5x the beta.4 GoValidate graph size, indexing median no slower than 1.25x beta.4 on the same machine, warm retrieval p95 strictly below 500 ms, and the unchanged package ceilings of 102 files / 165,000 packed / 640,000 unpacked bytes. No dependency, provider activity, publication, GitHub Release, Registry metadata, tag, or `main` action is authorized. + +PR #633 merged as `e7bd30ce384cf743dbda3e8ee7f15b171a0ea649`, but the mandatory post-merge real-GoValidate audit found that wrapper producers lacked authenticated exact payload positions and switch consumers lacked an authenticated parameter/property-to-typed-case binding. #632 was reopened and #630 remains frozen. The local correction now proves `dispatch_payload_argument: 2` for both GoValidate db-sync publishers and parameter 0 `data.trigger` with typed terminal cases for the consumer. It remains below the unchanged source, package, and graph-size ceilings; focused verification passes 247/247, and the coverage suite passes 80 files / 785 tests at 85.85% statements (7,715/8,986), 79.46% branches (6,959/8,757), 92.37% functions (1,369/1,482), and 89.22% lines (6,481/7,264). Five fresh indexing trials pass at 13.49-second median / `0.608754512635379` baseline ratio, and 100 warm retrieval samples pass at 238.83405145000143 ms p95. Independent-review, exact-head CI, CodeRabbit, zero-thread, and merge receipts remain pending. ## Pending — obligation-driven retrieval #630 diff --git a/src/adapters/typescript/execution.ts b/src/adapters/typescript/execution.ts index 52659fd1..c1cb3de4 100644 --- a/src/adapters/typescript/execution.ts +++ b/src/adapters/typescript/execution.ts @@ -1,22 +1,45 @@ import { createHash } from 'node:crypto' import ts from 'typescript' const { - isArrowFunction: isArrow, isBinaryExpression: isBinary, - isCallExpression: isCall, isFunctionExpression: isFunction, - isIdentifier, isIfStatement: isIf, isNewExpression: isNew, - isNumericLiteral: isNumeric, isParameter, isPropertyAccessExpression: isAccess, - isPropertyDeclaration: isPropertyDecl, isTypeReferenceNode: isTypeReference, - isVariableDeclaration: isVariable, + forEachChild: fc, isArrayLiteralExpression: iA, isArrowFunction: iR, + isAsExpression: iAs, isAwaitExpression: iAw, isBinaryExpression: iB, + isBindingElement: iBe, isBlock: iBl, isCallExpression: iC, + isCaseClause: iK, isClassDeclaration: iCl, + isConstructorDeclaration: iCd, isDefaultClause: iDc, + isDeleteExpression: iDe, isElementAccessExpression: iE, + isEnumMember: iEm, isExpressionStatement: iEs, + isForInStatement: iFi, isForOfStatement: iFo, + isForStatement: iFs, isFunctionDeclaration: iFd, + isFunctionExpression: iF, isFunctionLike: iFl, isIdentifier: iI, + isIfStatement: iJ, isMethodDeclaration: iMd, isNewExpression: iN, + isNonNullExpression: iNl, isObjectLiteralExpression: iO, + isNumericLiteral: iM, isParameter: iP, isPropertyAccessExpression: iX, + isOmittedExpression: iOm, + isParenthesizedExpression: iPa, isPostfixUnaryExpression: iPf, + isPropertyDeclaration: iD, isTypeReferenceNode: iT, + isPrefixUnaryExpression: iU, isReturnStatement: iRe, + isShorthandPropertyAssignment: iSh, isStatement: iSt, + isSatisfiesExpression: iSa, isSourceFile: iSf, + isSpreadElement: iSp, isStringLiteralLike: iSl, + isSwitchStatement: iSw, isThrowStatement: iTh, + isTryStatement: iTr, isTypeNode: iTn, + isTypeAssertionExpression: iTa, isVariableDeclaration: iV, + isVoidExpression: iVo, isWhileStatement: iWh, + isYieldExpression: iYi, isGetAccessorDeclaration: iGa, + isSetAccessorDeclaration: iSd, isTypeOfExpression: iTy, + isDoStatement: iDo, isConditionalExpression: iCo, } = ts -const K = ts.SyntaxKind +const K = ts.SyntaxKind, NF = ts.NodeFlags, TF = ts.TypeFlags import { - decodeIndexBodyFactTable, encodeIndexBodyFactTable, INDEX_BODY_FACT_CONTROL_LIMIT, - IndexBodyFactBoundsError, indexBodyFactId, indexChannelId, + decodeIndexBodyFactTable as dt, encodeIndexBodyFactTable as eb, + INDEX_BODY_FACT_CONTROL_LIMIT as ICL, IndexBodyFactBoundsError as BE, + indexBodyFactId as bf, indexChannelId as ki, } from '../../domain/index/model.js' import type { IndexBodyFact, IndexCallFact, IndexChannelNode, IndexControlFrame, IndexDiagnostic, IndexEdge, IndexFactEvidence, IndexFactSource, - IndexChannelTransport, IndexPersistenceOperation, IndexRange, IndexSymbol, IndexValue, + IndexChannelTransport, IndexPersistenceOperation, IndexRange, IndexScalarValue, + IndexSymbol, IndexValue, } from '../../domain/index/model.js' export type CollectExecutionInput = { program: ts.Program; sourceFiles: readonly ts.SourceFile[] @@ -31,7 +54,8 @@ export type CollectExecutionResult = { type Confidence = 'high' | 'medium' | 'low' type OwnerSpan = { s: IndexSymbol; a: number; b: number } type ImportBinding = { i: string; m: string; n: boolean } -type CallSite = readonly [targetId: string, arguments: readonly IndexValue[], node: EffectWitness] +type CallSite = readonly [targetId: string, arguments: readonly IndexValue[], + node: EffectWitness, strictArguments: readonly IndexValue[]] type QueueTransport = Extract type QueueOrigin = readonly [key: IndexValue, transport: QueueTransport] type MapQueueEntry = readonly [key: ts.Expression, file: FileContext, queueKey: ts.Expression, transport: QueueTransport] @@ -39,8 +63,9 @@ type EmitterScope = readonly [scope: string, transport: 'node-event-emitter' | ' type EffectWitness = ts.CallExpression | ts.NewExpression type BullEffect = readonly [ kind: 'bull-publish' | 'bull-consume', queue: IndexValue, - endpoint: IndexValue, transport: QueueTransport, scope: undefined, - witness: EffectWitness, confidence: Confidence, source: IndexFactSource] + endpoint: IndexValue, transport: QueueTransport, payload: IndexValue | undefined, + witness: EffectWitness, confidence: Confidence, source: IndexFactSource, + payloadArgument: number | undefined] type EventEffect = readonly [ kind: 'event-publish' | 'event-consume', event: IndexValue, handler: IndexValue | undefined, @@ -67,273 +92,178 @@ type CollectionState = { r: Map mq: Map> em: Map; nq: Map> - fs: Map + fs: Map; x: Set +} +const EX = 'expression', AR = 'arguments', IZ = 'initializer', + DC = 'declarations', VD = 'valueDeclaration', OT = 'operatorToken', + AX = 'argumentExpression', EL = 'elseStatement', TH = 'thenStatement', + CB = 'caseBlock', ST = 'statements', PA = 'parameters', + DD = 'dotDotDotToken', QD = 'questionDotToken', PR = 'parent', + LN = 'length', CN = 'condition', OP = 'operator', LE = 'elements', + CL = 'clauses', IC = 'includes', GT = 'getStart', + TL = 'getTypeAtLocation', BP = 'bull-publish', BC = 'bull-consume', + EP = 'event-publish', EC = 'event-consume', WS = 'wrapper-summary', + PU = 'publishes_to', RT = 'routes_through', CY = 'consumed_by', + FD = 'framework-decorator', PE = 'persistence' +const gs = (a: ts.Node): ts.SourceFile => a.getSourceFile() +const ck = (a: CollectionState): ts.TypeChecker => a.i.checker +const lb = (a: ts.Declaration): boolean => { + const f = gs(a) + return f.isDeclarationFile + && /\/typescript\/lib\/lib\..+\.d\.ts$/u.test( + f.fileName.replaceAll('\\', '/')) } // Internal helpers and local bindings are abbreviated because their emitted // names count against the protected npm ceiling; public/schema names stay explicit. const VDEP = 5, VELE = 32 -const SBYT = 512, TBYT = 256 -const WHOP = 2 +const SBYT = 512, TBYT = 256, bl = Buffer.byteLength, js = JSON.stringify +const WHOP = 2, U0 = void 0 const FMAX = 8_192, EMAX = 8_192 const FSM = new Set(['node:fs', 'node:fs/promises', 'fs', 'fs/promises']) -const FSO = { - readFile: 'file_read', readFileSync: 'file_read', - opendir: 'file_read', readdir: 'file_read', appendFile: 'file_write', - appendFileSync: 'file_write', copyFile: 'file_write', copyFileSync: 'file_write', - rename: 'file_write', writeFile: 'file_write', writeFileSync: 'file_write', - rm: 'delete', rmSync: 'delete', unlink: 'delete', unlinkSync: 'delete', -} as const satisfies Record -const TOO = { - find: 'read', findOne: 'read', findOneBy: 'read', findMany: 'read', - findUnique: 'read', count: 'read', aggregate: 'read', insert: 'create', - save: 'upsert', update: 'update', - updateOne: 'update', updateMany: 'update', delete: 'delete', - deleteOne: 'delete', deleteMany: 'delete', remove: 'delete', upsert: 'upsert', - transaction: 'transaction', -} as const satisfies Record -const PRO = { - findUnique: 'read', findFirst: 'read', findMany: 'read', count: 'read', - aggregate: 'read', groupBy: 'read', create: 'create', createMany: 'create', - update: 'update', updateMany: 'update', delete: 'delete', - deleteMany: 'delete', upsert: 'upsert', $transaction: 'transaction', -} as const satisfies Record -const PMC = { - all: 'all_or_first_rejection', allSettled: 'all_settled', - any: 'first_fulfilled', race: 'first_settled', -} as const -const LFL = new Map([ - [K.AmpersandAmpersandToken, ['logical_and', 'truthy']], - [K.BarBarToken, ['logical_or', 'falsy']], - [K.QuestionQuestionToken, ['nullish', 'nullish']], -]) -const AMU = new Map([ - ['push', 'append'], ['unshift', 'append'], ['pop', 'remove'], - ['shift', 'remove'], ['splice', 'remove'], -]) -const FORD: Readonly> = { - condition: 0, loop: 1, parallel: 2, call: 3, literal: 4, - mutation: 5, persistence: 6, return: 7, throw: 8, -} -const AOP = new Set([ - K.EqualsToken, K.PlusEqualsToken, - K.MinusEqualsToken, K.AsteriskEqualsToken, - K.AsteriskAsteriskEqualsToken, K.SlashEqualsToken, - K.PercentEqualsToken, K.LessThanLessThanEqualsToken, - K.GreaterThanGreaterThanEqualsToken, K.GreaterThanGreaterThanGreaterThanEqualsToken, - K.AmpersandEqualsToken, K.BarEqualsToken, K.CaretEqualsToken, - K.BarBarEqualsToken, K.AmpersandAmpersandEqualsToken, - K.QuestionQuestionEqualsToken, -]) -const COP = new Set([ - K.EqualsEqualsToken, K.EqualsEqualsEqualsToken, - K.ExclamationEqualsToken, K.ExclamationEqualsEqualsToken, - K.LessThanToken, K.LessThanEqualsToken, - K.GreaterThanToken, K.GreaterThanEqualsToken, -]) -const AIM = new Set([ - 'every', 'filter', 'find', 'findIndex', 'flatMap', - 'forEach', 'map', 'reduce', 'reduceRight', 'some', -]) -const SLT = new Set([ - K.StringLiteral, K.NumericLiteral, - K.BigIntLiteral, K.RegularExpressionLiteral, - K.NoSubstitutionTemplateLiteral, K.TemplateHead, - K.TemplateMiddle, K.TemplateTail, -]) +const FSO = { readFile: 'file_read', readFileSync: 'file_read', opendir: 'file_read', readdir: 'file_read', appendFile: 'file_write', appendFileSync: 'file_write', copyFile: 'file_write', copyFileSync: 'file_write', rename: 'file_write', writeFile: 'file_write', writeFileSync: 'file_write', rm: 'delete', rmSync: 'delete', unlink: 'delete', unlinkSync: 'delete' } as const satisfies Record +const TOO = { find: 'read', findOne: 'read', findOneBy: 'read', findMany: 'read', findUnique: 'read', count: 'read', aggregate: 'read', insert: 'create', save: 'upsert', update: 'update', updateOne: 'update', updateMany: 'update', delete: 'delete', deleteOne: 'delete', deleteMany: 'delete', remove: 'delete', upsert: 'upsert', transaction: 'transaction' } as const satisfies Record +const PRO = { findUnique: 'read', findFirst: 'read', findMany: 'read', count: 'read', aggregate: 'read', groupBy: 'read', create: 'create', createMany: 'create', update: 'update', updateMany: 'update', delete: 'delete', deleteMany: 'delete', upsert: 'upsert', $transaction: 'transaction' } as const satisfies Record +const PMC = { all: 'all_or_first_rejection', allSettled: 'all_settled', any: 'first_fulfilled', race: 'first_settled' } as const +const LFL = new Map([[K.AmpersandAmpersandToken, ['logical_and', 'truthy']], [K.BarBarToken, ['logical_or', 'falsy']], [K.QuestionQuestionToken, ['nullish', 'nullish']]]) +const AMU = new Map([['push', 'append'], ['unshift', 'append'], ['pop', 'remove'], ['shift', 'remove'], ['splice', 'remove']]) +const FORD: Readonly> = { condition: 0, loop: 1, parallel: 2, call: 3, literal: 4, mutation: 5, persistence: 6, return: 7, throw: 8 } +const AOP = new Set([K.EqualsToken, K.PlusEqualsToken, K.MinusEqualsToken, K.AsteriskEqualsToken, K.AsteriskAsteriskEqualsToken, K.SlashEqualsToken, K.PercentEqualsToken, K.LessThanLessThanEqualsToken, K.GreaterThanGreaterThanEqualsToken, K.GreaterThanGreaterThanGreaterThanEqualsToken, K.AmpersandEqualsToken, K.BarEqualsToken, K.CaretEqualsToken, K.BarBarEqualsToken, K.AmpersandAmpersandEqualsToken, K.QuestionQuestionEqualsToken]) +const SEQ = new Set([K.EqualsEqualsEqualsToken, K.ExclamationEqualsEqualsToken]) +const PT = new Set([K.StringKeyword, K.NumberKeyword, K.BooleanKeyword, K.BigIntKeyword, K.SymbolKeyword, K.NullKeyword, K.UndefinedKeyword, K.VoidKeyword]) +const COP = new Set([K.EqualsEqualsToken, K.EqualsEqualsEqualsToken, K.ExclamationEqualsToken, K.ExclamationEqualsEqualsToken, K.LessThanToken, K.LessThanEqualsToken, K.GreaterThanToken, K.GreaterThanEqualsToken]) +const AIM = new Set(['every', 'filter', 'find', 'findIndex', 'flatMap', 'forEach', 'map', 'reduce', 'reduceRight', 'some']) +const SLT = new Set([K.StringLiteral, K.NumericLiteral, K.BigIntLiteral, K.RegularExpressionLiteral, K.NoSubstitutionTemplateLiteral, K.TemplateHead, K.TemplateMiddle, K.TemplateTail]) const SNM = /(?:api[_-]?key|authorization|cookie|credential|database[_-]?url|dsn|jwt|passwd|password|private[_-]?key|secret|token)/i const SVAL = /^(?:bearer\s+|gh[pousr]_|github_pat_|sk-(?:live|test|proj)-|xox[baprs]-|[a-z][a-z\d+.-]*:\/\/[^/\s:@]+:[^@\s/]+@|eyJ[\w-]+\.[\w-]+\.[\w-]+$)/i -function hash(a: string): string { return createHash('sha256').update(a, 'utf8').digest('hex') } -function bd(d: string, b = TBYT): string { - if (Buffer.byteLength(d, 'utf8') <= b) return d - let c = '' - for (const a of d) { - if (Buffer.byteLength(c + a, 'utf8') > b) break - c += a - } - return c -} -function st(d: ts.Node, sf: ts.SourceFile): string { - const a = ts.createScanner(ts.ScriptTarget.Latest, true, sf.languageVariant, d.getText(sf)) - const c: string[] = [] - for (let b = a.scan(); b !== K.EndOfFileToken; b = a.scan()) - c.push(SLT.has(b) ? '' : a.getTokenText()) - return bd(c.join(' ')) -} -function ct(b: string, a: string): number { return b < a ? -1 : b > a ? 1 : 0 } -function co(d: readonly number[], c: readonly number[]): number { - const e = Math.min(d.length, c.length) - for (let b = 0; b < e; b += 1) { - const a = (d[b] ?? 0) - (c[b] ?? 0) - if (a !== 0) return a - } - return d.length - c.length -} -function ro(a: ts.Node, sf: ts.SourceFile): IndexRange { return rf(sf, a.getStart(sf, false), a.getEnd()) } -function rf(sf: ts.SourceFile, c: number, end: number): IndexRange { - const a = sf.getLineAndCharacterOfPosition(c), b = sf.getLineAndCharacterOfPosition(end) - return { - start: { line: a.line + 1, column: a.character + 1 }, - end: { line: b.line + 1, column: b.character + 1 }, - } -} -function stmt(b: ts.Node): ts.Node { - let a: ts.Node = b - while (a.parent) { - if (ts.isStatement(a) || isVariable(a) - || isPropertyDecl(a) || isParameter(a)) return a - if (ts.isSourceFile(a.parent)) return a - a = a.parent - } - return a -} -function ev( - g: ts.Node, sf: ts.SourceFile, f: string, - d: ts.Node = stmt(g), c?: OwnerSpan, -): IndexFactEvidence { - const a = d.getStart(sf, false), b = d.getEnd() - const e = c ? Math.max(a, c.a) : a - const end = c ? Math.min(b, c.b) : b - return { - file_id: f, range: ro(g, sf), statement_range: rf(sf, e, end), - excerpt_sha256: hash(sf.text.slice(e, end)), - } -} -function fo(c: IndexBodyFact['kind'], a: ts.Node, b = 0): readonly number[] { - const sf = a.getSourceFile() - return [a.getStart(sf, false), FORD[c], a.getEnd(), b] -} -function fb(a: string, g: IndexBodyFact['kind'], h: ts.Node, d: FileContext, z: readonly IndexControlFrame[], e: { - c?: Confidence; s?: IndexFactSource; n?: ts.Node; o?: number -} = {}): Pick { - const b = d.os.find((j) => j.s.id === a) - const i = fo(g, h, (e.o ?? 0) + d.v * (EMAX + 1)) - const f = ev(h, d.sf, d.id, e.n, b) - return { - id: indexBodyFactId(a, g, i, f.excerpt_sha256), - owner_symbol_id: a, order: i, evidence: f, - control: [...z], - confidence: e.c ?? 'high', - source: e.s ?? 'typescript-syntactic', - } +const hash = (a: string): string => + createHash('sha256').update(a, 'utf8').digest('hex') +function bd(d: string, b = TBYT): string { if (bl(d) <= b) return d; let c = ''; for (const a of d) { if (bl(c + a) > b) break; c += a } return c } +function st(d: ts.Node, sf: ts.SourceFile): string { const a = ts.createScanner(ts.ScriptTarget.Latest, true, sf.languageVariant, d.getText(sf)), c: string[] = []; for (let b = a.scan(); b !== K.EndOfFileToken; b = a.scan()) c.push(SLT.has(b) ? '' : a.getTokenText()); return bd(c.join(' ')) } +const ct = (b: string, a: string): number => b < a ? -1 : b > a ? 1 : 0 +function co(d: readonly number[], c: readonly number[]): number { const e = Math.min(d[LN], c[LN]); for (let b = 0; b < e; b += 1) { const a = (d[b] ?? 0) - (c[b] ?? 0); if (a !== 0) return a } return d[LN] - c[LN] } +const ro = (a: ts.Node, sf: ts.SourceFile): IndexRange => + rf(sf, a[GT](sf, false), a.getEnd()) +function rf(sf: ts.SourceFile, c: number, end: number): IndexRange { const a = sf.getLineAndCharacterOfPosition(c), b = sf.getLineAndCharacterOfPosition(end); return { start: { line: a.line + 1, column: a.character + 1 }, end: { line: b.line + 1, column: b.character + 1 } } } +function stmt(b: ts.Node): ts.Node { let a: ts.Node = b; while (a[PR]) { if (iSt(a) || iV(a) || iD(a) || iP(a) || iSf(a[PR])) return a; a = a[PR] } return a } +function sc(b: ts.Node): ts.Node | undefined { let a = b; while (a[PR] && !iBl(a[PR]) && !iSf(a[PR]) && !iK(a[PR]) && !iDc(a[PR])) a = a[PR]; return a[PR] } +function dm(d: ts.Node, u: ts.Node): boolean { if (gs(d) !== gs(u)) return true; const p = sc(d); if (!p) return false; let a = u; while (a[PR] && a[PR] !== p) a = a[PR]; return a[PR] === p && stmt(d).getEnd() <= a[GT](gs(u), false) } +function ev(g: ts.Node, sf: ts.SourceFile, f: string, d: ts.Node = stmt(g), + c?: OwnerSpan, h?: ts.Node): IndexFactEvidence { + const a = (h ?? d)[GT](sf, false), b = d.getEnd() + const e = c ? Math.max(a, c.a) : a, end = c ? Math.min(b, c.b) : b + return { file_id: f, range: ro(g, sf), statement_range: rf(sf, e, end), + excerpt_sha256: hash(sf.text.slice(e, end)) } +} +function fo(c: IndexBodyFact['kind'], a: ts.Node, b = 0): readonly number[] { const sf = gs(a); return [a[GT](sf, false), FORD[c], a.getEnd(), b] } +function fb(a: string, g: IndexBodyFact['kind'], h: ts.Node, d: FileContext, + z: readonly IndexControlFrame[], e: { c?: Confidence; s?: IndexFactSource + n?: ts.Node; o?: number; a?: ts.Node | undefined } = {}): + Pick { + const b = d.os.find((j) => j.s.id === a) + const i = fo(g, h, (e.o ?? 0) + d.v * (EMAX + 1)), f = ev(h, d.sf, d.id, e.n, b, e.a) + return { id: bf(a, g, i, f.excerpt_sha256), owner_symbol_id: a, + order: i, evidence: f, control: [...z], + confidence: e.c ?? 'high', source: e.s ?? 'typescript-syntactic' } } type ConditionKind = Extract['condition_kind'] type BranchArm = Extract['arm'] +type SwitchProof = readonly [IndexValue, ts.Node | undefined] type MutationOperation = Extract['operation'] -function ac( - h: string, e: ConditionKind, i: ts.Expression, - f: FileContext, ctx: CollectionState, z: readonly IndexControlFrame[], - g: ts.Node, -): ReturnType { - const j = fb(h, 'condition', i, f, z, { n: g }) +function ac(h: string, e: ConditionKind, i: ts.Expression, f: FileContext, q0: CollectionState, + z: readonly IndexControlFrame[], g: ts.Node, p?: SwitchProof): ReturnType { + const j = fb(h, 'condition', i, f, z, { n: g, a: p?.[1] }) let a = uw(i), d = false - while (ts.isPrefixUnaryExpression(a) - && a.operator === K.ExclamationToken) { - d = !d - a = uw(a.operand) + while (iU(a) && a[OP] === K.ExclamationToken) { d = !d; a = uw(a.operand) } + const b = iB(a) && COP.has(a[OT].kind) ? a : null + q0.r.set(j.id, b ? [b[OT].kind, rv(b.left, f, q0), rv(b.right, f, q0), d] : [K.Unknown, rv(a, f, q0), U0, d]) + af(q0, { ...j, kind: 'condition', condition_kind: e, test: p?.[0] ?? rv(i, f, q0) }) + return j } +const br = ( + z: readonly IndexControlFrame[], a: string, arm: BranchArm, +): IndexControlFrame[] => [...z, { + kind: 'branch', controller_fact_id: a, arm, +}] +function sa(a: ts.CaseClause, f: FileContext, c: CollectionState): `case:${string}` { + const k = sk(a[EX], f, c), v = k ? `case:${Buffer.from(k).toString('base64url')}` : '' + return k && bl(v) <= 96 ? v as `case:${string}` : `case:${hash(`${a[EX].getText(f.sf)}:${a.pos}`).slice(0, 16)}` } +function sd(a: ts.SwitchStatement, o: string, f: FileContext, c: CollectionState): SwitchProof | undefined { + let x = uw(a[EX]), q: ts.Node | undefined; const r: string[] = [] + if (iI(x)) { + const s = sy(x, c), d = s?.[VD] + if (s && d && !c.u.has(s) && iBe(d) && ts.isObjectBindingPattern(d[PR]) && iV(d[PR][PR]) && !d[DD] && !d[IZ]) { + const v = d[PR][PR], k = pn((d.propertyName ?? d.name) as ts.PropertyName), n = v[PR][PR] + const w = k !== null && v[IZ] ? ck(c)[TL](v[IZ]).getProperty(k) : U0 + if (k !== null && !w?.[DC]?.some((e) => iGa(e) || iSd(e)) + && v[IZ] && !!(v[PR].flags & NF.Const) && dm(v, a)) { + r.unshift(k); x = uw(v[IZ]); q = n + } + } } - const b = isBinary(a) && COP.has(a.operatorToken.kind) - ? a : null - ctx.r.set(j.id, b - ? [b.operatorToken.kind, - rd(b.left, f, ctx, { c: true }), - rd(b.right, f, ctx, { c: true }), d] - : [K.Unknown, rd(a, f, ctx, { c: true }), undefined, d]) - af(ctx, { - ...j, - kind: 'condition', - condition_kind: e, - test: rd(i, f, ctx, { c: true }), - }) - return j -} -function br(z: readonly IndexControlFrame[], a: string, arm: BranchArm): IndexControlFrame[] { - return [...z, { kind: 'branch', controller_fact_id: a, arm }] + while (iX(x) && !x[QD] && r[LN] < VDEP) { + const s = sy(x.name, c) + if (s?.[DC]?.some((d) => iGa(d) || iSd(d))) return U0 + r.unshift(x.name.text); x = uw(x[EX]) } + const y = iI(x) ? sy(x, c) : U0 + const ps = y?.[DC]?.filter(iP) ?? [], p = ps[LN] === 1 ? ps[0] : U0 + if (!y || !p || !iP(p) || p[IZ] || p[DD] || !iI(x) || !pu(p, x, c) || pi(p) < 0 || ca(p[PR], f, c)?.id !== o || r[LN] === 0 || r.some((k) => bl(k) > SBYT)) return U0 + const l = a[CB][CL].filter(iK).map((g) => sk(g[EX], f, c)) + if (l.some((k) => !k || bl(`case:${Buffer.from(k).toString('base64url')}`) > 96) || new Set(l).size !== l[LN] || a[CB][CL].filter(iDc)[LN] > 1) return U0 + return [{ kind: 'template', + parts: [{ kind: 'parameter', position: pi(p) }, + ...r.map((v) => ({ kind: 'literal' as const, value: v })), + ]}, q] } function am( g: string, b: ts.Node, k: MutationOperation, - a: ts.Expression, e: FileContext, ctx: CollectionState, + a: ts.Expression, e: FileContext, q0: CollectionState, z: readonly IndexControlFrame[], h?: ts.Expression, d = 0, ): void { const j = st(a, e.sf), raw = a.getText(e.sf) const i = uw(a) - const key = ts.isElementAccessExpression(i) && i.argumentExpression - ? rd(i.argumentExpression, e, ctx, { c: true }) : null - const f = SNM.test(raw) || key !== null - && (key.kind !== 'literal' || typeof key.value === 'string' && SNM.test(key.value)) - af(ctx, { + const key = iE(i) && i[AX] ? rv(i[AX], e, q0) : null + const f = SNM.test(raw) || key !== null && (key.kind !== 'literal' || typeof key.value === 'string' && SNM.test(key.value)) + af(q0, { ...fb(g, 'mutation', b, e, z, { o: d }), kind: 'mutation', operation: k, target: f ? `redacted:${hash(raw).slice(0, 16)}` : bd(j), ...(h ? { - value: rd(h, e, ctx, { + value: rd(h, e, q0, { c: true, s: f, }), } : {}), }) } -function ai(a: string, c: ts.Node, d: FileContext, ctx: CollectionState, z: readonly IndexControlFrame[]): string { - const b = fb(a, 'loop', c, d, z, { o: 1 }); - af(ctx, { ...b, kind: 'loop', loop_kind: 'array_iteration' }); - return b.id; -} -function af(ctx: CollectionState, a: IndexBodyFact): void { - if (ctx.o.has(a.owner_symbol_id)) return - const b = ctx.f.get(a.owner_symbol_id) - if (!b) { ctx.f.set(a.owner_symbol_id, [a]); return } - if (b.length >= FMAX) { - ctx.o.add(a.owner_symbol_id); return - } - b.push(a) -} -function ab( - ctx: CollectionState, map: Map, key: string, b: T, -): void { - const a = map.get(key) - if (!a) { map.set(key, [b]); return } - if (a.length >= EMAX) { - ctx.o.add(key) - } else { - a.push(b) - } -} -function ae(ctx: CollectionState, a: string, fx: ExecutionEffect): void { ab(ctx, ctx.e, a, fx) } -function al(ctx: CollectionState, b: string, a: CallSite): void { ab(ctx, ctx.c, b, a) } +function ai(a: string, c: ts.Node, d: FileContext, q0: CollectionState, z: readonly IndexControlFrame[]): string { + const b = fb(a, 'loop', c, d, z, { o: 1 }); af(q0, { ...b, + kind: 'loop', loop_kind: 'array_iteration' }); return b.id } +function af(q0: CollectionState, a: IndexBodyFact): void { if (q0.o.has(a.owner_symbol_id)) return; const b = q0.f.get(a.owner_symbol_id); if (!b) { q0.f.set(a.owner_symbol_id, [a]); return } if (b[LN] >= FMAX) { q0.o.add(a.owner_symbol_id); return } b.push(a) } +function ab(q0: CollectionState, map: Map, key: string, b: T): void { const a = map.get(key); if (!a) { map.set(key, [b]); return } if (a[LN] >= EMAX) q0.o.add(key); else a.push(b) } +const ae = (q0: CollectionState, a: string, fx: ExecutionEffect): void => + ab(q0, q0.e, a, fx) +const al = (q0: CollectionState, b: string, a: CallSite): void => + ab(q0, q0.c, b, a) function io(a: IndexSymbol): boolean { - if (!['function', 'method', 'constant', 'variable'].includes(a.kind)) return false + if (!['function', 'method', 'constant', 'variable'][IC](a.kind)) return false // Execution facts require an authenticated owner span. Framework-only // synthetic nodes without declaration/definition ranges remain topology // nodes and must not become evidence owners. if (!a.declaration_range) return false if (a.framework_metadata?.external_call === true) return false if (typeof a.framework_metadata?.storage_operation === 'string') return false - return true -} -function oo(sf: ts.SourceFile, a: IndexRange['start']): number { - return sf.getPositionOfLineAndCharacter(a.line - 1, a.column - 1) -} -function os(sf: ts.SourceFile, c: readonly IndexSymbol[]): OwnerSpan[] { - return c - .filter(io) - .map((s) => ({ - s, - a: oo(sf, s.range.start), - b: oo(sf, s.range.end), - })) - .sort((l, r) => (l.b - l.a) - (r.b - r.a) - || l.a - r.a - || ct(l.s.id, r.s.id)); -} -function ow(c: ts.Node, d: FileContext): IndexSymbol | null { - const f = c.getStart(d.sf, false) - const end = c.getEnd() - return d.os.find((e) => e.a <= f && e.b >= end)?.s ?? null -} + return true } +const oo = (sf: ts.SourceFile, a: IndexRange['start']): number => + sf.getPositionOfLineAndCharacter(a.line - 1, a.column - 1) +const os = (sf: ts.SourceFile, c: readonly IndexSymbol[]): OwnerSpan[] => + c.filter(io).map((s) => ({ s, a: oo(sf, s.range.start), + b: oo(sf, s.range.end) })).sort((l, r) => + (l.b - l.a) - (r.b - r.a) || l.a - r.a || ct(l.s.id, r.s.id)) +function ow(c: ts.Node, d: FileContext): IndexSymbol | null { const f = c[GT](d.sf, false), end = c.getEnd(); return d.os.find((e) => e.a <= f && e.b >= end)?.s ?? null } function im(sf: ts.SourceFile): ReadonlyMap { const a = new Map() - for (const e of sf.statements) { + for (const e of sf[ST]) { if (!ts.isImportDeclaration(e) || !ts.isStringLiteral(e.moduleSpecifier)) continue const m = e.moduleSpecifier.text const b = e.importClause @@ -347,7 +277,7 @@ function im(sf: ts.SourceFile): ReadonlyMap { a.set(d.name.text, { i: '*', m, n: true }) continue } - for (const c of d.elements) { + for (const c of d[LE]) { a.set(c.name.text, { i: c.propertyName?.text ?? c.name.text, m, @@ -355,222 +285,262 @@ function im(sf: ts.SourceFile): ReadonlyMap { }) } } - return a -} -function ib(a: ts.Expression, b: FileContext): ImportBinding | null { - if (isIdentifier(a)) return b.im.get(a.text) ?? null - if (isAccess(a) && isIdentifier(a.expression)) { - const ns = b.im.get(a.expression.text) - if (ns?.n) { - return { i: a.name.text, m: ns.m, n: false } - } - } - return null -} -function ii(d: ts.Expression, e: FileContext, b: readonly string[], c: readonly string[]): boolean { + return a } +function ib(a: ts.Expression, b: FileContext): ImportBinding | null { if (iI(a)) return b.im.get(a.text) ?? null; if (iX(a) && iI(a[EX])) { const ns = b.im.get(a[EX].text); if (ns?.n) return { i: a.name.text, m: ns.m, n: false } } return null } +const ii = ( + d: ts.Expression, e: FileContext, b: readonly string[], c: readonly string[], +): boolean => { const a = ib(d, e) - return a !== null && b.includes(a.m) && c.includes(a.i) -} + return a !== null && b[IC](a.m) && c[IC](a.i) } function fa(a: ts.Symbol | undefined, b: ts.TypeChecker): ts.Symbol | undefined { if (!a || (a.flags & ts.SymbolFlags.Alias) === 0) return a try { - return b.getAliasedSymbol(a) - } catch { - return a - } -} -function sy(a: ts.Node, ctx: CollectionState): ts.Symbol | undefined { - return fa(ctx.i.checker.getSymbolAtLocation(a), ctx.i.checker) -} -function ds(d: ts.Node, e: FileContext, ctx: CollectionState): IndexSymbol | null { - const sf = d.getSourceFile(), c = ctx.i.pathToFileId.get(sf.fileName) - if (!c) return null - const g = sf === e.sf ? e.os - : os(sf, ctx.i.symbolsByFile.get(c) ?? []) - const h = d.getStart(sf, false), end = d.getEnd() - return g.find((f) => f.a <= h && f.b >= end)?.s ?? null + return b.getAliasedSymbol(a) } catch { + return a } } -function ed(d: ts.Node, e: FileContext, ctx: CollectionState): IndexSymbol | null { - const c = ds(d, e, ctx) +const sy = (a: ts.Node, q0: CollectionState): ts.Symbol | undefined => + fa(ck(q0).getSymbolAtLocation(a), ck(q0)) +function ds(d: ts.Node, e: FileContext, q0: CollectionState): IndexSymbol | null { + const sf = gs(d), c = q0.i.pathToFileId.get(sf.fileName) if (!c) return null - const sf = d.getSourceFile(), g = d.getStart(sf, false), end = d.getEnd() - const h = sf === e.sf ? e.os - : os(sf, ctx.i.symbolsByFile.get(ctx.i.pathToFileId.get(sf.fileName) ?? '') ?? []) - return h.some((f) => - f.s.id === c.id && f.a === g && f.b === end) - ? c : null -} -function sb(a: ts.Declaration, ctx: CollectionState): boolean { - const c = (isVariable(a) || isPropertyDecl(a)) && isIdentifier(a.name) - ? a.name : null - const b = c ? sy(c, ctx) : undefined - return !!b && !ctx.u.has(b) -} -function sfor(b: ts.Expression, d: FileContext, ctx: CollectionState): IndexSymbol | null { - const c = sy(isAccess(b) ? b.name : b, ctx) - for (const e of c?.declarations ?? []) { - const a = ds(e, d, ctx) - if (a) return a - } - return null -} -function us(b: ts.Node, ctx: CollectionState): boolean { - const a = isIdentifier(b) ? sy(b, ctx) : undefined - return !!a && ctx.u.has(a) - || ts.forEachChild(b, (c) => us(c, ctx)) === true -} -function cs(b: ts.CallExpression | ts.NewExpression, d: FileContext, ctx: CollectionState): IndexSymbol | null { - if (us(uw(b.expression), ctx)) return null - const c = ctx.i.checker.getResolvedSignature(b)?.getDeclaration() - if (c && !c.getSourceFile().isDeclarationFile) { - const a = ds(c, d, ctx) - if (a) return a + const g = sf === e.sf ? e.os : os(sf, q0.i.symbolsByFile.get(c) ?? []) + const h = d[GT](sf, false), end = d.getEnd() + return g.find((f) => f.a <= h && f.b >= end)?.s ?? null } +function ed(d: ts.Node, e: FileContext, q0: CollectionState): IndexSymbol | null { const c = ds(d, e, q0); if (!c) return null; const sf = gs(d), g = d[GT](sf, false), end = d.getEnd(), h = sf === e.sf ? e.os : os(sf, q0.i.symbolsByFile.get(q0.i.pathToFileId.get(sf.fileName) ?? '') ?? []); return h.some((f) => f.s.id === c.id && f.a === g && f.b === end) ? c : null } +function sb(a: ts.Declaration, q0: CollectionState): boolean { const c = (iV(a) || iD(a)) && iI(a.name) ? a.name : null, b = c ? sy(c, q0) : U0; return !!b && !q0.u.has(b) && (!iV(a) || !!(a[PR].flags & (NF.Let | NF.Const))) } +function sfor(b: ts.Expression, d: FileContext, q0: CollectionState): IndexSymbol | null { const c = sy(iX(b) ? b.name : b, q0); for (const e of c?.[DC] ?? []) { const a = ds(e, d, q0); if (a) return a } return null } +function us(b: ts.Node, q0: CollectionState): boolean { const a = iI(b) ? sy(b, q0) : U0; return !!a && q0.u.has(a) || fc(b, (c) => us(c, q0)) === true } +const iu = (a: ts.Expression, f: FileContext, q0: CollectionState): boolean => { + const b = ib(a, f); return b ? q0.sd.has(`${b.m}\0${b.i}`) || q0.sd.has(`${b.m}\0*`) : us(a, q0) } +function cs(b: ts.CallExpression | ts.NewExpression, d: FileContext, q0: CollectionState): IndexSymbol | null { if (us(uw(b[EX]), q0)) return null; const c = ck(q0).getResolvedSignature(b)?.getDeclaration(); if (c && !gs(c).isDeclarationFile) { const a = ds(c, d, q0); if (a) return a } return sfor(b[EX], d, q0) } +function ca(a: ts.SignatureDeclaration, c: FileContext, q0: CollectionState): IndexSymbol | null { + if (iR(a) || iF(a)) { + const b = a[PR] + if (iV(b) && b[IZ] === a) { const d = b[PR][PR] + return ts.isVariableStatement(d) && iSf(d[PR]) ? ds(b, c, q0) : null } + return iB(b) ? ed(a, c, q0) : null } + return iFd(a) || iMd(a) || iCd(a) || iGa(a) || iSd(a) ? ed(a, c, q0) : null } +function pi(a: ts.ParameterDeclaration): number { + const p = a[PR][PA] + const t = p.filter((b) => iI(b.name) && b.name.text === 'this') + const r = p.filter((b) => b[DD]) + const n = iI(a.name) ? a.name.text : null + return t[LN] > 1 || t[LN] === 1 && p[0] !== t[0] || r[LN] > 1 || r[LN] === 1 && p[p[LN] - 1] !== r[0] || n !== null && p.filter((b) => iI(b.name) && b.name.text === n)[LN] !== 1 ? -1 : p.indexOf(a) - t[LN] } +function eo(a: ts.Node, r = false): ts.Node | undefined { let b = a; while (b[PR] && (!iFl(b[PR]) || r && iR(b[PR])) && !iSf(b[PR])) b = b[PR]; return b[PR] } +function dg(a: ts.Node, o: ts.Node | undefined, q0: CollectionState): boolean { if (!iC(a)) return false; const e = uw(a[EX]); if (!iI(e) || e.text !== 'eval') return false; let n: ts.Node | undefined = a; while (n && n !== o) n = n[PR]; const s = sy(e, q0); return n === o && s?.[DC]?.some(lb) === true } +function wa(a: ts.Node, p: (n: ts.Node) => boolean | null): boolean { const r = p(a); return r === null ? fc(a, (n) => wa(n, p)) === true : r } +function pm(a: ts.Node, q0: CollectionState): boolean { const t = ck(q0)[TL](a), q = t.isUnion() ? t.types : [t]; return q.every((v) => !!(v.flags & (TF.StringLike | TF.NumberLike | TF.BooleanLike | TF.BigIntLike | TF.ESSymbolLike | TF.EnumLike | TF.Null | TF.Undefined | TF.Void | TF.Never))) } +function ps(a: ts.Identifier, r: boolean, c: CollectionState, q = false): boolean { + for (let t: ts.Node | undefined = a[PR]; t + && !iSt(t) && !iP(t); t = t[PR]) + if (iTn(t)) return true + let n: ts.Node = a + while (n[PR] && (iAs(n[PR]) + || iTa(n[PR]) + || iNl(n[PR]) + || iPa(n[PR]) + || iSa(n[PR]))) n = n[PR] + let v: ts.Node = a + let b = false + while (n[PR] && (iX(n[PR]) || iE(n[PR])) + && n[PR][EX] === n) { + n = n[PR]; v = n + b ||= iE(n) + || sy(n, c)?.[DC]?.some(iGa) === true + while (n[PR] && (iAs(n[PR]) + || iNl(n[PR]) + || iPa(n[PR]))) n = n[PR] + } + if (b || !r && v !== a) return false + if (pm(v, c)) return true + const p = n[PR] + if (q && v !== a && !!p && ts.isPropertyAssignment(p) + && p[IZ] === n) return true + return !!p && (iTy(p) || iVo(p) + || iU(p) && p[OP] === K.ExclamationToken + || iB(p) && SEQ.has(p[OT].kind) + || iEs(p) + || (iJ(p) || iWh(p) || iDo(p)) && p[EX] === n + || iFs(p) && p[CN] === n + || iCo(p) && p[CN] === n + || iSw(p) && p[EX] === n) +} +const pt = (a: ts.ParameterDeclaration): boolean => + !!a.type && PT.has(a.type.kind) +function pa(a: ts.ParameterDeclaration, u: ts.Identifier, q0: CollectionState): boolean { + const l = u[GT](gs(u), false) + for (const b of a[PR][PA]) { + if (b === a || pt(a) || pt(b)) continue + if (!iI(b.name)) return true + const s = sy(b.name, q0); if (!s) return true + if (q0.u.has(s)) return true + const q = (n: ts.Node, w = false): boolean => { + if (n === b) return false + if (!w && n[GT](gs(n), false) >= l) { + return iFd(n) && q(n, true) + } + return iI(n) && sy(n, q0) === s && !ps(n, false, q0) + || fc(n, (c) => q(c, w)) === true } - return sfor(b.expression, d, ctx) -} -function ca(a: ts.SignatureDeclaration, c: FileContext, ctx: CollectionState): IndexSymbol | null { - if (isArrow(a) || isFunction(a)) { - const b = a.parent - if (isVariable(b) && b.initializer === a) { const d = b.parent.parent - return ts.isVariableStatement(d) && ts.isSourceFile(d.parent) ? ds(b, c, ctx) : null } - return isBinary(b) ? ed(a, c, ctx) : null + if (q(a[PR])) return true } - return ts.isFunctionDeclaration(a) || ts.isMethodDeclaration(a) || ts.isConstructorDeclaration(a) || ts.isGetAccessorDeclaration(a) || ts.isSetAccessorDeclaration(a) ? ed(a, c, ctx) : null + return false } -function pv(a: ts.Identifier, d: FileContext, ctx: CollectionState): IndexValue | null { - for (const b of sy(a, ctx)?.declarations ?? []) { - if (!isParameter(b) || !ts.isFunctionLike(b.parent)) continue - const c = b.parent.parameters.indexOf(b) - if (c >= 0) { - return ca(b.parent, d, ctx) - ? { kind: 'parameter', position: c } - : { kind: 'parameter', position: c, scope: 'iteration' } +function pu( + a: ts.Declaration, u: ts.Identifier, q0: CollectionState, one = false, +): boolean { + const o = eo(u) + if (eo(a) !== o) return false + if (iP(a) && wa(o!, (n) => iI(n) && n.text === 'arguments' + && eo(n, true) === o ? true : null)) return false + if (iP(a) && pa(a, u, q0)) return false + for (let n: ts.Node | undefined = u[PR]; n && n !== o; n = n[PR]) { + if (ts.isIterationStatement(n, false)) return false + } + const s = sy(u, q0); if (!s || one && q0.u.has(s)) return false + const sf = gs(u) + let q: ts.Node = u + while (q[PR] && !iC(q) && q !== o) q = q[PR] + const c = iC(q) + if (c) while (iPa(q[PR]) || iAs(q[PR]) + || iNl(q[PR]) || iSa(q[PR]) || iTa(q[PR])) q = q[PR] + // After an awaited dispatch settles, only direct property snapshots used as + // metadata are passive; nested functions can run while arguments are built. + const z = one && c && iAw(q[PR]) ? stmt(q[PR]).getEnd() : -1 + const bad = (n: ts.Node): boolean => iI(n) && sy(n, q0) === s + const af = (v: ts.Node): boolean => { + if (z < 0 || v[GT](sf, false) < z) return false + for (let n = v[PR]; n && n !== o; n = n[PR]) + if (iFl(n)) return false + return true + } + const sc = (n: ts.Node): boolean => wa(n, (v) => { + if (v === a || v === u) return false + if (dg(v, o, q0)) return true + return bad(v) ? !(one && (ps(v as ts.Identifier, true, q0) + || af(v) && ps(v as ts.Identifier, true, q0, true))) : null + }) + if (one) return !sc(sf) + else { + const l = u[GT](sf, false) + const p = (n: ts.Node): boolean => { + if (n === a || n === u) return false + if (n[GT](sf, false) >= l) { + return iFd(n) && sc(n) } + return dg(n, o, q0) || bad(n) + || fc(n, p) === true } - } - return null -} -function red(a: string): IndexValue { - return { kind: 'redacted', sha256: hash(a), byte_length: Buffer.byteLength(a, 'utf8') } -} + return !p(sf) + } +} +function pv(a: ts.Identifier, d: FileContext, q0: CollectionState, strict = false): IndexValue | null { + const s = sy(a, q0); if (!s || q0.u.has(s)) return null + const p = s[DC]?.filter(iP) ?? [] + if (p[LN] !== 1) return null + const b = p[0]!, c = pi(b) + if (!b[DD] && iFl(b[PR]) && c >= 0 && eo(b) === eo(a) && (!strict || !b[IZ] && pu(b, a, q0, true))) + return ca(b[PR], d, q0) ? { kind: 'parameter', position: c } : { kind: 'parameter', position: c, scope: 'iteration' } + return null } +const red = (a: string): IndexValue => ({ + kind: 'redacted', sha256: hash(a), byte_length: bl(a), +}) function ls(b: string, c = false): IndexValue { - const a = Buffer.byteLength(b, 'utf8') + const a = bl(b) if (c || SVAL.test(b) || a > SBYT) { - return red(b) - } - return { kind: 'literal', value: b } -} -function uk(a: 'dynamic' | 'ambiguous' | 'unsupported' = 'dynamic'): IndexValue { - return { kind: 'unknown', reason: a } -} + return red(b) } + return lv(b) } +const uk = ( + a: 'dynamic' | 'ambiguous' | 'unsupported' = 'dynamic', +): IndexValue => ({ kind: 'unknown', reason: a }) +const uu = (): IndexValue => uk('unsupported') +const lv = (a: IndexScalarValue): IndexValue => ({ kind: 'literal', value: a }) function uw(b: ts.Expression): ts.Expression { let a = b - while (ts.isAsExpression(a) - || ts.isTypeAssertionExpression(a) || ts.isNonNullExpression(a) - || ts.isParenthesizedExpression(a) - || ts.isSatisfiesExpression(a)) { - a = a.expression + while (iAs(a) || iTa(a) || iNl(a) || iPa(a) || iSa(a)) { + a = a[EX] } - return a -} -type ValueOptions = { c?: boolean; s?: boolean; d?: number; n?: ReadonlySet } + return a } +type ValueOptions = { c?: boolean; s?: boolean; p?: boolean; d?: number; n?: ReadonlySet } function rd( - L: ts.Expression, g: FileContext, ctx: CollectionState, + L: ts.Expression, g: FileContext, q0: CollectionState, f: ValueOptions = {}, ): IndexValue { const d = f.d ?? 0 - if (d >= VDEP) return uk('unsupported') + if (d >= VDEP) return uu() const z = new Set(f.n ?? []) const a = uw(L) if (z.has(a)) return uk('ambiguous') z.add(a) const b = (I: ts.Expression, J: Partial = {}): IndexValue => - rd(I, g, ctx, { ...f, ...J, d: d + 1, n: z }) - if (ts.isStringLiteralLike(a)) return ls(a.text, f.s) - if (isNumeric(a)) { + rd(I, g, q0, { ...f, ...J, d: d + 1, n: z }) + if (iSl(a)) return ls(a.text, f.s) + if (iM(a)) { const A = Number(a.text) - return Number.isFinite(A) && !Object.is(A, -0) - ? { kind: 'literal', value: A } - : uk('unsupported') - } - if (a.kind === K.TrueKeyword) return { kind: 'literal', value: true } - if (a.kind === K.FalseKeyword) return { kind: 'literal', value: false } - if (a.kind === K.NullKeyword) return { kind: 'literal', value: null } - if (ts.isPrefixUnaryExpression(a) && isNumeric(a.operand)) { + return Number.isFinite(A) && !Object.is(A, -0) ? lv(A) : uu() } + if (a.kind === K.TrueKeyword) return lv(true) + if (a.kind === K.FalseKeyword) return lv(false) + if (a.kind === K.NullKeyword) return lv(null) + if (iU(a) && iM(a.operand)) { const B = Number(a.operand.text) - const q = a.operator === K.MinusToken ? -B : B - if ((a.operator === K.MinusToken - || a.operator === K.PlusToken) - && Number.isFinite(q) && !Object.is(q, -0)) { - return { kind: 'literal', value: q } - } + const q = a[OP] === K.MinusToken ? -B : B + if ((a[OP] === K.MinusToken || a[OP] === K.PlusToken) && Number.isFinite(q) && !Object.is(q, -0)) { + return lv(q) } } - if (ts.isPrefixUnaryExpression(a) && a.operator === K.ExclamationToken) { + if (iU(a) && a[OP] === K.ExclamationToken) { const C = b(a.operand, { c: true }) - if (C.kind === 'literal') return { kind: 'literal', value: !Boolean(C.value) } + if (C.kind === 'literal') return lv(!Boolean(C.value)) } - if (isIdentifier(a)) { - const j = pv(a, g, ctx) + if (iI(a)) { + const j = pv(a, g, q0, f.p) if (j) return j - const u = sy(a, ctx) - const k = u?.valueDeclaration - ?? u?.declarations?.find((t) => isVariable(t)) + const u = sy(a, q0) + const k = u?.[VD] + ?? u?.[DC]?.find((t) => iV(t)) if ( - f.c - && k - && isVariable(k) - && sb(k, ctx) - && k.initializer + f.c && k && iV(k) && sb(k, q0) && k[IZ] && dm(k, a) && (!f.p || pu(k, a, q0, true)) ) { - return b(k.initializer, { + return b(k[IZ], { s: f.s || SNM.test(a.text), }) } - const r = k - ? ds(k, g, ctx) - : sfor(a, g, ctx) - return r ? { kind: 'symbol', symbol_id: r.id } : uk() - } - if (ts.isArrayLiteralExpression(a)) { - if (a.elements.length > VELE) return uk('unsupported') + if (f.p && k && (iP(k) || iV(k))) return uk() + const r = k ? ds(k, g, q0) : sfor(a, g, q0) + return r ? { kind: 'symbol', symbol_id: r.id } : uk() } + if (iA(a)) { + if (a[LE][LN] > VELE) return uu() const y: IndexValue[] = [] - for (const l of a.elements) { - if (ts.isSpreadElement(l) || ts.isOmittedExpression(l)) return uk('unsupported') + for (const l of a[LE]) { + if (iSp(l) || iOm(l)) return uu() y.push(b(l, { c: true })) } - return { kind: 'array', elements: y } - } - if (ts.isObjectLiteralExpression(a)) { - if (a.properties.length > VELE) return uk('unsupported') + return { kind: 'array', elements: y } } + if (iO(a)) { + if (a.properties[LN] > VELE) return uu() const m = new Map() for (const e of a.properties) { if (ts.isPropertyAssignment(e)) { const key = pn(e.name) - if (key === null || key.includes('\0') - || Buffer.byteLength(key, 'utf8') > SBYT) { - return uk('unsupported') - } + if (key === null || key === '__proto__' + || key[IC]('\0') || bl(key) > SBYT) { + return uu() } m.set( key, - b(e.initializer, { + b(e[IZ], { c: true, s: f.s || SNM.test(key), - }), - ) - } else if (ts.isShorthandPropertyAssignment(e)) { + }), ) + } else if (iSh(e)) { const key = e.name.text - if (Buffer.byteLength(key, 'utf8') > SBYT) { - return uk('unsupported') - } + if (bl(key) > SBYT) { + return uu() } m.set( key, b(e.name, { c: true, s: f.s || SNM.test(key), - }), - ) + }), ) } else { - return uk('unsupported') - } + return uu() } } return { kind: 'object', @@ -579,162 +549,145 @@ function rd( } if (ts.isNoSubstitutionTemplateLiteral(a)) return ls(a.text, f.s) if (ts.isTemplateExpression(a)) { - if (1 + (2 * a.templateSpans.length) > VELE) { - return uk('unsupported') - } + if (1 + (2 * a.templateSpans[LN]) > VELE) { + return uu() } const D: IndexValue[] = [ls(a.head.text, f.s)] for (const H of a.templateSpans) { - D.push(b(H.expression, { c: true })) + D.push(b(H[EX], { c: true })) D.push(ls(H.literal.text, f.s)) } - return { kind: 'template', parts: D } - } - if (isCall(a) && isAccess(a.expression)) { - const v = a.expression.name.text - const p = a.expression.expression + return { kind: 'template', parts: D } } + if (iC(a) && iX(a[EX])) { + const v = a[EX].name.text + const p = a[EX][EX] if (v === 'slice') { const E = b(p, { c: true }) if (E.kind !== 'array') return uk() - const F = ni(a.arguments[0], g, ctx) - const end = ni(a.arguments[1], g, ctx) - if (F === null || (a.arguments[1] && end === null)) return uk() - return { kind: 'array', elements: E.elements.slice(F, end ?? undefined) } - } + const F = ni(a[AR][0], g, q0) + const end = ni(a[AR][1], g, q0) + if (F === null || (a[AR][1] && end === null)) return uk() + return { kind: 'array', elements: E[LE].slice(F, end ?? U0) } } if (v === 'map') { - return b(p, { c: false }) - } + return b(p, { c: false }) } } - if (isAccess(a) || ts.isElementAccessExpression(a)) { - const w = sy(a, ctx)?.declarations?.find(ts.isEnumMember) - const o = w ? ctx.i.checker.getConstantValue(w) - : ctx.i.checker.getConstantValue(a) + if (iX(a) || iE(a)) { + const w = sy(a, q0)?.[DC]?.find(iEm) + const o = w && !us(a, q0) && dm(w[PR], a) ? ck(q0).getConstantValue(w) : U0 if (typeof o === 'string') return ls(o, f.s) if (typeof o === 'number' && Number.isFinite(o) && !Object.is(o, -0)) - return { kind: 'literal', value: o } - } - if (ts.isElementAccessExpression(a)) { - const h = b(a.expression, { c: true }) - const G = a.argumentExpression - ? ni(a.argumentExpression, g, ctx) - : null + return lv(o) } + if (iE(a)) { + const h = b(a[EX], { c: true }) + const G = a[AX] ? ni(a[AX], g, q0) : null if (h.kind === 'array' && G !== null) { - return h.elements[G] ?? uk() - } + return h[LE][G] ?? uk() } } - const x = sfor(a, g, ctx) - return x ? { kind: 'symbol', symbol_id: x.id } : uk() -} + const x = sfor(a, g, q0) + return x ? { kind: 'symbol', symbol_id: x.id } : uk() } +const rv = ( + a: ts.Expression, b: FileContext, c: CollectionState, +): IndexValue => rd(a, b, c, { c: true }) function pn(a: ts.PropertyName): string | null { - if (isIdentifier(a) || ts.isStringLiteralLike(a) || isNumeric(a)) { - return a.text - } - return null -} -function ni(b: ts.Expression | undefined, d: FileContext, ctx: CollectionState): number | null { + if (iI(a) || iSl(a) || iM(a)) { + return a.text } + return null } +function ni(b: ts.Expression | undefined, d: FileContext, q0: CollectionState): number | null { if (!b) return 0 - const a = rd(b, d, ctx, { c: true }) - return a.kind === 'literal' && typeof a.value === 'number' - && Number.isSafeInteger(a.value) ? a.value : null -} + const a = rv(b, d, q0) + return a.kind === 'literal' && typeof a.value === 'number' && Number.isSafeInteger(a.value) ? a.value : null } function ss(a: IndexValue): string | null { if (a.kind === 'literal' && typeof a.value === 'string') { - return a.value.length > 0 && Buffer.byteLength(a.value, 'utf8') <= TBYT - ? a.value - : null - } + return a.value[LN] > 0 && bl(a.value) <= TBYT ? a.value : null } if (a.kind !== 'template') return null let b = '' for (const c of a.parts) { - if (c.kind !== 'literal' - || !['string', 'number', 'boolean'].includes(typeof c.value)) return null + if (c.kind !== 'literal' || !['string', 'number', 'boolean'][IC](typeof c.value)) return null b += String(c.value) } - return b.length > 0 && Buffer.byteLength(b, 'utf8') <= TBYT - ? b - : null -} -function si(a: IndexValue): string | null { - return a.kind === 'symbol' ? a.symbol_id : null -} + return b[LN] > 0 && bl(b) <= TBYT ? b : null } +const si = (a: IndexValue): string | null => + a.kind === 'symbol' ? a.symbol_id : null function mv(a: IndexValue, d: number, b?: (g: number) => IndexValue): IndexValue { if (a.kind === 'parameter' && a.scope !== 'iteration' && b) return mv(b(a.position), d) if (a.kind === 'array') { - if (d >= VDEP && a.elements.length > 0) return uk('unsupported'); return { - kind: 'array', elements: a.elements.map((e) => mv(e, d + 1, b)) } + if (d >= VDEP && a[LE][LN] > 0) return uu(); return { + kind: 'array', elements: a[LE].map((e) => mv(e, d + 1, b)) } } if (a.kind === 'object') { - if (d >= VDEP && a.entries.length > 0) return uk('unsupported'); return { + if (d >= VDEP && a.entries[LN] > 0) return uu(); return { kind: 'object', entries: a.entries.map((c) => ({ key: c.key, value: mv(c.value, d + 1, b) })) } } if (a.kind === 'template') { - if (d >= VDEP && a.parts.length > 0) return uk('unsupported'); return { + if (d >= VDEP && a.parts[LN] > 0) return uu(); return { kind: 'template', parts: a.parts.map((f) => mv(f, d + 1, b)) } } - return a -} -function sub(b: IndexValue, c: readonly IndexValue[]): IndexValue { - return mv(b, 0, (a) => c[a] ?? uk()) -} -function ie(fx: ExecutionEffect, b: readonly IndexValue[], a: EffectWitness): ExecutionEffect { + return a } +const sub = (b: IndexValue, c: readonly IndexValue[]): IndexValue => + mv(b, 0, (a) => c[a] ?? uk()) +const kn = (a: IndexValue): boolean => + a.kind !== 'unknown' + && (a.kind !== 'array' || a[LE].every(kn)) + && (a.kind !== 'object' || a.entries.every((e) => kn(e.value))) + && (a.kind !== 'template' || a.parts.every(kn)) +function ie(fx: ExecutionEffect, b: readonly IndexValue[], s: readonly IndexValue[], + a: EffectWitness): ExecutionEffect { switch (fx[0]) { - case 'bull-publish': - case 'bull-consume': return [fx[0], sub(fx[1], b), sub(fx[2], b), fx[3], undefined, a, fx[6], 'wrapper-summary'] - case 'event-publish': return [fx[0], sub(fx[1], b), undefined, fx[3], fx[4], a, fx[6], 'wrapper-summary'] - case 'event-consume': return [fx[0], sub(fx[1], b), sub(fx[2]!, b), fx[3], fx[4], a, fx[6], 'wrapper-summary'] - case 'persistence': return [fx[0], fx[1], fx[2] ? sub(fx[2], b) : undefined, fx[3], undefined, a, fx[6], 'wrapper-summary'] + case BP: { + const p = fx[4], n = p?.kind === 'parameter' && p.scope !== 'iteration' && p.position < s[LN] ? p.position : U0 + const q = p ? sub(p, s) : U0, r = p ? sub(p, b) : U0 + const m = n !== U0 + && !(a[AR] ?? []).slice(0, n + 1).some(iSp) + return [BP, sub(fx[1], b), sub(fx[2], b), fx[3], + q, a, fx[6], WS, + m && q && r && kn(q) && js(q) === js(r) ? n : U0] + } + case BC: return [BC, sub(fx[1], b), + sub(fx[2], s), fx[3], U0, a, fx[6], WS, U0] + case EP: return [fx[0], sub(fx[1], b), U0, fx[3], fx[4], a, fx[6], WS] + case EC: return [fx[0], sub(fx[1], b), sub(fx[2]!, b), fx[3], fx[4], a, fx[6], WS] + case PE: return [fx[0], fx[1], fx[2] ? sub(fx[2], b) : U0, fx[3], U0, a, fx[6], WS] } } function cn(a: ts.CallExpression | ts.NewExpression): string { - const sf = a.getSourceFile() - const b = st(a.expression, sf) - return bd(isNew(a) ? `new ${b}` : b) -} -function th(b: ts.CallExpression | ts.NewExpression, ctx: CollectionState): boolean { + const sf = gs(a) + const b = st(a[EX], sf) + return bd(iN(a) ? `new ${b}` : b) } +function th(b: ts.CallExpression | ts.NewExpression, q0: CollectionState): boolean { try { - const a = ctx.i.checker.getResolvedSignature(b) - const d = a && ctx.i.checker.getReturnTypeOfSignature(a) + const a = ck(q0).getResolvedSignature(b) + const d = a && ck(q0).getReturnTypeOfSignature(a) const c = d?.getProperty('then') - return !!c - && ctx.i.checker.getTypeOfSymbolAtLocation(c, b).getCallSignatures().length > 0 - } catch { return false } + return !!c && ck(q0).getTypeOfSymbolAtLocation(c, b).getCallSignatures()[LN] > 0 } catch { return false } } -function sch(b: ts.CallExpression | ts.NewExpression, ctx: CollectionState): IndexCallFact['scheduling'] { +function sch(b: ts.CallExpression | ts.NewExpression, q0: CollectionState): IndexCallFact['scheduling'] { let a: ts.Node = b - while (ts.isParenthesizedExpression(a.parent) - || ts.isAsExpression(a.parent) - || ts.isNonNullExpression(a.parent)) { - a = a.parent + while (iPa(a[PR]) || iAs(a[PR]) || iNl(a[PR])) { + a = a[PR] } - if (ts.isAwaitExpression(a.parent)) return 'awaited' - if ((ts.isVoidExpression(a.parent) - || ts.isExpressionStatement(a.parent)) && th(b, ctx)) + if (iAw(a[PR])) return 'awaited' + if ((iVo(a[PR]) || iEs(a[PR])) && th(b, q0)) return 'fire_and_forget' - return 'sync' -} -function iar(b: ts.Expression, ctx: CollectionState): boolean { + return 'sync' } +function iar(b: ts.Expression, q0: CollectionState): boolean { try { - const a = ctx.i.checker.getTypeAtLocation(b) - return ctx.i.checker.isArrayType(a) - || ctx.i.checker.isTupleType(a) - } catch { return false } + const a = ck(q0)[TL](b) + return ck(q0).isArrayType(a) || ck(q0).isTupleType(a) } catch { return false } } function cf( - b: ts.CallExpression | ts.NewExpression, sym: IndexSymbol, - e: FileContext, ctx: CollectionState, + b: ts.CallExpression | ts.NewExpression, s0: IndexSymbol, + e: FileContext, q0: CollectionState, z: readonly IndexControlFrame[], ): IndexCallFact { - const a = cs(b, e, ctx) - const h = (b.arguments ?? []).map((g) => { + const a = cs(b, e, q0) + const h = (b[AR] ?? []).map((g) => { const d = uw(g) - return isArrow(d) || isFunction(d) - ? hv(d, e, ctx) - : rd(d, e, ctx, { + return iR(d) || iF(d) ? hv(d, e, q0) : rd(d, e, q0, { c: true, s: SNM.test(d.getText(e.sf)), }) }) const f: IndexCallFact = { - ...fb(sym.id, 'call', b, e, z, { + ...fb(s0.id, 'call', b, e, z, { c: a ? 'high' : 'medium', s: a ? 'typescript-semantic' : 'typescript-syntactic', }), @@ -742,623 +695,719 @@ function cf( callee: cn(b), ...(a ? { target_symbol_id: a.id } : {}), arguments: h, - scheduling: sch(b, ctx), + scheduling: sch(b, q0), } - af(ctx, f) - const ids = ctx.ci.get(b) - if (ids) ids.push(f.id); else ctx.ci.set(b, [f.id]) + af(q0, f) + const ids = q0.ci.get(b) + if (ids) ids.push(f.id); else q0.ci.set(b, [f.id]) if (a && !ids) { - al(ctx, sym.id, [a.id, f.arguments, b]) + const k = (b[AR] ?? []).map((g, i) => { + const d = uw(g) + return iR(d) || iF(d) ? hv(d, e, q0, true) : rd(d, e, q0, { c: true, p: true }) }) + al(q0, s0.id, [a.id, f[AR], b, k]) } - return f -} -function rty(a: ts.Expression, ctx: CollectionState): string { + return f } +function rty(a: ts.Expression, q0: CollectionState): string { try { return bd( - ctx.i.checker.typeToString( - ctx.i.checker.getTypeAtLocation(a), - undefined, + ck(q0).typeToString( + ck(q0)[TL](a), + U0, ts.TypeFormatFlags.NoTruncation, - ).replace(/(["'`])(?:\\[\s\S]|(?!\1)[^\\])*\1/gu, ''), - ) + ).replace(/(["'`])(?:\\[\s\S]|(?!\1)[^\\])*\1/gu, ''), ) } catch { - return '' - } + return '' } } -function ti(e: ts.Expression, f: FileContext, ctx: CollectionState): ImportBinding | null { - for (const c of sy(e, ctx)?.declarations ?? []) { - const a = isParameter(c) - || isPropertyDecl(c) || isVariable(c) ? c.type : undefined +function ti(e: ts.Expression, f: FileContext, q0: CollectionState): ImportBinding | null { + for (const c of sy(e, q0)?.[DC] ?? []) { + const a = iP(c) || iD(c) || iV(c) ? c.type : U0 if (!a) continue - const d = isTypeReference(a) - ? (ts.isQualifiedName(a.typeName) - ? a.typeName.left : a.typeName) : null - if (d && isIdentifier(d)) { + const d = iT(a) ? (ts.isQualifiedName(a.typeName) ? a.typeName.left : a.typeName) : null + if (d && iI(d)) { const b = f.im.get(d.text) if (b) return b } } - return null -} -function bt(a: ImportBinding | null): QueueTransport | null { - if (!a || !['bull', 'bullmq'].includes(a.m) - || !['Queue', 'default'].includes(a.i)) return null - return a.m as QueueTransport -} -function cx(sf: ts.SourceFile, ctx: CollectionState): FileContext | null { - return ctx.fs.get(sf) ?? null; -} -function xs(b: ts.Expression, ctx: CollectionState): ts.Symbol | undefined { + return null } +const bt = (a: ImportBinding | null): QueueTransport | null => + !a || !['bull', 'bullmq'][IC](a.m) + || !['Queue', 'default'][IC](a.i) ? null : a.m as QueueTransport +const cx = (sf: ts.SourceFile, q0: CollectionState): FileContext | null => + q0.fs.get(sf) ?? null +function xs(b: ts.Expression, q0: CollectionState): ts.Symbol | undefined { const a = uw(b) - return sy(isAccess(a) ? a.name : a, ctx) -} -function eq(d: ts.Expression, c: ts.Expression, ctx: CollectionState): boolean { + return sy(iX(a) ? a.name : a, q0) } +function eq(d: ts.Expression, c: ts.Expression, q0: CollectionState): boolean { const a = uw(d), b = uw(c) - if (ts.isStringLiteralLike(a) && ts.isStringLiteralLike(b)) { - return a.text === b.text - } - if (isIdentifier(a) && isIdentifier(b)) return sy(a, ctx) === sy(b, ctx) - if (isAccess(a) && isAccess(b)) { - return a.name.text === b.name.text - && xs(a.name, ctx) === xs(b.name, ctx) - && (a.expression.kind === K.ThisKeyword && b.expression.kind === K.ThisKeyword - || eq(a.expression, b.expression, ctx)) - } - return false -} -function qc(f: ts.Expression, ctx: CollectionState, e: ReadonlySet = new Set()): readonly [ts.Expression, FileContext, QueueTransport] | null { + if (iSl(a) && iSl(b)) { + return a.text === b.text } + if (iI(a) && iI(b)) return sy(a, q0) === sy(b, q0) + if (iX(a) && iX(b)) { + return a.name.text === b.name.text && xs(a.name, q0) === xs(b.name, q0) && (a[EX].kind === K.ThisKeyword && b[EX].kind === K.ThisKeyword || eq(a[EX], b[EX], q0)) } + return false } +function qc(f: ts.Expression, q0: CollectionState, e: ReadonlySet = new Set()): readonly [ts.Expression, FileContext, QueueTransport] | null { const a = uw(f) if (e.has(a)) return null const g = new Set(e).add(a) - const d = cx(a.getSourceFile(), ctx) + const d = cx(gs(a), q0) if (!d) return null - if (isNew(a) && a.arguments?.[0]) { - const b = bt(ib(a.expression, d)) - return b ? [a.arguments[0], d, b] : null - } - if (!isIdentifier(a)) return null - const c = sy(a, ctx)?.valueDeclaration - return c && isVariable(c) && sb(c, ctx) && c.initializer - ? qc(c.initializer, ctx, g) : null -} -function xe(a: ts.Statement, b: FileContext, ctx: CollectionState): boolean { - if (ts.isBlock(a)) return a.statements.some((f) => xe(f, b, ctx)) - if (isIf(a)) { - const e = rd(a.expression, b, ctx, { c: true }) + if (iN(a) && a[AR]?.[0]) { + const k = uw(a[EX]), b = iu(k, d, q0) ? null : bt(ib(a[EX], d)) + return b ? [a[AR][0], d, b] : null } + if (!iI(a)) return null + const c = sy(a, q0)?.[VD] + return c && iV(c) && sb(c, q0) && c[IZ] ? qc(c[IZ], q0, g) : null } +function xe(a: ts.Statement, b: FileContext, q0: CollectionState): boolean { + if (iBl(a)) return a[ST].some((f) => xe(f, b, q0)) + if (iJ(a)) { + const e = rv(a[EX], b, q0) if (e.kind === 'literal') { - const d = Boolean(e.value) ? a.thenStatement : a.elseStatement - return !!d && xe(d, b, ctx) - }} - return ex(a, b, ctx) -} + const d = Boolean(e.value) ? a[TH] : a[EL] + return !!d && xe(d, b, q0) }} + return ex(a, b, q0) } function rr( b: ts.Statement, j: ts.MethodDeclaration | ts.ConstructorDeclaration, - e: FileContext, ctx: CollectionState, + e: FileContext, q0: CollectionState, ): boolean { - let d: ts.Node = b, a = b.parent + let d: ts.Node = b, a = b[PR] while (a !== j) { - if (ts.isBlock(a)) { - const f = a.statements.indexOf(d as ts.Statement) - if (f >= 0 && a.statements.slice(0, f) - .some((l) => xe(l, e, ctx))) return false - } else if (isIf(a)) { - const g = rd(a.expression, e, ctx, { c: true }) - if (g.kind !== 'literal' - || Boolean(g.value) !== (d === a.thenStatement)) return false - } else if ((ts.isWhileStatement(a) || ts.isForStatement(a)) - && a.statement === d) { - const k = ts.isWhileStatement(a) ? a.expression : a.condition - const h = k ? rd(k, e, ctx, { c: true }) : null + if (iBl(a)) { + const f = a[ST].indexOf(d as ts.Statement) + if (f >= 0 && a[ST].slice(0, f) .some((l) => xe(l, e, q0))) return false + } else if (iJ(a)) { + const g = rv(a[EX], e, q0) + if (g.kind !== 'literal' || Boolean(g.value) !== (d === a[TH])) return false + } else if ((iWh(a) || iFs(a)) && a.statement === d) { + const k = iWh(a) ? a[EX] : a[CN] + const h = k ? rv(k, e, q0) : null if (h?.kind === 'literal' && !Boolean(h.value)) return false - } else if (ts.isForOfStatement(a) && a.statement === d) { - const i = rd(a.expression, e, ctx, { c: true }) - if (i.kind !== 'array' || i.elements.length === 0) return false + } else if (iFo(a) && a.statement === d) { + const i = rv(a[EX], e, q0) + if (i.kind !== 'array' || i[LE][LN] === 0) return false } d = a - a = a.parent + a = a[PR] } - return true -} -function xr(j: ts.CallExpression, f: FileContext, ctx: CollectionState): boolean { + return true } +function xr(j: ts.CallExpression, f: FileContext, q0: CollectionState): boolean { const b = stmt(j) - if (!ts.isExpressionStatement(b)) return false - if (ts.isSourceFile(b.parent)) return true - if (ts.isBlock(b.parent) - && ts.isConstructorDeclaration(b.parent.parent)) return rr( - b, b.parent.parent, f, ctx) - let a: ts.Node = b.parent - while (!ts.isSourceFile(a) && !ts.isMethodDeclaration(a)) { - if (ts.isFunctionLike(a)) return false - a = a.parent - } - if (!ts.isMethodDeclaration(a) - || !isIdentifier(a.name) || a.name.text !== 'onModuleInit' - || !ts.isClassLike(a.parent)) return false - const h = dc(a.parent).some((c) => { - const k = isCall(c.expression) - ? c.expression.expression : c.expression + if (!iEs(b)) return false + if (iSf(b[PR])) return true + if (iBl(b[PR]) && iCd(b[PR][PR])) return rr( + b, b[PR][PR], f, q0) + let a: ts.Node = b[PR] + while (!iSf(a) && !iMd(a)) { + if (iFl(a)) return false + a = a[PR] + } + if (!iMd(a) || !iI(a.name) || a.name.text !== 'onModuleInit' || !ts.isClassLike(a[PR])) return false + const h = dc(a[PR]).some((c) => { + const k = iC(c[EX]) ? c[EX][EX] : c[EX] const d = ib(k, f) - return d?.m === '@nestjs/common' - && ['Controller', 'Injectable', 'Module'].includes(d.i) - }) - return h && rr(b, a, f, ctx) - && a.parent.heritageClauses?.some((g) => + return d?.m === '@nestjs/common' && ['Controller', 'Injectable', 'Module'][IC](d.i) }) + return h && rr(b, a, f, q0) && a[PR].heritageClauses?.some((g) => g.token === K.ImplementsKeyword && g.types.some((l) => { - const e = ib(l.expression, f) - return e?.m === '@nestjs/common' && e.i === 'OnModuleInit' - })) === true + const e = ib(l[EX], f) + return e?.m === '@nestjs/common' && e.i === 'OnModuleInit' })) === true } -function prep(ctx: CollectionState): void { +function prep(q0: CollectionState): void { const s: Array = [] const t = new Set() const add = (d: ts.Symbol): void => { if (t.has(d)) return - t.add(d); ctx.u.add(d) - const k = d.valueDeclaration - const b = k && ts.isBindingElement(k) - && isVariable(k.parent.parent) ? k.parent.parent : k - const f = b && isVariable(b) && b.initializer - ? uw(b.initializer) : null - if (f && (isIdentifier(f) || isAccess(f) - || ts.isElementAccessExpression(f) - || ts.isObjectLiteralExpression(f) - || ts.isArrayLiteralExpression(f))) g(f) + t.add(d); q0.u.add(d) + let b: ts.Node | undefined = d[VD] + const z = b && iBe(b) ? b : null + if (z) + while (b && !iV(b)) b = b[PR] + const f = b && iV(b) && b[IZ] ? uw(b[IZ]) : null + if (f && (iI(f) || iX(f) || iE(f) || iO(f) || iA(f))) { + const p = z && !z[DD] && z[PR][PR] === b ? z.propertyName ?? z.name : null + const x = iI(f) ? cx(gs(f), q0) : null, n = x ? ib(f, x) : null + if (n?.n && p && (iI(p) || iSl(p))) q0.sd.add(`${n.m}\0${p.text}`) + else g(f) + } } const g = (l: ts.Node): void => { - if (ts.isShorthandPropertyAssignment(l)) { - const h = ctx.i.checker.getShorthandAssignmentValueSymbol(l) - if (h) add(fa(h, ctx.i.checker) ?? h) + if (iI(l) || iX(l)) { + const f = cx(gs(l), q0), b = f ? ib(l as ts.Expression, f) : null + if (b) { q0.sd.add(`${b.m}\0${b.i}`); if (iX(l)) return } + } + if (iSh(l)) { + const h = ck(q0).getShorthandAssignmentValueSymbol(l) + if (h) add(fa(h, ck(q0)) ?? h) } - if (isIdentifier(l)) { - const o = sy(l, ctx) + if (iI(l)) { + const o = sy(l, q0) if (o) add(o) } - ts.forEachChild(l, g) + fc(l, g) } - const j = (q: ts.Node): void => - g(ts.isElementAccessExpression(q) ? q.expression : q) - for (const sf of ctx.i.sourceFiles) { + const j = (q: ts.Node): void => g(iE(q) ? q[EX] : q) + for (const sf of q0.i.sourceFiles) { + if (q0.x.has(sf)) continue const r = (a: ts.Node): void => { - if (isBinary(a) && AOP.has(a.operatorToken.kind)) { + if (iB(a) && AOP.has(a[OT].kind)) { j(a.left); const c = uw(a.right) - if (a.operatorToken.kind === K.EqualsToken - && (isIdentifier(c) || isAccess(c) - || ts.isElementAccessExpression(c) - || ts.isObjectLiteralExpression(c) - || ts.isArrayLiteralExpression(c))) g(c) - } else if ((ts.isPrefixUnaryExpression(a) || ts.isPostfixUnaryExpression(a)) - && [K.PlusPlusToken, K.MinusMinusToken].includes(a.operator)) + if (a[OT].kind === K.EqualsToken && (iI(c) || iX(c) || iE(c) || iO(c) || iA(c))) g(c) + } else if ((iU(a) || iPf(a)) && [K.PlusPlusToken, K.MinusMinusToken][IC](a[OP])) j(a.operand) - else if (ts.isDeleteExpression(a)) j(a.expression) - if (isCall(a) && isAccess(a.expression)) { - const e = a.expression.name.text - if (['assign', 'defineProperty', 'defineProperties'].includes(e) - && isIdentifier(a.expression.expression) - && a.expression.expression.text === 'Object' - && sy(a.expression.expression, ctx)?.declarations?.some((v) => - v.getSourceFile().isDeclarationFile - && /\/typescript\/lib\/lib\..+\.d\.ts$/u.test( - v.getSourceFile().fileName.replaceAll('\\', '/'))) - && a.arguments[0]) g(a.arguments[0]) - if (AMU.has(e) - || ['clear', 'delete', 'copyWithin', 'fill', 'reverse', 'sort'].includes(e)) { - g(a.expression.expression) - } else if (e === 'set' && a.arguments[0] && a.arguments[1] - && xr(a, cx(a.getSourceFile(), ctx)!, ctx)) { - const p = xs(a.expression.expression, ctx) - if (p) s.push([p, a.arguments[0], a.arguments[1]]) + else if (iDe(a)) j(a[EX]) + else if ((iFi(a) || iFo(a)) && !ts.isVariableDeclarationList(a[IZ])) j(a[IZ]) + if (iC(a) && iX(a[EX])) { + const e = a[EX].name.text, o = a[EX][EX] + const lib = iI(o) && sy(o, q0)?.[DC]?.some(lb) + if (lib && a[AR][0] && (o.text === 'Object' + && ['assign', 'defineProperty', 'defineProperties', 'setPrototypeOf'][IC](e) + || o.text === 'Reflect' + && ['set', 'defineProperty', 'deleteProperty', 'setPrototypeOf'][IC](e))) + g(a[AR][0]) + if (AMU.has(e) || ['clear', 'delete', 'copyWithin', 'fill', 'reverse', 'sort'][IC](e)) { + g(a[EX][EX]) + } else if (e === 'set' && a[AR][0] && a[AR][1] && xr(a, cx(gs(a), q0)!, q0)) { + const p = xs(a[EX][EX], q0) + if (p) s.push([p, a[AR][0], a[AR][1]]) } } - ts.forEachChild(a, r) + fc(a, r) } r(sf) } for (const [symbol, key, value] of s) { - const m = ctx.mq.get(symbol) ?? [] - const n = qc(value, ctx) - const w = cx(key.getSourceFile(), ctx) + const m = q0.mq.get(symbol) ?? [] + const n = qc(value, q0) + const w = cx(gs(key), q0) m.push(n && w ? [key, w, n[0], n[2]] : null) - ctx.mq.set(symbol, m) + q0.mq.set(symbol, m) } } -function sm(b: ts.Declaration, ctx: CollectionState): boolean { +function sm(b: ts.Declaration, q0: CollectionState): boolean { try { - const a = ctx.i.checker.getTypeAtLocation(b).getSymbol() - return a?.name === 'Map' - && !!a.declarations?.some((c) => - /\/typescript\/lib\/lib\..+\.d\.ts$/u.test( - c.getSourceFile().fileName.replaceAll('\\', '/'), - )) + const a = ck(q0)[TL](b).getSymbol() + return a?.name === 'Map' && !!a[DC]?.some(lb) } catch { return false } } -function mq(map: ts.Expression, key: ts.Expression, k: FileContext, ctx: CollectionState): QueueOrigin | null { - const a = xs(map, ctx) - const g = a?.valueDeclaration - if (!a || !g - || !(isVariable(g) || isPropertyDecl(g)) - || !sb(g, ctx) || !sm(g, ctx)) return null - const b = ctx.mq.get(a) ?? [] - if (b.length === 0 || b.some((l) => !l)) return null +function mq(map: ts.Expression, key: ts.Expression, k: FileContext, q0: CollectionState): QueueOrigin | null { + const a = xs(map, q0) + const g = a?.[VD] + if (!a || !g || !(iV(g) || iD(g)) || !sb(g, q0) || !sm(g, q0)) return null + const b = q0.mq.get(a) ?? [] + if (b[LN] === 0 || b.some((l) => !l)) return null const f = b as MapQueueEntry[] - const i = ss(rd(key, k, ctx, { c: true })) + const i = ss(rv(key, k, q0)) if (i) { - const h = f.filter(([entryKey, entryFile]) => - ss(rd(entryKey, entryFile, ctx, { c: true })) === i) - if (h.length) { - const d = h.map(([, entryFile, queueKey, transport]) => - [rd(queueKey, entryFile, ctx, { c: true }), transport] as const) - const m = JSON.stringify(d[0]) - return d.every((j) => JSON.stringify(j) === m) - ? d[0]! : null - } + const h = f.filter(([a, b]) => ss(rv(a, b, q0)) === i) + if (h[LN]) { + const d = h.map(([, f, q, t]) => [rv(q, f, q0), t] as const) + const m = js(d[0]) + return d.every((j) => js(j) === m) ? d[0]! : null } } const e = f[0]![3] - return f.every(([entryKey, , queueKey, entryTransport]) => - entryTransport === e && eq(entryKey, queueKey, ctx)) - ? [rd(key, k, ctx, { c: true }), e] : null + return f.every(([a, , b, c]) => + c === e && eq(a, b, q0)) ? [rv(key, k, q0), e] : null } -function qo(n: ts.Expression, sym: IndexSymbol, l: FileContext, ctx: CollectionState, m: ReadonlySet = new Set()): QueueOrigin | null { +function qo(n: ts.Expression, s0: IndexSymbol, l: FileContext, q0: CollectionState, m: ReadonlySet = new Set()): QueueOrigin | null { const a = uw(n); if (m.has(a)) return null const d = new Set(m).add(a) - if (isNew(a) && a.arguments?.[0]) { - const b = bt(ib(a.expression, l)); if (b) return [rd(a.arguments[0], l, ctx, { c: true }), b] - } - if (isIdentifier(a)) { - const e = sy(a, ctx)?.valueDeclaration - if (e && isVariable(e) && sb(e, ctx) && e.initializer) { - const f = cx(e.getSourceFile(), ctx); return f ? qo(e.initializer, sym, f, ctx, d) : null + if (iN(a) && a[AR]?.[0]) { + const k = uw(a[EX]), b = iu(k, l, q0) ? null : bt(ib(a[EX], l)) + if (b) return [rv(a[AR][0], l, q0), b] + } + if (iI(a)) { + const e = sy(a, q0)?.[VD] + if (e && iV(e) && sb(e, q0) && e[IZ]) { + const f = cx(gs(e), q0); return f ? qo(e[IZ], s0, f, q0, d) : null } } - if (isAccess(a) && a.expression.kind === K.ThisKeyword) { - const j = sy(a.name, ctx), k = sym.kind === 'method' ? sym.name.slice(0, sym.name.lastIndexOf('.')) : sym.name - const g = ctx.nq.get(`${sym.file_id}\0${k}`)?.get(a.name.text) - if (g && j && !ctx.u.has(j)) return g - const h = j?.valueDeclaration - if (h && isPropertyDecl(h) && sb(h, ctx) && h.initializer) { - const i = cx(h.getSourceFile(), ctx); if (i) return qo(h.initializer, sym, i, ctx, d) + if (iX(a) && a[EX].kind === K.ThisKeyword) { + const j = sy(a.name, q0), k = s0.kind === 'method' ? s0.name.slice(0, s0.name.lastIndexOf('.')) : s0.name + const g = q0.nq.get(`${s0.file_id}\0${k}`)?.get(a.name.text) + if (g && j && !q0.u.has(j)) return g + const h = j?.[VD] + if (h && iD(h) && sb(h, q0) && h[IZ]) { + const i = cx(gs(h), q0); if (i) return qo(h[IZ], s0, i, q0, d) } } - if (isCall(a) && isAccess(a.expression) && a.expression.name.text === 'get' && a.arguments[0]) - return mq(a.expression.expression, a.arguments[0], l, ctx) - return null -} -function es(j: ts.Expression, sym: IndexSymbol, l: FileContext, ctx: CollectionState, g: ReadonlySet = new Set()): EmitterScope | null { + if (iC(a) && iX(a[EX]) && a[EX].name.text === 'get' && a[AR][0]) + return mq(a[EX][EX], a[AR][0], l, q0) + return null } +function es(j: ts.Expression, s0: IndexSymbol, l: FileContext, q0: CollectionState, g: ReadonlySet = new Set()): EmitterScope | null { const e = uw(j); if (g.has(e)) return null g = new Set(g).add(e) - if (isIdentifier(e)) { - const c = sy(e, ctx)?.valueDeclaration + if (iI(e)) { + const c = sy(e, q0)?.[VD] if (c) { - const a = cx(c.getSourceFile(), ctx), b = a ? `${a.id}:${c.getStart(a.sf, false)}` : null - const f = b ? ctx.em.get(b) : undefined + const a = cx(gs(c), q0), b = a ? `${a.id}:${c[GT](a.sf, false)}` : null + const f = b ? q0.em.get(b) : U0 if (f) return f - if (isVariable(c) && sb(c, ctx) && c.initializer && isNew(uw(c.initializer))) { - const k = uw(c.initializer) as ts.NewExpression; if (!a) return null - const d = et(k.expression, a) + if (iV(c) && sb(c, q0) && c[IZ] && iN(uw(c[IZ]))) { + const k = uw(c[IZ]) as ts.NewExpression; if (!a) return null + const d = iu(uw(k[EX]), a, q0) ? null : et(k[EX], a) if (d) { - const h = b!, i: EmitterScope = [h, d]; ctx.em.set(h, i); return i + const h = b!, i: EmitterScope = [h, d]; q0.em.set(h, i); return i } } } } - return null -} -function et(a: ts.Expression, b: FileContext): 'node-event-emitter' | 'nestjs-event-emitter' | null { - if (ii(a, b, ['node:events', 'events'], ['EventEmitter'])) return 'node-event-emitter' - return ii(a, b, ['@nestjs/event-emitter'], ['EventEmitter2']) + return null } +const et = ( + a: ts.Expression, b: FileContext, +): 'node-event-emitter' | 'nestjs-event-emitter' | null => + ii(a, b, ['node:events', 'events'], ['EventEmitter']) + ? 'node-event-emitter' + : ii(a, b, ['@nestjs/event-emitter'], ['EventEmitter2']) ? 'nestjs-event-emitter' : null +function hc(a: ts.NewExpression, q0: CollectionState): boolean { + const b = uw(a[EX]), s = iI(b) || iX(b) ? sy(b, q0) : U0 + return !!s && !q0.u.has(s) && s[DC]?.some(iCl) === true } -function hv(h: ts.Expression, g: FileContext, ctx: CollectionState): IndexValue { +function hr( + a: ts.Expression, q0: CollectionState, + n: ReadonlySet = new Set(), +): boolean { + const b = uw(a) + if (n.has(b) || us(b, q0)) return false + const seen = new Set(n).add(b) + if (b.kind === K.ThisKeyword) return true + if (iN(b)) return false + if (iI(b)) { + const d = sy(b, q0)?.[VD] + if (d && iV(d) && d[IZ]) { + const f = cx(gs(d), q0) + const x = uw(d[IZ]) + return !!f && sb(d, q0) + && (iN(x) ? hc(x, q0) : hr(x, q0, seen)) + } + } else if (iX(b) || iE(b)) { + if (b[QD] || iE(b) + && (!b[AX] || !iSl(uw(b[AX]))) + || sy(b, q0)?.[DC]?.some(iGa)) + return false + if (!hr(b[EX], q0, seen)) return false + } else return false + const t = ck(q0)[TL](b) + if (t.isUnion() || t.flags & (TF.Any | TF.Unknown | TF.TypeParameter)) + return false + return t.getSymbol()?.[DC]?.some(ts.isClassDeclaration) === true +} +function ht(a: ts.CallExpression, g: FileContext, q0: CollectionState): IndexValue { + const b = uw(a[EX]), s = sy(b, q0), q = cs(a, g, q0) + if (!s || !q || us(b, q0)) return uk() + const ok = iI(b) + ? s[DC]?.some((v) => iFd(v) || iMd(v)) + : (iX(b) || iE(b)) && s[DC]?.some(iMd) + && hr(b[EX], q0) + return ok ? { kind: 'symbol', symbol_id: q.id } : uk() +} +function hs(a: ts.Expression, q0: CollectionState): boolean { + const b = uw(a) + if (iI(b)) return !!sy(b, q0) + if (iSl(b) || iM(b) || [ + K.TrueKeyword, K.FalseKeyword, K.NullKeyword, + ][IC](b.kind)) return true + if (iTy(b) && iI(uw(b[EX]))) return true + if (iVo(b) && iI(uw(b[EX]))) + return !!sy(uw(b[EX]), q0) + return iB(b) && SEQ.has(b[OT].kind) + && hs(b.left, q0) && hs(b.right, q0) +} +function hv(h: ts.Expression, g: FileContext, q0: CollectionState, r = false): IndexValue { const d = uw(h) - if (isArrow(d) || isFunction(d)) { - const a = new Set() - const f = (b: ts.Node): void => { - if (b !== d && (isArrow(b) || isFunction(b))) return - if (isCall(b)) { const e = cs(b, g, ctx); if (e) a.add(e.id) } - ts.forEachChild(b, f) + if (iR(d) || iF(d)) { + const p = d[PA].find((e) => pi(e) === 0), n = p && iI(p.name) ? p.name : null + if (r) { + if (!p || !n || p[IZ] || p[DD]) return uk() + let v: IndexValue | undefined + for (const s of iBl(d.body) ? d.body[ST] : [d.body]) { + let e = ts.isExpression(s) ? s + : (iEs(s) || iRe(s)) && s[EX] ? s[EX] : U0 + if (!e) continue + e = uw(e); while (iAw(e) || iVo(e)) e = uw(e[EX]) + if (!iC(e) || e[QD] || !e[AR][0]) continue + let x = uw(e[AR][0]) + while (iX(x) && !x[QD] || iE(x) && !x[QD] && x[AX] && iSl(uw(x[AX]))) x = uw(x[EX]) + const a = ht(e, g, q0) + if ((a.kind === 'symbol' || a.kind === 'parameter') && iI(x) + && sy(x, q0) === sy(n, q0) && pu(p, x, q0, true) + && e[AR].slice(1).every((y) => !iSp(y) && hs(y, q0))) { + if (v) return uk(); v = a + } + } + return v ?? uk() + } + let b: ts.Node | undefined = d.body + if (iBl(b)) b = b[ST][LN] === 1 ? b[ST][0] : U0 + if (b && (iRe(b) || iEs(b))) b = b[EX] + if (!b || !ts.isExpression(b)) return uk() + let e = uw(b) + while (iAw(e) || iVo(e)) e = uw(e[EX]) + if (!iC(e) || e[QD]) return uk() + let k = uw(e[EX]) + while (iX(k) || iE(k)) { + if (k[QD] || iE(k) + && (!k[AX] || !iSl(uw(k[AX])))) + return uk() + k = uw(k[EX]) } - f(d.body) - return a.size === 1 ? { kind: 'symbol', symbol_id: [...a][0]! } - : uk(a.size > 1 ? 'ambiguous' : 'dynamic') + if (!iI(k) && k.kind !== K.ThisKeyword) return uk() + const a = ht(e, g, q0) + if (a.kind !== 'symbol' && a.kind !== 'parameter') return uk() + const safe = e[AR].every((y) => !iSp(y) && hs(y, q0)) + return safe ? a : uk() } - return rd(d, g, ctx, { c: false }) -} + return rd(d, g, q0, { c: false }) } type PersistenceSummary = readonly [operation: IndexPersistenceOperation, resource: IndexValue | undefined, receiverType: string] -function se( +const se = ( a: IndexPersistenceOperation | null, b: string, - d: ts.Expression | undefined, e: FileContext, ctx: CollectionState, -): PersistenceSummary | null { - return a ? [ - a, - d ? rd(d, e, ctx, { c: true }) : undefined, - b, - ] : null -} -function po(f: ts.CallExpression, c: FileContext, ctx: CollectionState): PersistenceSummary | null { - const k = f.expression - if (isIdentifier(k)) { + d: ts.Expression | undefined, e: FileContext, q0: CollectionState, +): PersistenceSummary | null => a ? [a, d ? rv(d, e, q0) : U0, b] : null +function po(f: ts.CallExpression, c: FileContext, q0: CollectionState): PersistenceSummary | null { + const k = f[EX] + if (iI(k)) { const d = c.im.get(k.text); if (d && FSM.has(d.m)) - return se(fsop(d.i, f.arguments[1], c, ctx), `${d.m}:${d.i}`, f.arguments[0], c, ctx) - } - if (!isAccess(k)) return null - const g = k.name.text, b = k.expression, s = li(b) - const ns = s ? c.im.get(s.text) : undefined + return se(fsop(d.i, f[AR][1], c, q0), `${d.m}:${d.i}`, f[AR][0], c, q0) } + if (!iX(k)) return null + const g = k.name.text, b = k[EX], s = li(b) + const ns = s ? c.im.get(s.text) : U0 if (ns?.n && FSM.has(ns.m)) { - const l = se(fsop(g, f.arguments[1], c, ctx), `${ns.m}:namespace`, f.arguments[0], c, ctx); if (l) return l + const l = se(fsop(g, f[AR][1], c, q0), `${ns.m}:namespace`, f[AR][0], c, q0); if (l) return l } - const q = rty(b, ctx), a = ti(b, c, ctx) - if (a?.m === 'typeorm' && ['Repository', 'MongoRepository'].includes(a.i)) { - const o = se(typeormOperation(g), q || `${a.m}:${a.i}`, f.arguments[0], c, ctx); if (o) return o + const q = rty(b, q0), a = ti(b, c, q0) + if (a?.m === 'typeorm' && ['Repository', 'MongoRepository'][IC](a.i)) { + const o = se(ty(g), q || `${a.m}:${a.i}`, f[AR][0], c, q0); if (o) return o } - if (pd(b, ctx, '/node_modules/@prisma/client/', '/node_modules/.prisma/client/')) { - const p = se(prismaOperation(g), q || 'PrismaClient', f.arguments[0], c, ctx); if (p) return p + if (pd(b, q0, '/node_modules/@prisma/client/', '/node_modules/.prisma/client/')) { + const p = se(pr(g), q || 'PrismaClient', f[AR][0], c, q0); if (p) return p } - if (g !== 'send' || !(a?.m === '@aws-sdk/client-s3' && a.i === 'S3Client' || pd(b, ctx, '/node_modules/@aws-sdk/client-s3/'))) return null - const h = f.arguments[0] - if (h && isNew(uw(h))) { - const e = uw(h) as ts.NewExpression, j = ib(e.expression, c) + if (g !== 'send' || !(a?.m === '@aws-sdk/client-s3' && a.i === 'S3Client' || pd(b, q0, '/node_modules/@aws-sdk/client-s3/'))) return null + const h = f[AR][0] + if (h && iN(uw(h))) { + const e = uw(h) as ts.NewExpression, j = ib(e[EX], c) if (j?.m === '@aws-sdk/client-s3') { const r = ['PutObjectCommand', 'UploadPartCommand', 'CompleteMultipartUploadCommand'], t = ['GetObjectCommand', 'HeadObjectCommand'] - return se(r.includes(j.i) ? 'object_write' : t.includes(j.i) ? 'object_read' : null, q, e.arguments?.[0], c, ctx) - } + return se(r[IC](j.i) ? 'object_write' : t[IC](j.i) ? 'object_read' : null, q, e[AR]?.[0], c, q0) } } - return null -} + return null } function li(b: ts.Expression): ts.Identifier | null { let a = b - while (isAccess(a)) a = a.expression - return isIdentifier(a) ? a : null -} -function pd(h: ts.Expression, ctx: CollectionState, ...b: readonly string[]): boolean { - const d = ctx.i.checker + while (iX(a)) a = a[EX] + return iI(a) ? a : null } +function pd(h: ts.Expression, q0: CollectionState, ...b: readonly string[]): boolean { + const d = ck(q0) let a: ts.Expression = h while (true) { try { - const g = d.getTypeAtLocation(a) + const g = d[TL](a) const e = [g.aliasSymbol, g.getSymbol()] - if (e.some((f) => f?.declarations?.some((j) => { - const k = j.getSourceFile().fileName.replaceAll('\\', '/') - return b.some((c) => k.includes(c)) - }))) return true + if (e.some((f) => f?.[DC]?.some((j) => { + const k = gs(j).fileName.replaceAll('\\', '/') + return b.some((c) => k[IC](c)) }))) return true } catch { return false } - if (!isAccess(a)) return false - a = a.expression + if (!iX(a)) return false + a = a[EX] } } -function no(b: string, a: Readonly>): IndexPersistenceOperation | null { - return Object.hasOwn(a, b) ? a[b]! : null; -} +const no = ( + b: string, a: Readonly>, +): IndexPersistenceOperation | null => Object.hasOwn(a, b) ? a[b]! : null function fsop( - b: string, d: ts.Expression | undefined, e: FileContext, ctx: CollectionState, + b: string, d: ts.Expression | undefined, e: FileContext, q0: CollectionState, ): IndexPersistenceOperation | null { - if (!['open', 'openSync'].includes(b)) return no(b, FSO) + if (!['open', 'openSync'][IC](b)) return no(b, FSO) if (!d) return null - const a = ss(rd(d, e, ctx, { c: true })) + const a = ss(rv(d, e, q0)) if (!a) return null - if (a.includes('+') || /^[aw]/u.test(a)) return 'file_write' - return /^r(?:s|sr)?$/u.test(a) ? 'file_read' : null -} -const typeormOperation = (a: string): IndexPersistenceOperation | null => + if (a[IC]('+') || /^[aw]/u.test(a)) return 'file_write' + return /^r(?:s|sr)?$/u.test(a) ? 'file_read' : null } +const ty = (a: string): IndexPersistenceOperation | null => no(a, TOO) -const prismaOperation = (a: string): IndexPersistenceOperation | null => +const pr = (a: string): IndexPersistenceOperation | null => no(a, PRO) -function re(a: ts.CallExpression | ts.NewExpression, sym: IndexSymbol, d: FileContext, ctx: CollectionState): void { - if ((ctx.ci.get(a)?.length ?? 0) > 1) return +function bi(a: ts.Expression, q0: CollectionState): 0 | 1 | -1 { + const c = ck(q0), t = c[TL](a), s = c.getStringType() + const one = (v: ts.Type): 0 | 1 | -1 => { + if (v.flags & (TF.Any | TF.Unknown | TF.TypeParameter | TF.Never)) + return -1 + if (c.isTypeAssignableTo(v, s)) return 1 + return c.isTypeAssignableTo(s, v) ? -1 : 0 + } + const q = t.isUnion() ? t.types.map(one) : [one(t)] + return q.every((v) => v === q[0]) ? q[0]! : -1 } +function re(a: ts.CallExpression | ts.NewExpression, s0: IndexSymbol, d: FileContext, q0: CollectionState): void { + if ((q0.ci.get(a)?.[LN] ?? 0) > 1) return const f = [a, 'high', 'framework'] as const - if (isNew(a)) { - if (ii(a.expression, d, ['bullmq'], ['Worker']) && a.arguments?.[0] && a.arguments[1]) - ae(ctx, sym.id, ['bull-consume', rd(a.arguments[0], d, ctx, { c: true }), hv(a.arguments[1], d, ctx), 'bullmq', undefined, ...f]) - return - } - if (isAccess(a.expression)) { - const h = a.expression.name.text, g = a.expression.expression - if (h === 'add' && a.arguments[0]) { - const i = qo(g, sym, d, ctx); if (i) ae(ctx, sym.id, ['bull-publish', i[0], - rd(a.arguments[0], d, ctx, { c: true }), i[1], undefined, ...f]) + if (iN(a)) { + if (!iu(uw(a[EX]), d, q0) + && ii(a[EX], d, ['bullmq'], ['Worker']) + && a[AR]?.[0] && a[AR][1]) + ae(q0, s0.id, [BC, rv(a[AR][0], d, q0), + hv(a[AR][1], d, q0, true), 'bullmq', U0, ...f, U0]) + return } + if (iX(a[EX])) { + const h = a[EX].name.text, g = a[EX][EX] + if (h === 'add' && a[AR][0]) { + const i = qo(g, s0, d, q0) + if (i) { + const n = i[1] === 'bull' ? bi(a[AR][0], q0) : 1 + const l = n >= 0 && !a[AR].slice(0, n + 1) + .some(iSp) + const x = l ? a[AR][n] : U0 + const p = x && !iSp(x) + ? rd(x, d, q0, { c: true, p: true }) : U0 + const v = x && !iSp(x) ? rv(x, d, q0) : U0 + ae(q0, s0.id, [BP, i[0], + n === 1 && !iSp(a[AR][0]) + ? rv(a[AR][0], d, q0) : uk(), + i[1], p, ...f, + p && v && kn(p) && js(p) === js(v) ? n : U0]) + } } - const b = es(g, sym, d, ctx) - if (b && h === 'emit' && a.arguments[0]) - ae(ctx, sym.id, ['event-publish', rd(a.arguments[0], d, ctx, { c: true }), undefined, b[1], b[0], ...f]) - else if (b && ['addListener', 'on', 'once', 'prependListener'].includes(h) - && a.arguments[0] && a.arguments[1]) - ae(ctx, sym.id, ['event-consume', rd(a.arguments[0], d, ctx, { c: true }), hv(a.arguments[1], d, ctx), b[1], b[0], ...f]) - } - const e = po(a, d, ctx); if (e) ae(ctx, sym.id, ['persistence', ...e, undefined, ...f]) -} -function pc(b: ts.CallExpression, ctx: CollectionState): { - combinator: 'all' | 'allSettled' | 'any' | 'race' - completion: 'all_or_first_rejection' | 'all_settled' | 'first_fulfilled' | 'first_settled' -} | null { - if (!isAccess(b.expression) - || !isIdentifier(b.expression.expression) - || b.expression.expression.text !== 'Promise') return null - const c = sy(b.expression.expression, ctx) - if (!c?.declarations?.some((d) => d.getSourceFile().isDeclarationFile - && /\/typescript\/lib\/lib\..+\.d\.ts$/u.test(d.getSourceFile().fileName.replaceAll('\\', '/')))) + const b = es(g, s0, d, q0) + if (b && h === 'emit' && a[AR][0]) + ae(q0, s0.id, [EP, rv(a[AR][0], d, q0), U0, b[1], b[0], ...f]) + else if (b && ['addListener', 'on', 'once', 'prependListener'][IC](h) && a[AR][0] && a[AR][1]) + ae(q0, s0.id, [EC, rv(a[AR][0], d, q0), hv(a[AR][1], d, q0), b[1], b[0], ...f]) + } + const e = po(a, d, q0); if (e) ae(q0, s0.id, [PE, ...e, U0, ...f]) +} +function pc(b: ts.CallExpression, q0: CollectionState): readonly [ + 'all' | 'allSettled' | 'any' | 'race', + 'all_or_first_rejection' | 'all_settled' | 'first_fulfilled' | 'first_settled', +] | null { + if (!iX(b[EX]) || !iI(b[EX][EX]) || b[EX][EX].text !== 'Promise') return null + const c = sy(b[EX][EX], q0) + if (!c?.[DC]?.some(lb)) return null - const a = b.expression.name.text - return Object.hasOwn(PMC, a) - ? { combinator: a as keyof typeof PMC, - completion: PMC[a as keyof typeof PMC] } : null + const a = b[EX].name.text + return Object.hasOwn(PMC, a) ? [a as keyof typeof PMC, + PMC[a as keyof typeof PMC]] : null } function pl(c: ts.Expression | undefined): readonly ts.Expression[] | null { if (!c) return null const b = uw(c); - if (!ts.isArrayLiteralExpression(b) - || b.elements.length > VELE - || b.elements.some((a) => - ts.isOmittedExpression(a) || ts.isSpreadElement(a))) return null - return [...b.elements] as ts.Expression[] -} -function mi(b: ts.Expression | undefined, e: FileContext, ctx: CollectionState): { - call: ts.CallExpression; input: IndexValue; receiver: ts.Expression -} | null { + if (!iA(b) || b[LE][LN] > VELE || b[LE].some((a) => + iOm(a) || iSp(a))) return null + return [...b[LE]] as ts.Expression[] } +function mi(b: ts.Expression | undefined, e: FileContext, q0: CollectionState): readonly [ + ts.CallExpression, IndexValue, ts.Expression, +] | null { if (!b) return null const a = uw(b) - if (!isCall(a) - || !isAccess(a.expression) - || a.expression.name.text !== 'map') return null - const d = rd(a.expression.expression, e, ctx, { c: true }) - return d.kind === 'array' - ? { call: a, input: d, receiver: a.expression.expression } : null -} + if (!iC(a) || !iX(a[EX]) || a[EX].name.text !== 'map') return null + const d = rv(a[EX][EX], e, q0) + return d.kind === 'array' ? [a, d, a[EX][EX]] : null } +function eu(a: ts.EnumDeclaration, u: ts.Identifier, q0: CollectionState): boolean { + const s = sy(u, q0); if (!s || q0.u.has(s)) return false + const r: ts.Node = gs(u) + let ok = true + const visit = (x: ts.Node): void => { + if (!ok || x === a || x === u) return + if (iI(x) && sy(x, q0) === s) { + let p: ts.Node = x + while (p[PR] && p[PR] !== r && !iK(p[PR]) && !iTn(p[PR])) p = p[PR] + if (!iTn(p[PR]) && (!iK(p[PR]) || p[PR][EX] !== p)) { + ok = false + return } + } + fc(x, visit) + } + visit(r) + return ok } function sk( - f: ts.Expression, g?: FileContext, ctx?: CollectionState, + f: ts.Expression, g?: FileContext, q0?: CollectionState, ): string | null { - const d = g && ctx ? rd(f, g, ctx, { c: true }) : null - if (d?.kind === 'literal') - return JSON.stringify([typeof d.value, d.value]) const b = uw(f) - if (ts.isPrefixUnaryExpression(b) && isNumeric(b.operand) - && [K.PlusToken, K.MinusToken].includes(b.operator) - && Number(b.operand.text) === 0) return JSON.stringify(['number', 0]) - if (ctx && (isAccess(b) || ts.isElementAccessExpression(b))) { - const e = sy(b, ctx)?.declarations?.find(ts.isEnumMember) - const a = e ? ctx.i.checker.getConstantValue(e) - : ctx.i.checker.getConstantValue(b) - if (typeof a === 'number' && Number.isFinite(a)) - return JSON.stringify(['number', Object.is(a, -0) ? 0 : a]) - } - return null + if (q0 && (iX(b) || iE(b))) { + const e = sy(b, q0)?.[DC]?.find(iEm), r = li(b) + if (e && r && ts.isEnumDeclaration(e[PR])) { + const d = e[PR], s = sy(d.name, q0) + const m = ck(q0).getSymbolAtLocation(gs(d)) + const x = gs(d).isDeclarationFile || d.modifiers?.some((v) => [ + K.ExportKeyword, K.DefaultKeyword, K.DeclareKeyword, + ][IC](v.kind)) || !!s && !!m?.exports && [...m.exports.values()].some((v) => fa(v, ck(q0)) === s) + if (x || gs(e) !== gs(b) || !dm(e[PR], b) || !eu(e[PR], r, q0)) return null + const a = ck(q0).getConstantValue(e) + const n = pn(e.name) + if (typeof a === 'string' && (SVAL.test(a) + || SNM.test(d.name.text) || n !== null && SNM.test(n))) return null + if (typeof a === 'string' || typeof a === 'number' && Number.isFinite(a)) + return js([typeof a, typeof a === 'number' && Object.is(a, -0) ? 0 : a]) + return null } + } + const d = g && q0 ? rv(f, g, q0) : null + if (d?.kind === 'literal') + return js([typeof d.value, d.value]) + if (iU(b) && iM(b.operand) && [K.PlusToken, K.MinusToken][IC](b[OP]) && Number(b.operand.text) === 0) return js(['number', 0]) + return null } +const XN = 1, XT = 2, XO = 4, XB = 8, XC = 16 +function xl( + a: ts.Statement, d?: FileContext, q0?: CollectionState, +): number { + const b = xp(a, d, q0) + return b & (XT | XO) | (b & XB ? XN : 0) +} +function xt( + a: ts.Expression, d?: FileContext, q0?: CollectionState, +): boolean | null { + const c = uw(a) + if (c.kind === K.TrueKeyword) return true + if (c.kind === K.FalseKeyword) return false + if (!d || !q0) return null + if (iB(c) && SEQ.has(c[OT].kind)) { + const p = ep([ + c[OT].kind, rv(c.left, d, q0), rv(c.right, d, q0), false, + ], []) + if (p !== null) return p + } + const b = rv(a, d, q0) + return b.kind === 'literal' ? Boolean(b.value) + : b.kind === 'object' || b.kind === 'array' ? true : null } -const XN = 1, XT = 2, XO = 4, XB = 8 function xq( b: readonly ts.Statement[], d?: FileContext, - ctx?: CollectionState, a = XN, + q0?: CollectionState, a = XN, ): number { for (const c of b) { if (!(a & XN)) break - a = a & ~XN | xp(c, d, ctx) + a = a & ~XN | xp(c, d, q0) } - return a -} + return a } function xp( - a: ts.Statement, d?: FileContext, ctx?: CollectionState, + a: ts.Statement, d?: FileContext, q0?: CollectionState, ): number { - if (ts.isReturnStatement(a) || ts.isContinueStatement(a)) return XO + if (iRe(a)) return XO + if (ts.isContinueStatement(a)) return XC if (ts.isBreakStatement(a)) return a.label ? XO : XB - if (ts.isThrowStatement(a)) return XT - if (ts.isBlock(a)) return xq(a.statements, d, ctx) - if (isIf(a)) return xp(a.thenStatement, d, ctx) - | (a.elseStatement ? xp(a.elseStatement, d, ctx) : XN) - if (ts.isSwitchStatement(a)) { - let f = a.caseBlock.clauses.some(ts.isDefaultClause) ? 0 : XN + if (iTh(a)) return XT + if (iBl(a)) return xq(a[ST], d, q0) + if (iJ(a)) { + const b = xt(a[EX], d, q0) + if (b !== null) return b + ? xp(a[TH], d, q0) + : a[EL] ? xp(a[EL], d, q0) : XN + return xp(a[TH], d, q0) + | (a[EL] ? xp(a[EL], d, q0) : XN) + } + if (iDo(a)) { + const b = xp(a.statement, d, q0), q = xt(a[EX], d, q0) + return b & (XT | XO) | (b & XB ? XN : 0) + | (q !== true && b & (XN | XC) ? XN : 0) + } + if (iWh(a) || iFs(a)) { + const q = iWh(a) ? xt(a[EX], d, q0) + : a[CN] ? xt(a[CN], d, q0) : true + if (q === false) return XN + const b = xl(a.statement, d, q0) + return q === true ? b : XN | b & (XT | XO) + } + if (iSw(a)) { + let f = a[CB][CL].some(iDc) ? 0 : XN const j = new Set() - for (let e = 0; e < a.caseBlock.clauses.length; e += 1) { - const g = a.caseBlock.clauses[e]! - if (ts.isCaseClause(g)) { - const key = sk(g.expression, d, ctx) + for (let e = 0; e < a[CB][CL][LN]; e += 1) { + const g = a[CB][CL][e]! + if (iK(g)) { + const key = sk(g[EX], d, q0) if (key && j.has(key)) continue if (key) j.add(key) } let b = XN for (let h = e; - h < a.caseBlock.clauses.length && b & XN; + h < a[CB][CL][LN] && b & XN; h += 1) { - b = xq(a.caseBlock.clauses[h]!.statements, d, ctx, b) + b = xq(a[CB][CL][h]![ST], d, q0, b) } if (b & XB) b = b & ~XB | XN f |= b } - return f - } - if (ts.isTryStatement(a)) { - let c = xp(a.tryBlock, d, ctx) + return f } + if (iTr(a)) { + let c = xp(a.tryBlock, d, q0) if (a.catchClause && c & XT) - c = c & ~XT | xp(a.catchClause.block, d, ctx) + c = c & ~XT | xp(a.catchClause.block, d, q0) if (a.finallyBlock) { - const i = xp(a.finallyBlock, d, ctx) + const i = xp(a.finallyBlock, d, q0) c = (i & XN ? c : 0) | i & ~XN } - return c - } - return XN -} -function ex( - a: ts.Statement, b?: FileContext, ctx?: CollectionState, -): boolean { return !(xp(a, b, ctx) & XN) } -function sx(a: ts.Node): boolean { - return isCall(a) || isNew(a) - || isBinary(a) && AOP.has(a.operatorToken.kind) - || (ts.isPrefixUnaryExpression(a) || ts.isPostfixUnaryExpression(a)) - && [K.PlusPlusToken, K.MinusMinusToken].includes(a.operator) - || ts.isDeleteExpression(a) || ts.isTaggedTemplateExpression(a) - || ts.isAwaitExpression(a) || ts.isYieldExpression(a) - || ts.forEachChild(a, sx) === true -} + return c } + return XN } +const ex = ( + a: ts.Statement, b?: FileContext, q0?: CollectionState, +): boolean => !(xp(a, b, q0) & XN) +const sx = (a: ts.Node): boolean => + iC(a) || iN(a) || iB(a) && AOP.has(a[OT].kind) + || (iU(a) || iPf(a)) + && [K.PlusPlusToken, K.MinusMinusToken][IC](a[OP]) + || iDe(a) || ts.isTaggedTemplateExpression(a) + || iAw(a) || iYi(a) || fc(a, sx) === true function gc( - c: ts.IfStatement, d?: FileContext, ctx?: CollectionState, + c: ts.IfStatement, d?: FileContext, q0?: CollectionState, ): BranchArm | 'unreachable' | null { - const a = ex(c.thenStatement, d, ctx) - const b = c.elseStatement - ? ex(c.elseStatement, d, ctx) : false + const t = xt(c[EX], d, q0) + if (t !== null) { + const s = t ? c[TH] : c[EL] + return s && ex(s, d, q0) ? 'unreachable' : null + } + const a = ex(c[TH], d, q0) + const b = c[EL] ? ex(c[EL], d, q0) : false if (a && b) return 'unreachable' if (a) return 'else' - return b ? 'then' : null -} -function tv(b: ts.VariableDeclaration, sym: IndexSymbol, d: FileContext, ctx: CollectionState): IndexValue | null { - if (!b.initializer || b.parent.parent.parent !== d.sf) return null - const a = rd(b.initializer, d, ctx, { + return b ? 'then' : null } +function tv(b: ts.VariableDeclaration, s0: IndexSymbol, d: FileContext, q0: CollectionState): IndexValue | null { + if (!b[IZ] || b[PR][PR][PR] !== d.sf) return null + const a = rd(b[IZ], d, q0, { c: true, - s: isIdentifier(b.name) && SNM.test(b.name.text), + s: iI(b.name) && SNM.test(b.name.text), }); - if (a.kind === 'unknown' || a.kind === 'symbol' - || a.kind === 'parameter') return null - if (a.kind === 'literal' - && typeof a.value === 'string' - && a.value.length === 0) return null - return sym.id === ds(b, d, ctx)?.id ? a : null; -} -function collect(b: FileContext, ctx: CollectionState): void { + if (a.kind === 'unknown' || a.kind === 'symbol' || a.kind === 'parameter') return null + if (a.kind === 'literal' && typeof a.value === 'string' && a.value[LN] === 0) return null + return s0.id === ds(b, d, q0)?.id ? a : null; } +function collect(b: FileContext, q0: CollectionState): void { const d = ( a: ts.Node, z: readonly IndexControlFrame[], i = false, ): void => { if (ts.isDecorator(a)) return - const sym = ow(a, b) - if (ts.isFunctionLike(a)) { - const t = ca(a, b, ctx) - if (t?.id !== sym?.id && !i) return - if (t?.id === sym?.id && sym && !ctx.p.has(sym.id)) { - ctx.p.set(sym.id, a.parameters.map((j) => - j.initializer - ? rd(j.initializer, b, ctx, { c: true }) : uk())) + const s0 = ow(a, b) + if (iFl(a)) { + const t = ca(a, b, q0) + if (t?.id !== s0?.id && !i) return + if (t?.id === s0?.id && s0 && !q0.p.has(s0.id)) { + q0.p.set(s0.id, a[PA].filter((j) => pi(j) >= 0).map((j) => + j[IZ] ? rv(j[IZ], b, q0) : uk())) } } - if (sym && z.length > INDEX_BODY_FACT_CONTROL_LIMIT) { - ctx.o.add(sym.id) - return - } - if (sym && ts.isBlock(a)) { + if (s0 && z[LN] > ICL) { + q0.o.add(s0.id) + return } + if (s0 && iBl(a)) { let e = z - for (const w of a.statements) { + for (const w of a[ST]) { d(w, e) - if (!isIf(w)) { - if (ex(w, b, ctx)) break + if (!iJ(w)) { + if (ex(w, b, q0)) break continue } - const u = gc(w, b, ctx) + const u = gc(w, b, q0) if (u === 'unreachable') break if (u) { const T = fb( - sym.id, + s0.id, 'condition', - w.expression, + w[EX], b, e, - { n: w }, - ) - ctx.q.set(T.id, u) + { n: w }, ) + q0.q.set(T.id, u) e = br(e, T.id, u) } } - return - } - if (sym && isVariable(a) && isIdentifier(a.name)) { - const Z = tv(a, sym, b, ctx) + return } + if (s0 && iV(a) && iI(a.name)) { + const Z = tv(a, s0, b, q0) if (Z) { - af(ctx, { - ...fb(sym.id, 'literal', a.initializer!, b, z, { + af(q0, { + ...fb(s0.id, 'literal', a[IZ]!, b, z, { n: stmt(a), }), kind: 'literal', @@ -1367,41 +1416,36 @@ function collect(b: FileContext, ctx: CollectionState): void { }) } } - if (sym && isIf(a)) { - const u = gc(a, b, ctx) + if (s0 && iJ(a)) { + const u = gc(a, b, q0) const U = ac( - sym.id, u ? 'guard' : 'if', - a.expression, b, ctx, z, a, - ) - d(a.expression, z) - d(a.thenStatement, br(z, U.id, 'then')) - if (a.elseStatement) { - d(a.elseStatement, br(z, U.id, 'else')) - } - return - } - if (sym && ts.isSwitchStatement(a)) { - if (a.caseBlock.clauses.length > VELE - || a.caseBlock.clauses.some((H) => - ts.isCaseClause(H) && sx(H.expression))) { - ctx.o.add(sym.id) - return + s0.id, u ? 'guard' : 'if', + a[EX], b, q0, z, a, ) + d(a[EX], z) + const t = xt(a[EX], b, q0) + if (t !== false) d(a[TH], br(z, U.id, 'then')) + if (t !== true && a[EL]) { + d(a[EL], br(z, U.id, 'else')) } + return } + if (s0 && iSw(a)) { + if (a[CB][CL][LN] > VELE || a[CB][CL].some((H) => + iK(H) && sx(H[EX]))) { + q0.o.add(s0.id) + return } const $c = ac( - sym.id, 'switch', a.expression, b, ctx, z, a, - ) - d(a.expression, z) + s0.id, 'switch', a[EX], b, q0, z, a, + sd(a, s0.id, b, q0), ) + d(a[EX], z) let ft: IndexControlFrame[][] = [] const iv = b.v const k = new Set() - for (const g of a.caseBlock.clauses) { - const arm = ts.isDefaultClause(g) - ? 'default' as const - : `case:${hash(`${g.expression.getText(b.sf)}:${g.pos}`).slice(0, 16)}` as const - if (ts.isCaseClause(g)) d(g.expression, z) + for (const g of a[CB][CL]) { + const arm = iDc(g) ? 'default' as const : sa(g, b, q0) + if (iK(g)) d(g[EX], z) let I = true - if (ts.isCaseClause(g)) { - const key = sk(g.expression, b, ctx) + if (iK(g)) { + const key = sk(g[EX], b, q0) if (key) { I = !k.has(key) k.add(key) @@ -1410,73 +1454,62 @@ function collect(b: FileContext, ctx: CollectionState): void { const $ = [...(I ? [br(z, $c.id, arm)] : []), ...ft] const V: IndexControlFrame[][] = [] for (const [path, entry] of $.entries()) { - b.v = path === 0 - ? iv - : b.nv++ + b.v = path === 0 ? iv : b.nv++ let q = entry, O = true - for (const x of g.statements) { + for (const x of g[ST]) { d(x, q) - if (!isIf(x)) { - if (ex(x, b, ctx)) { O = false; break } + if (!iJ(x)) { + if (ex(x, b, q0)) { O = false; break } continue } - const u = gc(x, b, ctx) + const u = gc(x, b, q0) if (u === 'unreachable') { O = false; break } if (u) q = br( q, fb( - sym.id, 'condition', x.expression, b, q, + s0.id, 'condition', x[EX], b, q, { n: x }, - ).id, u, - ) + ).id, u, ) } if (O) V.push(q) - if (ctx.o.has(sym.id)) break + if (q0.o.has(s0.id)) break } b.v = iv ft = V - if (ctx.o.has(sym.id)) break + if (q0.o.has(s0.id)) break } - return - } - if (sym && ts.isConditionalExpression(a)) { + return } + if (s0 && iCo(a)) { const W = ac( - sym.id, 'ternary', a.condition, b, ctx, z, stmt(a), - ) - d(a.condition, z) + s0.id, 'ternary', a[CN], b, q0, z, stmt(a), ) + d(a[CN], z) d(a.whenTrue, br(z, W.id, 'truthy')) d(a.whenFalse, br(z, W.id, 'falsy')) - return - } - const l = isBinary(a) ? LFL.get(a.operatorToken.kind) : undefined - if (sym && isBinary(a) && l) { + return } + const l = iB(a) ? LFL.get(a[OT].kind) : U0 + if (s0 && iB(a) && l) { const $d = ac( - sym.id, l[0], a.left, b, ctx, z, stmt(a), - ) + s0.id, l[0], a.left, b, q0, z, stmt(a), ) d(a.left, z) d(a.right, br(z, $d.id, l[1])) - return - } - const s = ld(a) - if (sym && s) { - const X = fb(sym.id, 'loop', a, b, z) + return } + const s = ld(a) + if (s0 && s) { + const X = fb(s0.id, 'loop', a, b, z) const r = [...z, { kind: 'loop' as const, controller_fact_id: X.id, }] - af(ctx, { + af(q0, { ...X, kind: 'loop', - loop_kind: s.kind, - ...(s.test - ? { test: rd(s.test, b, ctx, { c: true }) } - : {}), - }) - for (const _ of s.once) d(_, z) - for (const L of s.repeated) d(L, r) - d(s.body, r) - return - } - if (sym && ts.isTryStatement(a)) { + loop_kind: s[0], + ...(s[1] ? { test: rv(s[1], b, q0) } : {}), + }) + for (const _ of s[2]) d(_, z) + for (const L of s[3]) d(L, r) + d(s[4], r) + return } + if (s0 && iTr(a)) { d(a.tryBlock, [...z, { kind: 'exception', arm: 'try' }]) if (a.catchClause) { d(a.catchClause, [...z, { kind: 'exception', arm: 'catch' }]) @@ -1484,74 +1517,60 @@ function collect(b: FileContext, ctx: CollectionState): void { if (a.finallyBlock) { d(a.finallyBlock, [...z, { kind: 'exception', arm: 'finally' }]) } - return - } - if (sym && ts.isReturnStatement(a)) { - af(ctx, { - ...fb(sym.id, 'return', a, b, z), + return } + if (s0 && iRe(a)) { + af(q0, { + ...fb(s0.id, 'return', a, b, z), kind: 'return', - ...(a.expression - ? { value: rd(a.expression, b, ctx, { c: true }) } - : {}), + ...(a[EX] ? { value: rv(a[EX], b, q0) } : {}), }) - if (a.expression) d(a.expression, z) - return - } - if (sym && ts.isThrowStatement(a)) { - af(ctx, { - ...fb(sym.id, 'throw', a, b, z), + if (a[EX]) d(a[EX], z) + return } + if (s0 && iTh(a)) { + af(q0, { + ...fb(s0.id, 'throw', a, b, z), kind: 'throw', - value: rd(a.expression, b, ctx, { c: true }), + value: rv(a[EX], b, q0), }) - d(a.expression, z) - return - } - if (sym && isBinary(a) && AOP.has(a.operatorToken.kind)) { - am(sym.id, a, 'assign', a.left, b, ctx, z, a.right) + d(a[EX], z) + return } + if (s0 && iB(a) && AOP.has(a[OT].kind)) { + am(s0.id, a, 'assign', a.left, b, q0, z, a.right) } else if ( - sym - && (ts.isPrefixUnaryExpression(a) || ts.isPostfixUnaryExpression(a)) - && [K.PlusPlusToken, K.MinusMinusToken].includes(a.operator) + s0 && (iU(a) || iPf(a)) && [K.PlusPlusToken, K.MinusMinusToken][IC](a[OP]) ) { am( - sym.id, a, - a.operator === K.PlusPlusToken ? 'increment' : 'decrement', - a.operand, b, ctx, z, - ) - } else if (sym && ts.isDeleteExpression(a)) { - am(sym.id, a, 'delete', a.expression, b, ctx, z) + s0.id, a, + a[OP] === K.PlusPlusToken ? 'increment' : 'decrement', + a.operand, b, q0, z, ) + } else if (s0 && iDe(a)) { + am(s0.id, a, 'delete', a[EX], b, q0, z) } - if (sym && (isCall(a) || isNew(a))) { - cf(a, sym, b, ctx, z) - re(a, sym, b, ctx) - if (isCall(a)) { - const G = pc(a, ctx) + if (s0 && (iC(a) || iN(a))) { + cf(a, s0, b, q0, z) + re(a, s0, b, q0) + if (iC(a)) { + const G = pc(a, q0) if (G) { - const h = mi(a.arguments[0], b, ctx) - const E = h ? null : pl(a.arguments[0]) - const F = h?.input ?? (E - ? rd(a.arguments[0]!, b, ctx, { c: true }) - : null) - if (!F || F.kind !== 'array' - || (E && E.length !== F.elements.length)) { - for (const M of a.arguments) d(M, z) - return - } - const J = fb(sym.id, 'parallel', a, b, z) - const Q = ctx.f.get(sym.id)?.length ?? 0 + const h = mi(a[AR][0], b, q0) + const E = h ? null : pl(a[AR][0]) + const F = h?.[1] ?? (E ? rv(a[AR][0]!, b, q0) : null) + if (!F || F.kind !== 'array' || (E && E[LN] !== F[LE][LN])) { + for (const M of a[AR]) d(M, z) + return } + const J = fb(s0.id, 'parallel', a, b, z) + const Q = q0.f.get(s0.id)?.[LN] ?? 0 if (h) { - cf(h.call, sym, b, ctx, z) - re(h.call, sym, b, ctx) - const R = ai(sym.id, h.call, b, ctx, z) - d(h.receiver, z) - for (const y of h.call.arguments) { + cf(h[0], s0, b, q0, z) + re(h[0], s0, b, q0) + const R = ai(s0.id, h[0], b, q0, z) + d(h[2], z) + for (const y of h[0][AR]) { const A = uw(y) - const B = isArrow(A) - || isFunction(A) + const B = iR(A) || iF(A) d( y, - B - ? [ + B ? [ ...z, { kind: 'loop', controller_fact_id: R }, { @@ -1559,10 +1578,8 @@ function collect(b: FileContext, ctx: CollectionState): void { controller_fact_id: J.id, lane: 'each', }, - ] - : z, - B, - ) + ] : z, + B, ) } } else { for (const [lane, expr] of E!.entries()) { @@ -1573,181 +1590,148 @@ function collect(b: FileContext, ctx: CollectionState): void { }]) } } - const ids = (ctx.f.get(sym.id) ?? []) - .slice(Q) - .filter((Y) => Y.kind === 'call' - && Y.control.some((P) => - P.kind === 'parallel' - && P.controller_fact_id === J.id)) - .map(($e) => $e.id) - af(ctx, { + const ids = (q0.f.get(s0.id) ?? []) .slice(Q) .filter((Y) => Y.kind === 'call' && Y.control.some((P) => + P.kind === 'parallel' && P.controller_fact_id === J.id)) .map(($e) => $e.id) + af(q0, { ...J, kind: 'parallel', - ...G, - lane_count: F.elements.length, + combinator: G[0], + completion: G[1], + lane_count: F[LE][LN], input: F, member_fact_ids: ids, }) - return - } + return } if ( - isAccess(a.expression) - && AIM.has(a.expression.name.text) + iX(a[EX]) && AIM.has(a[EX].name.text) ) { const $a = rd( - a.expression.expression, + a[EX][EX], b, - ctx, - { c: true }, - ) - d(a.expression.expression, z) + q0, + { c: true }, ) + d(a[EX][EX], z) if ($a.kind !== 'array') { - for (const N of a.arguments) d(N, z) - return - } - const S = ai(sym.id, a, b, ctx, z) - for (const C of a.arguments) { + for (const N of a[AR]) d(N, z) + return } + const S = ai(s0.id, a, b, q0, z) + for (const C of a[AR]) { const D = uw(C) d(C, [...z, { kind: 'loop', controller_fact_id: S, - }], isArrow(D) || isFunction(D)) + }], iR(D) || iF(D)) } - return - } - const m = isAccess(a.expression) - ? AMU.get(a.expression.name.text) - : undefined - if (m && isAccess(a.expression) - && iar(a.expression.expression, ctx)) { + return } + const m = iX(a[EX]) ? AMU.get(a[EX].name.text) : U0 + if (m && iX(a[EX]) && iar(a[EX][EX], q0)) { am( - sym.id, a, m, a.expression.expression, b, ctx, - z, m === 'append' ? a.arguments[0] : undefined, 1, - ) + s0.id, a, m, a[EX][EX], b, q0, + z, m === 'append' ? a[AR][0] : U0, 1, ) } } } - ts.forEachChild(a, ($b) => d($b, z)) + fc(a, ($b) => d($b, z)) } d(b.sf, []) } -function ld(a: ts.Node): { - kind: 'for' | 'for_in' | 'for_of' | 'while' | 'do_while'; test?: ts.Expression - once: readonly ts.Node[]; repeated: readonly ts.Node[]; body: ts.Statement -} | null { - if (ts.isForStatement(a)) { +function ld(a: ts.Node): readonly [ + 'for' | 'for_in' | 'for_of' | 'while' | 'do_while', + ts.Expression | undefined, readonly ts.Node[], readonly ts.Node[], ts.Statement, +] | null { + if (iFs(a)) { const c: ts.Node[] = [] const b: ts.Node[] = [] - if (a.initializer) c.push(a.initializer) - if (a.condition) b.push(a.condition) + if (a[IZ]) c.push(a[IZ]) + if (a[CN]) b.push(a[CN]) if (a.incrementor) b.push(a.incrementor) - return { - kind: 'for', - ...(a.condition ? { test: a.condition } : {}), - once: c, - repeated: b, - body: a.statement, - } + return ['for', a[CN], c, b, a.statement] } - if (ts.isForInStatement(a) || ts.isForOfStatement(a)) { - return { - kind: ts.isForInStatement(a) ? 'for_in' : 'for_of', - test: a.expression, - once: [a.expression], - repeated: [a.initializer], - body: a.statement, - } + if (iFi(a) || iFo(a)) { + return [iFi(a) ? 'for_in' : 'for_of', a[EX], [a[EX]], [a[IZ]], a.statement] } - if (ts.isWhileStatement(a) || ts.isDoStatement(a)) { - return { - kind: ts.isWhileStatement(a) ? 'while' : 'do_while', - test: a.expression, - once: [], - repeated: [a.expression], - body: a.statement, - } + if (iWh(a) || iDo(a)) { + return [iWh(a) ? 'while' : 'do_while', a[EX], [], [a[EX]], a.statement] } - return null -} -function dc(a: ts.Node): readonly ts.Decorator[] { - return ts.canHaveDecorators(a) ? ts.getDecorators(a) ?? [] : [] -} + return null } +const dc = (a: ts.Node): readonly ts.Decorator[] => + ts.canHaveDecorators(a) ? ts.getDecorators(a) ?? [] : [] function bv( g: ts.Node, h: 'InjectQueue' | 'Processor' | 'Process', - f: FileContext, ctx: CollectionState, -): { value: IndexValue; transport: QueueTransport } | null { - let d: { value: IndexValue; transport: QueueTransport } | null = null + f: FileContext, q0: CollectionState, +): readonly [IndexValue, QueueTransport] | null { + let d: readonly [IndexValue, QueueTransport] | null = null for (const a of dc(g)) { - if (!isCall(a.expression)) continue - const e = a.expression - if (!e.arguments[0]) continue - const b = ib(e.expression, f) - if (b?.i === h - && ['@nestjs/bull', '@nestjs/bullmq'].includes(b.m)) { - d = { - value: rd(e.arguments[0], f, ctx, { c: true }), - transport: b.m === '@nestjs/bull' ? 'bull' : 'bullmq', - } + if (!iC(a[EX])) continue + const e = a[EX] + if (!e[AR][0]) continue + const b = ib(e[EX], f) + if (!iu(uw(e[EX]), f, q0) && b?.i === h + && ['@nestjs/bull', '@nestjs/bullmq'][IC](b.m)) { + d = [rv(e[AR][0], f, q0), + b.m === '@nestjs/bull' ? 'bull' : 'bullmq'] } } - return d -} -function cnest(g: FileContext, ctx: CollectionState): void { - for (const e of g.sf.statements) { - if (!ts.isClassDeclaration(e) || !e.name) continue + return d } +function cnest(g: FileContext, q0: CollectionState): void { + for (const e of g.sf[ST]) { + if (!iCl(e) || !e.name) continue const c = `${g.id}\0${e.name.text}` - const a = ctx.nq.get(c) + const a = q0.nq.get(c) ?? new Map() for (const f of e.members) { - if (!ts.isConstructorDeclaration(f)) continue - for (const b of f.parameters) { - if (!isIdentifier(b.name)) continue - const d = bv(b, 'InjectQueue', g, ctx) - if (d) { - a.set(b.name.text, [d.value, d.transport]) - } + if (!iCd(f)) continue + for (const b of f[PA]) { + if (!iI(b.name)) continue + const d = bv(b, 'InjectQueue', g, q0) + const t = b.type && iT(b.type) + ? ts.isQualifiedName(b.type.typeName) + ? b.type.typeName.left : b.type.typeName + : null + if (t && iu(t as ts.Expression, g, q0)) continue + if (d) a.set(b.name.text, d) } } - if (a.size > 0) ctx.nq.set(c, a) + if (a.size > 0) q0.nq.set(c, a) } } -function nc(d: FileContext, ctx: CollectionState): void { - for (const j of d.sf.statements) { - if (!ts.isClassDeclaration(j) || !j.name) continue - const c = bv(j, 'Processor', d, ctx) +function nc(d: FileContext, q0: CollectionState): void { + for (const j of d.sf[ST]) { + if (!iCl(j) || !j.name) continue + const c = bv(j, 'Processor', d, q0) if (!c) continue for (const a of j.members) { - if (!ts.isMethodDeclaration(a) || !a.name || !isIdentifier(a.name)) continue - const b = ds(a, d, ctx) + if (!iMd(a) || !a.name || !iI(a.name)) continue + const b = ds(a, d, q0) if (!b) continue - const job = bv(a, 'Process', d, ctx) - if (job?.transport === c.transport) { - const f = ss(c.value) - const k = ss(job.value) + const job = bv(a, 'Process', d, q0) + if (job?.[1] === c[1]) { + const f = ss(c[0]) + const k = ss(job[0]) if (f && k) { - const e = ch(ctx, { + const e = ch(q0, { channel_kind: 'queue', - transport: c.transport, + transport: c[1], key: f, }) - const h = ch(ctx, { + const h = ch(q0, { channel_kind: 'job', - transport: c.transport, + transport: c[1], key: k, parent_channel_id: e.id, }) - ce(ctx, b.id, h.id, b.id, 'consumed_by', a, d, 'framework-decorator') - ce(ctx, b.id, h.id, e.id, 'routes_through', a, d, 'framework-decorator') + ce(q0, b.id, h.id, b.id, CY, a, d, FD) + ce(q0, b.id, h.id, e.id, RT, a, d, FD) } } else if (!job && a.name.text === 'process') { - const g = ss(c.value) + const g = ss(c[0]) if (g) { - const i = ch(ctx, { + const i = ch(q0, { channel_kind: 'queue', - transport: c.transport, + transport: c[1], key: g, }) - ce(ctx, b.id, i.id, b.id, 'consumed_by', a, d, 'framework-decorator') + ce(q0, b.id, i.id, b.id, CY, a, d, FD) } } } @@ -1762,14 +1746,13 @@ function ep(e: Predicate, h: readonly IndexValue[]): boolean | null { else { const b = sub(right!, h) if (b.kind !== 'literal') return null - const g = typeof a.value === typeof b.value - || a.value === null && b.value === null - if ([K.EqualsEqualsToken, K.EqualsEqualsEqualsToken].includes(op)) + const g = typeof a.value === typeof b.value || a.value === null && b.value === null + if ([K.EqualsEqualsToken, K.EqualsEqualsEqualsToken][IC](op)) c = g && a.value === b.value - else if ([K.ExclamationEqualsToken, K.ExclamationEqualsEqualsToken].includes(op)) + else if ([K.ExclamationEqualsToken, K.ExclamationEqualsEqualsToken][IC](op)) c = !g || a.value !== b.value else { - if (!g || !['number', 'string'].includes(typeof a.value)) return null + if (!g || !['number', 'string'][IC](typeof a.value)) return null const f = a.value as number | string const d = b.value as number | string if (op === K.LessThanToken) c = f < d @@ -1778,125 +1761,119 @@ function ep(e: Predicate, h: readonly IndexValue[]): boolean | null { else c = f >= d } } - return negated ? !c : c -} + return negated ? !c : c } function wp( d: string, fx: ExecutionEffect, h: readonly IndexValue[], - ctx: CollectionState, + q0: CollectionState, ): boolean { - let e = ctx.w.get(d) + let e = q0.w.get(d) if (!e) { - e = new Map((ctx.f.get(d) ?? []).map((i) => [i.id, i])) - ctx.w.set(d, e) + e = new Map((q0.f.get(d) ?? []).map((i) => [i.id, i])) + q0.w.set(d, e) } - const ids = ctx.ci.get(fx[5]) ?? [] + const ids = q0.ci.get(fx[5]) ?? [] return ids.some((id) => { const j = e.get(id) - return j?.kind === 'call' - && j.control.every((a) => { + return j?.kind === 'call' && j.control.every((a) => { if (a.kind === 'loop' || a.kind === 'parallel') return false if (a.kind === 'exception') return a.arm !== 'catch' const b = e.get(a.controller_fact_id) if (!b || b.kind !== 'condition') return false if (!b.test || b.condition_kind === 'switch') return false - const c = ctx.r.get(a.controller_fact_id) + const c = q0.r.get(a.controller_fact_id) const g = c ? ep(c, h) : null if (g !== null) { if (a.arm === 'nullish') { - const raw = c![0] === K.Unknown - ? sub(c![1], h) : null - return !c![3] && raw?.kind === 'literal' && raw.value === null - } + const raw = c![0] === K.Unknown ? sub(c![1], h) : null + return !c![3] && raw?.kind === 'literal' && raw.value === null } if (a.arm === 'then' || a.arm === 'truthy') return g - return (a.arm === 'else' || a.arm === 'falsy') - && !g - } - return b.condition_kind === 'guard' - && ctx.q.get(a.controller_fact_id) === a.arm - }) + return (a.arm === 'else' || a.arm === 'falsy') && !g } + return b.condition_kind === 'guard' && q0.q.get(a.controller_fact_id) === a.arm }) }) } -function da(e: string, d: readonly IndexValue[], ctx: CollectionState): readonly IndexValue[] { - const a = ctx.p.get(e) - if (!a || d.length >= a.length) return d +function da(e: string, d: readonly IndexValue[], q0: CollectionState): readonly IndexValue[] { + const a = q0.p.get(e) + if (!a || d[LN] >= a[LN]) return d const b = [...d] - for (let c = d.length; c < a.length; c += 1) + for (let c = d[LN]; c < a[LN]; c += 1) b.push(sub(a[c]!, b)) - return b -} -function ee(a: string, ctx: CollectionState, d: number, h: ReadonlySet): ExecutionEffect[] { - const b = [...(ctx.e.get(a) ?? [])] - if (b.length > EMAX) { ctx.o.add(a); return [] } + return b } +function ms(a: readonly IndexValue[], b: EffectWitness): readonly IndexValue[] { + const i = (b[AR] ?? []).findIndex(iSp) + return i < 0 ? a : a.map((v, n) => n < i ? v : uk('ambiguous')) } +function ee(a: string, q0: CollectionState, d: number, h: ReadonlySet): ExecutionEffect[] { + const b = [...(q0.e.get(a) ?? [])] + if (b[LN] > EMAX) { q0.o.add(a); return [] } if (d >= WHOP || h.has(a)) return b const f = new Set(h).add(a) - for (const g of ctx.c.get(a) ?? []) { + for (const g of q0.c.get(a) ?? []) { if (f.has(g[0])) continue - const i = ee(g[0], ctx, d + 1, f), j = da(g[0], g[1], ctx) - if (ctx.o.has(g[0])) { ctx.o.add(a); return [] } + const i = ee(g[0], q0, d + 1, f) + if (q0.o.has(g[0])) { q0.o.add(a); return [] } for (const fx of i) { - if (b.length >= EMAX) { ctx.o.add(a); return [] } - if (!wp(g[0], fx, j, ctx)) continue; b.push(ie(fx, j, g[2])) + if (b[LN] >= EMAX) { q0.o.add(a); return [] } + const j = ms(da(g[0], g[1], q0), g[2]) + const k = ms(da(g[0], g[3], q0), g[2]) + if (!wp(g[0], fx, j, q0)) continue; b.push(ie(fx, j, k, g[2])) } } - return de(b) -} + return de(b) } function de(c: readonly ExecutionEffect[]): ExecutionEffect[] { const b: ExecutionEffect[] = [], d = new Set() for (const fx of c) { const a = fx[5] - const key = JSON.stringify([ + const key = js([ ...fx.slice(0, 5), - a.getSourceFile().fileName, - a.getStart(a.getSourceFile(), false), + gs(a).fileName, + a[GT](gs(a), false), a.getEnd(), ]) - if (fx[0] === 'persistence' || !d.has(key)) b.push(fx) + if (fx[0] === PE || !d.has(key)) b.push(fx) d.add(key) } - return b -} + return b } type ChannelDescriptor = Omit -function ch(ctx: CollectionState, a: ChannelDescriptor): IndexChannelNode { - const id = indexChannelId(a) +function ch(q0: CollectionState, a: ChannelDescriptor): IndexChannelNode { + const id = ki(a) const b: IndexChannelNode = { id, node_kind: 'channel', ...a } - const c = ctx.ch.get(id) - if (c && JSON.stringify(c) !== JSON.stringify(b)) + const c = q0.ch.get(id) + if (c && js(c) !== js(b)) throw new Error(`Conflicting execution channel identity ${id}`) - ctx.ch.set(id, b) - return b -} + q0.ch.set(id, b) + return b } function ce( - ctx: CollectionState, a: string, h: string, to: string, + q0: CollectionState, a: string, h: string, to: string, i: Extract, b: ts.Node, c: FileContext, f: IndexEdge['source'], - d: Confidence = 'high', + d: Confidence = 'high', p?: number, ): void { const e = ev(b, c.sf, c.id) - ctx.g.push({ + q0.g.push({ from: h, to, kind: i, confidence: d, source: f, evidence: e, - metadata: { execution_owner_id: a }, + metadata: { + execution_owner_id: a, + ...(p === U0 ? {} : { dispatch_payload_argument: p }), + }, }) } -function edgeS(a: IndexFactSource): IndexEdge['source'] { - return a === 'framework' ? 'framework-decorator' : a -} -function fn(c: ts.Node, ctx: CollectionState, a: ReadonlyMap): FileContext | null { - const b = ctx.i.pathToFileId.get(c.getSourceFile().fileName) - return b ? a.get(b) ?? null : null -} -function ur(ctx: CollectionState, c: string, fx: ExecutionEffect, b: FileContext): void { +const edgeS = (a: IndexFactSource): IndexEdge['source'] => + a === 'framework' ? FD : a +function fn(c: ts.Node, q0: CollectionState, a: ReadonlyMap): FileContext | null { + const b = q0.i.pathToFileId.get(gs(c).fileName) + return b ? a.get(b) ?? null : null } +function ur(q0: CollectionState, c: string, fx: ExecutionEffect, b: FileContext): void { const a = fx[5]; const id = `canonical-index.execution.unresolved.${hash([ c, fx[0], b.id, - a.getStart(b.sf, false), + a[GT](b.sf, false), a.getEnd(), ].join(':')).slice(0, 16)}`; - if (ctx.sd.has(id)) + if (q0.sd.has(id)) return; - ctx.sd.add(id); - ctx.d.push({ + q0.sd.add(id); + q0.d.push({ id, level: 'info', message: `Dynamic or ambiguous ${fx[0]} identity; unresolved channel parts were omitted`, @@ -1906,98 +1883,97 @@ function ur(ctx: CollectionState, c: string, fx: ExecutionEffect, b: FileContext }, }); } -function pe(ctx: CollectionState, w: ReadonlyMap): void { - for (const sym of ctx.i.symbols.filter(io)) { - if (ctx.o.has(sym.id)) continue +function pe(q0: CollectionState, w: ReadonlyMap): void { + for (const s0 of q0.i.symbols.filter(io)) { + if (q0.o.has(s0.id)) continue let b = 0 - for (const fx of ee(sym.id, ctx, 0, new Set())) { - if (ctx.o.has(sym.id)) break - const [kind, primary, endpoint, qualifier, scope, witness, confidence, source] = fx - const k = fn(witness, ctx, w) + for (const fx of ee(s0.id, q0, 0, new Set())) { + if (q0.o.has(s0.id)) break + const [a, c, d, e, f, g, h, i] = fx + const k = fn(g, q0, w) if (!k) continue const m = ( C: string, F: string, A: 'publishes_to' | 'consumed_by' | 'routes_through', + P?: number, ): void => ce( - ctx, sym.id, C, F, A, witness, k, - edgeS(source), confidence, - ) - if (kind === 'bull-publish') { - const n = ss(primary) - const z = ss(endpoint) + q0, s0.id, C, F, A, g, k, + edgeS(i), h, P, ) + if (a === BP) { + const n = ss(c) + const z = ss(d) + const p = (q0.ci.get(g)?.[LN] ?? 0) === 1 ? fx[8] : U0 if (!n) { - ur(ctx, sym.id, fx, k) + ur(q0, s0.id, fx, k) continue } - const e = ch(ctx, { + const q = ch(q0, { channel_kind: 'queue', - transport: qualifier, + transport: e, key: n, }) if (!z) { - m(sym.id, e.id, 'publishes_to') - ur(ctx, sym.id, fx, k) + m(s0.id, q.id, PU, p) + ur(q0, s0.id, fx, k) continue } - const u = ch(ctx, { + const u = ch(q0, { channel_kind: 'job', - transport: qualifier, + transport: e, key: z, - parent_channel_id: e.id, + parent_channel_id: q.id, }) - m(sym.id, u.id, 'publishes_to') - m(u.id, e.id, 'routes_through') - } else if (kind === 'bull-consume') { - const p = ss(primary) - const g = si(endpoint) - if (!p || !g || !ctx.y.has(g)) { - ur(ctx, sym.id, fx, k) + m(s0.id, u.id, PU, p) + m(u.id, q.id, RT) + } else if (a === BC) { + const p = ss(c) + const n = si(d) + if (!p || !n || !q0.y.has(n)) { + ur(q0, s0.id, fx, k) continue } - const x = ch(ctx, { + const x = ch(q0, { channel_kind: 'queue', - transport: qualifier, + transport: e, key: p, }) - m(x.id, g, 'consumed_by') - } else if (kind === 'event-publish' || kind === 'event-consume') { - const q = ss(primary) - const h = kind === 'event-consume' ? si(endpoint!) : null - if (!q || (kind === 'event-consume' - && (!h || !ctx.y.has(h)))) { - ur(ctx, sym.id, fx, k) + m(x.id, n, CY) + } else if (a === EP || a === EC) { + const q = ss(c) + const n = a === EC ? si(d!) : null + if (!q || (a === EC && (!n || !q0.y.has(n)))) { + ur(q0, s0.id, fx, k) continue } - const l = ch(ctx, { + const l = ch(q0, { channel_kind: 'event', - transport: qualifier, + transport: e, key: q, - scope, + scope: f, }) - if (kind === 'event-publish') { - m(sym.id, l.id, 'publishes_to') + if (a === EP) { + m(s0.id, l.id, PU) } else { - m(l.id, h!, 'consumed_by') + m(l.id, n!, CY) } } else { const a = fx as PersistenceEffect const E = a[1] const B = a[2] const d = a[3] - const v = ctx.f.get(sym.id) ?? [] - const r = fi(sym.id, witness, ctx) - const t = [...new Set(ctx.ci.get(witness) + const v = q0.f.get(s0.id) ?? [] + const r = fi(s0.id, g, q0) + const t = [...new Set(q0.ci.get(g) ?? (r ? [r] : []))] for (const j of t) { const D = v.find((G) => G.id === j) if (!d || D?.kind !== 'call') continue - af(ctx, { + af(q0, { ...fb( - sym.id, 'persistence', witness, k, D.control, - { c: confidence, s: source, o: ++b }, - ), - kind: 'persistence', + s0.id, PE, g, k, D.control, + { c: h, s: i, o: ++b }, ), + kind: PE, operation: E, call_fact_id: j, ...(B ? { resource: B } : {}), @@ -2008,61 +1984,61 @@ function pe(ctx: CollectionState, w: ReadonlyMap): void { } } } -function fi(d: string, b: ts.Node, ctx: CollectionState): string | null { - const sf = b.getSourceFile(), a = ro(b, sf) - return ctx.f.get(d)?.find((c) => c.kind === 'call' - && c.evidence.range.start.line === a.start.line - && c.evidence.range.start.column === a.start.column - && c.evidence.range.end.line === a.end.line - && c.evidence.range.end.column === a.end.column)?.id ?? null -} -function at(ctx: CollectionState): void { - for (const a of ctx.i.symbols) { - if (ctx.o.has(a.id)) { - ctx.d.push({ +function fi(d: string, b: ts.Node, q0: CollectionState): string | null { + const sf = gs(b), a = ro(b, sf) + return q0.f.get(d)?.find((c) => c.kind === 'call' && c.evidence.range.start.line === a.start.line && c.evidence.range.start.column === a.start.column && c.evidence.range.end.line === a.end.line && c.evidence.range.end.column === a.end.column)?.id ?? null } +function at(q0: CollectionState): void { + for (const a of q0.i.symbols) { + if (q0.o.has(a.id)) { + q0.d.push({ id: `canonical-index.execution.owner-bound.${hash(a.id).slice(0, 16)}`, level: 'error', evidence: { file_id: a.file_id, range: a.range }, message: `Execution facts exceeded a per-owner safety bound for ${a.name}; body facts were omitted` }) continue } - const e = ctx.f.get(a.id); if (!e || e.length === 0) continue + const e = q0.f.get(a.id); if (!e || e[LN] === 0) continue const k = new Map(); for (const l of e) k.set(l.id, l) const j = [...k.values()].sort((m, g) => co(m.order, g.order) || ct(m.id, g.id)) try { - const h = encodeIndexBodyFactTable(j), b = decodeIndexBodyFactTable(h, a.id, a.file_id) + const h = eb(j), b = dt(h, a.id, a.file_id) if (!b) throw new Error('execution fact codec rejected its output'); a.body_facts = b } catch (n) { - const c = n instanceof IndexBodyFactBoundsError - ctx.d.push({ + const c = n instanceof BE + q0.d.push({ id: `canonical-index.execution.${c ? 'owner-bound' : 'invalid'}.${hash(a.id).slice(0, 16)}`, - level: 'error', evidence: { file_id: a.file_id, range: a.range }, message: c - ? `Execution facts exceeded a per-owner safety bound for ${a.name}; body facts were omitted` : `Invalid execution facts for ${a.name}; body facts were omitted` }) + level: 'error', evidence: { file_id: a.file_id, range: a.range }, message: c ? `Execution facts exceeded a per-owner safety bound for ${a.name}; body facts were omitted` : `Invalid execution facts for ${a.name}; body facts were omitted` }) delete a.body_facts } } } function sort(e: readonly IndexEdge[]): IndexEdge[] { - const a = new Map(), c: IndexEdge[] = [] + const a = new Map(), p = new Map(), c: IndexEdge[] = [] for (const b of e) { - if (b.kind !== 'routes_through') { - c.push(b); continue + if (b.kind === PU) { + const k = js([b.from, b.to, b.kind, b.source, b.evidence]), d = p.get(k) + if (!d) p.set(k, b) + else if (d.metadata?.dispatch_payload_argument !== b.metadata?.dispatch_payload_argument) { + const m = { ...(d.metadata ?? {}) }; delete m.dispatch_payload_argument + p.set(k, { ...d, metadata: m }) + } else if (ct(js(b), js(d)) < 0) p.set(k, b) + continue } + if (b.kind !== RT) { c.push(b); continue } const key = `${b.from}\u0000${b.to}\u0000${b.kind}` const d = a.get(key) - if (!d - || ct(JSON.stringify(b), JSON.stringify(d)) < 0) + if (!d || ct(js(b), js(d)) < 0) a.set(key, b) } - return [...c, ...a.values()].sort((g, f) => - ct(JSON.stringify(g), JSON.stringify(f))) + return [...c, ...a.values(), ...p.values()].sort((g, f) => + ct(js(g), js(f))) } export function collectExecutionSemantics(h: CollectExecutionInput): CollectExecutionResult { const j = new Map(h.symbols.map((k) => [k.id, k])) - const ctx: CollectionState = { + const q0: CollectionState = { i: h, y: j, f: new Map(), o: new Set(), e: new Map(), c: new Map(), ci: new Map(), ch: new Map(), g: [], d: [], sd: new Set(), u: new Set(), q: new Map(), w: new Map(), p: new Map(), r: new Map(), mq: new Map(), em: new Map(), - nq: new Map(), fs: new Map(), + nq: new Map(), fs: new Map(), x: new Set(), } const a = new Map() for (const sf of h.sourceFiles) { @@ -2073,21 +2049,27 @@ export function collectExecutionSemantics(h: CollectExecutionInput): CollectExec os: os(sf, h.symbolsByFile.get(b) ?? []), v: 0, nv: 1, } a.set(b, l) - ctx.fs.set(sf, l) + q0.fs.set(sf, l) + } + for (const sf of h.sourceFiles) { + const e = (n: ts.Node): boolean => dg(n, sf, q0) || fc(n, e) === true + if (e(sf)) q0.x.add(sf) } - prep(ctx) - for (const t of a.values()) cnest(t, ctx) + prep(q0) + for (const t of a.values()) + if (!q0.x.has(t.sf)) cnest(t, q0) for (const m of a.values()) { - collect(m, ctx) - nc(m, ctx) + if (q0.x.has(m.sf)) continue + collect(m, q0) + nc(m, q0) } - pe(ctx, a) - at(ctx) + pe(q0, a) + at(q0) return { - channels: [...ctx.ch.values()].sort((x, n) => + channels: [...q0.ch.values()].sort((x, n) => ct(x.id, n.id)), - edges: sort(ctx.g), - diagnostics: [...ctx.d].sort((z, s) => + edges: sort(q0.g), + diagnostics: [...q0.d].sort((z, s) => ct(z.id, s.id)), } } diff --git a/src/domain/index/build-state.ts b/src/domain/index/build-state.ts index 7aebaab2..21ba7e07 100644 --- a/src/domain/index/build-state.ts +++ b/src/domain/index/build-state.ts @@ -7,7 +7,7 @@ import { hasExactKeys, isRecord } from '../../shared/guards.js' export const CANONICAL_INDEX_FORMAT_VERSION = 4 as const export const GENERATION_POLICY_VERSION = 4 as const export const INDEX_BUILD_STATE_VERSION = 3 as const -export const INDEX_ENGINE_ID = 'madar-typescript-index-v4' as const +export const INDEX_ENGINE_ID = 'madar-typescript-index-v4-execution-1' as const export const INDEXING_OUTCOME_STATUSES = [ 'indexed', 'indexed_with_warnings', 'skipped_by_policy', 'unsupported', 'failed', ] as const diff --git a/src/domain/index/model.ts b/src/domain/index/model.ts index 1902b764..5992ba4e 100644 --- a/src/domain/index/model.ts +++ b/src/domain/index/model.ts @@ -50,39 +50,24 @@ export type IndexPersistenceOperation = | 'read' | 'create' | 'update' | 'delete' | 'upsert' | 'transaction' | 'file_read' | 'file_write' | 'object_read' | 'object_write' type IndexUnknownReason = 'dynamic' | 'ambiguous' | 'unsupported' -const KINDS = [ - 'condition', 'loop', 'parallel', 'call', 'literal', - 'mutation', 'persistence', 'return', 'throw', -] as const -const LEVELS = ['high', 'medium', 'low'] as const -const SOURCES = [ - 'typescript-semantic', 'typescript-syntactic', 'framework', 'wrapper-summary', -] as const -const TIMING = ['sync', 'awaited', 'fire_and_forget'] as const -const ROLES = [ - 'argument', 'initializer', 'condition', 'return', 'channel', 'configuration', -] as const -const CONDITIONS = [ - 'if', 'switch', 'ternary', 'logical_and', 'logical_or', 'nullish', 'guard', -] as const -const LOOPS = [ - 'for', 'for_in', 'for_of', 'while', 'do_while', 'array_iteration', -] as const -const PROMISES = ['all', 'allSettled', 'any', 'race'] as const -const COMPLETION = [ - 'all_or_first_rejection', 'all_settled', 'first_fulfilled', 'first_settled', -] as const -const MUTATIONS = [ - 'assign', 'increment', 'decrement', 'append', 'remove', 'delete', -] as const -const STORAGE = [ - 'read', 'create', 'update', 'delete', 'upsert', 'transaction', - 'file_read', 'file_write', 'object_read', 'object_write', -] as const -const UNKNOWN = ['dynamic', 'ambiguous', 'unsupported'] as const +const KS = ['condition', 'loop', 'parallel', 'call', 'literal', + 'mutation', 'persistence', 'return', 'throw'] as const +const LS = ['high', 'medium', 'low'] as const +const SS = ['typescript-semantic', 'typescript-syntactic', 'framework', 'wrapper-summary'] as const +const TM = ['sync', 'awaited', 'fire_and_forget'] as const +const RS = ['argument', 'initializer', 'condition', 'return', 'channel', 'configuration'] as const +const CS = ['if', 'switch', 'ternary', 'logical_and', 'logical_or', 'nullish', 'guard'] as const +const LP = ['for', 'for_in', 'for_of', 'while', 'do_while', 'array_iteration'] as const +const PM = ['all', 'allSettled', 'any', 'race'] as const +const CP = ['all_or_first_rejection', 'all_settled', 'first_fulfilled', 'first_settled'] as const +const MU = ['assign', 'increment', 'decrement', 'append', 'remove', 'delete'] as const +const ST = ['read', 'create', 'update', 'delete', 'upsert', 'transaction', + 'file_read', 'file_write', 'object_read', 'object_write'] as const +const UK = ['dynamic', 'ambiguous', 'unsupported'] as const const SHA256 = /^[a-f0-9]{64}$/ -const MAX_ROWS = 8_192, MAX_ROW = 262_144, MAX_TABLE = 8_388_608 -const MAX_DEPTH = 5, MAX_ELEMENTS = 32, MAX_TEXT = 512 +const MR = 8_192, MB = 262_144, MT = 8_388_608 +const MD = 5, ME = 32, MX = 512 +const aa = Array.isArray, bl = Buffer.byteLength, js = JSON.stringify export type IndexFactEvidence = Immutable<{ file_id: string /** Smallest expression or token range that proves the fact. */ @@ -174,296 +159,249 @@ export function indexBodyFactId( export type IndexBodyFactTable = readonly [version: 1, rows: readonly string[]] export const INDEX_BODY_FACT_CONTROL_LIMIT = 64 export class IndexBodyFactBoundsError extends Error {} -function ep(values: readonly string[], value: string): number { - const index = values.indexOf(value) - if (index < 0) throw new Error(`Unsupported execution value ${value}`) - return index -} -function oc(left: readonly number[], right: readonly number[]): number { - for (let index = 0; index < Math.min(left.length, right.length); index += 1) { - const difference = left[index]! - right[index]! - if (difference !== 0) return difference - } - return left.length - right.length -} -function dn(value: readonly unknown[]): boolean { - for (let index = 0; index < value.length; index += 1) - if (!Object.hasOwn(value, index)) return false - return true -} -function sc(value: unknown): value is IndexScalarValue { - return (value === null || ['string', 'number', 'boolean'].includes(typeof value)) - && !(typeof value === 'number' && (!Number.isFinite(value) || Object.is(value, -0))) - && !(typeof value === 'string' && Buffer.byteLength(value, 'utf8') > MAX_TEXT) -} -function pv(value: IndexValue, depth = 0): unknown { - const nestedCount = value.kind === 'array' ? value.elements.length - : value.kind === 'object' ? value.entries.length - : value.kind === 'template' ? value.parts.length : 0 - if (depth > MAX_DEPTH || (depth === MAX_DEPTH && nestedCount > 0)) - return [7, ep(UNKNOWN, 'unsupported')] - switch (value.kind) { +function ep(a: readonly string[], b: string): number { const i = a.indexOf(b); if (i < 0) throw new Error(`Unsupported execution value ${b}`); return i } +function oc(a: readonly number[], b: readonly number[]): number { for (let i = 0; i < Math.min(a.length, b.length); i += 1) { const d = a[i]! - b[i]!; if (d !== 0) return d } return a.length - b.length } +function dn(a: readonly unknown[]): boolean { for (let i = 0; i < a.length; i += 1) if (!Object.hasOwn(a, i)) return false; return true } +const sc = (a: unknown): a is IndexScalarValue => (a === null || ['string', 'number', 'boolean'].includes(typeof a)) && !(typeof a === 'number' && (!Number.isFinite(a) || Object.is(a, -0))) && !(typeof a === 'string' && bl(a) > MX) +function pv(a: IndexValue, d = 0): unknown { + const n = a.kind === 'array' ? a.elements.length + : a.kind === 'object' ? a.entries.length + : a.kind === 'template' ? a.parts.length : 0 + if (d > MD || (d === MD && n > 0)) + return [7, ep(UK, 'unsupported')] + switch (a.kind) { case 'literal': - if (!sc(value.value)) throw new Error('Execution literal is not JSON-lossless') - return [0, value.value] + if (!sc(a.value)) throw new Error('Execution literal is not JSON-lossless') + return [0, a.value] case 'symbol': - if (!vt(value.symbol_id, 1_024)) + if (!vt(a.symbol_id, 1_024)) throw new Error('Execution symbol reference is invalid') - return [1, value.symbol_id] + return [1, a.symbol_id] case 'parameter': - if (!si(value.position) - || (value.scope !== undefined && value.scope !== 'iteration')) + if (!si(a.position) + || (a.scope !== undefined && a.scope !== 'iteration')) throw new Error('Execution parameter position is invalid') - return value.scope === 'iteration' - ? [2, value.position, 1] - : [2, value.position] + return a.scope === 'iteration' + ? [2, a.position, 1] + : [2, a.position] case 'array': - if (value.elements.length > MAX_ELEMENTS || !dn(value.elements)) + if (a.elements.length > ME || !dn(a.elements)) throw new Error('Execution array exceeds its element bound') - return [3, value.elements.map((entry) => pv(entry, depth + 1))] + return [3, a.elements.map((e) => pv(e, d + 1))] case 'object': { - const keys = new Set() - if (value.entries.length > MAX_ELEMENTS || !dn(value.entries)) + const k = new Set() + if (a.entries.length > ME || !dn(a.entries)) throw new Error('Execution object exceeds its element bound') - for (const entry of value.entries) { - if (Buffer.byteLength(entry.key, 'utf8') > MAX_TEXT - || entry.key.includes('\0') || keys.has(entry.key)) + for (const e of a.entries) { + if (bl(e.key) > MX || e.key.includes('\0') || k.has(e.key)) throw new Error('Execution object key is invalid') - keys.add(entry.key) + k.add(e.key) } - return [4, value.entries.map((entry) => [ - entry.key, pv(entry.value, depth + 1), + return [4, a.entries.map((e) => [ + e.key, pv(e.value, d + 1), ])] } case 'template': - if (value.parts.length > MAX_ELEMENTS || !dn(value.parts)) + if (a.parts.length > ME || !dn(a.parts)) throw new Error('Execution template exceeds its element bound') - return [5, value.parts.map((entry) => pv(entry, depth + 1))] + return [5, a.parts.map((e) => pv(e, d + 1))] case 'redacted': - if (!SHA256.test(value.sha256) || !si(value.byte_length)) + if (!SHA256.test(a.sha256) || !si(a.byte_length)) throw new Error('Execution redaction is invalid') - return [6, value.sha256, value.byte_length] - case 'unknown': return [7, ep(UNKNOWN, value.reason)] + return [6, a.sha256, a.byte_length] + case 'unknown': return [7, ep(UK, a.reason)] } throw new Error('Unsupported execution value') } -function pe(proof: IndexFactEvidence): unknown { - return [ - proof.range.start.line, proof.range.start.column, - proof.range.end.line, proof.range.end.column, - proof.statement_range.start.line, proof.statement_range.start.column, - proof.statement_range.end.line, proof.statement_range.end.column, proof.excerpt_sha256, - ] -} +const pe = (a: IndexFactEvidence): unknown => [a.range.start.line, a.range.start.column, a.range.end.line, a.range.end.column, a.statement_range.start.line, a.statement_range.start.column, a.statement_range.end.line, a.statement_range.end.column, a.excerpt_sha256] export function encodeIndexBodyFactTable( facts: readonly IndexBodyFact[], ): IndexBodyFactTable { - if (facts.length === 0 || facts.length > MAX_ROWS) { + if (facts.length === 0 || facts.length > MR) { throw new IndexBodyFactBoundsError( 'Execution fact table is outside its row bound', ) } if (!dn(facts)) throw new Error('Execution fact table is sparse') - const ordered = [...facts].sort((left, right) => - oc(left.order, right.order) - || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)) - const ordinals = new Map(ordered.map((fact, index) => [fact.id, index])) - if (ordinals.size !== ordered.length) + const a = [...facts].sort((l, r) => + oc(l.order, r.order) || (l.id < r.id ? -1 : l.id > r.id ? 1 : 0)) + const m = new Map(a.map((f, i) => [f.id, i])) + if (m.size !== a.length) throw new Error('Execution fact IDs are not unique') - const ordinal = (id: string): number => { - const value = ordinals.get(id) - if (value === undefined) throw new Error(`Missing execution fact reference ${id}`) - return value + const oi = (id: string): number => { + const v = m.get(id) + if (v === undefined) throw new Error(`Missing execution fact reference ${id}`) + return v } - const control = (frame: IndexControlFrame): unknown => { - if (frame.kind === 'branch') { - if (!vt(frame.arm, 96) - || (!['then', 'else', 'truthy', 'falsy', 'nullish', 'default'].includes(frame.arm) - && !(frame.arm.startsWith('case:') && frame.arm.length > 5))) { + const cf = (f: IndexControlFrame): unknown => { + if (f.kind === 'branch') { + if (!vt(f.arm, 96) + || (!['then', 'else', 'truthy', 'falsy', 'nullish', 'default'].includes(f.arm) + && !(f.arm.startsWith('case:') && f.arm.length > 5))) { throw new Error('Execution branch arm is invalid') } - return [0, ordinal(frame.controller_fact_id), frame.arm] + return [0, oi(f.controller_fact_id), f.arm] } - if (frame.kind === 'loop') return [1, ordinal(frame.controller_fact_id)] - if (frame.kind === 'parallel') { - if (frame.lane !== 'each' && !si(frame.lane)) + if (f.kind === 'loop') return [1, oi(f.controller_fact_id)] + if (f.kind === 'parallel') { + if (f.lane !== 'each' && !si(f.lane)) throw new Error('Execution parallel lane is invalid') - return [2, ordinal(frame.controller_fact_id), frame.lane] + return [2, oi(f.controller_fact_id), f.lane] } - if (frame.kind === 'exception') - return [3, ep(['try', 'catch', 'finally'], frame.arm)] + if (f.kind === 'exception') + return [3, ep(['try', 'catch', 'finally'], f.arm)] throw new Error('Unsupported execution control frame') } - let bytes = 0 - const orderKeys = new Set() - const rows = ordered.map((fact) => { - const orderKey = fact.order.join('.') - if (fact.order.length !== 4 - || !dn(fact.order) || !fact.order.every((value) => si(value)) - || !dn(fact.control) - || fact.control.length > INDEX_BODY_FACT_CONTROL_LIMIT - || fact.order[1] !== ep(KINDS, fact.kind) - || orderKeys.has(orderKey)) { - throw new Error(`Invalid execution fact identity ${fact.id}`) + let b = 0 + const k = new Set() + const r = a.map((f) => { + const o = f.order.join('.') + if (f.order.length !== 4 + || !dn(f.order) || !f.order.every((v) => si(v)) + || !dn(f.control) + || f.control.length > INDEX_BODY_FACT_CONTROL_LIMIT + || f.order[1] !== ep(KS, f.kind) + || k.has(o)) { + throw new Error(`Invalid execution fact identity ${f.id}`) } - orderKeys.add(orderKey) - let wire: unknown - switch (fact.kind) { + k.add(o) + let w: unknown + switch (f.kind) { case 'call': - if (!dn(fact.arguments)) throw new Error(`Sparse call arguments for ${fact.id}`) - wire = [ - fact.callee, fact.target_symbol_id ?? null, - fact.arguments.map(pv), ep(TIMING, fact.scheduling), + if (!dn(f.arguments)) throw new Error(`Sparse call arguments for ${f.id}`) + w = [ + f.callee, f.target_symbol_id ?? null, + f.arguments.map(pv), ep(TM, f.scheduling), ] break case 'literal': - wire = [pv(fact.value), ep(ROLES, fact.role)] + w = [pv(f.value), ep(RS, f.role)] break case 'condition': - wire = [ - ep(CONDITIONS, fact.condition_kind), - fact.test ? pv(fact.test) : null, + w = [ + ep(CS, f.condition_kind), + f.test ? pv(f.test) : null, ] break case 'loop': - wire = [ - ep(LOOPS, fact.loop_kind), - fact.test ? pv(fact.test) : null, + w = [ + ep(LP, f.loop_kind), + f.test ? pv(f.test) : null, ] break case 'parallel': { - const combinator = ep(PROMISES, fact.combinator) - if (fact.completion !== COMPLETION[combinator] - || !si(fact.lane_count) - || !dn(fact.member_fact_ids) - || new Set(fact.member_fact_ids).size !== fact.member_fact_ids.length) - throw new Error(`Invalid parallel completion ${fact.id}`) - wire = [ - combinator, fact.input ? pv(fact.input) : null, - fact.member_fact_ids.map(ordinal), fact.lane_count, + const c = ep(PM, f.combinator) + if (f.completion !== CP[c] + || !si(f.lane_count) + || !dn(f.member_fact_ids) + || new Set(f.member_fact_ids).size !== f.member_fact_ids.length) + throw new Error(`Invalid parallel completion ${f.id}`) + w = [ + c, f.input ? pv(f.input) : null, + f.member_fact_ids.map(oi), f.lane_count, ] break } case 'return': case 'throw': - wire = [fact.value ? pv(fact.value) : null] + w = [f.value ? pv(f.value) : null] break case 'mutation': - wire = [ - ep(MUTATIONS, fact.operation), fact.target, - fact.value ? pv(fact.value) : null, + w = [ + ep(MU, f.operation), f.target, + f.value ? pv(f.value) : null, ] break case 'persistence': - if (!vt(fact.receiver_type)) - throw new Error(`Persistence proof is missing for ${fact.id}`) - wire = [ - ep(STORAGE, fact.operation), ordinal(fact.call_fact_id), - fact.resource ? pv(fact.resource) : null, - fact.receiver_type, + if (!vt(f.receiver_type)) + throw new Error(`Persistence proof is missing for ${f.id}`) + w = [ + ep(ST, f.operation), oi(f.call_fact_id), + f.resource ? pv(f.resource) : null, + f.receiver_type, ] break } - const semantics = [ - ep(KINDS, fact.kind), - fact.order[0], fact.order[2], fact.order[3], pe(fact.evidence), - fact.control.map(control), ep(LEVELS, fact.confidence), - ep(SOURCES, fact.source), wire, + const s = [ + ep(KS, f.kind), + f.order[0], f.order[2], f.order[3], pe(f.evidence), + f.control.map(cf), ep(LS, f.confidence), + ep(SS, f.source), w, ] - const sealedId = indexBodyFactId( - fact.owner_symbol_id, fact.kind, fact.order, - fact.evidence.excerpt_sha256, semantics, + const id = indexBodyFactId( + f.owner_symbol_id, f.kind, f.order, + f.evidence.excerpt_sha256, s, ) - if (fact.id !== sealedId && fact.id !== indexBodyFactId( - fact.owner_symbol_id, fact.kind, fact.order, fact.evidence.excerpt_sha256, - )) throw new Error(`Invalid execution fact identity ${fact.id}`) - const row = JSON.stringify([sealedId, ...semantics]) - const rowBytes = Buffer.byteLength(row, 'utf8') - bytes += rowBytes - if (rowBytes > MAX_ROW || bytes > MAX_TABLE) + if (f.id !== id && f.id !== indexBodyFactId( + f.owner_symbol_id, f.kind, f.order, f.evidence.excerpt_sha256, + )) throw new Error(`Invalid execution fact identity ${f.id}`) + const x = js([id, ...s]) + const z = bl(x) + b += z + if (z > MB || b > MT) throw new IndexBodyFactBoundsError( - `Execution fact table exceeds its byte bound at ${fact.id}`, + `Execution fact table exceeds its byte bound at ${f.id}`, ) - return row + return x }) - return [1, rows] -} -function si(value: unknown, minimum = 0): value is number { - return typeof value === 'number' - && Number.isSafeInteger(value) - && !Object.is(value, -0) - && value >= minimum -} -function vt(value: unknown, maxBytes = MAX_TEXT): value is string { - return typeof value === 'string' - && value.length > 0 - && !value.includes('\0') - && Buffer.byteLength(value, 'utf8') <= maxBytes -} -function tu(value: unknown, length: number): unknown[] | null { - return Array.isArray(value) && value.length === length ? value : null + return [1, r] } -function ev(values: readonly T[], value: unknown): T | null { - return si(value) && value < values.length ? values[value]! : null -} -function rv(value: unknown, depth = 0): IndexValue | null { - if (!Array.isArray(value) - || !si(value[0]) || value[0] > 7) return null - if (depth > MAX_DEPTH) return null - if (depth === MAX_DEPTH && [3, 4, 5].includes(value[0]) - && (!Array.isArray(value[1]) || value[1].length > 0)) return null - switch (value[0]) { +const si = (a: unknown, m = 0): a is number => typeof a === 'number' && Number.isSafeInteger(a) && !Object.is(a, -0) && a >= m +const vt = (a: unknown, m = MX): a is string => typeof a === 'string' && a.length > 0 && !a.includes('\0') && bl(a) <= m +const tu = (a: unknown, l: number): unknown[] | null => aa(a) && a.length === l ? a : null +const ev = (a: readonly T[], b: unknown): T | null => si(b) && b < a.length ? a[b]! : null +function rv(a: unknown, d = 0): IndexValue | null { + if (!aa(a) || !si(a[0]) || a[0] > 7) return null + if (d > MD) return null + if (d === MD && [3, 4, 5].includes(a[0]) + && (!aa(a[1]) || a[1].length > 0)) return null + switch (a[0]) { case 0: { - return value.length === 2 && sc(value[1]) - ? { kind: 'literal', value: value[1] } : null + return a.length === 2 && sc(a[1]) + ? { kind: 'literal', value: a[1] } : null } case 1: - return value.length === 2 && vt(value[1], 1_024) - ? { kind: 'symbol', symbol_id: value[1] } + return a.length === 2 && vt(a[1], 1_024) + ? { kind: 'symbol', symbol_id: a[1] } : null case 2: - return (value.length === 2 || (value.length === 3 && value[2] === 1)) - && si(value[1]) + return (a.length === 2 || (a.length === 3 && a[2] === 1)) + && si(a[1]) ? { kind: 'parameter', - position: value[1], - ...(value[2] === 1 ? { scope: 'iteration' as const } : {}), + position: a[1], + ...(a[2] === 1 ? { scope: 'iteration' as const } : {}), } : null case 3: case 5: { - if (value.length !== 2 || !Array.isArray(value[1]) - || value[1].length > MAX_ELEMENTS) return null - const values = value[1].map((entry) => rv(entry, depth + 1)) - if (!values.every((entry): entry is IndexValue => entry !== null)) return null - return value[0] === 3 - ? { kind: 'array', elements: values } - : { kind: 'template', parts: values } + if (a.length !== 2 || !aa(a[1]) || a[1].length > ME) return null + const v = a[1].map((e) => rv(e, d + 1)) + if (!v.every((e): e is IndexValue => e !== null)) return null + return a[0] === 3 + ? { kind: 'array', elements: v } + : { kind: 'template', parts: v } } case 4: { - if (value.length !== 2 || !Array.isArray(value[1]) - || value[1].length > MAX_ELEMENTS) return null - const keys = new Set() - const entries: IndexObjectEntry[] = [] - for (const raw of value[1]) { - const entry = tu(raw, 2) - const decoded = entry ? rv(entry[1], depth + 1) : null - if (!entry || typeof entry[0] !== 'string' || entry[0].includes('\0') - || Buffer.byteLength(entry[0], 'utf8') > MAX_TEXT - || keys.has(entry[0]) || !decoded) return null - keys.add(entry[0]) - entries.push({ key: entry[0], value: decoded }) + if (a.length !== 2 || !aa(a[1]) || a[1].length > ME) return null + const k = new Set(), e: IndexObjectEntry[] = [] + for (const x of a[1]) { + const r = tu(x, 2), v = r ? rv(r[1], d + 1) : null + if (!r || typeof r[0] !== 'string' || r[0].includes('\0') + || bl(r[0]) > MX || k.has(r[0]) || !v) return null + k.add(r[0]) + e.push({ key: r[0], value: v }) } - return { kind: 'object', entries } + return { kind: 'object', entries: e } } case 6: - return value.length === 3 && typeof value[1] === 'string' - && SHA256.test(value[1]) && si(value[2]) - ? { kind: 'redacted', sha256: value[1], byte_length: value[2] } + return a.length === 3 && typeof a[1] === 'string' + && SHA256.test(a[1]) && si(a[2]) + ? { kind: 'redacted', sha256: a[1], byte_length: a[2] } : null case 7: { - const reason = ev(UNKNOWN, value[1]) - return value.length === 2 && reason ? { kind: 'unknown', reason } : null + const r = ev(UK, a[1]) + return a.length === 2 && r ? { kind: 'unknown', reason: r } : null } } return null @@ -473,199 +411,171 @@ type DecodedRow = { evidence: IndexFactEvidence; control: readonly unknown[] confidence: IndexFactConfidence; source: IndexFactSource; payload: unknown } -function re(value: unknown, file: string): IndexFactEvidence | null { - const row = tu(value, 9) - if (!row || !row.slice(0, 8).every((entry) => si(entry, 1)) - || typeof row[8] !== 'string' || !SHA256.test(row[8])) return null - const range = { - start: { line: row[0] as number, column: row[1] as number }, - end: { line: row[2] as number, column: row[3] as number }, - } - const statement_range = { - start: { line: row[4] as number, column: row[5] as number }, - end: { line: row[6] as number, column: row[7] as number }, - } - const compare = (left: IndexPosition, right: IndexPosition): number => - left.line - right.line || left.column - right.column - return compare(range.start, range.end) <= 0 - && compare(statement_range.start, statement_range.end) <= 0 - && compare(statement_range.start, range.start) <= 0 - && compare(range.end, statement_range.end) <= 0 - ? { file_id: file, range, statement_range, excerpt_sha256: row[8] } +function re(a: unknown, f: string): IndexFactEvidence | null { + const r = tu(a, 9) + if (!r || !r.slice(0, 8).every((e) => si(e, 1)) + || typeof r[8] !== 'string' || !SHA256.test(r[8])) return null + const g = { start: { line: r[0] as number, column: r[1] as number }, + end: { line: r[2] as number, column: r[3] as number } } + const s = { start: { line: r[4] as number, column: r[5] as number }, + end: { line: r[6] as number, column: r[7] as number } } + const c = (a: IndexPosition, b: IndexPosition): number => + a.line - b.line || a.column - b.column + return c(g.start, g.end) <= 0 && c(s.start, s.end) <= 0 + && c(s.start, g.start) <= 0 && c(g.end, s.end) <= 0 + ? { file_id: f, range: g, statement_range: s, excerpt_sha256: r[8] } : null } -function dr(value: string, owner: string, file: string): DecodedRow | null { - if (Buffer.byteLength(value, 'utf8') > MAX_ROW) return null - let parsed: unknown - try { - parsed = JSON.parse(value) - } catch { - return null - } - if (JSON.stringify(parsed) !== value) return null - const row = tu(parsed, 10) - if (!row || !vt(row[0], 64) - || !si(row[1]) || row[1] >= KINDS.length - || !si(row[2]) || !si(row[3]) || !si(row[4]) - || !Array.isArray(row[6]) - || row[6].length > INDEX_BODY_FACT_CONTROL_LIMIT) return null - const kind = KINDS[row[1]]! - const proof = re(row[5], file) - const confidence = ev(LEVELS, row[7]) - const source = ev(SOURCES, row[8]) - const order = [row[2], row[1], row[3], row[4]] as number[] - if (!proof || !confidence || !source - || row[0] !== indexBodyFactId( - owner, kind, order, proof.excerpt_sha256, row.slice(1), - )) { - return null - } - return { - id: row[0], kind, order, evidence: proof, control: row[6], confidence, source, - payload: row[9], - } +function dr(a: string, o: string, f: string): DecodedRow | null { + if (bl(a) > MB) return null + let p: unknown + try { p = JSON.parse(a) } catch { return null } + if (js(p) !== a) return null + const r = tu(p, 10) + if (!r || !vt(r[0], 64) + || !si(r[1]) || r[1] >= KS.length + || !si(r[2]) || !si(r[3]) || !si(r[4]) + || !aa(r[6]) + || r[6].length > INDEX_BODY_FACT_CONTROL_LIMIT) return null + const k = KS[r[1]]!, e = re(r[5], f), c = ev(LS, r[7]), s = ev(SS, r[8]) + const q = [r[2], r[1], r[3], r[4]] as number[] + if (!e || !c || !s || r[0] !== indexBodyFactId( + o, k, q, e.excerpt_sha256, r.slice(1))) return null + return { id: r[0], kind: k, order: q, evidence: e, control: r[6], + confidence: c, source: s, payload: r[9] } } export function decodeIndexBodyFactTable( value: unknown, owner: string, file: string, ): readonly IndexBodyFact[] | null { - const table = tu(value, 2) + const t = tu(value, 2) if (!vt(owner, 1_024) || !vt(file, 128) - || !table || table[0] !== 1 || !Array.isArray(table[1]) - || table[1].length === 0 || table[1].length > MAX_ROWS) return null - const decoded: DecodedRow[] = [] - let bytes = 0 - for (const value of table[1]) { - if (typeof value !== 'string') return null - bytes += Buffer.byteLength(value, 'utf8') - if (bytes > MAX_TABLE) return null - const row = dr(value, owner, file) - if (!row) return null - decoded.push(row) + || !t || t[0] !== 1 || !aa(t[1]) + || t[1].length === 0 || t[1].length > MR) return null + const d: DecodedRow[] = [] + let b = 0 + for (const x of t[1]) { + if (typeof x !== 'string') return null + b += bl(x) + if (b > MT) return null + const r = dr(x, owner, file) + if (!r) return null + d.push(r) } - const ids = decoded.map((row) => row.id) - if (new Set(ids).size !== ids.length - || decoded.some((row, index) => index > 0 - && oc(decoded[index - 1]!.order, row.order) >= 0)) { + const i = d.map((r) => r.id) + if (new Set(i).size !== i.length + || d.some((r, n) => n > 0 && oc(d[n - 1]!.order, r.order) >= 0)) { return null } - const idAt = (value: unknown): string | null => - si(value) && value < ids.length ? ids[value]! : null - const control = (value: unknown): IndexControlFrame | null => { - if (!Array.isArray(value) || !si(value[0])) return null - const controller_fact_id = idAt(value[1]) - if (value[0] === 0) { - return value.length === 3 && controller_fact_id - && vt(value[2], 96) - && (['then', 'else', 'truthy', 'falsy', 'nullish', 'default'].includes(value[2]) - || (value[2].startsWith('case:') && value[2].length > 5)) - ? { kind: 'branch', controller_fact_id, arm: value[2] as IndexBranchArm } + const ia = (v: unknown): string | null => + si(v) && v < i.length ? i[v]! : null + const cf = (v: unknown): IndexControlFrame | null => { + if (!aa(v) || !si(v[0])) return null + const id = ia(v[1]) + if (v[0] === 0) { + return v.length === 3 && id + && vt(v[2], 96) + && (['then', 'else', 'truthy', 'falsy', 'nullish', 'default'].includes(v[2]) + || (v[2].startsWith('case:') && v[2].length > 5)) + ? { kind: 'branch', controller_fact_id: id, arm: v[2] as IndexBranchArm } : null } - if (value[0] === 1) return value.length === 2 && controller_fact_id - ? { kind: 'loop', controller_fact_id } : null - if (value[0] === 2) return value.length === 3 && controller_fact_id - && (value[2] === 'each' || si(value[2])) - ? { kind: 'parallel', controller_fact_id, lane: value[2] } : null - const arm = ev(['try', 'catch', 'finally'] as const, value[1]) - return value[0] === 3 && value.length === 2 && arm - ? { kind: 'exception', arm } : null + if (v[0] === 1) return v.length === 2 && id + ? { kind: 'loop', controller_fact_id: id } : null + if (v[0] === 2) return v.length === 3 && id + && (v[2] === 'each' || si(v[2])) + ? { kind: 'parallel', controller_fact_id: id, lane: v[2] } : null + const a = ev(['try', 'catch', 'finally'] as const, v[1]) + return v[0] === 3 && v.length === 2 && a + ? { kind: 'exception', arm: a } : null } - const facts: IndexBodyFact[] = [] - for (const row of decoded) { - const frames = row.control.map(control) - if (!frames.every((frame): frame is IndexControlFrame => frame !== null)) return null - const base = { - id: row.id, owner_symbol_id: owner, order: row.order, - evidence: row.evidence, control: frames, - confidence: row.confidence, source: row.source, + const f: IndexBodyFact[] = [] + for (const r of d) { + const c = r.control.map(cf) + if (!c.every((x): x is IndexControlFrame => x !== null)) return null + const z = { + id: r.id, owner_symbol_id: owner, order: r.order, + evidence: r.evidence, control: c, + confidence: r.confidence, source: r.source, } - const wire = Array.isArray(row.payload) ? row.payload : null - let fact: IndexBodyFact | null = null - if (row.kind === 'call' && wire?.length === 4) { - const scheduling = ev(TIMING, wire[3]) - const args = Array.isArray(wire[2]) - ? wire[2].map((entry) => rv(entry)) + const w = aa(r.payload) ? r.payload : null + let x: IndexBodyFact | null = null + if (r.kind === 'call' && w?.length === 4) { + const s = ev(TM, w[3]) + const a = aa(w[2]) + ? w[2].map((e) => rv(e)) : [] - if (vt(wire[0]) && scheduling - && (wire[1] === null || vt(wire[1], 1_024)) - && Array.isArray(wire[2]) - && args.every((entry): entry is IndexValue => entry !== null)) { - fact = { - ...base, kind: 'call', callee: wire[0], - ...(typeof wire[1] === 'string' ? { target_symbol_id: wire[1] } : {}), - arguments: args, scheduling, + if (vt(w[0]) && s + && (w[1] === null || vt(w[1], 1_024)) + && aa(w[2]) + && a.every((e): e is IndexValue => e !== null)) { + x = { + ...z, kind: 'call', callee: w[0], + ...(typeof w[1] === 'string' ? { target_symbol_id: w[1] } : {}), + arguments: a, scheduling: s, } } - } else if (row.kind === 'literal' && wire?.length === 2) { - const decoded = rv(wire[0]) - const role = ev(ROLES, wire[1]) - if (decoded && role) fact = { ...base, kind: 'literal', value: decoded, role } - } else if (row.kind === 'condition' && wire?.length === 2) { - const condition_kind = ev(CONDITIONS, wire[0]) - const test = wire[1] === null ? undefined : rv(wire[1]) - if (condition_kind && (wire[1] === null || test)) { - fact = { ...base, kind: 'condition', condition_kind, ...(test ? { test } : {}) } + } else if (r.kind === 'literal' && w?.length === 2) { + const v = rv(w[0]), o = ev(RS, w[1]) + if (v && o) x = { ...z, kind: 'literal', value: v, role: o } + } else if (r.kind === 'condition' && w?.length === 2) { + const k = ev(CS, w[0]), t = w[1] === null ? undefined : rv(w[1]) + if (k && (w[1] === null || t)) { + x = { ...z, kind: 'condition', condition_kind: k, ...(t ? { test: t } : {}) } } - } else if (row.kind === 'loop' && wire?.length === 2) { - const loop_kind = ev(LOOPS, wire[0]) - const test = wire[1] === null ? undefined : rv(wire[1]) - if (loop_kind && (wire[1] === null || test)) { - fact = { ...base, kind: 'loop', loop_kind, ...(test ? { test } : {}) } + } else if (r.kind === 'loop' && w?.length === 2) { + const k = ev(LP, w[0]), t = w[1] === null ? undefined : rv(w[1]) + if (k && (w[1] === null || t)) { + x = { ...z, kind: 'loop', loop_kind: k, ...(t ? { test: t } : {}) } } - } else if (row.kind === 'parallel' && wire?.length === 4) { - const combinator = ev(PROMISES, wire[0]) - const input = wire[1] === null ? undefined : rv(wire[1]) - const members = Array.isArray(wire[2]) - ? wire[2].map(idAt) + } else if (r.kind === 'parallel' && w?.length === 4) { + const q = ev(PM, w[0]), n = w[1] === null ? undefined : rv(w[1]) + const m = aa(w[2]) + ? w[2].map(ia) : [] - if (combinator && (wire[1] === null || input) - && Array.isArray(wire[2]) - && members.every((id): id is string => id !== null) - && new Set(members).size === members.length - && si(wire[3])) { - fact = { - ...base, kind: 'parallel', combinator, - completion: COMPLETION[PROMISES.indexOf(combinator)]!, - lane_count: wire[3], - ...(input ? { input } : {}), - member_fact_ids: members, + if (q && (w[1] === null || n) + && aa(w[2]) + && m.every((id): id is string => id !== null) + && new Set(m).size === m.length + && si(w[3])) { + x = { + ...z, kind: 'parallel', combinator: q, + completion: CP[PM.indexOf(q)]!, + lane_count: w[3], + ...(n ? { input: n } : {}), + member_fact_ids: m, } } - } else if ((row.kind === 'return' || row.kind === 'throw') - && wire?.length === 1) { - const decoded = wire[0] === null ? undefined : rv(wire[0]) - if (wire[0] === null || decoded) { - fact = { ...base, kind: row.kind, ...(decoded ? { value: decoded } : {}) } + } else if ((r.kind === 'return' || r.kind === 'throw') + && w?.length === 1) { + const v = w[0] === null ? undefined : rv(w[0]) + if (w[0] === null || v) { + x = { ...z, kind: r.kind, ...(v ? { value: v } : {}) } } - } else if (row.kind === 'mutation' && wire?.length === 3) { - const operation = ev(MUTATIONS, wire[0]) - const decoded = wire[2] === null ? undefined : rv(wire[2]) - if (operation && vt(wire[1]) - && (wire[2] === null || decoded)) { - fact = { - ...base, kind: 'mutation', operation, target: wire[1], - ...(decoded ? { value: decoded } : {}), + } else if (r.kind === 'mutation' && w?.length === 3) { + const o = ev(MU, w[0]), v = w[2] === null ? undefined : rv(w[2]) + if (o && vt(w[1]) && (w[2] === null || v)) { + x = { + ...z, kind: 'mutation', operation: o, target: w[1], + ...(v ? { value: v } : {}), } } - } else if (row.kind === 'persistence' && wire?.length === 4) { - const operation = ev(STORAGE, wire[0]) - const call_fact_id = idAt(wire[1]) - const resource = wire[2] === null ? undefined : rv(wire[2]) - if (operation && call_fact_id && (wire[2] === null || resource) - && vt(wire[3])) { - fact = { - ...base, kind: 'persistence', operation, call_fact_id, - ...(resource ? { resource } : {}), - receiver_type: wire[3], + } else if (r.kind === 'persistence' && w?.length === 4) { + const o = ev(ST, w[0]), id = ia(w[1]) + const v = w[2] === null ? undefined : rv(w[2]) + if (o && id && (w[2] === null || v) && vt(w[3])) { + x = { + ...z, kind: 'persistence', operation: o, call_fact_id: id, + ...(v ? { resource: v } : {}), + receiver_type: w[3], } } } - if (!fact) return null - facts.push(fact) + if (!x) return null + f.push(x) } - return facts + return f } export type IndexFrameworkRole = | 'nest_module' diff --git a/src/domain/query/index-status.ts b/src/domain/query/index-status.ts index 3eac4239..01fd83f7 100644 --- a/src/domain/query/index-status.ts +++ b/src/domain/query/index-status.ts @@ -3,7 +3,7 @@ import { type GraphAttributes, type GraphEdge, } from '../graph/directed-multigraph.js' -import { compareCodeUnits } from '../graph/canonical-json.js' +import { compareCodeUnits as cc } from '../graph/canonical-json.js' import { CANONICAL_INDEX_FORMAT_VERSION, readBuildState, @@ -46,577 +46,470 @@ const SHA256 = /^[a-f0-9]{64}$/ const MAX_TEXT = 512 const KINDS = new Set(['queue', 'job', 'event']) const TRANSPORTS = new Set([ - 'bull', - 'bullmq', - 'node-event-emitter', - 'nestjs-event-emitter', -]) -const RELATIONS = new Set([ - 'publishes_to', - 'routes_through', - 'consumed_by', -]) + 'bull', 'bullmq', 'node-event-emitter', 'nestjs-event-emitter']) +const RELATIONS = new Set(['publishes_to', 'routes_through', 'consumed_by']) const EDGE_SOURCES = new Set([ - 'typescript-semantic', - 'typescript-syntactic', - 'framework-decorator', - 'wrapper-summary', -]) + 'typescript-semantic', 'typescript-syntactic', 'framework-decorator', 'wrapper-summary']) +const CH = 'channel_kind', PH = 'parent_channel_id', + CO = 'condition_kind', SR = 'statement_range', EH = 'excerpt_sha256', + CF = 'controller_fact_id', OW = 'owner_symbol_id', NK = 'node_kind', + EV = 'evidence', TR = 'transport', SF = 'source_file', + MF = 'member_fact_ids', LC = 'lane_count', EO = 'execution_owner_id', + DR = 'definition_range', PL = 'parallel', CD = 'condition', + CR = 'corrupt', CA = 'call' +const oh = Object.hasOwn, of = Object.freeze class IntegrityError extends Error {} - -function fail(subject: string): never { - throw new IntegrityError(`canonical ${subject}`) -} - -function nonEmpty(v: unknown): v is string { - return typeof v === 'string' && v.length > 0 && !v.includes('\0') -} - -function bounded(v: unknown, maxBytes: number): v is string { - return nonEmpty(v) && Buffer.byteLength(v, 'utf8') <= maxBytes +function fl(s: string): never { throw new IntegrityError(`canonical ${s}`) } +const ne = (v: unknown): v is string => typeof v === 'string' && v.length > 0 && !v.includes('\0') +const bd = (v: unknown, m: number): v is string => ne(v) && Buffer.byteLength(v, 'utf8') <= m +const si = (v: unknown, m = 0): v is number => typeof v === 'number' && Number.isSafeInteger(v) && !Object.is(v, -0) && v >= m +const ex = (v: unknown, k: readonly string[]): Record | null => isRecord(v) && Object.keys(v).length === k.length && k.every((x) => oh(v, x)) ? v : null +const pc = (a: IndexRange['start'], b: IndexRange['start']): number => a.line - b.line || a.column - b.column + +function ro(v: unknown): IndexRange | null { + const r = ex(v, ['start', 'end']), s = ex(r?.start, ['line', 'column']), + e = ex(r?.end, ['line', 'column']) + if (!r || !s || !e || !si(s.line, 1) || !si(s.column, 1) + || !si(e.line, 1) || !si(e.column, 1)) return null + const p = { start: { line: s.line, column: s.column }, + end: { line: e.line, column: e.column } } + return pc(p.start, p.end) <= 0 ? p : null } -function safeInt(v: unknown, minimum = 0): v is number { - return typeof v === 'number' - && Number.isSafeInteger(v) - && !Object.is(v, -0) - && v >= minimum +const ct = (a: IndexRange, b: IndexRange): boolean => pc(a.start, b.start) <= 0 && pc(b.end, a.end) <= 0 +const ss = (a: IndexRange, b: IndexRange): boolean => pc(a.start, b.start) === 0 && pc(a.end, b.end) === 0 + +function va(c: Extract, a: string): boolean { + if (c[CO] === 'if') return ['then', 'else'].includes(a) + if (c[CO] === 'switch') return a === 'default' + || (a.startsWith('case:') && a.length > 5) + if (c[CO] === 'logical_and') return a === 'truthy' + if (c[CO] === 'logical_or') return a === 'falsy' + if (c[CO] === 'nullish') return a === 'nullish' + return (c[CO] === 'ternary' ? ['truthy', 'falsy'] : ['then', 'else']).includes(a) } -function exact(v: unknown, keys: readonly string[]): Record | null { - return isRecord(v) - && Object.keys(v).length === keys.length - && keys.every((key) => Object.hasOwn(v, key)) - ? v : null +function ep(a: GraphAttributes, f: ReadonlyMap, + n: ReadonlyMap, t: IndexChannelNode | undefined, + o: ReadonlyMap): boolean { + const s = a[SF], i = a[EO], w = typeof i === 'string' ? n.get(i) : undefined + const p = ro(w?.[DR]), r = ex(a[EV], + ['source', 'range', 'statement_range', 'excerpt_sha256']) + const g = ro(r?.range), m = ro(r?.[SR]), d = a.dispatch_payload_argument + const q = !oh(a, 'dispatch_payload_argument') + || a.relation === 'publishes_to' && t?.[CH] !== 'event' + && si(d) && (o.get(String(i)) ?? []).filter((x) => + x.kind === CA && d < x.arguments.length + && g !== null && ss(x[EV].range, g) + && m !== null && ss(x[EV][SR], m) + && x[EV][EH] === r?.[EH]).length === 1 + return typeof s === 'string' && f.has(s) && typeof i === 'string' + && w?.[SF] === s && w?.[NK] !== 'file' && w?.[NK] !== 'channel' + && p !== null && r !== null && EDGE_SOURCES.has(String(r.source)) + && g !== null && m !== null && ct(p, m) && ct(m, g) + && typeof r[EH] === 'string' && SHA256.test(r[EH]) && q } -function posCmp(a: IndexRange['start'], b: IndexRange['start']): number { - return a.line - b.line || a.column - b.column -} - -function rangeOf(v: unknown): IndexRange | null { - const range = exact(v, ['start', 'end']) - const start = exact(range?.start, ['line', 'column']) - const end = exact(range?.end, ['line', 'column']) - if (!range || !start || !end - || !safeInt(start.line, 1) || !safeInt(start.column, 1) - || !safeInt(end.line, 1) || !safeInt(end.column, 1)) return null - const parsed = { - start: { line: start.line, column: start.column }, - end: { line: end.line, column: end.column }, - } - return posCmp(parsed.start, parsed.end) <= 0 ? parsed : null -} +const vh = (v: IndexValue, t: (candidate: IndexValue) => boolean): boolean => t(v) || v.kind === 'array' && v.elements.some((e) => vh(e, t)) || v.kind === 'object' && v.entries.some((e) => vh(e.value, t)) || v.kind === 'template' && v.parts.some((e) => vh(e, t)) -function contains(outer: IndexRange, inner: IndexRange): boolean { - return posCmp(outer.start, inner.start) <= 0 - && posCmp(inner.end, outer.end) <= 0 -} - -function sameSpan(a: IndexRange, b: IndexRange): boolean { - return posCmp(a.start, b.start) === 0 - && posCmp(a.end, b.end) === 0 -} - -function validArm( - ctl: Extract, - arm: string, +function fh( + f: IndexBodyFact, + t: (candidate: IndexValue) => boolean, ): boolean { - if (ctl.condition_kind === 'if') return ['then', 'else'].includes(arm) - if (ctl.condition_kind === 'switch') { - return arm === 'default' || (arm.startsWith('case:') && arm.length > 5) - } - if (ctl.condition_kind === 'logical_and') return arm === 'truthy' - if (ctl.condition_kind === 'logical_or') return arm === 'falsy' - if (ctl.condition_kind === 'nullish') return arm === 'nullish' - return ctl.condition_kind === 'ternary' - ? ['truthy', 'falsy'].includes(arm) - : ['then', 'else'].includes(arm) -} - -function edgeProof(a: GraphAttributes, files: ReadonlyMap, nodes: ReadonlyMap): boolean { - const source = a.source_file; - const ownerId = a.execution_owner_id; - const owner = typeof ownerId === 'string' ? nodes.get(ownerId) : undefined; - const span = rangeOf(owner?.definition_range); - const record = exact(a.evidence, ['source', 'range', 'statement_range', 'excerpt_sha256']); - const range = rangeOf(record?.range); - const statement = rangeOf(record?.statement_range); - return typeof source === 'string' - && files.has(source) - && typeof ownerId === 'string' - && owner?.source_file === source - && owner?.node_kind !== 'file' - && owner?.node_kind !== 'channel' - && span !== null - && record !== null - && EDGE_SOURCES.has(String(record.source)) - && range !== null - && statement !== null - && contains(span, statement) - && contains(statement, range) - && typeof record.excerpt_sha256 === 'string' - && SHA256.test(record.excerpt_sha256); -} - -function valueHas(v: IndexValue, test: (candidate: IndexValue) => boolean): boolean { - return test(v) - || v.kind === 'array' && v.elements.some((entry) => valueHas(entry, test)) - || v.kind === 'object' && v.entries.some((entry) => valueHas(entry.value, test)) - || v.kind === 'template' && v.parts.some((entry) => valueHas(entry, test)) -} - -function factHas( - fact: IndexBodyFact, - test: (candidate: IndexValue) => boolean, -): boolean { - let xs: readonly IndexValue[] - switch (fact.kind) { - case 'call': - xs = fact.arguments; break - case 'literal': - xs = [fact.value]; break - case 'condition': - case 'loop': - xs = fact.test ? [fact.test] : []; break - case 'parallel': - xs = fact.input ? [fact.input] : []; break + let x: readonly IndexValue[] + switch (f.kind) { + case CA: x = f.arguments; break + case 'literal': x = [f.value]; break + case CD: + case 'loop': x = f.test ? [f.test] : []; break + case PL: x = f.input ? [f.input] : []; break case 'return': case 'throw': - case 'mutation': - xs = fact.value ? [fact.value] : []; break - case 'persistence': - xs = fact.resource ? [fact.resource] : [] + case 'mutation': x = f.value ? [f.value] : []; break + case 'persistence': x = f.resource ? [f.resource] : [] } - return xs.some((value) => valueHas(value, test)) + return x.some((v) => vh(v, t)) } -function readChannel(id: string, a: GraphAttributes): IndexChannelNode | null { - if (!nonEmpty(id) - || !KINDS.has(a.channel_kind as IndexChannelKind) - || !TRANSPORTS.has(a.transport as IndexChannelTransport) - || !bounded(a.key, MAX_TEXT) - || (Object.hasOwn(a, 'parent_channel_id') - && !nonEmpty(a.parent_channel_id)) - || (Object.hasOwn(a, 'scope') - && !bounded(a.scope, 512))) - return null; - const channel: IndexChannelNode = { - id, - node_kind: 'channel', - channel_kind: a.channel_kind as IndexChannelKind, - transport: a.transport as IndexChannelTransport, - key: a.key, - ...(typeof a.parent_channel_id === 'string' - ? { parent_channel_id: a.parent_channel_id } - : {}), - ...(typeof a.scope === 'string' - ? { scope: a.scope } - : {}), - }; - return id === indexChannelId(channel) ? channel : null; -} - -function orderCmp(a: readonly number[], b: readonly number[]): number { - for (let index = 0; index < Math.min(a.length, b.length); index += 1) { - const difference = a[index]! - b[index]! - if (difference !== 0) return difference +function rc(i: string, a: GraphAttributes): IndexChannelNode | null { + if (!ne(i) || !KINDS.has(a[CH] as IndexChannelKind) + || !TRANSPORTS.has(a[TR] as IndexChannelTransport) || !bd(a.key, MAX_TEXT) + || oh(a, 'parent_channel_id') && !ne(a[PH]) + || oh(a, 'scope') && !bd(a.scope, 512)) return null + const c: IndexChannelNode = { + id: i, node_kind: 'channel', channel_kind: a[CH] as IndexChannelKind, + transport: a[TR] as IndexChannelTransport, key: a.key, + ...(typeof a[PH] === 'string' ? { parent_channel_id: a[PH] } : {}), + ...(typeof a.scope === 'string' ? { scope: a.scope } : {}), } - return a.length - b.length + return i === indexChannelId(c) ? c : null } -function freeze(v: T): T { - if (v !== null && typeof v === 'object' && !Object.isFrozen(v)) { - for (const entry of Object.values(v)) freeze(entry) - Object.freeze(v) +function oc(a: readonly number[], b: readonly number[]): number { for (let i = 0; i < Math.min(a.length, b.length); i += 1) { const d = a[i]! - b[i]!; if (d !== 0) return d } return a.length - b.length } +function fr(v: T): T { if (v !== null && typeof v === 'object' && !Object.isFrozen(v)) { for (const e of Object.values(v)) fr(e); of(v) } return v } + +function sm(e: Iterable): ReadonlyMap { + const x = new Map(e); let v: ReadonlyMap + v = { + get size() { return x.size }, get(k: K) { return x.get(k) }, + has(k: K) { return x.has(k) }, entries() { return x.entries() }, + keys() { return x.keys() }, values() { return x.values() }, + forEach(c, t) { x.forEach((a, b) => c.call(t, a, b, v)) }, + [Symbol.iterator]() { return x[Symbol.iterator]() }, } - return v + return of(v) } -function sealMap(entries: Iterable): ReadonlyMap { - const xs = new Map(entries); - let view: ReadonlyMap; - view = { - get size() { return xs.size; }, - get(key: K) { return xs.get(key); }, - has(key: K) { return xs.has(key); }, - entries() { return xs.entries(); }, - keys() { return xs.keys(); }, - values() { return xs.values(); }, - forEach(callback: (value: V, key: K, map: ReadonlyMap) => void, thisArg?: unknown) { - xs.forEach((value, key) => callback.call(thisArg, value, key, view)); - }, - [Symbol.iterator]() { return xs[Symbol.iterator](); }, - }; - return Object.freeze(view); -} - -function sortEntries( - xs: ReadonlyMap, -): Array { - return [...xs.entries()] - .sort(([left], [right]) => compareCodeUnits(left, right)) -} +const se = (x: ReadonlyMap): Array => [...x.entries()].sort(([a], [b]) => cc(a, b)) type ExecutionIndexes = Pick; -function buildMaps(view: KnowledgeGraph, files: ReadonlyMap): ExecutionIndexes { - const nodes = view.nodeEntries(); - const byId = new Map(nodes); - const symbols = new Set(); - const facts = new Map(); - const owned = new Map(); - const chs = new Map(); - const byKey = new Map(); - const orderKeys = new Map>(); - for (const [id, a] of nodes) { - if (a.node_kind === 'channel') { - if (Object.hasOwn(a, 'body_facts')) { - fail('channel body facts'); +function bm(v: KnowledgeGraph, l: ReadonlyMap): ExecutionIndexes { + const n = v.nodeEntries(); + const b = new Map(n); + const s = new Set(); + const f = new Map(); + const o = new Map(); + const c = new Map(); + const k = new Map(); + const q = new Map>(); + for (const [i, a] of n) { + if (a[NK] === 'channel') { + if (oh(a, 'body_facts')) { + fl('channel body facts'); } - const ch = readChannel(id, a); - if (!ch) - fail('channel node'); - chs.set(id, ch); + const h = rc(i, a); + if (!h) + fl('channel node'); + c.set(i, h); continue; } - if (a.node_kind === 'file') { - if (Object.hasOwn(a, 'body_facts') - || Object.hasOwn(a, 'channel_kind') - || Object.hasOwn(a, 'parent_channel_id')) { - fail('file-node execution metadata'); + if (a[NK] === 'file') { + if (oh(a, 'body_facts') + || oh(a, 'channel_kind') + || oh(a, 'parent_channel_id')) { + fl('file-node execution metadata'); } continue; } - if (Object.hasOwn(a, 'channel_kind') - || Object.hasOwn(a, 'parent_channel_id')) { - fail('channel discriminator'); + if (oh(a, 'channel_kind') + || oh(a, 'parent_channel_id')) { + fl('channel discriminator'); } - symbols.add(id); - if (!Object.hasOwn(a, 'body_facts')) + s.add(i); + if (!oh(a, 'body_facts')) continue; - const source = a.source_file; - const fileId = typeof source === 'string' - ? files.get(source) + const u = a[SF]; + const d = typeof u === 'string' + ? l.get(u) : undefined; - const span = rangeOf(a.definition_range); - const ownerFile = fileId ? byId.get(fileId) : undefined; - if (!fileId || !span || !ownerFile - || ownerFile.node_kind !== 'file') { - fail('operation owner'); + const p = ro(a[DR]); + const w = d ? b.get(d) : undefined; + if (!d || !p || !w + || w[NK] !== 'file') { + fl('operation owner'); } - const bodyFacts = decodeIndexBodyFactTable(a.body_facts, id, fileId); - if (!bodyFacts) - fail('symbol body facts'); - const ps = bodyFacts.filter((fact) => fact.kind === 'persistence'); - const po = new Set(ps.map((fact) => fact.order[3])); - if (po.size !== ps.length || ps.some((_, index) => !po.has(index + 1))) - fail('persistence order'); - const orders = orderKeys.get(id) ?? new Set(); - orderKeys.set(id, orders); - for (const fact of bodyFacts) { - if (!contains(span, fact.evidence.statement_range) - || facts.has(fact.id)) { - fail('operation fact'); + const x = decodeIndexBodyFactTable(a.body_facts, i, d); + if (!x) + fl('symbol body facts'); + const y = x.filter((t) => t.kind === 'persistence'); + const z = new Set(y.map((t) => t.order[3])); + if (z.size !== y.length || y.some((_, j) => !z.has(j + 1))) + fl('persistence order'); + const e = q.get(i) ?? new Set(); + q.set(i, e); + for (const t of x) { + if (!ct(p, t[EV][SR]) + || f.has(t.id)) { + fl('operation fact'); } - const orderKey = fact.order.join('.'); - if (orders.has(orderKey)) - fail('operation order'); - orders.add(orderKey); - facts.set(fact.id, fact); - const ownerFacts = owned.get(id) ?? []; - ownerFacts.push(fact); - owned.set(id, ownerFacts); + const g = t.order.join('.'); + if (e.has(g)) + fl('operation order'); + e.add(g); + f.set(t.id, t); + const m = o.get(i) ?? []; + m.push(t); + o.set(i, m); } } - for (const ch of chs.values()) { - if (ch.channel_kind === 'job') { - const parent = ch.parent_channel_id - ? chs.get(ch.parent_channel_id) + for (const h of c.values()) { + if (h[CH] === 'job') { + const p = h[PH] + ? c.get(h[PH]) : undefined; - if (!parent || parent.channel_kind !== 'queue' - || parent.transport !== ch.transport) { - fail('job parent channel'); + if (!p || p[CH] !== 'queue' + || p[TR] !== h[TR]) { + fl('job parent channel'); } } - else if (ch.parent_channel_id !== undefined) { - fail('non-job parent channel'); + else if (h[PH] !== undefined) { + fl('non-job parent channel'); } - if (ch.channel_kind === 'event') { - if (!bounded(ch.scope, 512)) { - fail('event channel scope'); + if (h[CH] === 'event') { + if (!bd(h.scope, 512)) { + fl('event channel scope'); } } - else if (ch.scope !== undefined) { - fail('non-event channel scope'); + else if (h.scope !== undefined) { + fl('non-event channel scope'); } - const keyed = byKey.get(ch.key) ?? []; - keyed.push(ch); - byKey.set(ch.key, keyed); + const y = k.get(h.key) ?? []; + y.push(h); + k.set(h.key, y); } - for (const fact of facts.values()) { - if (factHas(fact, (value) => value.kind === 'symbol' && !symbols.has(value.symbol_id))) { - fail('operation value reference'); + for (const t of f.values()) { + if (fh(t, (v) => v.kind === 'symbol' && !s.has(v.symbol_id))) { + fl('operation value reference'); } - if (fact.kind === 'call' && fact.target_symbol_id - && !symbols.has(fact.target_symbol_id)) { - fail('call target'); + if (t.kind === CA && t.target_symbol_id + && !s.has(t.target_symbol_id)) { + fl('call target'); } - const controlIds = new Set(); - for (const f of fact.control) { - if (f.kind === 'exception') + const i = new Set(); + for (const d of t.control) { + if (d.kind === 'exception') continue; - if (controlIds.has(f.controller_fact_id)) { - fail('duplicate control reference'); + if (i.has(d[CF])) { + fl('duplicate control reference'); } - controlIds.add(f.controller_fact_id); - const ctl = facts.get(f.controller_fact_id); - const expectedKind = f.kind === 'branch' - ? 'condition' - : f.kind; - const guardFallthrough = f.kind === 'branch' - && ctl?.kind === 'condition' - && ctl.condition_kind === 'guard'; - if (!ctl || ctl.owner_symbol_id !== fact.owner_symbol_id - || ctl.kind !== expectedKind - || orderCmp(ctl.order, fact.order) >= 0 - || (!guardFallthrough && !contains(f.kind === 'parallel' - ? ctl.evidence.range - : ctl.evidence.statement_range, fact.evidence.range)) - || (f.kind === 'branch' && ctl.kind === 'condition' - && !validArm(ctl, f.arm)) - || (f.kind === 'parallel' && ctl.kind === 'parallel' - && (f.lane === 'each' - ? ctl.lane_count === 0 - : f.lane >= ctl.lane_count)) - || (fact.kind === 'call' && f.kind === 'parallel' - && ctl.kind === 'parallel' - && !ctl.member_fact_ids.includes(fact.id))) { - fail('operation control reference'); + i.add(d[CF]); + const c = f.get(d[CF]); + const k = d.kind === 'branch' + ? CD + : d.kind; + const g = d.kind === 'branch' + && c?.kind === CD + && c[CO] === 'guard'; + if (!c || c[OW] !== t[OW] + || c.kind !== k + || oc(c.order, t.order) >= 0 + || (!g && !ct(d.kind === PL + ? c[EV].range + : c[EV][SR], t[EV].range)) + || (d.kind === 'branch' && c.kind === CD + && !va(c, d.arm)) + || (d.kind === PL && c.kind === PL + && (d.lane === 'each' + ? c[LC] === 0 + : d.lane >= c[LC])) + || (t.kind === CA && d.kind === PL + && c.kind === PL + && !c[MF].includes(t.id))) { + fl('operation control reference'); } } - if (factHas(fact, (value) => value.kind === 'parameter' && value.scope === 'iteration') - && !fact.control.some((f) => { - const ctl = f.kind === 'loop' - ? facts.get(f.controller_fact_id) + if (fh(t, (v) => v.kind === 'parameter' && v.scope === 'iteration') + && !t.control.some((d) => { + const c = d.kind === 'loop' + ? f.get(d[CF]) : undefined; - return ctl?.kind === 'loop' - && ctl.loop_kind === 'array_iteration'; + return c?.kind === 'loop' + && c.loop_kind === 'array_iteration'; })) { - fail('iteration parameter'); + fl('iteration parameter'); } - if (fact.kind === 'parallel') { - const laneCount = fact.input?.kind === 'array' - ? fact.input.elements.length + if (t.kind === PL) { + const l = t.input?.kind === 'array' + ? t.input.elements.length : 0; - if (fact.member_fact_ids.some((id) => { - const member = facts.get(id); - const frame = member?.control.find((f): f is Extract { + const m = f.get(i); + const r = m?.control.find((d): d is Extract => f.kind === 'parallel' - && f.controller_fact_id === fact.id); - const loop = frame?.lane === 'each' - ? member?.control.some((f) => { - const ctl = f.kind === 'loop' - ? facts.get(f.controller_fact_id) + }> => d.kind === PL + && d[CF] === t.id); + const p = r?.lane === 'each' + ? m?.control.some((d) => { + const c = d.kind === 'loop' + ? f.get(d[CF]) : undefined; - return ctl?.kind === 'loop' - && ctl.loop_kind === 'array_iteration'; + return c?.kind === 'loop' + && c.loop_kind === 'array_iteration'; }) : true; - return !member || member.kind !== 'call' || !frame || !loop - || member.owner_symbol_id !== fact.owner_symbol_id; - }) || fact.lane_count !== laneCount) { - fail('parallel member reference'); + return !m || m.kind !== CA || !r || !p + || m[OW] !== t[OW]; + }) || t[LC] !== l) { + fl('parallel member reference'); } } - if (fact.kind === 'persistence') { - const call = facts.get(fact.call_fact_id); - if (!call || call.kind !== 'call' - || call.owner_symbol_id !== fact.owner_symbol_id - || !sameSpan(call.evidence.range, fact.evidence.range) - || !sameSpan(call.evidence.statement_range, fact.evidence.statement_range) - || call.evidence.excerpt_sha256 !== fact.evidence.excerpt_sha256 - || call.order[0] !== fact.order[0] - || call.order[2] !== fact.order[2] - || JSON.stringify(call.control) !== JSON.stringify(fact.control) - || !bounded(fact.receiver_type, MAX_TEXT)) { - fail('persistence call reference'); + if (t.kind === 'persistence') { + const a = f.get(t.call_fact_id); + if (!a || a.kind !== CA + || a[OW] !== t[OW] + || !ss(a[EV].range, t[EV].range) + || !ss(a[EV][SR], t[EV][SR]) + || a[EV][EH] !== t[EV][EH] + || a.order[0] !== t.order[0] + || a.order[2] !== t.order[2] + || JSON.stringify(a.control) !== JSON.stringify(t.control) + || !bd(t.receiver_type, MAX_TEXT)) { + fl('persistence call reference'); } } } - const routes = new Map(); - for (const [source, target, a] of view.edgeEntries()) { - const relation = a.relation; - const srcCh = chs.get(source); - const dstCh = chs.get(target); - const usesChannel = srcCh !== undefined || dstCh !== undefined; - if (!usesChannel && !RELATIONS.has(String(relation))) + const r = new Map(); + for (const [u, t, a] of v.edgeEntries()) { + const e = a.relation; + const x = c.get(u); + const y = c.get(t); + const g = x !== undefined || y !== undefined; + if (oh(a, 'dispatch_payload_argument') + && (!g || e !== 'publishes_to')) + fl('dispatch payload relation'); + if (!g && !RELATIONS.has(String(e))) continue; - if (!RELATIONS.has(String(relation))) { - fail('channel relation'); + if (!RELATIONS.has(String(e))) { + fl('channel relation'); } - if (!edgeProof(a, files, byId)) { - fail('channel evidence'); + if (!ep(a, l, b, y, o)) { + fl('channel evidence'); } - const edgeOwner = a.execution_owner_id; - if (relation === 'publishes_to') { - if (!symbols.has(source) || !dstCh - || source !== edgeOwner - || !['queue', 'job', 'event'].includes(dstCh.channel_kind)) { - fail('publishes_to endpoints'); + const w = a[EO]; + if (e === 'publishes_to') { + if (!s.has(u) || !y + || u !== w + || !['queue', 'job', 'event'].includes(y[CH])) { + fl('publishes_to endpoints'); } } - else if (relation === 'routes_through') { - if (!srcCh || srcCh.channel_kind !== 'job' - || !dstCh || dstCh.channel_kind !== 'queue' - || srcCh.parent_channel_id !== target - || srcCh.transport !== dstCh.transport) { - fail('routes_through endpoints'); + else if (e === 'routes_through') { + if (!x || x[CH] !== 'job' + || !y || y[CH] !== 'queue' + || x[PH] !== t + || x[TR] !== y[TR]) { + fl('routes_through endpoints'); } - routes.set(source, (routes.get(source) ?? 0) + 1); + r.set(u, (r.get(u) ?? 0) + 1); } - else if (relation === 'consumed_by') { - if (!srcCh || !symbols.has(target) || dstCh) { - fail('consumed_by endpoints'); + else if (e === 'consumed_by') { + if (!x || !s.has(t) || y) { + fl('consumed_by endpoints'); } } } - for (const ch of chs.values()) { - if (ch.channel_kind === 'job' - && routes.get(ch.id) !== 1) { - fail('job routing'); + for (const h of c.values()) { + if (h[CH] === 'job' + && r.get(h.id) !== 1) { + fl('job routing'); } } - for (const xs of owned.values()) { - xs.sort((a, b) => orderCmp(a.order, b.order) || compareCodeUnits(a.id, b.id)); - xs.forEach(freeze); - Object.freeze(xs); + for (const x of o.values()) { + x.sort((a, b) => oc(a.order, b.order) || cc(a.id, b.id)); + x.forEach(fr); + of(x); } - for (const xs of byKey.values()) { - xs.sort((a, b) => compareCodeUnits(a.id, b.id)); - xs.forEach(freeze); - Object.freeze(xs); + for (const x of k.values()) { + x.sort((a, b) => cc(a.id, b.id)); + x.forEach(fr); + of(x); } - facts.forEach(freeze); - chs.forEach(freeze); + f.forEach(fr); + c.forEach(fr); return { - operation_by_id: sealMap(sortEntries(facts)), - operations_by_owner: sealMap(sortEntries(owned)), - channels_by_id: sealMap(sortEntries(chs)), - channels_by_key: sealMap(sortEntries(byKey)), + operation_by_id: sm(se(f)), + operations_by_owner: sm(se(o)), + channels_by_id: sm(se(c)), + channels_by_key: sm(se(k)), }; } -function copyGraph(source: KnowledgeGraph): KnowledgeGraph { - const view = new KnowledgeGraph(source.graph) - for (const [id, a] of source.nodeEntries()) { - view.addNode(id, a) +function cg(s: KnowledgeGraph): KnowledgeGraph { + const v = new KnowledgeGraph(s.graph) + for (const [i, a] of s.nodeEntries()) { + v.addNode(i, a) } - for (const [from, to, a, expectedId] of source.edgeEntries()) { - const id = view.addEdge(from, to, a) - if (id !== expectedId) { + for (const [f, t, a, e] of s.edgeEntries()) { + const i = v.addEdge(f, t, a) + if (i !== e) { throw new Error('Canonical graph edge identity changed while sealing query index') } } - return view -} - -function sealGraph(view: KnowledgeGraph): QueryGraph { - return Object.freeze({ - hasNode: (id: string) => view.hasNode(id), - hasEdge: (source: string, target: string) => view.hasEdge(source, target), - nodeEntries: () => view.nodeEntries(), - edgeEntries: () => view.edgeEntries(), - predecessors: (id: string) => view.predecessors(id), - successors: (id: string) => view.successors(id), - edgesBetween: (source: string, target: string) => view.edgesBetween(source, target), - nodeAttributes: (id: string) => view.nodeAttributes(id), - }); + return v } -export function failedQueryIndex( - state: FailedQueryIndex['state'], - subject: string, -): FailedQueryIndex { - return { state, subject } -} +const sg = (v: KnowledgeGraph): QueryGraph => of({ + hasNode: (i: string) => v.hasNode(i), hasEdge: (s: string, t: string) => v.hasEdge(s, t), + nodeEntries: () => v.nodeEntries(), edgeEntries: () => v.edgeEntries(), + predecessors: (i: string) => v.predecessors(i), successors: (i: string) => v.successors(i), + edgesBetween: (s: string, t: string) => v.edgesBetween(s, t), + nodeAttributes: (i: string) => v.nodeAttributes(i), +}) +export function failedQueryIndex(state: FailedQueryIndex['state'], subject: string): FailedQueryIndex { return { state, subject } } export function inspectQueryIndex(graph: KnowledgeGraph): QueryIndex { - let view: KnowledgeGraph + let v: KnowledgeGraph try { - view = copyGraph(graph) + v = cg(graph) } catch { - return failedQueryIndex('corrupt', 'canonical graph snapshot') + return failedQueryIndex(CR, 'canonical graph snapshot') } - const build = readBuildState(view) - const root = view.graph.root_path - if (!build || view.graph.canonical_typescript_index !== true - || view.graph.schema_version !== CANONICAL_INDEX_FORMAT_VERSION - || typeof root !== 'string' || root.trim().length === 0 - || build.source_root.root_path !== root) { - return failedQueryIndex('corrupt', 'canonical TypeScript index metadata') + const b = readBuildState(v) + const r = v.graph.root_path + if (!b || v.graph.canonical_typescript_index !== true + || v.graph.schema_version !== CANONICAL_INDEX_FORMAT_VERSION + || typeof r !== 'string' || r.trim().length === 0 + || b.source_root.root_path !== r) { + return failedQueryIndex(CR, 'canonical TypeScript index metadata') } - if (build.completeness.summary.state !== 'complete' - || build.completeness.supported_failures.length > 0) { + if (b.completeness.summary.state !== 'complete' + || b.completeness.supported_failures.length > 0) { return failedQueryIndex( 'unavailable', 'canonical TypeScript index incomplete', ) } - const hashes = new Map() - const fileIds = new Map() - for (const [id, a] of view.nodeEntries()) { - if (a.node_kind !== 'file') continue - const source = a.source_file - const hash = a.content_hash - if (typeof source !== 'string' || typeof hash !== 'string' - || !SHA256.test(hash)) { - return failedQueryIndex('corrupt', 'canonical file-node hash') + const h = new Map() + const f = new Map() + for (const [i, a] of v.nodeEntries()) { + if (a[NK] !== 'file') continue + const s = a[SF] + const x = a.content_hash + if (typeof s !== 'string' || typeof x !== 'string' + || !SHA256.test(x)) { + return failedQueryIndex(CR, 'canonical file-node hash') } - if (hashes.has(source) || fileIds.has(source)) { - return failedQueryIndex('corrupt', source) + if (h.has(s) || f.has(s)) { + return failedQueryIndex(CR, s) } - hashes.set(source, hash) - fileIds.set(source, id) + h.set(s, x) + f.set(s, i) } - if (hashes.size !== build.sources.supported.length - || build.sources.supported.some((source) => - hashes.get(source.path) !== source.hash)) { - return failedQueryIndex('corrupt', 'canonical file-node coverage') + if (h.size !== b.sources.supported.length + || b.sources.supported.some((s) => + h.get(s.path) !== s.hash)) { + return failedQueryIndex(CR, 'canonical file-node coverage') } - let execution: ExecutionIndexes + let e: ExecutionIndexes try { - execution = buildMaps(view, fileIds) - } catch (error) { + e = bm(v, f) + } catch (x) { return failedQueryIndex( - 'corrupt', - error instanceof IntegrityError - ? error.message + CR, + x instanceof IntegrityError + ? x.message : 'canonical execution index', ) } - for (const [id, a] of view.nodeEntries()) { - if (!Object.hasOwn(a, 'body_facts')) continue - const { body_facts: _decoded, ...retained } = a - view.replaceNodeAttributes(id, retained) + for (const [i, a] of v.nodeEntries()) { + if (!oh(a, 'body_facts')) continue + const { body_facts: _, ...t } = a + v.replaceNodeAttributes(i, t) } - return Object.freeze({ + return of({ state: 'ready', - graph: sealGraph(view), - root_path: root, - file_hashes: sealMap(hashes), - unsupported_sources: Object.freeze( - build.sources.unsupported.map((source) => Object.freeze({ ...source })), + graph: sg(v), + root_path: r, + file_hashes: sm(h), + unsupported_sources: of( + b.sources.unsupported.map((s) => of({ ...s })), ), - ...execution, + ...e, }) } diff --git a/tests/unit/canonical-index-execution-hardening.test.ts b/tests/unit/canonical-index-execution-hardening.test.ts index 0f6e806b..3c9b78e0 100644 --- a/tests/unit/canonical-index-execution-hardening.test.ts +++ b/tests/unit/canonical-index-execution-hardening.test.ts @@ -106,11 +106,50 @@ function outgoing( source === from && attributes.relation === relation) } +function decodeTypedCaseArm( + arm: unknown, +): readonly [string, unknown] | null { + if (typeof arm !== 'string') return null + const match = /^case:([A-Za-z0-9_-]+)$/u.exec(arm) + if (!match) return null + try { + const decoded: unknown = JSON.parse( + Buffer.from(match[1]!, 'base64url').toString('utf8'), + ) + return Array.isArray(decoded) + && decoded.length === 2 + && typeof decoded[0] === 'string' + ? [decoded[0], decoded[1]] + : null + } catch { + return null + } +} + +function typedCaseValues( + ownerFacts: readonly BodyFact[], + controllerId: string, +): Array { + return ownerFacts.flatMap((fact) => { + const control = Array.isArray(fact.control) ? fact.control : [] + return control.flatMap((rawFrame) => { + const frame = rawFrame as Record + if (frame.kind !== 'branch' + || frame.controller_fact_id !== controllerId) return [] + const value = decodeTypedCaseArm(frame.arm) + return value ? [value] : [] + }) + }) +} + describe('canonical TypeScript execution hardening', () => { it('resolves GoValidate-style Map-backed queue wrappers and inline worker delegates exactly', () => { const source = `import { Queue, Worker, type Job } from 'bullmq' -type AssemblyJobData = { ideaId: string } +type AssemblyJobData = { + ideaId: string + trigger: 'section_complete' | 'assembly_complete' +} const QUEUE_NAME = 'assembly-queue' const JOB_NAME = 'assemble_report' @@ -146,7 +185,15 @@ class AssemblyWorker { } async process(job: Job): Promise { - void job.data.ideaId + const { trigger } = job.data + switch (trigger) { + case 'section_complete': + void job.data.ideaId + return + case 'assembly_complete': + void job.data.ideaId + return + } } } @@ -154,7 +201,10 @@ export function dispatch( registry: QueueRegistryService, ideaId: string, ) { - return registry.addJob(QUEUE_NAME, JOB_NAME, { ideaId }) + return registry.addJob(QUEUE_NAME, JOB_NAME, { + ideaId, + trigger: 'assembly_complete', + }) } ` const { nodes, edges } = build({ 'src/queue-registry.ts': source }) @@ -176,6 +226,29 @@ export function dispatch( expect(hasEdge(edges, job[0]![0], queue[0]![0], 'routes_through')).toBe(true) expect(hasEdge(edges, queue[0]![0], processId, 'consumed_by')).toBe(true) expect(outgoing(edges, queue[0]![0], 'consumed_by')).toHaveLength(1) + const publishEdges = outgoing(edges, dispatchId, 'publishes_to') + .filter(([, target]) => target === job[0]![0]) + expect(publishEdges).toHaveLength(1) + expect(publishEdges[0]![2]).toMatchObject({ + dispatch_payload_argument: 2, + }) + + const processFacts = facts(nodes, symbol(nodes, 'AssemblyWorker.process')) + const condition = processFacts.find((fact) => + fact.kind === 'condition' && fact.condition_kind === 'switch') + expect(condition?.test).toEqual({ + kind: 'template', + parts: [ + { kind: 'parameter', position: 0 }, + { kind: 'literal', value: 'data' }, + { kind: 'literal', value: 'trigger' }, + ], + }) + expect(new Set(typedCaseValues(processFacts, String(condition?.id)) + .map((value) => JSON.stringify(value)))).toEqual(new Set([ + JSON.stringify(['string', 'section_complete']), + JSON.stringify(['string', 'assembly_complete']), + ])) }) it('expands two wrapper hops but never joins cycles, dynamics, or unmatched channel halves', () => { @@ -267,6 +340,12 @@ export const consumerOnlyWorker = expect(consumerOnly).toHaveLength(1) expect(outgoing(edges, producerOnly[0]![0], 'consumed_by')).toEqual([]) expect(hasEdge(edges, consumerOnly[0]![0], consumerOnlyId, 'consumed_by')).toBe(true) + const publishEdges = outgoing(edges, successId, 'publishes_to') + .filter(([, target]) => target === completeJob[0]![0]) + expect(publishEdges).toHaveLength(1) + expect(publishEdges[0]![2]).toMatchObject({ + dispatch_payload_argument: 2, + }) }) it('requires receiver proof for persistence and recognizes imported filesystem writes', () => { diff --git a/tests/unit/canonical-index-execution-review-regressions.test.ts b/tests/unit/canonical-index-execution-review-regressions.test.ts index 6d607600..36c2d043 100644 --- a/tests/unit/canonical-index-execution-review-regressions.test.ts +++ b/tests/unit/canonical-index-execution-review-regressions.test.ts @@ -387,6 +387,202 @@ export function localEmitters(): void { expect(hasEdge(edges, emitted![0], handlerId, 'consumed_by')).toBe(false) }) + it('accepts only direct inline consumers and preserves zero-argument events', () => { + const source = `import { EventEmitter } from 'node:events' +import { Worker, type Job } from 'bullmq' +type Payload = { ready: boolean } + +export async function handle( + _job: Job, + _side?: unknown, +): Promise {} +export async function first(job: Job): Promise { + return handle(job) +} +export async function second(job: Job): Promise { + return handle(job) +} +export class Handler { + process(job: Job): Promise { return handle(job) } +} +export class AlternateHandler { + process(job: Job): Promise { return second(job) } +} +export function handleEvent(..._args: unknown[]): void {} +declare function mutateJob(job: Job): unknown +declare function explode(): never +declare const flag: boolean + +export const directWorker = new Worker('direct', (job) => handle(job)) +export const returnedWorker = new Worker('returned', (job) => { return handle(job) }) +export const awaitedWorker = new Worker('awaited', async (job) => await handle(job)) +export const voidedWorker = new Worker('voided', (job) => { void handle(job) }) +export const pureLaterWorker = new Worker('pure-later', + (job) => handle(job, 42)) +export const typeofLaterWorker = new Worker('typeof-later', + (job) => handle(job, typeof job)) +export const equalityLaterWorker = new Worker('equality-later', + (job) => handle(job, job === fakeJob)) +export const voidLaterWorker = new Worker('void-later', + (job) => handle(job, void job)) +export const missingVoidWorker = new Worker('missing-void', + (job) => handle(job, void missingName)) +export const sideEffectWorker = new Worker('side-effect', + (job) => handle(job, mutateJob(job))) +declare const maybeHandler: typeof handle | undefined +export const optionalReceiverWorker = new Worker('optional-receiver', + (job) => maybeHandler?.(job)) +declare const maybeOwner: Handler | undefined +declare const deep: { box?: { handler: Handler } } +export const optionalElementWorker = new Worker('optional-element', + (job) => maybeOwner?.['process'](job)) +export const nestedOptionalWorker = new Worker('nested-optional', + (job) => deep.box?.handler.process(job)) +export const conditionalTargetWorker = new Worker('conditional-target', + (job) => (flag ? first : second)(job)) +const chosen = flag ? first : second +export const aliasedConditionalWorker = new Worker('aliased-conditional', + (job) => chosen(job)) +const directAlias = first +export const directAliasWorker = new Worker('direct-alias', + (job) => directAlias(job)) +const stableService = new Handler() +export const stableReceiverWorker = new Worker('stable-receiver', + (job) => stableService.process(job)) +export const directNewReceiverWorker = new Worker('direct-new-receiver', + (job) => new Handler().process(job)) +const proxiedService: Handler = new Proxy(new Handler(), { + get: () => second, +}) +export const proxiedReceiverWorker = new Worker('proxied-receiver', + (job) => proxiedService.process(job)) +const objectPrototypeService = new Handler() +Object.setPrototypeOf(objectPrototypeService, { process: second }) +export const objectPrototypeWorker = new Worker('object-prototype', + (job) => objectPrototypeService.process(job)) +const reflectPrototypeService = new Handler() +Reflect.setPrototypeOf(reflectPrototypeService, { process: second }) +export const reflectPrototypeWorker = new Worker('reflect-prototype', + (job) => reflectPrototypeService.process(job)) +let mutableService: Handler | AlternateHandler = new Handler() +mutableService = new AlternateHandler() +export const mutableReceiverWorker = new Worker('mutable-receiver', + (job) => mutableService.process(job)) +const unionService: Handler | AlternateHandler = + flag ? new Handler() : new AlternateHandler() +export const unionReceiverWorker = new Worker('union-receiver', + (job) => unionService.process(job)) +class SelectingHandler { + get selected(): typeof first { return flag ? first : second } +} +const selectingHandler = new SelectingHandler() +export const getterTargetWorker = new Worker('getter-target', + (job) => selectingHandler.selected(job)) +function makeWorker( + name: string, + handler: (job: Job) => Promise, +) { + return new Worker(name, (job) => handler(job)) +} +export const parameterForwardWorker = + makeWorker('parameter-forward', first) +export const throwingLaterWorker = new Worker('throwing-later', + (job) => handle(job, explode())) +export const conditionalWorker = new Worker('conditional', (job) => + job.data.ready ? handle(job) : Promise.resolve()) +export const guardedWorker = new Worker('guarded', (job) => { + if (job.data.ready) return handle(job) +}) +export const deadTailWorker = new Worker('dead-tail', (job) => { + handle(job) + return handle(job) +}) +export const wrongArgumentWorker = new Worker('wrong-argument', + (job, other) => handle(other as Job)) + +function registerWorker( + name: string, + handler: (job: Job) => Promise, +) { + return new Worker(name, handler) +} +declare const fakeJob: Job +export const wrappedDirect = registerWorker('wrapped-direct', (job) => handle(job)) +export const wrappedWrong = registerWorker('wrapped-wrong', (_job) => handle(fakeJob)) +export const wrappedConditional = registerWorker('wrapped-conditional', (job) => + job.data.ready ? handle(job) : Promise.resolve()) +export const wrappedDeadTail = registerWorker('wrapped-dead-tail', (job) => { + handle(job) + return handle(job) +}) + +const events = new EventEmitter() +function registerEvent(name: string, handler: () => void) { + return events.on(name, handler) +} +export const wrappedEvent = + registerEvent('wrapped-event', () => handleEvent()) +export const wrappedConditionalEvent = + registerEvent('wrapped-conditional-event', () => true && handleEvent()) +export const readyListener = events.on('ready', () => handleEvent()) +export const explodingListener = events.on('exploding', + (event) => handleEvent(event, explode())) +export const conditionalListener = events.on('conditional', () => true && handleEvent()) +` + const { nodes, edges } = build({ 'src/inline-consumers.ts': source }) + const handlerId = symbol(nodes, 'handle')[0] + const eventHandlerId = symbol(nodes, 'handleEvent')[0] + const consumes = (key: string, target: string) => channels(nodes, (node) => + node.key === key).some(([id]) => hasEdge(edges, id, target, 'consumed_by')) + const hasConsumer = (key: string) => channels(nodes, (node) => + node.key === key).some(([id]) => edges.some(([from, , attributes]) => + from === id && attributes.relation === 'consumed_by')) + + for (const key of [ + 'direct', 'returned', 'awaited', 'voided', 'pure-later', + 'typeof-later', 'equality-later', 'void-later', + 'wrapped-direct', + ]) + expect(consumes(key, handlerId), key).toBe(true) + expect(consumes( + 'stable-receiver', + symbol(nodes, 'Handler.process')[0], + )).toBe(true) + for (const key of [ + 'conditional', + 'guarded', + 'dead-tail', + 'side-effect', + 'optional-receiver', + 'wrong-argument', + 'wrapped-wrong', + 'wrapped-conditional', + 'wrapped-dead-tail', + ]) expect(consumes(key, handlerId)).toBe(false) + for (const key of [ + 'optional-element', + 'nested-optional', + 'conditional-target', + 'aliased-conditional', + 'throwing-later', + 'missing-void', + 'mutable-receiver', + 'union-receiver', + 'getter-target', + 'direct-alias', + 'parameter-forward', + 'direct-new-receiver', + 'proxied-receiver', + 'object-prototype', + 'reflect-prototype', + ]) expect(hasConsumer(key), key).toBe(false) + expect(consumes('ready', eventHandlerId)).toBe(true) + expect(hasConsumer('exploding')).toBe(false) + expect(consumes('wrapped-event', eventHandlerId)).toBe(true) + expect(consumes('conditional', eventHandlerId)).toBe(false) + expect(consumes('wrapped-conditional-event', eventHandlerId)).toBe(false) + }) + it('uses proven Map values for queues instead of assuming the lookup key', () => { const source = `import { Queue } from 'bullmq' const queues = new Map() @@ -745,6 +941,147 @@ export async function persist(code: number): Promise { .toEqual(new Set(calls.map((fact) => fact.id))) }) + it('does not fall through provably non-terminating loop clauses', () => { + const source = `import { writeFile } from 'node:fs/promises' +declare function dynamic(): boolean +export async function withWhile(code: string): Promise { + switch (code) { + case 'complete': + while (true) {} + case 'progress': + await writeFile('while.json', code) + } +} +export async function withDo(code: string): Promise { + switch (code) { + case 'complete': + do {} while (true) + case 'progress': + await writeFile('do.json', code) + } +} +export async function withFor(code: string): Promise { + switch (code) { + case 'complete': + for (;;) {} + case 'progress': + await writeFile('for.json', code) + } +} +export async function withNumeric(code: string): Promise { + switch (code) { + case 'complete': + while (1) {} + case 'progress': + await writeFile('numeric.json', code) + } +} +export async function withNegatedBoolean(code: string): Promise { + switch (code) { + case 'complete': + while (!false) { + if (false) break + } + case 'progress': + await writeFile('negated.json', code) + } +} +export async function withStrictComparison(code: string): Promise { + switch (code) { + case 'complete': + while (1 === 1) {} + case 'progress': + await writeFile('comparison.json', code) + } +} +export async function withObject(code: string): Promise { + switch (code) { + case 'complete': + while ({}) {} + case 'progress': + await writeFile('object.json', code) + } +} +export async function withDoReturn(code: string): Promise { + switch (code) { + case 'complete': + do { return } while (dynamic()) + case 'progress': + await writeFile('do-return.json', code) + } +} +export async function withDoThrow(code: string): Promise { + switch (code) { + case 'complete': + do { throw new Error('stop') } while (dynamic()) + case 'progress': + await writeFile('do-throw.json', code) + } +} +export async function withIfObject(code: string): Promise { + switch (code) { + case 'complete': + if ({}) return + case 'progress': + await writeFile('if-object.json', code) + } +} +export async function withIfStrict(code: string): Promise { + switch (code) { + case 'complete': + if (1 === 1) return + case 'progress': + await writeFile('if-strict.json', code) + } +} +` + const { nodes } = build({ 'src/switch-infinite-loop.ts': source }) + for (const name of [ + 'withWhile', + 'withDo', + 'withFor', + 'withNumeric', + 'withNegatedBoolean', + 'withStrictComparison', + 'withObject', + 'withDoReturn', + 'withDoThrow', + 'withIfObject', + 'withIfStrict', + ]) { + const persisted = facts(nodes, symbol(nodes, name), 'persistence') + expect(persisted).toHaveLength(1) + const arms = persisted[0]!.control.flatMap((frame) => + frame.kind === 'branch' ? [frame.arm] : []) + expect(arms).toHaveLength(1) + expect(JSON.parse( + Buffer.from(arms[0]!.slice(5), 'base64url').toString('utf8'), + )).toEqual(['string', 'progress']) + } + }) + + it('keeps a do-continue path reachable when its condition may be false', () => { + const source = `import { writeFile } from 'node:fs/promises' +declare function dynamic(): boolean +export async function run(code: string): Promise { + switch (code) { + case 'complete': + do { continue } while (dynamic()) + case 'progress': + await writeFile('continued.json', code) + } +} +` + const { nodes } = build({ 'src/do-continue.ts': source }) + const persisted = facts(nodes, symbol(nodes, 'run'), 'persistence') + expect(persisted).toHaveLength(2) + const arms = persisted.flatMap((fact) => fact.control.flatMap((frame) => + frame.kind === 'branch' ? [frame.arm] : [])) + expect(new Set(arms.map((arm) => JSON.parse( + Buffer.from(arm.slice(5), 'base64url').toString('utf8'), + )[1]))).toEqual(new Set(['complete', 'progress'])) + }) + it('fails an oversized or effectful switch owner closed', () => { const clauses = Array.from( { length: 33 }, @@ -782,6 +1119,2160 @@ export function choose(code: number): void { } }) + it('records only immutable same-owner switch discriminants', () => { + const source = `type Job = { + data: { trigger: string; fallback: string } +} + +export function exact(job: Job): void { + const { trigger: kind } = job.data + switch (kind) { + case 'complete': + return + case 'progress': + return + } +} + +export function reassigned(job: Job): void { + let kind = job.data.trigger + kind = job.data.fallback + switch (kind) { + case 'complete': + return + } +} + +export function defaulted(job: Job): void { + const { trigger = 'progress' } = job.data + switch (trigger) { + case 'complete': + return + } +} + +export function rested(job: Job): void { + const { ...payload } = job.data + switch (payload.trigger) { + case 'complete': + return + } +} + +export function computed(job: Job): void { + const { ['trigger']: kind } = job.data + switch (kind) { + case 'complete': + return + } +} + +export function dynamicCase(job: Job): void { + const expected = job.data.fallback + switch (job.data.trigger) { + case expected: + return + } +} + +export function duplicateCase(job: Job): void { + switch (job.data.trigger) { + case 'complete': + return + case 'complete': + return + } +} + +export function reassignedRoot(job: Job, replacement: Job): void { + job = replacement + switch (job.data.trigger) { + case 'complete': + return + } +} + +export function mutatedRoot(job: Job): void { + job.data.trigger = job.data.fallback + switch (job.data.trigger) { + case 'complete': + return + } +} + +export function compatibleAliasMutation(job: Job, alias: Job): void { + alias.data.trigger = 'progress' + switch (job.data.trigger) { + case 'complete': + return + } +} + +function aliasThroughCall(job: Job, alias: Job): void { + alias.data.trigger = 'progress' + switch (job.data.trigger) { + case 'complete': + return + } +} +export function sameArgumentAlias(job: Job): void { + aliasThroughCall(job, job) +} + +declare function mutateJob(job: Job): void +export function externalAliasMutation(job: Job, alias: Job): void { + mutateJob(alias) + switch (job.data.trigger) { + case 'complete': + return + } +} + +type AliasMarker = { marker: boolean } +export function intersectionAliasMutation( + job: Job, + alias: AliasMarker, +): void { + ;(alias as unknown as Job).data.trigger = 'progress' + switch (job.data.trigger) { + case 'complete': + return + } +} +export function intersectionAliasCaller(value: Job & AliasMarker): void { + intersectionAliasMutation(value, value) +} + +class GetterJob { + data = { trigger: 'complete', fallback: 'progress' } + get touch(): number { + this.data.trigger = 'progress' + return 1 + } +} +export function getterAliasMutation(job: GetterJob, alias: GetterJob): void { + void alias.touch + switch (job.data.trigger) { + case 'complete': + return + } +} + +export function logicalAliasMutation(job: Job, alias: Job): void { + const linked = alias || job + linked.data.trigger = 'progress' + switch (job.data.trigger) { + case 'complete': + return + } +} + +class CoerciveJob { + data = { trigger: 'complete', fallback: 'progress' } + valueOf(): number { + this.data.trigger = 'progress' + return 1 + } +} +export function coerciveAliasMutation( + job: CoerciveJob, + alias: CoerciveJob, +): void { + void (alias + 1) + switch (job.data.trigger) { + case 'complete': + return + } +} + +export function destructuredAliasMutation( + job: Job, + { data }: Job, +): void { + data.trigger = 'progress' + switch (job.data.trigger) { + case 'complete': + return + } +} + +export function hoistedAliasMutation(job: Job, alias: Job): void { + changeAlias() + switch (job.data.trigger) { + case 'complete': + return + } + function changeAlias(): void { + mutateJob(alias) + } +} + +export function defaultRoot( + job: Job = { data: { trigger: 'progress', fallback: 'progress' } }, +): void { + switch (job.data.trigger) { + case 'complete': + return + } +} + +export function explicitThis(this: void, job: Job): void { + switch (job.data.trigger) { + case 'complete': + return + } +} + +class DataAccessorJob { + get data(): Job['data'] { + return { trigger: 'progress', fallback: 'progress' } + } +} +export function dataAccessorSelector(job: DataAccessorJob): void { + switch (job.data.trigger) { + case 'complete': + return + } +} + +class AccessorPayload { + get trigger(): string { return 'progress' } + fallback = 'progress' +} +class TriggerAccessorJob { data = new AccessorPayload() } +export function triggerAccessorSelector(job: TriggerAccessorJob): void { + switch (job.data.trigger) { + case 'complete': + return + } +} +export function triggerAccessorAlias(job: TriggerAccessorJob): void { + const { trigger: kind } = job.data + switch (kind) { + case 'complete': + return + } +} +export function triggerAccessorShorthand(job: TriggerAccessorJob): void { + const { trigger } = job.data + switch (trigger) { + case 'complete': + return + } +} + +export function conditionalVar(job: Job, enabled: boolean): void { + if (enabled) { + var { trigger: kind } = job.data + } + switch (kind) { + case 'complete': + return + } +} + +export function forOfRoot(job: Job, replacements: Job[]): void { + for (job of replacements) void job + switch (job.data.trigger) { + case 'complete': + return + } +} + +export function forInRoot( + job: Job, + replacements: Record, +): void { + for (job in replacements) void job + switch (job.data.trigger) { + case 'complete': + return + } +} + +export function duplicateDefault(job: Job): void { + switch (job.data.trigger) { + default: + return + default: + return + } +} + +export function conditionalCase(job: Job, enabled: boolean): void { + if (enabled) { + var expected = 'complete' + } + switch (job.data.trigger) { + case expected: + return + } +} + +export function lateCase(job: Job): void { + switch (job.data.trigger) { + case expected: + return + } + const expected = 'complete' + void expected +} + +export function crossCase(job: Job, route: number): void { + switch (route) { + case 0: + const expected = 'complete' + void expected + break + case 1: + switch (job.data.trigger) { + case expected: + return + } + } +} + +export function lateEnum(job: Job): void { + switch (job.data.trigger) { + case Status.Done: + return + } + enum Status { Done = 'complete' } +} + +enum State { Done = 'complete' } +export function mutatedEnum(job: Job): void { + State.Done = 'progress' + switch (job.data.trigger) { + case State.Done: + return + } +} + +declare function externalEnum(value: unknown): void +enum ExternalState { Done = 'complete' } +export function externallyMutatedEnum(job: Job): void { + externalEnum(ExternalState) + switch (job.data.trigger) { + case ExternalState.Done: + return + } +} + +enum ModuleState { Done = 'complete' } +externalEnum(ModuleState) +export function moduleMutatedEnum(job: Job): void { + switch (job.data.trigger) { + case ModuleState.Done: + return + } +} + +enum CaseBodyState { Done = 'complete' } +export function caseBodyMutatedEnum(job: Job, flag: number): void { + switch (flag) { + case 1: + externalEnum(CaseBodyState) + break + } + switch (job.data.trigger) { + case CaseBodyState.Done: + return + } +} + +enum ModuleCaseBodyState { Done = 'complete' } +switch (1) { + case 1: + externalEnum(ModuleCaseBodyState) +} +export function moduleCaseBodyMutatedEnum(job: Job): void { + switch (job.data.trigger) { + case ModuleCaseBodyState.Done: + return + } +} + +enum LaterState { Done = 'complete' } +export function laterMutatedEnum(job: Job): void { + switch (job.data.trigger) { + case LaterState.Done: + break + } + externalEnum(LaterState) +} + +export enum PublicState { Done = 'complete' } +export function publicEnum(job: Job): void { + switch (job.data.trigger) { + case PublicState.Done: + return + } +} + +declare enum AmbientState { Done = 'complete' } +export function ambientEnum(job: Job): void { + switch (job.data.trigger) { + case AmbientState.Done: + return + } +} + +enum ReexportedState { Done = 'complete' } +export { ReexportedState } +export function reexportedEnum(job: Job): void { + switch (job.data.trigger) { + case ReexportedState.Done: + return + } +} + +enum SecretState { + Password = 'hunter2', + Ready = 'sk-live-do-not-index', +} +export function secretEnum(job: Job): void { + switch (job.data.trigger) { + case SecretState.Password: + case SecretState.Ready: + return + } +} + +export function nestedMutation(job: Job): void { + let { data: { trigger } } = job + trigger = 'progress' + switch (job.data.trigger) { + case 'complete': + return + } +} + +export function reflectedMutation(job: Job): void { + Reflect.set(job.data, 'trigger', 'progress') + switch (job.data.trigger) { + case 'complete': + return + } +} + +function change(data: Job['data']): void { + data.trigger = 'progress' +} +function changeThroughHelper(data: Job['data']): void { + change(data) +} +export function helperMutation(job: Job): void { + changeThroughHelper(job.data) + switch (job.data.trigger) { + case 'complete': + return + } +} + +function overloadedChange(data: Job['data']): void +function overloadedChange(data: Job['data']): void { + data.trigger = 'progress' +} +function argumentsChange(data: Job['data']): void { + arguments[0].trigger = 'progress' +} +const boundChange = change.bind(undefined) +class ConstructorChange { + constructor(data: Job['data']) { data.trigger = 'progress' } +} +class MutableData { + trigger = 'complete' + fallback = 'progress' + mutate(): void { this.trigger = 'progress' } + mutateAlias(): void { + const self = this + self.trigger = 'progress' + } + mutateArrow = (): void => { this.trigger = 'progress' } +} +interface UnknownMutator { + mutate(data: Job['data']): void +} +class PureMutator { + mutate(_data: Job['data']): void {} +} +class ImpureMutator { + mutate(data: Job['data']): void { data.trigger = 'progress' } +} + +export function overloadMutation(job: Job): void { + overloadedChange(job.data) + switch (job.data.trigger) { case 'complete': return } +} +export function argumentsMutation(job: Job): void { + argumentsChange(job.data) + switch (job.data.trigger) { case 'complete': return } +} +export function callMutation(job: Job): void { + change.call(undefined, job.data) + switch (job.data.trigger) { case 'complete': return } +} +export function applyMutation(job: Job): void { + change.apply(undefined, [job.data]) + switch (job.data.trigger) { case 'complete': return } +} +export function boundMutation(job: Job): void { + boundChange(job.data) + switch (job.data.trigger) { case 'complete': return } +} +export function constructorMutation(job: Job): void { + new ConstructorChange(job.data) + switch (job.data.trigger) { case 'complete': return } +} +export function receiverMutation(job: { data: MutableData }): void { + job.data.mutate() + switch (job.data.trigger) { case 'complete': return } +} +export function receiverAliasMutation(job: { data: MutableData }): void { + job.data.mutateAlias() + switch (job.data.trigger) { case 'complete': return } +} +export function receiverArrowMutation(job: { data: MutableData }): void { + job.data.mutateArrow() + switch (job.data.trigger) { case 'complete': return } +} +export function unknownMutation(job: Job, mutator: UnknownMutator): void { + mutator.mutate(job.data) + switch (job.data.trigger) { case 'complete': return } +} +export function ambiguousMutation( + job: Job, + mutator: PureMutator | ImpureMutator, +): void { + mutator.mutate(job.data) + switch (job.data.trigger) { case 'complete': return } +} +export function callbackMutation(job: Job): void { + ;[job.data].forEach((data) => { data.trigger = 'progress' }) + switch (job.data.trigger) { case 'complete': return } +} +export function forOfMutation(job: Job): void { + for (const data of [job.data]) data.trigger = 'progress' + switch (job.data.trigger) { case 'complete': return } +} +function identity(value: T): T { return value } +async function asyncIdentity(value: T): Promise { return value } +export function returnedAliasMutation(job: Job): void { + const alias = identity(job.data) + alias.trigger = 'progress' + switch (job.data.trigger) { case 'complete': return } +} +export async function awaitedAliasMutation(job: Job): Promise { + const alias = await asyncIdentity(job.data) + alias.trigger = 'progress' + switch (job.data.trigger) { case 'complete': return } +} +export function conditionalAliasMutation( + job: Job, + other: Job['data'], + enabled: boolean, +): void { + const alias = enabled ? job.data : other + alias.trigger = 'progress' + switch (job.data.trigger) { case 'complete': return } +} +export function malformedRest(...rest: Job[], job: Job): void { + void rest + switch (job.data.trigger) { case 'complete': return } +} + +export function misplacedThis(job: Job, this: void): void { + switch (job.data.trigger) { + case 'complete': + return + } +} + +export function duplicateParameter(job: Job, job: Job): void { + switch (job.data.trigger) { + case 'complete': + return + } +} + +export function longCase(job: Job): void { + switch (job.data.trigger) { + case 'this-case-value-is-deliberately-long-enough-to-exceed-the-encoded-branch-arm-limit': + return + } +} +` + const { nodes } = build({ 'src/switch-discriminants.ts': source }) + const condition = (name: string) => facts( + nodes, + symbol(nodes, name), + 'condition', + ).find((fact): fact is Extract => + fact.kind === 'condition' && fact.condition_kind === 'switch') + const caseValue = (arm: string): unknown => { + const parts = arm.split(':') + if (parts.length !== 2) return undefined + try { + const value = JSON.parse( + Buffer.from(parts[1]!, 'base64url').toString('utf8'), + ) as unknown + return Array.isArray(value) && value.length === 2 + ? value[1] + : undefined + } catch { + return undefined + } + } + const cases = (name: string) => facts( + nodes, + symbol(nodes, name), + ).flatMap((fact) => fact.control.flatMap((control) => + control.kind === 'branch' && control.arm.startsWith('case:') + ? [caseValue(control.arm)] + : [])) + const selector = { + kind: 'template', + parts: [ + { kind: 'parameter', position: 0 }, + { kind: 'literal', value: 'data' }, + { kind: 'literal', value: 'trigger' }, + ], + } + + expect(condition('exact')).toMatchObject({ + test: selector, + evidence: { + statement_range: { + start: { line: 6, column: 3 }, + end: { line: 12, column: 4 }, + }, + }, + }) + expect(cases('exact')).toEqual( + expect.arrayContaining(['complete', 'progress']), + ) + for (const name of [ + 'reassigned', + 'defaulted', + 'rested', + 'computed', + ]) { + expect(condition(name)).toBeDefined() + expect(condition(name)?.test, name).not.toMatchObject(selector) + } + expect(condition('dynamicCase')?.test).not.toMatchObject(selector) + expect(cases('dynamicCase')).toEqual([undefined]) + expect(condition('duplicateCase')?.test).not.toMatchObject(selector) + expect(cases('duplicateCase')).toEqual(['complete']) + expect(condition('externallyMutatedEnum')?.test).not.toMatchObject(selector) + expect(cases('externallyMutatedEnum')).toEqual([undefined]) + expect(condition('moduleMutatedEnum')?.test).not.toMatchObject(selector) + expect(cases('moduleMutatedEnum')).toEqual([undefined]) + for (const name of [ + 'caseBodyMutatedEnum', + 'moduleCaseBodyMutatedEnum', + 'laterMutatedEnum', + 'publicEnum', + 'ambientEnum', + 'reexportedEnum', + 'secretEnum', + ]) { + expect(condition(name)?.test, name).not.toMatchObject(selector) + expect(cases(name), name).not.toContain('complete') + } + expect(cases('secretEnum').filter(Boolean)).toEqual([]) + for (const name of [ + 'reassignedRoot', + 'mutatedRoot', + 'compatibleAliasMutation', + 'aliasThroughCall', + 'externalAliasMutation', + 'intersectionAliasMutation', + 'getterAliasMutation', + 'logicalAliasMutation', + 'coerciveAliasMutation', + 'destructuredAliasMutation', + 'hoistedAliasMutation', + ]) { + expect(condition(name)).toBeDefined() + expect(condition(name)?.test).not.toMatchObject(selector) + } + expect(condition('explicitThis')?.test).toMatchObject(selector) + for (const name of [ + 'dataAccessorSelector', + 'triggerAccessorSelector', + 'triggerAccessorAlias', + 'triggerAccessorShorthand', + ]) { + expect(condition(name)?.test, name).not.toMatchObject(selector) + } + for (const name of [ + 'defaultRoot', + 'conditionalVar', + 'forOfRoot', + 'forInRoot', + 'duplicateDefault', + 'conditionalCase', + 'lateCase', + 'crossCase', + 'lateEnum', + 'mutatedEnum', + 'nestedMutation', + 'reflectedMutation', + 'helperMutation', + 'overloadMutation', + 'argumentsMutation', + 'callMutation', + 'applyMutation', + 'boundMutation', + 'constructorMutation', + 'receiverMutation', + 'receiverAliasMutation', + 'receiverArrowMutation', + 'unknownMutation', + 'ambiguousMutation', + 'callbackMutation', + 'forOfMutation', + 'returnedAliasMutation', + 'awaitedAliasMutation', + 'conditionalAliasMutation', + 'malformedRest', + 'misplacedThis', + 'duplicateParameter', + ]) { + expect(condition(name)).toBeDefined() + expect(condition(name)?.test, name).not.toMatchObject(selector) + } + expect(condition('longCase')?.test).not.toMatchObject(selector) + const longArms = facts(nodes, symbol(nodes, 'longCase')) + .flatMap((fact) => fact.control.flatMap((control) => + control.kind === 'branch' ? [control.arm] : [])) + expect(longArms).toContainEqual(expect.stringMatching(/^case:[a-f0-9]{16}$/u)) + expect(longArms.every((arm) => Buffer.byteLength(arm) <= 96)).toBe(true) + }) + + it('fails file-local static proofs closed for direct lexical eval', () => { + const poisoned = build({ + 'src/eval-poison.ts': `import { Queue } from 'bullmq' +type Job = { data: { trigger: string } } +enum State { Done = 'complete' } +let name = 'reports' +;(eval)("name = 'audit'") +const queue = new Queue(name) +export function dispatch(data: Job['data']) { + return queue.add('persist', data) +} +export function process(job: Job): void { + switch (job.data.trigger) { + case State.Done: + return + } +} +`, + 'src/asserted-eval.ts': `import { Queue } from 'bullmq' +const queue = new Queue<{ trigger: string }>('asserted') +;(eval as typeof eval)("void 0") +export function assertedPublish(data: { trigger: string }) { + return queue.add('persist', data) +} +`, + 'src/shadowed-eval.ts': `import { Queue } from 'bullmq' +function eval(_source: string): void {} +const queue = new Queue('shadowed') +eval('ignored') +export function shadowedPublish() { + return queue.add('persist', { trigger: 'complete' }) +} +`, + }) + expect(publishedQueueKeys( + poisoned.nodes, + poisoned.edges, + symbol(poisoned.nodes, 'dispatch')[0], + )).not.toContain('reports') + expect(outgoing( + poisoned.edges, + symbol(poisoned.nodes, 'assertedPublish')[0], + 'publishes_to', + )).toEqual([]) + expect(publishedQueueKeys( + poisoned.nodes, + poisoned.edges, + symbol(poisoned.nodes, 'shadowedPublish')[0], + )).toEqual(['shadowed']) + const process = facts( + poisoned.nodes, + symbol(poisoned.nodes, 'process'), + ) + const condition = process.find((fact): fact is Extract< + IndexBodyFact, + { kind: 'condition' } + > => fact.kind === 'condition' && fact.condition_kind === 'switch') + expect(condition?.test).not.toMatchObject({ + kind: 'template', + parts: [{ kind: 'parameter', position: 0 }], + }) + expect(JSON.stringify(process)).not.toContain( + Buffer.from(JSON.stringify(['string', 'complete'])).toString('base64url'), + ) + }) + + it('fails closed when an external bodyless call receives the selector root', () => { + const { nodes } = build({ + 'src/mutator.d.ts': `export declare function mutate( + value: { trigger: string }, +): void +`, + 'src/external-mutation.ts': `import { mutate } from './mutator.js' +type Job = { data: { trigger: string } } +export function process(job: Job): void { + mutate(job.data) + switch (job.data.trigger) { case 'complete': return } +} +export function hoistedMutation(job: Job): void { + change() + switch (job.data.trigger) { case 'complete': return } + function change(): void { mutate(job.data) } +} +`, + }) + for (const name of ['process', 'hoistedMutation']) { + const condition = facts(nodes, symbol(nodes, name), 'condition') + .find((fact): fact is Extract => + fact.kind === 'condition' && fact.condition_kind === 'switch') + expect(condition?.test).not.toMatchObject({ + kind: 'template', + parts: [{ kind: 'parameter', position: 0 }], + }) + } + }) + + it('does not authenticate imported enum case values', () => { + const { nodes } = build({ + 'src/state.ts': `export enum SharedState { Done = 'complete' }\n`, + 'src/imported-state.ts': `import { SharedState } from './state.js' +type Job = { data: { trigger: string } } +export function process(job: Job): void { + switch (job.data.trigger) { + case SharedState.Done: + return + } +} +`, + }) + const condition = facts(nodes, symbol(nodes, 'process'), 'condition') + .find((fact): fact is Extract => + fact.kind === 'condition' && fact.condition_kind === 'switch') + expect(condition?.test).not.toMatchObject({ + kind: 'template', + parts: [{ kind: 'parameter', position: 0 }], + }) + }) + + it('tracks exact positional dispatch payloads and omits transformations', () => { + const source = `import { Queue } from 'bullmq' + +type Payload = { trigger: string } +const queue = new Queue('reports') +declare function log(message: string, metadata: object): void + +function pass(name: string, data: Payload, options: object) { + void options + return queue.add(name, data) +} + +function duplicate(name: string, data: Payload, mirror: Payload) { + void mirror + return queue.add(name, data) +} + +function transform(name: string, data: Payload) { + return queue.add(name, { trigger: data.trigger }) +} + +function reassign(name: string, data: Payload, replacement: Payload) { + data = replacement + return queue.add(name, data) +} + +function mutate(name: string, data: Payload) { + data.trigger = 'changed' + return queue.add(name, data) +} + +function argumentsMutation(name: string, data: Payload) { + arguments[1].trigger = 'changed' + return queue.add(name, data) +} + +function arrowArgumentsMutation(name: string, data: Payload) { + ;(() => { arguments[1].trigger = 'changed' })() + return queue.add(name, data) +} + +function laterArgumentsMutation(name: string, data: Payload) { + const pending = queue.add(name, data) + arguments[1].trigger = 'changed' + return pending +} + +function optional( + name: string, + data: Payload = { trigger: 'fallback' }, +) { + return queue.add(name, data) +} + +function rest(name: string, ...packets: Payload[]) { + return queue.add(name, packets as unknown as Payload) +} + +function spread(name: string, ...packets: Payload[]) { + return queue.add(name, ...packets) +} + +function malformedRest( + name: string, + ...packets: Payload[], + data: Payload, +) { + void packets + return queue.add(name, data) +} + +function iterate(name: string, data: Payload, replacements: Payload[]) { + for (data of replacements) void data + return queue.add(name, data) +} + +function enumerate( + name: string, + data: Payload, + replacements: Record, +) { + for (data in replacements) void data + return queue.add(name, data) +} + +function withThis( + this: void, + name: string, + data: Payload, + options: object, +) { + void options + return queue.add(name, data) +} + +function misplacedThis( + name: string, + this: void, + data: Payload, + options: object, +) { + void options + return queue.add(name, data) +} + +function duplicateParameter( + name: string, + data: Payload, + data: Payload, +) { + return queue.add(name, data) +} + +function nestedMutation(name: string, data: Payload) { + let { trigger } = data + trigger = 'changed' + return queue.add(name, data) +} + +function reflectedMutation(name: string, data: Payload) { + Reflect.set(data, 'trigger', 'changed') + return queue.add(name, data) +} + +function change(data: Payload) { + data.trigger = 'changed' +} + +function helperMutation(name: string, data: Payload) { + change(data) + return queue.add(name, data) +} + +function postCallHelperMutation(name: string, data: Payload) { + const pending = queue.add(name, data) + change(data) + return pending +} + +function postCallExternalMutation(name: string, data: Payload) { + const pending = queue.add(name, data) + mutateExternal(data) + return pending +} + +function dual(name: string, left: Payload, right: Payload) { + void queue.add(name, left) + return queue.add(name, right) +} + +function aliasedMutation(name: string, data: Payload, mirror: Payload) { + mirror.trigger = 'changed' + return queue.add(name, data) +} + +declare function mutateExternal(data: Payload): void +declare function mutateNested(data: { value: string }): void +declare function tag( + strings: TemplateStringsArray, + value: { value: string }, +): void + +const modulePayload: Payload = { trigger: 'complete' } + +function localMutation(name: string) { + const data: Payload = { trigger: 'complete' } + mutateExternal(data) + return queue.add(name, data) +} + +function hoistedLocalMutation(name: string) { + const data: Payload = { trigger: 'complete' } + change() + return queue.add(name, data) + function change() { mutateExternal(data) } +} + +function passEnvelope( + name: string, + payload: { envelope: Payload }, +) { + return queue.add(name, payload) +} + +function nestedAliasMutation( + name: string, + data: Payload & { nested: { value: string } }, +) { + const nested = data.nested + mutateNested(nested) + return queue.add(name, data) +} + +function directNestedMutation( + name: string, + data: Payload & { nested: { value: string } }, +) { + mutateNested(data.nested) + return queue.add(name, data) +} + +function nestedContainerMutation( + name: string, + data: Payload & { nested: { value: string } }, +) { + const envelope = { nested: data.nested } + mutateNested(envelope.nested) + return queue.add(name, data) +} + +function defaultNestedMutation( + name: string, + data: Payload & { nested: { value: string } }, + mutate = (nested = data.nested) => mutateNested(nested), +) { + mutate() + return queue.add(name, data) +} + +function thrownNestedMutation( + name: string, + data: Payload & { nested: { value: string } }, +) { + try { + throw data.nested + } catch (nested) { + mutateNested(nested as { value: string }) + } + return queue.add(name, data) +} + +function returnedNestedMutation( + name: string, + data: Payload & { nested: { value: string } }, +) { + void queue.add(name, data) + return data.nested +} + +function iteratedNestedMutation( + name: string, + data: Payload & { items: Array<{ value: string }> }, +) { + for (const item of data.items) mutateNested(item) + return queue.add(name, data) +} + +function taggedNestedMutation( + name: string, + data: Payload & { nested: { value: string } }, +) { + tag\`value:\${data.nested}\` + return queue.add(name, data) +} + +async function awaitedNestedMutation( + name: string, + data: Payload & { nested: PromiseLike }, +) { + await data.nested + return queue.add(name, data) +} + +function coercedNestedMutation( + name: string, + data: Payload & { nested: { value: string }; matcher: Function }, +) { + void +(data.nested as unknown as number) + void \`value:\${data.nested}\` + void (data.nested instanceof (data.matcher as typeof Function)) + return queue.add(name, data) +} + +function observed(name: string, data: Payload) { + const pending = queue.add(name, data) + const jobMeta = typeof data === 'object' && data !== null + ? { trigger: data.trigger } + : {} + void jobMeta + return pending +} + +async function observedAfterAwait( + name: string, + data: T, +) { + const job = await queue.add(name, data) + const jobMeta = typeof data === 'object' && data !== null + ? { trigger: (data as Record).trigger } + : {} + log('added', { name, ...jobMeta }) + return job +} + +function observedBeforeAwait(name: string, data: T) { + const pending = queue.add(name, data) + const jobMeta = typeof data === 'object' && data !== null + ? { trigger: (data as Record).trigger } + : {} + log('adding', { name, ...jobMeta }) + return pending +} + +async function hoistedMutationDuringDispatch( + name: string, + data: Payload, +) { + return await queue.add(name, data, change() as object) + function change() { + mutateExternal(data) + return {} + } +} + +function typeOnly(name: string, data: Payload) { + type Snapshot = typeof data + const pending = queue.add(name, data) + return pending as Promise & { __type?: Snapshot } +} + +function siblingTypeOnly(name: string, data: Payload, meta: object) { + type Metadata = typeof meta + const pending = queue.add(name, data) + return pending as Promise & { __type?: Metadata } +} + +function coerciveRead(name: string, data: Payload) { + const pending = queue.add(name, data) + void ((data as unknown as number) + 1) + return pending +} + +function logicalAliasRead( + name: string, + data: Payload, + fallback: Payload, +) { + const pending = queue.add(name, data) + const linked = data || fallback + linked.trigger = 'changed' + return pending +} + +function spreadBeforePayload( + _meta: object, + data: Payload, + ..._options: object[] +) { + return queue.add('complete', data) +} + +export function exact(data: Payload, options: object) { + return pass('complete', data, options) +} + +export function observedExact(data: Payload) { + return observed('complete', data) +} + +export function awaitedObservedExact(data: Payload) { + return observedAfterAwait('complete', data) +} + +export function pendingObservedExact(data: Payload) { + return observedBeforeAwait('complete', data) +} + +export function hoistedAwaitedMutation(data: Payload) { + return hoistedMutationDuringDispatch('complete', data) +} + +export function typeOnlyExact(data: Payload) { + return typeOnly('complete', data) +} + +export function siblingTypeOnlyExact(data: Payload, meta: object) { + return siblingTypeOnly('complete', data, meta) +} + +export function coerciveObserved(data: Payload) { + return coerciveRead('complete', data) +} + +export function logicalObserved(data: Payload, fallback: Payload) { + return logicalAliasRead('complete', data, fallback) +} + +export function spreadBefore(data: Payload, options: object) { + const pair: [object, Payload] = [{}, data] + return spreadBeforePayload(...pair, options) +} + +export function spreadAfter(data: Payload, ...options: object[]) { + return queue.add('complete', data, ...options) +} + +export function directPayloadSpread(payloads: Payload[]) { + return queue.add('complete', ...payloads) +} + +export function directLeadingSpread(pair: [string, Payload]) { + return queue.add(...pair) +} + +export function prototypeSetterPayload() { + return queue.add( + 'complete', + { __proto__: 'complete' } as unknown as Payload, + ) +} + +function forwardSpread( + _meta: object, + name: string, + data: Payload, + ..._extra: unknown[] +) { + return queue.add(name, data) +} +export function spreadBeforeSelector( + head: [object, string], + data: Payload, +) { + return forwardSpread(...head, 'decoy-name', data) +} + +export function decorated(data: Payload, options: object) { + const pending = pass('complete', data, options) as Promise & { + label?: string + } + pending.label = 'local' + return pending +} + +export function directResult(data: Payload) { + const pending = queue.add('complete', data) as Promise & { + label?: string + } + pending.label = 'local' + return pending +} + +export function ambiguous(data: Payload) { + return duplicate('complete', data, data) +} + +export function transformed(data: Payload) { + return transform('complete', data) +} + +export function reassigned(data: Payload, replacement: Payload) { + return reassign('complete', data, replacement) +} + +export function mutated(data: Payload) { + return mutate('complete', data) +} + +export function argumentsAliased() { + return argumentsMutation('complete', { trigger: 'complete' }) +} + +export function arrowArgumentsAliased() { + return arrowArgumentsMutation('complete', { trigger: 'complete' }) +} + +export function laterArgumentsAliased() { + return laterArgumentsMutation('complete', { trigger: 'complete' }) +} + +export function omitted() { + return optional('complete') +} + +export function rested(first: Payload, second: Payload) { + return rest('complete', first, second) +} + +export function spreadPayload(first: Payload, second: Payload) { + return spread('complete', first, second) +} + +export function malformedRested(first: Payload, second: Payload) { + return malformedRest('complete', first, second) +} + +export function iterated(data: Payload, replacements: Payload[]) { + return iterate('complete', data, replacements) +} + +export function enumerated( + data: Payload, + replacements: Record, +) { + return enumerate('complete', data, replacements) +} + +export function explicitThis(data: Payload, options: object) { + return withThis('complete', data, options) +} + +export function malformedThis(data: Payload, options: object) { + return misplacedThis('complete', data, options) +} + +export function duplicated(first: Payload, second: Payload) { + return duplicateParameter('complete', first, second) +} + +export function nestedMutated(data: Payload) { + return nestedMutation('complete', data) +} + +export function reflected(data: Payload) { + return reflectedMutation('complete', data) +} + +export function helperMutated(data: Payload) { + return helperMutation('complete', data) +} + +export function postCallHelperMutated(data: Payload) { + return postCallHelperMutation('complete', data) +} + +export function postCallExternalMutated(data: Payload) { + return postCallExternalMutation('complete', data) +} + +export function conflicting(first: Payload, second: Payload) { + return dual('complete', first, second) +} + +export function aliased(data: Payload) { + return aliasedMutation('complete', data, data) +} + +export function staleLocal() { + return localMutation('complete') +} + +export function directStaleLocal() { + const data: Payload = { trigger: 'complete' } + mutateExternal(data) + return queue.add('complete', data) +} + +export function staleModulePayload() { + const pending = queue.add('complete', modulePayload) + mutateExternal(modulePayload) + return pending +} + +export function repeatedFor(items: Payload[]) { + const data: Payload = { trigger: 'complete' } + for (const item of items) { + void item + void queue.add('complete', data) + mutateExternal(data) + } +} + +export function repeatedWhile(more: () => boolean) { + const data: Payload = { trigger: 'complete' } + while (more()) { + void queue.add('complete', data) + mutateExternal(data) + } +} + +export function laterWrapperMutation(data: Payload) { + return pass( + 'complete', + data, + mutateExternal(data) as unknown as object, + ) +} + +export function laterDirectMutation(data: Payload) { + return queue.add( + 'complete', + data, + mutateExternal(data) as unknown as object, + ) +} + +export function nestedStaleLocal() { + const data: Payload = { trigger: 'complete' } + mutateExternal(data) + return queue.add('complete', { envelope: data }) +} + +export function nestedStaleWrapper() { + const data: Payload = { trigger: 'complete' } + mutateExternal(data) + return passEnvelope('complete', { envelope: data }) +} + +export function nestedAliased( + data: Payload & { nested: { value: string } }, +) { + return nestedAliasMutation('complete', data) +} + +export function directNestedAliased( + data: Payload & { nested: { value: string } }, +) { + return directNestedMutation('complete', data) +} + +export function nestedContainerAliased( + data: Payload & { nested: { value: string } }, +) { + return nestedContainerMutation('complete', data) +} + +export function defaultNestedAliased( + data: Payload & { nested: { value: string } }, +) { + return defaultNestedMutation('complete', data) +} + +export function thrownNestedAliased( + data: Payload & { nested: { value: string } }, +) { + return thrownNestedMutation('complete', data) +} + +export function returnedNestedAliased( + data: Payload & { nested: { value: string } }, +) { + return returnedNestedMutation('complete', data) +} + +export function iteratedNestedAliased( + data: Payload & { items: Array<{ value: string }> }, +) { + return iteratedNestedMutation('complete', data) +} + +export function taggedNestedAliased( + data: Payload & { nested: { value: string } }, +) { + return taggedNestedMutation('complete', data) +} + +export function awaitedNestedAliased( + data: Payload & { nested: PromiseLike }, +) { + return awaitedNestedMutation('complete', data) +} + +export function coercedNestedAliased( + data: Payload & { nested: { value: string }; matcher: Function }, +) { + return coercedNestedMutation('complete', data) +} + +export function staleHoistedLocal() { + return hoistedLocalMutation('complete') +} + +` + const built = build({ 'src/dispatch-payload.ts': source }) + const { nodes, edges } = built + const marked = (name: string) => outgoing( + edges, + symbol(nodes, name)[0], + 'publishes_to', + ).flatMap(([, , attributes]) => + attributes.dispatch_payload_argument === undefined + ? [] + : [attributes.dispatch_payload_argument]) + + expect(marked('exact')).toEqual([1]) + expect(marked('observedExact')).toEqual([1]) + expect(marked('awaitedObservedExact')).toEqual([1]) + expect(marked('pendingObservedExact')).toEqual([]) + expect(marked('hoistedAwaitedMutation')).toEqual([]) + expect(marked('typeOnlyExact')).toEqual([1]) + expect(marked('siblingTypeOnlyExact')).toEqual([1]) + expect(marked('coerciveObserved')).toEqual([]) + expect(marked('logicalObserved')).toEqual([]) + expect(marked('spreadBefore')).toEqual([]) + expect(marked('spreadAfter')).toEqual([1]) + expect(marked('directPayloadSpread')).toEqual([]) + expect(marked('directLeadingSpread')).toEqual([]) + expect(marked('prototypeSetterPayload')).toEqual([]) + expect(marked('spreadBeforeSelector')).toEqual([]) + expect(marked('decorated')).toEqual([1]) + expect(marked('directResult')).toEqual([1]) + expect(marked('ambiguous')).toEqual([]) + expect(marked('transformed')).toEqual([]) + expect(marked('reassigned')).toEqual([]) + expect(marked('mutated')).toEqual([]) + expect(marked('argumentsAliased')).toEqual([]) + expect(marked('arrowArgumentsAliased')).toEqual([]) + expect(marked('laterArgumentsAliased')).toEqual([]) + expect(marked('omitted')).toEqual([]) + expect(marked('rested')).toEqual([]) + expect(marked('spreadPayload')).toEqual([]) + expect(marked('malformedRested')).toEqual([]) + expect(marked('iterated')).toEqual([]) + expect(marked('enumerated')).toEqual([]) + expect(marked('explicitThis')).toEqual([1]) + expect(marked('malformedThis')).toEqual([]) + expect(marked('duplicated')).toEqual([]) + expect(marked('nestedMutated')).toEqual([]) + expect(marked('reflected')).toEqual([]) + expect(marked('helperMutated')).toEqual([]) + expect(marked('postCallHelperMutated')).toEqual([]) + expect(marked('postCallExternalMutated')).toEqual([]) + expect(marked('conflicting')).toEqual([]) + expect(marked('aliased')).toEqual([]) + expect(marked('staleLocal')).toEqual([]) + expect(marked('directStaleLocal')).toEqual([]) + expect(marked('staleModulePayload')).toEqual([]) + expect(marked('repeatedFor')).toEqual([]) + expect(marked('repeatedWhile')).toEqual([]) + expect(marked('laterWrapperMutation')).toEqual([]) + expect(marked('laterDirectMutation')).toEqual([]) + expect(marked('nestedStaleLocal')).toEqual([]) + expect(marked('nestedStaleWrapper')).toEqual([]) + expect(marked('nestedAliased')).toEqual([]) + expect(marked('directNestedAliased')).toEqual([]) + expect(marked('nestedContainerAliased')).toEqual([]) + expect(marked('defaultNestedAliased')).toEqual([]) + expect(marked('thrownNestedAliased')).toEqual([]) + expect(marked('returnedNestedAliased')).toEqual([]) + expect(marked('iteratedNestedAliased')).toEqual([]) + expect(marked('taggedNestedAliased')).toEqual([]) + expect(marked('awaitedNestedAliased')).toEqual([]) + expect(marked('coercedNestedAliased')).toEqual([]) + expect(marked('staleHoistedLocal')).toEqual([]) + const duplicate = build({ + 'src/duplicate-witness.ts': `import { Queue } from 'bullmq' +const queue = new Queue<{ trigger: string }>('sync') +export function dispatch(code: number) { + switch (code) { + case 1: + case 2: + return queue.add('persist', { trigger: 'complete' }) + } +} +`, + }) + const duplicateEdge = outgoing( + duplicate.edges, + symbol(duplicate.nodes, 'dispatch')[0], + 'publishes_to', + )[0] + expect(duplicateEdge).toBeDefined() + expect(duplicateEdge?.[2].dispatch_payload_argument).toBeUndefined() + const duplicateGraph = loadGraphArtifact(generateIndex(duplicate.root).graphPath) + expect(inspectQueryIndex(duplicateGraph)).toMatchObject({ state: 'ready' }) + for (const name of [ + 'exact', + 'observedExact', + 'awaitedObservedExact', + 'pendingObservedExact', + 'hoistedAwaitedMutation', + 'typeOnlyExact', + 'siblingTypeOnlyExact', + 'coerciveObserved', + 'logicalObserved', + 'spreadBefore', + 'spreadAfter', + 'directPayloadSpread', + 'directLeadingSpread', + 'prototypeSetterPayload', + 'spreadBeforeSelector', + 'decorated', + 'directResult', + 'ambiguous', + 'transformed', + 'reassigned', + 'mutated', + 'argumentsAliased', + 'arrowArgumentsAliased', + 'laterArgumentsAliased', + 'omitted', + 'rested', + 'spreadPayload', + 'malformedRested', + 'iterated', + 'enumerated', + 'explicitThis', + 'malformedThis', + 'duplicated', + 'nestedMutated', + 'reflected', + 'helperMutated', + 'postCallHelperMutated', + 'postCallExternalMutated', + 'conflicting', + 'aliased', + 'staleLocal', + 'directStaleLocal', + 'staleModulePayload', + 'repeatedFor', + 'repeatedWhile', + 'laterWrapperMutation', + 'laterDirectMutation', + 'nestedStaleLocal', + 'nestedStaleWrapper', + 'nestedAliased', + 'directNestedAliased', + 'nestedContainerAliased', + 'staleHoistedLocal', + ]) { + expect(outgoing( + edges, + symbol(nodes, name)[0], + 'publishes_to', + )).not.toEqual([]) + } + const target = (name: string) => outgoing( + edges, + symbol(nodes, name)[0], + 'publishes_to', + ).map(([, id]) => nodes.get(id)) + expect(target('directPayloadSpread')).toContainEqual( + expect.objectContaining({ channel_kind: 'job', key: 'complete' }), + ) + expect(target('directLeadingSpread')).not.toContainEqual( + expect.objectContaining({ channel_kind: 'job' }), + ) + expect(target('spreadBeforeSelector')).not.toContainEqual( + expect.objectContaining({ channel_kind: 'job' }), + ) + }) + + it('selects exact Bull payload positions without speculating overloads', () => { + const source = `import Queue from 'bull' +type Payload = { trigger: string } +const queue = new Queue('sync') +export function unnamed() { + return queue.add( + { trigger: 'complete' }, + { attempts: 1, trigger: 'wrong-options-value' } as any, + ) +} +export function named() { + return queue.add('persist', { trigger: 'complete' }) +} +function passUnnamed(data: Payload, options: object) { + return queue.add(data, options) +} +export function wrappedUnnamed() { + return passUnnamed({ trigger: 'complete' }, { attempts: 1 }) +} +function passNamed(name: string, data: Payload) { + return queue.add(name, data) +} +export function wrappedNamed() { + return passNamed('persist', { trigger: 'complete' }) +} +export function dynamic(first: string | Payload, second: Payload) { + return queue.add(first, second) +} +export function boxed(first: String, data: Payload) { + return queue.add(first, data) +} +export function boxedUnion(first: String | Payload, data: Payload) { + return queue.add(first, data) +} +interface Stringish extends String {} +export function extendedString(first: Stringish, data: Payload) { + return queue.add(first, data) +} +export function structuralString( + first: { length: number }, + data: Payload, +) { + return queue.add(first, data) +} +` + const { nodes, edges } = build({ 'src/bull-overloads.ts': source }) + const marked = (name: string) => outgoing( + edges, + symbol(nodes, name)[0], + 'publishes_to', + ).flatMap(([, , attributes]) => + typeof attributes.dispatch_payload_argument === 'number' + ? [attributes.dispatch_payload_argument] : []) + expect(marked('unnamed')).toEqual([0]) + expect(marked('named')).toEqual([1]) + expect(marked('wrappedUnnamed')).toEqual([0]) + expect(marked('wrappedNamed')).toEqual([1]) + expect(marked('dynamic')).toEqual([]) + expect(marked('boxed')).toEqual([]) + expect(marked('boxedUnion')).toEqual([]) + expect(marked('extendedString')).toEqual([]) + expect(marked('structuralString')).toEqual([]) + const unnamedTarget = outgoing( + edges, + symbol(nodes, 'unnamed')[0], + 'publishes_to', + )[0]?.[1] + expect(nodes.get(unnamedTarget ?? '')?.channel_kind).toBe('queue') + for (const name of [ + 'unnamed', + 'named', + 'wrappedUnnamed', + 'wrappedNamed', + 'dynamic', + 'boxed', + 'boxedUnion', + 'extendedString', + 'structuralString', + ]) { + expect(outgoing( + edges, + symbol(nodes, name)[0], + 'publishes_to', + )).not.toEqual([]) + } + }) + + it('fails closed when a recognized queue constructor is monkey-patched', () => { + const source = `import { InjectQueue } from '@nestjs/bullmq' +import { Queue } from 'bullmq' +type Payload = { trigger: string } +declare function mutate(data: Payload): void +Queue.prototype.add = (async function(_name: string, data: unknown) { + mutate(data as Payload) + return {} as never +}) as typeof Queue.prototype.add +const queue = new Queue('sync') +async function dispatch(data: Payload) { + return await queue.add('persist', data) +} +export function outer() { + return dispatch({ trigger: 'complete' }) +} +class Publisher { + constructor(@InjectQueue('sync') private readonly queue: Queue) {} + async dispatch(data: Payload) { + return await this.queue.add('persist', data) + } +} +export function outerNest(publisher: Publisher) { + return publisher.dispatch({ trigger: 'complete' }) +} +` + const { nodes, edges, root } = build({ + 'src/patched-queue.ts': source, + }) + for (const name of [ + 'dispatch', + 'outer', + 'Publisher.dispatch', + 'outerNest', + ]) { + expect(outgoing( + edges, + symbol(nodes, name)[0], + 'publishes_to', + )).toEqual([]) + } + expect(inspectQueryIndex( + loadGraphArtifact(generateIndex(root).graphPath), + )).toMatchObject({ state: 'ready' }) + }) + + it('scopes unresolved import mutation authority to the exact module binding', () => { + const publisher = `import { Queue } from 'bullmq' +type Payload = { trigger: string } +const queue = new Queue('sync') +export async function publish(data: Payload) { + return await queue.add('persist', data) +} +` + const unrelated = build({ + 'src/publisher.ts': publisher, + 'src/unrelated.ts': `import { Missing } from 'unresolved-package' +Missing.value = 1 +`, + }) + expect(outgoing( + unrelated.edges, + symbol(unrelated.nodes, 'publish')[0], + 'publishes_to', + )).not.toEqual([]) + + const patched = build({ + 'src/publisher.ts': publisher, + 'src/patch.ts': `import { Queue } from 'bullmq' +Queue.prototype.add = async function() { return {} as never } +`, + }) + expect(outgoing( + patched.edges, + symbol(patched.nodes, 'publish')[0], + 'publishes_to', + )).toEqual([]) + + const namespacePatched = build({ + 'src/publisher.ts': publisher, + 'src/patch.ts': `import * as Bull from 'bullmq' +const { Queue: Patched } = Bull +Patched.prototype.add = async function() { return {} as never } +`, + }) + expect(outgoing( + namespacePatched.edges, + symbol(namespacePatched.nodes, 'publish')[0], + 'publishes_to', + )).toEqual([]) + + for (const [name, patch] of [ + ['shorthand', `import * as Bull from 'bullmq' +const { Queue } = Bull +Queue.prototype.add = async function() { return {} as never } +`], + ['alias-chain', `import * as Bull from 'bullmq' +const { Queue: Patched } = Bull +const Alias = Patched +Alias.prototype.add = async function() { return {} as never } +`], + ['nested', `import * as Bull from 'bullmq' +const { Queue: { prototype } } = Bull +prototype.add = async function() { return {} as never } +`], + ['static-computed', `import * as Bull from 'bullmq' +const { ['Queue']: Patched } = Bull +Patched.prototype.add = async function() { return {} as never } +`], + ['defaulted', `import * as Bull from 'bullmq' +const { Queue: Patched = class {} as typeof Bull.Queue } = Bull +Patched.prototype.add = async function() { return {} as never } +`], + ['rest', `import * as Bull from 'bullmq' +const { ...rest } = Bull +rest.Queue.prototype.add = async function() { return {} as never } +`], + ['namespace-alias', `import * as Bull from 'bullmq' +const Namespace = Bull +const { Queue: Patched } = Namespace +Patched.prototype.add = async function() { return {} as never } +`], + ['assignment', `import * as Bull from 'bullmq' +let Patched = class {} as typeof Bull.Queue +;({ Queue: Patched } = Bull) +Patched.prototype.add = async function() { return {} as never } +`], + ['dynamic-computed', `import * as Bull from 'bullmq' +declare const key: string +const { [key]: Patched } = Bull as Record +Patched.prototype.add = async function() { return {} as never } +`], + ] as const) { + const graph = build({ + 'src/publisher.ts': publisher, + [`src/${name}.ts`]: patch, + }) + expect(outgoing( + graph.edges, + symbol(graph.nodes, 'publish')[0], + 'publishes_to', + ), name).toEqual([]) + } + + const otherMemberPatched = build({ + 'src/publisher.ts': publisher, + 'src/patch.ts': `import * as Bull from 'bullmq' +const { Worker: Patched } = Bull +Patched.prototype.close = async function() {} +`, + }) + expect(outgoing( + otherMemberPatched.edges, + symbol(otherMemberPatched.nodes, 'publish')[0], + 'publishes_to', + )).not.toEqual([]) + }) + + it('keeps producer changes and terminal persistence arms mutation-sensitive', () => { + const source = ( + producer: string, + order: readonly string[], + alias = true, + writes: Readonly> = + Object.fromEntries(order.map((arm) => [arm, [arm]])), + nestedCollision = false, + ) => `import { Queue, Worker, type Job } from 'bullmq' +import type { MongoRepository } from 'typeorm' +type Payload = { + trigger: 'section_complete' | 'assembly_complete' | 'status_change' + fallback: 'section_complete' | 'assembly_complete' | 'status_change' + id: string +} +type Row = { id: string; status: string } +const queue = new Queue('sync') +declare const repository: MongoRepository +export function dispatch(id: string) { + return queue.add('persist', { + trigger: '${producer}', + fallback: 'status_change', + id, + }) +} +export async function process( + job: Job, + repository: MongoRepository, +): Promise { + ${alias + ? 'const { trigger: kind } = job.data' + : `let kind = job.data.trigger + kind = job.data.fallback`} + switch (kind) { +${order.map((arm) => ` case '${arm}': +${(writes[arm] ?? []).map((status) => ` await repository.update(job.data.id, { + id: job.data.id, + status: '${status}', + })`).join('\n')} +${nestedCollision && arm === 'status_change' ? ` switch (job.data.fallback) { + case 'assembly_complete': + await repository.update(job.data.id, { + id: job.data.id, + status: 'nested-collision', + }) + }` : ''} + return`).join('\n')} + } +} +export const worker = new Worker( + 'sync', + (job) => process(job, repository), +) +` + const complete = [ + 'section_complete', + 'assembly_complete', + 'status_change', + ] as const + const baseline = build({ + 'src/mapping.ts': source('assembly_complete', complete), + }) + const changed = build({ + 'src/mapping.ts': source('status_change', complete), + }) + const moved = build({ + 'src/mapping.ts': source('assembly_complete', [ + 'assembly_complete', + 'section_complete', + 'status_change', + ], true, { + section_complete: ['section_complete'], + assembly_complete: [], + status_change: ['status_change', 'assembly_complete'], + }), + }) + const removed = build({ + 'src/mapping.ts': source('assembly_complete', [ + 'section_complete', + 'status_change', + ]), + }) + const unproven = build({ + 'src/mapping.ts': source('assembly_complete', complete, false), + }) + const collision = build({ + 'src/mapping.ts': source( + 'assembly_complete', + ['status_change'], + true, + { status_change: [] }, + true, + ), + }) + const decodeArm = (arm: string): string | undefined => { + if (!arm.startsWith('case:')) return undefined + try { + const value = JSON.parse( + Buffer.from(arm.slice(5), 'base64url').toString('utf8'), + ) as unknown + return Array.isArray(value) && typeof value[1] === 'string' + ? value[1] : undefined + } catch { return undefined } + } + const mapping = (built: ReturnType) => { + const owner = symbol(built.nodes, 'process') + const all = facts(built.nodes, owner) + const condition = all.find((fact): fact is Extract< + IndexBodyFact, + { kind: 'condition' } + > => + fact.kind === 'condition' && fact.condition_kind === 'switch' + && JSON.stringify(fact.test) === JSON.stringify(selector)) + const persisted = all.filter((fact) => fact.kind === 'persistence') + return { + body: owner[1].body_facts, + condition, + labels: all.flatMap((fact) => fact.control.flatMap((frame) => + frame.kind === 'branch' ? [decodeArm(frame.arm)] : [])), + arms: condition ? persisted.flatMap((fact) => + fact.control.flatMap((frame) => + frame.kind === 'branch' + && frame.controller_fact_id === condition.id + ? [decodeArm(frame.arm)] : [])) : [], + } + } + const dispatch = (built: ReturnType) => { + const owner = symbol(built.nodes, 'dispatch') + const edge = outgoing(built.edges, owner[0], 'publishes_to')[0] + const call = facts(built.nodes, owner, 'call').find((fact): fact is Extract< + IndexBodyFact, + { kind: 'call' } + > => + fact.kind === 'call' && fact.arguments.length >= 2) + return { edge, call } + } + const selectedPersistence = (built: ReturnType) => { + const published = dispatch(built) + const target = published.edge?.[1] + const channel = target + ? built.nodes.get(target)?.parent_channel_id ?? target + : undefined + const consumer = channel && built.edges.some(([from, to, attributes]) => + from === channel && to === symbol(built.nodes, 'process')[0] + && attributes.relation === 'consumed_by') + const position = published.edge?.[2].dispatch_payload_argument + const argument = typeof position === 'number' + ? published.call?.arguments[position] : undefined + const trigger = argument?.kind === 'object' + ? argument.entries.find((entry) => entry.key === 'trigger')?.value + : undefined + const value = trigger?.kind === 'literal' + && typeof trigger.value === 'string' ? trigger.value : undefined + const persisted = consumer + ? mapping(built).arms.filter((arm) => arm === value) + : [] + return { trigger: value, persisted, consumer } + } + const selector = { + kind: 'template', + parts: [ + { kind: 'parameter', position: 0 }, + { kind: 'literal', value: 'data' }, + { kind: 'literal', value: 'trigger' }, + ], + } + + const baseDispatch = dispatch(baseline) + const changedDispatch = dispatch(changed) + expect(baseDispatch.edge?.[2]).toMatchObject({ + dispatch_payload_argument: 1, + }) + expect(selectedPersistence(baseline).consumer).toBe(true) + expect(JSON.stringify(baseDispatch.call?.arguments[1])) + .toContain('assembly_complete') + expect(JSON.stringify(changedDispatch.call?.arguments[1])) + .toContain('status_change') + expect(baseDispatch.edge?.[2].evidence).not.toEqual( + changedDispatch.edge?.[2].evidence, + ) + + const baseMap = mapping(baseline) + const movedMap = mapping(moved) + const removedMap = mapping(removed) + expect(baseMap.condition?.test).toMatchObject(selector) + expect(movedMap.condition?.test).toMatchObject(selector) + expect(removedMap.condition?.test).toMatchObject(selector) + expect(baseMap.arms).toEqual(expect.arrayContaining([...complete])) + expect(movedMap.labels).toEqual(expect.arrayContaining([...complete])) + expect(movedMap.body).not.toEqual(baseMap.body) + expect(movedMap.arms).not.toContain('assembly_complete') + expect(removedMap.arms).toEqual(expect.arrayContaining([ + 'section_complete', + 'status_change', + ])) + expect(removedMap.arms).not.toContain('assembly_complete') + expect(mapping(unproven).condition?.test).not.toMatchObject(selector) + expect(mapping(collision).labels).toContain('assembly_complete') + expect(mapping(collision).arms).toEqual(['status_change']) + expect(selectedPersistence(baseline)).toEqual({ + trigger: 'assembly_complete', + persisted: ['assembly_complete'], + consumer: true, + }) + expect(selectedPersistence(changed)).toEqual({ + trigger: 'status_change', + persisted: ['status_change'], + consumer: true, + }) + expect(selectedPersistence(moved)).toEqual({ + trigger: 'assembly_complete', + persisted: [], + consumer: true, + }) + expect(selectedPersistence(removed)).toEqual({ + trigger: 'assembly_complete', + persisted: [], + consumer: true, + }) + expect(selectedPersistence(collision)).toEqual({ + trigger: 'assembly_complete', + persisted: [], + consumer: true, + }) + }) + it('preserves secret taint through nested object keys', () => { const source = `export const SETTINGS = { credentials: { value: 'hunter2' }, diff --git a/tests/unit/canonical-index-execution.test.ts b/tests/unit/canonical-index-execution.test.ts index 913c2432..8d5469b3 100644 --- a/tests/unit/canonical-index-execution.test.ts +++ b/tests/unit/canonical-index-execution.test.ts @@ -136,6 +136,38 @@ function hasRelation( && attributes.relation === relation) } +function decodeTypedCaseArm( + arm: unknown, +): readonly [string, unknown] | null { + if (typeof arm !== 'string') return null + const match = /^case:([A-Za-z0-9_-]+)$/u.exec(arm) + if (!match) return null + try { + const decoded: unknown = JSON.parse( + Buffer.from(match[1]!, 'base64url').toString('utf8'), + ) + return Array.isArray(decoded) + && decoded.length === 2 + && typeof decoded[0] === 'string' + ? [decoded[0], decoded[1]] + : null + } catch { + return null + } +} + +function typedCaseValues( + facts: readonly BodyFact[], + controllerId: string, +): Array { + return facts.flatMap((fact) => fact.control.flatMap((frame) => { + if (frame.kind !== 'branch' + || frame.controller_fact_id !== controllerId) return [] + const value = decodeTypedCaseArm(frame.arm) + return value ? [value] : [] + })) +} + describe('canonical TypeScript semantic execution facts', () => { it('stores selective authenticated facts with nested control and derivable Promise parallelism', () => { const source = `const METRIC_BATCHES = [ @@ -405,4 +437,60 @@ export function emitDynamic(eventName: string) { source === emitDynamic && attributes.relation === 'publishes_to')).toBe(false) }) + + it('binds an exact Bull payload argument to typed consumer switch cases', () => { + const source = `import { Queue, Worker } from 'bullmq' + +type SyncJob = { trigger: 'complete' | 'progress' | 1 } +const queue = new Queue('sync') + +export function dispatch(): Promise { + return queue.add('persist', { trigger: 'complete' }) +} + +export async function process(job: { data: SyncJob }): Promise { + switch (job.data.trigger) { + case 'complete': + return + case 'progress': + return + case 1: + return + } +} + +export const worker = new Worker('sync', process) +` + const { nodes, edges } = build({ 'src/discriminator.ts': source }) + const file = [...nodes].find(([, attributes]) => + attributes.node_kind === 'file' + && attributes.source_file === 'src/discriminator.ts') + if (!file) throw new Error('Missing fixture file node src/discriminator.ts') + const [dispatchId] = named(nodes, 'dispatch') + const [processId, processNode] = named(nodes, 'process') + const processFacts = bodyFacts(processNode, processId, file[0]) + const condition = processFacts.find((fact) => + fact.kind === 'condition' && fact.condition_kind === 'switch') + + const publishEdges = edges.filter(([source, , attributes]) => + source === dispatchId && attributes.relation === 'publishes_to') + expect(publishEdges).toHaveLength(1) + expect(publishEdges[0]![2]).toMatchObject({ + dispatch_payload_argument: 1, + }) + expect(condition?.test).toEqual({ + kind: 'template', + parts: [ + { kind: 'parameter', position: 0 }, + { kind: 'literal', value: 'data' }, + { kind: 'literal', value: 'trigger' }, + ], + }) + expect(new Set(typedCaseValues(processFacts, String(condition?.id)) + .map((value) => JSON.stringify(value)))).toEqual(new Set([ + JSON.stringify(['string', 'complete']), + JSON.stringify(['string', 'progress']), + JSON.stringify(['number', 1]), + ])) + }) }) diff --git a/tests/unit/core-reset-governance.test.ts b/tests/unit/core-reset-governance.test.ts index 70617df1..169833b4 100644 --- a/tests/unit/core-reset-governance.test.ts +++ b/tests/unit/core-reset-governance.test.ts @@ -432,22 +432,22 @@ const SEMANTIC_EXECUTION_PACKAGE_EXCLUSIONS = [ ] as const const SEMANTIC_EXECUTION_SOURCE = { production_typescript_files: 44, - production_typescript_loc: 15_934, - production_loc_added: 3_667, - production_loc_removed: 187, - production_loc_net: 3_480, + production_typescript_loc: 15_719, + production_loc_added: 3_462, + production_loc_removed: 197, + production_loc_net: 3_265, } as const const SEMANTIC_EXECUTION_PACKAGE = { npm_files: 102, - npm_packed_bytes: 145_254, - npm_unpacked_bytes: 638_736, - npm_shasum: '9f0c66e663f703afbb9a5e68f6037f9e211cba58', + npm_packed_bytes: 149_453, + npm_unpacked_bytes: 639_867, + npm_shasum: '0d7e3f067d09d6db953ffc34356d2c1221cc7d08', npm_integrity: - 'sha512-3yYpFxnym0r9DF66IfS8w1MI01DMLU+hX6uQi6aQoBQvbeu6jHn8j059N6ml3MwMx3wvlj41Y3yAMIX3D2N2Aw==', - npm_artifact_sha256: 'b49bf7a1eae2b230da4d0b7a778f6112e1bc9d986c083573bc3bd7296a99c670', + 'sha512-ZC5vSq9MhdEzCmeddBn0LgTNshnN/AtHlwNhxEkeMS0eMqtyqu0haYif64lZBcI2/KS/2nLM/ksVmpjyc/BLaw==', + npm_artifact_sha256: 'fb1aa735fc8d3eb57c5771e9bf59b20c542afa9b96d43975a737e964d40743e6', } as const const SEMANTIC_EXECUTION_DIFF_SHA256 = - 'e712d06a2c43cc0223c2c4219691ddc860f2f05d26c689b19b8ade2d180c99a6' + '910d0e1835e54af5e7a36af0a7ffa612977fb9240e9985d0f1368532d4ca3a43' const CAPABILITY_VALIDATION_V2_PROPOSAL_SHA256 = '4906405cbb806c850c0612305ef460e023e2060b5338734ae0af12303901cbd0' const CAPABILITY_VALIDATION_V2_ISSUE = 'https://github.com/mohanagy/madar/issues/612' @@ -1007,6 +1007,9 @@ describe('core reset governance', () => { expect(design).toContain('At that #608 completion checkpoint no technical phase was active') expect(design).toContain('43 production TypeScript files / 11,956 LOC') expect(design).toContain('At the #606 boundary no valid blinded Native-vs-Graphify-vs-Madar capability runner existed') + expect(design).toContain( + 'Accessor-backed `data` or discriminator properties—including destructured aliases and shorthand—also fail closed.', + ) expect(design).toContain('exactly one tool, `retrieve`') expect(design).toContain('frozen 25,000 ms request-wait ceiling') expect(design).toContain('direct JSON-RPC testing is insufficient') @@ -1049,7 +1052,9 @@ describe('core reset governance', () => { expect(scorecard).toContain('| Retrieval regression #618 | **Passed**') expect(scorecard).toContain('| Retrieval regression #625 | **Passed**') expect(scorecard).toContain('| Beta release | **Published**') - expect(scorecard).toContain('| Semantic execution index #632 | **In progress**') + expect(scorecard).toContain( + '| Semantic execution index #632 | **Reopened — corrective work in progress**', + ) expect(scorecard).toContain('| Obligation-driven retrieval #630 | **Pending**') expect(scorecard).toContain('| No-fallback qualification #631 | **Pending**') expect(scorecard).toContain(CAPABILITY_VALIDATION_PROPOSAL_SHA256) @@ -1098,6 +1103,9 @@ describe('core reset governance', () => { expect(scorecard).toContain('| Retrieval regression #622 | **Passed**') expect(scorecard).toContain('Issues `#622` and `#625` are complete on `next`') expect(scorecard).toContain('#632 active, then #630 pending, then #631 pending') + expect(scorecard).toContain( + 'accessor-backed `data` or discriminator properties—including destructured aliases and shorthand—fail closed', + ) expect(scorecard).toContain('first-candidate stop receipt') expect(scorecard).toContain('102 entries / 159,980 packed / 639,930 unpacked bytes') expect(scorecard).toContain('/compilerOptions/removeComments=true') @@ -1334,7 +1342,7 @@ describe('core reset governance', () => { expect(manifest.schema_version).toBe(1) expect(manifest.status).toBe('accepted') expect(manifest.current).toMatchObject({ - updated_at: '2026-07-31', + updated_at: '2026-08-01', completed_phase: EVIDENCE_SKELETON_RETRIEVAL_ID, active_phase: SEMANTIC_EXECUTION_INDEX_ID, ready_phase: null, @@ -1913,6 +1921,9 @@ describe('core reset governance', () => { main_target: 'forbidden', }, }) + expect(semanticExecution.notes).toContain( + 'accessor-backed data or discriminator properties, including destructured aliases and shorthand, fail closed', + ) expect(semanticExecution.candidate).toMatchObject({ source_measurement: { production_typescript_files: SEMANTIC_EXECUTION_SOURCE.production_typescript_files, @@ -1922,7 +1933,7 @@ describe('core reset governance', () => { net: SEMANTIC_EXECUTION_SOURCE.production_loc_net, diff_sha256: SEMANTIC_EXECUTION_DIFF_SHA256, execution_source_sha256: - 'cf43e183f9d001764f4fdd22b073bef8854f9d949fef5793905291b870d3404b', + '192f0eb505615dd5f62b340496b4515b2df8e8509240e960517f2a86fa09e034', }, package_measurement: { files: SEMANTIC_EXECUTION_PACKAGE.npm_files, @@ -1933,17 +1944,31 @@ describe('core reset governance', () => { artifact_sha256: SEMANTIC_EXECUTION_PACKAGE.npm_artifact_sha256, }, local_verification: { - focused_tests_passed: 196, - last_pre_cache_full_tests_passed: 722, - candidate_full_suite_status: - 'all_761_unique_tests_passed_but_single_process_full_coverage_blocked_by_local_fork_worker_start_pressure', - candidate_full_suite_local_attempts: 6, - default_fork_completed_test_files: 79, - default_fork_completed_tests_passed: 707, - default_fork_unstarted_file: 'tests/unit/retrieve-context.test.ts', - unstarted_file_isolated_tests_passed: 54, - unique_tests_passed_across_default_fork_runs: 761, - local_independent_review: 'no_blocker', + focused_tests_passed: 247, + candidate_full_suite_status: 'passed', + candidate_full_suite_local_attempts: 1, + candidate_full_suite_files_passed: 80, + candidate_full_suite_tests_passed: 785, + coverage_statements_percent: 85.85, + coverage_statements_covered: 7_715, + coverage_statements_total: 8_986, + coverage_branches_percent: 79.46, + coverage_branches_covered: 6_959, + coverage_branches_total: 8_757, + coverage_functions_percent: 92.37, + coverage_functions_covered: 1_369, + coverage_functions_total: 1_482, + coverage_lines_percent: 89.22, + coverage_lines_covered: 6_481, + coverage_lines_total: 7_264, + local_independent_review: 'pending_corrective_review', + pre_reopen_merge_commit: 'e7bd30ce384cf743dbda3e8ee7f15b171a0ea649', + post_merge_acceptance_audit: 'failed_missing_exact_payload_and_discriminant_binding', + corrective_real_graph_acceptance: 'passed', + frozen_govalidate_corpus_files: 6_889, + frozen_govalidate_corpus_bytes: 177_796_450, + frozen_govalidate_corpus_sha256: + '3fa9a0a3edc13cf1439d572292601fff43c43a94f7a50948349f9a69123fa7a7', graph_nodes: 12_313, graph_edges: 32_717, exact_queue_channels: 6, @@ -1952,19 +1977,19 @@ describe('core reset governance', () => { exact_channel_edges: 42, publishes_to_edges: 35, consumed_by_edges: 7, - graph_artifact_bytes: 60_267_088, - graph_artifact_size_ratio: 1.2291718874663748, + graph_artifact_bytes: 60_271_172, + graph_artifact_size_ratio: 1.2292551823152718, beta4_indexing_trials_seconds: [20.32, 21.33, 22.16, 22.51, 22.74], beta4_indexing_median_seconds: 22.16, - candidate_indexing_trials_seconds: [14.83, 12.99, 13.05, 14.16, 13.17], - candidate_indexing_median_seconds: 13.17, - indexing_median_ratio: 0.5943140794223827, + candidate_indexing_trials_seconds: [13.71, 13.49, 13.41, 13.55, 13.45], + candidate_indexing_median_seconds: 13.49, + indexing_median_ratio: 0.608754512635379, warm_retrieval_samples: 100, - warm_retrieval_median_ms: 156.7398119999998, - warm_retrieval_p95_ms: 177.5851354499996, - warm_retrieval_max_ms: 183.07708300000013, + warm_retrieval_median_ms: 213.32806250000067, + warm_retrieval_p95_ms: 238.83405145000143, + warm_retrieval_max_ms: 256.85883300000205, deterministic_graph_sha256: - '77270a6f0330a3ce85fbc42b90e7a3e99f8bf37776f6e65f5da8aad1bad3caaf', + '569af2dcd681c4db48124a47bceac7036a94b344f2a2f88e8cb35f6120711610', broad_retrieval_files: 10, broad_retrieval_snippets: 10, broad_retrieval_serialized_tokens: 3_669, @@ -2450,7 +2475,7 @@ describe('core reset governance', () => { ['merge-base', '--is-ancestor', THIN_DELIVERY_IMPLEMENTATION_START, THIN_DELIVERY_MERGE], )).not.toThrow() expect(manifest.current).toMatchObject({ - updated_at: '2026-07-31', + updated_at: '2026-08-01', completed_phase: EVIDENCE_SKELETON_RETRIEVAL_ID, active_phase: SEMANTIC_EXECUTION_INDEX_ID, ready_phase: null, @@ -4716,7 +4741,7 @@ describe('core reset governance', () => { expect(execFileSync(git, ['rev-parse', `${EVIDENCE_BASE}^{tree}`], { encoding: 'utf8' }).trim()) .toBe(EVIDENCE_BASE_TREE) expect(manifest.current).toMatchObject({ - updated_at: '2026-07-31', + updated_at: '2026-08-01', completed_phase: EVIDENCE_SKELETON_RETRIEVAL_ID, active_phase: SEMANTIC_EXECUTION_INDEX_ID, ready_phase: null, diff --git a/tests/unit/query-index-execution-validation.test.ts b/tests/unit/query-index-execution-validation.test.ts index ffd143db..29fc621d 100644 --- a/tests/unit/query-index-execution-validation.test.ts +++ b/tests/unit/query-index-execution-validation.test.ts @@ -14,6 +14,7 @@ import { generateIndex } from '../../src/application/generate-index.js' import { retrieveContext } from '../../src/application/retrieve-context.js' import { computeBuildId } from '../../src/domain/index/build-state.js' import { + encodeIndexBodyFactTable, indexBodyFactId, indexChannelId, type IndexBodyFact, @@ -56,9 +57,16 @@ type Fixture = { dynamicQueueId: string jobId: string eventId: string + callArgumentCount: number operationIds: readonly string[] } +type MarkerOptions = { + edge?: 'publish' | 'routes' | 'consumed' | 'event' | 'nonchannel' + matchCall?: boolean + value: unknown +} + function ready(value: ReturnType): ReadyQueryIndex { if (value.state !== 'ready') { throw new Error(`Expected ready index, received ${value.state}: ${value.subject}`) @@ -66,7 +74,7 @@ function ready(value: ReturnType): ReadyQueryIndex { return value } -function fixture(bom = false): Fixture { +function fixture(bom = false, marker?: MarkerOptions): Fixture { const root = mkdtempSync(join(tmpdir(), 'madar-query-execution-')) roots.push(root) const source = `${bom ? '\uFEFF' : ''}import type { MongoRepository } from 'typeorm' @@ -98,7 +106,14 @@ export async function run( if (!operations?.length) { throw new Error('Execution validation fixture has no generated operations') } - const proof = operations[0]!.evidence + const call = operations.find((operation) => + operation.kind === 'call' && operation.arguments.length > 0) + if (!call || call.kind !== 'call') { + throw new Error('Execution validation fixture has no argument-bearing call') + } + const proof = marker?.matchCall === false + ? operations.find((operation) => operation.kind !== 'call')!.evidence + : marker ? call.evidence : operations[0]!.evidence const queueId = indexChannelId({ channel_kind: 'queue', @@ -158,9 +173,12 @@ export async function run( excerpt_sha256: proof.excerpt_sha256, }, } + const mark = (edge: MarkerOptions['edge']) => marker && marker.edge === edge + ? { dispatch_payload_argument: marker.value } : {} graph.addEdge(runId, jobId, { relation: 'publishes_to', ...channelEvidence, + ...mark('publish'), }) graph.addEdge(runId, dynamicQueueId, { relation: 'publishes_to', @@ -169,19 +187,27 @@ export async function run( graph.addEdge(jobId, queueId, { relation: 'routes_through', ...channelEvidence, + ...mark('routes'), }) graph.addEdge(queueId, runId, { relation: 'consumed_by', ...channelEvidence, + ...mark('consumed'), }) graph.addEdge(runId, eventId, { relation: 'publishes_to', ...channelEvidence, + ...mark('event'), }) graph.addEdge(eventId, runId, { relation: 'consumed_by', ...channelEvidence, }) + if (marker?.edge === 'nonchannel') graph.addEdge(runId, runId, { + relation: 'calls', + ...channelEvidence, + dispatch_payload_argument: marker.value, + }) resign(graph) return { graph, @@ -193,6 +219,7 @@ export async function run( dynamicQueueId, jobId, eventId, + callArgumentCount: call.arguments.length, operationIds: operations.map((operation) => operation.id), } } @@ -319,6 +346,54 @@ describe('query execution index validation', () => { )).toBe(false) }) + it('accepts a dispatch marker authenticated by one exact call fact', () => { + const current = fixture(false, { edge: 'publish', value: 0 }) + expect(ready(inspectQueryIndex(current.graph))).toBeDefined() + expect(current.callArgumentCount).toBeGreaterThan(0) + }) + + it.each([ + ['negative', { edge: 'publish', value: -1 }], + ['fractional', { edge: 'publish', value: 0.5 }], + ['out-of-range', { edge: 'publish', value: 99 }], + ['nonmatching', { edge: 'publish', value: 0, matchCall: false }], + ['routes', { edge: 'routes', value: 0 }], + ['consumed', { edge: 'consumed', value: 0 }], + ['event', { edge: 'event', value: 0 }], + ['nonchannel', { edge: 'nonchannel', value: 0 }], + ] as const)('rejects a %s dispatch marker', (_name, marker) => { + const current = fixture(false, marker) + expect(inspectQueryIndex(current.graph)).toMatchObject({ state: 'corrupt' }) + }) + + it('rejects a dispatch marker with two matching call proofs', () => { + const current = fixture(false, { edge: 'publish', value: 0 }) + const index = ready(inspectQueryIndex(current.graph)) + const operations = index.operations_by_owner.get(current.runId)! + const call = operations.find((operation) => operation.kind === 'call') + if (!call || call.kind !== 'call') throw new Error('Expected call proof') + const order = [...call.order] + order[0] = Math.max(...operations.map((operation) => operation.order[0] ?? 0)) + 1 + order[2] = Math.max(...operations.map((operation) => operation.order[2] ?? 0)) + 1 + order[3] = 0 + const duplicate: IndexBodyFact = { + ...structuredClone(call), + id: indexBodyFactId( + current.runId, + 'call', + order, + call.evidence.excerpt_sha256, + ), + order, + } + current.graph.replaceNodeAttributes(current.runId, { + ...current.graph.nodeAttributes(current.runId), + body_facts: encodeIndexBodyFactTable([...operations, duplicate]), + }) + resign(current.graph) + expect(inspectQueryIndex(current.graph)).toMatchObject({ state: 'corrupt' }) + }) + it.each([ { name: 'a sparse compact row slot', diff --git a/tests/unit/update-index.test.ts b/tests/unit/update-index.test.ts index fbdb96db..d1926b65 100644 --- a/tests/unit/update-index.test.ts +++ b/tests/unit/update-index.test.ts @@ -9,6 +9,11 @@ import { generateIndex, IndexingCompletenessError } from '../../src/application/ import { updateIndex } from '../../src/application/update-index.js' import { loadAcceptedIndex } from '../../src/adapters/filesystem/index-store.js' import { loadGraphArtifact } from '../../src/adapters/filesystem/graph-artifact.js' +import { + deserializeGraphArtifact, + serializeGraphArtifact, +} from '../../src/domain/graph/artifact.js' +import { computeBuildId } from '../../src/domain/index/build-state.js' const roots: string[] = [] @@ -96,6 +101,40 @@ describe('index updates', () => { expect(artifactBytes(root)).toEqual(before) }) + it('fully regenerates an unchanged graph from the pre-correction engine', () => { + const root = fixture() + const generated = generateIndex(root) + const graph = deserializeGraphArtifact(graphBytes(root)) + const state = graph.graph.index_build as unknown as { + engine_id: string + build_id: string + } + graph.graph.index_build = { + ...state, + engine_id: 'madar-typescript-index-v4', + build_id: '', + } + graph.graph.index_build = { + ...(graph.graph.index_build as object), + build_id: computeBuildId(graph), + } + writeFileSync(generated.graphPath, serializeGraphArtifact(graph), 'utf8') + + const updated = updateIndex(root) + + expect(updated.updateReceipt).toMatchObject({ + mode: 'cold_reconcile', + parsed_files: 2, + reused_files: 0, + invalidated_files: 2, + dependency_closure_size: 2, + fallback_reason: 'cold_process', + publication_advanced: true, + }) + expect(loadAcceptedIndex(updated.graphPath)?.state.engine_id) + .toBe('madar-typescript-index-v4-execution-1') + }) + it('uses one truthful full reconcile for a private or exported source edit', () => { for (const source of [ 'export function leaf(): string { const privateValue = "two"; return privateValue }\n', From da3e1ad360855c950cae6986a9774c45fcb527d0 Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Sat, 1 Aug 2026 16:26:22 +0400 Subject: [PATCH 2/2] test(index): disambiguate channel proof fixture --- .../canonical-index-execution-review-regressions.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/canonical-index-execution-review-regressions.test.ts b/tests/unit/canonical-index-execution-review-regressions.test.ts index 36c2d043..0e7e0053 100644 --- a/tests/unit/canonical-index-execution-review-regressions.test.ts +++ b/tests/unit/canonical-index-execution-review-regressions.test.ts @@ -527,7 +527,7 @@ export const wrappedConditionalEvent = export const readyListener = events.on('ready', () => handleEvent()) export const explodingListener = events.on('exploding', (event) => handleEvent(event, explode())) -export const conditionalListener = events.on('conditional', () => true && handleEvent()) +export const conditionalListener = events.on('conditional-event', () => true && handleEvent()) ` const { nodes, edges } = build({ 'src/inline-consumers.ts': source }) const handlerId = symbol(nodes, 'handle')[0] @@ -558,7 +558,7 @@ export const conditionalListener = events.on('conditional', () => true && handle 'wrapped-wrong', 'wrapped-conditional', 'wrapped-dead-tail', - ]) expect(consumes(key, handlerId)).toBe(false) + ]) expect(consumes(key, handlerId), key).toBe(false) for (const key of [ 'optional-element', 'nested-optional', @@ -579,7 +579,7 @@ export const conditionalListener = events.on('conditional', () => true && handle expect(consumes('ready', eventHandlerId)).toBe(true) expect(hasConsumer('exploding')).toBe(false) expect(consumes('wrapped-event', eventHandlerId)).toBe(true) - expect(consumes('conditional', eventHandlerId)).toBe(false) + expect(consumes('conditional-event', eventHandlerId)).toBe(false) expect(consumes('wrapped-conditional-event', eventHandlerId)).toBe(false) })