@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.
@@ -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
+ })
@@ -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>>()
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Canon type system exports
3
+ */
4
+
5
+ export * from './axioms.js'
6
+ export * from './canons.js'
7
+ export * from './guards.js'
8
+ export * from './js.js'
9
+ export * from './objects.js'
10
+ export * from './radar.js'
@@ -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>>()