ivue 2.5.0 → 2.6.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.
@@ -270,31 +270,50 @@ defineExpose(box as Box.Instance);
270
270
  </template>
271
271
  ```
272
272
 
273
- ## The namespace carries the WHOLE contract (one seam)
274
-
275
- A class FILE has exactly three residents: imports, the class, the
276
- namespace. Everything else the module owns lives INSIDE the namespace,
277
- in canonical section order:
278
-
279
- - **Identity** `$Class` (raw, for children to extend), `Class`
280
- (`Reactive()`, for you to `new`), `Instance` (and `Model` when used).
281
- - **Values** the component contract as plain data: `propsTypes`
282
- (defineComponent-style, no defaults, wrapped in `definePropTypes({...})`
283
- so the `required: true` literal survives `typeof`), `propsDefaults`
284
- (plain values, typed by `ExtractPropDefaultTypes<typeof propsTypes>` —
285
- required props are filtered out of the check automatically, and a
286
- deliberately default-free optional prop is declared `key: undefined`,
287
- stating the ruling in data), `props =
288
- propsWithDefaults(propsDefaults, propsTypes)`, and `emits`
289
- (object-declared validators). Module constants live here too — a
290
- value the module keeps to itself is a NON-EXPORTED namespace member,
291
- private to the file. Nothing lives at module level beside the seam.
292
- - **Types** DERIVED from the values, never hand-duplicated:
293
- `Props` is `ExtractPropTypes<typeof props>` (a generic component
273
+ ## The class carries the WHOLE contract; the namespace is identity and types
274
+
275
+ A class FILE is a SINGLE-FILE MODEL the model-side twin of the
276
+ single-file component. It has exactly three residents: imports, the
277
+ class, the namespace. The component contract — prop types, prop defaults, their
278
+ fusion, emits, and every tuning constant — lives ON THE CLASS as static
279
+ getters, beside the state and behavior it governs. The namespace holds
280
+ identity and TYPES only, every type DERIVED from `$Class`. Two worlds
281
+ would make a class half extensible: a `const` in a namespace cannot be
282
+ overridden by a subclass, is not inherited, and does not swap with
283
+ `Class` under a global override so a runtime declaration never lives
284
+ there.
285
+
286
+ - **Contract (on the class, static)** — `static get propsTypes()`
287
+ (defineComponent-style, no defaults, returned through
288
+ `definePropTypes({...})` so the `required: true` literal survives
289
+ `typeof`); `static get propsDefaults()` (plain values, annotated
290
+ `ExtractPropDefaultTypes<typeof $X.propsTypes>` required props are
291
+ filtered out of the check automatically, and a deliberately
292
+ default-free optional prop is declared `key: undefined`, stating the
293
+ ruling in data); `static get props()` the ONE fusion line,
294
+ `propsWithDefaults(this.propsDefaults, this.propsTypes)`, reading
295
+ through the receiver so a subclass's `props` fuses ITS types and
296
+ defaults; `static get emits()` (object-declared validators). Tuning
297
+ constants are plain static getters too — live knobs a subclass or test
298
+ double overrides; the `$` prefix stays reserved for compute-once caches.
299
+ Types and defaults stay two members ON PURPOSE: a variant re-tunes
300
+ defaults without re-typing. A nested object prop (a knobs tree) is
301
+ filled from the defaults at every depth with `nestedProps(props,
302
+ this.self.propsDefaults)` from `ivue/extras`, once, in the
303
+ constructor (in place — lodash's `defaultsDeep` with arrays taken
304
+ whole); the class reads complete props and never merges in a getter.
305
+ Vue itself never merges a supplied object with its default.
306
+ - **Identity (namespace)** — `$Class` (raw, for children to extend),
307
+ `Class` (`Reactive()`, for you to `new`), `Instance` (and `Model` when
308
+ used).
309
+ - **Types (namespace)** — DERIVED from the class, never hand-duplicated:
310
+ `Props` is `ExtractPropTypes<typeof $Class.props>` (a generic component
294
311
  grafts its parameter back over the one prop a runtime map cannot
295
- carry: `Omit<ExtractPropTypes<typeof props>, 'modelValue'> &
296
- { modelValue: T[] }`); `Emits` is `ExtractEmitTypes<typeof emits>`;
297
- `Slots`; `Exposed` is `ShallowUnwrapRef<Instance>`.
312
+ carry: `Omit<ExtractPropTypes<typeof $Class.props>, 'modelValue'> &
313
+ { modelValue: T[] }`); `Emits` is `ExtractEmitTypes<typeof
314
+ $Class.emits>`; `Slots`; `Exposed` is `ShallowUnwrapRef<Instance>`.
315
+ Domain types the contract refers to (an item shape, a variant preset)
316
+ live here as namespace types; the class reads them as `X.Item`.
298
317
 
299
318
  Combined — the canonical file, everything above in one shape:
300
319
 
@@ -308,10 +327,63 @@ import {
308
327
  type ExtractEmitTypes,
309
328
  type ExtractPropDefaultTypes,
310
329
  } from 'ivue';
330
+ import { Static } from 'ivue/extras';
311
331
 
312
332
  class $Box {
313
- // Forward references into the namespace below legal: type positions
314
- // resolve non-positionally, and the class never reads a contract VALUE.
333
+ /* Contract STATIC: owned by the class, extended with `super` */
334
+
335
+ /** 1 — the TYPES: a defineComponent-style object, no defaults inside.
336
+ * definePropTypes is an identity call that keeps `required: true` a
337
+ * LITERAL — a bare object widens it to boolean, which would blind the
338
+ * defaults check below. */
339
+ static get propsTypes() {
340
+ return definePropTypes({
341
+ title: { type: String as PropType<string>, required: true },
342
+ size: { type: Number as PropType<number> },
343
+ maxHeight: { type: Number as PropType<number> },
344
+ disabled: { type: Boolean as PropType<boolean> },
345
+ });
346
+ }
347
+
348
+ /** 2 — the DEFAULTS: plain values, typed against the types object.
349
+ * Required props (`title`) are filtered out of the check
350
+ * automatically; every OPTIONAL prop must appear — `undefined` is the
351
+ * explicit "no default ON PURPOSE" ruling, stated in data. */
352
+ static get propsDefaults(): ExtractPropDefaultTypes<typeof $Box.propsTypes> {
353
+ return {
354
+ size: this.defaultSize,
355
+ maxHeight: undefined, // unset = unbounded — deliberately default-free
356
+ disabled: false,
357
+ };
358
+ }
359
+
360
+ /** 3 — the FUSION: a standard Vue props object, ready for defineProps.
361
+ * Reads through the receiver — a subclass's `props` fuses ITS own
362
+ * types and defaults. Written once per hierarchy; a subclass that ADDS
363
+ * props re-declares this one line so its derived `Props` widens (a
364
+ * static's return type is not polymorphic). */
365
+ static get props() {
366
+ return propsWithDefaults(this.propsDefaults, this.propsTypes);
367
+ }
368
+
369
+ static get emits() {
370
+ return {
371
+ close: (title: string) => true,
372
+ };
373
+ }
374
+
375
+ /** A tuning constant: a LIVE static knob (no `$`), overridable. */
376
+ static get defaultSize() {
377
+ return 400;
378
+ }
379
+
380
+ /** The one cast per class: instance code reads its own statics here. */
381
+ protected get self() {
382
+ return this.constructor as typeof $Box;
383
+ }
384
+
385
+ // Type positions resolve non-positionally — the class names its own
386
+ // namespace's derived types freely.
315
387
  constructor(
316
388
  public props: Box.Props,
317
389
  public emit: Box.Emits,
@@ -333,48 +405,14 @@ class $Box {
333
405
  export namespace Box {
334
406
  /* Identity */
335
407
 
336
- export const $Class = $Box; // raw — children `extends` this
408
+ export const $Class = Static($Box); // anchorit declares statics; children `extends` this
337
409
  export let Class = Reactive($Class); // reactive — you `new` this
338
410
  export type Instance = typeof Class.Instance; // defineExpose type & reactive() interop
339
411
 
340
- /* Values */
341
-
342
- // A value the module keeps to itself: NON-EXPORTED — private to the
343
- // file, invisible to importers. Never a module-level const.
344
- const DEFAULT_SIZE = 400;
345
-
346
- /** 1 — the TYPES: a defineComponent-style object, no defaults inside.
347
- * definePropTypes is an identity call that keeps `required: true` a
348
- * LITERAL — a bare const widens it to boolean, which would blind the
349
- * defaults check below. */
350
- export const propsTypes = definePropTypes({
351
- title: { type: String as PropType<string>, required: true },
352
- size: { type: Number as PropType<number> },
353
- maxHeight: { type: Number as PropType<number> },
354
- disabled: { type: Boolean as PropType<boolean> },
355
- });
356
-
357
- /** 2 — the DEFAULTS: plain values, typed against the types object.
358
- * Required props (`title`) are filtered out of the check
359
- * automatically; every OPTIONAL prop must appear — `undefined` is the
360
- * explicit "no default ON PURPOSE" ruling, stated in data. */
361
- export const propsDefaults: ExtractPropDefaultTypes<typeof propsTypes> = {
362
- size: DEFAULT_SIZE,
363
- maxHeight: undefined, // unset = unbounded — deliberately default-free
364
- disabled: false,
365
- };
366
-
367
- /** 3 — the MERGE: a standard Vue props object, ready for defineProps. */
368
- export const props = propsWithDefaults(propsDefaults, propsTypes);
412
+ /* Types — DERIVED from the class's statics, never hand-duplicated */
369
413
 
370
- export const emits = {
371
- close: (title: string) => true,
372
- };
373
-
374
- /* Types — DERIVED from the values, never hand-duplicated */
375
-
376
- export type Props = ExtractPropTypes<typeof props>;
377
- export type Emits = ExtractEmitTypes<typeof emits>;
414
+ export type Props = ExtractPropTypes<typeof $Class.props>;
415
+ export type Emits = ExtractEmitTypes<typeof $Class.emits>;
378
416
 
379
417
  export interface Slots {
380
418
  default: (scope: { title: string }) => any;
@@ -385,37 +423,63 @@ export namespace Box {
385
423
  }
386
424
  ```
