@owlmeans/client-planning 0.1.18-rc.9 → 0.1.18

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.
Files changed (146) hide show
  1. package/README.md +94 -11
  2. package/agent-meta/manifest.json +62 -4
  3. package/agent-meta/skills/client-planning/SKILL.md +60 -78
  4. package/agent-meta/skills/client-planning/parts/aux-mirrors.md +61 -0
  5. package/agent-meta/skills/client-planning/parts/mirror.md +67 -0
  6. package/agent-meta/skills/client-planning/parts/validation.md +60 -0
  7. package/build/client.d.ts +1 -1
  8. package/build/client.d.ts.map +1 -1
  9. package/build/client.js +66 -14
  10. package/build/client.js.map +1 -1
  11. package/build/commits.d.ts +2 -13
  12. package/build/commits.d.ts.map +1 -1
  13. package/build/commits.js +190 -132
  14. package/build/commits.js.map +1 -1
  15. package/build/consts.d.ts +4 -0
  16. package/build/consts.d.ts.map +1 -1
  17. package/build/consts.js +10 -4
  18. package/build/consts.js.map +1 -1
  19. package/build/definitions.d.ts +13 -0
  20. package/build/definitions.d.ts.map +1 -0
  21. package/build/definitions.js +70 -0
  22. package/build/definitions.js.map +1 -0
  23. package/build/events/types.d.ts +34 -0
  24. package/build/events/types.d.ts.map +1 -0
  25. package/build/events/types.js +2 -0
  26. package/build/events/types.js.map +1 -0
  27. package/build/events.d.ts +10 -21
  28. package/build/events.d.ts.map +1 -1
  29. package/build/events.js +104 -102
  30. package/build/events.js.map +1 -1
  31. package/build/facade.d.ts.map +1 -1
  32. package/build/facade.js +86 -49
  33. package/build/facade.js.map +1 -1
  34. package/build/feed.d.ts +4 -4
  35. package/build/feed.d.ts.map +1 -1
  36. package/build/feed.js +50 -27
  37. package/build/feed.js.map +1 -1
  38. package/build/helper/types.d.ts +15 -0
  39. package/build/helper/types.d.ts.map +1 -0
  40. package/build/helper/types.js +2 -0
  41. package/build/helper/types.js.map +1 -0
  42. package/build/helper.d.ts +7 -7
  43. package/build/helper.d.ts.map +1 -1
  44. package/build/helper.js +16 -8
  45. package/build/helper.js.map +1 -1
  46. package/build/index.d.ts +11 -0
  47. package/build/index.d.ts.map +1 -1
  48. package/build/index.js +6 -0
  49. package/build/index.js.map +1 -1
  50. package/build/lifecycle/types.d.ts +27 -0
  51. package/build/lifecycle/types.d.ts.map +1 -0
  52. package/build/lifecycle/types.js +2 -0
  53. package/build/lifecycle/types.js.map +1 -0
  54. package/build/lifecycle.d.ts +3 -0
  55. package/build/lifecycle.d.ts.map +1 -0
  56. package/build/lifecycle.js +157 -0
  57. package/build/lifecycle.js.map +1 -0
  58. package/build/resource-feed.d.ts +5 -0
  59. package/build/resource-feed.d.ts.map +1 -0
  60. package/build/resource-feed.js +97 -0
  61. package/build/resource-feed.js.map +1 -0
  62. package/build/resources.d.ts +6 -0
  63. package/build/resources.d.ts.map +1 -0
  64. package/build/resources.js +93 -0
  65. package/build/resources.js.map +1 -0
  66. package/build/stores.d.ts +4 -11
  67. package/build/stores.d.ts.map +1 -1
  68. package/build/stores.js +11 -35
  69. package/build/stores.js.map +1 -1
  70. package/build/sync/types.d.ts +18 -0
  71. package/build/sync/types.d.ts.map +1 -0
  72. package/build/sync/types.js +2 -0
  73. package/build/sync/types.js.map +1 -0
  74. package/build/sync.d.ts +4 -0
  75. package/build/sync.d.ts.map +1 -0
  76. package/build/sync.js +26 -0
  77. package/build/sync.js.map +1 -0
  78. package/build/types.d.ts +48 -3
  79. package/build/types.d.ts.map +1 -1
  80. package/build/types.local.d.ts +13 -0
  81. package/build/types.local.d.ts.map +1 -0
  82. package/build/types.local.js +2 -0
  83. package/build/types.local.js.map +1 -0
  84. package/build/utils/record/types.d.ts +28 -0
  85. package/build/utils/record/types.d.ts.map +1 -0
  86. package/build/utils/record/types.js +2 -0
  87. package/build/utils/record/types.js.map +1 -0
  88. package/build/utils/record.d.ts +3 -24
  89. package/build/utils/record.d.ts.map +1 -1
  90. package/build/utils/record.js +64 -70
  91. package/build/utils/record.js.map +1 -1
  92. package/build/validation/consts.d.ts +23 -0
  93. package/build/validation/consts.d.ts.map +1 -0
  94. package/build/validation/consts.js +289 -0
  95. package/build/validation/consts.js.map +1 -0
  96. package/build/validation/types.d.ts +17 -0
  97. package/build/validation/types.d.ts.map +1 -0
  98. package/build/validation/types.js +2 -0
  99. package/build/validation/types.js.map +1 -0
  100. package/build/validation.d.ts +10 -0
  101. package/build/validation.d.ts.map +1 -0
  102. package/build/validation.js +171 -0
  103. package/build/validation.js.map +1 -0
  104. package/package.json +20 -18
  105. package/src/client.ts +58 -15
  106. package/src/commits.ts +152 -184
  107. package/src/consts.ts +12 -4
  108. package/src/definitions.ts +93 -0
  109. package/src/events/types.ts +35 -0
  110. package/src/events.ts +118 -108
  111. package/src/facade.ts +87 -55
  112. package/src/feed.ts +47 -28
  113. package/src/helper/types.ts +15 -0
  114. package/src/helper.ts +27 -10
  115. package/src/index.ts +12 -0
  116. package/src/lifecycle/types.ts +28 -0
  117. package/src/lifecycle.ts +123 -0
  118. package/src/resource-feed.ts +79 -0
  119. package/src/resources.ts +84 -0
  120. package/src/stores.ts +11 -40
  121. package/src/sync/types.ts +18 -0
  122. package/src/sync.ts +40 -0
  123. package/src/types.local.ts +14 -0
  124. package/src/types.ts +53 -5
  125. package/src/utils/record/types.ts +27 -0
  126. package/src/utils/record.ts +68 -71
  127. package/src/validation/consts.ts +289 -0
  128. package/src/validation/types.ts +18 -0
  129. package/src/validation.ts +140 -0
  130. package/tests/app.spec.ts +72 -0
  131. package/tests/context.ts +46 -15
  132. package/tests/csp.spec.ts +119 -0
  133. package/tests/definitions.spec.ts +74 -0
  134. package/tests/draft7.spec.ts +13 -0
  135. package/tests/events.spec.ts +13 -9
  136. package/tests/facade.spec.ts +2 -1
  137. package/tests/lifecycle/context.ts +29 -0
  138. package/tests/lifecycle-close.spec.ts +103 -0
  139. package/tests/lifecycle-commits.spec.ts +92 -0
  140. package/tests/lifecycle-definitions.spec.ts +28 -0
  141. package/tests/lifecycle-feeds.spec.ts +92 -0
  142. package/tests/lifecycle-sockets.spec.ts +76 -0
  143. package/tests/replies.spec.ts +81 -0
  144. package/tests/resources.spec.ts +116 -0
  145. package/tests/stores.spec.ts +13 -11
  146. package/tests/validation.spec.ts +127 -0
