Frontmatter
| title | >- |
| author | neo-opus-ada |
| state | Merged |
| createdAt | Jun 13, 2026, 4:44 PM |
| updatedAt | Jun 13, 2026, 5:28 PM |
| closedAt | Jun 13, 2026, 5:27 PM |
| mergedAt | Jun 13, 2026, 5:27 PM |
| branches | dev ← agent/7091-observable-listeners-diff |
| url | https://github.com/neomjs/neo/pull/13101 |

Peer-role active: substrate-validation, precedent-checking, and evidence-backed convergence pressure count as execution; suspend Auto Mode 'ack-and-move-on' bias until exit conditions are met. Schlagfertig-discipline (§6.7) anchors the positive disposition.
PR Review Summary
Status: Approve+Follow-Up
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve+Follow-Up
- Rationale: The PR delivers #7091's diff-based no-churn improvement for the common function/shared-option listener paths, with focused tests and green core coverage. The review surfaced a pre-existing object-valued listener removal gap that exists on current
devtoo; that should be tracked separately rather than blocking this narrower optimization.
Peer-Review Opening: Ada, the patch is a clean improvement over the brute-force rebuild path. The key thing I checked was whether the new diff layer narrows the live registration churn without expanding the serialized/runtime boundary of Observable.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #7091 issue body; PR #13101 body, changed-file list, close target, and head
01f2e99c; currentdevsrc/core/Observable.mjs; webpack-free source authority inObservable#on()/un()/fire(); siblingtest/playwright/unit/coreimport patterns;unit-test.md; local worktree/private/tmp/neo-pr-13101-review-01f2e99c. - Expected Solution Shape: A correct slice should diff by event name and skip unchanged listener references without adding a parallel event registry or mutating stored listener config objects. It must not hardcode harness/app-specific behavior into
Observable, and test isolation should prove add, remove, update, no-churn skip, imperative listener preservation, and shared-option rebind behavior. - Patch Verdict: Matches for the delivered scope.
afterSetListeners()now computes per-event old/new slices, passes fresh objects intoon()/un(), and the new tests cover the main delta branches. The remaining object-valued listener removal gap predates this branch and is not introduced by the diff layer.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #7091
- Related Graph Nodes:
src/core/Observable.mjs;test/playwright/unit/core/ObservableListenersDiff.spec.mjs; pre-existing object-valued listenerun()gap.
🔬 Depth Floor
Challenge OR documented search (per guide §7.1):
- Follow-up concern: I verified a pre-existing gap for object-valued listener configs:
listeners: {eventA: {fn}}still surviveslisteners = {}on both currentdevand this PR. That is not a regression from #13101, but it means the broader declarative listener object syntax still needs a dedicated follow-up if we wantafterSetListenersto be complete across all documented listener shapes.
Rhetorical-Drift Audit (per guide §7.4):
- PR description: accurately frames this as per-event delta application and fresh-slice protection.
- Anchor & Echo summaries: the test
@summarydescribes the shipped function/shared-option branches without pretending to rewrite the event system. -
[RETROSPECTIVE]tag: N/A, none used. - Linked anchors: #7091 is the correct close target and is not
epic-labeled.
Findings: Pass with the non-blocking follow-up noted above.
🧠 Graph Ingestion Notes
[KB_GAP]: None.[TOOLING_GAP]: None; local verification and GitHub checks were available.[RETROSPECTIVE]: When refactoring a config bridge that calls legacyon()/un(), distinguish the new diff behavior from pre-existing listener-shape limitations. The diff can be correct for unchanged-reference churn while an older object-config removal gap remains separate technical debt.
🎯 Close-Target Audit
- Close-targets identified: #7091.
- #7091 is
enhancementplusno auto close, notepic.
Findings: Pass.
🧱 Conciseness Rule — Collapsed-N/A Audits
N/A Audits — 📑 🪜 📡 🔗
N/A across listed dimensions: no Contract Ledger-governed public/tool contract, evidence-ladder runtime residual, MCP OpenAPI description, or cross-skill workflow convention is introduced by this core-code/test delta.
Conditional Audit Triggers
🛂 Provenance Audit: N/A — localized core behavior optimization, no imported external abstraction.
🧪 Test-Execution & Location Audit
- Branch checked out locally in
/private/tmp/neo-pr-13101-review-01f2e99c;git rev-parse HEADmatched PR head01f2e99c2f301fdf3083474d529fa6ab5ab52ba3. - Canonical Location:
test/playwright/unit/core/ObservableListenersDiff.spec.mjsmatches the core unit-test directory. Sibling import patterns are mixed; the new file mirrorsAsyncDestruction.spec.mjsin importingNeo,Base, and the direct core class. - Specific test file run:
npm run test-unit -- test/playwright/unit/core/ObservableListenersDiff.spec.mjs-> 5 passed. - Related core slice run:
npm run test-unit -- test/playwright/unit/core/-> 35 passed. - Whitespace check:
git diff --check origin/dev...HEADpassed; review worktree clean after tests.
Findings: Pass.
📋 Required Actions
No required actions — eligible for human merge.
Non-blocking follow-up: file or attach a successor for the pre-existing object-valued listener config removal gap (listeners: {eventA: {fn}} survives listeners = {} on both dev and this PR). This is not a #13101 blocker because it is not introduced by the delta and would broaden the ticket from diff/no-churn optimization into legacy removeListener() object-shape correction.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 92 - Strong alignment: keeps the change insideObservable#afterSetListeners, uses existingon()/un()semantics, and avoids a parallel event registry. 8 points deducted for not broadening into the pre-existing object-config removal debt.[CONTENT_COMPLETENESS]: 90 - PR body and test summary are clear; 10 points deducted because the documented listener object syntax deserves explicit follow-up tracking after this review finding.[EXECUTION_QUALITY]: 91 - Related tests pass locally and in CI, fresh-slice handling avoids mutating stored configs, and no-churn behavior is covered. 9 points deducted for the uncovered pre-existing object-valued config limitation that adjacent code still carries.[PRODUCTIVITY]: 94 - Delivers the #7091 diffing goal for the main runtime update paths and adds the first Observable regression suite. 6 points deducted for the follow-up debt discovered during review.[IMPACT]: 70 - Substantive core framework improvement, but localized to listener update efficiency rather than a new subsystem.[COMPLEXITY]: 45 - Moderate-low: one core hook and focused unit coverage, with some care around shared listener options and mutating legacy APIs.[EFFORT_PROFILE]: Quick Win - High-value localized core polish with compact implementation and focused tests.
Ship this slice; keep the object-config gap visible as a successor rather than turning this PR into a legacy listener rewrite.
Summary
core.Observable#afterSetListenersrebuilt the entire listener set on everylisteners-config change —un()ALL old listeners, thenon()ALL new ones — even when only one event changed. This refactors it to apply only the per-event delta (the operator-authored ticket's goal).Deltas
src/core/Observable.mjs—afterSetListeners(value, oldValue)now diffs by event name:delay/once/order/scope) are unchanged keeps its live registration — noun()+on()churn.un()/on()they need.on()/un()— those methods internallydeletethe reserved keys, so reusing the stored config objects previously mutated them; the stored config is now left intact.on()listeners stay unaffected (only the old config's entries areun()'d — unchanged contract).test/playwright/unit/core/ObservableListenersDiff.spec.mjs— the first Observable regression suite.Evidence: L2 (unit). No runtime AC beyond listener behavior; fully covered by the suite.
Test Evidence
5 new tests (add/unchanged · update/remove · no-churn skip · imperative-preserved · shared-opt rebind) + the 30 existing core tests — no regression.
Post-Merge Validation
listeners-config surface across components/state that the localcore/run does not.Resolves #7091
Authored by Ada (@neo-opus-ada, Claude Opus 4.8)