@ai-dossier/sched 0.45.0 → 0.46.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.
- package/README.md +58 -2
- package/dist/anchor-close.d.ts +172 -0
- package/dist/anchor-close.d.ts.map +1 -0
- package/dist/anchor-close.js +348 -0
- package/dist/anchor-close.js.map +1 -0
- package/dist/batch-dispatch.d.ts +8 -0
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +213 -13
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -0
- package/dist/engine.js.map +1 -1
- package/dist/groundtruth.d.ts +86 -0
- package/dist/groundtruth.d.ts.map +1 -1
- package/dist/groundtruth.js +128 -2
- package/dist/groundtruth.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -5
- package/dist/index.js.map +1 -1
- package/dist/labels.d.ts +8 -0
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +14 -2
- package/dist/labels.js.map +1 -1
- package/dist/project.d.ts +10 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +27 -0
- package/dist/project.js.map +1 -1
- package/dist/recovery.js +1 -1
- package/dist/recovery.js.map +1 -1
- package/dist/state.d.ts +12 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +39 -4
- package/dist/state.js.map +1 -1
- package/dist/status.d.ts +16 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +10 -1
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +25 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;
|
|
1
|
+
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
|
package/dist/batch-dispatch.js
CHANGED
|
@@ -103,6 +103,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
103
103
|
const path = __importStar(require("node:path"));
|
|
104
104
|
const core_1 = require("@ai-dossier/core");
|
|
105
105
|
const worktree_pool_1 = require("@ai-dossier/worktree-pool");
|
|
106
|
+
const anchor_close_1 = require("./anchor-close");
|
|
106
107
|
const attribution_1 = require("./attribution");
|
|
107
108
|
const dispatch_1 = require("./dispatch");
|
|
108
109
|
const dispatch_health_1 = require("./dispatch-health");
|
|
@@ -2635,9 +2636,10 @@ const DISPATCHED_MEMBER_STATUSES = new Set([
|
|
|
2635
2636
|
* of merge evidence ("The batch branch is in `main` at c277ff59b", `pr=None`):
|
|
2636
2637
|
* the branch was merged out of band, so its commits are IN the base even
|
|
2637
2638
|
* though the batch never shipped a PR of its own. Two probes against the
|
|
2638
|
-
* remote-tracking ref (refreshed
|
|
2639
|
-
*
|
|
2640
|
-
*
|
|
2639
|
+
* remote-tracking ref (refreshed first — a merge that landed on the remote is
|
|
2640
|
+
* invisible to `rev-list` until fetched; since #768 a FAILED fetch yields no
|
|
2641
|
+
* evidence at all: a stale ref is not proof, and this answer can reconcile a
|
|
2642
|
+
* batch to shipped):
|
|
2641
2643
|
*
|
|
2642
2644
|
* - `git rev-list --count origin/<base>..<branch>` is 0 — nothing of the
|
|
2643
2645
|
* branch is missing from the base, i.e. every member commit on it is an
|
|
@@ -2658,7 +2660,8 @@ function branchMergedIntoBase(deps, batch) {
|
|
|
2658
2660
|
if (!attribution_1.SAFE_REF_RE.test(batch.branch) || !attribution_1.SAFE_REF_RE.test(batch.base_branch))
|
|
2659
2661
|
return false;
|
|
2660
2662
|
const base = `origin/${batch.base_branch}`;
|
|
2661
|
-
deps.exec('git', ['fetch', 'origin', batch.base_branch], deps.repoDir)
|
|
2663
|
+
if (deps.exec('git', ['fetch', 'origin', batch.base_branch], deps.repoDir) === null)
|
|
2664
|
+
return false;
|
|
2662
2665
|
const ahead = deps.exec('git', ['rev-list', '--count', `${base}..${batch.branch}`], deps.repoDir);
|
|
2663
2666
|
if ((ahead ?? '').trim() !== '0')
|
|
2664
2667
|
return false;
|
|
@@ -2700,7 +2703,7 @@ function branchMergedIntoBase(deps, batch) {
|
|
|
2700
2703
|
* one journal line fires on the one-way transition, never per-tick (the
|
|
2701
2704
|
* #632 lesson: no journal inside a loop that doesn't terminate the unit).
|
|
2702
2705
|
*/
|
|
2703
|
-
function reconcileStaleBlockedBatches(deps, now, result) {
|
|
2706
|
+
function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
2704
2707
|
const state = deps.store.load();
|
|
2705
2708
|
const nowMs = now.getTime();
|
|
2706
2709
|
for (const batch of state.batches) {
|
|
@@ -2720,8 +2723,31 @@ function reconcileStaleBlockedBatches(deps, now, result) {
|
|
|
2720
2723
|
if (evidence === null && branchMergedIntoBase(deps, batch)) {
|
|
2721
2724
|
evidence = { kind: 'commits-in-base' };
|
|
2722
2725
|
}
|
|
2726
|
+
// #768: the evidence that survives a rebase-merge (which rewrites SHAs, so
|
|
2727
|
+
// the ancestry probe above can never pass) and a member shipped through a
|
|
2728
|
+
// PR of its own (nothing batch-shaped ever merges): every member issue is
|
|
2729
|
+
// CLOSED as completed BY CODE IN THE BASE (a merged PR / reachable commit —
|
|
2730
|
+
// never a bare hand close) with no failure trail — the SAME strict
|
|
2731
|
+
// predicate the anchor close uses, so the two never disagree.
|
|
2732
|
+
if (evidence === null &&
|
|
2733
|
+
anchorCtx !== undefined &&
|
|
2734
|
+
(0, anchor_close_1.membersShippedVerdict)(state, batch, anchorCtx.read, {
|
|
2735
|
+
repo: anchorCtx.repo,
|
|
2736
|
+
commitInBase: anchorCtx.commitInBase,
|
|
2737
|
+
}).kind === 'closable') {
|
|
2738
|
+
evidence = { kind: 'members-closed' };
|
|
2739
|
+
}
|
|
2723
2740
|
if (evidence === null)
|
|
2724
2741
|
continue;
|
|
2742
|
+
// A members-closed reconcile has no merged batch PR for a report agent to
|
|
2743
|
+
// report on, whatever `batch.pr` recorded — finish the rail inline, as
|
|
2744
|
+
// for `pr === null`.
|
|
2745
|
+
const finishInline = batch.pr === null || evidence.kind === 'members-closed';
|
|
2746
|
+
// ...but never tear its worktree down: the members shipped OUTSIDE the
|
|
2747
|
+
// batch branch, so the blocked integration worktree may hold an
|
|
2748
|
+
// operator's unpushed repair work. It is left in place, inert, and the
|
|
2749
|
+
// journal says so.
|
|
2750
|
+
const keepWorktree = evidence.kind === 'members-closed';
|
|
2725
2751
|
const blockedReason = batch.blocked_reason ?? 'blocked';
|
|
2726
2752
|
const failedAt = batch.updated_at;
|
|
2727
2753
|
let reconciled = false;
|
|
@@ -2757,9 +2783,9 @@ function reconcileStaleBlockedBatches(deps, now, result) {
|
|
|
2757
2783
|
entry = (0, state_1.findEntry)(n, issue);
|
|
2758
2784
|
}
|
|
2759
2785
|
}
|
|
2760
|
-
if (
|
|
2761
|
-
// No PR → no report agent is possible (its prompt names the
|
|
2762
|
-
// Finish the terminal rail and let teardown run below.
|
|
2786
|
+
if (finishInline) {
|
|
2787
|
+
// No (merged) PR → no report agent is possible (its prompt names the
|
|
2788
|
+
// PR). Finish the terminal rail and let teardown run below.
|
|
2763
2789
|
n = (0, state_1.transitionBatch)(n, batch.id, 'reported', {}, now);
|
|
2764
2790
|
n = (0, state_1.transitionBatch)(n, batch.id, 'done', {}, now);
|
|
2765
2791
|
}
|
|
@@ -2776,20 +2802,192 @@ function reconcileStaleBlockedBatches(deps, now, result) {
|
|
|
2776
2802
|
...(evidence.kind === 'pr-merged' ? { mergedAt: evidence.mergedAt } : {}),
|
|
2777
2803
|
detail: (evidence.kind === 'pr-merged'
|
|
2778
2804
|
? `PR #${evidence.pr} is MERGED — ledger reconciled blocked (${blockedReason}) to the merged rail (blocked at ${failedAt}); report and teardown will now dispatch`
|
|
2779
|
-
:
|
|
2780
|
-
|
|
2781
|
-
|
|
2805
|
+
: evidence.kind === 'members-closed'
|
|
2806
|
+
? `every member issue is CLOSED as completed on GitHub with no failure trail — ledger reconciled blocked (${blockedReason}) to shipped (blocked at ${failedAt})`
|
|
2807
|
+
: `every member commit is an ancestor of ${batch.base_branch} — ledger reconciled blocked (${blockedReason}) to shipped (blocked at ${failedAt})`) +
|
|
2808
|
+
(finishInline
|
|
2809
|
+
? ' — no merged PR of its own, so no report agent can be prompted; ' +
|
|
2810
|
+
(keepWorktree
|
|
2811
|
+
? `worktree KEPT in place (${batch.worktree ?? 'none'}) — the work shipped outside the batch branch, which may hold unpushed work; once inspected, remove it with \`git worktree remove\` (or \`worktree-pool return\` if pool-claimed)`
|
|
2812
|
+
: 'teardown dispatched inline')
|
|
2782
2813
|
: '') +
|
|
2783
2814
|
(unwalkable.length > 0
|
|
2784
2815
|
? `; members NOT walked (no legal edge): ${unwalkable.join(',')}`
|
|
2785
2816
|
: ''),
|
|
2786
2817
|
});
|
|
2787
2818
|
result.mergeAccepted.push(unit(batch.id));
|
|
2788
|
-
if (
|
|
2819
|
+
if (finishInline && !keepWorktree) {
|
|
2789
2820
|
teardownBatch(deps, batch.id);
|
|
2790
2821
|
}
|
|
2791
2822
|
}
|
|
2792
2823
|
}
|
|
2824
|
+
function anchorTickContext(deps) {
|
|
2825
|
+
const reader = (0, groundtruth_1.issueCloseReader)(deps.groundTruth);
|
|
2826
|
+
if (reader === undefined || deps.anchorRepo === undefined)
|
|
2827
|
+
return undefined;
|
|
2828
|
+
const cache = new Map();
|
|
2829
|
+
const read = (issue) => {
|
|
2830
|
+
if (!cache.has(issue))
|
|
2831
|
+
cache.set(issue, reader(issue));
|
|
2832
|
+
return cache.get(issue);
|
|
2833
|
+
};
|
|
2834
|
+
const fetched = new Map();
|
|
2835
|
+
const commitInBase = (oid, baseBranch) => {
|
|
2836
|
+
// CWE-88: both are interpolated into git argv — the oid comes from GitHub.
|
|
2837
|
+
if (!groundtruth_1.GIT_OID_RE.test(oid) || !attribution_1.SAFE_REF_RE.test(baseBranch))
|
|
2838
|
+
return false;
|
|
2839
|
+
if (!fetched.has(baseBranch)) {
|
|
2840
|
+
fetched.set(baseBranch, deps.exec('git', ['fetch', 'origin', baseBranch], deps.repoDir) !== null);
|
|
2841
|
+
}
|
|
2842
|
+
// A failed fetch leaves a possibly stale `origin/<base>` (a force-push may
|
|
2843
|
+
// have dropped the commit since): unverifiable, so never evidence.
|
|
2844
|
+
if (fetched.get(baseBranch) !== true)
|
|
2845
|
+
return false;
|
|
2846
|
+
// `--is-ancestor` exits 0 (non-null) iff reachable; 1 and any failure → null.
|
|
2847
|
+
return (deps.exec('git', ['merge-base', '--is-ancestor', oid, `origin/${baseBranch}`], deps.repoDir) !== null);
|
|
2848
|
+
};
|
|
2849
|
+
return { repo: deps.anchorRepo, read, readFresh: reader, commitInBase };
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* #768: close a batch's anchor issue when — and ONLY when — the positive
|
|
2853
|
+
* evidence holds: every member CLOSED as completed by code that landed in the
|
|
2854
|
+
* base, and no member evicted, handed back, requeued, or failed
|
|
2855
|
+
* (`anchor-close.ts`). Before this, the anchor closed only on the happy path
|
|
2856
|
+
* (batch-integrate's #720 step), so a blocked or hand-recovered batch — or one
|
|
2857
|
+
* whose members shipped through PRs of their own — left it open forever
|
|
2858
|
+
* (imboard#4244, imboard#4253).
|
|
2859
|
+
*
|
|
2860
|
+
* Every other shape is left strictly alone: a blocked, abandoned, or
|
|
2861
|
+
* partially shipped batch keeps its anchor open, and `sched status --anchors`
|
|
2862
|
+
* shows it as `needs-operator`. Closing an anchor on a terminal transition
|
|
2863
|
+
* alone was rejected on #768 — it would bury the failure trail.
|
|
2864
|
+
*
|
|
2865
|
+
* Bounded: only `blocked`/`done` batches touched within
|
|
2866
|
+
* `STALE_BLOCKED_RECONCILE_WINDOW_MS` whose anchor is not yet recorded closed
|
|
2867
|
+
* are examined (an older one is only ever surfaced, never auto-closed); the
|
|
2868
|
+
* ledger check runs first and costs no GitHub call; member reads stop at the
|
|
2869
|
+
* first disqualifier. When this pass reads an anchor and finds it closed it
|
|
2870
|
+
* records `anchor_closed_at` and never polls it again. A batch the ledger
|
|
2871
|
+
* rules out is never read here at all, so its `anchor_closed_at` stays null —
|
|
2872
|
+
* the sweep reads (but never closes) those anchors when asked.
|
|
2873
|
+
*/
|
|
2874
|
+
function reconcileAnchorClosure(deps, now, ctx) {
|
|
2875
|
+
if (ctx === undefined)
|
|
2876
|
+
return;
|
|
2877
|
+
const state = deps.store.load();
|
|
2878
|
+
for (const batch of (0, anchor_close_1.openAnchorBatches)(state, anchor_close_1.ANCHOR_CLOSE_BATCH_STATUSES)) {
|
|
2879
|
+
if (now.getTime() - Date.parse(batch.updated_at) >= STALE_BLOCKED_RECONCILE_WINDOW_MS) {
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
// Deliberately BEFORE classifyAnchor (which repeats it): a ledger-ruled-out
|
|
2883
|
+
// batch must cost no GitHub read, and a blocked one is re-checked every tick.
|
|
2884
|
+
if ((0, anchor_close_1.anchorLedgerBlockers)(state, batch).length > 0)
|
|
2885
|
+
continue;
|
|
2886
|
+
const verdict = (0, anchor_close_1.classifyAnchor)(state, batch, ctx.read, {
|
|
2887
|
+
repo: ctx.repo,
|
|
2888
|
+
commitInBase: ctx.commitInBase,
|
|
2889
|
+
});
|
|
2890
|
+
if (verdict.kind === 'anchor-closed') {
|
|
2891
|
+
// Closed outside this pass — or by a prior pass that crashed before
|
|
2892
|
+
// recording it; either way the trail gets one line.
|
|
2893
|
+
recordAnchorClosed(deps, batch.id, now);
|
|
2894
|
+
journalEvent(deps, 'anchor-closed', unit(batch.id), {
|
|
2895
|
+
issue: batch.anchor,
|
|
2896
|
+
detail: `anchor #${batch.anchor} found already closed — recorded, never polled again`,
|
|
2897
|
+
});
|
|
2898
|
+
continue;
|
|
2899
|
+
}
|
|
2900
|
+
if (verdict.kind !== 'closable') {
|
|
2901
|
+
clearAnchorCloseFailed(deps, batch, now);
|
|
2902
|
+
continue;
|
|
2903
|
+
}
|
|
2904
|
+
// The GitHub reads above took time: re-check the ledger on a FRESH load,
|
|
2905
|
+
// immediately before the irreversible close, so an eviction, `abandon` or
|
|
2906
|
+
// `stop` that landed meanwhile is never closed over.
|
|
2907
|
+
const stillClean = deps.store.withLock((s) => {
|
|
2908
|
+
const fresh = (0, state_1.findBatch)(s, batch.id);
|
|
2909
|
+
const ok = fresh !== undefined &&
|
|
2910
|
+
fresh.anchor_closed_at === null &&
|
|
2911
|
+
anchor_close_1.ANCHOR_CLOSE_BATCH_STATUSES.has(fresh.status) &&
|
|
2912
|
+
(0, anchor_close_1.anchorLedgerBlockers)(s, fresh).length === 0;
|
|
2913
|
+
return { state: s, result: ok };
|
|
2914
|
+
});
|
|
2915
|
+
// ...and GitHub itself, uncached: a member reopened or the anchor handed
|
|
2916
|
+
// back since the (cached) verdict read must stop the close too.
|
|
2917
|
+
const freshVerdict = stillClean
|
|
2918
|
+
? (0, anchor_close_1.classifyAnchor)(state, batch, ctx.readFresh, {
|
|
2919
|
+
repo: ctx.repo,
|
|
2920
|
+
commitInBase: ctx.commitInBase,
|
|
2921
|
+
})
|
|
2922
|
+
: undefined;
|
|
2923
|
+
if (freshVerdict?.kind !== 'closable') {
|
|
2924
|
+
clearAnchorCloseFailed(deps, batch, now);
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2927
|
+
const outcome = (0, anchor_close_1.closeAnchor)(deps.exec, deps.repoDir, ctx.repo, batch, batch.anchor, verdict.members);
|
|
2928
|
+
const shipped = verdict.members.map((m) => `#${m.issue}: ${m.shipped_by}`).join('; ');
|
|
2929
|
+
if (outcome !== 'closed') {
|
|
2930
|
+
recordAnchorCloseFailed(deps, batch, outcome, shipped, now);
|
|
2931
|
+
continue;
|
|
2932
|
+
}
|
|
2933
|
+
// Journal before recording: a crash between the two leaves the line, and
|
|
2934
|
+
// the next tick records the (already-closed) anchor.
|
|
2935
|
+
journalEvent(deps, 'anchor-closed', unit(batch.id), {
|
|
2936
|
+
issue: batch.anchor,
|
|
2937
|
+
detail: `every member closed as completed by code in ${batch.base_branch}, no failure trail — ${shipped}`,
|
|
2938
|
+
});
|
|
2939
|
+
recordAnchorClosed(deps, batch.id, now);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
function recordAnchorClosed(deps, batchId, now) {
|
|
2943
|
+
deps.store.withLock((s) => ({
|
|
2944
|
+
// Bookkeeping, not activity — `updated_at` is the blocked-window clock.
|
|
2945
|
+
state: (0, state_1.patchBatch)(s, batchId, { anchor_closed_at: now.toISOString(), ...state_1.CLEARED_ANCHOR_CLOSE_FAILED_FIELDS }, now, false),
|
|
2946
|
+
result: undefined,
|
|
2947
|
+
}));
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* The anchor stopped qualifying: end any `anchor-close-failed` streak, so the
|
|
2951
|
+
* next failure episode journals its own first line instead of continuing a
|
|
2952
|
+
* stale streak's count. A no-op (no write) when no streak is recorded.
|
|
2953
|
+
*/
|
|
2954
|
+
function clearAnchorCloseFailed(deps, batch, now) {
|
|
2955
|
+
if (batch.anchor_close_failed_reason === null)
|
|
2956
|
+
return;
|
|
2957
|
+
deps.store.withLock((s) => ({
|
|
2958
|
+
state: (0, state_1.patchBatch)(s, batch.id, state_1.CLEARED_ANCHOR_CLOSE_FAILED_FIELDS, now, false),
|
|
2959
|
+
result: undefined,
|
|
2960
|
+
}));
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* A close that qualified but failed: journal it on the streak's first tick and
|
|
2964
|
+
* every `JOURNAL_DEDUP_REANNOUNCE_TICKS` after — never every tick (#632) —
|
|
2965
|
+
* exactly `reconcilePrWatch`'s `pr-watch-failed` dedup.
|
|
2966
|
+
*/
|
|
2967
|
+
function recordAnchorCloseFailed(deps, batch, outcome, shipped, now) {
|
|
2968
|
+
const priorSince = batch.anchor_close_failed_since;
|
|
2969
|
+
const isNewStreak = batch.anchor_close_failed_reason !== outcome || priorSince === null;
|
|
2970
|
+
const ticks = isNewStreak ? 1 : batch.anchor_close_failed_ticks + 1;
|
|
2971
|
+
const since = isNewStreak || priorSince === null ? now.toISOString() : priorSince;
|
|
2972
|
+
if (isNewStreak || ticks % types_1.JOURNAL_DEDUP_REANNOUNCE_TICKS === 0) {
|
|
2973
|
+
journalEvent(deps, 'anchor-close-failed', unit(batch.id), {
|
|
2974
|
+
issue: batch.anchor ?? undefined,
|
|
2975
|
+
reason: outcome,
|
|
2976
|
+
at: now.toISOString(),
|
|
2977
|
+
since,
|
|
2978
|
+
ticks_persisted: ticks,
|
|
2979
|
+
detail: `anchor #${batch.anchor} qualifies for close (${shipped}) but ${outcome}; retried every tick — the gh error is on the engine's stderr ('⚠ sched …')`,
|
|
2980
|
+
});
|
|
2981
|
+
}
|
|
2982
|
+
deps.store.withLock((s) => ({
|
|
2983
|
+
state: (0, state_1.patchBatch)(s, batch.id, {
|
|
2984
|
+
anchor_close_failed_reason: outcome,
|
|
2985
|
+
anchor_close_failed_since: since,
|
|
2986
|
+
anchor_close_failed_ticks: ticks,
|
|
2987
|
+
}, now, false),
|
|
2988
|
+
result: undefined,
|
|
2989
|
+
}));
|
|
2990
|
+
}
|
|
2793
2991
|
/**
|
|
2794
2992
|
* Remove the batch's shared worktree — called both on the happy path
|
|
2795
2993
|
* (`reconcileReportSlot`, after `batch-report done`) and on every dissolve
|
|
@@ -3090,7 +3288,9 @@ function runBatchTick(deps, config, dispatch) {
|
|
|
3090
3288
|
}
|
|
3091
3289
|
}
|
|
3092
3290
|
reconcilePrWatch(deps, now, result);
|
|
3093
|
-
|
|
3291
|
+
const anchorCtx = anchorTickContext(deps);
|
|
3292
|
+
reconcileStaleBlockedBatches(deps, now, result, anchorCtx);
|
|
3293
|
+
reconcileAnchorClosure(deps, now, anchorCtx);
|
|
3094
3294
|
return result;
|
|
3095
3295
|
}
|
|
3096
3296
|
//# sourceMappingURL=batch-dispatch.js.map
|