@githolon/dsl 0.110.0 → 0.111.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
package/dart/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## 0.66.7
2
+
3
+ - Recover sync after loss of the upstream bookmark by fetching provider history and allowing the kernel to verify a shared ancestor. Preserve pending local work and reject unrelated history through the existing admission gate.
4
+ - Wait for a sealed initial sync base before sending a first suffix, and expose provider/local heads in sync diagnostics.
5
+
6
+ ## 0.66.6
7
+
8
+ - Let reception close promptly when presentation stream listeners are paused.
9
+
1
10
  ## 0.66.5
2
11
 
3
12
  - Resolve the Node runtime runner through `@githolon/runtime`, the public name for the application runtime package.
@@ -194,17 +194,20 @@ final class ReceptionEventDesk {
194
194
 
195
195
  Future<void> close() async {
196
196
  await _syncStatusChanges.cancel();
197
- if (!_copyChanges.isClosed) await _copyChanges.close();
197
+ // Public broadcast streams can have paused UI listeners. Their close()
198
+ // futures wait for those listeners to resume, so signal done without
199
+ // letting an external subscriber hold reception teardown open.
200
+ if (!_copyChanges.isClosed) unawaited(_copyChanges.close());
198
201
  await typedWatches.close();
199
- if (!_sync.isClosed) await _sync.close();
200
- if (!_custody.isClosed) await _custody.close();
201
- if (!_localWorkRejected.isClosed) await _localWorkRejected.close();
202
+ if (!_sync.isClosed) unawaited(_sync.close());
203
+ if (!_custody.isClosed) unawaited(_custody.close());
204
+ if (!_localWorkRejected.isClosed) unawaited(_localWorkRejected.close());
202
205
  if (!_receivingGroupConvergence.isClosed) {
203
- await _receivingGroupConvergence.close();
206
+ unawaited(_receivingGroupConvergence.close());
204
207
  }
205
- if (!_modelAdoption.isClosed) await _modelAdoption.close();
206
- if (!_preserved.isClosed) await _preserved.close();
207
- if (!_foreignPlacements.isClosed) await _foreignPlacements.close();
208
+ if (!_modelAdoption.isClosed) unawaited(_modelAdoption.close());
209
+ if (!_preserved.isClosed) unawaited(_preserved.close());
210
+ if (!_foreignPlacements.isClosed) unawaited(_foreignPlacements.close());
208
211
  await progress.close();
209
212
  await syncStatuses.close();
210
213
  }
@@ -260,8 +260,9 @@ final class ReceptionProgressDesk {
260
260
  }
261
261
 
262
262
  Future<void> close() async {
263
- if (!_spans.isClosed) await _spans.close();
264
- if (!_transfers.isClosed) await _transfers.close();
265
- if (!_kernel.isClosed) await _kernel.close();
263
+ // Observers may be paused while their widget tree is being replaced.
264
+ if (!_spans.isClosed) unawaited(_spans.close());
265
+ if (!_transfers.isClosed) unawaited(_transfers.close());
266
+ if (!_kernel.isClosed) unawaited(_kernel.close());
266
267
  }
267
268
  }
@@ -21835,6 +21835,15 @@ var ReplicaFollowController = class {
21835
21835
  }
