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.
- package/README.md +1 -1
- package/bin/ivue.mjs +62 -1
- package/dist/LazyShared.d.ts +4 -4
- package/dist/Reactive.d.ts +37 -10
- package/dist/clone.d.ts +3 -0
- package/dist/extras.cjs +1 -1
- package/dist/extras.d.ts +1 -0
- package/dist/extras.es.js +1 -49
- package/dist/index.cjs +1 -1
- package/dist/index.es.js +1 -99
- package/dist/nestedProps.d.ts +67 -0
- package/lib/LazyShared.ts +4 -4
- package/lib/Reactive.ts +54 -23
- package/lib/Static.ts +30 -0
- package/lib/__tests__/Reactive.vitest.spec.ts +70 -14
- package/lib/__tests__/Static.vitest.spec.ts +60 -1
- package/lib/__tests__/ivue.vitest.spec.ts +17 -5
- package/lib/__tests__/nestedProps.vitest.spec.ts +153 -0
- package/lib/clone.ts +21 -0
- package/lib/extras.ts +1 -0
- package/lib/ivue.ts +15 -1
- package/lib/nestedProps.ts +126 -0
- package/package.json +13 -5
- package/skills/ivue/SKILL.md +334 -22
- package/skills/ivue/constitution.test.ts +143 -0
- package/skills/ivue/ivue-docs-skip.json +37 -0
- package/skills/ivue/ivue-generator-standard.ts +410 -0
- package/skills/ivue/ivue-house-gate.ts +131 -0
- package/skills/ivue/ivue-standards-check.ts +2649 -0
- package/skills/ivue/ivue-standards-skip.json +17 -0
package/skills/ivue/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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 `
|
|
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;
|
|
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()`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
+
});
|