|
| 1 | +/** |
| 2 | + * Structural acceptance guards for the enum-typing-via-codec slice (TML-2952). |
| 3 | + * |
| 4 | + * A1 — every emit-path enum/value-set type goes through the codec seam |
| 5 | + * (`CodecLookup.renderValueTypeFor` / `CodecDescriptor.renderValueType`). |
| 6 | + * The pre-refactor helpers that rendered stored values straight to TS |
| 7 | + * literals (`renderValueSetUnionBase`, `renderValueSetLiteral`, |
| 8 | + * `renderEnumValueUnion`, `renderEnumMemberLiteral`) and the |
| 9 | + * `DomainEnumLookup` indirection stay deleted, and no emit-path source |
| 10 | + * reintroduces a direct value-to-literal render. |
| 11 | + * |
| 12 | + * A2 — no SQL or framework-emitter source reads `domain.enum` / `ns.enum` to |
| 13 | + * PRODUCE A TYPE. The single allowed `ns.enum` read in |
| 14 | + * `generate-contract-dts.ts` feeds `generateEnumBlockType`, which emits the |
| 15 | + * runtime `db.enums` dictionary block (not a field/column type). The only |
| 16 | + * domain-enum *typing* reader repo-wide is the Mongo interim resolver |
| 17 | + * (`packages/2-mongo-family/3-tooling/emitter/src/index.ts`), removed by |
| 18 | + * TML-2953. |
| 19 | + * |
| 20 | + * Comments are stripped before scanning so the doc comments naming the deleted |
| 21 | + * helpers (here and in the production sources) do not trip the guard. |
| 22 | + */ |
| 23 | + |
| 24 | +import * as fs from 'node:fs'; |
| 25 | +import * as path from 'node:path'; |
| 26 | +import { describe, expect, it } from 'vitest'; |
| 27 | + |
| 28 | +const repoRoot = path.resolve(__dirname, '../../../../..'); |
| 29 | + |
| 30 | +function stripComments(source: string): string { |
| 31 | + return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, ''); |
| 32 | +} |
| 33 | + |
| 34 | +function collectSourceFiles(dir: string): string[] { |
| 35 | + const files: string[] = []; |
| 36 | + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { |
| 37 | + const full = path.join(dir, entry.name); |
| 38 | + if (entry.isDirectory()) { |
| 39 | + if (entry.name === 'dist' || entry.name === 'node_modules' || entry.name === 'test') continue; |
| 40 | + files.push(...collectSourceFiles(full)); |
| 41 | + } else if (entry.isFile() && entry.name.endsWith('.ts')) { |
| 42 | + files.push(full); |
| 43 | + } |
| 44 | + } |
| 45 | + return files; |
| 46 | +} |
| 47 | + |
| 48 | +function scan( |
| 49 | + relDir: string, |
| 50 | + patterns: readonly RegExp[], |
| 51 | +): Array<{ file: string; line: number; text: string }> { |
| 52 | + const hits: Array<{ file: string; line: number; text: string }> = []; |
| 53 | + for (const file of collectSourceFiles(path.join(repoRoot, relDir))) { |
| 54 | + const lines = stripComments(fs.readFileSync(file, 'utf-8')).split('\n'); |
| 55 | + for (let i = 0; i < lines.length; i++) { |
| 56 | + const line = lines[i]; |
| 57 | + if (line === undefined) continue; |
| 58 | + for (const pattern of patterns) { |
| 59 | + if (pattern.test(line)) { |
| 60 | + hits.push({ file: path.relative(repoRoot, file), line: i + 1, text: line.trim() }); |
| 61 | + break; |
| 62 | + } |
| 63 | + } |
| 64 | + } |
| 65 | + } |
| 66 | + return hits; |
| 67 | +} |
| 68 | + |
| 69 | +// The deleted direct-render helpers and the retired domain-enum typing indirection. |
| 70 | +const DELETED_HELPER_PATTERNS = [ |
| 71 | + /\brenderValueSetUnionBase\b/, |
| 72 | + /\brenderValueSetLiteral\b/, |
| 73 | + /\brenderEnumValueUnion\b/, |
| 74 | + /\brenderEnumMemberLiteral\b/, |
| 75 | + /\bDomainEnumLookup\b/, |
| 76 | + /\bdomainEnumLookup\b/, |
| 77 | +]; |
| 78 | + |
| 79 | +// An actual read of the domain enum entity: `.enum` immediately followed by an |
| 80 | +// index/optional-index/close-paren (`(ns.enum)`, `.enum[…]`, `.enum?.[…]`). |
| 81 | +// This deliberately ignores `.enum` appearing inside a string literal (e.g. a |
| 82 | +// `blindCast` reason describing the cast) — only access syntax counts. |
| 83 | +const DOMAIN_ENUM_READ = /\.enum\s*(\?\.\[|\[|\))/; |
| 84 | + |
| 85 | +describe('A1: enum/value-set emit typing goes through the codec seam', () => { |
| 86 | + it('the deleted direct-render helpers do not reappear in the framework emitter source', () => { |
| 87 | + const hits = scan('packages/1-framework/3-tooling/emitter', DELETED_HELPER_PATTERNS); |
| 88 | + expect(hits).toEqual([]); |
| 89 | + }); |
| 90 | + |
| 91 | + it('the deleted direct-render helpers do not reappear in the SQL emit-path source', () => { |
| 92 | + const hits = scan('packages/2-sql', DELETED_HELPER_PATTERNS); |
| 93 | + expect(hits).toEqual([]); |
| 94 | + }); |
| 95 | + |
| 96 | + it('the deleted direct-render helpers do not reappear in the Mongo emitter source', () => { |
| 97 | + const hits = scan('packages/2-mongo-family/3-tooling/emitter', DELETED_HELPER_PATTERNS); |
| 98 | + expect(hits).toEqual([]); |
| 99 | + }); |
| 100 | +}); |
| 101 | + |
| 102 | +describe('A2: no SQL/framework-emitter source reads domain.enum to produce a type', () => { |
| 103 | + it('the SQL emitter sources enum field/column types from storage, not domain.enum', () => { |
| 104 | + // The SQL emitter (3-tooling) is the emit-path typing reader; it must source |
| 105 | + // from the storage value set. Authoring layers legitimately build domain.enum, |
| 106 | + // so the guard is scoped to the emitter package. |
| 107 | + const hits = scan('packages/2-sql/3-tooling/emitter', [DOMAIN_ENUM_READ]); |
| 108 | + expect(hits).toEqual([]); |
| 109 | + }); |
| 110 | + |
| 111 | + it('the only ns.enum read in the framework emitter feeds the db.enums dictionary block', () => { |
| 112 | + const hits = scan('packages/1-framework/3-tooling/emitter', [DOMAIN_ENUM_READ]); |
| 113 | + // Exactly one allowed read: generate-contract-dts.ts → generateEnumBlockType. |
| 114 | + expect(hits).toHaveLength(1); |
| 115 | + expect(hits[0]?.file).toBe( |
| 116 | + 'packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts', |
| 117 | + ); |
| 118 | + }); |
| 119 | + |
| 120 | + it('the only repo-wide domain-enum typing reader is the Mongo interim resolver', () => { |
| 121 | + // Mongo has no storage value set yet (TML-2953), so its emitter reads |
| 122 | + // domain.enum on an interim basis. This read is expected; the test pins that |
| 123 | + // it is the single Mongo-emitter domain-enum read so its removal is noticed. |
| 124 | + const hits = scan('packages/2-mongo-family/3-tooling/emitter', [DOMAIN_ENUM_READ]); |
| 125 | + expect(hits.map((h) => h.file)).toEqual([ |
| 126 | + 'packages/2-mongo-family/3-tooling/emitter/src/index.ts', |
| 127 | + ]); |
| 128 | + }); |
| 129 | +}); |
0 commit comments