@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
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
|
+
}
|
package/src/axioms/id.ts
ADDED
|
@@ -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
|
+
}
|