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.
- quilt_spreadsheet/__init__.py +50 -0
- quilt_spreadsheet/__main__.py +71 -0
- quilt_spreadsheet/backend.py +183 -0
- quilt_spreadsheet/cell.py +231 -0
- quilt_spreadsheet/clock.py +130 -0
- quilt_spreadsheet/color.py +90 -0
- quilt_spreadsheet/grid.py +135 -0
- quilt_spreadsheet/ledger.py +163 -0
- quilt_spreadsheet/perception.py +72 -0
- quilt_spreadsheet-0.1.0.dist-info/METADATA +181 -0
- quilt_spreadsheet-0.1.0.dist-info/RECORD +14 -0
- quilt_spreadsheet-0.1.0.dist-info/WHEEL +5 -0
- quilt_spreadsheet-0.1.0.dist-info/licenses/LICENSE +3 -0
- quilt_spreadsheet-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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 @@
|
|
|
1
|
+
quilt_spreadsheet
|