docs/plans/spec-command-group.md deferred this as "Abstract-staleness lint deferred — abstractOriginHash is selected (so a future pass / get can use it), but reliable staleness needs a server-provided signal; not enforced in v1."
That signal exists now, and the CLI already fetches it and throws it away.
What prompted this
While updating cor:urn:010:01 (URN grammar) I found its body saying:
Emission status — v2 emitted (the flip has landed, #697)
and its abstract ending:
The platform still EMITS v1 until the #697 migration flips emission to v2.
spec lint cor:urn:010:01 reported ✓ 1 spec(s) OK. Meanwhile hadron_get_node on the same node printed Source: abstract-stale — the MCP layer acts on the signal, the CLI doesn't.
That matters more than an ordinary doc lag: the abstract is the RAG retrieval surface. An agent asking what URN form the platform emits got back an authoritative-sounding "still EMITS v1" from the law corpus. Wrong answers of that shape propagate — this is a plausible upstream source of the v1 spellings that were still in CLAUDE.md until #333.
The signal is already on the wire
abstractOriginHash is selected by both GetNode and NodeBatch (internal/api/queries/nodes.graphql:122 and :172), so every spec get / spec lint / node get already pays for it. It is then dropped: it appears in no DTO, so hadron node get --json doesn't expose it and nothing can act on it.
Schema contract:
Spec 032 — fingerprint of the content value at the time abstract was authored. SHA-256 of plaintext content, truncated to 8 hex chars. Compared at read time against computeContentHash(node.content) to detect staleness; when the two values differ AND abstractOriginHash is non-null, the abstract may not reflect current content.
Verified computable client-side — no new query, no server change:
$ hadron api 'query($r:ID!){ node(ref:$r){ loc abstractOriginHash content } }' -F r=…cor:urn:010:01
server hash: 11a308dc
sha256(content)[:8]: 11a308dc → match
Scale: 176 of 264 spec nodes (67%) are currently stale
Swept hadronmemory.com::specs per module and computed the hash for every node:
spec nodes scanned: 264
with an abstract: 264
ABSTRACT-STALE: 176
abstract, no hash: 1 (authored pre-spec-032; undetectable)
no abstract: 0
Cross-checked against the server's own verdict rather than trusting my arithmetic — hadron_get_node reports Source: abstract-stale on cor:dmo:050:10, which the sweep flagged. Agreement.
Severity: warning, not error
Stale ≠ wrong. The hash only says the body moved since the abstract was authored; plenty of those 176 abstracts are still accurate. At 67% an error-level rule would make spec lint --all permanently red and --strict unusable — the same reasoning that made preflight's route-label-phrasing a warning in #328.
Proposed:
- rule
abstract-stale, warning, message naming both hashes so it's checkable by hand
- fires only when
abstractOriginHash is non-null and differs (a null hash is pre-spec-032, not a finding)
- silent when the node has no abstract —
abstract-present already covers that
Implementation notes
- Add
Content and AbstractOriginHash to the spec lint model; the rule is pure and unit-testable against a fixed (content, hash) pair.
- Consider exposing
abstractOriginHash in node get --json too, so it's checkable without dropping to hadron api.
- A whole-corpus sweep must batch per module:
nodeBatch(locPrefix:"cor") is rejected outright with "prefix matches more than the 200-node cap. Narrow the prefix; the batch read does not silently truncate." spec lint --all already pages via scanAllNodes, so this mostly matters for anyone writing a one-off sweep.
Out of scope
Fixing the 176. This issue is about detection; the corpus owner decides which abstracts genuinely need rewriting. cor:urn:010:01 is already fixed (#333 follow-up) and now reports clean.
docs/plans/spec-command-group.mddeferred this as "Abstract-staleness lint deferred —abstractOriginHashis selected (so a future pass /getcan use it), but reliable staleness needs a server-provided signal; not enforced in v1."That signal exists now, and the CLI already fetches it and throws it away.
What prompted this
While updating
cor:urn:010:01(URN grammar) I found its body saying:and its abstract ending:
spec lint cor:urn:010:01reported✓ 1 spec(s) OK. Meanwhilehadron_get_nodeon the same node printedSource: abstract-stale— the MCP layer acts on the signal, the CLI doesn't.That matters more than an ordinary doc lag: the abstract is the RAG retrieval surface. An agent asking what URN form the platform emits got back an authoritative-sounding "still EMITS v1" from the law corpus. Wrong answers of that shape propagate — this is a plausible upstream source of the v1 spellings that were still in CLAUDE.md until #333.
The signal is already on the wire
abstractOriginHashis selected by bothGetNodeandNodeBatch(internal/api/queries/nodes.graphql:122and:172), so everyspec get/spec lint/node getalready pays for it. It is then dropped: it appears in no DTO, sohadron node get --jsondoesn't expose it and nothing can act on it.Schema contract:
Verified computable client-side — no new query, no server change:
Scale: 176 of 264 spec nodes (67%) are currently stale
Swept
hadronmemory.com::specsper module and computed the hash for every node:Cross-checked against the server's own verdict rather than trusting my arithmetic —
hadron_get_nodereportsSource: abstract-staleoncor:dmo:050:10, which the sweep flagged. Agreement.Severity: warning, not error
Stale ≠ wrong. The hash only says the body moved since the abstract was authored; plenty of those 176 abstracts are still accurate. At 67% an error-level rule would make
spec lint --allpermanently red and--strictunusable — the same reasoning that made preflight'sroute-label-phrasinga warning in #328.Proposed:
abstract-stale, warning, message naming both hashes so it's checkable by handabstractOriginHashis non-null and differs (a null hash is pre-spec-032, not a finding)abstract-presentalready covers thatImplementation notes
ContentandAbstractOriginHashto the spec lint model; the rule is pure and unit-testable against a fixed (content, hash) pair.abstractOriginHashinnode get --jsontoo, so it's checkable without dropping tohadron api.nodeBatch(locPrefix:"cor")is rejected outright with "prefix matches more than the 200-node cap. Narrow the prefix; the batch read does not silently truncate."spec lint --allalready pages viascanAllNodes, so this mostly matters for anyone writing a one-off sweep.Out of scope
Fixing the 176. This issue is about detection; the corpus owner decides which abstracts genuinely need rewriting.
cor:urn:010:01is already fixed (#333 follow-up) and now reports clean.