forge-cache¶
Provenance¶
- Source:
.spec/spexcode/spec-forge/forge-cache/spec.md - Source SHA-256:
9a64338f6a8234a35494523a5d4ff671408bca0ac1520188b61f83025b2696fc
Keeping the [[links]] view fresh — incrementally, or live for a dashboard — is not a product choice.
(The node is named for what it IS — the forge-side cache; the bare word freshness belongs to the eval
axis — a reading's staleness against code — and means only that everywhere else in the tree.)
resolveLinks(issues, prs, nodeIds) is already a pure function, so the problem is the classic one of
incremental view maintenance: keep output = f(state) current as state changes, instead of paying
for a cold full pull on every look. This node owns that deterministic core; it sits beside [[links]] and,
like it, is host-agnostic.
What is incremental is the fetch, never the resolution. resolveLinks is microsecond-cheap and
pure, so the cache recomputes the whole view on every read rather than maintain a second, incremental
resolution path that could disagree with the full one. The cache adds freshness; it never adds a rival
answer.
State, delta, view.
- State = the cached open-issue set + open-PR set (node ids stay git-local, from
loadSpecs). - Delta = one observed change, the single currency every source emits: an upsert (the new object,
still open) or a remove (it left the open set — closed, merged, deleted, or its
Spec:marker dropped).applyfolds one delta in, keyed by number, so it is idempotent and order-tolerant — a duplicated or out-of-order delta re-sets the same key; a remove of an absent key is a no-op. - View =
resolveLinksover the cached set. The cache also exposes the raw set itself (state()), because one consumer — the unified Issue port (spec-cli's [[issues]]) — needs every cached issue, linked or not, to map onto the one Issue type; resolution stays the only derived view, so there is still no second answer to disagree with the full one.
The state also carries a monotonic content revision. It advances only when an upsert, removal, or reconcile changes the cached objects; duplicate deltas and an equal reconcile leave it unchanged. A consumer that publishes a snapshot alongside this state carries that revision, so a reconcile finishing after a background read cannot leave its published projection permanently behind the resident cache: it is a lower bound on the next publication, not permission to poll old rows. The revision is a freshness carrier, never a second resolution path. The deterministic product proof holds the first driver response until that snapshot is published, then releases seven GitLab-shaped rows and reads the same API again; this proves the late-publication race without standing in for a credentialed live-host probe.
Reconcile is the source of truth; sources are only hints. A live source (an ETag-conditional poll, or a forge webhook) may drop, duplicate, or re-order deltas, so it is never trusted as a clean stream. Correctness is restored by reconcile — a full read through the [[port]] that overwrites the cached set wholesale. The invariant the whole design rests on, and the one this node proves:
after
reconcile(),view()equals a cold full pull by construction; and a delta stream that represents a set of changes leaves the cache identical to a reconcile of that final state.
So any number of live sources can only ever leave the cache temporarily ahead of the last reconcile,
never durably wrong — the invariant holds by construction: a delta stream representing a set of changes
leaves the cache identical to a reconcile of that final state, and reconcile() makes view() equal a cold
full pull. (Verifying this is the job of the test framework — see [[spec-eval]] — not an ad-hoc script.)
The read-only contract holds unchanged: the cache caches a read of the forge; it never writes a node's version or status (that stays git-derived — see [[spec-forge]]).
The first delta source is live: the updated-since window — the [[port]]'s optional incremental read
(listIssuesSince) feeds applyIssues (an upsert merge: an issue never leaves the set, a closed one
updates in place) while the small open-PR list is simply re-set whole. The resident cycle is
incremental-first: after the seeding reconcile, each TTL tick fetches only that window (normally one page)
instead of re-listing the world, and a periodic full reconcile stays the backstop for what an update
window cannot see (deleted/transferred issues) — exactly the "sources are hints, reconcile is truth"
contract above. Still future siblings: an ETag/If-None-Match poller (free 304s) and a webhook
receiver pushing the same delta shape; both plug into apply, neither changes it.