Refines the staleness file resolution (00e00a0) by folding in the two
genuine merits of @TPAteeq's parallel fix (#1558), which independently
and correctly diagnosed the same root-mismatch bug:
- Layout-ordered candidates: try the layout-appropriate root FIRST (the
graphify-out parent for the standard layout, graph.json's own dir for a
flat layout) before the other. The prior order tried the grandparent
first unconditionally, which in a flat layout (graph.json at the project
root) could fingerprint a same-named file one directory up. Existence
checking is kept on top, so a defeated name heuristic or a stale
.graphify_root marker still falls through to the real file.
- Adds @TPAteeq's .graphify_root-marker-driven regression test, plus a
flat-layout test that pins the ordering (editing the real file flips
stale; editing the same-named decoy one dir up does not).
Co-Authored-By: tpateeq <mohammedateequddin399@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The overlay fingerprint resolved a node's source_file against
graph_path.parent (the graphify-out/ dir), but source_file is stored
relative to the PROJECT root — so graphify-out/auth.py never existed and
_is_stale flagged EVERY verdict "code changed since — re-verify" the
moment it was written. (The original staleness test used an absolute
source_file, which masked it.)
Fix: resolve the file by trying the likely roots in order (.graphify_root
marker, graphify-out's parent, graph.json's own dir, cwd) and use the
first that exists — the same search at write and read — and fingerprint
file CONTENT only (sha256 of bytes, no path mixed in) so the hash is
root-independent and a committed sidecar stays valid across checkouts.
Drops the brittle directory-name-based root guess.
Adds a regression test with a relative source_file under the graphify-out
layout (stale=False right after reflect, True after an edit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Projects the verdicts `graphify reflect` already distills (preferred /
tentative / contested, exponential time-decayed) into a derived
experiential layer the read surfaces consume, so accumulated agent
experience actually shows up where you look — without polluting the
structural graph.
Design (grounded in agent-memory + provenance literature; a redesign of
the #1542 approach):
- SIDECAR, not graph.json stamping. `reflect` writes `.graphify_learning.json`
next to graph.json (an additional output, so the git hooks produce it
automatically). graph.json stays purely structural; nothing leaks into
GraphML; no graph.json churn. Mirrors the named-graph / event-sourcing
separation of durable truth from a derived layer.
- Reuses the existing reflect aggregate (its `_decay` is the
recency-weighted exponential model; `_finalize_sources` the
classification) — no new scoring.
- PROVENANCE: each verdict carries the source questions/dates that produced
it (cap 5, most-recent first).
- STALENESS: each verdict stores the node's file fingerprint; on read, a
changed source file flags the verdict stale ("code changed since —
re-verify") rather than presenting a confident lesson on rewritten code.
- CONTESTED surfaced distinctly (useful N / dead-end M), not averaged away.
- DEAD-ENDS stay QUERY-SCOPED — never a node-level status; they appear only
in the report as question -> nodes.
- Read surfaces (explain / query+MCP / GRAPH_REPORT / graph.html) merge the
overlay at read time, sanitized; un-annotated graphs are byte-identical.
Deferred (logged): letting verdicts influence query/seed traversal — the
recommender feedback-loop / Matthew-effect risk means that needs
propensity correction + exploration, not naive biasing.
Builds on the idea in #1441/#1542 (thanks @TPAteeq).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two cross-platform fixes salvaged from #1502:
- to_graphml: nx.write_graphml raises ValueError on None attribute
values, so a node/edge carrying a null field crashed the export.
Coerce None -> "" for node and edge attributes before writing.
- save-result: add --answer-file as an alternative to --answer so long
or multiline answers can be passed via a file instead of a fragile
inline shell arg (notably Windows/PowerShell quoting). Exactly one of
--answer / --answer-file is required.
The rest of #1502 (a version downgrade and a hand-edited generated
skill-windows.md that fails skillgen --check, plus duplicated
windows-scripts) is left for rework on the PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
graphify reflect renders LESSONS.md from the memory docs and the
.graphify_analysis.json / .graphify_labels.json sidecars, but reflect --if-stale
only stat'd the memory docs and graph.json. So after community analysis or labels
changed (without the graph changing), --if-stale wrongly reported lessons fresh and
skipped the regen, leaving LESSONS.md stale. The freshness check now also stats the
analysis and labels sidecars, using the same custom --analysis / --labels paths the
run itself uses (so the check and the run can't disagree about which files matter),
and treats a missing sidecar as not-an-input. This makes the documented "no-op when
LESSONS.md is newer than every input" contract actually true.
Ported from PR #1470 by @oleksii-tumanov.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Saving the same Q&A more than once duplicated lines in the "known dead ends" and
"corrections" sections: both lists were appended per memory doc with no key, while
node scoring already dedups by node. They now collapse by question, keeping the
most recent entry (docs are processed oldest-first, so a re-corrected question
shows its latest correction). Output stays deterministic, ordered by (date,
question). Applied to both the flat lists and the per-community buckets.
Found by a user running it on a 104-file Go codebase. Added a regression test
covering the dedupe and the recency-wins correction. Full suite 2389 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both the agent (session start) and the post-commit hook can run reflect; the runs
are deterministic and idempotent, but back-to-back ones are wasted work. Add
`graphify reflect --if-stale`, which no-ops when LESSONS.md is already at least as
new as every input (the memory docs and the graph). The skill's session-start
guidance now uses `--if-stale`, so when the hook just refreshed the file the
agent's run costs almost nothing, while a skill-only install still refreshes
on demand.
New lessons_fresh() helper + 5 tests (mtime freshness in each direction, and the
CLI skip/run behavior). Regenerated per-host references + re-blessed expected/;
all five skillgen guards pass; full suite 2388 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the deterministic work-memory loop: `save-result --outcome
useful|dead_end|corrected [--correction]` records how a saved Q&A turned out, and
`graphify reflect` aggregates graphify-out/memory/ into a deterministic
reflections/LESSONS.md an agent loads next session.
Source nodes are scored, not counted: signed, recency-decayed (useful +,
dead_end/corrected -, configurable --half-life-days, default 30), so a fresh dead
end outweighs a stale useful. A node is "preferred" only once corroborated by
>=--min-corroboration distinct results (default 2); others are "tentative", and
mixed-signal nodes render once as "contested" (recency-wins). Source nodes are
matched to the graph by label OR id, and citations whose node no longer exists are
dropped, so a plain `graphify update` after deleting code clears stale lessons.
Deterministic, no LLM; bare save-result and existing behavior unchanged.
Rigorously verified end-to-end on real data: corroboration boundary, recency flip,
contested verdict, foreign/malformed memory docs, cold start, 300-doc scale +
byte-stable output, and the node-gate dropping deleted-code lessons after update.
Full suite 2383 passed; skillgen --check clean; ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>