ivue 2.3.0 → 2.5.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/bin/ivue.mjs +62 -1
- package/dist/Reactive.d.ts +27 -6
- package/dist/extras.cjs +1 -1
- package/dist/extras.es.js +23 -23
- package/dist/index.cjs +1 -1
- package/dist/index.es.js +77 -71
- package/lib/Reactive.ts +54 -26
- package/lib/Static.ts +14 -0
- package/lib/__tests__/Reactive.vitest.spec.ts +83 -13
- package/lib/__tests__/Static.vitest.spec.ts +60 -1
- package/lib/__tests__/ivue.vitest.spec.ts +17 -5
- package/lib/ivue.ts +34 -1
- package/package.json +8 -3
- package/skills/ivue/SKILL.md +209 -9
- package/skills/ivue/constitution.test.ts +143 -0
- package/skills/ivue/ivue-generator-standard.ts +410 -0
- package/skills/ivue/ivue-house-gate.ts +130 -0
- package/skills/ivue/ivue-standards-check.ts +2464 -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,180 @@ 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
|
|
294
|
+
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>`.
|
|
298
|
+
|
|
299
|
+
Combined — the canonical file, everything above in one shape:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
// Box.ts — the whole module: imports, the class, the namespace. Nothing else.
|
|
303
|
+
import type { ExtractPropTypes, PropType, ShallowUnwrapRef } from 'vue';
|
|
304
|
+
import {
|
|
305
|
+
definePropTypes,
|
|
306
|
+
propsWithDefaults,
|
|
307
|
+
Reactive,
|
|
308
|
+
type ExtractEmitTypes,
|
|
309
|
+
type ExtractPropDefaultTypes,
|
|
310
|
+
} from 'ivue';
|
|
311
|
+
|
|
312
|
+
class $Box {
|
|
313
|
+
// Forward references into the namespace below — legal: type positions
|
|
314
|
+
// resolve non-positionally, and the class never reads a contract VALUE.
|
|
315
|
+
constructor(
|
|
316
|
+
public props: Box.Props,
|
|
317
|
+
public emit: Box.Emits,
|
|
318
|
+
) {}
|
|
319
|
+
|
|
320
|
+
get title() {
|
|
321
|
+
return this.props.title;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
get sizeLabel() {
|
|
325
|
+
return `${this.props.size}px`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
close() {
|
|
329
|
+
this.emit('close', this.props.title);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export namespace Box {
|
|
334
|
+
/* Identity */
|
|
335
|
+
|
|
336
|
+
export const $Class = $Box; // raw — children `extends` this
|
|
337
|
+
export let Class = Reactive($Class); // reactive — you `new` this
|
|
338
|
+
export type Instance = typeof Class.Instance; // defineExpose type & reactive() interop
|
|
339
|
+
|
|
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);
|
|
369
|
+
|
|
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>;
|
|
378
|
+
|
|
379
|
+
export interface Slots {
|
|
380
|
+
default: (scope: { title: string }) => any;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** What consumers hold through a template ref (expose unwraps refs). */
|
|
384
|
+
export type Exposed = ShallowUnwrapRef<Instance>;
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
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:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
const props = defineProps(Box.props); // non-generic: the type is inferred
|
|
393
|
+
const emit = defineEmits(Box.emits) as Box.Emits;
|
|
394
|
+
defineSlots<Box.Slots>();
|
|
395
|
+
// generic components cast the one graft:
|
|
396
|
+
// defineProps(X.props) as unknown as X.Props<T>
|
|
397
|
+
```
|
|
398
|
+
|
|
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:
|
|
402
|
+
|
|
403
|
+
```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);
|
|
410
|
+
```
|
|
411
|
+
|
|
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.
|
|
419
|
+
|
|
420
|
+
**Overrides say so out loud.** `noImplicitOverride` is on: every member
|
|
421
|
+
that overrides a base member carries the `override` keyword
|
|
422
|
+
(`protected override get offsetSize() { ... }`). A silent override
|
|
423
|
+
refuses to compile, and a base rename breaks every subclass at the
|
|
424
|
+
exact overriding member instead of quietly orphaning it.
|
|
425
|
+
|
|
426
|
+
**`private` is banned — visibility is a three-tier semantic.** ivue's
|
|
427
|
+
core promise is extend-don't-fork, and `private` is the one keyword
|
|
428
|
+
that structurally revokes it: a subclass that needs a private member
|
|
429
|
+
has exactly one option, copy the file. TypeScript's `private` is
|
|
430
|
+
compile-time advisory anyway — it protects nothing at runtime and
|
|
431
|
+
forbids only the legitimate extender. So every member picks its tier
|
|
432
|
+
by AUDIENCE:
|
|
433
|
+
|
|
434
|
+
| tier | audience | meaning |
|
|
435
|
+
| --- | --- | --- |
|
|
436
|
+
| `public` | templates & consumers | the component/module surface |
|
|
437
|
+
| `protected` | subclasses | a seam of the hierarchy — reachable to extend, invisible to templates and consumers (TS enforces this) |
|
|
438
|
+
| `private` | nobody | banned — "must hide it even from subclasses" is a design smell; resolve by naming and documenting the member |
|
|
439
|
+
|
|
440
|
+
The pairing with `noImplicitOverride` is what makes protected-everything
|
|
441
|
+
safe rather than fragile: every subclass touchpoint is annotated
|
|
442
|
+
`override`, so a base renaming or removing a protected seam breaks
|
|
443
|
+
every extender's BUILD at the exact member — seam drift is loud, never
|
|
444
|
+
silent. (Both halves are load-bearing: `protected` opens every seam,
|
|
445
|
+
the tsconfig makes changing one detectable.)
|
|
446
|
+
|
|
273
447
|
## One template, one logic owner
|
|
274
448
|
|
|
275
449
|
Every behavioral SFC has exactly one ivue class as its template logic owner.
|
|
@@ -387,14 +561,32 @@ class $Session {
|
|
|
387
561
|
// Outliving instance: $watch/$watchEffect register in the
|
|
388
562
|
// instance's lazy effectScope — there is no component scope here
|
|
389
563
|
// to reap plain watch.
|
|
564
|
+
// WATCHERS live behind a method, not inline in the constructor —
|
|
565
|
+
// the constructor calls it once, and the instance can RESTART its
|
|
566
|
+
// watchers after a keep-state stop (see suspend() below).
|
|
390
567
|
constructor() {
|
|
568
|
+
this.startWatchers();
|
|
569
|
+
// If constructed INSIDE some scope, auto-wire teardown instead:
|
|
570
|
+
// getCurrentScope() && onScopeDispose(() => this.$stopEffects());
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
startWatchers() {
|
|
391
574
|
this.$watch(
|
|
392
575
|
() => this.user.value,
|
|
393
576
|
(user, previousUser) => this.onUserChanged(user, previousUser),
|
|
394
577
|
);
|
|
395
578
|
this.$watchEffect(() => this.persist());
|
|
396
|
-
|
|
397
|
-
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// SUSPEND / RESUME: { reset: false } stops the watchers ONLY — every
|
|
582
|
+
// cached cell survives with its current value. startWatchers() in a
|
|
583
|
+
// fresh scope resumes. (Default $stopEffects() also CLEARS the cells:
|
|
584
|
+
// the next touch re-runs initializers — disposal is a reset.)
|
|
585
|
+
suspend() {
|
|
586
|
+
this.$stopEffects({ reset: false });
|
|
587
|
+
}
|
|
588
|
+
resume() {
|
|
589
|
+
this.startWatchers();
|
|
398
590
|
}
|
|
399
591
|
|
|
400
592
|
// CLEANUP composes as an ORDINARY method — no hooks, no reserved
|
|
@@ -436,7 +628,7 @@ session.dispose();
|
|
|
436
628
|
| ✅ `.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 |
|
|
437
629
|
| ✅ derive with a PLAIN getter | ❌ wrap every derivation in `computed()` — pays ~300 bytes/instance for nothing |
|
|
438
630
|
| ✅ `computed()` only for expensive / render-suppressing / stable-handle needs | ❌ reach for `computed()` by default |
|
|
439
|
-
| ✅ inject stores via `
|
|
631
|
+
| ✅ inject stores via `protected get $store() { return useStore() }` | ❌ `store = useStore()` field initializer — runs at construction, breaks tests/SSR/cycles |
|
|
440
632
|
| ✅ `new X.Class(props, emit)` — raw instance everywhere | ❌ wrap in `reactive(instance)` or any shallow-unwrap view as the standard |
|
|
441
633
|
| ✅ destructure ALL template-touched Refs/Computeds + element refs, grouped | ❌ destructure plain getters or methods — snapshots a dead value / loses nothing but clarity |
|
|
442
634
|
| ✅ 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 |
|
|
@@ -446,6 +638,7 @@ session.dispose();
|
|
|
446
638
|
| ✅ 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 |
|
|
447
639
|
| ✅ compose cleanup as an ordinary method — `dispose() { /* non-Vue cleanup */ this.$stopEffects(); }` | ❌ expect a teardown hook — ivue auto-calls NOTHING (no `init()`, no `stopEffects()`) |
|
|
448
640
|
| ✅ 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
|
+
| ✅ `protected` for every internal member — subclasses reach every seam | ❌ `private` anywhere in an ivue class — it forbids only the legitimate extender |
|
|
449
642
|
| ✅ 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 |
|
|
450
643
|
|
|
451
644
|
## The unwrapping-surface typing invariant
|
|
@@ -490,7 +683,11 @@ until mount — use `?.` in watch getters).
|
|
|
490
683
|
its leaf reads subscribe directly (non-intuitive but structural).
|
|
491
684
|
- The source MUST be the FUNCTION form. `watch(instance.plainGetter, cb)` passes a
|
|
492
685
|
dead snapshot and never fires.
|
|
493
|
-
- `$stopEffects()` stops the instance scope and clears cached Refs/Computeds
|
|
686
|
+
- `$stopEffects()` stops the instance scope and clears cached Refs/Computeds
|
|
687
|
+
(the next touch re-materializes — disposal is a reset);
|
|
688
|
+
`$stopEffects({ reset: false })` stops the WATCHERS only — every cached
|
|
689
|
+
cell survives with its current value, and `startWatchers()` in a fresh
|
|
690
|
+
scope resumes (the suspend/resume pattern above);
|
|
494
691
|
instances that never `$watch` allocate no scope. There are NO hooks — richer
|
|
495
692
|
cleanup is an ordinary method that does its work and then calls
|
|
496
693
|
`$stopEffects()` itself. Every outliving instance needs an OWNER that calls
|
|
@@ -645,13 +842,13 @@ reactive primitives as plain values** and materialize per observation:
|
|
|
645
842
|
class $Sheet {
|
|
646
843
|
// Plain readonly fields — the COLLECTIONS aren't reactive;
|
|
647
844
|
// their VALUES are.
|
|
648
|
-
|
|
845
|
+
protected readonly cellVersions = new Map<number, Ref<number>>();
|
|
649
846
|
|
|
650
847
|
/**
|
|
651
848
|
* READ path: get-OR-CREATE, then subscribe — observation
|
|
652
849
|
* materializes.
|
|
653
850
|
*/
|
|
654
|
-
|
|
851
|
+
protected trackCell(cellKey: number): void {
|
|
655
852
|
let versionRef = this.cellVersions.get(cellKey);
|
|
656
853
|
if (!versionRef) {
|
|
657
854
|
versionRef = ref(0);
|
|
@@ -665,7 +862,7 @@ class $Sheet {
|
|
|
665
862
|
* WRITE path: PEEK-ONLY — unobserved keys allocate nothing,
|
|
666
863
|
* notify no one.
|
|
667
864
|
*/
|
|
668
|
-
|
|
865
|
+
protected bumpCell(cellKey: number): void {
|
|
669
866
|
const versionRef = this.cellVersions.get(cellKey);
|
|
670
867
|
if (versionRef) versionRef.value++;
|
|
671
868
|
}
|
|
@@ -1014,7 +1211,7 @@ convention and check it in review.
|
|
|
1014
1211
|
- [ ] Every mutable state member is `get x() { return ref(...) }` — no mutable plain fields.
|
|
1015
1212
|
- [ ] Inside the class, every Ref/Computed read/write uses `.value`; every plain field matches one role in the constants table.
|
|
1016
1213
|
- [ ] Derived values are PLAIN getters; `computed()` appears only for expensive / render-suppressing / stable-handle cases.
|
|
1017
|
-
- [ ] Stores/composables are injected via `
|
|
1214
|
+
- [ ] Stores/composables are injected via `protected get $store() { return useStore() }`, not field initializers.
|
|
1018
1215
|
- [ ] The class is exported through the namespace (`$Class` / `Class = Reactive($Class)` / `Instance`); generics cast `Class` and hand-apply `ReactiveInstance` to `Instance<T>`.
|
|
1019
1216
|
- [ ] The SFC does `new X.Class(...)` once — no `reactive()` wrapper, no unwrap view.
|
|
1020
1217
|
- [ ] `<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.
|
|
@@ -1031,3 +1228,6 @@ convention and check it in review.
|
|
|
1031
1228
|
- [ ] 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.
|
|
1032
1229
|
- [ ] Static members precede the constructor; the constructor precedes state, prop, and derived getters; methods come last.
|
|
1033
1230
|
- [ ] 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.
|
|
1232
|
+
- [ ] Every member that overrides a base member carries `override` (with `noImplicitOverride` enabled).
|
|
1233
|
+
- [ ] 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(30);
|
|
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(40);
|
|
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
|
+
});
|