Skip to content

Commit 6678b31

Browse files
authored
Merge pull request #9 from LessUp/architecture-deepening-20260531
docs(arch): contract inventory, ADRs, metadata module, and golden tests
2 parents 86f6c4c + 74355c3 commit 6678b31

11 files changed

Lines changed: 777 additions & 3 deletions

CONTEXT.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ Go 语言实现的共享工具模块,位于 `algorithms/shared/go/codec/`:
207207
- `openspec/specs/core-architecture/spec.md` - 核心架构规范
208208
- `openspec/specs/encoding-project/spec.md` - 项目需求规范
209209
- `openspec/specs/cross-language-testing/spec.md` - 测试规范
210+
- `docs/architecture/contract-inventory.md` - 架构合同清单(2026-05-31 存档)
211+
- `docs/adr/0001-validation-metadata-module-shape.md` - 验证元数据模块形状决策
212+
- `docs/adr/0002-semantic-error-alignment-across-languages.md` - 跨语言语义错误对齐决策
213+
- `docs/adr/0003-range-coder-corpus-cap-policy.md` - Range Coder 语料库上限策略决策
214+
- `docs/adr/0004-rle-buffered-streaming-stance.md` - RLE 缓冲流式语义决策
215+
- `docs/adr/0005-range-cpp-bench-mode-migration-path.md` - Range C++ bench 模式迁移路径决策
210216

