@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Relational Fabric
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,329 @@
1
+ ---
2
+ layout: home
3
+
4
+ hero:
5
+ name: '@relational-fabric/canon'
6
+ text: 'Universal Type Primitives & Axiomatic Systems'
7
+ tagline: 'Build robust, data-centric applications with consistent type blueprints and a curated library ecosystem'
8
+ actions:
9
+ - theme: brand
10
+ text: Get Started
11
+ link: /docs/
12
+ - theme: alt
13
+ text: View on GitHub
14
+ link: https://github.com/RelationalFabric/canon
15
+
16
+ features:
17
+ - icon: 🔄
18
+ title: Lazy Typing Pattern
19
+ details: Write type-safe code against semantic concepts while deferring specific implementations through axioms, canons, and universal APIs
20
+ - icon: 🧩
21
+ title: Axioms
22
+ details: Define semantic concepts independent of data shape - the "what" of your type system
23
+ - icon: 📐
24
+ title: Canons
25
+ details: Provide shape-specific implementations of axioms - the "how" for each data source
26
+ - icon: 🛠️
27
+ title: Universal APIs
28
+ details: Functions that work across all canons - the "interface" for your application code
29
+ - icon: 🎯
30
+ title: Universal Type Primitives
31
+ details: Battle-tested types from the TypeScript ecosystem providing a solid foundation for data-centric applications
32
+ - icon: 📚
33
+ title: Curated Library Ecosystem
34
+ details: Known good set of libraries and modules as a canonical starting point for your projects
35
+ - icon: 🏗️
36
+ title: Modern TypeScript
37
+ details: Based on Node.js 22+ with latest TypeScript features, ES modules, and comprehensive tooling
38
+ - icon: 📖
39
+ title: Architecture Decisions
40
+ details: Documented ADRs and transparent technology radar for strategic planning
41
+ ---
42
+
43
+ **Canon** solves the "empty room problem" by providing universal type primitives and axiomatic systems for building robust, data-centric applications. Instead of starting from scratch with each new project, Canon offers consistent design decisions and type blueprints that can be shared across projects and data shapes.
44
+
45
+ ## What is Canon?
46
+
47
+ Canon is a modern TypeScript package that enables **lazy typing** - writing type-safe code against semantic concepts while deferring specific implementations to runtime configuration. This is achieved through three complementary parts:
48
+
49
+ ### The Lazy Typing Triplet
50
+
51
+ #### 1. Axioms - Define Semantic Concepts
52
+
53
+ Axioms define **what** semantic concepts mean, independent of any specific data shape:
54
+
55
+ - Define the semantic concept (e.g., "unique identifier", "type classification", "temporal data")
56
+ - Specify the type structure without implementation details
57
+ - Enable compile-time type safety through TypeScript interfaces
58
+
59
+ #### 2. Canons - Implement for Shapes
60
+
61
+ Canons provide **how** each axiom is implemented for specific data shapes:
62
+
63
+ - Provide shape-specific field names and structures
64
+ - Multiple canons can coexist for different data sources
65
+ - Register both type-level and runtime configurations
66
+
67
+ #### 3. Universal APIs - Work Across Shapes
68
+
69
+ Universal APIs provide **the interface** your application code uses:
70
+
71
+ - Single API that works across all registered canons
72
+ - Type-safe functions that adapt to different data shapes
73
+ - No shape-specific code in your business logic
74
+
75
+ ### Additional Capabilities
76
+
77
+ #### Universal Type Primitives
78
+
79
+ - Battle-tested types from the TypeScript ecosystem
80
+ - Foundation for building data-centric applications
81
+ - Type-safe operations and proven patterns
82
+
83
+ #### Type Testing Utilities
84
+
85
+ - Zero-runtime-cost compile-time type assertions
86
+ - Guard against type regressions with `Expect<A, B>`
87
+ - Document type expectations directly in code
88
+ - Positive and negative type checks with `IsTrue` and `IsFalse`
89
+
90
+ #### Curated Library Ecosystem
91
+
92
+ Canon serves as a **canonical starting point** by providing:
93
+
94
+ - **Pre-configured TypeScript setup** - Base configurations that work out of the box
95
+ - **Curated dependency set** - Known good versions of essential libraries for common needs
96
+ - **Standardized patterns** - Consistent approaches to common problems
97
+ - **Best practice implementations** - Proven solutions for type safety and data handling
98
+ - **Sensible defaults** - Recommended choices when selecting data structures
99
+
100
+ This approach reduces decision fatigue and provides confidence in your technology stack while recognizing that in many real-world scenarios you must work with existing data structures.
101
+
102
+ ## Quick Start
103
+
104
+ ### Install
105
+
106
+ ```bash
107
+ npm install @relational-fabric/canon
108
+ ```
109
+
110
+ ### Use TypeScript Configuration
111
+
112
+ ```json
113
+ {
114
+ "extends": "@relational-fabric/canon/tsconfig"
115
+ }
116
+ ```
117
+
118
+ ### Use ESLint Configuration
119
+
120
+ ```javascript
121
+ // eslint.config.js
122
+ import createEslintConfig from '@relational-fabric/canon/eslint'
123
+
124
+ export default createEslintConfig()
125
+ ```
126
+
127
+ #### Providing Custom Options
128
+
129
+ ```javascript
130
+ // eslint.config.js
131
+ import createEslintConfig from '@relational-fabric/canon/eslint'
132
+
133
+ export default createEslintConfig({
134
+ ignores: ['custom-ignore'],
135
+ rules: {
136
+ 'no-console': 'warn',
137
+ },
138
+ })
139
+ ```
140
+
141
+ ## Requirements
142
+
143
+ This package requires the following peer dependencies:
144
+
145
+ - **Node.js**: 22.0.0 or higher
146
+ - **TypeScript**: 5.0.0 or higher
147
+ - **ESLint**: 9.0.0 or higher
148
+
149
+ ## Core Concepts
150
+
151
+ ### The Lazy Typing Pattern
152
+
153
+ Canon implements **lazy typing** through three complementary components that work together:
154
+
155
+ #### Axioms
156
+
157
+ Axioms define semantic concepts independent of data shape. They specify **what** concepts your code works with, such as:
158
+
159
+ - Unique identifiers across different systems
160
+ - Type information and classification
161
+ - Versioning and change tracking
162
+ - Temporal data with conversion between representations
163
+ - Relationships between entities
164
+
165
+ See [docs/axioms.md](docs/axioms.md) for the complete type system architecture.
166
+
167
+ #### Canons
168
+
169
+ Canons provide shape-specific implementations of axioms. They specify **how** each semantic concept is represented in a particular data shape:
170
+
171
+ - **Declarative Style**: Local configurations for specific use cases
172
+ - **Module Style**: Shareable configurations across projects
173
+ - **Type Safety**: Full TypeScript support with compile-time checking
174
+ - **Multiple Shapes**: Each canon handles one data shape's implementation
175
+
176
+ See [docs/canons.md](docs/canons.md) for implementation patterns.
177
+
178
+ #### Universal APIs
179
+
180
+ Universal APIs provide functions that work across all registered canons. They are **the interface** your application code uses:
181
+
182
+ - Write code once that works with any registered canon
183
+ - Automatic adaptation to different data shapes
184
+ - Full type safety maintained across all shapes
185
+
186
+ See [reference/api.md](reference/api.md) for available APIs.
187
+
188
+ ## Package Exports
189
+
190
+ - **Main package**: `@relational-fabric/canon` - Core axioms and canons
191
+ - **TypeScript config**: `@relational-fabric/canon/tsconfig` - Base TypeScript configuration
192
+ - **ESLint config**: `@relational-fabric/canon/eslint` - ESLint configuration function
193
+
194
+ ## Key Patterns
195
+
196
+ ### Module Augmentation
197
+
198
+ Register axioms and canons using TypeScript module augmentation:
199
+
200
+ ```typescript
201
+ declare module '@relational-fabric/canon' {
202
+ interface Axioms {
203
+ MyAxiom: MyAxiomType
204
+ }
205
+ interface Canons {
206
+ MyCanon: MyCanonType
207
+ }
208
+ }
209
+ ```
210
+
211
+ ### Naming Conventions
212
+
213
+ - **Axiom Keys**: PascalCase, plural for general concepts (`Timestamps`, `References`), singular for specific concepts (`Id`, `Type`)
214
+ - **Function Names**: Use relational `*Of` pattern (`idOf()`, `typeOf()`) not imperative `get*` patterns
215
+ - **Type Names**: PascalCase for all type definitions
216
+ - **Variables**: camelCase for all variables and parameters
217
+ - **Distinguished Keys**: Use `$` prefix only for Canon's internal keys (`$basis`, `$meta`)
218
+
219
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for complete conventions.
220
+
221
+ ## Integration
222
+
223
+ Canon is designed to compose with existing TypeScript libraries and provides:
224
+
225
+ - **Type Safety**: Full TypeScript support with strict type checking
226
+ - **Flexibility**: Work with existing data structures or define new ones
227
+ - **Shape Agnostic**: Support for diverse data shapes through universal semantic interfaces
228
+ - **Extensibility**: Add your own axioms and canons as needed
229
+
230
+ ## Planning and Strategy
231
+
232
+ Canon maintains transparent planning and strategic direction:
233
+
234
+ - **Technology Radar**: [planning/radar/](planning/radar/) - Technology recommendations and assessments
235
+ - **Strategic Vision**: [planning/](planning/) - Long-term direction and positioning
236
+ - **Development Roadmap**: [planning/](planning/) - Detailed development phases and milestones
237
+ - **Architecture Decisions**: [docs/adrs/](docs/adrs/) - Documented ADRs for major decisions
238
+ - **Update Radar**: `npm run build:radar` - Convert YAML to CSV for visualization
239
+
240
+ ## Development
241
+
242
+ ### Prerequisites
243
+
244
+ - Node.js 22+
245
+ - npm or yarn
246
+
247
+ ### Setup
248
+
249
+ ```bash
250
+ git clone <repository-url>
251
+ cd canon
252
+ npm install
253
+ ```
254
+
255
+ ### Available Scripts
256
+
257
+ **Checks (Validation):**
258
+
259
+ - `npm run check:all` - Run all checks (lint, type check, and tests)
260
+ - `npm run checks` - Alias for check:all
261
+ - `npm run check:types` - Type check all code (src + examples)
262
+ - `npm run check:types:src` - Type check source code only
263
+ - `npm run check:types:examples` - Type check examples only
264
+ - `npm run check:lint` - Lint code
265
+ - `npm run check:lint:fix` - Fix ESLint issues automatically
266
+ - `npm run check:radar` - Validate radar configuration
267
+ - `npm test` - Run tests (npm standard)
268
+ - `npm run check:test` - Run tests (includes examples)
269
+
270
+ **Development:**
271
+
272
+ - `npm run dev` - Run TypeScript in watch mode
273
+
274
+ **Documentation:**
275
+
276
+ - `npm run build:docs` - Build documentation for production
277
+ - `npm run build:docs:restore` - Restore README.md files from build
278
+
279
+ **Note**: The documentation build process uses a GitHub-first approach. All files use `README.md` naming in the repository for GitHub compatibility. During build, files are temporarily renamed to `index.md` for VitePress routing, then automatically restored. Always edit `README.md` files directly.
280
+
281
+ **Architecture Decision Records:**
282
+
283
+ - `cd docs/adrs && npx adr new "Title"` - Create a new ADR
284
+ - `npm run build:adr` - Build all ADR artifacts (TOC + index)
285
+ - `npm run build:adr:toc` - Generate table of contents
286
+ - `npm run build:adr:index` - Generate ADR index in documentation
287
+
288
+ **Technology Radar:**
289
+
290
+ - `npm run build:radar` - Convert YAML radar data to CSV
291
+
292
+ **Examples:**
293
+
294
+ - `npm run build:docs:examples` - Generate documentation from examples
295
+
296
+ ## Documentation
297
+
298
+ Canon provides comprehensive documentation to help you understand and use the system:
299
+
300
+ ### Core Documentation
301
+
302
+ - **[Getting Started](docs/)** - Introduction and quick start guide
303
+ - **[Axioms](docs/axioms.md)** - Fundamental building blocks and type system
304
+ - **[Canons](docs/canons.md)** - Universal type blueprints and implementation patterns
305
+ - **[Contributing](CONTRIBUTING.md)** - Conventions, naming patterns, and development workflow
306
+
307
+ ### Reference Documentation
308
+
309
+ - **[API Reference](reference/api.md)** - Complete API documentation
310
+ - **[Core Axioms](reference/axioms.md)** - Detailed axiom specifications
311
+ - **[Canons Reference](reference/canons.md)** - Canon implementation guide
312
+ - **[Type Testing Utilities](docs/type-testing/)** - Compile-time type assertions and invariants
313
+ - **[Third-Party Integrations](reference/third-party.md)** - External library integrations
314
+
315
+ ### Examples
316
+
317
+ - **[Deduplicating Entities](docs/examples/deduplicating-entities.md)** - Using axioms for entity deduplication
318
+ - **[Tree Walk Over Mixed Entities](docs/examples/tree-walk-over-mixed-entities.md)** - Working with heterogeneous data structures
319
+ - **[User Authentication Tokens](docs/examples/user-authentication-tokens.md)** - Implementing authentication patterns
320
+ - **[More Examples](docs/examples/)** - Additional examples and use cases
321
+
322
+ ### Architecture
323
+
324
+ - **[ADRs](docs/adrs/)** - Architecture Decision Records
325
+ - **[Technology Radar](planning/radar/methodology.md)** - Technology assessment methodology
326
+
327
+ ## License
328
+
329
+ MIT
package/eslint.js ADDED
@@ -0,0 +1,31 @@
1
+ import process from 'node:process'
2
+ import antfu from '@antfu/eslint-config'
3
+ import { defu } from 'defu'
4
+
5
+ /**
6
+ * Create an ESLint configuration using antfu's config with optional custom overrides
7
+ * @param {object} [options] - Optional configuration to merge with the default antfu config
8
+ * @returns {object} ESLint configuration object
9
+ */
10
+ export default function createEslintConfig(options = {}, ...configs) {
11
+ const defaultConfig = {
12
+ typescript: true,
13
+ node: true,
14
+ stylistic: true, // Use ESLint Stylistic for formatting instead of Prettier
15
+ ignores: ['dist', 'node_modules'],
16
+ }
17
+
18
+ const mergedConfig = defu(options, defaultConfig)
19
+
20
+ return antfu(
21
+ mergedConfig,
22
+ {
23
+ rules: {
24
+ 'no-console': process.env.CI ? 'off' : 'warn',
25
+ 'node/prefer-global/process': 'off',
26
+ 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
27
+ },
28
+ },
29
+ ...configs,
30
+ )
31
+ }
package/package.json ADDED
@@ -0,0 +1,126 @@
1
+ {
2
+ "name": "@relational-fabric/canon",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "A modern TypeScript package template with ESLint and TypeScript configurations for starting new projects",
6
+ "author": "Relational Fabric",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/RelationalFabric/canon.git"
11
+ },
12
+ "keywords": [
13
+ "typescript",
14
+ "eslint",
15
+ "template",
16
+ "starter",
17
+ "configuration",
18
+ "package",
19
+ "node",
20
+ "lts"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./src/index.ts",
25
+ "import": "./src/index.ts"
26
+ },
27
+ "./radar": {
28
+ "types": "./src/radar/index.ts",
29
+ "import": "./src/radar/index.ts"
30
+ },
31
+ "./tsconfig": "./tsconfig.base.json",
32
+ "./eslint": "./eslint.js"
33
+ },
34
+ "main": "src/index.ts",
35
+ "types": "src/index.ts",
36
+ "bin": {
37
+ "adr": "./scripts/adr.js"
38
+ },
39
+ "files": [
40
+ "eslint.js",
41
+ "src",
42
+ "tsconfig.base.json"
43
+ ],
44
+ "engines": {
45
+ "node": ">=22.0.0"
46
+ },
47
+ "scripts": {
48
+ "build:adr": "npm-run-all build:adr:toc build:adr:index",
49
+ "build:adr:index": "node scripts/generate-adr-index.js",
50
+ "build:adr:toc": "cd docs/adrs && npx adr generate toc",
51
+ "build:docs": "npm run build:docs:examples && scripts/rename-readmes-for-build.sh && npx vitepress build && scripts/restore-readmes-from-build.sh",
52
+ "build:docs:examples": "tsx scripts/generate-examples-docs.ts",
53
+ "build:docs:restore": "scripts/restore-readmes-from-build.sh",
54
+ "build:radar": "tsx scripts/convert-radar.ts",
55
+ "check:all": "npm-run-all check:lint check:types check:test check:radar ",
56
+ "check:all:fix": "npm-run-all check:lint:fix check:types check:test",
57
+ "check:lint": "eslint .",
58
+ "check:lint:fix": "eslint . --fix",
59
+ "check:radar": "tsx scripts/validate-radar.ts",
60
+ "check:test": "vitest run",
61
+ "check:test:json": "vitest run --reporter=default --reporter=json --outputFile=.scratch/vitest-report.json",
62
+ "check:test:coverage": "vitest run --coverage",
63
+ "check:test:watch": "vitest run --watch",
64
+ "check:test:ui": "vitest run --ui",
65
+ "check:types": "npm run check:types:all",
66
+ "check:types:all": "npm-run-all check:types:src check:types:examples",
67
+ "check:types:examples": "tsc --noEmit --project examples/tsconfig.json",
68
+ "check:types:src": "tsc --noEmit",
69
+ "checks": "npm run check:all",
70
+ "dev": "tsx --watch src/index.ts",
71
+ "test": "npm run check:test",
72
+ "prepare": "husky",
73
+ "postinstall": "husky"
74
+ },
75
+ "peerDependencies": {
76
+ "eslint": "^9.0.0",
77
+ "typescript": "^5.0.0"
78
+ },
79
+ "peerDependenciesMeta": {
80
+ "eslint": {
81
+ "optional": false
82
+ },
83
+ "typescript": {
84
+ "optional": false
85
+ }
86
+ },
87
+ "dependencies": {
88
+ "@antfu/eslint-config": "^3.0.0",
89
+ "@tsconfig/node-lts": "^20.0.0",
90
+ "yaml": "^2.4.1"
91
+ },
92
+ "optionalDependencies": {
93
+ "defu": "^6.1.4"
94
+ },
95
+ "devDependencies": {
96
+ "@types/mdast": "^4.0.4",
97
+ "@types/node": "^24.7.1",
98
+ "@vitest/coverage-v8": "^3.2.4",
99
+ "adr-tools": "^2.0.4",
100
+ "chokidar-cli": "^3.0.0",
101
+ "dedent": "^1.7.0",
102
+ "depcheck": "^1.4.7",
103
+ "eslint": "^9.10.0",
104
+ "husky": "^9.1.7",
105
+ "lint-staged": "^16.2.6",
106
+ "npm-run-all": "^4.1.5",
107
+ "remark": "^15.0.1",
108
+ "remark-parse": "^11.0.0",
109
+ "remark-stringify": "^11.0.0",
110
+ "tsx": "^4.7.0",
111
+ "typescript": "^5.4.0",
112
+ "unified": "^11.0.5",
113
+ "vitepress": "^1.0.0",
114
+ "vitest": "^3.2.4"
115
+ },
116
+ "husky": {
117
+ "hooks": {
118
+ "pre-commit": "lint-staged"
119
+ }
120
+ },
121
+ "lint-staged": {
122
+ "*.{js,jsx,ts,tsx,json,css,md}": [
123
+ "npx eslint --fix"
124
+ ]
125
+ }
126
+ }
package/src/axiom.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Axiom API functions
3
+ */
4
+
5
+ import type { AxiomConfig, Axioms } from './types/index.js'
6
+ import { inferCanon } from './canon.js'
7
+
8
+ /**
9
+ * Infer axiom configuration for a value
10
+ *
11
+ * Finds the canon that matches the value, then returns the specified axiom's config.
12
+ *
13
+ * @param axiomLabel - The axiom label to look up
14
+ * @param value - The value to check against
15
+ * @returns The matching axiom config or undefined
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const idConfig = inferAxiom('Id', { id: 'test-123' })
20
+ * const typeConfig = inferAxiom('Type', { type: 'user' })
21
+ * ```
22
+ */
23
+ export function inferAxiom<Label extends keyof Axioms>(
24
+ axiomLabel: Label,
25
+ value: unknown,
26
+ ): AxiomConfig | undefined {
27
+ const canon = inferCanon(value)
28
+
29
+ if (!canon) {
30
+ return undefined
31
+ }
32
+
33
+ return canon.axioms[axiomLabel as string]
34
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Id axiom implementation
3
+ *
4
+ * Provides universal access to entity identifiers across different data formats.
5
+ */
6
+
7
+ import type { KeyNameAxiom, Satisfies } from '../types/index.js'
8
+ import { inferAxiom } from '../axiom.js'
9
+
10
+ /**
11
+ * Register Id axiom in global Axioms interface
12
+ */
13
+ declare module '@relational-fabric/canon' {
14
+ interface Axioms {
15
+ /**
16
+ * Id concept - might be 'id', '@id', '_id', etc.
17
+ */
18
+ Id: KeyNameAxiom
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Extract the ID value from any entity that satisfies the Id axiom
24
+ *
25
+ * @param x - The entity to extract ID from
26
+ * @returns The ID value as a string
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const data = { id: 'test-123', name: 'Test' }
31
+ * const id = idOf(data) // "test-123"
32
+ * ```
33
+ */
34
+ export function idOf<T extends Satisfies<'Id'>>(x: T): string {
35
+ const config = inferAxiom('Id', x)
36
+
37
+ if (!config) {
38
+ throw new Error('No matching canon found for Id axiom')
39
+ }
40
+
41
+ // For KeyNameAxiom, extract using the key field
42
+ if ('key' in config && typeof config.key === 'string') {
43
+ const value = (x as Record<string, unknown>)[config.key]
44
+
45
+ if (typeof value !== 'string') {
46
+ throw new TypeError(`Expected string ID, got ${typeof value}`)
47
+ }
48
+
49
+ return value
50
+ }
51
+
52
+ throw new Error('Invalid Id axiom configuration')
53
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * References axiom implementation
3
+ *
4
+ * Provides universal access to entity references with format conversion.
5
+ */
6
+
7
+ import type { EntityReference, RepresentationAxiom, Satisfies } from '../types/index.js'
8
+ import { inferAxiom } from '../axiom.js'
9
+
10
+ /**
11
+ * Register References axiom in global Axioms interface
12
+ */
13
+ declare module '@relational-fabric/canon' {
14
+ interface Axioms {
15
+ /**
16
+ * References concept - might be string, object, etc.
17
+ */
18
+ References: RepresentationAxiom<string | object, EntityReference<string, unknown>>
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Check if a value is a canonical reference (EntityReference object)
24
+ *
25
+ * @param value - The value to check
26
+ * @returns True if the value is an EntityReference object
27
+ */
28
+ export function isCanonicalReference(
29
+ value: string | object,
30
+ ): value is EntityReference<string, unknown> {
31
+ return (
32
+ typeof value === 'object'
33
+ && value !== null
34
+ && 'ref' in value
35
+ && 'resolved' in value
36
+ && typeof (value as EntityReference<string, unknown>).ref === 'string'
37
+ && typeof (value as EntityReference<string, unknown>).resolved === 'boolean'
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Extract and convert reference data to canonical EntityReference format
43
+ *
44
+ * @param x - The reference value to convert
45
+ * @returns The reference as an EntityReference object
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const stringRef = 'user-123'
50
+ * const entityRef = { ref: 'user-123', resolved: false }
51
+ *
52
+ * console.log(referencesOf(stringRef)) // Converted to EntityReference
53
+ * console.log(referencesOf(entityRef)) // Already canonical EntityReference
54
+ * ```
55
+ */
56
+ export function referencesOf<T extends Satisfies<'References'>>(
57
+ x: T,
58
+ ): EntityReference<string, unknown> {
59
+ const config = inferAxiom('References', x)
60
+
61
+ if (!config) {
62
+ throw new Error('No matching canon found for References axiom')
63
+ }
64
+
65
+ // Check if already canonical
66
+ if (isCanonicalReference(x)) {
67
+ return x
68
+ }
69
+
70
+ // Convert to canonical format
71
+ if (typeof x === 'string') {
72
+ return {
73
+ ref: x,
74
+ resolved: false,
75
+ }
76
+ }
77
+
78
+ if (typeof x === 'object' && x !== null) {
79
+ // Try to extract reference from object
80
+ const obj = x as Record<string, unknown>
81
+
82
+ // Look for common reference field names
83
+ const refFields = ['ref', 'id', 'reference', 'uri', 'url']
84
+ for (const field of refFields) {
85
+ if (field in obj && typeof obj[field] === 'string') {
86
+ return {
87
+ ref: obj[field] as string,
88
+ resolved: 'resolved' in obj ? Boolean(obj.resolved) : false,
89
+ value: 'value' in obj ? obj.value : undefined,
90
+ }
91
+ }
92
+ }
93
+
94
+ throw new TypeError(`Could not extract reference from object: ${JSON.stringify(x)}`)
95
+ }
96
+
97
+ throw new TypeError(`Expected string or object, got ${typeof x}`)
98
+ }