@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,106 @@
1
+ /**
2
+ * Technology Radar data structures and types
3
+ */
4
+
5
+ import { type Expect, invariant } from '../testing.js'
6
+
7
+ export interface RadarEntry {
8
+ /** The name of the technology or practice */
9
+ name: string
10
+ /** Description of the technology or practice */
11
+ description: string
12
+ /** Whether this is a new entry */
13
+ isNew: boolean
14
+ /** Optional justification for the placement */
15
+ justification?: string
16
+ }
17
+
18
+ export interface Quadrant {
19
+ /** Unique identifier for the quadrant */
20
+ id: string
21
+ /** Display name for the quadrant */
22
+ name: string
23
+ /** Description of what this quadrant represents */
24
+ description: string
25
+ }
26
+
27
+ export interface Ring {
28
+ /** Unique identifier for the ring */
29
+ id: string
30
+ /** Display name for the ring */
31
+ name: string
32
+ /** Description of what this ring represents */
33
+ description: string
34
+ /** Color code for the ring (hex format) */
35
+ color: string
36
+ }
37
+
38
+ export interface RadarMetadata {
39
+ /** Title of the radar */
40
+ title: string
41
+ /** Subtitle of the radar */
42
+ subtitle: string
43
+ /** Version of the radar data */
44
+ version: string
45
+ /** Last updated date */
46
+ lastUpdated: string
47
+ /** Optional description */
48
+ description?: string
49
+ }
50
+
51
+ export interface RadarData {
52
+ /** Metadata about the radar */
53
+ metadata: RadarMetadata
54
+ /** Quadrant definitions */
55
+ quadrants: Quadrant[]
56
+ /** Ring definitions */
57
+ rings: Ring[]
58
+ /** Radar entries organized by quadrant and ring */
59
+ entries: Record<string, Record<string, RadarEntry[]>>
60
+ }
61
+
62
+ export interface RadarConfig {
63
+ /** Configuration for the radar */
64
+ metadata: RadarMetadata
65
+ /** Quadrant configuration */
66
+ quadrants: Quadrant[]
67
+ /** Ring configuration */
68
+ rings: Ring[]
69
+ /** Build configuration */
70
+ buildConfig: {
71
+ csvOutput: string
72
+ yamlInput: string
73
+ includeMetadata: boolean
74
+ sortByRing: boolean
75
+ }
76
+ }
77
+
78
+ export interface CsvRow {
79
+ name: string
80
+ ring: string
81
+ quadrant: string
82
+ isNew: boolean
83
+ description: string
84
+ }
85
+
86
+ export type QuadrantKey =
87
+ | 'tools-libraries'
88
+ | 'techniques-patterns'
89
+ | 'features-capabilities'
90
+ | 'data-structures-formats'
91
+ export type RingKey = 'adopt' | 'trial' | 'assess' | 'hold'
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Compile-time invariants
95
+ // ---------------------------------------------------------------------------
96
+
97
+ void invariant<Expect<RadarEntry['isNew'], boolean>>()
98
+ void invariant<Expect<Quadrant['id'], string>>()
99
+ void invariant<Expect<Ring['color'], string>>()
100
+ void invariant<
101
+ Expect<
102
+ QuadrantKey,
103
+ 'tools-libraries' | 'techniques-patterns' | 'features-capabilities' | 'data-structures-formats'
104
+ >
105
+ >()
106
+ void invariant<Expect<RingKey, 'adopt' | 'trial' | 'assess' | 'hold'>>()
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Tests for type guard utilities
3
+ */
4
+
5
+ import { describe, expect, it } from 'vitest'
6
+ import { typeGuard } from './guards.js'
7
+
8
+ describe('typeGuard', () => {
9
+ it('should convert predicate to TypeGuard', () => {
10
+ const isString = typeGuard<string>((v: unknown) => typeof v === 'string')
11
+
12
+ expect(isString('hello')).toBe(true)
13
+ expect(isString(123)).toBe(false)
14
+ expect(isString(null)).toBe(false)
15
+ })
16
+
17
+ it('should work with complex predicates', () => {
18
+ const hasId = typeGuard<{ id: string }>(
19
+ (v: unknown) =>
20
+ typeof v === 'object' && v !== null && 'id' in v && typeof (v as any).id === 'string',
21
+ )
22
+
23
+ expect(hasId({ id: '123' })).toBe(true)
24
+ expect(hasId({ name: 'test' })).toBe(false)
25
+ expect(hasId(null)).toBe(false)
26
+ })
27
+ })
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Type guard utility functions
3
+ */
4
+
5
+ import type { Predicate, TypeGuard } from '../types/index.js'
6
+
7
+ /**
8
+ * Convert a predicate function to a proper TypeGuard
9
+ *
10
+ * Accepts predicates (boolean-returning functions) and converts them to
11
+ * TypeGuards with proper overload signatures that discriminate types correctly.
12
+ *
13
+ * @param predicate - A boolean-returning predicate function
14
+ * @returns A TypeGuard with proper type discrimination
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * // Convert a simple predicate to a type guard
19
+ * const isString = typeGuard<string>(v => typeof v === 'string')
20
+ *
21
+ * // Use with object checks
22
+ * const hasId = typeGuard<{ id: string }>(v =>
23
+ * isPojo(v) && 'id' in v && typeof v.id === 'string'
24
+ * )
25
+ * ```
26
+ */
27
+ export function typeGuard<T>(predicate: Predicate<T>): TypeGuard<T> {
28
+ return predicate as TypeGuard<T>
29
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Tests for object utilities
3
+ */
4
+
5
+ import { describe, expect, it } from 'vitest'
6
+ import {
7
+ isPojo,
8
+ objectEntries,
9
+ objectKeys,
10
+ objectValues,
11
+ pojoHas,
12
+ pojoHasOfType,
13
+ pojoWithOfType,
14
+ } from './objects.js'
15
+
16
+ describe('isPojo', () => {
17
+ it('should return true for plain objects', () => {
18
+ expect(isPojo({})).toBe(true)
19
+ expect(isPojo({ id: '123' })).toBe(true)
20
+ expect(isPojo({ nested: { value: 1 } })).toBe(true)
21
+ })
22
+
23
+ it('should return false for arrays', () => {
24
+ expect(isPojo([])).toBe(false)
25
+ expect(isPojo([1, 2, 3])).toBe(false)
26
+ })
27
+
28
+ it('should return false for null', () => {
29
+ expect(isPojo(null)).toBe(false)
30
+ })
31
+
32
+ it('should return false for primitives', () => {
33
+ expect(isPojo('string')).toBe(false)
34
+ expect(isPojo(123)).toBe(false)
35
+ expect(isPojo(true)).toBe(false)
36
+ expect(isPojo(undefined)).toBe(false)
37
+ })
38
+
39
+ it('should return false for class instances', () => {
40
+ class MyClass {}
41
+ expect(isPojo(new MyClass())).toBe(false)
42
+ expect(isPojo(new Date())).toBe(false)
43
+ expect(isPojo(new Map())).toBe(false)
44
+ })
45
+
46
+ it('should return false for objects with custom prototypes', () => {
47
+ const customProto = Object.create({ custom: true })
48
+ expect(isPojo(customProto)).toBe(false)
49
+ })
50
+ })
51
+
52
+ describe('pojoHas', () => {
53
+ it('should return true when property exists', () => {
54
+ const obj = { id: '123', name: 'Test' }
55
+ expect(pojoHas(obj, 'id')).toBe(true)
56
+ expect(pojoHas(obj, 'name')).toBe(true)
57
+ })
58
+
59
+ it('should return false when property does not exist', () => {
60
+ const obj = { id: '123' }
61
+ expect(pojoHas(obj, 'missing')).toBe(false)
62
+ })
63
+ })
64
+
65
+ describe('pojoHasOfType', () => {
66
+ it('should return true for object with string field', () => {
67
+ expect(pojoHasOfType({ id: '123' }, 'id', 'string')).toBe(true)
68
+ expect(pojoHasOfType({ '@id': 'uri' }, '@id', 'string')).toBe(true)
69
+ })
70
+
71
+ it('should return false for non-string field when checking for string', () => {
72
+ expect(pojoHasOfType({ id: 123 }, 'id', 'string')).toBe(false)
73
+ expect(pojoHasOfType({ id: null }, 'id', 'string')).toBe(false)
74
+ })
75
+
76
+ it('should return false for missing field', () => {
77
+ expect(pojoHasOfType({ name: 'test' }, 'id', 'string')).toBe(false)
78
+ })
79
+
80
+ it('should return false for non-objects', () => {
81
+ expect(pojoHasOfType('string', 'id', 'string')).toBe(false)
82
+ expect(pojoHasOfType(null, 'id', 'string')).toBe(false)
83
+ })
84
+
85
+ it('should work with number type', () => {
86
+ expect(pojoHasOfType({ count: 123 }, 'count', 'number')).toBe(true)
87
+ expect(pojoHasOfType({ count: '123' }, 'count', 'number')).toBe(false)
88
+ })
89
+
90
+ it('should work with boolean type', () => {
91
+ expect(pojoHasOfType({ active: true }, 'active', 'boolean')).toBe(true)
92
+ expect(pojoHasOfType({ active: 'true' }, 'active', 'boolean')).toBe(false)
93
+ })
94
+
95
+ it('should work with object type', () => {
96
+ expect(pojoHasOfType({ meta: { nested: true } }, 'meta', 'object')).toBe(true)
97
+ expect(pojoHasOfType({ meta: 'string' }, 'meta', 'object')).toBe(false)
98
+ })
99
+ })
100
+
101
+ describe('pojoWithOfType', () => {
102
+ it('should create reusable type guard for string fields', () => {
103
+ const hasStringId = pojoWithOfType('id', 'string')
104
+ expect(hasStringId({ id: '123' })).toBe(true)
105
+ expect(hasStringId({ id: 123 })).toBe(false)
106
+ expect(hasStringId({ name: 'test' })).toBe(false)
107
+ })
108
+
109
+ it('should create reusable type guard for number fields', () => {
110
+ const hasNumberCount = pojoWithOfType('count', 'number')
111
+ expect(hasNumberCount({ count: 42 })).toBe(true)
112
+ expect(hasNumberCount({ count: '42' })).toBe(false)
113
+ })
114
+ })
115
+
116
+ describe('objectKeys', () => {
117
+ it('should return typed keys', () => {
118
+ const obj = { a: 1, b: 2, c: 3 }
119
+ const keys = objectKeys(obj)
120
+ expect(keys).toEqual(['a', 'b', 'c'])
121
+ })
122
+ })
123
+
124
+ describe('objectValues', () => {
125
+ it('should return values', () => {
126
+ const obj = { a: 1, b: 2, c: 3 }
127
+ const values = objectValues(obj)
128
+ expect(values).toEqual([1, 2, 3])
129
+ })
130
+ })
131
+
132
+ describe('objectEntries', () => {
133
+ it('should return entries', () => {
134
+ const obj = { a: 1, b: 2 }
135
+ const entries = objectEntries(obj)
136
+ expect(entries).toEqual([
137
+ ['a', 1],
138
+ ['b', 2],
139
+ ])
140
+ })
141
+ })
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Object utilities for Canon
3
+ *
4
+ * Helpers for working with plain JavaScript objects.
5
+ */
6
+
7
+ import type { JsType, JsTypeName, Pojo, PojoWith, TypeGuard } from '../types/index.js'
8
+ import { typeGuard } from './guards.js'
9
+
10
+ /**
11
+ * Re-export types for convenience
12
+ */
13
+ export type { Pojo, PojoWith }
14
+
15
+ /**
16
+ * Type guard to check if a value is a plain JavaScript object
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * isPojo({ id: '123' }) // true
21
+ * isPojo([1, 2, 3]) // false
22
+ * isPojo(null) // false
23
+ * isPojo('string') // false
24
+ * ```
25
+ */
26
+ export const isPojo = typeGuard<Pojo>(
27
+ (value: unknown) =>
28
+ typeof value === 'object'
29
+ && value !== null
30
+ && !Array.isArray(value)
31
+ && Object.getPrototypeOf(value) === Object.prototype,
32
+ )
33
+
34
+ /**
35
+ * Create a type guard for Pojo with a specific property
36
+ *
37
+ * Higher-order function that returns a reusable TypeGuard.
38
+ *
39
+ * @param key - The property name to check for
40
+ * @returns TypeGuard that checks if value is a Pojo with that property
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * const hasId = pojoWith('id')
45
+ * hasId({ id: '123' }) // true
46
+ * hasId({ name: 'test' }) // false
47
+ * ```
48
+ */
49
+ export function pojoWith<K extends string>(key: K): TypeGuard<PojoWith<Pojo, K, unknown>> {
50
+ return typeGuard((value: unknown) => isPojo(value) && key in value)
51
+ }
52
+
53
+ /**
54
+ * Convenience function to check if a Pojo has a specific property
55
+ *
56
+ * Uses pojoWith() internally.
57
+ *
58
+ * @param obj - The object to check
59
+ * @param key - The property name to look for
60
+ * @returns True if the object has the property
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const data = { id: '123', name: 'Test' }
65
+ * pojoHas(data, 'id') // true
66
+ * pojoHas(data, 'missing') // false
67
+ * ```
68
+ */
69
+ export function pojoHas<T extends Pojo, K extends string>(
70
+ obj: T | unknown,
71
+ key: K,
72
+ ): obj is PojoWith<T, K, unknown> {
73
+ return pojoWith(key)(obj)
74
+ }
75
+
76
+ export function pojoWithOfType<K extends string, V extends JsTypeName>(
77
+ key: K,
78
+ type: V,
79
+ ): TypeGuard<PojoWith<Pojo, K, JsType[V]>> {
80
+ return typeGuard((value: unknown) => {
81
+ if (!pojoHas(value, key)) {
82
+ return false
83
+ }
84
+ const valueType: JsTypeName = typeof value[key] // Needs to be seperate because we're not using a string literal
85
+ return valueType === type
86
+ })
87
+ }
88
+
89
+ /**
90
+ * Convenience function to check if a value has a specific string field
91
+ *
92
+ * @param value - The value to check
93
+ * @param key - The field name that should contain a string
94
+ * @returns True if value is a Pojo with the specified string field
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * pojoHasString({ id: '123' }, 'id') // true
99
+ * pojoHasString({ id: 123 }, 'id') // false
100
+ * pojoHasString('not object', 'id') // false
101
+ * ```
102
+ */
103
+ export function pojoHasOfType<T extends Pojo, K extends string, V extends JsTypeName>(
104
+ value: T | unknown,
105
+ key: K,
106
+ type: V,
107
+ ): value is PojoWith<T, K, JsType[V]> {
108
+ return pojoWithOfType(key, type)(value)
109
+ }
110
+
111
+ /**
112
+ * Type-safe Object.keys that handles both objects and arrays correctly
113
+ *
114
+ * For arrays, returns numeric indices. For objects, returns property keys.
115
+ *
116
+ * @param obj - The object to get keys from
117
+ * @returns Array of object keys
118
+ *
119
+ * @example
120
+ * ```typescript
121
+ * objectKeys({ a: 1, b: 2 }) // ['a', 'b']
122
+ * objectKeys([1, 2, 3]) // [0, 1, 2]
123
+ * ```
124
+ */
125
+ export function objectKeys<T extends object>(obj: T): Array<keyof T> {
126
+ if (Array.isArray(obj)) {
127
+ return Array.from(obj.keys()) as Array<keyof T>
128
+ }
129
+ return Object.keys(obj) as Array<keyof T>
130
+ }
131
+
132
+ /**
133
+ * Type-safe Object.values that handles both objects and arrays correctly
134
+ *
135
+ * For arrays, returns array values. For objects, returns property values.
136
+ *
137
+ * @param obj - The object to get values from
138
+ * @returns Array of object values
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * objectValues({ a: 1, b: 2 }) // [1, 2]
143
+ * objectValues([1, 2, 3]) // [1, 2, 3]
144
+ * ```
145
+ */
146
+ export function objectValues<T extends object>(obj: T): unknown[] {
147
+ if (Array.isArray(obj)) {
148
+ return Array.from(obj)
149
+ }
150
+ return Object.values(obj)
151
+ }
152
+
153
+ /**
154
+ * Type-safe Object.entries that handles both objects and arrays correctly
155
+ *
156
+ * For arrays, returns [index, value] pairs. For objects, returns [key, value] pairs.
157
+ *
158
+ * @param obj - The object to get entries from
159
+ * @returns Array of [key, value] tuples
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * objectEntries({ a: 1, b: 2 }) // [['a', 1], ['b', 2]]
164
+ * objectEntries([1, 2, 3]) // [[0, 1], [1, 2], [2, 3]]
165
+ * ```
166
+ */
167
+ export function objectEntries<T extends object>(obj: T): Array<[keyof T, T[keyof T]]> {
168
+ if (Array.isArray(obj)) {
169
+ return Array.from(obj.entries()) as Array<[keyof T, T[keyof T]]>
170
+ }
171
+ return Object.entries(obj) as Array<[keyof T, T[keyof T]]>
172
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "@tsconfig/node-lts/tsconfig.json",
3
+ "compilerOptions": {
4
+ "resolveJsonModule": true,
5
+ "strict": true,
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "skipLibCheck": true
10
+ }
11
+ }