211217
## 文档术语表
212218

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Byte-exact golden tests for the shared Frequency Table wire format.
2+
//
3+
// These tests pin the exact bytes emitted by WriteFrequenciesToBytes / AppendFrequencies
4+
// so that refactors that silently change endianness, field order, or count encoding
5+
// will fail immediately rather than silently producing incompatible output.
6+
//
7+
// Wire format (from contract-inventory.md § Binary format and Frequency Table facts):
8+
// count : uint32 little-endian -- number of frequency entries
9+
// freq[0] : uint32 little-endian
10+
// ...
11+
// freq[n-1] : uint32 little-endian
12+
package codec
13+
14+
import (
15+
"bytes"
16+
"testing"
17+
)
18+
19+
// TestGolden_WriteFrequenciesToBytes_Empty verifies that an empty table encodes
20+
// as exactly 4 zero bytes (count=0 in little-endian).
21+
// Wire: 00 00 00 00
22+
func TestGolden_WriteFrequenciesToBytes_Empty(t *testing.T) {
23+
var out []byte
24+
WriteFrequenciesToBytes(&out, nil)
25+
want := []byte{0x00, 0x00, 0x00, 0x00}
26+
if !bytes.Equal(out, want) {
27+
t.Errorf("empty table: got %x, want %x", out, want)
28+
}
29+
}
30+
31+
// TestGolden_WriteFrequenciesToBytes_SingleEntry verifies count=1 then value=1.
32+
// Wire: 01 00 00 00 01 00 00 00
33+
func TestGolden_WriteFrequenciesToBytes_SingleEntry(t *testing.T) {
34+
var out []byte
35+
WriteFrequenciesToBytes(&out, []uint32{1})
36+
want := []byte{
37+
0x01, 0x00, 0x00, 0x00, // count = 1
38+
0x01, 0x00, 0x00, 0x00, // freq[0] = 1
39+
}
40+
if !bytes.Equal(out, want) {
41+
t.Errorf("single-entry table: got %x, want %x", out, want)
42+
}
43+
}
44+
45+
// TestGolden_WriteFrequenciesToBytes_ThreeEntries verifies count=3 then each value.
46+
// Wire: 03 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00
47+
func TestGolden_WriteFrequenciesToBytes_ThreeEntries(t *testing.T) {
48+
var out []byte
49+
WriteFrequenciesToBytes(&out, []uint32{1, 2, 3})
50+
want := []byte{
51+
0x03, 0x00, 0x00, 0x00, // count = 3
52+
0x01, 0x00, 0x00, 0x00, // freq[0] = 1
53+
0x02, 0x00, 0x00, 0x00, // freq[1] = 2
54+
0x03, 0x00, 0x00, 0x00, // freq[2] = 3
55+
}
56+
if !bytes.Equal(out, want) {
57+
t.Errorf("three-entry table: got %x, want %x", out, want)
58+
}
59+
}
60+
61+
// TestGolden_WriteFrequenciesToBytes_MaxU32 verifies that u32::MAX is preserved.
62+
// Wire: 01 00 00 00 FF FF FF FF
63+
func TestGolden_WriteFrequenciesToBytes_MaxU32(t *testing.T) {
64+
var out []byte
65+
WriteFrequenciesToBytes(&out, []uint32{^uint32(0)})
66+
want := []byte{
67+
0x01, 0x00, 0x00, 0x00, // count = 1
68+
0xFF, 0xFF, 0xFF, 0xFF, // freq[0] = 0xFFFFFFFF
69+
}
70+
if !bytes.Equal(out, want) {
71+
t.Errorf("max u32 entry: got %x, want %x", out, want)
72+
}
73+
}
74+
75+
// TestGolden_WriteFrequenciesToBytes_Count256 verifies multi-byte count encoding.
76+
// count=256 must encode as 00 01 00 00 in little-endian.
77+
func TestGolden_WriteFrequenciesToBytes_Count256(t *testing.T) {
78+
freq := make([]uint32, 256)
79+
var out []byte
80+
WriteFrequenciesToBytes(&out, freq)
81+
wantCountBytes := []byte{0x00, 0x01, 0x00, 0x00}
82+
if !bytes.Equal(out[:4], wantCountBytes) {
83+
t.Errorf("count=256 field: got %x, want %x", out[:4], wantCountBytes)
84+
}
85+
wantLen := 4 + 256*4
86+
if len(out) != wantLen {
87+
t.Errorf("total length: got %d, want %d", len(out), wantLen)
88+
}
89+
}
90+
91+
// TestGolden_AppendFrequencies_PreservesLeadingBytes verifies that AppendFrequencies
92+
// appends to (not replaces) an existing byte slice.
93+
func TestGolden_AppendFrequencies_PreservesLeadingBytes(t *testing.T) {
94+
out := []byte{0xDE, 0xAD}
95+
AppendFrequencies(&out, []uint32{5})
96+
want := []byte{
97+
0xDE, 0xAD, // original prefix
98+
0x01, 0x00, 0x00, 0x00, // count = 1
99+
0x05, 0x00, 0x00, 0x00, // freq[0] = 5
100+
}
101+
if !bytes.Equal(out, want) {
102+
t.Errorf("AppendFrequencies: got %x, want %x", out, want)
103+
}
104+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Byte-exact golden tests for the shared Frequency Table wire format.
2+
//
3+
// These tests pin the exact bytes emitted by write_frequencies so that
4+
// refactors that silently change endianness, field order, or count encoding
5+
// will fail immediately rather than silently producing incompatible output.
6+
//
7+
// Wire format (from contract-inventory.md § Binary format and Frequency Table facts):
8+
// count : uint32 little-endian -- number of frequency entries
9+
// freq[0] : uint32 little-endian
10+
// ...
11+
// freq[n-1] : uint32 little-endian
12+
//
13+
// The exact 257-symbol layout used by Huffman, Arithmetic, and Range algorithms
14+
// is not tested here because it depends on input data; instead we test the
15+
// wire-format primitives with small, hand-computed vectors.
16+
17+
use compresskit_codec::codec::frequency::{read_frequencies_exact, write_frequencies};
18+
19+
// ---------------------------------------------------------------------------
20+
// Golden byte vectors (hand-computed, do not edit without updating comments)
21+
// ---------------------------------------------------------------------------
22+
23+
/// write_frequencies([]) must produce count=0 encoded as 4 little-endian zero bytes.
24+
/// Wire: 00 00 00 00
25+
#[test]
26+
fn golden_write_empty_table() {
27+
let freq: Vec<u32> = vec![];
28+
let mut out = Vec::new();
29+
write_frequencies(&mut out, &freq);
30+
assert_eq!(
31+
out,
32+
vec![0x00, 0x00, 0x00, 0x00],
33+
"empty frequency table must encode as 4-byte LE zero count"
34+
);
35+
}
36+
37+
/// write_frequencies([1]) must produce count=1 then value=1, each as 4 LE bytes.
38+
/// Wire: 01 00 00 00 01 00 00 00
39+
#[test]
40+
fn golden_write_single_entry() {
41+
let freq: Vec<u32> = vec![1];
42+
let mut out = Vec::new();
43+
write_frequencies(&mut out, &freq);
44+
assert_eq!(
45+
out,
46+
vec![
47+
0x01, 0x00, 0x00, 0x00, // count = 1
48+
0x01, 0x00, 0x00, 0x00, // freq[0] = 1
49+
],
50+
"single-entry table wire bytes mismatch"
51+
);
52+
}
53+
54+
/// write_frequencies([1, 2, 3]) must produce count=3 then each value as 4 LE bytes.
55+
/// Wire: 03 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00
56+
#[test]
57+
fn golden_write_three_entries() {
58+
let freq: Vec<u32> = vec![1, 2, 3];
59+
let mut out = Vec::new();
60+
write_frequencies(&mut out, &freq);
61+
assert_eq!(
62+
out,
63+
vec![
64+
0x03, 0x00, 0x00, 0x00, // count = 3
65+
0x01, 0x00, 0x00, 0x00, // freq[0] = 1
66+
0x02, 0x00, 0x00, 0x00, // freq[1] = 2
67+
0x03, 0x00, 0x00, 0x00, // freq[2] = 3
68+
],
69+
"three-entry table wire bytes mismatch"
70+
);
71+
}
72+
73+
/// Max u32 value must be preserved exactly in the wire format.
74+
/// Wire: 01 00 00 00 FF FF FF FF
75+
#[test]
76+
fn golden_write_max_u32_entry() {
77+
let freq: Vec<u32> = vec![u32::MAX];
78+
let mut out = Vec::new();
79+
write_frequencies(&mut out, &freq);
80+
assert_eq!(
81+
out,
82+
vec![
83+
0x01, 0x00, 0x00, 0x00, // count = 1
84+
0xFF, 0xFF, 0xFF, 0xFF, // freq[0] = u32::MAX
85+
],
86+
"max u32 entry wire bytes mismatch"
87+
);
88+
}
89+
90+
/// Multi-byte count value: 256 entries.
91+
/// Wire: 00 01 00 00 (count=256 LE) followed by 256 * 4 bytes.
92+
#[test]
93+
fn golden_write_256_entries_count_encoding() {
94+
let freq: Vec<u32> = vec![0u32; 256];
95+
let mut out = Vec::new();
96+
write_frequencies(&mut out, &freq);
97+
// Only check the first 4 bytes (count field).
98+
assert_eq!(
99+
&out[..4],
100+
&[0x00, 0x01, 0x00, 0x00],
101+
"count=256 must encode as 00 01 00 00 in little-endian"
102+
);
103+
assert_eq!(out.len(), 4 + 256 * 4, "total length must be 4 + 256*4");
104+
}
105+
106+
// ---------------------------------------------------------------------------
107+
// Round-trip tests: write then read_frequencies_exact must recover original data
108+
// ---------------------------------------------------------------------------
109+
110+
#[test]
111+
fn roundtrip_three_entries_via_exact_reader() {
112+
let original: Vec<u32> = vec![7, 11, 13];
113+
let mut out = Vec::new();
114+
write_frequencies(&mut out, &original);
115+
116+
let mut pos = 0;
117+
let recovered = read_frequencies_exact(
118+
&out,
119+
&mut pos,
120+
3,
121+
"truncated count",
122+
"truncated entries",
123+
"invalid count",
124+
)
125+
.expect("read_frequencies_exact must succeed on valid data");
126+
127+
assert_eq!(recovered, original, "round-trip must recover original frequencies");
128+
assert_eq!(pos, out.len(), "reader must consume all bytes");
129+
}
130+
131+
#[test]
132+
fn roundtrip_exact_reader_rejects_count_mismatch() {
133+
let freq: Vec<u32> = vec![1, 2, 3];
134+
let mut out = Vec::new();
135+
write_frequencies(&mut out, &freq);
136+
137+
let mut pos = 0;
138+
let result = read_frequencies_exact(
139+
&out,
140+
&mut pos,
141+
4, // wrong expected count
142+
"truncated count",
143+
"truncated entries",
144+
"invalid count",
145+
);
146+
147+
assert!(
148+
result.is_err(),
149+
"read_frequencies_exact must reject count mismatch"
150+
);
151+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# ADR 0001: Validation metadata module shape
2+
3+
Date: 2026-05-31
4+
Status: Accepted
5+
6+
## Context
7+
8+
Conformance, CLI smoke, test-data generation, and benchmark orchestration scripts each hardcode overlapping facts: algorithm names, binary paths, file extensions, corpus names, Range corpus caps, benchmark dataset choices, language ordering, and report schema fields. This duplication causes drift when adding algorithms, adjusting corpus policy, or changing binary paths.
9+
10+
Current consumers and their metadata shapes are recorded in `docs/architecture/contract-inventory.md` (§ Validation and test metadata consumers, § Benchmark metadata consumers).
11+
12+
## Decision
13+
14+
Introduce a single versioned Python metadata module (`tests/metadata.py` or similar) that exposes:
15+
16+
- Algorithm registry: name, extension, binary paths per language
17+
- Corpus registry: corpus names, sizes, generation parameters, eligibility flags
18+
- Range corpus cap: explicit named constant with documented rationale
19+
- Benchmark job registry: per-algorithm selected input file
20+
- Report schema: field names and ordering for benchmark JSON output
21+
22+
Conformance, smoke, test-data, and benchmark runners become thin adapters over this module. The module is versioned with a `METADATA_VERSION` string; consumers assert the version they were tested against.
23+
24+
## Constraints
25+
26+
- This change does **not** alter algorithm/language matrix, corpus eligibility rules, Range cap value, or benchmark schema: it only consolidates existing facts.
27+
- Any intentional change to Range cap policy, corpus eligibility, or conformance matrix requires an OpenSpec change first (see `docs/architecture/contract-inventory.md` § OpenSpec-triggering decisions).
28+
- Python module must remain compatible with the existing `uv`/`python3` invocation patterns in the Makefile.
29+
30+
## Consequences
31+
32+
**Positive**: Single source of truth for test/bench metadata; adding an algorithm or corpus file requires one edit instead of many; Range cap policy is explicit and findable.
33+
34+
**Negative**: Additional indirection layer; scripts must import the module, requiring Python path management.
35+
36+
**Deferred**: Deciding whether the metadata module is the right place to record Range cap policy permanently, or whether that policy should move into OpenSpec. Record that decision in ADR 0003 before migrating.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# ADR 0002: Semantic error alignment across languages
2+
3+
Date: 2026-05-31
4+
Status: Accepted
5+
6+
## Context
7+
8+
The three language implementations expose the same seven semantic error kinds (`BUF_TOO_SMALL`, `ERR_TRUNCATED`, `ERR_CORRUPT`, `ERR_INVALID_STATE`, `ERR_SIZE_LIMIT`, `ERR_VERSION_UNSUPPORTED`, `ERR_UNKNOWN_ALGO`) but through structurally different APIs:
9+
10+
- **Go**: `ErrorKind` enum + `CodecError` struct + sentinel errors + `errors.Is` mapping (plus `KindIO` for wrapped I/O)
11+
- **Rust**: `CodecError` enum with `Display` + helper mapping from `io::Error`
12+
- **C++**: `StatusCode` inside `Result<T>`; CLI returns boolean success/failure
13+
14+
Some algorithm-level direct APIs still map errors through local messages before the shared codec adapter normalizes them (Range Rust, RLE Rust, Range Go).
15+
16+
The `contract-inventory.md` notes that structural API identity across languages is explicitly **not** the goal.
17+
18+
## Decision
19+
20+
Align at the **semantic** level only:
21+
22+
1. The seven canonical error kinds listed in `CONTEXT.md` are the normative contract. Each language must be able to produce and identify each kind.
23+
2. Language-idiomatic representations (enum, struct, sentinel, Result type) are preserved. No forced structural uniformity.
24+
3. Algorithm-level error messages flowing to CLI output must be English and actionable (OpenSpec requirement). Internal mapping paths may remain language-local provided the final kind is correct.
25+
4. A shared cross-language contract test (see ADR phase-2 deliverables) verifies that encode/decode round-trips produce the correct kind on corruption, truncation, size-limit, and invalid-state inputs.
26+
27+
## Constraints
28+
29+
- Changing the public streaming/buffer API error kind semantics or lifecycle requires an OpenSpec change.
30+
- Go `KindIO` is a Go-local extension, not a cross-language contract; it must not appear in conformance test assertions.
31+
32+
## Consequences
33+
34+
**Positive**: Each language stays idiomatic; shared tests pin semantic equivalence; algorithm-level mapping quirks are explicitly tolerated while remaining testable.
35+
36+
**Negative**: Developers must understand that identical code structure is not the goal; code review must check semantic equivalence rather than structural similarity.
37+
38+
**Deferred**: Whether `KindIO` should be formally excluded from the CONTEXT vocabulary, or documented as Go-only, is a minor cleanup deferred to documentation alignment (Todo 8 / ADR 0002 amendment).
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# ADR 0003: Range Coder corpus cap policy
2+
3+
Date: 2026-05-31
4+
Status: Accepted
5+
6+
## Context
7+
8+
The Range Coder has a known large-file decode performance problem. The conformance matrix currently skips corpus files over **100 KiB** with reason code `range_coder_corpus_cap_100_kib`. This cap is hardcoded in `tests/conformance/run_decode_matrix.py` and documented in:
9+
10+
- `tests/conformance/README.md` (workaround note)
11+
- `docs/en/algorithms/range.md` (known issue section)
12+
- `algorithms/range/benchmark/bench.py` (benchmark default input note)
13+
14+
The cap value and its rationale have never been formally recorded as a policy decision.
15+
16+
## Decision
17+
18+
The 100 KiB conformance skip cap is **retained as-is** and recorded here as a deliberate policy rather than an incidental constant.
19+
20+
Rationale:
21+
- The underlying performance issue is a known limitation documented in OpenSpec (`openspec/specs/cross-language-testing/spec.md`).
22+
- Fixing the performance issue requires a non-trivial algorithm change that may affect binary format or cross-language behavior, triggering a new OpenSpec change.
23+
- Raising the cap without fixing the root cause would cause CI to hang or time out, which is worse than an explicit skip.
24+
25+
The cap is **not permanent**. If the performance issue is resolved, the cap can be raised or removed, but doing so changes test-gate semantics and therefore requires an OpenSpec change before implementation.
26+
27+
## Constraints
28+
29+
- Do not silently raise, lower, or remove the cap without an OpenSpec change.
30+
- Do not treat the cap value (100 KiB) as canonical in user-facing docs; document it as "current workaround" to preserve flexibility.
31+
- When the metadata module (ADR 0001) is implemented, the cap must appear as a named constant with this ADR reference.
32+
33+
## Consequences
34+
35+
**Positive**: Policy is now explicit and findable; reviewers know the cap is intentional; future removal has a clear trigger condition.
36+
37+
**Negative**: The known limitation persists; Range Coder is under-tested on real-world corpus sizes.
38+
39+
**Deferred**: Root-cause fix for Range Coder large-file performance. That work requires a separate OpenSpec proposal and is out of scope for this architecture deepening cycle.

0 commit comments

Comments
 (0)