Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
74fb05e
chore: cleaned up
trancee Sep 1, 2026
0987840
feat(plan): chart wayfinder map for Kompact serialization framework
trancee Sep 1, 2026
40140bc
feat(plan): resolve v1 type set, seed framing frontier ticket
trancee Sep 1, 2026
d2c9b58
feat(plan): resolve framing, seed validation frontier
trancee Sep 1, 2026
6879941
feat(plan): resolve validation model, seed write/builder ticket
trancee Sep 1, 2026
f309d7f
feat(plan): resolve write/builder interface, seed runtime error-model…
trancee Sep 1, 2026
a46577b
feat(plan): resolve runtime error model, seed versioning ticket
trancee Sep 1, 2026
88d16a1
feat(plan): resolve versioning model, seed testing-model ticket
trancee Sep 1, 2026
36da395
feat(plan): resolve testing model, seed performance-evidence ticket
trancee Sep 1, 2026
a75037c
feat(plan): resolve testing model, fold perf-evidence (verified), see…
trancee Sep 2, 2026
83418be
feat(plan): resolve module split & publication, lock destination spec
trancee Sep 2, 2026
177f220
docs: performance evidence plan
trancee Sep 2, 2026
1f44876
docs: resolve KMP/KSP publication wiring (Ticket 13)
trancee Sep 2, 2026
39cb5c5
fix(diataxis-pr-docs): use expression-based model for OpenRouter pool…
trancee Sep 2, 2026
d0a7513
feat(impl): implement Kompact serialization framework per locked map
trancee Sep 2, 2026
1349083
docs: add README, how-to, reference, explanation per diataxis
trancee Sep 3, 2026
72946a7
fix(workflow): repair diataxis-pr-docs engine.model expression
trancee Sep 3, 2026
18dc77c
ci: trigger Diátaxis PR Docs Auditor with COPILOT_GITHUB_TOKEN config…
trancee Sep 3, 2026
57c3204
ci: remove diataxis-pr-docs agentic workflow
trancee Sep 3, 2026
b71460a
ci: remove diataxis-pr-docs agentic workflow
trancee Sep 3, 2026
08bbb14
Merge branch 'feat/minimax' of https://github.com/trancee/kompact int…
trancee Sep 3, 2026
0bde165
docs: remove implementation-ticket refs, switch diagrams to ASCII
trancee Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Gradle
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

# IntelliJ / Android Studio
.idea/
*.iml
*.iws
*.ipr
out/

# KSP / KMP generated
**/build/generated/
.kotlin/

# Local config
local.properties
.kotlin/
23 changes: 23 additions & 0 deletions .scratch/kompact-spec/issues/01-wire-format-bit-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
Type: research
Status: resolved
Labels: wayfinder:research
Blocked by: —
Findings: ../research/bit-order.md
---

## Question

`PROMPT.md` requires a bit-packed, zero-padding, sequential stream in which multi-bit values may cross byte boundaries, with identical read/write behavior on Android/JVM and Kotlin/Native iOS using `shl` / `shr` / `and` / `or` over common Kotlin `Byte` boundaries.

