@czap/core 0.1.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 +19 -0
- package/dist/addressed-digest.d.ts +15 -0
- package/dist/addressed-digest.d.ts.map +1 -0
- package/dist/addressed-digest.js +35 -0
- package/dist/addressed-digest.js.map +1 -0
- package/dist/animation.d.ts +46 -0
- package/dist/animation.d.ts.map +1 -0
- package/dist/animation.js +70 -0
- package/dist/animation.js.map +1 -0
- package/dist/assembly.d.ts +25 -0
- package/dist/assembly.d.ts.map +1 -0
- package/dist/assembly.js +58 -0
- package/dist/assembly.js.map +1 -0
- package/dist/av-bridge.d.ts +74 -0
- package/dist/av-bridge.d.ts.map +1 -0
- package/dist/av-bridge.js +107 -0
- package/dist/av-bridge.js.map +1 -0
- package/dist/av-renderer.d.ts +56 -0
- package/dist/av-renderer.d.ts.map +1 -0
- package/dist/av-renderer.js +65 -0
- package/dist/av-renderer.js.map +1 -0
- package/dist/blend.d.ts +61 -0
- package/dist/blend.d.ts.map +1 -0
- package/dist/blend.js +100 -0
- package/dist/blend.js.map +1 -0
- package/dist/boundary.d.ts +154 -0
- package/dist/boundary.d.ts.map +1 -0
- package/dist/boundary.js +269 -0
- package/dist/boundary.js.map +1 -0
- package/dist/brands.d.ts +63 -0
- package/dist/brands.d.ts.map +1 -0
- package/dist/brands.js +31 -0
- package/dist/brands.js.map +1 -0
- package/dist/caps.d.ts +49 -0
- package/dist/caps.d.ts.map +1 -0
- package/dist/caps.js +73 -0
- package/dist/caps.js.map +1 -0
- package/dist/capsule.d.ts +77 -0
- package/dist/capsule.d.ts.map +1 -0
- package/dist/capsule.js +18 -0
- package/dist/capsule.js.map +1 -0
- package/dist/capsules/boundary-evaluate.d.ts +28 -0
- package/dist/capsules/boundary-evaluate.d.ts.map +1 -0
- package/dist/capsules/boundary-evaluate.js +117 -0
- package/dist/capsules/boundary-evaluate.js.map +1 -0
- package/dist/capsules/canonical-cbor.d.ts +13 -0
- package/dist/capsules/canonical-cbor.d.ts.map +1 -0
- package/dist/capsules/canonical-cbor.js +60 -0
- package/dist/capsules/canonical-cbor.js.map +1 -0
- package/dist/capsules/token-buffer.d.ts +24 -0
- package/dist/capsules/token-buffer.d.ts.map +1 -0
- package/dist/capsules/token-buffer.js +53 -0
- package/dist/capsules/token-buffer.js.map +1 -0
- package/dist/capture.d.ts +40 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +10 -0
- package/dist/capture.js.map +1 -0
- package/dist/cbor.d.ts +33 -0
- package/dist/cbor.d.ts.map +1 -0
- package/dist/cbor.js +179 -0
- package/dist/cbor.js.map +1 -0
- package/dist/cell.d.ts +53 -0
- package/dist/cell.d.ts.map +1 -0
- package/dist/cell.js +83 -0
- package/dist/cell.js.map +1 -0
- package/dist/codec.d.ts +30 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +25 -0
- package/dist/codec.js.map +1 -0
- package/dist/component.d.ts +52 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/component.js +44 -0
- package/dist/component.js.map +1 -0
- package/dist/composable.d.ts +76 -0
- package/dist/composable.d.ts.map +1 -0
- package/dist/composable.js +221 -0
- package/dist/composable.js.map +1 -0
- package/dist/compositor-pool.d.ts +74 -0
- package/dist/compositor-pool.d.ts.map +1 -0
- package/dist/compositor-pool.js +119 -0
- package/dist/compositor-pool.js.map +1 -0
- package/dist/compositor.d.ts +90 -0
- package/dist/compositor.d.ts.map +1 -0
- package/dist/compositor.js +278 -0
- package/dist/compositor.js.map +1 -0
- package/dist/config.d.ts +72 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- package/dist/dag.d.ts +251 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +450 -0
- package/dist/dag.js.map +1 -0
- package/dist/defaults.d.ts +45 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +45 -0
- package/dist/defaults.js.map +1 -0
- package/dist/derived.d.ts +34 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +101 -0
- package/dist/derived.js.map +1 -0
- package/dist/diagnostics.d.ts +77 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +122 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/dirty.d.ts +55 -0
- package/dist/dirty.d.ts.map +1 -0
- package/dist/dirty.js +80 -0
- package/dist/dirty.js.map +1 -0
- package/dist/easing.d.ts +55 -0
- package/dist/easing.d.ts.map +1 -0
- package/dist/easing.js +291 -0
- package/dist/easing.js.map +1 -0
- package/dist/ecs.d.ts +105 -0
- package/dist/ecs.d.ts.map +1 -0
- package/dist/ecs.js +245 -0
- package/dist/ecs.js.map +1 -0
- package/dist/fnv.d.ts +14 -0
- package/dist/fnv.d.ts.map +1 -0
- package/dist/fnv.js +28 -0
- package/dist/fnv.js.map +1 -0
- package/dist/frame-budget.d.ts +73 -0
- package/dist/frame-budget.d.ts.map +1 -0
- package/dist/frame-budget.js +114 -0
- package/dist/frame-budget.js.map +1 -0
- package/dist/gen-frame.d.ts +102 -0
- package/dist/gen-frame.d.ts.map +1 -0
- package/dist/gen-frame.js +121 -0
- package/dist/gen-frame.js.map +1 -0
- package/dist/harness/arbitrary-from-schema.d.ts +28 -0
- package/dist/harness/arbitrary-from-schema.d.ts.map +1 -0
- package/dist/harness/arbitrary-from-schema.js +262 -0
- package/dist/harness/arbitrary-from-schema.js.map +1 -0
- package/dist/harness/cached-projection.d.ts +19 -0
- package/dist/harness/cached-projection.d.ts.map +1 -0
- package/dist/harness/cached-projection.js +39 -0
- package/dist/harness/cached-projection.js.map +1 -0
- package/dist/harness/index.d.ts +16 -0
- package/dist/harness/index.d.ts.map +1 -0
- package/dist/harness/index.js +15 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/harness/policy-gate.d.ts +18 -0
- package/dist/harness/policy-gate.d.ts.map +1 -0
- package/dist/harness/policy-gate.js +46 -0
- package/dist/harness/policy-gate.js.map +1 -0
- package/dist/harness/pure-transform.d.ts +42 -0
- package/dist/harness/pure-transform.d.ts.map +1 -0
- package/dist/harness/pure-transform.js +76 -0
- package/dist/harness/pure-transform.js.map +1 -0
- package/dist/harness/receipted-mutation.d.ts +23 -0
- package/dist/harness/receipted-mutation.d.ts.map +1 -0
- package/dist/harness/receipted-mutation.js +52 -0
- package/dist/harness/receipted-mutation.js.map +1 -0
- package/dist/harness/scene-composition.d.ts +19 -0
- package/dist/harness/scene-composition.d.ts.map +1 -0
- package/dist/harness/scene-composition.js +47 -0
- package/dist/harness/scene-composition.js.map +1 -0
- package/dist/harness/site-adapter.d.ts +18 -0
- package/dist/harness/site-adapter.d.ts.map +1 -0
- package/dist/harness/site-adapter.js +38 -0
- package/dist/harness/site-adapter.js.map +1 -0
- package/dist/harness/state-machine.d.ts +19 -0
- package/dist/harness/state-machine.d.ts.map +1 -0
- package/dist/harness/state-machine.js +44 -0
- package/dist/harness/state-machine.js.map +1 -0
- package/dist/hlc.d.ts +99 -0
- package/dist/hlc.d.ts.map +1 -0
- package/dist/hlc.js +219 -0
- package/dist/hlc.js.map +1 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolate.d.ts +14 -0
- package/dist/interpolate.d.ts.map +1 -0
- package/dist/interpolate.js +31 -0
- package/dist/interpolate.js.map +1 -0
- package/dist/live-cell.d.ts +46 -0
- package/dist/live-cell.d.ts.map +1 -0
- package/dist/live-cell.js +154 -0
- package/dist/live-cell.js.map +1 -0
- package/dist/op.d.ts +58 -0
- package/dist/op.d.ts.map +1 -0
- package/dist/op.js +171 -0
- package/dist/op.js.map +1 -0
- package/dist/plan.d.ts +195 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +211 -0
- package/dist/plan.js.map +1 -0
- package/dist/protocol.d.ts +33 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +10 -0
- package/dist/protocol.js.map +1 -0
- package/dist/quantizer-types.d.ts +28 -0
- package/dist/quantizer-types.d.ts.map +1 -0
- package/dist/quantizer-types.js +9 -0
- package/dist/quantizer-types.js.map +1 -0
- package/dist/receipt.d.ts +294 -0
- package/dist/receipt.d.ts.map +1 -0
- package/dist/receipt.js +352 -0
- package/dist/receipt.js.map +1 -0
- package/dist/runtime-coordinator.d.ts +75 -0
- package/dist/runtime-coordinator.d.ts.map +1 -0
- package/dist/runtime-coordinator.js +149 -0
- package/dist/runtime-coordinator.js.map +1 -0
- package/dist/scheduler.d.ts +58 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +109 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/ship-capsule.d.ts +54 -0
- package/dist/ship-capsule.d.ts.map +1 -0
- package/dist/ship-capsule.js +142 -0
- package/dist/ship-capsule.js.map +1 -0
- package/dist/ship-manifest.d.ts +45 -0
- package/dist/ship-manifest.d.ts.map +1 -0
- package/dist/ship-manifest.js +175 -0
- package/dist/ship-manifest.js.map +1 -0
- package/dist/signal.d.ts +149 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +277 -0
- package/dist/signal.js.map +1 -0
- package/dist/speculative.d.ts +67 -0
- package/dist/speculative.d.ts.map +1 -0
- package/dist/speculative.js +139 -0
- package/dist/speculative.js.map +1 -0
- package/dist/store.d.ts +39 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +42 -0
- package/dist/store.js.map +1 -0
- package/dist/style.d.ts +119 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +168 -0
- package/dist/style.js.map +1 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +14 -0
- package/dist/testing.js.map +1 -0
- package/dist/theme.d.ts +78 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +109 -0
- package/dist/theme.js.map +1 -0
- package/dist/timeline.d.ts +45 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +101 -0
- package/dist/timeline.js.map +1 -0
- package/dist/token-buffer.d.ts +43 -0
- package/dist/token-buffer.d.ts.map +1 -0
- package/dist/token-buffer.js +112 -0
- package/dist/token-buffer.js.map +1 -0
- package/dist/token.d.ts +107 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +143 -0
- package/dist/token.js.map +1 -0
- package/dist/tuple.d.ts +16 -0
- package/dist/tuple.d.ts.map +1 -0
- package/dist/tuple.js +16 -0
- package/dist/tuple.js.map +1 -0
- package/dist/type-utils.d.ts +41 -0
- package/dist/type-utils.d.ts.map +1 -0
- package/dist/type-utils.js +10 -0
- package/dist/type-utils.js.map +1 -0
- package/dist/typed-ref.d.ts +50 -0
- package/dist/typed-ref.d.ts.map +1 -0
- package/dist/typed-ref.js +59 -0
- package/dist/typed-ref.js.map +1 -0
- package/dist/ui-quality.d.ts +50 -0
- package/dist/ui-quality.d.ts.map +1 -0
- package/dist/ui-quality.js +64 -0
- package/dist/ui-quality.js.map +1 -0
- package/dist/validation-error.d.ts +25 -0
- package/dist/validation-error.d.ts.map +1 -0
- package/dist/validation-error.js +32 -0
- package/dist/validation-error.js.map +1 -0
- package/dist/vector-clock.d.ts +46 -0
- package/dist/vector-clock.d.ts.map +1 -0
- package/dist/vector-clock.js +91 -0
- package/dist/vector-clock.js.map +1 -0
- package/dist/video.d.ts +62 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +59 -0
- package/dist/video.js.map +1 -0
- package/dist/wasm-dispatch.d.ts +52 -0
- package/dist/wasm-dispatch.d.ts.map +1 -0
- package/dist/wasm-dispatch.js +204 -0
- package/dist/wasm-dispatch.js.map +1 -0
- package/dist/wasm-fallback.d.ts +19 -0
- package/dist/wasm-fallback.d.ts.map +1 -0
- package/dist/wasm-fallback.js +93 -0
- package/dist/wasm-fallback.js.map +1 -0
- package/dist/wire.d.ts +49 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +201 -0
- package/dist/wire.js.map +1 -0
- package/dist/zap.d.ts +42 -0
- package/dist/zap.d.ts.map +1 -0
- package/dist/zap.js +172 -0
- package/dist/zap.js.map +1 -0
- package/package.json +71 -0
- package/src/addressed-digest.ts +48 -0
- package/src/animation.ts +103 -0
- package/src/assembly.ts +76 -0
- package/src/av-bridge.ts +161 -0
- package/src/av-renderer.ts +118 -0
- package/src/blend.ts +135 -0
- package/src/boundary.ts +363 -0
- package/src/brands.ts +86 -0
- package/src/caps.ts +100 -0
- package/src/capsule.ts +95 -0
- package/src/capsules/boundary-evaluate.ts +128 -0
- package/src/capsules/canonical-cbor.ts +60 -0
- package/src/capsules/token-buffer.ts +57 -0
- package/src/capture.ts +48 -0
- package/src/cbor.ts +199 -0
- package/src/cell.ts +130 -0
- package/src/codec.ts +39 -0
- package/src/component.ts +102 -0
- package/src/composable.ts +328 -0
- package/src/compositor-pool.ts +162 -0
- package/src/compositor.ts +387 -0
- package/src/config.ts +157 -0
- package/src/dag.ts +527 -0
- package/src/defaults.ts +60 -0
- package/src/derived.ts +164 -0
- package/src/diagnostics.ts +186 -0
- package/src/dirty.ts +101 -0
- package/src/easing.ts +334 -0
- package/src/ecs.ts +382 -0
- package/src/fnv.ts +31 -0
- package/src/frame-budget.ts +149 -0
- package/src/gen-frame.ts +229 -0
- package/src/harness/arbitrary-from-schema.ts +270 -0
- package/src/harness/cached-projection.ts +46 -0
- package/src/harness/index.ts +16 -0
- package/src/harness/policy-gate.ts +51 -0
- package/src/harness/pure-transform.ts +121 -0
- package/src/harness/receipted-mutation.ts +59 -0
- package/src/harness/scene-composition.ts +54 -0
- package/src/harness/site-adapter.ts +43 -0
- package/src/harness/state-machine.ts +49 -0
- package/src/hlc.ts +238 -0
- package/src/index.ts +274 -0
- package/src/interpolate.ts +37 -0
- package/src/live-cell.ts +199 -0
- package/src/op.ts +233 -0
- package/src/plan.ts +317 -0
- package/src/protocol.ts +49 -0
- package/src/quantizer-types.ts +29 -0
- package/src/receipt.ts +444 -0
- package/src/runtime-coordinator.ts +230 -0
- package/src/scheduler.ts +161 -0
- package/src/ship-capsule.ts +191 -0
- package/src/signal.ts +345 -0
- package/src/speculative.ts +186 -0
- package/src/store.ts +77 -0
- package/src/style.ts +249 -0
- package/src/testing.ts +14 -0
- package/src/theme.ts +153 -0
- package/src/timeline.ts +146 -0
- package/src/token-buffer.ts +151 -0
- package/src/token.ts +197 -0
- package/src/tuple.ts +19 -0
- package/src/type-utils.ts +48 -0
- package/src/typed-ref.ts +79 -0
- package/src/ui-quality.ts +105 -0
- package/src/validation-error.ts +34 -0
- package/src/vector-clock.ts +111 -0
- package/src/video.ts +106 -0
- package/src/wasm-dispatch.ts +300 -0
- package/src/wasm-fallback.ts +102 -0
- package/src/wire.ts +274 -0
- package/src/zap.ts +241 -0
package/src/boundary.ts
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoundaryDef -- the core primitive of constraint-based adaptive rendering.
|
|
3
|
+
*
|
|
4
|
+
* A boundary defines quantization: how a continuous signal value maps
|
|
5
|
+
* to a discrete set of named states. Content-addressed via FNV-1a.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SignalInput, ThresholdValue, ContentAddress } from './brands.js';
|
|
11
|
+
import { SignalInput as mkSignalInput, ThresholdValue as mkThresholdValue } from './brands.js';
|
|
12
|
+
import { CanonicalCbor } from './cbor.js';
|
|
13
|
+
import { fnv1aBytes } from './fnv.js';
|
|
14
|
+
import { CzapValidationError } from './validation-error.js';
|
|
15
|
+
|
|
16
|
+
/** The core primitive. Source of truth for quantization boundaries. */
|
|
17
|
+
interface BoundaryDef<
|
|
18
|
+
I extends string = string,
|
|
19
|
+
S extends readonly [string, ...string[]] = readonly [string, ...string[]],
|
|
20
|
+
> {
|
|
21
|
+
readonly _tag: 'BoundaryDef';
|
|
22
|
+
readonly _version: 1;
|
|
23
|
+
readonly id: ContentAddress;
|
|
24
|
+
readonly input: SignalInput<I>;
|
|
25
|
+
readonly thresholds: readonly ThresholdValue[];
|
|
26
|
+
readonly states: S;
|
|
27
|
+
readonly hysteresis?: number;
|
|
28
|
+
readonly spec?: BoundarySpec;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface BoundaryFactory {
|
|
32
|
+
make<I extends string, const S extends readonly [string, ...string[]]>(config: {
|
|
33
|
+
readonly input: I;
|
|
34
|
+
readonly at: { readonly [K in keyof S]: readonly [number, S[K]] };
|
|
35
|
+
readonly hysteresis?: number;
|
|
36
|
+
readonly spec?: BoundarySpec;
|
|
37
|
+
}): BoundaryDef<I, S>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Compute the content address for a boundary synchronously.
|
|
42
|
+
* FNV-1a hash of the RFC 8949 §4.2.1 canonical CBOR encoding (ADR-0003).
|
|
43
|
+
* Cross-machine stable: identical definitions produce byte-identical IDs.
|
|
44
|
+
*/
|
|
45
|
+
function deterministicId(
|
|
46
|
+
input: string,
|
|
47
|
+
thresholds: readonly number[],
|
|
48
|
+
states: readonly string[],
|
|
49
|
+
hysteresis?: number,
|
|
50
|
+
spec?: BoundarySpec,
|
|
51
|
+
): ContentAddress {
|
|
52
|
+
return fnv1aBytes(
|
|
53
|
+
CanonicalCbor.encode({
|
|
54
|
+
_tag: 'BoundaryDef',
|
|
55
|
+
_version: 1,
|
|
56
|
+
input,
|
|
57
|
+
thresholds,
|
|
58
|
+
states,
|
|
59
|
+
hysteresis: hysteresis ?? null,
|
|
60
|
+
spec: spec ?? null,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Evaluate which state a value falls into given a boundary (binary search).
|
|
67
|
+
*
|
|
68
|
+
* Uses binary search to find the rightmost threshold `<= value`, returning
|
|
69
|
+
* the corresponding state name.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const bp = Boundary.make({ input: 'viewport.width', at: [[0, 'sm'], [768, 'md'], [1024, 'lg']] });
|
|
74
|
+
* const state = Boundary.evaluate(bp, 800);
|
|
75
|
+
* // state === 'md'
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
function _evaluate<B extends BoundaryDef>(boundary: B, value: number): B['states'][number] {
|
|
79
|
+
const { thresholds, states } = boundary;
|
|
80
|
+
const len = thresholds.length;
|
|
81
|
+
|
|
82
|
+
// Fast path: unrolled if-chain for small threshold arrays (≤4).
|
|
83
|
+
// Avoids loop overhead and branch prediction misses of binary search.
|
|
84
|
+
// Check from highest threshold downward; first match wins.
|
|
85
|
+
if (len <= 4) {
|
|
86
|
+
if (len === 1) {
|
|
87
|
+
return states[0]!;
|
|
88
|
+
}
|
|
89
|
+
if (len === 2) {
|
|
90
|
+
if (value >= (thresholds[1] as number)) return states[1]!;
|
|
91
|
+
return states[0]!;
|
|
92
|
+
}
|
|
93
|
+
if (len === 3) {
|
|
94
|
+
if (value >= (thresholds[2] as number)) return states[2]!;
|
|
95
|
+
if (value >= (thresholds[1] as number)) return states[1]!;
|
|
96
|
+
return states[0]!;
|
|
97
|
+
}
|
|
98
|
+
// len === 4
|
|
99
|
+
if (value >= (thresholds[3] as number)) return states[3]!;
|
|
100
|
+
if (value >= (thresholds[2] as number)) return states[2]!;
|
|
101
|
+
if (value >= (thresholds[1] as number)) return states[1]!;
|
|
102
|
+
return states[0]!;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Binary search: find the rightmost threshold <= value
|
|
106
|
+
let lo = 0;
|
|
107
|
+
let hi = len;
|
|
108
|
+
while (lo < hi) {
|
|
109
|
+
const mid = (lo + hi) >>> 1;
|
|
110
|
+
if ((thresholds[mid] as number) <= value) {
|
|
111
|
+
lo = mid + 1;
|
|
112
|
+
} else {
|
|
113
|
+
hi = mid;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// lo is the first threshold > value, so lo-1 is the match (or 0 if none)
|
|
117
|
+
return states[lo > 0 ? lo - 1 : 0]!;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Evaluate with hysteresis (requires previous state). Half-width dead zone algorithm.
|
|
122
|
+
*
|
|
123
|
+
* Prevents flickering at boundary edges by requiring the value to cross
|
|
124
|
+
* beyond a dead zone (half the hysteresis width) before transitioning states.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* const bp = Boundary.make({ input: 'viewport.width', at: [[0, 'sm'], [768, 'md']], hysteresis: 20 });
|
|
129
|
+
* const state1 = Boundary.evaluateWithHysteresis(bp, 770, 'sm');
|
|
130
|
+
* // state1 === 'sm' (within dead zone, stays at previous)
|
|
131
|
+
* const state2 = Boundary.evaluateWithHysteresis(bp, 780, 'sm');
|
|
132
|
+
* // state2 === 'md' (past dead zone, transitions)
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
function _evaluateWithHysteresis<B extends BoundaryDef>(
|
|
136
|
+
boundary: B,
|
|
137
|
+
value: number,
|
|
138
|
+
previousState: B['states'][number],
|
|
139
|
+
): B['states'][number] {
|
|
140
|
+
if (!boundary.hysteresis || boundary.hysteresis <= 0) return _evaluate(boundary, value);
|
|
141
|
+
|
|
142
|
+
const half = boundary.hysteresis / 2;
|
|
143
|
+
const { thresholds, states } = boundary;
|
|
144
|
+
const prevIdx = (states as readonly string[]).indexOf(previousState as string);
|
|
145
|
+
|
|
146
|
+
// Unknown previous state -- fall back to raw evaluation
|
|
147
|
+
if (prevIdx === -1) return _evaluate(boundary, value);
|
|
148
|
+
|
|
149
|
+
// Find raw state via linear scan (matching evaluate semantics)
|
|
150
|
+
let rawIdx = 0;
|
|
151
|
+
for (let i = thresholds.length - 1; i >= 0; i--) {
|
|
152
|
+
if (value >= thresholds[i]!) {
|
|
153
|
+
rawIdx = i;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// No crossing needed
|
|
159
|
+
if (rawIdx === prevIdx) return states[rawIdx]!;
|
|
160
|
+
|
|
161
|
+
// Dead-zone suppression: when crossing a threshold, require the value to exceed the
|
|
162
|
+
// threshold by half the hysteresis width before committing. Prevents jitter when a
|
|
163
|
+
// signal oscillates near a boundary.
|
|
164
|
+
if (rawIdx > prevIdx) {
|
|
165
|
+
for (let i = prevIdx + 1; i <= rawIdx; i++) {
|
|
166
|
+
if (value < thresholds[i]! + half) {
|
|
167
|
+
return states[i - 1]!;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
for (let i = prevIdx; i > rawIdx; i--) {
|
|
172
|
+
if (value > thresholds[i]! - half) {
|
|
173
|
+
return states[i]!;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return states[rawIdx]!;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Boundary namespace -- the core primitive of constraint-based adaptive rendering.
|
|
183
|
+
*
|
|
184
|
+
* Create boundaries that quantize continuous signal values into discrete named
|
|
185
|
+
* states. Supports hysteresis for flicker-free transitions at threshold edges.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* import { Boundary } from '@czap/core';
|
|
190
|
+
*
|
|
191
|
+
* const bp = Boundary.make({
|
|
192
|
+
* input: 'viewport.width',
|
|
193
|
+
* at: [[0, 'mobile'], [768, 'tablet'], [1024, 'desktop']],
|
|
194
|
+
* hysteresis: 20,
|
|
195
|
+
* });
|
|
196
|
+
* const state = Boundary.evaluate(bp, 900);
|
|
197
|
+
* // state === 'tablet'
|
|
198
|
+
* const stableState = Boundary.evaluateWithHysteresis(bp, 770, 'mobile');
|
|
199
|
+
* // stableState === 'mobile' (within dead zone)
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
/**
|
|
203
|
+
* Check whether a boundary is active given its optional spec and current context.
|
|
204
|
+
* Returns true if the boundary has no spec or the spec allows evaluation.
|
|
205
|
+
*/
|
|
206
|
+
function _isActive<B extends BoundaryDef>(
|
|
207
|
+
boundary: B,
|
|
208
|
+
context?: {
|
|
209
|
+
capabilities?: Record<string, unknown>;
|
|
210
|
+
nowMs?: number;
|
|
211
|
+
activeExperiments?: ReadonlyArray<string>;
|
|
212
|
+
},
|
|
213
|
+
): boolean {
|
|
214
|
+
return _isSpecActive(boundary.spec, context);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Boundary — core primitive of constraint-based adaptive rendering.
|
|
219
|
+
*
|
|
220
|
+
* A boundary quantizes a continuous signal (viewport, scroll, audio, …) into
|
|
221
|
+
* a discrete set of named states. Every boundary is content-addressed via
|
|
222
|
+
* FNV-1a, supports optional hysteresis to prevent flicker at thresholds, and
|
|
223
|
+
* can be gated by a {@link BoundarySpec} for A/B or device-conditional activation.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```ts
|
|
227
|
+
* import { Boundary } from '@czap/core';
|
|
228
|
+
*
|
|
229
|
+
* const viewport = Boundary.make({
|
|
230
|
+
* input: 'viewport.width',
|
|
231
|
+
* at: [[0, 'mobile'], [640, 'tablet'], [1024, 'desktop']] as const,
|
|
232
|
+
* hysteresis: 16,
|
|
233
|
+
* });
|
|
234
|
+
* Boundary.evaluate(viewport, 800); // 'tablet'
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export const Boundary: BoundaryFactory & {
|
|
238
|
+
evaluate: typeof _evaluate;
|
|
239
|
+
evaluateWithHysteresis: typeof _evaluateWithHysteresis;
|
|
240
|
+
isActive: typeof _isActive;
|
|
241
|
+
} = {
|
|
242
|
+
/**
|
|
243
|
+
* Create a new `BoundaryDef` from a configuration object.
|
|
244
|
+
*
|
|
245
|
+
* Thresholds must be strictly ascending. The boundary is content-addressed
|
|
246
|
+
* via FNV-1a hash of its definition.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* const bp = Boundary.make({
|
|
251
|
+
* input: 'viewport.width',
|
|
252
|
+
* at: [[0, 'sm'], [768, 'md'], [1024, 'lg']],
|
|
253
|
+
* hysteresis: 10,
|
|
254
|
+
* });
|
|
255
|
+
* // bp._tag === 'BoundaryDef'
|
|
256
|
+
* // bp.id === 'fnv1a:...' (content address)
|
|
257
|
+
* // bp.states === ['sm', 'md', 'lg']
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
make<I extends string, const S extends readonly [string, ...string[]]>(config: {
|
|
261
|
+
readonly input: I;
|
|
262
|
+
readonly at: { readonly [K in keyof S]: readonly [number, S[K]] };
|
|
263
|
+
readonly hysteresis?: number;
|
|
264
|
+
readonly spec?: BoundarySpec;
|
|
265
|
+
}): BoundaryDef<I, S> {
|
|
266
|
+
const pairs = config.at;
|
|
267
|
+
for (let i = 1; i < pairs.length; i++) {
|
|
268
|
+
if (pairs[i]![0] <= pairs[i - 1]![0]) {
|
|
269
|
+
throw new CzapValidationError(
|
|
270
|
+
'Boundary.make',
|
|
271
|
+
`thresholds must be strictly ascending. Got ${pairs[i - 1]![0]} before ${pairs[i]![0]} at index ${i}.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const stateNames = pairs.map(([, s]) => s);
|
|
276
|
+
const seen = new Set<string>();
|
|
277
|
+
for (const name of stateNames) {
|
|
278
|
+
if (seen.has(name)) {
|
|
279
|
+
throw new CzapValidationError(
|
|
280
|
+
'Boundary.make',
|
|
281
|
+
`duplicate state name "${name}". Each state must have a unique name.`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
seen.add(name);
|
|
285
|
+
}
|
|
286
|
+
const thresholds = pairs.map(([t]) => mkThresholdValue(t));
|
|
287
|
+
// tupleMap preserves arity but fn returns `string`, not per-element S[K]; one narrow cast is unavoidable.
|
|
288
|
+
const states = pairs.map(([, s]) => s) as unknown as S;
|
|
289
|
+
const id = deterministicId(config.input, thresholds, states, config.hysteresis, config.spec);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
_tag: 'BoundaryDef',
|
|
293
|
+
_version: 1,
|
|
294
|
+
id,
|
|
295
|
+
input: mkSignalInput(config.input),
|
|
296
|
+
thresholds,
|
|
297
|
+
states,
|
|
298
|
+
...(config.hysteresis !== undefined ? { hysteresis: config.hysteresis } : {}),
|
|
299
|
+
...(config.spec !== undefined ? { spec: config.spec } : {}),
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
evaluate: _evaluate,
|
|
303
|
+
evaluateWithHysteresis: _evaluateWithHysteresis,
|
|
304
|
+
isActive: _isActive,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* BoundarySpec: optional filter that gates whether a boundary is active.
|
|
309
|
+
* Enables A/B testing, time-bounded experiments, and device targeting
|
|
310
|
+
* without external wrapping logic.
|
|
311
|
+
*
|
|
312
|
+
* **Phase 2**: Spec evaluation is implemented and tested but not yet wired
|
|
313
|
+
* into the Compositor evaluation loop. Call `BoundarySpec.isActive()` or
|
|
314
|
+
* `Boundary.isActive()` manually to check activation in the interim.
|
|
315
|
+
*/
|
|
316
|
+
export interface BoundarySpec {
|
|
317
|
+
/** Only evaluate this boundary when the device filter returns true. */
|
|
318
|
+
readonly deviceFilter?: (capabilities: Record<string, unknown>) => boolean;
|
|
319
|
+
/** Only evaluate this boundary within this time range (epoch ms). */
|
|
320
|
+
readonly timeRange?: { readonly from?: number; readonly until?: number };
|
|
321
|
+
/** Only evaluate this boundary for participants in this experiment. */
|
|
322
|
+
readonly experimentId?: string;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Check if a BoundarySpec allows evaluation given current context. */
|
|
326
|
+
function _isSpecActive(
|
|
327
|
+
spec: BoundarySpec | undefined,
|
|
328
|
+
context?: {
|
|
329
|
+
capabilities?: Record<string, unknown>;
|
|
330
|
+
nowMs?: number;
|
|
331
|
+
activeExperiments?: ReadonlyArray<string>;
|
|
332
|
+
},
|
|
333
|
+
): boolean {
|
|
334
|
+
if (!spec) return true;
|
|
335
|
+
if (spec.deviceFilter && context?.capabilities) {
|
|
336
|
+
if (!spec.deviceFilter(context.capabilities)) return false;
|
|
337
|
+
}
|
|
338
|
+
if (spec.timeRange) {
|
|
339
|
+
const now = context?.nowMs ?? Date.now();
|
|
340
|
+
if (spec.timeRange.from !== undefined && now < spec.timeRange.from) return false;
|
|
341
|
+
if (spec.timeRange.until !== undefined && now > spec.timeRange.until) return false;
|
|
342
|
+
}
|
|
343
|
+
if (spec.experimentId && context?.activeExperiments) {
|
|
344
|
+
if (!context.activeExperiments.includes(spec.experimentId)) return false;
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** BoundarySpec namespace — helpers for working with the optional activation filter on a boundary. */
|
|
350
|
+
export const BoundarySpec = {
|
|
351
|
+
/** Check whether a {@link BoundarySpec} allows evaluation in the given context. */
|
|
352
|
+
isActive: _isSpecActive,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export declare namespace Boundary {
|
|
356
|
+
/** Structural shape of a boundary definition parameterized by input name `I` and state tuple `S`. */
|
|
357
|
+
export type Shape<
|
|
358
|
+
I extends string = string,
|
|
359
|
+
S extends readonly [string, ...string[]] = readonly [string, ...string[]],
|
|
360
|
+
> = BoundaryDef<I, S>;
|
|
361
|
+
/** Alias for {@link BoundarySpec}. */
|
|
362
|
+
export type Spec = BoundarySpec;
|
|
363
|
+
}
|
package/src/brands.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded type factories for `@czap/core`.
|
|
3
|
+
*
|
|
4
|
+
* All custom brands use unique symbols for nominal typing safety.
|
|
5
|
+
* Brand helpers produce branded wrappers at zero runtime cost.
|
|
6
|
+
*
|
|
7
|
+
* Types are re-anchored from `@czap/_spine` (the canonical source) via local
|
|
8
|
+
* type aliases so the names remain valid value exports (runtime constructors)
|
|
9
|
+
* in the same module without triggering `isolatedModules` conflicts.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
SignalInput as _SignalInput,
|
|
16
|
+
ThresholdValue as _ThresholdValue,
|
|
17
|
+
StateName as _StateName,
|
|
18
|
+
ContentAddress as _ContentAddress,
|
|
19
|
+
IntegrityDigest as _IntegrityDigest,
|
|
20
|
+
AddressedDigest as _AddressedDigest,
|
|
21
|
+
TokenRef as _TokenRef,
|
|
22
|
+
Millis as _Millis,
|
|
23
|
+
} from '@czap/_spine';
|
|
24
|
+
|
|
25
|
+
// Re-anchor types from the canonical source (_spine).
|
|
26
|
+
// Using local type aliases preserves declaration-merging with the const constructors below.
|
|
27
|
+
|
|
28
|
+
/** Branded input signal name. Dot-notation signal path (e.g. viewport.width, prefers-color-scheme). */
|
|
29
|
+
export type SignalInput<I extends string = string> = _SignalInput<I>;
|
|
30
|
+
|
|
31
|
+
/** Branded threshold number on a boundary. Finite number on the signal's continuous range. */
|
|
32
|
+
export type ThresholdValue = _ThresholdValue;
|
|
33
|
+
|
|
34
|
+
/** Branded state name -- e.g. 'mobile', 'tablet', 'desktop' */
|
|
35
|
+
export type StateName<S extends string = string> = _StateName<S>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Content-addressed hash.
|
|
39
|
+
* Format: fnv1a:XXXXXXXX (8 hex digits). Computed from CBOR-canonical payload via FNV-1a hash.
|
|
40
|
+
*/
|
|
41
|
+
export type ContentAddress = _ContentAddress;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cryptographic content digest brand. Format: `sha256:<64-hex>` or `blake3:<64-hex>`.
|
|
45
|
+
* The algorithmic complement to ContentAddress for external/release artifacts (ADR-0011).
|
|
46
|
+
*/
|
|
47
|
+
export type IntegrityDigest = _IntegrityDigest;
|
|
48
|
+
|
|
49
|
+
/** Pair of identity hash + cryptographic digest over the same canonical bytes (ADR-0011). */
|
|
50
|
+
export type AddressedDigest = _AddressedDigest;
|
|
51
|
+
|
|
52
|
+
/** Branded token reference name */
|
|
53
|
+
export type TokenRef<N extends string = string> = _TokenRef<N>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Branded millisecond duration -- forces explicit wrapping of raw numbers at temporal API boundaries.
|
|
57
|
+
* Non-negative millisecond duration. Fractional values allowed. Use Millis(0) for immediate.
|
|
58
|
+
*/
|
|
59
|
+
export type Millis = _Millis;
|
|
60
|
+
|
|
61
|
+
/** Hybrid Logical Clock */
|
|
62
|
+
export interface HLC {
|
|
63
|
+
readonly wall_ms: number;
|
|
64
|
+
readonly counter: number;
|
|
65
|
+
readonly node_id: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Generic brand factory */
|
|
69
|
+
export function brand<T, B extends symbol>(value: T): T & { readonly [K in B]: true } {
|
|
70
|
+
return value as T & { readonly [K in B]: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Wrap a plain string as a {@link SignalInput} — the one sanctioned cast site for this brand. */
|
|
74
|
+
export const SignalInput = <I extends string>(value: I): SignalInput<I> => value as SignalInput<I>;
|
|
75
|
+
/** Wrap a plain number as a {@link ThresholdValue} — the one sanctioned cast site for this brand. */
|
|
76
|
+
export const ThresholdValue = (value: number): ThresholdValue => value as ThresholdValue;
|
|
77
|
+
/** Wrap a plain string as a {@link StateName} — the one sanctioned cast site for this brand. */
|
|
78
|
+
export const StateName = <S extends string>(value: S): StateName<S> => value as StateName<S>;
|
|
79
|
+
/** Wrap a plain string as a {@link ContentAddress} — the one sanctioned cast site for this brand. */
|
|
80
|
+
export const ContentAddress = (value: string): ContentAddress => value as ContentAddress;
|
|
81
|
+
/** Wrap a plain string as an {@link IntegrityDigest} — the one sanctioned cast site for this brand. */
|
|
82
|
+
export const IntegrityDigest = (value: string): IntegrityDigest => value as IntegrityDigest;
|
|
83
|
+
/** Wrap a plain string as a {@link TokenRef} — the one sanctioned cast site for this brand. */
|
|
84
|
+
export const TokenRef = <N extends string>(value: N): TokenRef<N> => value as TokenRef<N>;
|
|
85
|
+
/** Wrap a plain number as a {@link Millis} — the one sanctioned cast site for this brand. */
|
|
86
|
+
export const Millis = (value: number): Millis => value as Millis;
|
package/src/caps.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CapSet -- capability lattice.
|
|
3
|
+
*
|
|
4
|
+
* Re-parameterized from `@kit`: `pure < read < ... < system` becomes `static < styled < reactive < animated < gpu`.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rung on the rendering-capability ladder. Higher levels imply lower ones:
|
|
11
|
+
* `gpu > animated > reactive > styled > static`.
|
|
12
|
+
*/
|
|
13
|
+
export type CapLevel = 'static' | 'styled' | 'reactive' | 'animated' | 'gpu';
|
|
14
|
+
|
|
15
|
+
const LEVEL_ORD: Record<CapLevel, number> = {
|
|
16
|
+
static: 0,
|
|
17
|
+
styled: 1,
|
|
18
|
+
reactive: 2,
|
|
19
|
+
animated: 3,
|
|
20
|
+
gpu: 4,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Immutable set of {@link CapLevel}s — the tagged value returned by {@link Cap} combinators. */
|
|
24
|
+
export interface CapSet {
|
|
25
|
+
readonly _tag: 'CapSet';
|
|
26
|
+
readonly levels: ReadonlySet<CapLevel>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const _empty = (): CapSet => ({ _tag: 'CapSet', levels: new Set() });
|
|
30
|
+
|
|
31
|
+
const _from = (levels: ReadonlyArray<CapLevel>): CapSet => ({
|
|
32
|
+
_tag: 'CapSet',
|
|
33
|
+
levels: new Set(levels),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const _grant = (caps: CapSet, level: CapLevel): CapSet => ({
|
|
37
|
+
_tag: 'CapSet',
|
|
38
|
+
levels: new Set([...caps.levels, level]),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const _revoke = (caps: CapSet, level: CapLevel): CapSet => ({
|
|
42
|
+
_tag: 'CapSet',
|
|
43
|
+
levels: new Set([...caps.levels].filter((l) => l !== level)),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const _has = (caps: CapSet, level: CapLevel): boolean => caps.levels.has(level);
|
|
47
|
+
|
|
48
|
+
const _superset = (a: CapSet, b: CapSet): boolean => {
|
|
49
|
+
for (const level of b.levels) {
|
|
50
|
+
if (!a.levels.has(level)) return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const _union = (a: CapSet, b: CapSet): CapSet => ({
|
|
56
|
+
_tag: 'CapSet',
|
|
57
|
+
levels: new Set([...a.levels, ...b.levels]),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const _intersection = (a: CapSet, b: CapSet): CapSet => ({
|
|
61
|
+
_tag: 'CapSet',
|
|
62
|
+
levels: new Set([...a.levels].filter((l) => b.levels.has(l))),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const _atLeast = (a: CapLevel, b: CapLevel): boolean => LEVEL_ORD[a] >= LEVEL_ORD[b];
|
|
66
|
+
|
|
67
|
+
const _ordinal = (level: CapLevel): number => LEVEL_ORD[level];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Cap — algebra over {@link CapSet}.
|
|
71
|
+
* Pure, immutable helpers for building, combining, and comparing capability
|
|
72
|
+
* sets; the underlying `CapLevel` lattice is totally ordered via {@link Cap.ordinal}.
|
|
73
|
+
*/
|
|
74
|
+
export const Cap = {
|
|
75
|
+
/** The empty {@link CapSet}. */
|
|
76
|
+
empty: _empty,
|
|
77
|
+
/** Build a {@link CapSet} from an array of {@link CapLevel}s. */
|
|
78
|
+
from: _from,
|
|
79
|
+
/** Return a new {@link CapSet} with the given level added. */
|
|
80
|
+
grant: _grant,
|
|
81
|
+
/** Return a new {@link CapSet} with the given level removed. */
|
|
82
|
+
revoke: _revoke,
|
|
83
|
+
/** Whether a {@link CapSet} contains the given level. */
|
|
84
|
+
has: _has,
|
|
85
|
+
/** Whether `a` contains every level of `b` (i.e. `a ⊇ b`). */
|
|
86
|
+
superset: _superset,
|
|
87
|
+
/** Set union of two {@link CapSet}s. */
|
|
88
|
+
union: _union,
|
|
89
|
+
/** Set intersection of two {@link CapSet}s. */
|
|
90
|
+
intersection: _intersection,
|
|
91
|
+
/** Whether `a` ranks `>=` `b` on the underlying ordered ladder. */
|
|
92
|
+
atLeast: _atLeast,
|
|
93
|
+
/** Integer ordinal for a {@link CapLevel} — useful for sorting / comparison. */
|
|
94
|
+
ordinal: _ordinal,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export declare namespace Cap {
|
|
98
|
+
/** Alias for {@link CapSet}. */
|
|
99
|
+
export type Shape = CapSet;
|
|
100
|
+
}
|
package/src/capsule.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capsule — typed declaration of a business-logic unit that emits
|
|
3
|
+
* runtime behavior plus generated tests, benches, docs, and audit
|
|
4
|
+
* receipts through the czap factory.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type Effect, Schema } from 'effect';
|
|
10
|
+
import type { ContentAddress } from '@czap/_spine';
|
|
11
|
+
|
|
12
|
+
/** Closed seven-arm catalog of capsule kinds. Adding an eighth requires ADR amendment. */
|
|
13
|
+
export type AssemblyKind =
|
|
14
|
+
| 'pureTransform'
|
|
15
|
+
| 'receiptedMutation'
|
|
16
|
+
| 'stateMachine'
|
|
17
|
+
| 'siteAdapter'
|
|
18
|
+
| 'policyGate'
|
|
19
|
+
| 'cachedProjection'
|
|
20
|
+
| 'sceneComposition';
|
|
21
|
+
|
|
22
|
+
/** Where a capsule may run. */
|
|
23
|
+
export type Site = 'node' | 'browser' | 'worker' | 'edge';
|
|
24
|
+
|
|
25
|
+
/** What services a capsule reads / writes. `_R` parameter carried for type-level inference. */
|
|
26
|
+
export interface CapabilityDecl<_R> {
|
|
27
|
+
readonly reads: readonly string[];
|
|
28
|
+
readonly writes: readonly string[];
|
|
29
|
+
readonly effects?: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Performance + memory budgets a capsule promises to honor. */
|
|
33
|
+
export interface BudgetDecl {
|
|
34
|
+
readonly p95Ms?: number;
|
|
35
|
+
readonly memoryMb?: number;
|
|
36
|
+
readonly allocClass?: 'zero' | 'bounded' | 'unbounded';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A typed invariant over input and output that the harness will check. */
|
|
40
|
+
export interface Invariant<In, Out> {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly check: (input: In, output: Out) => boolean;
|
|
43
|
+
readonly message: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** License and authorship metadata carried for audit receipts. */
|
|
47
|
+
export interface AttributionDecl {
|
|
48
|
+
readonly license: string;
|
|
49
|
+
readonly author: string;
|
|
50
|
+
readonly url?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The contract shape a capsule declaration must satisfy. The factory
|
|
55
|
+
* uses this to generate tests, benches, docs, and audit receipts.
|
|
56
|
+
*
|
|
57
|
+
* `run` is optional: when present, the harness invokes it inside generated
|
|
58
|
+
* property tests so each declared {@link Invariant} is checked against
|
|
59
|
+
* real (input, output) pairs sampled from the input schema. Without `run`
|
|
60
|
+
* the harness emits an `it.skip` honest-placeholder so vacuous tests can't
|
|
61
|
+
* masquerade as proof.
|
|
62
|
+
*/
|
|
63
|
+
export interface CapsuleContract<K extends AssemblyKind, In, Out, R> {
|
|
64
|
+
readonly _kind: K;
|
|
65
|
+
readonly id: ContentAddress;
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly input: Schema.Schema<In>;
|
|
68
|
+
readonly output: Schema.Schema<Out>;
|
|
69
|
+
readonly capabilities: CapabilityDecl<R>;
|
|
70
|
+
readonly invariants: readonly Invariant<In, Out>[];
|
|
71
|
+
readonly budgets: BudgetDecl;
|
|
72
|
+
readonly site: readonly Site[];
|
|
73
|
+
readonly attribution?: AttributionDecl;
|
|
74
|
+
/**
|
|
75
|
+
* Optional pure-transform handler: takes a decoded input and returns a
|
|
76
|
+
* decoded output. Used by the harness to drive generated property tests
|
|
77
|
+
* end-to-end. Only meaningful for `pureTransform` arms today.
|
|
78
|
+
*/
|
|
79
|
+
readonly run?: (input: In) => Out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Runtime validator that verifies values against _spine-derived schemas.
|
|
84
|
+
* Used by capsule dispatchers to check inputs before invoking handlers.
|
|
85
|
+
*/
|
|
86
|
+
export const TypeValidator = {
|
|
87
|
+
validate<T>(schema: Schema.Codec<T, T, never>, value: unknown): Effect.Effect<T, Schema.SchemaError> {
|
|
88
|
+
return Schema.decodeUnknownEffect(schema)(value);
|
|
89
|
+
},
|
|
90
|
+
} as const;
|
|
91
|
+
|
|
92
|
+
export declare namespace TypeValidator {
|
|
93
|
+
/** Effect returned by {@link TypeValidator.validate} on a successful decode. */
|
|
94
|
+
export type Result<T> = Effect.Effect<T, Schema.SchemaError>;
|
|
95
|
+
}
|