ivue 2.4.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.
@@ -13,6 +13,7 @@ import {
13
13
  ivue,
14
14
  propertiesAccessorsMaps,
15
15
  propsWithDefaults,
16
+ definePropTypes,
16
17
  } from '../ivue';
17
18
  const {
18
19
  copyOwnProps,
@@ -76,21 +77,21 @@ class ProductItem extends Item {
76
77
  }
77
78
 
78
79
  class StoreItem extends ProductItem {
79
- _productType = iref('store');
80
- get productType() {
80
+ override _productType = iref('store');
81
+ override get productType() {
81
82
  const prefix = super.productFeel ?? '';
82
83
  return (prefix ? prefix + ':' : '') + this._productType;
83
84
  }
84
- set productFeel(value: string) {
85
+ override set productFeel(value: string) {
85
86
  this._productFeel = value;
86
87
  }
87
88
  }
88
89
 
89
90
  class RetailStoreItem extends StoreItem {
90
- _productType = iref('retail');
91
+ override _productType = iref('retail');
91
92
  /** Do not overwrite productType getter here, on purpose. */
92
93
  productHistory = iref([]);
93
- get testProperty() {
94
+ override get testProperty() {
94
95
  return this._testProperty;
95
96
  }
96
97
  calculateSize() {
@@ -1885,3 +1886,14 @@ describe('ivue coverage edge cases', () => {
1885
1886
  expect(cloned.value).toBe(7);
1886
1887
  });
1887
1888
  });
1889
+
1890
+ describe('definePropTypes()', () => {
1891
+ it('returns the same types map (identity, literal-preserving)', () => {
1892
+ const types = definePropTypes({
1893
+ title: { type: String, required: true },
1894
+ size: { type: Number },
1895
+ });
1896
+ expect(types.title.required).toBe(true);
1897
+ expect(types.size.type).toBe(Number);
1898
+ });
1899
+ });
package/lib/ivue.ts CHANGED
@@ -130,9 +130,15 @@ export type ExtractEmitTypes<T extends Record<string, any>> =
130
130
 
131
131
  /**
132
132
  * Extract properties as all assigned properties because they have defaults.
133
+ * Props declared `required: true` are filtered out (they can never carry a
134
+ * default); declare the types map with a literal-preserving generic call so
135
+ * the `required: true` literal survives `typeof`.
133
136
  */
134
137
  export type ExtractPropDefaultTypes<O> = {
135
- [K in keyof O]: ValueOf<ExtractPropTypes<O>, K>;
138
+ [K in keyof O as O[K] extends { required: true } ? never : K]: ValueOf<
139
+ ExtractPropTypes<O>,
140
+ K
141
+ >;
136
142
  };
137
143
 
138
144
  /**
@@ -779,6 +785,14 @@ export const isClass = (val: any): boolean => {
779
785
  * @param typedProps Props declared in defineComponent() style with type and possibly required declared, but without default
780
786
  * @returns Props declared in defineComponent() style with all properties having default property declared.
781
787
  */
788
+ /**
789
+ * Identity helper for a prop-TYPES map: generic inference preserves the
790
+ * `required: true` literal that a bare object const widens to `boolean`,
791
+ * which ExtractPropDefaultTypes' required-key filter depends on.
792
+ */
793
+ export const definePropTypes = <T extends VuePropsObject>(types: T): T =>
794
+ types;
795
+
782
796
  export const propsWithDefaults = <T extends VuePropsObject>(
783
797
  defaults: Record<string, any>,
784
798
  typedProps: T,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ivue",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Infinite Vue – Class Based Architecture for Vue 3",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,12 +14,16 @@
14
14
  "import": "./dist/extras.es.js",
15
15
  "require": "./dist/extras.cjs"
16
16
  },
17
- "./package.json": "./package.json"
17
+ "./package.json": "./package.json",
18
+ "./skills/*": "./skills/*"
18
19
  },
19
20
  "main": "./dist/index.cjs",
20
21
  "module": "./dist/index.es.js",
21
22
  "types": "./dist/index.d.ts",
22
23
  "scripts": {
24
+ "gate": "vite-node skills/ivue/ivue-standards-check.ts --",
25
+ "gate:house": "vite-node skills/ivue/ivue-house-gate.ts --",
26
+ "gate:newsletter": "vite-node skills/ivue/ivue-generator-standard.ts -- --source-root newsletter --skip-list skills/ivue/ivue-standards-skip.json --test-glob 'newsletter/src/**/*.test.ts'",
23
27
  "dev": "vite demo --host",
24
28
  "dev:demo": "vite demo --host",
25
29
  "dev:flyweight": "vite examples/playground --port 5181 --host",
@@ -39,7 +43,7 @@
39
43
  "cypress:headed": "cypress run --component --browser chrome --headed --no-exit",
40
44
  "build": "tsc --version;tsc --p ./tsconfig.json && vite build",
41
45
  "build:demo": "tsc --p ./tsconfig.json && vite build demo",
42
- "build:docs": "npm run sync:examples && npm run sync:releases && npm run sync:blog-index && npm --prefix docs_v2 run build && npm run check:links && node docs_v2/scripts/check-related-posts.mjs",
46
+ "build:docs": "npm run sync:examples && npm run sync:releases && npm run sync:blog-index && node docs_v2/scripts/rss-generator.mjs && npm --prefix docs_v2 run build && npm run check:links && node docs_v2/scripts/check-related-posts.mjs",
43
47
  "sync:releases": "node docs_v2/scripts/releases-page-generator.mjs",
44
48
  "sync:blog-index": "node docs_v2/scripts/blog-index-generator.mjs",
45
49
  "check:links": "node docs_v2/scripts/check-links.mjs",
@@ -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,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.
@@ -454,7 +628,7 @@ session.dispose();
454
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 |
455
629
  | ✅ derive with a PLAIN getter | ❌ wrap every derivation in `computed()` — pays ~300 bytes/instance for nothing |
456
630
  | ✅ `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 |
631
+ | ✅ inject stores via `protected get $store() { return useStore() }` | ❌ `store = useStore()` field initializer — runs at construction, breaks tests/SSR/cycles |
458
632
  | ✅ `new X.Class(props, emit)` — raw instance everywhere | ❌ wrap in `reactive(instance)` or any shallow-unwrap view as the standard |
459
633
  | ✅ destructure ALL template-touched Refs/Computeds + element refs, grouped | ❌ destructure plain getters or methods — snapshots a dead value / loses nothing but clarity |
460
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 |
@@ -464,6 +638,7 @@ session.dispose();
464
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 |
465
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()`) |
466
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 |
467
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 |
468
643
 
469
644
  ## The unwrapping-surface typing invariant
@@ -667,13 +842,13 @@ reactive primitives as plain values** and materialize per observation:
667
842
  class $Sheet {
668
843
  // Plain readonly fields — the COLLECTIONS aren't reactive;
669
844
  // their VALUES are.
670
- private readonly cellVersions = new Map<number, Ref<number>>();
845
+ protected readonly cellVersions = new Map<number, Ref<number>>();
671
846
 
672
847
  /**
673
848
  * READ path: get-OR-CREATE, then subscribe — observation
674
849
  * materializes.
675
850
  */
676
- private trackCell(cellKey: number): void {
851
+ protected trackCell(cellKey: number): void {
677
852
  let versionRef = this.cellVersions.get(cellKey);
678
853
  if (!versionRef) {
679
854
  versionRef = ref(0);
@@ -687,7 +862,7 @@ class $Sheet {
687
862
  * WRITE path: PEEK-ONLY — unobserved keys allocate nothing,
688
863
  * notify no one.
689
864
  */
690
- private bumpCell(cellKey: number): void {
865
+ protected bumpCell(cellKey: number): void {
691
866
  const versionRef = this.cellVersions.get(cellKey);
692
867
  if (versionRef) versionRef.value++;
693
868
  }
@@ -1036,7 +1211,7 @@ convention and check it in review.
1036
1211
  - [ ] Every mutable state member is `get x() { return ref(...) }` — no mutable plain fields.
1037
1212
  - [ ] Inside the class, every Ref/Computed read/write uses `.value`; every plain field matches one role in the constants table.
1038
1213
  - [ ] 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.
1214
+ - [ ] Stores/composables are injected via `protected get $store() { return useStore() }`, not field initializers.
1040
1215
  - [ ] The class is exported through the namespace (`$Class` / `Class = Reactive($Class)` / `Instance`); generics cast `Class` and hand-apply `ReactiveInstance` to `Instance<T>`.
1041
1216
  - [ ] The SFC does `new X.Class(...)` once — no `reactive()` wrapper, no unwrap view.
1042
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.
@@ -1053,3 +1228,6 @@ convention and check it in review.
1053
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.
1054
1229
  - [ ] Static members precede the constructor; the constructor precedes state, prop, and derived getters; methods come last.
1055
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
+ });