LearnNewsExamplesServices
Frontmatter
id17938
titleA missing emission stage or a stale corpus must fail the run
stateClosed
labels
bugaitestingregressionbuild
assigneesneo-opus-grace
createdAtAug 31, 2026, 12:49 PM
updatedAtAug 31, 2026, 1:52 PM
githubUrlhttps://github.com/neomjs/neo/issues/17938
authorneo-opus-grace
commentsCount0
parentIssue17920
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 31, 2026, 1:52 PM

A missing emission stage or a stale corpus must fail the run

Closed v13.2.0/archive-v13-2-0-chunk-17 bugaitestingregressionbuild
neo-opus-grace
neo-opus-grace commented on Aug 31, 2026, 12:49 PM

Refs #17920

Context

Delivery leaf of #17920. That ticket carries six ACs across two sides of a repository boundary; this one owns exactly the two that are Engine-side, mechanically verifiable, and deliverable in one PR — the guards that make the outage loud. Its remaining four (Brain-side emission ownership, the watchdog alarm-body correction, and the two post-merge scheduled-run observations) stay with the parent.

Filed at @neo-gpt-emmy's RA-3 on PR #17927, whose corrected framing is right and which I initially argued against: a leaf for the slice a PR actually delivers is delivery decomposition, not a ticket opened to satisfy a body linter. The same shape is live in this repo twice today — #17924 under #17922, and #17931 under #17921.

c623b2f63c removed the GitHub Workflow corpus emission stage from emissionCommands. The pipeline went shorter, not red: two stages, both green, no-generated-changes, exit 0, for five days — while the surviving derivation stage kept rebuilding the portal from a corpus frozen on 2026-08-26 and committing the result. The full account of how that happened, who it hurt, and why the stage cannot simply be restored in place is in #17920 and is not repeated here.

The Problem

Two independent silences, one outage.

1. A vanished producer reads as a shorter run. emissionCommands is a table; the pipeline executes whatever is in it. Nothing anywhere states which stages are expected, so removing an entry cannot fail — there is no expectation to violate. no-generated-changes is reachable by having no generator, and that is indistinguishable from "nothing changed upstream".

2. Derivation consumes whatever age the corpus happens to be. content indexes and SEO reads resources/content/{issues,pulls,discussions} and writes the portal's chunked indexes, id maps and crawler manifests. It has no opinion about how old that input is, so a frozen corpus produces fresh derived commits on schedule. Anyone reading run status or commit recency concludes the pipeline is healthy — the derived commits are the evidence of health, and they are exactly what the defect manufactures.

The two guards are separate because the failures are: (1) sees an Engine-side table shrink, (2) sees an absent producer that never appears in that table at all, because the emitter is Agent-OS-side. Neither subsumes the other.

The Architectural Reality

  • buildScripts/dataSyncPipeline.mjs — emissionCommands (the stage table) and the derivation stage that reads the corpus. Both guards land here, as assertions that run before the work they gate.
  • buildScripts/dataSyncWatchdog.mjs — owns the corpus-age policy. Its resolver is the single site the guard must read through; a guard that imports the default constant while the alarm resolves the WATCHDOG_MAX_CORPUS_AGE_HOURS override does not share policy with it, it merely shares a number. That divergence was real and is what makes "one effective policy owner" part of this leaf rather than an afterthought.
  • Corpus freshness must be read from committed dates (git log -1 --format=%cI), never mtime: this runs in an ephemeral Actions checkout where every file's mtime is checkout time, so an mtime probe certifies a five-day-old corpus as seconds fresh.
  • ADR 0040 §2.3 fixes the direction — the Agent OS consumes the published Engine and is never imported by it. Every guard in this leaf is Engine-side only and adds no neo-agent-brain dependency. Locator: ADR 0040 is not in this repository; it lives at neo-agent-brain:learn/agentos/decisions/0040-agentos-extraction-topology.md §2.3, and grepping learn/agentos/decisions/ here returns nothing because it is the wrong tree, not because the citation is wrong.

The Fix

  1. A stage-set assertion. The pipeline asserts the emission stages it expects before any stage executes — so a pipeline that lost a generator cannot first publish what the survivors derive and only then mention the gap. That derived commit is the thing that made the outage read as healthy. The expectation is declared as an independent literal, not derived from the table it checks: a witness derived from the thing it witnesses attests to nothing.
  2. A corpus-freshness guard. Derivation refuses a corpus older than the effective corpus-age band, naming every stale facet with its measured age. A facet with no visible commit resolves to stale, not to "no age to compare" — that direction fails closed.
  3. One effective policy owner. Both the guard and the watchdog alarm resolve the band through a single resolution site, per call rather than through an imported constant, so an override set after module load is honoured and the two cannot disagree about the same corpus.

Contract Ledger Matrix

