Skip to content

feat(v4): add createGroup, the missing half of v3's withGroup - #826

Merged
titouanmathis merged 2 commits into
mainfrom
feature/v4-grouping
Aug 16, 2026
Merged

feat(v4): add createGroup, the missing half of v3's withGroup#826
titouanmathis merged 2 commits into
mainfrom
feature/v4-grouping

Conversation

@titouanmathis

Copy link
Copy Markdown
Contributor

Verdict: one small primitive was missing, and it is not $group

Contexts cover finding the group. They do not cover being the group.

provide/inject + subscribeContext() already solve the hard half of v3's withGroup: nearest-provider-wins gives a member its closest group, a pending request is replayed when a provider appears later, and the mount announcement re-answers a member when a nearer group shows up. That is the whole of Disclosure's DISCLOSURE_CONNECTED / DISCLOSURE_GROUP_CONNECTED / useMutation / __connect / __disconnect dance, deleted.

What nothing in v4 provides is the roster on the coordinator's side:

  • $watchChildren is the closest thing, and it is wrong here: it collects every matching descendant across nested group boundaries, so a nested DisclosureGroup's items land in the outer group's collection. Disclosure has to filter them back out by asking each item which group it belongs to.
  • A bare Set — what v3 handed out — has no value cell. A coordinator can read its peers but never learn that one arrived. With v4 mounting on DOM insertion and no ordering guarantee, an invariant over the set (one open at a time) has to be re-checked on every membership change, not established once.

So: createGroup() — 90 lines, one new runtime export, built on signal() and reached as a context value. No parallel registry; the report's argument against one stands and this does not touch it.

interface Group<T extends GroupMember = GroupMember> {
  /** The roster in document order, a new array on every change. */
  readonly members: Signal<readonly T[]>;
  /** Join, and get the leave function back. */
  join(member: T): () => void;
}

Two design points worth arguing about:

  • join() returns leave because that is already the shape of subscribeContext()'s answer/teardown contract. A member's whole group wiring is one call, and migration to a nearer group leaves the old one before joining the new one for free.
  • Document order is the tie-breaker, deliberately. The task asked what happens when a peer mounts later; the answer is that mount order never decides. Which peer keeps its state is a fact about the markup.

The Disclosure pattern in v4

Whole thing, no decorators, no build step. This is src/group.spec.ts verbatim.

const DisclosureGroupContext = createContext('DisclosureGroup');

class DisclosureGroup extends Base {
  static config = { name: 'DisclosureGroup', options: { multiple: Boolean } };

  #peers = createGroup();

  // Provided during construction, so a member that mounts first still resolves it.
  api = this.$provide(DisclosureGroupContext, {
    members: this.#peers.members,
    join: (peer) => this.#peers.join(peer),
    open: (peer) => this.open(peer),
  });

  get members() {
    return this.#peers.members.value;
  }

  /** A peer arriving or leaving can break the invariant, so re-check the roster. */
  mounted() {
    return this.#peers.members.subscribe(() => this.reconcile(), { immediate: true });
  }

  open(peer) {
    if (!this.$options.multiple) {
      for (const other of this.members) {
        if (other !== peer) other.setOpen(false);
      }
    }
    peer.setOpen(true);
  }

  /** Document order decides, so the outcome does not depend on mount order. */
  reconcile() {
    if (this.$options.multiple) return;
    const [, ...extra] = this.members.filter((peer) => peer.isOpen);
    for (const peer of extra) peer.setOpen(false);
  }
}

class Disclosure extends Base {
  static config = { name: 'Disclosure', refs: ['trigger', 'panel'], options: { open: Boolean } };

  group;
  isOpen = false;

  mounted() {
    this.setOpen(this.$options.open);
    // The nearest group, whenever it appears.
    return subscribeContext(this.$el, DisclosureGroupContext, (group) => {
      this.group = group;
      const leave = group.join(this);
      return () => {
        leave();
        this.group = undefined;
      };
    });
  }

  onTriggerClick() {
    this.toggle();
  }

  toggle() {
    if (this.isOpen) this.setOpen(false);
    else if (this.group) this.group.open(this);
    else this.setOpen(true);
  }