387
425
 
388
- The SFC is pure wiring against the seam. The macros receive the
389
- RUNTIME objects, so no compiler macro ever resolves a cross-file type:
426
+ The SFC is pure wiring against the seam, and it reads the contract
427
+ through `Class` the mutable slot so a global override swaps the
428
+ contract together with the runner. The macros receive RUNTIME objects,
429
+ so no compiler macro ever resolves a cross-file type:
390
430
 
391
431
  ```ts
392
- const props = defineProps(Box.props); // non-generic: the type is inferred
393
- const emit = defineEmits(Box.emits) as Box.Emits;
432
+ const props = defineProps(Box.Class.props); // non-generic: the type is inferred
433
+ const emit = defineEmits(Box.Class.emits) as Box.Emits;
394
434
  defineSlots<Box.Slots>();
395
435
  // generic components cast the one graft:
396
- // defineProps(X.props) as unknown as X.Props<T>
436
+ // defineProps(X.Class.props) as unknown as X.Props<T>
397
437
  ```
398
438
 
399
- A subclass composes its surface the way it composes behavior — by
400
- SPREADING the parent's maps and overriding only what defines the
401
- specialization, with the reason on the line:
439
+ A subclass extends its contract the way it extends behavior — with
440
+ `super`, overriding only what defines the specialization, with the
441
+ reason on the line. Re-tuning a default needs ONE override; adding a
442
+ prop needs the types override plus the one-line `props` re-declaration:
402
443
 
