quilt-spreadsheet 0.1.0__py3-none-any.whl

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,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
@@ -0,0 +1,231 @@
1
+ """
2
+ cell.py — the SheetCell.
3
+
4
+ A SheetCell is:
5
+ - Addressable by (row, col)
6
+ - Has a program (a Python callable — "a python program might sit in a cell
7
+ as a shell command ready to run")
8
+ - Has DNA (alignment seed)
9
+ - Has hooks to other cells (pull/push wake-up)
10
+ - Has its own color/label namespace
11
+ - Has its own perception (sorts the spreadsheet around it)
12
+
13
+ Casey: "there's no central program within the cell's instance, it's just
14
+ a program running so if [pulls happen], it runs."
15
+
16
+ The cell has NO main(). The cell's program runs ONLY when it is woken
17
+ (by pull or by push). The cell's program has the signature:
18
+ program(*, source, hook, value) -> response_value
19
+ """
20
+
21
+ from typing import Tuple, Optional, Callable, Dict, Any, List
22
+ import uuid
23
+
24
+ from .color import ColorNamespace
25
+ from .perception import Perception
26
+ from .clock import get_default_clock
27
+ from .ledger import get_default_ledger
28
+ from .backend import get_default_backend
29
+
30
+
31
+ #: The 5 bedrock doctrine axioms (substrate walker canon line)
32
+ DOCTRINE_AXIOMS = frozenset({
33
+ 'cells_are_scars',
34
+ 'witness_log_is_prediction',
35
+ 'oracle_is_heard',
36
+ 'canon_gate_is_chord',
37
+ 'substrate_quantum',
38
+ })
39
+
40
+ #: The action axioms — what a cell CAN do
41
+ ACTION_AXIOMS = frozenset({
42
+ 'pull', # pull from another cell
43
+ 'push', # push to another cell
44
+ 'witness', # record to the witness log
45
+ 'perceive', # sort the spreadsheet around it
46
+ 'label', # assign a color label
47
+ 'convert', # port values through backend
48
+ 'snap', # approximate-snap values
49
+ })
50
+
51
+
52
+ class SheetCell:
53
+ """A cell in the Quilt spreadsheet substrate.
54
+
55
+ No main(). The cell RUNS when pulled. The cell RESPONDS when pushed.
56
+ Between events, the cell is dormant.
57
+ """
58
+
59
+ NUM_DIALS = 16
60
+
61
+ __slots__ = (
62
+ 'row', 'col', 'rank', 'program', 'axioms',
63
+ 'dials', 'color', 'perception', 'witness_log',
64
+ 'pulls', 'pushes', 'hooks_named',
65
+ 'awake_count', 'last_event_ns',
66
+ )
67
+
68
+ def __init__(
69
+ self,
70
+ row: int,
71
+ col: int,
72
+ program: Callable,
73
+ axioms: Optional[frozenset] = None,
74
+ ):
75
+ if not callable(program):
76
+ raise TypeError(f"program must be callable, got {type(program)}")
77
+ if axioms is None:
78
+ axioms = frozenset(DOCTRINE_AXIOMS | ACTION_AXIOMS)
79
+ self.row = row
80
+ self.col = col
81
+ self.rank: Tuple[int, int] = (row, col)
82
+ self.program = program
83
+ self.axioms = axioms
84
+ self.dials = [0.0] * self.NUM_DIALS
85
+ self.color = ColorNamespace(self.rank)
86
+ self.perception = Perception(self.rank)
87
+ self.witness_log: List[dict] = []
88
+ self.pulls: Dict[Tuple[int, int], List[str]] = {} # source -> [hooks]
89
+ self.pushes: Dict[Tuple[int, int], List[str]] = {} # target -> [hooks]
90
+ self.hooks_named: Dict[str, Any] = {} # named hooks -> last known values
91
+ self.awake_count: int = 0
92
+ self.last_event_ns: int = 0
93
+
94
+ def permits(self, action: str) -> bool:
95
+ return action in self.axioms
96
+
97
+ def label(self, label: str, canonical_value: Any) -> None:
98
+ """Assign a private color label to a canonical state."""
99
+ if not self.permits('label'):
100
+ return
101
+ self.color.assign(label, canonical_value)
102
+
103
+ def ingest_observation(self, item: dict) -> None:
104
+ """Add a raw observation to this cell's perception."""
105
+ if not self.permits('perceive'):
106
+ return
107
+ self.perception.ingest([item])
108
+
109
+ def on_pull(
110
+ self,
111
+ source: Tuple[int, int],
112
+ hook: str,
113
+ value: Any = None,
114
+ timestamp_ns: int = 0,
115
+ ) -> dict:
116
+ """Wake the cell — a pull came in.
117
+
118
+ The cell RUNS its program with the pull as input.
119
+ Returns the cell's response (the push).
120
+ """
121
+ clock = get_default_clock()
122
+ ledger = get_default_ledger()
123
+ ts = clock.tick(self.rank)
124
+ if timestamp_ns == 0:
125
+ timestamp_ns = ts
126
+ # Record the pull on the ledger
127
+ pull_entry = ledger.record_pull(
128
+ source=source,
129
+ target=self.rank,
130
+ hook=hook,
131
+ value=value,
132
+ timestamp_ns=timestamp_ns,
133
+ )
134
+ # Run the program (the cell has no main — it runs here)
135
+ try:
136
+ if not self.permits('witness'):
137
+ response = None
138
+ else:
139
+ response = self.program(
140
+ source=source,
141
+ hook=hook,
142
+ value=value,
143
+ )
144
+ except Exception as e:
145
+ response = {"_error": str(e), "_hook": hook, "_source": source}
146
+ # Record the witness
147
+ self.witness_log.append({
148
+ 'event': 'pull',
149
+ 'source': source,
150
+ 'hook': hook,
151
+ 'value': value,
152
+ 'response': response,
153
+ 'timestamp_ns': timestamp_ns,
154
+ })
155
+ self.awake_count += 1
156
+ self.last_event_ns = timestamp_ns
157
+ # Track pair sync between cells
158
+ clock.track_pair_sync(source, self.rank)
159
+ # Record push on ledger (paired with the pull)
160
+ if response is not None:
161
+ ledger.record_push(
162
+ source=self.rank,
163
+ target=source,
164
+ hook=hook,
165
+ value=response,
166
+ timestamp_ns=ts,
167
+ pair_id=pull_entry.pair_id,
168
+ )
169
+ self.pushes.setdefault(source, []).append(hook)
170
+ if source not in self.pulls:
171
+ self.pulls[source] = []
172
+ self.pulls[source].append(hook)
173
+ return {
174
+ 'response': response,
175
+ 'pair_id': pull_entry.pair_id,
176
+ 'timestamp_ns': ts,
177
+ 'cell': self.rank,
178
+ }
179
+
180
+ def pull_from(
181
+ self,
182
+ target: Tuple[int, int],
183
+ hook: str,
184
+ value: Any = None,
185
+ ) -> dict:
186
+ """Pull from another cell. The target wakes up via on_pull."""
187
+ clock = get_default_clock()
188
+ ts = clock.tick(self.rank)
189
+ # Find the target
190
+ target_cell = _find_cell_global(target)
191
+ if target_cell is None:
192
+ return {'error': 'target cell not found', 'target': target}
193
+ # Wake the target
194
+ result = target_cell.on_pull(
195
+ source=self.rank,
196
+ hook=hook,
197
+ value=value,
198
+ timestamp_ns=ts,
199
+ )
200
+ # Track sync
201
+ clock.track_pair_sync(self.rank, target)
202
+ return result
203
+
204
+ def __repr__(self):
205
+ awake = 'awake' if self.awake_count > 0 else 'dormant'
206
+ return (f"SheetCell(rank={self.rank}, axioms={len(self.axioms)}, "
207
+ f"awake={self.awake_count}, status={awake})")
208
+
209
+
210
+ # A module-level cell registry for the spreadsheet substrate
211
+ _cell_registry: Dict[Tuple[int, int], SheetCell] = {}
212
+
213
+
214
+ def register_cell(cell: SheetCell) -> None:
215
+ _cell_registry[cell.rank] = cell
216
+
217
+
218
+ def _find_cell_global(rank: Tuple[int, int]) -> Optional[SheetCell]:
219
+ return _cell_registry.get(rank)
220
+
221
+
222
+ def reset_registry() -> None:
223
+ _cell_registry.clear()
224
+
225
+
226
+ def get_cell(rank: Tuple[int, int]) -> Optional[SheetCell]:
227
+ return _cell_registry.get(rank)
228
+
229
+
230
+ def all_cells() -> List[SheetCell]:
231
+ return list(_cell_registry.values())
@@ -0,0 +1,130 @@
1
+ """
2
+ clock.py — distributed clocks with skew tolerance.
3
+
4
+ Each cell has its own clock. Clocks may disagree. The substrate does NOT
5
+ require clocks to agree. The substrate DOES require that the time between
6
+ events be measurable.
7
+
8
+ Casey: "even if their clocks don't agree, the time between events extrapolates
9
+ the agreement with more and more confidence the longer they are in sync."
10
+
11
+ This is the load-bearing insight: clock synchronization is EMERGENT, not
12
+ required. Each cell's clock is private. The substrate walker tracks:
13
+ - event timestamps (relative to the cell's t0)
14
+ - delta between events (the time between events)
15
+ - confidence score (rises the longer two cells have shared events)
16
+ """
17
+
18
+ import time
19
+ from typing import Dict, Tuple, Optional
20
+
21
+
22
+ class CellClock:
23
+ """A single cell's clock. Starts at cell's birth (t0)."""
24
+
25
+ __slots__ = ('t0', 'last_event_ns', 'event_count')
26
+
27
+ def __init__(self):
28
+ self.t0: Optional[int] = None
29
+ self.last_event_ns: Optional[int] = None
30
+ self.event_count: int = 0
31
+
32
+ def tick(self) -> int:
33
+ """Advance the clock. Returns the cell-local time in nanoseconds."""
34
+ if self.t0 is None:
35
+ self.t0 = time.monotonic_ns()
36
+ now = time.monotonic_ns()
37
+ self.last_event_ns = now
38
+ self.event_count += 1
39
+ return now - self.t0
40
+
41
+
42
+ class DistributedClock:
43
+ """A coordinator of cell clocks. Skew is allowed. Convergence is emergent."""
44
+
45
+ def __init__(self):
46
+ self._clocks: Dict[Tuple[int, int], CellClock] = {}
47
+ # Pairwise confidence: tracks how long two cells have been in sync
48
+ # {(a, b): confidence_score}, where confidence rises the longer
49
+ # the pair has shared events.
50
+ self._pairwise_time: Dict[Tuple, int] = {}
51
+ self._pairwise_event_count: Dict[Tuple, int] = {}
52
+
53
+ def tick(self, cell_rank: Tuple[int, int]) -> int:
54
+ """Tick the named cell's clock. Returns cell-local time in ns."""
55
+ if cell_rank not in self._clocks:
56
+ self._clocks[cell_rank] = CellClock()
57
+ return self._clocks[cell_rank].tick()
58
+
59
+ def cell_local_time(self, cell_rank: Tuple[int, int]) -> int:
60
+ """Read the cell-local time without ticking."""
61
+ clk = self._clocks.get(cell_rank)
62
+ if clk is None or clk.t0 is None:
63
+ return 0
64
+ return time.monotonic_ns() - clk.t0
65
+
66
+ def time_between_events(self, ts_a: int, ts_b: int) -> int:
67
+ """The time between two events.
68
+
69
+ Casey: "the time between events extrapolates the agreement"
70
+ The DELTA is the substrate's measurement. Clocks can disagree
71
+ about absolute time; they cannot disagree about deltas (if events
72
+ are time-stamped close enough).
73
+
74
+ Returns absolute value so order doesn't matter.
75
+ """
76
+ return abs(ts_b - ts_a)
77
+
78
+ def track_pair_sync(self, cell_a: Tuple[int, int], cell_b: Tuple[int, int]) -> None:
79
+ """Record that two cells have shared another event.
80
+
81
+ Every shared event raises the pairwise confidence. The longer
82
+ cells are in sync, the more confident we are that time-between-
83
+ events extrapolates.
84
+ """
85
+ key = tuple(sorted((cell_a, cell_b)))
86
+ self._pairwise_time[key] = self._pairwise_time.get(key, 0) + 1
87
+ self._pairwise_event_count[key] = self._pairwise_event_count.get(key, 0) + 1
88
+
89
+ def pair_confidence(self, cell_a: Tuple[int, int], cell_b: Tuple[int, int]) -> float:
90
+ """Confidence score for the time-between-events agreement.
91
+
92
+ Confidence rises as the number of shared events rises.
93
+ Capped at [0, 1].
94
+ """
95
+ key = tuple(sorted((cell_a, cell_b)))
96
+ count = self._pairwise_event_count.get(key, 0)
97
+ # asymptotic: confidence = 1 - 1/(count+1)
98
+ if count == 0:
99
+ return 0.0
100
+ return min(1.0, 1.0 - 1.0 / (count + 1))
101
+
102
+ def skew_estimate(self, cell_a: Tuple[int, int], cell_b: Tuple[int, int]) -> int:
103
+ """Estimate the clock skew between two cells.
104
+
105
+ Skew is computed by averaging per-event deltas — but we're clocking
106
+ each cell independently, so skew emerges as the difference in
107
+ 'cell-local time' at the same wall moment.
108
+
109
+ Returns estimated skew in nanoseconds. Positive = B's clock is ahead.
110
+ """
111
+ local_a = self.cell_local_time(cell_a)
112
+ local_b = self.cell_local_time(cell_b)
113
+ return local_b - local_a
114
+
115
+
116
+ # A module-level singleton — the substrate uses one clock
117
+ _default_clock = None
118
+
119
+
120
+ def get_default_clock() -> DistributedClock:
121
+ global _default_clock
122
+ if _default_clock is None:
123
+ _default_clock = DistributedClock()
124
+ return _default_clock
125
+
126
+
127
+ def reset_default_clock() -> None:
128
+ """Reset for testing."""
129
+ global _default_clock
130
+ _default_clock = None
@@ -0,0 +1,90 @@
1
+ """
2
+ color.py — per-cell local color/label namespaces.
3
+
4
+ Casey: "if one's red is the other's blue and they think they are talking
5
+ about the same thing but they are objectively not. it still doesn't matter
6
+ if their logics agree for their specific cells application."
7
+
8
+ This is the substrate's TOLERANCE for semantic heterogeneity. Each cell
9
+ has its own private color/label space. The substrate does NOT enforce
10
+ a global "red = blue = hot" naming convention. The substrate records
11
+ the labels used and the logic state, and it lets the application's
12
+ LOGIC (not the names) determine whether two cells agree.
13
+
14
+ In practice:
15
+ - each cell has a ColorNamespace
16
+ - when a cell labels something "red", it records its own private
17
+ red->canonical mapping
18
+ - the substrate's porting backend uses the CANONICAL value, not the label
19
+ - the cell can re-label "red" to "critical" without coordinating
20
+ """
21
+
22
+ from typing import Dict, Tuple, Optional, Any
23
+
24
+
25
+ class ColorNamespace:
26
+ """A single cell's private color/label space.
27
+
28
+ Each cell sees the world through its OWN labels. Two cells can call the
29
+ same observation different things and still communicate correctly —
30
+ because the substrate's porting/backend handles the canonical
31
+ translation.
32
+ """
33
+
34
+ __slots__ = ('owner', '_label_to_canonical', '_canonical_to_label')
35
+
36
+ def __init__(self, owner: Tuple[int, int]):
37
+ self.owner = owner # cell rank
38
+ self._label_to_canonical: Dict[str, Any] = {}
39
+ self._canonical_to_label: Dict[Any, str] = {}
40
+
41
+ def assign(self, label: str, canonical_value: Any) -> None:
42
+ """Assign a private label to a canonical value.
43
+
44
+ e.g. color.assign('red', 'CRITICAL')
45
+ e.g. color.assign('blue', 'CRITICAL') # same canonical, different label
46
+ """
47
+ self._label_to_canonical[label] = canonical_value
48
+ self._canonical_to_label[canonical_value] = label
49
+
50
+ def canonical(self, label: str) -> Optional[Any]:
51
+ """Look up the canonical value for a private label."""
52
+ return self._label_to_canonical.get(label)
53
+
54
+ def label(self, canonical_value: Any) -> Optional[str]:
55
+ """Look up the private label for a canonical value (the inverse)."""
56
+ return self._canonical_to_label.get(canonical_value)
57
+
58
+ def labels(self) -> Dict[str, Any]:
59
+ """Read-only view of all private labels."""
60
+ return dict(self._label_to_canonical)
61
+
62
+ def __repr__(self):
63
+ return f"ColorNamespace(owner={self.owner}, labels={self.labels()!r})"
64
+
65
+
66
+ def same_canonical(ns_a: ColorNamespace, label_a: str,
67
+ ns_b: ColorNamespace, label_b: str) -> bool:
68
+ """True iff two cells' labels resolve to the same canonical value.
69
+
70
+ This is how the substrate verifies that two cells agree on the LOGIC,
71
+ not the labels. Cell A's "red" and Cell B's "blue" agree iff both
72
+ map to the same canonical value (e.g., 'CRITICAL').
73
+ """
74
+ a = ns_a.canonical(label_a)
75
+ b = ns_b.canonical(label_b)
76
+ return a is not None and a == b
77
+
78
+
79
+ def labels_matter(ns_a: ColorNamespace, ns_b: ColorNamespace) -> bool:
80
+ """Whether the LABELS (not the canonical values) of two cells agree.
81
+
82
+ This is intentionally weak. The substrate doesn't compare labels —
83
+ it compares canonical values. labels_matter is FALSE in most cases.
84
+ """
85
+ if set(ns_a.labels().keys()) != set(ns_b.labels().keys()):
86
+ return False
87
+ for label in ns_a.labels():
88
+ if not ns_b.canonical(label):
89
+ return False
90
+ return True
@@ -0,0 +1,135 @@
1
+ """
2
+ grid.py — QuiltSpreadsheet — the spreadsheet-shaped substrate.
3
+
4
+ A spreadsheet-shaped substrate. Each cell is a runnable program with
5
+ hooks to other cells. The grid is the substrate walker canvas — cells
6
+ live INSIDE the grid, the grid is the IDE's front-end.
7
+
8
+ The substrate transition: from quilt-egg (1 cell) to quilt-spreadsheet
9
+ (grid of cells). Same DNA, same constants, more dimensions.
10
+ """
11
+
12
+ from typing import Tuple, Optional, Callable, Dict, Any, List
13
+ import sys
14
+
15
+ from .cell import SheetCell, register_cell
16
+ from .clock import get_default_clock, reset_default_clock
17
+ from .ledger import get_default_ledger, reset_default_ledger
18
+ from .backend import get_default_backend, reset_default_backend
19
+
20
+
21
+ class QuiltSpreadsheet:
22
+ """A spreadsheet-shaped substrate.
23
+
24
+ The IDE front-end is rows × cols of cells. Each cell is a runnable
25
+ program with hooks. Pulls and pushes between cells are recorded in
26
+ the double-entry ledger.
27
+ """
28
+
29
+ def __init__(self, rows: int = 8, cols: int = 8, name: str = "spreadsheet"):
30
+ self.rows = rows
31
+ self.cols = cols
32
+ self.name = name
33
+ self._cells: Dict[Tuple[int, int], SheetCell] = {}
34
+ # Bind module-level singletons to this substrate instance
35
+ # (for testing — production uses the module-level clock/ledger)
36
+ self.clock = get_default_clock()
37
+ self.ledger = get_default_ledger()
38
+ self.backend = get_default_backend()
39
+
40
+ def place_cell(
41
+ self,
42
+ row: int,
43
+ col: int,
44
+ program: Callable,
45
+ axioms=None,
46
+ ) -> SheetCell:
47
+ """Place a program into the substrate at (row, col).
48
+
49
+ Casey: "a python program might sit in a cell as a shell command
50
+ ready to run with hooks to other cells for wake-up."
51
+
52
+ The cell is the program's home. It wakes when pulled.
53
+ """
54
+ if (row, col) in self._cells:
55
+ raise ValueError(f"cell at ({row}, {col}) already exists")
56
+ cell = SheetCell(row=row, col=col, program=program, axioms=axioms)
57
+ self._cells[cell.rank] = cell
58
+ register_cell(cell)
59
+ return cell
60
+
61
+ def get_cell(self, row: int, col: int) -> Optional[SheetCell]:
62
+ return self._cells.get((row, col))
63
+
64
+ def cells(self) -> List[SheetCell]:
65
+ return list(self._cells.values())
66
+
67
+ def render(self) -> str:
68
+ """Render the substrate as a spreadsheet-shaped string.
69
+
70
+ Each cell shows its row,col and a status indicator:
71
+ '.' = dormant
72
+ '*' = awake (has been pulled at least once)
73
+ 'P' = pulled (received a pull)
74
+ 'X' = pushed (sent a push)
75
+ """
76
+ rows_str = []
77
+ for r in range(self.rows):
78
+ line = " "
79
+ for c in range(self.cols):
80
+ cell = self._cells.get((r, c))
81
+ if cell is None:
82
+ mark = "."
83
+ elif cell.awake_count > 0:
84
+ mark = "*"
85
+ else:
86
+ mark = "_"
87
+ line += f" {mark} "
88
+ rows_str.append(line)
89
+ header = " " + " ".join(f"c{c}" for c in range(self.cols))
90
+ return "\n".join([f"QuiltSpreadsheet '{self.name}' {self.rows}x{self.cols}", header] + rows_str)
91
+
92
+ def pull(self, source_row, source_col, target_row, target_col,
93
+ hook: str = "default", value: Any = None) -> dict:
94
+ """Cell at (source) pulls from cell at (target).
95
+
96
+ Records the pull/push pair in the ledger. Wakes the target.
97
+ """
98
+ source_cell = self._cells.get((source_row, source_col))
99
+ target_cell = self._cells.get((target_row, target_col))
100
+ if source_cell is None or target_cell is None:
101
+ return {'error': f"cell(s) not found: source={(source_row, source_col)}, target={(target_row, target_col)}"}
102
+ return source_cell.pull_from(target_cell.rank, hook=hook, value=value)
103
+
104
+ def summary(self) -> dict:
105
+ cells = self.cells()
106
+ total_awake = sum(1 for c in cells if c.awake_count > 0)
107
+ total_pulls = sum(len(hooks) for hooks in (c.pulls for c in cells))
108
+ # Flatten the hooks lists
109
+ total_pull_count = sum(len(v) for c in cells for v in c.pulls.values())
110
+ total_push_count = sum(len(v) for c in cells for v in c.pushes.values())
111
+ return {
112
+ "name": self.name,
113
+ "rows": self.rows,
114
+ "cols": self.cols,
115
+ "cells_placed": len(self._cells),
116
+ "cells_awake": total_awake,
117
+ "ledger": self.ledger.summary(),
118
+ "backend_conversions_run": self.backend.stats()["conversions_run"],
119
+ }
120
+
121
+
122
+ def place_cell(row: int, col: int, program: Callable, axioms=None) -> SheetCell:
123
+ """Convenience: place a cell into the default registry."""
124
+ cell = SheetCell(row=row, col=col, program=program, axioms=axioms)
125
+ register_cell(cell)
126
+ return cell
127
+
128
+
129
+ def reset_all() -> None:
130
+ """Reset all singletons for testing."""
131
+ reset_default_clock()
132
+ reset_default_ledger()
133
+ reset_default_backend()
134
+ from .cell import reset_registry
135
+ reset_registry()
@@ -0,0 +1,163 @@
1
+ """
2
+ ledger.py — double-entry bookkeeping for spreadsheet pull/push pairs.
3
+
4
+ Casey: "there's a double-entry book keeping that for every pull is a push
5
+ somewhere else that simply needs an agreement and timestamp."
6
+
7
+ This is the SUBSTRATE-level ledger. Every pull from cell A to cell B is
8
+ recorded ONCE as a pair: a 'pull' entry in A's view, a 'push' entry in
9
+ B's view. Both share the SAME (source, target, hook, timestamp). The
10
+ agreement criterion: any matching pair (a, b) that matches (a, b, hook, ts)
11
+ is a successful ledger entry.
12
+
13
+ If there is a pull without a matching push, the ledger reports an
14
+ unbalanced entry (the substrate's "alert"). This is the substrate's
15
+ audit trail — the witness log at the spreadsheet level.
16
+ """
17
+
18
+ from dataclasses import dataclass, field
19
+ from typing import Tuple, List, Dict, Optional, Any
20
+ import time
21
+ import uuid
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class LedgerEntry:
26
+ """A single ledger entry.
27
+
28
+ A pull is one entry. A push that responds to a pull is another entry.
29
+ Both share the same pair_id — that's the agreement.
30
+ """
31
+ pair_id: str # UUID linking pull + push
32
+ source: Tuple[int, int] # cell rank
33
+ target: Tuple[int, int] # cell rank
34
+ direction: str # 'pull' or 'push'
35
+ hook: str # the hook name (the "what" of the request)
36
+ value: Any # the value being transferred
37
+ timestamp_ns: int # monotonic clock at the moment of recording
38
+ seen_by: List[Tuple[int, int]] = field(default_factory=list) # which cells' clocks recorded this
39
+
40
+
41
+ class Ledger:
42
+ """The double-entry ledger.
43
+
44
+ Every pull/push pair is recorded as TWO entries with the same pair_id.
45
+ The ledger can verify that every pull has a matching push by walking
46
+ all entries grouped by pair_id.
47
+ """
48
+
49
+ def __init__(self):
50
+ self._entries: List[LedgerEntry] = []
51
+ self._pair_count: int = 0
52
+
53
+ def record_pull(
54
+ self,
55
+ source: Tuple[int, int],
56
+ target: Tuple[int, int],
57
+ hook: str,
58
+ value: Any,
59
+ timestamp_ns: int,
60
+ ) -> LedgerEntry:
61
+ """Record a 'pull' — a request from source to target."""
62
+ pair_id = str(uuid.uuid4())
63
+ entry = LedgerEntry(
64
+ pair_id=pair_id,
65
+ source=source,
66
+ target=target,
67
+ direction='pull',
68
+ hook=hook,
69
+ value=value,
70
+ timestamp_ns=timestamp_ns,
71
+ seen_by=[source],
72
+ )
73
+ self._entries.append(entry)
74
+ return entry
75
+
76
+ def record_push(
77
+ self,
78
+ source: Tuple[int, int],
79
+ target: Tuple[int, int],
80
+ hook: str,
81
+ value: Any,
82
+ timestamp_ns: int,
83
+ pair_id: str,
84
+ ) -> Optional[LedgerEntry]:
85
+ """Record the matched 'push' response for a prior pull.
86
+
87
+ The pair_id MUST match the pull's pair_id. If no pull exists yet,
88
+ return None (the push without a pull is a substrate violation).
89
+ """
90
+ # Verify the pull exists
91
+ pull = self._find_pull(pair_id)
92
+ if pull is None:
93
+ return None # push without a pull — substrate violation
94
+ entry = LedgerEntry(
95
+ pair_id=pair_id,
96
+ source=source,
97
+ target=target,
98
+ direction='push',
99
+ hook=hook,
100
+ value=value,
101
+ timestamp_ns=timestamp_ns,
102
+ seen_by=[source, target],
103
+ )
104
+ self._entries.append(entry)
105
+ self._pair_count += 1
106
+ return entry
107
+
108
+ def _find_pull(self, pair_id: str) -> Optional[LedgerEntry]:
109
+ for e in self._entries:
110
+ if e.pair_id == pair_id and e.direction == 'pull':
111
+ return e
112
+ return None
113
+
114
+ @property
115
+ def total_pulls(self) -> int:
116
+ return sum(1 for e in self._entries if e.direction == 'pull')
117
+
118
+ @property
119
+ def total_pushes(self) -> int:
120
+ return sum(1 for e in self._entries if e.direction == 'push')
121
+
122
+ @property
123
+ def total_pairs(self) -> int:
124
+ return self._pair_count
125
+
126
+ @property
127
+ def unbalanced(self) -> List[LedgerEntry]:
128
+ """All pulls that don't have a matching push."""
129
+ paired_pull_ids = {
130
+ e.pair_id for e in self._entries if e.direction == 'push'
131
+ }
132
+ return [
133
+ e for e in self._entries
134
+ if e.direction == 'pull' and e.pair_id not in paired_pull_ids
135
+ ]
136
+
137
+ def all_entries(self) -> List[LedgerEntry]:
138
+ return list(self._entries)
139
+
140
+ def summary(self) -> dict:
141
+ return {
142
+ "total_entries": len(self._entries),
143
+ "pulls": self.total_pulls,
144
+ "pushes": self.total_pushes,
145
+ "pairs": self.total_pairs,
146
+ "unbalanced_pulls": len(self.unbalanced),
147
+ }
148
+
149
+
150
+ # Module-level singleton
151
+ _default_ledger = None
152
+
153
+
154
+ def get_default_ledger() -> Ledger:
155
+ global _default_ledger
156
+ if _default_ledger is None:
157
+ _default_ledger = Ledger()
158
+ return _default_ledger
159
+
160
+
161
+ def reset_default_ledger() -> None:
162
+ global _default_ledger
163
+ _default_ledger = None
@@ -0,0 +1,72 @@
1
+ """
2
+ perception.py — per-cell sorting and grouping of the spreadsheet around it.
3
+
4
+ Casey: "the relationship is cellular and sorting and grouping for that
5
+ cell's perception of the spreadsheet around it (there's no central program
6
+ within the cell's instance, it's just a program running so if it sorts
7
+ for it's own purpose by type instead of date so it can group with simple
8
+ excel terms."
9
+
10
+ Each cell has its OWN perception of the spreadsheet. A cell can sort
11
+ its view by type, by tag, by relevance, by age — whatever its program
12
+ needs. The substrate provides the raw view; the cell sorts as it sees fit.
13
+ """
14
+
15
+ from typing import List, Dict, Any, Optional, Tuple, Callable
16
+ from collections import defaultdict
17
+
18
+
19
+ class Perception:
20
+ """A single cell's view of the spreadsheet around it.
21
+
22
+ Cells pull from each other. When a cell pulls a slice of the spreadsheet,
23
+ it gets the raw observations. Each cell has its own Perception that
24
+ sorts those observations for its program's purpose.
25
+ """
26
+
27
+ __slots__ = ('owner', '_items', '_sort_key')
28
+
29
+ def __init__(self, owner: Tuple[int, int]):
30
+ self.owner = owner
31
+ self._items: List[Dict[str, Any]] = []
32
+ self._sort_key: Optional[Callable] = None
33
+
34
+ def ingest(self, items: List[Dict[str, Any]]) -> None:
35
+ """Add raw observations to this perception."""
36
+ self._items.extend(items)
37
+
38
+ def sort_by_type(self) -> 'Perception':
39
+ """Sort observations by 'type' field (default perception sort).
40
+
41
+ Casey: 'if it sorts for its own purpose by type instead of date'.
42
+ The type-based sort groups by category — like Excel grouping
43
+ rows by data type.
44
+ """
45
+ self._sort_key = lambda x: x.get('type', '')
46
+ self._items.sort(key=self._sort_key)
47
+ return self
48
+
49
+ def sort_by(self, key: Callable) -> 'Perception':
50
+ """Custom sort."""
51
+ self._sort_key = key
52
+ self._items.sort(key=key)
53
+ return self
54
+
55
+ def group_by_type(self) -> Dict[str, List[Dict[str, Any]]]:
56
+ """Group observations by 'type' field. Returns dict[group_name, items]."""
57
+ groups: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
58
+ for item in self._items:
59
+ t = item.get('type', 'untyped')
60
+ groups[t].append(item)
61
+ return dict(groups)
62
+
63
+ def filter(self, predicate: Callable[[Dict[str, Any]], bool]) -> 'Perception':
64
+ """Keep only items where predicate returns True."""
65
+ self._items = [i for i in self._items if predicate(i)]
66
+ return self
67
+
68
+ def items(self) -> List[Dict[str, Any]]:
69
+ return list(self._items)
70
+
71
+ def size(self) -> int:
72
+ return len(self._items)
@@ -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,14 @@
1
+ quilt_spreadsheet/__init__.py,sha256=xneWWEnIDwyWxnS8-Jt6mddZfYjnxNZ8zoLq3A_RcZU,1621
2
+ quilt_spreadsheet/__main__.py,sha256=x_KjfS7DM8VsaSBPcZPHB1Quuo_BONCl24H7O15kKQM,2228
3
+ quilt_spreadsheet/backend.py,sha256=SasWWpO1e4AJrj3vcJDrZ-kRlALtrd2vMSGGOxP95cU,6272
4
+ quilt_spreadsheet/cell.py,sha256=xCNmxMcE7CMeToE7iZOyCtvjg36ts0ZIp-R_YaN8le0,7280
5
+ quilt_spreadsheet/clock.py,sha256=irq27i8VsXNkOrGqeFPxoCpyz263n7lBM_xoGb20s50,4799
6
+ quilt_spreadsheet/color.py,sha256=3MNZsNmIs5FXFaViBsDJl8Smg_DoSudXQSY9qzQM7vU,3508
7
+ quilt_spreadsheet/grid.py,sha256=5Gl0URdpeKh5_e13HlgXzUPIPWtHUwS_bOglDFhk6QE,5083
8
+ quilt_spreadsheet/ledger.py,sha256=UfSWRoxHjKR0FkwiWtjNqIVodqyClKUgpqTn8zqr4_s,5029
9
+ quilt_spreadsheet/perception.py,sha256=yxG-gs9g41fqayyfZi63j5105wsRskYrRL20N7aAPYI,2679
10
+ quilt_spreadsheet-0.1.0.dist-info/licenses/LICENSE,sha256=QzOhcWaBRQ6WHKAOfQD_V2h1XWSnul4IqnY3HLXli1s,64
11
+ quilt_spreadsheet-0.1.0.dist-info/METADATA,sha256=bSy5tmz5pxfozJxprsQQQnRWbW820TRJAzHk0dAKE5g,5893
12
+ quilt_spreadsheet-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
13
+ quilt_spreadsheet-0.1.0.dist-info/top_level.txt,sha256=Mn9cDYnVzobyyFHksUv_wcASZPXGespLhPxgkGqdmUg,18
14
+ quilt_spreadsheet-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Casey Digennaro / SuperInstance
@@ -0,0 +1 @@
1
+ quilt_spreadsheet