diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b13ba136..34b56691 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,10 @@ jobs: steps: - uses: actions/checkout@v4 + with: + # The contract gate records the merge base with origin/icarus-cloud. + # Fetch all branch history so that ref exists on PR and push runs. + fetch-depth: 0 - uses: actions/setup-node@v4 with: @@ -68,6 +72,16 @@ jobs: shell: pwsh run: fvm flutter pub get + - name: Validate Convex Client Contract Gate + shell: pwsh + working-directory: tool/convex_client_gauntlet + run: | + fvm dart pub get + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + fvm dart test + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + fvm dart analyze + - name: Analyze shell: pwsh run: fvm flutter analyze --no-fatal-infos diff --git a/analysis_options.yaml b/analysis_options.yaml index 12180237..f3c276bd 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -11,6 +11,7 @@ analyzer: exclude: - build/** - lib/hive/hive_adapters.g.dart + - tool/convex_client_gauntlet/** errors: curly_braces_in_flow_control_structures: ignore include: package:flutter_lints/flutter.yaml diff --git a/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_handoff.html b/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_handoff.html new file mode 100644 index 00000000..8e1ee822 --- /dev/null +++ b/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_handoff.html @@ -0,0 +1,767 @@ + + + + + + + + + Rerun the Dart client gauntlet with the missing control + + + + + + + +
+
+
+ + HANDOFF + +
+
+
+ + + +
+ + +
+
+

Icarus cloud · Ready to rerun · Decision reopened

+

Rerun the Dart client gauntlet with the missing control

+

The first gate found a real Dartvex strictness gap, but it did not settle Dartvex versus convex_flutter. Its result-field leg regenerated the same fixture, whose return contract was null, instead of testing an actual result rename. A diagnostic control with an explicit return schema did catch the rename at analysis time. The fair next step is to make the server contract explicit, add a thin fail-closed Icarus wrapper, and then run the same runtime faults against both clients.

+ +
+

Decision now

+ +
+
1missing result mutation
+
0runtime seeds completed
+
3analyzer exit on real rename
+
50rerun seeds required
+
+ +

No client winner yet. Keep the first gate as evidence that Dartvex 0.2.0 is not fail-closed by default. Reopen the runtime decision because neither client has completed the symmetric chaos test.

+ +

The goal is still less JSON plumbing and stronger generated Dart APIs. The first run shows that Dartvex can supply useful function and argument types, but Icarus must enforce complete public return validators and reject degraded output. That compensation is small enough to test before writing a generator or forking another package.

+
+ +
+

What the first gate proved

+ +
+ + + + + + + + + + + +
CheckObservedMeaningStatus
Function renameOld method failed analysis with exit 3Generated function names protect callersproved
Argument renameOld named argument failed analysis with exit 3Generated arguments protect callersproved
Result-field renameBaseline fixture regenerated; returns stayed nullNo result mutation was exercisedinvalid leg
Missing return schemaDartvex emitted Future<dynamic>Unspecified server output cannot become a typed Dart resultreal gap
Unknown validatorWarning, exit 0, field degraded to dynamicDefault generation is not fail-closedreal gap
DeterminismSecond baseline generation produced no diffSame input generated the same outputproved
Runtime chaos and profileSkipped by the original stop ruleNo correctness, latency, memory, reconnect, or auth comparison existsnot run
+
+

Evidence: contract_gate.dart, baseline function spec, contract_gate.json, and the first result note.

+ +

The exact fairness failure

+

In contract_gate.dart, the result-field leg calls generate('folders_list_for_parent.json') again. The fixture contains "returns": null. The real folders:listForParent query also has no explicit returns: validator. The old caller therefore remained valid because there was no typed result contract to rename.

+ +

A diagnostic control supplied an explicit object return, generated the typed caller, then changed publicId to folderPublicId. Baseline analysis exited 0; analysis against the renamed result exited 3 with an undefined getter. That does not make Dartvex the winner. It proves the missing control can reverse the narrow conclusion about result-drift detection.

+
+ +
+

The neutral test boundary

+ + + +

Icarus continues to own the outbox, clientId/opId identity, revision and conflict rules, canonical JSON, and .ica round-trip. A client package is transport and tooling, not the owner of those promises. This is also why adopting either package unchanged would be the wrong abstraction boundary.

+

Before touching server sync boundaries, read server_side_sync_boundaries_handoff.md.

+
+ +
+

Fair rerun plan

+ +

Phase 1: repair the contract gate

+
    +
  1. Add explicit returns: validators to the stable public Convex functions used by the comparison. Match the real payload exactly; do not create a test-only fantasy type.
  2. +
  3. Regenerate a scrubbed baseline fixture and add a separate result-renamed fixture where publicId becomes folderPublicId.
  4. +
  5. Compile the unchanged caller against both. Baseline must exit 0. The renamed fixture must exit nonzero and identify the old getter.
  6. +
  7. Add an Icarus-owned wrapper around Dartvex generation. It fails if generation logs Warning:, if a stable public module contains unexpected dynamic, or if the generator exits nonzero.
  8. +
  9. Keep the unknown-validator mutation. It must fail the wrapper with the function and field path.
  10. +
  11. Run generation twice and require a clean repository diff.
  12. +