403
444
  ```ts
404
- export const propsTypes = { ...Box.propsTypes };
405
- export const propsDefaults = {
406
- ...Box.propsDefaults,
407
- assumedSize: 300, // cards are hundreds of px wide; rows were tens tall
408
- };
409
- export const props = propsWithDefaults(propsDefaults, propsTypes);
445
+ class $CardBox extends Box.$Class {
446
+ static override get propsDefaults(): typeof Box.$Class.propsDefaults {
447
+ return {
448
+ ...super.propsDefaults,
449
+ size: 300, // cards are hundreds of px wide; rows were tens tall
450
+ };
451
+ }
452
+ }
453
+
454
+ class $TaggedBox extends Box.$Class {
455
+ static override get propsTypes() {
456
+ return definePropTypes({
457
+ ...super.propsTypes,
458
+ tag: { type: String as PropType<string> },
459
+ });
460
+ }
461
+ static override get propsDefaults(): ExtractPropDefaultTypes<typeof $TaggedBox.propsTypes> {
462
+ return { ...super.propsDefaults, tag: '' };
463
+ }
464
+ static override get props() {
465
+ return propsWithDefaults(this.propsDefaults, this.propsTypes); // widens TaggedBox.Props
466
+ }
467
+ }
410
468
  ```
411
469
 
