quilt-spreadsheet 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Casey Digennaro / SuperInstance
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: quilt-spreadsheet
3
+ Version: 0.1.0
4
+ Summary: The Quilt IDE as spreadsheet substrate — cells as programs with hooks, double-entry bookkeeping, per-cell color namespaces, backend porting, distributed clocks
5
+ Author: Casey / SuperInstance
6
+ License: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Dynamic: license-file
14
+ Dynamic: requires-python
15
+
16
+ # quilt-spreadsheet
17
+
18
+ > **The Quilt IDE as spreadsheet substrate.**
19
+ >
20
+ > Front-end: rows × cols. Each cell is a runnable program with hooks.
21
+ > Double-entry bookkeeping tracks every pull/push. Per-cell color
22
+ > namespaces. Backend porting across scales. Distributed clocks with
23
+ > skew tolerance.
24
+
25
+ ## What this is
26
+
27
+ A runnable Python substrate where:
28
+
29
+ 1. **Front-end is a spreadsheet** — `QuiltSpreadsheet(rows, cols)`. Each cell is a `SheetCell`.
30
+ 2. **Each cell is a runnable program** with hooks (pulls wake it). No `main()`. Event-driven.
31
+ 3. **Double-entry bookkeeping** — every pull/push pair is recorded as a ledger pair. Same UUID, same timestamp.
32
+ 4. **Per-cell color namespaces** — cells can label anything anything. The substrate checks canonical agreement, not labels.
33
+ 5. **Backend porting** — different cells can use different scales/units. The backend converts at the boundary.
34
+ 6. **Distributed clocks with skew** — clocks can disagree. Time-between-events is the sync signal. Confidence rises with shared events.
35
+
36
+ ## Quick start
37
+
38
+ ```bash
39
+ # Run the canonical demo
40
+ python3 -m quilt_spreadsheet
41
+
42
+ # Or with custom dimensions
43
+ python3 -m quilt_spreadsheet --rows 4 --cols 4
44
+
45
+ # Run individual demos
46
+ PYTHONPATH=. python3 demos/demo_double_entry.py
47
+ PYTHONPATH=. python3 demos/demo_color_independent.py
48
+ PYTHONPATH=. python3 demos/demo_backend_conversion.py
49
+ PYTHONPATH=. python3 demos/demo_clock_skew.py
50
+ ```
51
+
52
+ ## Quick example
53
+
54
+ ```python
55
+ from quilt_spreadsheet.grid import QuiltSpreadsheet
56
+
57
+ sheet = QuiltSpreadsheet(rows=4, cols=4)
58
+
59
+ def adder(*, source, hook, value):
60
+ return (value or 0) + 1
61
+
62
+ def doubler(*, source, hook, value):
63
+ return (value or 0) * 2
64
+
65
+ # Place programs into cells
66
+ sheet.place_cell(0, 0, adder)
67
+ sheet.place_cell(0, 1, doubler)
68
+
69
+ # Cell (0, 0) pulls from cell (0, 1) with value=5
70
+ result = sheet.pull(0, 0, 0, 1, hook="double", value=5)
71
+
72
+ print(result)
73
+ # {'response': 10, 'pair_id': '...', 'timestamp_ns': ..., 'cell': (0, 1)}
74
+ ```
75
+
76
+ ## The 4 demos
77
+
78
+ ### 1. `demos/demo_double_entry.py`
79
+
80
+ Demonstrates the load-bearing ledger. Pulls and pushes are paired by UUID
81
+ + timestamp. Unbalanced pulls are flagged but not prevented.
82
+
83
+ ### 2. `demos/demo_color_independent.py`
84
+
85
+ Two cells with different color labels (red/blue) for the same canonical
86
+ (CRITICAL). The substrate verifies agreement at canonical level, not
87
+ labels.
88
+
89
+ ### 3. `demos/demo_backend_conversion.py`
90
+
91
+ Backend ports values between scales (celsius → fahrenheit). Gates values
92
+ below threshold. Snaps approximations to discrete grid. Time is preserved
93
+ in same units on both sides.
94
+
95
+ ### 4. `demos/demo_clock_skew.py`
96
+
97
+ Two cells share 10 events. Confidence rises asymptotically from 0.5 to 0.91.
98
+ Skew is observable but manageable.
99
+
100
+ ## Architecture
101
+
102
+ ```
103
+ QuiltSpreadsheet (rows × cols)
104
+ ├── CellClock [per-cell, private]
105
+ ├── Ledger [double-entry pair ledger]
106
+ ├── Backend [conversions, gates, snaps]
107
+ └── SheetCell (rows × cols)
108
+ ├── program: Callable (no main, runs on pull)
109
+ ├── axioms: frozenset (DNA)
110
+ ├── dials: List[float] (16 mutable state slots)
111
+ ├── color: ColorNamespace (private labels)
112
+ ├── perception: Perception (own sort/group)
113
+ ├── witness_log: List[dict]
114
+ ├── pulls: Dict[cell, List[hook]]
115
+ └── pushes: Dict[cell, List[hook]]
116
+ ```
117
+
118
+ ## The 3 commands
119
+
120
+ ```python
121
+ # 1. Place a program into a cell
122
+ sheet.place_cell(row, col, program, axioms=None)
123
+
124
+ # 2. Pull from another cell (cell A at (r0, c0) wakes cell at (r1, c1))
125
+ sheet.pull(r0, c0, r1, c1, hook='name', value=...)
126
+
127
+ # 3. Inspect state
128
+ sheet.render() # spreadsheet-shaped ascii view
129
+ sheet.summary() # substrate state dict
130
+ sheet.ledger.summary() # double-entry pair ledger
131
+ ```
132
+
133
+ ## Substrate transition
134
+
135
+ This repo is the substrate transition FROM `quilt-egg` (single cell) TO
136
+ `quilt-spreadsheet` (grid of cells with hooks). Same constants, same DNA.
137
+ More dimensions.
138
+
139
+ | quilt-egg | quilt-spreadsheet |
140
+ |---|---|
141
+ | 1 cell | rows × cols cells |
142
+ | 1 tick loop | pull-triggered wake |
143
+ | DNA + dials | DNA + dials + hooks + color + perception |
144
+ | 18 tests | 21 tests |
145
+ | 4 demos | 4 demos |
146
+
147
+ ## Layered navigation
148
+
149
+ | Layer | Where |
150
+ |---|---|
151
+ | **CANON.md** | [CANON.md](CANON.md) — what this repo is, in 24 lines |
152
+ | **README** | [README.md](README.md) — quick start, navigation |
153
+ | **Spec** | [docs/SPEC.md](docs/SPEC.md) — half-canon, half-spec |
154
+ | **Source** | [quilt_spreadsheet/](quilt_spreadsheet/) — `clock.py`, `ledger.py`, `color.py`, `backend.py`, `perception.py`, `cell.py`, `grid.py` |
155
+ | **Demos** | [demos/](demos/) — 4 runnable demos |
156
+ | **Tests** | [tests/](tests/) — 21 unit tests |
157
+ | **Origin** | [quilt-egg](../quilt-egg/) — the prior substrate |
158
+
159
+ ## Polyformalism
160
+
161
+ This substrate is canonically a polyformalism port. The canary
162
+ `fnv1a-64("café Δ 日本語") = 0x024a555471370b18d` is verified on every
163
+ Quilt port.
164
+
165
+ ## Tests
166
+
167
+ ```bash
168
+ PYTHONPATH=. python3 -m unittest discover -s tests -v
169
+ ```
170
+
171
+ 21 tests covering:
172
+ - Clock skew tolerance and pairwise confidence
173
+ - Double-entry ledger pairing (and unbalanced-pull handling)
174
+ - Per-cell color namespaces
175
+ - Backend conversions, gates, snaps
176
+ - Spreadsheet pull/push wiring
177
+ - Perception sorting/grouping/filtering
178
+
179
+ ## License
180
+
181
+ MIT — Casey / SuperInstance, Sept 23, 2026
@@ -0,0 +1,166 @@
1
+ # quilt-spreadsheet
2
+
3
+ > **The Quilt IDE as spreadsheet substrate.**
4
+ >
5
+ > Front-end: rows × cols. Each cell is a runnable program with hooks.
6
+ > Double-entry bookkeeping tracks every pull/push. Per-cell color
7
+ > namespaces. Backend porting across scales. Distributed clocks with
8
+ > skew tolerance.
9
+
10
+ ## What this is
11
+
12
+ A runnable Python substrate where:
13
+
14
+ 1. **Front-end is a spreadsheet** — `QuiltSpreadsheet(rows, cols)`. Each cell is a `SheetCell`.
15
+ 2. **Each cell is a runnable program** with hooks (pulls wake it). No `main()`. Event-driven.
16
+ 3. **Double-entry bookkeeping** — every pull/push pair is recorded as a ledger pair. Same UUID, same timestamp.
17
+ 4. **Per-cell color namespaces** — cells can label anything anything. The substrate checks canonical agreement, not labels.
18
+ 5. **Backend porting** — different cells can use different scales/units. The backend converts at the boundary.
19
+ 6. **Distributed clocks with skew** — clocks can disagree. Time-between-events is the sync signal. Confidence rises with shared events.
20
+
21
+ ## Quick start
22
+
23
+ ```bash
24
+ # Run the canonical demo
25
+ python3 -m quilt_spreadsheet
26
+
27
+ # Or with custom dimensions
28
+ python3 -m quilt_spreadsheet --rows 4 --cols 4
29
+
30
+ # Run individual demos
31
+ PYTHONPATH=. python3 demos/demo_double_entry.py
32
+ PYTHONPATH=. python3 demos/demo_color_independent.py
33
+ PYTHONPATH=. python3 demos/demo_backend_conversion.py
34
+ PYTHONPATH=. python3 demos/demo_clock_skew.py
35
+ ```
36
+
37
+ ## Quick example
38
+
39
+ ```python
40
+ from quilt_spreadsheet.grid import QuiltSpreadsheet
41
+
42
+ sheet = QuiltSpreadsheet(rows=4, cols=4)
43
+
44
+ def adder(*, source, hook, value):
45
+ return (value or 0) + 1
46
+
47
+ def doubler(*, source, hook, value):
48
+ return (value or 0) * 2
49
+
50
+ # Place programs into cells
51
+ sheet.place_cell(0, 0, adder)
52
+ sheet.place_cell(0, 1, doubler)
53
+
54
+ # Cell (0, 0) pulls from cell (0, 1) with value=5
55
+ result = sheet.pull(0, 0, 0, 1, hook="double", value=5)
56
+
57
+ print(result)
58
+ # {'response': 10, 'pair_id': '...', 'timestamp_ns': ..., 'cell': (0, 1)}
59
+ ```
60
+
61
+ ## The 4 demos
62
+
63
+ ### 1. `demos/demo_double_entry.py`
64
+
65
+ Demonstrates the load-bearing ledger. Pulls and pushes are paired by UUID
66
+ + timestamp. Unbalanced pulls are flagged but not prevented.
67
+
68
+ ### 2. `demos/demo_color_independent.py`
69
+
70
+ Two cells with different color labels (red/blue) for the same canonical
71
+ (CRITICAL). The substrate verifies agreement at canonical level, not
72
+ labels.
73
+
74
+ ### 3. `demos/demo_backend_conversion.py`
75
+
76
+ Backend ports values between scales (celsius → fahrenheit). Gates values
77
+ below threshold. Snaps approximations to discrete grid. Time is preserved
78
+ in same units on both sides.
79
+
80
+ ### 4. `demos/demo_clock_skew.py`
81
+
82
+ Two cells share 10 events. Confidence rises asymptotically from 0.5 to 0.91.
83
+ Skew is observable but manageable.
84
+
85
+ ## Architecture
86
+
87
+ ```
88
+ QuiltSpreadsheet (rows × cols)
89
+ ├── CellClock [per-cell, private]
90
+ ├── Ledger [double-entry pair ledger]
91
+ ├── Backend [conversions, gates, snaps]
92
+ └── SheetCell (rows × cols)
93
+ ├── program: Callable (no main, runs on pull)
94
+ ├── axioms: frozenset (DNA)
95
+ ├── dials: List[float] (16 mutable state slots)
96
+ ├── color: ColorNamespace (private labels)
97
+ ├── perception: Perception (own sort/group)
98
+ ├── witness_log: List[dict]
99
+ ├── pulls: Dict[cell, List[hook]]
100
+ └── pushes: Dict[cell, List[hook]]
101
+ ```
102
+
103
+ ## The 3 commands
104
+
105
+ ```python
106
+ # 1. Place a program into a cell
107
+ sheet.place_cell(row, col, program, axioms=None)
108
+
109
+ # 2. Pull from another cell (cell A at (r0, c0) wakes cell at (r1, c1))
110
+ sheet.pull(r0, c0, r1, c1, hook='name', value=...)
111
+
112
+ # 3. Inspect state
113
+ sheet.render() # spreadsheet-shaped ascii view
114
+ sheet.summary() # substrate state dict
115
+ sheet.ledger.summary() # double-entry pair ledger
116
+ ```
117
+
118
+ ## Substrate transition
119
+
120
+ This repo is the substrate transition FROM `quilt-egg` (single cell) TO
121
+ `quilt-spreadsheet` (grid of cells with hooks). Same constants, same DNA.
122
+ More dimensions.
123
+
124
+ | quilt-egg | quilt-spreadsheet |
125
+ |---|---|
126
+ | 1 cell | rows × cols cells |
127
+ | 1 tick loop | pull-triggered wake |
128
+ | DNA + dials | DNA + dials + hooks + color + perception |
129
+ | 18 tests | 21 tests |
130
+ | 4 demos | 4 demos |
131
+
132
+ ## Layered navigation
133
+
134
+ | Layer | Where |
135
+ |---|---|
136
+ | **CANON.md** | [CANON.md](CANON.md) — what this repo is, in 24 lines |
137
+ | **README** | [README.md](README.md) — quick start, navigation |
138
+ | **Spec** | [docs/SPEC.md](docs/SPEC.md) — half-canon, half-spec |
139
+ | **Source** | [quilt_spreadsheet/](quilt_spreadsheet/) — `clock.py`, `ledger.py`, `color.py`, `backend.py`, `perception.py`, `cell.py`, `grid.py` |
140
+ | **Demos** | [demos/](demos/) — 4 runnable demos |
141
+ | **Tests** | [tests/](tests/) — 21 unit tests |
142
+ | **Origin** | [quilt-egg](../quilt-egg/) — the prior substrate |
143
+
144
+ ## Polyformalism
145
+
146
+ This substrate is canonically a polyformalism port. The canary
147
+ `fnv1a-64("café Δ 日本語") = 0x024a555471370b18d` is verified on every
148
+ Quilt port.
149
+
150
+ ## Tests
151
+
152
+ ```bash
153
+ PYTHONPATH=. python3 -m unittest discover -s tests -v
154
+ ```
155
+
156
+ 21 tests covering:
157
+ - Clock skew tolerance and pairwise confidence
158
+ - Double-entry ledger pairing (and unbalanced-pull handling)
159
+ - Per-cell color namespaces
160
+ - Backend conversions, gates, snaps
161
+ - Spreadsheet pull/push wiring
162
+ - Perception sorting/grouping/filtering
163
+
164
+ ## License
165
+
166
+ MIT — Casey / SuperInstance, Sept 23, 2026
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "quilt-spreadsheet"
7
+ version = "0.1.0"
8
+ description = "The Quilt IDE as spreadsheet substrate — cells as programs with hooks, double-entry bookkeeping, per-cell color namespaces, backend porting, distributed clocks"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "Casey / SuperInstance"}]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ ]
18
+
19
+ [tool.setuptools]
20
+ packages = ["quilt_spreadsheet"]
@@ -0,0 +1,50 @@
1
+ """
2
+ quilt-spreadsheet — the IDE substrate.
3
+
4
+ A spreadsheet-as-IDE where:
5
+ - front-end looks like a spreadsheet (rows x cols)
6
+ - each cell is a runnable program with hooks
7
+ - double-entry bookkeeping tracks every pull/push pair
8
+ - per-cell color namespaces (semantic heterogeneity is OK)
9
+ - backend porting handles scale/unit conversions
10
+ - distributed clocks allow skew; time-between-events converges
11
+
12
+ This is the SUBSTRATE TRANSITION from quilt-egg (a single cell) to
13
+ quilt-spreadsheet (a grid of cells with hooks).
14
+ """
15
+
16
+ from .clock import (
17
+ CellClock, DistributedClock, get_default_clock, reset_default_clock
18
+ )
19
+ from .ledger import (
20
+ LedgerEntry, Ledger, get_default_ledger, reset_default_ledger
21
+ )
22
+ from .color import (
23
+ ColorNamespace, same_canonical, labels_matter
24
+ )
25
+ from .backend import (
26
+ Backend, get_default_backend, reset_default_backend,
27
+ convert, REGISTRY,
28
+ )
29
+ from .perception import Perception
30
+ from .cell import (
31
+ SheetCell, DOCTRINE_AXIOMS, ACTION_AXIOMS,
32
+ register_cell, reset_registry, get_cell, all_cells,
33
+ )
34
+ from .grid import QuiltSpreadsheet, place_cell
35
+
36
+ __all__ = [
37
+ "CellClock", "DistributedClock",
38
+ "get_default_clock", "reset_default_clock",
39
+ "LedgerEntry", "Ledger",
40
+ "get_default_ledger", "reset_default_ledger",
41
+ "ColorNamespace", "same_canonical", "labels_matter",
42
+ "Backend", "get_default_backend", "reset_default_backend",
43
+ "convert", "REGISTRY",
44
+ "Perception",
45
+ "SheetCell", "DOCTRINE_AXIOMS", "ACTION_AXIOMS",
46
+ "register_cell", "reset_registry", "get_cell", "all_cells",
47
+ "QuiltSpreadsheet", "place_cell",
48
+ ]
49
+
50
+ __version__ = "0.1.0"
@@ -0,0 +1,71 @@
1
+ """
2
+ __main__.py — `python -m quilt_spreadsheet` runs a demo spreadsheet.
3
+ """
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from .grid import QuiltSpreadsheet, reset_all
9
+
10
+
11
+ def demo_spreadsheet():
12
+ """Build a 4×4 substrate with 4 programs that respond to pulls."""
13
+ reset_all()
14
+ sheet = QuiltSpreadsheet(rows=4, cols=4, name="canonical_demo")
15
+
16
+ def adder(*, source, hook, value):
17
+ # Cell that adds 1 to whatever is pulled
18
+ return (value or 0) + 1
19
+
20
+ def doubler(*, source, hook, value):
21
+ return (value or 0) * 2
22
+
23
+ def labeler(*, source, hook, value):
24
+ # Cell that translates a label
25
+ return "doubled" if hook == "double" else "added"
26
+
27
+ def tally(*, source, hook, value):
28
+ # Counts how many times it has been pulled
29
+ return f"tally:{value}"
30
+
31
+ # Place four programs
32
+ sheet.place_cell(0, 0, adder)
33
+ sheet.place_cell(0, 1, doubler)
34
+ sheet.place_cell(1, 0, labeler)
35
+ sheet.place_cell(1, 1, tally)
36
+
37
+ # Wire a workflow: cell at (2, 2) pulls adder, then doubler
38
+ def workflow(*, source, hook, value):
39
+ return f"workflow[{hook}]={value}"
40
+
41
+ sheet.place_cell(2, 2, workflow)
42
+ # Pull adder
43
+ sheet.pull(2, 2, 0, 0, hook="step1", value=5)
44
+ # Pull doubler — note this uses ledger.record_pair
45
+ sheet.pull(0, 0, 0, 1, hook="double", value=5)
46
+
47
+ print(sheet.render())
48
+ print()
49
+ print("Summary:")
50
+ for k, v in sheet.summary().items():
51
+ print(f" {k}: {v}")
52
+ print()
53
+ print("Pulls and pushes recorded in the ledger (double-entry pairs).")
54
+
55
+
56
+ def main():
57
+ parser = argparse.ArgumentParser(
58
+ prog="quilt_spreadsheet",
59
+ description="The Quilt spreadsheet IDE substrate — double-entry bookkeeping, distributed clocks, per-cell color namespaces, backend porting.",
60
+ )
61
+ parser.add_argument("--rows", type=int, default=4, help="Spreadsheet rows (default: 4)")
62
+ parser.add_argument("--cols", type=int, default=4, help="Spreadsheet cols (default: 4)")
63
+ parser.add_argument("--demo", choices=["canonical"], default="canonical", help="Demo to run")
64
+ args = parser.parse_args()
65
+ if args.demo == "canonical":
66
+ demo_spreadsheet()
67
+ return 0
68
+
69
+
70
+ if __name__ == "__main__":
71
+ sys.exit(main())
@@ -0,0 +1,183 @@
1
+ """
2
+ backend.py — the porting, conversion, gating, and snapping layer.
3
+
4
+ Casey: "the true backend is the actual porting and conversions. one cell
5
+ might be using a different scale than another but if the backend can convert
6
+ so their equal exchange for their applications, it doesn't matter. the
7
+ backend can gate variables or convert approximations to snaps but the time
8
+ is in the same units on both sides."
9
+
10
+ This is the polyformalism-port layer at the substrate level. Different
11
+ cells may use:
12
+ - different scales (celsius vs fahrenheit)
13
+ - different units (ns vs ms)
14
+ - different precision (continuous vs snapped)
15
+ - different visibility (some variables are gated)
16
+
17
+ The backend DOES NOT enforce agreement. The backend PROVIDES porting
18
+ for when cells need to exchange data.
19
+ """
20
+
21
+ from typing import Any, Tuple
22
+ import math
23
+
24
+
25
+ #: The canonical conversions registry — extensible.
26
+ #: Each entry: name -> callable(value, from_unit, to_unit)
27
+ #: Time is preserved in the SAME units on both sides (per Casey).
28
+ REGISTRY = {}
29
+
30
+
31
+ def convert(name: str):
32
+ """Decorator to register a backend conversion."""
33
+ def deco(fn):
34
+ REGISTRY[name] = fn
35
+ return fn
36
+ return deco
37
+
38
+
39
+ @convert("temperature")
40
+ def convert_temperature(value: float, from_unit: str, to_unit: str) -> float:
41
+ """Convert temperature scales."""
42
+ if from_unit == to_unit:
43
+ return value
44
+ if from_unit == 'celsius' and to_unit == 'fahrenheit':
45
+ return value * 9/5 + 32
46
+ if from_unit == 'fahrenheit' and to_unit == 'celsius':
47
+ return (value - 32) * 5/9
48
+ if from_unit == 'celsius' and to_unit == 'kelvin':
49
+ return value + 273.15
50
+ raise ValueError(f"unsupported temperature conversion: {from_unit} -> {to_unit}")
51
+
52
+
53
+ @convert("time")
54
+ def convert_time(value: float, from_unit: str, to_unit: str) -> float:
55
+ """Time is preserved in the SAME units on both sides (per Casey).
56
+
57
+ Per Casey's directive: 'time is in the same units on both sides'.
58
+ This converter refuses to convert across units — it normalizes to
59
+ nanoseconds (canonical) and back.
60
+
61
+ Use ms and ns. The substrate does not negotiate time across calendars.
62
+ """
63
+ if from_unit == to_unit:
64
+ return value
65
+ if from_unit not in ('ns', 'ms', 's') or to_unit not in ('ns', 'ms', 's'):
66
+ raise ValueError(f"time must be in same units: {from_unit} vs {to_unit}")
67
+ # Casey: convert for compatibility but it's a unit-preserving op
68
+ if from_unit == 'ns' and to_unit == 'ms':
69
+ return value / 1e6
70
+ if from_unit == 'ms' and to_unit == 'ns':
71
+ return value * 1e6
72
+ if from_unit == 'ns' and to_unit == 's':
73
+ return value / 1e9
74
+ if from_unit == 's' and to_unit == 'ns':
75
+ return value * 1e9
76
+ return value
77
+
78
+
79
+ @convert("distance")
80
+ def convert_distance(value: float, from_unit: str, to_unit: str) -> float:
81
+ """Distance scaling."""
82
+ if from_unit == to_unit:
83
+ return value
84
+ if from_unit == 'm' and to_unit == 'cm':
85
+ return value * 100
86
+ if from_unit == 'cm' and to_unit == 'm':
87
+ return value / 100
88
+ raise ValueError(f"unsupported distance conversion: {from_unit} -> {to_unit}")
89
+
90
+
91
+ @convert("ratio")
92
+ def convert_ratio(value: float, from_unit: str, to_unit: str) -> float:
93
+ """Ratios are unitless — convert between percent, decimal, ppm."""
94
+ if from_unit == to_unit:
95
+ return value
96
+ if from_unit == 'decimal' and to_unit == 'percent':
97
+ return value * 100
98
+ if from_unit == 'percent' and to_unit == 'decimal':
99
+ return value / 100
100
+ if from_unit == 'ppm' and to_unit == 'decimal':
101
+ return value / 1e6
102
+ raise ValueError(f"unsupported ratio conversion: {from_unit} -> {to_unit}")
103
+
104
+
105
+ class Backend:
106
+ """The porting backend — applied at cell boundaries.
107
+
108
+ Functions: convert, gate, snap, time-aware porting.
109
+ """
110
+
111
+ def __init__(self):
112
+ self.conversions_run: int = 0
113
+
114
+ def port(self, value: Any, from_unit: str, to_unit: str,
115
+ domain: str = "ratio") -> Any:
116
+ """Port a value across scales/units in the same domain.
117
+
118
+ Default domain is 'ratio' (unitless). For physical conversions
119
+ pass domain='temperature' or 'distance'.
120
+ """
121
+ if from_unit == to_unit:
122
+ return value
123
+ if domain not in REGISTRY:
124
+ raise ValueError(f"unknown backend domain: {domain}")
125
+ self.conversions_run += 1
126
+ return REGISTRY[domain](value, from_unit, to_unit)
127
+
128
+ def gate(self, value: Any, predicate) -> Any:
129
+ """Backend 'gate' — release a value only if predicate is satisfied.
130
+
131
+ Casey: 'the backend can gate variables or convert approximations
132
+ to snaps'. The gate is controlled release/transformation.
133
+
134
+ Returns None if predicate fails. The cell receives None and can
135
+ decide what to do (default action: skip).
136
+ """
137
+ if not predicate(value):
138
+ return None
139
+ return value
140
+
141
+ def gate_below(self, value: float, threshold: float) -> Any:
142
+ """Common gate: block values below threshold."""
143
+ if value < threshold:
144
+ return None
145
+ return value
146
+
147
+ def snap(self, value: float, granularity: float) -> float:
148
+ """Approximation snap — snap to discrete grid.
149
+
150
+ Casey: 'the backend can gate variables or convert approximations
151
+ to snaps'. A snap rounds the value to the nearest multiple of
152
+ granularity.
153
+
154
+ e.g. snap(3.7, 0.5) = 3.5 (or 4.0, depending on tie-break)
155
+ """
156
+ if granularity <= 0:
157
+ return value
158
+ return round(value / granularity) * granularity
159
+
160
+ def port_time(self, value: float, from_unit: str, to_unit: str) -> float:
161
+ """Time porting — preserves same units on both sides (per Casey)."""
162
+ return REGISTRY["time"](value, from_unit, to_unit)
163
+
164
+ def stats(self) -> dict:
165
+ return {
166
+ "conversions_run": self.conversions_run,
167
+ "available_conversions": sorted(REGISTRY.keys()),
168
+ }
169
+
170
+
171
+ _default_backend = None
172
+
173
+
174
+ def get_default_backend() -> Backend:
175
+ global _default_backend
176
+ if _default_backend is None:
177
+ _default_backend = Backend()
178
+ return _default_backend
179
+
180
+
181
+ def reset_default_backend() -> None:
182
+ global _default_backend
183
+ _default_backend = None