+

Boundary: do not fork Dartvex or write a replacement generator in this phase. First prove whether explicit server schemas plus a thin strict wrapper deliver the typed API we want.

+ +

Phase 2: make the runtime comparison symmetric

+

Define one small typed Icarus transport interface for the operations used by the gauntlet. Implement it once with Dartvex and once with convex_flutter. Both adapters must receive the same serialized operations, tokens, reconnect schedule, timeouts, and deployment. Package-specific convenience APIs cannot change the workload.

+

Keep two scorecards. The tooling score covers generated coverage, compile-time mutation catches, determinism, warnings, diff size, and maintenance. The runtime score covers correctness, convergence, recovery, latency, CPU, memory, and platform builds. A tooling loss cannot masquerade as a runtime loss, and a fast runtime cannot excuse corrupted library state.

+ +

Phase 3: run correctness before performance

+
    +
  • Run 50 deterministic seeds × 1,000 operations for each adapter.
  • +
  • Use editors A and B plus a clean verifier C. Begin every seed from base-test-v43.ica.
  • +
  • Exercise offline edits, delayed and duplicated delivery, reconnect, subscription restart, delete/recreate, revision conflict, and bounded retries.
  • +
  • Exercise an expired or rejected access token, call the current Supabase Flutter refreshSession() path, reconnect, and prove the queued op lands exactly once.
  • +
  • Persist the runner ledger and reuse identical clientId/opId values for both adapters so a process restart does not give one client an easier test.
  • +
  • After every seed, verifier C exports canonical state. Compare strategies, pages, folders, lineups, order, revisions, and round-trip output. Never compare timestamps or transport-only metadata that the product does not promise.
  • +
+

Use disposable test accounts and publishable client credentials. Never put a Supabase secret or service_role key in Flutter, fixtures, committed output, or logs. The current Supabase Dart API documents refreshSession() as refreshing and returning a new session even when the current session is not expired; the fault injector should assert the session actually changes or is accepted before replaying the op.

+ +

Phase 4: profile only after both are correct

+

Run at least 10 paired profile-build trials, alternating which adapter runs first. Report median and p95 remote convergence, reconnect-to-live time, peak RSS, steady-state CPU, transferred bytes, and build size on every supported desktop target. Record raw samples, tool versions, machine state, commit, and deployment identity.

+
+ +
+

What settles the gauntlet

+ +
+ + + + + + + + + +
ConditionDartvex consequenceconvex_flutter consequence
Any dropped, duplicated, misordered, or silently conflicted library changeImmediate loss, regardless of speed
Cannot recover queued work after auth refresh or reconnectImmediate loss
Generated stable API contains unexpected dynamicLoss unless the thin strict gate rejects it before commitNot a generated-code criterion
Both complete all 50 seeds with canonical equalityCompare profile results, API clarity, adapter size, dependency health, and maintenance cost
Runtime is tied within measurement noiseWins if the generated boundary materially removes JSON plumbingWins if Dartvex still needs broad custom generation or fragile patches
+
+ +

The expected best outcome is not “Dartvex untouched.” It is Dartvex plus a narrow Icarus strictness policy. If that produces complete, deterministic types and passes the same runtime gauntlet, Dartvex earns the win because it moves contract failures into analysis and removes hand-written JSON decoding. If the compensation grows into a package fork, a second generator, or recurring patches for common Convex validators, convex_flutter remains the more honest base.

+
+ +
+

Remote machine handoff

+ +

Start here

+
    +
  1. Check out t3code/convex-client-gauntlet and pull the latest commit.
  2. +
  3. Read this handoff, the first result note, and the server sync boundary handoff.
  4. +
  5. Reproduce the committed first gate before changing fixtures.
  6. +
  7. Implement Phase 1 as a distinct commit. Do not begin runtime work until every repaired contract check is green.
  8. +
  9. Implement the neutral interface and two adapters without changing local Hive models, .ica, UI, outbox semantics, revision rules, or server payload semantics.
  10. +
  11. Run correctness, then profile. Commit raw machine-readable results and a short human verdict. Leave the old result file intact as historical evidence.
  12. +
+ +

Current commands that exist

+
git switch t3code/convex-client-gauntlet
+git pull --ff-only
+
+cd tool/convex_client_gauntlet
+fvm dart pub get
+fvm dart run bin/run.dart
+fvm dart test
+fvm dart analyze
+
+cd ../..
+npx tsc --noEmit
+npm run test:convex
+fvm flutter test
+fvm flutter analyze --no-fatal-infos
+fvm flutter build web --no-tree-shake-icons
+

