From f547d755dc999bf4ba0aa2ae3d67c42ea723dc76 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 09:50:14 -0700 Subject: [PATCH 01/15] feat(weave): construct exact numeral references and prime-path replay --- research/weave/NUMERAL_CONSTRUCTION.md | 184 +++++++++ research/weave/SPECIFICATION.md | 50 ++- research/weave/numeral.py | 551 +++++++++++++++++++++++++ research/weave/test.py | 17 +- research/weave/tests/test_numeral.py | 344 +++++++++++++++ 5 files changed, 1120 insertions(+), 26 deletions(-) create mode 100644 research/weave/NUMERAL_CONSTRUCTION.md create mode 100644 research/weave/numeral.py create mode 100644 research/weave/tests/test_numeral.py diff --git a/research/weave/NUMERAL_CONSTRUCTION.md b/research/weave/NUMERAL_CONSTRUCTION.md new file mode 100644 index 0000000..4261579 --- /dev/null +++ b/research/weave/NUMERAL_CONSTRUCTION.md @@ -0,0 +1,184 @@ +# Weave numeral construction v1 + +Status: executable representation/replay layer; not an encryption implementation. +Starting Stack revision: `d0e76d58f832a5664578af39103cdd0697c091d3`. + +## Current construction and placement + +The latest user clarification controls this work: + +1. Normalize the message's bit length using corpus material. +2. Parse repeated multi-byte sequences. Save each selected sequence at an angle + on the eighth/origin circle; a circle among the seven records its occurrences + and their order. +3. First/last interleave the resulting bitstream. Repeat steps 2 and 3. + +The proposed numerical extension makes a selected bit block one exact integer, +then uses a Unicode symbol to refer to its recoverable definition. Prime-index +continuation and embedded prime spans can supply executable recipes. This layer +constructs that extension, not the whole cycle or a replacement cryptosystem. + +The earlier bit-per-circle interpretation does not constrain this layer. A byte +is eight bits; a selected multi-byte sequence remains an ordered sequence. Neither +one-bit-per-circle assignment nor per-bit public substitution is introduced. +No fixed total expansion follows from this representation; all wire bytes are +measured. Earlier whole-cipher length proposals require whole-cycle accounting. + +## Delivered objects + +`BitBlock(value, length)` holds exact bits, including leading zeros and partial +bytes. Values are arbitrary-size Python integers, never decimal floats or hashes. +`BitBlock.from_bytes` and `to_bytes` use MSB-first packing and explicit bit length. +Nonzero unused padding is rejected. + +`Entry(symbol, block, angle, circle, recipe=None)` binds a private-use Unicode +scalar to a block and its caller-supplied exact angular attachment. `circle` is +one of the seven occurrence circles. The angle is a rational in [0,2) turns, +retaining the 720-degree coordinate distinction without inventing a UCNS law. +The origin/circle fields are attachment records, not native geometric objects. +This module does not claim to construct a UCNS Gonol or consume a UCHC constructor. + +`Packet(origin, round_id, entries, symbols)` stores the scoped definitions and +ordered occurrences. `origin` is a caller's scope identifier, not a substitute +geometric origin. Repeated references preserve multiplicity. Their exact offsets +and per-circle order are derived from the stream and block lengths, not duplicated +in a second source of truth. The table can represent residual material as literal +entries as well. Participant selection and repeated-sequence parsing remain with +the caller; no fixed chunking rule is silently installed. + +Symbols are aliases, not Unicode's standardized numeric values. All three private- +use ranges are supported. BMP aliases take three UTF-8 bytes; supplementary aliases +four. Unicode private-use semantics and ranges are defined at: +https://www.unicode.org/faq/private_use.html + +## Exact prime recipes + +`PrimePath(seed, steps)` supports two explicitly selected operations: + +- `('next',)` replaces the current prime by the prime at that index. +- `('span', base, start, width)` selects a contiguous span from the current + prime's canonical base-2 or base-10 numeral. Both the source and selected value + must be prime. Start is zero-based from the left. The original span position + and width remain in the recipe, including when the span has leading zeros. + +For example: + +```python +PrimePath(5381, (('span', 10, 0, 2), ('next',), ('next',))) +# Exact replay: 5381 -> 53 -> 241 -> 1523 +``` + +Different occurrences leading to the same destination are distinct recipes. The +source prime, base, start, and width survive serialization; the destination alone +does not replace the route. The evaluator uses exact trial division and sieving, +not a probable-prime verdict or a published answer substituted for computation. +Literal blocks need not be prime. No automatic search claims every block has a +short prime recipe. A supplied recipe must reconstruct its bound integer exactly; +mismatch never silently falls back to a literal. + +Default recipe limits are operational: index 100,000; prime value 2^32-1; +4,000,000 sieve bytes; 256 steps per recipe; 16,000,000 aggregate work units per +packet validation/replay. Work units conservatively count trial divisors, sieve +cells, and recipe steps. Budgets are adjustable via `Limits`. Exceeding one is +`ResourceLimit`, not evidence against the proposed prime construction. The +nineteenth prime recursion is not claimed executed by this evaluator. + +## Wire and accounting + +The self-contained versioned binary record starts with `WNC` followed by byte 1. +It contains origin scope, round, total reconstructed bit length, every definition, +and the actual UTF-8 occurrence stream. A definition carries its Unicode scalar, +exact angle, occurrence-circle number, bit length, and either packed literal bits +or a prime recipe. Recipes replace literal payload bytes, not their length field. +Unsigned metadata integers have canonical unsigned LEB128 encoding within 64 bits; +this limit does not constrain the size of literal integer values. + +`accounting(packet)` reports source bits, packed source bytes, header bytes, +definition bytes, occurrence bytes, and total bytes. Everything necessary for this +layer's reconstruction is included. No external dictionary, original-message +copy, producer trace, or hidden corpus is needed. This is intentionally not a +choice of public/private exposure for the eventual cipher. + +The decoder rejects malformed UTF-8, noncanonical integers/fractions, undefined +symbols, duplicate symbols or angles, wrong lengths, truncation, trailing data, +invalid prime steps, and over-budget operations. It sizes literal wire allocation +before producing it and checks total reconstructed length before output allocation. +A structurally valid modification can produce a different message: syntax checks +are not authentication, and private-use naming does not create secrecy. + +## Usage + +Python 3.11+ standard library only; no install or network dependency. + +```bash +cd research/weave +python numeral.py demo +python numeral.py bind input.bin block.wnc --origin message-1 --angle 1/7 --circle 3 +python numeral.py inspect block.wnc +python numeral.py recover block.wnc recovered.bin +python -m unittest discover -s tests -p test_numeral.py -v +python test.py --receipt /tmp/weave-check.json +``` + +`bind` treats the supplied file as one block; it is not an automatic compressor. +The symbol is U+E000 within the supplied scope. The angle/circle are required inputs. +For multiple selected blocks and repeated references, use `Entry` and `Packet`: + +```python +from fractions import Fraction +from numeral import BitBlock, Entry, Packet, encode, decode, accounting + +block = BitBlock.from_bytes(b'ABCD' * 256) +entry = Entry(chr(0xE000), block, Fraction(1, 7), 3) +packet = Packet('message-1', 0, (entry,), entry.symbol * 4) +wire = encode(packet) +assert decode(wire).restore().to_bytes() == b'ABCD' * 1024 +print(accounting(packet)) +``` + +Output files use exclusive creation and are never overwritten. `recover` reports +exact output bit length; a partial final byte is right-padded with zeros. Keep that +reported length when using non-byte-aligned API inputs. Default CLI input/output +budgets are finite; library callers may supply an explicit `Limits` profile. + +## Evidence and interpretation + +The 15 new tests include all 8,191 bit blocks of lengths 0 through 12; mixed partial- +byte concatenation; an 8,388,608-bit integer; four repeated large-block occurrences; +all seven occurrence circles; all private-use range boundaries; exact prime-index +and decimal/binary span replay; scope distinction; malformed input and aggregate +resource refusal; and new-process recovery after the source file is deleted. + +The nonsecret demo measured: + +| Case | Packed source bytes | All packet bytes | Exact recovery | +|---|---:|---:|---| +| One 8,388,608-bit literal block | 1,048,576 | 1,048,613 | yes | +| Four references to that block | 4,194,304 | 1,048,622 | yes | +| Prime recipe ending at 1523, length 16 | 2 | 40 | yes | + +A single glyph is genuinely a short reference, not a claim that the complete +packet is that small. The small prime example expands after accounting. Repetition +produces a real saving in the large-block example. No universal compression, +asymmetric advantage, or cryptographic-strength result follows from these tests. + +## File plan and rollout + +- `numeral.py`: constructor, exact evaluator, codec, accounting, CLI. Main risks: + lost bit length, unresolved aliases, recipe mismatch, and excessive decode work. +- `tests/test_numeral.py`: witnesses for those boundaries and separate-process replay. +- `test.py`: add all 15 methods to the existing workspace test gate; no skipped tests. +- `SPECIFICATION.md`: point the active clarification at sequence/occurrence semantics. +- This document: usage, source attribution, scope, and explicit construction choices. + +This is opt-in research. It does not fill any missing native encryption operator, +alter the transport experiment, select key material, or modify UCNS/UCHC authority. +Rollback removes this module/tests/link and adjusts the test inventory. Existing +sealed evidence remains tied to its original sources. + +## hmmm + +Automatic discovery of compact prime recipes, native origin/axis attachment, and +integration with corpus normalization, repeated-sequence parsing, and the iterative +interleave cycle remain separate construction work. The numeric-reference layer +now runs end to end and exposes its actual cost; it is not the whole Weave cipher. diff --git a/research/weave/SPECIFICATION.md b/research/weave/SPECIFICATION.md index c1e5d01..90cd907 100644 --- a/research/weave/SPECIFICATION.md +++ b/research/weave/SPECIFICATION.md @@ -111,26 +111,36 @@ recovered plaintext must be specified explicitly before implementation can claim this layer. -## 5.1. Current byte/whole-part clarification - -The current raw-byte construction is message-scoped and byte-based: each message -is an origin, each byte has eight bits, and one big circle G0 plus seven small -circles G1..G7 supplies the byte's eight-circle structure. One bit participates -through each circle. G0 also remains the dynamic whole; it is not merely a -reference or checksum. Every public two-circle view is (G0, Gi), where 1 <= i <= 7. -Do not replace this with four disjoint pairs or two public sheet bits per source bit. - -Each circle has the complete 720-degree return and key-set-specific relationships -to space. The stated output expansion is 16 ciphertext bits per source byte; the -actual serialization and source of that expansion remain to be constructed. -No universal space-placement or active-small-circle schedule is imposed. - -[EIGHT_CIRCLE.md](EIGHT_CIRCLE.md) records the repair of PRs #73/#74 and its precise -implementation boundary. The current code constructs byte/placement records and -whole-plus-one key views, not a complete native positional cipher. The removed -per-bit encoders and public-feedback recurrence were specification mismatches; -their attacks do not falsify Weave. [QUESTIONS.md](QUESTIONS.md) contains the next -proposed answers for approval. All previously declared system layers remain. +## 5.1. Current sequence/occurrence and numeral clarification + +The latest user clarification supersedes the earlier one-bit-per-circle reading: +normalize message bit length with corpus material; parse repeated multi-byte +sequences; save each selected sequence at an angle on the eighth/origin circle; +a circle among the seven records its occurrence count and order; first/last +interleave the resulting bitstream; repeat parsing/affixiation and interleaving. +The message remains the origin and sequential byte occurrences remain individually +addressable. A saved multi-byte sequence is not reduced to a single source bit. + +The current numerical extension interprets an exact selected bit block as an +integer with its bit length preserved, then gives it a scoped Unicode reference. +A reference resolves to stored literal construction or an executable prime path. +Prime-index continuation and embedded prime spans preserve route and occurrence +information. [NUMERAL_CONSTRUCTION.md](NUMERAL_CONSTRUCTION.md) records its +implemented boundary, full size accounting, exact replay, and usage. + +The numeral module is an explicitly selected representation layer. Its supplied +origin identifiers, angular attachments, and occurrence-circle numbers do not +constitute a replacement UCNS/UCHC constructor. It does not implement repeated- +sequence discovery, corpus normalization, a whole-cycle schedule, or encryption. +No fixed total ciphertext expansion is inferred from a short Unicode reference. +The earlier twofold-length proposal requires accounting over the completed cycle. + +The existing 720-degree return and key-set-specific space relationships remain +preserved. Earlier whole-plus-one public-view research is not promoted into an +asymmetric result by this codec. [EIGHT_CIRCLE.md](EIGHT_CIRCLE.md) records the +prior byte/star repair; its bit-per-circle interpretation is historical rather +than the active rule for the corrected sequence cycle. No discarded PR #73/#74 +encoder or attack is revived. All other declared system layers remain. ## 6. Asymmetric relation diff --git a/research/weave/numeral.py b/research/weave/numeral.py new file mode 100644 index 0000000..c5b9999 --- /dev/null +++ b/research/weave/numeral.py @@ -0,0 +1,551 @@ +# === MODULE_BUILD === +# id: weave_numeral_construction +# module_name: numeral +# module_kind: experiment +# summary: exact length-bearing bit blocks, executable prime paths, and scoped Unicode references with self-contained recovery and full wire-size accounting +# owner: Erin Spencer +# public_surface: BitBlock, PrimePath, Entry, Packet, Limits, encode, decode, main +# internal_surface: canonical wire reader and bounded exact prime evaluator +# auth_boundary: none +# storage_boundary: write +# network_boundary: none +# user_data_boundary: read +# admin_only: false +# tests: tests/test_numeral.py +# rollout: explicit research API and CLI; not installed as Weave encryption +# rollback: remove numeral.py, its tests, and its documentation link +# unresolved: native gonol attachment and automatic prime-recipe discovery are not supplied by this representation layer +# === END MODULE_BUILD === +# === CONTRACTS === +# id: numeral_exact_recovery +# given: admitted blocks including leading zero bits and partial bytes +# then: decoding the self-contained packet restores each value, length, occurrence order, and supplied attachment +# id: numeral_recipe_replay +# given: an admitted prime-index and embedded-span recipe +# then: exact evaluation reproduces the bound block or refuses without substituting a literal +# id: numeral_scoped_symbols +# given: a Unicode symbol in a message-origin and round scope +# then: it resolves uniquely within that scope; no standard Unicode numeric meaning is claimed +# id: numeral_accounting +# given: a serialized packet +# then: header, definitions, and occurrence stream sizes sum to all transmitted bytes +# id: numeral_strict_input +# given: malformed, noncanonical, ambiguous, or over-budget input +# then: reject before unsafe allocation or prime work; budget refusal is not mathematical falsification +# === END CONTRACTS === +"""Weave numeral construction, not encryption or a competing gonol constructor. + +Usage: python numeral.py demo + python numeral.py bind INPUT OUTPUT --origin message-1 --angle 1/7 --circle 3 + python numeral.py recover INPUT OUTPUT + python numeral.py inspect INPUT + +A Unicode symbol names a length-bearing integer or an executable prime path. +The packet includes definitions: there is no hidden dictionary or original-message +lookup. Angles/occurrence-circle assignments are caller-supplied attachment data, +not geometry generated here. No automatic splitting, corpus selection, interleave, +private/public key derivation, authentication, or native UCNS/UCHC claim is made. +""" +from __future__ import annotations + +import argparse +from dataclasses import dataclass, field +from fractions import Fraction +from math import isqrt +from pathlib import Path +import json + +MAGIC = b"WNC\x01" + + +class Refused(ValueError): + """Malformed or inconsistent construction.""" + + +class ResourceLimit(Refused): + """This execution profile cannot admit the requested work.""" + + +@dataclass(frozen=True) +class Limits: + """Operational budgets, adjustable by the caller; not mathematical bounds.""" + output_bits: int = 64 * 1024 * 1024 * 8 + wire_bytes: int = 80 * 1024 * 1024 + entries: int = 137468 + occurrences: int = 1000000 + prime_index: int = 100000 + prime_value: int = 2**32 - 1 + sieve_bytes: int = 4000000 + recipe_steps: int = 256 + prime_work: int = 16000000 + + def __post_init__(self): + if any(type(v) is not int or v < 1 for v in vars(self).values()): + raise Refused("budgets must be positive integers") + + +def _nat(value: int) -> int: + if type(value) is not int or value < 0: + raise Refused("nonnegative integer required; Booleans are not integers here") + return value + + +@dataclass(frozen=True) +class BitBlock: + """Exact ordered bits as (integer, length); the length is part of identity.""" + value: int = field(repr=False) + length: int + + def __post_init__(self): + _nat(self.value) + _nat(self.length) + if self.value.bit_length() > self.length: + raise Refused("integer does not fit declared bit length") + + @classmethod + def from_bytes(cls, data: bytes, length: int | None = None) -> BitBlock: + """MSB-first input; any unused low bits in the last byte must be zero.""" + if type(data) is not bytes: + raise Refused("exact bytes required") + length = len(data) * 8 if length is None else _nat(length) + if len(data) != (length + 7) // 8: + raise Refused("byte count does not match bit length") + padding = (-length) % 8 + value = int.from_bytes(data, "big") + if padding and value & ((1 << padding) - 1): + raise Refused("nonzero unused padding bits") + return cls(value >> padding, length) + + def to_bytes(self) -> bytes: + """Return MSB-first bytes, with zero low padding for a partial last byte.""" + return (self.value << ((-self.length) % 8)).to_bytes((self.length + 7) // 8, "big") + + +@dataclass(frozen=True) +class PrimePath: + """Exact recipe: ('next',) or ('span', base, start, width). + + Spans are contiguous, zero-based from the left, in canonical base 2 or 10 + notation. Both the source and selected value must be prime. A leading-zero + span keeps its original start/width in the recipe, not just its numeric value. + These are supported candidate operations, not a universal Weave routing rule. + """ + seed: int + steps: tuple[tuple, ...] + + def replay(self, limits: Limits = Limits(), *, _engine=None) -> tuple[int, ...]: + if type(self.steps) is not tuple or len(self.steps) > limits.recipe_steps: + raise ResourceLimit("recipe step budget or immutable steps required") + primes = _engine if _engine is not None else _Primes(limits) + primes.charge(len(self.steps) + 1) + value = _nat(self.seed) + primes.require(value) + trace = [value] + for step in self.steps: + if type(step) is not tuple or not step: + raise Refused("invalid recipe step") + if step == ("next",): + value = primes.nth(value) + elif len(step) == 4 and step[0] == "span": + _, base, start, width = step + if type(base) is not int or base not in (2, 10): + raise Refused("span base must be 2 or 10") + _nat(start) + if _nat(width) == 0: + raise Refused("empty span") + digits = format(value, "b") if base == 2 else str(value) + if start + width > len(digits): + raise Refused("span outside source numeral") + value = int(digits[start:start + width], base) + primes.require(value) + else: + raise Refused("unknown recipe operation") + trace.append(value) + return tuple(trace) + + +class _Primes: + def __init__(self, limits: Limits): + self.limits = limits + self.table: list[int] = [] + self.work = 0 + + def charge(self, amount: int) -> None: + self.work += amount + if self.work > self.limits.prime_work: + raise ResourceLimit("aggregate prime work exceeds execution budget") + + def require(self, value: int) -> None: + if value > self.limits.prime_value: + raise ResourceLimit("exact primality work exceeds prime-value budget") + if value < 2 or (value != 2 and value % 2 == 0): + raise Refused("selected value is not prime") + self.charge(len(range(3, isqrt(value) + 1, 2))) + for divisor in range(3, isqrt(value) + 1, 2): + if value % divisor == 0: + raise Refused("selected value is not prime") + + def nth(self, index: int) -> int: + if index > self.limits.prime_index: + raise ResourceLimit("nth-prime index exceeds execution budget") + if index <= len(self.table): + return self.table[index - 1] + bound = max(16, index * 20) + while True: + bound = min(bound, self.limits.prime_value) + if bound + 1 > self.limits.sieve_bytes: + raise ResourceLimit("prime sieve exceeds allocation budget") + self.charge(bound + 1) + sieve = bytearray(b"\x01") * (bound + 1) + sieve[0:2] = b"\x00\x00" + for divisor in range(2, isqrt(bound) + 1): + if sieve[divisor]: + start = divisor * divisor + sieve[start:bound + 1:divisor] = b"\x00" * ((bound - start) // divisor + 1) + self.table = [i for i in range(2, bound + 1) if sieve[i]] + if len(self.table) >= index: + return self.table[index - 1] + if bound == self.limits.prime_value: + raise ResourceLimit("nth prime exceeds prime-value budget") + bound *= 2 + + +def _symbol(symbol: str) -> None: + if type(symbol) is not str or len(symbol) != 1: + raise Refused("one Unicode scalar required") + cp = ord(symbol) + if not (0xE000 <= cp <= 0xF8FF or 0xF0000 <= cp <= 0xFFFFD + or 0x100000 <= cp <= 0x10FFFD): + raise Refused("symbol must be a Unicode private-use scalar") + + +@dataclass(frozen=True) +class Entry: + symbol: str + block: BitBlock = field(repr=False) + angle: Fraction + circle: int + recipe: PrimePath | None = field(default=None, repr=False) + + def validate(self, limits: Limits, *, _engine=None) -> None: + _symbol(self.symbol) + if type(self.block) is not BitBlock or self.block.length == 0: + raise Refused("a definition must contain a nonempty BitBlock") + if self.block.length > limits.output_bits: + raise ResourceLimit("definition exceeds bit budget") + if type(self.angle) is not Fraction or not 0 <= self.angle < 2: + raise Refused("exact supplied angle must be in [0,2) turns") + if type(self.circle) is not int or not 1 <= self.circle <= 7: + raise Refused("occurrence circle must be one of the seven") + if self.recipe is not None: + if type(self.recipe) is not PrimePath or self.recipe.replay(limits, _engine=_engine)[-1] != self.block.value: + raise Refused("recipe does not construct its bound integer") + + +@dataclass(frozen=True) +class Packet: + """One scoped definition table and ordered occurrence stream. + + origin is a caller's scope identifier, not a fabricated UCNS origin object. + The same glyph may legitimately denote another block in another scope. + """ + origin: str + round_id: int + entries: tuple[Entry, ...] + symbols: str + + def validate(self, limits: Limits = Limits()) -> int: + if type(self.origin) is not str or not self.origin or len(self.origin.encode("utf-8")) > 1024: + raise Refused("nonempty UTF-8 origin scope of at most 1024 bytes required") + _nat(self.round_id) + if type(self.entries) is not tuple or len(self.entries) > limits.entries: + raise ResourceLimit("immutable definitions within entry budget required") + if type(self.symbols) is not str or len(self.symbols) > limits.occurrences: + raise ResourceLimit("occurrence stream exceeds execution budget") + mapping = {} + angles = set() + primes = _Primes(limits) + for entry in self.entries: + if type(entry) is not Entry: + raise Refused("Entry required") + entry.validate(limits, _engine=primes) + if entry.symbol in mapping or entry.angle in angles: + raise Refused("duplicate symbol or angular attachment in this scope") + mapping[entry.symbol] = entry + angles.add(entry.angle) + total = 0 + for symbol in self.symbols: + if symbol not in mapping: + raise Refused("undefined symbol") + total += mapping[symbol].block.length + if total > limits.output_bits: + raise ResourceLimit("reconstructed stream exceeds bit budget") + return total + + def occurrences(self, limits: Limits = Limits()) -> tuple[tuple[str, int, int, int], ...]: + """(symbol, bit offset, ordinal on its occurrence circle, circle).""" + self.validate(limits) + mapping = {entry.symbol: entry for entry in self.entries} + counts = [0] * 8 + offset = 0 + result = [] + for symbol in self.symbols: + entry = mapping[symbol] + result.append((symbol, offset, counts[entry.circle], entry.circle)) + counts[entry.circle] += 1 + offset += entry.block.length + return tuple(result) + + def restore(self, limits: Limits = Limits()) -> BitBlock: + """Reconstruct in linear output-byte work; no original input or hidden table.""" + total = self.validate(limits) + mapping = {entry.symbol: entry.block for entry in self.entries} + out = bytearray((total + 7) // 8) + offset = 0 + for symbol in self.symbols: + block = mapping[symbol] + raw = block.to_bytes() + byte, shift = divmod(offset, 8) + if shift == 0: + out[byte:byte + len(raw)] = raw + else: + for i, value in enumerate(raw): + out[byte + i] |= value >> shift + if byte + i + 1 < len(out): + out[byte + i + 1] |= (value << (8 - shift)) & 255 + offset += block.length + return BitBlock.from_bytes(bytes(out), total) + + +def _uint(value: int) -> bytes: + _nat(value) + if value >= 2**64: + raise Refused("wire integer field exceeds 64 bits; literal block values use bytes") + out = bytearray() + while value >= 128: + out.append((value & 127) | 128) + value >>= 7 + out.append(value) + return bytes(out) + + +def _blob(data: bytes) -> bytes: + return _uint(len(data)) + data + + +class _Reader: + def __init__(self, data: bytes): + self.data, self.pos = data, 0 + + def take(self, count: int) -> bytes: + if count > len(self.data) - self.pos: + raise Refused("truncated packet") + result = self.data[self.pos:self.pos + count] + self.pos += count + return result + + def uint(self) -> int: + start = self.pos + value = 0 + for shift in range(0, 70, 7): + byte = self.take(1)[0] + value |= (byte & 127) << shift + if byte < 128: + if value >= 2**64 or self.data[start:self.pos] != _uint(value): + raise Refused("noncanonical integer field") + return value + raise Refused("oversized integer field") + + def blob(self, maximum: int) -> bytes: + count = self.uint() + if count > maximum: + raise ResourceLimit("field exceeds byte budget") + return self.take(count) + + +def _parts(packet: Packet, limits: Limits) -> tuple[bytes, bytes, bytes]: + total = packet.validate(limits) + header = MAGIC + _blob(packet.origin.encode("utf-8")) + _uint(packet.round_id) + _uint(total) + count = _uint(len(packet.entries)) + # Size the entire representation before allocating literal output buffers. + # In particular, unused definitions cannot evade the output-length budget. + stream_size = sum(3 if ord(c) <= 0xFFFF else 4 for c in packet.symbols) + projected = len(header) + len(count) + len(_uint(stream_size)) + stream_size + pieces = [] + for entry in packet.entries: + prefix = (_blob(entry.symbol.encode("utf-8")) + + _uint(entry.angle.numerator) + _uint(entry.angle.denominator) + + _uint(entry.circle) + _uint(entry.block.length)) + if entry.recipe is None: + prefix += b"\x00" + payload_size = (entry.block.length + 7) // 8 + pieces.append((prefix, entry.block)) + else: + recipe = entry.recipe + prefix += b"\x01" + _uint(recipe.seed) + _uint(len(recipe.steps)) + for step in recipe.steps: + if step == ("next",): + prefix += b"\x00" + else: + prefix += b"\x01" + b"".join(_uint(v) for v in step[1:]) + payload_size = 0 + pieces.append((prefix, None)) + projected += len(prefix) + payload_size + if projected > limits.wire_bytes: + raise ResourceLimit("serialized packet exceeds byte budget") + if projected > limits.wire_bytes: + raise ResourceLimit("serialized packet exceeds byte budget") + definitions = count + b"".join(prefix + (block.to_bytes() if block is not None else b"") + for prefix, block in pieces) + stream = _blob(packet.symbols.encode("utf-8")) + return header, definitions, stream + + + +def encode(packet: Packet, limits: Limits = Limits()) -> bytes: + """Serialize all reconstruction information, including the definition table.""" + return b"".join(_parts(packet, limits)) + + +def decode(data: bytes, limits: Limits = Limits()) -> Packet: + """Strict self-contained decoder; malformed input never becomes a partial result.""" + if type(data) is not bytes: + raise Refused("packet must be bytes") + if len(data) > limits.wire_bytes: + raise ResourceLimit("packet exceeds byte budget") + reader = _Reader(data) + if reader.take(len(MAGIC)) != MAGIC: + raise Refused("unknown packet version") + try: + origin = reader.blob(1024).decode("utf-8") + round_id, declared = reader.uint(), reader.uint() + if declared > limits.output_bits: + raise ResourceLimit("declared output exceeds bit budget") + count = reader.uint() + if count > limits.entries: + raise ResourceLimit("definition count exceeds budget") + entries = [] + primes = _Primes(limits) + for _ in range(count): + symbol = reader.blob(4).decode("utf-8") + _symbol(symbol) + numerator, denominator = reader.uint(), reader.uint() + if denominator == 0: + raise Refused("zero angle denominator") + angle = Fraction(numerator, denominator) + if (angle.numerator, angle.denominator) != (numerator, denominator): + raise Refused("angle fraction is not canonical") + circle, length = reader.uint(), reader.uint() + if length > limits.output_bits: + raise ResourceLimit("definition exceeds bit budget") + tag = reader.take(1)[0] + recipe = None + if tag == 0: + block = BitBlock.from_bytes(reader.take((length + 7) // 8), length) + elif tag == 1: + seed, step_count = reader.uint(), reader.uint() + if step_count > limits.recipe_steps: + raise ResourceLimit("recipe step count exceeds budget") + steps = [] + for _ in range(step_count): + kind = reader.take(1)[0] + if kind == 0: + steps.append(("next",)) + elif kind == 1: + steps.append(("span", reader.uint(), reader.uint(), reader.uint())) + else: + raise Refused("unknown recipe opcode") + recipe = PrimePath(seed, tuple(steps)) + block = BitBlock(recipe.replay(limits, _engine=primes)[-1], length) + else: + raise Refused("unknown definition tag") + entries.append(Entry(symbol, block, angle, circle, recipe)) + symbols = reader.blob(limits.occurrences * 4).decode("utf-8") + except UnicodeError as exc: + raise Refused("invalid UTF-8") from exc + if reader.pos != len(data): + raise Refused("trailing packet data") + packet = Packet(origin, round_id, tuple(entries), symbols) + if packet.validate(limits) != declared: + raise Refused("declared output length disagrees with occurrences") + return packet + + +def accounting(packet: Packet, limits: Limits = Limits()) -> dict[str, int]: + header, table, stream = _parts(packet, limits) + bits = packet.validate(limits) + return {"source_bits": bits, "source_packed_bytes": (bits + 7) // 8, + "header_bytes": len(header), "definition_bytes": len(table), + "occurrence_bytes": len(stream), "total_bytes": len(header) + len(table) + len(stream), + "symbol_utf8_bytes": len(packet.symbols.encode("utf-8"))} + + +def demo() -> dict: + """Measured nonsecret examples; no secret key or personal input.""" + block = BitBlock.from_bytes(bytes(range(256)) * 4096) + entry = Entry(chr(0xE000), block, Fraction(1, 7), 3) + single = Packet("demo-large", 0, (entry,), entry.symbol) + repeated = Packet("demo-large", 0, (entry,), entry.symbol * 4) + recipe = PrimePath(5381, (("span", 10, 0, 2), ("next",), ("next",))) + r_entry = Entry(chr(0xE001), BitBlock(1523, 16), Fraction(2, 7), 4, recipe) + replay = Packet("demo-prime", 0, (r_entry,), r_entry.symbol) + result = {} + for name, packet in (("single_large_block", single), ("four_repetitions", repeated), ("prime_path", replay)): + wire = encode(packet) + recovered = decode(wire).restore() + result[name] = {**accounting(packet), "exact_recovery": recovered == packet.restore()} + result["prime_path"]["trace"] = list(recipe.replay()) + result["standing"] = "representation-and-replay only; not encryption" + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + sub.add_parser("demo") + bind = sub.add_parser("bind") + bind.add_argument("input", type=Path) + bind.add_argument("output", type=Path) + bind.add_argument("--origin", required=True) + bind.add_argument("--angle", required=True, type=Fraction) + bind.add_argument("--circle", required=True, type=int) + recover = sub.add_parser("recover") + recover.add_argument("input", type=Path) + recover.add_argument("output", type=Path) + inspect = sub.add_parser("inspect") + inspect.add_argument("input", type=Path) + args = parser.parse_args() + try: + if args.command == "demo": + result = demo() + else: + limit = Limits().output_bits // 8 if args.command == "bind" else Limits().wire_bytes + if args.input.stat().st_size > limit: + raise ResourceLimit("input file exceeds execution budget") + with args.input.open("rb") as source: + raw = source.read(limit + 1) + if len(raw) > limit: + raise ResourceLimit("input grew beyond execution budget") + if args.command == "bind": + entry = Entry(chr(0xE000), BitBlock.from_bytes(raw), args.angle, args.circle) + packet = Packet(args.origin, 0, (entry,) if raw else (), entry.symbol if raw else "") + wire = encode(packet) + with args.output.open("xb") as target: + target.write(wire) + result = accounting(packet) + else: + packet = decode(raw) + result = accounting(packet) + if args.command == "recover": + restored = packet.restore() + with args.output.open("xb") as target: + target.write(restored.to_bytes()) + result["output_bit_length"] = restored.length + print(json.dumps(result, indent=2)) + return 0 + except (OSError, Refused, UnicodeError) as exc: + parser.exit(2, f"refused: {exc}\n") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/research/weave/test.py b/research/weave/test.py index 38acb35..1a89c13 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -2,7 +2,7 @@ """Run the full Weave workspace suite and emit a source-bound receipt. Usage: python test.py --receipt /tmp/weave-check.json -Transport behavior, byte/star construction records and their boundaries are tested. +Transport behavior, earlier byte/star records, and numeral construction are tested. Native positional encryption and native corpus replay remain explicitly unexecuted. """ import argparse @@ -33,10 +33,10 @@ def main(): result = unittest.TextTestRunner(stream=report, verbosity=2).run( unittest.defaultTestLoader.discover(str(ROOT/'tests'))) after = snapshot() - # 52 unchanged transport/assembly methods + 12 byte/star + 10 message methods. + # 74 existing methods + 15 exact numeral/reference/recipe methods. passed = (result.wasSuccessful() and not result.skipped and not result.expectedFailures and not result.unexpectedSuccesses - and result.testsRun == 74 and before == after) + and result.testsRun == 89 and before == after) receipt = { 'schema':'weave.transport-evidence/v1', 'status':'PASSED' if passed else 'FAILED', @@ -44,7 +44,7 @@ def main(): 'tests':result.testsRun, 'failures':len(result.failures),'errors':len(result.errors),'skipped':len(result.skipped), 'source_unchanged':before == after, - 'expected_test_methods':74, + 'expected_test_methods':89, 'coverage_status':'WITNESSED' if passed else 'NOT_ACCEPTED', 'source_sha256':hashlib.sha256(json.dumps(before,sort_keys=True,separators=(',',':')).encode()).hexdigest(), 'source_files':before, @@ -54,9 +54,14 @@ def main(): 'largest_roundtrip_bytes':65536,'separate_process_roundtrip_bytes':1026, 'byte_values':256,'bit_to_circle_assignments':40320, 'whole_part_key_views':7,'bit_placements_per_byte':8, - 'message_participants_per_byte':1}, + 'message_participants_per_byte':1, + 'numeral_short_blocks':8191,'numeral_large_block_bits':8388608, + 'numeral_repeated_roundtrip_bytes':4194304, + 'numeral_prime_path':[5381,53,241,1523]}, 'declared_ciphertext_bits_per_byte':16, - 'positional_cipher':'NOT_IMPLEMENTED: byte/star structure repaired; transform proposals await approval', + 'declared_ciphertext_scope':'Earlier positional proposal only; not an inferred property of numeral construction or the corrected sequence cycle', + 'numeral_construction':'EXPLICIT_LAYER: length-bearing integers, supplied attachments, Unicode references, exact bounded prime replay, complete wire accounting', + 'positional_cipher':'NOT_IMPLEMENTED: no numeral test establishes native positional encryption', 'full_weave':Pipeline().plan(), 'native_corpus_replay':'NOT_EXECUTED: no constructed database materialized by this suite', 'native_adapter_tests':'missing-input/source-identity refusal only; not successful real-corpus replay', diff --git a/research/weave/tests/test_numeral.py b/research/weave/tests/test_numeral.py new file mode 100644 index 0000000..3f1765f --- /dev/null +++ b/research/weave/tests/test_numeral.py @@ -0,0 +1,344 @@ +# === CHECKS === +# id: check_numeral_all_8191_short_bitblocks +# proves: numeral_exact_recovery +# call: self::test_all_8191_short_bitblocks +# mutates: none +# cleanup: none +# +# id: check_numeral_leading_zeros_and_partial_byte_concatenation +# proves: numeral_exact_recovery +# call: self::test_leading_zeros_and_partial_byte_concatenation +# mutates: none +# cleanup: none +# +# id: check_numeral_large_integer_and_four_occurrences +# proves: numeral_exact_recovery, numeral_accounting +# call: self::test_large_integer_and_four_occurrences +# mutates: none +# cleanup: none +# +# id: check_numeral_origin_and_round_scope +# proves: numeral_scoped_symbols +# call: self::test_origin_and_round_scope +# mutates: none +# cleanup: none +# +# id: check_numeral_all_private_use_boundaries_and_utf8_width +# proves: numeral_scoped_symbols +# call: self::test_all_private_use_boundaries_and_utf8_width +# mutates: none +# cleanup: none +# +# id: check_numeral_prime_index_and_embedded_paths +# proves: numeral_recipe_replay +# call: self::test_prime_index_and_embedded_paths +# mutates: none +# cleanup: none +# +# id: check_numeral_prime_errors_and_budget_are_distinct +# proves: numeral_recipe_replay, numeral_strict_input +# call: self::test_prime_errors_and_budget_are_distinct +# mutates: none +# cleanup: none +# +# id: check_numeral_recipe_mismatch_does_not_fallback +# proves: numeral_recipe_replay +# call: self::test_recipe_mismatch_does_not_fallback +# mutates: none +# cleanup: none +# +# id: check_numeral_order_offsets_and_seven_occurrence_circles +# proves: numeral_exact_recovery +# call: self::test_order_offsets_and_seven_occurrence_circles +# mutates: none +# cleanup: none +# +# id: check_numeral_invalid_definitions_and_numeric_types +# proves: numeral_strict_input +# call: self::test_invalid_definitions_and_numeric_types +# mutates: none +# cleanup: none +# +# id: check_numeral_every_truncation_unknown_version_and_trailing_data +# proves: numeral_strict_input +# call: self::test_every_truncation_unknown_version_and_trailing_data +# mutates: none +# cleanup: none +# +# id: check_numeral_decoder_length_and_work_limits +# proves: numeral_strict_input +# call: self::test_decoder_length_and_work_limits +# mutates: none +# cleanup: none +# +# id: check_numeral_accounting_counts_every_byte +# proves: numeral_accounting +# call: self::test_accounting_counts_every_byte +# mutates: none +# cleanup: none +# +# id: check_numeral_fresh_process_recovery_and_no_overwrite +# proves: numeral_exact_recovery +# call: self::test_fresh_process_recovery_and_no_overwrite +# mutates: filesystem +# cleanup: tempdir_teardown +# +# id: check_numeral_fresh_process_prime_recipe +# proves: numeral_recipe_replay +# call: self::test_fresh_process_prime_recipe +# mutates: filesystem +# cleanup: tempdir_teardown +# +# === END CHECKS === +"""Run: python -m unittest discover -s tests -p test_numeral.py -v. + +Tests concern this representation and its exact replay, not native gonol geometry +or Weave security. No user data, network, or persistent writes outside tempdirs. +""" +from dataclasses import replace +from fractions import Fraction +from pathlib import Path +import random +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +from numeral import (BitBlock, Entry, Packet, PrimePath, Limits, Refused, + ResourceLimit, encode, decode, accounting, _uint) + +ROOT = Path(__file__).resolve().parents[1] +A, B = chr(0xE000), chr(0xE001) + + +def packet(block, symbol=A): + return Packet("test-origin", 0, (Entry(symbol, block, Fraction(1, 7), 1),), symbol) + + +class NumeralTests(unittest.TestCase): + def test_all_8191_short_bitblocks(self): + checked = 0 + for length in range(13): + for value in range(1 << length): + block = BitBlock(value, length) + p = packet(block) if length else Packet("empty", 0, (), "") + restored = decode(encode(p)).restore() + self.assertEqual(restored, block) + self.assertEqual(BitBlock.from_bytes(block.to_bytes(), length), block) + checked += 1 + self.assertEqual(checked, 8191) + + def test_leading_zeros_and_partial_byte_concatenation(self): + self.assertNotEqual(BitBlock(5, 3), BitBlock(5, 8)) + rng = random.Random(71005) + for _ in range(100): + lengths = [rng.randrange(1, 65) for _ in range(12)] + blocks = [BitBlock(rng.getrandbits(n), n) for n in lengths] + entries = tuple(Entry(chr(0xE000+i), v, Fraction(i, 13), 1+i%7) + for i, v in enumerate(blocks)) + symbols = ''.join(e.symbol for e in entries) + p = Packet("partials", 7, entries, symbols) + expected = ''.join(format(v.value, f'0{v.length}b') for v in blocks) + rebuilt = decode(encode(p)).restore() + self.assertEqual(rebuilt, BitBlock(int(expected, 2), len(expected))) + + def test_large_integer_and_four_occurrences(self): + data = bytes(range(256)) * 4096 + block = BitBlock.from_bytes(data) + single = packet(block) + p = replace(single, symbols=A * 4) + wire = encode(p) + self.assertEqual(decode(wire).restore().to_bytes(), data * 4) + self.assertEqual(block.length, 8388608) + self.assertLess(len(wire), len(data) * 2) + self.assertGreater(len(encode(single)), len(data)) + # A small reference is real, but does not erase its definition cost. + self.assertEqual(accounting(single)['symbol_utf8_bytes'], 3) + self.assertGreater(accounting(single)['definition_bytes'], len(data)) + + def test_origin_and_round_scope(self): + p = packet(BitBlock(5, 8)) + q = replace(packet(BitBlock(9, 8)), origin="another-origin", round_id=2) + self.assertEqual(p.symbols, q.symbols) + self.assertNotEqual(decode(encode(p)).restore(), decode(encode(q)).restore()) + self.assertEqual(decode(encode(q)), q) + + def test_all_private_use_boundaries_and_utf8_width(self): + for cp in (0xE000, 0xF8FF, 0xF0000, 0xFFFFD, 0x100000, 0x10FFFD): + p = packet(BitBlock(7, 8), chr(cp)) + self.assertEqual(decode(encode(p)), p) + self.assertEqual(accounting(p)['symbol_utf8_bytes'], 3 if cp < 0x10000 else 4) + for symbol in ('', 'AB', '7', '\ud800', '\ufdd0', chr(0x10FFFF)): + with self.assertRaises((Refused, UnicodeError)): + encode(packet(BitBlock(7, 8), symbol)) + + def test_prime_index_and_embedded_paths(self): + recursion = PrimePath(2, (("next",),) * 8) + self.assertEqual(recursion.replay(), (2, 3, 5, 11, 31, 127, 709, 5381, 52711)) + jump = PrimePath(5381, (("span", 10, 0, 2), ("next",), ("next",))) + self.assertEqual(jump.replay(), (5381, 53, 241, 1523)) + binary = PrimePath(5381, (("span", 2, 0, 3), ("next",))) + self.assertEqual(binary.replay(), (5381, 5, 11)) + # Both occurrences lead to 3, but the serialized route preserves which one. + left = PrimePath(313, (("span", 10, 0, 1),)) + right = PrimePath(313, (("span", 10, 2, 1),)) + self.assertEqual(left.replay(), right.replay()) + self.assertNotEqual(left, right) + for path in (jump, binary, left, right): + value = path.replay()[-1] + e = Entry(A, BitBlock(value, 32), Fraction(1, 7), 7, path) + p = Packet("prime", 0, (e,), A * 3) + self.assertEqual(decode(encode(p)), p) + self.assertEqual(decode(encode(p)).restore(), p.restore()) + + def test_prime_errors_and_budget_are_distinct(self): + for path in (PrimePath(4, ()), PrimePath(True, ()), + PrimePath(5381, (("span", 10, 1, 2),)), + PrimePath(5381, (("span", 10, 3, 2),)), + PrimePath(5381, (("span", 2, 0, 0),)), + PrimePath(5381, (("span", True, 0, 1),)), + PrimePath(53, (("execute",),))): + with self.assertRaises(Refused): + path.replay() + with self.assertRaises(ResourceLimit): + PrimePath(101, (("next",),)).replay(Limits(prime_index=100)) + with self.assertRaises(ResourceLimit): + PrimePath(101, ()).replay(Limits(prime_value=100)) + with self.assertRaises(ResourceLimit): + PrimePath(101, (("next",),)).replay(Limits(sieve_bytes=16)) + with self.assertRaises(ResourceLimit): + PrimePath(2, (("next",),) * 3).replay(Limits(recipe_steps=2)) + with self.assertRaises(ResourceLimit): + PrimePath(101, ()).replay(Limits(prime_work=1)) + entries = (Entry(A, BitBlock(101, 16), Fraction(1, 7), 1, PrimePath(101, ())), + Entry(B, BitBlock(101, 16), Fraction(2, 7), 2, PrimePath(101, ()))) + p = Packet("aggregate-work", 0, entries, A+B) + with self.assertRaises(ResourceLimit): + encode(p, Limits(prime_work=8)) + with self.assertRaises(ResourceLimit): + decode(encode(p), Limits(prime_work=8)) + + def test_recipe_mismatch_does_not_fallback(self): + entry = Entry(A, BitBlock(241, 16), Fraction(1, 7), 1, PrimePath(53, ())) + with self.assertRaises(Refused): + encode(Packet("mismatch", 0, (entry,), A)) + # Arbitrary composite values are fully admitted by literal representation. + p = packet(BitBlock(2**1000-2, 1001)) + self.assertEqual(decode(encode(p)), p) + + def test_order_offsets_and_seven_occurrence_circles(self): + entries = tuple(Entry(chr(0xE000+i), BitBlock(i, 16), Fraction(i, 8), i+1) + for i in range(7)) + symbols = ''.join(e.symbol for e in entries) * 3 + p = Packet("occurrences", 5, entries, symbols) + occurrences = decode(encode(p)).occurrences() + self.assertEqual(len(occurrences), 21) + for i, (symbol, offset, ordinal, circle) in enumerate(occurrences): + self.assertEqual((symbol, offset, ordinal, circle), (symbols[i], i*16, i//7, i%7+1)) + + def test_invalid_definitions_and_numeric_types(self): + p = packet(BitBlock(5, 8)) + bad = (replace(p, origin=""), replace(p, round_id=True), + replace(p, entries=list(p.entries)), replace(p, symbols=B), + replace(p, entries=p.entries * 2), + replace(p, entries=(replace(p.entries[0], circle=0),)), + replace(p, entries=(replace(p.entries[0], circle=True),)), + replace(p, entries=(replace(p.entries[0], angle=0.5),)), + replace(p, entries=(replace(p.entries[0], angle=Fraction(2)),)), + replace(p, entries=(replace(p.entries[0], block=BitBlock(0, 0)),))) + for item in bad: + with self.assertRaises(Refused): + encode(item) + for value, length in ((True, 1), (0, False), (-1, 1), (8, 3), (0, -1)): + with self.assertRaises(Refused): + BitBlock(value, length) + with self.assertRaises(Refused): + BitBlock.from_bytes(b'\x01', 1) + with self.assertRaises(Refused): + BitBlock.from_bytes(b'\x00\x00', 1) + + def test_every_truncation_unknown_version_and_trailing_data(self): + wire = encode(packet(BitBlock(5, 8))) + for offset in range(len(wire)): + with self.assertRaises(Refused): + decode(wire[:offset]) + for corrupt in (b'bad!' + wire[4:], wire+b'\x00'): + with self.assertRaises(Refused): + decode(corrupt) + # Origin-length varint widened noncanonically from one byte to two. + with self.assertRaises(Refused): + decode(wire[:4] + bytes([wire[4] | 128, 0]) + wire[5:]) + # A structurally valid modification need not be detected: no authentication claim. + changed = encode(packet(BitBlock(6, 8))) + self.assertEqual(decode(changed).restore(), BitBlock(6, 8)) + + def test_decoder_length_and_work_limits(self): + p = packet(BitBlock(5, 8)) + wire = encode(p) + with self.assertRaises(ResourceLimit): + decode(wire, Limits(output_bits=7)) + with self.assertRaises(ResourceLimit): + decode(wire, Limits(wire_bytes=len(wire)-1)) + with self.assertRaises(ResourceLimit): + encode(replace(p, symbols=A*2), Limits(occurrences=1)) + with self.assertRaises(ResourceLimit): + encode(p, Limits(wire_bytes=3)) + # Byte-budget preflight precedes literal materialization. + with patch.object(BitBlock, 'to_bytes', side_effect=AssertionError('allocated early')): + with self.assertRaises(ResourceLimit): + encode(p, Limits(wire_bytes=3)) + # Declared output mismatch, without changing the remainder of the packet. + output_pos = 4 + 1 + len(p.origin.encode()) + 1 + wrong = wire[:output_pos] + _uint(9) + wire[output_pos+1:] + with self.assertRaises(Refused): + decode(wrong) + + def test_accounting_counts_every_byte(self): + p = packet(BitBlock.from_bytes(b'\x00AB' * 32)) + for symbols in (A, A*7): + candidate = replace(p, symbols=symbols) + stats = accounting(candidate) + self.assertEqual(stats['total_bytes'], len(encode(candidate))) + self.assertEqual(stats['total_bytes'], stats['header_bytes']+ + stats['definition_bytes']+stats['occurrence_bytes']) + self.assertEqual(stats['source_bits'], candidate.restore().length) + path = PrimePath(5381, (("span", 10, 0, 2), ("next",), ("next",))) + e = Entry(A, BitBlock(1523, 16), Fraction(1, 7), 1, path) + p = Packet("recipe-cost", 0, (e,), A) + # The tested recipe is real but larger than storing this small block. + self.assertGreater(accounting(p)['total_bytes'], 2) + + def test_fresh_process_recovery_and_no_overwrite(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + src, wire, restored = root/'source', root/'packet', root/'restored' + data = bytes(range(256))*8 + b'\x00\x00\xff' + src.write_bytes(data) + command = [sys.executable, str(ROOT/'numeral.py')] + encoded = subprocess.run(command + ['bind', str(src), str(wire), '--origin', + 'process-test', '--angle', '1/7', '--circle', '3'], capture_output=True) + self.assertEqual(encoded.returncode, 0, encoded.stderr) + src.unlink() + decoded = subprocess.run(command+['recover', str(wire), str(restored)], capture_output=True) + self.assertEqual(decoded.returncode, 0, decoded.stderr) + self.assertEqual(restored.read_bytes(), data) + again = subprocess.run(command+['recover', str(wire), str(restored)], capture_output=True) + self.assertEqual(again.returncode, 2) + self.assertEqual(restored.read_bytes(), data) + + def test_fresh_process_prime_recipe(self): + path = PrimePath(5381, (("span", 10, 0, 2), ("next",), ("next",))) + p = Packet("prime-process", 0, (Entry(A, BitBlock(1523, 16), Fraction(1, 7), 3, path),), A) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + wire, result = root/'recipe', root/'recovered' + wire.write_bytes(encode(p)) + completed = subprocess.run([sys.executable, str(ROOT/'numeral.py'), + 'recover', str(wire), str(result)], capture_output=True) + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertEqual(result.read_bytes(), b'\x05\xf3') + + +if __name__ == '__main__': + unittest.main() From 9f633356255e179b2bb9d268210e3a98d61e4211 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 10:58:52 -0700 Subject: [PATCH 02/15] feat(weave): complete native sequence cycle and repair numeral review findings --- .github/workflows/weave.yml | 24 +- research/weave/CYCLE.md | 171 +++++++ research/weave/CYCLE_NATIVE.json | 13 + research/weave/CYCLE_WORK_GRAPH.json | 49 ++ research/weave/EIGHT_CIRCLE.md | 163 ++----- research/weave/NUMERAL_CONSTRUCTION.md | 245 ++++------ research/weave/QUESTIONS.md | 141 ++---- research/weave/SPECIFICATION.md | 50 +- research/weave/affixiation.py | 223 +++++++++ research/weave/cycle.py | 488 +++++++++++++++++++ research/weave/cycle_native.py | 64 +++ research/weave/numeral.py | 33 +- research/weave/prime_schedule.py | 143 ++++++ research/weave/profiles/cycle-v1.json | 40 ++ research/weave/stages/eight_circle.py | 202 -------- research/weave/stages/message_origin.py | 100 ---- research/weave/test.py | 97 ++-- research/weave/tests/test_cycle.py | 515 ++++++++++++++++++++ research/weave/tests/test_eight_circle.py | 180 ------- research/weave/tests/test_message_origin.py | 117 ----- research/weave/tests/test_numeral_review.py | 64 +++ 21 files changed, 2059 insertions(+), 1063 deletions(-) create mode 100644 research/weave/CYCLE.md create mode 100644 research/weave/CYCLE_NATIVE.json create mode 100644 research/weave/CYCLE_WORK_GRAPH.json create mode 100644 research/weave/affixiation.py create mode 100644 research/weave/cycle.py create mode 100644 research/weave/cycle_native.py create mode 100644 research/weave/prime_schedule.py create mode 100644 research/weave/profiles/cycle-v1.json delete mode 100644 research/weave/stages/eight_circle.py delete mode 100644 research/weave/stages/message_origin.py create mode 100644 research/weave/tests/test_cycle.py delete mode 100644 research/weave/tests/test_eight_circle.py delete mode 100644 research/weave/tests/test_message_origin.py create mode 100644 research/weave/tests/test_numeral_review.py diff --git a/.github/workflows/weave.yml b/.github/workflows/weave.yml index 51e5b87..f2d5262 100644 --- a/.github/workflows/weave.yml +++ b/.github/workflows/weave.yml @@ -16,18 +16,34 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the event source with GitHub PR isolation - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: persist-credentials: false - name: Verify exact event identity env: SOURCE_SHA: ${{ github.sha }} run: test "$(git rev-parse HEAD)" = "$SOURCE_SHA" - # Push validates the exact branch head; pull_request validates GitHub's - # isolated merge ref. Neither grants write credentials to tested code. - - name: Verify assembly and provisional transport, not native cipher security + - name: Checkout exact native geometry producer + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + repository: The-Interdependency/ucns + ref: 905e66964a495d7596a577bb64e4158db9465864 + path: .weave-inputs/ucns + persist-credentials: false + - name: Checkout exact binary-domain producer + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + repository: The-Interdependency/uchc + ref: 65ac43d5d4cf79082c0836f8545adeb0242e88fc + path: .weave-inputs/uchc + persist-credentials: false + - name: Verify source pins, native cycle and existing transport regressions run: | set -euo pipefail + export WEAVE_SOURCES="$GITHUB_WORKSPACE/.weave-inputs" + test "$(git -C "$WEAVE_SOURCES/ucns" rev-parse HEAD)" = "905e66964a495d7596a577bb64e4158db9465864" + test "$(git -C "$WEAVE_SOURCES/uchc" rev-parse HEAD)" = "65ac43d5d4cf79082c0836f8545adeb0242e88fc" python3 -VV PYTHONDONTWRITEBYTECODE=1 python3 research/weave/test.py --receipt "$RUNNER_TEMP/weave-check.json" cat "$RUNNER_TEMP/weave-check.json" + PYTHONDONTWRITEBYTECODE=1 python3 research/weave/cycle.py demo diff --git a/research/weave/CYCLE.md b/research/weave/CYCLE.md new file mode 100644 index 0000000..077fab0 --- /dev/null +++ b/research/weave/CYCLE.md @@ -0,0 +1,171 @@ +# Weave iterative sequence cycle v1 + +Status: executable construction and exact recovery. Public/private asymmetric +key generation and cryptographic security are not implemented by this profile. + +## The complete implemented path + +1. Normalize once using actual corpus bits. Preserve the exact original byte + length inside the normalized frame. +2. Discover repeated multi-byte sequences in the current byte stream. Select + nonoverlapping occurrences, retaining every residual byte. +3. Close the sequences through the UCHC binary-domain producer. The eighth/whole + circle holds native sequence attachments; seven occurrence circles hold count, + order and native positions. Sequential byte occurrences remain addressable axes. +4. Bind definitions through the existing length-bearing integer/Unicode numeral + layer. Emit the seven native occurrence-circle streams and the positions needed + to reconstruct source order. Include every definition and relation in accounting. +5. Execute the configured prime-index and embedded-prime-span route. Derive the + section arity and exact nonempty bit boundaries from that route and current size. +6. First/last interleave the bits within each section. Feed those actual rearranged + bytes into step 2 of the next round. + +Reverse performs the last bit permutation's inverse, native sequence-position +recovery, preceding rounds in reverse order, and corpus denormalization. The only +inputs are the final record, the same supplied profile/material, and pinned producer +sources. Original plaintext and an untransmitted encoder trace are not accepted. + +This implements the corrected normalization/affixiation/interleave cycle, not all +historical proposals or a disguised replacement cryptosystem. There is no +one-bit-per-circle mapping, two-bit substitution, fixed 16-bit byte codeword, +conventional cipher fallback, or inferred twofold expansion. + +## Explicit implementation profiles + +The following choices make the construction executable and are named/versioned, +not attributed to the user as universal laws: + +- **corpus-tail bucket normalization:** frame the original byte length and a corpus + source digest with the message, then fill the next strictly larger configured + bucket with actual cyclic corpus bits starting at the configured bit offset. + At least one filler byte is used. The source digest is not the corpus substitute: + the actual filler is checked during recovery. The public format is research only. +- **maximal-lcp-longest-first-v1:** suffix-array/LCP discovery, longest candidates + first, with earliest-source tie breaking and leftmost nonoverlapping selection. + Counts mean selected partition occurrences, not all overlapping substring matches. + No fixed chunk length or silent candidate truncation is introduced. This is not + a globally optimal compression claim. +- **native binary attachment:** UCHC attaches each saved sequence at its first + selected occurrence's native byte axis. UCNS owns the exact `r/N` axis position + and complete 720-degree state. Source data, occurrences and native objects remain + inside the construction; hashes merely identify it. +- **circle assignment:** definition admission order indexes the profile's explicit + permutation of circles 1..7. Eight configured exact fractions govern native + circle-space displacement, including the whole circle at index 0. +- **prime-route-compositions-v1:** execute the full prime path, including every + source/span position and returned value. A prefix-bearing radix numeral of those + route tokens selects among explicit candidate arities and ranks an ordered + composition of the actual bit length. Different routes may select the same map; + no secret entropy is inferred from the size of the determinant. +- **end order:** `first-last` is explicit in the sample profile, matching the latest + clarified cycle. `last-first` remains supported and separate. Historical transport + ordering is not silently changed. + +All choices are visible in `profiles/cycle-v1.json`, the named implementation +profiles, or the native producer documentation. They can be replaced under a new +profile identity without redefining UCNS or UCHC geometry. + +## Native dependency boundary + +`CYCLE_NATIVE.json` pins the exact UCHC producer commit/module blob and its two +unchanged UCNS source modules. `cycle_native.py` verifies and executes those exact +buffers. It refuses missing/changed sources before message processing, never fills +that gap with a fabricated class or language corpus fixture. + +`CYCLE_WORK_GRAPH.json` records the complete consumer work graph. The native binary +producer is in UCHC, not an application-owned imitation. UCHC uses actual UCNS +`AxisCirclePosition` and `NativeMobiusState` instances. This does not claim general +cross-origin metric geometry, English/Python graduation, a private lift, or security. + +In recovery, transmitted native complete positions and configured space relations +are inverted through `NativeMobiusState.advance`. The resulting exact source axes +determine byte placement. Complete coverage, no overlap/gaps, source/round identity, +whole-circle attachments and canonical seven-stream order must all replay. + +## Framing and full size accounting + +`WVC` + version byte 1 identifies the outer cycle record. It binds native-source lock, +profile identity, retained message-origin receipt, round count and final payload. +The original-length/corpus frame is inside the repeated transformations, not an +unreported external reconstruction file. + +Each `WAF` + version byte 1 affixiation record contains source byte length, a complete +numeral packet, seven occurrence counts and exact native source-position fractions. +The numeral occurrence stream is in native circle order, NOT plaintext order. +The next round consumes the entire preceding permuted record, including information +that will be needed to undo earlier rounds. No hidden sidecar or overwritten history. + +An attached Unicode symbol is a scoped reference, not one byte containing arbitrary +information. All its definitions and actual UTF-8 bytes are counted. Research +source/profile digests and structural rejection are not authentication. + +The three-round nonsecret demo measured 784 original bytes, normalized to 1,024: + +| Round | Input bytes | Definitions | Selected occurrences | Complete output bytes | +|---|---:|---:|---:|---:| +| 0 | 1,024 | 8 | 11 | 910 | +| 1 | 910 | 32 | 44 | 1,494 | +| 2 | 1,494 | 178 | 248 | 5,054 | + +The outer frame adds 103 bytes: total **5,157 bytes**, with exact recovery. +This example expands overall. Later rounds can create metadata cost larger than +any repeated-sequence saving. No claim that additional rounds always compress, +strengthen secrecy, or produce independent transformations is made. + +## Usage + +Python 3.12+; the consumer and native operations require only the standard library. +The source root contains the locked `ucns/` and `uchc/` checkouts. The runnable +bundle supplies exact, read-only minimal snapshots under `sources/`. + +```sh +cd research/weave +python cycle.py demo --sources /checkouts +python cycle.py forward input.bin output.wvc --corpus corpus.bin \ + --profile profiles/cycle-v1.json --sources /checkouts +python cycle.py reverse output.wvc recovered.bin --corpus corpus.bin \ + --profile profiles/cycle-v1.json --sources /checkouts +WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_cycle.py -v +WEAVE_SOURCES=/checkouts python test.py --receipt /tmp/weave-cycle-check.json +``` + +`--sources` defaults to `WEAVE_SOURCES`, then the local `sources/` folder. Output +files use exclusive creation; existing files are never overwritten. Actual corpus +bytes and the same profile are required for recovery. Neither should be mistaken +for an asymmetric private/public key pair. Do not use this research format for secrets. + +Default execution budgets: 256 KiB source message, 1 MiB intermediate records and +corpus, 32 rounds, 16 million candidate-occurrence visits, 131,072 native occurrences +per round, and 16 million aggregate scheduler prime-work units. Library callers +can supply `CycleLimits`. Guards produce visible refusal before exceeding the +admitted scope; no mid-run truncation is returned as success. These are operational +profiles, not mathematical limits or cryptographic security parameters. + +## Tests, integration, and rollback + +Thirty cycle tests cover all 8,191 binary-alphabet byte strings of lengths 0..12; +competing overlaps/residuals; all 4,083 small ranked compositions in the declared +range; all byte values; both end orders; effective native spaces; exact prime paths; +empty/random/multiround messages; a 65,536-byte roundtrip; malformed records/configs; +resource guards; and a fresh recovery process after original-file deletion. + +The numeral dependency's four live review findings are repaired: conflicting active +circle documentation, reset decode prime budgets, zero-denominator CLI handling, +and malformed-input versus resource-limit classification. Three new regressions +bring its suite to 18. The obsolete bit-per-circle modules and their 22 tests are +removed, with these sequence/native operations as replacement. Fifty-two original +transport/assembly tests retain their original limited scope. Full Weave inventory: +52 + 18 + 30 = 100. Native UCHC has a separate 13-test producer suite. + +The cycle is opt-in. The original transport experiment and the all-on native cipher +refusal remain separate; no missing asymmetric operator is silently filled. +Rollback removes the cycle modules/profile/dependency locks/workflow additions as +one change. Historical discarded encoders remain in Git, not in active imports. + +## hmmm + +Automatic discovery of short prime recipes for arbitrary literal sequences, an +actual private/public degeneration and recovery advantage, authentication/replay, +and security analysis of the completed cipher remain open. The named normalization, +selection, attachment and determinant profiles are implemented candidates, not +universal rules. This cycle now executes those choices and reports their real cost. diff --git a/research/weave/CYCLE_NATIVE.json b/research/weave/CYCLE_NATIVE.json new file mode 100644 index 0000000..0ca551b --- /dev/null +++ b/research/weave/CYCLE_NATIVE.json @@ -0,0 +1,13 @@ +{ + "schema": "weave.native-inputs/v1", + "ucns_commit": "905e66964a495d7596a577bb64e4158db9465864", + "ucns_sources": { + "src/ucns/axis_circle.py": "768777c8eca6e65537fdbab2003835d6f4f40569", + "src/ucns/direct_mobius.py": "14a4cee36b5bbfa72cf3c03703c427abdac7f33d" + }, + "uchc_source": { + "commit": "65ac43d5d4cf79082c0836f8545adeb0242e88fc", + "path": "binary/uchc_binary/origin.py", + "git_blob": "300db7383b403acbff7ce9a94f7536ad047bc78b" + } +} diff --git a/research/weave/CYCLE_WORK_GRAPH.json b/research/weave/CYCLE_WORK_GRAPH.json new file mode 100644 index 0000000..f30a5a4 --- /dev/null +++ b/research/weave/CYCLE_WORK_GRAPH.json @@ -0,0 +1,49 @@ +{ + "schema": "the-interdependency.stack-manifest", + "version": "1.0.0", + "work_graph_sha256": "f75fda260793ff46acab5bdc5f4a597065c1f7fe3d25cd781b9a48de05f062e3", + "repositories": [ + { + "repository": "The-Interdependency/uchc", + "commit": "65ac43d5d4cf79082c0836f8545adeb0242e88fc", + "authority": "binary-domain closure and public contract", + "relation": "exact new native binary producer consumed by the cycle" + }, + { + "repository": "The-Interdependency/ucns", + "commit": "905e66964a495d7596a577bb64e4158db9465864", + "authority": "native exact geometry", + "relation": "unchanged mathematical producer consumed by the new domain" + }, + { + "repository": "The-Interdependency/stack", + "commit": "f547d755dc999bf4ba0aa2ae3d67c42ea723dc76", + "authority": "Weave sequence selection and round composition", + "relation": "consumer starting point; this change completes the explicit sequence-cycle profile" + }, + { + "repository": "The-Interdependency/skill-lib", + "commit": "9867ab33877f2b1f50f8a501cf379aa99360bd52", + "authority": "build and evidence discipline", + "relation": "governing workflow source" + }, + { + "repository": "The-Interdependency/metapat", + "commit": "86415a5368c1a1417c2b6731f19967a6b1fce6bb", + "authority": "domain restraint and affixiation semantics", + "relation": "semantic consultation only; no security or geometry transfer" + } + ], + "boundaries": { + "authority_transfer": false, + "proof_status_transfer": false, + "measurement_status_transfer": false, + "semantic_mapping": "declared binary-domain candidate; no language or cryptographic equivalence", + "agent_scope": "cross-repository-work-graph", + "hmmm": [ + "Named normalization, selection, attachment and split profiles are executable candidates, not universal rules.", + "Public/private cryptographic lifting, authentication and full-cipher security remain unimplemented.", + "This graph changes no English/Python graduation or UCNS geometry authority." + ] + } +} diff --git a/research/weave/EIGHT_CIRCLE.md b/research/weave/EIGHT_CIRCLE.md index 8bcfd5e..1583a2c 100644 --- a/research/weave/EIGHT_CIRCLE.md +++ b/research/weave/EIGHT_CIRCLE.md @@ -1,145 +1,46 @@ -# Weave: byte-scoped whole-plus-seven repair +# Weave: superseded byte/star interpretation -Status: **structural repair implemented; native positional cipher not implemented**. -This replaces the incorrect active constructions from Stack PRs #73 and #74. -The next proposed answers are in [QUESTIONS.md](QUESTIONS.md); none is silently enabled. +**Historical only. The active cycle is [CYCLE.md](CYCLE.md).** -## Settled construction +The one-bit-per-circle interpretation from PR #76 is superseded by the user's +later sequence/occurrence clarification. A selected multi-byte sequence is saved +at an angle on the eighth/origin circle. A circle among the seven records its +occurrences and order. This is not an eight-circle allocation for every byte. +The eight circle count does not establish a 16-bit output codeword or total 2x size. -Each raw-byte message is an origin. Each byte occurrence participates at that origin -as one eight-bit unit, with one big circle G0 and seven small circles G1 through G7. -One source bit participates through each circle. The big circle is both a bit-bearing -member and the retained whole; its dynamic role must not be replaced by metadata, -a fixed reference, or a checksum. +The old `stages/eight_circle.py` and `stages/message_origin.py` structural records +and their 22 tests are removed. Native replacement is the UCHC binary-domain +origin/sequence producer, consumed by `cycle_native.py` and `cycle.py`. It uses +actual UCNS axis-circle and Mobius objects and performs exact coordinate recovery. +The associated bit-per-circle questions Q11/Q13/Q14 no longer control construction. -Every public two-circle view contains G0 and exactly one Gi, with 1 <= i <= 7. -There are seven possible whole-part views, not four disjoint pairs. This does not -require emitting all seven views, fix a visitation order, or assign one pair per bit. +Retained architectural distinctions: one whole plus seven occurrence circles; +720-degree complete return; key/profile-specific space relationships; byte-occurrence +identity; and the intended whole-plus-one public exposure. The last remains an +asymmetric research objective, not a property claimed by the current cycle record. -Every circle has the complete 720-degree return. Its relationship to space is -instantiated by the key set, not fixed by a universal placement formula. The full -key retains the eight-circle configuration; the public object exposes two circles -at a time. Deriving the actual cryptographic public object is distinct from enforcing -that pair shape. +## Evidence correction -The stated expansion remains **8 source bits -> 16 ciphertext bits per byte**. It is -a requirement, not an implemented serializer and not a consequence of the number of -circles alone. The removed per-bit duplication did not implement this requirement -for the correct reason. +PR #73's per-bit two-sheet encoder and PR #74's bit-axis feedback were wrong +implementations. Their public recovery attacks apply to those implementations, +not to the intended Weave construction. Changing data unavailable to a public +sender while holding every sender input fixed cannot demonstrate whether a private +key is necessary for recovery. That inference remains withdrawn. -## Executable repair +Historical source identities: -`stages/eight_circle.py` now has byte-level construction records: +- PR #73: `a3836f5632ed3babe1bc8c3dbebab60186c2bc78`; +- PR #74: `92ccda0620c06bc46654939c061ceb2d36476cb9`; +- PR #76 structural repair: `d0e76d58f832a5664578af39103cdd0697c091d3`. -- `Circle` stores a key-set circle binding and exact space coordinate. It is not a - replacement UCNS Gonol constructor. One scalar coordinate is not the complete - native relationship to space. -- `FullKeySet` accepts eight bindings and an explicit source-bit/circle assignment. - `degenerate(i)` can produce only `(G0, Gi)`; direct `PublicPair` construction also - rejects a missing whole, repeated whole, extra member, or malformed circle. -- `construct_byte` accepts a complete byte and eight caller-supplied exact positions, - preserving each source bit once. It emits a plaintext-bearing construction record, - **not ciphertext**. `source_value` reads that retained source, not a private inverse. -- Positions are stored as exact rational turns modulo two: a 360-degree displacement - stays distinct, while the complete 720-degree return agrees. No sheet threshold - converts bit values into public output codes. +These are historical provenance, not active capabilities or security evidence. -`stages/message_origin.py` now contains one ordered participant per **byte**. -`construct_at` attaches that complete byte's eight placements at its message-origin -occurrence. Repeated values preserve their separate occurrence indices. The supplied -origin name is a namespace, not content hashing, nonce generation, or a secrecy claim. +## Usage -The invented `CouplingKey`, public-feedback recurrence, per-bit `MessageAxis`, -`CoupledWitness`, bitwise `encode`/`decode`, and `public_recover` have been removed. -Their supported replacement is the corrected byte/star structure, not another cipher -substitute. The existing transport experiment and full-profile missing-operation -refusal remain separate and unchanged. - -## What this repair does not implement - -The real native origin-axis binding, full relational circle-space state, key generation, -whole-byte positional transformation, dynamic G0 evolution, cryptographic degeneration, -private reconstruction and 16-bit ciphertext layout are not implemented by these records. -No claim is made that merely requiring a `FullKeySet` argument makes its hidden circles -necessary for recovery. The next questions concern these actual operations. - -UCNS retains geometry authority. UCHC origin-axis architecture remains the intended -construction dependency; this repair does **not** claim to execute UCHC by copying a -language source or relabeling occurrence metadata. Existing Stack/UCHC implementation -and migration boundaries are unchanged. No `libs/` source or source pin is changed. - -## Corrected evidence scope - -PR #73 encoded each individual source bit into two public sheet bits. PR #74 then -added public feedback to that wrong unit. Those programs were publicly recoverable, -but neither represented the requested byte-scoped construction. Their failures are -not falsifications of Weave, and their successful tests did not establish that the -requested geometry or message-origin construction had been implemented. - -The former hidden-circle perturbation conclusion was also invalid. For a public-only -sender, encryption is a function of its public key, message and explicit public-side -inputs/randomness. Holding all those inputs fixed holds its output fixed regardless -of changes to unavailable private records. That observation cannot establish whether -private information is necessary for inversion. A correct test must examine recovery -and unauthorized inversion for the actual forward/recovery relation, accounting for -valid or equivalent keys rather than demanding arbitrary private changes alter a -public sender's output. - -Historical source remains in Git at: - -- PR #73 merge: `a3836f5632ed3babe1bc8c3dbebab60186c2bc78`; -- PR #74 merge and repair base: `92ccda0620c06bc46654939c061ceb2d36476cb9`. - -These are provenance for removed implementations, not live capability or security -claims. Their invalid observations are not copied into the current security results. - -## File plan and verification - -The repair changes only `research/weave`: the two structural modules, their two test -modules, the existing full-suite receipt runner, this document, the question register, -and the specification. It removes the stale `verify.py` command from the specification. -Roll back as one transaction; restoring a deprecated encoder is not an accepted -recovery path. Other Weave layers and independent stage switches remain intact. - -Focused coverage: all 256 byte values, all 40,320 source-bit/circle assignments, all -seven whole-plus-part views, direct-constructor validation, exact coordinates, source -occurrence preservation, and removal of the old callable cipher surfaces. These are -construction tests, not claims about attack resistance. Full expected test inventory: -52 existing methods plus 22 byte/star/message methods = 74. - -## Usage guidance - -From `research/weave/`: - -```python -from fractions import Fraction -from stages.eight_circle import Circle, FullKeySet -from stages.message_origin import construct_origin, construct_at - -# Explicit, nonsecret structural fixture: NOT key generation or encryption. -full = FullKeySet( - tuple(Circle(i, f"G{i}", Fraction(i, 9)) for i in range(8)), - (7, 2, 5, 0, 3, 1, 6, 4), -) -origin = construct_origin(b"AA", identity="example-message") -positions = tuple(Fraction(i, 7) for i in range(8)) -byte_state = construct_at(origin, 0, full, positions) -assert byte_state.source_value == 65 -assert full.degenerate(3).indices == (0, 3) -assert len(origin.bytesets) == 2 -``` - -```bash -python -m unittest discover -s tests -p 'test_eight_circle.py' -python -m unittest discover -s tests -p 'test_message_origin.py' -python test.py --receipt /tmp/weave-check.json -``` - -The repair follows the source-preservation, native-first metadata, removal and -verification boundaries of the loaded skill-lib instructions. Skill usage-counter -persistence is not available in this runtime; no exposure count is fabricated. +Use `python cycle.py demo --sources /checkouts` and the commands in `CYCLE.md`. +Do not import the removed bit-per-circle records or restore them as a fallback. ## hmmm -Q11-Q14 and carried-forward Q9 await approval. The concrete forward/private-recovery -operation remains research work, not something created by approving its description. +The native sequence cycle is executable. Public/private key generation and a +computational recovery advantage remain separate construction work. diff --git a/research/weave/NUMERAL_CONSTRUCTION.md b/research/weave/NUMERAL_CONSTRUCTION.md index 4261579..2f05b90 100644 --- a/research/weave/NUMERAL_CONSTRUCTION.md +++ b/research/weave/NUMERAL_CONSTRUCTION.md @@ -1,184 +1,109 @@ # Weave numeral construction v1 -Status: executable representation/replay layer; not an encryption implementation. -Starting Stack revision: `d0e76d58f832a5664578af39103cdd0697c091d3`. - -## Current construction and placement - -The latest user clarification controls this work: - -1. Normalize the message's bit length using corpus material. -2. Parse repeated multi-byte sequences. Save each selected sequence at an angle - on the eighth/origin circle; a circle among the seven records its occurrences - and their order. -3. First/last interleave the resulting bitstream. Repeat steps 2 and 3. - -The proposed numerical extension makes a selected bit block one exact integer, -then uses a Unicode symbol to refer to its recoverable definition. Prime-index -continuation and embedded prime spans can supply executable recipes. This layer -constructs that extension, not the whole cycle or a replacement cryptosystem. - -The earlier bit-per-circle interpretation does not constrain this layer. A byte -is eight bits; a selected multi-byte sequence remains an ordered sequence. Neither -one-bit-per-circle assignment nor per-bit public substitution is introduced. -No fixed total expansion follows from this representation; all wire bytes are -measured. Earlier whole-cipher length proposals require whole-cycle accounting. - -## Delivered objects - -`BitBlock(value, length)` holds exact bits, including leading zeros and partial -bytes. Values are arbitrary-size Python integers, never decimal floats or hashes. -`BitBlock.from_bytes` and `to_bytes` use MSB-first packing and explicit bit length. -Nonzero unused padding is rejected. - -`Entry(symbol, block, angle, circle, recipe=None)` binds a private-use Unicode -scalar to a block and its caller-supplied exact angular attachment. `circle` is -one of the seven occurrence circles. The angle is a rational in [0,2) turns, -retaining the 720-degree coordinate distinction without inventing a UCNS law. -The origin/circle fields are attachment records, not native geometric objects. -This module does not claim to construct a UCNS Gonol or consume a UCHC constructor. - -`Packet(origin, round_id, entries, symbols)` stores the scoped definitions and -ordered occurrences. `origin` is a caller's scope identifier, not a substitute -geometric origin. Repeated references preserve multiplicity. Their exact offsets -and per-circle order are derived from the stream and block lengths, not duplicated -in a second source of truth. The table can represent residual material as literal -entries as well. Participant selection and repeated-sequence parsing remain with -the caller; no fixed chunking rule is silently installed. - -Symbols are aliases, not Unicode's standardized numeric values. All three private- -use ranges are supported. BMP aliases take three UTF-8 bytes; supplementary aliases -four. Unicode private-use semantics and ranges are defined at: +Status: executable numerical representation and replay. The complete iterative +construction using this layer is now [CYCLE.md](CYCLE.md). Neither layer establishes +public/private encryption or universal compression. + +## Exact objects + +`BitBlock(value, length)` represents ordered bits as an arbitrary-size integer and +its exact bit length. Leading zeros remain part of identity. Byte conversion uses +MSB-first packing; unused low bits in a partial last byte must be zero. + +`Entry(symbol, block, angle, circle, recipe=None)` gives a nonempty block a scoped +Unicode private-use reference and exact supplied attachment. Angles are Fractions +in [0,2) turns and occurrence circles are 1..7. This standalone codec does not +invent native geometry. The cycle supplies attachments from real UCNS objects +through the UCHC binary producer. + +`Packet(origin, round_id, entries, symbols)` contains definitions and ordered +references. The same symbol may denote different blocks in different scopes. +Definitions, source positions implicit in occurrence order, multiplicity, length +and attachment data remain recoverable without the original input. In the cycle, +references are instead organized into native circle order; the surrounding WAF +record carries native coordinates used to recover source order. + +Private-use scalars are aliases, not standardized Unicode numeric values. All +three private-use ranges are admitted. Their actual UTF-8 widths are counted: +three bytes in the BMP range and four in the supplementary ranges. Reference: https://www.unicode.org/faq/private_use.html -## Exact prime recipes +## Prime-path replay -`PrimePath(seed, steps)` supports two explicitly selected operations: - -- `('next',)` replaces the current prime by the prime at that index. -- `('span', base, start, width)` selects a contiguous span from the current - prime's canonical base-2 or base-10 numeral. Both the source and selected value - must be prime. Start is zero-based from the left. The original span position - and width remain in the recipe, including when the span has leading zeros. - -For example: +`PrimePath(seed, steps)` supports `('next',)` for the prime at the current prime's +index and `('span', base, start, width)` for an embedded prime-valued span in base +2 or 10. Both source and selected values must be prime. Span offsets are zero-based +from the left; source, base, start and width remain in the recipe, including leading +zero spans. Equal destinations do not erase distinct selection occurrences. ```python -PrimePath(5381, (('span', 10, 0, 2), ('next',), ('next',))) -# Exact replay: 5381 -> 53 -> 241 -> 1523 +PrimePath(5381, (('span', 10, 0, 2), ('next',), ('next',))).replay() +# (5381, 53, 241, 1523) ``` -Different occurrences leading to the same destination are distinct recipes. The -source prime, base, start, and width survive serialization; the destination alone -does not replace the route. The evaluator uses exact trial division and sieving, -not a probable-prime verdict or a published answer substituted for computation. -Literal blocks need not be prime. No automatic search claims every block has a -short prime recipe. A supplied recipe must reconstruct its bound integer exactly; -mismatch never silently falls back to a literal. - -Default recipe limits are operational: index 100,000; prime value 2^32-1; -4,000,000 sieve bytes; 256 steps per recipe; 16,000,000 aggregate work units per -packet validation/replay. Work units conservatively count trial divisors, sieve -cells, and recipe steps. Budgets are adjustable via `Limits`. Exceeding one is -`ResourceLimit`, not evidence against the proposed prime construction. The -nineteenth prime recursion is not claimed executed by this evaluator. - -## Wire and accounting - -The self-contained versioned binary record starts with `WNC` followed by byte 1. -It contains origin scope, round, total reconstructed bit length, every definition, -and the actual UTF-8 occurrence stream. A definition carries its Unicode scalar, -exact angle, occurrence-circle number, bit length, and either packed literal bits -or a prime recipe. Recipes replace literal payload bytes, not their length field. -Unsigned metadata integers have canonical unsigned LEB128 encoding within 64 bits; -this limit does not constrain the size of literal integer values. - -`accounting(packet)` reports source bits, packed source bytes, header bytes, -definition bytes, occurrence bytes, and total bytes. Everything necessary for this -layer's reconstruction is included. No external dictionary, original-message -copy, producer trace, or hidden corpus is needed. This is intentionally not a -choice of public/private exposure for the eventual cipher. +Literal blocks may be composite. A supplied recipe must reproduce its bound integer +exactly; mismatch never silently becomes a literal. Exact trial division and sieving +perform the admitted work. No automatic compact-recipe finder or nineteenth prime +recursion computation is claimed. The cycle scheduler separately executes these +routes to determine interleave splits. + +Default adjustable `Limits`: 64 MiB output, 80 MiB wire, 137468 definitions, +1000000 references, prime index100000, prime value2^32-1, sieve4000000 cells, +256 steps per recipe, and16000000 aggregate prime-work units. Resource refusal +means the chosen execution profile cannot complete that work, not falsification. +Malformed immutable structures raise `Refused`, not `ResourceLimit`. Decode parsing +and validation now share one charged prime engine rather than resetting the budget. +## Self-contained wire and accounting + +WNC plus version byte1 carries scope, round, reconstructed bit length, all +definitions and the actual UTF-8 reference stream. Each definition contains its +scalar, exact angle, circle and bit length, then either packed literal bytes or +an executable prime recipe. Canonical unsigned LEB128 metadata fields are limited +to64 bits; arbitrary-size literal values use their packed bytes instead. + +`accounting(packet)` reports header, definition, occurrence and total bytes. The decoder rejects malformed UTF-8, noncanonical integers/fractions, undefined -symbols, duplicate symbols or angles, wrong lengths, truncation, trailing data, -invalid prime steps, and over-budget operations. It sizes literal wire allocation -before producing it and checks total reconstructed length before output allocation. -A structurally valid modification can produce a different message: syntax checks -are not authentication, and private-use naming does not create secrecy. +references, duplicate symbols/angles, wrong lengths, truncation, trailing data, +invalid recipes and exceeded budgets. These structural checks are not authentication. -## Usage +Observed standalone examples, including all definitions and metadata: -Python 3.11+ standard library only; no install or network dependency. +| Case | Original packed bytes | Complete packet bytes | +|---|---:|---:| +| One 8,388,608-bit literal block | 1,048,576 | 1,048,613 | +| Four references to that block | 4,194,304 | 1,048,622 | +| Recipe ending at1523 in16 bits | 2 | 40 | -```bash +All recover exactly. The repeated large block saves space; the other examples expand. +A short symbol does not erase the reconstruction information it identifies. + +## Usage + +```sh cd research/weave python numeral.py demo python numeral.py bind input.bin block.wnc --origin message-1 --angle 1/7 --circle 3 -python numeral.py inspect block.wnc python numeral.py recover block.wnc recovered.bin -python -m unittest discover -s tests -p test_numeral.py -v -python test.py --receipt /tmp/weave-check.json -``` - -`bind` treats the supplied file as one block; it is not an automatic compressor. -The symbol is U+E000 within the supplied scope. The angle/circle are required inputs. -For multiple selected blocks and repeated references, use `Entry` and `Packet`: - -```python -from fractions import Fraction -from numeral import BitBlock, Entry, Packet, encode, decode, accounting - -block = BitBlock.from_bytes(b'ABCD' * 256) -entry = Entry(chr(0xE000), block, Fraction(1, 7), 3) -packet = Packet('message-1', 0, (entry,), entry.symbol * 4) -wire = encode(packet) -assert decode(wire).restore().to_bytes() == b'ABCD' * 1024 -print(accounting(packet)) +python numeral.py inspect block.wnc +python -m unittest discover -s tests -p 'test_numeral*.py' -v ``` -Output files use exclusive creation and are never overwritten. `recover` reports -exact output bit length; a partial final byte is right-padded with zeros. Keep that -reported length when using non-byte-aligned API inputs. Default CLI input/output -budgets are finite; library callers may supply an explicit `Limits` profile. - -## Evidence and interpretation - -The 15 new tests include all 8,191 bit blocks of lengths 0 through 12; mixed partial- -byte concatenation; an 8,388,608-bit integer; four repeated large-block occurrences; -all seven occurrence circles; all private-use range boundaries; exact prime-index -and decimal/binary span replay; scope distinction; malformed input and aggregate -resource refusal; and new-process recovery after the source file is deleted. - -The nonsecret demo measured: - -| Case | Packed source bytes | All packet bytes | Exact recovery | -|---|---:|---:|---| -| One 8,388,608-bit literal block | 1,048,576 | 1,048,613 | yes | -| Four references to that block | 4,194,304 | 1,048,622 | yes | -| Prime recipe ending at 1523, length 16 | 2 | 40 | yes | - -A single glyph is genuinely a short reference, not a claim that the complete -packet is that small. The small prime example expands after accounting. Repetition -produces a real saving in the large-block example. No universal compression, -asymmetric advantage, or cryptographic-strength result follows from these tests. - -## File plan and rollout - -- `numeral.py`: constructor, exact evaluator, codec, accounting, CLI. Main risks: - lost bit length, unresolved aliases, recipe mismatch, and excessive decode work. -- `tests/test_numeral.py`: witnesses for those boundaries and separate-process replay. -- `test.py`: add all 15 methods to the existing workspace test gate; no skipped tests. -- `SPECIFICATION.md`: point the active clarification at sequence/occurrence semantics. -- This document: usage, source attribution, scope, and explicit construction choices. +`bind` selects the entire supplied file as one block; automatic repeated-sequence +selection belongs to `affixiation.py`. `recover` reports the exact output bit length; +a partial final byte is right-padded with zeros. Existing output files are never +overwritten. Invalid CLI angles such as1/0 now produce concise status2 refusal. -This is opt-in research. It does not fill any missing native encryption operator, -alter the transport experiment, select key material, or modify UCNS/UCHC authority. -Rollback removes this module/tests/link and adjusts the test inventory. Existing -sealed evidence remains tied to its original sources. +The original15 tests cover exhaustive8191 short blocks, large integers, partial-byte +concatenation, all private-use boundaries, exact prime paths, all seven circles, +malformed input, resource limits and source-deleted new-process recovery. Three +review regressions cover aggregate decode work and error classifications. The full +cycle suite retains all18 alongside its native composition tests. ## hmmm -Automatic discovery of compact prime recipes, native origin/axis attachment, and -integration with corpus normalization, repeated-sequence parsing, and the iterative -interleave cycle remain separate construction work. The numeric-reference layer -now runs end to end and exposes its actual cost; it is not the whole Weave cipher. +Native attachment, automatic sequence discovery, corpus normalization and iterative +interleaving are connected in the separately named cycle profile. Automatic short +prime-recipe discovery and an asymmetric private reconstruction advantage remain +unimplemented. Reverting this codec requires retiring or rebinding its cycle consumer; +no discarded bit-per-circle implementation is a supported fallback. diff --git a/research/weave/QUESTIONS.md b/research/weave/QUESTIONS.md index 30a04f0..f8e8410 100644 --- a/research/weave/QUESTIONS.md +++ b/research/weave/QUESTIONS.md @@ -1,109 +1,60 @@ -# Weave: current questions and proposed answers +# Weave: current decisions and remaining questions -**Approval status: PENDING.** The proposed answers below are not implemented defaults. -The byte/star repair is authorized separately; it does not approve a new cipher. -Existing Q1-Q10 identities remain in the disposition register below. +The latest sequence/occurrence clarification controls the construction. Earlier +bit-per-circle and fixed-twofold-output assumptions are superseded, not pending +approval. The executable candidate cycle is documented in [CYCLE.md](CYCLE.md). -## Settled; do not ask again +## Established sequence -- The input is raw bytes, not Unicode or an English-word prerequisite. -- Each message is an origin; each byte has eight bits and an eight-circle construction. -- One big circle plus seven small circles remains eight. The big circle participates - dynamically and carries a bit; it is not merely an external reference. -- Public degeneration uses two circles at a time. One is always the big circle. - The earlier four-circle proposal and arbitrary/disjoint pairings are superseded. -- Every circle uses the complete 720-degree return. -- Circle-space relationships vary per key set; do not impose a universal relation. -- The stated ciphertext expansion is twofold. Do not obtain it by copying or - independently coding each bit twice. -- UCNS geometry and UCHC origin-axis relations retain their respective authorities. +Normalize message bit length with actual corpus material once. Parse repeated +multi-byte sequences, save each selected sequence at an angle on the eighth/origin +circle, and record occurrences and order on a circle among the seven. First/last +interleave the resulting bits. Repeat affixiation and interleaving; reverse in the +opposite order. Preserve residual material and each round's reconstruction information. -## Next question set for approval +The message is the origin; sequential byte occurrences are axes. Equal byte values +remain separate occurrences. UCNS owns geometry, UCHC owns the binary-domain native +closure, and Stack/Weave owns selection, scheduling and cycle composition. -### Q11 — Is the placement operation relocating values or changing them? +## Implemented candidate answers -**Probable answer:** Preserve each bit's value and occurrence identity in the -positional layer; transform its place and its relationships as part of the complete -byte. The ciphertext encodes the resulting relation jointly. Do not install a -separate two-symbol substitution for each bit. +These are explicit, versioned implementation choices, not newly inferred universal +laws or hidden approvals: corpus-tail bit-offset bucket filling; maximal-LCP +longest-first nonoverlapping sequence selection; first-occurrence native whole-circle +attachment; profile-defined occurrence-circle order and eight space offsets; exact +prime-route ranked-composition splits; and an explicit first-last end order. -**Still to construct:** the actual joint placement and its exact inverse. This answer -does not prohibit a separately authorized transformation in another Weave layer. +The sample profile can run without further policy decisions. Its complete forward +and inverse, failures, budgets and size accounting are in `CYCLE.md`. -### Q12 — What information does the public whole-plus-one pair retain? - -**Probable answer:** A forward positional relation derived from the complete -eight-circle configuration, with the inverse-completing relationships omitted. It is -not merely a raw copy of two otherwise independent private circle records. The sender -can operate with its public object; the full private configuration supplies recovery. - -**Still to construct:** a concrete degeneration and recovery operation. Reduced key -exposure is not itself evidence of computational difficulty or strongest security. - -### Q13 — What carries forward between successive byte constructions? - -**Probable answer:** The key-set geometry and an evolving message-scoped relational -state carry forward. Each byte keeps its own occurrence identity and eight placements. -G0 participates in each relation; the active small circle is selected within that -key-defined evolution, not by an added universal schedule, fixed four-pair partition, -or a rule that emits two public bits for every source bit. - -**Still to construct:** a deterministic forward/recovery state transition that uses -the established native relations. No numerical feedback formula is selected here. - -### Q14 — What do the additional eight ciphertext bits represent? - -**Probable answer:** One joint 16-bit relational codeword for the complete source -byte, rather than eight independent two-bit codes. Its additional capacity carries -recoverable placement/relation information; exact circle positions may be reconstructed -through the key rather than dumped as arbitrary coordinates into the wire format. - -**Still to construct:** the actual 16-bit mapping, its length accounting and unique -recovery. No field split, extra header, or claim of two possible lifts is assumed. -Two public circles alone do not mathematically force twofold expansion. - -### Q9 — Should identical messages under one key repeat the same trajectory? - -**Probable answer, carried forward:** No. Fresh per-message input should participate -in the origin/placement relation, and authorized recovery must obtain its needed -state. Any transmitted origin metadata must be accounted for within the agreed -length policy, not silently added after claiming exact twofold expansion. - -**Still to construct:** a concrete native variation mechanism and recovery path. -A namespace called `O_M`, a counter, or a public nonce alone is not a secrecy result. - -## Earlier questions: preserved disposition +## Preserved question identities | ID | Current disposition | |---|---| -| Q1 — Native plaintext admission | Raw-byte, byte-scoped shape is settled. This repair constructs occurrence/placement records, not native encryption. No text database is a prerequisite. Actual geometric transformation and serialization remain Q11/Q14. | -| Q2 — Private gonol/public counterpart | Eight full circles and G0-plus-one public shape are settled. The actual public/private transformation is Q12. | -| Q3 — Threads | Preserve complementary native contributions, source occurrences and cross-thread relationships. Existing lane routing is a transport trial, not the native relation. | -| Q4 — Corpus | Preserve actual selected material and its construction/recovery role. The existing left/right traversal formula remains an assistant-selected transport trial; native attachment is not settled by it. | -| Q5 — Join/final operation | Preserve explicit route execution and the literal final last/first interleave. The demonstrated cyclic join is not a private route generator or a new universal rule. | -| Q6 — Arity composition/remainders | Existing experiment uses sequential passes and quotient/remainder sections, preserving order, repetitions and empty sections. Keep its proposed policy separate from user-ratified general design. | -| Q7 — Sender boundary | Intended public-side sender must not require recipient private state. API separation alone is not a mathematical asymmetric construction. | -| Q8 — Control derivation | Key/message relation and inverse planning must provide controls before they are needed. No private encoder trace is silently supplied as ciphertext or an inverse plan. See Q12/Q13. | -| Q9 — Repeated-message variation | Carried forward above; the earlier proposed answer remains pending, not newly approved. | -| Q10 — Integrity/length/replay | Remains a distinct unresolved mechanism. Authentication is independently switchable and is not silently made a replacement for Weave or a newly mandatory layer by this repair. | - -## Removed premises - -The PR #73 per-bit sheet code and PR #74 bit-axis feedback code were specification -mismatches. Their attacks do not adjudicate the requested construction. The premise -that changing private-only data must change ciphertext while every sender input is -held fixed is also removed; it is not a valid private-recovery-dependence test. -See [EIGHT_CIRCLE.md](EIGHT_CIRCLE.md) for exact historical identities and repair scope. - -## Usage guidance - -Approve or amend by ID, for example `Q11 approve; Q12: ...`. -Approved descriptions guide construction; they do not constitute an implemented -algorithm, a proof, or permission to silently fill missing mathematics with a substitute. -No additional decision about a particular degree, space offset, or universal circle -visitation order is required merely to retain the repaired structure. +| Q1 — Native admission | Exact raw byte occurrences now use UCHC binary origins and native UCNS axes. No text database prerequisite. | +| Q2 — Private gonol/public counterpart | Intended full configuration and whole-plus-one exposure retained; asymmetric derivation is not implemented by positional recovery. | +| Q3 — Threads | Seven occurrence-circle streams participate in current native recovery. No automatic equivalence to all historical complementary cryptographic-thread proposals is asserted. | +| Q4 — Corpus | Actual corpus bits normalize once in the explicit bucket profile and are verified on reverse. Earlier traversal trial is not substituted for this cycle. | +| Q5 — Join/final operation | The new iterative profile serializes native occurrence streams then interleaves each round. Earlier standalone transport joining and optional final whole pass retain their distinct historical scope. | +| Q6 — Arity/remainders | Executed prime routes determine ranked nonempty compositions of each current bitstream. The sample selects among 3/5/7; no universal schedule or round count is imposed. | +| Q7 — Sender boundary | Genuine asymmetric mode still requires a public-only sender and recipient private recovery relation. Current cycle uses the same supplied profile/material. | +| Q8 — Control derivation | The implemented round schedule is regenerated from its prime routes and each available intermediate length; no plaintext trace supplies inverse controls. This is not a private trapdoor. | +| Q9 — Repeated-message variation | Remains an unimplemented cryptographic design objective. Identical inputs/profile/material produce identical construction records in this deterministic research profile. | +| Q10 — Integrity/length/replay | Full byte accounting is implemented. Structural/source mismatch refusal is not authentication; authentication and replay remain separate unimplemented mechanisms. | +| Q11 — Bit placement/value | Superseded as a prerequisite: the corrected unit is a repeated multi-byte sequence with ordered occurrences, not independently positioned source bits. | +| Q12 — Public whole-plus-one relation | Still an asymmetric research boundary. The current self-contained positional record does not establish secret reconstruction. | +| Q13 — Eight placements per byte | Superseded. One retained message-origin and separate round-specific byte-axis frames now support the sequence cycle. | +| Q14 — Additional eight ciphertext bits | Superseded. No 16-bit-per-byte mapping or fixed 2x expansion follows from this construction; all records are counted. | + +## Usage + +Run `python cycle.py demo --sources /checkouts`. Change supported operating choices +through an explicit profile; do not silently change a named profile's meaning. +The current next mathematical question is the private/public relation, not another +request to decide what a byte means or where an arbitrary circle must be placed. ## hmmm -The actual positional cipher remains unimplemented. These are the next proposed -construction answers, not another round of declarations that it already works. +Short prime-recipe discovery, private/public asymmetry, meaningful secret key-space +analysis, authentication/replay and security evidence remain unfinished. Passing +construction/recovery tests does not settle those questions. diff --git a/research/weave/SPECIFICATION.md b/research/weave/SPECIFICATION.md index 90cd907..6d8e8bd 100644 --- a/research/weave/SPECIFICATION.md +++ b/research/weave/SPECIFICATION.md @@ -1,6 +1,11 @@ # Weave — preserved architecture and specification floor -Status: **PRE-SPECIFICATION / FULL-ARCHITECTURE PRESERVATION**. +Status: **CORRECTED SEQUENCE CYCLE IMPLEMENTED AS AN EXPLICIT PROFILE; ASYMMETRY UNIMPLEMENTED**. + +The latest sequence/occurrence clarification in section 5.1 controls over older +bit-per-circle interpretations. [CYCLE.md](CYCLE.md) records the executable native +normalization/affixiation/interleave cycle and all candidate choices. Other historical +layers below remain preserved objectives, not automatic claims about that profile. This document records the construction that must survive implementation. It separates fixed architecture from unresolved mechanics. An unresolved mechanic must remain @@ -32,7 +37,8 @@ These layers are analytically separable but belong to one intended cryptosystem. Let `S = s_0, s_1, ..., s_(n-1)`. -Define the preserved end-interleave ordering: +The historical transport experiment retains this last-first ordering. The current +cycle selects `first-last` explicitly in its profile and also supports `last-first`: ```text I(S) = s_(n-1), s_0, s_(n-2), s_1, ... @@ -77,7 +83,9 @@ no one thread == complete recoverable plaintext complete recovery requires the declared thread relation ``` -The exact split/recombination law remains unresolved. +The current cycle constructs seven native occurrence-circle streams and exactly +recombines their sequence positions. Their equivalence to every earlier +cryptographic-thread proposal is not asserted; that broader relation remains open. ## 4. Corpus/material participation @@ -87,7 +95,9 @@ contemplated include literary, musical, technical-manual, and sound material. The material is intended to participate in construction/recovery. Treating it as an unused label or merely hashing its filename does not satisfy this architecture. -Exact extraction, addressing, mixing, and recovery dependence remain unresolved. +The current cycle implements actual cyclic corpus-bit extraction at a configured +bit offset, bucket normalization, and exact reverse checks. The broader private +corpus/key binding remains unimplemented. ## 5. Hyperspace / gonol layer @@ -128,18 +138,20 @@ Prime-index continuation and embedded prime spans preserve route and occurrence information. [NUMERAL_CONSTRUCTION.md](NUMERAL_CONSTRUCTION.md) records its implemented boundary, full size accounting, exact replay, and usage. -The numeral module is an explicitly selected representation layer. Its supplied -origin identifiers, angular attachments, and occurrence-circle numbers do not -constitute a replacement UCNS/UCHC constructor. It does not implement repeated- -sequence discovery, corpus normalization, a whole-cycle schedule, or encryption. +The numeral module remains a representation layer, not replacement geometry. +The separate cycle now connects automatic repeated-sequence discovery, actual +corpus normalization, native UCHC/UCNS sequence closure and coordinate recovery, +prime-route split derivation, and the repeated first/last bit interleave. See +[CYCLE.md](CYCLE.md) for the exact named profile, inverse, tests and byte accounting. +This implements the corrected sequence cycle, not public-key encryption. No fixed total ciphertext expansion is inferred from a short Unicode reference. The earlier twofold-length proposal requires accounting over the completed cycle. The existing 720-degree return and key-set-specific space relationships remain preserved. Earlier whole-plus-one public-view research is not promoted into an asymmetric result by this codec. [EIGHT_CIRCLE.md](EIGHT_CIRCLE.md) records the -prior byte/star repair; its bit-per-circle interpretation is historical rather -than the active rule for the corrected sequence cycle. No discarded PR #73/#74 +prior byte/star repair; its bit-per-circle interpretation is historical and its obsolete modules/tests +are removed in favor of the native sequence cycle. No discarded PR #73/#74 encoder or attack is revived. All other declared system layers remain. ## 6. Asymmetric relation @@ -220,14 +232,16 @@ python probe.py --check receipt.json ## hmmm -1. Uneven partition selection rule; the component runner accepts explicit positive - section lengths without selecting a remainder policy. -2. Whether each arity stage consumes the preceding stage output or composes independent - partitions before a later merge. The runner's sequential choice is an explicitly - labelled experiment, not a newly inferred user law. -3. Exact thread split/recombine law. -4. Corpus/material extraction and binding law. -5. Nested gonol representation and reversible serialization. +1. Prime-route ranked compositions implement uneven partitions for the named cycle + profile; other determinant profiles remain open. +2. The current cycle consumes preceding-round output, as clarified. Historical + alternatives are not current prerequisites or silently selected rules. +3. Seven native occurrence streams reconstruct exactly in the current profile; + broader cryptographic-thread dependence remains unestablished. +4. Corpus-tail normalization is implemented as an explicit profile; broader + cryptographic corpus binding remains open. +5. The native binary sequence profile now supplies scoped origin/axis closure and + reversible serialization; general nested-gonol and private-key use remain separate. 6. Private-gonol generation and binding. 7. Public/private key derivation and the exact source of asymmetry. 8. Authentication, nonces, state evolution, truncation/replay handling. diff --git a/research/weave/affixiation.py b/research/weave/affixiation.py new file mode 100644 index 0000000..230c570 --- /dev/null +++ b/research/weave/affixiation.py @@ -0,0 +1,223 @@ +# === MODULE_BUILD === +# id: weave_sequence_discovery +# module_name: affixiation +# module_kind: engine +# summary: deterministic repeated-multibyte discovery and exact nonoverlapping partition for the Weave sequence cycle +# owner: Erin Spencer +# public_surface: Partition, discover +# internal_surface: suffix array, LCP intervals, occurrence selection +# auth_boundary: none +# storage_boundary: none +# network_boundary: none +# user_data_boundary: read +# admin_only: false +# tests: tests/test_cycle.py +# rollout: explicitly selected maximal-lcp-longest-first-v1 profile +# rollback: remove this module and its round-runner binding +# unresolved: selection is a declared executable profile, not optimal compression or a universal affixiation rule +# === END MODULE_BUILD === +# === CONTRACTS === +# id: discovery_exact_partition +# given: an admitted byte stream +# then: selected repeated sequences and residual literals partition every source byte exactly once in order +# id: discovery_repeat_selection +# given: competing and overlapping repeated multi-byte candidates +# then: deterministic longest-first selection records nonoverlapping occurrences without erasing residual data +# id: discovery_resource_refusal +# given: input or candidate-visit work exceeding the configured budget +# then: refuse instead of silently truncating the candidate search +# === END CONTRACTS === +"""Usage: partition = discover(raw_bytes); partition.restore() == raw_bytes. + +This explicit profile selects maximal LCP candidates, longest first, breaking ties +by earliest source occurrence. When overlaps compete it retains leftmost available +nonoverlapping occurrences. Occurrence counts mean selected partition occurrences, +not every possible overlapping substring match. No maximum sequence-length heuristic +or fixed byte chunks are imposed. Residual runs remain exact literal definitions. +This module discovers participants; UCHC closes them using native UCNS geometry. +""" +from __future__ import annotations + +from dataclasses import dataclass, field +from numeral import Refused, ResourceLimit + +PROFILE = 'maximal-lcp-longest-first-v1' + + +@dataclass(frozen=True) +class Partition: + blocks: tuple[bytes, ...] = field(repr=False) + order: tuple[int, ...] + starts: tuple[int, ...] + candidate_visits: int + + def restore(self) -> bytes: + return b''.join(self.blocks[i] for i in self.order) + + @property + def repeat_ids(self) -> tuple[int, ...]: + counts = [0] * len(self.blocks) + for i in self.order: + counts[i] += 1 + return tuple(i for i, b in enumerate(self.blocks) if len(b) >= 2 and counts[i] >= 2) + + +def _suffix_array(data: bytes) -> list[int]: + n = len(data) + suffixes, ranks, width = list(range(n)), list(data), 1 + while width < n: + suffixes.sort(key=lambda i: (ranks[i], ranks[i+width] if i+width < n else -1)) + next_ranks = [0] * n + for j in range(1, n): + a, b = suffixes[j-1], suffixes[j] + different = ((ranks[a], ranks[a+width] if a+width < n else -1) != + (ranks[b], ranks[b+width] if b+width < n else -1)) + next_ranks[b] = next_ranks[a] + int(different) + ranks = next_ranks + if ranks[suffixes[-1]] == n-1: + break + width *= 2 + return suffixes + + +def _lcp(data: bytes, suffixes: list[int]) -> list[int]: + n, common = len(data), 0 + rank, values = [0]*n, [0]*n + for i, pos in enumerate(suffixes): + rank[pos] = i + for start in range(n): + i = rank[start] + if i == 0: + common = 0 + continue + other = suffixes[i-1] + while start+common < n and other+common < n and data[start+common] == data[other+common]: + common += 1 + values[i] = common + common = max(0, common-1) + return values + + +class _Ranges: + """Range min/max over suffix starts, with linear storage.""" + def __init__(self, values: list[int]): + self.size = 1 << max(0, (len(values)-1).bit_length()) + self.low = [len(values)] * (2*self.size) + self.high = [-1] * (2*self.size) + self.low[self.size:self.size+len(values)] = values + self.high[self.size:self.size+len(values)] = values + for i in range(self.size-1, 0, -1): + self.low[i] = min(self.low[2*i], self.low[2*i+1]) + self.high[i] = max(self.high[2*i], self.high[2*i+1]) + + def query(self, start: int, end: int) -> tuple[int, int]: + low, high = self.size, -1 + start, end = start+self.size, end+self.size + while start < end: + if start & 1: + low, high = min(low, self.low[start]), max(high, self.high[start]) + start += 1 + if end & 1: + end -= 1 + low, high = min(low, self.low[end]), max(high, self.high[end]) + start //= 2 + end //= 2 + return low, high + + +class _Occupied: + def __init__(self, size: int): + self.tree = [0] * (size+1) + + def prefix(self, end: int) -> int: + total = 0 + while end: + total += self.tree[end] + end -= end & -end + return total + + def free(self, start: int, length: int) -> bool: + return self.prefix(start+length) == self.prefix(start) + + def mark(self, start: int, length: int) -> None: + # Each source byte is marked at most once during a complete run. + for pos in range(start+1, start+length+1): + while pos < len(self.tree): + self.tree[pos] += 1 + pos += pos & -pos + + +def discover(data: bytes, *, max_bytes: int = 1048576, + visit_budget: int = 16000000) -> Partition: + """Discover a complete deterministic partition or refuse its resource profile.""" + if type(data) is not bytes: + raise Refused('byte-aligned source required for sequence discovery') + if any(type(v) is not int or v < 1 for v in (max_bytes, visit_budget)): + raise Refused('positive integer discovery budgets required') + if len(data) > max_bytes: + raise ResourceLimit('sequence source exceeds discovery byte budget') + n = len(data) + if n == 0: + return Partition((), (), (), 0) + suffixes = _suffix_array(data) + common = _lcp(data, suffixes) + ranges = _Ranges(suffixes) + stack, candidates = [], [] + for i in range(1, n+1): + depth = common[i] if i < n else 0 + left = i-1 + while stack and stack[-1][0] > depth: + length, begin = stack.pop() + first, last = ranges.query(begin, i) + length = min(length, last-first) # At least two disjoint occurrences. + if length >= 2: + candidates.append((-length, first, begin, i)) + left = begin + if depth and (not stack or stack[-1][0] < depth): + stack.append((depth, left)) + candidates.sort() + occupied = _Occupied(n) + chosen, patterns = [], set() + covered = visits = 0 + for negative, first, begin, end in candidates: + length = -negative + if n-covered < 2*length: + continue + visits += end-begin + if visits > visit_budget: + raise ResourceLimit('candidate occurrence visits exceed discovery budget') + selected, previous_end = [], -1 + for start in sorted(suffixes[begin:end]): + if start >= previous_end and occupied.free(start, length): + selected.append(start) + previous_end = start+length + if len(selected) < 2: + continue + block = data[selected[0]:selected[0]+length] + if block in patterns: + continue + patterns.add(block) + for start in selected: + chosen.append((start, length)) + occupied.mark(start, length) + covered += length + if covered == n: + break + chosen.sort() + segments, offset = [], 0 + for start, length in chosen: + if start > offset: + segments.append((offset, start-offset)) + segments.append((start, length)) + offset = start+length + if offset < n: + segments.append((offset, n-offset)) + blocks, order, starts, ids = [], [], [], {} + for start, length in segments: + block = data[start:start+length] + if block not in ids: + ids[block] = len(blocks) + blocks.append(block) + order.append(ids[block]) + starts.append(start) + return Partition(tuple(blocks), tuple(order), tuple(starts), visits) diff --git a/research/weave/cycle.py b/research/weave/cycle.py new file mode 100644 index 0000000..2709c4c --- /dev/null +++ b/research/weave/cycle.py @@ -0,0 +1,488 @@ +# === MODULE_BUILD === +# id: weave_iterative_sequence_cycle +# module_name: cycle +# module_kind: engine +# summary: corpus normalization once followed by native repeated-sequence affixiation and prime-determined bit interleaving, with exact reverse recovery +# owner: Erin Spencer +# public_surface: CycleLimits, RoundSpec, Profile, normalize, denormalize, affix, unaffix, forward, reverse, main +# internal_surface: versioned research records and complete size accounting +# auth_boundary: none +# storage_boundary: write +# network_boundary: none +# user_data_boundary: read +# admin_only: false +# tests: tests/test_cycle.py +# rollout: explicit research cycle API and CLI; no full-profile cipher promotion +# rollback: remove cycle, profile, tests, native binding and workflow additions +# requires: weave_numeral_construction, weave_sequence_discovery, weave_prime_schedule, weave_native_binary_binding +# unresolved: private/public key generation and cryptographic security; automatic short prime-recipe search +# === END MODULE_BUILD === +# === CONTRACTS === +# id: cycle_normalize_once +# given: exact message bytes and actual corpus bytes +# then: normalize once into a corpus-filled bit-length bucket with recoverable original length +# id: cycle_reversible_rounds +# given: an admitted profile with native inputs +# then: reverse every round from the final record, corpus and profile without the original message or encoder trace +# id: cycle_native_occurrence_recovery +# given: seven occurrence-circle streams and their native complete positions +# then: inverse native displacement restores ordered byte-sequence placement at the retained message-origin +# id: cycle_complete_accounting +# given: a complete cycle record +# then: count normalization, dictionaries, references, coordinates and outer framing without asserting a fixed expansion +# id: cycle_strict_refusal +# given: malformed records, mismatched configuration/material, or exhausted budgets +# then: refuse clearly without overwrite, partial recovery or silent truncation +# === END CONTRACTS === +"""Usage: python cycle.py demo --sources /checkouts + python cycle.py forward INPUT OUTPUT --profile cycle-profile.json --corpus CORPUS --sources /checkouts + python cycle.py reverse INPUT OUTPUT --profile cycle-profile.json --corpus CORPUS --sources /checkouts + +/checkouts contains source-locked ucns/ and uchc/ trees; CYCLE_NATIVE.json names +exact modules. No packages beyond Python's standard library are required by this +consumer. The runnable profile is explicit construction research, not secure storage. +The newest sequence cycle replaces the historical one-bit-per-circle interpretation. +""" +from __future__ import annotations + +import argparse +from collections import Counter +from dataclasses import dataclass, field +from fractions import Fraction +from hashlib import sha256 +from pathlib import Path +import json +import os + +from numeral import (BitBlock, Entry, Packet, PrimePath, Limits, Refused, ResourceLimit, + encode as encode_numeral, decode as decode_numeral, + accounting, _Reader, _uint, _blob, _Primes) +from affixiation import discover, PROFILE as SELECTION_PROFILE +from prime_schedule import Route, plan, interleave, PROFILE as SPLIT_PROFILE +from cycle_native import load_native + +ROOT = Path(__file__).resolve().parent +MAGIC, AFFIX_MAGIC, NORMAL_MAGIC = b'WVC\x01', b'WAF\x01', b'WNM\x01' +PROFILE_SCHEMA = 'weave.sequence-cycle-profile/v1' + + +def canonical(obj) -> bytes: + return json.dumps(obj, sort_keys=True, separators=(',', ':'), + ensure_ascii=False, allow_nan=False).encode('utf-8') + + +def strict_json(data: bytes): + def pairs(items): + result = {} + for key,value in items: + if key in result: + raise Refused('duplicate JSON key') + result[key] = value + return result + try: + return json.loads(data.decode('utf-8'), object_pairs_hook=pairs, + parse_constant=lambda s: (_ for _ in ()).throw(Refused('nonfinite JSON'))) + except (UnicodeError, ValueError, RecursionError) as exc: + if isinstance(exc, Refused): + raise + raise Refused('invalid profile JSON') from exc + + +def _fields(value, fields): + if type(value) is not dict or set(value) != set(fields): + raise Refused('profile fields do not match the selected schema') + + +def _positive(value, maximum: int, name: str, *, zero: bool = False): + if type(value) is not int or not (0 if zero else 1) <= value <= maximum: + raise Refused(f'{name} outside its admitted integer domain') + return value + + +@dataclass(frozen=True) +class CycleLimits: + input_bytes: int = 262144 + round_bytes: int = 1048576 + rounds: int = 32 + discovery_visits: int = 16000000 + prime_work: int = 16000000 + native_occurrences: int = 131072 + + def __post_init__(self): + if any(type(v) is not int or v < 1 for v in vars(self).values()): + raise Refused('positive integer execution budgets required') + + def numeral(self): + return Limits(output_bits=self.round_bytes*8, wire_bytes=self.round_bytes, + occurrences=self.round_bytes, prime_work=self.prime_work) + + +@dataclass(frozen=True) +class RoundSpec: + path: PrimePath + arities: tuple[int, ...] + circle_order: tuple[int, ...] + spaces: tuple[Fraction, ...] + end_order: str + + def __post_init__(self): + if type(self.path) is not PrimePath or type(self.path.steps) is not tuple: + raise Refused('exact immutable PrimePath required') + if (type(self.arities) is not tuple or not self.arities + or any(type(a) is not int or not 2 <= a <= 64 for a in self.arities) + or len(set(self.arities)) != len(self.arities)): + raise Refused('distinct candidate section arities must be integers 2..64') + if (type(self.circle_order) is not tuple or len(self.circle_order) != 7 + or any(type(c) is not int for c in self.circle_order) + or set(self.circle_order) != set(range(1,8))): + raise Refused('circle_order must be a permutation of 1..7') + if (type(self.spaces) is not tuple or len(self.spaces) != 8 + or any(type(x) is not Fraction or not 0 <= x < 2 or x.denominator >= 2**32 + for x in self.spaces)): + raise Refused('eight exact canonical space turns with denominators below 2^32 required') + if self.end_order not in ('first-last','last-first'): + raise Refused('explicit first-last or last-first order required') + + def as_dict(self): + return {'prime_path': {'seed': self.path.seed, 'steps': [list(x) for x in self.path.steps]}, + 'arities': list(self.arities), 'circle_order': list(self.circle_order), + 'spaces': [[x.numerator,x.denominator] for x in self.spaces], + 'end_order': self.end_order} + + +@dataclass(frozen=True) +class Profile: + scope: str + bucket_bytes: int + corpus_bit_offset: int + rounds: tuple[RoundSpec, ...] + + def __post_init__(self): + if type(self.scope) is not str or not self.scope or len(self.scope.encode('utf-8')) > 1024: + raise Refused('nonempty UTF-8 message scope of at most 1024 bytes required') + _positive(self.bucket_bytes, 1048576, 'normalization bucket') + _positive(self.corpus_bit_offset, 2**64-1, 'corpus bit offset', zero=True) + if type(self.rounds) is not tuple or not self.rounds or any(type(r) is not RoundSpec for r in self.rounds): + raise Refused('nonempty immutable round specification required') + + def as_dict(self): + return {'schema': PROFILE_SCHEMA, 'selection': SELECTION_PROFILE, 'split': SPLIT_PROFILE, + 'scope': self.scope, 'bucket_bytes': self.bucket_bytes, + 'corpus_bit_offset': self.corpus_bit_offset, 'rounds': [r.as_dict() for r in self.rounds]} + + @property + def identity(self): + return sha256(canonical(self.as_dict())).digest() + + @classmethod + def read(cls, data: bytes, limits: CycleLimits = CycleLimits()): + if type(data) is not bytes or len(data) > 65536: + raise Refused('profile must be at most 65536 bytes') + obj = strict_json(data) + _fields(obj, ('schema','selection','split','scope','bucket_bytes','corpus_bit_offset','rounds')) + if obj['schema'] != PROFILE_SCHEMA or obj['selection'] != SELECTION_PROFILE or obj['split'] != SPLIT_PROFILE: + raise Refused('unsupported profile construction') + if type(obj['rounds']) is not list or not obj['rounds']: + raise Refused('round list required') + if len(obj['rounds']) > limits.rounds: + raise ResourceLimit('profile exceeds round budget') + rounds = [] + for row in obj['rounds']: + _fields(row, ('prime_path','arities','circle_order','spaces','end_order')) + path = row['prime_path'] + _fields(path, ('seed','steps')) + if type(path['steps']) is not list or any(type(x) is not list for x in path['steps']): + raise Refused('prime steps must be arrays') + for name in ('arities','circle_order','spaces'): + if type(row[name]) is not list: + raise Refused(f'{name} must be an array') + spaces = [] + for pair in row['spaces']: + if type(pair) is not list or len(pair) != 2 or any(type(x) is not int for x in pair): + raise Refused('space coordinate requires numerator/denominator integers') + if pair[0] < 0 or not 1 <= pair[1] < 2**32: + raise Refused('space coordinate outside rational wire domain') + value = Fraction(*pair) + if [value.numerator,value.denominator] != pair: + raise Refused('space fraction must be canonical') + spaces.append(value) + rounds.append(RoundSpec(PrimePath(path['seed'],tuple(tuple(s) for s in path['steps'])), + tuple(row['arities']),tuple(row['circle_order']),tuple(spaces),row['end_order'])) + return cls(obj['scope'],obj['bucket_bytes'],obj['corpus_bit_offset'],tuple(rounds)) + + +def _corpus_bytes(corpus: bytes, count: int, bit_offset: int) -> bytes: + if type(corpus) is not bytes or not corpus: + raise Refused('actual nonempty corpus bytes required') + start, shift = divmod(bit_offset % (8*len(corpus)), 8) + if shift == 0: + whole = corpus[start:] + corpus[:start] + return (whole*((count+len(whole)-1)//len(whole)))[:count] + return bytes(((corpus[(start+i)%len(corpus)] << shift) & 255) + | (corpus[(start+i+1)%len(corpus)] >> (8-shift)) for i in range(count)) + + +def normalize(message: bytes, corpus: bytes, profile: Profile, + limits: CycleLimits = CycleLimits()) -> bytes: + """Selected corpus-tail bucket profile; original length remains inside the cycle.""" + if type(message) is not bytes: + raise Refused('exact message bytes required') + if len(message) > limits.input_bytes: + raise ResourceLimit('message exceeds input byte budget') + if type(corpus) is not bytes or not corpus: + raise Refused('actual nonempty corpus bytes required') + if len(corpus) > limits.round_bytes: + raise ResourceLimit('corpus exceeds input material budget') + header = NORMAL_MAGIC + len(message).to_bytes(8,'big') + sha256(corpus).digest() + occupied = len(header)+len(message) + target = (occupied//profile.bucket_bytes+1)*profile.bucket_bytes + if target > limits.round_bytes: + raise ResourceLimit('normalized frame exceeds round byte budget') + return header + message + _corpus_bytes(corpus,target-occupied,profile.corpus_bit_offset) + + +def denormalize(data: bytes, corpus: bytes, profile: Profile, + limits: CycleLimits = CycleLimits()) -> bytes: + if type(data) is not bytes or len(data) < 45 or data[:4] != NORMAL_MAGIC: + raise Refused('invalid normalized source frame') + if type(corpus) is not bytes or not corpus: + raise Refused('actual nonempty corpus required') + length = int.from_bytes(data[4:12],'big') + if length > limits.input_bytes or len(data) > limits.round_bytes or len(corpus) > limits.round_bytes: + raise ResourceLimit('normalization recovery exceeds byte budget') + occupied = 44+length + if occupied >= len(data) or len(data) != (occupied//profile.bucket_bytes+1)*profile.bucket_bytes: + raise Refused('normalized bucket or original length mismatch') + if data[12:44] != sha256(corpus).digest(): + raise Refused('corpus source identity differs') + if data[occupied:] != _corpus_bytes(corpus,len(data)-occupied,profile.corpus_bit_offset): + raise Refused('corpus normalization content differs') + return data[44:occupied] + + +def _symbol(index: int) -> str: + if index < 6400: + return chr(0xE000+index) + index -= 6400 + if index < 65534: + return chr(0xF0000+index) + index -= 65534 + if index < 65534: + return chr(0x100000+index) + raise ResourceLimit('private-use symbol inventory exhausted') + + +def _fraction(reader: _Reader) -> Fraction: + numerator, denominator = reader.uint(),reader.uint() + if denominator == 0: + raise Refused('zero coordinate denominator') + value = Fraction(numerator,denominator) + if not 0 <= value < 2 or (value.numerator,value.denominator) != (numerator,denominator): + raise Refused('noncanonical complete-circle position') + return value + + +def affix(data: bytes, *, api, geometry, scope: str, root: str, round_id: int, + spec: RoundSpec, limits: CycleLimits = CycleLimits()): + partition = discover(data,max_bytes=limits.round_bytes,visit_budget=limits.discovery_visits) + if len(partition.order) > limits.native_occurrences: + raise ResourceLimit('native occurrence count exceeds execution budget') + origin = api.ByteOrigin(data,scope,round_id,geometry,root) + circles = tuple(spec.circle_order[i%7] for i in range(len(partition.blocks))) + table = api.close_sequences(origin,partition.blocks,partition.order,circles,spec.spaces) + entries = tuple(Entry(_symbol(i),BitBlock.from_bytes(d.data),geometry.lift(d.state),circles[i]) + for i,d in enumerate(table.definitions)) + wire_items = table.wire_occurrences() + symbols = ''.join(entries[i].symbol for i,_ in wire_items) + packet = Packet(origin.identity,round_id,entries,symbols) + encoded = encode_numeral(packet,limits.numeral()) + counts = tuple(sum(o.circle==c for _,o in wire_items) for c in range(1,8)) + out = bytearray(AFFIX_MAGIC + _uint(len(data)) + _blob(encoded)) + for count in counts: + out.extend(_uint(count)) + for _,occurrence in wire_items: + position = geometry.lift(occurrence.source_state) + out.extend(_uint(position.numerator)+_uint(position.denominator)) + if len(out) > limits.round_bytes: + raise ResourceLimit('affixiation coordinates exceed round byte budget') + if len(out) > limits.round_bytes: + raise ResourceLimit('affixiation frame exceeds round byte budget') + counts_by_id = Counter(partition.order) + stats = {'input_bytes':len(data),'definitions':len(entries), + 'repeated_definitions':len(partition.repeat_ids),'occurrences':len(symbols), + 'repeated_occurrences':sum(counts_by_id[i] for i in partition.repeat_ids), + 'longest_definition_bytes':max(map(len,partition.blocks),default=0), + 'discovery_candidate_visits':partition.candidate_visits, + 'numeral_bytes':len(encoded),'native_coordinate_and_frame_bytes':len(out)-len(encoded), + 'output_bytes':len(out)} + return bytes(out),stats + + +def unaffix(data: bytes, *, api, geometry, scope: str, root: str, round_id: int, + spec: RoundSpec, limits: CycleLimits = CycleLimits()) -> bytes: + if type(data) is not bytes: + raise Refused('affixiation input must be bytes') + if len(data) > limits.round_bytes: + raise ResourceLimit('affixiation input exceeds byte budget') + reader = _Reader(data) + if reader.take(4) != AFFIX_MAGIC: + raise Refused('unrecognized affixiation frame') + source_length = reader.uint() + if source_length > limits.round_bytes: + raise ResourceLimit('affixiation source length exceeds byte budget') + packet = decode_numeral(reader.blob(limits.round_bytes),limits.numeral()) + if packet.round_id != round_id: + raise Refused('round identity mismatch') + # decode_numeral already validates/replays recipes under one budget. Do not + # reset that budget by replaying them again just to sum the output length. + by_symbol = {e.symbol: e for e in packet.entries} + if sum(by_symbol[s].block.length for s in packet.symbols) != source_length*8: + raise Refused('affixiation source bit length mismatch') + if len(packet.symbols) > limits.native_occurrences: + raise ResourceLimit('native occurrence count exceeds execution budget') + if any(e.block.length % 8 for e in packet.entries): + raise Refused('cycle affixiation definitions must be byte aligned') + counts = tuple(reader.uint() for _ in range(7)) + if sum(counts) != len(packet.symbols) or len(packet.symbols) > source_length: + raise Refused('circle counts disagree with occurrence stream') + positions = tuple(_fraction(reader) for _ in packet.symbols) + if reader.pos != len(data): + raise Refused('trailing affixiation bytes') + ids = {e.symbol:i for i,e in enumerate(packet.entries)} + if tuple(e.symbol for e in packet.entries) != tuple(_symbol(i) for i in range(len(packet.entries))): + raise Refused('cycle reference inventory is not canonical') + circles = tuple(e.circle for e in packet.entries) + if circles != tuple(spec.circle_order[i%7] for i in range(len(packet.entries))): + raise Refused('definition circle assignments disagree with profile') + table = api.recover_sequences(geometry,scope=scope,message_origin=root,round_id=round_id, + origin_identity=packet.origin,byte_length=source_length, + blocks=tuple(e.block.to_bytes() for e in packet.entries),circles=circles,spaces=spec.spaces, + counts=counts,wire_order=tuple(ids[s] for s in packet.symbols),source_turns=positions, + max_bytes=limits.round_bytes) + if tuple(geometry.lift(d.state) for d in table.definitions) != tuple(e.angle for e in packet.entries): + raise Refused('whole-circle sequence attachments do not reconstruct') + return table.restore() + + +def _prepared(profile: Profile, limits: CycleLimits): + if type(profile) is not Profile: + raise Refused('Profile required') + if len(profile.rounds) > limits.rounds: + raise ResourceLimit('round count exceeds execution budget') + engine = _Primes(limits.numeral()) + return tuple(Route.evaluate(s.path,limits.numeral(),engine=engine) for s in profile.rounds) + + +def _native_identity() -> bytes: + return sha256((ROOT/'CYCLE_NATIVE.json').read_bytes()).digest() + + +def forward(message: bytes, corpus: bytes, profile: Profile, sources: str | Path, + limits: CycleLimits = CycleLimits()): + """Run the stated cycle; return its full research record and size-only report.""" + routes = _prepared(profile,limits) + api,geometry = load_native(sources) + data = normalize(message,corpus,profile,limits) + root = api.ByteOrigin(data,profile.scope,0,geometry).message_origin + normalized_size = len(data) + rows = [] + for i,(spec,route) in enumerate(zip(profile.rounds,routes)): + packed,stats = affix(data,api=api,geometry=geometry,scope=profile.scope,root=root, + round_id=i,spec=spec,limits=limits) + split = plan(len(packed)*8,route,spec.arities) + data = interleave(packed,split,end_order=spec.end_order) + rows.append({'round':i,**stats,'permutation_bit_count':split.bit_length}) + header = MAGIC + _native_identity() + profile.identity + bytes.fromhex(root) + _uint(len(rows)) + wire = header + _blob(data) + if len(wire) > limits.round_bytes+128: + raise ResourceLimit('cycle record exceeds final byte budget') + report = {'schema':'weave.sequence-cycle-evidence/v1','classification':'CONSTRUCTION_EXPERIMENT', + 'source_bytes':len(message),'corpus_bytes':len(corpus),'normalized_bytes':normalized_size, + 'rounds':rows,'final_payload_bytes':len(data),'outer_frame_bytes':len(wire)-len(data), + 'total_bytes':len(wire),'security':'not established; no asymmetric key generation'} + return wire,report + + +def reverse(wire: bytes, corpus: bytes, profile: Profile, sources: str | Path, + limits: CycleLimits = CycleLimits()) -> bytes: + """Recover from final bytes, corpus, profile and fixed producer sources only.""" + if type(wire) is not bytes: + raise Refused('cycle record must be bytes') + if len(wire) > limits.round_bytes+128: + raise ResourceLimit('cycle record exceeds final byte budget') + routes = _prepared(profile,limits) + api,geometry = load_native(sources) + reader = _Reader(wire) + if reader.take(4) != MAGIC: + raise Refused('unrecognized cycle record') + if reader.take(32) != _native_identity() or reader.take(32) != profile.identity: + raise Refused('native source lock or profile identity mismatch') + root = reader.take(32).hex() + if reader.uint() != len(profile.rounds): + raise Refused('cycle round count mismatch') + data = reader.blob(limits.round_bytes) + if reader.pos != len(wire): + raise Refused('trailing cycle bytes') + for i in range(len(profile.rounds)-1,-1,-1): + spec,route = profile.rounds[i],routes[i] + split = plan(len(data)*8,route,spec.arities) + packed = interleave(data,split,inverse=True,end_order=spec.end_order) + data = unaffix(packed,api=api,geometry=geometry,scope=profile.scope,root=root, + round_id=i,spec=spec,limits=limits) + if api.ByteOrigin(data,profile.scope,0,geometry).message_origin != root: + raise Refused('message-origin root does not reconstruct') + return denormalize(data,corpus,profile,limits) + + +def _read(path: Path, limit: int) -> bytes: + with path.open('rb') as source: + data = source.read(limit+1) + if len(data) > limit: + raise ResourceLimit(f'file exceeds byte budget: {path.name}') + return data + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('command',choices=('demo','forward','reverse')) + parser.add_argument('input',nargs='?',type=Path) + parser.add_argument('output',nargs='?',type=Path) + parser.add_argument('--profile',type=Path,default=ROOT/'profiles/cycle-v1.json') + parser.add_argument('--corpus',type=Path) + parser.add_argument('--sources',type=Path,default=Path(os.environ.get('WEAVE_SOURCES',ROOT/'sources'))) + args = parser.parse_args() + try: + limits = CycleLimits() + profile = Profile.read(_read(args.profile,65536),limits) + if args.command == 'demo': + message = b'Weave repeats byte sequences. '*24 + bytes(range(64)) + corpus = bytes(range(256)) + b'nonsecret corpus material' + wire,report = forward(message,corpus,profile,args.sources,limits) + restored = reverse(wire,corpus,profile,args.sources,limits) + if restored != message: + raise Refused('demo failed exact recovery') + report['exact_recovery'] = True + print(json.dumps(report,indent=2)) + return 0 + if args.input is None or args.output is None or args.corpus is None: + raise Refused('input, output, and --corpus are required') + if args.output.exists(): + raise Refused('output already exists; refusing overwrite') + corpus = _read(args.corpus,limits.round_bytes) + if args.command == 'forward': + data = _read(args.input,limits.input_bytes) + output,report = forward(data,corpus,profile,args.sources,limits) + else: + data = _read(args.input,limits.round_bytes+128) + output = reverse(data,corpus,profile,args.sources,limits) + report = {'output_bytes':len(output),'standing':'exact construction recovery; not security evidence'} + with args.output.open('xb') as target: + target.write(output) + print(json.dumps(report,indent=2)) + return 0 + except (OSError, ValueError, UnicodeError) as exc: + parser.exit(2,f'refused: {exc}\n') + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/research/weave/cycle_native.py b/research/weave/cycle_native.py new file mode 100644 index 0000000..94885f5 --- /dev/null +++ b/research/weave/cycle_native.py @@ -0,0 +1,64 @@ +# === MODULE_BUILD === +# id: weave_native_binary_binding +# module_name: cycle_native +# module_kind: adapter +# summary: loads the exact UCHC binary producer which consumes the pinned native UCNS constructors +# owner: Erin Spencer +# public_surface: load_native +# internal_surface: locked source loading +# auth_boundary: none +# storage_boundary: read +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: tests/test_cycle.py +# rollout: required source-locked native dependency for the explicit cycle profile +# rollback: remove with the cycle consumer, never substitute fixture geometry +# === END MODULE_BUILD === +# === CONTRACTS === +# id: cycle_native_source_pinned +# given: supplied UCNS and UCHC checkouts +# then: execute only the declared exact source modules or refuse before processing data +# === END CONTRACTS === +"""Usage: api, geometry = load_native('/checkouts-containing-ucns-and-uchc'). + +Reads source-locked producer buffers and executes those exact buffers. There is +no network access, mutable upstream vendoring, or reconstruction from labels. +The producer's scoped constructors retain their own status and authority. +""" +from hashlib import sha1 +from pathlib import Path +from types import ModuleType +import json +import sys +from numeral import Refused + +ROOT = Path(__file__).resolve().parent + + +def load_native(sources: str | Path): + lock = json.loads((ROOT/'CYCLE_NATIVE.json').read_text()) + entry = lock['uchc_source'] + path = Path(sources)/'uchc'/entry['path'] + try: + with path.open('rb') as source: + data = source.read(65537) + except OSError as exc: + raise Refused(f'native UCHC source unavailable: {path}') from exc + actual = sha1(b'blob '+str(len(data)).encode()+b'\0'+data).hexdigest() + if len(data)>65536 or actual != entry['git_blob']: + raise Refused('native UCHC source does not match CYCLE_NATIVE.json') + name = '_weave_uchc_'+actual + if name not in sys.modules: + module = ModuleType(name) + module.__file__ = str(path) + sys.modules[name] = module + try: + exec(compile(data,str(path),'exec'),module.__dict__) + except BaseException: + sys.modules.pop(name,None) + raise + api = sys.modules[name] + if api.UCNS_COMMIT != lock['ucns_commit'] or api.UCNS_BLOBS != lock['ucns_sources']: + raise Refused('producer and consumer native dependency locks disagree') + return api, api.Geometry(Path(sources)/'ucns') diff --git a/research/weave/numeral.py b/research/weave/numeral.py index c5b9999..800f895 100644 --- a/research/weave/numeral.py +++ b/research/weave/numeral.py @@ -134,8 +134,10 @@ class PrimePath: steps: tuple[tuple, ...] def replay(self, limits: Limits = Limits(), *, _engine=None) -> tuple[int, ...]: - if type(self.steps) is not tuple or len(self.steps) > limits.recipe_steps: - raise ResourceLimit("recipe step budget or immutable steps required") + if type(self.steps) is not tuple: + raise Refused("recipe steps must be an immutable tuple") + if len(self.steps) > limits.recipe_steps: + raise ResourceLimit("recipe step budget exceeded") primes = _engine if _engine is not None else _Primes(limits) primes.charge(len(self.steps) + 1) value = _nat(self.seed) @@ -254,17 +256,21 @@ class Packet: entries: tuple[Entry, ...] symbols: str - def validate(self, limits: Limits = Limits()) -> int: + def validate(self, limits: Limits = Limits(), *, _engine=None) -> int: if type(self.origin) is not str or not self.origin or len(self.origin.encode("utf-8")) > 1024: raise Refused("nonempty UTF-8 origin scope of at most 1024 bytes required") _nat(self.round_id) - if type(self.entries) is not tuple or len(self.entries) > limits.entries: - raise ResourceLimit("immutable definitions within entry budget required") - if type(self.symbols) is not str or len(self.symbols) > limits.occurrences: + if type(self.entries) is not tuple: + raise Refused("definitions must be an immutable tuple") + if len(self.entries) > limits.entries: + raise ResourceLimit("definition count exceeds execution budget") + if type(self.symbols) is not str: + raise Refused("occurrence stream must be a string") + if len(self.symbols) > limits.occurrences: raise ResourceLimit("occurrence stream exceeds execution budget") mapping = {} angles = set() - primes = _Primes(limits) + primes = _engine if _engine is not None else _Primes(limits) for entry in self.entries: if type(entry) is not Entry: raise Refused("Entry required") @@ -466,7 +472,8 @@ def decode(data: bytes, limits: Limits = Limits()) -> Packet: if reader.pos != len(data): raise Refused("trailing packet data") packet = Packet(origin, round_id, tuple(entries), symbols) - if packet.validate(limits) != declared: + # Parsing and structural validation share one charged prime-work budget. + if packet.validate(limits, _engine=primes) != declared: raise Refused("declared output length disagrees with occurrences") return packet @@ -499,6 +506,14 @@ def demo() -> dict: return result +def _angle_argument(text: str) -> Fraction: + """Translate malformed CLI fractions into argparse's status-2 refusal.""" + try: + return Fraction(text) + except (ValueError, ZeroDivisionError) as exc: + raise argparse.ArgumentTypeError("angle must be an exact finite fraction") from exc + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) sub = parser.add_subparsers(dest="command", required=True) @@ -507,7 +522,7 @@ def main() -> int: bind.add_argument("input", type=Path) bind.add_argument("output", type=Path) bind.add_argument("--origin", required=True) - bind.add_argument("--angle", required=True, type=Fraction) + bind.add_argument("--angle", required=True, type=_angle_argument) bind.add_argument("--circle", required=True, type=int) recover = sub.add_parser("recover") recover.add_argument("input", type=Path) diff --git a/research/weave/prime_schedule.py b/research/weave/prime_schedule.py new file mode 100644 index 0000000..f8812f7 --- /dev/null +++ b/research/weave/prime_schedule.py @@ -0,0 +1,143 @@ +# === MODULE_BUILD === +# id: weave_prime_schedule +# module_name: prime_schedule +# module_kind: engine +# summary: executed prime-index and embedded-span routes determine nonempty bit partitions and reversible first/last interleaving +# owner: Erin Spencer +# public_surface: Route, SplitPlan, plan, interleave +# internal_surface: exact composition unranking +# auth_boundary: none +# storage_boundary: none +# network_boundary: none +# user_data_boundary: read +# admin_only: false +# tests: tests/test_cycle.py +# rollout: explicit prime-route-compositions-v1 profile only +# rollback: remove scheduler and its cycle bindings +# requires: weave_numeral_construction +# unresolved: distinct routes can induce identical maps; no key entropy or asymmetry claim +# === END MODULE_BUILD === +# === CONTRACTS === +# id: prime_split_replay +# given: a verified prime route, admissible arities and current bit length +# then: exact deterministic nonempty sections cover the stream and reproduce without source plaintext +# id: first_last_inverse +# given: admitted sections and an explicit first-last or last-first order +# then: every source bit appears once and reversing the operation restores exact bytes +# === END CONTRACTS === +"""Usage: route = Route.evaluate(PrimePath(53, (('next',),))) + split = plan(128, route, (3,5,7)) + assert interleave(interleave(data, split), split, inverse=True) == data + +This is a named candidate determinant, not a universal arity law. The actual +prime-index/span route is evaluated, including source, base, span and step order. +A prefix-bearing radix numeral of that route selects an arity and a ranked +composition. Routes remain distinct records even when their resulting maps agree. +""" +from __future__ import annotations +from dataclasses import dataclass +from math import comb +from numeral import PrimePath, Limits, Refused + +PROFILE = 'prime-route-compositions-v1' + + +@dataclass(frozen=True) +class Route: + path: PrimePath + trace: tuple[int, ...] + determinant: int + + @classmethod + def evaluate(cls, path: PrimePath, limits: Limits = Limits(), *, engine=None): + if type(path) is not PrimePath: + raise Refused('PrimePath required') + trace = path.replay(limits, _engine=engine) + tokens = [path.seed, len(path.steps)] + for step, value in zip(path.steps, trace[1:]): + tokens.extend((0, value) if step == ('next',) else (1, *step[1:], value)) + base = max(tokens)+2 + determinant = 1 # Retains leading zero tokens and sequence length. + for token in tokens: + determinant = determinant*base + token + return cls(path, trace, determinant) + + +@dataclass(frozen=True) +class SplitPlan: + bit_length: int + lengths: tuple[int, ...] + rank: int + + +def _composition(n: int, arity: int, rank: int) -> tuple[int, ...]: + """Unrank ordered nonempty sections via lexicographic gap combinations.""" + total = comb(n-1, arity-1) + if not 0 <= rank < total: + raise Refused('composition rank outside domain') + gaps, minimum, remaining = [], 1, arity-1 + while remaining: + low, high = minimum, n-remaining + all_remaining = comb(n-minimum, remaining) + while low < high: + middle = (low+high+1)//2 + skipped = all_remaining - comb(n-middle, remaining) + if skipped <= rank: + low = middle + else: + high = middle-1 + selected = low + rank -= all_remaining - comb(n-selected, remaining) + gaps.append(selected) + minimum, remaining = selected+1, remaining-1 + points = (0, *gaps, n) + return tuple(b-a for a,b in zip(points, points[1:])) + + +def plan(bit_length: int, route: Route, arities: tuple[int, ...]) -> SplitPlan: + if type(bit_length) is not int or bit_length < 1: + raise Refused('positive bit length required') + if type(route) is not Route: + raise Refused('executed Route required') + if (type(arities) is not tuple or not arities + or any(type(a) is not int or not 2 <= a <= min(64,bit_length) for a in arities) + or len(set(arities)) != len(arities)): + raise Refused('distinct admissible arities must be integers 2..min(64, bit_length)') + arity = arities[route.determinant % len(arities)] + rank = (route.determinant//len(arities)) % comb(bit_length-1, arity-1) + return SplitPlan(bit_length, _composition(bit_length, arity, rank), rank) + + +def interleave(data: bytes, split: SplitPlan, *, inverse: bool = False, + end_order: str = 'first-last') -> bytes: + """Rearrange bits; do not reinterpret the output as bytes until the pass ends.""" + if type(data) is not bytes or type(split) is not SplitPlan: + raise Refused('bytes and SplitPlan required') + if type(inverse) is not bool or end_order not in ('first-last','last-first'): + raise Refused('explicit Boolean inverse and supported end order required') + if (type(split.bit_length) is not int or split.bit_length != len(data)*8 + or type(split.lengths) is not tuple or not split.lengths + or any(type(n) is not int or n <= 0 for n in split.lengths) + or sum(split.lengths) != split.bit_length): + raise Refused('sections must cover every input bit exactly once') + out = bytearray(len(data)) + offset = 0 + for length in split.lengths: + left, right = offset, offset+length-1 + first = end_order == 'first-last' + for destination in range(offset, offset+length): + if first: + source = left + left += 1 + else: + source = right + right -= 1 + first = not first + if inverse: + read, write = destination, source + else: + read, write = source, destination + bit = (data[read//8] >> (7-read%8)) & 1 + out[write//8] |= bit << (7-write%8) + offset += length + return bytes(out) diff --git a/research/weave/profiles/cycle-v1.json b/research/weave/profiles/cycle-v1.json new file mode 100644 index 0000000..372d142 --- /dev/null +++ b/research/weave/profiles/cycle-v1.json @@ -0,0 +1,40 @@ +{ + "schema": "weave.sequence-cycle-profile/v1", + "selection": "maximal-lcp-longest-first-v1", + "split": "prime-route-compositions-v1", + "scope": "weave-cycle-demo", + "bucket_bytes": 256, + "corpus_bit_offset": 5, + "rounds": [ + { + "prime_path": { + "seed": 5381, + "steps": [["span", 10, 0, 2], ["next"], ["next"]] + }, + "arities": [3, 5, 7], + "circle_order": [3, 1, 7, 2, 6, 4, 5], + "spaces": [[0, 1], [1, 9], [2, 9], [1, 3], [4, 9], [5, 9], [2, 3], [7, 9]], + "end_order": "first-last" + }, + { + "prime_path": { + "seed": 2, + "steps": [["next"], ["next"], ["next"], ["next"], ["next"], ["next"], ["next"]] + }, + "arities": [3, 5, 7], + "circle_order": [3, 1, 7, 2, 6, 4, 5], + "spaces": [[2, 9], [1, 3], [4, 9], [5, 9], [2, 3], [7, 9], [8, 9], [1, 1]], + "end_order": "first-last" + }, + { + "prime_path": { + "seed": 313, + "steps": [["span", 10, 2, 1], ["next"], ["next"]] + }, + "arities": [3, 5, 7], + "circle_order": [3, 1, 7, 2, 6, 4, 5], + "spaces": [[4, 9], [5, 9], [2, 3], [7, 9], [8, 9], [1, 1], [10, 9], [11, 9]], + "end_order": "first-last" + } + ] +} diff --git a/research/weave/stages/eight_circle.py b/research/weave/stages/eight_circle.py deleted file mode 100644 index 16f4a5e..0000000 --- a/research/weave/stages/eight_circle.py +++ /dev/null @@ -1,202 +0,0 @@ -# === MODULE_BUILD === -# id: weave_byte_circle_structure -# module_name: eight_circle -# module_kind: schema -# summary: byte-scoped bit placements and whole-plus-one key views, without a substitute cipher -# owner: Erin Spencer -# public_surface: Circle, PublicPair, FullKeySet, BitPlacement, ByteConstruction, construct_byte -# internal_surface: strict input validation -# auth_boundary: none -# storage_boundary: none -# network_boundary: none -# user_data_boundary: read -# admin_only: false -# tests: tests/test_eight_circle.py -# rollout: replaces the incorrect PR 73 per-bit sheet encoder -# rollback: revert this repair as one transaction; do not reactivate the rejected cipher -# unresolved: approved native positional transform and ciphertext serialization -# === END MODULE_BUILD === -# === CONTRACTS === -# id: weave_byte_has_eight_circle_placements -# given: one byte and an explicit bit-to-circle assignment -# then: each source bit occurs exactly once among G0 through G7 -# class: correctness -# id: weave_public_pair_always_contains_whole -# given: a full eight-circle key-set record -# then: every public view is G0 plus exactly one of G1 through G7 -# class: correctness -# id: weave_positions_are_not_bit_codes -# given: exact key-set and placement coordinates -# then: their 720-degree coordinates are retained without converting each source bit into two sheet bits -# class: boundary -# === END CONTRACTS === -"""Byte structure, not a cipher or a replacement UCNS Gonol constructor. - -Usage: construct_byte(0xA5, full, positions, origin_id="message-1", byte_index=0). -The caller supplies eight exact positions and an explicit bit-to-circle assignment. -Positions are data in turns (360 degrees); 720 degrees is the complete return. -No formula deriving positions, native geometry, or ciphertext is invented here. -PublicPair is a key-view boundary, never a two-bit encoding of each source bit. -""" -from __future__ import annotations - -from dataclasses import dataclass, field -from fractions import Fraction - -TURN_720 = Fraction(2) - - -def _index(value: int, upper: int, name: str) -> None: - if type(value) is not int or not 0 <= value < upper: - raise ValueError(f"{name} must be an integer in [0, {upper - 1}]") - - -def _identity(value: str) -> None: - if type(value) is not str or not value.strip(): - raise ValueError("a nonempty identity is required") - - -def _phase(value: Fraction) -> Fraction: - if type(value) is not Fraction: - raise TypeError("positions must be exact Fraction values") - return value % TURN_720 - - -def _assignment(values: tuple[int, ...]) -> None: - if type(values) is not tuple or len(values) != 8: - raise ValueError("an explicit eight-member assignment is required") - for value in values: - _index(value, 8, "assignment member") - if set(values) != set(range(8)): - raise ValueError("every source bit and circle must occur exactly once") - - -@dataclass(frozen=True) -class Circle: - """Key-set circle binding; index 0 is the whole, indices 1..7 are parts. - - This stores the circle's exact space coordinate, not its complete native - space relation. A scalar offset does not implement that relation. - """ - index: int - identity: str - space: Fraction = field(repr=False) - - def __post_init__(self) -> None: - _index(self.index, 8, "circle index") - _identity(self.identity) - object.__setattr__(self, "space", _phase(self.space)) - - -@dataclass(frozen=True) -class PublicPair: - """Only the whole and one part; no full-key or plaintext reference.""" - circles: tuple[Circle, Circle] = field(repr=False) - - def __post_init__(self) -> None: - if (type(self.circles) is not tuple or len(self.circles) != 2 - or any(type(c) is not Circle for c in self.circles)): - raise ValueError("exactly two Circle records are required") - whole, part = self.circles - if whole.index != 0 or part.index == 0 or whole.identity == part.identity: - raise ValueError("public pair must be G0 followed by one distinct small circle") - - @property - def indices(self) -> tuple[int, int]: - return self.circles[0].index, self.circles[1].index - - -@dataclass(frozen=True) -class FullKeySet: - """Eight circle bindings plus caller-supplied assignment, not KeyGen. - - bit_to_circle[i] locates the source byte's ith bit (MSB first). No default - assignment, active-part sequence, or private-to-public transform is selected. - """ - circles: tuple[Circle, ...] = field(repr=False) - bit_to_circle: tuple[int, ...] = field(repr=False) - - def __post_init__(self) -> None: - if (type(self.circles) is not tuple or len(self.circles) != 8 - or any(type(c) is not Circle for c in self.circles)): - raise ValueError("the full record requires exactly eight Circle records") - if tuple(c.index for c in self.circles) != tuple(range(8)): - raise ValueError("store the whole first, followed by parts 1 through 7") - if len({c.identity for c in self.circles}) != 8: - raise ValueError("circle identities must be distinct") - _assignment(self.bit_to_circle) - - def degenerate(self, part_index: int) -> PublicPair: - """Select the established pair shape; this does not derive a trapdoor.""" - _index(part_index, 8, "part index") - if part_index == 0: - raise ValueError("select one small circle, not a second whole") - return PublicPair((self.circles[0], self.circles[part_index])) - - -@dataclass(frozen=True) -class BitPlacement: - """One source-bit occurrence assigned to one circle within a byte.""" - source_bit_index: int - circle_index: int - bit: int = field(repr=False) - position: Fraction = field(repr=False) - - def __post_init__(self) -> None: - _index(self.source_bit_index, 8, "source bit index") - _index(self.circle_index, 8, "circle index") - _index(self.bit, 2, "bit") - object.__setattr__(self, "position", _phase(self.position)) - - -@dataclass(frozen=True) -class ByteConstruction: - """One byte's eight placements at its message-origin occurrence. - - Source recovery here reads the retained construction. It is NOT decryption. - The whole carries one bit and remains addressable in every whole-part relation. - Evolution of its state is not implemented by an invented public-feedback rule. - """ - origin_id: str - byte_index: int - placements: tuple[BitPlacement, ...] = field(repr=False) - - def __post_init__(self) -> None: - _identity(self.origin_id) - if type(self.byte_index) is not int or self.byte_index < 0: - raise ValueError("byte index must be a nonnegative integer") - if (type(self.placements) is not tuple or len(self.placements) != 8 - or any(type(p) is not BitPlacement for p in self.placements)): - raise ValueError("one byte requires exactly eight BitPlacement records") - if tuple(p.circle_index for p in self.placements) != tuple(range(8)): - raise ValueError("each circle must carry exactly one placement") - _assignment(tuple(p.source_bit_index for p in self.placements)) - - @property - def source_value(self) -> int: - return sum(p.bit << (7 - p.source_bit_index) for p in self.placements) - - -def construct_byte(value: int, full: FullKeySet, positions: tuple[Fraction, ...], - *, origin_id: str, byte_index: int) -> ByteConstruction: - """Attach all eight bits together using supplied positions in circle order. - - Caller-supplied data is not an approved transform. This function emits a - plaintext-bearing construction record, never ciphertext or a public packet. - """ - _index(value, 256, "byte") - if type(full) is not FullKeySet: - raise TypeError("a full key-set record is required") - if type(positions) is not tuple or len(positions) != 8: - raise ValueError("supply all eight circle positions for the byte") - phases = tuple(_phase(p) for p in positions) - by_circle = sorted( - (circle_index, source_index) - for source_index, circle_index in enumerate(full.bit_to_circle) - ) - placements = tuple( - BitPlacement(source_index, circle_index, (value >> (7 - source_index)) & 1, - phases[circle_index]) - for circle_index, source_index in by_circle - ) - return ByteConstruction(origin_id, byte_index, placements) diff --git a/research/weave/stages/message_origin.py b/research/weave/stages/message_origin.py deleted file mode 100644 index 7567965..0000000 --- a/research/weave/stages/message_origin.py +++ /dev/null @@ -1,100 +0,0 @@ -# === MODULE_BUILD === -# id: weave_message_byte_origin -# module_name: message_origin -# module_kind: schema -# summary: one message origin containing ordered byte occurrences with eight-circle attachments -# owner: Erin Spencer -# public_surface: ByteOccurrence, MessageOrigin, construct_origin, construct_at -# internal_surface: strict occurrence and scope validation -# auth_boundary: none -# storage_boundary: none -# network_boundary: none -# user_data_boundary: read -# admin_only: false -# tests: tests/test_message_origin.py -# rollout: replaces PR 74 bit-axis feedback substitution -# rollback: revert this repair as one transaction without restoring the rejected cipher -# unresolved: native UCHC binding and full-byte keyed positional evolution -# === END MODULE_BUILD === -# === CONTRACTS === -# id: weave_message_origin_contains_bytes -# given: an admitted raw-byte message -# then: its origin contains one ordered occurrence per byte, not eight independent message axes per byte -# class: correctness -# id: weave_byte_occurrences_preserve_scope -# given: repeated byte values at one origin or equal messages at distinct named origins -# then: occurrence order and supplied origin identity remain distinct and recoverable -# class: correctness -# === END CONTRACTS === -"""Message -> byte occurrences -> eight-circle placements. - -Usage: origin = construct_origin(b"AA", identity="message-1") - byte_state = construct_at(origin, 0, full_key_record, eight_positions) - -These are Weave construction records, not a copied UCHC constructor or a new -UCNS geometry. An origin identifier scopes occurrences; it is not a hash of the -message, a nonce, or a source of cryptographic secrecy. Native origin/axis -integration and dynamic positional encryption remain unimplemented. -""" -from __future__ import annotations - -from dataclasses import dataclass, field -from fractions import Fraction -from .eight_circle import ByteConstruction, FullKeySet, construct_byte, _identity, _index - - -@dataclass(frozen=True) -class ByteOccurrence: - origin_id: str - index: int - value: int = field(repr=False) - - def __post_init__(self) -> None: - _identity(self.origin_id) - if type(self.index) is not int or self.index < 0: - raise ValueError("byte occurrence index must be nonnegative") - _index(self.value, 256, "byte") - - -@dataclass(frozen=True) -class MessageOrigin: - identity: str - bytesets: tuple[ByteOccurrence, ...] = field(repr=False) - - def __post_init__(self) -> None: - _identity(self.identity) - if (type(self.bytesets) is not tuple - or any(type(b) is not ByteOccurrence for b in self.bytesets)): - raise TypeError("message participants must be a tuple of byte occurrences") - for index, occurrence in enumerate(self.bytesets): - if occurrence.index != index or occurrence.origin_id != self.identity: - raise ValueError("byte occurrence order and origin must match this message") - - @property - def byte_length(self) -> int: - return len(self.bytesets) - - def recover_bytes(self) -> bytes: - """Read the exact retained source construction, not decrypt a ciphertext.""" - return bytes(b.value for b in self.bytesets) - - -def construct_origin(data: bytes, *, identity: str) -> MessageOrigin: - """Admit each raw byte once at its caller-named message origin.""" - if type(data) is not bytes: - raise TypeError("raw bytes required; no text normalization") - _identity(identity) - return MessageOrigin(identity, tuple( - ByteOccurrence(identity, index, value) for index, value in enumerate(data) - )) - - -def construct_at(origin: MessageOrigin, byte_index: int, full: FullKeySet, - positions: tuple[Fraction, ...]) -> ByteConstruction: - """Attach one whole byte, carrying its source occurrence into all placements.""" - if type(origin) is not MessageOrigin: - raise TypeError("a MessageOrigin is required") - _index(byte_index, origin.byte_length, "byte index") - occurrence = origin.bytesets[byte_index] - return construct_byte(occurrence.value, full, positions, - origin_id=origin.identity, byte_index=occurrence.index) diff --git a/research/weave/test.py b/research/weave/test.py index 1a89c13..efc45fd 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -"""Run the full Weave workspace suite and emit a source-bound receipt. +"""Run the full repository Weave suite and emit a source-bound receipt. -Usage: python test.py --receipt /tmp/weave-check.json -Transport behavior, earlier byte/star records, and numeral construction are tested. -Native positional encryption and native corpus replay remain explicitly unexecuted. +Usage: WEAVE_SOURCES=/checkouts python test.py --receipt /tmp/weave-check.json +The exact source lock is required for the native cycle tests. All remaining old +transport tests retain their scope; no cycle result establishes cipher security. +This runner belongs to the full Stack checkout, not the smaller standalone bundle. """ import argparse import hashlib @@ -16,12 +17,14 @@ from assembly import Pipeline ROOT = Path(__file__).resolve().parent +EXPECTED_TESTS = 100 # 52 original + 15 numeral + 3 review repair + 30 cycle. def snapshot(): paths = list(ROOT.rglob('*.py')) + list((ROOT/'profiles').glob('*.json')) + paths += [ROOT/'CYCLE_NATIVE.json', ROOT/'CYCLE_WORK_GRAPH.json'] return {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() - for p in sorted(paths) if '__pycache__' not in p.parts} + for p in sorted(set(paths)) if '__pycache__' not in p.parts and 'sources' not in p.parts} def main(): @@ -33,56 +36,56 @@ def main(): result = unittest.TextTestRunner(stream=report, verbosity=2).run( unittest.defaultTestLoader.discover(str(ROOT/'tests'))) after = snapshot() - # 74 existing methods + 15 exact numeral/reference/recipe methods. passed = (result.wasSuccessful() and not result.skipped and not result.expectedFailures and not result.unexpectedSuccesses - and result.testsRun == 89 and before == after) + and result.testsRun == EXPECTED_TESTS and before == after) receipt = { - 'schema':'weave.transport-evidence/v1', - 'status':'PASSED' if passed else 'FAILED', - 'python':platform.python_version(), - 'tests':result.testsRun, - 'failures':len(result.failures),'errors':len(result.errors),'skipped':len(result.skipped), - 'source_unchanged':before == after, - 'expected_test_methods':89, - 'coverage_status':'WITNESSED' if passed else 'NOT_ACCEPTED', - 'source_sha256':hashlib.sha256(json.dumps(before,sort_keys=True,separators=(',',':')).encode()).hexdigest(), - 'source_files':before, - 'coverage':{'binary_messages':8191,'binary_lengths_inclusive':[0,12], - 'assembly_switch_masks':512,'transport_switch_masks':32, - 'valid_transport_masks':28,'incompatible_transport_masks':4, - 'largest_roundtrip_bytes':65536,'separate_process_roundtrip_bytes':1026, - 'byte_values':256,'bit_to_circle_assignments':40320, - 'whole_part_key_views':7,'bit_placements_per_byte':8, - 'message_participants_per_byte':1, - 'numeral_short_blocks':8191,'numeral_large_block_bits':8388608, - 'numeral_repeated_roundtrip_bytes':4194304, - 'numeral_prime_path':[5381,53,241,1523]}, - 'declared_ciphertext_bits_per_byte':16, - 'declared_ciphertext_scope':'Earlier positional proposal only; not an inferred property of numeral construction or the corrected sequence cycle', - 'numeral_construction':'EXPLICIT_LAYER: length-bearing integers, supplied attachments, Unicode references, exact bounded prime replay, complete wire accounting', - 'positional_cipher':'NOT_IMPLEMENTED: no numeral test establishes native positional encryption', - 'full_weave':Pipeline().plan(), - 'native_corpus_replay':'NOT_EXECUTED: no constructed database materialized by this suite', - 'native_adapter_tests':'missing-input/source-identity refusal only; not successful real-corpus replay', - 'security_observations':[ - 'fixed-map recovery succeeds against the older transport candidate at 137 bits in 8 queries', - 'wrong material can yield wrong plaintext without an authentication error' + 'schema': 'weave.sequence-cycle-suite/v1', + 'status': 'PASSED' if passed else 'FAILED', + 'python': platform.python_version(), + 'tests': result.testsRun, + 'failures': len(result.failures), 'errors': len(result.errors), 'skipped': len(result.skipped), + 'expected_test_methods': EXPECTED_TESTS, + 'source_unchanged': before == after, + 'source_sha256': hashlib.sha256(json.dumps(before, sort_keys=True, separators=(',', ':')).encode()).hexdigest(), + 'source_files': before, + 'native_sources': json.loads((ROOT/'CYCLE_NATIVE.json').read_text()), + 'work_graph_sha256': json.loads((ROOT/'CYCLE_WORK_GRAPH.json').read_text())['work_graph_sha256'], + 'coverage_status': 'WITNESSED' if passed else 'NOT_ACCEPTED', + 'coverage': { + 'old_transport_binary_messages': 8191, 'assembly_switch_masks': 512, + 'transport_switch_masks': 32, 'valid_transport_masks': 28, + 'incompatible_transport_masks': 4, 'old_transport_largest_roundtrip_bytes': 65536, + 'numeral_short_bitblocks': 8191, 'numeral_large_block_bits': 8388608, + 'numeral_repeated_roundtrip_bytes': 4194304, 'numeral_review_regressions': 3, + 'discovery_binary_alphabet_byte_strings': 8191, + 'small_compositions': 4083, 'cycle_largest_roundtrip_bytes': 65536, + 'cycle_sample_rounds': 3, 'native_occurrence_circles': 7, + 'prime_jump_trace': [5381, 53, 241, 1523] + }, + 'cycle': 'EXPLICIT_CANDIDATE: corpus normalization once; native sequence affixiation and prime-derived bit interleave; exact reverse', + 'full_weave': Pipeline().plan(), + 'asymmetric_cipher': 'NOT_IMPLEMENTED: cycle/profile recovery is not a public/private trapdoor', + 'native_language_corpus_replay': 'NOT_EXECUTED: binary construction does not use or replace a language corpus', + 'security_observations': [ + 'The older fixed transport map remains recoverable in its declared attack experiment.', + 'The cycle is deterministic from the same source, corpus and profile; syntax/digest checks are not authentication.' ], - 'retired_substitutions':{ - 'stack_pr_73':'REMOVED: per-bit two-sheet encoding was not the specified byte construction', - 'stack_pr_74':'REMOVED: bit-axis public feedback was not the specified whole-byte coupling', - 'evidence_scope':'their attacks concern those rejected implementations, not Weave', - 'private_perturbation_correction':'unchanged public encryption inputs cannot establish private-key noncausality' + 'retired_substitutions': { + 'stack_pr_73': 'REMOVED: per-bit public sheet substitution', + 'stack_pr_74': 'REMOVED: bit-axis public feedback', + 'stack_pr_76': 'SUPERSEDED: one-bit-per-circle byte records replaced by native sequence-cycle construction', + 'evidence_scope': 'Rejected encoders do not falsify the corrected sequence construction.' }, - 'nonclaim':'Construction-source recovery is not decryption. No native Weave security result.' + 'nonclaim': 'No fixed total expansion, universal compression, cryptographic strength, or language-graduation result.' } sys.stderr.write(report.getvalue()) if args.receipt: - args.receipt.parent.mkdir(parents=True,exist_ok=True) - args.receipt.write_text(json.dumps(receipt,indent=2)+'\n') - print(json.dumps({key:receipt[key] for key in ('status','tests','failures','errors','skipped','source_sha256')})) + args.receipt.parent.mkdir(parents=True, exist_ok=True) + args.receipt.write_text(json.dumps(receipt, indent=2)+'\n') + print(json.dumps({k: receipt[k] for k in ('status','tests','failures','errors','skipped','source_sha256')})) return 0 if passed else 1 -if __name__=='__main__':raise SystemExit(main()) +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/research/weave/tests/test_cycle.py b/research/weave/tests/test_cycle.py new file mode 100644 index 0000000..7da8af6 --- /dev/null +++ b/research/weave/tests/test_cycle.py @@ -0,0 +1,515 @@ +# === CHECKS === +# id: check_all_8191_short_binary_alphabet_byte_strings +# proves: discovery_exact_partition, discovery_repeat_selection +# call: self::test_all_8191_short_binary_alphabet_byte_strings +# mutates: none +# cleanup: none +# +# id: check_repeated_multibyte_and_residuals +# proves: discovery_exact_partition, discovery_repeat_selection +# call: self::test_repeated_multibyte_and_residuals +# mutates: none +# cleanup: none +# +# id: check_competing_overlaps_are_deterministic +# proves: discovery_exact_partition, discovery_repeat_selection +# call: self::test_competing_overlaps_are_deterministic +# mutates: none +# cleanup: none +# +# id: check_nonrepeated_and_single_byte_remain_literal +# proves: discovery_exact_partition, discovery_repeat_selection +# call: self::test_nonrepeated_and_single_byte_remain_literal +# mutates: none +# cleanup: none +# +# id: check_discovery_budget_refuses_not_truncates +# proves: discovery_resource_refusal +# call: self::test_discovery_budget_refuses_not_truncates +# mutates: none +# cleanup: none +# +# id: check_normalization_exact_bucket_and_corpus_bits +# proves: cycle_normalize_once +# call: self::test_normalization_exact_bucket_and_corpus_bits +# mutates: none +# cleanup: none +# +# id: check_exact_bucket_input_still_uses_corpus +# proves: cycle_normalize_once +# call: self::test_exact_bucket_input_still_uses_corpus +# mutates: none +# cleanup: none +# +# id: check_normalization_source_zeroes_and_empty +# proves: cycle_normalize_once +# call: self::test_normalization_source_zeroes_and_empty +# mutates: none +# cleanup: none +# +# id: check_wrong_corpus_and_padding_are_rejected +# proves: cycle_normalize_once +# call: self::test_wrong_corpus_and_padding_are_rejected +# mutates: none +# cleanup: none +# +# id: check_all_small_composition_ranks +# proves: prime_split_replay +# call: self::test_all_small_composition_ranks +# mutates: none +# cleanup: none +# +# id: check_actual_prime_recursion_and_jumps +# proves: prime_split_replay +# call: self::test_actual_prime_recursion_and_jumps +# mutates: none +# cleanup: none +# +# id: check_route_occurrence_identity_is_not_destination_only +# proves: prime_split_replay +# call: self::test_route_occurrence_identity_is_not_destination_only +# mutates: none +# cleanup: none +# +# id: check_split_controls_replay_without_source +# proves: prime_split_replay +# call: self::test_split_controls_replay_without_source +# mutates: none +# cleanup: none +# +# id: check_both_end_orders_preserve_each_bit +# proves: first_last_inverse +# call: self::test_both_end_orders_preserve_each_bit +# mutates: none +# cleanup: none +# +# id: check_all_256_byte_values_interleave_inverse +# proves: first_last_inverse +# call: self::test_all_256_byte_values_interleave_inverse +# mutates: none +# cleanup: none +# +# id: check_invalid_split_parameters_refused +# proves: first_last_inverse +# call: self::test_invalid_split_parameters_refused +# mutates: none +# cleanup: none +# +# id: check_native_objects_and_720_return_are_used +# proves: cycle_native_occurrence_recovery +# call: self::test_native_objects_and_720_return_are_used +# mutates: none +# cleanup: none +# +# id: check_native_affix_and_coordinate_recovery +# proves: cycle_native_occurrence_recovery +# call: self::test_native_affix_and_coordinate_recovery +# mutates: none +# cleanup: none +# +# id: check_spaces_are_effective_native_inputs +# proves: cycle_native_occurrence_recovery +# call: self::test_spaces_are_effective_native_inputs +# mutates: none +# cleanup: none +# +# id: check_one_round_empty_and_all_byte_values +# proves: cycle_reversible_rounds +# call: self::test_one_round_empty_and_all_byte_values +# mutates: none +# cleanup: none +# +# id: check_three_round_cycle_exact_and_once_normalized +# proves: cycle_reversible_rounds +# call: self::test_three_round_cycle_exact_and_once_normalized +# mutates: none +# cleanup: none +# +# id: check_seeded_random_mixed_data_two_rounds +# proves: cycle_reversible_rounds +# call: self::test_seeded_random_mixed_data_two_rounds +# mutates: none +# cleanup: none +# +# id: check_65536_byte_declared_case_roundtrip +# proves: cycle_reversible_rounds +# call: self::test_65536_byte_declared_case_roundtrip +# mutates: none +# cleanup: none +# +# id: check_every_outer_truncation_and_trailing_bytes_refused +# proves: cycle_strict_refusal +# call: self::test_every_outer_truncation_and_trailing_bytes_refused +# mutates: none +# cleanup: none +# +# id: check_profile_material_native_identity_mismatches_refused +# proves: cycle_native_occurrence_recovery +# call: self::test_profile_material_native_identity_mismatches_refused +# mutates: none +# cleanup: none +# +# id: check_profile_strict_fields_and_types +# proves: cycle_strict_refusal +# call: self::test_profile_strict_fields_and_types +# mutates: none +# cleanup: none +# +# id: check_cycle_budgets_fail_without_partial_results +# proves: cycle_strict_refusal +# call: self::test_cycle_budgets_fail_without_partial_results +# mutates: none +# cleanup: none +# +# id: check_source_tampering_refused_before_execution +# proves: cycle_native_source_pinned +# call: self::test_source_tampering_refused_before_execution +# mutates: filesystem +# cleanup: tempdir_teardown +# +# id: check_cli_fresh_process_source_deleted_and_no_overwrite +# proves: cycle_reversible_rounds +# call: self::test_cli_fresh_process_source_deleted_and_no_overwrite +# mutates: filesystem +# cleanup: tempdir_teardown +# +# id: check_profile_roundtrip_and_all_accounting_fields +# proves: cycle_complete_accounting +# call: self::test_profile_roundtrip_and_all_accounting_fields +# mutates: none +# cleanup: none +# +# === END CHECKS === +"""Run: WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_cycle.py -v. + +The suite exercises the declared complete sequence cycle and real locked producers. +It is not a test of public-key secrecy. Files are isolated in temporary directories. +""" +from copy import deepcopy +from dataclasses import replace +from fractions import Fraction +from itertools import combinations +from math import comb +from pathlib import Path +import json +import os +import random +import shutil +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +from affixiation import discover +from prime_schedule import Route, SplitPlan, plan, interleave, _composition +from numeral import PrimePath, Refused, ResourceLimit +import cycle +from cycle import Profile, CycleLimits, normalize, denormalize, affix, unaffix, forward, reverse +from cycle_native import load_native + +ROOT = Path(__file__).resolve().parents[1] +SOURCES = Path(os.environ.get('WEAVE_SOURCES', ROOT/'sources')) +PROFILE = Profile.read((ROOT/'profiles/cycle-v1.json').read_bytes()) +CORPUS = bytes(range(256)) + b'actual corpus bytes' + + +def one_round(): + return replace(PROFILE, rounds=PROFILE.rounds[:1]) + + +class CycleTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.api, cls.geometry = load_native(SOURCES) + + def test_all_8191_short_binary_alphabet_byte_strings(self): + count = 0 + for n in range(13): + for value in range(1 << n): + data = bytes((value >> i) & 1 for i in range(n)) + part = discover(data) + self.assertEqual(part.restore(), data) + self.assertEqual(part.starts, tuple(sum(len(part.blocks[j]) for j in part.order[:i]) + for i in range(len(part.order)))) + count += 1 + self.assertEqual(count, 8191) + + def test_repeated_multibyte_and_residuals(self): + p = discover(b'ABxABy') + self.assertEqual(p.blocks, (b'AB', b'x', b'y')) + self.assertEqual(p.order, (0,1,0,2)) + self.assertEqual(p.starts, (0,2,3,5)) + self.assertEqual(p.repeat_ids, (0,)) + + def test_competing_overlaps_are_deterministic(self): + for data in (b'AAAAA', b'ABABABA', b'banana bandana banana', b'abcabcabcab'): + p = discover(data) + self.assertEqual(p, discover(data)) + self.assertEqual(p.restore(), data) + for a,b,i in zip(p.starts,p.starts[1:],p.order): + self.assertLessEqual(a+len(p.blocks[i]), b) + self.assertEqual(discover(b'AAAAA').blocks, (b'AA', b'A')) + self.assertEqual(discover(b'AAAAA').order, (0,0,1)) + + def test_nonrepeated_and_single_byte_remain_literal(self): + data = bytes(range(256)) + self.assertEqual(discover(data).blocks, (data,)) + self.assertEqual(discover(b'x').blocks, (b'x',)) + self.assertEqual(discover(b'').restore(), b'') + self.assertEqual(discover(b'abaca').repeat_ids, ()) + + def test_discovery_budget_refuses_not_truncates(self): + with self.assertRaises(ResourceLimit): + discover(b'ABABAB',visit_budget=1) + with self.assertRaises(ResourceLimit): + discover(b'ABABAB',max_bytes=5) + for bad in ('abc', bytearray(b'abc'), None): + with self.assertRaises(Refused): discover(bad) + + def test_normalization_exact_bucket_and_corpus_bits(self): + profile = one_round() + data = normalize(b'abc', CORPUS, profile) + self.assertEqual(len(data), 256) + bits = ''.join(format(x,'08b') for x in CORPUS) + n = len(data)-47 + expected = ''.join(bits[(profile.corpus_bit_offset+i)%len(bits)] for i in range(n*8)) + self.assertEqual(data[47:], int(expected,2).to_bytes(n,'big')) + self.assertEqual(denormalize(data,CORPUS,profile),b'abc') + + def test_exact_bucket_input_still_uses_corpus(self): + profile = one_round() + data = normalize(b'x'*212,b'\xa5',profile) + self.assertEqual(len(data),512) + self.assertEqual(denormalize(data,b'\xa5',profile),b'x'*212) + + def test_normalization_source_zeroes_and_empty(self): + for raw in (b'',b'\x00',b'\x00\x00\x01',bytes(range(256))): + for offset in (0,1,7,8,15,100003): + p = replace(one_round(),corpus_bit_offset=offset) + self.assertEqual(denormalize(normalize(raw,CORPUS,p),CORPUS,p),raw) + + def test_wrong_corpus_and_padding_are_rejected(self): + profile = one_round() + data = normalize(b'abc',CORPUS,profile) + with self.assertRaises(Refused): denormalize(data,CORPUS+b'!',profile) + with self.assertRaises(Refused): denormalize(data[:-1]+bytes([data[-1]^1]),CORPUS,profile) + with self.assertRaises(Refused): normalize(b'abc',b'',profile) + with self.assertRaises(ResourceLimit): normalize(b'abc',CORPUS,profile,CycleLimits(round_bytes=100)) + + def test_all_small_composition_ranks(self): + cases = 0 + for n in range(2,13): + for arity in range(2,n+1): + for rank,gaps in enumerate(combinations(range(1,n),arity-1)): + points = (0,*gaps,n) + self.assertEqual(_composition(n,arity,rank),tuple(b-a for a,b in zip(points,points[1:]))) + cases += 1 + self.assertEqual(cases,4083) + + def test_actual_prime_recursion_and_jumps(self): + route = Route.evaluate(PROFILE.rounds[0].path) + self.assertEqual(route.trace,(5381,53,241,1523)) + self.assertEqual(Route.evaluate(PrimePath(2, (('next',),)*7)).trace, + (2,3,5,11,31,127,709,5381)) + self.assertEqual(Route.evaluate(PrimePath(5381,(('span',2,0,3),))).trace,(5381,5)) + + def test_route_occurrence_identity_is_not_destination_only(self): + a = Route.evaluate(PrimePath(313,(('span',10,0,1),))) + b = Route.evaluate(PrimePath(313,(('span',10,2,1),))) + self.assertEqual(a.trace,b.trace) + self.assertNotEqual(a.determinant,b.determinant) + self.assertNotEqual(plan(1024,a,(3,5,7)),plan(1024,b,(3,5,7))) + + def test_split_controls_replay_without_source(self): + route = Route.evaluate(PROFILE.rounds[0].path) + for n in (8,16,128,1024,65536): + split = plan(n,route,(3,5,7)) + self.assertEqual(sum(split.lengths),n) + self.assertTrue(all(x>0 for x in split.lengths)) + self.assertEqual(plan(n,route,(3,5,7)),split) + self.assertLess(split.rank,comb(n-1,len(split.lengths)-1)) + + def test_both_end_orders_preserve_each_bit(self): + split = SplitPlan(24,(7,5,12),0) + for end in ('first-last','last-first'): + mapped=[] + for index in range(24): + data = (1 << (23-index)).to_bytes(3,'big') + out = interleave(data,split,end_order=end) + self.assertEqual(int.from_bytes(out,'big').bit_count(),1) + self.assertEqual(interleave(out,split,inverse=True,end_order=end),data) + mapped.append(out) + self.assertEqual(len(set(mapped)),24) + + def test_all_256_byte_values_interleave_inverse(self): + for v in range(256): + data = bytes([v]) + for end in ('first-last','last-first'): + for split in (SplitPlan(8,(1,7),0),SplitPlan(8,(3,2,3),0)): + self.assertEqual(interleave(interleave(data,split,end_order=end),split,inverse=True,end_order=end),data) + + def test_invalid_split_parameters_refused(self): + route = Route.evaluate(PrimePath(53,())) + for arities in ((1,), (9,), (3,3), (True,), ([3],), [3]): + with self.assertRaises(Refused): plan(8,route,arities) + for lengths in ((3,3),(0,8),(True,7),(-1,9)): + with self.assertRaises(Refused): interleave(b'a',SplitPlan(8,lengths,0)) + + def test_native_objects_and_720_return_are_used(self): + origin = self.api.ByteOrigin(b'ABxABy','native-test',0,self.geometry) + axis = origin.byte_axis(3) + self.assertEqual(type(axis).__name__,'AxisCirclePosition') + self.assertEqual(axis.turn,Fraction(1,2)) + state = self.geometry.placed(axis,Fraction(1,7)) + self.assertEqual(type(state).__name__,'NativeMobiusState') + self.assertNotEqual(state.complete_key,state.advance(1).complete_key) + self.assertEqual(state.complete_key,state.advance(2).complete_key) + + def test_native_affix_and_coordinate_recovery(self): + raw = b'ABxABy' + origin = self.api.ByteOrigin(raw,'native-test',0,self.geometry) + spec = PROFILE.rounds[0] + wire,stats = affix(raw,api=self.api,geometry=self.geometry,scope=origin.scope, + root=origin.message_origin,round_id=0,spec=spec) + restored = unaffix(wire,api=self.api,geometry=self.geometry,scope=origin.scope, + root=origin.message_origin,round_id=0,spec=spec) + self.assertEqual(restored,raw) + self.assertEqual(stats['repeated_definitions'],1) + self.assertEqual(stats['occurrences'],4) + self.assertEqual(stats['output_bytes'],stats['numeral_bytes']+stats['native_coordinate_and_frame_bytes']) + + def test_spaces_are_effective_native_inputs(self): + raw = b'ABxABy' + root = self.api.ByteOrigin(raw,'native-test',0,self.geometry).message_origin + a = PROFILE.rounds[0] + b = replace(a,spaces=tuple((x+Fraction(1,3))%2 for x in a.spaces)) + kwargs = dict(api=self.api,geometry=self.geometry,scope='native-test',root=root,round_id=0) + wa,_ = affix(raw,spec=a,**kwargs) + wb,_ = affix(raw,spec=b,**kwargs) + self.assertNotEqual(wa,wb) + self.assertEqual(unaffix(wa,spec=a,**kwargs),raw) + self.assertEqual(unaffix(wb,spec=b,**kwargs),raw) + with self.assertRaises(ValueError): unaffix(wa,spec=b,**kwargs) + + def test_one_round_empty_and_all_byte_values(self): + for raw in (b'',b'\x00',b'\xff',bytes(range(256)),b'ababababcabc'): + wire,stats = forward(raw,CORPUS,one_round(),SOURCES) + self.assertEqual(reverse(wire,CORPUS,one_round(),SOURCES),raw) + self.assertEqual(len(stats['rounds']),1) + + def test_three_round_cycle_exact_and_once_normalized(self): + raw = b'Weave repeats byte sequences. '*24 + bytes(range(64)) + with patch.object(cycle,'normalize',wraps=normalize) as call: + wire,stats = forward(raw,CORPUS,PROFILE,SOURCES) + self.assertEqual(call.call_count,1) + self.assertEqual(reverse(wire,CORPUS,PROFILE,SOURCES),raw) + self.assertEqual(len(stats['rounds']),3) + for left,right in zip(stats['rounds'],stats['rounds'][1:]): + self.assertEqual(left['output_bytes'],right['input_bytes']) + self.assertEqual(stats['total_bytes'],len(wire)) + self.assertEqual(stats['final_payload_bytes']+stats['outer_frame_bytes'],len(wire)) + + def test_seeded_random_mixed_data_two_rounds(self): + rng = random.Random(20261005) + p = replace(PROFILE,rounds=PROFILE.rounds[:2]) + for n in (0,7,31,128,511): + raw = rng.randbytes(n)+b'\x00xy\x00xy'*7 + wire,_ = forward(raw,CORPUS,p,SOURCES) + self.assertEqual(reverse(wire,CORPUS,p,SOURCES),raw) + + def test_65536_byte_declared_case_roundtrip(self): + raw = bytes(range(256))*256 + p = replace(PROFILE,bucket_bytes=4096,rounds=PROFILE.rounds[:2]) + wire,stats = forward(raw,CORPUS,p,SOURCES) + self.assertEqual(reverse(wire,CORPUS,p,SOURCES),raw) + self.assertEqual(stats['source_bytes'],65536) + + def test_every_outer_truncation_and_trailing_bytes_refused(self): + wire,_ = forward(b'ABxABy',CORPUS,one_round(),SOURCES) + for length in range(len(wire)): + with self.assertRaises(ValueError): reverse(wire[:length],CORPUS,one_round(),SOURCES) + with self.assertRaises(Refused): reverse(wire+b'!',CORPUS,one_round(),SOURCES) + + def test_profile_material_native_identity_mismatches_refused(self): + wire,_ = forward(b'test',CORPUS,one_round(),SOURCES) + with self.assertRaises(Refused): reverse(wire,CORPUS,replace(one_round(),scope='other'),SOURCES) + with self.assertRaises(Refused): reverse(wire,CORPUS+b'!',one_round(),SOURCES) + for pos in (4,36,68,100): + damaged = bytearray(wire); damaged[pos] ^= 1 + with self.assertRaises(ValueError): reverse(bytes(damaged),CORPUS,one_round(),SOURCES) + + def test_profile_strict_fields_and_types(self): + obj = PROFILE.as_dict() + cases=[] + c=deepcopy(obj); c['unknown']=True; cases.append(c) + c=deepcopy(obj); c['rounds'][0]['spaces'][0]=[1,0]; cases.append(c) + c=deepcopy(obj); c['rounds'][0]['spaces'][0]=[2,4]; cases.append(c) + c=deepcopy(obj); c['rounds'][0]['arities']=[True]; cases.append(c) + c=deepcopy(obj); c['rounds'][0]['arities']=[[3]]; cases.append(c) + c=deepcopy(obj); c['rounds'][0]['circle_order']=[1]*7; cases.append(c) + c=deepcopy(obj); c['rounds'][0]['end_order']='arbitrary'; cases.append(c) + for c in cases: + with self.assertRaises(ValueError): Profile.read(json.dumps(c).encode()) + with self.assertRaises(Refused): Profile.read(b'{"schema":1,"schema":2}') + with self.assertRaises(ResourceLimit): Profile.read(json.dumps(obj).encode(),CycleLimits(rounds=2)) + + def test_cycle_budgets_fail_without_partial_results(self): + with self.assertRaises(ResourceLimit): forward(b'1234',CORPUS,PROFILE,SOURCES,CycleLimits(input_bytes=3)) + with self.assertRaises(ResourceLimit): forward(b'1234',CORPUS,PROFILE,SOURCES,CycleLimits(rounds=2)) + with self.assertRaises(ResourceLimit): forward(b'1234',CORPUS,PROFILE,SOURCES,CycleLimits(prime_work=1)) + with self.assertRaises(ResourceLimit): forward(b'ABxABy',CORPUS,one_round(),SOURCES,CycleLimits(native_occurrences=1)) + + def test_source_tampering_refused_before_execution(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory) + shutil.copytree(SOURCES/'uchc',root/'uchc') + shutil.copytree(SOURCES/'ucns',root/'ucns') + src=root/'uchc/binary/uchc_binary/origin.py' + marker=root/'executed' + src.write_text(f"open({str(marker)!r},'w').write('oops')\n") + with self.assertRaises(Refused): load_native(root) + self.assertFalse(marker.exists()) + + def test_cli_fresh_process_source_deleted_and_no_overwrite(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory) + src,wire,out,corpus,profile = (root/x for x in ('source','packet','restored','corpus','profile.json')) + raw=bytes(range(256))*4+b'\x00AB\x00AB'*17 + src.write_bytes(raw);corpus.write_bytes(CORPUS) + profile.write_bytes(cycle.canonical(PROFILE.as_dict())) + args=['--sources',str(SOURCES),'--profile',str(profile),'--corpus',str(corpus)] + command=[sys.executable,str(ROOT/'cycle.py')] + enc=subprocess.run(command+['forward',str(src),str(wire),*args],capture_output=True) + self.assertEqual(enc.returncode,0,enc.stderr.decode()) + src.unlink() + dec=subprocess.run(command+['reverse',str(wire),str(out),*args],capture_output=True) + self.assertEqual(dec.returncode,0,dec.stderr.decode()) + self.assertEqual(out.read_bytes(),raw) + again=subprocess.run(command+['reverse',str(wire),str(out),*args],capture_output=True) + self.assertEqual(again.returncode,2) + self.assertEqual(out.read_bytes(),raw) + + def test_profile_roundtrip_and_all_accounting_fields(self): + lock = json.loads((ROOT/'CYCLE_NATIVE.json').read_text()) + graph = json.loads((ROOT/'CYCLE_WORK_GRAPH.json').read_text()) + payload = {k: graph[k] for k in ('repositories', 'boundaries')} + from hashlib import sha256 + digest = sha256(json.dumps(payload, sort_keys=True, separators=(',', ':')).encode()).hexdigest() + self.assertEqual(digest, graph['work_graph_sha256']) + repositories = {x['repository']: x for x in graph['repositories']} + self.assertEqual(repositories['The-Interdependency/uchc']['commit'], lock['uchc_source']['commit']) + self.assertEqual(repositories['The-Interdependency/ucns']['commit'], lock['ucns_commit']) + self.assertEqual(len(lock['uchc_source']['commit']), 40) + self.assertFalse(graph['boundaries']['authority_transfer']) + self.assertEqual(Profile.read(cycle.canonical(PROFILE.as_dict())),PROFILE) + wire,stats=forward(b'ABCD'*64,CORPUS,PROFILE,SOURCES) + for row in stats['rounds']: + self.assertEqual(row['input_bytes']*8 % 8,0) + self.assertEqual(row['permutation_bit_count'],row['output_bytes']*8) + self.assertEqual(row['numeral_bytes']+row['native_coordinate_and_frame_bytes'],row['output_bytes']) + self.assertEqual(len(wire),stats['total_bytes']) + self.assertEqual(reverse(wire,CORPUS,PROFILE,SOURCES),b'ABCD'*64) + + +if __name__=='__main__': unittest.main() diff --git a/research/weave/tests/test_eight_circle.py b/research/weave/tests/test_eight_circle.py deleted file mode 100644 index 54076bf..0000000 --- a/research/weave/tests/test_eight_circle.py +++ /dev/null @@ -1,180 +0,0 @@ -# === CHECKS === -# id: weave_byte_mapping_witness -# proves: weave_byte_has_eight_circle_placements -# call: tests.test_eight_circle.EightCircleTests.test_all_bytes -# id: weave_star_witness -# proves: weave_public_pair_always_contains_whole -# call: tests.test_eight_circle.EightCircleTests.test_all_seven_star_views -# id: weave_no_bit_code_witness -# proves: weave_positions_are_not_bit_codes -# call: tests.test_eight_circle.EightCircleTests.test_removed_bitwise_cipher_surface -# === END CHECKS === -"""Run with: python -m unittest discover -s tests -p test_eight_circle.py. - -Coverage: all 256 byte values, all 40,320 bit/circle assignments, all seven -whole-part views and invalid direct construction. This is not cryptanalysis. -""" -from dataclasses import FrozenInstanceError, replace -from fractions import Fraction -from itertools import permutations -import unittest - -from stages import eight_circle as module -from stages.eight_circle import ( - BitPlacement, ByteConstruction, Circle, FullKeySet, PublicPair, construct_byte, -) - -POSITIONS = tuple(Fraction(i, 7) for i in range(8)) - - -def key(mapping=tuple(range(8))): - return FullKeySet(tuple(Circle(i, f"G{i}", Fraction(i, 9)) for i in range(8)), mapping) - - -def build(value=0xA5, full=None, positions=POSITIONS): - return construct_byte(value, key() if full is None else full, positions, - origin_id="message", byte_index=0) - - -class EightCircleTests(unittest.TestCase): - def test_all_bytes(self): - full = key() - for value in range(256): - state = build(value, full) - self.assertEqual(len(state.placements), 8) - self.assertEqual(state.source_value, value) - self.assertEqual(tuple(p.circle_index for p in state.placements), tuple(range(8))) - self.assertEqual(tuple(p.bit for p in state.placements), - tuple((value >> i) & 1 for i in range(7, -1, -1))) - - def test_all_40320_assignments(self): - circles = key().circles - count = 0 - for mapping in permutations(range(8)): - state = build(0xA5, FullKeySet(circles, mapping)) - self.assertEqual(state.source_value, 0xA5) - for source, circle in enumerate(mapping): - self.assertEqual(state.placements[circle].source_bit_index, source) - count += 1 - self.assertEqual(count, 40320) - - def test_all_seven_star_views(self): - full = key() - for part in range(1, 8): - pair = full.degenerate(part) - self.assertEqual(pair.indices, (0, part)) - self.assertEqual(len(pair.circles), 2) - self.assertIs(pair.circles[0], full.circles[0]) - self.assertIs(pair.circles[1], full.circles[part]) - - def test_arbitrary_and_malformed_pairs_rejected(self): - full = key() - for bad in (0, -1, 8, True, 1.0, "1", None): - with self.subTest(part=bad), self.assertRaises(ValueError): - full.degenerate(bad) - for members in ((full.circles[1], full.circles[2]), - (full.circles[1], full.circles[0]), - (full.circles[0], full.circles[0]), - list(full.circles[:2]), (), (None, None)): - with self.subTest(members=members), self.assertRaises(ValueError): - PublicPair(members) - with self.assertRaises(TypeError): - full.degenerate(0, 1) - - def test_full_key_validation(self): - full = key() - for circles in (full.circles[:-1], list(full.circles), - tuple(reversed(full.circles)), (None,) * 8, - (full.circles[0],) * 8): - with self.assertRaises(ValueError): - FullKeySet(circles, tuple(range(8))) - duplicate_names = tuple(Circle(i, "same", Fraction(0)) for i in range(8)) - with self.assertRaises(ValueError): - FullKeySet(duplicate_names, tuple(range(8))) - for mapping in ((), (0,) * 8, (False,) + tuple(range(1, 8)), - (0.0,) + tuple(range(1, 8)), list(range(8)), tuple(range(1, 9))): - with self.assertRaises(ValueError): - FullKeySet(full.circles, mapping) - - def test_exact_720_positions_not_two_bit_codes(self): - phases = (Fraction(0), Fraction(1), Fraction(2), Fraction(-1), - Fraction(1, 10**40 + 7), Fraction(9, 4), Fraction(3, 2), Fraction(7, 4)) - state = build(0, positions=phases) - self.assertEqual(tuple(p.position for p in state.placements), - tuple(p % 2 for p in phases)) - self.assertNotEqual(state.placements[0].position, state.placements[1].position) - self.assertEqual(state.placements[0].position, state.placements[2].position) - self.assertEqual(state.placements[4].position, phases[4]) - - def test_positions_and_space_are_independent_supplied_data(self): - full = key() - other = FullKeySet(tuple(replace(c, space=c.space + Fraction(1, 17)) - for c in full.circles), full.bit_to_circle) - self.assertNotEqual(full.circles, other.circles) - shifted = tuple(p + Fraction(1, 19) for p in POSITIONS) - first, second = build(full=full), build(full=other, positions=shifted) - self.assertEqual(first.source_value, second.source_value) - self.assertNotEqual(first.placements[0].position, second.placements[0].position) - # No claim that changing private data with identical public inputs must - # change encryption. This test checks supplied construction data only. - - def test_invalid_byte_and_position_inputs(self): - for value in (-1, 256, True, 1.0, b"A", None): - with self.assertRaises(ValueError): - build(value) - for phases in ((), list(POSITIONS), POSITIONS[:-1]): - with self.assertRaises(ValueError): - build(positions=phases) - for phase in (0, 0.0, True, None): - with self.assertRaises(TypeError): - build(positions=(phase,) + POSITIONS[1:]) - with self.assertRaises(TypeError): - build(full="not a full key") - - def test_direct_construction_validation(self): - state = build() - for changes in ({"placements": state.placements[:-1]}, - {"placements": tuple(reversed(state.placements))}, - {"placements": (state.placements[0],) * 8}, - {"placements": (None,) * 8}, - {"placements": list(state.placements)}, - {"origin_id": " "}, {"byte_index": True}, {"byte_index": -1}): - with self.assertRaises(ValueError): - replace(state, **changes) - for changes in ({"source_bit_index": True}, {"circle_index": 8}, {"bit": False}): - with self.assertRaises(ValueError): - replace(state.placements[0], **changes) - duplicate_source = (replace(state.placements[0], source_bit_index=1),) + state.placements[1:] - with self.assertRaises(ValueError): - replace(state, placements=duplicate_source) - for bad in (True, -1, 8): - with self.assertRaises(ValueError): - Circle(bad, "bad", Fraction(0)) - - def test_public_view_does_not_export_byte_or_other_six(self): - full = key() - pair = full.degenerate(4) - self.assertEqual(set(vars(pair)), {"circles"}) - self.assertEqual(pair.indices, (0, 4)) - self.assertFalse(hasattr(pair, "bit_to_circle")) - self.assertFalse(hasattr(pair, "full")) - self.assertFalse(hasattr(pair, "placements")) - self.assertEqual({c.index for c in pair.circles}, {0, 4}) - - def test_removed_bitwise_cipher_surface(self): - for name in ("encode", "decode", "encode_byte", "decode_byte"): - self.assertFalse(hasattr(module, name), name) - self.assertFalse(hasattr(Circle, "public_sheet")) - self.assertFalse(hasattr(PublicPair, "witness")) - - def test_records_are_immutable_and_payload_not_repr(self): - state = build() - with self.assertRaises(FrozenInstanceError): - state.byte_index = 2 - self.assertNotIn("placements=", repr(state)) - self.assertNotIn("bit=", repr(state.placements[0])) - self.assertNotIn("space=", repr(key().circles[0])) - - -if __name__ == "__main__": - unittest.main() diff --git a/research/weave/tests/test_message_origin.py b/research/weave/tests/test_message_origin.py deleted file mode 100644 index dfefe28..0000000 --- a/research/weave/tests/test_message_origin.py +++ /dev/null @@ -1,117 +0,0 @@ -# === CHECKS === -# id: weave_message_byte_origin_witness -# proves: weave_message_origin_contains_bytes -# call: tests.test_message_origin.MessageOriginTests.test_one_participant_per_byte -# id: weave_message_occurrence_scope_witness -# proves: weave_byte_occurrences_preserve_scope -# call: tests.test_message_origin.MessageOriginTests.test_repeated_bytes_preserve_occurrences -# === END CHECKS === -"""Run: python -m unittest discover -s tests -p test_message_origin.py. - -The source construction can be recovered because it retains its plaintext. -These tests never report that operation as private-key decryption. -""" -from dataclasses import replace -from fractions import Fraction -import unittest - -from stages.eight_circle import Circle, FullKeySet -from stages import message_origin as module -from stages.message_origin import ByteOccurrence, MessageOrigin, construct_at, construct_origin - - -def key(): - return FullKeySet(tuple(Circle(i, f"G{i}", Fraction(i, 9)) for i in range(8)), - (7, 2, 5, 0, 3, 1, 6, 4)) - - -class MessageOriginTests(unittest.TestCase): - def test_one_participant_per_byte(self): - origin = construct_origin(b"AA", identity="M") - self.assertEqual(origin.byte_length, 2) - self.assertEqual(len(origin.bytesets), 2) - self.assertEqual(tuple(b.index for b in origin.bytesets), (0, 1)) - self.assertFalse(hasattr(origin, "axes")) - - def test_eight_placements_are_inside_each_byte(self): - origin = construct_origin(b"ABC", identity="M") - full = key() - positions = tuple(Fraction(i, 7) for i in range(8)) - for index, value in enumerate(b"ABC"): - state = construct_at(origin, index, full, positions) - self.assertEqual((state.origin_id, state.byte_index), ("M", index)) - self.assertEqual(len(state.placements), 8) - self.assertEqual(state.source_value, value) - self.assertEqual(tuple(p.circle_index for p in state.placements), tuple(range(8))) - - def test_repeated_bytes_preserve_occurrences(self): - origin = construct_origin(b"AA", identity="M") - self.assertEqual(origin.bytesets[0].value, origin.bytesets[1].value) - self.assertNotEqual(origin.bytesets[0], origin.bytesets[1]) - self.assertEqual(origin.recover_bytes(), b"AA") - - def test_all_bytes_without_text_normalization(self): - data = bytes(range(256)) + b"\x00\xff\r\n" - origin = construct_origin(data, identity="binary") - self.assertEqual(origin.recover_bytes(), data) - self.assertEqual(origin.byte_length, len(data)) - - def test_empty_message_keeps_its_origin(self): - origin = construct_origin(b"", identity="empty") - self.assertEqual(origin.bytesets, ()) - self.assertEqual(origin.recover_bytes(), b"") - self.assertEqual(origin.identity, "empty") - with self.assertRaises(ValueError): - construct_at(origin, 0, key(), (Fraction(0),) * 8) - - def test_message_names_scope_occurrences_not_content_hashes(self): - first = construct_origin(b"same", identity="first") - second = construct_origin(b"same", identity="second") - self.assertNotEqual(first, second) - self.assertNotEqual(first.bytesets[0], second.bytesets[0]) - self.assertEqual(first.recover_bytes(), second.recover_bytes()) - self.assertEqual(first.identity, "first") - - def test_bad_input_and_identity_rejected(self): - for data in ("text", bytearray(b"A"), [65], None): - with self.assertRaises(TypeError): - construct_origin(data, identity="M") - for identity in (None, "", " ", True, 1): - with self.assertRaises(ValueError): - construct_origin(b"A", identity=identity) - with self.assertRaises(TypeError): - construct_origin(b"A") - - def test_direct_origin_validation(self): - origin = construct_origin(b"AB", identity="M") - for members in (list(origin.bytesets), (None,)): - with self.assertRaises(TypeError): - MessageOrigin("M", members) - for members in (tuple(reversed(origin.bytesets)), (origin.bytesets[1],), - (origin.bytesets[0], origin.bytesets[0]), - (replace(origin.bytesets[0], origin_id="other"),)): - with self.assertRaises(ValueError): - MessageOrigin("M", members) - for index, value in ((True, 0), (-1, 0), (0, False), (0, 256)): - with self.assertRaises(ValueError): - ByteOccurrence("M", index, value) - - def test_attachment_indices_fail_closed(self): - origin = construct_origin(b"A", identity="M") - positions = (Fraction(0),) * 8 - for index in (True, -1, 1, 0.0, None): - with self.assertRaises(ValueError): - construct_at(origin, index, key(), positions) - with self.assertRaises(TypeError): - construct_at("M", 0, key(), positions) - self.assertNotIn("value=", repr(origin.bytesets[0])) - self.assertNotIn("bytesets=", repr(origin)) - - def test_removed_feedback_and_bit_axis_surface(self): - for name in ("MessageAxis", "CouplingKey", "CoupledWitness", "encode", "public_recover", - "_axis_origin", "_source_bits", "witness_bits"): - self.assertFalse(hasattr(module, name), name) - - -if __name__ == "__main__": - unittest.main() diff --git a/research/weave/tests/test_numeral_review.py b/research/weave/tests/test_numeral_review.py new file mode 100644 index 0000000..2c45a76 --- /dev/null +++ b/research/weave/tests/test_numeral_review.py @@ -0,0 +1,64 @@ +# === CHECKS === +# id: numeral_review_aggregate_budget +# proves: numeral_strict_input +# call: self::test_decode_has_one_aggregate_prime_budget +# mutates: none +# cleanup: none +# id: numeral_review_malformed_not_capacity +# proves: numeral_strict_input +# call: self::test_malformed_structures_are_not_capacity_failures +# mutates: none +# cleanup: none +# id: numeral_review_cli_angle +# proves: numeral_strict_input +# call: self::test_zero_denominator_cli_is_concise_refusal +# mutates: none +# cleanup: none +# === END CHECKS === +"""Run: python -m unittest discover -s tests -p test_numeral_review.py -v. + +Regression witnesses for the live PR #77 input/budget findings. The CLI refusal +occurs before input reads or output writes; no original tests are weakened. +""" +from dataclasses import replace +from fractions import Fraction +from pathlib import Path +import subprocess +import sys +import unittest +from numeral import BitBlock, Entry, Packet, PrimePath, Limits, Refused, ResourceLimit, encode, decode + +ROOT = Path(__file__).resolve().parents[1] +A = chr(0xE000) + + +class NumeralReviewTests(unittest.TestCase): + def test_decode_has_one_aggregate_prime_budget(self): + path = PrimePath(101, ()) + p = Packet('budget', 0, (Entry(A, BitBlock(101, 16), Fraction(1, 7), 1, path),), A) + wire = encode(p) + # One recipe step and four trial-divisor candidates per pass: 10 total. + with self.assertRaises(ResourceLimit): + decode(wire, Limits(prime_work=9)) + self.assertEqual(decode(wire, Limits(prime_work=10)), p) + + def test_malformed_structures_are_not_capacity_failures(self): + p = Packet('scope', 0, (Entry(A, BitBlock(5,8), Fraction(1,7), 1),), A) + operations = (lambda: PrimePath(2, []).replay(), + lambda: encode(replace(p, entries=list(p.entries))), + lambda: encode(replace(p, symbols=[A]))) + for operation in operations: + with self.assertRaises(Refused) as raised: + operation() + self.assertNotIsInstance(raised.exception, ResourceLimit) + + def test_zero_denominator_cli_is_concise_refusal(self): + result = subprocess.run([sys.executable, str(ROOT/'numeral.py'), 'bind', + 'unused-input', 'unused-output', '--origin', 'test', '--angle', '1/0', + '--circle', '3'], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertNotIn('Traceback', result.stderr) + self.assertIn('exact finite fraction', result.stderr) + + +if __name__ == '__main__': unittest.main() From 8c737f1e129b90004e986fc74940990c507762f0 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 15:05:29 -0700 Subject: [PATCH 03/15] ci(weave): retain exact-source inspection bundles and test receipt --- .github/workflows/weave.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/weave.yml b/.github/workflows/weave.yml index f2d5262..fb2e072 100644 --- a/.github/workflows/weave.yml +++ b/.github/workflows/weave.yml @@ -47,3 +47,23 @@ jobs: PYTHONDONTWRITEBYTECODE=1 python3 research/weave/test.py --receipt "$RUNNER_TEMP/weave-check.json" cat "$RUNNER_TEMP/weave-check.json" PYTHONDONTWRITEBYTECODE=1 python3 research/weave/cycle.py demo + - name: Package tracked source and evidence for inspection, not release + if: always() + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/weave-inspection" + git archive --format=zip --output="$RUNNER_TEMP/weave-inspection/stack.zip" HEAD research/weave tools stack-manifest.json STACK_MANIFEST.md AGENTS.md README.md docs + git -C .weave-inputs/ucns archive --format=zip --output="$RUNNER_TEMP/weave-inspection/ucns.zip" HEAD + git -C .weave-inputs/uchc archive --format=zip --output="$RUNNER_TEMP/weave-inspection/uchc.zip" HEAD binary AGENTS.md README.md pyproject.toml + git rev-parse HEAD > "$RUNNER_TEMP/weave-inspection/STACK_HEAD.txt" + git -C .weave-inputs/ucns rev-parse HEAD > "$RUNNER_TEMP/weave-inspection/UCNS_HEAD.txt" + git -C .weave-inputs/uchc rev-parse HEAD > "$RUNNER_TEMP/weave-inspection/UCHC_HEAD.txt" + if test -f "$RUNNER_TEMP/weave-check.json"; then cp "$RUNNER_TEMP/weave-check.json" "$RUNNER_TEMP/weave-inspection/"; fi + - name: Retain exact-source inspection bundle + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: weave-inspection-${{ github.sha }} + path: ${{ runner.temp }}/weave-inspection/ + if-no-files-found: error + retention-days: 7 From c360b92e101ff4ac2af7dc33155edec95d7f5887 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 15:26:21 -0700 Subject: [PATCH 04/15] fix(weave): keep native cycle in forge and close review integrity gaps Preserve exact UCNS geometry and UCHC architecture without prematurely shipping a binary UCHC domain. Validate full sequence closures, load fresh verified source, install outputs atomically without clobber, validate empty-input attachments, and reconcile Stack manifests. Local evidence: 125 tests passed, zero failures/errors/skips. Source profile digest 4f2e432b4ccf5c803e5438e2a26f266c7c0812dd9bde9f05f6090904fd49789f. Hosted checks remain a separate gate. --- .github/workflows/weave.yml | 28 +- AGENTS.md | 7 +- README.md | 12 + STACK_MANIFEST.md | 22 +- research/weave/ASSEMBLY.md | 6 + research/weave/CYCLE.md | 57 ++- research/weave/CYCLE_NATIVE.json | 15 +- research/weave/CYCLE_WORK_GRAPH.json | 17 +- research/weave/EIGHT_CIRCLE.md | 4 +- research/weave/IMPLEMENTED.md | 6 + research/weave/NATIVE_BINARY_PROVENANCE.json | 29 ++ research/weave/NUMERAL_CONSTRUCTION.md | 2 +- research/weave/QUESTIONS.md | 7 +- research/weave/README.md | 166 ++----- research/weave/SPECIFICATION.md | 2 +- research/weave/cycle.py | 6 +- research/weave/cycle_native.py | 64 +-- research/weave/native_binary.py | 441 +++++++++++++++++++ research/weave/numeral.py | 20 +- research/weave/safe_output.py | 50 +++ research/weave/test.py | 5 +- research/weave/tests/test_binary_origin.py | 246 +++++++++++ research/weave/tests/test_cycle.py | 14 +- research/weave/tests/test_cycle_repairs.py | 215 +++++++++ stack-manifest.json | 26 +- 25 files changed, 1233 insertions(+), 234 deletions(-) create mode 100644 research/weave/NATIVE_BINARY_PROVENANCE.json create mode 100644 research/weave/native_binary.py create mode 100644 research/weave/safe_output.py create mode 100644 research/weave/tests/test_binary_origin.py create mode 100644 research/weave/tests/test_cycle_repairs.py diff --git a/.github/workflows/weave.yml b/.github/workflows/weave.yml index fb2e072..b437a9b 100644 --- a/.github/workflows/weave.yml +++ b/.github/workflows/weave.yml @@ -4,10 +4,14 @@ on: paths: - 'research/weave/**' - '.github/workflows/weave.yml' + - 'stack-manifest.json' + - 'STACK_MANIFEST.md' push: paths: - 'research/weave/**' - '.github/workflows/weave.yml' + - 'stack-manifest.json' + - 'STACK_MANIFEST.md' workflow_dispatch: permissions: contents: read @@ -15,7 +19,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - name: Checkout the event source with GitHub PR isolation + - name: Checkout event source with PR isolation uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: persist-credentials: false @@ -30,36 +34,28 @@ jobs: ref: 905e66964a495d7596a577bb64e4158db9465864 path: .weave-inputs/ucns persist-credentials: false - - name: Checkout exact binary-domain producer - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - repository: The-Interdependency/uchc - ref: 65ac43d5d4cf79082c0836f8545adeb0242e88fc - path: .weave-inputs/uchc - persist-credentials: false - - name: Verify source pins, native cycle and existing transport regressions + - name: Verify native cycle and existing regressions run: | set -euo pipefail export WEAVE_SOURCES="$GITHUB_WORKSPACE/.weave-inputs" test "$(git -C "$WEAVE_SOURCES/ucns" rev-parse HEAD)" = "905e66964a495d7596a577bb64e4158db9465864" - test "$(git -C "$WEAVE_SOURCES/uchc" rev-parse HEAD)" = "65ac43d5d4cf79082c0836f8545adeb0242e88fc" python3 -VV PYTHONDONTWRITEBYTECODE=1 python3 research/weave/test.py --receipt "$RUNNER_TEMP/weave-check.json" cat "$RUNNER_TEMP/weave-check.json" - PYTHONDONTWRITEBYTECODE=1 python3 research/weave/cycle.py demo - - name: Package tracked source and evidence for inspection, not release + PYTHONDONTWRITEBYTECODE=1 python3 research/weave/cycle.py demo | tee "$RUNNER_TEMP/weave-demo.json" + - name: Package tracked research source and observed evidence if: always() run: | set -euo pipefail mkdir -p "$RUNNER_TEMP/weave-inspection" git archive --format=zip --output="$RUNNER_TEMP/weave-inspection/stack.zip" HEAD research/weave tools stack-manifest.json STACK_MANIFEST.md AGENTS.md README.md docs git -C .weave-inputs/ucns archive --format=zip --output="$RUNNER_TEMP/weave-inspection/ucns.zip" HEAD - git -C .weave-inputs/uchc archive --format=zip --output="$RUNNER_TEMP/weave-inspection/uchc.zip" HEAD binary AGENTS.md README.md pyproject.toml git rev-parse HEAD > "$RUNNER_TEMP/weave-inspection/STACK_HEAD.txt" git -C .weave-inputs/ucns rev-parse HEAD > "$RUNNER_TEMP/weave-inspection/UCNS_HEAD.txt" - git -C .weave-inputs/uchc rev-parse HEAD > "$RUNNER_TEMP/weave-inspection/UCHC_HEAD.txt" - if test -f "$RUNNER_TEMP/weave-check.json"; then cp "$RUNNER_TEMP/weave-check.json" "$RUNNER_TEMP/weave-inspection/"; fi - - name: Retain exact-source inspection bundle + for evidence in weave-check.json weave-demo.json; do + if test -f "$RUNNER_TEMP/$evidence"; then cp "$RUNNER_TEMP/$evidence" "$RUNNER_TEMP/weave-inspection/"; fi + done + - name: Retain exact-source research bundle, not a security release if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: diff --git a/AGENTS.md b/AGENTS.md index 2d6a98c..7e8b2ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -150,9 +150,10 @@ coherence; it does not replace workspace behavioral tests. independent repository/release authority boundary; exact UCNS affixiation geometry is unresolved. - URPCS is retired for specification divergence. Why GPT substituted the different architecture remains `hmmm`; its retained evidence is historical and implementation-local. -- Weave is specification-first research. Uneven partitions, stage-input - composition, schedule encoding, thread/corpus binding, hyperspace/gonol binding, - authentication/state, and the threat model remain `hmmm` until explicitly resolved. +- Weave remains research. Its explicit native binary sequence-cycle candidate + lives in `research/weave/native_binary.py`; UCNS geometry is consumed, not + redefined. UCHC binary graduation has not occurred. Private/public lifting, + authentication/state and full-cipher security remain `hmmm`. - The complete root `skill-lib/` snapshot refresh remains separate because the current provenance-bound fresh-making doctrine is newer than the local generator snapshot. - Project graduation automation remains unimplemented. diff --git a/README.md b/README.md index df73a78..87828ce 100644 --- a/README.md +++ b/README.md @@ -305,3 +305,15 @@ and detailed design attribution remain unresolved; see Weave native public/private key derivation and source of asymmetry remain unresolved. Provenance metadata is not an implementation of that missing law. + +### Executable Weave sequence cycle + +`research/weave/CYCLE.md` documents the current corpus-normalization → repeated +sequence affixiation → prime-derived bit-interleave profile and exact inverse. +The binary origin/sequence candidate remains Stack-owned in +`research/weave/native_binary.py` and consumes unchanged native UCNS geometry. +UCHC supplies the origin/axis architecture reference; no new binary package has +graduated there. Private/public key derivation and security remain separate work. + +Run `WEAVE_SOURCES=/checkouts python research/weave/cycle.py demo` with the exact +UCNS source named in `research/weave/CYCLE_NATIVE.json`. diff --git a/STACK_MANIFEST.md b/STACK_MANIFEST.md index 0ef3272..dce11bf 100644 --- a/STACK_MANIFEST.md +++ b/STACK_MANIFEST.md @@ -17,7 +17,7 @@ Provenance and authority-boundary record for `The-Interdependency/stack`. - English four-view complete-corpus audit UTC: `2026-09-22`, exact view source `1f9a35eb355296fc88d09784c7a3e2e95511ca31`; all 164,864 words, native and independent agreement. - Stack-manifest schema: `the-interdependency.stack-manifest` version `1.1.0` - Work-graph digest (SHA-256 over canonical `repositories` + `research_participants` + `boundaries` JSON): - `889a1234456c29c27473bfddb7808b209b2e1451da10e596fa9feea0f6ea4918` + `dc2cac5ac1d609f7f72434514ec95a55e8020b0b9326c17883b1ef25e86baaa3` - Weave workspace creation UTC: `2026-09-28`; new specification-first research starts from the intended multi-arity interleaving mechanism without URPCS inheritance - Machine-readable copy: [`stack-manifest.json`](stack-manifest.json) @@ -63,7 +63,9 @@ release identity. | `research/epac/` | `The-Interdependency/stack` | `0e8384bbb60e4c2189016a212bdd0030d04aed7d` | historical forge evidence; active implementation consumed from the independent EPAC release | no | | `research/epac-derived-carrier/` | `The-Interdependency/stack` | `545e135e2efbbcf29f033ab5530ac4876a68b718` | active stack-local carrier audit consuming exact EPAC source; no EPAC implementation/public-contract or scientific standing transfer | no | | `research/urpcs/` | `The-Interdependency/stack` | `a428a41a38a8b30bacb7025a4d54070b228a8089` | retired substituted-codec historical evidence only; no active URPCS authority or evidence transfer | no | -| `research/weave/` | `The-Interdependency/stack` | `1ba201449731337dc20c564788f4303c8910bfdd` | full gonol/private-key/thread/corpus/interleave/asymmetric construction research; native derivation unresolved; no URPCS implementation inheritance or security standing | no | +| `research/weave/` | `The-Interdependency/stack` | `1ba201449731337dc20c564788f4303c8910bfdd` | Stack-owned native binary sequence-cycle candidate; private/public derivation unresolved; no URPCS inheritance or security standing | no | +| `research/weave/` | `The-Interdependency/ucns` | `905e66964a495d7596a577bb64e4158db9465864` | native exact axis-circle and Mobius geometry; source-verified unchanged mathematical producer for native_binary.py | no | +| `research/weave/` | `The-Interdependency/uchc` | `4ad94e92be10d2c4c875a848addfda46f3c7cfdb` | origin/axis architecture and domain migration boundary; architectural reference only; speculative binary implementation remains in Stack until graduation gates complete | no | | `research/english-gonol/` | `The-Interdependency/stack` | `99b3598b02a6e683b3c84184d8ea443b12fc0e1a` | stack-local English lexical/gonol construction separated from EDCM; full pinned-corpus v2 build/replay survived; consumes UCNS geometry; EDCM may evaluate outputs but does not define construction | no | | `research/english-gonol/` | `The-Interdependency/edcm` | `0873c105799681ea1f7ccb3e619d1f7aebbd85e0` | exact repaired EDCM semantic metric-origin producer; preserves canonical O/L identities and proxy-alignment metadata without refreshing `libs/edcm` or transferring measurement authority | no | | `research/python-gonol/` | `The-Interdependency/stack` | `0e8384bbb60e4c2189016a212bdd0030d04aed7d` | stack-local bottom-up Python 3.12 source gonol construction; applies METAPAT affixiation semantics, consumes optional UCNS geometry, and transfers no language authority to UCNS or EDCM | no | @@ -237,7 +239,7 @@ independent repository, release, or security-reviewed cryptosystem. - English Gonol Construction remains stack-local research; independent repository/release authority and the exact UCNS displacement law have not been established. - Python Gonol Construction remains stack-local research; independent repository/release authority and exact UCNS affixiation geometry remain unresolved. - URPCS is retired for specification divergence; why GPT flattened/substituted the intended construction remains unresolved, and retained evidence is scoped only to the substituted implementation. -- Weave preserves the intended mechanism, while uneven partitioning, stage-input composition, schedule encoding, thread/corpus binding, hyperspace/gonol binding, authentication/state, and the threat model remain unresolved. +- Weave now runs one explicit native sequence-cycle profile with corpus normalization and prime-derived splits; cryptographic lifting, authentication/state and full-cipher security remain unresolved. - `skill-lib/` remains a special operational snapshot at stack root rather than following the ordinary `libs/` + `research/` pair. ## UCHC input candidate @@ -255,3 +257,17 @@ and detailed design attribution remain unresolved; see Weave native public/private key derivation and source of asymmetry remain unresolved. Provenance metadata is not an implementation of that missing law. + +## Weave native sequence-cycle candidate + +`research/weave/native_binary.py` is the Stack-owned binary-origin and sequence +closure implementation. It consumes the exact UCNS sources above and follows the +UCHC origin/axis architecture, without shipping an ungraduated UCHC domain. The +misplaced UCHC #7 proposal is retained only as source provenance in +`research/weave/NATIVE_BINARY_PROVENANCE.json`. No geometry authority transfers. + +The cycle normalizes once with corpus bytes, discovers repeated multi-byte +sequences, retains native whole/occurrence-circle relations, uses scoped numeral +references and prime-route bit partitions, and reverses the completed rounds. +See `research/weave/CYCLE.md` for the explicit profile, exact source lock, bounds +and all-cost accounting. This is construction/recovery, not established encryption. diff --git a/research/weave/ASSEMBLY.md b/research/weave/ASSEMBLY.md index 75c5699..715c170 100644 --- a/research/weave/ASSEMBLY.md +++ b/research/weave/ASSEMBLY.md @@ -1,5 +1,11 @@ # Weave: full modular assembly v1 +**Historical transport/assembly scope.** This document describes the retained +transport experiment, not the current binary sequence cycle. Current executable +construction and ownership are in [CYCLE.md](CYCLE.md); current question +status is in [QUESTIONS.md](QUESTIONS.md). The older unresolved declarations below +apply to that assembly, not to already-implemented parts of the new cycle. + Status: ASSEMBLY IMPLEMENTED; COMPLETE ENCRYPTION NOT IMPLEMENTED. This is a source-bound composition contract and switchable runner, not a replacement diff --git a/research/weave/CYCLE.md b/research/weave/CYCLE.md index 077fab0..ea6b407 100644 --- a/research/weave/CYCLE.md +++ b/research/weave/CYCLE.md @@ -9,7 +9,7 @@ key generation and cryptographic security are not implemented by this profile. length inside the normalized frame. 2. Discover repeated multi-byte sequences in the current byte stream. Select nonoverlapping occurrences, retaining every residual byte. -3. Close the sequences through the UCHC binary-domain producer. The eighth/whole +3. Close the sequences through the Stack binary-domain candidate. The eighth/whole circle holds native sequence attachments; seven occurrence circles hold count, order and native positions. Sequential byte occurrences remain addressable axes. 4. Bind definitions through the existing length-bearing integer/Unicode numeral @@ -45,7 +45,7 @@ not attributed to the user as universal laws: Counts mean selected partition occurrences, not all overlapping substring matches. No fixed chunk length or silent candidate truncation is introduced. This is not a globally optimal compression claim. -- **native binary attachment:** UCHC attaches each saved sequence at its first +- **native binary attachment:** The Stack candidate attaches each saved sequence at its first selected occurrence's native byte axis. UCNS owns the exact `r/N` axis position and complete 720-degree state. Source data, occurrences and native objects remain inside the construction; hashes merely identify it. @@ -67,20 +67,27 @@ profile identity without redefining UCNS or UCHC geometry. ## Native dependency boundary -`CYCLE_NATIVE.json` pins the exact UCHC producer commit/module blob and its two -unchanged UCNS source modules. `cycle_native.py` verifies and executes those exact -buffers. It refuses missing/changed sources before message processing, never fills -that gap with a fabricated class or language corpus fixture. +`CYCLE_NATIVE.json` v2 binds the exact local `native_binary.py` blob and its two +unchanged UCNS producer modules at `905e66964a495d7596a577bb64e4158db9465864`. +`cycle_native.py` executes a fresh copy of each verified buffer on every load; +mutable `sys.modules` cache slots are never accepted as source evidence. -`CYCLE_WORK_GRAPH.json` records the complete consumer work graph. The native binary -producer is in UCHC, not an application-owned imitation. UCHC uses actual UCNS -`AxisCirclePosition` and `NativeMobiusState` instances. This does not claim general -cross-origin metric geometry, English/Python graduation, a private lift, or security. +The binary-origin/sequence implementation remains in its owning Stack forge. +UCHC `4ad94e92be10d2c4c875a848addfda46f3c7cfdb` supplies the architecture and +migration boundary, not a graduated binary runtime. The premature UCHC #7 code +is retained as provenance in `NATIVE_BINARY_PROVENANCE.json` and replaced by the +repaired forge-owned candidate. No English/Python migration or UCNS authority moves. -In recovery, transmitted native complete positions and configured space relations -are inverted through `NativeMobiusState.advance`. The resulting exact source axes -determine byte placement. Complete coverage, no overlap/gaps, source/round identity, -whole-circle attachments and canonical seven-stream order must all replay. +Actual UCNS `AxisCirclePosition` and `NativeMobiusState` objects construct and +recover positions. The eighth remains the whole/origin; seven circle streams +preserve sequence occurrence order. `SequenceTable` validates source, order, +definitions, every occurrence, and native positions including space offsets on +construction and before exporting a receipt or reconstruction. A frozen record +with a forged relation is rejected, including through `dataclasses.replace`. + +The v2 lock and binary-domain identity deliberately reject records from the old +ungraduated candidate. Rebuild research records using this exact profile/source. +Native representation is not authentication, a trapdoor, or a security claim. ## Framing and full size accounting @@ -104,10 +111,10 @@ The three-round nonsecret demo measured 784 original bytes, normalized to 1,024: | Round | Input bytes | Definitions | Selected occurrences | Complete output bytes | |---|---:|---:|---:|---:| | 0 | 1,024 | 8 | 11 | 910 | -| 1 | 910 | 32 | 44 | 1,494 | -| 2 | 1,494 | 178 | 248 | 5,054 | +| 1 | 910 | 41 | 56 | 1,669 | +| 2 | 1,669 | 231 | 321 | 6,632 | -The outer frame adds 103 bytes: total **5,157 bytes**, with exact recovery. +The outer frame adds 103 bytes: total **6,735 bytes**, with exact recovery. This example expands overall. Later rounds can create metadata cost larger than any repeated-sequence saving. No claim that additional rounds always compress, strengthen secrecy, or produce independent transformations is made. @@ -115,7 +122,7 @@ strengthen secrecy, or produce independent transformations is made. ## Usage Python 3.12+; the consumer and native operations require only the standard library. -The source root contains the locked `ucns/` and `uchc/` checkouts. The runnable +The source root contains the locked `ucns/` checkout. No UCHC runtime checkout is required. The runnable bundle supplies exact, read-only minimal snapshots under `sources/`. ```sh @@ -155,7 +162,10 @@ and malformed-input versus resource-limit classification. Three new regressions bring its suite to 18. The obsolete bit-per-circle modules and their 22 tests are removed, with these sequence/native operations as replacement. Fifty-two original transport/assembly tests retain their original limited scope. Full Weave inventory: -52 + 18 + 30 = 100. Native UCHC has a separate 13-test producer suite. +52 + 18 + 30 + 13 native-origin + 12 repair regressions = 125. The original +13 native producer tests are retained in Stack as `tests/test_binary_origin.py`. +The repair regressions exercise forged closed records, fresh verified module +loading, atomic failed-write cleanup, empty-input attachments and manifest edges. The cycle is opt-in. The original transport experiment and the all-on native cipher refusal remain separate; no missing asymmetric operator is silently filled. @@ -169,3 +179,12 @@ actual private/public degeneration and recovery advantage, authentication/replay and security analysis of the completed cipher remain open. The named normalization, selection, attachment and determinant profiles are implemented candidates, not universal rules. This cycle now executes those choices and reports their real cost. + +## Output failure boundary + +Both `cycle.py` and `numeral.py` write a private temporary sibling, flush, fsync +and close it, then install it with an atomic no-clobber hard link. Write, flush, +fsync or close failure leaves no partial destination. Existing files and symlinks +are never overwritten. A filesystem without the required hard-link semantics +refuses explicitly. Abrupt process-crash cleanup and directory durability are +not guaranteed. Empty numeral input still validates its required attachments. diff --git a/research/weave/CYCLE_NATIVE.json b/research/weave/CYCLE_NATIVE.json index 0ca551b..d745f4b 100644 --- a/research/weave/CYCLE_NATIVE.json +++ b/research/weave/CYCLE_NATIVE.json @@ -1,13 +1,18 @@ { - "schema": "weave.native-inputs/v1", + "schema": "weave.native-inputs/v2", "ucns_commit": "905e66964a495d7596a577bb64e4158db9465864", "ucns_sources": { "src/ucns/axis_circle.py": "768777c8eca6e65537fdbab2003835d6f4f40569", "src/ucns/direct_mobius.py": "14a4cee36b5bbfa72cf3c03703c427abdac7f33d" }, - "uchc_source": { - "commit": "65ac43d5d4cf79082c0836f8545adeb0242e88fc", - "path": "binary/uchc_binary/origin.py", - "git_blob": "300db7383b403acbff7ce9a94f7536ad047bc78b" + "binary_source": { + "owner": "The-Interdependency/stack", + "path": "native_binary.py", + "git_blob": "8b22cefa39129a6e1ab41de6ac7d495e8b90542e", + "standing": "forge-owned candidate, not graduated" + }, + "uchc_architecture": { + "commit": "4ad94e92be10d2c4c875a848addfda46f3c7cfdb", + "relation": "origin/axis architecture and migration boundary; not a binary runtime dependency" } } diff --git a/research/weave/CYCLE_WORK_GRAPH.json b/research/weave/CYCLE_WORK_GRAPH.json index f30a5a4..897c118 100644 --- a/research/weave/CYCLE_WORK_GRAPH.json +++ b/research/weave/CYCLE_WORK_GRAPH.json @@ -1,13 +1,13 @@ { "schema": "the-interdependency.stack-manifest", "version": "1.0.0", - "work_graph_sha256": "f75fda260793ff46acab5bdc5f4a597065c1f7fe3d25cd781b9a48de05f062e3", + "work_graph_sha256": "42bcc4c2b1b4e13aa42d71d2760e47762b65fe0c9f92fdd1fcd9808ba3bdb546", "repositories": [ { "repository": "The-Interdependency/uchc", - "commit": "65ac43d5d4cf79082c0836f8545adeb0242e88fc", - "authority": "binary-domain closure and public contract", - "relation": "exact new native binary producer consumed by the cycle" + "commit": "4ad94e92be10d2c4c875a848addfda46f3c7cfdb", + "authority": "origin/axis architecture and migration boundary", + "relation": "architectural reference only; no ungraduated binary runtime or public package" }, { "repository": "The-Interdependency/ucns", @@ -17,9 +17,9 @@ }, { "repository": "The-Interdependency/stack", - "commit": "f547d755dc999bf4ba0aa2ae3d67c42ea723dc76", - "authority": "Weave sequence selection and round composition", - "relation": "consumer starting point; this change completes the explicit sequence-cycle profile" + "commit": "8c737f1e129b90004e986fc74940990c507762f0", + "authority": "Weave binary-origin candidate, sequence selection and round composition", + "relation": "exact repair starting point; reviewed local native_binary.py is source-locked in CYCLE_NATIVE.json" }, { "repository": "The-Interdependency/skill-lib", @@ -43,7 +43,8 @@ "hmmm": [ "Named normalization, selection, attachment and split profiles are executable candidates, not universal rules.", "Public/private cryptographic lifting, authentication and full-cipher security remain unimplemented.", - "This graph changes no English/Python graduation or UCNS geometry authority." + "This graph changes no English/Python graduation or UCNS geometry authority.", + "Binary closure remains in the Stack forge; UCHC proposal #7 is provenance, not graduation or a runtime input." ] } } diff --git a/research/weave/EIGHT_CIRCLE.md b/research/weave/EIGHT_CIRCLE.md index 1583a2c..ca8cdcb 100644 --- a/research/weave/EIGHT_CIRCLE.md +++ b/research/weave/EIGHT_CIRCLE.md @@ -9,8 +9,8 @@ occurrences and order. This is not an eight-circle allocation for every byte. The eight circle count does not establish a 16-bit output codeword or total 2x size. The old `stages/eight_circle.py` and `stages/message_origin.py` structural records -and their 22 tests are removed. Native replacement is the UCHC binary-domain -origin/sequence producer, consumed by `cycle_native.py` and `cycle.py`. It uses +and their 22 tests are removed. Native replacement is the Stack-owned binary-origin/sequence candidate +(`native_binary.py`), consumed by `cycle_native.py` and `cycle.py`. It uses actual UCNS axis-circle and Mobius objects and performs exact coordinate recovery. The associated bit-per-circle questions Q11/Q13/Q14 no longer control construction. diff --git a/research/weave/IMPLEMENTED.md b/research/weave/IMPLEMENTED.md index beebe0e..b3facf6 100644 --- a/research/weave/IMPLEMENTED.md +++ b/research/weave/IMPLEMENTED.md @@ -1,5 +1,11 @@ # Weave — executable provisional stages +**Historical transport/assembly scope.** This document describes the retained +transport experiment, not the current binary sequence cycle. Current executable +construction and ownership are in [CYCLE.md](CYCLE.md); current question +status is in [QUESTIONS.md](QUESTIONS.md). The older unresolved declarations below +apply to that assembly, not to already-implemented parts of the new cycle. + **Delivered:** a five-stage transport experiment, each stage independently switchable, plus a source-pinned native-word adapter. **Not delivered:** a complete native asymmetric Weave cipher. The all-on native profile remains the default and reports unresolved diff --git a/research/weave/NATIVE_BINARY_PROVENANCE.json b/research/weave/NATIVE_BINARY_PROVENANCE.json new file mode 100644 index 0000000..09de2fa --- /dev/null +++ b/research/weave/NATIVE_BINARY_PROVENANCE.json @@ -0,0 +1,29 @@ +{ + "schema": "weave.binary-candidate-provenance/v1", + "source_repository": "The-Interdependency/uchc", + "source_commit": "65ac43d5d4cf79082c0836f8545adeb0242e88fc", + "source_path": "binary/uchc_binary/origin.py", + "source_git_blob": "300db7383b403acbff7ce9a94f7536ad047bc78b", + "source_proposal": "The-Interdependency/uchc#7", + "current_owner": "The-Interdependency/stack", + "current_path": "research/weave/native_binary.py", + "standing": "incubating", + "reason": "Premature UCHC placement is withdrawn; existing migration gates require the candidate to remain in the Stack forge. The native UCNS geometry is unchanged.", + "changes": [ + "Complete closed-table validation", + "Fresh verified-buffer module instances, never sys.modules cache reuse", + "Explicit source and authority records" + ], + "authority_transfer": false, + "hmmm": [ + "No domain graduation, public-key secrecy, or authentication is established." + ], + "license": "FSL-1.1-ALv2", + "license_url": "https://github.com/The-Interdependency/uchc/blob/65ac43d5d4cf79082c0836f8545adeb0242e88fc/LICENSE", + "copyright": "Copyright 2026 Erin Spencer", + "notice_url": "https://github.com/The-Interdependency/uchc/blob/65ac43d5d4cf79082c0836f8545adeb0242e88fc/NOTICE", + "license_scope": [ + "native_binary.py", + "tests/test_binary_origin.py" + ] +} diff --git a/research/weave/NUMERAL_CONSTRUCTION.md b/research/weave/NUMERAL_CONSTRUCTION.md index 2f05b90..0ba843e 100644 --- a/research/weave/NUMERAL_CONSTRUCTION.md +++ b/research/weave/NUMERAL_CONSTRUCTION.md @@ -14,7 +14,7 @@ MSB-first packing; unused low bits in a partial last byte must be zero. Unicode private-use reference and exact supplied attachment. Angles are Fractions in [0,2) turns and occurrence circles are 1..7. This standalone codec does not invent native geometry. The cycle supplies attachments from real UCNS objects -through the UCHC binary producer. +through the Stack binary-origin candidate. `Packet(origin, round_id, entries, symbols)` contains definitions and ordered references. The same symbol may denote different blocks in different scopes. diff --git a/research/weave/QUESTIONS.md b/research/weave/QUESTIONS.md index f8e8410..f076a1e 100644 --- a/research/weave/QUESTIONS.md +++ b/research/weave/QUESTIONS.md @@ -13,8 +13,9 @@ interleave the resulting bits. Repeat affixiation and interleaving; reverse in t opposite order. Preserve residual material and each round's reconstruction information. The message is the origin; sequential byte occurrences are axes. Equal byte values -remain separate occurrences. UCNS owns geometry, UCHC owns the binary-domain native -closure, and Stack/Weave owns selection, scheduling and cycle composition. +remain separate occurrences. UCNS owns geometry. Stack/Weave owns this binary-domain closure candidate, +selection, scheduling and cycle composition. UCHC supplies the origin/axis +architecture and migration boundary; no binary domain has graduated there. ## Implemented candidate answers @@ -31,7 +32,7 @@ and inverse, failures, budgets and size accounting are in `CYCLE.md`. | ID | Current disposition | |---|---| -| Q1 — Native admission | Exact raw byte occurrences now use UCHC binary origins and native UCNS axes. No text database prerequisite. | +| Q1 — Native admission | Exact raw byte occurrences use the Stack binary-origin candidate and native UCNS axes. No text database prerequisite. | | Q2 — Private gonol/public counterpart | Intended full configuration and whole-plus-one exposure retained; asymmetric derivation is not implemented by positional recovery. | | Q3 — Threads | Seven occurrence-circle streams participate in current native recovery. No automatic equivalence to all historical complementary cryptographic-thread proposals is asserted. | | Q4 — Corpus | Actual corpus bits normalize once in the explicit bucket profile and are verified on reverse. Earlier traversal trial is not substituted for this cycle. | diff --git a/research/weave/README.md b/research/weave/README.md index c27e070..aa5e6eb 100644 --- a/research/weave/README.md +++ b/research/weave/README.md @@ -1,138 +1,62 @@ # Weave -Standing: **stack-local research; specification-first; no security claim**. +Standing: **Stack-owned construction research; no established encryption security.** -[Executed source-bound results](REPORT.md) include literal-operation recovery, -schedule equivalences and a fixed-map attack. The complete design remains untested; -these results are not a whole-system verdict. +Weave preserves the intended system: native gonol/origin construction, private-gonol +recovery, reconstruction-related streams, selected corpus material, ordered +multi-arity bit interleaving, and an eventual asymmetric public/private relation. +The interleave is one operation, not the project identity. URPCS remains retired; +its substituted codec, implementation and security findings are not inherited. -Weave is the replacement research workspace for Erin Spencer's intended encryption -system after retirement of the substituted URPCS implementation. +## Current executable cycle -The system is **not** merely an interleaving permutation. The preserved architecture -contains multiple cooperating layers. Development may stage those layers for testing, -but no layer may be silently discarded or replaced by a familiar construction. +Normalize once with actual corpus bits. Discover repeated multi-byte sequences, +retain each sequence at an attachment on the eighth/whole circle and its ordered +occurrences among the other seven. Serialize exact numeral references and native +positions, derive bit divisions from executed prime-index and embedded-span paths, +first/last interleave, and repeat. Reverse from the final record, corpus, profile +and exact source dependencies without the original input or encoder trace. -## Preserved architecture +[The cycle contract](CYCLE.md) records the explicit candidate choices, all-cost +accounting, budgets and exact source lock. These choices are not universal laws. +[The numeral layer](NUMERAL_CONSTRUCTION.md) separately records length-bearing +integers, Unicode references and exact prime recipes. -### 1. Hyperspace / gonol plaintext construction +`native_binary.py` is the Stack-owned binary-origin/sequence candidate, consuming +actual source-verified UCNS `AxisCirclePosition` and `NativeMobiusState` objects. +UCHC supplies the origin/axis architecture reference; the premature UCHC #7 +implementation is retained only as provenance. Domain graduation has not occurred. +The eighth remains the whole/origin, not one bit of a byte. Old bit-per-circle +records and their misleading attack interpretations remain removed. -Plaintext is intended to be processed into a hyperspace gonol construction rather than -treated only as an opaque byte string. The information is represented as a nested gonol -set before or as part of encryption. +## Usage -Recovery is intended to require a **private gonol** associated with the private key. -The exact reversible binding and key representation remain unresolved and must be -specified rather than invented. +Python 3.12+ and the standard library, with the exact UCNS source in `/checkouts/ucns`: -### 2. Multiple threads - -The encrypted construction uses multiple threads/data streams, with arity at least -three and contemplated thread counts including three, five, seven, or more. - -Each thread must contribute to reconstruction. A single independently useful stream is -not the intended construction. - -### 3. Thread-associated corpus/material - -Threads may be associated with user-selected corpus/material such as literary works, -music, technical manuals, sounds, or other chosen source material. - -The corpus/material is intended to participate in reconstruction as semi-secret -context, not merely as documentation or a label. Its exact derivation and binding rule -remain to be frozen. - -### 4. Multi-arity bit interleaving - -The bit transform is one load-bearing layer inside Weave. - -For an explicitly declared ordered sequence of division arities: - -1. divide the working bit sequence into the declared number of sections; -2. within each section interleave inward from opposite ends: - last bit, first bit, next-to-last bit, next-from-first bit, and so on; -3. repeat for the declared arity sequence, examples already given including fifths, - sevenths, and thirds; -4. after the declared levels, interleave the resulting whole sequence in the same - opposite-end manner. - -Stage count is distinct from arity. “Arity three, minimum” does not establish a -minimum of three stages; the unsupported assistant-added restriction is removed. - -Knowing the division choices, their order, and the number of levels is part of the -reconstruction problem unless an explicit later law changes that role. Different -schedule descriptions can induce the same map; measure equivalence rather than -assuming schedule uniqueness. - -### 5. Keying / recovery structure - -The intended system ultimately requires asymmetric recovery structure with no silent -dependency on an external cryptographic system. The private side is intended to include -the private gonol and the structural information needed to reverse the construction. - -The exact public/private derivation law is not yet specified. That absence is `hmmm`, -not permission to substitute RSA, ECC, Diffie-Hellman, a conventional KEM, or URPCS. - -## Non-inheritance boundary - -`research/urpcs/` is historical evidence of a substituted GPT-produced construction. -No URPCS law, codec, test, result, or security conclusion is an implementation -dependency of Weave. - -URPCS may be consulted only as a specification-divergence witness: an example of what -must not happen again. - -## Development discipline - -The full architecture is preserved from the start, while implementation proceeds in -separable layers so each contribution can be falsified. The order listed below is an -assistant proposal, not a user-selected dependency law; actual dependencies must come -from the complete source-backed construction. - -1. Freeze the complete dataflow and the inverse dependencies among gonol construction, - threads, corpus/material, interleaving, and key structure. -2. Specify exact bit-level interleaving and inverse rules without changing the mechanism. -3. Specify thread formation and recombination. -4. Specify corpus/material derivation and binding. -5. Specify hyperspace/private-gonol construction and recovery. -6. Specify the asymmetric public/private relation. -7. Implement the smallest complete round-trip profile containing every required layer. -8. Build an independently structured decoder/recovery implementation from the written - contract. -9. Perform adversarial analysis before making any confidentiality or production claim. - -A reduced test harness may isolate one layer, but results from a reduced harness may not -be presented as results for Weave as a whole. - -## Usage guidance - -Start here: - -```bash -cat research/weave/SPECIFICATION.md -cat research/weave/BASE.json -``` - -Reproduce the executed component evidence with Python's standard library only: - -```bash +```sh cd research/weave -python probe.py > receipt.json -python probe.py --check receipt.json +WEAVE_SOURCES=/checkouts python cycle.py demo +python cycle.py forward input.bin output.wvc --corpus corpus.bin --sources /checkouts +python cycle.py reverse output.wvc recovered.bin --corpus corpus.bin --sources /checkouts +WEAVE_SOURCES=/checkouts python test.py --receipt /tmp/weave-check.json ``` -See [PLAN.md](PLAN.md) for the pre-execution scope and [REPORT.md](REPORT.md) for -results, mathematical derivations, exact assumptions and the expected receipt digest. +`CYCLE_NATIVE.json` binds the actual source blobs. The runnable bundle includes the +minimal exact UCNS files under `sources/`. Existing files are never overwritten; +ordinary write/flush/close failures leave no partial destination. The output is +an experimental representation, not safe storage for secrets. + +## Evidence boundaries -Do not use Weave to protect real secrets until an explicit security contract and -adversarial evidence justify that use. +`SPECIFICATION.md` preserves the full intended system and latest clarification. +`QUESTIONS.md` preserves question identities and distinguishes implemented candidate +answers from unfinished cryptography. `EIGHT_CIRCLE.md` is historical correction. +`ASSEMBLY.md`, `IMPLEMENTED.md`, `PLAN.md` and `REPORT.md` retain the earlier transport +experiment under its original scope. Its fixed-map attacks are not full-system verdicts. ## hmmm -Uneven section partition selection; exact ordering/composition of arity stages; thread -formation; corpus/material derivation; hyperspace/gonol encoding; private-gonol binding; -public/private key derivation; authentication, nonce/state, replay behavior; and the -threat model remain unresolved. They are preserved as required design boundaries, not -optional features and not invitations for model substitution. The original pre-substitution -thirteen-law conversation was not recovered; a missing retrieved source is not evidence -that the user never supplied the relation. +Actual private/public key derivation and recovery advantage, authentication/replay, +secret-key-space analysis, automatic compact prime-recipe discovery, and security +of the completed cryptosystem remain unfinished. Exact reconstruction and real +geometry do not themselves establish confidentiality or a fixed expansion ratio. diff --git a/research/weave/SPECIFICATION.md b/research/weave/SPECIFICATION.md index 6d8e8bd..db347b5 100644 --- a/research/weave/SPECIFICATION.md +++ b/research/weave/SPECIFICATION.md @@ -140,7 +140,7 @@ implemented boundary, full size accounting, exact replay, and usage. The numeral module remains a representation layer, not replacement geometry. The separate cycle now connects automatic repeated-sequence discovery, actual -corpus normalization, native UCHC/UCNS sequence closure and coordinate recovery, +corpus normalization, Stack-owned sequence closure using native UCNS geometry and coordinate recovery, prime-route split derivation, and the repeated first/last bit interleave. See [CYCLE.md](CYCLE.md) for the exact named profile, inverse, tests and byte accounting. This implements the corrected sequence cycle, not public-key encryption. diff --git a/research/weave/cycle.py b/research/weave/cycle.py index 2709c4c..a58edd4 100644 --- a/research/weave/cycle.py +++ b/research/weave/cycle.py @@ -38,7 +38,7 @@ python cycle.py forward INPUT OUTPUT --profile cycle-profile.json --corpus CORPUS --sources /checkouts python cycle.py reverse INPUT OUTPUT --profile cycle-profile.json --corpus CORPUS --sources /checkouts -/checkouts contains source-locked ucns/ and uchc/ trees; CYCLE_NATIVE.json names +/checkouts contains the source-locked ucns/ tree; CYCLE_NATIVE.json names exact modules. No packages beyond Python's standard library are required by this consumer. The runnable profile is explicit construction research, not secure storage. The newest sequence cycle replaces the historical one-bit-per-circle interpretation. @@ -52,6 +52,7 @@ from hashlib import sha256 from pathlib import Path import json +from safe_output import write_new import os from numeral import (BitBlock, Entry, Packet, PrimePath, Limits, Refused, ResourceLimit, @@ -476,8 +477,7 @@ def main() -> int: data = _read(args.input,limits.round_bytes+128) output = reverse(data,corpus,profile,args.sources,limits) report = {'output_bytes':len(output),'standing':'exact construction recovery; not security evidence'} - with args.output.open('xb') as target: - target.write(output) + write_new(args.output, output) print(json.dumps(report,indent=2)) return 0 except (OSError, ValueError, UnicodeError) as exc: diff --git a/research/weave/cycle_native.py b/research/weave/cycle_native.py index 94885f5..8d011a1 100644 --- a/research/weave/cycle_native.py +++ b/research/weave/cycle_native.py @@ -2,33 +2,33 @@ # id: weave_native_binary_binding # module_name: cycle_native # module_kind: adapter -# summary: loads the exact UCHC binary producer which consumes the pinned native UCNS constructors +# summary: loads this forge's exact binary candidate and its unchanged native UCNS producers # owner: Erin Spencer # public_surface: load_native -# internal_surface: locked source loading +# internal_surface: fresh checked-buffer module loading # auth_boundary: none # storage_boundary: read # network_boundary: none # user_data_boundary: none -# admin_only: false -# tests: tests/test_cycle.py -# rollout: required source-locked native dependency for the explicit cycle profile -# rollback: remove with the cycle consumer, never substitute fixture geometry +# tests: tests/test_cycle.py, tests/test_cycle_repairs.py +# rollout: research only; no UCHC package, graduation, or geometry replacement +# rollback: remove with the native binary cycle # === END MODULE_BUILD === # === CONTRACTS === # id: cycle_native_source_pinned -# given: supplied UCNS and UCHC checkouts -# then: execute only the declared exact source modules or refuse before processing data +# given: the local source lock and a supplied UCNS checkout +# then: execute freshly verified native buffers or refuse before processing data # === END CONTRACTS === -"""Usage: api, geometry = load_native('/checkouts-containing-ucns-and-uchc'). +"""Usage: api, geometry = load_native('/directory-containing-ucns'). -Reads source-locked producer buffers and executes those exact buffers. There is -no network access, mutable upstream vendoring, or reconstruction from labels. -The producer's scoped constructors retain their own status and authority. +The binary construction is Stack-owned research. UCHC supplies its architecture +reference, not a falsely graduated runtime package. UCNS remains geometry owner. +There is no cache reuse, network fallback, or fabricated native object. """ from hashlib import sha1 from pathlib import Path from types import ModuleType +from uuid import uuid4 import json import sys from numeral import Refused @@ -38,27 +38,33 @@ def load_native(sources: str | Path): lock = json.loads((ROOT/'CYCLE_NATIVE.json').read_text()) - entry = lock['uchc_source'] - path = Path(sources)/'uchc'/entry['path'] + if lock.get('schema') != 'weave.native-inputs/v2': + raise Refused('unsupported native source lock') + entry = lock['binary_source'] + if entry['path'] != 'native_binary.py' or entry['owner'] != 'The-Interdependency/stack': + raise Refused('binary candidate must remain in its declared forge') + path = ROOT/entry['path'] try: with path.open('rb') as source: data = source.read(65537) except OSError as exc: - raise Refused(f'native UCHC source unavailable: {path}') from exc + raise Refused(f'native binary source unavailable: {path}') from exc actual = sha1(b'blob '+str(len(data)).encode()+b'\0'+data).hexdigest() if len(data)>65536 or actual != entry['git_blob']: - raise Refused('native UCHC source does not match CYCLE_NATIVE.json') - name = '_weave_uchc_'+actual - if name not in sys.modules: - module = ModuleType(name) - module.__file__ = str(path) - sys.modules[name] = module - try: - exec(compile(data,str(path),'exec'),module.__dict__) - except BaseException: - sys.modules.pop(name,None) - raise - api = sys.modules[name] + raise Refused('native binary source does not match CYCLE_NATIVE.json') + name = '_weave_binary_' + actual + '_' + uuid4().hex + api = ModuleType(name) + api.__file__ = str(path) + sys.modules[name] = api + try: + exec(compile(data,str(path),'exec'),api.__dict__) + finally: + if sys.modules.get(name) is api: + del sys.modules[name] if api.UCNS_COMMIT != lock['ucns_commit'] or api.UCNS_BLOBS != lock['ucns_sources']: - raise Refused('producer and consumer native dependency locks disagree') - return api, api.Geometry(Path(sources)/'ucns') + raise Refused('candidate and consumer native dependency locks disagree') + try: + geometry = api.Geometry(Path(sources)/'ucns') + except api.BinaryError as exc: + raise Refused(str(exc)) from exc + return api, geometry diff --git a/research/weave/native_binary.py b/research/weave/native_binary.py new file mode 100644 index 0000000..8b22cef --- /dev/null +++ b/research/weave/native_binary.py @@ -0,0 +1,441 @@ +# === MODULE_BUILD === +# id: weave_binary_origin_affixiation +# module_name: native_binary +# module_kind: candidate +# summary: raw-byte origins and closed repeated-sequence participation using source-pinned native UCNS axis and Mobius constructors +# owner: Erin Spencer +# public_surface: Geometry, ByteOrigin, Occurrence, ClosedSequence, SequenceTable, close_sequences, recover_sequences +# internal_surface: exact source loader and canonical construction receipts +# auth_boundary: none +# storage_boundary: read +# network_boundary: none +# user_data_boundary: read +# admin_only: false +# tests: tests/test_binary_origin.py +# rollout: Stack-owned binary-domain candidate; no UCHC graduation or public package +# rollback: remove with the Weave cycle; original misplaced proposal is provenance only +# requires: ucns_axis_circle_position_candidate, ucns_native_mobius_geometry +# unresolved: cryptographic public/private lift and cross-origin metric geometry are not supplied +# === END MODULE_BUILD === +# === CONTRACTS === +# id: binary_origin_byte_axes +# given: exact raw bytes at a message-origin and round +# then: every byte occurrence is addressable by its native UCNS axis without collapsing equal values +# id: binary_sequence_closure +# given: unique byte-sequence definitions and an ordered partition of a source +# then: closure retains sequence values, byte order, multiplicity, and both source and occurrence-circle attachments +# id: binary_native_geometry +# given: the exact declared UCNS source modules and supplied space relations +# then: native axis and Mobius objects perform all geometric construction; 360 and 720 returns remain distinct +# id: binary_coordinate_recovery +# given: definitions, seven ordered circle streams and their native complete positions +# then: inverse native displacement recovers exact source placement without an original-message copy +# id: binary_source_refusal +# given: missing or changed UCNS source or invalid occurrence/partition inputs +# then: refuse instead of substituting geometry or silently repairing data +# === END CONTRACTS === +"""Binary-domain origin and sequence closure, not a cipher or language model. + +Usage: geometry = Geometry('/checkout/ucns') + origin = ByteOrigin(b'ABxABy', 'message-1', 0, geometry) + table = close_sequences(origin, (b'AB', b'x', b'y'), (0,1,0,2), + (1,2,3), (Fraction(0),) * 8) + assert table.restore() == b'ABxABy' + +The caller owns segmentation and circle/space selection. This producer owns raw +byte participation and closure; UCNS owns every geometric object. Source receipts +identify constructions; they are not substitutes for the retained bytes, secret +keys, authentication, or mathematical proof. Byte axes are determinable on demand. +The eighth/whole circle uses index 0 in this API; occurrence circles are 1..7. +""" +from __future__ import annotations + +from dataclasses import dataclass, field +from fractions import Fraction +from hashlib import sha1, sha256 +from pathlib import Path +from types import ModuleType +import json +import re +import sys +from uuid import uuid4 + +SCHEMA = 'weave.binary-sequence-origin' +VERSION = '0.2.0' +UCNS_COMMIT = '905e66964a495d7596a577bb64e4158db9465864' +UCNS_BLOBS = { + 'src/ucns/axis_circle.py': '768777c8eca6e65537fdbab2003835d6f4f40569', + 'src/ucns/direct_mobius.py': '14a4cee36b5bbfa72cf3c03703c427abdac7f33d', +} + + +class BinaryError(ValueError): + """Invalid native source, byte-origin, or sequence closure.""" + + +def canonical(obj) -> bytes: + return json.dumps(obj, sort_keys=True, separators=(',', ':'), + ensure_ascii=False, allow_nan=False).encode('utf-8') + + +def _digest(obj) -> str: + return sha256(canonical(obj)).hexdigest() + + +def _natural(value, name: str) -> int: + if type(value) is not int or value < 0: + raise BinaryError(f'{name} must be a nonnegative integer') + return value + + +def _sha(value: str) -> str: + if type(value) is not str or re.fullmatch('[0-9a-f]{64}', value) is None: + raise BinaryError('exact lowercase SHA-256 identity required') + return value + + +def _load_exact(path: Path, expected: str) -> ModuleType: + try: + with path.open('rb') as source: + data = source.read(65537) + except OSError as exc: + raise BinaryError(f'UCNS source unavailable: {path}') from exc + actual = sha1(b'blob ' + str(len(data)).encode() + b'\0' + data).hexdigest() + if len(data) > 65536 or actual != expected: + raise BinaryError(f'UCNS source identity mismatch: {path}') + # Each instance executes its own checked bytes. Never reuse a mutable public + # sys.modules slot or an earlier Geometry instance's instrumented functions. + name = '_weave_ucns_' + expected + '_' + uuid4().hex + module = ModuleType(name) + module.__file__ = str(path) + sys.modules[name] = module # dataclass annotation resolution during execution + try: + exec(compile(data, str(path), 'exec'), module.__dict__) + finally: + if sys.modules.get(name) is module: + del sys.modules[name] + return module + + + +class Geometry: + """Read the two exact UCNS modules; no network, package shadow, or fallback.""" + def __init__(self, ucns_root: str | Path): + root = Path(ucns_root) + self.axis_module = _load_exact(root/'src/ucns/axis_circle.py', + UCNS_BLOBS['src/ucns/axis_circle.py']) + self.mobius_module = _load_exact(root/'src/ucns/direct_mobius.py', + UCNS_BLOBS['src/ucns/direct_mobius.py']) + + def axis(self, origin_sha256: str, count: int, ordinal: int): + return self.axis_module.build_axis_circle_position( + origin_sha256=origin_sha256, axis_count=count, axis_ordinal=ordinal) + + def placed(self, axis, space: Fraction): + if type(space) is not Fraction: + raise BinaryError('space relationship must be an exact Fraction') + return self.mobius_module.native_mobius_state(axis.turn - space) + + def recover_axis(self, origin: str, count: int, position: Fraction, space: Fraction): + """Invert the native frame displacement and recover its exact source axis.""" + if type(position) is not Fraction or not 0 <= position < 2: + raise BinaryError('canonical complete position in [0,2) required') + if type(space) is not Fraction: + raise BinaryError('exact Fraction space relationship required') + state = self.mobius_module.native_mobius_state(position).advance(space) + ordinal = state.phase_turns * count + if state.frame.sign != 1 or ordinal.denominator != 1: + raise BinaryError('position does not lift to a source byte axis') + return self.axis(origin, count, ordinal.numerator) + + def lift(self, state) -> Fraction: + """Serialize the native complete frame in [0,2), not just visible phase.""" + return state.phase_turns + (1 if state.frame.sign < 0 else 0) + + +@dataclass(frozen=True) +class ByteOrigin: + source: bytes = field(repr=False) + scope: str + round_id: int + geometry: Geometry = field(repr=False, compare=False) + message_origin: str | None = None + + def __post_init__(self): + if type(self.source) is not bytes: + raise BinaryError('exact raw bytes required') + if type(self.scope) is not str or not self.scope or len(self.scope.encode('utf-8')) > 1024: + raise BinaryError('nonempty message scope of at most 1024 UTF-8 bytes required') + _natural(self.round_id, 'round') + if type(self.geometry) is not Geometry: + raise BinaryError('native Geometry required') + source_digest = sha256(self.source).hexdigest() + if self.round_id == 0: + root = _digest({'schema': SCHEMA, 'version': VERSION, 'scope': self.scope, + 'byte_length': len(self.source), 'source_sha256': source_digest}) + if self.message_origin is not None and self.message_origin != root: + raise BinaryError('initial message-origin does not identify the source') + object.__setattr__(self, 'message_origin', root) + else: + _sha(self.message_origin) + # Memoize the source digest once. Axis queries must not re-hash the source. + identity = _digest({'schema': SCHEMA, 'version': VERSION, 'scope': self.scope, + 'message_origin': self.message_origin, 'round': self.round_id, + 'byte_length': len(self.source), 'source_sha256': source_digest}) + object.__setattr__(self, '_identity', identity) + + @property + def identity(self) -> str: + return self._identity + + def byte_axis(self, offset: int): + _natural(offset, 'byte offset') + if offset >= len(self.source): + raise BinaryError('byte offset outside this round') + return self.geometry.axis(self.identity, len(self.source), offset) + + +@dataclass(frozen=True) +class Occurrence: + source_offset: int + circle: int + ordinal: int + source_axis: object + circle_axis: object + state: object + source_state: object + + def __post_init__(self): + _natural(self.source_offset, 'source offset') + _natural(self.ordinal, 'circle ordinal') + if type(self.circle) is not int or not 1 <= self.circle <= 7: + raise BinaryError('occurrence circle must be 1..7') + + +@dataclass(frozen=True) +class ClosedSequence: + data: bytes = field(repr=False) + attachment_axis: object + state: object + occurrences: tuple[Occurrence, ...] + + def __post_init__(self): + if type(self.data) is not bytes or not self.data: + raise BinaryError('closed sequence requires nonempty exact bytes') + if (type(self.occurrences) is not tuple or not self.occurrences + or any(type(o) is not Occurrence for o in self.occurrences)): + raise BinaryError('closed sequence requires immutable occurrence records') + + @property + def repeated(self) -> bool: + return len(self.data) >= 2 and len(self.occurrences) >= 2 + + +@dataclass(frozen=True) +class SequenceTable: + origin: ByteOrigin + definitions: tuple[ClosedSequence, ...] + order: tuple[int, ...] + spaces: tuple[Fraction, ...] + + def __post_init__(self): + self.validate() + + def validate(self) -> None: + """Reconstruct and compare the entire closed relation, not its type label. + + Direct constructors and dataclasses.replace must satisfy the same source, + ordering, native axis, space-displacement and occurrence invariants as + close_sequences. Frozen does not mean intrinsically consistent. + """ + if type(self.origin) is not ByteOrigin: + raise BinaryError('closed table requires a native byte-origin') + if (type(self.definitions) is not tuple + or any(type(d) is not ClosedSequence for d in self.definitions)): + raise BinaryError('immutable closed definitions required') + origin = self.origin + reidentified = ByteOrigin(origin.source, origin.scope, origin.round_id, + origin.geometry, origin.message_origin) + if origin.identity != reidentified.identity: + raise BinaryError('origin identity no longer binds its source') + for definition in self.definitions: + definition.__post_init__() + for occurrence in definition.occurrences: + occurrence.__post_init__() + blocks = tuple(d.data for d in self.definitions) + circles = tuple(d.occurrences[0].circle for d in self.definitions) + expected = _closed_definitions(origin, blocks, self.order, circles, self.spaces) + if self.definitions != expected: + raise BinaryError('closed native relations disagree with source or placement') + + def restore(self) -> bytes: + """Read definitions and occurrence order, not origin.source.""" + self.validate() + return b''.join(self.definitions[i].data for i in self.order) + + def wire_occurrences(self) -> tuple[tuple[int, Occurrence], ...]: + """Seven circle streams, each ordered by its native complete local position.""" + self.validate() + items = [(i, o) for i, d in enumerate(self.definitions) for o in d.occurrences] + return tuple(sorted(items, key=lambda item: ( + item[1].circle, self.origin.geometry.lift(item[1].state)))) + + def receipt(self) -> dict: + self.validate() + return {'schema': SCHEMA, 'version': VERSION, 'origin': self.origin.identity, + 'message_origin': self.origin.message_origin, 'round': self.origin.round_id, + 'spaces': [str(space) for space in self.spaces], + 'definitions': [ + {'length': len(d.data), 'source_sha256': sha256(d.data).hexdigest(), + 'attachment': d.attachment_axis.as_dict(), + 'complete_turn': str(self.origin.geometry.lift(d.state)), + 'occurrences': [ + {'source_offset': o.source_offset, 'circle': o.circle, + 'ordinal': o.ordinal, 'source_axis': o.source_axis.as_dict(), + 'circle_axis': o.circle_axis.as_dict(), + 'complete_turn': str(self.origin.geometry.lift(o.state)), + 'source_turn': str(self.origin.geometry.lift(o.source_state))} + for o in d.occurrences]} + for d in self.definitions], 'order': list(self.order)} + + +def _closed_definitions(origin: ByteOrigin, blocks: tuple[bytes, ...], + order: tuple[int, ...], circles: tuple[int, ...], + spaces: tuple[Fraction, ...]) -> tuple[ClosedSequence, ...]: + """Close an exact ordered partition using native whole/occurrence attachments. + + The caller supplies selection and assignments. The explicit binary-domain + attachment rule uses the first selected occurrence's native byte axis for a + sequence's whole-circle attachment; occurrence circles retain source offsets + and ordered native local axes. No cross-origin angle or inner product is claimed. + """ + if type(origin) is not ByteOrigin: + raise BinaryError('ByteOrigin required') + if any(type(v) is not tuple for v in (blocks, order, circles, spaces)): + raise BinaryError('immutable partition inputs required') + if len(circles) != len(blocks) or len(spaces) != 8: + raise BinaryError('one circle per definition and exactly eight space relationships required') + if any(type(s) is not Fraction for s in spaces): + raise BinaryError('exact Fraction space relationships required') + if any(type(b) is not bytes or not b for b in blocks) or len(set(blocks)) != len(blocks): + raise BinaryError('definitions must be unique nonempty byte sequences') + if any(type(c) is not int or not 1 <= c <= 7 for c in circles): + raise BinaryError('occurrence circle must be 1..7') + if len(blocks) > len(origin.source) or len(order) > len(origin.source): + raise BinaryError('partition cardinality exceeds source byte count') + slots = [[] for _ in range(8)] + uses = [[] for _ in blocks] + offset = 0 + for index in order: + if type(index) is not int or not 0 <= index < len(blocks): + raise BinaryError('undefined sequence reference') + block = blocks[index] + if offset + len(block) > len(origin.source) or origin.source[offset:offset+len(block)] != block: + raise BinaryError('partition does not reconstruct the declared source') + circle = circles[index] + ordinal = len(slots[circle]) + slots[circle].append((offset, len(block))) + uses[index].append((offset, circle, ordinal)) + offset += len(block) + if offset != len(origin.source) or any(not group for group in uses): + raise BinaryError('partition omits source bytes or contains unused definitions') + circle_origins = [_digest({'origin': origin.identity, 'circle': i, 'slots': slots[i]}) + for i in range(8)] + definitions = [] + for index, block in enumerate(blocks): + attachment = origin.byte_axis(uses[index][0][0]) + occurrences = [] + for start, circle, ordinal in uses[index]: + source_axis = origin.byte_axis(start) + local_axis = origin.geometry.axis(circle_origins[circle], len(slots[circle]), ordinal) + occurrences.append(Occurrence(start, circle, ordinal, source_axis, local_axis, + origin.geometry.placed(local_axis, spaces[circle]), + origin.geometry.placed(source_axis, spaces[circle]))) + definitions.append(ClosedSequence(block, attachment, + origin.geometry.placed(attachment, spaces[0]), tuple(occurrences))) + return tuple(definitions) + + +def close_sequences(origin: ByteOrigin, blocks: tuple[bytes, ...], + order: tuple[int, ...], circles: tuple[int, ...], + spaces: tuple[Fraction, ...]) -> SequenceTable: + """Construct and validate the complete source-bound native relation. + + Usage: close_sequences(origin, blocks, order, circles, spaces). Selection and + the eight space relations are caller inputs; UCNS constructs all geometry. + """ + definitions = _closed_definitions(origin, blocks, order, circles, spaces) + return SequenceTable(origin, definitions, order, spaces) + + +def recover_sequences(geometry: Geometry, *, scope: str, message_origin: str, + round_id: int, origin_identity: str, byte_length: int, + blocks: tuple[bytes, ...], circles: tuple[int, ...], + spaces: tuple[Fraction, ...], counts: tuple[int, ...], + wire_order: tuple[int, ...], source_turns: tuple[Fraction, ...], + max_bytes: int = 1048576) -> SequenceTable: + """Recover source placement from seven native circle streams, then close again. + + No original message, plaintext occurrence list, or encoder trace is accepted. + The transmitted complete positions and supplied circle-space relationships + determine source axes. This is a reversible positional representation, not a + secret-key advantage or authenticated decryption. + """ + if type(geometry) is not Geometry: + raise BinaryError('native Geometry required') + _sha(origin_identity) + _sha(message_origin) + _natural(byte_length, 'source byte length') + if type(max_bytes) is not int or max_bytes < 1 or byte_length > max_bytes: + raise BinaryError('source byte length exceeds reconstruction budget') + if any(type(v) is not tuple for v in (blocks, circles, spaces, counts, wire_order, source_turns)): + raise BinaryError('immutable reconstruction records required') + if len(circles) != len(blocks) or len(spaces) != 8 or len(counts) != 7: + raise BinaryError('invalid circle reconstruction cardinality') + if any(type(b) is not bytes or not b for b in blocks): + raise BinaryError('nonempty byte definitions required') + if any(type(c) is not int or not 1 <= c <= 7 for c in circles): + raise BinaryError('occurrence circle must be 1..7') + if any(type(s) is not Fraction for s in spaces): + raise BinaryError('exact space relationships required') + if any(type(n) is not int or n < 0 for n in counts): + raise BinaryError('nonnegative circle occurrence counts required') + if sum(counts) != len(wire_order) or len(source_turns) != len(wire_order): + raise BinaryError('circle counts and coordinate records disagree') + if len(wire_order) > byte_length or len(blocks) > byte_length: + raise BinaryError('reconstruction cardinality exceeds byte length') + positions, cursor, total = [], 0, 0 + for circle, count in enumerate(counts, 1): + for _ in range(count): + index, turn = wire_order[cursor], source_turns[cursor] + if type(index) is not int or not 0 <= index < len(blocks) or circles[index] != circle: + raise BinaryError('reference is not on its declared occurrence circle') + axis = geometry.recover_axis(origin_identity, byte_length, turn, spaces[circle]) + positions.append((axis.axis_ordinal, index)) + total += len(blocks[index]) + if total > byte_length: + raise BinaryError('definitions exceed reconstructed byte length') + cursor += 1 + positions.sort() + offset, pieces, order = 0, [], [] + for start, index in positions: + if start != offset: + raise BinaryError('source positions overlap or leave a gap') + pieces.append(blocks[index]) + order.append(index) + offset += len(blocks[index]) + if offset != byte_length: + raise BinaryError('source positions do not cover the declared stream') + origin = ByteOrigin(b''.join(pieces), scope, round_id, geometry, message_origin) + if origin.identity != origin_identity: + raise BinaryError('reconstructed origin identity differs') + table = close_sequences(origin, blocks, tuple(order), circles, spaces) + replay = table.wire_occurrences() + if tuple(i for i, _ in replay) != wire_order or tuple(geometry.lift(o.source_state) for _, o in replay) != source_turns: + raise BinaryError('circle streams do not replay in canonical geometric order') + return table + + +__all__ = ['SCHEMA', 'VERSION', 'UCNS_COMMIT', 'UCNS_BLOBS', 'BinaryError', + 'Geometry', 'ByteOrigin', 'Occurrence', 'ClosedSequence', + 'SequenceTable', 'close_sequences', 'recover_sequences'] diff --git a/research/weave/numeral.py b/research/weave/numeral.py index 800f895..1b69fb7 100644 --- a/research/weave/numeral.py +++ b/research/weave/numeral.py @@ -54,6 +54,7 @@ from math import isqrt from pathlib import Path import json +from safe_output import write_new MAGIC = b"WNC\x01" @@ -221,6 +222,13 @@ def _symbol(symbol: str) -> None: raise Refused("symbol must be a Unicode private-use scalar") +def _validate_attachment(angle: Fraction, circle: int) -> None: + if type(angle) is not Fraction or not 0 <= angle < 2: + raise Refused("exact supplied angle must be in [0,2) turns") + if type(circle) is not int or not 1 <= circle <= 7: + raise Refused("occurrence circle must be one of the seven") + + @dataclass(frozen=True) class Entry: symbol: str @@ -235,10 +243,7 @@ def validate(self, limits: Limits, *, _engine=None) -> None: raise Refused("a definition must contain a nonempty BitBlock") if self.block.length > limits.output_bits: raise ResourceLimit("definition exceeds bit budget") - if type(self.angle) is not Fraction or not 0 <= self.angle < 2: - raise Refused("exact supplied angle must be in [0,2) turns") - if type(self.circle) is not int or not 1 <= self.circle <= 7: - raise Refused("occurrence circle must be one of the seven") + _validate_attachment(self.angle, self.circle) if self.recipe is not None: if type(self.recipe) is not PrimePath or self.recipe.replay(limits, _engine=_engine)[-1] != self.block.value: raise Refused("recipe does not construct its bound integer") @@ -542,19 +547,18 @@ def main() -> int: if len(raw) > limit: raise ResourceLimit("input grew beyond execution budget") if args.command == "bind": + _validate_attachment(args.angle, args.circle) entry = Entry(chr(0xE000), BitBlock.from_bytes(raw), args.angle, args.circle) packet = Packet(args.origin, 0, (entry,) if raw else (), entry.symbol if raw else "") wire = encode(packet) - with args.output.open("xb") as target: - target.write(wire) + write_new(args.output, wire) result = accounting(packet) else: packet = decode(raw) result = accounting(packet) if args.command == "recover": restored = packet.restore() - with args.output.open("xb") as target: - target.write(restored.to_bytes()) + write_new(args.output, restored.to_bytes()) result["output_bit_length"] = restored.length print(json.dumps(result, indent=2)) return 0 diff --git a/research/weave/safe_output.py b/research/weave/safe_output.py new file mode 100644 index 0000000..2c84a5d --- /dev/null +++ b/research/weave/safe_output.py @@ -0,0 +1,50 @@ +# === MODULE_BUILD === +# id: weave_atomic_output +# module_name: safe_output +# module_kind: io +# summary: no-clobber publication of a fully written and closed research output +# owner: Erin Spencer +# public_surface: write_new +# storage_boundary: write +# network_boundary: none +# tests: tests/test_cycle_repairs.py +# rollback: retain equivalent no-partial-output and no-overwrite guarantees +# === END MODULE_BUILD === +# === CONTRACTS === +# id: weave_output_atomic_no_clobber +# given: bytes and a target path on a hard-link-capable filesystem +# then: install only after successful write, flush, fsync and close; never overwrite an existing path +# === END CONTRACTS === +"""Usage: write_new(Path('result.wvc'), complete_record). + +A temporary sibling is created with owner-only permissions. Successful close +precedes atomic no-clobber linking. Write/flush/fsync/close failures leave no +partial destination. Unsupported filesystem semantics refuse; they do not fall +back to an overwrite-prone rename. Process-crash cleanup/directory durability +are not guaranteed by this helper; ordinary failure cleans its temporary file. +""" +import os +from pathlib import Path +import tempfile + + +def write_new(path: Path, data: bytes) -> None: + if type(data) is not bytes: + raise TypeError('complete bytes required') + path = Path(path) + fd, name = tempfile.mkstemp(prefix='.'+path.name+'.', suffix='.tmp', dir=path.parent) + temporary = Path(name) + try: + try: + target = os.fdopen(fd, 'wb') + except BaseException: + os.close(fd) + raise + with target: + if target.write(data) != len(data): + raise OSError('short output write') + target.flush() + os.fsync(target.fileno()) + os.link(temporary, path) # atomic create; existing files/symlinks fail + finally: + temporary.unlink(missing_ok=True) diff --git a/research/weave/test.py b/research/weave/test.py index efc45fd..a0c7df2 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -17,7 +17,7 @@ from assembly import Pipeline ROOT = Path(__file__).resolve().parent -EXPECTED_TESTS = 100 # 52 original + 15 numeral + 3 review repair + 30 cycle. +EXPECTED_TESTS = 125 # 52 original + 18 numeral + 30 cycle + 13 native origin + 12 repairs. def snapshot(): @@ -61,7 +61,8 @@ def main(): 'discovery_binary_alphabet_byte_strings': 8191, 'small_compositions': 4083, 'cycle_largest_roundtrip_bytes': 65536, 'cycle_sample_rounds': 3, 'native_occurrence_circles': 7, - 'prime_jump_trace': [5381, 53, 241, 1523] + 'prime_jump_trace': [5381, 53, 241, 1523], + 'native_origin_methods': 13, 'cycle_repair_methods': 12 }, 'cycle': 'EXPLICIT_CANDIDATE: corpus normalization once; native sequence affixiation and prime-derived bit interleave; exact reverse', 'full_weave': Pipeline().plan(), diff --git a/research/weave/tests/test_binary_origin.py b/research/weave/tests/test_binary_origin.py new file mode 100644 index 0000000..e2f704c --- /dev/null +++ b/research/weave/tests/test_binary_origin.py @@ -0,0 +1,246 @@ +# === CHECKS === +# id: check_all_256_values_use_byte_axes_not_bit_axes +# proves: binary_origin_byte_axes +# call: self::test_all_256_values_use_byte_axes_not_bit_axes +# mutates: none +# cleanup: none +# +# id: check_equal_values_preserve_occurrences +# proves: binary_origin_byte_axes +# call: self::test_equal_values_preserve_occurrences +# mutates: none +# cleanup: none +# +# id: check_repeat_and_residual_closure +# proves: binary_sequence_closure +# call: self::test_repeat_and_residual_closure +# mutates: none +# cleanup: none +# +# id: check_native_720_return +# proves: binary_native_geometry +# call: self::test_native_720_return +# mutates: none +# cleanup: none +# +# id: check_space_changes_actual_positions_not_labels +# proves: binary_native_geometry +# call: self::test_space_changes_actual_positions_not_labels +# mutates: none +# cleanup: none +# +# id: check_recover_from_circle_positions +# proves: binary_coordinate_recovery, binary_sequence_closure +# call: self::test_recover_from_circle_positions +# mutates: none +# cleanup: none +# +# id: check_all_seven_circles_participate +# proves: binary_coordinate_recovery, binary_sequence_closure +# call: self::test_all_seven_circles_participate +# mutates: none +# cleanup: none +# +# id: check_round_context_changes_frame_not_message +# proves: binary_origin_byte_axes +# call: self::test_round_context_changes_frame_not_message +# mutates: none +# cleanup: none +# +# id: check_leading_zero_and_empty_sources +# proves: binary_sequence_closure +# call: self::test_leading_zero_and_empty_sources +# mutates: none +# cleanup: none +# +# id: check_bad_partitions_refuse +# proves: binary_source_refusal +# call: self::test_bad_partitions_refuse +# mutates: none +# cleanup: none +# +# id: check_bad_coordinate_counts_gaps_and_frames_refuse +# proves: binary_source_refusal +# call: self::test_bad_coordinate_counts_gaps_and_frames_refuse +# mutates: none +# cleanup: none +# +# id: check_changed_native_source_refuses_before_execution +# proves: binary_source_refusal +# call: self::test_changed_native_source_refuses_before_execution +# mutates: filesystem +# cleanup: tempdir_teardown +# +# id: check_work_graph_identity +# proves: binary_source_refusal +# call: self::test_work_graph_identity +# mutates: none +# cleanup: none +# === END CHECKS === +"""Run: WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_binary_origin.py. + +The tests execute the pinned native UCNS modules, never fabricated circle classes. +All writes are confined to temporary directories. No network or corpus database. +""" +from dataclasses import replace +from fractions import Fraction +from pathlib import Path +from hashlib import sha256 +import json +import os +import tempfile +import unittest +from native_binary import * + +UCNS = Path(os.environ['WEAVE_SOURCES'])/'ucns' +SPACES = tuple(Fraction(i,9) for i in range(8)) + + +class BinaryOriginTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.geometry = Geometry(UCNS) + + def fixture(self, data=b'ABxABy'): + origin = ByteOrigin(data,'native-test',0,self.geometry) + return close_sequences(origin,(b'AB',b'x',b'y'),(0,1,0,2),(1,2,3),SPACES) + + def replay(self, table, **changes): + wire = table.wire_occurrences() + kwargs = dict(scope=table.origin.scope,message_origin=table.origin.message_origin, + round_id=table.origin.round_id,origin_identity=table.origin.identity, + byte_length=len(table.origin.source),blocks=tuple(d.data for d in table.definitions), + circles=tuple(d.occurrences[0].circle for d in table.definitions),spaces=SPACES, + counts=tuple(sum(o.circle==c for _,o in wire) for c in range(1,8)), + wire_order=tuple(i for i,_ in wire), + source_turns=tuple(self.geometry.lift(o.source_state) for _,o in wire)) + kwargs.update(changes) + return recover_sequences(self.geometry,**kwargs) + + def test_all_256_values_use_byte_axes_not_bit_axes(self): + origin = ByteOrigin(bytes(range(256)),'all-bytes',0,self.geometry) + identities = set() + for i in range(256): + axis = origin.byte_axis(i) + self.assertIsInstance(axis,self.geometry.axis_module.AxisCirclePosition) + self.assertEqual(axis.axis_count,256) + self.assertEqual(axis.axis_ordinal,i) + self.assertEqual(axis.turn,Fraction(i,256)) + identities.add(axis.identity_sha256) + self.assertEqual(len(identities),256) + with self.assertRaises(BinaryError): + origin.byte_axis(256) + + def test_equal_values_preserve_occurrences(self): + origin = ByteOrigin(b'AA','same-byte',0,self.geometry) + self.assertEqual(len(origin.source),2) + self.assertNotEqual(origin.byte_axis(0),origin.byte_axis(1)) + table = close_sequences(origin,(b'A',),(0,0),(1,),SPACES) + self.assertEqual(tuple(o.source_offset for o in table.definitions[0].occurrences),(0,1)) + + def test_repeat_and_residual_closure(self): + table = self.fixture() + self.assertEqual(table.restore(),b'ABxABy') + self.assertTrue(table.definitions[0].repeated) + self.assertFalse(table.definitions[1].repeated) + self.assertEqual(tuple(o.source_offset for o in table.definitions[0].occurrences),(0,3)) + self.assertEqual(table.definitions[1].attachment_axis.turn,Fraction(1,3)) + + def test_native_720_return(self): + table = self.fixture() + state = table.definitions[0].state + self.assertIsInstance(state,self.geometry.mobius_module.NativeMobiusState) + self.assertNotEqual(state.advance(1).complete_key,state.complete_key) + self.assertEqual(state.advance(1).visible_key,state.visible_key) + self.assertEqual(state.advance(2).complete_key,state.complete_key) + + def test_space_changes_actual_positions_not_labels(self): + table = self.fixture() + origin = table.origin + other = close_sequences(origin,(b'AB',b'x',b'y'),(0,1,0,2),(1,2,3),(Fraction(0),)*8) + self.assertEqual(other.restore(),table.restore()) + self.assertNotEqual(tuple(self.geometry.lift(o.source_state) for _,o in other.wire_occurrences()), + tuple(self.geometry.lift(o.source_state) for _,o in table.wire_occurrences())) + + def test_recover_from_circle_positions(self): + table = self.fixture() + rebuilt = self.replay(table) + self.assertEqual(rebuilt.receipt(),table.receipt()) + self.assertEqual(rebuilt.restore(),b'ABxABy') + # Circle 1's space relation moves its second occurrence ahead of its first. + self.assertEqual(tuple(o.source_offset for _,o in table.wire_occurrences())[:2],(3,0)) + + def test_all_seven_circles_participate(self): + blocks = tuple(bytes([i,0,i]) for i in range(7)) + order = tuple(range(7))*3 + origin = ByteOrigin(b''.join(blocks[i] for i in order),'seven',0,self.geometry) + table = close_sequences(origin,blocks,order,tuple(range(1,8)),SPACES) + self.assertEqual({o.circle for _,o in table.wire_occurrences()},set(range(1,8))) + self.assertEqual(self.replay(table).restore(),origin.source) + + def test_round_context_changes_frame_not_message(self): + a = ByteOrigin(b'AB','one',0,self.geometry) + b = ByteOrigin(b'BA','one',1,self.geometry,a.message_origin) + self.assertEqual(a.message_origin,b.message_origin) + self.assertNotEqual(a.identity,b.identity) + self.assertNotEqual(a.byte_axis(0),b.byte_axis(0)) + with self.assertRaises(BinaryError): + ByteOrigin(b'AB','one',0,self.geometry,'0'*64) + with self.assertRaises(BinaryError): + ByteOrigin(b'AB','one',1,self.geometry) + + def test_leading_zero_and_empty_sources(self): + data=b'\x00\x00A\x00\x00A' + origin=ByteOrigin(data,'zeros',0,self.geometry) + table=close_sequences(origin,(b'\x00\x00A',),(0,0),(1,),SPACES) + self.assertEqual(self.replay(table).restore(),data) + empty=ByteOrigin(b'','empty',0,self.geometry) + self.assertEqual(close_sequences(empty,(),(),(),SPACES).restore(),b'') + with self.assertRaises(BinaryError): + empty.byte_axis(0) + + def test_bad_partitions_refuse(self): + o=ByteOrigin(b'ABAB','bad',0,self.geometry) + bad=(( (b'AB',),(0,),(1,),SPACES), + ( (b'AB',b'AB'),(0,1),(1,2),SPACES), + ( (b'AB',),(0,0),(0,),SPACES), + ( (b'AB',),(True,0),(1,),SPACES), + ( (b'AB',),[0,0],(1,),SPACES), + ( (b'AB',),(0,0),(1,),(0,)*8)) + for args in bad: + with self.assertRaises(BinaryError): + close_sequences(o,*args) + + def test_bad_coordinate_counts_gaps_and_frames_refuse(self): + t=self.fixture() + wire=t.wire_occurrences() + positions=tuple(self.geometry.lift(o.source_state) for _,o in wire) + for changes in (dict(counts=(0,)*7),dict(source_turns=(Fraction(1,11),)+positions[1:]), + dict(source_turns=(positions[1],)+positions[1:]),dict(max_bytes=5), + dict(source_turns=(positions[0]+1,)+positions[1:])): + with self.assertRaises(ValueError): + self.replay(t,**changes) + + def test_work_graph_identity(self): + path = Path(__file__).resolve().parents[1]/'CYCLE_WORK_GRAPH.json' + record = json.loads(path.read_text()) + payload = {k:record[k] for k in ('repositories','boundaries')} + digest = sha256(json.dumps(payload,sort_keys=True,separators=(',',':')).encode()).hexdigest() + self.assertEqual(digest,record['work_graph_sha256']) + entries = {e['repository']:e for e in record['repositories']} + self.assertEqual(entries['The-Interdependency/ucns']['commit'],UCNS_COMMIT) + self.assertFalse(record['boundaries']['authority_transfer']) + self.assertTrue(record['boundaries']['hmmm']) + + def test_changed_native_source_refuses_before_execution(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);path=root/'src/ucns';path.mkdir(parents=True) + marker=root/'executed' + (path/'axis_circle.py').write_text(f"open({str(marker)!r},'w').write('bad')") + with self.assertRaises(BinaryError): + Geometry(root) + self.assertFalse(marker.exists()) + + +if __name__=='__main__': + unittest.main() diff --git a/research/weave/tests/test_cycle.py b/research/weave/tests/test_cycle.py index 7da8af6..dbd6901 100644 --- a/research/weave/tests/test_cycle.py +++ b/research/weave/tests/test_cycle.py @@ -207,6 +207,7 @@ import cycle from cycle import Profile, CycleLimits, normalize, denormalize, affix, unaffix, forward, reverse from cycle_native import load_native +import cycle_native ROOT = Path(__file__).resolve().parents[1] SOURCES = Path(os.environ.get('WEAVE_SOURCES', ROOT/'sources')) @@ -463,12 +464,11 @@ def test_cycle_budgets_fail_without_partial_results(self): def test_source_tampering_refused_before_execution(self): with tempfile.TemporaryDirectory() as directory: root=Path(directory) - shutil.copytree(SOURCES/'uchc',root/'uchc') - shutil.copytree(SOURCES/'ucns',root/'ucns') - src=root/'uchc/binary/uchc_binary/origin.py' + shutil.copy2(ROOT/'CYCLE_NATIVE.json',root/'CYCLE_NATIVE.json') marker=root/'executed' - src.write_text(f"open({str(marker)!r},'w').write('oops')\n") - with self.assertRaises(Refused): load_native(root) + (root/'native_binary.py').write_text(f"open({str(marker)!r},'w').write('oops')\n") + with patch.object(cycle_native,'ROOT',root): + with self.assertRaises(Refused): load_native(SOURCES) self.assertFalse(marker.exists()) def test_cli_fresh_process_source_deleted_and_no_overwrite(self): @@ -498,9 +498,9 @@ def test_profile_roundtrip_and_all_accounting_fields(self): digest = sha256(json.dumps(payload, sort_keys=True, separators=(',', ':')).encode()).hexdigest() self.assertEqual(digest, graph['work_graph_sha256']) repositories = {x['repository']: x for x in graph['repositories']} - self.assertEqual(repositories['The-Interdependency/uchc']['commit'], lock['uchc_source']['commit']) + self.assertEqual(repositories['The-Interdependency/uchc']['commit'], lock['uchc_architecture']['commit']) self.assertEqual(repositories['The-Interdependency/ucns']['commit'], lock['ucns_commit']) - self.assertEqual(len(lock['uchc_source']['commit']), 40) + self.assertEqual(lock['binary_source']['owner'], 'The-Interdependency/stack') self.assertFalse(graph['boundaries']['authority_transfer']) self.assertEqual(Profile.read(cycle.canonical(PROFILE.as_dict())),PROFILE) wire,stats=forward(b'ABCD'*64,CORPUS,PROFILE,SOURCES) diff --git a/research/weave/tests/test_cycle_repairs.py b/research/weave/tests/test_cycle_repairs.py new file mode 100644 index 0000000..21cf7d7 --- /dev/null +++ b/research/weave/tests/test_cycle_repairs.py @@ -0,0 +1,215 @@ +# === CHECKS === +# id: native_closed_records_reject_forgery +# proves: binary_sequence_closure, binary_coordinate_recovery +# call: self::test_closed_table_rejects_order_source_and_definition_mutations +# id: native_modules_are_fresh +# proves: binary_source_refusal, cycle_native_source_pinned +# call: self::test_geometry_instances_ignore_mutated_earlier_modules +# id: output_write_failures_leave_no_partial_destination +# proves: weave_output_atomic_no_clobber, cycle_strict_refusal +# call: self::test_write_flush_and_close_failures_leave_no_partial_output +# id: empty_attachment_arguments_are_validated +# proves: numeral_strict_input +# call: self::test_empty_bind_rejects_invalid_attachments +# === END CHECKS === +"""Regression replay: WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_cycle_repairs.py. + +Temporary data only. These exercise genuine failure modes, not encryption claims. +""" +from dataclasses import replace +from fractions import Fraction +from pathlib import Path +from types import ModuleType +from unittest.mock import patch +import contextlib +import io +import json +import os +import subprocess +import sys +import tempfile +import unittest + +import cycle +import cycle_native +import native_binary as native +import safe_output +from numeral import Refused + +ROOT = Path(__file__).resolve().parents[1] +SOURCES = Path(os.environ['WEAVE_SOURCES']) +SPACES = tuple(Fraction(i,9) for i in range(8)) + + +class CycleRepairTests(unittest.TestCase): + def fixture(self): + geometry = native.Geometry(SOURCES/'ucns') + origin = native.ByteOrigin(b'ABxABy', 'repair', 0, geometry) + return native.close_sequences(origin, (b'AB', b'x', b'y'), (0,1,0,2), (1,2,3), SPACES) + + def test_closed_table_rejects_order_source_and_definition_mutations(self): + table = self.fixture() + wrong_origin = native.ByteOrigin(b'ABxABy!', 'repair', 0, table.origin.geometry) + changes = (dict(order=()), dict(order=(2,0,1,0)), dict(definitions=list(table.definitions)), + dict(definitions=table.definitions[:-1]), dict(origin=wrong_origin), + dict(spaces=(Fraction(0),)*8)) + for change in changes: + with self.subTest(change=tuple(change)): + with self.assertRaises(native.BinaryError): replace(table, **change) + direct = native.SequenceTable(table.origin, table.definitions, table.order, table.spaces) + self.assertEqual(direct.receipt(), table.receipt()) + + def test_closed_table_rejects_native_axes_frames_and_occurrence_mutations(self): + table = self.fixture() + first = table.definitions[0] + occurrence = first.occurrences[0] + bad_occurrences = ( + replace(occurrence, ordinal=1), + replace(occurrence, source_offset=1), + replace(occurrence, circle=2), + replace(occurrence, source_axis=table.origin.byte_axis(1)), + replace(occurrence, source_state=occurrence.source_state.advance(1)), + replace(occurrence, state=occurrence.state.advance(1)), + ) + bad_definitions = [replace(first, occurrences=(o,)+first.occurrences[1:]) for o in bad_occurrences] + bad_definitions += [replace(first, data=b'ZZ'), replace(first, state=first.state.advance(1)), + replace(first, attachment_axis=table.origin.byte_axis(1)), + replace(first, occurrences=first.occurrences[::-1])] + for definition in bad_definitions: + with self.assertRaises(native.BinaryError): + replace(table, definitions=(definition,)+table.definitions[1:]) + + def test_receipt_and_restore_recheck_even_bypassed_frozen_records(self): + table = self.fixture() + object.__setattr__(table, 'order', ()) + for operation in (table.restore, table.receipt, table.wire_occurrences): + with self.assertRaises(native.BinaryError): operation() + table = self.fixture() + object.__setattr__(table.origin, '_identity', '0'*64) + with self.assertRaises(native.BinaryError): table.receipt() + + def test_empty_and_invalid_native_record_types_refuse(self): + table = self.fixture() + definition = table.definitions[0] + for changes in (dict(data=b''), dict(occurrences=()), dict(occurrences=[])): + with self.assertRaises(native.BinaryError): replace(definition, **changes) + for changes in (dict(circle=True), dict(circle=8), dict(ordinal=-1), dict(source_offset=True)): + with self.assertRaises(native.BinaryError): replace(definition.occurrences[0], **changes) + + def test_geometry_instances_ignore_mutated_earlier_modules(self): + first = native.Geometry(SOURCES/'ucns') + first.axis_module.build_axis_circle_position = lambda **kw: 'not native' + first.mobius_module.native_mobius_state = lambda *a,**kw: 'not native' + poisoned = {'_uchc_ucns_'+sha: ModuleType('poison') for sha in native.UCNS_BLOBS.values()} + with patch.dict(sys.modules, poisoned): + second = native.Geometry(SOURCES/'ucns') + axis = second.axis('0'*64, 8, 3) + state = second.placed(axis,Fraction(1,7)) + self.assertEqual(type(axis).__name__, 'AxisCirclePosition') + self.assertEqual(type(state).__name__, 'NativeMobiusState') + self.assertEqual(axis.turn,Fraction(3,8)) + self.assertIsNot(first.axis_module, second.axis_module) + + def test_cycle_loads_fresh_candidate_and_has_no_uchc_runtime_dependency(self): + api,geometry = cycle_native.load_native(SOURCES) + api.close_sequences = lambda *a,**kw: 'poisoned candidate' + geometry.axis_module.build_axis_circle_position = lambda **kw: 'poisoned geometry' + with tempfile.TemporaryDirectory() as directory: + src = Path(directory) + (src/'ucns').symlink_to(SOURCES/'ucns',target_is_directory=True) + newer,geom = cycle_native.load_native(src) + origin = newer.ByteOrigin(b'ABAB','fresh',0,geom) + self.assertEqual(newer.close_sequences(origin,(b'AB',),(0,0),(1,),SPACES).restore(),b'ABAB') + self.assertIsNot(newer,api) + self.assertFalse(any(name.startswith('_weave_binary_') or name.startswith('_weave_ucns_') for name in sys.modules)) + + def test_write_success_is_owner_only_and_never_overwrites(self): + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'out' + safe_output.write_new(path,b'complete') + self.assertEqual(path.read_bytes(),b'complete') + self.assertEqual(path.stat().st_mode & 0o777,0o600) + with self.assertRaises(FileExistsError): safe_output.write_new(path,b'replace') + self.assertEqual(path.read_bytes(),b'complete') + self.assertEqual(list(Path(directory).iterdir()),[path]) + link=Path(directory)/'symlink' + link.symlink_to(path) + with self.assertRaises(FileExistsError): safe_output.write_new(link,b'replace') + self.assertTrue(link.is_symlink()) + self.assertEqual(path.read_bytes(),b'complete') + + def test_write_flush_and_close_failures_leave_no_partial_output(self): + original = os.fdopen + class Broken: + def __init__(self, fd, mode, stage): self.raw,self.stage=original(fd,mode),stage + def __enter__(self): return self + def write(self,data): + if self.stage=='write': + self.raw.write(data[:2]); raise OSError('disk full') + if self.stage=='short': return self.raw.write(data[:2]) + return self.raw.write(data) + def flush(self): + if self.stage=='flush': raise OSError('flush failure') + self.raw.flush() + def fileno(self): return self.raw.fileno() + def __exit__(self,*args): + self.raw.close() + if self.stage=='close': raise OSError('close failure') + for stage in ('write','short','flush','close','fsync'): + with self.subTest(stage=stage),tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'out' + with patch.object(safe_output.os,'fdopen',side_effect=lambda fd,mode:Broken(fd,mode,stage)): + cm=patch.object(safe_output.os,'fsync',side_effect=OSError('quota')) if stage=='fsync' else contextlib.nullcontext() + with cm,self.assertRaises(OSError): safe_output.write_new(path,b'complete record') + self.assertFalse(path.exists()) + self.assertEqual(list(Path(directory).iterdir()),[]) + + def test_atomic_install_failure_preserves_other_files(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory); path=root/'out'; other=root/'other'; other.write_bytes(b'unchanged') + with patch.object(safe_output.os,'link',side_effect=OSError('unsupported atomic link')): + with self.assertRaises(OSError): safe_output.write_new(path,b'complete') + self.assertEqual(list(root.iterdir()),[other]) + self.assertEqual(other.read_bytes(),b'unchanged') + + def test_cli_both_directions_refuse_failed_output_without_partial_files(self): + for command in ('forward','reverse'): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory); src=root/'input'; dst=root/'output'; corpus=root/'corpus' + src.write_bytes(b'input');corpus.write_bytes(b'corpus') + argv=['cycle.py',command,str(src),str(dst),'--corpus',str(corpus)] + effect=(b'full record',{}) if command=='forward' else b'restored' + with patch.object(sys,'argv',argv),patch.object(cycle,command,return_value=effect): + with patch.object(safe_output.os,'fsync',side_effect=OSError('quota')): + with contextlib.redirect_stderr(io.StringIO()),self.assertRaises(SystemExit) as failure: cycle.main() + self.assertEqual(failure.exception.code,2) + self.assertFalse(dst.exists()) + self.assertEqual({p.name for p in root.iterdir()},{'input','corpus'}) + + def test_empty_bind_rejects_invalid_attachments(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);src=root/'empty';src.write_bytes(b'') + for angle,circle in (('99','1'),('1/7','999'),('99','999'),('-1','2')): + out=root/'record' + run=subprocess.run([sys.executable,str(ROOT/'numeral.py'),'bind',str(src),str(out), + '--origin','empty','--angle',angle,'--circle',circle],capture_output=True) + self.assertEqual(run.returncode,2,run.stderr) + self.assertNotIn(b'Traceback',run.stderr) + self.assertFalse(out.exists()) + run=subprocess.run([sys.executable,str(ROOT/'numeral.py'),'bind',str(src),str(root/'valid'), + '--origin','empty','--angle','1/7','--circle','3'],capture_output=True) + self.assertEqual(run.returncode,0,run.stderr) + + def test_native_edges_and_forge_ownership_match_root_manifest(self): + manifest=json.loads((ROOT.parents[1]/'stack-manifest.json').read_text()) + human=(ROOT.parents[1]/'STACK_MANIFEST.md').read_text() + lock=json.loads((ROOT/'CYCLE_NATIVE.json').read_text()) + participants={p['participant_id']:p for p in manifest['research_participants'] if p['workspace']=='research/weave/'} + self.assertEqual(participants['weave-ucns-geometry']['commit'],lock['ucns_commit']) + self.assertEqual(participants['weave-uchc-architecture']['commit'],lock['uchc_architecture']['commit']) + self.assertIn('Stack-owned binary',participants['weave']['relation']) + self.assertIn('native_binary.py',human) + self.assertNotIn('uchc_source',lock) + + +if __name__=='__main__': unittest.main() diff --git a/stack-manifest.json b/stack-manifest.json index 1cda58a..66c1246 100644 --- a/stack-manifest.json +++ b/stack-manifest.json @@ -1,7 +1,7 @@ { "schema": "the-interdependency.stack-manifest", "version": "1.1.0", - "work_graph_sha256": "889a1234456c29c27473bfddb7808b209b2e1451da10e596fa9feea0f6ea4918", + "work_graph_sha256": "dc2cac5ac1d609f7f72434514ec95a55e8020b0b9326c17883b1ef25e86baaa3", "repositories": [ { "repository": "The-Interdependency/skill-lib", @@ -70,7 +70,7 @@ "Python Gonol Construction remains stack-local research without independent repository or release authority", "URPCS is retired historical evidence after GPT-assisted implementation diverged from Erin Spencer's intended multi-arity interleaving specification; why the substitution occurred remains unresolved; immutable public intended-design transcript identity and detailed attribution remain unresolved", "skill-lib remains a special operational snapshot at stack root rather than following the libs/research pair", - "Weave preserves the intended multi-arity interleaving mechanism, while uneven partitioning, stage-input composition, schedule encoding, thread/corpus binding, hyperspace/gonol binding, authentication/state, and threat model remain unresolved", + "Weave has an explicit Stack-owned native binary sequence-cycle candidate with exact recovery; other profiles, private/public lifting, authentication/state and full-cipher security remain unresolved.", "Weave native public/private key derivation and source of asymmetry remain unresolved; source-bound key metadata does not establish a native asymmetric law.", "a0 Municipality is specification-first composition research; native semantic-origin/affixiation integration, operational authorities and its fifth workflow remain unresolved; no runtime or six-month proof is established by placement." ] @@ -198,7 +198,7 @@ "canonical_release": false, "commit": "1ba201449731337dc20c564788f4303c8910bfdd", "participant_id": "weave", - "relation": "new stack-local specification and research for Erin Spencer's full intended multi-layer encryption construction: hyperspace/gonol plaintext representation, private-gonol recovery, reconstruction-dependent threads, corpus/material binding, multi-arity bit interleaving, and intended asymmetric key relation; no URPCS implementation inheritance and no security standing yet", + "relation": "Stack-owned binary origin/sequence candidate and complete declared corpus-normalization, repeated-sequence affixiation, prime-route split and interleave recovery profile; private/public cryptographic relation remains unresolved; no URPCS inheritance or security standing", "repository": "The-Interdependency/stack", "workspace": "research/weave/" }, @@ -421,6 +421,26 @@ "relation": "unmerged draft PR #66 protocol reference only; no implemented recovery, theorem or minimum-complexity claim", "canonical_release": false, "authority_transfer": false + }, + { + "workspace": "research/weave/", + "participant_id": "weave-ucns-geometry", + "repository": "The-Interdependency/ucns", + "commit": "905e66964a495d7596a577bb64e4158db9465864", + "authority": "native exact axis-circle and Mobius geometry", + "relation": "source-verified unchanged mathematical producer for native_binary.py", + "authority_transfer": false, + "canonical_release": false + }, + { + "workspace": "research/weave/", + "participant_id": "weave-uchc-architecture", + "repository": "The-Interdependency/uchc", + "commit": "4ad94e92be10d2c4c875a848addfda46f3c7cfdb", + "authority": "origin/axis architecture and domain migration boundary", + "relation": "architectural reference only; speculative binary implementation remains in Stack until graduation gates complete", + "authority_transfer": false, + "canonical_release": false } ] } From fe7cfabebb40f51fb81e3d46f853173680560d1d Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 15:35:29 -0700 Subject: [PATCH 05/15] ci(weave): replace deprecated artifact runtime with pinned Node 24 action --- .github/workflows/weave.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/weave.yml b/.github/workflows/weave.yml index b437a9b..b5dfe1c 100644 --- a/.github/workflows/weave.yml +++ b/.github/workflows/weave.yml @@ -57,7 +57,7 @@ jobs: done - name: Retain exact-source research bundle, not a security release if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1, Node 24 with: name: weave-inspection-${{ github.sha }} path: ${{ runner.temp }}/weave-inspection/ From d3c60e45a2570ca873093b6beaa88662abd3cb68 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 20:03:05 -0700 Subject: [PATCH 06/15] fix(weave): close four final-review validation findings Reuse accounting validation and share CLI prime-work budgets; correct discovery closure ownership; reject equality-spoofed native records and scalar subclasses using exact producer types and canonical fields; enforce the selected discovery profile during recovery. Add 11 regression methods, update source lock and documented count. Baseline: 125 tests pass. Repaired exact local source: 136 pass, zero failures/errors/skips; source digest 87c6e30f0ab68259391518c421a6bf340d3be174d9b75baf14f4986373b0bf2c. Three-round demo remains 6735 bytes with exact recovery. Hosted CI and final review are separate gates. No cipher/profile/producer authority change. --- research/weave/CYCLE.md | 22 +- research/weave/CYCLE_NATIVE.json | 2 +- research/weave/NUMERAL_CONSTRUCTION.md | 11 + research/weave/affixiation.py | 6 +- research/weave/cycle.py | 11 +- research/weave/native_binary.py | 56 ++++- research/weave/numeral.py | 41 ++-- research/weave/test.py | 5 +- research/weave/tests/test_review_closure.py | 251 ++++++++++++++++++++ 9 files changed, 379 insertions(+), 26 deletions(-) create mode 100644 research/weave/tests/test_review_closure.py diff --git a/research/weave/CYCLE.md b/research/weave/CYCLE.md index ea6b407..18ee061 100644 --- a/research/weave/CYCLE.md +++ b/research/weave/CYCLE.md @@ -162,7 +162,7 @@ and malformed-input versus resource-limit classification. Three new regressions bring its suite to 18. The obsolete bit-per-circle modules and their 22 tests are removed, with these sequence/native operations as replacement. Fifty-two original transport/assembly tests retain their original limited scope. Full Weave inventory: -52 + 18 + 30 + 13 native-origin + 12 repair regressions = 125. The original +52 + 18 + 30 + 13 native-origin + 12 repair + 11 review-closure regressions = 136. The original 13 native producer tests are retained in Stack as `tests/test_binary_origin.py`. The repair regressions exercise forged closed records, fresh verified module loading, atomic failed-write cleanup, empty-input attachments and manifest edges. @@ -188,3 +188,23 @@ fsync or close failure leaves no partial destination. Existing files and symlink are never overwritten. A filesystem without the required hard-link semantics refuses explicitly. Abrupt process-crash cleanup and directory durability are not guaranteed. Empty numeral input still validates its required attachments. + +## Final-review closure repairs + +The four later PR #77 findings are covered by `tests/test_review_closure.py`. +Accounting reuses its validated bit count; numeral CLI operations share one charged +prime-work engine through parsing, accounting, and recovery. Budget failure occurs +before publishing output. Native closure comparison checks the exact UCNS classes +from the origin's Geometry instance and compares only typed canonical fields, never +arbitrary object equality. This also rejects equality-spoofing scalar subclasses. + +The inverse rediscovers sequences under the declared selection profile and requires +both definitions and source order to match. Its rediscovery obeys the same explicit +per-round byte and candidate-visit limits as forward discovery. A different valid +partition is not silently attributed to the selected deterministic profile. + +Usage: `WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_review_closure.py -v`. +The profile, forward operations, UCNS producers, ownership and cryptographic standing +are unchanged. The local candidate source lock changes with the repaired source; +records bound to the previous lock must be regenerated rather than accepted under +false source provenance. This is stricter validation, not authentication. diff --git a/research/weave/CYCLE_NATIVE.json b/research/weave/CYCLE_NATIVE.json index d745f4b..64a7225 100644 --- a/research/weave/CYCLE_NATIVE.json +++ b/research/weave/CYCLE_NATIVE.json @@ -8,7 +8,7 @@ "binary_source": { "owner": "The-Interdependency/stack", "path": "native_binary.py", - "git_blob": "8b22cefa39129a6e1ab41de6ac7d495e8b90542e", + "git_blob": "b3afde7f95678b47ceda7fbbd931972ebeaf8b7e", "standing": "forge-owned candidate, not graduated" }, "uchc_architecture": { diff --git a/research/weave/NUMERAL_CONSTRUCTION.md b/research/weave/NUMERAL_CONSTRUCTION.md index 0ba843e..eb8dd55 100644 --- a/research/weave/NUMERAL_CONSTRUCTION.md +++ b/research/weave/NUMERAL_CONSTRUCTION.md @@ -107,3 +107,14 @@ interleaving are connected in the separately named cycle profile. Automatic shor prime-recipe discovery and an asymmetric private reconstruction advantage remain unimplemented. Reverting this codec requires retiring or rebinding its cycle consumer; no discarded bit-per-circle implementation is a supported fallback. + +## Accounting and CLI work budget + +`accounting(packet, limits)` returns sizes from the same validation that constructs +its wire parts. It does not replay prime recipes a second time merely to obtain +source length. Each numeral CLI `bind`, `inspect`, or `recover` command shares one +charged prime-work engine across its component operations; it does not reset the +allowance between decode, accounting and restore. A command that exhausts its +budget refuses before output publication. Standalone library calls retain their +individual default budgets. The private `_engine` keyword is internal orchestration, +not serialized state or a bypass of validation. diff --git a/research/weave/affixiation.py b/research/weave/affixiation.py index 230c570..79fa0d5 100644 --- a/research/weave/affixiation.py +++ b/research/weave/affixiation.py @@ -26,6 +26,9 @@ # id: discovery_resource_refusal # given: input or candidate-visit work exceeding the configured budget # then: refuse instead of silently truncating the candidate search +# id: discovery_closure_authority +# given: sequence participants selected by this module +# then: closure remains Stack-owned, consuming UCNS geometry; UCHC is the architecture reference # === END CONTRACTS === """Usage: partition = discover(raw_bytes); partition.restore() == raw_bytes. @@ -34,7 +37,8 @@ nonoverlapping occurrences. Occurrence counts mean selected partition occurrences, not every possible overlapping substring match. No maximum sequence-length heuristic or fixed byte chunks are imposed. Residual runs remain exact literal definitions. -This module discovers participants; UCHC closes them using native UCNS geometry. +This module discovers participants. Stack-owned native_binary.py closes them +using native UCNS geometry; UCHC supplies the origin/axis architecture reference. """ from __future__ import annotations diff --git a/research/weave/cycle.py b/research/weave/cycle.py index a58edd4..cc54473 100644 --- a/research/weave/cycle.py +++ b/research/weave/cycle.py @@ -33,6 +33,9 @@ # id: cycle_strict_refusal # given: malformed records, mismatched configuration/material, or exhausted budgets # then: refuse clearly without overwrite, partial recovery or silent truncation +# id: cycle_discovery_profile +# given: a decoded record claiming the selected deterministic discovery profile +# then: its definitions and occurrence order equal discovery on the restored bytes or recovery refuses # === END CONTRACTS === """Usage: python cycle.py demo --sources /checkouts python cycle.py forward INPUT OUTPUT --profile cycle-profile.json --corpus CORPUS --sources /checkouts @@ -362,7 +365,13 @@ def unaffix(data: bytes, *, api, geometry, scope: str, root: str, round_id: int, max_bytes=limits.round_bytes) if tuple(geometry.lift(d.state) for d in table.definitions) != tuple(e.angle for e in packet.entries): raise Refused('whole-circle sequence attachments do not reconstruct') - return table.restore() + restored = table.restore() + selected = discover(restored, max_bytes=limits.round_bytes, + visit_budget=limits.discovery_visits) + if (selected.blocks != tuple(d.data for d in table.definitions) + or selected.order != table.order): + raise Refused('reconstructed partition disagrees with the discovery profile') + return restored def _prepared(profile: Profile, limits: CycleLimits): diff --git a/research/weave/native_binary.py b/research/weave/native_binary.py index 8b22cef..b3afde7 100644 --- a/research/weave/native_binary.py +++ b/research/weave/native_binary.py @@ -88,6 +88,14 @@ def _natural(value, name: str) -> int: return value +def _fraction_fields(value: Fraction) -> tuple[int, int]: + """Extract builtin fields only; hostile equality must never authorize a record.""" + if (type(value) is not Fraction or type(value.numerator) is not int + or type(value.denominator) is not int or value.denominator <= 0): + raise BinaryError('native coordinate requires exact Fraction fields') + return value.numerator, value.denominator + + def _sha(value: str) -> str: if type(value) is not str or re.fullmatch('[0-9a-f]{64}', value) is None: raise BinaryError('exact lowercase SHA-256 identity required') @@ -148,6 +156,37 @@ def recover_axis(self, origin: str, count: int, position: Fraction, space: Fract raise BinaryError('position does not lift to a source byte axis') return self.axis(origin, count, ordinal.numerator) + def _axis_fields(self, axis) -> tuple: + """Inspect the exact class from this verified producer instance, then fields. + + Comparison to a newly constructed native axis owns geometric validation; + these guards prevent Python equality overrides from bypassing that comparison. + """ + if type(axis) is not self.axis_module.AxisCirclePosition: + raise BinaryError('axis must be an exact native object from this Geometry') + try: + return (_sha(axis.origin_sha256), _natural(axis.axis_count, 'axis count'), + _natural(axis.axis_ordinal, 'axis ordinal'), _fraction_fields(axis.turn), + _sha(axis.identity_sha256)) + except AttributeError as exc: + raise BinaryError('native axis is missing required fields') from exc + + def _state_fields(self, state) -> tuple: + """Native frame identity and typed scalars, without user-defined equality.""" + if type(state) is not self.mobius_module.NativeMobiusState: + raise BinaryError('state must be an exact native object from this Geometry') + try: + frames = self.mobius_module.NativeMobiusFrame + if state.frame is frames.POSITIVE: + frame = 0 + elif state.frame is frames.REVERSED: + frame = 1 + else: + raise BinaryError('state must carry this Geometry native frame') + return _fraction_fields(state.phase_turns), frame + except AttributeError as exc: + raise BinaryError('native state is missing required fields') from exc + def lift(self, state) -> Fraction: """Serialize the native complete frame in [0,2), not just visible phase.""" return state.phase_turns + (1 if state.frame.sign < 0 else 0) @@ -256,7 +295,8 @@ def validate(self) -> None: origin = self.origin reidentified = ByteOrigin(origin.source, origin.scope, origin.round_id, origin.geometry, origin.message_origin) - if origin.identity != reidentified.identity: + _sha(origin.message_origin) + if _sha(origin.identity) != reidentified.identity: raise BinaryError('origin identity no longer binds its source') for definition in self.definitions: definition.__post_init__() @@ -265,7 +305,19 @@ def validate(self) -> None: blocks = tuple(d.data for d in self.definitions) circles = tuple(d.occurrences[0].circle for d in self.definitions) expected = _closed_definitions(origin, blocks, self.order, circles, self.spaces) - if self.definitions != expected: + def fields(definitions): + geometry = origin.geometry + return tuple((d.data, geometry._axis_fields(d.attachment_axis), + geometry._state_fields(d.state), + tuple((o.source_offset, o.circle, o.ordinal, + geometry._axis_fields(o.source_axis), + geometry._axis_fields(o.circle_axis), + geometry._state_fields(o.state), + geometry._state_fields(o.source_state)) + for o in d.occurrences)) for d in definitions) + # Only builtin tuples/bytes/strings/integers reach equality. A foreign + # object or native subclass cannot license itself with __eq__ == True. + if fields(self.definitions) != fields(expected): raise BinaryError('closed native relations disagree with source or placement') def restore(self) -> bytes: diff --git a/research/weave/numeral.py b/research/weave/numeral.py index 1b69fb7..6b78fd0 100644 --- a/research/weave/numeral.py +++ b/research/weave/numeral.py @@ -307,9 +307,9 @@ def occurrences(self, limits: Limits = Limits()) -> tuple[tuple[str, int, int, i offset += entry.block.length return tuple(result) - def restore(self, limits: Limits = Limits()) -> BitBlock: + def restore(self, limits: Limits = Limits(), *, _engine=None) -> BitBlock: """Reconstruct in linear output-byte work; no original input or hidden table.""" - total = self.validate(limits) + total = self.validate(limits, _engine=_engine) mapping = {entry.symbol: entry.block for entry in self.entries} out = bytearray((total + 7) // 8) offset = 0 @@ -374,8 +374,9 @@ def blob(self, maximum: int) -> bytes: return self.take(count) -def _parts(packet: Packet, limits: Limits) -> tuple[bytes, bytes, bytes]: - total = packet.validate(limits) +def _parts(packet: Packet, limits: Limits, *, _engine=None) -> tuple[int, bytes, bytes, bytes]: + """Return the validated bit count with wire parts; never replay it for accounting.""" + total = packet.validate(limits, _engine=_engine) header = MAGIC + _blob(packet.origin.encode("utf-8")) + _uint(packet.round_id) + _uint(total) count = _uint(len(packet.entries)) # Size the entire representation before allocating literal output buffers. @@ -409,16 +410,17 @@ def _parts(packet: Packet, limits: Limits) -> tuple[bytes, bytes, bytes]: definitions = count + b"".join(prefix + (block.to_bytes() if block is not None else b"") for prefix, block in pieces) stream = _blob(packet.symbols.encode("utf-8")) - return header, definitions, stream + return total, header, definitions, stream -def encode(packet: Packet, limits: Limits = Limits()) -> bytes: +def encode(packet: Packet, limits: Limits = Limits(), *, _engine=None) -> bytes: """Serialize all reconstruction information, including the definition table.""" - return b"".join(_parts(packet, limits)) + _, header, table, stream = _parts(packet, limits, _engine=_engine) + return header + table + stream -def decode(data: bytes, limits: Limits = Limits()) -> Packet: +def decode(data: bytes, limits: Limits = Limits(), *, _engine=None) -> Packet: """Strict self-contained decoder; malformed input never becomes a partial result.""" if type(data) is not bytes: raise Refused("packet must be bytes") @@ -436,7 +438,7 @@ def decode(data: bytes, limits: Limits = Limits()) -> Packet: if count > limits.entries: raise ResourceLimit("definition count exceeds budget") entries = [] - primes = _Primes(limits) + primes = _engine if _engine is not None else _Primes(limits) for _ in range(count): symbol = reader.blob(4).decode("utf-8") _symbol(symbol) @@ -483,9 +485,9 @@ def decode(data: bytes, limits: Limits = Limits()) -> Packet: return packet -def accounting(packet: Packet, limits: Limits = Limits()) -> dict[str, int]: - header, table, stream = _parts(packet, limits) - bits = packet.validate(limits) +def accounting(packet: Packet, limits: Limits = Limits(), *, _engine=None) -> dict[str, int]: + """Count all serialized bytes under a single validation/prime-work budget.""" + bits, header, table, stream = _parts(packet, limits, _engine=_engine) return {"source_bits": bits, "source_packed_bytes": (bits + 7) // 8, "header_bytes": len(header), "definition_bytes": len(table), "occurrence_bytes": len(stream), "total_bytes": len(header) + len(table) + len(stream), @@ -539,7 +541,10 @@ def main() -> int: if args.command == "demo": result = demo() else: - limit = Limits().output_bits // 8 if args.command == "bind" else Limits().wire_bytes + # One CLI operation has one budget across decode/account/recover. + limits = Limits() + engine = _Primes(limits) + limit = limits.output_bits // 8 if args.command == "bind" else limits.wire_bytes if args.input.stat().st_size > limit: raise ResourceLimit("input file exceeds execution budget") with args.input.open("rb") as source: @@ -550,14 +555,14 @@ def main() -> int: _validate_attachment(args.angle, args.circle) entry = Entry(chr(0xE000), BitBlock.from_bytes(raw), args.angle, args.circle) packet = Packet(args.origin, 0, (entry,) if raw else (), entry.symbol if raw else "") - wire = encode(packet) + wire = encode(packet, limits, _engine=engine) + result = accounting(packet, limits, _engine=engine) write_new(args.output, wire) - result = accounting(packet) else: - packet = decode(raw) - result = accounting(packet) + packet = decode(raw, limits, _engine=engine) + result = accounting(packet, limits, _engine=engine) if args.command == "recover": - restored = packet.restore() + restored = packet.restore(limits, _engine=engine) write_new(args.output, restored.to_bytes()) result["output_bit_length"] = restored.length print(json.dumps(result, indent=2)) diff --git a/research/weave/test.py b/research/weave/test.py index a0c7df2..2e9b572 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -17,7 +17,7 @@ from assembly import Pipeline ROOT = Path(__file__).resolve().parent -EXPECTED_TESTS = 125 # 52 original + 18 numeral + 30 cycle + 13 native origin + 12 repairs. +EXPECTED_TESTS = 136 # 125 existing + 11 final-review closure regressions. def snapshot(): @@ -62,7 +62,8 @@ def main(): 'small_compositions': 4083, 'cycle_largest_roundtrip_bytes': 65536, 'cycle_sample_rounds': 3, 'native_occurrence_circles': 7, 'prime_jump_trace': [5381, 53, 241, 1523], - 'native_origin_methods': 13, 'cycle_repair_methods': 12 + 'native_origin_methods': 13, 'cycle_repair_methods': 12, + 'review_closure_methods': 11 }, 'cycle': 'EXPLICIT_CANDIDATE: corpus normalization once; native sequence affixiation and prime-derived bit interleave; exact reverse', 'full_weave': Pipeline().plan(), diff --git a/research/weave/tests/test_review_closure.py b/research/weave/tests/test_review_closure.py new file mode 100644 index 0000000..7e2455d --- /dev/null +++ b/research/weave/tests/test_review_closure.py @@ -0,0 +1,251 @@ +# === CHECKS === +# id: review_accounting_one_replay +# proves: numeral_strict_input, numeral_accounting +# call: self::test_accounting_reuses_the_single_validation +# mutates: none +# cleanup: none +# id: review_cli_aggregate_budget +# proves: numeral_strict_input +# call: self::test_cli_shares_prime_budget_through_inspect_and_recover +# mutates: filesystem +# cleanup: tempdir_teardown +# id: review_closure_owner +# proves: discovery_closure_authority +# call: self::test_discovery_documentation_preserves_closure_owner +# mutates: none +# cleanup: none +# id: review_native_objects_no_spoof +# proves: binary_sequence_closure, binary_source_refusal +# call: self::test_all_native_record_slots_reject_equality_spoofs +# mutates: none +# cleanup: none +# id: review_native_fields_no_spoof +# proves: binary_sequence_closure, binary_source_refusal +# call: self::test_native_scalar_fields_reject_spoofs_and_subclasses +# mutates: none +# cleanup: none +# id: review_native_geometry_instance +# proves: binary_sequence_closure, binary_source_refusal +# call: self::test_records_from_another_geometry_instance_are_not_substituted +# mutates: none +# cleanup: none +# id: review_native_export_revalidation +# proves: binary_sequence_closure, binary_coordinate_recovery +# call: self::test_all_export_paths_reject_bypassed_frozen_spoofs +# mutates: none +# cleanup: none +# id: review_decode_discovery_identity +# proves: cycle_discovery_profile +# call: self::test_unaffix_rejects_valid_alternate_partitions +# mutates: none +# cleanup: none +# id: review_reverse_discovery_identity +# proves: cycle_discovery_profile +# call: self::test_whole_cycle_reverse_rejects_alternate_selection +# mutates: none +# cleanup: none +# id: review_discovery_roundtrip +# proves: cycle_discovery_profile, cycle_exact_recovery +# call: self::test_canonical_discovery_records_still_recover +# mutates: none +# cleanup: none +# id: review_inverse_discovery_budget +# proves: cycle_discovery_profile, discovery_resource_refusal +# call: self::test_inverse_discovery_honors_the_visit_budget +# mutates: none +# cleanup: none +# === END CHECKS === +"""PR #77 final-review regressions, not cryptographic-strength tests. + +Usage: WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_review_closure.py -v +Only temporary files are written. The original profile, UCNS geometry and wire +representation remain unchanged; invalid native records/false profile claims fail. +""" +from copy import copy +from dataclasses import replace +from fractions import Fraction +from pathlib import Path +from unittest.mock import patch +import contextlib +import io +import os +import sys +import tempfile +import unittest + +import affixiation +import cycle +import native_binary as native +import numeral +from affixiation import Partition, discover +from numeral import BitBlock, Entry, Packet, PrimePath, Limits, Refused, ResourceLimit + +ROOT = Path(__file__).resolve().parents[1] +SOURCES = Path(os.environ['WEAVE_SOURCES']) +PROFILE = cycle.Profile.read((ROOT/'profiles/cycle-v1.json').read_bytes()) +SPACES = tuple(Fraction(i, 9) for i in range(8)) + + +class EqualitySpoof: + calls = 0 + + def __eq__(self, other): + type(self).calls += 1 + return True + + def __ne__(self, other): + type(self).calls += 1 + return False + + +class FractionSpoof(Fraction): + def __eq__(self, other): + return True + + def __ne__(self, other): + return False + + +class ReviewClosureTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.geometry = native.Geometry(SOURCES/'ucns') + + def fixture(self): + origin = native.ByteOrigin(b'ABxABy', 'review', 0, self.geometry) + return native.close_sequences(origin, (b'AB', b'x', b'y'), (0,1,0,2), (1,2,3), SPACES) + + def recipe_packet(self): + symbol = chr(0xE000) + return Packet('budget', 0, (Entry(symbol, BitBlock(101,16), Fraction(1,7), + 1, PrimePath(101,())),), symbol) + + def test_accounting_reuses_the_single_validation(self): + packet = self.recipe_packet() + seen = [] + replay = PrimePath.replay + def record(path, *args, **kwargs): + seen.append(path) + return replay(path, *args, **kwargs) + with patch.object(PrimePath, 'replay', record): + stats = numeral.accounting(packet, Limits(prime_work=5)) + self.assertEqual(len(seen), 1, 'accounting repeated the recipe validation') + self.assertEqual(stats['source_bits'], 16) + self.assertEqual(stats['total_bytes'], len(numeral.encode(packet))) + with self.assertRaises(ResourceLimit): + numeral.accounting(packet, Limits(prime_work=4)) + + def test_cli_shares_prime_budget_through_inspect_and_recover(self): + wire = numeral.encode(self.recipe_packet()) + # Decode charges 10, accounting 5, and restore a further 5 work units. + for command, minimum in (('inspect',15), ('recover',20)): + for budget, success in ((minimum-1,False), (minimum,True)): + with self.subTest(command=command,budget=budget), tempfile.TemporaryDirectory() as directory: + root=Path(directory); src=root/'record'; dst=root/'out'; src.write_bytes(wire) + argv=['numeral.py',command,str(src)] + ([str(dst)] if command=='recover' else []) + with patch.object(sys,'argv',argv), patch.object(numeral,'Limits',return_value=Limits(prime_work=budget)): + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + if success: + self.assertEqual(numeral.main(),0) + else: + with self.assertRaises(SystemExit) as failure: + numeral.main() + self.assertEqual(failure.exception.code,2) + if command=='recover' and success: + self.assertEqual(dst.read_bytes(),b'\x00e') + else: + self.assertFalse(dst.exists()) + + def test_discovery_documentation_preserves_closure_owner(self): + self.assertNotIn('UCHC closes', affixiation.__doc__) + self.assertIn('Stack-owned', affixiation.__doc__) + self.assertIn('native_binary.py', affixiation.__doc__) + self.assertIn('UCNS geometry', affixiation.__doc__) + + def test_all_native_record_slots_reject_equality_spoofs(self): + for target, fields in (('definition',('attachment_axis','state')), + ('occurrence',('source_axis','circle_axis','state','source_state'))): + for field in fields: + with self.subTest(target=target,field=field): + table=self.fixture(); first=table.definitions[0]; EqualitySpoof.calls=0 + if target=='definition': + forged=replace(first, **{field:EqualitySpoof()}) + else: + occurrence=replace(first.occurrences[0], **{field:EqualitySpoof()}) + forged=replace(first,occurrences=(occurrence,)+first.occurrences[1:]) + with self.assertRaises(native.BinaryError): + replace(table,definitions=(forged,)+table.definitions[1:]) + self.assertEqual(EqualitySpoof.calls,0,'untrusted equality was invoked') + + def test_native_scalar_fields_reject_spoofs_and_subclasses(self): + cases=(('attachment_axis','origin_sha256',EqualitySpoof()), + ('attachment_axis','identity_sha256',EqualitySpoof()), + ('attachment_axis','axis_count',EqualitySpoof()), + ('attachment_axis','axis_ordinal',EqualitySpoof()), + ('attachment_axis','turn',FractionSpoof(0)), + ('state','phase_turns',FractionSpoof(0)), + ('state','frame',EqualitySpoof())) + for slot, field, value in cases: + with self.subTest(slot=slot,field=field): + table=self.fixture(); first=table.definitions[0] + forged=copy(getattr(first,slot)); object.__setattr__(forged,field,value) + changed=replace(first,**{slot:forged}) + with self.assertRaises(native.BinaryError): + replace(table,definitions=(changed,)+table.definitions[1:]) + + def test_records_from_another_geometry_instance_are_not_substituted(self): + table=self.fixture(); first=table.definitions[0] + other=native.Geometry(SOURCES/'ucns') + axis=other.axis(table.origin.identity,len(table.origin.source),0) + with self.assertRaises(native.BinaryError): + replace(table,definitions=(replace(first,attachment_axis=axis),)+table.definitions[1:]) + self.assertEqual(table.restore(),b'ABxABy') + + def test_all_export_paths_reject_bypassed_frozen_spoofs(self): + for method in ('restore','receipt','wire_occurrences'): + with self.subTest(method=method): + table=self.fixture() + object.__setattr__(table.definitions[0],'attachment_axis',EqualitySpoof()) + with self.assertRaises(native.BinaryError): getattr(table,method)() + + def native_args(self, source): + return dict(api=native,geometry=self.geometry,scope=PROFILE.scope, + root=native.ByteOrigin(source,PROFILE.scope,0,self.geometry).message_origin, + round_id=0,spec=PROFILE.rounds[0]) + + def test_unaffix_rejects_valid_alternate_partitions(self): + source=b'ABxABy'; canonical=discover(source) + alternatives=(Partition((b'A',b'B',b'x',b'y'),(0,1,2,0,1,3),tuple(range(6)),0), + Partition((b'x',b'AB',b'y'),(1,0,1,2),(0,2,3,5),0), + Partition((source,),(0,),(0,),0)) + for alternative in alternatives: + self.assertEqual(alternative.restore(),source) + self.assertNotEqual((alternative.blocks,alternative.order),(canonical.blocks,canonical.order)) + with patch.object(cycle,'discover',return_value=alternative): + wire,_=cycle.affix(source,**self.native_args(source)) + with self.assertRaisesRegex(Refused,'discovery profile'): + cycle.unaffix(wire,**self.native_args(source)) + + def test_whole_cycle_reverse_rejects_alternate_selection(self): + profile=replace(PROFILE,rounds=PROFILE.rounds[:1]) + def single_literal(data,**kwargs): + return Partition((data,),(0,),(0,),0) + with patch.object(cycle,'discover',side_effect=single_literal): + wire,_=cycle.forward(b'ABABABAB',b'corpus',profile,SOURCES) + with self.assertRaisesRegex(Refused,'discovery profile'): + cycle.reverse(wire,b'corpus',profile,SOURCES) + + def test_canonical_discovery_records_still_recover(self): + for source in (b'',b'x',b'ABxABy',b'AAAAA',b'banana bandana banana',bytes(range(256))): + with self.subTest(length=len(source)): + wire,_=cycle.affix(source,**self.native_args(source)) + self.assertEqual(cycle.unaffix(wire,**self.native_args(source)),source) + + def test_inverse_discovery_honors_the_visit_budget(self): + source=b'ABABABAB'; args=self.native_args(source) + wire,_=cycle.affix(source,**args) + with self.assertRaises(ResourceLimit): + cycle.unaffix(wire,**args,limits=cycle.CycleLimits(discovery_visits=1)) + + +if __name__=='__main__': unittest.main() From 54b62c407344f27ede1c213f75880516629950a6 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 20:06:24 -0700 Subject: [PATCH 07/15] test(weave): bind canonical replay witness to existing round contract --- research/weave/tests/test_review_closure.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/research/weave/tests/test_review_closure.py b/research/weave/tests/test_review_closure.py index 7e2455d..ad54760 100644 --- a/research/weave/tests/test_review_closure.py +++ b/research/weave/tests/test_review_closure.py @@ -45,7 +45,7 @@ # mutates: none # cleanup: none # id: review_discovery_roundtrip -# proves: cycle_discovery_profile, cycle_exact_recovery +# proves: cycle_discovery_profile, cycle_reversible_rounds # call: self::test_canonical_discovery_records_still_recover # mutates: none # cleanup: none @@ -103,6 +103,7 @@ def __eq__(self, other): return True def __ne__(self, other): + type(self).calls += 1 return False From e006fca3644b4b2627f39c8ffb5b25178b8c5d02 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 20:07:29 -0700 Subject: [PATCH 08/15] test(weave): preserve exact hostile-fraction fixture behavior --- research/weave/tests/test_review_closure.py | 1 - 1 file changed, 1 deletion(-) diff --git a/research/weave/tests/test_review_closure.py b/research/weave/tests/test_review_closure.py index ad54760..2d012be 100644 --- a/research/weave/tests/test_review_closure.py +++ b/research/weave/tests/test_review_closure.py @@ -103,7 +103,6 @@ def __eq__(self, other): return True def __ne__(self, other): - type(self).calls += 1 return False From f672bb227ef0ef5e2659a61deb0bf5b67dbeeb59 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 20:14:56 -0700 Subject: [PATCH 09/15] fix(weave): prevent records from replacing their own validators Close the remaining instance-shadow route within native-record finding 4189637581 by invoking trusted ClosedSequence and Occurrence class validators directly. Add a regression for definition and occurrence shadowing, update the native source lock, and retain all four repairs without changing the forward profile or UCNS sources. Observed local full suite: 137 passed, zero failures/errors/skips, source unchanged. Source/profile/native-lock SHA-256: 39684d99cb2e8a4373ecd3279c999cb633d483721cd96fd8140a5b5e7004f309. Hosted checks and exact-head review remain separate gates. --- research/weave/CYCLE.md | 4 +++- research/weave/CYCLE_NATIVE.json | 2 +- research/weave/native_binary.py | 4 ++-- research/weave/test.py | 4 ++-- research/weave/tests/test_review_closure.py | 22 +++++++++++++++++++++ 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/research/weave/CYCLE.md b/research/weave/CYCLE.md index 18ee061..62948c1 100644 --- a/research/weave/CYCLE.md +++ b/research/weave/CYCLE.md @@ -162,7 +162,7 @@ and malformed-input versus resource-limit classification. Three new regressions bring its suite to 18. The obsolete bit-per-circle modules and their 22 tests are removed, with these sequence/native operations as replacement. Fifty-two original transport/assembly tests retain their original limited scope. Full Weave inventory: -52 + 18 + 30 + 13 native-origin + 12 repair + 11 review-closure regressions = 136. The original +52 + 18 + 30 + 13 native-origin + 12 repair + 12 review-closure regressions = 137. The original 13 native producer tests are retained in Stack as `tests/test_binary_origin.py`. The repair regressions exercise forged closed records, fresh verified module loading, atomic failed-write cleanup, empty-input attachments and manifest edges. @@ -197,6 +197,8 @@ prime-work engine through parsing, accounting, and recovery. Budget failure occu before publishing output. Native closure comparison checks the exact UCNS classes from the origin's Geometry instance and compares only typed canonical fields, never arbitrary object equality. This also rejects equality-spoofing scalar subclasses. +Validation calls the trusted record classes directly; per-instance methods cannot +replace the validation of a supplied record. The inverse rediscovers sequences under the declared selection profile and requires both definitions and source order to match. Its rediscovery obeys the same explicit diff --git a/research/weave/CYCLE_NATIVE.json b/research/weave/CYCLE_NATIVE.json index 64a7225..63820b0 100644 --- a/research/weave/CYCLE_NATIVE.json +++ b/research/weave/CYCLE_NATIVE.json @@ -8,7 +8,7 @@ "binary_source": { "owner": "The-Interdependency/stack", "path": "native_binary.py", - "git_blob": "b3afde7f95678b47ceda7fbbd931972ebeaf8b7e", + "git_blob": "a917287bdb8bddf239cb48313abd865b2be635c4", "standing": "forge-owned candidate, not graduated" }, "uchc_architecture": { diff --git a/research/weave/native_binary.py b/research/weave/native_binary.py index b3afde7..a917287 100644 --- a/research/weave/native_binary.py +++ b/research/weave/native_binary.py @@ -299,9 +299,9 @@ def validate(self) -> None: if _sha(origin.identity) != reidentified.identity: raise BinaryError('origin identity no longer binds its source') for definition in self.definitions: - definition.__post_init__() + ClosedSequence.__post_init__(definition) for occurrence in definition.occurrences: - occurrence.__post_init__() + Occurrence.__post_init__(occurrence) blocks = tuple(d.data for d in self.definitions) circles = tuple(d.occurrences[0].circle for d in self.definitions) expected = _closed_definitions(origin, blocks, self.order, circles, self.spaces) diff --git a/research/weave/test.py b/research/weave/test.py index 2e9b572..6502eb1 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -17,7 +17,7 @@ from assembly import Pipeline ROOT = Path(__file__).resolve().parent -EXPECTED_TESTS = 136 # 125 existing + 11 final-review closure regressions. +EXPECTED_TESTS = 137 # 125 existing + 12 final-review closure regressions. def snapshot(): @@ -63,7 +63,7 @@ def main(): 'cycle_sample_rounds': 3, 'native_occurrence_circles': 7, 'prime_jump_trace': [5381, 53, 241, 1523], 'native_origin_methods': 13, 'cycle_repair_methods': 12, - 'review_closure_methods': 11 + 'review_closure_methods': 12 }, 'cycle': 'EXPLICIT_CANDIDATE: corpus normalization once; native sequence affixiation and prime-derived bit interleave; exact reverse', 'full_weave': Pipeline().plan(), diff --git a/research/weave/tests/test_review_closure.py b/research/weave/tests/test_review_closure.py index 2d012be..0bf31b5 100644 --- a/research/weave/tests/test_review_closure.py +++ b/research/weave/tests/test_review_closure.py @@ -54,6 +54,11 @@ # call: self::test_inverse_discovery_honors_the_visit_budget # mutates: none # cleanup: none +# id: review_native_validator_not_shadowed +# proves: binary_sequence_closure, binary_source_refusal +# call: self::test_record_instances_cannot_shadow_validation +# mutates: none +# cleanup: none # === END CHECKS === """PR #77 final-review regressions, not cryptographic-strength tests. @@ -208,6 +213,23 @@ def test_all_export_paths_reject_bypassed_frozen_spoofs(self): object.__setattr__(table.definitions[0],'attachment_axis',EqualitySpoof()) with self.assertRaises(native.BinaryError): getattr(table,method)() + def test_record_instances_cannot_shadow_validation(self): + for target in ('definition', 'occurrence'): + with self.subTest(target=target): + table=self.fixture(); definition=table.definitions[0] + item=definition if target=='definition' else definition.occurrences[0] + called=[] + object.__setattr__(item,'__post_init__',lambda:called.append(True)) + self.assertEqual(table.restore(),b'ABxABy') + self.assertEqual(called,[], 'record-supplied validation was invoked') + if target=='definition': + object.__setattr__(item,'occurrences',(EqualitySpoof(),)) + else: + object.__setattr__(item,'source_offset',EqualitySpoof()) + for method in (table.restore,table.receipt,table.wire_occurrences): + with self.assertRaises(native.BinaryError): method() + self.assertEqual(called,[]) + def native_args(self, source): return dict(api=native,geometry=self.geometry,scope=PROFILE.scope, root=native.ByteOrigin(source,PROFILE.scope,0,self.geometry).message_origin, From 97915a39239d827d6361c52748cbd64dcd8ce0d9 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 20:32:21 -0700 Subject: [PATCH 10/15] fix(weave): close supplied-record dispatch validation bypasses Invoke trusted SequenceTable, Packet and Entry validators; use trusted BitBlock conversion and PrimePath replay; revalidate bit fields and require exact opcode strings. Close both final-review findings without changing the forward profile, UCNS geometry or authority. Bound the protection to supplied records, not arbitrary replacement of runtime classes. Six new regression methods exposed the prior bypasses. Full local and fresh-bundle suites: 143 passed, zero failures/errors/skips, source unchanged. Source/profile/native-lock SHA-256: cebb5d274a21d0cc774f5e387f5bae9c36fded7101f42efad122ce52524a92e9. Native source blob: 5a8adb61297d328d69f9efeb19053315059bef43. Hosted checks and exact-head review remain separate gates. --- research/weave/CYCLE.md | 15 +++- research/weave/CYCLE_NATIVE.json | 2 +- research/weave/NUMERAL_CONSTRUCTION.md | 8 ++ research/weave/native_binary.py | 10 ++- research/weave/numeral.py | 27 +++--- research/weave/test.py | 4 +- research/weave/tests/test_review_closure.py | 99 +++++++++++++++++++++ 7 files changed, 146 insertions(+), 19 deletions(-) diff --git a/research/weave/CYCLE.md b/research/weave/CYCLE.md index 62948c1..9b73649 100644 --- a/research/weave/CYCLE.md +++ b/research/weave/CYCLE.md @@ -162,7 +162,7 @@ and malformed-input versus resource-limit classification. Three new regressions bring its suite to 18. The obsolete bit-per-circle modules and their 22 tests are removed, with these sequence/native operations as replacement. Fifty-two original transport/assembly tests retain their original limited scope. Full Weave inventory: -52 + 18 + 30 + 13 native-origin + 12 repair + 12 review-closure regressions = 137. The original +52 + 18 + 30 + 13 native-origin + 12 repair + 18 review-closure regressions = 143. The original 13 native producer tests are retained in Stack as `tests/test_binary_origin.py`. The repair regressions exercise forged closed records, fresh verified module loading, atomic failed-write cleanup, empty-input attachments and manifest edges. @@ -210,3 +210,16 @@ The profile, forward operations, UCNS producers, ownership and cryptographic sta are unchanged. The local candidate source lock changes with the repaired source; records bound to the previous lock must be regenerated rather than accepted under false source provenance. This is stricter validation, not authentication. + +## Record-owned dispatch boundary + +Sequence-table exports call the trusted class validator rather than `self.validate`. +The numeral layer likewise invokes trusted Packet, Entry, BitBlock and PrimePath +operations while handling supplied records. Altered block fields are revalidated; +record-owned `validate`, `replay`, and `to_bytes` overrides cannot authorize or +change serialized content. Prime opcodes require exact string values. + +Six additional regressions cover the table/Entry follow-up findings and the same +pattern in enclosing packets, bit blocks and recipes. This boundary concerns +supplied data records; it does not sandbox hostile Python code that replaces the +trusted runtime classes or producer modules themselves. Runtime code is trusted. diff --git a/research/weave/CYCLE_NATIVE.json b/research/weave/CYCLE_NATIVE.json index 63820b0..ae9bf6c 100644 --- a/research/weave/CYCLE_NATIVE.json +++ b/research/weave/CYCLE_NATIVE.json @@ -8,7 +8,7 @@ "binary_source": { "owner": "The-Interdependency/stack", "path": "native_binary.py", - "git_blob": "a917287bdb8bddf239cb48313abd865b2be635c4", + "git_blob": "5a8adb61297d328d69f9efeb19053315059bef43", "standing": "forge-owned candidate, not graduated" }, "uchc_architecture": { diff --git a/research/weave/NUMERAL_CONSTRUCTION.md b/research/weave/NUMERAL_CONSTRUCTION.md index eb8dd55..db540a9 100644 --- a/research/weave/NUMERAL_CONSTRUCTION.md +++ b/research/weave/NUMERAL_CONSTRUCTION.md @@ -118,3 +118,11 @@ allowance between decode, accounting and restore. A command that exhausts its budget refuses before output publication. Standalone library calls retain their individual default budgets. The private `_engine` keyword is internal orchestration, not serialized state or a bypass of validation. + +## Trusted record operations + +Serialization, accounting and reconstruction use the trusted Packet/Entry validation +and BitBlock/PrimePath implementations, not methods attached to supplied instances. +BitBlock fields are rechecked before use. A caller-provided override cannot turn an +invalid entry into an admitted packet or change a literal/recipe during serialization. +This is a data-record boundary, not a sandbox against replacement of the runtime. diff --git a/research/weave/native_binary.py b/research/weave/native_binary.py index a917287..5a8adb6 100644 --- a/research/weave/native_binary.py +++ b/research/weave/native_binary.py @@ -278,7 +278,7 @@ class SequenceTable: spaces: tuple[Fraction, ...] def __post_init__(self): - self.validate() + SequenceTable.validate(self) def validate(self) -> None: """Reconstruct and compare the entire closed relation, not its type label. @@ -287,6 +287,8 @@ def validate(self) -> None: ordering, native axis, space-displacement and occurrence invariants as close_sequences. Frozen does not mean intrinsically consistent. """ + if type(self) is not SequenceTable: + raise BinaryError('exact SequenceTable required') if type(self.origin) is not ByteOrigin: raise BinaryError('closed table requires a native byte-origin') if (type(self.definitions) is not tuple @@ -322,18 +324,18 @@ def fields(definitions): def restore(self) -> bytes: """Read definitions and occurrence order, not origin.source.""" - self.validate() + SequenceTable.validate(self) return b''.join(self.definitions[i].data for i in self.order) def wire_occurrences(self) -> tuple[tuple[int, Occurrence], ...]: """Seven circle streams, each ordered by its native complete local position.""" - self.validate() + SequenceTable.validate(self) items = [(i, o) for i, d in enumerate(self.definitions) for o in d.occurrences] return tuple(sorted(items, key=lambda item: ( item[1].circle, self.origin.geometry.lift(item[1].state)))) def receipt(self) -> dict: - self.validate() + SequenceTable.validate(self) return {'schema': SCHEMA, 'version': VERSION, 'origin': self.origin.identity, 'message_origin': self.origin.message_origin, 'round': self.origin.round_id, 'spaces': [str(space) for space in self.spaces], diff --git a/research/weave/numeral.py b/research/weave/numeral.py index 6b78fd0..eca3f40 100644 --- a/research/weave/numeral.py +++ b/research/weave/numeral.py @@ -145,7 +145,7 @@ def replay(self, limits: Limits = Limits(), *, _engine=None) -> tuple[int, ...]: primes.require(value) trace = [value] for step in self.steps: - if type(step) is not tuple or not step: + if type(step) is not tuple or not step or type(step[0]) is not str: raise Refused("invalid recipe step") if step == ("next",): value = primes.nth(value) @@ -239,13 +239,16 @@ class Entry: def validate(self, limits: Limits, *, _engine=None) -> None: _symbol(self.symbol) - if type(self.block) is not BitBlock or self.block.length == 0: + if type(self.block) is not BitBlock: + raise Refused("a definition must contain an exact BitBlock") + BitBlock.__post_init__(self.block) + if self.block.length == 0: raise Refused("a definition must contain a nonempty BitBlock") if self.block.length > limits.output_bits: raise ResourceLimit("definition exceeds bit budget") _validate_attachment(self.angle, self.circle) if self.recipe is not None: - if type(self.recipe) is not PrimePath or self.recipe.replay(limits, _engine=_engine)[-1] != self.block.value: + if type(self.recipe) is not PrimePath or PrimePath.replay(self.recipe, limits, _engine=_engine)[-1] != self.block.value: raise Refused("recipe does not construct its bound integer") @@ -262,6 +265,8 @@ class Packet: symbols: str def validate(self, limits: Limits = Limits(), *, _engine=None) -> int: + if type(self) is not Packet: + raise Refused("exact Packet required") if type(self.origin) is not str or not self.origin or len(self.origin.encode("utf-8")) > 1024: raise Refused("nonempty UTF-8 origin scope of at most 1024 bytes required") _nat(self.round_id) @@ -279,7 +284,7 @@ def validate(self, limits: Limits = Limits(), *, _engine=None) -> int: for entry in self.entries: if type(entry) is not Entry: raise Refused("Entry required") - entry.validate(limits, _engine=primes) + Entry.validate(entry, limits, _engine=primes) if entry.symbol in mapping or entry.angle in angles: raise Refused("duplicate symbol or angular attachment in this scope") mapping[entry.symbol] = entry @@ -295,7 +300,7 @@ def validate(self, limits: Limits = Limits(), *, _engine=None) -> int: def occurrences(self, limits: Limits = Limits()) -> tuple[tuple[str, int, int, int], ...]: """(symbol, bit offset, ordinal on its occurrence circle, circle).""" - self.validate(limits) + Packet.validate(self, limits) mapping = {entry.symbol: entry for entry in self.entries} counts = [0] * 8 offset = 0 @@ -309,13 +314,13 @@ def occurrences(self, limits: Limits = Limits()) -> tuple[tuple[str, int, int, i def restore(self, limits: Limits = Limits(), *, _engine=None) -> BitBlock: """Reconstruct in linear output-byte work; no original input or hidden table.""" - total = self.validate(limits, _engine=_engine) + total = Packet.validate(self, limits, _engine=_engine) mapping = {entry.symbol: entry.block for entry in self.entries} out = bytearray((total + 7) // 8) offset = 0 for symbol in self.symbols: block = mapping[symbol] - raw = block.to_bytes() + raw = BitBlock.to_bytes(block) byte, shift = divmod(offset, 8) if shift == 0: out[byte:byte + len(raw)] = raw @@ -376,7 +381,7 @@ def blob(self, maximum: int) -> bytes: def _parts(packet: Packet, limits: Limits, *, _engine=None) -> tuple[int, bytes, bytes, bytes]: """Return the validated bit count with wire parts; never replay it for accounting.""" - total = packet.validate(limits, _engine=_engine) + total = Packet.validate(packet, limits, _engine=_engine) header = MAGIC + _blob(packet.origin.encode("utf-8")) + _uint(packet.round_id) + _uint(total) count = _uint(len(packet.entries)) # Size the entire representation before allocating literal output buffers. @@ -407,7 +412,7 @@ def _parts(packet: Packet, limits: Limits, *, _engine=None) -> tuple[int, bytes, raise ResourceLimit("serialized packet exceeds byte budget") if projected > limits.wire_bytes: raise ResourceLimit("serialized packet exceeds byte budget") - definitions = count + b"".join(prefix + (block.to_bytes() if block is not None else b"") + definitions = count + b"".join(prefix + (BitBlock.to_bytes(block) if block is not None else b"") for prefix, block in pieces) stream = _blob(packet.symbols.encode("utf-8")) return total, header, definitions, stream @@ -469,7 +474,7 @@ def decode(data: bytes, limits: Limits = Limits(), *, _engine=None) -> Packet: else: raise Refused("unknown recipe opcode") recipe = PrimePath(seed, tuple(steps)) - block = BitBlock(recipe.replay(limits, _engine=primes)[-1], length) + block = BitBlock(PrimePath.replay(recipe, limits, _engine=primes)[-1], length) else: raise Refused("unknown definition tag") entries.append(Entry(symbol, block, angle, circle, recipe)) @@ -480,7 +485,7 @@ def decode(data: bytes, limits: Limits = Limits(), *, _engine=None) -> Packet: raise Refused("trailing packet data") packet = Packet(origin, round_id, tuple(entries), symbols) # Parsing and structural validation share one charged prime-work budget. - if packet.validate(limits, _engine=primes) != declared: + if Packet.validate(packet, limits, _engine=primes) != declared: raise Refused("declared output length disagrees with occurrences") return packet diff --git a/research/weave/test.py b/research/weave/test.py index 6502eb1..63a7dfb 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -17,7 +17,7 @@ from assembly import Pipeline ROOT = Path(__file__).resolve().parent -EXPECTED_TESTS = 137 # 125 existing + 12 final-review closure regressions. +EXPECTED_TESTS = 143 # 125 existing + 18 final-review closure regressions. def snapshot(): @@ -63,7 +63,7 @@ def main(): 'cycle_sample_rounds': 3, 'native_occurrence_circles': 7, 'prime_jump_trace': [5381, 53, 241, 1523], 'native_origin_methods': 13, 'cycle_repair_methods': 12, - 'review_closure_methods': 12 + 'review_closure_methods': 18 }, 'cycle': 'EXPLICIT_CANDIDATE: corpus normalization once; native sequence affixiation and prime-derived bit interleave; exact reverse', 'full_weave': Pipeline().plan(), diff --git a/research/weave/tests/test_review_closure.py b/research/weave/tests/test_review_closure.py index 0bf31b5..91f1fab 100644 --- a/research/weave/tests/test_review_closure.py +++ b/research/weave/tests/test_review_closure.py @@ -59,6 +59,36 @@ # call: self::test_record_instances_cannot_shadow_validation # mutates: none # cleanup: none +# id: review_table_dispatch +# proves: binary_sequence_closure, binary_coordinate_recovery +# call: self::test_table_exports_use_trusted_validation +# mutates: none +# cleanup: none +# id: review_entry_dispatch +# proves: numeral_strict_input, numeral_exact_recovery +# call: self::test_entry_validation_cannot_be_shadowed +# mutates: none +# cleanup: none +# id: review_packet_dispatch +# proves: numeral_strict_input, numeral_exact_recovery +# call: self::test_packet_validation_cannot_be_shadowed +# mutates: none +# cleanup: none +# id: review_block_conversion +# proves: numeral_exact_recovery +# call: self::test_block_conversion_cannot_be_shadowed +# mutates: none +# cleanup: none +# id: review_block_fields +# proves: numeral_strict_input +# call: self::test_entry_revalidates_supplied_bitblock_fields +# mutates: none +# cleanup: none +# id: review_recipe_dispatch +# proves: numeral_recipe_replay, numeral_strict_input +# call: self::test_recipe_replay_cannot_be_shadowed +# mutates: none +# cleanup: none # === END CHECKS === """PR #77 final-review regressions, not cryptographic-strength tests. @@ -230,6 +260,75 @@ def test_record_instances_cannot_shadow_validation(self): with self.assertRaises(native.BinaryError): method() self.assertEqual(called,[]) + def test_table_exports_use_trusted_validation(self): + for change in ('order', 'definitions', 'origin'): + table=self.fixture(); called=[] + object.__setattr__(table,'validate',lambda:called.append(True)) + self.assertEqual(table.restore(),b'ABxABy') + self.assertEqual(called,[]) + if change=='order': + object.__setattr__(table,'order',(1,0,0,2)) + elif change=='definitions': + object.__setattr__(table,'definitions',table.definitions[:-1]) + else: + object.__setattr__(table,'origin',EqualitySpoof()) + for method in (table.restore,table.receipt,table.wire_occurrences): + with self.subTest(change=change,method=method.__name__): + with self.assertRaises(native.BinaryError):method() + self.assertEqual(called,[]) + + def test_entry_validation_cannot_be_shadowed(self): + packet=self.recipe_packet();entry=packet.entries[0];called=[] + object.__setattr__(entry,'validate',lambda *a,**kw:called.append(True)) + self.assertEqual(numeral.decode(numeral.encode(packet)).restore(),BitBlock(101,16)) + self.assertEqual(called,[]) + object.__setattr__(entry,'circle',999) + for method in (lambda:numeral.encode(packet),lambda:numeral.accounting(packet), + packet.restore,packet.occurrences): + with self.assertRaises(Refused):method() + self.assertEqual(called,[]) + + def test_packet_validation_cannot_be_shadowed(self): + packet=self.recipe_packet();called=[] + object.__setattr__(packet,'validate',lambda *a,**kw:called.append(True) or 16) + self.assertEqual(numeral.decode(numeral.encode(packet)).restore(),BitBlock(101,16)) + self.assertEqual(called,[]) + object.__setattr__(packet,'entries',packet.entries*2) + for method in (lambda:numeral.encode(packet),lambda:numeral.accounting(packet), + packet.restore,packet.occurrences): + with self.assertRaises(Refused):method() + self.assertEqual(called,[]) + + def test_block_conversion_cannot_be_shadowed(self): + packet=self.recipe_packet();entry=replace(packet.entries[0],recipe=None) + packet=replace(packet,entries=(entry,));called=[] + object.__setattr__(entry.block,'to_bytes',lambda:called.append(True) or b'ZZ') + self.assertEqual(numeral.decode(numeral.encode(packet)).restore(),BitBlock(101,16)) + self.assertEqual(packet.restore(),BitBlock(101,16)) + self.assertEqual(called,[]) + + def test_entry_revalidates_supplied_bitblock_fields(self): + for field,value in (('length',True),('length',-1),('length',EqualitySpoof()), + ('value',True),('value',-1),('value',1<<20)): + packet=self.recipe_packet();entry=replace(packet.entries[0],recipe=None) + packet=replace(packet,entries=(entry,)) + object.__setattr__(entry.block,field,value) + with self.subTest(field=field,value_type=type(value).__name__): + with self.assertRaises(Refused):numeral.encode(packet) + + def test_recipe_replay_cannot_be_shadowed(self): + packet=self.recipe_packet();recipe=packet.entries[0].recipe;called=[] + object.__setattr__(recipe,'replay',lambda *a,**kw:called.append(True) or (101,)) + self.assertEqual(numeral.decode(numeral.encode(packet)).restore(),BitBlock(101,16)) + self.assertEqual(called,[]) + object.__setattr__(recipe,'seed',4) + with self.assertRaises(Refused):numeral.encode(packet) + self.assertEqual(called,[]) + # A spoofed opcode is malformed data, not an executable next-prime step. + path=PrimePath(2,((EqualitySpoof(),),));EqualitySpoof.calls=0 + with self.assertRaises(Refused):PrimePath.replay(path) + self.assertEqual(EqualitySpoof.calls,0) + def native_args(self, source): return dict(api=native,geometry=self.geometry,scope=PROFILE.scope, root=native.ByteOrigin(source,PROFILE.scope,0,self.geometry).message_origin, From 76508332f3535d5f5e60af69e8e1ca2afb9c50d9 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 23:01:21 -0700 Subject: [PATCH 11/15] fix(weave): preserve complete repeated matches and trusted construction paths Repair all five P2 findings at 97915a: construct native byte axes through trusted class methods; revalidate profiles and derive identity through trusted serializers; expand capped LCP intervals before nonoverlapping selection; reject recipe-backed definitions not emitted by this cycle profile; and use trusted PrimePath replay for schedule derivation. Nine new regression methods fail against the baseline and pass after repair. Full local suite: 152 passed, zero failures/errors/skips, source unchanged. Runtime/source/profile/native-lock digest: 18ac9197e0d081246a56a02c763e54b28e64ea05224be633976628c4cd22e5d3. Exact recovery and full-cost 6735-byte demo preserved. Uploaded blobs match tested bytes. Hosted checks and fresh terminal review remain separate gates. UCNS sources, UCHC architecture reference, Stack ownership and cryptographic standing are unchanged. --- research/weave/CYCLE.md | 36 +++- research/weave/CYCLE_NATIVE.json | 2 +- research/weave/affixiation.py | 41 ++++- research/weave/cycle.py | 39 ++++- research/weave/native_binary.py | 40 ++--- research/weave/prime_schedule.py | 2 +- research/weave/test.py | 4 +- research/weave/tests/test_final_findings.py | 181 ++++++++++++++++++++ 8 files changed, 308 insertions(+), 37 deletions(-) create mode 100644 research/weave/tests/test_final_findings.py diff --git a/research/weave/CYCLE.md b/research/weave/CYCLE.md index 9b73649..708b185 100644 --- a/research/weave/CYCLE.md +++ b/research/weave/CYCLE.md @@ -162,7 +162,7 @@ and malformed-input versus resource-limit classification. Three new regressions bring its suite to 18. The obsolete bit-per-circle modules and their 22 tests are removed, with these sequence/native operations as replacement. Fifty-two original transport/assembly tests retain their original limited scope. Full Weave inventory: -52 + 18 + 30 + 13 native-origin + 12 repair + 18 review-closure regressions = 143. The original +52 + 18 + 30 + 13 native-origin + 12 repair + 18 review-closure + 9 follow-up regressions = 152. The original 13 native producer tests are retained in Stack as `tests/test_binary_origin.py`. The repair regressions exercise forged closed records, fresh verified module loading, atomic failed-write cleanup, empty-input attachments and manifest edges. @@ -223,3 +223,37 @@ Six additional regressions cover the table/Entry follow-up findings and the same pattern in enclosing packets, bit blocks and recipes. This boundary concerns supplied data records; it does not sandbox hostile Python code that replaces the trusted runtime classes or producer modules themselves. Runtime code is trusted. + +## Follow-up discovery and dispatch repairs + +`tests/test_final_findings.py` covers the five subsequent review findings. A capped +LCP candidate now expands to the full matching suffix interval before selection; +duplicate intervals are merged before applying the existing longest-first and +leftmost-nonoverlapping rules. For `00 01 00 01 00 01 01`, all three disjoint +`00 01` occurrences are retained, followed by the residual `01`. The selected +profile is unchanged; the earlier implementation omitted a qualifying occurrence. +The regression suite compares the LCP-derived candidate family against explicit +prefix matches on all 1,022 nonempty binary-alphabet strings of lengths 1..9. + +Native closure uses the trusted ByteOrigin byte-axis constructor and trusted +geometry/axis export methods, so a record-owned method cannot replace the origin +under validation. Profile identity revalidates scalar and nested round fields +and uses trusted serializers. The scheduler invokes trusted PrimePath replay, +including its aggregate budget, rather than a method on the supplied path. + +This cycle profile emits literal sequence definitions. Recovery now rejects a +recipe-backed substitute even when the recipe produces the same bytes. The +standalone numeral layer retains its explicit prime-recipe support; it has not +been removed or silently disabled there. Supporting such definitions in a cycle +requires a profile that actually emits them. The existing native-source lock +rotation refuses old records instead of attributing them to the repaired code. + +Run the added regressions with: + +```sh +WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_final_findings.py -v +``` + +All 152 tests pass on the repaired local source. The three-round demo still costs +6,735 total bytes and recovers exactly. These repairs change neither the UCNS +producer pins nor the ownership or cryptographic standing of the construction. diff --git a/research/weave/CYCLE_NATIVE.json b/research/weave/CYCLE_NATIVE.json index ae9bf6c..223509e 100644 --- a/research/weave/CYCLE_NATIVE.json +++ b/research/weave/CYCLE_NATIVE.json @@ -8,7 +8,7 @@ "binary_source": { "owner": "The-Interdependency/stack", "path": "native_binary.py", - "git_blob": "5a8adb61297d328d69f9efeb19053315059bef43", + "git_blob": "5a49a4f43ab2baefbf5beda2e17cb3ba131c4b15", "standing": "forge-owned candidate, not graduated" }, "uchc_architecture": { diff --git a/research/weave/affixiation.py b/research/weave/affixiation.py index 79fa0d5..047ae47 100644 --- a/research/weave/affixiation.py +++ b/research/weave/affixiation.py @@ -129,6 +129,32 @@ def query(self, start: int, end: int) -> tuple[int, int]: return low, high +def _matching_interval(common: _Ranges, begin: int, end: int, + length: int, size: int) -> tuple[int, int]: + """Expand a suffix interval to all occurrences of its shortened prefix. + + Adjacent suffixes share a length-L prefix exactly when all intervening LCP + values are at least L. Range minima and binary search retain linear storage + and avoid copying/enumerating long prefixes to locate their full interval. + """ + low, high = 0, begin + while low < high: + middle = (low+high)//2 + if common.query(middle+1, begin+1)[0] >= length: + high = middle + else: + low = middle+1 + expanded_begin = low + low, high = end, size + while low < high: + middle = (low+high+1)//2 + if common.query(end, middle)[0] >= length: + low = middle + else: + high = middle-1 + return expanded_begin, low + + class _Occupied: def __init__(self, size: int): self.tree = [0] * (size+1) @@ -166,20 +192,25 @@ def discover(data: bytes, *, max_bytes: int = 1048576, suffixes = _suffix_array(data) common = _lcp(data, suffixes) ranges = _Ranges(suffixes) - stack, candidates = [], [] + common_ranges = _Ranges(common) + stack, candidates = [], set() for i in range(1, n+1): depth = common[i] if i < n else 0 left = i-1 while stack and stack[-1][0] > depth: length, begin = stack.pop() first, last = ranges.query(begin, i) - length = min(length, last-first) # At least two disjoint occurrences. - if length >= 2: - candidates.append((-length, first, begin, i)) + capped = min(length, last-first) # At least two disjoint occurrences. + if capped >= 2: + start, end = begin, i + if capped != length: + start, end = _matching_interval(common_ranges, begin, i, capped, n) + first, _ = ranges.query(start, end) + candidates.add((-capped, first, start, end)) left = begin if depth and (not stack or stack[-1][0] < depth): stack.append((depth, left)) - candidates.sort() + candidates = sorted(candidates) occupied = _Occupied(n) chosen, patterns = [], set() covered = visits = 0 diff --git a/research/weave/cycle.py b/research/weave/cycle.py index cc54473..e6725ae 100644 --- a/research/weave/cycle.py +++ b/research/weave/cycle.py @@ -130,8 +130,21 @@ class RoundSpec: end_order: str def __post_init__(self): + if type(self) is not RoundSpec: + raise Refused('exact RoundSpec required') if type(self.path) is not PrimePath or type(self.path.steps) is not tuple: raise Refused('exact immutable PrimePath required') + if type(self.path.seed) is not int or self.path.seed < 2: + raise Refused('prime seed must be an integer at least two') + for step in self.path.steps: + if type(step) is not tuple or not step or type(step[0]) is not str: + raise Refused('prime steps require immutable fields and exact opcode strings') + if step == ('next',): + continue + if (len(step) != 4 or step[0] != 'span' + or any(type(x) is not int for x in step[1:]) + or step[1] not in (2,10) or step[2] < 0 or step[3] < 1): + raise Refused('invalid prime-path step') if (type(self.arities) is not tuple or not self.arities or any(type(a) is not int or not 2 <= a <= 64 for a in self.arities) or len(set(self.arities)) != len(self.arities)): @@ -141,13 +154,19 @@ def __post_init__(self): or set(self.circle_order) != set(range(1,8))): raise Refused('circle_order must be a permutation of 1..7') if (type(self.spaces) is not tuple or len(self.spaces) != 8 - or any(type(x) is not Fraction or not 0 <= x < 2 or x.denominator >= 2**32 - for x in self.spaces)): + or any(type(x) is not Fraction + or type(x.numerator) is not int or type(x.denominator) is not int + or not 1 <= x.denominator < 2**32 + or not 0 <= x.numerator < 2*x.denominator + or (Fraction(x.numerator,x.denominator).numerator, + Fraction(x.numerator,x.denominator).denominator) + != (x.numerator,x.denominator) for x in self.spaces)): raise Refused('eight exact canonical space turns with denominators below 2^32 required') - if self.end_order not in ('first-last','last-first'): + if type(self.end_order) is not str or self.end_order not in ('first-last','last-first'): raise Refused('explicit first-last or last-first order required') def as_dict(self): + RoundSpec.__post_init__(self) return {'prime_path': {'seed': self.path.seed, 'steps': [list(x) for x in self.path.steps]}, 'arities': list(self.arities), 'circle_order': list(self.circle_order), 'spaces': [[x.numerator,x.denominator] for x in self.spaces], @@ -162,21 +181,26 @@ class Profile: rounds: tuple[RoundSpec, ...] def __post_init__(self): + if type(self) is not Profile: + raise Refused('exact Profile required') if type(self.scope) is not str or not self.scope or len(self.scope.encode('utf-8')) > 1024: raise Refused('nonempty UTF-8 message scope of at most 1024 bytes required') _positive(self.bucket_bytes, 1048576, 'normalization bucket') _positive(self.corpus_bit_offset, 2**64-1, 'corpus bit offset', zero=True) if type(self.rounds) is not tuple or not self.rounds or any(type(r) is not RoundSpec for r in self.rounds): raise Refused('nonempty immutable round specification required') + for spec in self.rounds: + RoundSpec.__post_init__(spec) def as_dict(self): + Profile.__post_init__(self) return {'schema': PROFILE_SCHEMA, 'selection': SELECTION_PROFILE, 'split': SPLIT_PROFILE, 'scope': self.scope, 'bucket_bytes': self.bucket_bytes, - 'corpus_bit_offset': self.corpus_bit_offset, 'rounds': [r.as_dict() for r in self.rounds]} + 'corpus_bit_offset': self.corpus_bit_offset, 'rounds': [RoundSpec.as_dict(r) for r in self.rounds]} @property def identity(self): - return sha256(canonical(self.as_dict())).digest() + return sha256(canonical(Profile.as_dict(self))).digest() @classmethod def read(cls, data: bytes, limits: CycleLimits = CycleLimits()): @@ -335,6 +359,8 @@ def unaffix(data: bytes, *, api, geometry, scope: str, root: str, round_id: int, if source_length > limits.round_bytes: raise ResourceLimit('affixiation source length exceeds byte budget') packet = decode_numeral(reader.blob(limits.round_bytes),limits.numeral()) + if any(entry.recipe is not None for entry in packet.entries): + raise Refused('cycle profile requires literal definitions, not prime recipes') if packet.round_id != round_id: raise Refused('round identity mismatch') # decode_numeral already validates/replays recipes under one budget. Do not @@ -375,8 +401,7 @@ def unaffix(data: bytes, *, api, geometry, scope: str, root: str, round_id: int, def _prepared(profile: Profile, limits: CycleLimits): - if type(profile) is not Profile: - raise Refused('Profile required') + Profile.__post_init__(profile) if len(profile.rounds) > limits.rounds: raise ResourceLimit('round count exceeds execution budget') engine = _Primes(limits.numeral()) diff --git a/research/weave/native_binary.py b/research/weave/native_binary.py index 5a8adb6..5a49a4f 100644 --- a/research/weave/native_binary.py +++ b/research/weave/native_binary.py @@ -231,7 +231,7 @@ def byte_axis(self, offset: int): _natural(offset, 'byte offset') if offset >= len(self.source): raise BinaryError('byte offset outside this round') - return self.geometry.axis(self.identity, len(self.source), offset) + return Geometry.axis(self.geometry, self.identity, len(self.source), offset) @dataclass(frozen=True) @@ -309,13 +309,13 @@ def validate(self) -> None: expected = _closed_definitions(origin, blocks, self.order, circles, self.spaces) def fields(definitions): geometry = origin.geometry - return tuple((d.data, geometry._axis_fields(d.attachment_axis), - geometry._state_fields(d.state), + return tuple((d.data, Geometry._axis_fields(geometry, d.attachment_axis), + Geometry._state_fields(geometry, d.state), tuple((o.source_offset, o.circle, o.ordinal, - geometry._axis_fields(o.source_axis), - geometry._axis_fields(o.circle_axis), - geometry._state_fields(o.state), - geometry._state_fields(o.source_state)) + Geometry._axis_fields(geometry, o.source_axis), + Geometry._axis_fields(geometry, o.circle_axis), + Geometry._state_fields(geometry, o.state), + Geometry._state_fields(geometry, o.source_state)) for o in d.occurrences)) for d in definitions) # Only builtin tuples/bytes/strings/integers reach equality. A foreign # object or native subclass cannot license itself with __eq__ == True. @@ -332,7 +332,7 @@ def wire_occurrences(self) -> tuple[tuple[int, Occurrence], ...]: SequenceTable.validate(self) items = [(i, o) for i, d in enumerate(self.definitions) for o in d.occurrences] return tuple(sorted(items, key=lambda item: ( - item[1].circle, self.origin.geometry.lift(item[1].state)))) + item[1].circle, Geometry.lift(self.origin.geometry, item[1].state)))) def receipt(self) -> dict: SequenceTable.validate(self) @@ -341,14 +341,14 @@ def receipt(self) -> dict: 'spaces': [str(space) for space in self.spaces], 'definitions': [ {'length': len(d.data), 'source_sha256': sha256(d.data).hexdigest(), - 'attachment': d.attachment_axis.as_dict(), - 'complete_turn': str(self.origin.geometry.lift(d.state)), + 'attachment': self.origin.geometry.axis_module.AxisCirclePosition.as_dict(d.attachment_axis), + 'complete_turn': str(Geometry.lift(self.origin.geometry, d.state)), 'occurrences': [ {'source_offset': o.source_offset, 'circle': o.circle, - 'ordinal': o.ordinal, 'source_axis': o.source_axis.as_dict(), - 'circle_axis': o.circle_axis.as_dict(), - 'complete_turn': str(self.origin.geometry.lift(o.state)), - 'source_turn': str(self.origin.geometry.lift(o.source_state))} + 'ordinal': o.ordinal, 'source_axis': self.origin.geometry.axis_module.AxisCirclePosition.as_dict(o.source_axis), + 'circle_axis': self.origin.geometry.axis_module.AxisCirclePosition.as_dict(o.circle_axis), + 'complete_turn': str(Geometry.lift(self.origin.geometry, o.state)), + 'source_turn': str(Geometry.lift(self.origin.geometry, o.source_state))} for o in d.occurrences]} for d in self.definitions], 'order': list(self.order)} @@ -397,16 +397,16 @@ def _closed_definitions(origin: ByteOrigin, blocks: tuple[bytes, ...], for i in range(8)] definitions = [] for index, block in enumerate(blocks): - attachment = origin.byte_axis(uses[index][0][0]) + attachment = ByteOrigin.byte_axis(origin, uses[index][0][0]) occurrences = [] for start, circle, ordinal in uses[index]: - source_axis = origin.byte_axis(start) - local_axis = origin.geometry.axis(circle_origins[circle], len(slots[circle]), ordinal) + source_axis = ByteOrigin.byte_axis(origin, start) + local_axis = Geometry.axis(origin.geometry, circle_origins[circle], len(slots[circle]), ordinal) occurrences.append(Occurrence(start, circle, ordinal, source_axis, local_axis, - origin.geometry.placed(local_axis, spaces[circle]), - origin.geometry.placed(source_axis, spaces[circle]))) + Geometry.placed(origin.geometry, local_axis, spaces[circle]), + Geometry.placed(origin.geometry, source_axis, spaces[circle]))) definitions.append(ClosedSequence(block, attachment, - origin.geometry.placed(attachment, spaces[0]), tuple(occurrences))) + Geometry.placed(origin.geometry, attachment, spaces[0]), tuple(occurrences))) return tuple(definitions) diff --git a/research/weave/prime_schedule.py b/research/weave/prime_schedule.py index f8812f7..dea91ca 100644 --- a/research/weave/prime_schedule.py +++ b/research/weave/prime_schedule.py @@ -52,7 +52,7 @@ class Route: def evaluate(cls, path: PrimePath, limits: Limits = Limits(), *, engine=None): if type(path) is not PrimePath: raise Refused('PrimePath required') - trace = path.replay(limits, _engine=engine) + trace = PrimePath.replay(path, limits, _engine=engine) tokens = [path.seed, len(path.steps)] for step, value in zip(path.steps, trace[1:]): tokens.extend((0, value) if step == ('next',) else (1, *step[1:], value)) diff --git a/research/weave/test.py b/research/weave/test.py index 63a7dfb..afbaaf3 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -17,7 +17,7 @@ from assembly import Pipeline ROOT = Path(__file__).resolve().parent -EXPECTED_TESTS = 143 # 125 existing + 18 final-review closure regressions. +EXPECTED_TESTS = 152 # 143 existing + 9 final-findings regression methods. def snapshot(): @@ -63,7 +63,7 @@ def main(): 'cycle_sample_rounds': 3, 'native_occurrence_circles': 7, 'prime_jump_trace': [5381, 53, 241, 1523], 'native_origin_methods': 13, 'cycle_repair_methods': 12, - 'review_closure_methods': 18 + 'review_closure_methods': 18, 'final_findings_methods': 9 }, 'cycle': 'EXPLICIT_CANDIDATE: corpus normalization once; native sequence affixiation and prime-derived bit interleave; exact reverse', 'full_weave': Pipeline().plan(), diff --git a/research/weave/tests/test_final_findings.py b/research/weave/tests/test_final_findings.py new file mode 100644 index 0000000..c05bbe9 --- /dev/null +++ b/research/weave/tests/test_final_findings.py @@ -0,0 +1,181 @@ +# === CHECKS === +# id: byte_origin_dispatch_is_trusted +# proves: binary_sequence_closure, binary_coordinate_recovery +# call: self::test_byte_axis_shadow_cannot_change_closure +# id: profile_serialization_is_trusted +# proves: cycle_strict_refusal +# call: self::test_profile_and_round_serializers_cannot_change_identity +# id: capped_candidates_preserve_occurrences +# proves: discovery_repeat_selection +# call: self::test_capped_candidate_collects_all_disjoint_occurrences +# id: cycle_definitions_are_literal +# proves: cycle_discovery_profile +# call: self::test_cycle_rejects_recipe_backed_definition +# id: scheduler_replay_is_trusted +# proves: prime_split_replay +# call: self::test_scheduler_ignores_record_owned_replay +# === END CHECKS === +"""Five follow-up findings on PR #77; replay without network or user data. + +Usage: WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_final_findings.py -v +The closure/profile tests mutate supplied records, never trusted runtime classes. +The repetition tests exercise the declared selection rule, not a new compressor. +""" +from copy import copy +from dataclasses import replace +from fractions import Fraction +from itertools import product +from pathlib import Path +from unittest.mock import patch +import os +import unittest + +import affixiation +import cycle +import native_binary as native +import numeral +from prime_schedule import Route +from numeral import PrimePath, Refused, ResourceLimit, Limits + +ROOT = Path(__file__).resolve().parents[1] +SOURCES = Path(os.environ['WEAVE_SOURCES']) +SPACES = tuple(Fraction(i, 9) for i in range(8)) + + +class FinalFindingsTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.geometry = native.Geometry(SOURCES/'ucns') + + def profile(self): + return cycle.Profile.read((ROOT/'profiles/cycle-v1.json').read_bytes()) + + def test_byte_axis_shadow_cannot_change_closure(self): + origin = native.ByteOrigin(b'ABxABy', 'review', 0, self.geometry) + args = ((b'AB', b'x', b'y'), (0,1,0,2), (1,2,3), SPACES) + expected = native.close_sequences(origin, *args) + other = native.ByteOrigin(origin.source, 'other', 0, self.geometry) + called = [] + object.__setattr__(origin, 'byte_axis', lambda n: called.append(n) or other.byte_axis(n)) + actual = native.close_sequences(origin, *args) + self.assertEqual(actual.receipt(), expected.receipt()) + self.assertEqual(called, []) + self.assertTrue(all(d.attachment_axis.origin_sha256 == origin.identity for d in actual.definitions)) + + def test_forged_origin_axes_are_rejected_by_all_exports(self): + origin = native.ByteOrigin(b'ABxABy', 'review', 0, self.geometry) + table = native.close_sequences(origin,(b'AB',b'x',b'y'),(0,1,0,2),(1,2,3),SPACES) + other = native.ByteOrigin(origin.source, 'other', 0, self.geometry) + object.__setattr__(origin, 'byte_axis', other.byte_axis) + definitions = [] + for d in table.definitions: + axis = other.byte_axis(d.occurrences[0].source_offset) + occurrences = [] + for o in d.occurrences: + src = other.byte_axis(o.source_offset) + occurrences.append(replace(o,source_axis=src,source_state=self.geometry.placed(src,SPACES[o.circle]))) + definitions.append(replace(d,attachment_axis=axis,state=self.geometry.placed(axis,SPACES[0]),occurrences=tuple(occurrences))) + object.__setattr__(table,'definitions',tuple(definitions)) + for operation in (table.restore,table.receipt,table.wire_occurrences): + with self.subTest(operation=operation.__name__), self.assertRaises(native.BinaryError): + operation() + + def test_profile_and_round_serializers_cannot_change_identity(self): + for slot in ('profile','round'): + p = self.profile(); identity = p.identity; called = [] + target = p if slot == 'profile' else p.rounds[0] + object.__setattr__(target,'as_dict',lambda: called.append(1) or {'spoof':'unexecuted'}) + self.assertEqual(p.identity,identity) + self.assertEqual(called,[]) + wire,_ = cycle.forward(b'ABxABy',b'corpus',replace(p,rounds=p.rounds[:1]),SOURCES) + clean=replace(self.profile(),rounds=self.profile().rounds[:1]) + self.assertEqual(cycle.reverse(wire,b'corpus',clean,SOURCES),b'ABxABy') + + def test_profile_revalidates_mutated_record_fields(self): + cases = (('profile','bucket_bytes',0),('profile','corpus_bit_offset',True), + ('round','arities',(True,)),('round','end_order','unknown'), + ('round','circle_order',(1,)*7),('round','spaces',(Fraction(0),)*7), + ('path','seed',True),('path','steps',(('span',True,0,1),))) + for slot,name,value in cases: + p=self.profile(); target = p if slot=='profile' else p.rounds[0] if slot=='round' else p.rounds[0].path + object.__setattr__(target,name,value) + object.__setattr__(target,'__post_init__',lambda:None) + with self.subTest(slot=slot,name=name),self.assertRaises(Refused): + _ = p.identity + + def test_scheduler_ignores_record_owned_replay(self): + path=PrimePath(53,(('next',),));expected=Route.evaluate(path);called=[] + object.__setattr__(path,'replay',lambda *a,**k:called.append(1) or (53,999)) + self.assertEqual(Route.evaluate(path),expected) + self.assertEqual(called,[]) + object.__setattr__(path,'seed',4) + with self.assertRaises(Refused):Route.evaluate(path) + self.assertEqual(called,[]) + + def test_scheduler_shadow_cannot_bypass_work_budget(self): + path=PrimePath(101,());called=[] + object.__setattr__(path,'replay',lambda *a,**k:called.append(1) or (101,)) + with self.assertRaises(ResourceLimit):Route.evaluate(path,Limits(prime_work=1)) + self.assertEqual(called,[]) + + def test_capped_candidate_collects_all_disjoint_occurrences(self): + p=affixiation.discover(b'\x00\x01\x00\x01\x00\x01\x01') + self.assertEqual(p.blocks,(b'\x00\x01',b'\x01')) + self.assertEqual(p.order,(0,0,0,1)) + self.assertEqual(p.starts,(0,2,4,6)) + self.assertEqual(p.restore(),b'\x00\x01\x00\x01\x00\x01\x01') + + def test_capped_suffix_intervals_match_full_prefix_occurrences(self): + # Check the existing LCP-derived candidate family, not all substring sizes. + # The oracle expands each candidate with byte comparisons and checks the + # leftmost nonoverlapping result under the declared longest-first rule. + for n in range(1,10): + for values in product(range(2),repeat=n): + data=bytes(values);suffixes=affixiation._suffix_array(data) + common=affixiation._lcp(data,suffixes);stack=[];patterns=set() + for i in range(1,n+1): + depth=common[i] if idepth: + size,begin=stack.pop();positions=suffixes[begin:i] + size=min(size,max(positions)-min(positions)) + if size>=2:patterns.add(data[positions[0]:positions[0]+size]) + left=begin + if depth and (not stack or stack[-1][0]=end and not used.intersection(range(start,start+len(block))): + selected.append(start);end=start+len(block) + if len(selected)>=2: + for start in selected: + used.update(range(start,start+len(block)));chosen.append((start,len(block))) + result=affixiation.discover(data) + for start,size in chosen: + self.assertIn(start,result.starts,(data,start,size,result)) + self.assertEqual(result.blocks[result.order[result.starts.index(start)]],data[start:start+size]) + self.assertEqual(result.restore(),data) + + def test_cycle_rejects_recipe_backed_definition(self): + source=b'\x00eX\x00eY';p=self.profile() + args=dict(api=native,geometry=self.geometry,scope=p.scope, + root=native.ByteOrigin(source,p.scope,0,self.geometry).message_origin, + round_id=0,spec=p.rounds[0]) + wire,_=cycle.affix(source,**args);r=numeral._Reader(wire) + magic=r.take(4);size=r.uint();packet=numeral.decode(r.blob(cycle.CycleLimits().round_bytes)) + tail=wire[r.pos:];entries=[];changed=0 + for e in packet.entries: + if numeral.BitBlock.to_bytes(e.block)==b'\x00e': + e=replace(e,recipe=PrimePath(101,()));changed+=1 + entries.append(e) + self.assertEqual(changed,1) + variant=replace(packet,entries=tuple(entries)) + self.assertEqual(numeral.decode(numeral.encode(variant)).restore(),packet.restore()) + altered=magic+numeral._uint(size)+numeral._blob(numeral.encode(variant))+tail + with self.assertRaisesRegex(Refused,'literal|recipe'): + cycle.unaffix(altered,**args) + self.assertEqual(cycle.unaffix(wire,**args),source) + + +if __name__=='__main__':unittest.main() From 649e31284973de421f1d61a9be2fb8f17d742264 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 5 Oct 2026 23:25:45 -0700 Subject: [PATCH 12/15] fix(weave): validate canonical coordinates and replay-bound split routes Repair all three P2 findings at 7650833: canonical Fraction fields before serialization/geometric work; complete Route replay and determinant validation before planning, with one scheduler engine through each cycle; trusted Geometry recovery, axis creation and coordinate replay. Nine regression methods are red on the prior head and green after repair. Full local suite: 161 passed, zero failures/errors/skips, source unchanged. Runtime/source/profile/native-lock SHA-256 0724f421ff8404b0c7585113dd5ae498692a8dd5790b9e270073d0e0b5632ecc. The 6735-byte full-cost three-round demo still recovers exactly. All uploaded files match tested bytes. Hosted checks and fresh terminal review remain separate gates. UCNS pins, UCHC architecture reference, Stack ownership and cryptographic standing are unchanged. Native source lock rotates; prior bound research records require regeneration. --- research/weave/CYCLE.md | 36 +++- research/weave/CYCLE_NATIVE.json | 2 +- research/weave/cycle.py | 21 +-- research/weave/native_binary.py | 48 +++-- research/weave/numeral.py | 14 +- research/weave/prime_schedule.py | 28 ++- research/weave/test.py | 5 +- research/weave/tests/test_terminal_records.py | 168 ++++++++++++++++++ 8 files changed, 284 insertions(+), 38 deletions(-) create mode 100644 research/weave/tests/test_terminal_records.py diff --git a/research/weave/CYCLE.md b/research/weave/CYCLE.md index 708b185..1e6bb13 100644 --- a/research/weave/CYCLE.md +++ b/research/weave/CYCLE.md @@ -162,7 +162,7 @@ and malformed-input versus resource-limit classification. Three new regressions bring its suite to 18. The obsolete bit-per-circle modules and their 22 tests are removed, with these sequence/native operations as replacement. Fifty-two original transport/assembly tests retain their original limited scope. Full Weave inventory: -52 + 18 + 30 + 13 native-origin + 12 repair + 18 review-closure + 9 follow-up regressions = 152. The original +52 + 18 + 30 + 13 native-origin + 12 repair + 18 review-closure + 9 follow-up + 9 terminal-record regressions = 161. The original 13 native producer tests are retained in Stack as `tests/test_binary_origin.py`. The repair regressions exercise forged closed records, fresh verified module loading, atomic failed-write cleanup, empty-input attachments and manifest edges. @@ -257,3 +257,37 @@ WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_final_find All 152 tests pass on the repaired local source. The three-round demo still costs 6,735 total bytes and recovers exactly. These repairs change neither the UCNS producer pins nor the ownership or cryptographic standing of the construction. + +## Canonical rational, route and inverse validation + +The three terminal-review findings at `76508332f353` are exercised by +`tests/test_terminal_records.py`. Numeral attachments validate the exact integer +numerator and positive denominator and compare them with a newly normalized +Fraction before serialization. Altering an exact Fraction object's internals no +longer produces a record the canonical decoder would reject. Native coordinate +and space inputs receive the same canonical-field check before geometric work. + +`plan` validates a supplied Route by re-executing its PrimePath with trusted +methods and comparing the typed trace and determinant. Direct construction and +mutation do not create an evaluated route merely through the record's class. +Its optional `limits` and `engine` parameters account for this actual replay. +Both cycle directions share one scheduler engine across initial evaluation and +all subsequent route validations. The aggregate work allowance is not reset; +a workload that formerly fit only because revalidation was omitted may now +refuse within its declared budget. The split formula and valid results are unchanged. + +Native inverse recovery, its axis construction and coordinate replay call trusted +Geometry methods. The cycle's corresponding geometric exports do so as well. +Supplying replacement instance methods cannot license arbitrary coordinates. This +remains a data-record validation boundary, not protection against modification of +trusted Python classes or producer modules. + +Run these nine regressions with: + +```sh +WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_terminal_records.py -v +``` + +The native source lock rotates with this repair. Regenerate records tied to the +previous lock rather than treating them as evidence for the new source. UCNS +producer source, UCHC architectural reference and Stack ownership are unchanged. diff --git a/research/weave/CYCLE_NATIVE.json b/research/weave/CYCLE_NATIVE.json index 223509e..0997b01 100644 --- a/research/weave/CYCLE_NATIVE.json +++ b/research/weave/CYCLE_NATIVE.json @@ -8,7 +8,7 @@ "binary_source": { "owner": "The-Interdependency/stack", "path": "native_binary.py", - "git_blob": "5a49a4f43ab2baefbf5beda2e17cb3ba131c4b15", + "git_blob": "6a6ba9f57b83b62116a57113a5c6358fab7e7831", "standing": "forge-owned candidate, not graduated" }, "uchc_architecture": { diff --git a/research/weave/cycle.py b/research/weave/cycle.py index e6725ae..7f262f2 100644 --- a/research/weave/cycle.py +++ b/research/weave/cycle.py @@ -318,9 +318,9 @@ def affix(data: bytes, *, api, geometry, scope: str, root: str, round_id: int, origin = api.ByteOrigin(data,scope,round_id,geometry,root) circles = tuple(spec.circle_order[i%7] for i in range(len(partition.blocks))) table = api.close_sequences(origin,partition.blocks,partition.order,circles,spec.spaces) - entries = tuple(Entry(_symbol(i),BitBlock.from_bytes(d.data),geometry.lift(d.state),circles[i]) + entries = tuple(Entry(_symbol(i),BitBlock.from_bytes(d.data),api.Geometry.lift(geometry,d.state),circles[i]) for i,d in enumerate(table.definitions)) - wire_items = table.wire_occurrences() + wire_items = api.SequenceTable.wire_occurrences(table) symbols = ''.join(entries[i].symbol for i,_ in wire_items) packet = Packet(origin.identity,round_id,entries,symbols) encoded = encode_numeral(packet,limits.numeral()) @@ -329,7 +329,7 @@ def affix(data: bytes, *, api, geometry, scope: str, root: str, round_id: int, for count in counts: out.extend(_uint(count)) for _,occurrence in wire_items: - position = geometry.lift(occurrence.source_state) + position = api.Geometry.lift(geometry,occurrence.source_state) out.extend(_uint(position.numerator)+_uint(position.denominator)) if len(out) > limits.round_bytes: raise ResourceLimit('affixiation coordinates exceed round byte budget') @@ -389,9 +389,9 @@ def unaffix(data: bytes, *, api, geometry, scope: str, root: str, round_id: int, blocks=tuple(e.block.to_bytes() for e in packet.entries),circles=circles,spaces=spec.spaces, counts=counts,wire_order=tuple(ids[s] for s in packet.symbols),source_turns=positions, max_bytes=limits.round_bytes) - if tuple(geometry.lift(d.state) for d in table.definitions) != tuple(e.angle for e in packet.entries): + if tuple(api.Geometry.lift(geometry,d.state) for d in table.definitions) != tuple(e.angle for e in packet.entries): raise Refused('whole-circle sequence attachments do not reconstruct') - restored = table.restore() + restored = api.SequenceTable.restore(table) selected = discover(restored, max_bytes=limits.round_bytes, visit_budget=limits.discovery_visits) if (selected.blocks != tuple(d.data for d in table.definitions) @@ -405,7 +405,8 @@ def _prepared(profile: Profile, limits: CycleLimits): if len(profile.rounds) > limits.rounds: raise ResourceLimit('round count exceeds execution budget') engine = _Primes(limits.numeral()) - return tuple(Route.evaluate(s.path,limits.numeral(),engine=engine) for s in profile.rounds) + routes = tuple(Route.evaluate(s.path,limits.numeral(),engine=engine) for s in profile.rounds) + return routes, engine def _native_identity() -> bytes: @@ -415,7 +416,7 @@ def _native_identity() -> bytes: def forward(message: bytes, corpus: bytes, profile: Profile, sources: str | Path, limits: CycleLimits = CycleLimits()): """Run the stated cycle; return its full research record and size-only report.""" - routes = _prepared(profile,limits) + routes, engine = _prepared(profile,limits) api,geometry = load_native(sources) data = normalize(message,corpus,profile,limits) root = api.ByteOrigin(data,profile.scope,0,geometry).message_origin @@ -424,7 +425,7 @@ def forward(message: bytes, corpus: bytes, profile: Profile, sources: str | Path for i,(spec,route) in enumerate(zip(profile.rounds,routes)): packed,stats = affix(data,api=api,geometry=geometry,scope=profile.scope,root=root, round_id=i,spec=spec,limits=limits) - split = plan(len(packed)*8,route,spec.arities) + split = plan(len(packed)*8,route,spec.arities,limits.numeral(),engine=engine) data = interleave(packed,split,end_order=spec.end_order) rows.append({'round':i,**stats,'permutation_bit_count':split.bit_length}) header = MAGIC + _native_identity() + profile.identity + bytes.fromhex(root) + _uint(len(rows)) @@ -445,7 +446,7 @@ def reverse(wire: bytes, corpus: bytes, profile: Profile, sources: str | Path, raise Refused('cycle record must be bytes') if len(wire) > limits.round_bytes+128: raise ResourceLimit('cycle record exceeds final byte budget') - routes = _prepared(profile,limits) + routes, engine = _prepared(profile,limits) api,geometry = load_native(sources) reader = _Reader(wire) if reader.take(4) != MAGIC: @@ -460,7 +461,7 @@ def reverse(wire: bytes, corpus: bytes, profile: Profile, sources: str | Path, raise Refused('trailing cycle bytes') for i in range(len(profile.rounds)-1,-1,-1): spec,route = profile.rounds[i],routes[i] - split = plan(len(data)*8,route,spec.arities) + split = plan(len(data)*8,route,spec.arities,limits.numeral(),engine=engine) packed = interleave(data,split,inverse=True,end_order=spec.end_order) data = unaffix(packed,api=api,geometry=geometry,scope=profile.scope,root=root, round_id=i,spec=spec,limits=limits) diff --git a/research/weave/native_binary.py b/research/weave/native_binary.py index 5a49a4f..6a6ba9f 100644 --- a/research/weave/native_binary.py +++ b/research/weave/native_binary.py @@ -90,10 +90,18 @@ def _natural(value, name: str) -> int: def _fraction_fields(value: Fraction) -> tuple[int, int]: """Extract builtin fields only; hostile equality must never authorize a record.""" - if (type(value) is not Fraction or type(value.numerator) is not int - or type(value.denominator) is not int or value.denominator <= 0): + if type(value) is not Fraction: + raise BinaryError('native coordinate requires an exact Fraction') + try: + numerator, denominator = value.numerator, value.denominator + except AttributeError as exc: + raise BinaryError('native coordinate is missing rational fields') from exc + if type(numerator) is not int or type(denominator) is not int or denominator <= 0: raise BinaryError('native coordinate requires exact Fraction fields') - return value.numerator, value.denominator + normalized = Fraction(numerator, denominator) + if (normalized.numerator, normalized.denominator) != (numerator, denominator): + raise BinaryError('native coordinate requires canonical Fraction fields') + return numerator, denominator def _sha(value: str) -> str: @@ -140,21 +148,21 @@ def axis(self, origin_sha256: str, count: int, ordinal: int): origin_sha256=origin_sha256, axis_count=count, axis_ordinal=ordinal) def placed(self, axis, space: Fraction): - if type(space) is not Fraction: - raise BinaryError('space relationship must be an exact Fraction') + _fraction_fields(space) + Geometry._axis_fields(self, axis) return self.mobius_module.native_mobius_state(axis.turn - space) def recover_axis(self, origin: str, count: int, position: Fraction, space: Fraction): """Invert the native frame displacement and recover its exact source axis.""" - if type(position) is not Fraction or not 0 <= position < 2: + numerator, denominator = _fraction_fields(position) + if not 0 <= numerator < 2*denominator: raise BinaryError('canonical complete position in [0,2) required') - if type(space) is not Fraction: - raise BinaryError('exact Fraction space relationship required') + _fraction_fields(space) state = self.mobius_module.native_mobius_state(position).advance(space) ordinal = state.phase_turns * count if state.frame.sign != 1 or ordinal.denominator != 1: raise BinaryError('position does not lift to a source byte axis') - return self.axis(origin, count, ordinal.numerator) + return Geometry.axis(self, origin, count, ordinal.numerator) def _axis_fields(self, axis) -> tuple: """Inspect the exact class from this verified producer instance, then fields. @@ -183,13 +191,17 @@ def _state_fields(self, state) -> tuple: frame = 1 else: raise BinaryError('state must carry this Geometry native frame') - return _fraction_fields(state.phase_turns), frame + phase = _fraction_fields(state.phase_turns) + if not 0 <= phase[0] < phase[1]: + raise BinaryError('native state phase is not canonical') + return phase, frame except AttributeError as exc: raise BinaryError('native state is missing required fields') from exc def lift(self, state) -> Fraction: """Serialize the native complete frame in [0,2), not just visible phase.""" - return state.phase_turns + (1 if state.frame.sign < 0 else 0) + phase, frame = Geometry._state_fields(self, state) + return Fraction(*phase) + frame @dataclass(frozen=True) @@ -369,8 +381,8 @@ def _closed_definitions(origin: ByteOrigin, blocks: tuple[bytes, ...], raise BinaryError('immutable partition inputs required') if len(circles) != len(blocks) or len(spaces) != 8: raise BinaryError('one circle per definition and exactly eight space relationships required') - if any(type(s) is not Fraction for s in spaces): - raise BinaryError('exact Fraction space relationships required') + for space in spaces: + _fraction_fields(space) if any(type(b) is not bytes or not b for b in blocks) or len(set(blocks)) != len(blocks): raise BinaryError('definitions must be unique nonempty byte sequences') if any(type(c) is not int or not 1 <= c <= 7 for c in circles): @@ -450,8 +462,8 @@ def recover_sequences(geometry: Geometry, *, scope: str, message_origin: str, raise BinaryError('nonempty byte definitions required') if any(type(c) is not int or not 1 <= c <= 7 for c in circles): raise BinaryError('occurrence circle must be 1..7') - if any(type(s) is not Fraction for s in spaces): - raise BinaryError('exact space relationships required') + for space in spaces: + _fraction_fields(space) if any(type(n) is not int or n < 0 for n in counts): raise BinaryError('nonnegative circle occurrence counts required') if sum(counts) != len(wire_order) or len(source_turns) != len(wire_order): @@ -464,7 +476,7 @@ def recover_sequences(geometry: Geometry, *, scope: str, message_origin: str, index, turn = wire_order[cursor], source_turns[cursor] if type(index) is not int or not 0 <= index < len(blocks) or circles[index] != circle: raise BinaryError('reference is not on its declared occurrence circle') - axis = geometry.recover_axis(origin_identity, byte_length, turn, spaces[circle]) + axis = Geometry.recover_axis(geometry, origin_identity, byte_length, turn, spaces[circle]) positions.append((axis.axis_ordinal, index)) total += len(blocks[index]) if total > byte_length: @@ -484,8 +496,8 @@ def recover_sequences(geometry: Geometry, *, scope: str, message_origin: str, if origin.identity != origin_identity: raise BinaryError('reconstructed origin identity differs') table = close_sequences(origin, blocks, tuple(order), circles, spaces) - replay = table.wire_occurrences() - if tuple(i for i, _ in replay) != wire_order or tuple(geometry.lift(o.source_state) for _, o in replay) != source_turns: + replay = SequenceTable.wire_occurrences(table) + if tuple(i for i, _ in replay) != wire_order or tuple(Geometry.lift(geometry, o.source_state) for _, o in replay) != source_turns: raise BinaryError('circle streams do not replay in canonical geometric order') return table diff --git a/research/weave/numeral.py b/research/weave/numeral.py index eca3f40..9aee98f 100644 --- a/research/weave/numeral.py +++ b/research/weave/numeral.py @@ -223,8 +223,18 @@ def _symbol(symbol: str) -> None: def _validate_attachment(angle: Fraction, circle: int) -> None: - if type(angle) is not Fraction or not 0 <= angle < 2: - raise Refused("exact supplied angle must be in [0,2) turns") + if type(angle) is not Fraction: + raise Refused("exact supplied angle must be a Fraction") + try: + numerator, denominator = angle.numerator, angle.denominator + except AttributeError as exc: + raise Refused("angle is missing its rational fields") from exc + if (type(numerator) is not int or type(denominator) is not int + or denominator <= 0 or not 0 <= numerator < 2*denominator): + raise Refused("exact angle fields must describe [0,2) turns") + canonical_angle = Fraction(numerator, denominator) + if (canonical_angle.numerator, canonical_angle.denominator) != (numerator, denominator): + raise Refused("angle fraction must have canonical fields") if type(circle) is not int or not 1 <= circle <= 7: raise Refused("occurrence circle must be one of the seven") diff --git a/research/weave/prime_schedule.py b/research/weave/prime_schedule.py index dea91ca..667ff6a 100644 --- a/research/weave/prime_schedule.py +++ b/research/weave/prime_schedule.py @@ -48,6 +48,23 @@ class Route: trace: tuple[int, ...] determinant: int + def validate(self, limits: Limits = Limits(), *, engine=None) -> int: + """Re-execute the claimed relation before planning from a supplied record. + + The optional engine is trusted execution context. Cycle callers share it + with initial evaluation and every round, so validation cannot reset the + scheduler's aggregate work allowance. No record-owned method is invoked. + """ + if (type(self) is not Route or type(self.path) is not PrimePath + or type(self.trace) is not tuple or not self.trace + or any(type(v) is not int or v < 2 for v in self.trace) + or type(self.determinant) is not int or self.determinant < 1): + raise Refused('route requires exact path, trace and determinant fields') + expected = Route.evaluate(self.path, limits, engine=engine) + if self.trace != expected.trace or self.determinant != expected.determinant: + raise Refused('route fields do not reproduce the claimed prime path') + return expected.determinant + @classmethod def evaluate(cls, path: PrimePath, limits: Limits = Limits(), *, engine=None): if type(path) is not PrimePath: @@ -94,7 +111,9 @@ def _composition(n: int, arity: int, rank: int) -> tuple[int, ...]: return tuple(b-a for a,b in zip(points, points[1:])) -def plan(bit_length: int, route: Route, arities: tuple[int, ...]) -> SplitPlan: +def plan(bit_length: int, route: Route, arities: tuple[int, ...], + limits: Limits = Limits(), *, engine=None) -> SplitPlan: + """Validate the route relation under the supplied budget, then derive splits.""" if type(bit_length) is not int or bit_length < 1: raise Refused('positive bit length required') if type(route) is not Route: @@ -103,8 +122,9 @@ def plan(bit_length: int, route: Route, arities: tuple[int, ...]) -> SplitPlan: or any(type(a) is not int or not 2 <= a <= min(64,bit_length) for a in arities) or len(set(arities)) != len(arities)): raise Refused('distinct admissible arities must be integers 2..min(64, bit_length)') - arity = arities[route.determinant % len(arities)] - rank = (route.determinant//len(arities)) % comb(bit_length-1, arity-1) + determinant = Route.validate(route, limits, engine=engine) + arity = arities[determinant % len(arities)] + rank = (determinant//len(arities)) % comb(bit_length-1, arity-1) return SplitPlan(bit_length, _composition(bit_length, arity, rank), rank) @@ -113,7 +133,7 @@ def interleave(data: bytes, split: SplitPlan, *, inverse: bool = False, """Rearrange bits; do not reinterpret the output as bytes until the pass ends.""" if type(data) is not bytes or type(split) is not SplitPlan: raise Refused('bytes and SplitPlan required') - if type(inverse) is not bool or end_order not in ('first-last','last-first'): + if type(inverse) is not bool or type(end_order) is not str or end_order not in ('first-last','last-first'): raise Refused('explicit Boolean inverse and supported end order required') if (type(split.bit_length) is not int or split.bit_length != len(data)*8 or type(split.lengths) is not tuple or not split.lengths diff --git a/research/weave/test.py b/research/weave/test.py index afbaaf3..1c542d7 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -17,7 +17,7 @@ from assembly import Pipeline ROOT = Path(__file__).resolve().parent -EXPECTED_TESTS = 152 # 143 existing + 9 final-findings regression methods. +EXPECTED_TESTS = 161 # 152 existing + 9 terminal-record regression methods. def snapshot(): @@ -63,7 +63,8 @@ def main(): 'cycle_sample_rounds': 3, 'native_occurrence_circles': 7, 'prime_jump_trace': [5381, 53, 241, 1523], 'native_origin_methods': 13, 'cycle_repair_methods': 12, - 'review_closure_methods': 18, 'final_findings_methods': 9 + 'review_closure_methods': 18, 'final_findings_methods': 9, + 'terminal_record_methods': 9 }, 'cycle': 'EXPLICIT_CANDIDATE: corpus normalization once; native sequence affixiation and prime-derived bit interleave; exact reverse', 'full_weave': Pipeline().plan(), diff --git a/research/weave/tests/test_terminal_records.py b/research/weave/tests/test_terminal_records.py new file mode 100644 index 0000000..0d090a5 --- /dev/null +++ b/research/weave/tests/test_terminal_records.py @@ -0,0 +1,168 @@ +# === CHECKS === +# id: numeral_fraction_internals_are_canonical +# proves: numeral_exact_recovery, numeral_strict_input +# call: self::test_numeral_rejects_mutated_fraction_fields +# id: route_relation_is_replayed_before_planning +# proves: prime_split_replay +# call: self::test_plan_rejects_inconsistent_direct_and_mutated_routes +# id: scheduler_validation_budget_is_shared +# proves: prime_split_replay, cycle_strict_refusal +# call: self::test_route_planning_uses_one_charged_engine +# id: recovery_uses_trusted_geometry +# proves: binary_coordinate_recovery, binary_source_refusal +# call: self::test_recovery_cannot_use_shadowed_geometry_to_accept_coordinates +# === END CHECKS === +"""Replay three terminal-review gaps using only nonsecret local records. + +Usage: WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_terminal_records.py -v +These tests mutate supplied records, not trusted producer code or runtime classes. +""" +from dataclasses import replace +from fractions import Fraction +from pathlib import Path +from unittest.mock import patch +import os +import unittest + +import cycle +import native_binary as native +import numeral +from numeral import BitBlock, Entry, Packet, PrimePath, Limits, Refused, ResourceLimit +from prime_schedule import Route, plan + +ROOT = Path(__file__).resolve().parents[1] +SOURCES = Path(os.environ['WEAVE_SOURCES']) +SPACES = tuple(Fraction(i, 9) for i in range(8)) + + +def bad_fraction(numerator, denominator): + value = Fraction(1, 2) + object.__setattr__(value, '_numerator', numerator) + object.__setattr__(value, '_denominator', denominator) + return value + + +class TerminalRecordTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.geometry = native.Geometry(SOURCES/'ucns') + + def table(self, geometry): + origin = native.ByteOrigin(b'ABxABy', 'terminal', 0, geometry) + return native.close_sequences(origin, (b'AB', b'x', b'y'), (0,1,0,2), (1,2,3), SPACES) + + def recovery(self, table): + items = native.SequenceTable.wire_occurrences(table) + return dict(scope=table.origin.scope, message_origin=table.origin.message_origin, + round_id=0, origin_identity=table.origin.identity, byte_length=6, + blocks=tuple(d.data for d in table.definitions), circles=(1,2,3), + spaces=SPACES, counts=tuple(sum(o.circle == c for _,o in items) for c in range(1,8)), + wire_order=tuple(i for i,_ in items), + source_turns=tuple(native.Geometry.lift(table.origin.geometry,o.source_state) for _,o in items)) + + def test_numeral_rejects_mutated_fraction_fields(self): + class EqualInt(int): + def __eq__(self, other): return True + for n,d in ((2,2),(0,2),(-1,-2),(1,0),(True,2),(1,EqualInt(2)),(1.0,2)): + angle = bad_fraction(n,d) + p = Packet('fraction',0,(Entry(chr(0xE000),BitBlock(7,8),angle,1),),chr(0xE000)) + for operation in (numeral.encode,numeral.accounting,Packet.restore,Packet.occurrences): + with self.subTest(n=n,d=d,operation=operation.__name__), self.assertRaises(Refused): + operation(p) + for angle in (Fraction(0),Fraction(1,2),Fraction(3,2)): + p=Packet('valid',0,(Entry(chr(0xE000),BitBlock(7,8),angle,1),),chr(0xE000)) + self.assertEqual(numeral.decode(numeral.encode(p)),p) + + def test_native_rejects_noncanonical_fraction_inputs(self): + g = self.geometry + axis = native.Geometry.axis(g,'0'*64,8,0) + for value in (bad_fraction(2,2),bad_fraction(0,2),bad_fraction(1,0),bad_fraction(True,2)): + with self.subTest(operation='placed'),self.assertRaises(native.BinaryError): + native.Geometry.placed(g,axis,value) + with self.subTest(operation='recover'),self.assertRaises(native.BinaryError): + native.Geometry.recover_axis(g,'0'*64,8,value,Fraction(0)) + + def test_plan_rejects_inconsistent_direct_and_mutated_routes(self): + path=PrimePath(53,(('next',),));good=Route.evaluate(path) + bad=(Route(path,good.trace,0), Route(path,(53,239),good.determinant), + Route(PrimePath(59,()),good.trace,good.determinant), + Route(path,list(good.trace),good.determinant), + Route(path,good.trace,True)) + for value in bad: + object.__setattr__(value,'validate',lambda *a,**k: good.determinant) + with self.subTest(value=value),self.assertRaises(Refused):plan(128,value,(3,5,7)) + mutated=Route.evaluate(path) + object.__setattr__(mutated,'determinant',0) + with self.assertRaises(Refused):plan(128,mutated,(3,5,7)) + self.assertEqual(plan(128,Route(path,good.trace,good.determinant),(3,5,7)),plan(128,good,(3,5,7))) + + def test_plan_rejects_spoofed_trace_scalars_and_replays_actual_path(self): + class EqualInt(int): + def __eq__(self, other):return True + route=Route.evaluate(PrimePath(101,())) + for changed in (replace(route,trace=(EqualInt(101),)),replace(route,determinant=EqualInt(route.determinant))): + with self.assertRaises(Refused):plan(128,changed,(3,5,7)) + called=[] + object.__setattr__(route.path,'replay',lambda *a,**k: called.append(1) or (101,)) + object.__setattr__(route.path,'seed',4) + with self.assertRaises(Refused):plan(128,route,(3,5,7)) + self.assertEqual(called,[]) + + def test_route_planning_uses_one_charged_engine(self): + # Seed 101 costs one step plus four trial divisors: five units per replay. + for budget in (9,10): + limits=Limits(prime_work=budget);engine=numeral._Primes(limits) + route=Route.evaluate(PrimePath(101,()),limits,engine=engine) + self.assertEqual(engine.work,5) + if budget==9: + with self.assertRaises(ResourceLimit):plan(128,route,(3,5,7),limits=limits,engine=engine) + else: + result=plan(128,route,(3,5,7),limits=limits,engine=engine) + self.assertEqual(sum(result.lengths),128) + self.assertEqual(engine.work,10) + + def test_cycle_shares_route_validation_work_across_evaluation_and_plan(self): + p=cycle.Profile.read((ROOT/'profiles/cycle-v1.json').read_bytes()) + spec=replace(p.rounds[0],path=PrimePath(101,())) + p=replace(p,rounds=(spec,)) + with self.assertRaises(ResourceLimit): + cycle.forward(b'ABxABy',b'corpus',p,SOURCES,cycle.CycleLimits(prime_work=9)) + wire,_=cycle.forward(b'ABxABy',b'corpus',p,SOURCES,cycle.CycleLimits(prime_work=10)) + with self.assertRaises(ResourceLimit): + cycle.reverse(wire,b'corpus',p,SOURCES,cycle.CycleLimits(prime_work=9)) + self.assertEqual(cycle.reverse(wire,b'corpus',p,SOURCES,cycle.CycleLimits(prime_work=10)),b'ABxABy') + + def test_recovery_cannot_use_shadowed_geometry_to_accept_coordinates(self): + geometry=native.Geometry(SOURCES/'ucns');table=self.table(geometry);args=self.recovery(table) + items=native.SequenceTable.wire_occurrences(table) + expected_axes=iter(o.source_axis for _,o in items);called=[] + object.__setattr__(geometry,'recover_axis',lambda *a,**k: called.append('recover') or next(expected_axes)) + object.__setattr__(geometry,'lift',lambda state: called.append('lift') or Fraction(1,16)) + args['source_turns']=(Fraction(1,16),)*len(items) + with self.assertRaises(native.BinaryError):native.recover_sequences(geometry,**args) + self.assertEqual(called,[]) + good=self.recovery(table) + self.assertEqual(native.recover_sequences(geometry,**good).restore(),b'ABxABy') + self.assertEqual(called,[]) + + def test_recovery_axis_constructor_ignores_instance_override(self): + geometry=native.Geometry(SOURCES/'ucns');called=[] + object.__setattr__(geometry,'axis',lambda *a,**k: called.append(1) or None) + result=native.Geometry.recover_axis(geometry,'0'*64,8,Fraction(3,8),Fraction(0)) + self.assertEqual(result.axis_ordinal,3) + self.assertEqual(result.origin_sha256,'0'*64) + self.assertEqual(called,[]) + + def test_cycle_uses_trusted_lift_for_supplied_geometry(self): + geometry=native.Geometry(SOURCES/'ucns');p=cycle.Profile.read((ROOT/'profiles/cycle-v1.json').read_bytes()) + data=b'ABxABy';root=native.ByteOrigin(data,p.scope,0,geometry).message_origin + args=dict(api=native,geometry=geometry,scope=p.scope,root=root,round_id=0,spec=p.rounds[0]) + expected,_=cycle.affix(data,**args);called=[] + object.__setattr__(geometry,'lift',lambda *a: called.append(1) or Fraction(0)) + actual,_=cycle.affix(data,**args) + self.assertEqual(actual,expected) + self.assertEqual(cycle.unaffix(actual,**args),data) + self.assertEqual(called,[]) + + +if __name__=='__main__':unittest.main() From 8bc5e1f7ce7f678fddde05e5b90c0aa42020f72c Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 6 Oct 2026 03:13:39 -0700 Subject: [PATCH 13/15] fix(weave): reject invalid outer framing before expensive recovery Repair final review findings at 649e312: parse complete bounded framing and match source/profile identities and round count before scheduler replay or native-source execution. Keep valid reverse operations and the existing aggregate scheduler budget unchanged. Three regressions cover every truncated prefix, wrong magic, identity/count/length mismatch, noncanonical integers, trailing bytes, and valid-header budget enforcement. Invalid-header regressions fail on 649e312 and pass after repair. Full local suite: 164 passed, zero failures/errors/skips, source unchanged. Runtime/test/profile/native-lock SHA-256: 9f0b8812dc3b181ab0fe72780f7d0254bf2d9db7343f7fefdd7526715bfd8628. Demo remains 6735 complete bytes with exact recovery. Update evidence inventory to 164 and explicitly mark 152 as a historical checkpoint. All four uploaded blobs match tested local bytes. No source-lock, wire format, profile, geometry, ownership or cryptographic-standing change. Hosted exact-head checks and terminal review remain separate gates. --- research/weave/CYCLE.md | 32 ++++++++-- research/weave/cycle.py | 18 ++++-- research/weave/test.py | 4 +- research/weave/tests/test_terminal_records.py | 64 +++++++++++++++++++ 4 files changed, 105 insertions(+), 13 deletions(-) diff --git a/research/weave/CYCLE.md b/research/weave/CYCLE.md index 1e6bb13..951c155 100644 --- a/research/weave/CYCLE.md +++ b/research/weave/CYCLE.md @@ -162,7 +162,7 @@ and malformed-input versus resource-limit classification. Three new regressions bring its suite to 18. The obsolete bit-per-circle modules and their 22 tests are removed, with these sequence/native operations as replacement. Fifty-two original transport/assembly tests retain their original limited scope. Full Weave inventory: -52 + 18 + 30 + 13 native-origin + 12 repair + 18 review-closure + 9 follow-up + 9 terminal-record regressions = 161. The original +52 + 18 + 30 + 13 native-origin + 12 repair + 18 review-closure + 9 follow-up + 9 terminal-record + 3 header-admission regressions = 164. The original 13 native producer tests are retained in Stack as `tests/test_binary_origin.py`. The repair regressions exercise forged closed records, fresh verified module loading, atomic failed-write cleanup, empty-input attachments and manifest edges. @@ -254,9 +254,9 @@ Run the added regressions with: WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_final_findings.py -v ``` -All 152 tests pass on the repaired local source. The three-round demo still costs -6,735 total bytes and recovers exactly. These repairs change neither the UCNS -producer pins nor the ownership or cryptographic standing of the construction. +The earlier follow-up checkpoint passed its then-current 152-test suite. The +three-round demo still costs 6,735 total bytes and recovers exactly. These repairs +change neither the UCNS producer pins nor the ownership or cryptographic standing of the construction. ## Canonical rational, route and inverse validation @@ -282,7 +282,7 @@ Supplying replacement instance methods cannot license arbitrary coordinates. Thi remains a data-record validation boundary, not protection against modification of trusted Python classes or producer modules. -Run these nine regressions with: +Run the terminal-record and header-admission regressions with: ```sh WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_terminal_records.py -v @@ -291,3 +291,25 @@ WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_terminal_r The native source lock rotates with this repair. Regenerate records tied to the previous lock rather than treating them as evidence for the new source. UCNS producer source, UCHC architectural reference and Stack ownership are unchanged. + +## Outer-header admission before expensive recovery + +Recovery parses the complete outer frame first: magic, fixed identity fields, +root, canonical round count, bounded payload length, and absence of trailing +bytes. It then validates the supplied profile and matches the source/profile +identities and round count. Only admitted framing reaches prime-route replay +or loading and execution of the pinned native producers. Valid records retain +the same single scheduler budget and reverse operations; no wire field, split +formula, producer pin, ownership or security claim changes. + +Three added tests in `tests/test_terminal_records.py` check every truncated +prefix, wrong magic, identity/count/length mismatches, noncanonical integers +and trailing bytes without invoking either expensive step. A complete header +still reaches real scheduler evaluation and its existing budget refusal. + +Usage: `WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_terminal_records.py -k HeaderAdmissionTests -v`. + +Current repaired local result: **164 tests passed, zero failures, errors or skips**, +with unchanged sources during execution. The 152-test result above is an earlier +checkpoint, not the current suite result. Hosted checks and terminal review remain +separate acceptance gates. diff --git a/research/weave/cycle.py b/research/weave/cycle.py index 7f262f2..4e5e37c 100644 --- a/research/weave/cycle.py +++ b/research/weave/cycle.py @@ -446,19 +446,25 @@ def reverse(wire: bytes, corpus: bytes, profile: Profile, sources: str | Path, raise Refused('cycle record must be bytes') if len(wire) > limits.round_bytes+128: raise ResourceLimit('cycle record exceeds final byte budget') - routes, engine = _prepared(profile,limits) - api,geometry = load_native(sources) + # Admit the complete cheap outer frame before prime replay or native code. + # Tiny malformed inputs must not consume the scheduler's full work budget. reader = _Reader(wire) if reader.take(4) != MAGIC: raise Refused('unrecognized cycle record') - if reader.take(32) != _native_identity() or reader.take(32) != profile.identity: - raise Refused('native source lock or profile identity mismatch') + source_identity = reader.take(32) + profile_identity = reader.take(32) root = reader.take(32).hex() - if reader.uint() != len(profile.rounds): - raise Refused('cycle round count mismatch') + round_count = reader.uint() data = reader.blob(limits.round_bytes) if reader.pos != len(wire): raise Refused('trailing cycle bytes') + Profile.__post_init__(profile) + if source_identity != _native_identity() or profile_identity != profile.identity: + raise Refused('native source lock or profile identity mismatch') + if round_count != len(profile.rounds): + raise Refused('cycle round count mismatch') + routes, engine = _prepared(profile,limits) + api,geometry = load_native(sources) for i in range(len(profile.rounds)-1,-1,-1): spec,route = profile.rounds[i],routes[i] split = plan(len(data)*8,route,spec.arities,limits.numeral(),engine=engine) diff --git a/research/weave/test.py b/research/weave/test.py index 1c542d7..3c6a5c7 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -17,7 +17,7 @@ from assembly import Pipeline ROOT = Path(__file__).resolve().parent -EXPECTED_TESTS = 161 # 152 existing + 9 terminal-record regression methods. +EXPECTED_TESTS = 164 # 161 existing + 3 outer-header admission regression methods. def snapshot(): @@ -64,7 +64,7 @@ def main(): 'prime_jump_trace': [5381, 53, 241, 1523], 'native_origin_methods': 13, 'cycle_repair_methods': 12, 'review_closure_methods': 18, 'final_findings_methods': 9, - 'terminal_record_methods': 9 + 'terminal_record_methods': 9, 'header_admission_methods': 3 }, 'cycle': 'EXPLICIT_CANDIDATE: corpus normalization once; native sequence affixiation and prime-derived bit interleave; exact reverse', 'full_weave': Pipeline().plan(), diff --git a/research/weave/tests/test_terminal_records.py b/research/weave/tests/test_terminal_records.py index 0d090a5..788ace9 100644 --- a/research/weave/tests/test_terminal_records.py +++ b/research/weave/tests/test_terminal_records.py @@ -11,6 +11,15 @@ # id: recovery_uses_trusted_geometry # proves: binary_coordinate_recovery, binary_source_refusal # call: self::test_recovery_cannot_use_shadowed_geometry_to_accept_coordinates +# id: cycle_header_admission_precedes_expensive_work +# proves: cycle_strict_refusal +# call: self::test_reverse_rejects_every_truncated_header_before_preparation +# id: cycle_invalid_framing_never_loads_producers +# proves: cycle_strict_refusal +# call: self::test_reverse_rejects_outer_identity_count_and_length_before_preparation +# id: cycle_valid_header_retains_scheduler_budget +# proves: cycle_strict_refusal, prime_split_replay +# call: self::test_reverse_prepares_valid_headers_under_the_existing_budget # === END CHECKS === """Replay three terminal-review gaps using only nonsecret local records. @@ -165,4 +174,59 @@ def test_cycle_uses_trusted_lift_for_supplied_geometry(self): self.assertEqual(called,[]) +class HeaderAdmissionTests(unittest.TestCase): + """Cheap outer-record admission must precede prime replay and source execution.""" + + def fixture(self): + profile = cycle.Profile.read((ROOT/'profiles/cycle-v1.json').read_bytes()) + # Syntactically admitted, genuinely executable prime path; invalid wire + # cases must not perform its two nth-prime calculations. + spec = replace(profile.rounds[0], path=PrimePath(5381, (("next",),)*2)) + profile = replace(profile, rounds=(spec,)) + header = cycle.MAGIC + cycle._native_identity() + profile.identity + bytes(32) + frame = header + numeral._uint(1) + numeral._blob(b'x') + return profile, header, frame + + def test_reverse_rejects_every_truncated_header_before_preparation(self): + profile, header, frame = self.fixture() + cases = [frame[:i] for i in range(len(frame))] + cases.append(b'bad!' + frame[4:]) + with patch.object(cycle, '_prepared', side_effect=AssertionError('prime work before admission')) as prepared: + with patch.object(cycle, 'load_native', side_effect=AssertionError('native source before admission')) as native_load: + for i, wire in enumerate(cases): + with self.subTest(case=i), self.assertRaises(Refused): + cycle.reverse(wire, b'corpus', profile, '/unused-sources') + prepared.assert_not_called() + native_load.assert_not_called() + + def test_reverse_rejects_outer_identity_count_and_length_before_preparation(self): + profile, header, frame = self.fixture() + cases = ( + frame[:4] + bytes([frame[4] ^ 1]) + frame[5:], + frame[:36] + bytes([frame[36] ^ 1]) + frame[37:], + header + numeral._uint(2) + numeral._blob(b'x'), + header + b'\x81\x00' + numeral._blob(b'x'), + header + numeral._uint(1) + b'\x81\x00x', + header + numeral._uint(1) + numeral._uint(cycle.CycleLimits().round_bytes + 1), + frame + b'trailing', + ) + with patch.object(cycle, '_prepared', side_effect=AssertionError('prime work before admission')) as prepared: + with patch.object(cycle, 'load_native', side_effect=AssertionError('native source before admission')) as native_load: + for i, wire in enumerate(cases): + with self.subTest(case=i), self.assertRaises(Refused): + cycle.reverse(wire, b'corpus', profile, '/unused-sources') + prepared.assert_not_called() + native_load.assert_not_called() + + def test_reverse_prepares_valid_headers_under_the_existing_budget(self): + profile, header, frame = self.fixture() + limits = cycle.CycleLimits(prime_work=1) + with patch.object(cycle, '_prepared', wraps=cycle._prepared) as prepared: + with patch.object(cycle, 'load_native', side_effect=AssertionError('native load after exhausted budget')) as native_load: + with self.assertRaisesRegex(ResourceLimit, 'aggregate prime work'): + cycle.reverse(frame, b'corpus', profile, '/unused-sources', limits) + prepared.assert_called_once_with(profile, limits) + native_load.assert_not_called() + + if __name__=='__main__':unittest.main() From 07ac59976b9eb5c73974ea39c162670b676bf19c Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 6 Oct 2026 03:34:11 -0700 Subject: [PATCH 14/15] fix(weave): decode prime recipes once within the encoding budget Separate trusted structural checks from recipe evaluation. Decoder-owned recipes each construct their block once under the shared engine; final packet validation checks all typed fields, attachments, uniqueness, occurrences and declared length without a second recipe replay. Public validation/export paths still replay supplied recipes, and no reusable validation flag or caller bypass is introduced. Four regressions cover exact same-budget roundtrip, one replay per definition with aggregate refusal, malformed structure and mutation after decode. Update earlier budget tests to count actual necessary work and keep CLI operations under one shared allowance. The new roundtrip tests fail on 8bc5e1f7 and pass after repair. Full local suite: 168 passed, zero failures/errors/skips; sources unchanged. Source/profile/native-lock SHA-256 c228c5c2a9b076757c5861ce275b21deff8fe73f30f9d6d561ff467e76187682. No native lock, wire format, profile formula, producer authority or cryptographic-standing change. Hosted checks and terminal review remain separate gates. --- research/weave/CYCLE.md | 38 ++++++- research/weave/NUMERAL_CONSTRUCTION.md | 26 ++++- research/weave/numeral.py | 40 ++++++-- research/weave/test.py | 5 +- research/weave/tests/test_numeral_replay.py | 108 ++++++++++++++++++++ research/weave/tests/test_numeral_review.py | 6 +- research/weave/tests/test_review_closure.py | 5 +- 7 files changed, 205 insertions(+), 23 deletions(-) create mode 100644 research/weave/tests/test_numeral_replay.py diff --git a/research/weave/CYCLE.md b/research/weave/CYCLE.md index 951c155..6d1492b 100644 --- a/research/weave/CYCLE.md +++ b/research/weave/CYCLE.md @@ -162,7 +162,7 @@ and malformed-input versus resource-limit classification. Three new regressions bring its suite to 18. The obsolete bit-per-circle modules and their 22 tests are removed, with these sequence/native operations as replacement. Fifty-two original transport/assembly tests retain their original limited scope. Full Weave inventory: -52 + 18 + 30 + 13 native-origin + 12 repair + 18 review-closure + 9 follow-up + 9 terminal-record + 3 header-admission regressions = 164. The original +52 + 18 + 30 + 13 native-origin + 12 repair + 18 review-closure + 9 follow-up + 9 terminal-record + 3 header-admission + 4 numeral-replay regressions = 168. The original 13 native producer tests are retained in Stack as `tests/test_binary_origin.py`. The repair regressions exercise forged closed records, fresh verified module loading, atomic failed-write cleanup, empty-input attachments and manifest edges. @@ -309,7 +309,35 @@ still reaches real scheduler evaluation and its existing budget refusal. Usage: `WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_terminal_records.py -k HeaderAdmissionTests -v`. -Current repaired local result: **164 tests passed, zero failures, errors or skips**, -with unchanged sources during execution. The 152-test result above is an earlier -checkpoint, not the current suite result. Hosted checks and terminal review remain -separate acceptance gates. +The outer-header repair passed its then-current **164-test** local suite with +zero failures, errors or skips. The current result is recorded below; neither the +152-test nor 164-test checkpoint is presented as the latest inventory. + + +## Single recipe replay during numeral decoding + +The review of `8bc5e1f7ce7f` found that a shared counter still charged decoding +for a redundant second recipe evaluation. A recipe needing five work units could +encode under `prime_work=5` but fail to decode under those identical limits. + +The decoder now reconstructs each recipe-backed block once using the trusted +PrimePath implementation and one aggregate engine. It then checks the packet's +complete typed structure, angular attachments, definition uniqueness, occurrences +and declared length without replaying those same definitions. No caller-supplied +bypass or reusable validation flag is introduced. Public validation, encoding, +accounting, occurrence export and restoration continue to revalidate supplied +objects and recipes; mutation after decoding is not trusted. + +Four regressions in `tests/test_numeral_replay.py` verify the exact same-budget +roundtrip, one replay per definition and true aggregate refusal, malformed recipe +record rejection, and revalidation after a decoded record is changed. The prior +budget tests now count necessary actual work rather than mandating duplicate work. +CLI inspect/recover still share one counter across their distinct operations. + +Usage: `python -m unittest discover -s tests -p test_numeral_replay.py -v`. + +Current repaired local result: **168 tests passed, zero failures, errors or skips**, +with unchanged sources during execution. The native source lock, profile, wire +format, UCNS producers and 6,735-byte exact-recovery demo are unchanged. This +numeral resource repair does not change Weave's cryptographic standing. Hosted +checks and current-head terminal review remain separate acceptance gates. diff --git a/research/weave/NUMERAL_CONSTRUCTION.md b/research/weave/NUMERAL_CONSTRUCTION.md index db540a9..e9499ed 100644 --- a/research/weave/NUMERAL_CONSTRUCTION.md +++ b/research/weave/NUMERAL_CONSTRUCTION.md @@ -51,8 +51,8 @@ Default adjustable `Limits`: 64 MiB output, 80 MiB wire, 137468 definitions, 1000000 references, prime index100000, prime value2^32-1, sieve4000000 cells, 256 steps per recipe, and16000000 aggregate prime-work units. Resource refusal means the chosen execution profile cannot complete that work, not falsification. -Malformed immutable structures raise `Refused`, not `ResourceLimit`. Decode parsing -and validation now share one charged prime engine rather than resetting the budget. +Malformed immutable structures raise `Refused`, not `ResourceLimit`. Decode evaluates each recipe once under one charged prime engine. Final structural +validation reuses the values just constructed, without a redundant recipe replay. ## Self-contained wire and accounting @@ -126,3 +126,25 @@ and BitBlock/PrimePath implementations, not methods attached to supplied instanc BitBlock fields are rechecked before use. A caller-provided override cannot turn an invalid entry into an admitted packet or change a literal/recipe during serialization. This is a data-record boundary, not a sandbox against replacement of the runtime. + + +## Same-budget recipe recovery + +A packet admitted by `encode(packet, limits)` no longer needs a second recipe-work +allowance merely to decode. During `decode`, trusted recipe replay constructs each +bound integer once, with one aggregate engine across definitions. Typed fields, +angles, unique symbols/attachments, references and output length are checked after +parsing without re-executing the recipes. For `PrimePath(101, ())`, encoding and +decoding each consume five units and both succeed at `prime_work=5`; both refuse +at four. Two such five-unit definitions consume ten, not five or twenty. + +This is local reuse of decoder-owned reconstruction, not cached authorization of +mutable data. Ordinary Packet/Entry validation still verifies supplied recipe/value +relations. Altering a decoded object's recipe or block cannot bypass later encoding, +accounting or restoration. Trusted class dispatch is preserved for structural checks. +Separate operations in one CLI call still share their actual total work allowance: +for the five-unit example, inspect uses ten units and recover uses fifteen. + +Run `python -m unittest discover -s tests -p test_numeral_replay.py -v` for four +focused witnesses. The binary wire format, recipe algebra and native dependencies +are unchanged. Successful reconstruction is not cryptographic decryption. diff --git a/research/weave/numeral.py b/research/weave/numeral.py index 9aee98f..32125e8 100644 --- a/research/weave/numeral.py +++ b/research/weave/numeral.py @@ -247,7 +247,10 @@ class Entry: circle: int recipe: PrimePath | None = field(default=None, repr=False) - def validate(self, limits: Limits, *, _engine=None) -> None: + def _validate_fields(self, limits: Limits) -> None: + """Check representation fields; this alone does not establish recipe truth.""" + if type(self) is not Entry: + raise Refused("exact Entry required") _symbol(self.symbol) if type(self.block) is not BitBlock: raise Refused("a definition must contain an exact BitBlock") @@ -257,8 +260,14 @@ def validate(self, limits: Limits, *, _engine=None) -> None: if self.block.length > limits.output_bits: raise ResourceLimit("definition exceeds bit budget") _validate_attachment(self.angle, self.circle) + if self.recipe is not None and type(self.recipe) is not PrimePath: + raise Refused("exact PrimePath required") + + def validate(self, limits: Limits, *, _engine=None) -> None: + """Validate supplied fields and independently replay any supplied recipe.""" + Entry._validate_fields(self, limits) if self.recipe is not None: - if type(self.recipe) is not PrimePath or PrimePath.replay(self.recipe, limits, _engine=_engine)[-1] != self.block.value: + if PrimePath.replay(self.recipe, limits, _engine=_engine)[-1] != self.block.value: raise Refused("recipe does not construct its bound integer") @@ -274,7 +283,12 @@ class Packet: entries: tuple[Entry, ...] symbols: str - def validate(self, limits: Limits = Limits(), *, _engine=None) -> int: + def _validate_fields(self, limits: Limits) -> int: + """Check typed structure and count bits, without claiming recipe validation. + + Decoder-owned recipes already produced their bound blocks by trusted replay. + Other public operations must use validate(), which replays supplied recipes. + """ if type(self) is not Packet: raise Refused("exact Packet required") if type(self.origin) is not str or not self.origin or len(self.origin.encode("utf-8")) > 1024: @@ -290,11 +304,8 @@ def validate(self, limits: Limits = Limits(), *, _engine=None) -> int: raise ResourceLimit("occurrence stream exceeds execution budget") mapping = {} angles = set() - primes = _engine if _engine is not None else _Primes(limits) for entry in self.entries: - if type(entry) is not Entry: - raise Refused("Entry required") - Entry.validate(entry, limits, _engine=primes) + Entry._validate_fields(entry, limits) if entry.symbol in mapping or entry.angle in angles: raise Refused("duplicate symbol or angular attachment in this scope") mapping[entry.symbol] = entry @@ -308,6 +319,14 @@ def validate(self, limits: Limits = Limits(), *, _engine=None) -> int: raise ResourceLimit("reconstructed stream exceeds bit budget") return total + def validate(self, limits: Limits = Limits(), *, _engine=None) -> int: + """Validate all supplied data, including one recipe replay per definition.""" + total = Packet._validate_fields(self, limits) + primes = _engine if _engine is not None else _Primes(limits) + for entry in self.entries: + Entry.validate(entry, limits, _engine=primes) + return total + def occurrences(self, limits: Limits = Limits()) -> tuple[tuple[str, int, int, int], ...]: """(symbol, bit offset, ordinal on its occurrence circle, circle).""" Packet.validate(self, limits) @@ -494,8 +513,11 @@ def decode(data: bytes, limits: Limits = Limits(), *, _engine=None) -> Packet: if reader.pos != len(data): raise Refused("trailing packet data") packet = Packet(origin, round_id, tuple(entries), symbols) - # Parsing and structural validation share one charged prime-work budget. - if Packet.validate(packet, limits, _engine=primes) != declared: + # Every recipe above produced its block through trusted replay under this + # decoder's single charged engine. Check the remaining typed structure and + # occurrence length without replaying those same recipes a second time. + # No reusable "validated" flag or caller-supplied bypass is attached to Packet. + if Packet._validate_fields(packet, limits) != declared: raise Refused("declared output length disagrees with occurrences") return packet diff --git a/research/weave/test.py b/research/weave/test.py index 3c6a5c7..85d5342 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -17,7 +17,7 @@ from assembly import Pipeline ROOT = Path(__file__).resolve().parent -EXPECTED_TESTS = 164 # 161 existing + 3 outer-header admission regression methods. +EXPECTED_TESTS = 168 # 164 existing + 4 single-replay roundtrip regression methods. def snapshot(): @@ -64,7 +64,8 @@ def main(): 'prime_jump_trace': [5381, 53, 241, 1523], 'native_origin_methods': 13, 'cycle_repair_methods': 12, 'review_closure_methods': 18, 'final_findings_methods': 9, - 'terminal_record_methods': 9, 'header_admission_methods': 3 + 'terminal_record_methods': 9, 'header_admission_methods': 3, + 'numeral_single_replay_methods': 4 }, 'cycle': 'EXPLICIT_CANDIDATE: corpus normalization once; native sequence affixiation and prime-derived bit interleave; exact reverse', 'full_weave': Pipeline().plan(), diff --git a/research/weave/tests/test_numeral_replay.py b/research/weave/tests/test_numeral_replay.py new file mode 100644 index 0000000..b5eb999 --- /dev/null +++ b/research/weave/tests/test_numeral_replay.py @@ -0,0 +1,108 @@ +# === CHECKS === +# id: numeral_equal_limits_roundtrip +# proves: numeral_exact_recovery, numeral_recipe_replay, numeral_strict_input +# call: self::test_single_recipe_roundtrip_at_exact_encode_budget +# id: numeral_decode_one_replay_per_definition +# proves: numeral_recipe_replay, numeral_strict_input +# call: self::test_multiple_recipes_share_budget_without_second_replay +# id: numeral_replay_does_not_replace_structural_validation +# proves: numeral_strict_input +# call: self::test_recipe_packets_still_reject_invalid_structure +# id: numeral_decode_does_not_grant_mutable_validation_capability +# proves: numeral_recipe_replay, numeral_strict_input +# call: self::test_decoded_packets_are_revalidated_after_mutation +# === END CHECKS === +"""Run: python -m unittest discover -s tests -p test_numeral_replay.py -v. + +One exact recipe evaluation per definition during decode; typed structure is +still validated. No persistent validation flag is attached to decoded objects. +Only nonsecret in-memory fixtures. This tests recovery, not cryptographic secrecy. +""" +from dataclasses import replace +from fractions import Fraction +from unittest.mock import patch +import unittest + +import numeral +from numeral import (BitBlock, Entry, Packet, PrimePath, Limits, Refused, + ResourceLimit, encode, decode, _uint, _blob) + +A, B = chr(0xE000), chr(0xE001) + + +class NumeralReplayTests(unittest.TestCase): + def packet(self): + return Packet('replay', 0, (Entry(A, BitBlock(101,16), Fraction(1,7), + 1, PrimePath(101, ())),), A) + + def test_single_recipe_roundtrip_at_exact_encode_budget(self): + packet = self.packet() + limits = Limits(prime_work=5) + wire = encode(packet, limits) + decoded = decode(wire, limits) + self.assertEqual(decoded, packet) + self.assertEqual(Packet.restore(decoded, limits), BitBlock(101,16)) + with self.assertRaises(ResourceLimit): encode(packet, Limits(prime_work=4)) + with self.assertRaises(ResourceLimit): decode(wire, Limits(prime_work=4)) + + def test_multiple_recipes_share_budget_without_second_replay(self): + first = self.packet().entries[0] + second = Entry(B, BitBlock(103,16), Fraction(2,7), 2, PrimePath(103, ())) + packet = Packet('aggregate',0,(first,second),A+B+A) + limits = Limits(prime_work=10) + encoder = numeral._Primes(limits) + wire = encode(packet, limits, _engine=encoder) + self.assertEqual(encoder.work,10) + decoder = numeral._Primes(limits) + seen = [] + replay = PrimePath.replay + def record(path,*args,**kwargs): + seen.append(path) + return replay(path,*args,**kwargs) + with patch.object(PrimePath,'replay',record): + restored = decode(wire, limits, _engine=decoder) + self.assertEqual(restored,packet) + self.assertEqual(seen,[first.recipe,second.recipe]) + self.assertEqual(decoder.work,encoder.work) + with self.assertRaises(ResourceLimit): encode(packet, Limits(prime_work=9)) + with self.assertRaises(ResourceLimit): decode(wire, Limits(prime_work=9)) + + def test_recipe_packets_still_reject_invalid_structure(self): + # Construct deliberately invalid wire without asking the encoder to accept it. + # Each valid recipe evaluates to 101; every other constraint remains checked. + def definition(symbol=A,n=1,d=7,circle=1,length=16): + return (_blob(symbol.encode()) + _uint(n)+_uint(d)+_uint(circle)+_uint(length) + + b'\x01'+_uint(101)+_uint(0)) + def wire(rows, symbols=A, declared=16, origin=b'replay'): + return (numeral.MAGIC + _blob(origin)+_uint(0)+_uint(declared) + + _uint(len(rows))+b''.join(rows)+_blob(symbols.encode())) + invalid = ( + wire([definition(circle=0)]), wire([definition(circle=8)]), + wire([definition(n=2,d=1)]), wire([definition(length=0)]), + wire([definition()],origin=b''), wire([definition()],declared=17), + wire([definition()],symbols=B), + wire([definition(),definition(n=2)],declared=16), + wire([definition(),definition(symbol=B)],symbols=A+B,declared=32), + ) + for i,data in enumerate(invalid): + with self.subTest(case=i),self.assertRaises(Refused) as failure: + decode(data,Limits(prime_work=100)) + self.assertNotIsInstance(failure.exception,ResourceLimit) + self.assertEqual(decode(wire([definition()]),Limits(prime_work=5)), self.packet()) + + def test_decoded_packets_are_revalidated_after_mutation(self): + packet = decode(encode(self.packet())) + # A successful earlier decode is not authority over changed fields. + bad = replace(packet, entries=(replace(packet.entries[0],recipe=PrimePath(103,())),)) + for operation in (encode,numeral.accounting,Packet.restore,Packet.occurrences): + with self.subTest(operation=operation.__name__),self.assertRaises(Refused): + operation(bad) + object.__setattr__(packet.entries[0].recipe,'seed',4) + object.__setattr__(packet,'validate',lambda *args,**kwargs:16) + object.__setattr__(packet,'_validate_fields',lambda *args,**kwargs:16) + for operation in (encode,numeral.accounting,Packet.restore,Packet.occurrences): + with self.subTest(operation=operation.__name__),self.assertRaises(Refused): + operation(packet) + + +if __name__=='__main__': unittest.main() diff --git a/research/weave/tests/test_numeral_review.py b/research/weave/tests/test_numeral_review.py index 2c45a76..b6dd1be 100644 --- a/research/weave/tests/test_numeral_review.py +++ b/research/weave/tests/test_numeral_review.py @@ -37,10 +37,10 @@ def test_decode_has_one_aggregate_prime_budget(self): path = PrimePath(101, ()) p = Packet('budget', 0, (Entry(A, BitBlock(101, 16), Fraction(1, 7), 1, path),), A) wire = encode(p) - # One recipe step and four trial-divisor candidates per pass: 10 total. + # One trusted replay: one step and four trial-divisor candidates, five total. with self.assertRaises(ResourceLimit): - decode(wire, Limits(prime_work=9)) - self.assertEqual(decode(wire, Limits(prime_work=10)), p) + decode(wire, Limits(prime_work=4)) + self.assertEqual(decode(wire, Limits(prime_work=5)), p) def test_malformed_structures_are_not_capacity_failures(self): p = Packet('scope', 0, (Entry(A, BitBlock(5,8), Fraction(1,7), 1),), A) diff --git a/research/weave/tests/test_review_closure.py b/research/weave/tests/test_review_closure.py index 91f1fab..8d2be65 100644 --- a/research/weave/tests/test_review_closure.py +++ b/research/weave/tests/test_review_closure.py @@ -172,8 +172,9 @@ def record(path, *args, **kwargs): def test_cli_shares_prime_budget_through_inspect_and_recover(self): wire = numeral.encode(self.recipe_packet()) - # Decode charges 10, accounting 5, and restore a further 5 work units. - for command, minimum in (('inspect',15), ('recover',20)): + # Decode charges 5, accounting 5, and restore a further 5 work units. + # Separate public operations revalidate; the shared CLI counter never resets. + for command, minimum in (('inspect',10), ('recover',15)): for budget, success in ((minimum-1,False), (minimum,True)): with self.subTest(command=command,budget=budget), tempfile.TemporaryDirectory() as directory: root=Path(directory); src=root/'record'; dst=root/'out'; src.write_bytes(wire) From cd6a1ffdf158e84c90d98bb36b609a633a0beb35 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 6 Oct 2026 04:00:00 -0700 Subject: [PATCH 15/15] fix(weave): admit complete metadata before recipe work and validate public helpers Repair the two P2 findings at 07ac5997. Decode defers recipe evaluation until all definition metadata, complete recipe shapes, scope, uniqueness, references, length and framing have passed cheap validation. Accepted recipes still execute exactly once under the aggregate engine. No placeholder values, cached validation flag or caller bypass is introduced. Normalize/denormalize invoke the trusted Profile validator; affix/unaffix close the same supplied-record boundary for RoundSpec before discovery or decoding. Valid formulas, native-source locks, producer pins, wire format and cryptographic standing remain unchanged. Eight regression methods fail on 07ac5997 (32 subtest failures, two errors) and pass after repair. Full local suite: 176 passed, zero failures/errors/skips, source unchanged. Source/profile/native-lock SHA-256: 607a9af7c0d749d5ab1233b260906a2dfa0d0c50c22abc4a186ed6142dc188b8. Six remote repair blobs match tested local files. Hosted exact-head tests and terminal review remain separate gates. --- research/weave/CYCLE.md | 29 +++- research/weave/NUMERAL_CONSTRUCTION.md | 16 ++ research/weave/cycle.py | 4 + research/weave/numeral.py | 120 +++++++++----- research/weave/test.py | 4 +- research/weave/tests/test_admission_order.py | 159 +++++++++++++++++++ 6 files changed, 286 insertions(+), 46 deletions(-) create mode 100644 research/weave/tests/test_admission_order.py diff --git a/research/weave/CYCLE.md b/research/weave/CYCLE.md index 6d1492b..e704297 100644 --- a/research/weave/CYCLE.md +++ b/research/weave/CYCLE.md @@ -162,7 +162,7 @@ and malformed-input versus resource-limit classification. Three new regressions bring its suite to 18. The obsolete bit-per-circle modules and their 22 tests are removed, with these sequence/native operations as replacement. Fifty-two original transport/assembly tests retain their original limited scope. Full Weave inventory: -52 + 18 + 30 + 13 native-origin + 12 repair + 18 review-closure + 9 follow-up + 9 terminal-record + 3 header-admission + 4 numeral-replay regressions = 168. The original +52 + 18 + 30 + 13 native-origin + 12 repair + 18 review-closure + 9 follow-up + 9 terminal-record + 3 header-admission + 4 numeral-replay + 8 cheap-admission regressions = 176. The original 13 native producer tests are retained in Stack as `tests/test_binary_origin.py`. The repair regressions exercise forged closed records, fresh verified module loading, atomic failed-write cleanup, empty-input attachments and manifest edges. @@ -336,8 +336,33 @@ CLI inspect/recover still share one counter across their distinct operations. Usage: `python -m unittest discover -s tests -p test_numeral_replay.py -v`. -Current repaired local result: **168 tests passed, zero failures, errors or skips**, +The single-replay checkpoint passed **168 tests, zero failures, errors or skips**, with unchanged sources during execution. The native source lock, profile, wire format, UCNS producers and 6,735-byte exact-recovery demo are unchanged. This numeral resource repair does not change Weave's cryptographic standing. Hosted checks and current-head terminal review remain separate acceptance gates. + + +## Complete cheap admission and public helper validation + +The review at `07ac59976b9e` identified two remaining gaps. Numeral decoding now +parses every definition into decoder-owned pending data, validates all cheap entry +metadata and recipe shapes, uniqueness, references, declared length and trailing +bytes, and only then executes accepted prime recipes once under its aggregate +engine. No fabricated block values or persistent validation flags are used. Shared +scope/reference checks keep public Packet validation and wire admission aligned. +A mathematical property requiring actual prime evaluation still receives that +bounded evaluation; malformed metadata cannot consume that work first. + +Public normalize/denormalize operations validate Profile through the trusted class +before reading its fields. Affix/unaffix apply the equivalent check to RoundSpec +before discovery or decoding. This closes the same supplied-profile boundary for +all four public helpers, including frozen-record mutation and instance-method +shadowing. Valid formulas, wire format, native lock, producer ownership and the +6,735-byte exact-recovery demonstration remain unchanged. + +Run `WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_admission_order.py -v`. +Eight regressions cover early and late malformed definitions, recipe shape, +single-replay accounting, direct helper mutation/refusal and valid recovery. +Current suite inventory is **176**; run `test.py` for source-bound results. Earlier +counts in this document are explicitly historical checkpoints, not current results. diff --git a/research/weave/NUMERAL_CONSTRUCTION.md b/research/weave/NUMERAL_CONSTRUCTION.md index e9499ed..a99ef97 100644 --- a/research/weave/NUMERAL_CONSTRUCTION.md +++ b/research/weave/NUMERAL_CONSTRUCTION.md @@ -148,3 +148,19 @@ for the five-unit example, inspect uses ten units and recover uses fifteen. Run `python -m unittest discover -s tests -p test_numeral_replay.py -v` for four focused witnesses. The binary wire format, recipe algebra and native dependencies are unchanged. Successful reconstruction is not cryptographic decryption. + + +## Cheap admission before recipe work + +The decoder first parses deferred recipe definitions and literal blocks and +validates scope, exact attachments, nonempty lengths, unique symbols/angles, +all recipe shapes, occurrence references, total length and complete framing. +Only an admitted structure reaches prime evaluation. Recipe results must then fit +their declared lengths, with one evaluation per definition under the shared engine. +No metadata-invalid packet is mislabeled as a prime-work shortage merely because +its configured prime budget is small. Mathematical validity that depends on a +computed prime path still requires actual evaluation. + +The existing same-budget replay guarantee and post-decode mutation checks remain. +See `tests/test_admission_order.py` for early/late malformed input and unchanged +valid reconstruction witnesses. No wire format or native-source dependency changes. diff --git a/research/weave/cycle.py b/research/weave/cycle.py index 4e5e37c..9730d60 100644 --- a/research/weave/cycle.py +++ b/research/weave/cycle.py @@ -253,6 +253,7 @@ def _corpus_bytes(corpus: bytes, count: int, bit_offset: int) -> bytes: def normalize(message: bytes, corpus: bytes, profile: Profile, limits: CycleLimits = CycleLimits()) -> bytes: """Selected corpus-tail bucket profile; original length remains inside the cycle.""" + Profile.__post_init__(profile) if type(message) is not bytes: raise Refused('exact message bytes required') if len(message) > limits.input_bytes: @@ -271,6 +272,7 @@ def normalize(message: bytes, corpus: bytes, profile: Profile, def denormalize(data: bytes, corpus: bytes, profile: Profile, limits: CycleLimits = CycleLimits()) -> bytes: + Profile.__post_init__(profile) if type(data) is not bytes or len(data) < 45 or data[:4] != NORMAL_MAGIC: raise Refused('invalid normalized source frame') if type(corpus) is not bytes or not corpus: @@ -312,6 +314,7 @@ def _fraction(reader: _Reader) -> Fraction: def affix(data: bytes, *, api, geometry, scope: str, root: str, round_id: int, spec: RoundSpec, limits: CycleLimits = CycleLimits()): + RoundSpec.__post_init__(spec) partition = discover(data,max_bytes=limits.round_bytes,visit_budget=limits.discovery_visits) if len(partition.order) > limits.native_occurrences: raise ResourceLimit('native occurrence count exceeds execution budget') @@ -348,6 +351,7 @@ def affix(data: bytes, *, api, geometry, scope: str, root: str, round_id: int, def unaffix(data: bytes, *, api, geometry, scope: str, root: str, round_id: int, spec: RoundSpec, limits: CycleLimits = CycleLimits()) -> bytes: + RoundSpec.__post_init__(spec) if type(data) is not bytes: raise Refused('affixiation input must be bytes') if len(data) > limits.round_bytes: diff --git a/research/weave/numeral.py b/research/weave/numeral.py index 32125e8..1e9b7f1 100644 --- a/research/weave/numeral.py +++ b/research/weave/numeral.py @@ -134,35 +134,45 @@ class PrimePath: seed: int steps: tuple[tuple, ...] - def replay(self, limits: Limits = Limits(), *, _engine=None) -> tuple[int, ...]: + def _validate_fields(self, limits: Limits) -> None: + """Validate the whole recipe shape without executing prime operations.""" + if type(self) is not PrimePath or _nat(self.seed) < 2: + raise Refused("exact PrimePath with a seed at least two required") if type(self.steps) is not tuple: raise Refused("recipe steps must be an immutable tuple") if len(self.steps) > limits.recipe_steps: raise ResourceLimit("recipe step budget exceeded") + for step in self.steps: + if type(step) is not tuple or not step or type(step[0]) is not str: + raise Refused("invalid recipe step") + if step == ("next",): + continue + if len(step) != 4 or step[0] != "span": + raise Refused("unknown recipe operation") + _, base, start, width = step + if type(base) is not int or base not in (2, 10): + raise Refused("span base must be 2 or 10") + _nat(start) + if _nat(width) == 0: + raise Refused("empty span") + + def replay(self, limits: Limits = Limits(), *, _engine=None) -> tuple[int, ...]: + PrimePath._validate_fields(self, limits) primes = _engine if _engine is not None else _Primes(limits) primes.charge(len(self.steps) + 1) - value = _nat(self.seed) + value = self.seed primes.require(value) trace = [value] for step in self.steps: - if type(step) is not tuple or not step or type(step[0]) is not str: - raise Refused("invalid recipe step") if step == ("next",): value = primes.nth(value) - elif len(step) == 4 and step[0] == "span": + else: _, base, start, width = step - if type(base) is not int or base not in (2, 10): - raise Refused("span base must be 2 or 10") - _nat(start) - if _nat(width) == 0: - raise Refused("empty span") digits = format(value, "b") if base == 2 else str(value) if start + width > len(digits): raise Refused("span outside source numeral") value = int(digits[start:start + width], base) primes.require(value) - else: - raise Refused("unknown recipe operation") trace.append(value) return tuple(trace) @@ -260,8 +270,8 @@ def _validate_fields(self, limits: Limits) -> None: if self.block.length > limits.output_bits: raise ResourceLimit("definition exceeds bit budget") _validate_attachment(self.angle, self.circle) - if self.recipe is not None and type(self.recipe) is not PrimePath: - raise Refused("exact PrimePath required") + if self.recipe is not None: + PrimePath._validate_fields(self.recipe, limits) def validate(self, limits: Limits, *, _engine=None) -> None: """Validate supplied fields and independently replay any supplied recipe.""" @@ -271,6 +281,28 @@ def validate(self, limits: Limits, *, _engine=None) -> None: raise Refused("recipe does not construct its bound integer") +def _validate_scope(origin: str, round_id: int) -> None: + if type(origin) is not str or not origin or len(origin.encode("utf-8")) > 1024: + raise Refused("nonempty UTF-8 origin scope of at most 1024 bytes required") + _nat(round_id) + + +def _reference_length(symbols: str, lengths: dict[str, int], limits: Limits) -> int: + """Validate references against admitted lengths without requiring recipe values.""" + if type(symbols) is not str: + raise Refused("occurrence stream must be a string") + if len(symbols) > limits.occurrences: + raise ResourceLimit("occurrence stream exceeds execution budget") + total = 0 + for symbol in symbols: + if symbol not in lengths: + raise Refused("undefined symbol") + total += lengths[symbol] + if total > limits.output_bits: + raise ResourceLimit("reconstructed stream exceeds bit budget") + return total + + @dataclass(frozen=True) class Packet: """One scoped definition table and ordered occurrence stream. @@ -291,33 +323,20 @@ def _validate_fields(self, limits: Limits) -> int: """ if type(self) is not Packet: raise Refused("exact Packet required") - if type(self.origin) is not str or not self.origin or len(self.origin.encode("utf-8")) > 1024: - raise Refused("nonempty UTF-8 origin scope of at most 1024 bytes required") - _nat(self.round_id) + _validate_scope(self.origin, self.round_id) if type(self.entries) is not tuple: raise Refused("definitions must be an immutable tuple") if len(self.entries) > limits.entries: raise ResourceLimit("definition count exceeds execution budget") - if type(self.symbols) is not str: - raise Refused("occurrence stream must be a string") - if len(self.symbols) > limits.occurrences: - raise ResourceLimit("occurrence stream exceeds execution budget") mapping = {} angles = set() for entry in self.entries: Entry._validate_fields(entry, limits) if entry.symbol in mapping or entry.angle in angles: raise Refused("duplicate symbol or angular attachment in this scope") - mapping[entry.symbol] = entry + mapping[entry.symbol] = entry.block.length angles.add(entry.angle) - total = 0 - for symbol in self.symbols: - if symbol not in mapping: - raise Refused("undefined symbol") - total += mapping[symbol].block.length - if total > limits.output_bits: - raise ResourceLimit("reconstructed stream exceeds bit budget") - return total + return _reference_length(self.symbols, mapping, limits) def validate(self, limits: Limits = Limits(), *, _engine=None) -> int: """Validate all supplied data, including one recipe replay per definition.""" @@ -455,7 +474,7 @@ def encode(packet: Packet, limits: Limits = Limits(), *, _engine=None) -> bytes: def decode(data: bytes, limits: Limits = Limits(), *, _engine=None) -> Packet: - """Strict self-contained decoder; malformed input never becomes a partial result.""" + """Admit the entire cheap wire structure, then replay accepted recipes once.""" if type(data) is not bytes: raise Refused("packet must be bytes") if len(data) > limits.wire_bytes: @@ -466,13 +485,14 @@ def decode(data: bytes, limits: Limits = Limits(), *, _engine=None) -> Packet: try: origin = reader.blob(1024).decode("utf-8") round_id, declared = reader.uint(), reader.uint() + _validate_scope(origin, round_id) if declared > limits.output_bits: raise ResourceLimit("declared output exceeds bit budget") count = reader.uint() if count > limits.entries: raise ResourceLimit("definition count exceeds budget") - entries = [] - primes = _engine if _engine is not None else _Primes(limits) + pending = [] + lengths, angles = {}, set() for _ in range(count): symbol = reader.blob(4).decode("utf-8") _symbol(symbol) @@ -483,12 +503,18 @@ def decode(data: bytes, limits: Limits = Limits(), *, _engine=None) -> Packet: if (angle.numerator, angle.denominator) != (numerator, denominator): raise Refused("angle fraction is not canonical") circle, length = reader.uint(), reader.uint() + _validate_attachment(angle, circle) + if length == 0: + raise Refused("a definition must contain a nonempty BitBlock") if length > limits.output_bits: raise ResourceLimit("definition exceeds bit budget") + if symbol in lengths or angle in angles: + raise Refused("duplicate symbol or angular attachment in this scope") + lengths[symbol] = length + angles.add(angle) tag = reader.take(1)[0] - recipe = None if tag == 0: - block = BitBlock.from_bytes(reader.take((length + 7) // 8), length) + payload = BitBlock.from_bytes(reader.take((length + 7) // 8), length) elif tag == 1: seed, step_count = reader.uint(), reader.uint() if step_count > limits.recipe_steps: @@ -502,21 +528,31 @@ def decode(data: bytes, limits: Limits = Limits(), *, _engine=None) -> Packet: steps.append(("span", reader.uint(), reader.uint(), reader.uint())) else: raise Refused("unknown recipe opcode") - recipe = PrimePath(seed, tuple(steps)) - block = BitBlock(PrimePath.replay(recipe, limits, _engine=primes)[-1], length) + payload = PrimePath(seed, tuple(steps)) + PrimePath._validate_fields(payload, limits) else: raise Refused("unknown definition tag") - entries.append(Entry(symbol, block, angle, circle, recipe)) + # Decoder-owned pending data, not an Entry with a fabricated bit value. + pending.append((symbol, length, angle, circle, payload)) symbols = reader.blob(limits.occurrences * 4).decode("utf-8") except UnicodeError as exc: raise Refused("invalid UTF-8") from exc if reader.pos != len(data): raise Refused("trailing packet data") + if _reference_length(symbols, lengths, limits) != declared: + raise Refused("declared output length disagrees with occurrences") + # All cheap fields, every recipe shape and the complete occurrence stream are + # admitted before any primality test or nth-prime calculation is attempted. + primes = _engine if _engine is not None else _Primes(limits) + entries = [] + for symbol, length, angle, circle, payload in pending: + recipe = payload if type(payload) is PrimePath else None + block = (BitBlock(PrimePath.replay(recipe, limits, _engine=primes)[-1], length) + if recipe is not None else payload) + entries.append(Entry(symbol, block, angle, circle, recipe)) packet = Packet(origin, round_id, tuple(entries), symbols) - # Every recipe above produced its block through trusted replay under this - # decoder's single charged engine. Check the remaining typed structure and - # occurrence length without replaying those same recipes a second time. - # No reusable "validated" flag or caller-supplied bypass is attached to Packet. + # Check actual constructed blocks without a second recipe replay. No cached + # authorization escapes: later public operations revalidate supplied records. if Packet._validate_fields(packet, limits) != declared: raise Refused("declared output length disagrees with occurrences") return packet diff --git a/research/weave/test.py b/research/weave/test.py index 85d5342..7d2941a 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -17,7 +17,7 @@ from assembly import Pipeline ROOT = Path(__file__).resolve().parent -EXPECTED_TESTS = 168 # 164 existing + 4 single-replay roundtrip regression methods. +EXPECTED_TESTS = 176 # 168 existing + 8 cheap-admission/public-helper regressions. def snapshot(): @@ -65,7 +65,7 @@ def main(): 'native_origin_methods': 13, 'cycle_repair_methods': 12, 'review_closure_methods': 18, 'final_findings_methods': 9, 'terminal_record_methods': 9, 'header_admission_methods': 3, - 'numeral_single_replay_methods': 4 + 'numeral_single_replay_methods': 4, 'cheap_admission_methods': 8 }, 'cycle': 'EXPLICIT_CANDIDATE: corpus normalization once; native sequence affixiation and prime-derived bit interleave; exact reverse', 'full_weave': Pipeline().plan(), diff --git a/research/weave/tests/test_admission_order.py b/research/weave/tests/test_admission_order.py new file mode 100644 index 0000000..38a25f0 --- /dev/null +++ b/research/weave/tests/test_admission_order.py @@ -0,0 +1,159 @@ +# === CHECKS === +# id: numeral_metadata_before_prime_work +# proves: numeral_strict_input +# call: self::test_recipe_metadata_rejected_before_any_replay +# id: numeral_late_metadata_before_prime_work +# proves: numeral_strict_input +# call: self::test_late_entry_failure_does_not_replay_earlier_recipe +# id: numeral_shapes_before_prime_work +# proves: numeral_strict_input, numeral_recipe_replay +# call: self::test_all_recipe_shapes_checked_before_replay +# id: numeral_deferred_same_budget +# proves: numeral_exact_recovery, numeral_recipe_replay +# call: self::test_deferred_decode_replays_once_at_same_limit +# id: normalization_validates_public_profile +# proves: cycle_normalize_once, cycle_strict_refusal +# call: self::test_direct_normalization_rejects_mutated_profiles +# id: normalization_trusted_validation +# proves: cycle_strict_refusal +# call: self::test_normalization_uses_class_validator +# id: affixiation_validates_public_round +# proves: cycle_strict_refusal +# call: self::test_public_affixiation_rejects_mutated_round_before_work +# id: public_helpers_preserve_valid_cycle +# proves: cycle_normalize_once, cycle_reversible_rounds +# call: self::test_valid_helpers_preserve_cycle_output +# === END CHECKS === +"""Run: WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_admission_order.py -v. + +Cheap complete input admission precedes prime work. Public normalization and +round helpers validate caller-supplied profiles through trusted class operations. +Nonsecret fixtures only; no mutation of trusted runtime code outside scoped mocks. +""" +from dataclasses import replace +from fractions import Fraction +from pathlib import Path +from unittest.mock import patch +import os +import unittest + +import cycle +import native_binary as native +import numeral as n + +ROOT = Path(__file__).resolve().parents[1] +SOURCES = Path(os.environ['WEAVE_SOURCES']) +A, B = chr(0xE000), chr(0xE001) + + +def definition(symbol=A, angle=(1,7), circle=1, length=16, seed=101, steps=()): + encoded = (n._blob(symbol.encode()) + n._uint(angle[0])+n._uint(angle[1]) + + n._uint(circle)+n._uint(length)+b'\x01'+n._uint(seed)+n._uint(len(steps))) + for step in steps: + if step == ('next',): encoded += b'\x00' + else: encoded += b'\x01' + b''.join(n._uint(x) for x in step[1:]) + return encoded + + +def wire(rows, symbols=A, declared=16, origin=b'admission'): + return (n.MAGIC+n._blob(origin)+n._uint(0)+n._uint(declared)+n._uint(len(rows)) + + b''.join(rows)+n._blob(symbols.encode())) + + +class AdmissionOrderTests(unittest.TestCase): + def assert_cheap_refusal(self, data): + with patch.object(n.PrimePath,'replay',side_effect=AssertionError('recipe ran before admission')) as replay: + with self.assertRaises(n.Refused) as failure: + n.decode(data,n.Limits(prime_work=4)) + self.assertNotIsInstance(failure.exception,n.ResourceLimit) + replay.assert_not_called() + + def test_recipe_metadata_rejected_before_any_replay(self): + cases=(wire([definition(circle=0)]),wire([definition(circle=8)]), + wire([definition(angle=(2,1))]),wire([definition(angle=(2,2))]), + wire([definition(length=0)]),wire([definition()],origin=b''), + wire([definition()],symbols=B),wire([definition()],declared=17), + wire([definition()])+b'extra',wire([definition()])[:-1]) + for i,data in enumerate(cases): + with self.subTest(case=i):self.assert_cheap_refusal(data) + + def test_late_entry_failure_does_not_replay_earlier_recipe(self): + cases=(definition(symbol=B,circle=0),definition(symbol=B,angle=(2,1)), + definition(symbol=A,angle=(2,7)),definition(symbol=B,angle=(1,7)), + definition(symbol=B,angle=(2,7),length=0)) + for i,second in enumerate(cases): + with self.subTest(case=i):self.assert_cheap_refusal(wire([definition(),second],symbols=A+B,declared=32)) + self.assert_cheap_refusal(wire([definition(),definition(symbol=B,angle=(2,7))],symbols=A+B,declared=31)) + + def test_all_recipe_shapes_checked_before_replay(self): + cases=(dict(seed=1),dict(steps=(('span',3,0,1),)),dict(steps=(('span',10,0,0),))) + for i,changes in enumerate(cases): + data=wire([definition(),definition(symbol=B,angle=(2,7),**changes)],symbols=A+B,declared=32) + with self.subTest(case=i):self.assert_cheap_refusal(data) + # A malformed late step in a public recipe also refuses before the first + # expensive nth-prime operation or prime-work charge. + path=n.PrimePath(101,(('next',),('span',3,0,1))) + with patch.object(n._Primes,'charge',side_effect=AssertionError('charged before shape admission')): + with self.assertRaises(n.Refused) as failure:n.PrimePath.replay(path,n.Limits(prime_work=1)) + self.assertNotIsInstance(failure.exception,n.ResourceLimit) + + def test_deferred_decode_replays_once_at_same_limit(self): + limits=n.Limits(prime_work=10) + data=wire([definition(),definition(symbol=B,angle=(2,7),seed=103)],symbols=A+B,declared=32) + engine=n._Primes(limits) + with patch.object(n.PrimePath,'replay',autospec=True,side_effect=n.PrimePath.replay) as replay: + packet=n.decode(data,limits,_engine=engine) + self.assertEqual(replay.call_count,2) + self.assertEqual(engine.work,10) + self.assertEqual(n.encode(packet,limits),data) + self.assertEqual(n.Packet.restore(packet,limits).to_bytes(),b'\x00e\x00g') + with self.assertRaises(n.ResourceLimit):n.decode(data,n.Limits(prime_work=9)) + + def profile(self): + return cycle.Profile.read((ROOT/'profiles/cycle-v1.json').read_bytes()) + + def test_direct_normalization_rejects_mutated_profiles(self): + original=self.profile();frame=cycle.normalize(b'AB',b'corpus',original) + for field,value in (('bucket_bytes',0),('bucket_bytes',True),('corpus_bit_offset',True), + ('corpus_bit_offset',-1),('scope',''),('rounds',[])): + p=self.profile();object.__setattr__(p,field,value) + with self.subTest(field=field),self.assertRaises(n.Refused):cycle.normalize(b'AB',b'corpus',p) + with self.subTest(field=field),self.assertRaises(n.Refused):cycle.denormalize(frame,b'corpus',p) + p=self.profile();object.__setattr__(p.rounds[0],'circle_order',(1,)*7) + with self.assertRaises(n.Refused):cycle.normalize(b'AB',b'corpus',p) + with self.assertRaises(n.Refused):cycle.denormalize(frame,b'corpus',p) + + def test_normalization_uses_class_validator(self): + p=self.profile();called=[] + object.__setattr__(p,'__post_init__',lambda:called.append(True)) + frame=cycle.normalize(b'AB',b'corpus',p) + self.assertEqual(cycle.denormalize(frame,b'corpus',p),b'AB') + self.assertEqual(called,[]) + object.__setattr__(p,'corpus_bit_offset',True) + for operation,args in ((cycle.normalize,(b'AB',b'corpus',p)),(cycle.denormalize,(frame,b'corpus',p))): + with self.assertRaises(n.Refused):operation(*args) + self.assertEqual(called,[]) + + def test_public_affixiation_rejects_mutated_round_before_work(self): + for field,value in (('circle_order',(1,)*7),('spaces',()),('arities',(True,)),('end_order','unknown')): + spec=self.profile().rounds[0];object.__setattr__(spec,field,value) + kwargs=dict(api=None,geometry=None,scope='x',root='0'*64,round_id=0,spec=spec) + with self.subTest(field=field),patch.object(cycle,'discover',side_effect=AssertionError('discovery before round admission')): + with self.assertRaises(n.Refused):cycle.affix(b'ABAB',**kwargs) + with self.subTest(field=field),patch.object(cycle,'decode_numeral',side_effect=AssertionError('decode before round admission')): + with self.assertRaises(n.Refused):cycle.unaffix(cycle.AFFIX_MAGIC+b'\x00',**kwargs) + + def test_valid_helpers_preserve_cycle_output(self): + p=self.profile();g=native.Geometry(SOURCES/'ucns') + for offset in (0,1,7,8,19): + profile=replace(p,corpus_bit_offset=offset) + for data in (b'',b'ABxABy',bytes(range(256))): + frame=cycle.normalize(data,b'corpus',profile) + self.assertEqual(cycle.denormalize(frame,b'corpus',profile),data) + data=b'ABxABy';root=native.ByteOrigin(data,p.scope,0,g).message_origin + kwargs=dict(api=native,geometry=g,scope=p.scope,root=root,round_id=0,spec=p.rounds[0]) + record,_=cycle.affix(data,**kwargs) + self.assertEqual(cycle.unaffix(record,**kwargs),data) + + +if __name__=='__main__':unittest.main()