  setOpen(open) {
    this.isOpen = open;
    this.$refs.panel.hidden = !open;
    this.$refs.trigger.setAttribute('aria-expanded', String(open));
  }
}

Against v3's Disclosure.ts + DisclosureGroup.ts this drops: two document-level CustomEvent channels and their listeners, useMutation(document, …) with its per-instance key, __connect / __disconnect / register / unregister, __scheduleReconcile and its nextTick debounce, and the items getter's own DOM sort and contains filter. What is left is the invariant, which was always the only part that belonged to the group.

What group.spec.ts asserts

13 tests in real Chromium. The ones that matter:

case outcome
open peer mounts after the group, later in the DOM it loses; the earlier one keeps its state
open peer mounts after the group, earlier in the DOM it wins; the later one closes
group's data-component added after its members mounted it takes them all
nested group takes its own members; the outer roster excludes them
inner group inserted around an already-mounted member the member changes hands — same instance, asserted, so this is the subscribeContext() re-answer and not a destroy/remount
member's element removed it leaves the roster
no group above the disclosure still toggles on its own

Removing the members.subscribe() reconcile fails exactly the two late-peer tests and nothing else, so the roster-as-a-value is load-bearing and not decoration.

How the two Data* sites translate

They already are translated, in packages/v4/migration/Data/ — and they do not use createGroup(). That is the interesting part, so spelling it out:

  • DataScope (v3: getScopedGroups(this) + a WeakMap of scope → name → Set) becomes one field initializer: registry = this.$provide(DataRegistryContext, new DataRegistry({ scoped: true, … })). The scope resolution, the WeakMap and the page-global fallback all collapse into nearest-provider-wins plus provideRootContext().
  • DataBind (v3: withGroup(Base, 'data:', { getScope, getGroup }) and this.$group) becomes one subscribeContext() in mounted(): the callback joins registry.join(group, this), the teardown leaves, and mounted() returns the unsubscribe. getDataScope()'s manual __base__ ancestor walk is gone.

DataRegistry keeps its own membership Set rather than a Group, and that is the right call: the two v3 consumers of withGroup divide on what "group" means. Disclosure groups by ancestry — the group is a component in the DOM, and the whole difficulty was the two sides finding each other. Data* groups by name, with the nearest DataScope only choosing which partition table to read; membership is one field of a per-name record beside values, sources and hydration state, and nothing ever subscribes to a roster. It has the identical join(group, member) → leave shape for the identical reason. A group that is a set of peers gets createGroup(); a group that is a partition of a keyed store keeps the store. DESIGN.md §5 now records that line.

What is deliberately not here

  • No $group. Nothing implicit, nothing global, no name option. The name a v3 consumer passed was the partition key of a page-global registry; a group whose membership is a DOM fact needs neither a name nor a scope resolver.
  • No Base method. createGroup() imports signal and nothing else — no component graph on the ./createGroup subpath — and the member side is a subscribeContext() call that already has a home.
  • No disconnected-member sweep. v3 swept on every read of $group because membership lived on a registry that outlived the instance. v4 destroys a component when its element leaves the DOM, so the member's own teardown removes it. Asserted.

Verification

npm run lint, npm run lint:types, npm run test:v4 (988 passed) and npm run check:package are green on each commit. Root runtime exports go 79 → 80; packages/v4/src/subpaths/createGroup.ts and the exports map are generated by npm run subpaths.

🤖 Generated with Claude Code

https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Code Review

Risk: Medium — issues that should be addressed before merge.

Adds and exports createGroup(), a signal-backed, document-ordered peer roster with join/leave teardown semantics. It also adds comprehensive group and Disclosure behavior tests plus design documentation.

1 issue found:

  • issuepackages/v4/node_modules:1 — Remove the machine-specific absolute node_modules symlink

Review usage: 29,309 in (3,056 cached) / 667 out tokens — $0.0186 (openrouter/openai/gpt-5.6-luna, thinking: low)

Reviewed by @weareikko/code-review v0.9.5 for commit 842427d.

Previous review runs

Previous run archived 2026-08-16T11:48:58Z

Code Review

Risk: Low — The change adds the createGroup roster primitive, exports it from the root and subpath entry points, and documents the context-scoped grouping design. It also adds comprehensive tests covering document ordering, membership teardown, nested groups, late mounting, and context migration. No blocking defects were found.