These reproduce the committed first gate and repository baseline. Add named contract-v2, runtime, and profile entry points as part of the rerun; document their exact commands beside the resulting artifacts rather than pretending they already exist.

+ +

Required artifacts from the rerun

+
    +
  • Explicit-return baseline and actual result-renamed fixtures.
  • +
  • A strict wrapper test proving warnings and unexpected dynamic fail with a useful path.
  • +
  • Generated-output snapshots or hashes proving determinism.
  • +
  • Per-seed runtime JSON for both adapters, including fault schedule and canonical verifier hash.
  • +
  • Paired profile samples with run order, machine, build mode, and package versions.
  • +
  • A final matrix that distinguishes compile-time safety, runtime correctness, performance, and maintenance.
  • +
+
+ +
+

Acceptance checklist

+
    +
  • The result rename mutates a return field, not the function name, argument, caller, or baseline fixture.
  • +
  • The stable public Convex functions in scope have explicit return validators that match real payloads.
  • +
  • Dartvex baseline generation is warning-free and contains no unexpected dynamic.
  • +
  • The unchanged caller fails analysis for function, argument, and result renames.
  • +
  • Unsupported validators fail before generated code can be committed.
  • +
  • Both adapters receive byte-for-byte equivalent operation traces and fault schedules.
  • +
  • All 50 seeds end with exact canonical equality and no unresolved op.
  • +
  • Auth refresh is performed with the client session only; no elevated credential appears anywhere.
  • +
  • All exported strategies and library backups still round-trip.
  • +
  • No winner is declared from the tooling gate alone.
  • +
+
+ +
+

Appendix

+
+ Raw fairness evidence · first gate and diagnostic control +
Committed first gate
+  result-field step: generate('folders_list_for_parent.json')
+  baseline fixture: "returns": null
+  runtime seeds: 0
+
+Diagnostic missing control
+  explicit typed baseline analysis: exit 0
+  publicId -> folderPublicId analysis: exit 3
+  failure: undefined getter on the unchanged caller
+
+Correct interpretation
+  Dartvex 0.2.0 is not fail-closed by default.
+  The original test did not compare runtime clients.
+  A complete result schema lets the generated caller catch result drift.
+

Captured 2026-08-26 against branch base df9f1934bbc7ab71e144208036f385b8f73c77aa. The diagnostic fixture was intentionally not committed; the fair implementation must add a reviewed equivalent.

+
+
+ +

Generated 2026-08-26 · Icarus cloud client evaluation · base df9f193 · Dartvex 0.2.0 · convex_flutter 3.0.1 · Supabase Flutter auth guidance checked 2026-08-26 · revision v1