412
- **Two tiers, one seam.** A small contract (roughly under fifteen props)
413
- is authored inline in the namespace. A LARGE surface earns a sibling
414
- `XProps.ts`: authored there, imported ONLY by its own class file and by
415
- extending contract files, and re-exported through the namespace 1:1
416
- the re-export block is the seam's table of contents. Consumers never
417
- import `XProps.ts`; the namespace stays the whole truth either way.
418
- File placement is expression; the one-seam rule is the invariant.
470
+ The anchor rule is unchanged and now reaches every component class: a
471
+ class that DECLARES statics — and the contract is statics anchors at
472
+ `$Class` with `Static()` (`export const $Class = Static($Box)`), and so
473
+ does a subclass that overrides one. A subclass that only inherits stays
474
+ raw. The anchor costs nothing on getters (native reads) and is what
475
+ gives a `$`-cached static its compute-once semantics.
476
+
477
+ **One seam, any size.** A contract of forty documented props is still
478
+ authored on its class — a static getter scrolls like any other member,
479
+ and a sibling `XProps.ts` would be the parallel world again (a second
480
+ runtime owner the class mechanics cannot reach). Shared base surfaces
481
+ are a base CLASS (`class $ChooseField extends Field.$Class`), never a
482
+ spread-in const: inheritance is the only composition the contract uses.
419
483
 
420
484
  **Overrides say so out loud.** `noImplicitOverride` is on: every member
421
485
  that overrides a base member carries the `override` keyword
@@ -460,6 +524,17 @@ belong in plain getters, setup work belongs in the constructor, and event
460
524
  handlers belong in methods — even when the handler only normalizes a DOM event
461
525
  before delegating to a domain model.
462
526
 
527
+ **One DOM event, one handler, named for the event.** A template never binds
528
+ two events to the same method (`@pointerup="x.onUp" @pointercancel="x.onUp"`),
529
+ and a class never registers one method for two event types. A cancel gets
530
+ `onPointerCancel`, whose body may be one line delegating to `onPointerUp`;
531
+ a track's `touchstart` and `touchmove` get `onTrackTouchStart` and
532
+ `onTrackTouchMove` even when both only claim the touch. The reason is the
533
+ override seam: a subclass that must treat a cancel differently can override
534
+ `onPointerCancel` alone, where a shared handler would make it re-derive
535
+ which event it is handling from the event object — and the standard's
536
+ whole point is that behavior extends by name.
537
+
463
538
  When building on a class-backed component, **extend its class, not its
464
539
  `<script setup>`**. Add behavior to the existing class when it belongs to the
465
540
  same component contract. When it is a real specialization, subclass the raw
@@ -527,7 +602,10 @@ call site. Rules that keep it clean:
527
602
  In ordinary Vue this discipline costs a `computed()` per condition, so
528
603
  nobody keeps it; here a named plain getter costs zero bytes, so there is
529
604
  no excuse. Templates read as prose: bindings, names, and events — never
530
- expressions.
605
+ expressions. The split is stage directions and script: the template says
606
+ who is on stage and what happens when someone acts; the class says what
607
+ everything MEANS. Structure stays in the template, meaning moves to the
608
+ class.
531
609
  - **The rule covers EVERY binding kind, not just `v-if`** — the common
532
610
  leaks are display strings, disabled states, and class objects:
533
611
 
@@ -553,6 +631,8 @@ created in a callback — watchers go in the instance's OWN scope, and the
553
631
  owner of its lifetime disposes it:
554
632
 