Review usage: 15,918 in (3,056 cached) / 484 out tokens — $0.0096 (openrouter/openai/gpt-5.6-luna, thinking: low)

Reviewed by @weareikko/code-review v0.9.5 for commit 22edb6d.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Export size

Bundled per export with peer dependencies left external, dynamic imports excluded and the output minified; sizes are gzipped.

@studiometa/js-toolkit-v4

Export Size (gzip) Diff
createGroup 1.06 kB +1.06 kB (+100.0%)
(barrel) 20.51 kB +86 B (+0.4%)
watchAttributes 1.98 kB +1 B (+0.0%)
withScroll 2.93 kB +1 B (+0.0%)
withPointer 2.11 kB -1 B (-0.0%)
Unchanged (383)

@studiometa/js-toolkit

Export Size (gzip) Diff
(barrel) 17.44 kB
AbstractService 598 B
Base 9.06 kB
ComponentLoader 2.31 kB
DEFAULT_DIAGNOSTIC_PREFIX 102 B
DragService 2.02 kB
IDLE_TIMEOUT 57 B
KeyService 935 B
LoadService 666 B
MutationService 849 B
PointerService 1.13 kB
RafService 1020 B
ResizeService 1.12 kB
ScrollService 1.36 kB
VISIBLE_ROOT_MARGIN 72 B
autoload 2.4 kB
closestComponent 419 B
composeManifests 119 B
createApp 996 B
defineFeatures 326 B
defineManifest 512 B
fromMetaGlob 228 B
fromWebpackContext 131 B
getClosestParent 197 B
getDirectChildren 202 B
getInstanceFromElement 125 B
getInstances 187 B
getScopedGroups 104 B
importOnInteraction 926 B
importOnMediaQuery 243 B
importWhenIdle 225 B
importWhenPrefersMotion 271 B
importWhenVisible 935 B
isDirectChild 218 B
logTree 551 B
queryComponent 594 B
queryComponentAll 601 B
readEagerTokens 201 B
registerComponent 305 B
registerComponents 356 B
registerManifest 2.87 kB
registerManifests 2.89 kB
useDrag 2.05 kB
useKey 943 B
useLoad 676 B
useMutation 876 B
usePointer 1.15 kB
useRaf 1 kB
useResize 1.13 kB
useScroll 1.36 kB
utils 10.05 kB
utils/Queue 269 B
utils/SmartQueue 440 B
utils/addClass 240 B
utils/addStyle 239 B
utils/animate 3.34 kB
utils/boundingRectToCircle 206 B
utils/cache 208 B
utils/camelCase 405 B
utils/clamp 98 B
utils/clamp01 114 B
utils/collideCircleCircle 129 B
utils/collideCircleRect 192 B
utils/collidePointCircle 128 B
utils/collidePointRect 122 B
utils/collideRectRect 128 B
utils/createEaseInOut 123 B
utils/createEaseOut 91 B
utils/createElement 635 B
utils/createLocalStorage 1.32 kB
utils/createLocalStorageProvider 296 B
utils/createMemoryStorageProvider 174 B
utils/createNoopProvider 128 B
utils/createRange 115 B
utils/createSessionStorage 1.32 kB
utils/createSessionStorageProvider 288 B
utils/createStorage 1.3 kB
utils/createUrlSearchParamsInHashProvider 461 B
utils/createUrlSearchParamsInHashStorage 1.35 kB
utils/createUrlSearchParamsProvider 429 B
utils/createUrlSearchParamsStorage 1.34 kB
utils/damp 106 B
utils/dashCase 404 B
utils/debounce 122 B
utils/domScheduler 310 B
utils/ease 519 B
utils/easeInCirc 285 B
utils/easeInCubic 287 B
utils/easeInExpo 286 B
utils/easeInOutCirc 288 B
utils/easeInOutCubic 289 B
utils/easeInOutExpo 288 B
utils/easeInOutQuad 288 B
utils/easeInOutQuart 289 B
utils/easeInOutQuint 289 B
utils/easeInOutSine 288 B
utils/easeInQuad 285 B
utils/easeInQuart 286 B
utils/easeInQuint 286 B
utils/easeInSine 285 B
utils/easeLinear 77 B
utils/easeOutCirc 286 B
utils/easeOutCubic 288 B
utils/easeOutExpo 286 B
utils/easeOutQuad 286 B
utils/easeOutQuart 286 B
utils/easeOutQuint 286 B
utils/easeOutSine 286 B
utils/endsWith 128 B
utils/fold 168 B
utils/getAncestorWhere 123 B
utils/getAncestorWhereUntil 148 B
utils/getComponentResolver 140 B
utils/getOffsetSizes 194 B
utils/hasWindow 88 B
utils/historyPush 524 B
utils/historyReplace 526 B
utils/inertiaFinalValue 169 B
utils/isArray 63 B
utils/isBoolean 78 B
utils/isDefined 75 B
utils/isDev 78 B
utils/isEmpty 206 B
utils/isEmptyString 108 B
utils/isFunction 79 B
utils/isNull 68 B
utils/isNumber 91 B
utils/isObject 108 B
utils/isString 77 B
utils/keyCodes 122 B
utils/lerp 84 B
utils/loadElement 220 B
utils/loadIframe 241 B
utils/loadImage 241 B
utils/loadLink 237 B
utils/loadScript 251 B
utils/localStorageProvider 839 B
utils/lowerCase 404 B
utils/map 93 B
utils/matrix 136 B
utils/mean 126 B
utils/memo 130 B
utils/memoize 228 B
utils/memoryStorageProvider 843 B
utils/nextFrame 179 B
utils/nextMicrotask 133 B
utils/nextTick 148 B
utils/noop 62 B
utils/noopValue 76 B
utils/objectToURLSearchParams 322 B
utils/pascalCase 407 B
utils/random 93 B
utils/randomInt 113 B
utils/randomItem 234 B
utils/removeClass 242 B
utils/removeStyle 243 B
utils/round 95 B
utils/saveActiveElement 92 B
utils/scrollTo 2.31 kB
utils/sessionStorageProvider 838 B
utils/smoothTo 476 B
utils/snakeCase 406 B
utils/spring 154 B
utils/startsWith 125 B
utils/throttle 125 B
utils/toggleClass 242 B
utils/transform 347 B
utils/transition 1010 B
utils/trapFocus 441 B
utils/tween 1.72 kB
utils/untrapFocus 120 B
utils/upperCase 404 B
utils/urlSearchParamsInHashProvider 845 B
utils/urlSearchParamsProvider 839 B
utils/useScheduler 309 B
utils/wait 103 B
utils/withLeadingCharacters 135 B
utils/withLeadingSlash 142 B
utils/withTrailingCharacters 135 B
utils/withTrailingSlash 142 B
utils/withoutLeadingCharacters 122 B
utils/withoutLeadingCharactersRecursive 165 B
utils/withoutLeadingSlash 133 B
utils/withoutTrailingCharacters 122 B
utils/withoutTrailingCharactersRecursive 165 B
utils/withoutTrailingSlash 133 B
utils/wrap 122 B
version 56 B
withBreakpointManager 1.54 kB
withBreakpointObserver 1.71 kB
withDrag 2.18 kB
withExtraConfig 163 B
withFreezedOptions 187 B
withGroup 455 B
withIntersectionObserver 303 B
withMountOnMediaQuery 393 B
withMountWhenInView 347 B
withMountWhenPrefersMotion 431 B
withMutation 1010 B
withName 109 B
withRelativePointer 1.29 kB
withResponsiveOptions 2.4 kB
withScrolledInView 3.05 kB