+
+
+
+ + + + diff --git a/docs/cloud_sync_refactor/convex_dart_client_gauntlet_result.md b/docs/cloud_sync_refactor/convex_dart_client_gauntlet_result.md new file mode 100644 index 00000000..e123e867 --- /dev/null +++ b/docs/cloud_sync_refactor/convex_dart_client_gauntlet_result.md @@ -0,0 +1,120 @@ +# Convex Dart client gauntlet result + +Status: first gate recorded; runtime decision reopened on 2026-08-26 + +Base: `origin/icarus-cloud` at +`e59402eedee9035cf14693fbd26fe8b097d6abfa` on 2026-08-26 + +Candidate versions: `dartvex` 0.2.0 and `dartvex_codegen` 0.2.0 + +Decision: no client winner yet + +## Fairness correction + +The first gate found a real fail-open strictness gap in Dartvex 0.2.0, but its +result-field leg regenerated the baseline fixture instead of exercising a +renamed result fixture. Because that baseline declares `returns: null`, it +could not test whether a typed generated caller catches result drift. + +A diagnostic control with an explicit object return passed baseline analysis +and then failed analysis with exit 3 after `publicId` was renamed to +`folderPublicId`. The runtime comparison still has not run. The corrected, +authoritative next-step plan is +[convex_dart_client_fair_rerun_handoff.html](convex_dart_client_fair_rerun_handoff.html). + +## Result + +The original run stopped at the compile-time contract gate and therefore did +not run the runtime chaos or profile stages. Treat that stop as a recorded +strictness finding, not as a final package verdict. + +The stable `folders:listForParent` function declares argument validators but +no `returns:` validator. Convex represents that result as `returns: null` in a +function spec. Dartvex 0.2.0 deliberately maps the absent result contract to +`Future`. A caller that reads `result.first.publicId` still passes Dart +analysis. The committed runner did not actually rename a result field, so this +observation does not establish whether a complete generated return type catches +that change. + +Dartvex also treats an unknown validator as a warning and generates the +affected field as `dynamic`. Generation exits zero instead of stopping at the +function and field path. + +These are declared losing conditions in the comparison plan. They are not +performance observations and cannot be averaged away. + +| Contract check | Required | Observed | Result | +| --- | --- | --- | --- | +| Function rename | Old method fails analysis | Analysis exits 3 | Pass | +| Argument rename | Old named argument fails analysis | Analysis exits 3 | Pass | +| Result-field rename | Old field access fails analysis | No result rename was exercised; baseline return is `dynamic`; analysis exits 0 | **Invalid leg** | +| Unsupported validator | Generation exits nonzero with a path | Warning with path; generation exits 0 and emits `dynamic` | **Fail** | +| Second generation | No repository diff | No generated-file change | Pass | + +## Reproduce + +The evaluation lives in an isolated Dart package so the rejected candidate is +not added to the Icarus application or its lockfile. + +```bash +cd tool/convex_client_gauntlet +fvm dart pub get +fvm dart run bin/run.dart +fvm dart test +fvm dart analyze +``` + +The committed machine-readable result is +[`tool/convex_client_gauntlet/results/contract_gate.json`](../../tool/convex_client_gauntlet/results/contract_gate.json). +The runner regenerates bindings for the baseline, function-rename, +argument-rename, and unsupported-validator fixtures and compiles the same old +caller after each relevant change. + +## Runtime stage + +Skipped by the comparison's explicit stop rule: + +> If Dartvex generates function names but leaves public results as `dynamic`, +> stop the runtime comparison and record that gap before writing new generator +> code. + +Consequently, this result makes no claim about Dartvex runtime correctness, +latency, memory, reconnect behavior, auth recovery, or platform builds. The +recorded counts are zero because the runtime workload was not started, not +because either client completed it without faults. + +No client abstraction, adapter, application migration, custom generator, +production deployment, local Hive model, `.ica` format, outbox, revision rule, +or server payload changed in this comparison. + +## Refreshed baseline + +Before the gate, the refreshed cloud base passed: + +- `npm ci` (with the existing npm audit report of 4 dependency + vulnerabilities: 2 moderate, 1 high, 1 critical) +- `npx tsc --noEmit` +- `npm run test:convex` (22 tests) +- the six focused Flutter files named by the two handoffs (82 tests) + +After recording the decision, the comparison branch passed: + +- `fvm dart test` in `tool/convex_client_gauntlet` (1 test) +- `fvm dart analyze` in `tool/convex_client_gauntlet` (no issues) +- `npx tsc --noEmit` +- `npm run test:convex` (22 tests) +- `fvm flutter test` (343 tests) +- `fvm flutter analyze --no-fatal-infos` (exit 0 with the same 6 + pre-existing info-level lints) +- `fvm flutter build web --no-tree-shake-icons` + +The exact `fvm flutter build web` command still fails on the refreshed base's +three existing non-constant `IconData` sites in `folder_provider.dart`, +`hive_adapters.g.dart`, and `archive_manifest.dart`. This comparison does not +change those files. Disabling icon tree shaking proves the web target otherwise +compiles; the pre-existing release-build cleanup remains separate work. + +The next comparison may add explicit public result validators and a thin, +Icarus-owned strict wrapper that rejects warnings and unexpected `dynamic`. +Writing a replacement generator or package fork remains out of scope until that +smaller compensation is tested. diff --git a/tool/convex_client_gauntlet/.gitignore b/tool/convex_client_gauntlet/.gitignore new file mode 100644 index 00000000..52fc8e3e --- /dev/null +++ b/tool/convex_client_gauntlet/.gitignore @@ -0,0 +1,3 @@ +/.dart_tool/ +/lib/_probe_caller.dart +/lib/_probe_generated/ diff --git a/tool/convex_client_gauntlet/README.md b/tool/convex_client_gauntlet/README.md new file mode 100644 index 00000000..988fc5ff --- /dev/null +++ b/tool/convex_client_gauntlet/README.md @@ -0,0 +1,16 @@ +# Convex Dart client contract gate + +This isolated Dart package reproduces the compile-time contract gate declared +for Icarus's Convex client comparison. It pins `dartvex` and +`dartvex_codegen` to 0.2.0 without adding either package to the application. + +Run the evaluation and its regression test from this directory: + +```bash +fvm dart pub get +fvm dart run bin/run.dart +fvm dart test +``` + +The runtime chaos and profile stages are intentionally absent. The handoff +requires them to stop when generated public results remain `dynamic`. diff --git a/tool/convex_client_gauntlet/analysis_options.yaml b/tool/convex_client_gauntlet/analysis_options.yaml new file mode 100644 index 00000000..84879e80 --- /dev/null +++ b/tool/convex_client_gauntlet/analysis_options.yaml @@ -0,0 +1,4 @@ +analyzer: + exclude: + - lib/_probe_caller.dart + - lib/_probe_generated/** diff --git a/tool/convex_client_gauntlet/bin/run.dart b/tool/convex_client_gauntlet/bin/run.dart new file mode 100644 index 00000000..399e637c --- /dev/null +++ b/tool/convex_client_gauntlet/bin/run.dart @@ -0,0 +1,16 @@ +import 'dart:io'; + +import 'package:icarus_convex_client_gauntlet/contract_gate.dart'; + +Future main(List args) async { + final outputIndex = args.indexOf('--output'); + final outputPath = outputIndex == -1 || outputIndex + 1 >= args.length + ? null + : args[outputIndex + 1]; + final result = await evaluateContractGate(); + final json = '${result.toPrettyJson()}\n'; + stdout.write(json); + if (outputPath != null) { + File(outputPath).writeAsStringSync(json); + } +} diff --git a/tool/convex_client_gauntlet/fixtures/folders_argument_renamed.json b/tool/convex_client_gauntlet/fixtures/folders_argument_renamed.json new file mode 100644 index 00000000..33a811b3 --- /dev/null +++ b/tool/convex_client_gauntlet/fixtures/folders_argument_renamed.json @@ -0,0 +1,31 @@ +{ + "url": "https://your-deployment.convex.cloud", + "functions": [ + { + "functionType": "Query", + "args": { + "type": "object", + "value": { + "parentPublicId": { + "fieldType": { "type": "string" }, + "optional": true + }, + "scope": { + "fieldType": { + "type": "union", + "value": [ + { "type": "literal", "value": "owned" }, + { "type": "literal", "value": "shared" }, + { "type": "literal", "value": "all" } + ] + }, + "optional": true + } + } + }, + "returns": null, + "identifier": "folders.ts:listForParent", + "visibility": { "kind": "public" } + } + ] +} diff --git a/tool/convex_client_gauntlet/fixtures/folders_function_renamed.json b/tool/convex_client_gauntlet/fixtures/folders_function_renamed.json new file mode 100644 index 00000000..140ae90c --- /dev/null +++ b/tool/convex_client_gauntlet/fixtures/folders_function_renamed.json @@ -0,0 +1,31 @@ +{ + "url": "https://your-deployment.convex.cloud", + "functions": [ + { + "functionType": "Query", + "args": { + "type": "object", + "value": { + "parentFolderPublicId": { + "fieldType": { "type": "string" }, + "optional": true + }, + "scope": { + "fieldType": { + "type": "union", + "value": [ + { "type": "literal", "value": "owned" }, + { "type": "literal", "value": "shared" }, + { "type": "literal", "value": "all" } + ] + }, + "optional": true + } + } + }, + "returns": null, + "identifier": "folders.ts:listWithinParent", + "visibility": { "kind": "public" } + } + ] +} diff --git a/tool/convex_client_gauntlet/fixtures/folders_list_for_parent.json b/tool/convex_client_gauntlet/fixtures/folders_list_for_parent.json new file mode 100644 index 00000000..dacb4e14 --- /dev/null +++ b/tool/convex_client_gauntlet/fixtures/folders_list_for_parent.json @@ -0,0 +1,31 @@ +{ + "url": "https://your-deployment.convex.cloud", + "functions": [ + { + "functionType": "Query", + "args": { + "type": "object", + "value": { + "parentFolderPublicId": { + "fieldType": { "type": "string" }, + "optional": true + }, + "scope": { + "fieldType": { + "type": "union", + "value": [ + { "type": "literal", "value": "owned" }, + { "type": "literal", "value": "shared" }, + { "type": "literal", "value": "all" } + ] + }, + "optional": true + } + } + }, + "returns": null, + "identifier": "folders.ts:listForParent", + "visibility": { "kind": "public" } + } + ] +} diff --git a/tool/convex_client_gauntlet/fixtures/folders_unsupported_validator.json b/tool/convex_client_gauntlet/fixtures/folders_unsupported_validator.json new file mode 100644 index 00000000..a508757f --- /dev/null +++ b/tool/convex_client_gauntlet/fixtures/folders_unsupported_validator.json @@ -0,0 +1,20 @@ +{ + "url": "https://your-deployment.convex.cloud", + "functions": [ + { + "functionType": "Query", + "args": { "type": "object", "value": {} }, + "returns": { + "type": "object", + "value": { + "futureField": { + "fieldType": { "type": "future-validator" }, + "optional": false + } + } + }, + "identifier": "folders.ts:listForParent", + "visibility": { "kind": "public" } + } + ] +} diff --git a/tool/convex_client_gauntlet/lib/contract_gate.dart b/tool/convex_client_gauntlet/lib/contract_gate.dart new file mode 100644 index 00000000..a5d3e616 --- /dev/null +++ b/tool/convex_client_gauntlet/lib/contract_gate.dart @@ -0,0 +1,313 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dartvex_codegen/dartvex_codegen.dart'; + +const _dartvexVersion = '0.2.0'; +const _dartvexCodegenVersion = '0.2.0'; + +final class ContractGateResult { + ContractGateResult({required this.report}); + + final Map report; + + String toPrettyJson() => const JsonEncoder.withIndent(' ').convert(report); +} + +Future evaluateContractGate({String? packageRoot}) async { + final root = Directory(packageRoot ?? Directory.current.path).absolute; + final generated = Directory('${root.path}/lib/_probe_generated'); + final caller = File('${root.path}/lib/_probe_caller.dart'); + + Future<_Generation> generate(String fixtureName) async { + final logs = []; + final errors = []; + final exitCode = await runConvexCodegen( + [ + 'generate', + '--spec-file', + '${root.path}/fixtures/$fixtureName', + '--output', + generated.path, + ], + log: logs.add, + errorLog: errors.add, + ); + return _Generation(exitCode: exitCode, logs: logs, errors: errors); + } + + Future<_Analysis> analyzeCaller() async { + final process = await Process.run(Platform.resolvedExecutable, [ + 'analyze', + caller.path, + generated.path, + ], workingDirectory: root.path); + return _Analysis( + exitCode: process.exitCode, + output: '${process.stdout}${process.stderr}'.trim(), + ); + } + + try { + if (generated.existsSync()) { + generated.deleteSync(recursive: true); + } + if (caller.existsSync()) { + caller.deleteSync(); + } + + final baselineGeneration = await generate('folders_list_for_parent.json'); + caller.writeAsStringSync(_oldCaller); + final baselineAnalysis = await analyzeCaller(); + final baselineSource = File( + '${generated.path}/modules/folders.dart', + ).readAsStringSync(); + final resultIsDynamic = baselineSource.contains( + 'Future listForParent', + ); + final firstGeneration = _directorySnapshot(generated); + + final secondGenerationResult = await generate( + 'folders_list_for_parent.json', + ); + final secondGeneration = _directorySnapshot(generated); + final deterministic = + secondGenerationResult.exitCode == 0 && + _snapshotsEqual(firstGeneration, secondGeneration); + + final functionRenameGeneration = await generate( + 'folders_function_renamed.json', + ); + final functionRenameAnalysis = await analyzeCaller(); + + final argumentRenameGeneration = await generate( + 'folders_argument_renamed.json', + ); + final argumentRenameAnalysis = await analyzeCaller(); + + await generate('folders_list_for_parent.json'); + final resultFieldAnalysis = await analyzeCaller(); + + final unsupportedGeneration = await generate( + 'folders_unsupported_validator.json', + ); + final unsupportedSource = File( + '${generated.path}/modules/folders.dart', + ).readAsStringSync(); + + final functionRenameCaught = + functionRenameGeneration.exitCode == 0 && + functionRenameAnalysis.exitCode != 0; + final argumentRenameCaught = + argumentRenameGeneration.exitCode == 0 && + argumentRenameAnalysis.exitCode != 0; + final resultRenameCaught = + !resultIsDynamic && resultFieldAnalysis.exitCode != 0; + final unsupportedRejected = unsupportedGeneration.exitCode != 0; + final baselineCompiles = + baselineGeneration.exitCode == 0 && baselineAnalysis.exitCode == 0; + final gatePassed = + baselineCompiles && + functionRenameCaught && + argumentRenameCaught && + resultRenameCaught && + unsupportedRejected && + deterministic; + + final report = { + 'schemaVersion': 1, + 'evaluation': 'convex_dart_client_contract_gate', + 'baseCommit': _gitBaseCommit(root), + 'adapterCandidate': 'dartvex', + 'sdkVersion': _dartvexVersion, + 'codegenVersion': _dartvexCodegenVersion, + 'platform': _platformName(), + 'dartVersion': Platform.version, + 'fixture': 'folders:listForParent', + 'baselineCompiles': baselineCompiles, + 'checks': >[ + { + 'id': 'function_rename', + 'required': 'old_generated_method_fails_analysis', + 'status': functionRenameCaught ? 'pass' : 'fail', + 'generationExitCode': functionRenameGeneration.exitCode, + 'analysisExitCode': functionRenameAnalysis.exitCode, + }, + { + 'id': 'argument_rename', + 'required': 'old_named_argument_fails_analysis', + 'status': argumentRenameCaught ? 'pass' : 'fail', + 'generationExitCode': argumentRenameGeneration.exitCode, + 'analysisExitCode': argumentRenameAnalysis.exitCode, + }, + { + 'id': 'result_field_rename', + 'required': 'old_result_field_fails_analysis', + 'status': resultRenameCaught ? 'pass' : 'fail', + 'analysisExitCode': resultFieldAnalysis.exitCode, + 'generatedReturnType': resultIsDynamic ? 'dynamic' : 'typed', + 'detail': resultIsDynamic + ? 'The unvalidated Convex result is absent from function-spec and the old dynamic field access still analyzes.' + : 'The generated result is typed.', + }, + { + 'id': 'unsupported_validator', + 'required': 'generation_stops_with_function_and_field_path', + 'status': unsupportedRejected ? 'pass' : 'fail', + 'generationExitCode': unsupportedGeneration.exitCode, + 'generatedFieldType': + unsupportedSource.contains('dynamic futureField') + ? 'dynamic' + : 'not_dynamic', + 'diagnostics': _sanitizeDiagnostics([ + ...unsupportedGeneration.logs, + ...unsupportedGeneration.errors, + ], root), + }, + { + 'id': 'deterministic_regeneration', + 'required': 'second_generation_has_no_diff', + 'status': deterministic ? 'pass' : 'fail', + 'changedFiles': _changedFiles(firstGeneration, secondGeneration), + }, + ], + 'gatePassed': gatePassed, + 'decision': gatePassed + ? 'continue_runtime_gauntlet' + : 'keep_convex_flutter', + 'runtimeGauntlet': { + 'status': gatePassed ? 'pending' : 'skipped', + 'reason': gatePassed ? null : 'compile_time_contract_gate_failed', + 'correctnessSeedsRun': 0, + 'operationsRun': 0, + 'pairedProfileRuns': 0, + 'p95RemoteConvergenceMs': null, + 'peakMemoryBytes': null, + }, + }; + return ContractGateResult(report: report); + } finally { + if (generated.existsSync()) { + generated.deleteSync(recursive: true); + } + if (caller.existsSync()) { + caller.deleteSync(); + } + } +} + +String _gitBaseCommit(Directory root) { + final result = Process.runSync('git', [ + 'merge-base', + 'HEAD', + 'origin/icarus-cloud', + ], workingDirectory: root.path); + if (result.exitCode != 0) { + throw StateError('Unable to resolve the Icarus base commit.'); + } + return result.stdout.toString().trim(); +} + +String _platformName() { + final match = RegExp(r'on "([^"]+)"').firstMatch(Platform.version); + final rawArchitecture = match?.group(1) ?? 'unknown'; + final architecture = rawArchitecture.replaceFirst( + '${Platform.operatingSystem}_', + '', + ); + return '${Platform.operatingSystem}-$architecture'; +} + +List _sanitizeDiagnostics(List diagnostics, Directory root) => + diagnostics + .map((message) => message.replaceAll(root.path, '')) + .toList(growable: false); + +Map> _directorySnapshot(Directory directory) { + final snapshot = >{}; + final files = + directory + .listSync(recursive: true) + .whereType() + .toList(growable: false) + ..sort((left, right) => left.path.compareTo(right.path)); + for (final file in files) { + final relative = file.path.substring(directory.path.length + 1); + snapshot[relative] = file.readAsBytesSync(); + } + return snapshot; +} + +bool _snapshotsEqual( + Map> left, + Map> right, +) { + if (left.length != right.length) { + return false; + } + for (final entry in left.entries) { + final other = right[entry.key]; + if (other == null || !_bytesEqual(entry.value, other)) { + return false; + } + } + return true; +} + +List _changedFiles( + Map> left, + Map> right, +) { + final paths = {...left.keys, ...right.keys}.toList()..sort(); + return paths + .where((path) { + final leftBytes = left[path]; + final rightBytes = right[path]; + return leftBytes == null || + rightBytes == null || + !_bytesEqual(leftBytes, rightBytes); + }) + .toList(growable: false); +} + +bool _bytesEqual(List left, List right) { + if (left.length != right.length) { + return false; + } + for (var index = 0; index < left.length; index += 1) { + if (left[index] != right[index]) { + return false; + } + } + return true; +} + +final class _Generation { + const _Generation({ + required this.exitCode, + required this.logs, + required this.errors, + }); + + final int exitCode; + final List logs; + final List errors; +} + +final class _Analysis { + const _Analysis({required this.exitCode, required this.output}); + + final int exitCode; + final String output; +} + +const _oldCaller = ''' +import '_probe_generated/api.dart'; + +Future readFirstFolderPublicId(ConvexApi api) async { + final result = await api.folders.listForParent( + parentFolderPublicId: const Optional.of('parent-folder'), + ); + return result.first.publicId as String; +} +'''; diff --git a/tool/convex_client_gauntlet/pubspec.lock b/tool/convex_client_gauntlet/pubspec.lock new file mode 100644 index 00000000..a1f80238 --- /dev/null +++ b/tool/convex_client_gauntlet/pubspec.lock @@ -0,0 +1,437 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d + url: "https://pub.dev" + source: hosted + version: "91.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 + url: "https://pub.dev" + source: hosted + version: "8.4.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + url: "https://pub.dev" + source: hosted + version: "1.15.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + url: "https://pub.dev" + source: hosted + version: "3.1.3" + dartvex: + dependency: "direct main" + description: + name: dartvex + sha256: "7a343c5853f25a1a136051d2d37002a0e1e3f6c230b6f24560797880de33b5d8" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + dartvex_codegen: + dependency: "direct main" + description: + name: dartvex_codegen + sha256: "07f81e4b16460eeb58673f6e514911df92ba5b38513d9207a4211a92646512a7" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + meta: + dependency: transitive + description: + name: meta + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" + url: "https://pub.dev" + source: hosted + version: "1.19.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "14c2945847669b44089bb1222f66873d7ff7103c58911917f2a63c5a62327898" + url: "https://pub.dev" + source: hosted + version: "0.10.14" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + url: "https://pub.dev" + source: hosted + version: "1.26.3" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + url: "https://pub.dev" + source: hosted + version: "0.6.12" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + url: "https://pub.dev" + source: hosted + version: "15.3.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.11.0 <4.0.0" diff --git a/tool/convex_client_gauntlet/pubspec.yaml b/tool/convex_client_gauntlet/pubspec.yaml new file mode 100644 index 00000000..58e43ece --- /dev/null +++ b/tool/convex_client_gauntlet/pubspec.yaml @@ -0,0 +1,12 @@ +name: icarus_convex_client_gauntlet +publish_to: none + +environment: + sdk: ">=3.11.0 <4.0.0" + +dependencies: + dartvex: 0.2.0 + dartvex_codegen: 0.2.0 + +dev_dependencies: + test: 1.26.3 diff --git a/tool/convex_client_gauntlet/results/contract_gate.json b/tool/convex_client_gauntlet/results/contract_gate.json new file mode 100644 index 00000000..cccaf265 --- /dev/null +++ b/tool/convex_client_gauntlet/results/contract_gate.json @@ -0,0 +1,64 @@ +{ + "schemaVersion": 1, + "evaluation": "convex_dart_client_contract_gate", + "baseCommit": "e59402eedee9035cf14693fbd26fe8b097d6abfa", + "adapterCandidate": "dartvex", + "sdkVersion": "0.2.0", + "codegenVersion": "0.2.0", + "platform": "macos-arm64", + "dartVersion": "3.11.0 (stable) (Mon Feb 9 00:38:07 2026 -0800) on \"macos_arm64\"", + "fixture": "folders:listForParent", + "baselineCompiles": true, + "checks": [ + { + "id": "function_rename", + "required": "old_generated_method_fails_analysis", + "status": "pass", + "generationExitCode": 0, + "analysisExitCode": 3 + }, + { + "id": "argument_rename", + "required": "old_named_argument_fails_analysis", + "status": "pass", + "generationExitCode": 0, + "analysisExitCode": 3 + }, + { + "id": "result_field_rename", + "required": "old_result_field_fails_analysis", + "status": "fail", + "analysisExitCode": 0, + "generatedReturnType": "dynamic", + "detail": "The unvalidated Convex result is absent from function-spec and the old dynamic field access still analyzes." + }, + { + "id": "unsupported_validator", + "required": "generation_stops_with_function_and_field_path", + "status": "fail", + "generationExitCode": 0, + "generatedFieldType": "dynamic", + "diagnostics": [ + "Generated 4 files in /lib/_probe_generated", + "Warning: folders.ts:listForParent → returns → field \"futureField\": Unknown Convex type \"future-validator\"; generated as dynamic." + ] + }, + { + "id": "deterministic_regeneration", + "required": "second_generation_has_no_diff", + "status": "pass", + "changedFiles": [] + } + ], + "gatePassed": false, + "decision": "keep_convex_flutter", + "runtimeGauntlet": { + "status": "skipped", + "reason": "compile_time_contract_gate_failed", + "correctnessSeedsRun": 0, + "operationsRun": 0, + "pairedProfileRuns": 0, + "p95RemoteConvergenceMs": null, + "peakMemoryBytes": null + } +} diff --git a/tool/convex_client_gauntlet/test/contract_gate_test.dart b/tool/convex_client_gauntlet/test/contract_gate_test.dart new file mode 100644 index 00000000..d01571cd --- /dev/null +++ b/tool/convex_client_gauntlet/test/contract_gate_test.dart @@ -0,0 +1,24 @@ +import 'package:icarus_convex_client_gauntlet/contract_gate.dart'; +import 'package:test/test.dart'; + +void main() { + test('Dartvex 0.2.0 fails the declared Icarus contract gate', () async { + final result = await evaluateContractGate(); + final report = result.report; + final checks = (report['checks']! as List) + .cast>(); + + Map check(String id) => + checks.singleWhere((item) => item['id'] == id); + + expect(check('function_rename')['status'], 'pass'); + expect(check('argument_rename')['status'], 'pass'); + expect(check('result_field_rename')['status'], 'fail'); + expect(check('result_field_rename')['generatedReturnType'], 'dynamic'); + expect(check('unsupported_validator')['status'], 'fail'); + expect(check('unsupported_validator')['generationExitCode'], 0); + expect(check('deterministic_regeneration')['status'], 'pass'); + expect(report['gatePassed'], isFalse); + expect(report['decision'], 'keep_convex_flutter'); + }); +}