555
633
  ```ts
634
+ import { Reactive, type ReactiveHelpers } from 'ivue';
635
+
556
636
  class $Session {
557
637
  get user() {
558
638
  return ref<User | null>(null);
@@ -615,6 +695,12 @@ export namespace Session {
615
695
  export type Instance = typeof Class.Instance;
616
696
  }
617
697
 
698
+ // The engine installs $watch / $watchEffect / $stopEffects at Reactive(),
699
+ // AFTER the class body was typed — so a class that calls them merges the
700
+ // helpers into its own instance type. One line, zero runtime; the gate
701
+ // reads it as the class's second half, never as a stray type.
702
+ interface $Session extends ReactiveHelpers {}
703
+
618
704
  // The owner disposes — the class's own method, like any other:
619
705
  session.dispose();
620
706
  ```
@@ -636,6 +722,7 @@ session.dispose();
636
722
  | ✅ `defineExpose(box as X.Instance)` | ❌ `defineExpose(box)` raw — readonly-accessor writes will type-error for consumers |
637
723
  | ✅ constructor runs init; register hooks/watchers there | ❌ add an `init()` method expecting auto-call — ivue never calls it |
638
724
  | ✅ plain `watch` in component-scoped constructors; `$watch` + a `$stopEffects` dispose path for outliving instances | ❌ default to `this.$watch` in a component-scoped class — its scope silently outlives unmount |
725
+ | ✅ a class that calls `this.$watch` / `$watchEffect` / `$stopEffects` merges the engine's helpers beside itself: `interface $X extends ReactiveHelpers {}` (one line, zero runtime) | ❌ `(this as any).$watch(...)` or per-member `declare $watch: …` lines — the body should typecheck without a cast |
639
726
  | ✅ compose cleanup as an ordinary method — `dispose() { /* non-Vue cleanup */ this.$stopEffects(); }` | ❌ expect a teardown hook — ivue auto-calls NOTHING (no `init()`, no `stopEffects()`) |
640
727
  | ✅ a class with static members anchors them: `const $Class = Static($X)` (`ivue/extras`) | ❌ `extends X.Class` — the mutable slot is an eager snapshot of one generation; always extend `$Class` |
641
728
  | ✅ `protected` for every internal member — subclasses reach every seam | ❌ `private` anywhere in an ivue class — it forbids only the legitimate extender |
@@ -755,8 +842,21 @@ threads one object through every component and constructor signature it
755
842
  crosses; the store pattern deletes the thread.
756
843
 
757
844
  ```ts
758
- // app/AppStore.ts — the store IS an ivue class; `use()` owns the singleton
845
+ // app/AppStore.ts — the store IS an ivue class; a static owns the singleton
846
+ // (imports: Reactive from 'ivue'; Static from 'ivue/extras')
759
847
  class $AppStore {
848
+ // The ONE instance, as a `$`-static: constructed on first read, after
849
+ // the app exists, and cached on the receiver. It constructs through the
850
+ // namespace slot, so a test double swapped into `Class` is what gets
851
+ // built — the store has one receiver, the slot, so nothing forks.
852
+ protected static get $shared(): AppStore.Instance {
853
+ return new AppStore.Class();
854
+ }
855
+
856
+ static use(): AppStore.Instance {
857
+ return this.$shared;
858
+ }
859
+
760
860
  get authenticated() {
761
861
  return ref(false);
762
862
  }
@@ -767,14 +867,9 @@ class $AppStore {
767
867
  }
768
868
 
769
869
  export namespace AppStore {
770
- export const $Class = $AppStore;
771
- export let Class = Reactive($Class);
870
+ export const $Class = Static($AppStore); // anchor — it declares statics
871
+ export let Class = Reactive($Class); // reactive — use() does the one `new`
772
872
  export type Instance = typeof Class.Instance;
773
-
774
- let singleton: Instance | null = null;
775
- export function use(): Instance {
776
- return (singleton ??= new Class());
777
- }
778
873
  }
779
874
  ```
780
875
 
@@ -784,7 +879,7 @@ Consumers never receive it — they REACH for it:
784
879
  // any model — the `$`-getter caches the store per instance, forever