@studiometa/js-toolkit-v4

Export Size (gzip) Diff
BREAKPOINTS 776 B
Base 8.09 kB
DIAGNOSTICS 629 B
DRAG_MODES 162 B
EVENTS 155 B
MOUNT_ATTRIBUTE 69 B
SWAP_MODES 129 B
children 243 B
component 10.55 kB
createContext 472 B
createFallbackProvider 1.34 kB
createLocalStorage 2.34 kB
createLocalStorageProvider 1.23 kB
createMemoryStorageProvider 1.23 kB
createService 630 B
createServiceMixin 509 B
createSessionStorage 2.34 kB
createSessionStorageProvider 1.23 kB
createStorage 2.32 kB
createUrlSearchParamsInHashProvider 1.23 kB
createUrlSearchParamsInHashStorage 2.36 kB
createUrlSearchParamsProvider 1.23 kB
createUrlSearchParamsStorage 2.36 kB
defaultScheduler 1.5 kB
defineManifest 983 B
domUpdate 1.23 kB
emitExtendable 1.09 kB
fromMetaGlob 203 B
fromWebpackContext 131 B
getBreakpoints 776 B
getInstances 2.77 kB
inject 175 B
injectContext 675 B
injectContextSync 634 B
jsonSerializer 95 B
localStorageProvider 1.22 kB
memoryStorageProvider 1.23 kB
nextFrame 115 B
on 8.42 kB
perTarget 176 B
provide 178 B
provideContext 704 B
provideRootContext 748 B
read 127 B
registerComponent 10.47 kB
registerComponents 10.48 kB
registerManifest 10.56 kB
sessionStorageProvider 1.22 kB
setBreakpoints 805 B
signal 920 B
subscribeContext 1.45 kB
swap 2.63 kB
toggle 176 B
until 172 B
urlSearchParamsInHashProvider 1.22 kB
urlSearchParamsProvider 1.22 kB
useBreakpoint 1.44 kB
useDrag 3.22 kB
useInView 1.31 kB
useMediaQuery 1.05 kB
useMutation 1.29 kB
usePointer 1.72 kB
usePrefersReducedMotion 1.08 kB
useRaf 1.93 kB
useResize 1.3 kB
useScroll 2.54 kB
useScrollProgress 3.51 kB
useWindowScroll 2.52 kB
useWindowSize 1.3 kB
utils 8.65 kB
utils/DEFAULT_DAMP_FACTOR 109 B
utils/INERTIA_FRAME 97 B
utils/MAX_SPRING_RATIO 100 B
utils/SCROLL_AXES 100 B
utils/TRANSFORM_PROPS 137 B
utils/TRANSITION_OPTIONS 132 B
utils/camelCase 449 B
utils/capitalize 119 B
utils/clamp 133 B
utils/clamp01 149 B
utils/clampDampFactor 157 B
utils/createEaseInOut 120 B
utils/createEaseOut 91 B
utils/createElement 638 B
utils/createRange 205 B
utils/damp 211 B
utils/debounce 121 B
utils/decayOver 162 B
utils/deepmerge 312 B
utils/easeInCirc 94 B
utils/easeInCubic 81 B
utils/easeInExpo 97 B
utils/easeInOutCirc 150 B
utils/easeInOutCubic 141 B
utils/easeInOutExpo 150 B
utils/easeInOutQuad 139 B
utils/easeInOutQuart 140 B
utils/easeInOutQuint 140 B
utils/easeInOutSine 156 B
utils/easeInQuad 80 B
utils/easeInQuart 81 B
utils/easeInQuint 81 B
utils/easeInSine 104 B
utils/easeLinear 77 B
utils/easeOutCirc 121 B
utils/easeOutCubic 111 B
utils/easeOutExpo 124 B
utils/easeOutQuad 110 B
utils/easeOutQuart 112 B
utils/easeOutQuint 111 B
utils/easeOutSine 132 B
utils/enterTransition 679 B
utils/fold 200 B
utils/getOffsetSizes 268 B
utils/historyPush 380 B
utils/historyReplace 381 B
utils/inertiaDecay 199 B
utils/inertiaFinalValue 187 B
utils/inertiaStep 232 B
utils/inertiaTimeConstant 178 B
utils/isBoolean 90 B
utils/isDefined 87 B
utils/isFunction 86 B
utils/isNull 78 B
utils/isNumber 103 B
utils/isObject 115 B
utils/isString 89 B
utils/kebabCase 421 B
utils/leaveTransition 679 B
utils/lerp 120 B
utils/loadImage 245 B
utils/loadLink 776 B
utils/loadScript 697 B
utils/lowerCase 84 B
utils/map 128 B
utils/matrix 150 B
utils/mean 147 B
utils/memo 217 B
utils/noop 62 B
utils/noopValue 76 B
utils/objectToURLSearchParams 253 B
utils/pascalCase 434 B
utils/random 93 B
utils/randomInt 132 B
utils/randomItem 163 B
utils/round 130 B
utils/saveActiveElement 571 B
utils/scrollTo 1.72 kB
utils/selectorFor 2.72 kB
utils/setClassesOrStyles 218 B
utils/smoothTo 2.56 kB
utils/snakeCase 421 B
utils/spring 343 B
utils/throttle 151 B
utils/transform 286 B
utils/transition 577 B
utils/trapFocus 717 B
utils/untrapFocus 587 B
utils/upperCase 84 B
utils/wait 103 B
utils/withLeadingCharacters 142 B
utils/withLeadingSlash 152 B
utils/withTrailingCharacters 143 B
utils/withTrailingSlash 153 B
utils/withoutLeadingCharacters 127 B
utils/withoutLeadingCharactersRecursive 144 B
utils/withoutLeadingSlash 138 B
utils/withoutTrailingCharacters 129 B
utils/withoutTrailingCharactersRecursive 147 B
utils/withoutTrailingSlash 140 B
utils/wrap 154 B
viewTransition 1.66 kB
whenDOMSettled 2.31 kB
withDrag 3.61 kB
withInView 1.76 kB
withMutation 1.78 kB
withRaf 2.31 kB
withResize 1.7 kB
withScrollProgress 3.94 kB
write 125 B

