From 9cc00771e54336d58ee377faeec87a3a33dbd4bf Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Thu, 29 Oct 2026 18:49:25 -0700 Subject: [PATCH] Add exact recursive attachment stacks, joins and replay Retain root frames and closed interiors with explicit bearing candidates. Repair descendant PID namespace mapping exposed by the full outcome gate. GPT generated; context, prompt Erin Spencer. --- README.md | 9 + docs/ATTACHMENT_STACK.md | 112 ++++++ src/ucns/attachment_codec.py | 190 ++++++++++ src/ucns/attachment_stack.py | 448 ++++++++++++++++++++++++ tests/test_attachment_stack.py | 331 +++++++++++++++++ tests/test_skill_lib_boundary_runner.py | 23 +- tools/_boundary_supervisor.py | 47 ++- 7 files changed, 1150 insertions(+), 10 deletions(-) create mode 100644 docs/ATTACHMENT_STACK.md create mode 100644 src/ucns/attachment_codec.py create mode 100644 src/ucns/attachment_stack.py create mode 100644 tests/test_attachment_stack.py diff --git a/README.md b/README.md index f7c2e9de..1cde7dcc 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,15 @@ assert s360.complete_key != s0.complete_key assert s720 == s0 ``` +## Recursive attachment stack + +The [native attachment stack](docs/ATTACHMENT_STACK.md) implements disk-depth +halving, inherited SPACE directions, origins at attachments, reversible common +and attached views, compound transport and complete graph replay. The same stack +accepts closed participants with retained interiors. Closed-bearing extraction +has two explicit unselected candidates and no default; recursive stacking itself +is specified. Use the linked examples and the complete validation gate below. + ## Build ```bash diff --git a/docs/ATTACHMENT_STACK.md b/docs/ATTACHMENT_STACK.md new file mode 100644 index 00000000..eb8b1e38 --- /dev/null +++ b/docs/ATTACHMENT_STACK.md @@ -0,0 +1,112 @@ +# Recursive attachment stack + +GPT generated; context, prompt Erin Spencer. + +This version implements the operator-specified planar attachment and halving +construction. Each halving is one disk depth. The same operation accepts a +primitive or an already closed construction. It does not require a physical +camera model, a separately allocated SPACE object, or a linguistic scale ladder. + +## Operation and exact representation + +For ordered occurrences numbered from zero, depth is `n-i` and local radius is +`R/2**(n-i)`. The local direction composes with the previous placement heading; +the previous endpoint supplies the next local origin. Appending halves earlier +parent placements and adds a newest radius `R/2`. Closed interiors retain their +own local coordinates. Parent depiction and child-local coordinates are distinct. + +`Direction` represents an exact unit complex expression: a rational turn and +zero or more normalized endpoints of retained closed constructions. `Point` +represents an exact rational linear combination of these directed rays. This +is constructive geometry, not a floating-point coordinate array or a hash used +in place of geometry. A hash indexes a complete retained construction graph. + +Expression equality combines identical terms and cancels inverse factors. It +is **not** a complete test for geometric equality between different expressions. +For example, it does not decide every cyclotomic identity. `display` is secondary +and refuses detected floating range/collapse failures; small numeric differences +or coincidence in a display are not evidence of exact equality or inequality. + +Common-origin views retain the ordered local displacement rays. Attached-origin +views are their ordered prefix sums. Subtraction recovers the rays without glyph +labels. The root origin and SPACE direction travel with each exported view. +These are recoverably related views, not generally congruent point sets. + +## Closed bearings: explicit candidate boundary + +Recursive stacking is specified. The exact direction extracted from a whole +closed construction has no selected numerical functional in the source record. +`Closed(interior, bearing)` therefore requires one explicit candidate: + +| Binding | Exact direction | Standing | +|---|---|---| +| `root-endpoint-v1` | Normalize the vector from the construction root to its endpoint | Unselected candidate | +| `exit-heading-v1` | Reuse its accumulated final placement heading | Unselected candidate | + +A finite dyadic stack's endpoint is nonzero: its final radius `R/2` exceeds +the sum of all earlier radii `R/2 - R/2**n`. Endpoint normalization is therefore +well-defined at every recursive stack level, regardless of the participant +directions. This does not prove a particular candidate is the intended bearing. +The exit-heading candidate can coincide for permuted interiors; interiors are +never replaced by that heading. No hash, centroid, separate word carrier or +terminal glyph identity is substituted for the whole closed construction. + +## Usage + +```python +from ucns.ordinal_affixiation import Carrier, Participant +from ucns.attachment_stack import Closed, stack, common_view, attached_view, recover_common, attach +from ucns.attachment_codec import export_graph, replay_graph, export_view, replay_view +from ucns.public_gonol import PUBLIC_GONOL_157, PUBLIC_GONOL_SHA256 + +carrier = Carrier(PUBLIC_GONOL_157, PUBLIC_GONOL_SHA256) +def build(text): + return stack(text, tuple(Participant.atom(carrier, g) for g in text), + relation='declared-order', source='example') + +left, right = build('bull'), build('shit') +assert recover_common(attached_view(common_view(left))) == left.rays +# Explicit compound transport: second l, zero-based occurrence 3. +incoming = attach(left, 3, right) +# Explicit candidate evaluation; no selection is implied. +sentence = stack('example', (Closed(left, 'root-endpoint-v1'), + Closed(right, 'root-endpoint-v1')), + relation='declared-sequence', source='example') +assert replay_graph(export_graph(sentence)).recover() == tuple('bullshit') +assert replay_view(export_view(sentence))[0].receipt == sentence.receipt +``` + +`attach` rigidly transports an incoming stack without recontracting its radii. +`join` closes an explicit terminal attachment as an `AttachmentJoin`, retaining +both components, their boundary and relation. A joined construction can itself +participate atomically in a later stack. Unlike a dyadic stack, a join can have +a zero endpoint. Endpoint-bearing promotion therefore requires an exact nonzero +proof: strict ray-length dominance or a pure prime-order cyclotomic coefficient +check. Zero or unsupported cases refuse that candidate; the exit-frame candidate +remains defined. UCNS does not infer compound spelling, +sentences, punctuation functions or definition ownership. + +`export_graph` includes all carriers and interiors. Replay requires the complete +canonical graph and rejects cycles, missing references, extra nodes, changed +bindings and substituted receipts. It accepts data, never executable expressions. +The historical `ordinal_affixiation` schema remains explicitly replayable; its +old shared-origin geometry is not relabeled as this new attachment construction. + +## Verification and boundary + +Run the complete outcome gate, contract audit, build and Twine checks in README. +Focused checks: `.venv/bin/python -m pytest tests/test_attachment_stack.py`. +The native suite includes all 24,336 non-SPACE Public Gonol pairs against an +independent exact cyclotomic coefficient oracle; iterative append examples; +both-view recovery and root-frame controls; closed recursion; compound transport; +deep graph replay; malformed/tampered records; and display underflow controls. + +The new CHECKS entries leave the selected-boundary executor's required timeout +as `hmmm`: no claim-relevant or externally imposed per-check deadline has been +specified. The complete suite outcome gate executes without an invented time +cutoff. Running these new checks through the separate timeout-boundary executor +requires resolving that operational field first; its refusal is not waived. + +**hmmm:** Selection of the closed-construction bearing functional; complete +geometric equivalence of different exact expressions; native Möbius lift and +sheet-state transport; domain-specific admission and closure outside this API. diff --git a/src/ucns/attachment_codec.py b/src/ucns/attachment_codec.py new file mode 100644 index 00000000..5e761f8e --- /dev/null +++ b/src/ucns/attachment_codec.py @@ -0,0 +1,190 @@ +# ratios: loc_comments=138:34 imports_exports=3:4 calls_definitions=87:8 +# === MODULE_BUILD === +# id: ucns_attachment_codec +# module_name: attachment_codec +# module_kind: adapter +# summary: complete replayable graphs and view receipts for attachment stacks +# owner: Erin Spencer +# public_surface: export_graph, replay_graph, export_view, replay_view +# internal_surface: strict exact data parsing and topological graph replay +# auth_boundary: integrity only; receipts are not producer authentication +# storage_boundary: caller owns persistence; this module performs no IO +# network_boundary: none +# user_data_boundary: caller source remains in returned records +# admin_only: false +# tests: tests/test_attachment_stack.py +# rollout: new schema; old ordinal records retain their old meanings +# rollback: remove the explicit new codec API +# requires: ucns_attachment_stack +# unresolved: cryptographic authentication remains external +# === END MODULE_BUILD === +# === CONTRACTS === +# id: attachment_complete_replay +# given: a serialized recursive stack or view +# then: replay reconstructs every carrier and closed interior and rejects tampering, missing root data, cycles, extra records and noncanonical exact values +# === END CONTRACTS === +"""Safe data-only replay of complete attachment geometry. + +GPT generated; context, prompt Erin Spencer. +Usage: ``copy = replay_graph(json.loads(json.dumps(export_graph(closed))))``. +No labels, carrier definitions or interiors are fetched from another service. +The root SPACE datum is retained; view transforms never change the closed +construction. Receipts authenticate no producer. Expressions are exact recipes, +not a claim that unrelated coordinate expressions have different values. +""" +from fractions import Fraction + +from .ordinal_affixiation import Carrier, Participant, _digest +from .attachment_stack import Closed, Direction, Point, AttachmentJoin, stack, join, common_view, attached_view + + +def _shape(value, keys): + if not isinstance(value, dict) or set(value) != set(keys): + raise ValueError('RECORD_SHAPE_MISMATCH') + + +def _rational(value): + if (not isinstance(value, list) or len(value) != 2 + or any(type(v) is not int for v in value) or value[1] <= 0): + raise ValueError('EXACT_CANONICAL_RATIONAL_REQUIRED') + number = Fraction(*value) + if [number.numerator, number.denominator] != value: + raise ValueError('EXACT_CANONICAL_RATIONAL_REQUIRED') + return number + + +def export_graph(root): + """One deterministic topological graph, retaining repeated participation.""" + nodes, carriers, seen, todo = [], {}, set(), [(root, False)] + while todo: + obj, ready = todo.pop() + if obj.receipt in seen: + continue + if not ready: + todo.append((obj, True)) + children = ((obj.right, obj.left) if isinstance(obj, AttachmentJoin) else + tuple(p.interior for p in reversed(obj.participants) if isinstance(p, Closed))) + todo.extend((child, False) for child in children) + continue + for part in (() if isinstance(obj, AttachmentJoin) else obj.participants): + if not isinstance(part, Closed): + c = part.carrier + carriers[c.receipt] = {'receipt': c.receipt, 'identities': list(c.identities), 'source': c.source} + nodes.append({'receipt': obj.receipt, 'construction': obj.record()}) + seen.add(obj.receipt) + body = {'schema': 'ucns.attachment-graph.v1', 'root': root.receipt, + 'root_space': [0, 1], 'carriers': [carriers[k] for k in sorted(carriers)], 'nodes': nodes} + return {**body, 'receipt': _digest(body)} + + +def _replay_graph(record): + _shape(record, ('schema', 'root', 'root_space', 'carriers', 'nodes', 'receipt')) + if record['schema'] != 'ucns.attachment-graph.v1' or _rational(record['root_space']) != 0: + raise ValueError('GRAPH_SCHEMA_OR_ROOT_DATUM_MISMATCH') + if not isinstance(record['carriers'], list) or not isinstance(record['nodes'], list): + raise ValueError('GRAPH_ARRAYS_REQUIRED') + carriers, nodes = {}, {} + for row in record['carriers']: + _shape(row, ('receipt', 'identities', 'source')) + if not isinstance(row['identities'], list): + raise ValueError('CARRIER_IDENTITIES_REQUIRED') + c = Carrier(tuple(row['identities']), row['source']) + if c.receipt != row['receipt'] or c.receipt in carriers: + raise ValueError('CARRIER_RECEIPT_MISMATCH') + carriers[c.receipt] = c + for row in record['nodes']: + _shape(row, ('receipt', 'construction')) + data = row['construction'] + if isinstance(data, dict) and data.get('schema') == 'ucns.attachment-join.v1': + _shape(data, ('schema', 'identity', 'relation', 'source', 'left', 'right', 'attachment_ordinal')) + if data['left'] not in nodes or data['right'] not in nodes: + raise ValueError('MISSING_CYCLIC_OR_FORWARD_INTERIOR') + obj = join(data['identity'], nodes[data['left']], nodes[data['right']], + relation=data['relation'], source=data['source']) + if (type(data['attachment_ordinal']) is not int + or data['attachment_ordinal'] != len(obj.left.rays) - 1 + or obj.receipt != row['receipt'] or obj.receipt in nodes): + raise ValueError('JOIN_RECEIPT_MISMATCH') + nodes[obj.receipt] = obj + continue + _shape(data, ('schema', 'identity', 'relation', 'source', 'radius', 'participants')) + if data['schema'] != 'ucns.attachment-stack.v1' or not isinstance(data['participants'], list): + raise ValueError('STACK_SCHEMA_MISMATCH') + participants = [] + for part in data['participants']: + if not isinstance(part, dict): + raise ValueError('PARTICIPANT_RECORD_REQUIRED') + if part.get('kind') == 'primitive': + _shape(part, ('kind', 'carrier', 'identity')) + if part['carrier'] not in carriers: + raise ValueError('CARRIER_NOT_INCLUDED') + participants.append(Participant.atom(carriers[part['carrier']], part['identity'])) + elif part.get('kind') == 'closed': + _shape(part, ('kind', 'interior', 'bearing')) + if part['interior'] not in nodes: + raise ValueError('MISSING_CYCLIC_OR_FORWARD_INTERIOR') + participants.append(Closed(nodes[part['interior']], part['bearing'])) + else: + raise ValueError('UNKNOWN_PARTICIPANT_KIND') + obj = stack(data['identity'], tuple(participants), relation=data['relation'], + source=data['source'], radius=_rational(data['radius'])) + if obj.receipt != row['receipt'] or obj.receipt in nodes: + raise ValueError('CONSTRUCTION_RECEIPT_MISMATCH') + nodes[obj.receipt] = obj + if record['root'] not in nodes: + raise ValueError('ROOT_NOT_INCLUDED') + root = nodes[record['root']] + if _digest(export_graph(root)) != _digest(record): + raise ValueError('GRAPH_REPLAY_MISMATCH') + return root, nodes + + +def replay_graph(record): + return _replay_graph(record)[0] + + +def export_view(root, *, kind='attached', space=Direction(), origin=Point()): + """Self-contained exact view with root frame and every referenced interior.""" + if kind not in ('attached', 'common'): + raise ValueError('UNKNOWN_VIEW') + graph = export_graph(root) + included = {row['receipt'] for row in graph['nodes']} + directions = (space,) + tuple(d for d, _q in origin.terms) + if any(child.receipt not in included for d in directions for child, _power in d.endpoints): + raise ValueError('VIEW_REFERENCES_EXTERNAL_INTERIOR') + rays = common_view(root, space=space) + rows = ([ray.record() for ray in rays] if kind == 'common' else + [{'origin': a.record(), 'point': b.record()} for a, b in attached_view(rays, origin=origin)]) + body = {'schema': 'ucns.attachment-view.v1', 'kind': kind, 'graph': graph, + 'space': space.record(), 'origin': origin.record(), 'rows': rows} + return {**body, 'receipt': _digest(body)} + + +def replay_view(record): + _shape(record, ('schema', 'kind', 'graph', 'space', 'origin', 'rows', 'receipt')) + if record['schema'] != 'ucns.attachment-view.v1': + raise ValueError('VIEW_SCHEMA_MISMATCH') + root, nodes = _replay_graph(record['graph']) + def direction(data): + _shape(data, ('turn', 'unit_endpoints')) + if not isinstance(data['unit_endpoints'], list): + raise ValueError('DIRECTION_FACTORS_REQUIRED') + factors = [] + for factor in data['unit_endpoints']: + if (not isinstance(factor, list) or len(factor) != 2 + or type(factor[1]) is not int or factor[0] not in nodes): + raise ValueError('INVALID_DIRECTION_FACTOR') + factors.append((nodes[factor[0]], factor[1])) + return Direction(_rational(data['turn']), tuple(factors)) + space = direction(record['space']) + if not isinstance(record['origin'], list): + raise ValueError('ORIGIN_TERMS_REQUIRED') + terms = [] + for term in record['origin']: + _shape(term, ('direction', 'coefficient')) + terms.append((direction(term['direction']), _rational(term['coefficient']))) + origin = Point(tuple(terms)) + if _digest(export_view(root, kind=record['kind'], space=space, origin=origin)) != _digest(record): + raise ValueError('VIEW_REPLAY_MISMATCH') + return root, space, origin +# ratios: loc_comments=138:34 imports_exports=3:4 calls_definitions=87:8 diff --git a/src/ucns/attachment_stack.py b/src/ucns/attachment_stack.py new file mode 100644 index 00000000..0ce38792 --- /dev/null +++ b/src/ucns/attachment_stack.py @@ -0,0 +1,448 @@ +# ratios: loc_comments=291:80 imports_exports=6:13 calls_definitions=150:49 +# === MODULE_BUILD === +# id: ucns_attachment_stack +# module_name: attachment_stack +# module_kind: engine +# summary: exact expression geometry for recursive dyadic stacks and attachment views +# owner: Erin Spencer +# public_surface: Direction, Point, Ray, Closed, DiskStack, AttachmentJoin, stack, join, common_view, attached_view, recover_common, attach, display +# internal_surface: canonical rational expression arithmetic and construction receipts +# auth_boundary: none +# storage_boundary: immutable in-memory constructions; graph codec is separate +# network_boundary: none +# user_data_boundary: caller supplied identities and sources stay in memory +# admin_only: false +# tests: tests/test_attachment_stack.py +# rollout: explicit versioned planar construction; closed-bearing candidates require a named choice +# rollback: select the historical ordinal profile explicitly; never rewrite old receipts +# requires: ordinal_affixiation_geometry +# unresolved: closed-construction bearing selection and native Mobius lift +# === END MODULE_BUILD === +# === CONTRACTS === +# id: attachment_stack_recurrence +# given: ordered primitive or explicitly bound closed participants +# then: each append halves earlier parent radii, advances their disk depths, and inherits the preceding placement heading and endpoint +# id: attachment_stack_views +# given: common-origin or attached-origin exact expressions and their root frame +# then: prefix sums and differences recover the same ordered local rays without glyph labels or a stored SPACE object +# id: attachment_stack_closure +# given: closed participants including repeated interiors +# then: parent stacking retains each occurrence and unchanged child-local geometry; no closed bearing is chosen implicitly +# id: attachment_stack_transport +# given: an explicit attachment occurrence and incoming closed stack +# then: rigid transport preserves incoming local radii and recoverable interiors without another halving +# id: attachment_stack_display_boundary +# given: exact expressions outside finite floating display range +# then: display refuses numerical collapse while exact construction remains available +# === END CONTRACTS === +"""Recursive planar attachment, with exact constructive coordinate expressions. + +GPT generated; context, prompt Erin Spencer. + +Usage:: + + from ucns.ordinal_affixiation import Carrier, Participant + c = Carrier(tuple(' ABC'), 'example-v1') + word = stack('AB', tuple(Participant.atom(c, x) for x in 'AB'), + relation='ordered', source='example') + parent = stack('AB AB', (Closed(word, 'exit-heading-v1'),) * 2, + relation='explicit-pair', source='example') + +Both closed-bearing policies are named, unselected candidates. The stack law +itself is fixed. Coordinates are exact sums of rational-length directed rays; +endpoint directions are exact normalized sums, not rounded angles. Expression +equality is NOT a complete decision procedure for geometric equality. The +optional display is lossy and never participates in construction or receipts. +No physical camera law or native Mobius/sheet transport is asserted. +""" +from __future__ import annotations + +from dataclasses import dataclass, field +from fractions import Fraction +import math + +from .ordinal_affixiation import Participant, _digest, _integer, _positive, _text + +POLICIES = ('root-endpoint-v1', 'exit-heading-v1') + + +def _fraction(value): + if isinstance(value, bool) or not isinstance(value, (int, Fraction)): + raise ValueError('exact integer or Fraction required') + return Fraction(value) + + +def _pair(value): + return [value.numerator, value.denominator] + + +@dataclass(frozen=True, slots=True, eq=False) +class Direction: + """Unit complex expression: rational turn times normalized endpoint factors. + + Endpoint references retain actual closed constructions. Multiplication and + inverse combine factors exactly; this does not equate unrelated expressions. + """ + turn: Fraction = Fraction(0) + endpoints: tuple = () + + def __post_init__(self): + turn = _fraction(self.turn) % 1 + if not isinstance(self.endpoints, tuple): + raise TypeError('endpoint factors must be a tuple') + factors = {} + for item in self.endpoints: + if not isinstance(item, tuple) or len(item) != 2: + raise ValueError('invalid endpoint factor') + interior, power = item + if not isinstance(interior, (DiskStack, AttachmentJoin)) or type(power) is not int: + raise TypeError('factor requires a closed construction and integer power') + if interior.endpoint_nonzero() is not True: + raise ValueError('ENDPOINT_DIRECTION_REQUIRES_NONZERO_PROOF') + prior = factors.get(interior.receipt, (interior, 0)) + factors[interior.receipt] = (interior, prior[1] + power) + object.__setattr__(self, 'turn', turn) + object.__setattr__(self, 'endpoints', tuple(factors[k] for k in sorted(factors) if factors[k][1])) + + @property + def key(self): + return self.turn, tuple((obj.receipt, power) for obj, power in self.endpoints) + + def __eq__(self, other): + return isinstance(other, Direction) and self.key == other.key + + def __hash__(self): + return hash(self.key) + + def compose(self, other): + if not isinstance(other, Direction): + raise TypeError('Direction required') + return Direction(self.turn + other.turn, self.endpoints + other.endpoints) + + def inverse(self): + return Direction(-self.turn, tuple((obj, -power) for obj, power in self.endpoints)) + + def record(self): + return {'turn': _pair(self.turn), + 'unit_endpoints': [[obj.receipt, power] for obj, power in self.endpoints]} + + +@dataclass(frozen=True, slots=True) +class Point: + """Exact planar vector expression; origin is the empty sum, not Structural Null.""" + terms: tuple = () + + def __post_init__(self): + if not isinstance(self.terms, tuple): + raise TypeError('terms must be a tuple') + terms = {} + for direction, coefficient in self.terms: + if not isinstance(direction, Direction): + raise TypeError('term direction must be a Direction') + terms[direction] = terms.get(direction, Fraction(0)) + _fraction(coefficient) + object.__setattr__(self, 'terms', tuple(sorted( + ((d, q) for d, q in terms.items() if q), key=lambda term: term[0].key))) + + def __add__(self, other): + if not isinstance(other, Point): + return NotImplemented + return Point(self.terms + other.terms) + + def __sub__(self, other): + return self + other.scaled(-1) + + def scaled(self, factor): + factor = _fraction(factor) + return Point(tuple((d, q * factor) for d, q in self.terms)) + + def rotated(self, direction): + return Point(tuple((d.compose(direction), q) for d, q in self.terms)) + + def record(self): + return [{'direction': d.record(), 'coefficient': _pair(q)} for d, q in self.terms] + + +@dataclass(frozen=True, slots=True) +class Ray: + radius: Fraction + heading: Direction + + def __post_init__(self): + object.__setattr__(self, 'radius', _positive(self.radius)) + if not isinstance(self.heading, Direction): + raise TypeError('ray heading requires a Direction') + + @property + def vector(self): + return Point(((self.heading, self.radius),)) + + def record(self): + return {'radius': _pair(self.radius), 'heading': self.heading.record()} + + +@dataclass(frozen=True, slots=True) +class Closed: + """An atomic participant retaining its interior and explicit candidate binding.""" + interior: DiskStack | AttachmentJoin + bearing: str + + def __post_init__(self): + if not isinstance(self.interior, (DiskStack, AttachmentJoin)): + raise TypeError('an already closed native construction is required') + if self.bearing not in POLICIES: + raise ValueError('CLOSED_BEARING_POLICY_REQUIRED') + + @property + def direction(self): + if self.bearing == 'exit-heading-v1': + return self.interior.exit_heading + # A finite dyadic stack cannot have a zero endpoint: its newest radius + # exceeds the sum of every earlier radius, regardless of directions. + return Direction(endpoints=((self.interior, 1),)) + + +@dataclass(frozen=True, slots=True, eq=False) +class DiskStack: + identity: str + participants: tuple + relation: str + source: str + radius: Fraction = Fraction(1) + receipt: str = field(init=False) + rays: tuple = field(init=False, repr=False) + + def __post_init__(self): + for name in ('identity', 'relation', 'source'): + _text(getattr(self, name), name) + object.__setattr__(self, 'radius', _positive(self.radius)) + if not isinstance(self.participants, tuple) or not self.participants: + raise ValueError('nonempty immutable participants required') + heading, rays = Direction(), [] + n = len(self.participants) + for i, part in enumerate(self.participants): + if isinstance(part, Participant) and part.interior is None: + direction = Direction(part.carrier.turn(part.identity)) + elif isinstance(part, Closed): + direction = part.direction + else: + raise TypeError('declared primitive or explicitly bound closed stack required') + heading = heading.compose(direction) + rays.append(Ray(self.radius / (1 << (n - i)), heading)) + object.__setattr__(self, 'rays', tuple(rays)) + object.__setattr__(self, 'receipt', _digest(self.record())) + + @property + def exit_heading(self): + return self.rays[-1].heading + + @property + def endpoint(self): + return sum((ray.vector for ray in self.rays), Point()) + + def depth(self, ordinal): + _integer(ordinal, 'ordinal') + if ordinal >= len(self.rays): + raise IndexError('ordinal outside stack') + return len(self.rays) - ordinal + + def append(self, participant, *, identity, source): + return stack(identity, self.participants + (participant,), + relation=self.relation, source=source, radius=self.radius) + + def recover(self): + """Iterative primitive recovery preserves multiplicity across nesting.""" + return _recover(self) + + def endpoint_nonzero(self): + return True # Strict newest-ray dominance, independently of directions. + + def record(self): + parts = [] + for part in self.participants: + if isinstance(part, Closed): + parts.append({'kind': 'closed', 'interior': part.interior.receipt, 'bearing': part.bearing}) + else: + parts.append({'kind': 'primitive', 'carrier': part.carrier.receipt, 'identity': part.identity}) + return {'schema': 'ucns.attachment-stack.v1', 'identity': self.identity, + 'relation': self.relation, 'source': self.source, + 'radius': _pair(self.radius), 'participants': parts} + + +def stack(identity, participants, *, relation, source, radius=Fraction(1)): + """Close the same ordered dyadic operation at any declared consuming scale.""" + return DiskStack(identity, participants, relation, source, radius) + + +def _recover(root): + result, pending = [], [root] + while pending: + part = pending.pop() + if isinstance(part, DiskStack): + pending.extend(reversed(part.participants)) + elif isinstance(part, AttachmentJoin): + pending.extend((part.right, part.left)) + elif isinstance(part, Closed): + pending.append(part.interior) + else: + result.append(part.identity) + return tuple(result) + + +@dataclass(frozen=True, slots=True, eq=False) +class AttachmentJoin: + """Closed terminal attachment retaining both components without contraction.""" + identity: str + left: DiskStack | AttachmentJoin + right: DiskStack | AttachmentJoin + relation: str + source: str + rays: tuple = field(init=False, repr=False) + receipt: str = field(init=False) + + def __post_init__(self): + for name in ('identity', 'relation', 'source'): + _text(getattr(self, name), name) + if not all(isinstance(x, (DiskStack, AttachmentJoin)) for x in (self.left, self.right)): + raise TypeError('closed native components required') + object.__setattr__(self, 'rays', self.left.rays + common_view(self.right, space=self.left.exit_heading)) + object.__setattr__(self, 'receipt', _digest(self.record())) + + @property + def exit_heading(self): + return self.rays[-1].heading + + @property + def endpoint(self): + return sum((ray.vector for ray in self.rays), Point()) + + def recover(self): + return _recover(self) + + def record(self): + return {'schema': 'ucns.attachment-join.v1', 'identity': self.identity, + 'relation': self.relation, 'source': self.source, + 'left': self.left.receipt, 'right': self.right.receipt, + 'attachment_ordinal': len(self.left.rays) - 1} + + def endpoint_nonzero(self): + """Exact sufficient proof; unresolved algebra never becomes a unit vector. + + Pure prime-order rational directions use Phi_p=1+X+...+X**(p-1). + Mixed/radical directions use strict triangle dominance when available. + False means proved zero; None means this implementation cannot decide. + """ + lengths = [ray.radius for ray in self.rays] + if 2 * max(lengths) > sum(lengths): + return True + if any(ray.heading.endpoints for ray in self.rays): + return None + orders = {ray.heading.turn.denominator for ray in self.rays} - {1} + if not orders: + return True + if len(orders) != 1: + return None + p = orders.pop() + if any(p % d == 0 for d in range(2, math.isqrt(p) + 1)): + return None + coefficients = [Fraction(0)] * p + for ray in self.rays: + coefficients[int(ray.heading.turn * p)] += ray.radius + return any(q != coefficients[-1] for q in coefficients[:-1]) + + +def join(identity, left, right, *, relation, source): + """Close an explicitly declared terminal join; never split domain input.""" + return AttachmentJoin(identity, left, right, relation, source) + + +def common_view(construction, *, space=Direction()): + """One displayed origin; root SPACE stays explicit in the caller's view.""" + if not isinstance(construction, (DiskStack, AttachmentJoin)): + raise TypeError('closed native construction required') + return tuple(Ray(ray.radius, space.compose(ray.heading)) for ray in construction.rays) + + +def attached_view(rays, *, origin=Point()): + """Prefix-sum transport: every row is (local origin, placed endpoint).""" + rows, point = [], origin + for ray in rays: + endpoint = point + ray.vector + rows.append((point, endpoint)) + point = endpoint + return tuple(rows) + + +def recover_common(rows, *, origin=Point()): + """Recover local rays from exact differences; no labels or reset heading.""" + rays, previous = [], origin + for start, end in rows: + if start != previous: + raise ValueError('ATTACHMENT_CHAIN_MISMATCH') + terms = (end - start).terms + if len(terms) != 1 or terms[0][1] <= 0: + raise ValueError('SINGLE_POSITIVE_RAY_REQUIRED') + rays.append(Ray(terms[0][1], terms[0][0])) + previous = end + return tuple(rays) + + +def attach(left, ordinal, right): + """Transport right at one left occurrence; returns recoverable exact rows. + + The caller retains both closed stacks and the selected occurrence. This + transport does not independently close or parse a compound relation. + """ + _integer(ordinal, 'ordinal') + if ordinal >= len(left.rays): + raise IndexError('ordinal outside construction') + anchor = attached_view(left.rays)[ordinal][1] + return attached_view(common_view(right, space=left.rays[ordinal].heading), origin=anchor) + + +def display(point): + """Lossy complex coordinate; raises on underflow/overflow/zero normalization. + + Endpoint evaluation uses a stack to avoid Python recursion depth limits. + Exact expressions and child references remain the authoritative result. + """ + if not isinstance(point, Point): + raise TypeError('Point required') + cache, work = {}, [] + for direction, _q in point.terms: + work.extend((child, False) for child, _power in direction.endpoints) + def evaluate(vector): + terms = [] + for direction, q in vector.terms: + value = float(q) + if value == 0 or not math.isfinite(value): + raise ArithmeticError('DISPLAY_RANGE; use exact expressions') + turn = float(direction.turn) + if direction.turn and (turn == 0 or turn == 1): + raise ArithmeticError('DISPLAY_RANGE; exact turn rounded to zero') + angle = math.tau * turn + factor = complex(math.cos(angle), math.sin(angle)) + for child, power in direction.endpoints: + factor *= cache[child.receipt] ** power + terms.append(value * factor) + result = complex(math.fsum(v.real for v in terms), math.fsum(v.imag for v in terms)) + if not math.isfinite(result.real) or not math.isfinite(result.imag): + raise ArithmeticError('DISPLAY_RANGE; use exact expressions') + if terms and result == 0: + raise ArithmeticError('DISPLAY_COLLAPSE; use exact expressions') + return result + while work: + child, ready = work.pop() + if child.receipt in cache: + continue + if not ready: + work.append((child, True)) + for ray in child.rays: + work.extend((obj, False) for obj, _power in ray.heading.endpoints if obj.receipt not in cache) + else: + scale = max(ray.radius for ray in child.rays) + value = evaluate(child.endpoint.scaled(1 / scale)) + if abs(value) == 0: + raise ArithmeticError('DISPLAY_ZERO_DIRECTION; exact endpoint is nonzero') + cache[child.receipt] = value / abs(value) + return evaluate(point) +# ratios: loc_comments=291:80 imports_exports=6:13 calls_definitions=150:49 diff --git a/tests/test_attachment_stack.py b/tests/test_attachment_stack.py new file mode 100644 index 00000000..0a875145 --- /dev/null +++ b/tests/test_attachment_stack.py @@ -0,0 +1,331 @@ +# ratios: loc_comments=229:75 imports_exports=11:11 calls_definitions=160:11 +# === CHECKS === +# id: check_public_pair_census +# proves: attachment_stack_recurrence, attachment_stack_views +# call: self::test_public_pair_census +# requires: python3, pytest +# timeout: hmmm +# mutates: none +# cleanup: none +# id: check_append_oracle_and_disk_depth +# proves: attachment_stack_recurrence +# call: self::test_append_oracle_and_disk_depth +# requires: python3, pytest +# timeout: hmmm +# mutates: none +# cleanup: none +# id: check_both_views_and_root_frame +# proves: attachment_stack_views, attachment_complete_replay +# call: self::test_both_views_and_root_frame +# requires: python3, pytest +# timeout: hmmm +# mutates: none +# cleanup: none +# id: check_recursive_candidates_preserve_interiors +# proves: attachment_stack_closure, attachment_stack_recurrence +# call: self::test_recursive_candidates_preserve_interiors +# requires: python3, pytest +# timeout: hmmm +# mutates: none +# cleanup: none +# id: check_no_implicit_bearing_and_candidates_differ +# proves: attachment_stack_closure +# call: self::test_no_implicit_bearing_and_candidates_differ +# requires: python3, pytest +# timeout: hmmm +# mutates: none +# cleanup: none +# id: check_compound_transport_and_wrong_occurrence +# proves: attachment_stack_transport +# call: self::test_compound_transport_and_wrong_occurrence +# requires: python3, pytest +# timeout: hmmm +# mutates: none +# cleanup: none +# id: check_complete_graph_and_mutation_controls +# proves: attachment_complete_replay +# call: self::test_complete_graph_and_mutation_controls +# requires: python3, pytest +# timeout: hmmm +# mutates: none +# cleanup: none +# id: check_deep_closure_and_display_underflow +# proves: attachment_stack_closure, attachment_stack_display_boundary +# call: self::test_deep_closure_and_display_underflow +# requires: python3, pytest +# timeout: hmmm +# mutates: none +# cleanup: none +# id: check_invalid_inputs_and_chain_mutants +# proves: attachment_stack_recurrence, attachment_stack_views +# call: self::test_invalid_inputs_and_chain_mutants +# requires: python3, pytest +# timeout: hmmm +# mutates: none +# cleanup: none +# === END CHECKS === +"""Native geometry falsifiers. Run: python -m pytest tests/test_attachment_stack.py. + +GPT generated; context, prompt Erin Spencer. +The Public Gonol pair census compares exact coefficients against an independent +cyclotomic representation. Candidate-bearing tests establish execution, not +selection. All tests use the real native constructors. +""" +from copy import deepcopy +from fractions import Fraction as F +from itertools import product +import cmath +import json +import math + +import pytest + +from ucns.ordinal_affixiation import Carrier, Participant +from ucns.public_gonol import PUBLIC_GONOL_157, PUBLIC_GONOL_SHA256 +from ucns.attachment_stack import ( + POLICIES, Closed, Direction, Point, Ray, stack, join, attached_view, + recover_common, common_view, attach, display, +) +from ucns.attachment_codec import export_graph, replay_graph, export_view, replay_view + + +def make(text, carrier=None): + carrier = carrier or Carrier(PUBLIC_GONOL_157, PUBLIC_GONOL_SHA256) + return stack(text, tuple(Participant.atom(carrier, c) for c in text), + relation='fixture-ordered', source='fixture:' + text) + + +def coefficients(point): + values = [F(0)] * 157 + for direction, q in point.terms: + assert not direction.endpoints + index = direction.turn * 157 + assert index.denominator == 1 + values[index.numerator] += q + return tuple(v - values[-1] for v in values[:-1]) + + +def test_public_pair_census(): + carrier = Carrier(PUBLIC_GONOL_157, PUBLIC_GONOL_SHA256) + for a, b in product(range(1, 157), repeat=2): + obj = make(carrier.identities[a] + carrier.identities[b], carrier) + expected = [F(0)] * 157 + expected[a] += F(1, 4) + expected[(a + b) % 157] += F(1, 2) + assert coefficients(obj.endpoint) == tuple(v - expected[-1] for v in expected[:-1]) + rows = attached_view(obj.rays) + assert rows[1][0] == rows[0][1] + assert recover_common(rows) == obj.rays + assert obj.rays[0].heading.turn == F(a, 157) + assert obj.rays[1].heading.turn == F((a + b) % 157, 157) + assert obj.recover() == (carrier.identities[a], carrier.identities[b]) + + +def test_append_oracle_and_disk_depth(): + carrier = Carrier(tuple('ABCD'), 'quarter-turn-fixture') + for n in range(1, 6): + for text in product('ABC', repeat=n): + obj = make(''.join(text), carrier) + oracle, heading = [], 0.0 + for glyph in text: + oracle = [p / 2 for p in oracle] + heading += carrier.identities.index(glyph) * math.pi / 2 + oracle.append((oracle[-1] if oracle else 0) + .5 * cmath.exp(1j * heading)) + actual = attached_view(obj.rays) + for (origin, endpoint), expected in zip(actual, oracle): + assert abs(display(endpoint) - expected) < 2e-14 + assert [obj.depth(i) for i in range(n)] == list(range(n, 0, -1)) + if n > 1: + previous = make(''.join(text[:-1]), carrier) + appended = previous.append(Participant.atom(carrier, text[-1]), identity=obj.identity, source=obj.source) + assert appended.receipt == obj.receipt + assert tuple(ray.radius for ray in obj.rays[:-1]) == tuple(ray.radius / 2 for ray in previous.rays) + + +def test_both_views_and_root_frame(): + obj = make('lll') + space = Direction(F(7, 157)) + origin = Point(((Direction(F(1, 4)), F(3, 2)),)) + common = common_view(obj, space=space) + rows = attached_view(common, origin=origin) + assert recover_common(rows, origin=origin) == common + previous = space + for ray, glyph in zip(recover_common(rows, origin=origin), obj.participants): + local = ray.heading.compose(previous.inverse()) + assert local == Direction(glyph.carrier.turn(glyph.identity)) + previous = ray.heading + assert common != obj.rays + for kind in ('common', 'attached'): + packet = json.loads(json.dumps(export_view(obj, kind=kind, space=space, origin=origin))) + rebuilt, s, o = replay_view(packet) + assert rebuilt.receipt == obj.receipt and s == space and o == origin + changed = deepcopy(packet) + changed['space']['turn'] = [8, 157] + with pytest.raises(ValueError): + replay_view(changed) + del packet['space'] + with pytest.raises(ValueError): + replay_view(packet) + # Moving every root leaves local displacements recoverable, not congruent + # to a view which moves each local origin to the same displayed point. + common_distance = abs(display(common[1].vector - common[0].vector)) + attached_distance = abs(display(rows[1][1] - rows[0][1])) + assert not math.isclose(common_distance, attached_distance) + + +def test_recursive_candidates_preserve_interiors(): + word, other = make('bull'), make('shit') + before = export_graph(word) + for policy in POLICIES: + parent = stack('fixture-parent', (Closed(word, policy), Closed(other, policy)), + relation='declared-sequence', source='fixture') + after = parent.append(Closed(word, policy), identity='fixture-three', source='fixture') + assert after.recover() == tuple('bullshitbull') + assert [after.depth(i) for i in range(3)] == [3, 2, 1] + assert after.participants[0].interior is after.participants[2].interior is word + assert export_graph(word) == before + for old, new in zip(parent.rays, after.rays): + assert old.heading == new.heading and new.radius == old.radius / 2 + assert recover_common(attached_view(after.rays)) == after.rays + higher = stack('next-scale', (Closed(after, policy), Closed(parent, policy)), + relation='declared-recursion', source='fixture') + assert replay_graph(export_graph(higher)).recover() == higher.recover() + for kind in ('common', 'attached'): + assert replay_view(export_view(higher, kind=kind))[0].receipt == higher.receipt + assert math.isfinite(abs(display(higher.endpoint))) + + +def test_no_implicit_bearing_and_candidates_differ(): + c = Carrier(tuple('ABCD'), 'quarter-turn-fixture') + word = make('AB', c) # endpoint=1/4 + i/2; exit heading=i + with pytest.raises(TypeError): + Closed(word) + with pytest.raises(ValueError, match='CLOSED_BEARING_POLICY_REQUIRED'): + Closed(word, 'automatic') + values = [] + for policy in POLICIES: + result = stack('parent', (Closed(word, policy),), relation='fixture', source='fixture') + values.append(display(result.endpoint)) + assert abs(values[0] - (.25 + .5j) / abs(.25 + .5j) / 2) < 1e-14 + assert abs(values[1] - .5j) < 1e-14 + assert abs(values[0] - values[1]) > .1 + assert make('AB', c).exit_heading == make('BA', c).exit_heading + assert make('AB', c).receipt != make('BA', c).receipt + + +def test_compound_transport_and_wrong_occurrence(): + left, right = make('bull'), make('shit') + rows = attach(left, 3, right) + left_rows = attached_view(left.rays) + assert rows[0][0] == left_rows[3][1] + assert rows[0][0] != left_rows[2][1] + assert rows != attach(left, 2, right) + local = recover_common(rows, origin=left_rows[3][1]) + assert [r.radius for r in local] == [r.radius for r in right.rays] + for transformed, original in zip(local, right.rays): + assert transformed.heading.compose(left.exit_heading.inverse()) == original.heading + assert [r.radius / 2 for r in local] != [r.radius for r in right.rays] + assert right.recover() == tuple('shit') and left.recover() == tuple('bull') + compound = join('compound', left, right, relation='declared-join', source='fixture') + assert compound.left is left and compound.right is right + assert attached_view(compound.rays)[4:] == rows + assert compound.endpoint_nonzero() is True + for policy in POLICIES: + parent = stack('compound-parent', (Closed(compound, policy), Closed(left, policy)), relation='fixture', source='fixture') + assert replay_graph(export_graph(parent)).recover() == tuple('bullshitbull') + assert abs(display(parent.endpoint)) > 0 + assert replay_view(export_view(parent))[0].receipt == parent.receipt + # A closed join can have a zero endpoint; unlike a dyadic stack, it cannot + # be normalized without a nonzero proof. Its exit frame still exists. + carrier = Carrier(('A', 'B'), 'half-turn-fixture') + zero = join('zero', make('A', carrier), make('B', carrier), relation='join', source='fixture') + assert zero.endpoint_nonzero() is False + with pytest.raises(ValueError, match='NONZERO_PROOF'): + stack('invalid', (Closed(zero, 'root-endpoint-v1'),), relation='fixture', source='fixture') + assert stack('exit', (Closed(zero, 'exit-heading-v1'),), relation='fixture', source='fixture').recover() == ('A', 'B') + + +def test_complete_graph_and_mutation_controls(): + word = make('word') + parent = stack('repeated', (Closed(word, 'exit-heading-v1'),) * 2, relation='pair', source='fixture') + packet = json.loads(json.dumps(export_graph(parent))) + rebuilt = replay_graph(packet) + assert rebuilt.participants[0].interior is rebuilt.participants[1].interior + assert rebuilt.recover() == tuple('wordword') + mutants = [] + for mutate in ( + lambda x: x.pop('root_space'), + lambda x: x.update(root_space=[False, True]), + lambda x: x['nodes'].pop(0), + lambda x: x['nodes'].append(x['nodes'][0]), + lambda x: x['nodes'][-1]['construction']['participants'][0].update(interior=x['root']), + lambda x: x['nodes'][-1]['construction']['participants'].pop(), + lambda x: x['nodes'][-1]['construction'].update(radius=[2, 2]), + lambda x: x['nodes'][-1]['construction']['participants'][0].update(bearing='root-endpoint-v1'), + lambda x: x['carriers'][0]['identities'].reverse(), + ): + bad = deepcopy(packet) + mutate(bad) + mutants.append(bad) + for bad in mutants: + with pytest.raises(ValueError): + replay_graph(bad) + view = export_view(parent) + for mutate in ( + lambda x: x['rows'][1].update(origin=x['rows'][0]['origin']), + lambda x: x['rows'][0]['point'][0].update(coefficient=[1, 1]), + lambda x: x['space'].update(turn=[False, True]), + ): + bad = deepcopy(view) + mutate(bad) + with pytest.raises(ValueError): + replay_view(bad) + + +def test_deep_closure_and_display_underflow(): + obj = make('A') + for i in range(1100): + obj = stack(str(i), (Closed(obj, 'exit-heading-v1'),), relation='nested', source='fixture') + assert obj.recover() == ('A',) + assert replay_graph(export_graph(obj)).recover() == ('A',) + endpoint_nested = make('AB', Carrier(tuple('ABCD'), 'quarter-turn-fixture')) + for i in range(1100): + endpoint_nested = stack(str(i), (Closed(endpoint_nested, 'root-endpoint-v1'),), relation='nested', source='fixture') + assert replay_graph(export_graph(endpoint_nested)).recover() == ('A', 'B') + assert abs(display(endpoint_nested.endpoint)) > 0 + primitive = make('A', Carrier(('A', 'B'), 'half-turn-fixture')) + joined = primitive + for i in range(1100): + joined = join(str(i), joined, primitive, relation='join', source='fixture') + assert replay_graph(export_graph(joined)).recover() == ('A',) * 1101 + assert display(joined.endpoint) == complex(1101 / 2, 0) + parent = stack('joined-parent', (Closed(joined, 'root-endpoint-v1'),), relation='fixture', source='fixture') + assert display(parent.endpoint) == complex(.5, 0) + deep = make('l' * 1076) + assert deep.rays[0].radius == F(1, 1 << 1076) + assert deep.depth(0) == 1076 + with pytest.raises(ArithmeticError, match='DISPLAY_RANGE'): + display(deep.rays[0].vector) + tiny_turn = Point(((Direction(), F(1)), (Direction(F(1, 1 << 1076)), F(-1)))) + with pytest.raises(ArithmeticError, match='DISPLAY_RANGE'): + display(tiny_turn) + + +def test_invalid_inputs_and_chain_mutants(): + word = make('ll') + carrier = word.participants[0].carrier + for invalid in ((), list(word.participants), (Closed,), (object(),)): + with pytest.raises((ValueError, TypeError)): + stack('bad', invalid, relation='fixture', source='fixture') + for invalid in (True, 1.0, 0, -1): + with pytest.raises(ValueError): + stack('bad', word.participants, relation='fixture', source='fixture', radius=invalid) + for invalid in (True, 1.5, -1): + with pytest.raises(ValueError): + word.depth(invalid) + rows = attached_view(word.rays) + with pytest.raises(ValueError, match='ATTACHMENT_CHAIN_MISMATCH'): + recover_common((rows[0], (Point(), rows[1][1]))) + reset = (word.rays[0], Ray(word.rays[1].radius, word.rays[0].heading)) + assert attached_view(reset) != rows +# ratios: loc_comments=229:75 imports_exports=11:11 calls_definitions=160:11 diff --git a/tests/test_skill_lib_boundary_runner.py b/tests/test_skill_lib_boundary_runner.py index 201639c1..4ee1326f 100644 --- a/tests/test_skill_lib_boundary_runner.py +++ b/tests/test_skill_lib_boundary_runner.py @@ -1,4 +1,4 @@ -# ratios: loc_comments=195:410 imports_exports=23:18 calls_definitions=248:20 +# ratios: loc_comments=195:429 imports_exports=23:18 calls_definitions=264:20 # === CHECKS === # id: check_boundary_runner_audit_gate # proves: boundary_runner_audits_before_execution @@ -584,6 +584,25 @@ def test_background_descendants_block_acceptance(tmp_path: Path) -> None: task.mkdir(parents=True) (task / "children").write_text("101 104\n") assert _owned_children(proc) == [101, 104] + # procfs may be mounted in an ancestor namespace. Parent comparison uses + # procfs IDs; kill/wait targets must use this supervisor's namespace IDs. + (proc / 'self/status').write_text(f'Pid:\t70000\nNSpid:\t70000 {os.getpid()}\n') + (proc / '101/stat').write_text('101 (owned) S 70000 0 0 0\n') + mapped_one, mapped_two = os.getpid() + 101, os.getpid() + 104 + (proc / '101/status').write_text(f'Pid:\t101\nNSpid:\t101 {mapped_one} 1\n') + assert _owned_children(proc) == [mapped_one] + ancestor_task = proc / 'self/task/70000' + ancestor_task.mkdir() + (ancestor_task / 'children').write_text('101 104\n') + (proc / '104').mkdir() + (proc / '104/status').write_text(f'Pid:\t104\nNSpid:\t104 {mapped_two}\n') + assert _owned_children(proc) == [mapped_one, mapped_two] + (proc / '104/status').write_text('Pid:\t104\nNSpid:\t104\n') + with pytest.raises(OSError, match='namespace'): + _owned_children(proc) + (proc / 'self/status').write_text('Pid:\t70000\nNSpid:\t70000 123456789\n') + with pytest.raises(OSError, match='namespace'): + _owned_children(proc) child = "import time; from pathlib import Path; time.sleep(30); Path('src/pkg/feature.py').write_text('late mutation')" body = f"import subprocess, sys\nfrom pathlib import Path\ndef test_probe():\n p=subprocess.Popen([sys.executable, '-c', {child!r}], start_new_session=True)\n Path('child.pid').write_text(str(p.pid))\n" root = _repo(tmp_path, body, [{"id": "check_probe", "function": "test_probe"}]) @@ -653,4 +672,4 @@ def test_node24_capability_runs_typescript_witness(tmp_path: Path) -> None: assert receipt["outcomes"][0]["status"] == "ERROR", receipt with pytest.raises(ProcessLookupError): os.kill(int(pid_path.read_text()), 0) -# ratios: loc_comments=195:410 imports_exports=23:18 calls_definitions=248:20 +# ratios: loc_comments=195:429 imports_exports=23:18 calls_definitions=264:20 diff --git a/tools/_boundary_supervisor.py b/tools/_boundary_supervisor.py index 8ee93802..a727553b 100644 --- a/tools/_boundary_supervisor.py +++ b/tools/_boundary_supervisor.py @@ -1,4 +1,4 @@ -# ratios: loc_comments=79:32 imports_exports=8:1 calls_definitions=32:5 +# ratios: loc_comments=105:36 imports_exports=8:1 calls_definitions=48:5 # === MODULE_BUILD === # id: boundary_process_supervisor # module_name: _boundary_supervisor @@ -49,22 +49,53 @@ def _enable_descendant_reaping() -> None: def _owned_children(proc_root: Path = Path("/proc")) -> list[int]: - """Use the optional task list, falling back to ordinary procfs parent IDs.""" - parent = os.getpid() + """Find owned children in procfs and translate them to caller-namespace IDs. + + A procfs mount can describe an ancestor PID namespace. Its stat/children + numbers must never be compared to getpid or passed directly to kill/waitpid. + The synthetic proc_root used by fixtures may omit status for local-ID data. + """ + local_parent = os.getpid() + try: + fields = dict(line.split(":", 1) for line in (proc_root / "self/status").read_text().splitlines() if ":" in line) + except FileNotFoundError: + if proc_root == Path("/proc"): + raise OSError("procfs cannot establish the supervisor PID namespace") + fields = {} # Local-ID synthetic snapshot; never the live default. + parent = int(fields.get("Pid", local_parent)) + namespace_pids = tuple(int(p) for p in fields.get("NSpid", str(parent)).split()) + if not namespace_pids or namespace_pids[0] != parent or namespace_pids[-1] != local_parent: + raise OSError("procfs PID namespace does not resolve to this supervisor") + namespace_index = len(namespace_pids) - 1 try: - return [int(pid) for pid in (proc_root / f"self/task/{parent}/children").read_text().split()] + raw_children = {int(pid) for pid in (proc_root / f"self/task/{parent}/children").read_text().split()} except FileNotFoundError: - children = [] + raw_children = set() for process in proc_root.iterdir(): if not process.name.isdecimal(): continue try: state = (process / "stat").read_text().rpartition(")")[2].split() if int(state[1]) == parent: - children.append(int(process.name)) + raw_children.add(int(process.name)) except (FileNotFoundError, ProcessLookupError, PermissionError): continue # Exited processes or unrelated users hidden by procfs. - return children + children = set() + for pid in raw_children: + try: + child_fields = dict(line.split(":", 1) for line in (proc_root / str(pid) / "status").read_text().splitlines() if ":" in line) + except (FileNotFoundError, ProcessLookupError): + if fields: + continue # Exited before namespace translation. + child_fields = {} # Synthetic local-ID fixture. + child_pids = tuple(int(p) for p in child_fields.get("NSpid", str(pid)).split()) + if len(child_pids) <= namespace_index or child_pids[0] != pid: + raise OSError("owned child PID namespace cannot be resolved") + local_pid = child_pids[namespace_index] + if local_pid <= 0 or local_pid == local_parent: + raise OSError("invalid owned child PID mapping") + children.add(local_pid) + return sorted(children) def _reap_descendants() -> int: @@ -125,4 +156,4 @@ def main() -> int: if __name__ == "__main__": raise SystemExit(main()) -# ratios: loc_comments=79:32 imports_exports=8:1 calls_definitions=32:5 +# ratios: loc_comments=105:36 imports_exports=8:1 calls_definitions=48:5