785
880
  class $SubscribersModel {
786
881
  protected get $app() {
787
- return AppStore.use();
882
+ return AppStore.Class.use();
788
883
  }
789
884
 
790
885
  async refresh() {
@@ -802,7 +897,7 @@ class $SubscribersModel {
802
897
  // any component — call use() directly; no prop, no provide/inject
803
898
  import { AppStore } from '../app/AppStore';
804
899
 
805
- const app = AppStore.use();
900
+ const app = AppStore.Class.use();
806
901
  const { authenticated } = app;
807
902
  </script>
808
903
 
@@ -815,13 +910,25 @@ Why this shape and not alternatives:
815
910
 
816
911
  - **`use()` is lazy** — the singleton constructs on first touch, after the
817
912
  app exists, so module-load order and circular imports stay non-events
818
- (the same late-read property as every cross-module reference).
913
+ (the same late-read property as every cross-module reference). It lives
914
+ in a `$`-static on the class — never a namespace `let`, which is a
915
+ parallel world no subclass can reach (the gate's
916
+ `the_namespace_holds_identity_and_types_only` check refuses it). A
917
+ `$`-static caches per receiver, and a store reached only through
918
+ `X.Class.use()` has one receiver, so nothing forks; `LazyShared` is for
919
+ a REGISTRY that several receivers (subclasses) must share.
819
920
  - **The `$`-getter is the injection point** — cached whole, per instance,
820
921
  on first read. A model names its dependency once; every method reads
821
922
  `this.$app` with zero lookup cost and zero constructor plumbing.
923
+ - **A store may publish itself as a `reactive()` view** — `use()` stays
924
+ the one door; the `$`-static behind it returns
925
+ `reactive(new AppStore.Class() as AppStore.Instance)` and consumers read
926
+ and write with no `.value`. The `as Instance` cast is the interop form
927
+ the gate sanctions (it is what makes the unwrapped writes typecheck);
928
+ bare `reactive(new …)` is refused.
822
929
  - **Tests swap the slot, not the callers** — `AppStore.Class = $TestStore`
823
- before the first `use()` (or reset the singleton) and every consumer
824
- gets the double through the same seam.
930
+ before the first `use()` and every consumer, calling
931
+ `AppStore.Class.use()`, gets the double through the same seam.
825
932
  - A store is component-OUTLIVING by definition: watchers inside it use
826
933
  `this.$watch`/`$watchEffect`, never plain `watch`, and lifecycle hooks
827
934
  never belong in it.
@@ -1029,6 +1136,26 @@ export namespace Settings {
1029
1136
 
1030
1137
  No static members → no wrapper: `$Class = $X`, the standard form unchanged.
1031
1138
 
1139
+ **Hot loops read the method through the accessor — hoist it, not the
1140
+ class.** The bound method itself is plain-function speed (measured,
1141
+ Chromium, 9M calls, fresh page per variant: module function 31.7 ms,
1142
+ hoisted bound method 30.0 ms); the ONLY per-call cost is re-reading it
1143
+ through the accessor inside the loop (84.6 ms same loop — the
1144
+ own-property guard that buys per-receiver binding). Ordinary call
1145
+ frequency never notices. In a million-call loop, destructure once,
1146
+ INSIDE the function:
1147
+
1148
+ ```ts
1149
+ // one accessor read per method — a late read of the mutable slot,
1150
+ // so a swapped-in subclass is still honored
1151
+ const { isDataCol, numDataValue } = FlyweightLogic.Class;
1152
+ for (let row = 0; row < ROWS_1M; row++) sum += numDataValue(row, col) ?? 0;
1153
+ ```
1154
+
1155
+ Never hoist at module scope (captures today's `Class` forever, blind to
1156
+ swaps) and never reach for `$Class` as a "fast path" — the raw class
1157
+ skips per-receiver binding, which is the capability seam itself.
1158
+
1032
1159
  ## Reading your own statics — the ladder
1033
1160
 
1034
1161
  `Reactive(X) === X`, so a namespace's `Class` slot IS the base class. A getter
@@ -1126,6 +1253,11 @@ like prose — don't ruin it with letter soup:
1126
1253
  - Abbreviate only when the abbreviation IS the domain term (`px`, `id`,
1127
1254
  `fx`, A1-notation like `startRow`/`endCol`).
1128
1255
  - Tests are code — the same rules apply to specs.
1256
+ - **A `v-for` alias is a declaration the template makes** — the same
1257
+ rule: `v-for="(cell, columnIndex) in sheet.grid[row]"`, never
1258
+ `(cell, ci) in sheet.grid[r]`. The template is read by the same
1259
+ people as the class; `r`, `c`, `ci` cost them the same re-derivation
1260
+ there.
1129
1261
 
1130
1262
  ```ts
1131
1263
  // ❌ const v = this.cellVersions.get(k);
@@ -1153,7 +1285,7 @@ Constants use one form per role:
1153
1285
 
1154
1286
  | Role | Form |
1155
1287
  | --- | --- |
1156
- | Tunable or overridable class constant | `static get SCREAMING_SNAKE_CASE()` |
1288
+ | Tunable or overridable class constant — a literal, or a tree composed of other SCREAMING constants (`{ mouse: Selection.Class.AUTOSCROLL_MOUSE }`) | `static get SCREAMING_SNAKE_CASE()` |
1157
1289
  | Protocol or byte constant on a hot path, never overridden | `static readonly SCREAMING_SNAKE_CASE` with a one-line hot-path comment |
1158
1290
  | Contributor or pane identity data | Instance `readonly lowerCamelCase` field |
1159
1291
  | Extensible constructed dependency | Field assigned from a prototype `createX()` factory method |
@@ -1222,12 +1354,14 @@ convention and check it in review.
1222
1354
  - [ ] Watch sources are the FUNCTION form; component-scoped constructors use plain `watch`/`watchEffect`; `this.$watch`/`this.$watchEffect` only for component-outliving instances — each with a dispose path (`$stopEffects()` owner or `onScopeDispose` auto-wire).
1223
1355
  - [ ] Lifecycle hooks / init logic live in the constructor (no `init()` expecting auto-call); template refs guarded with `?.` where read pre-mount.
1224
1356
  - [ ] Every `computed()`/constructor-watch CALLBACK delegates to a method (`computed(() => this.recalculate())`) — no logic inlined in reactive closures; the arrow form, never `computed(this.method)`.
1225
- - [ ] Identifiers are unfolded to domain words (`row`/`col`/`cell`/`cellValue`/`versionRef`…), loop indices and specs included — no single-letter names, no name meaning different things in different methods.
1357
+ - [ ] Identifiers are unfolded to domain words (`row`/`col`/`cell`/`cellValue`/`versionRef`…), loop indices, `v-for` aliases and specs included — no single-letter names, no name meaning different things in different methods.
1226
1358
  - [ ] Keyed/sparse state uses the Map-of-refs shape (get-or-create on read, peek-only bump on write, explicit release path) — never one getter per key, never a deep `reactive()` collection.
1227
1359
  - [ ] Static members are anchored (`const $Class = Static($X)`); `$`-prefixed static getters are compute-once-per-receiver caches, non-`$` statics stay live knobs, and inheritance extends `$Class` — never the mutable `Class`.
1360
+ - [ ] Million-call loops over a `Static()` class destructure the bound methods once inside the function (never module-scope, never `$Class`); `Class.method()` stays the form everywhere else.
1228
1361
  - [ ] Instance reads of own statics go through `this.self` (declared once per class needing it, cast to `typeof $X`, plain getter never `$self`); 2+ reads or loops hoist `const self = this.self`; no per-site `this.constructor` casts; `Namespace.Class` reads stay reserved for late-bound capability dispatch.
1229
1362
  - [ ] Static members precede the constructor; the constructor precedes state, prop, and derived getters; methods come last.
1230
1363
  - [ ] Spacing carries meaning: declaration-like getters contiguous within their group; blank lines only where a doc comment / multi-line body / category boundary begins; methods always separated.
1231
- - [ ] The namespace is the ONE seam: no module-level consts beside imports/class/namespace (file-private values are non-exported namespace members); the contract is namespace data in Identity Values Types order, its types derived from its values; a large contract's sibling `XProps.ts` is imported only by its class file and extending contract files.
1364
+ - [ ] A class that calls `this.$watch` / `$watchEffect` / `$stopEffects` merges the engine's helpers beside itself `interface $X extends ReactiveHelpers {}` so the body typechecks (never `(this as any)`, never per-member `declare` lines).
1365
+ - [ ] The class carries the WHOLE contract as static getters (`propsTypes`, `propsDefaults`, the one-line `props` fusion, `emits`, tuning knobs) and the namespace holds identity and types ONLY, every type derived from `$Class`; no module-level consts or TYPE declarations beside imports/class/namespace (every type a class file declares is a namespace member, read as `X.Name`), no `const`, `let`, or `function` of any kind in the namespace — contract data, tuning knobs, seed data, singletons (`use()`), helpers all live on the class as statics (the gate's `the_namespace_holds_identity_and_types_only` check enforces it), no sibling `XProps.ts`; the SFC reads `X.Class.props` / `X.Class.emits`; a subclass extends the contract with `super` and re-declares the fusion line only when it ADDS props.
1232
1366
  - [ ] Every member that overrides a base member carries `override` (with `noImplicitOverride` enabled).
1233
1367
  - [ ] No `private` members — internal members are `protected` (three-tier visibility: public = consumer surface, protected = hierarchy seam, private = banned).
@@ -26,7 +26,7 @@ import { GeneratorStandard } from './ivue-generator-standard';
26
26
  // domain-invariant: $CheckStandard — If a check is in the manifest, then its proofs entry carries the claim, the impossibility, and at least one red and one green arm
27
27
  test('the shipped constitution is complete for every manifest check', () => {
28
28
  const GateClass = Gate.CheckStandard.Class;
29
- expect(GateClass.checks.length).toBe(30);
29
+ expect(GateClass.checks.length).toBe(32);
30
30
  const report = GateClass.prove({ completenessOnly: true });
31
31
  expect(report.problems).toEqual([]);
32
32
  for (const check of GateClass.checks) {
@@ -55,7 +55,7 @@ test('the generator standard is the extension mechanism eating its own cooking',
55
55
  // ten methodology checks arrive the same way a house check does:
56
56
  // getters + checks + proofs on a subclass — fully proven, opt-in
57
57
  const GeneratorClass = GeneratorStandard.Class;
58
- expect(GeneratorClass.checks.length).toBe(40);
58
+ expect(GeneratorClass.checks.length).toBe(42);
59
59
  const report = GeneratorClass.prove({ completenessOnly: true });
60
60
  expect(report.problems).toEqual([]);
61
61
  // the base stays ivue-only: no header check leaks upward
@@ -0,0 +1,37 @@
1
+ [
2
+ {
3
+ "path": "examples/playground/src/examples/benchmarks/creationBench.ts",
4
+ "check": "a_public_class_publishes_its_namespace_manifest",
5
+ "reason": "benchmark arms: PlainBox and reactive(new PlainBox()) ARE the measured competitors; changing their shape changes the numbers the docs quote"
6
+ },
7
+ {
8
+ "path": "examples/playground/src/examples/benchmarks/creationBench.ts",
9
+ "check": "construction_goes_through_the_namespace_class_slot",
10
+ "reason": "benchmark arms: PlainBox and reactive(new PlainBox()) ARE the measured competitors; changing their shape changes the numbers the docs quote"
11
+ },
12
+ {
13
+ "path": "examples/playground/src/ivue.ts",
14
+ "check": "declarations_use_full_descriptive_names",
15
+ "reason": "vendored copy of the engine (lib/Reactive.ts, synced by sync:examples) \u2014 the engine keeps its own conventions"
16
+ },
17
+ {
18
+ "path": "examples/playground/src/lenis/Emitter.ts",
19
+ "check": "declarations_use_full_descriptive_names",
20
+ "reason": "vendored third-party (the Lenis smooth-scroll library, ported as-is) \u2014 not teaching code"
21
+ },
22
+ {
23
+ "path": "examples/playground/src/lenis/Lenis.ts",
24
+ "check": "declarations_use_full_descriptive_names",
25
+ "reason": "vendored third-party (the Lenis smooth-scroll library, ported as-is) \u2014 not teaching code"
26
+ },
27
+ {
28
+ "path": "examples/playground/src/lenis/LenisUtils.ts",
29
+ "check": "declarations_use_full_descriptive_names",
30
+ "reason": "vendored third-party (the Lenis smooth-scroll library, ported as-is) \u2014 not teaching code"
31
+ },
32
+ {
33
+ "path": "docs_v2/.vitepress/theme/components/grid/GridBenchmark.vue",
34
+ "check": "one_handler_per_event",
35
+ "reason": "benchmark arms: the composable and the ivue grid ARE the measured competitors, bound identically on purpose; giving either its own per-event methods changes the shape the numbers compare"
36
+ }
37
+ ]