@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.16%. Comparing base (ae3c716) to head (842427d).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #826   +/-   ##
=======================================
  Coverage   97.16%   97.16%           
=======================================
  Files         170      170           
  Lines        4133     4133           
  Branches     1152     1152           
=======================================
  Hits         4016     4016           
  Misses        106      106           
  Partials       11       11           
Flag Coverage Δ
eslint-plugin-js-toolkit 93.79% <ø> (ø)
js-toolkit 97.92% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq

codspeed-hq Bot commented Aug 16, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 21.7%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 140 untouched benchmarks
⏩ 141 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
translate3d only (x, y, z) 294.4 µs 376 µs -21.7%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feature/v4-grouping (842427d) with main (ae3c716)

Open in CodSpeed

Footnotes

  1. 141 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

v3's `withGroup` gave each instance a `$group: Set<Base>` keyed by a group
name on a page-global registry. It is not ported: the set had no value cell,
so a coordinator could read its peers but never learn that one arrived — and
v4 mounts on DOM insertion with no ordering guarantee, which makes "the set of
my peers" a moving target.

`createGroup()` publishes the roster as one `Signal`, in document order.
`join()` returns its leave function, which is already the shape of a
`subscribeContext()` answer and teardown, so a member joins the nearest group
whenever it appears and leaves it before a nearer one takes over. Scoping comes
from nearest-provider-wins, so a nested group takes its own members — the case
`$watchChildren` cannot express.