21836
21836
  if (result.stillAhead || result.pushFailed || result.directOfferDeferred || result.reconnectRequired) {
21837
21837
  if (result.directOfferDeferred) {
21838
+ if (!result.reconnectRequired) {
21839
+ this.#desk.transition(result.workspace, {
21840
+ case: ReplicaFollowEventKind.Failed,
21841
+ nowUnixMillis,
21842
+ failure: "remote workspace opening; retrying",
21843
+ retryWithinMillis: 5e3
21844
+ });
21845
+ return delayUntilDue(this.#desk.get(result.workspace), nowUnixMillis);
21846
+ }
21838
21847
  this.#desk.transition(result.workspace, {
21839
21848
  case: ReplicaFollowEventKind.DependencyDeferred,
21840
21849
  nowUnixMillis,
@@ -23740,6 +23749,34 @@ function resolveCustodyDisagreement(kernel, mount, requestId, policy, verdict) {
23740
23749
  init_define_NOMOS_KERNEL_RELEASE();
23741
23750
  var CUSTODY_OFFER_MEDIA_TYPE = "application/vnd.nomos.custody-offer-batch+protobuf";
23742
23751
  var CUSTODY_OFFER_RESULT_MEDIA_TYPE = "application/vnd.nomos.custody-offer-result+protobuf";
23752
+ function settleCustodyOfferExchange(awaiting, response) {
23753
+ if (response.status === 202 && response.contentType?.includes("application/json")) {
23754
+ try {
23755
+ const pending = JSON.parse(new TextDecoder().decode(response.body));
23756
+ if (pending?.retryable === true && typeof pending.preparing === "string") {
23757
+ return { case: "maintenance", kind: pending.preparing };
23758
+ }
23759
+ } catch {
23760
+ }
23761
+ }
23762
+ if (!response.contentType?.includes(CUSTODY_OFFER_RESULT_MEDIA_TYPE)) {
23763
+ return {
23764
+ case: "unexpected",
23765
+ status: response.status,
23766
+ ...response.contentType === void 0 ? {} : { contentType: response.contentType }
23767
+ };
23768
+ }
23769
+ const verdict = decodeCustodyOffer(response.body, awaiting.requestId);
23770
+ const receipt = custodyOfferReceipt(awaiting.requestId, verdict);
23771
+ switch (verdict.case) {
23772
+ case "admission":
23773
+ return { case: "admission", value: verdict.value, receipt, status: response.status };
23774
+ case "failure":
23775
+ return { case: "failure", value: verdict.value, receipt, status: response.status };
23776
+ case "blocked":
23777
+ return { case: "blocked", value: verdict.value, receipt, status: response.status };
23778
+ }
23779
+ }
23743
23780
  function encodeInlineCustodyOffer(args) {
23744
23781
  return toBinary(CustodyOfferBatchRequestSchema, create(CustodyOfferBatchRequestSchema, {
23745
23782
  protocolVersion: 2,
@@ -23866,6 +23903,7 @@ function summarizeCustodyAdmission(admission) {
23866
23903
  var PLACEMENT_REF3 = "refs/nomos/placement";
23867
23904
  var LAW_PRODUCTS_MEDIA_TYPE = "application/vnd.nomos.export-law-products-response+protobuf";
23868
23905
  var WorkspaceSyncPhase = Object.freeze({
23906
+ Idle: "idle",
23869
23907
  Probe: "probe",
23870
23908
  Offer: "offer",
23871
23909
  Lease: "lease",
@@ -23875,7 +23913,6 @@ var WorkspaceSyncPhase = Object.freeze({
23875
23913
  Done: "done"
23876
23914
  });
23877
23915
  var trackedMain = "refs/remotes/nomos/main";
23878
- var NO_COMMON_BASE = "this replica holds local history with no frontier its upstream ever confirmed; convergence needs a common admitted base \u2014 drop the local copy to refetch it, or re-discover a provider holding this lineage";
23879
23916
  var deferredRemote = "refs/remotes/nomos/deferred";
23880
23917
  var deferredLocal = "refs/nomos/sync-deferred-local";
23881
23918
  var judgedThisLifetime = /* @__PURE__ */ new Map();
@@ -23899,7 +23936,6 @@ var WorkspaceSyncWork = class {
23899
23936
  #rediscover;
23900
23937
  /** The route the directory's answer yields, recomputed once that answer is observed. */
23901
23938
  #reroute;
23902
- #placementFetch;
23903
23939
  #discoveredFrontier;
23904
23940
  #observePlacement;
23905
23941
  /** Ask the directory for the provider's current placement frontier this round although a
@@ -23908,19 +23944,14 @@ var WorkspaceSyncWork = class {
23908
23944
  #directoryAnswered;
23909
23945
  #placementFetched;
23910
23946
  /** A round which converged without leasing, finishing once placement alone is fetched. */
23911
- #afterPlacement;
23912
23947
  #leased = false;
23913
- #lease;
23914
23948
  #command;
23915
23949
  #requestId;
23916
23950
  #started;
23917
- #phase = WorkspaceSyncPhase.Lease;
23951
+ #state = { phase: WorkspaceSyncPhase.Idle };
23918
23952
  #localHead;
23919
23953
  #offerHead;
23920
23954
  #previousHead;
23921
- #fetchStarted;
23922
- #offerStarted;
23923
- #offerRequestId;
23924
23955
  #pushMillis = 0;
23925
23956
  #pushBytes = 0n;
23926
23957
  #desiredHead;
@@ -24015,7 +24046,7 @@ var WorkspaceSyncWork = class {
24015
24046
  throw new Error(`replica '${this.#binding.replicaId}' has no custody remote`);
24016
24047
  }
24017
24048
  if (this.#rediscover || this.#recheckPlacement || !this.#route?.placementObserved && !this.#route?.placementFrontier && !this.#kernel.resolveRef(this.#mount, this.#requestId, PLACEMENT_REF3)) {
24018
- this.#phase = WorkspaceSyncPhase.Discover;
24049
+ this.#state = { phase: WorkspaceSyncPhase.Discover };
24019
24050
  return this.#remember({
24020
24051
  case: "pending",
24021
24052
  value: create(CorrespondencePendingSchema, {
@@ -24035,13 +24066,13 @@ var WorkspaceSyncWork = class {
24035
24066
  #openRound() {
24036
24067
  if (this.#receiveState || this.#replaceLocalHead !== void 0) return this.#leaseEffect();
24037
24068
  if (!this.#localHead) return this.#leaseEffect();
24038
- this.#offerRequestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:frontier`;
24069
+ const requestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:frontier`;
24039
24070
  return this.#beginOffer(encodeInlineCustodyOffer({
24040
24071
  session: this.#binding.replicaId,
24041
- requestId: this.#offerRequestId,
24072
+ requestId,
24042
24073
  observedHead: this.#localHead,
24043
24074
  intents: []
24044
- }), true);
24075
+ }), { case: "awaiting", requestId }, true);
24045
24076
  }
24046
24077
  #offerOrReconcile() {
24047
24078
  if (this.#command.admit && this.#localHead && this.#localHead !== this.#previousHead) {
@@ -24062,13 +24093,13 @@ var WorkspaceSyncWork = class {
24062
24093
  throw new Error(`kernel did not classify transport for intent '${unspecified.intentId}'`);
24063
24094
  }
24064
24095
  if (suffix.length > 0) {
24065
- this.#offerStarted = this.#clock.monotonicNanoseconds();
24066
- this.#offerRequestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}`;
24096
+ const requestId2 = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}`;
24097
+ const startedAt = this.#clock.monotonicNanoseconds();
24067
24098
  this.#offerAfterConverge = true;
24068
24099
  this.#rebaseFrom = this.#previousHead;
24069
24100
  return this.#beginOffer(encodeInlineCustodyOffer({
24070
24101
  session: this.#binding.replicaId,
24071
- requestId: this.#offerRequestId,
24102
+ requestId: requestId2,
24072
24103
  ...this.#previousHead === void 0 ? {} : { baseHead: this.#previousHead },
24073
24104
  observedHead: this.#localHead,
24074
24105
  intents: suffix,
@@ -24079,51 +24110,26 @@ var WorkspaceSyncWork = class {
24079
24110
  this.#previousHead
24080
24111
  ) : void 0,
24081
24112
  fastForwardOnly: true
24082
- }));
24113
+ }), { case: "awaiting", requestId: requestId2, startedAt });
24083
24114
  }
24084
24115
  }
24085
- this.#offerRequestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:reconcile`;
24116
+ const requestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:reconcile`;
24086
24117
  return this.#beginOffer(encodeInlineCustodyOffer({
24087
24118
  session: this.#binding.replicaId,
24088
- requestId: this.#offerRequestId,
24119
+ requestId,
24089
24120
  baseHead: this.#previousHead,
24090
24121
  observedHead: this.#localHead,
24091
24122
  intents: []
24092
- }));
24123
+ }), { case: "awaiting", requestId });
24093
24124
  }
24094
24125
  /**
24095
- * NO COMMON BASE — a round that cannot converge, and must not pay for history to find out.
24096
- *
24097
- * This replica holds a non-empty ledger but has never recorded a frontier its upstream
24098
- * confirmed (`refs/remotes/nomos/main` is absent). The kernel refuses convergence in that
24099
- * state ("a non-empty live ledger requires a common admitted base"), so every step after the
24100
- * O(1) frontier probe is wasted work — and it is not cheap waste: the fetch negotiates with
24101
- * no `have` (a full-history pack), and the dependency planner decodes EVERY intent from
24102
- * genesis and inspects every change, once per round. That is O(history) per follow round,
24103
- * repeated at every retry, for a round that can only end in a refusal.
24104
- *
24105
- * The one base this round can establish costs nothing and is established before this is
24106
- * asked: an upstream holding exactly this replica's head (the probe's answer) converges it
24107
- * and records the tracking frontier. Anything else needs the local copy to be dropped and
24108
- * refetched (an application decision, which preserves the copy), or a re-discovery that
24109
- * names a provider holding this lineage. Both are reported, at O(1), as this failure.
24110
- */
24111
- /**
24112
- * Establish this round's base when none is tracked, without walking any history it can avoid.
24113
- *
24114
- * The upstream just named its own frontier. If that commit is in THIS replica's lineage, it is
24115
- * a common admitted base by definition — both sides hold it — and adopting it turns the round
24116
- * into an ordinary fast-forward: the offer carries only the suffix above it, the fetch can name
24117
- * it as a truthful `have`, and convergence has the base the kernel requires. One commit walk
24118
- * (no change decoding, no pack, no network) decides it.
24119
- *
24120
- * If it is not in this replica's lineage, no base exists to establish and the round is refused
24121
- * (#noCommonBase) before it leases, fetches or plans anything.
24122
- *
24123
- * A kernel without the commit walk cannot be asked, so it keeps the previous behaviour.
24126
+ * Establish a cheap base when the provider's current tip is in our local chain.
24127
+ * A negative answer means the base is unknown: both sides may have advanced from
24128
+ * an earlier shared commit. Acquire the candidate and let the kernel discover
24129
+ * and judge that history. A tracking bookmark is not an admission prerequisite.
24124
24130
  */
24125
24131
  #establishBase(upstreamHead) {
24126
- if (!this.#noCommonBase() || !this.#kernel.historySummary) return true;
24132
+ if (!this.#missingTrackedBase() || !this.#kernel.historySummary) return true;
24127
24133
  const summary = this.#kernel.historySummary(this.#mount, this.#requestId, upstreamHead, this.#localHead);
24128
24134
  if (!summary.reachedBase) return false;
24129
24135
  this.#previousHead = upstreamHead;
@@ -24138,26 +24144,9 @@ var WorkspaceSyncWork = class {
24138
24144
  get establishedBase() {
24139
24145
  return this.#establishedBase;
24140
24146
  }
24141
- #noCommonBase() {
24147
+ #missingTrackedBase() {
24142
24148
  return this.#previousHead === void 0 && this.#localHead !== void 0 && this.#replaceLocalHead === void 0;
24143
24149
  }
24144
- #noCommonBaseResult() {
24145
- return this.#finish(create(SyncWorkspaceResponseSchema, {
24146
- workspace: this.#mount.workspace,
24147
- ...this.#localHead === void 0 ? {} : { currentHead: this.#localHead },
24148
- stillAhead: true,
24149
- ...this.#offerReceipt === void 0 ? {} : { offerReceipt: this.#offerReceipt },
24150
- pushError: create(RuntimeTransportFailureSchema, {
24151
- endpoint: this.#offerEndpoint(),
24152
- status: 409,
24153
- refused: true,
24154
- message: NO_COMMON_BASE
24155
- }),
24156
- timings: create(SyncPhaseTimingsSchema, {
24157
- convergeMillis: elapsedMillis(this.#clock, this.#started)
24158
- })
24159
- }));
24160
- }
24161
24150
  #holdsLocalWork() {
24162
24151
  return this.#command.admit && !!this.#localHead && this.#localHead !== this.#previousHead;
24163
24152
  }
@@ -24165,18 +24154,18 @@ var WorkspaceSyncWork = class {
24165
24154
  #pullBeforeOffer() {
24166
24155
  this.#offerAfterConverge = true;
24167
24156
  this.#rebaseFrom = this.#previousHead;
24168
- this.#offerRequestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:pull-${this.#rebases}`;
24157
+ const requestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:pull-${this.#rebases}`;
24169
24158
  return this.#beginOffer(encodeInlineCustodyOffer({
24170
24159
  session: this.#binding.replicaId,
24171
- requestId: this.#offerRequestId,
24160
+ requestId,
24172
24161
  ...this.#previousHead === void 0 ? {} : { baseHead: this.#previousHead },
24173
24162
  observedHead: this.#localHead,
24174
24163
  intents: []
24175
- }));
24164
+ }), { case: "awaiting", requestId });
24176
24165
  }
24177
- #leaseEffect() {
24166
+ #leaseEffect(afterPlacement) {
24178
24167
  const token = this.#authToken();
24179
- this.#phase = WorkspaceSyncPhase.Lease;
24168
+ this.#state = { phase: WorkspaceSyncPhase.Lease, ...afterPlacement ? { afterPlacement } : {} };
24180
24169
  return this.#remember({
24181
24170
  case: "pending",
24182
24171
  value: create(CorrespondencePendingSchema, {
@@ -24205,35 +24194,38 @@ var WorkspaceSyncWork = class {
24205
24194
  `workspace sync '${this.workId}' expected effect '${expected}', received '${receipt.effectId}'`
24206
24195
  );
24207
24196
  }
24197
+ const state = this.#state;
24208
24198
  const previousReceipt = this.#lastReceiptEffectId;
24209
24199
  this.#lastReceiptEffectId = receipt.effectId;
24210
24200
  try {
24211
24201
  if (receipt.outcome.case === "failure") {
24212
- if (this.#phase === WorkspaceSyncPhase.Discover) return this.#openRound();
24202
+ if (state.phase === WorkspaceSyncPhase.Discover) return this.#openRound();
24213
24203
  return this.#transportFailure(
24214
24204
  receipt.outcome.value.message,
24215
24205
  receipt.outcome.value.reason === PeerEffectFailureReason.OFFLINE
24216
24206
  );
24217
24207
  }
24218
- if (this.#phase === WorkspaceSyncPhase.Offer || this.#phase === WorkspaceSyncPhase.Probe) return this.#acceptOffer(receipt);
24219
- if (this.#phase === WorkspaceSyncPhase.Lease) return this.#acceptLease(receipt);
24220
- if (this.#phase === WorkspaceSyncPhase.Discover) return this.#acceptDiscovery(receipt);
24221
- if (this.#phase === WorkspaceSyncPhase.Placement) {
24222
- if (!this.#placementFetch || !this.#lease) throw new Error("placement transfer lost its lease");
24223
- this.#placementFetch.accept(receipt);
24208
+ if (state.phase === WorkspaceSyncPhase.Offer || state.phase === WorkspaceSyncPhase.Probe) {
24209
+ return this.#acceptOffer(receipt, state);
24210
+ }
24211
+ if (state.phase === WorkspaceSyncPhase.Lease) return this.#acceptLease(receipt, state);
24212
+ if (state.phase === WorkspaceSyncPhase.Discover) return this.#acceptDiscovery(receipt);
24213
+ if (state.phase === WorkspaceSyncPhase.Placement) {
24214
+ state.transfer.accept(receipt);
24224
24215
  this.#placementFetched?.();
24225
- if (this.#afterPlacement) return this.#finish(this.#afterPlacement);
24226
- return this.#beginMainFetch(this.#lease);
24216
+ if (state.afterPlacement) return this.#finish(state.afterPlacement);
24217
+ return this.#beginMainFetch(state.lease);
24227
24218
  }
24228
- if (this.#phase === WorkspaceSyncPhase.Fetch) return this.#acceptFetch(receipt);
24219
+ if (state.phase === WorkspaceSyncPhase.Fetch) return this.#acceptFetch(receipt, state);
24229
24220
  throw new Error(`workspace sync '${this.workId}' is already complete`);
24230
24221
  } catch (error) {
24231
24222
  this.#lastReceiptEffectId = previousReceipt;
24232
24223
  throw error;
24233
24224
  }
24234
24225
  }
24235
- #beginOffer(body, probe = false) {
24236
- this.#phase = probe ? WorkspaceSyncPhase.Probe : WorkspaceSyncPhase.Offer;
24226
+ #beginOffer(body, exchange, probe = false) {
24227
+ const phase = probe ? WorkspaceSyncPhase.Probe : WorkspaceSyncPhase.Offer;
24228
+ this.#state = { phase, exchange };
24237
24229
  const token = this.#authToken();
24238
24230
  this.#pushBytes += BigInt(body.byteLength);
24239
24231
  return this.#remember({
@@ -24243,7 +24235,7 @@ var WorkspaceSyncWork = class {
24243
24235
  effect: {
24244
24236
  case: "http",
24245
24237
  value: create(PeerHttpEffectSchema, {
24246
- effectId: `${this.workId}:${this.#phase}`,
24238
+ effectId: `${this.workId}:${phase}`,
24247
24239
  method: PeerHttpMethod.POST,
24248
24240
  url: this.#offerEndpoint(),
24249
24241
  contentType: CUSTODY_OFFER_MEDIA_TYPE,
@@ -24260,28 +24252,30 @@ var WorkspaceSyncWork = class {
24260
24252
  })
24261
24253
  });
24262
24254
  }
24263
- #acceptOffer(receipt) {
24264
- this.#pushMillis = this.#offerStarted === void 0 ? 0 : elapsedMillis(this.#clock, this.#offerStarted);
24255
+ #acceptOffer(receipt, state) {
24256
+ const exchange = state.exchange;
24257
+ this.#pushMillis = exchange.startedAt === void 0 ? 0 : elapsedMillis(this.#clock, exchange.startedAt);
24265
24258
  if (receipt.outcome.case !== "response") {
24266
24259
  throw new Error("platform returned the wrong receipt for a custody offer");
24267
24260
  }
24268
- const response = receipt.outcome.value;
24269
- if (!response.contentType?.includes(CUSTODY_OFFER_RESULT_MEDIA_TYPE)) {
24261
+ const outcome = settleCustodyOfferExchange(exchange, receipt.outcome.value);
24262
+ if (outcome.case === "maintenance") {
24263
+ return this.#finishOfferOutcome(true, false);
24264
+ }
24265
+ if (outcome.case === "unexpected") {
24270
24266
  this.#pushError = create(RuntimeTransportFailureSchema, {
24271
24267
  endpoint: this.#offerEndpoint(),
24272
- status: response.status,
24268
+ status: outcome.status,
24273
24269
  refused: false,
24274
- message: `Sync received an unexpected server response (HTTP ${response.status}; ${response.contentType || "no content type"}). No custody receipt was returned.`
24270
+ message: `Sync received an unexpected server response (HTTP ${outcome.status}; ${outcome.contentType || "no content type"}). No custody receipt was returned.`
24275
24271
  });
24276
24272
  return this.#finishOfferOutcome(false);
24277
24273
  }
24278
- if (!this.#offerRequestId) throw new Error("custody offer has no request identity");
24279
- const outcome = decodeCustodyOffer(response.body, this.#offerRequestId);
24280
- this.#offerReceipt = custodyOfferReceipt(this.#offerRequestId, outcome);
24274
+ this.#offerReceipt = outcome.receipt;
24281
24275
  if (outcome.case === "failure") {
24282
24276
  this.#pushError = create(RuntimeTransportFailureSchema, {
24283
24277
  endpoint: this.#offerEndpoint(),
24284
- status: response.status,
24278
+ status: outcome.status,
24285
24279
  refused: false,
24286
24280
  message: `${outcome.value.code}: ${outcome.value.message}`
24287
24281
  });
@@ -24290,12 +24284,12 @@ var WorkspaceSyncWork = class {
24290
24284
  if (outcome.case === "blocked") {
24291
24285
  return this.#finishOfferOutcome(true);
24292
24286
  }
24293
- if (response.status < 200 || response.status >= 300) {
24287
+ if (outcome.status < 200 || outcome.status >= 300) {
24294
24288
  this.#pushError = create(RuntimeTransportFailureSchema, {
24295
24289
  endpoint: this.#offerEndpoint(),
24296
- status: response.status,
24297
- refused: response.status === 422,
24298
- message: `custody offer returned an admission verdict with HTTP ${response.status}`
24290
+ status: outcome.status,
24291
+ refused: outcome.status === 422,
24292
+ message: `custody offer returned an admission verdict with HTTP ${outcome.status}`
24299
24293
  });
24300
24294
  return this.#finishOfferOutcome(false);
24301
24295
  }
@@ -24303,8 +24297,7 @@ var WorkspaceSyncWork = class {
24303
24297
  this.#desiredHead = admission.main || void 0;
24304
24298
  if (!this.#desiredHead) return this.#transportFailure("resident peer returned no admitted frontier", false);
24305
24299
  this.#candidateDurable = admission.sealedMain === this.#desiredHead;
24306
- if (this.#phase === WorkspaceSyncPhase.Probe && this.#desiredHead !== this.#localHead) {
24307
- if (!this.#establishBase(this.#desiredHead)) return this.#noCommonBaseResult();
24300
+ if (state.phase === WorkspaceSyncPhase.Probe && this.#desiredHead !== this.#localHead) {
24308
24301
  if (this.#alreadyJudged(this.#desiredHead)) {
24309
24302
  return this.#finish(create(SyncWorkspaceResponseSchema, {
24310
24303
  workspace: this.#mount.workspace,
@@ -24316,6 +24309,22 @@ var WorkspaceSyncWork = class {
24316
24309
  offerReceipt: this.#offerReceipt
24317
24310
  }));
24318
24311
  }
24312
+ if (!this.#establishBase(this.#desiredHead)) {
24313
+ this.#offerAfterConverge = this.#command.admit;
24314
+ this.#rebaseFrom = void 0;
24315
+ return this.#leaseEffect();
24316
+ }
24317
+ if (this.#establishedBase && !this.#candidateDurable && this.#kernel.resolveRef(this.#mount, this.#requestId, trackedMain) === void 0) {
24318
+ return this.#finish(create(SyncWorkspaceResponseSchema, {
24319
+ workspace: this.#mount.workspace,
24320
+ currentHead: this.#localHead,
24321
+ previousHead: this.#previousHead,
24322
+ stillAhead: true,
24323
+ directOfferDeferred: true,
24324
+ reconnectRequired: false,
24325
+ offerReceipt: this.#offerReceipt
24326
+ }));
24327
+ }
24319
24328
  if (this.#probeOnly) {
24320
24329
  this.#needsReconciliation = true;
24321
24330
  return this.#finish(create(SyncWorkspaceResponseSchema, {
@@ -24361,11 +24370,11 @@ var WorkspaceSyncWork = class {
24361
24370
  return this.#converge(this.#desiredHead, admission.resultPack.byteLength, 0);
24362
24371
  }
24363
24372
  if (this.#desiredHead === this.#previousHead) return this.#converge(this.#desiredHead, 0, 0);
24364
- if (!this.#establishBase(this.#desiredHead)) return this.#noCommonBaseResult();
24373
+ this.#establishBase(this.#desiredHead);
24365
24374
  if (admission.recoveryRequired !== false) return this.#leaseEffect();
24366
24375
  return this.#transportFailure("resident peer supplied neither the missing delta nor a history gap", false);
24367
24376
  }
24368
- #acceptLease(receipt) {
24377
+ #acceptLease(receipt, state) {
24369
24378
  if (receipt.outcome.case !== "response") {
24370
24379
  throw new Error("platform returned the wrong receipt for a Git read lease");
24371
24380
  }
@@ -24381,21 +24390,25 @@ var WorkspaceSyncWork = class {
24381
24390
  this.#leased = true;
24382
24391
  const frontier = this.#placementFrontier();
24383
24392
  if (frontier) {
24384
- this.#placementFetch = new PlacementFetch(
24393
+ const transfer = new PlacementFetch(
24385
24394
  this.#kernel,
24386
24395
  this.#mount,
24387
24396
  this.#requestId,
24388
24397
  this.workId,
24389
24398
  frontier
24390
24399
  );
24391
- const pending = this.#placementFetch.start(lease);
24400
+ const pending = transfer.start(lease);
24392
24401
  if (pending) {
24393
- this.#phase = WorkspaceSyncPhase.Placement;
24394
- this.#lease = lease;
24402
+ this.#state = {
24403
+ phase: WorkspaceSyncPhase.Placement,
24404
+ transfer,
24405
+ lease,
24406
+ ...state.afterPlacement ? { afterPlacement: state.afterPlacement } : {}
24407
+ };
24395
24408
  return this.#remember({ case: "pending", value: pending });
24396
24409
  }
24397
24410
  }
24398
- if (this.#afterPlacement) return this.#finish(this.#afterPlacement);
24411
+ if (state.afterPlacement) return this.#finish(state.afterPlacement);
24399
24412
  return this.#beginMainFetch(lease);
24400
24413
  }
24401
24414
  /** The provider's placement frontier as this round knows it: what the directory named in this
@@ -24415,8 +24428,7 @@ var WorkspaceSyncWork = class {
24415
24428
  if (!new PlacementFetch(this.#kernel, this.#mount, this.#requestId, this.workId, frontier).wanted()) {
24416
24429
  return void 0;
24417
24430
  }
24418
- this.#afterPlacement = presented;
24419
- return this.#leaseEffect();
24431
+ return this.#leaseEffect(presented);
24420
24432
  }
24421
24433
  #acceptDiscovery(receipt) {
24422
24434
  this.#directoryAnswered?.();
@@ -24440,9 +24452,11 @@ var WorkspaceSyncWork = class {
24440
24452
  return this.#replaceLocalHead !== void 0 || this.#localHead === void 0 && this.#previousHead === void 0;
24441
24453
  }
24442
24454
  #beginMainFetch(lease) {
24443
- this.#phase = WorkspaceSyncPhase.Fetch;
24444
24455
  const lawProducts = this.#lawProductsAlongside();
24445
- this.#fetchStarted = this.#clock.monotonicNanoseconds();
24456
+ this.#state = {
24457
+ phase: WorkspaceSyncPhase.Fetch,
24458
+ startedAt: this.#clock.monotonicNanoseconds()
24459
+ };
24446
24460
  const haves = this.#replaceLocalHead !== void 0 ? this.#repairMissingCommit ? [] : [this.#kernel.resolveRef(this.#mount, this.#requestId, deferredRemote)].filter(
24447
24461
  (head) => !!head
24448
24462
  ) : [...new Set([
@@ -24473,7 +24487,7 @@ var WorkspaceSyncWork = class {
24473
24487
  })
24474
24488
  });
24475
24489
  }
24476
- #acceptFetch(receipt) {
24490
+ #acceptFetch(receipt, state) {
24477
24491
  if (receipt.outcome.case !== "gitFetch") {
24478
24492
  throw new Error("platform returned the wrong receipt for a Git object fetch");
24479
24493
  }
@@ -24483,7 +24497,7 @@ var WorkspaceSyncWork = class {
24483
24497
  if (fetched.pack.byteLength > 0) {
24484
24498
  this.#kernel.applyPack(this.#mount, this.#requestId, fetched.pack, fetched.head, fetched.shallow);
24485
24499
  }
24486
- const prefetchMillis = this.#fetchStarted === void 0 ? 0 : elapsedMillis(this.#clock, this.#fetchStarted);
24500
+ const prefetchMillis = elapsedMillis(this.#clock, state.startedAt);
24487
24501
  const candidateHead = this.#receiveState ? this.#kernel.resolveStatePosition?.(this.#mount, this.#requestId, fetched.head, this.#receiveState) : fetched.head;
24488
24502
  if (!candidateHead) {
24489
24503
  return this.#finish(create(SyncWorkspaceResponseSchema, {
@@ -24579,7 +24593,7 @@ var WorkspaceSyncWork = class {
24579
24593
  }
24580
24594
  }
24581
24595
  #converge(candidateHead, receivedBytes, prefetchMillis) {
24582
- if (!this.#establishBase(candidateHead)) return this.#noCommonBaseResult();
24596
+ this.#establishBase(candidateHead);
24583
24597
  this.#beforeConverge?.(candidateHead, this.#previousHead);
24584
24598
  const convergeStarted = this.#clock.monotonicNanoseconds();
24585
24599
  let judged;
@@ -24834,11 +24848,11 @@ var WorkspaceSyncWork = class {
24834
24848
  pushBytes: this.#pushBytes,
24835
24849
  pullBytes: BigInt(receivedBytes),
24836
24850
  serverMillis: this.#admissionSummary?.serverMillis ?? 0,
24837
- offerInPush: this.#offerRequestId !== void 0
24851
+ offerInPush: this.#pushBytes > 0n
24838
24852
  })
24839
24853
  });
24840
24854
  }
24841
- #finishOfferOutcome(blocked) {
24855
+ #finishOfferOutcome(blocked, reconnectRequired = true) {
24842
24856
  return this.#finish(create(SyncWorkspaceResponseSchema, {
24843
24857
  workspace: this.#mount.workspace,
24844
24858
  previousHead: this.#previousHead,
@@ -24846,7 +24860,7 @@ var WorkspaceSyncWork = class {
24846
24860
  admitted: BigInt(this.#admissionSummary?.admittedIntentIds.length ?? 0),
24847
24861
  refused: BigInt((this.#admissionSummary?.refusedMessages.length ?? 0) + this.#parkedLocal),
24848
24862
  deferred: BigInt(this.#admissionSummary ? this.#admissionSummary.blockedMessages.length + this.#admissionSummary.deadLetteredMessages.length + this.#admissionSummary.quarantinedMessages.length + this.#admissionSummary.deferredMessages.length : blocked ? 1 : 0),
24849
- reconnectRequired: true,
24863
+ reconnectRequired,
24850
24864
  stillAhead: this.#localHead !== this.#previousHead,
24851
24865
  directOfferDeferred: blocked,
24852
24866
  offerReceipt: this.#offerReceipt,
@@ -24860,15 +24874,16 @@ var WorkspaceSyncWork = class {
24860
24874
  }));
24861
24875
  }
24862
24876
  #transportFailure(message, offline, status = -1) {
24863
- const pushFailed = this.#phase === WorkspaceSyncPhase.Offer;
24877
+ const state = this.#state;
24878
+ const pushFailed = state.phase === WorkspaceSyncPhase.Offer;
24864
24879
  const failedEndpoint = this.#offerEndpoint();
24865
24880
  const failure = create(RuntimeTransportFailureSchema, {
24866
24881
  endpoint: pushFailed ? failedEndpoint : this.#binding.cloud ?? "",
24867
24882
  status,
24868
24883
  message
24869
24884
  });
24870
- if (pushFailed) {
24871
- this.#pushMillis = this.#offerStarted === void 0 ? 0 : elapsedMillis(this.#clock, this.#offerStarted);
24885
+ if (state.phase === WorkspaceSyncPhase.Offer) {
24886
+ this.#pushMillis = state.exchange.startedAt === void 0 ? 0 : elapsedMillis(this.#clock, state.exchange.startedAt);
24872
24887
  this.#pushError = failure;
24873
24888
  }
24874
24889
  return this.#finish(create(SyncWorkspaceResponseSchema, {
@@ -24885,7 +24900,7 @@ var WorkspaceSyncWork = class {
24885
24900
  pushMillis: this.#pushMillis,
24886
24901
  pushBytes: this.#pushBytes,
24887
24902
  prefetchMillis: elapsedMillis(this.#clock, this.#started),
24888
- offerInPush: this.#offerRequestId !== void 0
24903
+ offerInPush: this.#pushBytes > 0n
24889
24904
  })
24890
24905
  }));
24891
24906
  }
@@ -24895,7 +24910,7 @@ var WorkspaceSyncWork = class {
24895
24910
  return step;
24896
24911
  }
24897
24912
  #finish(value) {
24898
- this.#phase = WorkspaceSyncPhase.Done;
24913
+ this.#state = { phase: WorkspaceSyncPhase.Done };
24899
24914
  this.#current = void 0;
24900
24915
  return this.#remember({ case: "synced", value });
24901
24916
  }
@@ -27033,13 +27048,21 @@ var CorrespondenceDesk = class {
27033
27048
  }
27034
27049
  #observeSyncResult(requestId, workId2, result) {
27035
27050
  const timings = result.timings;
27051
+ const receipt = result.offerReceipt?.outcome;
27052
+ const admission = receipt?.case === "admission" ? receipt.value : void 0;
27036
27053
  this.#events.instant("peer.follow.sync-result", {
27037
27054
  request: create(RuntimeRequestSchema, { protocolVersion: 2, requestId }),
27038
27055
  workspace: result.workspace
27039
27056
  }, {
27040
27057
  "sync.workId": workId2,
27041
27058
  ...result.currentHead ? { "sync.head": result.currentHead } : {},
27059
+ ...result.previousHead ? { "sync.previousHead": result.previousHead } : {},
27060
+ ...admission?.main ? { "sync.providerHead": admission.main } : {},
27061
+ ...admission?.sealedMain ? { "sync.sealedHead": admission.sealedMain } : {},
27042
27062
  "sync.stillAhead": result.stillAhead,
27063
+ "sync.reconnectRequired": result.reconnectRequired,
27064
+ "sync.refused": result.refused,
27065
+ "sync.deferred": result.deferred,
27043
27066
  "sync.directOfferDeferred": result.directOfferDeferred,
27044
27067
  "sync.offline": result.offline,
27045
27068
  ...timings ? {
@@ -543,6 +543,8 @@ final class NmsSyncStatusProjection {
543
543
  for (final stream in streams) {
544
544
  if (!stream.isClosed) unawaited(stream.close());
545
545
  }
546
- if (!_application.isClosed) await _application.close();
546
+ // A paused UI subscriber may not process the done event until it resumes.
547
+ // Closing this broadcast stream must not hold reception teardown open.
548
+ if (!_application.isClosed) unawaited(_application.close());
547
549
  }
548
550
  }
package/dart/pubspec.yaml CHANGED
@@ -7,7 +7,7 @@ repository: https://github.com/Captain-App/nomos2
7
7
  description: >-
8
8
  Nomos 2 Dart client — generated domain values, intent payloads and application
9
9
  lifecycle types over the typed Nomos kernel protocol and USDA custody model.
10
- version: 0.66.5
10
+ version: 0.66.7
11
11
 
12
12
  environment:
13
13
  sdk: ^3.5.0
@@ -0,0 +1,16 @@
1
+ import 'package:nomos_client/src/reception/event_desk.dart';
2
+ import 'package:test/test.dart';
3
+
4
+ void main() {
5
+ test('reception closes while presentation observers are paused', () async {
6
+ final desk = ReceptionEventDesk();
7
+ final copies = desk.copyChanges.listen((_) {})..pause();
8
+ final application = desk.syncStatuses.applicationStream.listen((_) {})
9
+ ..pause();
10
+
11
+ await desk.close().timeout(const Duration(seconds: 1));
12
+
13
+ await copies.cancel();
14
+ await application.cancel();
15
+ });
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.110.0",
3
+ "version": "0.111.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
@@ -72,7 +72,7 @@
72
72
  "test": "vitest run"
73
73
  },
74
74
  "dependencies": {
75
- "@githolon/runtime": "^0.110.0",
75
+ "@githolon/runtime": "^0.111.0",
76
76
  "@bufbuild/protobuf": "^2.13.0",
77
77
  "@bufbuild/protoc-gen-es": "^2.13.0",
78
78
  "esbuild": "0.24.2",
@@ -1237,7 +1237,7 @@ export function emitDartCallProtoEncoder(
1237
1237
  for (const field of fields) {
1238
1238
  const fieldAccess = access.get(field.name);
1239
1239
  if (fieldAccess?.omit === true || fieldAccess?.present === "false") continue;
1240
- const raw = fieldAccess?.slot === true ? `${fieldAccess.value}.value` : fieldAccess?.value ?? `payload.${dartMemberName(field.name)}`;
1240
+ const raw = fieldAccess?.slot === true ? `${fieldAccess.value}.valueOrNull` : fieldAccess?.value ?? `payload.${dartMemberName(field.name)}`;
1241
1241
  const fieldSemantic = typeNode(spec.nodes, field.value.id).expression.semantic;
1242
1242
  const fieldValueIsCanonical = fieldSemantic?.kind === "id-reference" ||
1243
1243
  fieldSemantic?.kind === "address-reference" ||
@@ -1252,7 +1252,9 @@ export function emitDartCallProtoEncoder(
1252
1252
  emitValue(body, raw, field.value.id, field.name, (expression) => {
1253
1253
  body.push(`${member} = ${expression};`);
1254
1254
  }, fieldValueIsCanonical);
1255
- if (field.required) lines.push(...body.map((line) => ` ${line}`));
1255
+ if (fieldAccess?.slot === true) {
1256
+ lines.push(` if (!${fieldAccess.value}.isOmitted) {`, ...body.map((line) => ` ${line}`), ` }`);
1257
+ } else if (field.required) lines.push(...body.map((line) => ` ${line}`));
1256
1258
  else lines.push(` if (${fieldAccess?.present ?? `${raw} != null`}) {`, ...body.map((line) => ` ${line}`), ` }`);
1257
1259
  }
1258
1260
  lines.push(
@@ -5479,6 +5479,10 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
5479
5479
  * Every tuple and subject is nominally typed from the same DSL graph; there is no JSON leaf or map-shaped
5480
5480
  * boundary. INFORMATIONAL only (the one wasm gate is still authoritative). */
5481
5481
  export function dartRoleHelpers(mod: DomainModule): string {
5482
+ // Some packages have role policy declarations but do not project the framework
5483
+ // RelationTuple aggregate into this domain. In that case there is no typed
5484
+ // read model for a client-side hint to consume.
5485
+ if (!mod.aggregates.some((aggregate) => aggregate.id === "RelationTuple")) return "";
5482
5486
  const editRels = new Map<string, Set<string>>();
5483
5487
  const viewRels = new Map<string, Set<string>>();
5484
5488
  for (const r of mod.roles ?? []) {
@@ -2544,9 +2544,10 @@ const NOMOS_TYPES_DEP = "^0.11.2";
2544
2544
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
2545
2545
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
2546
2546
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
2547
- const NOMOS_CLIENT_DEP = "^0.66.5";
2548
- // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
2549
- // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
2547
+ const NOMOS_CLIENT_DEP = "^0.66.7";
2548
+ const NOMOS_FLUTTER_DEP = "^0.65.9";
2549
+ // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in the Dart and Flutter
2550
+ // package pubspecs — a release without this bump fails the suite, so tenants never
2550
2551
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
2551
2552
 
2552
2553
  const isPubPackageName = (s: string): boolean => /^[a-z][a-z0-9_]*$/.test(s);
@@ -3166,10 +3167,13 @@ function reconcilePubspec(
3166
3167
  index < dependenciesEnd &&
3167
3168
  line.startsWith(prefix)
3168
3169
  );
3169
- const replacement = ` ${name}: ${constraint}`;
3170
+ const replacement =
3171
+ name === "flutter" && constraint === "sdk:flutter"
3172
+ ? [" flutter:", " sdk: flutter"]
3173
+ : [` ${name}: ${constraint}`];
3170
3174
  if (existing < 0) {
3171
- lines.splice(dependenciesEnd, 0, replacement);
3172
- dependenciesEnd += 1;
3175
+ lines.splice(dependenciesEnd, 0, ...replacement);
3176
+ dependenciesEnd += replacement.length;
3173
3177
  continue;
3174
3178
  }
3175
3179
  let nestedEnd = existing + 1;
@@ -3178,8 +3182,8 @@ function reconcilePubspec(
3178
3182
  /^(?: {4,}|\t)/.test(lines[nestedEnd]!)
3179
3183
  )
3180
3184
  nestedEnd += 1;
3181
- lines.splice(existing, nestedEnd - existing, replacement);
3182
- dependenciesEnd -= nestedEnd - existing - 1;
3185
+ lines.splice(existing, nestedEnd - existing, ...replacement);
3186
+ dependenciesEnd += replacement.length - (nestedEnd - existing);
3183
3187
  }
3184
3188
 
3185
3189
  const next = `${lines.join("\n")}${trailingNewline ? "\n" : ""}`;
@@ -4045,6 +4049,7 @@ function emitDartPackages(
4045
4049
  ` flutter:`,
4046
4050
  ` sdk: flutter`,
4047
4051
  ` nomos_client: ${NOMOS_CLIENT_DEP}`,
4052
+ ` nomos_flutter: ${NOMOS_FLUTTER_DEP}`,
4048
4053
  ` ${packages.client}:`,
4049
4054
  ` path: ../${packages.client} # co-emitted sibling; replace with a hosted constraint when publishing`,
4050
4055
  `dev_dependencies:`,
@@ -4054,7 +4059,11 @@ function emitDartPackages(
4054
4059
  ` - lib/${genRel}/${embeddedLawName}`,
4055
4060
  ``,
4056
4061
  ].join("\n"),
4057
- { nomos_client: NOMOS_CLIENT_DEP }
4062
+ {
4063
+ flutter: "sdk:flutter",
4064
+ nomos_client: NOMOS_CLIENT_DEP,
4065
+ nomos_flutter: NOMOS_FLUTTER_DEP,
4066
+ }
4058
4067
  );
4059
4068
  reconcileGeneratedLawAsset(
4060
4069
  pkgDir(packages.test),
@@ -8918,12 +8927,14 @@ async function main(): Promise<void> {
8918
8927
  const client = path.resolve(selfDir, "..", "dart");
8919
8928
  const types = path.resolve(selfDir, "..", "dart_types");
8920
8929
  const peerHost = path.resolve(selfDir, "..", "..", "nomos_peer_host");
8930
+ const flutter = path.resolve(selfDir, "..", "..", "nomos_flutter");
8921
8931
  if (
8922
8932
  existsSync(path.join(client, "pubspec.yaml")) &&
8923
8933
  existsSync(path.join(types, "pubspec.yaml")) &&
8924
- existsSync(path.join(peerHost, "pubspec.yaml"))
8934
+ existsSync(path.join(peerHost, "pubspec.yaml")) &&
8935
+ existsSync(path.join(flutter, "pubspec.yaml"))
8925
8936
  ) {
8926
- return { client, types, peerHost };
8937
+ return { client, types, peerHost, flutter };
8927
8938
  }
8928
8939
  return undefined;
8929
8940
  })();
@@ -8961,6 +8972,7 @@ async function main(): Promise<void> {
8961
8972
  const lines = ["dependency_overrides:"];
8962
8973
  const usesClient = /(^|\n)\s+nomos_client\s*:/.test(spec);
8963
8974
  const usesTypes = /(^|\n)\s+nomos_types\s*:/.test(spec);
8975
+ const usesFlutter = /(^|\n)\s+nomos_flutter\s*:/.test(spec);
8964
8976
  if (usesClient)
8965
8977
  lines.push(` nomos_client:`, ` path: ${localDart.client}`);
8966
8978
  // A generated CLIENT or TEST package may only mention nomos_client directly, but that local
@@ -8978,6 +8990,8 @@ async function main(): Promise<void> {
8978
8990
  ` nomos_peer_host:`,
8979
8991
  ` path: ${localDart.peerHost}`
8980
8992
  );
8993
+ if (usesFlutter)
8994
+ lines.push(` nomos_flutter:`, ` path: ${localDart.flutter}`);
8981
8995
  if (lines.length > 1) {
8982
8996
  writeTextIfChanged(overrideFile, lines.join("\n") + "\n");
8983
8997
  }
@@ -9039,7 +9053,10 @@ async function main(): Promise<void> {
9039
9053
  for (const pkg of pkgDirs) {
9040
9054
  if (dartPackageResolutionCurrent(pkg)) continue;
9041
9055
  try {
9042
- execFileSync("dart", ["pub", "get"], {
9056
+ const isFlutterPackage = /^\s{2}flutter:\s*\n\s{4}sdk:\s*flutter\s*$/m.test(
9057
+ readFileSync(path.join(pkg, "pubspec.yaml"), "utf8")
9058
+ );
9059
+ execFileSync(isFlutterPackage ? "flutter" : "dart", ["pub", "get"], {
9043
9060
  cwd: pkg,
9044
9061
  stdio: "pipe",
9045
9062
  encoding: "utf8",