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.
@@ -0,0 +1,37 @@
1
+ [
2
+ {
3
+ "path": "examples/playground/src/examples/benchmarks/creationBench.ts",
4
+ "check": "a_public_class_publishes_its_namespace_manifest",
5
+ "reason": "benchmark arms: PlainBox and reactive(new PlainBox()) ARE the measured competitors; changing their shape changes the numbers the docs quote"
6
+ },
7
+ {
8
+ "path": "examples/playground/src/examples/benchmarks/creationBench.ts",
9
+ "check": "construction_goes_through_the_namespace_class_slot",
10
+ "reason": "benchmark arms: PlainBox and reactive(new PlainBox()) ARE the measured competitors; changing their shape changes the numbers the docs quote"
11
+ },
12
+ {
13
+ "path": "examples/playground/src/ivue.ts",
14
+ "check": "declarations_use_full_descriptive_names",
15
+ "reason": "vendored copy of the engine (lib/Reactive.ts, synced by sync:examples) \u2014 the engine keeps its own conventions"
16
+ },
17
+ {
18
+ "path": "examples/playground/src/lenis/Emitter.ts",
19
+ "check": "declarations_use_full_descriptive_names",
20
+ "reason": "vendored third-party (the Lenis smooth-scroll library, ported as-is) \u2014 not teaching code"
21
+ },
22
+ {
23
+ "path": "examples/playground/src/lenis/Lenis.ts",
24
+ "check": "declarations_use_full_descriptive_names",
25
+ "reason": "vendored third-party (the Lenis smooth-scroll library, ported as-is) \u2014 not teaching code"
26
+ },
27
+ {
28
+ "path": "examples/playground/src/lenis/LenisUtils.ts",
29
+ "check": "declarations_use_full_descriptive_names",
30
+ "reason": "vendored third-party (the Lenis smooth-scroll library, ported as-is) \u2014 not teaching code"
31
+ },
32
+ {
33
+ "path": "docs_v2/.vitepress/theme/components/grid/GridBenchmark.vue",
34
+ "check": "one_handler_per_event",
35
+ "reason": "benchmark arms: the composable and the ivue grid ARE the measured competitors, bound identically on purpose; giving either its own per-event methods changes the shape the numbers compare"
36
+ }
37
+ ]
@@ -0,0 +1,410 @@
1
+ /**
2
+ * ivue-generator-standard.ts — the invariant-methodology extension of the
3
+ * ivue Standard gate, built the same way a house gate is: check getters,
4
+ * `checks` appending them, `proofs` spreading their constitution entries.
5
+ * The extension mechanism eating its own cooking.
6
+ *
7
+ * The base gate (`ivue-standards-check.ts`) is ivue-only. This subclass
8
+ * adds the ten generator-header checks — test files open with the
9
+ * generator sentinel header, claims bind to tests one-to-one,
10
+ * impossibilities carry exact negative proofs, contract pointers resolve —
11
+ * the discipline of the invariants / invariant-spec-tests skills. Opt in
12
+ * by extending THIS class in your house gate instead of the base:
13
+ *
14
+ * class $HouseGate extends GeneratorStandard.$Class { ... }
15
+ *
16
+ * Run standalone:
17
+ *
18
+ * vite-node skills/ivue/ivue-generator-standard.ts -- \
19
+ * --source-root src --test-glob 'src/**\/*.test.ts'
20
+ */
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { basename, dirname, resolve } from 'node:path';
23
+ import { Static } from '../../lib/Static';
24
+ import { CheckStandard } from './ivue-standards-check';
25
+
26
+ class $GeneratorStandard extends CheckStandard.$Class {
27
+ static get invariants_a_test_file_opens_with_its_generator_header(): CheckStandard.StandardCheck {
28
+ return this.defineCheck('invariants_a_test_file_opens_with_its_generator_header', (context) => {
29
+ const findings: CheckStandard.Finding[] = [];
30
+ for (const unit of context.tests) {
31
+ const header = this.parseHeader(unit);
32
+ if (!header.present) findings.push(this.finding(this.invariants_a_test_file_opens_with_its_generator_header, unit, 1, `no \`${this.$grammar.GENERATOR}\` header — the test file opens with its generator header, before any import`));
33
+ else if (!header.firstContent) findings.push(this.finding(this.invariants_a_test_file_opens_with_its_generator_header, unit, 1, 'the generator header is not the first content — nothing precedes it, imports follow it'));
34
+ }
35
+ return findings;
36
+ });
37
+ }
38
+
39
+ static get invariants_a_generator_header_carries_both_registers_in_order(): CheckStandard.StandardCheck {
40
+ return this.defineCheck('invariants_a_generator_header_carries_both_registers_in_order', (context) => {
41
+ const findings: CheckStandard.Finding[] = [];
42
+ const grammar = this.$grammar;
43
+ for (const unit of context.tests) {
44
+ const header = this.parseHeader(unit);
45
+ if (!header.present) continue;
46
+ const line = unit.lines.findIndex((text) => text.includes(grammar.GENERATOR)) + 1;
47
+ if (unit.text.split(grammar.GENERATOR).length > 2) findings.push(this.finding(this.invariants_a_generator_header_carries_both_registers_in_order, unit, line, `duplicate \`${grammar.GENERATOR}\` sentinel`));
48
+ if (!header.bothRegisters) findings.push(this.finding(this.invariants_a_generator_header_carries_both_registers_in_order, unit, line, `missing \`${grammar.GENERATOR_DESCRIBED}\` register`));
49
+ else if (!header.orderedRegisters) findings.push(this.finding(this.invariants_a_generator_header_carries_both_registers_in_order, unit, line, `\`${grammar.GENERATOR_DESCRIBED}\` must follow \`${grammar.GENERATOR}\``));
50
+ if (!header.goal) findings.push(this.finding(this.invariants_a_generator_header_carries_both_registers_in_order, unit, line, 'the formal register needs a `Goal:` line'));
51
+ if (!header.impossibilities.size) findings.push(this.finding(this.invariants_a_generator_header_carries_both_registers_in_order, unit, line, 'the formal register needs at least one `Impossible if true:` line'));
52
+ }
53
+ return findings;
54
+ });
55
+ }
56
+
57
+ static get invariants_a_header_symbol_is_declared_in_the_sibling_source(): CheckStandard.StandardCheck {
58
+ return this.defineCheck('invariants_a_header_symbol_is_declared_in_the_sibling_source', (context) => {
59
+ const findings: CheckStandard.Finding[] = [];
60
+ for (const unit of context.tests) {
61
+ const header = this.parseHeader(unit);
62
+ if (!header.present) continue;
63
+ let subjectTexts: string[] = [];
64
+ if (header.subjects.length) {
65
+ let broken = false;
66
+ for (const subject of header.subjects) {
67
+ const candidates = [resolve(dirname(unit.path), subject.path), resolve(context.cwd, subject.path)];
68
+ const found = candidates.find(existsSync);
69
+ if (!found) {
70
+ findings.push(this.finding(this.invariants_a_header_symbol_is_declared_in_the_sibling_source, unit, subject.line, `Subject path does not exist: ${subject.path}`));
71
+ broken = true;
72
+ continue;
73
+ }
74
+ subjectTexts.push(readFileSync(found, 'utf8'));
75
+ }
76
+ if (broken) continue;
77
+ } else {
78
+ const sourcePath = this.siblingSourcePath(unit.path);
79
+ if (!existsSync(sourcePath)) {
80
+ findings.push(this.finding(this.invariants_a_header_symbol_is_declared_in_the_sibling_source, unit, 1, `no sibling source \`${basename(sourcePath)}\` for this test file's header symbols — name the source with a \`Subject:\` line, or colocate the test`));
81
+ continue;
82
+ }
83
+ subjectTexts = [readFileSync(sourcePath, 'utf8')];
84
+ }
85
+ const subjectDescription = header.subjects.length ? header.subjects.map((subject) => basename(subject.path)).join(', ') : basename(this.siblingSourcePath(unit.path));
86
+ for (const { symbol, line } of header.domainClaims.values()) {
87
+ if (!subjectTexts.some((text) => this.declaredInSource(text, symbol)))
88
+ findings.push(this.finding(this.invariants_a_header_symbol_is_declared_in_the_sibling_source, unit, line, `header symbol \`${symbol}\` is not declared in ${subjectDescription}`));
89
+ }
90
+ }
91
+ return findings;
92
+ });
93
+ }
94
+
95
+ static get invariants_a_claim_annotation_sits_directly_above_its_test(): CheckStandard.StandardCheck {
96
+ return this.defineCheck('invariants_a_claim_annotation_sits_directly_above_its_test', (context) => {
97
+ const findings: CheckStandard.Finding[] = [];
98
+ for (const unit of context.tests) {
99
+ const header = this.parseHeader(unit);
100
+ if (!header.present) continue;
101
+ for (const proof of this.parseProofs(unit, header)) {
102
+ if (!proof.bound) findings.push(this.finding(this.invariants_a_claim_annotation_sits_directly_above_its_test, unit, proof.line, 'proof annotation must sit directly above a test (an optional doc comment may sit between)'));
103
+ }
104
+ }
105
+ return findings;
106
+ });
107
+ }
108
+
109
+ static get invariants_header_claims_and_annotated_tests_match_one_to_one(): CheckStandard.StandardCheck {
110
+ return this.defineCheck('invariants_header_claims_and_annotated_tests_match_one_to_one', (context) => {
111
+ const findings: CheckStandard.Finding[] = [];
112
+ for (const unit of context.tests) {
113
+ const header = this.parseHeader(unit);
114
+ if (!header.present) continue;
115
+ const proofs = this.parseProofs(unit, header).filter((proof) => proof.bound && proof.type === 'domain');
116
+ const proved = new Set<string>();
117
+ for (const proof of proofs) {
118
+ const key = `${proof.symbol} — ${proof.claim}`;
119
+ if (header.domainClaims.has(key)) proved.add(key);
120
+ else if (!header.impossibilities.has(proof.claim ?? '')) findings.push(this.finding(this.invariants_header_claims_and_annotated_tests_match_one_to_one, unit, proof.line, `annotated test claim is absent from the header: ${key}`));
121
+ }
122
+ for (const [key, { line }] of header.domainClaims) {
123
+ if (!proved.has(key)) findings.push(this.finding(this.invariants_header_claims_and_annotated_tests_match_one_to_one, unit, line, `header ${this.$grammar.DOMAIN} has no annotated test: ${key}`));
124
+ }
125
+ }
126
+ return findings;
127
+ });
128
+ }
129
+
130
+ static get invariants_an_impossibility_is_proved_by_an_exact_negative_test(): CheckStandard.StandardCheck {
131
+ return this.defineCheck('invariants_an_impossibility_is_proved_by_an_exact_negative_test', (context) => {
132
+ const findings: CheckStandard.Finding[] = [];
133
+ for (const unit of context.tests) {
134
+ const header = this.parseHeader(unit);
135
+ if (!header.present) continue;
136
+ const proofs = this.parseProofs(unit, header).filter((proof) => proof.bound);
137
+ const proved = new Set<string>();
138
+ for (const proof of proofs) {
139
+ if (proof.type === 'impossible') {
140
+ if (header.impossibilities.has(proof.claim ?? '')) {
141
+ proved.add(proof.claim ?? '');
142
+ if (!header.domainSymbols.has(proof.symbol ?? '')) findings.push(this.finding(this.invariants_an_impossibility_is_proved_by_an_exact_negative_test, unit, proof.line, `impossibility proof symbol \`${proof.symbol}\` is absent from the header`));
143
+ } else if (header.domainClaims.has(`${proof.symbol} — ${proof.claim}`)) findings.push(this.finding(this.invariants_an_impossibility_is_proved_by_an_exact_negative_test, unit, proof.line, `an invariant is labeled as an impossibility: ${proof.claim}`));
144
+ else findings.push(this.finding(this.invariants_an_impossibility_is_proved_by_an_exact_negative_test, unit, proof.line, `impossibility text is not exact — no header line reads: ${proof.claim}`));
145
+ }
146
+ if (proof.type === 'domain' && header.impossibilities.has(proof.claim ?? ''))
147
+ findings.push(this.finding(this.invariants_an_impossibility_is_proved_by_an_exact_negative_test, unit, proof.line, `an impossibility is labeled as an invariant: ${proof.claim}`));
148
+ }
149
+ for (const [claim, line] of header.impossibilities) {
150
+ if (!proved.has(claim)) findings.push(this.finding(this.invariants_an_impossibility_is_proved_by_an_exact_negative_test, unit, line, `Impossible if true has no annotated negative test: ${claim}`));
151
+ }
152
+ }
153
+ return findings;
154
+ });
155
+ }
156
+
157
+ static get invariants_a_contract_pointer_resolves_and_is_proved(): CheckStandard.StandardCheck {
158
+ return this.defineCheck('invariants_a_contract_pointer_resolves_and_is_proved', (context) => {
159
+ const findings: CheckStandard.Finding[] = [];
160
+ for (const unit of context.tests) {
161
+ const header = this.parseHeader(unit);
162
+ if (!header.present) continue;
163
+ const proofs = this.parseProofs(unit, header).filter((proof) => proof.bound && proof.type === 'record');
164
+ const provedNames = new Set(proofs.map((proof) => this.headingSlug(proof.name ?? '')));
165
+ for (const link of header.contractLinks) {
166
+ if (!link.anchor) {
167
+ findings.push(this.finding(this.invariants_a_contract_pointer_resolves_and_is_proved, unit, link.line, `contract link \`${link.file}\` needs a record anchor`));
168
+ continue;
169
+ }
170
+ const candidates = [resolve(dirname(unit.path), link.file), resolve(context.cwd, link.file)];
171
+ const slugs = candidates.map((candidate) => this.contractSlugs(candidate)).find((set) => set !== null) ?? null;
172
+ if (!slugs) {
173
+ findings.push(this.finding(this.invariants_a_contract_pointer_resolves_and_is_proved, unit, link.line, `contract not found: ${link.file}`));
174
+ continue;
175
+ }
176
+ if (!slugs.has(link.anchor)) {
177
+ findings.push(this.finding(this.invariants_a_contract_pointer_resolves_and_is_proved, unit, link.line, `anchor \`#${link.anchor}\` does not resolve in ${link.file}`));
178
+ continue;
179
+ }
180
+ if (!provedNames.has(link.anchor)) findings.push(this.finding(this.invariants_a_contract_pointer_resolves_and_is_proved, unit, link.line, `header contract-record pointer has no annotated test: ${link.anchor}`));
181
+ }
182
+ for (const proof of proofs) {
183
+ if (!header.contractLinks.some((link) => link.anchor === this.headingSlug(proof.name ?? ''))) findings.push(this.finding(this.invariants_a_contract_pointer_resolves_and_is_proved, unit, proof.line, `annotated record is absent from the header: ${proof.name}`));
184
+ }
185
+ }
186
+ return findings;
187
+ });
188
+ }
189
+
190
+ static get invariants_a_source_tripwire_resolves_to_its_sibling_header(): CheckStandard.StandardCheck {
191
+ return this.defineCheck('invariants_a_source_tripwire_resolves_to_its_sibling_header', (context) => {
192
+ const findings: CheckStandard.Finding[] = [];
193
+ const grammar = this.$grammar;
194
+ const SYMBOL_ONLY = new RegExp(`^\\s*//\\s*${grammar.DOMAIN}:\\s*([^—\\n]+?)\\s*$`);
195
+ for (const unit of context.sources) {
196
+ const testPath = unit.path.replace(/\.ts$/, '.test.ts');
197
+ let siblingSymbols: Set<string> | null = null;
198
+ unit.lines.forEach((line, index) => {
199
+ if (!line.includes(`${grammar.DOMAIN}:`)) return;
200
+ const symbolOnly = SYMBOL_ONLY.exec(line);
201
+ if (!symbolOnly) {
202
+ findings.push(this.finding(this.invariants_a_source_tripwire_resolves_to_its_sibling_header, unit, index + 1, `source tripwires carry only the symbol: \`// ${grammar.DOMAIN}: <symbol>\``));
203
+ return;
204
+ }
205
+ if (siblingSymbols === null) {
206
+ siblingSymbols = existsSync(testPath) ? this.parseHeader(this.toUnit(context.cwd, testPath)).domainSymbols : new Set();
207
+ }
208
+ if (!siblingSymbols.has(symbolOnly[1].trim()))
209
+ findings.push(this.finding(this.invariants_a_source_tripwire_resolves_to_its_sibling_header, unit, index + 1, `tripwire \`${symbolOnly[1].trim()}\` has no header claim in ${basename(testPath)}`));
210
+ });
211
+ if (unit.text.includes(grammar.GENERATOR)) findings.push(this.finding(this.invariants_a_source_tripwire_resolves_to_its_sibling_header, unit, unit.lines.findIndex((line) => line.includes(grammar.GENERATOR)) + 1, `\`${grammar.GENERATOR}\` belongs at the top of the sibling test file, not in source`));
212
+ }
213
+ return findings;
214
+ });
215
+ }
216
+
217
+ static get invariants_a_test_caveat_derives_from_a_tested_claim(): CheckStandard.StandardCheck {
218
+ return this.defineCheck('invariants_a_test_caveat_derives_from_a_tested_claim', (context) => {
219
+ const findings: CheckStandard.Finding[] = [];
220
+ for (const unit of context.tests) {
221
+ const header = this.parseHeader(unit);
222
+ if (!header.present || !header.described) continue;
223
+ const symbols = [...header.domainSymbols];
224
+ const startLine = unit.lines.findIndex((line) => line.includes(this.$grammar.GENERATOR_DESCRIBED)) + 1;
225
+ const sentences = header.described.replace(/^\s*\*\s?/gm, '').split(/(?<=[.!?])\s+/);
226
+ for (const sentence of sentences) {
227
+ if (!/\b(?:must|never|always|only|cannot)\b/i.test(sentence)) continue;
228
+ if (/Open question:/i.test(sentence)) continue;
229
+ if (symbols.some((symbol) => sentence.includes(symbol))) continue;
230
+ findings.push(this.finding(this.invariants_a_test_caveat_derives_from_a_tested_claim, unit, startLine, `described-register caveat names no header symbol — a constraint the tests do not reach is a claim without a proof: "${sentence.trim().slice(0, 90)}"`));
231
+ }
232
+ }
233
+ return findings;
234
+ });
235
+ }
236
+
237
+ static get invariants_two_test_files_do_not_share_one_generator_header(): CheckStandard.StandardCheck {
238
+ return this.defineCheck('invariants_two_test_files_do_not_share_one_generator_header', (context) => {
239
+ const findings: CheckStandard.Finding[] = [];
240
+ const normalized = new Map<string, CheckStandard.SourceUnit>();
241
+ for (const unit of context.tests) {
242
+ const header = this.parseHeader(unit);
243
+ if (!header.present) continue;
244
+ let text = `${header.goal}\n${header.described}`.replace(/\s+/g, ' ').trim();
245
+ for (const symbol of header.domainSymbols) text = text.replaceAll(symbol, '<symbol>');
246
+ text = text.replaceAll(basename(unit.path).replace(/\.test\.ts$/, ''), '<file>');
247
+ if (!text) continue;
248
+ const twin = normalized.get(text);
249
+ if (twin) findings.push(this.finding(this.invariants_two_test_files_do_not_share_one_generator_header, unit, 1, `generator header is a template twin of ${twin.relativePath} — a Goal that fits another file with the name swapped is not a Goal`));
250
+ else normalized.set(text, unit);
251
+ }
252
+ return findings;
253
+ });
254
+ }
255
+
256
+ static get checks(): readonly CheckStandard.StandardCheck[] {
257
+ return [
258
+ ...super.checks,
259
+ this.invariants_a_test_file_opens_with_its_generator_header,
260
+ this.invariants_a_generator_header_carries_both_registers_in_order,
261
+ this.invariants_a_header_symbol_is_declared_in_the_sibling_source,
262
+ this.invariants_a_claim_annotation_sits_directly_above_its_test,
263
+ this.invariants_header_claims_and_annotated_tests_match_one_to_one,
264
+ this.invariants_an_impossibility_is_proved_by_an_exact_negative_test,
265
+ this.invariants_a_contract_pointer_resolves_and_is_proved,
266
+ this.invariants_a_source_tripwire_resolves_to_its_sibling_header,
267
+ this.invariants_a_test_caveat_derives_from_a_tested_claim,
268
+ this.invariants_two_test_files_do_not_share_one_generator_header,
269
+ ];
270
+ }
271
+
272
+ static get proofs(): Readonly<Record<string, CheckStandard.CheckProof>> {
273
+ const fixture = this.$fixtures;
274
+ const grammar = this.$grammar;
275
+ const contractName = `demo${grammar.CONTRACT_SUFFIX}`;
276
+ const box = { 'src/Box.ts': fixture.validClass };
277
+ const boxAndTest = { ...box, 'src/Box.test.ts': fixture.validTest };
278
+ const crate = (text: string) => text.replaceAll('Box', 'Crate');
279
+ const pointerTest = (pointer: string, annotation: string) =>
280
+ fixture.validTest
281
+ .replace('Impossible if true:', `${pointer}\nImpossible if true:`)
282
+ .replace(`// ${grammar.IMPOSSIBLE}: $Box — height decreases without a grow call\ntest('height never decreases on its own'`, `${annotation}// ${grammar.IMPOSSIBLE}: $Box — height decreases without a grow call\ntest('height never decreases on its own'`);
283
+ return {
284
+ ...super.proofs,
285
+ 'invariants_a_test_file_opens_with_its_generator_header': {
286
+ claim: 'If a file is a test, then its first content is the generator header',
287
+ impossibility: 'a file breaking invariants_a_test_file_opens_with_its_generator_header passes the gate',
288
+ red: [{
289
+ files: {
290
+ ...box,
291
+ 'src/Box.test.ts': fixture.validTest.slice(fixture.validTest.indexOf('import { expect')),
292
+ 'src/Crate.ts': crate(fixture.validClass),
293
+ 'src/Crate.test.ts': crate(`import { expect, test } from 'vitest';\n${fixture.validTest}`),
294
+ },
295
+ expectFindings: [/opens with its generator header, before any import/, /not the first content/],
296
+ expectCount: 2,
297
+ }],
298
+ green: [{ files: boxAndTest }],
299
+ },
300
+ 'invariants_a_generator_header_carries_both_registers_in_order': {
301
+ claim: 'If a header exists, then it has one Goal, the formal register, at least one Impossible if true, and the described register after the formal one',
302
+ impossibility: 'a file breaking invariants_a_generator_header_carries_both_registers_in_order passes the gate',
303
+ red: [{
304
+ files: {
305
+ ...box,
306
+ 'src/Box.test.ts': fixture.validTest.replace(`${grammar.GENERATOR}\nGoal:`, `${grammar.GENERATOR_DESCRIBED}\nThe $Box prose.\n${grammar.GENERATOR}\nGoal:`).replace(`\n${grammar.GENERATOR_DESCRIBED}\nThe $Box height is the only mutable state, so growth is the single write path the tests must hold.\n`, '\n'),
307
+ 'src/Crate.ts': crate(fixture.validClass),
308
+ 'src/Crate.test.ts': crate(fixture.validTest.replace('Goal: Prove the box grows by exactly one height unit per grow call and that height never moves on its own.\n', '').replace('Impossible if true: height decreases without a grow call\n', '').replace(`// ${grammar.IMPOSSIBLE}: $Box — height decreases without a grow call\n`, '')),
309
+ },
310
+ expectFindings: [/must follow/, /needs a `Goal:` line/, /at least one `Impossible if true:`/],
311
+ }],
312
+ green: [{ files: boxAndTest }],
313
+ },
314
+ 'invariants_a_header_symbol_is_declared_in_the_sibling_source': {
315
+ claim: 'If a header names a symbol, then the named Subject or the same-named sibling source declares it',
316
+ impossibility: 'a file breaking invariants_a_header_symbol_is_declared_in_the_sibling_source passes the gate',
317
+ red: [
318
+ { files: { ...box, 'src/Box.test.ts': fixture.validTest.replaceAll('$Box —', '$Crate —') }, expectFindings: [/`\$Crate` is not declared in Box\.ts/] },
319
+ { files: { ...box, 'src/Box.test.ts': fixture.validTest.replace('Goal:', 'Subject: Missing.ts\nGoal:') }, expectFindings: [/Subject path does not exist: Missing\.ts/] },
320
+ ],
321
+ green: [
322
+ { files: boxAndTest },
323
+ { files: { ...box, 'specs/Growth.test.ts': fixture.validTest.replace('Goal:', 'Subject: src/Box.ts\nGoal:') }, options: { testGlobs: ['specs/**/*.test.ts'] } },
324
+ ],
325
+ },
326
+ 'invariants_a_claim_annotation_sits_directly_above_its_test': {
327
+ claim: 'If a proof annotation is written, then a test follows it directly, an optional doc comment between',
328
+ impossibility: 'a file breaking invariants_a_claim_annotation_sits_directly_above_its_test passes the gate',
329
+ red: [{ files: { ...box, 'src/Box.test.ts': fixture.validTest.replace(`// ${grammar.DOMAIN}: $Box — If grow is called, then height increases by one\ntest('grow`, `// ${grammar.DOMAIN}: $Box — If grow is called, then height increases by one\nconst seed = 1;\ntest('grow`) }, expectFindings: [/must sit directly above a test/] }],
330
+ green: [{ files: { ...box, 'src/Box.test.ts': fixture.validTest.replace(`// ${grammar.DOMAIN}: $Box — If grow is called, then height increases by one\ntest('grow`, `// ${grammar.DOMAIN}: $Box — If grow is called, then height increases by one\n/** The spec: one grow, one unit. */\ntest('grow`) } }],
331
+ },
332
+ 'invariants_header_claims_and_annotated_tests_match_one_to_one': {
333
+ claim: 'If a header states a domain claim, then an annotated test proves it, and every annotated claim is in the header',
334
+ impossibility: 'a file breaking invariants_header_claims_and_annotated_tests_match_one_to_one passes the gate',
335
+ red: [{ files: { ...box, 'src/Box.test.ts': fixture.validTest.replace(`// ${grammar.DOMAIN}: $Box — If grow is called, then height increases by one\ntest('grow`, `// ${grammar.DOMAIN}: $Box — If grow is called, then height doubles\ntest('grow`) }, expectFindings: [/has no annotated test/, /absent from the header/] }],
336
+ green: [{ files: boxAndTest }],
337
+ },
338
+ 'invariants_an_impossibility_is_proved_by_an_exact_negative_test': {
339
+ claim: 'If a header states an impossibility, then a negative test carries its exact text and a header symbol',
340
+ impossibility: 'a file breaking invariants_an_impossibility_is_proved_by_an_exact_negative_test passes the gate',
341
+ red: [{
342
+ files: {
343
+ ...box,
344
+ 'src/Box.test.ts': fixture.validTest.replace(`// ${grammar.IMPOSSIBLE}: $Box — height decreases without a grow call`, `// ${grammar.IMPOSSIBLE}: $Box — height decreases spontaneously`),
345
+ 'src/Crate.ts': crate(fixture.validClass),
346
+ 'src/Crate.test.ts': crate(fixture.validTest.replace(`// ${grammar.IMPOSSIBLE}: $Box — height decreases without a grow call`, `// ${grammar.DOMAIN}: $Box — height decreases without a grow call`)),
347
+ },
348
+ expectFindings: [/impossibility text is not exact/, /has no annotated negative test/, /an impossibility is labeled as an invariant/],
349
+ }],
350
+ green: [{ files: boxAndTest }],
351
+ },
352
+ 'invariants_a_contract_pointer_resolves_and_is_proved': {
353
+ claim: 'If a header links a contract record, then the anchor resolves and an annotated test proves it',
354
+ impossibility: 'a file breaking invariants_a_contract_pointer_resolves_and_is_proved passes the gate',
355
+ red: [{
356
+ files: {
357
+ [contractName]: fixture.demoContract,
358
+ ...box,
359
+ 'src/Box.test.ts': pointerTest(`[A box never shrinks by itself](../${contractName}#a-box-never-grows)`, ''),
360
+ 'src/Crate.ts': crate(fixture.validClass),
361
+ 'src/Crate.test.ts': crate(pointerTest(`[A box never shrinks by itself](../${contractName}#a-box-never-shrinks-by-itself)`, '')),
362
+ },
363
+ expectFindings: [/does not resolve/, /pointer has no annotated test/],
364
+ }],
365
+ green: [{
366
+ files: {
367
+ [contractName]: fixture.demoContract,
368
+ ...box,
369
+ 'src/Box.test.ts': pointerTest(`[A box never shrinks by itself](../${contractName}#a-box-never-shrinks-by-itself)`, `// ${grammar.RECORD}: A box never shrinks by itself (${contractName})\n`),
370
+ },
371
+ }],
372
+ },
373
+ 'invariants_a_source_tripwire_resolves_to_its_sibling_header': {
374
+ claim: 'If source carries a domain tripwire, then it names only a symbol the sibling header claims',
375
+ impossibility: 'a file breaking invariants_a_source_tripwire_resolves_to_its_sibling_header passes the gate',
376
+ red: [{ files: { 'src/Box.ts': fixture.validClass.replace(' grow() {', ` // ${grammar.DOMAIN}: $Crate\n grow() {`), 'src/Box.test.ts': fixture.validTest }, expectFindings: [/tripwire `\$Crate` has no header claim in Box\.test\.ts/] }],
377
+ green: [{ files: { 'src/Box.ts': fixture.validClass.replace(' grow() {', ` // ${grammar.DOMAIN}: $Box\n grow() {`), 'src/Box.test.ts': fixture.validTest } }],
378
+ },
379
+ 'invariants_a_test_caveat_derives_from_a_tested_claim': {
380
+ claim: 'If the described register constrains, then the constraint names a header symbol',
381
+ impossibility: 'a file breaking invariants_a_test_caveat_derives_from_a_tested_claim passes the gate',
382
+ red: [{ files: { ...box, 'src/Box.test.ts': fixture.validTest.replace('so growth is the single write path the tests must hold.', 'so growth is the single write path the tests must hold. Width must never change after construction.') }, expectFindings: [/Width must never change/] }],
383
+ green: [{ files: boxAndTest }],
384
+ },
385
+ 'invariants_two_test_files_do_not_share_one_generator_header': {
386
+ claim: 'If two test files exist, then their Goal and described registers differ beyond their own symbol names',
387
+ impossibility: 'a file breaking invariants_two_test_files_do_not_share_one_generator_header passes the gate',
388
+ red: [{ files: { ...boxAndTest, 'src/Crate.ts': crate(fixture.validClass), 'src/Crate.test.ts': crate(fixture.validTest) }, expectFindings: [/template twin/] }],
389
+ green: [{
390
+ files: {
391
+ ...boxAndTest,
392
+ 'src/Crate.ts': crate(fixture.validClass),
393
+ 'src/Crate.test.ts': crate(fixture.validTest)
394
+ .replace('Goal: Prove the box grows by exactly one height unit per grow call and that height never moves on its own.', 'Goal: Prove a crate reports the area its width and height imply, and nothing else moves it.')
395
+ .replace('The $Crate height is the only mutable state, so growth is the single write path the tests must hold.', 'Area is derived on every read; the $Crate class holds no cached area to drift.'),
396
+ },
397
+ }],
398
+ },
399
+ };
400
+ }
401
+ }
402
+
403
+ export namespace GeneratorStandard {
404
+ export const $Class = Static($GeneratorStandard);
405
+ export let Class = $Class;
406
+
407
+ // Registers this gate as the CLI entry — superseding the base gate's own
408
+ // registration, because the entry module evaluates last.
409
+ CheckStandard.bootstrapCli(Class);
410
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * ivue-house-gate.ts — YOUR gate. The severity menu at the top is the part
3
+ * you edit; the block below it is a working example of how to extend.
4
+ *
5
+ * `npx ivue skill` installs this file into your project and never
6
+ * overwrites a copy you have modified — upgrade a customized gate with
7
+ * your AI agent using the ivue skill, which reconciles your rulings with
8
+ * the new Standard.
9
+ *
10
+ * In this repo the imports are relative (the file lives beside the
11
+ * engine); the installed copy imports from the package instead
12
+ * (`ivue/extras` and `ivue/skills/ivue/ivue-standards-check`).
13
+ *
14
+ * Try it:
15
+ *
16
+ * npm run gate:house -- --list
17
+ * npm run gate:house -- --prove a_source_file_stays_under_the_line_budget
18
+ * npm run gate:house -- --prove
19
+ * npm run gate:house -- --source-root src --test-glob 'src/**\/*.test.ts'
20
+ */
21
+ import { Static } from '../../lib/Static';
22
+ import { CheckStandard } from './ivue-standards-check';
23
+
24
+ // This gate extends the ivue-only Standard. Teams using the invariant
25
+ // methodology (generator headers on test files, claims bound to tests)
26
+ // extend GeneratorStandard.$Class from './ivue-generator-standard'
27
+ // instead — its ten invariants_* checks then join the manifest and this
28
+ // menu.
29
+ class $HouseGate extends CheckStandard.$Class {
30
+ // ── YOUR RULINGS ─────────────────────────────────────────────────────
31
+ // The severity menu: every check at its default, so this gate blocks
32
+ // exactly what the base gate blocks. Flip an entry to 'warn' (report,
33
+ // never block) or 'off' (skip globally, announced in the summary) and
34
+ // the whole team inherits the ruling. Per-file exceptions belong in the
35
+ // skip list (ivue-standards-skip.json), not here.
36
+ static get severities(): Readonly<Record<string, 'error' | 'warn' | 'off'>> {
37
+ return {
38
+ exactly_one_reactive_source_is_installed: 'error',
39
+ a_public_class_publishes_its_namespace_manifest: 'error',
40
+ a_class_file_is_named_after_its_class: 'error',
41
+ a_class_file_holds_only_imports_class_namespace_and_types: 'error',
42
+ behavior_lives_on_the_prototype_not_in_fields: 'error',
43
+ construction_goes_through_the_namespace_class_slot: 'error',
44
+ the_anchor_is_static_only_when_statics_exist: 'error',
45
+ static_binds_methods_and_caches_dollar_getters_per_receiver: 'error',
46
+ a_shared_store_is_a_static_readonly_field: 'error',
47
+ a_derived_static_getter_is_lower_camel_case: 'error',
48
+ static_reads_go_through_self_not_the_base_class: 'error',
49
+ mutable_state_is_a_ref_returning_getter: 'error',
50
+ a_ref_is_read_and_written_through_value: 'error',
51
+ a_derivation_is_a_plain_getter_unless_computed_is_justified: 'error',
52
+ a_composable_is_injected_by_a_one_call_dollar_getter: 'error',
53
+ instance_types_only_unwrapping_surfaces: 'error',
54
+ a_component_has_one_model_owner: 'error',
55
+ script_setup_is_wiring_only: 'error',
56
+ // a hook with logic in its body is a thin-closure slip, not an
57
+ // ownership breach — advisory, like the line budget below
58
+ a_lifecycle_hook_delegates_to_one_method: 'warn',
59
+ the_state_destructure_is_total: 'error',
60
+ template_expressions_carry_no_logic: 'error',
61
+ one_handler_per_event: 'error',
62
+ watch_lifetime_matches_the_instance_owner: 'error',
63
+ a_reactive_closure_delegates_to_one_method: 'error',
64
+ a_store_is_used_lazily_and_swapped_at_the_class_slot: 'error',
65
+ keyed_state_creates_on_read_and_peeks_on_write: 'error',
66
+ a_generic_reactive_class_casts_its_constructor: 'error',
67
+ cross_module_class_reads_happen_inside_bodies: 'error',
68
+ declarations_use_full_descriptive_names: 'error',
69
+ class_members_are_ordered_and_spaced: 'error',
70
+ the_population_and_skip_list_are_exact: 'error',
71
+ // the house example ships FLIPPED: a line budget is advisory by
72
+ // nature — it reports, it never blocks (real classes can be long)
73
+ a_source_file_stays_under_the_line_budget: 'warn',
74
+ };
75
+ }
76
+
77
+ // ── EXTENSION EXAMPLE — how to ADD a house check ─────────────────────
78
+ // A house check is three members, and everything else is inherited
79
+ // (run(), the CLI, the skip-list vocabulary, prove()):
80
+ //
81
+ // 1. the check getter — its name IS the check's one snake_case
82
+ // identity (getter name, finding label, skip token, severity key);
83
+ // 2. `checks` — appends it to the inherited manifest;
84
+ // 3. `proofs` — spreads its constitution entry (claim,
85
+ // impossibility, red arm, green arm) over the inherited ones.
86
+ // prove() REFUSES a check that skips this step.
87
+ //
88
+ // Delete these members to run the plain Standard; copy their shape to
89
+ // add your own rules.
90
+
91
+ // A literal tunable constant — SCREAMING_SNAKE per the Standard. The
92
+ // check's name deliberately carries no number, so pinching this knob
93
+ // (here, or in a deeper subclass) never falsifies it; the finding
94
+ // message states the current budget.
95
+ static get MAX_SOURCE_LINES() {
96
+ return 900;
97
+ }
98
+
99
+ static get a_source_file_stays_under_the_line_budget(): CheckStandard.StandardCheck {
100
+ return this.defineCheck('a_source_file_stays_under_the_line_budget', (context) =>
101
+ context.sources
102
+ .filter((unit) => unit.lines.length > this.MAX_SOURCE_LINES)
103
+ .map((unit) => this.finding(this.a_source_file_stays_under_the_line_budget, unit, 1, `${unit.lines.length} lines — the budget is ${this.MAX_SOURCE_LINES}; split the module`)),
104
+ );
105
+ }
106
+
107
+ static get checks(): readonly CheckStandard.StandardCheck[] {
108
+ return [...super.checks, this.a_source_file_stays_under_the_line_budget];
109
+ }
110
+
111
+ static get proofs(): Readonly<Record<string, CheckStandard.CheckProof>> {
112
+ return {
113
+ ...super.proofs,
114
+ [this.a_source_file_stays_under_the_line_budget.name]: {
115
+ claim: 'If a source file exceeds the line budget, then the gate names it and states the budget',
116
+ impossibility: 'a source file over the line budget passes the gate',
117
+ red: [{ files: { 'src/Long.ts': `${'// filler\n'.repeat(901)}export type Filler = number;\n` }, expectFindings: [/90\d lines — the budget is \d+; split the module/] }],
118
+ green: [{ files: { 'src/Short.ts': 'export type Short = number;\n' } }],
119
+ },
120
+ };
121
+ }
122
+ }
123
+
124
+ export namespace HouseGate {
125
+ export const $Class = Static($HouseGate);
126
+ export let Class = $Class;
127
+
128
+ // Registers this gate as the CLI entry — superseding the base gate's own
129
+ // registration, because the entry module evaluates last.
130
+ CheckStandard.bootstrapCli(Class);
131
+ }