notegraph 0.2.0__tar.gz → 0.3.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.
- {notegraph-0.2.0/notegraph.egg-info → notegraph-0.3.0}/PKG-INFO +1 -1
- {notegraph-0.2.0 → notegraph-0.3.0}/note_cli/SKILL.md +8 -3
- {notegraph-0.2.0 → notegraph-0.3.0}/note_cli/_cli_check.py +32 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/note_cli/cli.py +27 -8
- {notegraph-0.2.0 → notegraph-0.3.0}/note_cli/goal.py +94 -11
- {notegraph-0.2.0 → notegraph-0.3.0}/note_cli/store.py +11 -0
- {notegraph-0.2.0 → notegraph-0.3.0/notegraph.egg-info}/PKG-INFO +1 -1
- {notegraph-0.2.0 → notegraph-0.3.0}/pyproject.toml +1 -1
- {notegraph-0.2.0 → notegraph-0.3.0}/LICENSE +0 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/README.md +0 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/note_cli/__init__.py +0 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/note_cli/_concurrency_check.py +0 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/note_cli/graph.py +0 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/note_cli/render.py +0 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/notegraph.egg-info/SOURCES.txt +0 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/notegraph.egg-info/dependency_links.txt +0 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/notegraph.egg-info/entry_points.txt +0 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/notegraph.egg-info/requires.txt +0 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/notegraph.egg-info/top_level.txt +0 -0
- {notegraph-0.2.0 → notegraph-0.3.0}/setup.cfg +0 -0
|
@@ -37,15 +37,20 @@ to agents working other branches in parallel.
|
|
|
37
37
|
- **Only criteria named in the current goal are accepted.** If a rubric no
|
|
38
38
|
longer fits the work, change the goal (`note goal --set ... --criterion ...`)
|
|
39
39
|
rather than inventing a criterion at score time.
|
|
40
|
-
- **Totals are recomputed against the current goal
|
|
41
|
-
|
|
40
|
+
- **Totals are recomputed against the current goal**, so when the goal moves,
|
|
41
|
+
every earlier score is marked `STALE` and `note check` fails until you
|
|
42
|
+
re-score. A stale total is arithmetic, not judgement — do not compare arms
|
|
43
|
+
on one. `note goal --set` tells you how many scores it just invalidated.
|
|
42
44
|
|
|
43
45
|
## Correcting a node
|
|
44
46
|
|
|
45
47
|
Nothing is ever edited in place. `note supersede <id> "corrected content"`
|
|
46
48
|
appends a new node and an edge `old -superseded_by-> new`, which moves the
|
|
47
49
|
frontier to the new node and takes the old one out of `note tails`. Type and
|
|
48
|
-
tags are inherited unless you pass new ones
|
|
50
|
+
tags are inherited unless you pass new ones, along with whatever goal the
|
|
51
|
+
original targeted — so the correction is already linked and only needs a
|
|
52
|
+
score. The node you corrected stops competing for best and stops being asked
|
|
53
|
+
for one.
|
|
49
54
|
|
|
50
55
|
## Orientation
|
|
51
56
|
|
|
@@ -78,8 +78,40 @@ def main_check():
|
|
|
78
78
|
after = {n["id"] for n in graphtails()}
|
|
79
79
|
assert 5 not in after and 6 in after, (before, after)
|
|
80
80
|
|
|
81
|
+
# supersede carries the targets edge forward, so the correction is already
|
|
82
|
+
# linked and only needs judging
|
|
83
|
+
from note_cli import store as _st
|
|
84
|
+
assert {"from": 6, "to": 1, "relation": "targets"} in _st.read("edges.jsonl")
|
|
85
|
+
run("score", "6", "consistency=0.5")
|
|
86
|
+
|
|
87
|
+
# and the node it corrected must no longer compete for best
|
|
88
|
+
from note_cli import goal as _g
|
|
89
|
+
rows, _unassigned = _g.rollup()
|
|
90
|
+
winners = [b[1] for _n, b in rows if b]
|
|
91
|
+
assert 5 not in winners, f"superseded #5 still winning: {winners}"
|
|
92
|
+
|
|
81
93
|
run("status")
|
|
82
94
|
|
|
95
|
+
# the bug this feature exists for: moving the goal must not leave a
|
|
96
|
+
# confident-looking number with `check` calling it clean
|
|
97
|
+
import time
|
|
98
|
+
time.sleep(1) # timestamps are second-resolution
|
|
99
|
+
run("goal", "--set", "moved the bar",
|
|
100
|
+
"--criterion", "consistency:3:style holds",
|
|
101
|
+
"--criterion", "renamed:1:was legibility")
|
|
102
|
+
try:
|
|
103
|
+
run("check")
|
|
104
|
+
raise AssertionError("expected SystemExit(1): every score is now stale")
|
|
105
|
+
except SystemExit as e:
|
|
106
|
+
assert e.code == 1, e.code
|
|
107
|
+
run("goals") # must mark the stale rows, not just print
|
|
108
|
+
run("show", "3")
|
|
109
|
+
|
|
110
|
+
for nid in ("2", "3", "4", "5", "6"):
|
|
111
|
+
run("score", nid, "consistency=0.5", "renamed=0.5")
|
|
112
|
+
run("check") # re-scored under the new goal: clean again
|
|
113
|
+
|
|
114
|
+
|
|
83
115
|
# --version must report what the package metadata says, so a bug report
|
|
84
116
|
# names a real release
|
|
85
117
|
from note_cli import __version__
|
|
@@ -50,8 +50,13 @@ def cmd_goal(args):
|
|
|
50
50
|
criteria = [goalmod.parse_criterion(c) for c in (args.criterion or [])]
|
|
51
51
|
if not criteria:
|
|
52
52
|
raise SystemExit("--set needs at least one --criterion name:weight:rubric")
|
|
53
|
+
was_fresh = [s for s in goalmod.latest_scores().values()
|
|
54
|
+
if not goalmod.is_stale(s, goalmod.current_goal())]
|
|
53
55
|
goalmod.set_goal(args.set, criteria)
|
|
54
56
|
print(f"goal set, {len(criteria)} criteria")
|
|
57
|
+
if was_fresh:
|
|
58
|
+
print(f"{len(was_fresh)} score(s) now judged against an older goal — "
|
|
59
|
+
f"`note check` lists them")
|
|
55
60
|
return
|
|
56
61
|
g = goalmod.current_goal()
|
|
57
62
|
if not g:
|
|
@@ -87,18 +92,26 @@ def cmd_score(args):
|
|
|
87
92
|
print(f"{goalmod.add_score(args.id, scores, args.note):.3f}")
|
|
88
93
|
|
|
89
94
|
|
|
95
|
+
def _stale_mark(score, g):
|
|
96
|
+
return " STALE — judged against an older goal" if goalmod.is_stale(score, g) else ""
|
|
97
|
+
|
|
98
|
+
|
|
90
99
|
def cmd_goals(args):
|
|
91
100
|
rows, unassigned = goalmod.rollup()
|
|
101
|
+
g = goalmod.current_goal()
|
|
92
102
|
for n, best in rows:
|
|
93
103
|
if best:
|
|
94
|
-
total, aid,
|
|
95
|
-
|
|
104
|
+
total, aid, score = best
|
|
105
|
+
mark = _stale_mark(score, g)
|
|
106
|
+
print(f"{n['id']:>4} {n['content']}\n"
|
|
107
|
+
f" best {total:.3f} from #{aid} ({score['timestamp']}){mark}")
|
|
96
108
|
else:
|
|
97
109
|
print(f"{n['id']:>4} {n['content']}\n (no scored attempts)")
|
|
98
110
|
if unassigned:
|
|
99
111
|
print("\n(unassigned) — scored but targeting no goal:")
|
|
100
|
-
for n, total,
|
|
101
|
-
print(f"{n['id']:>4} {total:.3f} {n['content']}
|
|
112
|
+
for n, total, score in unassigned:
|
|
113
|
+
print(f"{n['id']:>4} {total:.3f} {n['content']} "
|
|
114
|
+
f"({score['timestamp']}){_stale_mark(score, g)}")
|
|
102
115
|
|
|
103
116
|
|
|
104
117
|
def cmd_supersede(args):
|
|
@@ -109,9 +122,9 @@ def cmd_supersede(args):
|
|
|
109
122
|
def cmd_check(args):
|
|
110
123
|
"""Exit nonzero while any attempt is loose, so a hook or an agent can gate
|
|
111
124
|
on it rather than trusting itself to remember the loop."""
|
|
112
|
-
untargeted, unscored = goalmod.unfinished()
|
|
113
|
-
if not untargeted and not unscored:
|
|
114
|
-
print("clean: every attempt targets a goal and
|
|
125
|
+
untargeted, unscored, stale = goalmod.unfinished()
|
|
126
|
+
if not untargeted and not unscored and not stale:
|
|
127
|
+
print("clean: every attempt targets a goal and is scored against the current one")
|
|
115
128
|
return
|
|
116
129
|
if untargeted:
|
|
117
130
|
print("attempts targeting no goal — `note link <id> <goal> --rel targets`:")
|
|
@@ -121,6 +134,11 @@ def cmd_check(args):
|
|
|
121
134
|
print("attempts with no score — `note score <id> name=0.0-1.0`:")
|
|
122
135
|
for n in unscored:
|
|
123
136
|
print(_line(n))
|
|
137
|
+
if stale:
|
|
138
|
+
print("attempts judged against an older goal — their totals are "
|
|
139
|
+
"arithmetic, not judgement. Re-score them:")
|
|
140
|
+
for n in stale:
|
|
141
|
+
print(_line(n))
|
|
124
142
|
raise SystemExit(1)
|
|
125
143
|
|
|
126
144
|
|
|
@@ -152,7 +170,8 @@ def cmd_show(args):
|
|
|
152
170
|
if s:
|
|
153
171
|
g = goalmod.current_goal()
|
|
154
172
|
total = goalmod.weighted_total(s["scores"], g["criteria"] if g else [])
|
|
155
|
-
print(f" score: {total:.3f} {s['scores']} ({s['timestamp']})"
|
|
173
|
+
print(f" score: {total:.3f} {s['scores']} ({s['timestamp']})"
|
|
174
|
+
f"{_stale_mark(s, g)}")
|
|
156
175
|
if s.get("note"):
|
|
157
176
|
print(f" {s['note']}")
|
|
158
177
|
if args.id in {m["id"] for m in graphmod.merges(nodes, edges)}:
|
|
@@ -21,6 +21,39 @@ def set_goal(text, criteria):
|
|
|
21
21
|
"timestamp": store.now()})
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
SUPERSEDED = "superseded_by"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def superseded_ids(edges):
|
|
28
|
+
"""Nodes that have been corrected by a later one.
|
|
29
|
+
|
|
30
|
+
Append-only never removes anything, so a superseded node keeps its goal
|
|
31
|
+
link and its score. Both the rollup and the loop check have to skip them:
|
|
32
|
+
otherwise a corrected-away attempt goes on competing for best, and a goal
|
|
33
|
+
revision makes the check demand that it be re-judged.
|
|
34
|
+
"""
|
|
35
|
+
return {e["from"] for e in edges if e["relation"] == SUPERSEDED}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def goal_revision(g):
|
|
39
|
+
"""Identity of a goal revision. `goal.jsonl` is append-only and written
|
|
40
|
+
under the lock, so its timestamp identifies the revision."""
|
|
41
|
+
return g["timestamp"] if g else None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_stale(score, g):
|
|
45
|
+
"""True when this score was judged against a different goal revision than
|
|
46
|
+
the current one.
|
|
47
|
+
|
|
48
|
+
Its total is still computed — against today's criteria — which is exactly
|
|
49
|
+
why it has to be flagged: the number looks like a judgement and is
|
|
50
|
+
arithmetic. A score line written before this field existed has no
|
|
51
|
+
`goal_ts`, so it reads stale, which is the honest answer: we do not know
|
|
52
|
+
what standard it was made against.
|
|
53
|
+
"""
|
|
54
|
+
return score.get("goal_ts") != goal_revision(g)
|
|
55
|
+
|
|
56
|
+
|
|
24
57
|
def parse_criterion(s):
|
|
25
58
|
parts = s.split(":", 2) # first two colons only; rubrics contain colons
|
|
26
59
|
if len(parts) != 3:
|
|
@@ -69,6 +102,7 @@ def add_score(nid, scores, note=None):
|
|
|
69
102
|
if unknown:
|
|
70
103
|
raise SystemExit(f"unknown criteria {unknown}; goal has {sorted(known)}")
|
|
71
104
|
store.append(SCORE_FILE, {"node": nid, "scores": scores, "note": note,
|
|
105
|
+
"goal_ts": goal_revision(g),
|
|
72
106
|
"timestamp": store.now()})
|
|
73
107
|
return weighted_total(scores, g["criteria"])
|
|
74
108
|
|
|
@@ -86,6 +120,7 @@ def rollup():
|
|
|
86
120
|
latest = latest_scores()
|
|
87
121
|
by_id = {n["id"]: n for n in nodes}
|
|
88
122
|
|
|
123
|
+
corrected = superseded_ids(edges)
|
|
89
124
|
targeted, assigned = {}, set()
|
|
90
125
|
for e in edges:
|
|
91
126
|
if e["relation"] == "targets":
|
|
@@ -98,10 +133,10 @@ def rollup():
|
|
|
98
133
|
continue
|
|
99
134
|
best = None
|
|
100
135
|
for aid in targeted.get(n["id"], ()):
|
|
101
|
-
if aid not in latest:
|
|
136
|
+
if aid not in latest or aid in corrected:
|
|
102
137
|
continue
|
|
103
138
|
cand = (weighted_total(latest[aid]["scores"], criteria), aid,
|
|
104
|
-
latest[aid]
|
|
139
|
+
latest[aid])
|
|
105
140
|
if best is None or cand[0] > best[0]:
|
|
106
141
|
best = cand
|
|
107
142
|
rows.append((n, best))
|
|
@@ -109,31 +144,40 @@ def rollup():
|
|
|
109
144
|
# a scored attempt with no goal is a linking mistake, not a category of
|
|
110
145
|
# work — it must not simply vanish from the rollup
|
|
111
146
|
unassigned = [(by_id[i], weighted_total(latest[i]["scores"], criteria),
|
|
112
|
-
latest[i]
|
|
113
|
-
for i in sorted(latest)
|
|
147
|
+
latest[i])
|
|
148
|
+
for i in sorted(latest)
|
|
149
|
+
if i not in assigned and i in by_id and i not in corrected]
|
|
114
150
|
return rows, unassigned
|
|
115
151
|
|
|
116
152
|
|
|
117
153
|
def unfinished():
|
|
118
|
-
"""Attempts that break the loop, as (untargeted, unscored).
|
|
154
|
+
"""Attempts that break the loop, as (untargeted, unscored, stale).
|
|
119
155
|
|
|
120
156
|
Only `attempt` nodes are held to it — a decision or a plain note has
|
|
121
157
|
nothing to score. This is what `note check` exits nonzero on, so a hook
|
|
122
158
|
or an agent can gate on it instead of trusting itself to remember.
|
|
159
|
+
|
|
160
|
+
`stale` is the one that used to pass silently: the attempt is linked and
|
|
161
|
+
scored, but judged against a goal revision that is no longer current, so
|
|
162
|
+
its total is arithmetic rather than judgement.
|
|
123
163
|
"""
|
|
124
164
|
nodes = store.read("nodes.jsonl")
|
|
125
165
|
edges = store.read("edges.jsonl")
|
|
126
166
|
latest = latest_scores()
|
|
167
|
+
g = current_goal()
|
|
127
168
|
targeting = {e["from"] for e in edges if e["relation"] == "targets"}
|
|
128
|
-
|
|
169
|
+
corrected = superseded_ids(edges)
|
|
170
|
+
untargeted, unscored, stale = [], [], []
|
|
129
171
|
for n in nodes:
|
|
130
|
-
if n["type"] != "attempt":
|
|
172
|
+
if n["type"] != "attempt" or n["id"] in corrected:
|
|
131
173
|
continue
|
|
132
174
|
if n["id"] not in targeting:
|
|
133
175
|
untargeted.append(n)
|
|
134
176
|
elif n["id"] not in latest:
|
|
135
177
|
unscored.append(n)
|
|
136
|
-
|
|
178
|
+
elif is_stale(latest[n["id"]], g):
|
|
179
|
+
stale.append(n)
|
|
180
|
+
return untargeted, unscored, stale
|
|
137
181
|
|
|
138
182
|
|
|
139
183
|
def demo():
|
|
@@ -196,15 +240,54 @@ def demo():
|
|
|
196
240
|
assert [u[0]["id"] for u in unassigned] == [orphan], unassigned
|
|
197
241
|
|
|
198
242
|
# orphan targets nothing; a1/a2 target g and are scored
|
|
199
|
-
untargeted, unscored = unfinished()
|
|
243
|
+
untargeted, unscored, stale = unfinished()
|
|
200
244
|
assert [n["id"] for n in untargeted] == [orphan], untargeted
|
|
201
|
-
assert unscored == [], unscored
|
|
245
|
+
assert unscored == [] and stale == [], (unscored, stale)
|
|
202
246
|
|
|
203
247
|
fresh = store.add_node("just tried, not judged yet", type="attempt")
|
|
204
248
|
store.add_edge(fresh, g, "targets")
|
|
205
|
-
untargeted, unscored = unfinished()
|
|
249
|
+
untargeted, unscored, stale = unfinished()
|
|
206
250
|
assert [n["id"] for n in untargeted] == [orphan], untargeted
|
|
207
251
|
assert [n["id"] for n in unscored] == [fresh], unscored
|
|
252
|
+
assert stale == [], stale
|
|
253
|
+
|
|
254
|
+
# a score carries the goal revision it was judged against
|
|
255
|
+
assert latest_scores()[a1]["goal_ts"] == current_goal()["timestamp"]
|
|
256
|
+
assert not is_stale(latest_scores()[a1], current_goal())
|
|
257
|
+
|
|
258
|
+
# renaming a criterion must NOT silently leave a confident-looking number
|
|
259
|
+
import time
|
|
260
|
+
time.sleep(1) # timestamps are second-resolution
|
|
261
|
+
set_goal("moved the bar", [{"name": "consistency", "weight": 3.0, "rubric": "x"},
|
|
262
|
+
{"name": "renamed", "weight": 1.0, "rubric": "y"}])
|
|
263
|
+
assert is_stale(latest_scores()[a1], current_goal())
|
|
264
|
+
untargeted, unscored, stale = unfinished()
|
|
265
|
+
assert [n["id"] for n in stale] == [a1, a2], stale # orphan is untargeted, fresh unscored
|
|
266
|
+
|
|
267
|
+
# a re-score under the new goal clears it
|
|
268
|
+
add_score(a1, {"consistency": 1.0})
|
|
269
|
+
assert not is_stale(latest_scores()[a1], current_goal())
|
|
270
|
+
_, _, stale = unfinished()
|
|
271
|
+
assert [n["id"] for n in stale] == [a2], stale
|
|
272
|
+
|
|
273
|
+
# a score line predating this feature has no goal_ts and must read stale
|
|
274
|
+
assert is_stale({"scores": {}}, current_goal())
|
|
275
|
+
|
|
276
|
+
# --- a corrected attempt must stop competing ---
|
|
277
|
+
add_score(a2, {"consistency": 1.0, "renamed": 1.0}) # a2 is now the best
|
|
278
|
+
rows, _ = rollup()
|
|
279
|
+
assert rows[0][1][1] == a2, rows[0]
|
|
280
|
+
|
|
281
|
+
fixed = store.supersede(a2, "arm B1_P2, corrected")
|
|
282
|
+
rows, _ = rollup()
|
|
283
|
+
best_id = rows[0][1][1] if rows[0][1] else None
|
|
284
|
+
assert best_id != a2, "a superseded attempt must not win the rollup"
|
|
285
|
+
assert best_id == a1, (best_id, a1) # fixed is unscored, so a1 wins
|
|
286
|
+
|
|
287
|
+
# and it must not be demanded back by the loop check either
|
|
288
|
+
_, unscored, stale = unfinished()
|
|
289
|
+
assert a2 not in [n["id"] for n in unscored] + [n["id"] for n in stale]
|
|
290
|
+
assert fixed in [n["id"] for n in unscored], "the correction still needs judging"
|
|
208
291
|
|
|
209
292
|
print("goal: ok")
|
|
210
293
|
|
|
@@ -138,6 +138,14 @@ def supersede(old, content, type=None, tags=()):
|
|
|
138
138
|
"timestamp": now(),
|
|
139
139
|
})
|
|
140
140
|
append("edges.jsonl", {"from": old, "to": nid, "relation": "superseded_by"})
|
|
141
|
+
# Carry the goal links forward. Without this the correction targets
|
|
142
|
+
# nothing while the corrected node keeps its link AND its score, so
|
|
143
|
+
# `note goals` can report an attempt that was corrected away as the
|
|
144
|
+
# best one — silently.
|
|
145
|
+
for e in read("edges.jsonl"):
|
|
146
|
+
if e["from"] == old and e["relation"] == "targets":
|
|
147
|
+
append("edges.jsonl", {"from": nid, "to": e["to"],
|
|
148
|
+
"relation": "targets"})
|
|
141
149
|
return nid
|
|
142
150
|
|
|
143
151
|
|
|
@@ -198,6 +206,9 @@ def demo():
|
|
|
198
206
|
assert rows[new_id]["type"] == rows[a]["type"], "type is inherited"
|
|
199
207
|
assert rows[new_id]["tags"] == rows[a]["tags"], "tags are inherited"
|
|
200
208
|
assert {"from": a, "to": new_id, "relation": "superseded_by"} in read("edges.jsonl")
|
|
209
|
+
# the correction must inherit what the original targeted, or the rollup
|
|
210
|
+
# keeps crediting the node that was corrected away
|
|
211
|
+
assert {"from": new_id, "to": g, "relation": "targets"} in read("edges.jsonl")
|
|
201
212
|
try:
|
|
202
213
|
supersede(999, "nope")
|
|
203
214
|
raise AssertionError("expected SystemExit")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|