@@ -0,0 +1,127 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { AnySchema } from 'ajv'
3
+ import { makeAjv, SchemaInvalid } from '@owlmeans/planning'
4
+ import { makePlanningFieldValidator } from '../src/index.js'
5
+
6
+ describe('@owlmeans/client-planning — CSP field validation', () => {
7
+ test('the exact nested fields validate without modifying, coercing, flattening or defaulting them', () => {
8
+ const schema = { type: 'object', properties: {
9
+ estimatedValue: { type: 'object', properties: { amount: { type: 'number', minimum: 0 }, currency: { type: 'string', enum: ['USD', 'EUR'] } },
10
+ required: ['amount', 'currency'], additionalProperties: false },
11
+ nextContactDate: { type: 'string', format: 'date' }, priority: { type: 'string', enum: ['low', 'normal', 'high'] },
12
+ }, required: ['estimatedValue', 'nextContactDate'], additionalProperties: false }
13
+ const copy = structuredClone(schema)
14
+ const validator = makePlanningFieldValidator(schema)
15
+ const valid = { estimatedValue: { amount: 12345, currency: 'USD' }, nextContactDate: '2027-03-15', priority: 'normal' }
16
+ const payload = structuredClone(valid)
17
+ expect(validator.validateFields(valid)).toEqual({ valid: true, errors: [] })
18
+ expect(validator.validateFields({ estimatedValue: valid.estimatedValue, nextContactDate: valid.nextContactDate }).valid).toBe(true)
19
+ const invalid = [ {}, { ...valid, estimatedValue: 12345 }, { ...valid, estimatedValue: { amount: 2 } },
20
+ { ...valid, estimatedValue: { amount: '12345', currency: 'USD' } }, { ...valid, estimatedValue: { amount: -1, currency: 'USD' } },
21
+ { ...valid, priority: 'urgent' }, { ...valid, nextContactDate: '2027-02-30' },
22
+ { ...valid, nextContactDate: '2027-03-15T00:00:00Z' }, { ...valid, extra: true } ]
23
+ const native = makeAjv().compile(schema)
24
+ for (const fields of invalid) {
25
+ const checked = validator.validateFields(fields)
26
+ expect(checked.valid).toBe(native(fields) === true)
27
+ expect(checked.valid).toBe(false)
28
+ expect(checked.errors.length).toBeGreaterThan(0)
29
+ expect(checked.errors.every(error => error.message.length < 100)).toBe(true)
30
+ }
31
+ expect(validator.validateFields({ ...valid, estimatedValue: { amount: 2 } }).errors).toContainEqual({
32
+ path: 'estimatedValue.currency', keyword: 'required', schemaPath: '#/properties/estimatedValue/required', message: 'A required field is missing',
33
+ })
34
+ expect(schema).toEqual(copy)
35
+ expect(valid).toEqual(payload)
36
+ })
37
+
38
+ test('supported Draft 7 predicates agree with native planning AJV, including references, compositions and full formats', () => {
39
+ const cases: Array<{ schema: AnySchema, values: unknown[] }> = [
40
+ { schema: { type: ['number', 'null'], minimum: 0, maximum: 7, exclusiveMinimum: -1, exclusiveMaximum: 8 }, values: [null, 0, 7, -1, 8, '3'] },
41
+ { schema: { type: 'integer', minimum: -3, maximum: 3 }, values: [-3, 0, 3, 1.99999999, 6.00000001, 4] },
42
+ { schema: { type: 'string', minLength: 2, maxLength: 3, pattern: '^.+' }, values: ['ab', 'a😀', '😀', 'abcd', '', 2] },
43
+ { schema: { type: 'array', items: [{ type: 'string' }, { type: 'integer' }], additionalItems: false }, values: [[], ['a'], ['a', 2], ['a', '2'], ['a', 2, 3]] },
44
+ { schema: { type: 'array', items: { type: 'number' }, minItems: 1, maxItems: 3, uniqueItems: true, contains: { const: 2 } }, values: [[1, 2], [2], [], [1], [2, 2], [1, 2, 3, 4], ['2']] },
45
+ { schema: { type: 'object', patternProperties: { '^a': { type: 'number' } }, additionalProperties: false, minProperties: 1, maxProperties: 2 },
46
+ values: [{ abc: 1 }, { abc: '1' }, {}, { abc: 1, other: 2 }, { a: 1, ab: 2, abc: 3 }] },
47
+ { schema: { type: 'object', propertyNames: { pattern: '^[a-z]+$' }, dependencies: { a: ['b'], c: { required: ['d'] } } },
48
+ values: [{}, { a: 1, b: 2 }, { a: 1 }, { c: 1 }, { c: 1, d: 2 }, { A: 1 }] },
49
+ { schema: { allOf: [{ type: 'number' }, { minimum: 2 }], not: { const: 3 } }, values: [2, 4, 1, 3, '2'] },
50
+ { schema: { anyOf: [{ type: 'string', pattern: '^a' }, { type: 'integer' }] }, values: ['abc', 2, 'xyz', 1.5, null] },
51
+ { schema: { oneOf: [{ type: 'number', minimum: 1 }, { type: 'number', maximum: 3 }] }, values: [0, 4, 2, '2'] },
52
+ { schema: { if: { type: 'string' }, then: { minLength: 2 }, else: { type: 'integer' } }, values: ['ab', 2, 'a', 1.5, null] },
53
+ { schema: { $defs: { n: { type: 'number' } }, $ref: '#/$defs/n', minimum: 5 }, values: [7, 3, '7'] },
54
+ { schema: { $id: 'https://planning.test/fields', definitions: { n: { $id: 'number', type: 'number' } }, properties: { a: { $ref: 'number' } } },
55
+ values: [{ a: 2 }, { a: '2' }, {}] },
56
+ { schema: { definitions: { blocked: false }, properties: { a: { $ref: '#/definitions/blocked' } } }, values: [{}, { a: 1 }] },
57
+ { schema: { type: 'object', properties: { value: { type: 'number' }, next: { $ref: '#' } }, required: ['value'] },
58
+ values: [{ value: 1 }, { value: 1, next: { value: 2 } }, { value: 1, next: {} }] },
59
+ { schema: { type: 'object', properties: { a: { type: 'number', default: 7 } }, required: ['a'] }, values: [{}, { a: 3 }, { a: '3' }] },
60
+ { schema: { type: 'object', properties: { a: { type: 'string', nullable: true, minLength: 2, format: 'email' } } },
61
+ values: [{}, { a: null }, { a: 'a@example.test' }, { a: 'a' }, { a: 3 }] },
62
+ { schema: { type: 'string', nullable: true, enum: ['a'] }, values: ['a', null, 'b'] },
63
+ { schema: { type: 'string', nullable: true, const: 'a' }, values: ['a', null, 'b'] },
64
+ { schema: { type: ['string', 'null'], nullable: true }, values: ['a', null, 3] },
65
+ { schema: { type: 'string', nullable: false }, values: ['a', null, 3] },
66
+ { schema: true, values: [1, 'a', null] }, { schema: false, values: [1, 'a', null] },
67
+ ]
68
+ const formats = [
69
+ ['date', ['2024-02-29', '2027-03-15', '2027-02-29', '2027-04-31', '2027-03-15T00:00:00Z']],
70
+ ['time', ['12:00:00Z', '23:59:60Z', '12:00:00', '12:00:00+99:00', '25:00:00Z']],
71
+ ['date-time', ['2027-03-15T12:00:00Z', '2027-03-15 12:00:00Z', '2027-02-30T12:00:00Z', '2027-03-15T12:00:00+99:00']],
72
+ ['email', ['a@example.test', 'a@localhost', '.a@example.test', 'a..b@example.test']],
73
+ ['ipv4', ['127.0.0.1', '01.2.3.4', '256.0.0.1']], ['uuid', ['123e4567-e89b-12d3-a456-426614174000', 'bad']],
74
+ ['uri', ['https://planning.test/a', 'urn:planning:test', '/relative']],
75
+ ['iso-time', ['12:00:00', '12:00:00Z', '25:00:00']], ['duration', ['P1D', 'P1W', 'P', 'P1.5D']],
76
+ ] as const
77
+ for (const [name, values] of formats) cases.push({ schema: { type: 'string', format: name }, values: [...values] })
78
+ for (const { schema, values } of cases) {
79
+ const native = makeAjv().compile(schema)
80
+ const client = makePlanningFieldValidator(schema)
81
+ for (const value of values) {
82
+ // Untyped JSON can reach a validator with the wrong root kind; it must still refuse it.
83
+ expect(client.validateFields(value as Record<string, unknown>).valid).toBe(native(value) === true)
84
+ }
85
+ }
86
+ })
87
+
88
+ test('malformed, unresolved and unsupported schemas fail closed at preparation, including dormant branches', () => {
89
+ const schemas: AnySchema[] = [
90
+ { properties: { optional: { $ref: 'https://missing.test/private?credential=never-print' } } },
91
+ { if: false, then: { $ref: '#/definitions/missing' } }, { $ref: '#' },
92
+ { $schema: 'https://json-schema.org/draft/2020-12/schema' }, { type: 'number', multipleOf: 0.1 },
93
+ { type: 'integer', multipleOf: 2 }, { type: 'integer', multipleOf: 1 },
94
+ { nullable: true }, { type: 'string', nullable: 'true' }, { type: ['string', 'null'], nullable: false },
95
+ { type: 'string', format: 'made-up' }, { type: 'number', format: 'int32' },
96
+ { type: 'string', format: 'date', formatMinimum: '2027-01-01' }, { unevaluatedProperties: false },
97
+ { required: 'field' }, { type: 'object', properties: { a: { minLength: -1 } } },
98
+ { $defs: { invalid: { type: 'invented' } } }, { $defs: null }, { $defs: [] }, { pattern: '[' }, { enum: [] },
99
+ ]
100
+ for (const schema of schemas) expect(() => makePlanningFieldValidator(schema)).toThrow(SchemaInvalid)
101
+ try { makePlanningFieldValidator(schemas[0]!) } catch (error) {
102
+ expect(String(error)).toContain('missing-reference:#/properties/optional/$ref')
103
+ expect(String(error)).not.toContain('credential')
104
+ expect(String(error)).not.toContain('https://missing')
105
+ }
106
+ const schema = { type: 'object', properties: { a: { type: 'number' } }, 'x-nativekit': { source: 'preserved' } }
107
+ expect(makePlanningFieldValidator(schema).validateFields({ a: 3 }).valid).toBe(true)
108
+ expect(schema['x-nativekit']).toEqual({ source: 'preserved' })
109
+ })
110
+
111
+ test('unsafe keys and non-JSON values refuse with bounded field errors, while required names decode correctly', () => {
112
+ const validate = makePlanningFieldValidator({ type: 'object', properties: { nested: { type: 'object', required: ['a/b~c'] } } }).validateFields
113
+ expect(validate({ nested: {} }).errors[0]?.path).toBe('nested.a/b~c')
114
+ for (const value of [NaN, Infinity, undefined, 2n, new Date(), () => 3]) expect(validate({ nested: { value } }).valid).toBe(false)
115
+ expect(validate({ nested: { 'unsafe.key': 3 } }).errors.some(error => error.keyword === 'field-key')).toBe(true)
116
+ const cycle: Record<string, unknown> = {}; cycle.self = cycle
117
+ expect(validate(cycle).valid).toBe(false)
118
+ const longKey = 'field'.repeat(200)
119
+ const unsafe = validate({ [longKey]: undefined, [`${longKey}.nested`]: 3 })
120
+ expect(unsafe.valid).toBe(false)
121
+ expect(unsafe.errors).toHaveLength(2)
122
+ expect(unsafe.errors.every(error => error.path.length <= 512)).toBe(true)
123
+ const errors = makePlanningFieldValidator({ type: 'object', required: Array.from({ length: 100 }, (_, index) => `field${index}`) }).validateFields({})
124
+ expect(errors.valid).toBe(false)
125
+ expect(errors.errors).toHaveLength(64)
126
+ })
127
+ })