The spec builds the Disclosure pattern end to end from this helper plus
`$provide` and `subscribeContext` alone, and asserts both late-peer orders: a
disclosure written open that mounts late loses to the one before it in the DOM
and wins over the ones after it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9
Section 5 already said `withGroup` is not ported but left open where
membership itself lives. Answer it: a roster is a value published through the
context system, not a second registry — and note the line the two v3 consumers
divide on, since `Data*` groups by name over a keyed store and keeps its own
records while `Disclosure` groups by ancestry and takes the helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9
@titouanmathis

Copy link
Copy Markdown
Contributor Author

The packages/v4/node_modules finding is stale — it describes a push that has since been amended away.

Verified on the reviewed commit itself (842427d1, the current head):

gh pr diff 826 | grep -c node_modules            → 0
git ls-tree -r origin/feature/v4-grouping        → 0 matches

The PR's file set is eight files, all under packages/v4/: DESIGN.md, package.json, exports.spec.ts, group.ts, group.spec.ts, index.ts, subpaths/createGroup.ts and test/package-node-consumer.js.

The symlink was mine — I created it so the agent worktree could resolve dependencies, and an amend swept it into the commit. It was removed before this head was pushed. Merging.

@titouanmathis
titouanmathis merged commit 555405f into main Aug 16, 2026
11 of 12 checks passed
@titouanmathis
titouanmathis deleted the feature/v4-grouping branch August 16, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant