@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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness template for the `cachedProjection` assembly arm.
|
|
3
|
+
*
|
|
4
|
+
* Cached projections derive an output from a source via a deterministic
|
|
5
|
+
* pipeline with cache invalidation tied to source content addresses.
|
|
6
|
+
* Without a `derive(source)` channel on the contract the harness can't
|
|
7
|
+
* exercise cache-hit equality or invalidation, so each case is emitted
|
|
8
|
+
* as `it.skip` rather than a vacuous placeholder.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { CapsuleDef } from '../assembly.js';
|
|
14
|
+
import type { HarnessOutput } from './pure-transform.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate the test + bench file contents for a `cachedProjection` capsule.
|
|
18
|
+
* Emits `it.skip` placeholders for cache-hit and invalidation tests.
|
|
19
|
+
*/
|
|
20
|
+
export function generateCachedProjection(
|
|
21
|
+
cap: CapsuleDef<'cachedProjection', unknown, unknown, unknown>,
|
|
22
|
+
): HarnessOutput {
|
|
23
|
+
const testFile = `// GENERATED — do not edit by hand
|
|
24
|
+
import { describe, it } from 'vitest';
|
|
25
|
+
|
|
26
|
+
describe('${cap.name}', () => {
|
|
27
|
+
it.skip('cache hit: identical source yields the same derived output', () => {
|
|
28
|
+
// TODO(harness): needs cap.derive handler + content-addressed source.
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.skip('invalidation: source change produces new cache entry', () => {
|
|
32
|
+
// TODO(harness): same — needs cap.derive.
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const benchFile = `// GENERATED — do not edit by hand
|
|
38
|
+
import { bench } from 'vitest';
|
|
39
|
+
|
|
40
|
+
bench('${cap.name} — decode throughput', () => {
|
|
41
|
+
// decode a canonical source, measure p95 vs budget (${cap.budgets.p95Ms ?? 'n/a'}ms)
|
|
42
|
+
}, { time: 500 });
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
return { testFile, benchFile };
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness — per-arm templates that emit test + bench + audit files
|
|
3
|
+
* from a capsule declaration. Each arm has its own generator.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { generatePureTransform } from './pure-transform.js';
|
|
9
|
+
export type { HarnessOutput, HarnessContext } from './pure-transform.js';
|
|
10
|
+
export { ArbitraryFromSchema, schemaToArbitrary, UnsupportedSchemaError } from './arbitrary-from-schema.js';
|
|
11
|
+
export { generateReceiptedMutation } from './receipted-mutation.js';
|
|
12
|
+
export { generateStateMachine } from './state-machine.js';
|
|
13
|
+
export { generateSiteAdapter } from './site-adapter.js';
|
|
14
|
+
export { generatePolicyGate } from './policy-gate.js';
|
|
15
|
+
export { generateCachedProjection } from './cached-projection.js';
|
|
16
|
+
export { generateSceneComposition } from './scene-composition.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness template for the `policyGate` assembly arm.
|
|
3
|
+
*
|
|
4
|
+
* Policy gates resolve allow/deny against typed subjects. Without a
|
|
5
|
+
* `decide(subject)` channel on the capsule contract the harness can't
|
|
6
|
+
* exercise allow/deny branches or check reason chains, so each case is
|
|
7
|
+
* emitted as `it.skip` rather than a vacuous placeholder.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CapsuleDef } from '../assembly.js';
|
|
13
|
+
import type { HarnessOutput } from './pure-transform.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate the test + bench file contents for a `policyGate` capsule.
|
|
17
|
+
* Emits `it.skip` placeholders for allow / deny / reason-chain coverage.
|
|
18
|
+
*/
|
|
19
|
+
export function generatePolicyGate(cap: CapsuleDef<'policyGate', unknown, unknown, unknown>): HarnessOutput {
|
|
20
|
+
const testFile = `// GENERATED — do not edit by hand
|
|
21
|
+
import { describe, it } from 'vitest';
|
|
22
|
+
|
|
23
|
+
describe('${cap.name}', () => {
|
|
24
|
+
it.skip('allow branch: a subject meeting the policy resolves to allow', () => {
|
|
25
|
+
// TODO(harness): needs cap.decide handler to drive subject -> outcome.
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it.skip('deny branch: a subject failing the policy resolves to deny', () => {
|
|
29
|
+
// TODO(harness): same — needs cap.decide.
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it.skip('reason chain present on every decision', () => {
|
|
33
|
+
// TODO(harness): same — needs cap.decide and a typed reasons schema.
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it.skip('no silent deny: every deny has a typed reason code', () => {
|
|
37
|
+
// TODO(harness): same — needs reasons enum on the contract.
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const benchFile = `// GENERATED — do not edit by hand
|
|
43
|
+
import { bench } from 'vitest';
|
|
44
|
+
|
|
45
|
+
bench('${cap.name}', () => {
|
|
46
|
+
// policy decision with a canonical fixture
|
|
47
|
+
}, { time: 500 });
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
return { testFile, benchFile };
|
|
51
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness template for the `pureTransform` assembly arm.
|
|
3
|
+
*
|
|
4
|
+
* Emits a property test per declared invariant: derives a fast-check
|
|
5
|
+
* arbitrary from the capsule's input schema (`schemaToArbitrary`),
|
|
6
|
+
* invokes the capsule's `run` handler against each sample, and asserts
|
|
7
|
+
* the invariant `check(input, output)` holds.
|
|
8
|
+
*
|
|
9
|
+
* If the capsule does not export a `run` handler the test is emitted as
|
|
10
|
+
* `it.skip` with a TODO comment — vacuous `() => true` placeholders are
|
|
11
|
+
* banned (per memory: "no vanity tests, real APIs, deterministic
|
|
12
|
+
* assertions").
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import type { CapsuleDef } from '../assembly.js';
|
|
17
|
+
|
|
18
|
+
/** Emitted file contents for a capsule harness (test + bench pair). */
|
|
19
|
+
export interface HarnessOutput {
|
|
20
|
+
readonly testFile: string;
|
|
21
|
+
readonly benchFile: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Optional metadata the compile-time driver passes to harness templates so
|
|
26
|
+
* the generated test file can `import` the real capsule binding from its
|
|
27
|
+
* source file. When `bindingImport` is undefined, the harness emits an
|
|
28
|
+
* `it.skip` placeholder rather than a vacuous test.
|
|
29
|
+
*/
|
|
30
|
+
export interface HarnessContext {
|
|
31
|
+
/** ESM-style import specifier (with `.js` extension) for the test file. */
|
|
32
|
+
readonly bindingImport?: string;
|
|
33
|
+
/** Exported binding name to import from `bindingImport`. */
|
|
34
|
+
readonly bindingName?: string;
|
|
35
|
+
/** Import specifier for `schemaToArbitrary`, default to source path. */
|
|
36
|
+
readonly arbitraryImport?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_ARBITRARY_IMPORT = '../../packages/core/src/harness/arbitrary-from-schema.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate the test + bench file contents for a `pureTransform` capsule.
|
|
43
|
+
* The emitted files are strings; the repo compiler writes them to
|
|
44
|
+
* `tests/generated/<name>.{test,bench}.ts`.
|
|
45
|
+
*/
|
|
46
|
+
export function generatePureTransform(
|
|
47
|
+
cap: CapsuleDef<'pureTransform', unknown, unknown, unknown>,
|
|
48
|
+
ctx: HarnessContext = {},
|
|
49
|
+
): HarnessOutput {
|
|
50
|
+
const arbitraryImport = ctx.arbitraryImport ?? DEFAULT_ARBITRARY_IMPORT;
|
|
51
|
+
|
|
52
|
+
if (ctx.bindingImport === undefined || ctx.bindingName === undefined) {
|
|
53
|
+
// No real binding wired — emit honest skip per task constraint.
|
|
54
|
+
const testFile = `// GENERATED — do not edit by hand
|
|
55
|
+
import { describe, it } from 'vitest';
|
|
56
|
+
|
|
57
|
+
describe('${cap.name}', () => {
|
|
58
|
+
it.skip('invariants under random input (no binding wired)', () => {
|
|
59
|
+
// TODO(harness): no capsule binding import wired by capsule-compile.
|
|
60
|
+
// Add bindingImport + bindingName to the manifest entry to enable.
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
`;
|
|
64
|
+
const benchFile = `// GENERATED — do not edit by hand
|
|
65
|
+
import { bench } from 'vitest';
|
|
66
|
+
|
|
67
|
+
bench('${cap.name}', () => {
|
|
68
|
+
// handler invocation with a canonical fixture
|
|
69
|
+
}, { time: 500 });
|
|
70
|
+
`;
|
|
71
|
+
return { testFile, benchFile };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const testFile = `// GENERATED — do not edit by hand
|
|
75
|
+
import { describe, it } from 'vitest';
|
|
76
|
+
import * as fc from 'fast-check';
|
|
77
|
+
import { ${ctx.bindingName} } from '${ctx.bindingImport}';
|
|
78
|
+
import { schemaToArbitrary, UnsupportedSchemaError } from '${arbitraryImport}';
|
|
79
|
+
|
|
80
|
+
describe('${cap.name}', () => {
|
|
81
|
+
const cap = ${ctx.bindingName};
|
|
82
|
+
let arb: fc.Arbitrary<unknown>;
|
|
83
|
+
let arbError: unknown;
|
|
84
|
+
try {
|
|
85
|
+
arb = schemaToArbitrary(cap.input as never) as fc.Arbitrary<unknown>;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
arbError = err;
|
|
88
|
+
}
|
|
89
|
+
if (cap.run === undefined || arbError !== undefined) {
|
|
90
|
+
it.skip(
|
|
91
|
+
arbError instanceof UnsupportedSchemaError
|
|
92
|
+
? \`invariants — input schema not arbitrary-derivable (\${arbError.message})\`
|
|
93
|
+
: 'invariants — capsule has no run handler',
|
|
94
|
+
() => {},
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
for (const inv of cap.invariants) {
|
|
98
|
+
it(\`invariant: \${inv.name}\`, () => {
|
|
99
|
+
fc.assert(
|
|
100
|
+
fc.property(arb, (input) => {
|
|
101
|
+
const output = cap.run!(input as never);
|
|
102
|
+
return inv.check(input as never, output as never);
|
|
103
|
+
}),
|
|
104
|
+
{ numRuns: 100 },
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
const benchFile = `// GENERATED — do not edit by hand
|
|
113
|
+
import { bench } from 'vitest';
|
|
114
|
+
|
|
115
|
+
bench('${cap.name}', () => {
|
|
116
|
+
// handler invocation with a canonical fixture
|
|
117
|
+
}, { time: 500 });
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
return { testFile, benchFile };
|
|
121
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness template for the `receiptedMutation` assembly arm.
|
|
3
|
+
*
|
|
4
|
+
* Receipted mutations have side effects, so the harness can't drive them
|
|
5
|
+
* with random inputs and assert generic invariants. Each test case is
|
|
6
|
+
* emitted as `it.skip` with a TODO until the arm acquires a typed
|
|
7
|
+
* runtime invocation channel.
|
|
8
|
+
*
|
|
9
|
+
* Per memory: "no vanity tests" — emitting a `() => true` placeholder
|
|
10
|
+
* pretending to verify behavior is worse than skipping honestly.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { CapsuleDef } from '../assembly.js';
|
|
16
|
+
import type { HarnessOutput } from './pure-transform.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate the test + bench file contents for a `receiptedMutation` capsule.
|
|
20
|
+
* Emits `it.skip` placeholders covering contract shape, idempotency, audit
|
|
21
|
+
* receipt, and fault reachability — each carries a TODO naming the
|
|
22
|
+
* invocation channel it would need.
|
|
23
|
+
*/
|
|
24
|
+
export function generateReceiptedMutation(
|
|
25
|
+
cap: CapsuleDef<'receiptedMutation', unknown, unknown, unknown>,
|
|
26
|
+
): HarnessOutput {
|
|
27
|
+
const testFile = `// GENERATED — do not edit by hand
|
|
28
|
+
import { describe, it } from 'vitest';
|
|
29
|
+
|
|
30
|
+
describe('${cap.name}', () => {
|
|
31
|
+
it.skip('contract shape: input and output decode/encode round-trip', () => {
|
|
32
|
+
// TODO(harness): wire schema round-trip via cap.input / cap.output.
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.skip('is idempotent: two identical inputs produce equivalent receipts', () => {
|
|
36
|
+
// TODO(harness): receipted mutations need a runtime channel to invoke
|
|
37
|
+
// — until cap exposes a typed mutate handler, skip rather than fake.
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it.skip('emits audit receipt with declared capabilities', () => {
|
|
41
|
+
// TODO(harness): same — needs runtime channel to read emitted receipts.
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it.skip('fault injection: declared faults are reachable', () => {
|
|
45
|
+
// TODO(harness): faults table not yet on the capsule contract.
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const benchFile = `// GENERATED — do not edit by hand
|
|
51
|
+
import { bench } from 'vitest';
|
|
52
|
+
|
|
53
|
+
bench('${cap.name}', () => {
|
|
54
|
+
// mutation invocation with a canonical fixture
|
|
55
|
+
}, { time: 500 });
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
return { testFile, benchFile };
|
|
59
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness template for the `sceneComposition` assembly arm.
|
|
3
|
+
*
|
|
4
|
+
* Scene composition tests need a deterministic frame-stream channel
|
|
5
|
+
* (`compileScene` + `renderFrame`) on the capsule contract to drive
|
|
6
|
+
* determinism, sync-accuracy, per-frame budget, and invariant
|
|
7
|
+
* preservation. Without that channel each case is emitted as `it.skip`.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CapsuleDef } from '../assembly.js';
|
|
13
|
+
import type { HarnessOutput } from './pure-transform.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate the test + bench file contents for a `sceneComposition` capsule.
|
|
17
|
+
* Emits `it.skip` placeholders for determinism, sync, budget, and
|
|
18
|
+
* invariant-preservation cases.
|
|
19
|
+
*/
|
|
20
|
+
export function generateSceneComposition(
|
|
21
|
+
cap: CapsuleDef<'sceneComposition', unknown, unknown, unknown>,
|
|
22
|
+
): HarnessOutput {
|
|
23
|
+
const testFile = `// GENERATED — do not edit by hand
|
|
24
|
+
import { describe, it } from 'vitest';
|
|
25
|
+
|
|
26
|
+
describe('${cap.name}', () => {
|
|
27
|
+
it.skip('determinism: identical seed produces identical frame stream across 3 runs', () => {
|
|
28
|
+
// TODO(harness): needs cap.compile + cap.renderFrame on the contract.
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.skip('sync accuracy: audio and video frame timestamps align within +/- 1ms', () => {
|
|
32
|
+
// TODO(harness): same — needs typed frame stream.
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.skip('per-frame budget: p95 frame time below declared budget (${cap.budgets.p95Ms ?? 'n/a'}ms)', () => {
|
|
36
|
+
// TODO(harness): needs cap.renderFrame to time individual frames.
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it.skip('invariant preservation: every declared scene invariant holds across playback', () => {
|
|
40
|
+
// TODO(harness): same — needs frame walker.
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const benchFile = `// GENERATED — do not edit by hand
|
|
46
|
+
import { bench } from 'vitest';
|
|
47
|
+
|
|
48
|
+
bench('${cap.name} — full playback', () => {
|
|
49
|
+
// render full scene duration, measure total wall-clock
|
|
50
|
+
}, { time: 2000 });
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
return { testFile, benchFile };
|
|
54
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness template for the `siteAdapter` assembly arm.
|
|
3
|
+
*
|
|
4
|
+
* Site adapters convert between native host objects and czap representations.
|
|
5
|
+
* Without typed `toCzap` / `fromCzap` channels on the capsule contract the
|
|
6
|
+
* harness can't drive round-trip equality, so each case is emitted as
|
|
7
|
+
* `it.skip` rather than a vacuous placeholder.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CapsuleDef } from '../assembly.js';
|
|
13
|
+
import type { HarnessOutput } from './pure-transform.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate the test + bench file contents for a `siteAdapter` capsule.
|
|
17
|
+
* Emits `it.skip` placeholders for round-trip and host-capability tests.
|
|
18
|
+
*/
|
|
19
|
+
export function generateSiteAdapter(cap: CapsuleDef<'siteAdapter', unknown, unknown, unknown>): HarnessOutput {
|
|
20
|
+
const testFile = `// GENERATED — do not edit by hand
|
|
21
|
+
import { describe, it } from 'vitest';
|
|
22
|
+
|
|
23
|
+
describe('${cap.name}', () => {
|
|
24
|
+
it.skip('round-trip equality: native -> czap -> native preserves structure', () => {
|
|
25
|
+
// TODO(harness): needs cap.toCzap / cap.fromCzap on the contract.
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it.skip('host capability matrix: each declared site supports the adapter', () => {
|
|
29
|
+
// TODO(harness): needs per-site dispatcher to invoke under each runtime.
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const benchFile = `// GENERATED — do not edit by hand
|
|
35
|
+
import { bench } from 'vitest';
|
|
36
|
+
|
|
37
|
+
bench('${cap.name}', () => {
|
|
38
|
+
// adapter call with a canonical native fixture
|
|
39
|
+
}, { time: 500 });
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
return { testFile, benchFile };
|
|
43
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness template for the `stateMachine` assembly arm.
|
|
3
|
+
*
|
|
4
|
+
* State machines need a typed `step(state, event)` channel to drive
|
|
5
|
+
* randomized event sequences and check invariants at every step. The
|
|
6
|
+
* capsule contract doesn't yet expose one, so each case is emitted as
|
|
7
|
+
* `it.skip` rather than a vacuous `() => true` placeholder.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CapsuleDef } from '../assembly.js';
|
|
13
|
+
import type { HarnessOutput } from './pure-transform.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate the test + bench file contents for a `stateMachine` capsule.
|
|
17
|
+
* Emits `it.skip` placeholders covering illegal transitions, replay, and
|
|
18
|
+
* invariant preservation — each carries a TODO naming the missing handler.
|
|
19
|
+
*/
|
|
20
|
+
export function generateStateMachine(cap: CapsuleDef<'stateMachine', unknown, unknown, unknown>): HarnessOutput {
|
|
21
|
+
const testFile = `// GENERATED — do not edit by hand
|
|
22
|
+
import { describe, it } from 'vitest';
|
|
23
|
+
|
|
24
|
+
describe('${cap.name}', () => {
|
|
25
|
+
it.skip('rejects every illegal transition', () => {
|
|
26
|
+
// TODO(harness): needs cap.transitions table + cap.step handler.
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it.skip('replays deterministically from an event log', () => {
|
|
30
|
+
// TODO(harness): needs cap.step + cap.initialState.
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.skip('invariant holds across random event paths', () => {
|
|
34
|
+
// TODO(harness): same — schemaToArbitrary on cap.input would feed
|
|
35
|
+
// events, but invariants need (state, event) → state to be checkable.
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const benchFile = `// GENERATED — do not edit by hand
|
|
41
|
+
import { bench } from 'vitest';
|
|
42
|
+
|
|
43
|
+
bench('${cap.name}', () => {
|
|
44
|
+
// state-machine step with a canonical event
|
|
45
|
+
}, { time: 500 });
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
return { testFile, benchFile };
|
|
49
|
+
}
|
package/src/hlc.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HLC -- Hybrid Logical Clock.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions + Effect-based managed clock.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Effect } from 'effect';
|
|
10
|
+
import { Ref } from 'effect';
|
|
11
|
+
|
|
12
|
+
// Hybrid Logical Clock: physical wall-clock + logical counter for causal ordering. DagPosition encodes (timestamp, counter, nodeId) for DAG vertex identity.
|
|
13
|
+
|
|
14
|
+
interface HLCShape {
|
|
15
|
+
readonly wall_ms: number;
|
|
16
|
+
readonly counter: number;
|
|
17
|
+
readonly node_id: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a new HLC timestamp initialized to zero for the given node.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const hlc = HLC.create('node-1');
|
|
26
|
+
* // hlc === { wall_ms: 0, counter: 0, node_id: 'node-1' }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
const _create = (nodeId: string): HLCShape => ({
|
|
30
|
+
wall_ms: 0,
|
|
31
|
+
counter: 0,
|
|
32
|
+
node_id: nodeId,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compare two HLC timestamps. Returns -1, 0, or 1.
|
|
37
|
+
*
|
|
38
|
+
* Compares wall_ms first, then counter, then node_id lexicographically.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* const a = HLC.create('node-1');
|
|
43
|
+
* const b = HLC.increment(a, 1000);
|
|
44
|
+
* const cmp = HLC.compare(a, b);
|
|
45
|
+
* // cmp === -1 (a is before b)
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export const compare = (a: HLCShape, b: HLCShape): -1 | 0 | 1 => {
|
|
49
|
+
if (a.wall_ms < b.wall_ms) return -1;
|
|
50
|
+
if (a.wall_ms > b.wall_ms) return 1;
|
|
51
|
+
if (a.counter < b.counter) return -1;
|
|
52
|
+
if (a.counter > b.counter) return 1;
|
|
53
|
+
if (a.node_id < b.node_id) return -1;
|
|
54
|
+
if (a.node_id > b.node_id) return 1;
|
|
55
|
+
return 0;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// 16-bit counter — supports 65535 events per ms before overflow
|
|
59
|
+
const MAX_COUNTER = 0xffff;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Increment an HLC for a local event.
|
|
63
|
+
*
|
|
64
|
+
* Advances wall_ms to max(current, now) and bumps the counter if the wall
|
|
65
|
+
* time didn't advance. Throws on counter overflow (`> 0xFFFF`).
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* const hlc0 = HLC.create('node-1');
|
|
70
|
+
* const hlc1 = HLC.increment(hlc0, Date.now());
|
|
71
|
+
* // hlc1.wall_ms >= hlc0.wall_ms
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
const _increment = (hlc: HLCShape, now: number = 0): HLCShape => {
|
|
75
|
+
const newWallMs = Math.max(hlc.wall_ms, now);
|
|
76
|
+
if (newWallMs === hlc.wall_ms) {
|
|
77
|
+
const next = hlc.counter + 1;
|
|
78
|
+
if (next > MAX_COUNTER)
|
|
79
|
+
throw new Error(
|
|
80
|
+
`HLC counter overflow: exceeded ${MAX_COUNTER} (>65535 events in 1ms — consider batching or increasing clock resolution)`,
|
|
81
|
+
);
|
|
82
|
+
return { wall_ms: newWallMs, counter: next, node_id: hlc.node_id };
|
|
83
|
+
}
|
|
84
|
+
return { wall_ms: newWallMs, counter: 0, node_id: hlc.node_id };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Merge a local HLC with a remote HLC on message receive.
|
|
89
|
+
*
|
|
90
|
+
* Takes the max of local, remote, and now for wall_ms, then adjusts the
|
|
91
|
+
* counter accordingly. Preserves the local node_id.
|
|
92
|
+
*
|
|
93
|
+
* Lamport causality: if wall clocks agree, increment the higher counter to preserve
|
|
94
|
+
* happened-before ordering. Reset counter only when wall clock advances (new causal epoch).
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* const local = HLC.increment(HLC.create('A'), 1000);
|
|
99
|
+
* const remote = HLC.increment(HLC.create('B'), 2000);
|
|
100
|
+
* const merged = HLC.merge(local, remote, 1500);
|
|
101
|
+
* // merged.wall_ms === 2000, merged.node_id === 'A'
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
const _merge = (local: HLCShape, remote: HLCShape, now: number = 0): HLCShape => {
|
|
105
|
+
const newWallMs = Math.max(local.wall_ms, remote.wall_ms, now);
|
|
106
|
+
let newCounter: number;
|
|
107
|
+
if (newWallMs === local.wall_ms && newWallMs === remote.wall_ms) {
|
|
108
|
+
newCounter = Math.max(local.counter, remote.counter) + 1;
|
|
109
|
+
} else if (newWallMs === local.wall_ms) {
|
|
110
|
+
newCounter = local.counter + 1;
|
|
111
|
+
} else if (newWallMs === remote.wall_ms) {
|
|
112
|
+
newCounter = remote.counter + 1;
|
|
113
|
+
} else {
|
|
114
|
+
newCounter = 0;
|
|
115
|
+
}
|
|
116
|
+
if (newCounter > MAX_COUNTER)
|
|
117
|
+
throw new Error(
|
|
118
|
+
`HLC counter overflow: exceeded ${MAX_COUNTER} (>65535 events in 1ms — consider batching or increasing clock resolution)`,
|
|
119
|
+
);
|
|
120
|
+
return { wall_ms: newWallMs, counter: newCounter, node_id: local.node_id };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Encode an HLC timestamp to a colon-separated hex string.
|
|
125
|
+
*
|
|
126
|
+
* Format: `{wall_ms_hex_12}:{counter_hex_4}:{node_id}`
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* const hlc = HLC.increment(HLC.create('node-1'), 1000);
|
|
131
|
+
* const encoded = HLC.encode(hlc);
|
|
132
|
+
* // encoded === '0000000003e8:0000:node-1'
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
const _encode = (hlc: HLCShape): string => {
|
|
136
|
+
// 12 hex digits = 48-bit wall clock (good to year 10889), 4 hex = 16-bit counter
|
|
137
|
+
const wallHex = hlc.wall_ms.toString(16).padStart(12, '0');
|
|
138
|
+
const counterHex = hlc.counter.toString(16).padStart(4, '0');
|
|
139
|
+
return `${wallHex}:${counterHex}:${hlc.node_id}`;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Decode an HLC timestamp from a colon-separated hex string.
|
|
144
|
+
*
|
|
145
|
+
* Inverse of `encode`. Supports node IDs containing colons.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const hlc = HLC.decode('0000000003e8:0000:node-1');
|
|
150
|
+
* // hlc === { wall_ms: 1000, counter: 0, node_id: 'node-1' }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
const _decode = (s: string): HLCShape => {
|
|
154
|
+
const parts = s.split(':');
|
|
155
|
+
if (parts.length < 3) throw new Error(`Invalid HLC format: expected at least 3 colon-separated parts, got "${s}"`);
|
|
156
|
+
const wall_ms = parseInt(parts[0]!, 16);
|
|
157
|
+
const counter = parseInt(parts[1]!, 16);
|
|
158
|
+
const node_id = parts.slice(2).join(':');
|
|
159
|
+
if (isNaN(wall_ms)) throw new Error(`Invalid HLC format: wall_ms is not valid hex in "${s}"`);
|
|
160
|
+
if (isNaN(counter)) throw new Error(`Invalid HLC format: counter is not valid hex in "${s}"`);
|
|
161
|
+
return { wall_ms, counter, node_id };
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create a managed HLC clock as an Effect Ref.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* import { Effect } from 'effect';
|
|
170
|
+
*
|
|
171
|
+
* const program = Effect.gen(function* () {
|
|
172
|
+
* const clock = yield* HLC.makeClock('node-1');
|
|
173
|
+
* const ts = yield* HLC.tick(clock);
|
|
174
|
+
* // ts.wall_ms === Date.now() (approximately)
|
|
175
|
+
* });
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
export const makeClock = (nodeId: string): Effect.Effect<Ref.Ref<HLCShape>> => Ref.make(_create(nodeId));
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Tick a managed clock forward, returning the new HLC timestamp.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* const ts = yield* HLC.tick(clock);
|
|
186
|
+
* // ts.wall_ms >= previous wall_ms
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export const tick = (clock: Ref.Ref<HLCShape>): Effect.Effect<HLCShape> =>
|
|
190
|
+
Ref.updateAndGet(clock, (current) => _increment(current, Date.now()));
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Receive a remote HLC timestamp and merge it into the managed clock.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```ts
|
|
197
|
+
* const remoteTs = HLC.decode(remoteEncoded);
|
|
198
|
+
* const merged = yield* HLC.receive(clock, remoteTs);
|
|
199
|
+
* // merged.wall_ms >= remoteTs.wall_ms
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export const receive = (clock: Ref.Ref<HLCShape>, remote: HLCShape): Effect.Effect<HLCShape> =>
|
|
203
|
+
Ref.updateAndGet(clock, (current) => _merge(current, remote, Date.now()));
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* HLC namespace -- Hybrid Logical Clock.
|
|
207
|
+
*
|
|
208
|
+
* Pure functions for creating, comparing, incrementing, and merging HLC
|
|
209
|
+
* timestamps, plus Effect-based managed clock helpers. Encodes to/from
|
|
210
|
+
* a deterministic colon-separated hex string format.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* import { HLC } from '@czap/core';
|
|
215
|
+
*
|
|
216
|
+
* const a = HLC.increment(HLC.create('A'), Date.now());
|
|
217
|
+
* const b = HLC.increment(HLC.create('B'), Date.now());
|
|
218
|
+
* const merged = HLC.merge(a, b, Date.now());
|
|
219
|
+
* const encoded = HLC.encode(merged);
|
|
220
|
+
* const decoded = HLC.decode(encoded);
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
export const HLC = {
|
|
224
|
+
create: _create,
|
|
225
|
+
compare,
|
|
226
|
+
increment: _increment,
|
|
227
|
+
merge: _merge,
|
|
228
|
+
encode: _encode,
|
|
229
|
+
decode: _decode,
|
|
230
|
+
makeClock,
|
|
231
|
+
tick,
|
|
232
|
+
receive,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export declare namespace HLC {
|
|
236
|
+
/** Structural shape of a hybrid logical clock timestamp: `{ wall_ms, counter, node_id }`. */
|
|
237
|
+
export type Shape = HLCShape;
|
|
238
|
+
}
|