@relational-fabric/canon 1.0.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/LICENSE +21 -0
- package/README.md +329 -0
- package/eslint.js +31 -0
- package/package.json +126 -0
- package/src/axiom.ts +34 -0
- package/src/axioms/id.ts +53 -0
- package/src/axioms/references.ts +98 -0
- package/src/axioms/timestamps.ts +77 -0
- package/src/axioms/type.ts +53 -0
- package/src/axioms/version.ts +53 -0
- package/src/canon.ts +47 -0
- package/src/index.ts +30 -0
- package/src/radar/converter.ts +124 -0
- package/src/radar/index.ts +11 -0
- package/src/radar/validator.ts +310 -0
- package/src/registry.test.ts +75 -0
- package/src/registry.ts +72 -0
- package/src/shell.test.ts +63 -0
- package/src/shell.ts +49 -0
- package/src/testing.ts +43 -0
- package/src/types/axioms.ts +131 -0
- package/src/types/canons.ts +78 -0
- package/src/types/guards.ts +51 -0
- package/src/types/index.ts +10 -0
- package/src/types/js.ts +25 -0
- package/src/types/objects.ts +32 -0
- package/src/types/radar.ts +106 -0
- package/src/utils/guards.test.ts +27 -0
- package/src/utils/guards.ts +29 -0
- package/src/utils/objects.test.ts +141 -0
- package/src/utils/objects.ts +172 -0
- package/tsconfig.base.json +11 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for canon registry
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CanonConfig } from './types/index.js'
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { createRegistry, Registry } from './registry.js'
|
|
8
|
+
|
|
9
|
+
// Declare test canons in the type system
|
|
10
|
+
declare module './types/canons.js' {
|
|
11
|
+
interface Canons {
|
|
12
|
+
TestCanon: {
|
|
13
|
+
Id: {
|
|
14
|
+
$basis: { id: string }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
Canon1: Record<string, never>
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Test canon configurations
|
|
22
|
+
const testCanonConfig: CanonConfig = {
|
|
23
|
+
axioms: {
|
|
24
|
+
Id: {
|
|
25
|
+
$basis: (v: unknown): v is { id: string } => true,
|
|
26
|
+
key: 'id',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const emptyCanonConfig: CanonConfig = {
|
|
32
|
+
axioms: {},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('registry', () => {
|
|
36
|
+
describe('register', () => {
|
|
37
|
+
it('should register a canon', () => {
|
|
38
|
+
const registry = new Registry()
|
|
39
|
+
|
|
40
|
+
registry.register('TestCanon', testCanonConfig)
|
|
41
|
+
|
|
42
|
+
expect(registry.has('TestCanon')).toBe(true)
|
|
43
|
+
expect(registry.get('TestCanon')).toBe(testCanonConfig)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('size', () => {
|
|
48
|
+
it('should return number of registered canons', () => {
|
|
49
|
+
const registry = new Registry()
|
|
50
|
+
expect(registry.size).toBe(0)
|
|
51
|
+
|
|
52
|
+
registry.register('Canon1', emptyCanonConfig)
|
|
53
|
+
expect(registry.size).toBe(1)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('clear', () => {
|
|
58
|
+
it('should remove all canons', () => {
|
|
59
|
+
const registry = new Registry()
|
|
60
|
+
registry.register('Canon1', emptyCanonConfig)
|
|
61
|
+
expect(registry.size).toBe(1)
|
|
62
|
+
|
|
63
|
+
registry.clear()
|
|
64
|
+
expect(registry.size).toBe(0)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('createRegistry', () => {
|
|
70
|
+
it('should create an empty registry', () => {
|
|
71
|
+
const registry = createRegistry()
|
|
72
|
+
expect(registry).toBeInstanceOf(Registry)
|
|
73
|
+
expect(registry.size).toBe(0)
|
|
74
|
+
})
|
|
75
|
+
})
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canon Registry
|
|
3
|
+
*
|
|
4
|
+
* Class for storing and managing canon configurations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CanonConfig, Canons } from './types/index.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Canon Registry class
|
|
11
|
+
*
|
|
12
|
+
* Stores canon configurations. Axioms are defined within canons.
|
|
13
|
+
*/
|
|
14
|
+
export class Registry {
|
|
15
|
+
private canons = new Map<string, CanonConfig>()
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register a canon
|
|
19
|
+
*/
|
|
20
|
+
register<Label extends keyof Canons>(label: Label, config: CanonConfig): void {
|
|
21
|
+
this.canons.set(label as string, config)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get a canon configuration by label
|
|
26
|
+
*/
|
|
27
|
+
get(label: string): CanonConfig | undefined {
|
|
28
|
+
return this.canons.get(label)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get all registered canon configurations
|
|
33
|
+
*/
|
|
34
|
+
values(): IterableIterator<CanonConfig> {
|
|
35
|
+
return this.canons.values()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Make registry iterable - iterates over canon configs
|
|
40
|
+
*/
|
|
41
|
+
*[Symbol.iterator](): Iterator<CanonConfig> {
|
|
42
|
+
yield * this.canons.values()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if a canon is registered
|
|
47
|
+
*/
|
|
48
|
+
has(label: string): boolean {
|
|
49
|
+
return this.canons.has(label)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the number of registered canons
|
|
54
|
+
*/
|
|
55
|
+
get size(): number {
|
|
56
|
+
return this.canons.size
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Clear all registered canons
|
|
61
|
+
*/
|
|
62
|
+
clear(): void {
|
|
63
|
+
this.canons.clear()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a new empty registry
|
|
69
|
+
*/
|
|
70
|
+
export function createRegistry(): Registry {
|
|
71
|
+
return new Registry()
|
|
72
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for global shell
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CanonConfig } from './types/index.js'
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
7
|
+
import { Registry } from './registry.js'
|
|
8
|
+
import { declareCanon, getRegistry, resetRegistry } from './shell.js'
|
|
9
|
+
|
|
10
|
+
describe('shell', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
resetRegistry()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
// Just clear - isolated tests don't need full restoration
|
|
17
|
+
resetRegistry()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('getRegistry', () => {
|
|
21
|
+
it('should return the global registry', () => {
|
|
22
|
+
const registry = getRegistry()
|
|
23
|
+
expect(registry).toBeInstanceOf(Registry)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('resetRegistry', () => {
|
|
28
|
+
it('should clear the global registry', () => {
|
|
29
|
+
const config: CanonConfig = {
|
|
30
|
+
axioms: {
|
|
31
|
+
Id: {
|
|
32
|
+
$basis: (v: unknown): v is { id: string } => true,
|
|
33
|
+
key: 'id',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declareCanon('TestCanon' as any, config)
|
|
39
|
+
expect(getRegistry().size).toBe(1)
|
|
40
|
+
|
|
41
|
+
resetRegistry()
|
|
42
|
+
expect(getRegistry().size).toBe(0)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('declareCanon', () => {
|
|
47
|
+
it('should register canon in global registry', () => {
|
|
48
|
+
const config: CanonConfig = {
|
|
49
|
+
axioms: {
|
|
50
|
+
Id: {
|
|
51
|
+
$basis: (v: unknown): v is { id: string } => true,
|
|
52
|
+
key: 'id',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declareCanon('TestCanon' as any, config)
|
|
58
|
+
|
|
59
|
+
expect(getRegistry().has('TestCanon')).toBe(true)
|
|
60
|
+
expect(getRegistry().get('TestCanon')).toBe(config)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
package/src/shell.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canon Shell
|
|
3
|
+
*
|
|
4
|
+
* Singleton registry instance with convenience API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CanonConfig, Canons } from './types/index.js'
|
|
8
|
+
import { createRegistry, type Registry } from './registry.js'
|
|
9
|
+
import { defineCanon } from './types/index.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Global singleton registry
|
|
13
|
+
*/
|
|
14
|
+
const globalRegistry: Registry = createRegistry()
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the current global registry
|
|
18
|
+
*/
|
|
19
|
+
export function getRegistry(): Registry {
|
|
20
|
+
return globalRegistry
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reset the global registry to empty
|
|
25
|
+
*/
|
|
26
|
+
export function resetRegistry(): void {
|
|
27
|
+
globalRegistry.clear()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Declare a canon in the global registry
|
|
32
|
+
*
|
|
33
|
+
* @param label - The canon label
|
|
34
|
+
* @param config - The canon configuration
|
|
35
|
+
*/
|
|
36
|
+
export function declareCanon<Label extends keyof Canons>(label: Label, config: CanonConfig): void {
|
|
37
|
+
globalRegistry.register(label, defineCanon(config))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Register multiple canons at once (for module-style registration)
|
|
42
|
+
*
|
|
43
|
+
* @param canons - Object mapping canon labels to their configurations
|
|
44
|
+
*/
|
|
45
|
+
export function registerCanons(canons: Record<string, CanonConfig>): void {
|
|
46
|
+
for (const [label, config] of Object.entries(canons)) {
|
|
47
|
+
globalRegistry.register(label as keyof Canons, defineCanon(config))
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time type testing utilities for Canon
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Expect the first type to extend the second type.
|
|
7
|
+
*/
|
|
8
|
+
export type Expect<A, B> = A extends B ? true : false
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Assert that a type resolves to `true`.
|
|
12
|
+
*/
|
|
13
|
+
export type IsTrue<A> = Expect<A, true>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Assert that a type resolves to `false`.
|
|
17
|
+
*/
|
|
18
|
+
export type IsFalse<A> = A extends false ? true : false
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Runtime no-op function that enforces the provided type resolves to `true`.
|
|
22
|
+
*/
|
|
23
|
+
export function invariant<_ extends true>(): void {
|
|
24
|
+
// Intentionally empty – the generic constraint encodes the assertion.
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Self-validation of the utilities
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
void invariant<Expect<true, true>>()
|
|
32
|
+
void invariant<Expect<'value', string>>()
|
|
33
|
+
void invariant<Expect<1 | 2, number>>()
|
|
34
|
+
void invariant<IsFalse<Expect<string, number>>>()
|
|
35
|
+
|
|
36
|
+
// @ts-expect-error - Expect should fail when the left side does not extend the right.
|
|
37
|
+
void invariant<Expect<{ id: string }, { id: number }>>()
|
|
38
|
+
|
|
39
|
+
// @ts-expect-error - IsTrue rejects non-true values.
|
|
40
|
+
void invariant<IsTrue<false>>()
|
|
41
|
+
|
|
42
|
+
// @ts-expect-error - IsFalse only accepts precisely `false`.
|
|
43
|
+
void invariant<IsFalse<true>>()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Axiom type system for Canon
|
|
3
|
+
*
|
|
4
|
+
* Axioms are atomic building blocks that define semantic concepts
|
|
5
|
+
* (like ID, type, version) that can be found in different data structures.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TypeGuard } from './guards.js'
|
|
9
|
+
import { type Expect, invariant } from '../testing.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base axiom type that merges configuration with metadata
|
|
13
|
+
*
|
|
14
|
+
* @template TConfig - The axiom configuration shape
|
|
15
|
+
* @template TMeta - The metadata shape
|
|
16
|
+
*/
|
|
17
|
+
export type Axiom<TConfig, TMeta> = TConfig & { $meta: TMeta }
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Key-name axiom pattern for field-name-based concepts
|
|
21
|
+
*
|
|
22
|
+
* This represents concepts that can be found by looking for a specific
|
|
23
|
+
* key name within an object (e.g., 'id', '@id', '_id').
|
|
24
|
+
*/
|
|
25
|
+
export type KeyNameAxiom = Axiom<
|
|
26
|
+
{
|
|
27
|
+
$basis: Record<string, unknown>
|
|
28
|
+
key: string
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
[key: string]: unknown
|
|
32
|
+
}
|
|
33
|
+
>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Representation axiom for data with multiple representations
|
|
37
|
+
*
|
|
38
|
+
* This represents concepts that can be converted between different formats
|
|
39
|
+
* with a canonical representation (e.g., timestamps, references).
|
|
40
|
+
*
|
|
41
|
+
* @template T - The input type that can be converted
|
|
42
|
+
* @template C - The canonical type (defaults to unknown)
|
|
43
|
+
*/
|
|
44
|
+
export type RepresentationAxiom<T, C = unknown> = Axiom<
|
|
45
|
+
{
|
|
46
|
+
$basis: T | TypeGuard<unknown>
|
|
47
|
+
isCanonical: (value: unknown) => value is C
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
[key: string]: unknown
|
|
51
|
+
}
|
|
52
|
+
>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Global registry of axioms available in Canon
|
|
56
|
+
*
|
|
57
|
+
* This interface is meant to be augmented by axiom implementers.
|
|
58
|
+
* Each axiom represents a semantic concept that might vary in shape
|
|
59
|
+
* between codebases but is otherwise equivalent.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* declare module '@relational-fabric/canon' {
|
|
64
|
+
* interface Axioms {
|
|
65
|
+
* Id: KeyNameAxiom
|
|
66
|
+
* Type: KeyNameAxiom
|
|
67
|
+
* }
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export interface Axioms {}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Canonical reference type for entity relationships
|
|
75
|
+
*
|
|
76
|
+
* @template R - The reference type (usually string)
|
|
77
|
+
* @template T - The value type (defaults to unknown)
|
|
78
|
+
*/
|
|
79
|
+
export interface EntityReference<R, T = unknown> {
|
|
80
|
+
ref: R
|
|
81
|
+
value?: T
|
|
82
|
+
resolved: boolean
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract the value type from an axiom's $basis field
|
|
87
|
+
*
|
|
88
|
+
* @template TLabel - The axiom label (e.g., 'Id', 'Type')
|
|
89
|
+
*/
|
|
90
|
+
export type AxiomValue<TLabel extends keyof Axioms> = Axioms[TLabel] extends {
|
|
91
|
+
$basis: infer TBasis
|
|
92
|
+
}
|
|
93
|
+
? TBasis extends TypeGuard<infer T>
|
|
94
|
+
? T
|
|
95
|
+
: TBasis
|
|
96
|
+
: never
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Runtime configuration for an axiom
|
|
100
|
+
*
|
|
101
|
+
* This represents the actual runtime behavior including type guards
|
|
102
|
+
* and metadata values.
|
|
103
|
+
*/
|
|
104
|
+
export interface AxiomConfig {
|
|
105
|
+
$basis: TypeGuard<unknown>
|
|
106
|
+
[key: string]: unknown
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Define an axiom runtime configuration
|
|
111
|
+
*
|
|
112
|
+
* Simply returns the config unchanged - useful for creating exportable axioms.
|
|
113
|
+
*
|
|
114
|
+
* @param config - The runtime axiom configuration
|
|
115
|
+
* @returns The same config object
|
|
116
|
+
*/
|
|
117
|
+
export function defineAxiom(config: AxiomConfig): AxiomConfig {
|
|
118
|
+
return config
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Compile-time invariants
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
void invariant<Expect<KeyNameAxiom['key'], string>>()
|
|
126
|
+
void invariant<
|
|
127
|
+
Expect<RepresentationAxiom<string, string>['isCanonical'], (value: unknown) => value is string>
|
|
128
|
+
>()
|
|
129
|
+
void invariant<Expect<EntityReference<string>['ref'], string>>()
|
|
130
|
+
void invariant<Expect<EntityReference<string>['resolved'], boolean>>()
|
|
131
|
+
void invariant<Expect<AxiomConfig['$basis'], TypeGuard<unknown>>>()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canon type system
|
|
3
|
+
*
|
|
4
|
+
* Canons are universal type blueprints that implement axioms for specific
|
|
5
|
+
* data formats. Multiple canons can exist simultaneously, each representing
|
|
6
|
+
* different formats.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AxiomConfig, Axioms } from './axioms.js'
|
|
10
|
+
import { type Expect, invariant } from '../testing.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Canon type that maps axiom labels to their type-level configurations
|
|
14
|
+
*
|
|
15
|
+
* @template TAxioms - Object mapping axiom labels to their configurations
|
|
16
|
+
*/
|
|
17
|
+
export type Canon<TAxioms extends Partial<Axioms>> = TAxioms
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Global registry of canons available in Canon
|
|
21
|
+
*
|
|
22
|
+
* This interface is meant to be augmented by canon implementers.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* declare module '@relational-fabric/canon' {
|
|
27
|
+
* interface Canons {
|
|
28
|
+
* Internal: InternalCanon
|
|
29
|
+
* JsonLd: JsonLdCanon
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export interface Canons {}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Runtime configuration for a canon
|
|
38
|
+
*
|
|
39
|
+
* Maps axiom labels to their runtime configurations including type guards.
|
|
40
|
+
*/
|
|
41
|
+
export interface CanonConfig {
|
|
42
|
+
axioms: Record<string, AxiomConfig>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Constraint type ensuring a value satisfies an axiom's requirements
|
|
47
|
+
*
|
|
48
|
+
* @template TAxiomLabel - The axiom label (e.g., 'Id', 'Type')
|
|
49
|
+
* @template TCanonLabel - Optional specific canon to check against
|
|
50
|
+
*/
|
|
51
|
+
export type Satisfies<
|
|
52
|
+
TAxiomLabel extends keyof Axioms,
|
|
53
|
+
TCanonLabel extends keyof Canons = keyof Canons,
|
|
54
|
+
> = {
|
|
55
|
+
[K in keyof Canons]: TAxiomLabel extends keyof Canons[K]
|
|
56
|
+
? Canons[K][TAxiomLabel] extends { $basis: infer TBasis }
|
|
57
|
+
? TBasis
|
|
58
|
+
: never
|
|
59
|
+
: never
|
|
60
|
+
}[TCanonLabel]
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Define a canon runtime configuration (for module-style exports)
|
|
64
|
+
*
|
|
65
|
+
* Simply returns the config unchanged - useful for creating exportable canons.
|
|
66
|
+
*
|
|
67
|
+
* @param config - The runtime canon configuration
|
|
68
|
+
* @returns The same config object
|
|
69
|
+
*/
|
|
70
|
+
export function defineCanon(config: CanonConfig): CanonConfig {
|
|
71
|
+
return config
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Compile-time invariants
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
void invariant<Expect<CanonConfig['axioms'], Record<string, AxiomConfig>>>()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard utilities for Canon type system
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type Expect, invariant } from '../testing.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Type guard pattern that preserves specific types when narrowing
|
|
9
|
+
*
|
|
10
|
+
* CRITICAL: The parameter type must be `T | unknown` (NOT just `unknown`).
|
|
11
|
+
*
|
|
12
|
+
* While `T | unknown` reduces to `unknown` at runtime (since unknown is the
|
|
13
|
+
* top type), the explicit union is essential for TypeScript's type narrowing.
|
|
14
|
+
* Using just `unknown` makes T disjoint from the parameter, breaking type
|
|
15
|
+
* discrimination. The union ensures T is NOT disjoint with the parameter type.
|
|
16
|
+
*
|
|
17
|
+
* This interface uses overloads to properly discriminate the target type
|
|
18
|
+
* from unknown, allowing TypeScript to infer the most specific type.
|
|
19
|
+
*/
|
|
20
|
+
export interface TypeGuard<T> {
|
|
21
|
+
<U extends T>(obj: U | unknown): obj is U
|
|
22
|
+
(obj: T | unknown): obj is T
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Predicate function type - a boolean-returning function
|
|
27
|
+
*
|
|
28
|
+
* This is the input to typeGuard(), which converts it to a TypeGuard.
|
|
29
|
+
* CRITICAL: Must use `T | unknown` to maintain type compatibility.
|
|
30
|
+
*/
|
|
31
|
+
export interface Predicate<T> {
|
|
32
|
+
(obj: T | unknown): boolean
|
|
33
|
+
<U extends T>(obj: U | unknown): boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Compile-time invariants
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
type ExampleGuard = TypeGuard<{ id: string }>
|
|
41
|
+
type ExamplePredicate = Predicate<{ id: string }>
|
|
42
|
+
|
|
43
|
+
type GuardTarget<T> = T extends TypeGuard<infer Target> ? Target : never
|
|
44
|
+
type PredicateTarget<T> = T extends Predicate<infer Target> ? Target : never
|
|
45
|
+
type GuardReturn<T> = T extends (value: unknown) => value is infer R ? R : never
|
|
46
|
+
type PredicateReturn<T> = T extends (value: unknown) => infer R ? R : never
|
|
47
|
+
|
|
48
|
+
void invariant<Expect<GuardTarget<ExampleGuard>, { id: string }>>()
|
|
49
|
+
void invariant<Expect<PredicateTarget<ExamplePredicate>, { id: string }>>()
|
|
50
|
+
void invariant<Expect<GuardReturn<ExampleGuard>, { id: string }>>()
|
|
51
|
+
void invariant<Expect<PredicateReturn<ExamplePredicate>, boolean>>()
|
package/src/types/js.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Expect, invariant } from '../testing.js'
|
|
2
|
+
|
|
3
|
+
export interface JsType {
|
|
4
|
+
string: string
|
|
5
|
+
number: number
|
|
6
|
+
boolean: boolean
|
|
7
|
+
object: object
|
|
8
|
+
array: unknown[]
|
|
9
|
+
null: null
|
|
10
|
+
undefined: undefined
|
|
11
|
+
symbol: symbol
|
|
12
|
+
bigint: bigint
|
|
13
|
+
// @eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
function: (...args: any[]) => any // This is the only time we're allowing any
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type JsTypeName = keyof JsType
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Compile-time invariants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
void invariant<Expect<JsType['string'], string>>()
|
|
24
|
+
void invariant<Expect<JsType['array'], unknown[]>>()
|
|
25
|
+
void invariant<Expect<JsTypeName, keyof JsType>>()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object type definitions for Canon
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type Expect, invariant } from '../testing.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Plain old JavaScript object type
|
|
9
|
+
*/
|
|
10
|
+
export type Pojo = Record<string, unknown>
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pojo with a specific property of a given type
|
|
14
|
+
*
|
|
15
|
+
* @template T - The base Pojo type
|
|
16
|
+
* @template K - The key name
|
|
17
|
+
* @template V - The value type
|
|
18
|
+
*/
|
|
19
|
+
export type PojoWith<T extends Pojo, K extends string, V = unknown> = T & { [P in K]: V }
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Compile-time invariants
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
interface SampleBase extends Pojo {
|
|
26
|
+
name: string
|
|
27
|
+
}
|
|
28
|
+
type SamplePojo = PojoWith<SampleBase, 'id', string>
|
|
29
|
+
|
|
30
|
+
void invariant<Expect<Pojo, Record<string, unknown>>>()
|
|
31
|
+
void invariant<Expect<SamplePojo['id'], string>>()
|
|
32
|
+
void invariant<Expect<SamplePojo['name'], string>>()
|