squadrant 0.16.3 → 0.16.5

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "squadrant",
3
3
  "packageManager": "pnpm@10.30.3",
4
- "version": "0.16.3",
4
+ "version": "0.16.5",
5
5
  "description": "Multi-project orchestration for your coding agents (Claude, Codex, opencode, Gemini)",
6
6
  "type": "module",
7
7
  "bin": {
@@ -228,6 +228,23 @@ This is the captain-side backstop: even if the completion-protocol imperative is
228
228
 
229
229
  When a crew sends you a status message via `squadrant runtime send <project> "<message>"`, it lands in your captain pane. Acknowledge, then update your handoff if a meaningful decision was made.
230
230
 
231
+ ### Handling CREW REVIEW (#599 review gate)
232
+
233
+ CREW REVIEW is **unambiguous** — a crew ran `squadrant crew signal review` after committing its work to `crew/<name>`. Unlike CREW IDLE, this is never a stray heartbeat miss: the crew has explicitly paused and is waiting for your verdict. The task is **NOT terminal** — don't treat it like CREW DONE.
234
+
235
+ On CREW REVIEW:
236
+
237
+ 1. **Open the diff** — `squadrant diff <project> <crew>` (branch-vs-base; the default is exactly the review surface). Use `--staged`/`--unstaged`/`--working` if you also want to peek at anything left uncommitted.
238
+ 2. **Classify:**
239
+
240
+ | Diff looks | Captain action |
241
+ |-----------|-----------------|
242
+ | Good — matches the task, tests pass, no scope creep | `squadrant crew approve <project> <crew>` — pushes `crew/<name>` to origin, opens the PR, terminalizes DONE. |
243
+ | Needs changes | `squadrant crew send <project> <crew> "<feedback>"` — the crew iterates, re-commits, and re-signals `review`. Loop until approved. |
244
+
245
+ 3. **Never auto-terminalize a CREW REVIEW yourself** by emitting `task.done` directly — always go through `squadrant crew approve` so the push+PR actually happens before the task closes.
246
+ 4. Do **not** re-send the original task or close the crew while it's awaiting review — `crew close` on a `review`-state task discards work that hasn't been pushed anywhere yet.
247
+
231
248
  ## When Crew Finishes
232
249
 
233
250
  After a crew task completes:
@@ -0,0 +1,160 @@
1
+ ---
2
+ name: explainer-reel
3
+ description: This skill should be used when the user asks to "animate a system diagram", "make a reel explaining X", "turn this architecture into a GIF", "explain this flow with motion", "make a JWT/auth/cache-style animated explainer", "map who calls whom across these systems", "make an interactive system-flow diagram with bands/swimlanes", or wants either a short looping GIF (dark-neon, monoline, terminal-panel style) or a clickable interactive HTML diagram that explains a structure, system, or flow — for embedding in or alongside HTML/markdown docs. Covers self-contained HTML scene authoring, a dark-neon component library, an HTML→Playwright→FFmpeg GIF pipeline, and a swimlane-band interactive preset that composes with `visual-explainer`. Optional MP4 export via Remotion.
4
+ version: 0.2.0
5
+ ---
6
+
7
+ # explainer-reel
8
+
9
+ Two output modes for explaining a structure/system/flow with motion or interactivity, instead of
10
+ a static diagram:
11
+
12
+ | Mode | Output | Use when |
13
+ |---|---|---|
14
+ | **`reel`** (default) | a short, looping animated **GIF** — dark-neon, thin monoline, terminal-panel chrome | the ask is a passive, embeddable loop (e.g. "how JWT auth works" as a `<img>` in docs) |
15
+ | **`interactive`** | a self-contained **interactive HTML** page — dark IBM Plex dev-console theme, horizontal system bands, click-node detail panel, tabs | the ask is to *explore* who-calls-whom across systems (e.g. "map this request across three services") |
16
+
17
+ Both modes are **wrap-the-engine, build-the-style-pack** — see provenance below. Pick the mode
18
+ from the user's ask; default to `reel` when the request is ambiguous (issue #598's original scope
19
+ and golden reference are `reel` mode).
20
+
21
+ **Provenance:**
22
+ - `mode=reel` *wraps* `iart-ai/explainer-video-skills` (the engine — motion primitives + a
23
+ verify-loop toolkit) with a squadrant style-pack (design tokens + component library + a GIF
24
+ pipeline the engine doesn't ship).
25
+ - `mode=interactive` *wraps* `visual-explainer`'s `generate-web-diagram` (the engine for
26
+ self-contained interactive HTML) with a named **swimlane preset** (bands/particles/detail-panel
27
+ layout grammar) so that command doesn't reinvent the layout each time.
28
+
29
+ See `docs/specs/2026-07-23-animated-system-graph-skill.md` (issue #598, addendum for the two-mode
30
+ scope) for the full build-vs-buy rationale. Don't rebuild either engine's techniques from
31
+ scratch — reuse them; this skill only supplies the style/preset layer on top.
32
+
33
+ ## When to use this vs. plain `visual-explainer`
34
+
35
+ - Plain `visual-explainer` — static or entrance-only diagrams (one-shot reveal on load), or an
36
+ interactive diagram with no particular bands/swimlane shape. Use for most architecture docs,
37
+ plans, and diagrams.
38
+ - `explainer-reel` `mode=reel` — **continuous, looping motion** is the explicit ask: a packet
39
+ traveling a path, a counter climbing, a verify/reject beat.
40
+ - `explainer-reel` `mode=interactive` — the ask specifically wants **bands = systems** with
41
+ network-hop edges and flow-particles (a "swimlane" system-flow map), not a general diagram.
42
+
43
+ ## Mode: `reel` (default)
44
+
45
+ ### Prerequisites
46
+
47
+ 1. **The engine.** If `diagram-animation`'s recipe table isn't already available in this session,
48
+ install it once per project:
49
+ ```bash
50
+ npx skills add iart-ai/explainer-video-skills -a claude-code -s diagram-animation -y
51
+ ```
52
+ This gives you the motion-primitive reference (`references/diagram-and-chart-recipes.md`) for
53
+ node/edge reveals, `offset-path` traveling dots, `stroke-dashoffset` edge-draw, and rAF
54
+ count-ups. This skill's own `assets/scene-kit.js` already implements the specific components you
55
+ need for the dark-neon look — read the engine's recipes when you need a primitive `scene-kit.js`
56
+ doesn't cover yet, rather than inventing new CSS/JS from scratch.
57
+ 2. **Tooling.** `npx` (Playwright auto-fetches Chromium on first use: `npx playwright install
58
+ chromium`) and `ffmpeg`/`ffprobe` on PATH.
59
+
60
+ ### The pipeline (zero-React default)
61
+
62
+ ```
63
+ scene brief → self-contained .html (SVG + GSAP, dark-neon theme, ?t=N seek harness)
64
+ → verify: scripts/seek-shot.sh + scripts/contact-sheet.sh (freeze/tile/eyeball)
65
+ → scripts/render-gif.sh (Playwright frame capture → FFmpeg palettegen/paletteuse)
66
+ → looping .gif
67
+ ```
68
+
69
+ MP4 is optional and secondary — only reach for Remotion (the engine's Heavy tier) if the user
70
+ explicitly wants a social/video export; the GIF path has no React/build dependency.
71
+
72
+ ### 1. Author the scene
73
+
74
+ Start from `examples/jwt-reel.html` — copy it, then swap the nodes/palette/beats for your topic.
75
+ It's a fully worked, self-contained example: theme tokens inlined, `scene-kit.js`-style builders
76
+ inlined, a GSAP master timeline, the `?t=N` seek harness, `prefers-reduced-motion` handling, and
77
+ the `window.__ready` signal the verify scripts wait on. Don't build a scene from a blank file —
78
+ adapt the working one.
79
+
80
+ - Design tokens (colors, fonts, stroke, glow): `references/design-tokens.md` /
81
+ `assets/theme.css`.
82
+ - Component builders (panels, packets, badges, highlight-step, counters, layer stacks, session
83
+ grids): `references/component-library.md` / `assets/scene-kit.js`.
84
+ - Keep the reel-vocabulary shape: nodes have a fixed accent color that never changes meaning,
85
+ a traveling `packet` is the payload, `scenes`/beats are timed and captioned, everything loops.
86
+ - Honor `prefers-reduced-motion`: freeze to the final composed frame, no looping motion (see the
87
+ example's harness code — this mirrors both the engine's and `visual-explainer`'s a11y rule).
88
+
89
+ ### 2. Verify fidelity before rendering
90
+
91
+ ```bash
92
+ scripts/seek-shot.sh your-scene.html 0 <mid> <end>
93
+ scripts/contact-sheet.sh /tmp/sheet.png frame-0.png frame-<mid>.png frame-<end>.png
94
+ ```
95
+ Eyeball the contact sheet: reveal order correct, connectors land on the right nodes, no
96
+ clipped/off-canvas text, color grammar consistent, badges land on the intended frame.
97
+
98
+ ### 3. Render the GIF
99
+
100
+ ```bash
101
+ scripts/render-gif.sh your-scene.html <duration_s> <fps> out.gif [width] [viewport WxH]
102
+ # e.g.
103
+ scripts/render-gif.sh examples/jwt-reel.html 12 15 jwt-reel.gif 480 400,640
104
+ ```
105
+ Pass `[viewport WxH]` matching your scene's stage pixel size for a tight crop (no black margin).
106
+ Cap width ~480–720px and fps ~15–20 to keep GIF size sane; the script already loops seamlessly
107
+ (`-loop 0`) and dithers (`paletteuse=dither=sierra2_4a`) so neon-on-black gradients don't band.
108
+
109
+ ### 4. (Optional) MP4 export
110
+
111
+ Only if the user asks for a social/video export: build the scene as a Remotion composition per
112
+ `diagram-animation`'s Heavy tier, then assert it with the engine's `scripts/probe-mp4.sh`. Not
113
+ required for the GIF path — don't block on it.
114
+
115
+ ### Output contract (`reel`)
116
+
117
+ - Primary: `<name>.gif` — looping, embed-friendly, drops into HTML/markdown.
118
+ - Always also produce: `<name>.html` — the self-contained authoring/preview scene (so it can be
119
+ scrubbed and re-rendered later).
120
+ - Optional: `<name>.mp4` — only on explicit request.
121
+
122
+ ### Golden reference (`reel`)
123
+
124
+ `examples/jwt-reel.html` reproduces the *style/technique* of a JWT auth-flow reel (mint → travel
125
+ client→server → verify badge → tamper → HACKER reject, "STATELESS · NO SESSION STORE" footer) —
126
+ the acceptance demo for this skill (issue #598). It's a technique reproduction, not a copy of any
127
+ specific creator's video: swap the placeholder `@your_handle` chrome for your own before
128
+ publishing, and don't reuse anyone's exact copy/branding.
129
+
130
+ ## Mode: `interactive`
131
+
132
+ A self-contained, clickable HTML page: horizontal **bands = systems**, nodes placed in time order
133
+ within their band, edges that jump a band = a network hop (labeled), continuous flow-particles
134
+ along each edge (CSS `offset-path`), a click-node → detail panel, and tabs for switching between
135
+ flow variants. Full schema + composition instructions: `references/interactive-mode.md`.
136
+
137
+ 1. **Don't build this from scratch or from `visual-explainer`'s general guidance alone.** Start
138
+ from `assets/swimlane-preset.html` — copy it, then replace the `BANDS`/`FLOWS` data with your
139
+ own systems/nodes/edges. It already has the working mechanism (bands, SVG edges + particles,
140
+ detail panel, tabs, "Animate flow", `prefers-reduced-motion` handling).
141
+ 2. Fill in nodes per `references/interactive-mode.md`'s schema (`sys`, `lane`, `col`, title,
142
+ subtitle, and optional `detail`/`path`/`writes`/`flag`/`badge`/`ok`/`svc`).
143
+ 3. Open the result in a browser (or `visual-explainer`'s render step) — no GIF/render pipeline
144
+ needed; the interactive HTML *is* the deliverable.
145
+ 4. If the user also wants a static preview image, one `playwright screenshot` of the default tab
146
+ is enough — don't run the `reel` mode's GIF pipeline for this.
147
+
148
+ ### Output contract (`interactive`)
149
+
150
+ - Primary: `<name>.html` — self-contained, interactive, opens directly in a browser.
151
+ - No GIF/MP4 by default (there's no single "frame" to loop); add a screenshot only if asked.
152
+
153
+ ## Attribution
154
+
155
+ Dark-neon monoline reel style (`mode=reel`) decoded from `@duchminh_nguyen`'s reel series
156
+ (research handoff, `docs/specs/2026-07-23-animated-system-graph-skill.md` §0) — style/technique
157
+ reproduction only. Engine (`diagram-animation`, `seek-shot.sh`, `contact-sheet.sh`) from
158
+ `iart-ai/explainer-video-skills`, MIT license — see `scripts/README.md`. Swimlane preset
159
+ (`mode=interactive`) generalizes the bands/particles/detail-panel mechanism first produced by the
160
+ `visual-explainer` skill — see `references/interactive-mode.md`.
@@ -0,0 +1,196 @@
1
+ /*
2
+ explainer-reel scene-kit — reusable SVG component builders + a GSAP master-timeline helper.
3
+ Reference library: copy the functions you need inline into a scene's single <script> block.
4
+ Depends on GSAP (load from CDN in the scene HTML, matching diagram-animation's stack) and the
5
+ `.er-*` classes from theme.css. See ../references/component-library.md for usage + a full example.
6
+
7
+ Primitives map 1:1 onto the reel's shared vocabulary (spec §0):
8
+ createPanel -> CLIENT/SERVER terminal node
9
+ createFunnel -> "mint" node
10
+ createPacket -> the traveling token/cookie/packet (edge payload)
11
+ travel -> flow-particle: move a packet along an edge
12
+ morphPacket -> content morph (text + accent color)
13
+ badgeCheck / badgeHacker -> pop-in state badges
14
+ magnifier -> verify affordance
15
+ dimOthers -> highlight-step (focus one element, dim the rest)
16
+ countUp -> rAF count-up (Caching Layers style)
17
+ layerStack / sessionGrid -> bonus nodes for the Cookies / Caching Layers scenes
18
+ */
19
+
20
+ const SVG_NS = 'http://www.w3.org/2000/svg';
21
+
22
+ function el(tag, attrs = {}, parent) {
23
+ const n = document.createElementNS(SVG_NS, tag);
24
+ for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v);
25
+ if (parent) parent.appendChild(n);
26
+ return n;
27
+ }
28
+
29
+ /** Chrome: handle / title / footer above and below the SVG stage. Call once per scene. */
30
+ function createChrome(root, { handle, title, footer }) {
31
+ root.insertAdjacentHTML('afterbegin', `
32
+ <div class="er-handle">@${handle}</div>
33
+ <div class="er-title">${title}</div>
34
+ <hr class="er-title-rule">
35
+ `);
36
+ root.insertAdjacentHTML('beforeend', `<div class="er-footer">${footer}</div>`);
37
+ }
38
+
39
+ /** Standard glow filter — call once, then apply class="er-glow" to any accent element. */
40
+ function defineGlow(svg, blur = 4) {
41
+ const defs = el('defs', {}, svg);
42
+ const f = el('filter', { id: 'er-glow', x: '-60%', y: '-60%', width: '220%', height: '220%' }, defs);
43
+ el('feGaussianBlur', { stdDeviation: blur, result: 'blur' }, f);
44
+ const merge = el('feMerge', {}, f);
45
+ el('feMergeNode', { in: 'blur' }, merge);
46
+ el('feMergeNode', { in: 'SourceGraphic' }, merge);
47
+ return f;
48
+ }
49
+
50
+ /** The "mint" node — a thin V-funnel where a packet is spawned. */
51
+ function createFunnel(svg, { x, y, w = 44, h = 40 }) {
52
+ return el('path', {
53
+ class: 'er-mono',
54
+ d: `M ${x - w / 2} ${y} L ${x} ${y + h} L ${x + w / 2} ${y}`,
55
+ }, svg);
56
+ }
57
+
58
+ /** A terminal-style panel node (CLIENT / SERVER): header dots + title + N fake code lines. */
59
+ function createPanel(svg, { x, y, w = 150, h = 90, label, accent, lines = 4, activeLine = 0 }) {
60
+ const g = el('g', { class: 'er-panel', 'data-id': label }, svg);
61
+ el('rect', { class: 'er-panel-rect', x, y, width: w, height: h, stroke: accent }, g);
62
+ el('circle', { class: 'er-panel-dot', cx: x + 12, cy: y + 14, r: 2, fill: accent }, g);
63
+ el('circle', { class: 'er-panel-dot', cx: x + 20, cy: y + 14, r: 2, fill: accent }, g);
64
+ el('circle', { class: 'er-panel-dot', cx: x + 28, cy: y + 14, r: 2, fill: accent }, g);
65
+ el('text', { class: 'er-panel-title', x: x + 38, y: y + 18, fill: accent }, g).textContent = `>>> ${label}`;
66
+ const lineH = (h - 30) / lines;
67
+ for (let i = 0; i < lines; i++) {
68
+ const ly = y + 30 + i * lineH + lineH / 2;
69
+ el('line', { class: 'er-panel-line', x1: x + 12, y1: ly, x2: x + w - 24, y2: ly }, g);
70
+ el('circle', {
71
+ class: 'er-panel-dot', cx: x + w - 12, cy: ly, r: 2.5,
72
+ fill: i === activeLine ? accent : 'var(--er-line-dim)',
73
+ }, g);
74
+ }
75
+ return g;
76
+ }
77
+
78
+ /** The traveling packet: [icon cell | label cell | icon cell], a perforated-ticket pill. */
79
+ function createPacket(svg, { x, y, label, sublabel = '', accent = 'var(--er-line)' }) {
80
+ const g = el('g', { class: 'er-packet', transform: `translate(${x},${y})` }, svg);
81
+ el('rect', { class: 'er-pill-rect', x: -14, y: -12, width: 28, height: 24, stroke: accent }, g);
82
+ for (const dy of [-6, 0, 6]) el('circle', { cx: -14, cy: dy, r: 1.2, fill: accent }, g);
83
+ const body = el('rect', { class: 'er-pill-rect', x: 16, y: -12, width: 64, height: 24, stroke: accent }, g);
84
+ const text = el('text', {
85
+ class: 'er-pill-text', x: 48, y: sublabel ? -1 : 4, 'text-anchor': 'middle', fill: accent,
86
+ }, g);
87
+ text.textContent = label;
88
+ let subEl = null;
89
+ if (sublabel) {
90
+ subEl = el('text', {
91
+ class: 'er-pill-text', x: 48, y: 9, 'text-anchor': 'middle', fill: 'var(--er-line-dim)',
92
+ style: 'font-size:6px',
93
+ }, g);
94
+ subEl.textContent = sublabel;
95
+ }
96
+ el('rect', { class: 'er-pill-rect', x: 84, y: -12, width: 20, height: 24, stroke: accent }, g);
97
+ for (let i = 0; i < 4; i++) {
98
+ el('rect', {
99
+ x: 88 + i * 4, y: 2 - i * 2, width: 2, height: 6 + i * 2, fill: accent,
100
+ }, g);
101
+ }
102
+ return { group: g, body, text, subEl };
103
+ }
104
+
105
+ /** Move a packet group along a straight edge (from -> to), added to the master timeline `tl`. */
106
+ function travel(tl, packet, { from, to, duration = 2, at }) {
107
+ tl.fromTo(packet.group, { x: from.x, y: from.y }, {
108
+ x: to.x, y: to.y, duration, ease: 'power1.inOut',
109
+ }, at);
110
+ }
111
+
112
+ /** Morph a packet's label/sublabel text + accent color (content morph). */
113
+ function morphPacket(tl, packet, { label, sublabel, color }, at) {
114
+ tl.call(() => {
115
+ packet.text.textContent = label;
116
+ if (sublabel && packet.subEl) packet.subEl.textContent = sublabel;
117
+ packet.body.setAttribute('stroke', color);
118
+ packet.text.setAttribute('fill', color);
119
+ }, null, at);
120
+ }
121
+
122
+ /** Verify affordance: a small magnifying-glass glyph pinned near a node. */
123
+ function magnifier(svg, { x, y }) {
124
+ const g = el('g', { class: 'er-magnifier', transform: `translate(${x},${y})`, opacity: 0 }, svg);
125
+ el('circle', { class: 'er-mono', cx: -2, cy: -2, r: 6 }, g);
126
+ el('line', { class: 'er-mono', x1: 2, y1: 2, x2: 7, y2: 7 }, g);
127
+ return g;
128
+ }
129
+
130
+ /** Green check badge pop-in (verified / ok). */
131
+ function badgeCheck(svg, { x, y }) {
132
+ const g = el('g', {
133
+ class: 'er-badge-check er-glow', transform: `translate(${x},${y}) scale(0.6)`, opacity: 0,
134
+ }, svg);
135
+ el('circle', { class: 'er-badge-hex', r: 9, stroke: 'var(--er-ok)' }, g);
136
+ el('path', { d: 'M -4 0 L -1 4 L 5 -5', class: 'er-mono', stroke: 'var(--er-ok)', 'stroke-width': 2 }, g);
137
+ return g;
138
+ }
139
+
140
+ /** Red HACKER hexagon badge pop-in (tamper detected / reject). */
141
+ function badgeHacker(svg, { x, y }) {
142
+ const g = el('g', {
143
+ class: 'er-badge-hacker er-glow', transform: `translate(${x},${y}) scale(0.6)`, opacity: 0,
144
+ }, svg);
145
+ const pts = [0, -12, 10, -6, 10, 6, 0, 12, -10, 6, -10, -6].reduce((s, v, i) =>
146
+ s + (i % 2 === 0 ? `${v},` : `${v} `), '');
147
+ el('polygon', { points: pts, class: 'er-badge-hex', stroke: 'var(--er-danger)' }, g);
148
+ el('circle', { r: 6, class: 'er-badge-hex', stroke: 'var(--er-danger)' }, g);
149
+ el('line', { x1: -4, y1: -4, x2: 4, y2: 4, stroke: 'var(--er-danger)', 'stroke-width': 1.5 }, g);
150
+ el('text', {
151
+ class: 'er-badge-label', x: 0, y: 24, 'text-anchor': 'middle', fill: 'var(--er-danger)',
152
+ }, g).textContent = 'HACKER';
153
+ return g;
154
+ }
155
+
156
+ /** Highlight-step: fade every `.er-panel` except the active one (focus + context). */
157
+ function dimOthers(tl, svg, activeId, at) {
158
+ tl.to(svg.querySelectorAll(`.er-panel:not([data-id="${activeId}"])`), { opacity: 0.35, duration: 0.3 }, at)
159
+ .to(svg.querySelector(`.er-panel[data-id="${activeId}"]`), { opacity: 1, duration: 0.3 }, at);
160
+ }
161
+
162
+ /** rAF-driven count-up for a <text> element (Caching Layers "DB queries" style counter). */
163
+ function countUp(el, to, dur = 1200) {
164
+ const t0 = performance.now();
165
+ (function tick(now) {
166
+ const k = Math.min(1, (now - t0) / dur);
167
+ const e = 1 - Math.pow(1 - k, 3);
168
+ el.textContent = Math.round(to * e);
169
+ if (k < 1) requestAnimationFrame(tick);
170
+ })(t0);
171
+ }
172
+
173
+ /** Bonus node: vertical layer stack (Caching Layers scene) — client / cache / redis / db. */
174
+ function layerStack(svg, { x, y, w = 140, layers }) {
175
+ const g = el('g', { class: 'er-layer-stack' }, svg);
176
+ layers.forEach((layer, i) => {
177
+ const ly = y + i * 56;
178
+ el('rect', { class: 'er-panel-rect', x, y: ly, width: w, height: 40, stroke: layer.accent }, g);
179
+ el('text', { class: 'er-panel-title', x: x + 10, y: ly + 24, fill: layer.accent }, g).textContent = layer.label;
180
+ });
181
+ return g;
182
+ }
183
+
184
+ /** Bonus node: session-store grid (Cookies scene) — a cell lights up green when a session lands. */
185
+ function sessionGrid(svg, { x, y, cols = 5, rows = 2, cell = 18, gap = 4 }) {
186
+ const g = el('g', { class: 'er-session-grid' }, svg);
187
+ const cells = [];
188
+ for (let r = 0; r < rows; r++) {
189
+ for (let c = 0; c < cols; c++) {
190
+ cells.push(el('rect', {
191
+ class: 'er-mono', x: x + c * (cell + gap), y: y + r * (cell + gap), width: cell, height: cell, rx: 2,
192
+ }, g));
193
+ }
194
+ }
195
+ return { group: g, cells };
196
+ }