kpopper 0.21.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kpopper-0.21.0/LICENSE +20 -0
- kpopper-0.21.0/PKG-INFO +242 -0
- kpopper-0.21.0/README.md +219 -0
- kpopper-0.21.0/kpopper.egg-info/PKG-INFO +242 -0
- kpopper-0.21.0/kpopper.egg-info/SOURCES.txt +18 -0
- kpopper-0.21.0/kpopper.egg-info/dependency_links.txt +1 -0
- kpopper-0.21.0/kpopper.egg-info/entry_points.txt +2 -0
- kpopper-0.21.0/kpopper.egg-info/requires.txt +1 -0
- kpopper-0.21.0/kpopper.egg-info/top_level.txt +1 -0
- kpopper-0.21.0/pyproject.toml +54 -0
- kpopper-0.21.0/scripts/__init__.py +4 -0
- kpopper-0.21.0/scripts/cli.py +95 -0
- kpopper-0.21.0/scripts/page/page.css +159 -0
- kpopper-0.21.0/scripts/page/page.js +275 -0
- kpopper-0.21.0/scripts/provenance.py +2061 -0
- kpopper-0.21.0/scripts/render_page.py +1202 -0
- kpopper-0.21.0/scripts/verify_page.js +258 -0
- kpopper-0.21.0/setup.cfg +4 -0
- kpopper-0.21.0/tests/test_contract.py +717 -0
- kpopper-0.21.0/tests/test_release.py +98 -0
kpopper-0.21.0/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilan Bar Magen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR OTHER DEALINGS IN THE SOFTWARE.
|
kpopper-0.21.0/PKG-INFO
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kpopper
|
|
3
|
+
Version: 0.21.0
|
|
4
|
+
Summary: An epistemic record for work that gets revisited: what is known, how each piece is grounded, and what would make each judgment wrong.
|
|
5
|
+
Author: Ilan Bar Magen
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ilanbm/kpopper
|
|
8
|
+
Project-URL: Source, https://github.com/ilanbm/kpopper
|
|
9
|
+
Project-URL: Issues, https://github.com/ilanbm/kpopper/issues
|
|
10
|
+
Keywords: provenance,epistemic,falsifiability,truth-maintenance,traceability,research
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
17
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: PyYAML>=5.1
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# kpopper
|
|
25
|
+
|
|
26
|
+
*Named for Karl Popper: nothing here is ever verified, only exposed to refutation. Every
|
|
27
|
+
judgment must say what would make it wrong — one that cannot be wrong is an opinion.*
|
|
28
|
+
|
|
29
|
+
The stores remember. This remembers **how you know.**
|
|
30
|
+
|
|
31
|
+
kpopper keeps an epistemic record for work that gets revisited: what was read from the world,
|
|
32
|
+
what was worked out from it, what was concluded — and what every conclusion is still standing
|
|
33
|
+
on. It is not another knowledge store, and it does not model what exists. A fact from
|
|
34
|
+
anywhere — a document, a database, another knowledge system — enters as an entry that says
|
|
35
|
+
where it came from; the record governs its life: who vouches for it, when it was last
|
|
36
|
+
checked, and what falls when it moves.
|
|
37
|
+
|
|
38
|
+
Two mechanics carry the whole method:
|
|
39
|
+
|
|
40
|
+
- **Every judgment states, in advance, what would make it wrong — or declares out loud why
|
|
41
|
+
it cannot yet.** `wrong_if` is a predicate over the things the judgment declares it rests
|
|
42
|
+
on. The reader refuses prose in its place, rejects a predicate that reads anything
|
|
43
|
+
undeclared — and when the predicate is a plain comparison, evaluates it: a judgment whose
|
|
44
|
+
own falsifier holds **fails the build**. Anything richer is surfaced beside exactly what
|
|
45
|
+
moved, never guessed at.
|
|
46
|
+
- **Staleness fires on reality, not on the calendar.** Every judgment carries `seen`, a
|
|
47
|
+
snapshot of what its dependencies held when it was last reviewed. Nothing stores a stale
|
|
48
|
+
flag; drift is *derived* by comparison, so it cannot be forgotten, cleared by accident, or
|
|
49
|
+
survive a revert. What it compares is the value the judgment used — re-reading a source is
|
|
50
|
+
still a human act, and a value the record never re-read cannot drift.
|
|
51
|
+
|
|
52
|
+
And one boundary: **invalidation spreads — along what each judgment declared it rests on,
|
|
53
|
+
and through the rules of worked-out values — but a re-derivation never applies itself.** A
|
|
54
|
+
change marks everything downstream of it; deciding what to do about that waits for a person.
|
|
55
|
+
A stale recommendation someone read beats a current one nobody did.
|
|
56
|
+
|
|
57
|
+
A content hash over a source answers a different question than the one a conclusion needs:
|
|
58
|
+
it fires when a file is reformatted and stays silent when the number you relied on moves
|
|
59
|
+
somewhere the hash never covered. Matching bytes prove a file is unchanged — never that what
|
|
60
|
+
you concluded from it still holds. So kpopper snapshots the *value the judgment used*, not
|
|
61
|
+
the bytes it came from, and states the breaking condition in advance instead of waiting for
|
|
62
|
+
a checksum to notice.
|
|
63
|
+
|
|
64
|
+
## What it installs
|
|
65
|
+
|
|
66
|
+
The package is the command line: `kpopper`, with the reader and the renderer behind it. The
|
|
67
|
+
plugin is all of that plus the method and the session hooks:
|
|
68
|
+
|
|
69
|
+
| | |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `skills/kpopper` | The method. Loads when work will be revisited, or when resuming such work. |
|
|
72
|
+
| `skills/kpopper/PAGE.md` | The page reference — briefs, renderers, the tree. Read only when building a page. |
|
|
73
|
+
| `scripts/kpopper` | One entry point: `open · check · affects · pull · page`. The dispatcher itself is `scripts/cli.py` — the same code the installed `kpopper` command runs. |
|
|
74
|
+
| `scripts/provenance.py` | The reader underneath. Field names are inferred by shape, so it reads records written in any vocabulary. |
|
|
75
|
+
| `scripts/render_page.py` | The record as one self-contained page, three tabs, no dependencies beyond the reader. |
|
|
76
|
+
| `scripts/verify_page.js` | Browser checks for that page, in both themes and under reduced motion. Playwright. Reached as `kpopper page --checks`. |
|
|
77
|
+
| `tests/` | The contract the reader and the page keep, run as `python3 -m unittest discover -s tests` against the fixture record in `tests/fixtures/page` — every field they accept, exercised once. |
|
|
78
|
+
| `hooks/` | A session opener and a stop gate. The opener runs `provenance.py open` when the project keeps a record — at its root, or registered with the checkout — and is silent everywhere else; the gate bounces a session once, with the failures, if it tries to finish having left `check` worse than it found it. |
|
|
79
|
+
|
|
80
|
+
## The record
|
|
81
|
+
|
|
82
|
+
One file, one place: `PROVENANCE.yaml` at the project root — or a pointer to wherever the
|
|
83
|
+
content actually sits, including a mapping of several files; the reader follows it. A project
|
|
84
|
+
whose tree cannot hold the file keeps it elsewhere and registers the path with the checkout
|
|
85
|
+
(one line in the git common dir; `kpopper where` prints it), and the opener and every command
|
|
86
|
+
find it from any worktree. It holds three things: what was taken from a source (and where
|
|
87
|
+
within it), what was worked out (the rule, never the result), and what was concluded (with
|
|
88
|
+
what would make it wrong).
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
sources:
|
|
92
|
+
msa: {file: "contracts/acme-msa-2026.pdf", of: "2026-04-02"}
|
|
93
|
+
|
|
94
|
+
known:
|
|
95
|
+
acme.seat_price: {v: 42, from: msa, at: "Enterprise tier", name: "Acme seat price"}
|
|
96
|
+
acme.seats: {v: 180, from: msa, name: "Committed seats"}
|
|
97
|
+
acme.annual: {rule: "acme.seat_price * acme.seats * 12", name: "Annual Acme cost"}
|
|
98
|
+
beta.annual: {v: 84000, from: msa, name: "Annual Beta cost"}
|
|
99
|
+
|
|
100
|
+
judgments:
|
|
101
|
+
why_acme:
|
|
102
|
+
rests_on: [acme.annual, beta.annual, acme.seats]
|
|
103
|
+
verdict: "prefer Acme above ~150 seats"
|
|
104
|
+
wrong_if: "acme.seats < 150"
|
|
105
|
+
seen: {acme.annual: 90720, beta.annual: 84000, acme.seats: 180}
|
|
106
|
+
seats_fit:
|
|
107
|
+
rests_on: [acme.seats]
|
|
108
|
+
verdict: "the committed seats fit one contract tier"
|
|
109
|
+
wrong_if: "acme.seats > 500"
|
|
110
|
+
seen: {acme.seats: 180}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Drop the seat count to 120 and `check` fails with `why_acme: wrong_if holds (acme.seats <
|
|
114
|
+
150) - broken by its own condition`; `affects acme.seat_price` reaches `why_acme` through
|
|
115
|
+
the rule that computes the annual figure. The record does not wait to be asked.
|
|
116
|
+
|
|
117
|
+
Nothing is created on the first turn. The file appears when there is a first thing to put in
|
|
118
|
+
it, and structure is added only when something observable forces it. A project that never
|
|
119
|
+
grows past a single file is a correct outcome.
|
|
120
|
+
|
|
121
|
+
## Opening a session costs what moved, not what exists
|
|
122
|
+
|
|
123
|
+
With the plugin installed, every session in a project that keeps a record opens with the
|
|
124
|
+
record's own head — its name, its namespace, what needs a person, the open questions — inside
|
|
125
|
+
a fixed character budget. Measured on a live record of 107 entries: reading it whole costs
|
|
126
|
+
39,777 characters; the opener costs 948 on a clean record. The opener also re-runs after a
|
|
127
|
+
context compaction, which is exactly when a session most needs to be re-grounded.
|
|
128
|
+
|
|
129
|
+
The rest is pulled, never preloaded:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
kpopper open # what a session reads instead of the whole record
|
|
133
|
+
kpopper check # does the record still hold together
|
|
134
|
+
kpopper affects <entry> # what a change reaches, through intermediate judgments
|
|
135
|
+
kpopper pull <entry|prefix> # a subject's values with their sources - and what moved
|
|
136
|
+
# since each judgment last looked
|
|
137
|
+
kpopper where # the record this directory answers for
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Each command reads the record in the current directory, or the files you name. Without the
|
|
141
|
+
command line on your path, the same dispatcher ships inside the plugin — find it once, then run
|
|
142
|
+
`"$K" open`. An installed plugin sits under a version directory and may hold worktrees of its
|
|
143
|
+
own, so the search skips those and takes the highest version rather than the first hit:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
K=$(find ~/.claude -name worktrees -prune -o -path '*kpopper*/scripts/kpopper' -print 2>/dev/null | sort -V | tail -1)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`check` exits non-zero on an undeclared gap: a dependency that is not an entry (unless the
|
|
150
|
+
judgment declares it missing with `blocked_on`), a dependency with no snapshot, a predicate
|
|
151
|
+
naming something undeclared, prose sitting in a predicate field, or a plain-comparison
|
|
152
|
+
predicate that currently holds — a judgment broken by its own condition. A declared hole is a
|
|
153
|
+
note, not a failure — a build that stays red over an honest declaration teaches records to
|
|
154
|
+
stop declaring. A dependency that moved since a judgment's snapshot is reported as `MOVED`
|
|
155
|
+
and does not fail the build either: it puts the judgment in front of a person, and it is
|
|
156
|
+
muted when the predicate names it and still evaluates false — moved, not across the line.
|
|
157
|
+
|
|
158
|
+
## The page — and the tree
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
kpopper page --open # the record as one page, in the browser
|
|
162
|
+
kpopper page --open --tree # landing on the tree
|
|
163
|
+
kpopper page --verify # deterministic checks, no browser
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Three tabs. **Now** is the arrangement this session chose, written in a brief the page keeps
|
|
167
|
+
honest: it may order, it cannot drop. **Record** is everything, arranged by nothing. **Tree**
|
|
168
|
+
is the whole record as one growing thing: what was read from the world is the root fan below
|
|
169
|
+
the ground line, worked-out values branch above it, and conclusions blossom in a canopy —
|
|
170
|
+
the three node kinds in their three colors. Everything on every tab is the same live card:
|
|
171
|
+
hover or tap for where a value came from, click a dependency to walk to it, and on the tree,
|
|
172
|
+
opening a node lights the sap — the full chain of evidence feeding it — while its neighbours
|
|
173
|
+
stir like branches in wind. A card's `tree` button prunes the tree to that node's world; one
|
|
174
|
+
chip brings the whole tree back. The tab grows in the first time it is opened, and any node can
|
|
175
|
+
be pulled — it resists like a branch, stretches its limbs, and springs back when you let go,
|
|
176
|
+
because the page keeps nothing.
|
|
177
|
+
|
|
178
|
+
## The part that does not change
|
|
179
|
+
|
|
180
|
+
1. **Every entry declares what it rests on.** Something with no declaration does not go in.
|
|
181
|
+
2. **Store what produces an output, never the output.**
|
|
182
|
+
3. **Invalidation spreads automatically; re-running a judgment never applies itself.**
|
|
183
|
+
4. **A change to the shape is valid only with a migration that leaves the build green.**
|
|
184
|
+
|
|
185
|
+
Everything else — field names, sections, renderers — is open to revision. A record that can
|
|
186
|
+
rewrite its own rules will drift unless something in it is not up for revision. These four
|
|
187
|
+
are that floor.
|
|
188
|
+
|
|
189
|
+
## Installing
|
|
190
|
+
|
|
191
|
+
The command line on its own, for any project and any editor:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pipx install kpopper # or: pip install kpopper
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The browser checks come with it — they are Node, but they install where everything else does:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
kpopper page --out record.html # then: kpopper page --checks record.html
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The Claude Code plugin — the method as a skill, the session opener, the stop gate, and the
|
|
204
|
+
same commands:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
/plugin marketplace add /path/to/kpopper
|
|
208
|
+
/plugin install kpopper@kpopper
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
or non-interactively:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
claude plugin install kpopper@kpopper --scope user
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`--scope user` makes it available in every project on the machine; `--scope project` commits
|
|
218
|
+
it to the repo you are in. Other editors are wired up from `adapters/`.
|
|
219
|
+
|
|
220
|
+
Where the channels overlap they are the same files rather than copies of them: the packages are
|
|
221
|
+
mapped onto the plugin's `scripts/`, so the reader, the renderer, the dispatcher and the browser
|
|
222
|
+
checks have nothing to keep in step, and one version number covers all of them. There is a third
|
|
223
|
+
package, on npm, and it is that same mapping: it holds the name while an open question in the
|
|
224
|
+
record — whether the renderer, the command line and the checks move to TypeScript — is still open,
|
|
225
|
+
and it carries the browser checks rather than nothing, because they are the part that is already
|
|
226
|
+
Node. This repository is the only place any of it is edited; every installed copy is a read-only
|
|
227
|
+
distribution.
|
|
228
|
+
|
|
229
|
+
## Requirements
|
|
230
|
+
|
|
231
|
+
Python 3.9+ and PyYAML — `pipx` brings it along; a plugin-only install wants
|
|
232
|
+
`pip3 install pyyaml`. The browser checks additionally want Node 18+, a Chrome/Chromium
|
|
233
|
+
binary (`CHROME=/path/to/chrome` when it is not on a known path), and `playwright-core` — the
|
|
234
|
+
driver, which none of the packages ship: it is found beside the page when the project already
|
|
235
|
+
uses Playwright, and otherwise `npm i --no-save playwright-core` next to the page is enough.
|
|
236
|
+
Nothing else.
|
|
237
|
+
|
|
238
|
+
## What would show this was not worth it
|
|
239
|
+
|
|
240
|
+
The method promises exactly one measurable thing: the opening cost of session number N. If
|
|
241
|
+
after five sessions the sixth does not open cheaper, the method did not return what it cost,
|
|
242
|
+
and you can drop it with a clear conscience.
|
kpopper-0.21.0/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# kpopper
|
|
2
|
+
|
|
3
|
+
*Named for Karl Popper: nothing here is ever verified, only exposed to refutation. Every
|
|
4
|
+
judgment must say what would make it wrong — one that cannot be wrong is an opinion.*
|
|
5
|
+
|
|
6
|
+
The stores remember. This remembers **how you know.**
|
|
7
|
+
|
|
8
|
+
kpopper keeps an epistemic record for work that gets revisited: what was read from the world,
|
|
9
|
+
what was worked out from it, what was concluded — and what every conclusion is still standing
|
|
10
|
+
on. It is not another knowledge store, and it does not model what exists. A fact from
|
|
11
|
+
anywhere — a document, a database, another knowledge system — enters as an entry that says
|
|
12
|
+
where it came from; the record governs its life: who vouches for it, when it was last
|
|
13
|
+
checked, and what falls when it moves.
|
|
14
|
+
|
|
15
|
+
Two mechanics carry the whole method:
|
|
16
|
+
|
|
17
|
+
- **Every judgment states, in advance, what would make it wrong — or declares out loud why
|
|
18
|
+
it cannot yet.** `wrong_if` is a predicate over the things the judgment declares it rests
|
|
19
|
+
on. The reader refuses prose in its place, rejects a predicate that reads anything
|
|
20
|
+
undeclared — and when the predicate is a plain comparison, evaluates it: a judgment whose
|
|
21
|
+
own falsifier holds **fails the build**. Anything richer is surfaced beside exactly what
|
|
22
|
+
moved, never guessed at.
|
|
23
|
+
- **Staleness fires on reality, not on the calendar.** Every judgment carries `seen`, a
|
|
24
|
+
snapshot of what its dependencies held when it was last reviewed. Nothing stores a stale
|
|
25
|
+
flag; drift is *derived* by comparison, so it cannot be forgotten, cleared by accident, or
|
|
26
|
+
survive a revert. What it compares is the value the judgment used — re-reading a source is
|
|
27
|
+
still a human act, and a value the record never re-read cannot drift.
|
|
28
|
+
|
|
29
|
+
And one boundary: **invalidation spreads — along what each judgment declared it rests on,
|
|
30
|
+
and through the rules of worked-out values — but a re-derivation never applies itself.** A
|
|
31
|
+
change marks everything downstream of it; deciding what to do about that waits for a person.
|
|
32
|
+
A stale recommendation someone read beats a current one nobody did.
|
|
33
|
+
|
|
34
|
+
A content hash over a source answers a different question than the one a conclusion needs:
|
|
35
|
+
it fires when a file is reformatted and stays silent when the number you relied on moves
|
|
36
|
+
somewhere the hash never covered. Matching bytes prove a file is unchanged — never that what
|
|
37
|
+
you concluded from it still holds. So kpopper snapshots the *value the judgment used*, not
|
|
38
|
+
the bytes it came from, and states the breaking condition in advance instead of waiting for
|
|
39
|
+
a checksum to notice.
|
|
40
|
+
|
|
41
|
+
## What it installs
|
|
42
|
+
|
|
43
|
+
The package is the command line: `kpopper`, with the reader and the renderer behind it. The
|
|
44
|
+
plugin is all of that plus the method and the session hooks:
|
|
45
|
+
|
|
46
|
+
| | |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `skills/kpopper` | The method. Loads when work will be revisited, or when resuming such work. |
|
|
49
|
+
| `skills/kpopper/PAGE.md` | The page reference — briefs, renderers, the tree. Read only when building a page. |
|
|
50
|
+
| `scripts/kpopper` | One entry point: `open · check · affects · pull · page`. The dispatcher itself is `scripts/cli.py` — the same code the installed `kpopper` command runs. |
|
|
51
|
+
| `scripts/provenance.py` | The reader underneath. Field names are inferred by shape, so it reads records written in any vocabulary. |
|
|
52
|
+
| `scripts/render_page.py` | The record as one self-contained page, three tabs, no dependencies beyond the reader. |
|
|
53
|
+
| `scripts/verify_page.js` | Browser checks for that page, in both themes and under reduced motion. Playwright. Reached as `kpopper page --checks`. |
|
|
54
|
+
| `tests/` | The contract the reader and the page keep, run as `python3 -m unittest discover -s tests` against the fixture record in `tests/fixtures/page` — every field they accept, exercised once. |
|
|
55
|
+
| `hooks/` | A session opener and a stop gate. The opener runs `provenance.py open` when the project keeps a record — at its root, or registered with the checkout — and is silent everywhere else; the gate bounces a session once, with the failures, if it tries to finish having left `check` worse than it found it. |
|
|
56
|
+
|
|
57
|
+
## The record
|
|
58
|
+
|
|
59
|
+
One file, one place: `PROVENANCE.yaml` at the project root — or a pointer to wherever the
|
|
60
|
+
content actually sits, including a mapping of several files; the reader follows it. A project
|
|
61
|
+
whose tree cannot hold the file keeps it elsewhere and registers the path with the checkout
|
|
62
|
+
(one line in the git common dir; `kpopper where` prints it), and the opener and every command
|
|
63
|
+
find it from any worktree. It holds three things: what was taken from a source (and where
|
|
64
|
+
within it), what was worked out (the rule, never the result), and what was concluded (with
|
|
65
|
+
what would make it wrong).
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
sources:
|
|
69
|
+
msa: {file: "contracts/acme-msa-2026.pdf", of: "2026-04-02"}
|
|
70
|
+
|
|
71
|
+
known:
|
|
72
|
+
acme.seat_price: {v: 42, from: msa, at: "Enterprise tier", name: "Acme seat price"}
|
|
73
|
+
acme.seats: {v: 180, from: msa, name: "Committed seats"}
|
|
74
|
+
acme.annual: {rule: "acme.seat_price * acme.seats * 12", name: "Annual Acme cost"}
|
|
75
|
+
beta.annual: {v: 84000, from: msa, name: "Annual Beta cost"}
|
|
76
|
+
|
|
77
|
+
judgments:
|
|
78
|
+
why_acme:
|
|
79
|
+
rests_on: [acme.annual, beta.annual, acme.seats]
|
|
80
|
+
verdict: "prefer Acme above ~150 seats"
|
|
81
|
+
wrong_if: "acme.seats < 150"
|
|
82
|
+
seen: {acme.annual: 90720, beta.annual: 84000, acme.seats: 180}
|
|
83
|
+
seats_fit:
|
|
84
|
+
rests_on: [acme.seats]
|
|
85
|
+
verdict: "the committed seats fit one contract tier"
|
|
86
|
+
wrong_if: "acme.seats > 500"
|
|
87
|
+
seen: {acme.seats: 180}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Drop the seat count to 120 and `check` fails with `why_acme: wrong_if holds (acme.seats <
|
|
91
|
+
150) - broken by its own condition`; `affects acme.seat_price` reaches `why_acme` through
|
|
92
|
+
the rule that computes the annual figure. The record does not wait to be asked.
|
|
93
|
+
|
|
94
|
+
Nothing is created on the first turn. The file appears when there is a first thing to put in
|
|
95
|
+
it, and structure is added only when something observable forces it. A project that never
|
|
96
|
+
grows past a single file is a correct outcome.
|
|
97
|
+
|
|
98
|
+
## Opening a session costs what moved, not what exists
|
|
99
|
+
|
|
100
|
+
With the plugin installed, every session in a project that keeps a record opens with the
|
|
101
|
+
record's own head — its name, its namespace, what needs a person, the open questions — inside
|
|
102
|
+
a fixed character budget. Measured on a live record of 107 entries: reading it whole costs
|
|
103
|
+
39,777 characters; the opener costs 948 on a clean record. The opener also re-runs after a
|
|
104
|
+
context compaction, which is exactly when a session most needs to be re-grounded.
|
|
105
|
+
|
|
106
|
+
The rest is pulled, never preloaded:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
kpopper open # what a session reads instead of the whole record
|
|
110
|
+
kpopper check # does the record still hold together
|
|
111
|
+
kpopper affects <entry> # what a change reaches, through intermediate judgments
|
|
112
|
+
kpopper pull <entry|prefix> # a subject's values with their sources - and what moved
|
|
113
|
+
# since each judgment last looked
|
|
114
|
+
kpopper where # the record this directory answers for
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Each command reads the record in the current directory, or the files you name. Without the
|
|
118
|
+
command line on your path, the same dispatcher ships inside the plugin — find it once, then run
|
|
119
|
+
`"$K" open`. An installed plugin sits under a version directory and may hold worktrees of its
|
|
120
|
+
own, so the search skips those and takes the highest version rather than the first hit:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
K=$(find ~/.claude -name worktrees -prune -o -path '*kpopper*/scripts/kpopper' -print 2>/dev/null | sort -V | tail -1)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`check` exits non-zero on an undeclared gap: a dependency that is not an entry (unless the
|
|
127
|
+
judgment declares it missing with `blocked_on`), a dependency with no snapshot, a predicate
|
|
128
|
+
naming something undeclared, prose sitting in a predicate field, or a plain-comparison
|
|
129
|
+
predicate that currently holds — a judgment broken by its own condition. A declared hole is a
|
|
130
|
+
note, not a failure — a build that stays red over an honest declaration teaches records to
|
|
131
|
+
stop declaring. A dependency that moved since a judgment's snapshot is reported as `MOVED`
|
|
132
|
+
and does not fail the build either: it puts the judgment in front of a person, and it is
|
|
133
|
+
muted when the predicate names it and still evaluates false — moved, not across the line.
|
|
134
|
+
|
|
135
|
+
## The page — and the tree
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
kpopper page --open # the record as one page, in the browser
|
|
139
|
+
kpopper page --open --tree # landing on the tree
|
|
140
|
+
kpopper page --verify # deterministic checks, no browser
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Three tabs. **Now** is the arrangement this session chose, written in a brief the page keeps
|
|
144
|
+
honest: it may order, it cannot drop. **Record** is everything, arranged by nothing. **Tree**
|
|
145
|
+
is the whole record as one growing thing: what was read from the world is the root fan below
|
|
146
|
+
the ground line, worked-out values branch above it, and conclusions blossom in a canopy —
|
|
147
|
+
the three node kinds in their three colors. Everything on every tab is the same live card:
|
|
148
|
+
hover or tap for where a value came from, click a dependency to walk to it, and on the tree,
|
|
149
|
+
opening a node lights the sap — the full chain of evidence feeding it — while its neighbours
|
|
150
|
+
stir like branches in wind. A card's `tree` button prunes the tree to that node's world; one
|
|
151
|
+
chip brings the whole tree back. The tab grows in the first time it is opened, and any node can
|
|
152
|
+
be pulled — it resists like a branch, stretches its limbs, and springs back when you let go,
|
|
153
|
+
because the page keeps nothing.
|
|
154
|
+
|
|
155
|
+
## The part that does not change
|
|
156
|
+
|
|
157
|
+
1. **Every entry declares what it rests on.** Something with no declaration does not go in.
|
|
158
|
+
2. **Store what produces an output, never the output.**
|
|
159
|
+
3. **Invalidation spreads automatically; re-running a judgment never applies itself.**
|
|
160
|
+
4. **A change to the shape is valid only with a migration that leaves the build green.**
|
|
161
|
+
|
|
162
|
+
Everything else — field names, sections, renderers — is open to revision. A record that can
|
|
163
|
+
rewrite its own rules will drift unless something in it is not up for revision. These four
|
|
164
|
+
are that floor.
|
|
165
|
+
|
|
166
|
+
## Installing
|
|
167
|
+
|
|
168
|
+
The command line on its own, for any project and any editor:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pipx install kpopper # or: pip install kpopper
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The browser checks come with it — they are Node, but they install where everything else does:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
kpopper page --out record.html # then: kpopper page --checks record.html
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The Claude Code plugin — the method as a skill, the session opener, the stop gate, and the
|
|
181
|
+
same commands:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
/plugin marketplace add /path/to/kpopper
|
|
185
|
+
/plugin install kpopper@kpopper
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
or non-interactively:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
claude plugin install kpopper@kpopper --scope user
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`--scope user` makes it available in every project on the machine; `--scope project` commits
|
|
195
|
+
it to the repo you are in. Other editors are wired up from `adapters/`.
|
|
196
|
+
|
|
197
|
+
Where the channels overlap they are the same files rather than copies of them: the packages are
|
|
198
|
+
mapped onto the plugin's `scripts/`, so the reader, the renderer, the dispatcher and the browser
|
|
199
|
+
checks have nothing to keep in step, and one version number covers all of them. There is a third
|
|
200
|
+
package, on npm, and it is that same mapping: it holds the name while an open question in the
|
|
201
|
+
record — whether the renderer, the command line and the checks move to TypeScript — is still open,
|
|
202
|
+
and it carries the browser checks rather than nothing, because they are the part that is already
|
|
203
|
+
Node. This repository is the only place any of it is edited; every installed copy is a read-only
|
|
204
|
+
distribution.
|
|
205
|
+
|
|
206
|
+
## Requirements
|
|
207
|
+
|
|
208
|
+
Python 3.9+ and PyYAML — `pipx` brings it along; a plugin-only install wants
|
|
209
|
+
`pip3 install pyyaml`. The browser checks additionally want Node 18+, a Chrome/Chromium
|
|
210
|
+
binary (`CHROME=/path/to/chrome` when it is not on a known path), and `playwright-core` — the
|
|
211
|
+
driver, which none of the packages ship: it is found beside the page when the project already
|
|
212
|
+
uses Playwright, and otherwise `npm i --no-save playwright-core` next to the page is enough.
|
|
213
|
+
Nothing else.
|
|
214
|
+
|
|
215
|
+
## What would show this was not worth it
|
|
216
|
+
|
|
217
|
+
The method promises exactly one measurable thing: the opening cost of session number N. If
|
|
218
|
+
after five sessions the sixth does not open cheaper, the method did not return what it cost,
|
|
219
|
+
and you can drop it with a clear conscience.
|