ivue 2.4.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.
@@ -145,7 +145,7 @@ class $Box {
145
145
  // STORE / COMPOSABLE — `$`-getter caches WHOLE, forever, per
146
146
  // instance. Resolves on first touch (after Pinia/app ready);
147
147
  // circular-import safe.
148
- private get $project() {
148
+ protected get $project() {
149
149
  return useProjectStore();
150
150
  }
151
151
  get projectId() {
@@ -270,6 +270,244 @@ defineExpose(box as Box.Instance);
270
270
  </template>
271
271
  ```
272
272
 
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
311
+ grafts its parameter back over the one prop a runtime map cannot
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`.
317
+
318
+ Combined — the canonical file, everything above in one shape:
319
+
320
+ ```ts
321
+ // Box.ts — the whole module: imports, the class, the namespace. Nothing else.
322
+ import type { ExtractPropTypes, PropType, ShallowUnwrapRef } from 'vue';
323
+ import {
324
+ definePropTypes,
325
+ propsWithDefaults,
326
+ Reactive,
327
+ type ExtractEmitTypes,
328
+ type ExtractPropDefaultTypes,
329
+ } from 'ivue';
330
+ import { Static } from 'ivue/extras';
331
+
332
+ class $Box {
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.
387
+ constructor(
388
+ public props: Box.Props,
389
+ public emit: Box.Emits,
390
+ ) {}
391
+
392
+ get title() {
393
+ return this.props.title;
394
+ }
395
+
396
+ get sizeLabel() {
397
+ return `${this.props.size}px`;
398
+ }
399
+
400
+ close() {
401
+ this.emit('close', this.props.title);
402
+ }
403
+ }
404
+
405
+ export namespace Box {
406
+ /* Identity */
407
+
408
+ export const $Class = Static($Box); // anchor — it declares statics; children `extends` this
409
+ export let Class = Reactive($Class); // reactive — you `new` this
410
+ export type Instance = typeof Class.Instance; // defineExpose type & reactive() interop
411
+
412
+ /* Types — DERIVED from the class's statics, never hand-duplicated */
413
+
414
+ export type Props = ExtractPropTypes<typeof $Class.props>;
415
+ export type Emits = ExtractEmitTypes<typeof $Class.emits>;
416
+
417
+ export interface Slots {
418
+ default: (scope: { title: string }) => any;
419
+ }
420
+
421
+ /** What consumers hold through a template ref (expose unwraps refs). */
422
+ export type Exposed = ShallowUnwrapRef<Instance>;
423
+ }
424
+ ```
425
+
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:
430
+
431
+ ```ts
432
+ const props = defineProps(Box.Class.props); // non-generic: the type is inferred
433
+ const emit = defineEmits(Box.Class.emits) as Box.Emits;
434
+ defineSlots<Box.Slots>();
435
+ // generic components cast the one graft:
436
+ // defineProps(X.Class.props) as unknown as X.Props<T>
437
+ ```
438
+
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:
443
+
444
+ ```ts
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
+ }
468
+ ```
469
+
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.
483
+
484
+ **Overrides say so out loud.** `noImplicitOverride` is on: every member
485
+ that overrides a base member carries the `override` keyword
486
+ (`protected override get offsetSize() { ... }`). A silent override
487
+ refuses to compile, and a base rename breaks every subclass at the
488
+ exact overriding member instead of quietly orphaning it.
489
+
490
+ **`private` is banned — visibility is a three-tier semantic.** ivue's
491
+ core promise is extend-don't-fork, and `private` is the one keyword
492
+ that structurally revokes it: a subclass that needs a private member
493
+ has exactly one option, copy the file. TypeScript's `private` is
494
+ compile-time advisory anyway — it protects nothing at runtime and
495
+ forbids only the legitimate extender. So every member picks its tier
496
+ by AUDIENCE:
497
+
498
+ | tier | audience | meaning |
499
+ | --- | --- | --- |
500
+ | `public` | templates & consumers | the component/module surface |
501
+ | `protected` | subclasses | a seam of the hierarchy — reachable to extend, invisible to templates and consumers (TS enforces this) |
502
+ | `private` | nobody | banned — "must hide it even from subclasses" is a design smell; resolve by naming and documenting the member |
503
+
504
+ The pairing with `noImplicitOverride` is what makes protected-everything
505
+ safe rather than fragile: every subclass touchpoint is annotated
506
+ `override`, so a base renaming or removing a protected seam breaks
507
+ every extender's BUILD at the exact member — seam drift is loud, never
508
+ silent. (Both halves are load-bearing: `protected` opens every seam,
509
+ the tsconfig makes changing one detectable.)
510
+
273
511
  ## One template, one logic owner
274
512
 
275
513
  Every behavioral SFC has exactly one ivue class as its template logic owner.
@@ -286,6 +524,17 @@ belong in plain getters, setup work belongs in the constructor, and event
286
524
  handlers belong in methods — even when the handler only normalizes a DOM event
287
525
  before delegating to a domain model.
288
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
+
289
538
  When building on a class-backed component, **extend its class, not its
290
539
  `<script setup>`**. Add behavior to the existing class when it belongs to the
291
540
  same component contract. When it is a real specialization, subclass the raw
@@ -353,7 +602,10 @@ call site. Rules that keep it clean:
353
602
  In ordinary Vue this discipline costs a `computed()` per condition, so
354
603
  nobody keeps it; here a named plain getter costs zero bytes, so there is
355
604
  no excuse. Templates read as prose: bindings, names, and events — never
356
- 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.
357
609
  - **The rule covers EVERY binding kind, not just `v-if`** — the common
358
610
  leaks are display strings, disabled states, and class objects:
359
611
 
@@ -379,6 +631,8 @@ created in a callback — watchers go in the instance's OWN scope, and the
379
631
  owner of its lifetime disposes it:
380
632
 
381
633
  ```ts
634
+ import { Reactive, type ReactiveHelpers } from 'ivue';
635
+
382
636
  class $Session {
383
637
  get user() {
384
638
  return ref<User | null>(null);
@@ -441,6 +695,12 @@ export namespace Session {
441
695
  export type Instance = typeof Class.Instance;
442
696
  }
443
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
+
444
704
  // The owner disposes — the class's own method, like any other:
445
705
  session.dispose();
446
706
  ```
@@ -454,7 +714,7 @@ session.dispose();
454
714
  | ✅ `.value` for every Ref/Computed inside the class and in the script body | ❌ write `this.x = v` for a Ref/Computed in the class — it clobbers the ref or no-ops |
455
715
  | ✅ derive with a PLAIN getter | ❌ wrap every derivation in `computed()` — pays ~300 bytes/instance for nothing |
456
716
  | ✅ `computed()` only for expensive / render-suppressing / stable-handle needs | ❌ reach for `computed()` by default |
457
- | ✅ inject stores via `private get $store() { return useStore() }` | ❌ `store = useStore()` field initializer — runs at construction, breaks tests/SSR/cycles |
717
+ | ✅ inject stores via `protected get $store() { return useStore() }` | ❌ `store = useStore()` field initializer — runs at construction, breaks tests/SSR/cycles |
458
718
  | ✅ `new X.Class(props, emit)` — raw instance everywhere | ❌ wrap in `reactive(instance)` or any shallow-unwrap view as the standard |
459
719
  | ✅ destructure ALL template-touched Refs/Computeds + element refs, grouped | ❌ destructure plain getters or methods — snapshots a dead value / loses nothing but clarity |
460
720
  | ✅ state bindings in templates; dotted `box.x` only for plain getters/methods | ❌ reach a Ref through the instance in a template — `v-if="box.someRef"` is always-truthy |
@@ -462,8 +722,10 @@ session.dispose();
462
722
  | ✅ `defineExpose(box as X.Instance)` | ❌ `defineExpose(box)` raw — readonly-accessor writes will type-error for consumers |
463
723
  | ✅ constructor runs init; register hooks/watchers there | ❌ add an `init()` method expecting auto-call — ivue never calls it |
464
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 |
465
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()`) |
466
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` |
728
+ | ✅ `protected` for every internal member — subclasses reach every seam | ❌ `private` anywhere in an ivue class — it forbids only the legitimate extender |
467
729
  | ✅ instance code reads its own statics through `this.self` (the one cast per class); hoist `const self = this.self` for 2+ reads or any loop | ❌ per-site `(this.constructor as typeof $X)` casts — each one is an unchecked class-name assertion |
468
730
 
469
731
  ## The unwrapping-surface typing invariant
@@ -580,8 +842,21 @@ threads one object through every component and constructor signature it
580
842
  crosses; the store pattern deletes the thread.
581
843
 
582
844
  ```ts
583
- // 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')
584
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
+
585
860
  get authenticated() {
586
861
  return ref(false);
587
862
  }
@@ -592,14 +867,9 @@ class $AppStore {
592
867
  }
593
868
 
594
869
  export namespace AppStore {
595
- export const $Class = $AppStore;
596
- 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`
597
872
  export type Instance = typeof Class.Instance;
598
-
599
- let singleton: Instance | null = null;
600
- export function use(): Instance {
601
- return (singleton ??= new Class());
602
- }
603
873
  }
604
874
  ```
605
875
 
@@ -609,7 +879,7 @@ Consumers never receive it — they REACH for it:
609
879
  // any model — the `$`-getter caches the store per instance, forever
610
880
  class $SubscribersModel {
611
881
  protected get $app() {
612
- return AppStore.use();
882
+ return AppStore.Class.use();
613
883
  }
614
884
 
615
885
  async refresh() {
@@ -627,7 +897,7 @@ class $SubscribersModel {
627
897
  // any component — call use() directly; no prop, no provide/inject
628
898
  import { AppStore } from '../app/AppStore';
629
899
 
630
- const app = AppStore.use();
900
+ const app = AppStore.Class.use();
631
901
  const { authenticated } = app;
632
902
  </script>
633
903
 
@@ -640,13 +910,25 @@ Why this shape and not alternatives:
640
910
 
641
911
  - **`use()` is lazy** — the singleton constructs on first touch, after the
642
912
  app exists, so module-load order and circular imports stay non-events
643
- (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.
644
920
  - **The `$`-getter is the injection point** — cached whole, per instance,
645
921
  on first read. A model names its dependency once; every method reads
646
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.
647
929
  - **Tests swap the slot, not the callers** — `AppStore.Class = $TestStore`
648
- before the first `use()` (or reset the singleton) and every consumer
649
- 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.
650
932
  - A store is component-OUTLIVING by definition: watchers inside it use
651
933
  `this.$watch`/`$watchEffect`, never plain `watch`, and lifecycle hooks
652
934
  never belong in it.
@@ -667,13 +949,13 @@ reactive primitives as plain values** and materialize per observation:
667
949
  class $Sheet {
668
950
  // Plain readonly fields — the COLLECTIONS aren't reactive;
669
951
  // their VALUES are.
670
- private readonly cellVersions = new Map<number, Ref<number>>();
952
+ protected readonly cellVersions = new Map<number, Ref<number>>();
671
953
 
672
954
  /**
673
955
  * READ path: get-OR-CREATE, then subscribe — observation
674
956
  * materializes.
675
957
  */
676
- private trackCell(cellKey: number): void {
958
+ protected trackCell(cellKey: number): void {
677
959
  let versionRef = this.cellVersions.get(cellKey);
678
960
  if (!versionRef) {
679
961
  versionRef = ref(0);
@@ -687,7 +969,7 @@ class $Sheet {
687
969
  * WRITE path: PEEK-ONLY — unobserved keys allocate nothing,
688
970
  * notify no one.
689
971
  */
690
- private bumpCell(cellKey: number): void {
972
+ protected bumpCell(cellKey: number): void {
691
973
  const versionRef = this.cellVersions.get(cellKey);
692
974
  if (versionRef) versionRef.value++;
693
975
  }
@@ -854,6 +1136,26 @@ export namespace Settings {
854
1136
 
855
1137
  No static members → no wrapper: `$Class = $X`, the standard form unchanged.
856
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
+
857
1159
  ## Reading your own statics — the ladder
858
1160
 
859
1161
  `Reactive(X) === X`, so a namespace's `Class` slot IS the base class. A getter
@@ -951,6 +1253,11 @@ like prose — don't ruin it with letter soup:
951
1253
  - Abbreviate only when the abbreviation IS the domain term (`px`, `id`,
952
1254
  `fx`, A1-notation like `startRow`/`endCol`).
953
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.
954
1261
 
955
1262
  ```ts
956
1263
  // ❌ const v = this.cellVersions.get(k);
@@ -978,7 +1285,7 @@ Constants use one form per role:
978
1285
 
979
1286
  | Role | Form |
980
1287
  | --- | --- |
981
- | 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()` |
982
1289
  | Protocol or byte constant on a hot path, never overridden | `static readonly SCREAMING_SNAKE_CASE` with a one-line hot-path comment |
983
1290
  | Contributor or pane identity data | Instance `readonly lowerCamelCase` field |
984
1291
  | Extensible constructed dependency | Field assigned from a prototype `createX()` factory method |
@@ -1036,7 +1343,7 @@ convention and check it in review.
1036
1343
  - [ ] Every mutable state member is `get x() { return ref(...) }` — no mutable plain fields.
1037
1344
  - [ ] Inside the class, every Ref/Computed read/write uses `.value`; every plain field matches one role in the constants table.
1038
1345
  - [ ] Derived values are PLAIN getters; `computed()` appears only for expensive / render-suppressing / stable-handle cases.
1039
- - [ ] Stores/composables are injected via `private get $store() { return useStore() }`, not field initializers.
1346
+ - [ ] Stores/composables are injected via `protected get $store() { return useStore() }`, not field initializers.
1040
1347
  - [ ] The class is exported through the namespace (`$Class` / `Class = Reactive($Class)` / `Instance`); generics cast `Class` and hand-apply `ReactiveInstance` to `Instance<T>`.
1041
1348
  - [ ] The SFC does `new X.Class(...)` once — no `reactive()` wrapper, no unwrap view.
1042
1349
  - [ ] `<script setup>` is wiring only: no component-local Ref/Computed, watcher, lifecycle hook, or free function beside the class instance; extend an existing class-backed component through its class, never through parallel setup behavior.
@@ -1047,9 +1354,14 @@ convention and check it in review.
1047
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).
1048
1355
  - [ ] Lifecycle hooks / init logic live in the constructor (no `init()` expecting auto-call); template refs guarded with `?.` where read pre-mount.
1049
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)`.
1050
- - [ ] 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.
1051
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.
1052
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.
1053
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.
1054
1362
  - [ ] Static members precede the constructor; the constructor precedes state, prop, and derived getters; methods come last.
1055
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.
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.
1366
+ - [ ] Every member that overrides a base member carries `override` (with `noImplicitOverride` enabled).
1367
+ - [ ] No `private` members — internal members are `protected` (three-tier visibility: public = consumer surface, protected = hierarchy seam, private = banned).
@@ -0,0 +1,143 @@
1
+ /*
2
+ === GENERATOR ===
3
+ Subject: ivue-standards-check.ts ivue-generator-standard.ts
4
+ Goal: Prove the gate's constitution is data a subclass inherits and extends — every manifest check travels with its claim, its impossibility, and both proof arms, and a gate that grows a check without them refuses itself.
5
+ // 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
6
+ // domain-invariant: $CheckStandard — If a red arm's fixture runs through the gate, then its check reports the expected finding
7
+ // domain-invariant: $CheckStandard — If a green arm's fixture runs through the gate, then its check stays silent
8
+ // domain-invariant: $CheckStandard — If a subclass overrides or adds a check getter, then the manifest, the skip-list validation, and the proofs follow the receiving class
9
+ Impossible if true: a check enters the manifest without a red and a green proof arm
10
+ Impossible if true: a file breaking a manifest check passes the gate
11
+
12
+ === GENERATOR-DESCRIBED ===
13
+ The $CheckStandard class carries its own proof kit as per-receiver static
14
+ data, so an agent extending someone's gate inherits the proven
15
+ checks and is refused the moment it adds one more without arms —
16
+ the discipline teaches itself to whoever extends it. The driver below
17
+ runs every arm through the same run() the command line uses; hand-written
18
+ tests here carry the meta-claims, and the per-check claims live in the
19
+ proof data where prove() reads them.
20
+ */
21
+ import { expect, test } from 'vitest';
22
+ import { Static } from '../../lib/Static';
23
+ import * as Gate from './ivue-standards-check';
24
+ import { GeneratorStandard } from './ivue-generator-standard';
25
+
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
+ test('the shipped constitution is complete for every manifest check', () => {
28
+ const GateClass = Gate.CheckStandard.Class;
29
+ expect(GateClass.checks.length).toBe(32);
30
+ const report = GateClass.prove({ completenessOnly: true });
31
+ expect(report.problems).toEqual([]);
32
+ for (const check of GateClass.checks) {
33
+ // one form everywhere: the check's name IS its getter's name
34
+ expect((GateClass as unknown as Record<string, Gate.StandardCheck>)[check.name]?.name).toBe(check.name);
35
+ const proof = GateClass.proofs[check.name];
36
+ expect(proof, check.name).toBeDefined();
37
+ expect(proof.claim).toMatch(/^If .+, then .+/);
38
+ expect(proof.impossibility.length).toBeGreaterThan(0);
39
+ expect(proof.red.length).toBeGreaterThan(0);
40
+ expect(proof.green.length).toBeGreaterThan(0);
41
+ }
42
+ });
43
+
44
+ // domain-invariant: $CheckStandard — If a red arm's fixture runs through the gate, then its check reports the expected finding
45
+ // domain-invariant: $CheckStandard — If a green arm's fixture runs through the gate, then its check stays silent
46
+ // impossible-if-true: $CheckStandard — a file breaking a manifest check passes the gate
47
+ test('every red arm produces its named finding and every green arm stays silent', () => {
48
+ const report = Gate.CheckStandard.Class.prove();
49
+ expect(report.problems).toEqual([]);
50
+ expect(report.ran.red).toBeGreaterThanOrEqual(30);
51
+ expect(report.ran.green).toBeGreaterThanOrEqual(30);
52
+ });
53
+
54
+ test('the generator standard is the extension mechanism eating its own cooking', () => {
55
+ // ten methodology checks arrive the same way a house check does:
56
+ // getters + checks + proofs on a subclass — fully proven, opt-in
57
+ const GeneratorClass = GeneratorStandard.Class;
58
+ expect(GeneratorClass.checks.length).toBe(42);
59
+ const report = GeneratorClass.prove({ completenessOnly: true });
60
+ expect(report.problems).toEqual([]);
61
+ // the base stays ivue-only: no header check leaks upward
62
+ // the methodology checks wear their jurisdiction as a prefix
63
+ expect(Gate.CheckStandard.Class.checks.map((check) => check.name)).not.toContain('invariants_a_test_file_opens_with_its_generator_header');
64
+ expect(GeneratorClass.checks.map((check) => check.name)).toContain('invariants_a_test_file_opens_with_its_generator_header');
65
+ expect(GeneratorClass.checks.filter((check) => check.name.startsWith('invariants_')).length).toBe(10);
66
+ });
67
+
68
+ test('prove isolates one check when asked', () => {
69
+ const GateClass = Gate.CheckStandard.Class;
70
+ const report = GateClass.prove({ only: 'a_ref_is_read_and_written_through_value' });
71
+ expect(report.problems).toEqual([]);
72
+ expect(report.ran.red).toBe(1);
73
+ expect(report.ran.green).toBe(1);
74
+ const unknown = GateClass.prove({ only: 'No such check' });
75
+ expect(unknown.problems.some((problem) => problem.includes('No such check'))).toBe(true);
76
+ expect(unknown.ran.red + unknown.ran.green).toBe(0);
77
+ });
78
+
79
+ test('the CLI refuses to combine --prove with a gate run', async () => {
80
+ const GateClass = Gate.CheckStandard.Class;
81
+ expect(await GateClass.main(['--prove', '--source-root', '../../'])).toBe(2);
82
+ expect(await GateClass.main(['--source-root', 'src', '--prove'])).toBe(2);
83
+ });
84
+
85
+ // impossible-if-true: $CheckStandard — a check enters the manifest without a red and a green proof arm
86
+ test('an armless check is refused by its own constitution', () => {
87
+ class $ArmlessGate extends Gate.CheckStandard.$Class {
88
+ static get a_house_rule_without_arms(): Gate.StandardCheck {
89
+ return { name: 'a_house_rule_without_arms', enforced: true, run: () => [] };
90
+ }
91
+
92
+ static get checks(): readonly Gate.StandardCheck[] {
93
+ return [...super.checks, this.a_house_rule_without_arms];
94
+ }
95
+ }
96
+ const ArmlessGate = Static($ArmlessGate);
97
+ const report = ArmlessGate.prove({ completenessOnly: true });
98
+ expect(report.problems.some((problem) => problem.includes('a_house_rule_without_arms'))).toBe(true);
99
+ });
100
+
101
+ // domain-invariant: $CheckStandard — If a subclass overrides or adds a check getter, then the manifest, the skip-list validation, and the proofs follow the receiving class
102
+ test('a house gate extends the manifest and its constitution through the receiver', () => {
103
+ const houseCheck: Gate.StandardCheck = {
104
+ name: 'a_source_file_stays_under_nine_hundred_lines',
105
+ enforced: true,
106
+ run: (context) =>
107
+ context.sources
108
+ .filter((unit) => unit.lines.length > 900)
109
+ .map((unit) => ({ check: 'a_source_file_stays_under_nine_hundred_lines', file: unit.relativePath, line: 1, message: `${unit.lines.length} lines — split the module` })),
110
+ };
111
+ const longFile = `${'// filler\n'.repeat(901)}export type Filler = number;\n`;
112
+ class $HouseGate extends Gate.CheckStandard.$Class {
113
+ static get a_source_file_stays_under_nine_hundred_lines(): Gate.StandardCheck {
114
+ return houseCheck;
115
+ }
116
+
117
+ static get checks(): readonly Gate.StandardCheck[] {
118
+ return [...super.checks, this.a_source_file_stays_under_nine_hundred_lines];
119
+ }
120
+
121
+ static get proofs(): Readonly<Record<string, Gate.CheckProof>> {
122
+ return {
123
+ ...super.proofs,
124
+ [houseCheck.name]: {
125
+ check: houseCheck,
126
+ claim: 'If a source file exceeds nine hundred lines, then the gate names it',
127
+ impossibility: 'a nine-hundred-line source file passes the gate',
128
+ red: [{ files: { 'src/Long.ts': longFile }, expectFindings: [/90\d lines — split the module/] }],
129
+ green: [{ files: { 'src/Short.ts': 'export type Short = number;\n' } }],
130
+ },
131
+ };
132
+ }
133
+ }
134
+ const HouseGate = Static($HouseGate);
135
+ // the manifest follows the receiver…
136
+ expect(HouseGate.checks.map((check) => check.name)).toContain(houseCheck.name);
137
+ // …the constitution follows the receiver (39 proven checks, zero problems)…
138
+ const report = HouseGate.prove();
139
+ expect(report.problems).toEqual([]);
140
+ expect(report.ran.red).toBeGreaterThanOrEqual(31);
141
+ // …and the base class is untouched: no house rule leaks upward
142
+ expect(Gate.CheckStandard.Class.checks.map((check) => check.name)).not.toContain(houseCheck.name);
143
+ });