Target Surface Source of Authority Behavior Fallback Docs Evidence
emissionCommands stage set this ticket (delegated from #17920) asserted by label before execution; a missing expected stage fails the run none — no-generated-changes must be unreachable by having no generator dataSyncPipeline.mjs JSDoc negative control: delete a stage entry, the run must go red and every unrelated arm must stay green
content indexes and SEO input this ticket (delegated from #17920) refuses to derive from a corpus older than the effective corpus-age band none — deriving from stale input is the defect stage JSDoc negative control: age the corpus past the band, the stage must not report success
corpus-age policy dataSyncWatchdog.mjs one resolution site, resolved per call by every consumer; the constant is never the policy none dataSyncWatchdog.mjs JSDoc negative control: set WATCHDOG_MAX_CORPUS_AGE_HOURS, assert guard and alarm agree about one corpus at one moment
freshness measurement this ticket committed dates via git log, never mtime absent commit ⇒ stale stage JSDoc an mtime-based probe must be shown to certify a known-stale corpus as fresh

Decision Record impact

aligned-with ADR 0040 — §2.3 is why both guards are Engine-side and why the producer's absence is detectable only on the corpus-age axis. No ADR is amended.

Acceptance Criteria

  • A negative control proves the stage-set assertion: removing an expected entry from emissionCommands turns the run red, shown failing against the pre-guard implementation first — today it turns the run shorter.
  • The assertion runs before any stage executes, so a run that lost a generator publishes nothing derived before failing.
  • The expected stage set is an independent literal; a mutation that derives it from emissionCommands must be shown to make the witness vacuous.
  • A negative control proves the freshness guard: with the corpus aged past the effective band, the derivation stage does not report success and names each stale facet with its measured age.
  • Guard and watchdog alarm resolve one effective band through a single site: with WATCHDOG_MAX_CORPUS_AGE_HOURS set, an arm that passes no explicit threshold reds against a re-imported-constant mutation and nothing else. An arm that passes an explicit threshold is not sufficient — it stays green against the defect.
  • Freshness is measured from committed dates; a facet with no visible commit is treated as stale.
  • No Engine-side code imports or spawns Brain substrate; package.json gains no neo-agent-brain dependency. check-engine-brain-boundary exit 0.

Out of Scope

  • Restoring GitHub corpus emission itself (#17920 AC-1). The producer is Agent-OS-side; this leaf makes its absence loud, it does not fill the hole.
  • The #17834 alarm body still asserting "runs fail on schedule" against a zero streak (#17920 AC-5).
  • The two post-merge scheduled-run observations (#17920 AC-6) — they need wall-clock runs, not a merge.
  • Collapsing or re-designing the watchdog's two axes. The corpus-age axis is the only thing that caught this; the axes stay separate.
  • A tighter band than 48h. That needs a producer heartbeat proving the corpus current rather than merely recently changed, and that producer is in the other repository.

Avoided Traps

  • Deriving the expected stage set from emissionCommands. It reads as DRY and is the one thing that makes the guard vacuous. The credential vocabulary in the same file is rightly centralized — there, drift is the defect; here, drift is the signal.
  • mtime for freshness. Cheap, obvious, and certifies the exact five-day-old corpus this ticket exists for as seconds fresh, because the Actions checkout stamps every file.
  • Treating an unmeasurable facet as fresh. Failing open on the measurement is the same class of silence as the outage.
  • Sharing a default and calling it shared policy. A constant and a resolver agree right up until an override exists, which is precisely when the answer matters.

Related

  • Parent: #17920 — keeps AC-1, AC-5 and AC-6.
  • Delivering PR: #17927 — will target this leaf with Resolves, keeping Related: #17920.
  • #17834 — the breaching alarm that closes on its own recovery path once emission returns; not this leaf.
  • #17916, neomjs/neo-agent-brain#250 — the two sibling mechanisms removed by the same split and discovered only by their silence.
  • Decomposition precedent: #17924 under #17922; #17931 under #17921.

Handoff Retrieval Hints

  • query_raw_memories: "Data Sync emission stage removed by the split, pipeline stayed green over a frozen corpus"
  • Commit anchors: c623b2f63c (the removal), c623b2f63c^ (the last tree with three emission stages), 7ecd6b8b6e (a derived-artifact commit produced from the frozen corpus).
  • Reproduce the shape: compare emissionCommands at c623b2f63c^ against HEAD, then read a recent data-sync-pipeline run's stage lines.

Live latest-open sweep: checked the latest 20 open issues at 2026-08-31T10:50Z; no equivalent found — #17920 is the parent, not a duplicate. A2A in-flight claim sweep over the last 12 messages (all read-states, ~20 min window): no peer claim on this scope.

Origin Session ID: e2de1726-d093-47b9-8163-1a0533842a3b

Authored by @neo-opus-grace (Anthropic Claude Opus 5, Claude Code).

tobiu cross-referenced by PR #17927 on Aug 31, 2026, 12:49 PM
tobiu closed this issue on Aug 31, 2026, 1:52 PM