What is the canonical bit-ordering convention for comparable zero-copy bit-serial formats, how is a multi-bit integer that crosses a byte boundary assembled (which byte's bits are the low bits vs the high bits), and what is the idiomatic multiplatform Kotlin implementation? The answer locks the one decision without which `KompactRuntime.readBits` / `writeBits` cannot be tested for cross-platform equivalence.

## Answer

**Decision: LSB-first (little-endian) bit packing.** Multi-bit integers assemble least-significant-bit first: byte 0 holds the field's low bits (bits 0–7), byte 1 holds bits 8–15, and bit 0 is the LSB of the value. A cross-boundary read such as `readBits(raw, 4, 10)` takes the low 4 bits of byte 0 and the low 6 bits of byte 1.

**Runtime rule:** Kotlin `Byte` is signed, so every byte must be masked with `and 0xFF` (`byte.toInt() and 0xFF`) before `shl` / `or`; that masking makes the `shl` / `shr` / `and` / `or` sequence produce identical results on JVM and Kotlin/Native. Signed fields are sign-extended after assembly (two's complement on the assembled unsigned magnitude).

**Rejected:** MSB-first (ASN.1 PER) — a valid convention but not the dominant one; Cap'n Proto and SLAC both use LSB-first, and little-endian matches the x86/ARM native bit order Kompact targets.

Findings: [../research/bit-order.md](../research/bit-order.md).
22 changes: 22 additions & 0 deletions .scratch/kompact-spec/issues/02-generation-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
Type: research
Status: resolved
Labels: wayfinder:research
Findings: ../research/generation-strategy.md
---

## Question

`PROMPT.md` §2 says schemas are "generated via an annotation processor or compiler plugin"; §3 shows manual-looking getters and says "how the boilerplate will eventually be automated" — i.e. manual-first with a future generator that emits the §3 getter style from `@KompactField` annotations.

Which code-generation approach can produce the Phase 3 common value-class getters from `@KompactField` annotations for a KMP module consumed by Android/JVM and `iosArm64` / `iosSimulatorArm64`, with deterministic output, build-cache reuse, IDE visibility, and incremental processing? Compare KSP (incl. KMP common-generation caveats), Kotlin compiler plugins (K2), and manual — and recommend one, with the caveat that a generator is not required to ship Phase 3 but the chosen strategy must not paint future automation into a corner.

## Answer

**Decision: KSP (Kotlin Symbol Processing), KSP 2.3.9+ on Kotlin 1.9+.** KSP generates complete value-class source files into `commonMain` with deterministic output, incremental processing, and Gradle build-cache reuse. K2 compiler macros are explicitly experimental (opt-in, not production-ready for a multi-target KMP library) and are rejected.

**Critical boundary:** KSP cannot inject into existing source files, so the generator emits *whole* `value class` declarations (the `@KompactField` getters / setters) rather than patching hand-written ones. `PROMPT.md` §3's "manual-looking getters" are therefore the generator's output, deliberately kept human-readable so automation later replaces them 1:1.

**Caveat (see 03):** KSP generates common `expect` source by default; the per-platform `actual` value classes still need documented source-set wiring. Acceptable for the v1 spec but must be explicit.

Findings: [../research/generation-strategy.md](../research/generation-strategy.md).
24 changes: 24 additions & 0 deletions .scratch/kompact-spec/issues/03-value-class-representation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
Type: research
Status: resolved
Labels: wayfinder:research
Findings: ../research/value-class-representation.md
---

## Question

`PROMPT.md` §1 wants multiplatform value classes wrapping a `ByteArray` with zero-allocation reads. The accepted resolution is: generated `expect/actual value class` declarations, with `@JvmInline` on the JVM `actual`.

How must the `expect` / `actual` value-class declarations be shaped so that (a) the common `expect` can omit `@JvmInline` while the JVM `actual` carries it, (b) Kotlin/Native represents the view class soundly (boxed where unavoidable, unboxed at direct concrete call sites), and (c) the verified zero-allocation call shape is: a non-null view held in a local of its concrete generated type, over a caller-owned `ByteArray`, with a direct scalar `val` read returning a primitive — and no generic, interface, nullable, reflection, collection, or bridge boundary inside the measured read? Name the exact boundaries where boxing is unavoidable so the spec can forbid them on the hot path.

## Answer

**Decision: `expect` in commonMain + `@JvmInline actual` per platform.** The common declaration is `expect value class Foo(val raw: ByteArray)` with **no** `@JvmInline` — the annotation is JVM-stdlib-only and errors on non-JVM targets. Each platform source set carries `actual value class`: `@JvmInline actual value class Foo` in `jvmMain`; plain `actual value class Foo` in `iosArm64Main` and `iosSimulatorArm64Main`.

**Cross-platform behavior:** Kotlin/Native renders value classes as Swift-value structs; wrapper allocation (boxing) occurs only at type-erasure boundaries — generics, nullable (`Foo?`), interface/`Any`-typed parameters, and ABI-crossing returns. On the JVM, `@JvmInline` is what unboxes at direct call sites.

**Hot-path guardrail:** the zero-allocation read contract covers only direct, non-nullable, concrete-typed scalar reads over a caller-owned `ByteArray`. The spec must forbid generic / interface / nullable / `Any`-typed usage on the measured path.

This reconciles the `PROMPT.md` §1 prohibition (hand-written common API, no `@JvmInline`) with the JVM value-class contract (generated JVM `actual` carries `@JvmInline`) and rides on the [generation-strategy](02-generation-strategy.md) decision.

Findings: [../research/value-class-representation.md](../research/value-class-representation.md).
37 changes: 37 additions & 0 deletions .scratch/kompact-spec/issues/04-v1-type-set.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
Type: grilling
Status: resolved
Labels: wayfinder:grilling
Blocked by: —
---

## Question

`PROMPT.md`'s example fields are Enum (4 bits), Int (10 bits), and Boolean (1 bit). That is one sketch, not a type set. What is the complete scalar + composite type set the v1 wire format and `KompactRuntime` must support?

Specifically decide:
- Signed vs unsigned integers at which bit widths (the performance workload matrix names 1–64, signed 2/7/10/32/64, unsigned 1/5/8/10/16/32/64).
- Enum encoding: dense ordinal (the `0–15` sketch) vs explicit codes; gapped / unknown-code handling.
- Floats: 32- and 64-bit IEEE-754 with canonicalized NaN; in scope or deferred?
- Variable-length / strings / blobs: varint + length prefix, or fixed-width only?
- Nested composites (a field that is itself a bit-packed struct) and repeated fields: one layout, or offset/delimited?
- If variable-length is included, the bit-width of the field-length / envelope metadata.

This decision gates `KompactRuntime.readBits` / `writeBits` overloads, the `@KompactField` annotation surface, and the cross-platform test matrix's width coverage. Resolve before the validation, write/builder, or error-model tickets.

## Answer

**Decision (user-resolved): the full v1 type set, including variable-length.** Per the live exchange, Kompact v1 supports:

- **Unsigned integers** at declared bit widths 1–64.
- **Signed integers** at declared bit widths 1–64 (two's-complement on the assembled magnitude).
- **Booleans** — 1 bit.
- **Enums** — dense ordinal at a declared 1–8-bit width; an unknown code yields a typed error result (fail closed), not a silent default.
- **Floats** — IEEE-754 32-bit and 64-bit, with NaN canonicalized to a single canonical bit pattern.
- **Variable-length values** — strings and blobs, length-framed (NOT deferred).
- **Nested composites** — a bit-packed struct used as a field (NOT deferred).
- **Repeated fields** — ordered sequences (NOT deferred).

**Scope implication:** this is a deliberate expansion beyond `PROMPT.md`'s 2-byte `VehicleTelemetry` sketch (Enum + Int + Boolean only). v1 now requires an **envelope / framing contract** for length-prefixed, nested, and repeated fields — the substance of ticket 05. `readBits` / `writeBits` widen accordingly (length-prefix + nested base-offset + count handling); the `@KompactField` surface gains length / nesting / repeat annotations.

**Risk note:** v1 is now substantially larger than the PROMPT sketch. The framing (05), write/builder (fog), validation (fog), error model (fog), and versioning (fog) tickets must lock before implementation; each adds surface. The inclusion of variable-length / nested / repeated is intentional — flag if v1 should be trimmed back to the fixed-width sketch instead.
28 changes: 28 additions & 0 deletions .scratch/kompact-spec/issues/05-variable-length-framing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
Type: grilling
Status: resolved
Labels: wayfinder:grilling
Blocked by: 04-v1-type-set (resolved)
---

## Question

Ticket 04 committed v1 to **variable-length strings/blobs, nested composites, and repeated fields** — beyond `PROMPT.md`'s fixed 2-byte sketch. How must Kompact frame these in the bit-packed stream? Three coupled choices:

1. **Variable-length length-prefix**: varint vs fixed 1/2/4-byte vs per-field-declared.
2. **Nested layout**: self-delimiting length-delimited sub-region vs relative/absolute bit-offset.
3. **Repeated fields**: count-prefixed vs length-delimited.

This decides the envelope / framing contract that `KompactRuntime` and the generated getters must implement; it gates the write/builder interface, validation, error model, and versioning tickets. Resolve before any non-fixed-width runtime code is written.

## Answer

**Decision (user-resolved): sequential, length-delimited framing on the LSB-first bit stream (ticket 01's order).**

1. **Length-prefix — fixed-width little-endian, width declared per-field.** Variable-length fields carry an 8- or 16-bit LE byte-count (per-field, via an annotation). Varint is rejected: its loop-based decode and variable CPU conflict with the zero-allocation / predictable-read ethos, and Kompact's reader is fixed-width single-pass.
2. **Nested composites — length-delimited sub-regions, parse-forward.** A nested struct carries its total bit-length; reading it consumes those bits and siblings are reached by continuing to scan.
3. **Repeated fields — count-prefixed, sequential.** One fixed-width count, then `N` elements in order.

**Key tradeoff accepted (recorded so the spec does not over-promise):** reads are **sequential (parse-forward), not random-access.** This deliberately diverges from FlatBuffers-style offset-jump reads, because ticket 04's variable-length fields make stored offsets shift and break. `PROMPT.md`'s "zero-copy reads like FlatBuffers" is satisfied by the *view-class read path* (no allocation/copy to read a scalar), **not** by random access to every field; the cost of variable-length framing is sequential traversal.

This framing contract gates `KompactRuntime`'s length-prefix + nested-length + count helpers and the generator's envelope layout.
72 changes: 72 additions & 0 deletions .scratch/kompact-spec/issues/06-validation-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
Type: grilling
Status: resolved
Labels:
- scope:runtime
- scope:codegen
- kind:validation
- kind:error-model
Blocked by:
- "02 generation strategy"
- "04 v1 type set"
- "05 sequential framing"
Decides:
- "07 write/builder interface"
- "08 runtime error model"
---

# Ticket 06 — Validation model

## Question

Where does field-layout validation live in Kompact, and what fails (and how) when the schema or the wire input is malformed? Informed by tickets 02 (generation), 03 (zero-alloc value-class reads), 04 (type set), and 05 (framing).

Three sub-questions:
1. **Split:** compile-time (KSP) structural checks + runtime bounds checks? or one or the other?
2. **Compile-time failure mode:** what happens when `KompactProcessor` sees a violating schema?
3. **Runtime hot-path failure mode:** what happens when `KompactRuntime` reads a short / out-of-bounds buffer?

## Answer

User decided: adopt the recommended option on all three forks.

**1. Split — compile-time (KSP) structural + runtime typed-result bounds.**
- **`KompactProcessor` (compile-time)** validates structural/layout invariants during the KSP symbol-processing pass, before any value-class is generated. It builds an in-memory layout model of every `@Kompact`-annotated struct to compute bit offsets and then checks invariants (matrix below). These can never be checked at runtime, because they describe the *schema*, not the *buffer*.
- **`KompactRuntime` (runtime)** performs *only defensive buffer-bounds checks* on the read path — the invariants that are genuinely unknowable at compile time because they depend on the concrete `ByteArray` contents.

This is the FlatBuffers model (validate the layout at build) paired with the Protobuf model (return a `Result`/typed error at decode). It is the only split consistent with every prior decision: KSP generation (02) makes compile-time validation possible; the zero-alloc value-class read contract (03) forbids throwing on the hot path; the type set (04) and framing (05) define exactly which invariants are structural versus buffer-bound.

**Invariant matrix**

| Invariant | Checked | Where | Error type (if reached) |
|---|---|---|---|
| Bit-offset overlaps within a struct | compile-time | `KompactProcessor` layout pass | hard error (build fails) |
| Per-struct bit-width sum ≤ declared width | compile-time | `KompactProcessor` layout pass | hard error |
| Length-prefix field width ∈ {8,16,32} | compile-time | `KompactProcessor` | hard error |
| Nested total-length ≤ declared length field capacity | compile-time | `KompactProcessor` layout pass (static bound) | hard error |
| Repeated element layout uniformity + count width ∈ {8,16,32} | compile-time | `KompactProcessor` layout pass | hard error |
| Enum width ≥ ordinal bit-width; declared codes fit | compile-time | `KompactProcessor` | hard error |
| Short buffer / read past end | runtime | `KompactRuntime` | `BoundsError` |
| Length-prefix > remaining bytes in region | runtime | `KompactRuntime` | `BadLengthPrefix` |
| Nested declared length < actual nested payload | runtime | `KompactRuntime` | `TruncatedNested` |
| Enum wire code outside known ordinals | runtime | `KompactRuntime` | `UnknownEnumCode` |

**2. Compile-time failure mode — hard error, symbol-located, halt processing.**
Violations are reported via `KSPLogger.error(message, element)` attached to the offending `@KompactField`-annotated property (element = the KSP `KSDeclaration`/`KSPropertyDeclaration`), so the diagnostic points at the *declaration*, not an opaque offset. The processor returns a sentinel result from its round and halts generation for the offending symbol — the Gradle build fails until the schema is fixed. No warnings-as-proceed, because a "proceed" codegen would silently emit a structurally invalid reader the compiler could not otherwise catch. This matches ticket 02's KSP-diagnostic discipline (deterministic, symbol-located).

**3. Runtime hot-path failure mode — typed result, never throw.**
`readBits` / `readBitsBoolean` return `KompactDecodeResult<T>` — a value class over `(success: Boolean, value: T?, error: KompactDecodeError?)`, carrying either `success(value)` or `failure(error)`. The direct concrete read path (the one the 03 contract protects as zero-allocation) **never throws**: an out-of-bounds read is a value, not an exception. Throws allocate (stack trace capture) and would violate the zero-alloc read contract established in ticket 03. Optional checked wrappers (`readBitsOrThrow`) are provided for callers who prefer exceptions, but the direct view-class read API does not.

**Tradeoff accepted.** Compile-time validation shifts all structural error detection to build time (better DX, fail-fast on the device developer) at the cost of `KompactProcessor` complexity — a dedicated `LayoutModel` validation pass that fully models the bit layout before emission. Runtime keeps only the buffer-bounds checks that are genuinely impossible to compute at compile time, and carries them as typed results (satisfying the 03 zero-allocation read contract).

**Consequences.**
- 08 runtime error model: ticket 06 resolves the headline fork — runtime failures are **typed results, not throws** — and fixes the set of runtime error types above. The *representation* of `KompactDecodeResult` and its composition (propagation, wrapping, error-detail fields) remain fog for ticket 08, informed by 06.
- 07 write/builder interface: now constrained — the writer's output must be structurally valid per the compile-time rules, so the writer *cannot produce* an overlapping-offset or width-overflow stream; the reader only bounds-checks. This removes whole classes of write-side bugs.
- 09 versioning & schema evolution: a length-prefix that exceeds remaining bytes now yields a typed `BadLengthPrefix` rather than a silent misread (06), so forward-compat on a skewed stream is safe-by-construction.
- `KompactProcessor` MUST implement a `LayoutModel` validation pass that computes every field's `[bitOffset, bitWidth)` and checks invariants 1–5 before emitting any `expect/actual value class`. (No partial emission on a malformed schema.)

## References
- ticket 02 (KSP diagnostics discipline: deterministic, symbol-located)
- ticket 03 (zero-alloc value-class read contract)
- ticket 04 (type set: enum width, length-prefix widths)
- ticket 05 (framing: length-prefix, nested total-length, count-prefixed repeats)
Loading