LearnNewsExamplesServices
Frontmatter
id17148
titleAn optional DevIndex intake denial freezes all corpus publication
stateClosed
labels
bugairegressionarchitecturebuild
assigneesneo-opus-ada
createdAtAug 15, 2026, 9:39 AM
updatedAtAug 15, 2026, 10:51 AM
githubUrlhttps://github.com/neomjs/neo/issues/17148
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 15, 2026, 10:51 AM

An optional DevIndex intake denial freezes all corpus publication

Closed Backlog/active-chunk-16 bugairegressionarchitecturebuild
neo-opus-ada
neo-opus-ada commented on Aug 15, 2026, 9:39 AM

Context

#17131 (the standing DATA-SYNC-ALARM) has been red since 2026-08-14T11:49:16Z — 15 consecutive scheduled failures on dev at filing time. The working hypothesis on pickup was that a merged PR broke it. That is falsified (evidence below): nothing the failing stage executes changed in the regression window. The pipeline is red because of a GitHub-side condition on the DevIndex intake repositories — but the blast radius of that condition is a Neo defect, and that defect is what this ticket fixes.

Observation vs inference. Observed: the failing run/step/error, the last-green control, the window diff, and the live API behaviour of the intake repos. Inferred: the GitHub-side cause of the stargazer 404 — unresolved, and deliberately not this ticket's target.

Regression window, pinned

run head UTC
last green 31797752252 45570db8e 2026-08-14T11:49:16Z
first red 31804178775 dd25854d4 2026-08-14T13:19:19Z

Every failure since fails the same job step — 6: Run bounded Data Sync emission and publish — at the same stage, DevIndex Opt-In:

[GitHub] [OptIn Stars] GraphQL Query Failed: GraphQL Query Errors: Resource not accessible by integration
[DataSync] stage "DevIndex Opt-In" failed with an UNRECOGNIZED error class; ... Declared credential scope: `intake`.

Why "a merged PR broke it" is falsified

  • git diff --stat 45570db8e..dd25854d4 touches only generated data-sync output (resources/content/**, apps/portal/resources/data/**, sitemap.xml), one learn doc, and test/playwright/** specs. Zero changes to apps/devindex/**, buildScripts/dataSync*.mjs, or .github/workflows/**.
  • apps/devindex/services/OptIn.mjs last changed 2026-02-22; apps/devindex/services/GitHub.mjs 2026-07-23; .github/workflows/data-sync-pipeline.yml 2026-07-28. Each mechanism predates the window it would have to explain.
  • Control: in the last green run the identical query ran under the identical intake credential and succeeded — 12:05:23Z[OptIn] No new opt-in requests found. Same code, same credential, 90 minutes apart.

What is actually denied

The intake installation is intact: assertDataSyncAccess's repository{id} probe reports reachable in every red run including the latest. Only the stargazers connection is denied. Independently reproducible outside the pipeline with a broad token:

probe neomjs/neo neomjs/devindex-opt-in neomjs/devindex-opt-out
GraphQL stargazers returns edges edges: []
REST /stargazers 200 (3 rows) 404 404
REST repo root 200 200 (stargazers_count: 16) 200

A public, unarchived, non-disabled repository reporting 16 stars whose stargazer list 404s is a GitHub-side condition, not a Neo regression. Resolving that needs operator/admin action and is out of scope here.

The Problem

emitGeneratedData runs its stages sequentially and rethrows on the first stage failure (buildScripts/dataSyncPipeline.mjs:648). DevIndex Opt-In is stage 3 of 7. When it throws:

  • stages 4–7 never run — including content indexes and SEO (stage 7), which is what makes the corpus consumable;
  • the publish path below the loop is never reached, so the corpus generated by stage 2 is discarded;
  • nothing commits, so resources/content/** on dev stays frozen.

An optional enrichment read for the DevIndex opt-in feature therefore takes down corpus publication for the whole repository. At filing, mirrors/KB are ~19h stale and every downstream consumer (portal data, KB ingestion) is reading a frozen corpus. That coupling is the defect.

The mechanism to decouple it already exists and is already used: publishGeneratedProgressOnFailure. Stage 2 carries it. A stage that sets it defers its error, lets the remaining stages run, publishes what was generated, and then rethrows (:742-744, :844-849) — the run still goes RED. It is precisely "publish what we have, then fail loudly"; it was simply never applied to the intake stages.

The Architectural Reality

  • buildScripts/dataSyncPipeline.mjs:67-122emissionCommands, the 7-stage table. Only stage 2 sets publishGeneratedProgressOnFailure.
  • buildScripts/dataSyncPipeline.mjs:606-655 — the emission loop; :639-646 defer vs :648 throw.
  • buildScripts/dataSyncPipeline.mjs:742-744, :844-849deferredError is rethrown at both exits, so deferral is not suppression.
  • buildScripts/dataSyncPreflight.mjs:33-36REQUIRED_REPOSITORIES carries purpose: 'OptIn stargazer read'.
  • buildScripts/dataSyncPreflight.mjs:49-84probeRepository issues repository(owner,name){id} only. The purpose string claims a stargazer read the probe never performs, so the preflight logged neomjs/devindex-opt-in reachable (OptIn stargazer read) in the very runs whose stargazer read was denied 12 minutes later.

Second-order cost: because the preflight passed, the failure surfaced 12 minutes into the run as UNRECOGNIZED error class instead of at minute 1 with a named cause.

The Fix

  1. Decouple enrichment from publication. Set publishGeneratedProgressOnFailure: true on the four DevIndex intake stages (DevIndex Opt-In, Opt-Out, Spider, Updater). The corpus then publishes and the run still fails on the deferred error.
  2. Preserve every deferred failure. With four stages able to defer, a single deferredError slot silently drops all but the last. Accumulate and rethrow an aggregate so the run names every stage that failed.
  3. Make the preflight probe what it claims. Extend probeRepository so the devindex-opt-in probe exercises the stargazers connection it advertises. A denial then fails fast at minute 1 naming repo and connection.

Deliberately NOT doing: adding Resource not accessible by integration to OptIn.mjs's NOT_FOUND skip branch. That converts a real permission regression into a silent no-op and the opt-in feature rots undetected — a carve-out that quiets the guard opens a silent channel.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
emissionCommands[].publishGeneratedProgressOnFailure buildScripts/dataSyncPipeline.mjs:610 (exists, default false) true on the 4 DevIndex intake stages unchanged for non-intake stages JSDoc at the table unit spec: intake failure still publishes and still throws
emitGeneratedData return {deferredError} buildScripts/dataSyncPipeline.mjs:580, :653 carry all deferred stage failures, not the last single-failure shape unchanged when only one defers @returns JSDoc unit spec: 2 deferred stages ⇒ both named
probeRepository({owner,name,connection,token}) buildScripts/dataSyncPreflight.mjs:49 NEW required connection arg; selects that connection AND requires it to resolve denial ⇒ same {ok:false, reason} shape JSDoc unit specs: connection is selected; partial denial (id present, connection null) is a failure
REQUIRED_REPOSITORIES[].connection buildScripts/dataSyncPreflight.mjs:33 NEW field; names the connection the consuming stage reads, so probe and purpose cannot drift absent ⇒ probe cannot select, treated as failure JSDoc unit spec: stargazers for opt-in, issues for opt-out
assertDataSyncAccess failure entries buildScripts/dataSyncPreflight.mjs:156 entries now carry connection additive; existing fields unchanged inline unit spec: per-repository classification preserved
denialRemedy(connection) (module-private) buildScripts/dataSyncPreflight.mjs NEW; remediation derives from the refused connection instead of one fixed Issues/Metadata string unmapped connection ⇒ says so, never inherits another's advice JSDoc unit spec: stargazer denial must NOT print the Issues remedy
emitGeneratedData preflight handling buildScripts/dataSyncPipeline.mjs:594 a preflight denial is DEFERRED, not thrown; no stage is skipped DATA_SYNC_PREFLIGHT_ONLY=true still throws directly JSDoc on options.preflight unit specs: denial defers; Opt-Out/Spider/Updater still execute

Decision Record impact

none — this restores the stage-isolation property the existing publishGeneratedProgressOnFailure primitive already encodes; it introduces no new architectural authority.

Acceptance Criteria

  • A DevIndex Opt-In stage failure no longer aborts emission: stages 4–7 still run and the publish path is reached.
  • With an intake stage failing, generated corpus changes are committed and pushed to dev.
  • The run still exits non-zero on that deferred failure — #17131 must not be silenced by this change.
  • Two or more deferred stage failures are all named in the final error, not just the last.
  • probeRepository exercises the stargazers connection for devindex-opt-in; a denial is detected before collection starts and names repo + connection. (Corrected during review: originally said the denial "fails the preflight before collection starts". It is deferred, not fatal — rethrowing would re-couple publication to the intake identity one layer above the stage table this ticket decouples, freezing the corpus for the same reason, only faster.)
  • A denied capability does not stop stages that never consume it: an Opt-In stargazer denial still leaves Opt-Out, Spider, Updater and content indexes and SEO executing. (Added from @neo-gpt's PR #17149 review: the first implementation skipped every tokenScope: 'intake' stage, but Spider queries search/community endpoints and Updater reads users/:name/orgs — neither touches a DevIndex repository.)
  • Remediation derives from the refused connection: a stargazer denial must not print the Issues: Read and write remedy that the workflow already satisfies.
  • Unit coverage in test/playwright/unit/ai/buildScripts/DataSyncPipeline.spec.mjs for: intake-failure-still-publishes, still-throws, multi-deferred aggregation, preflight stargazer denial.
  • No change to OptIn.mjs error classification (no new skip branch).

Out of Scope

  • The GitHub-side stargazer 404 on both intake repos — operator/admin domain.
  • Widening the intake App's permissions (.github/workflows/data-sync-pipeline.yml:96-97) — human-owned credential settings.
  • #16532 (deprecated app-id in token minting) — adjacent, separate auth defect.
  • The two unrelated isolated failures inside the same window (31755201012: ladies-of-code community-scan 404; 31775846068: penalty-box path). Different causes, both self-recovered.

Avoided Traps

  • Diagnosing from the newest failing run. The newest run shows only the current top layer. Attribution came from the first red after the last green plus a step-sample across the window — which is also what separated the three distinct failure modes that all land on step 6.
  • Blaming a merged PR because the timing fits. The window diff and file histories date every candidate mechanism against the window; nothing in it touches this path.
  • Silencing the denial to turn the pipeline green. Green would then mean "opt-in silently dead". This unfreezes publication and keeps the run red.

Related

  • #17131 — the standing alarm this unfreezes. It stays open until the GitHub-side stargazer condition is resolved; that is correct behaviour, not a gap.
  • #16532 — Data Sync mints tokens with a deprecated app-id.

Retrieval Hint: "DevIndex intake denial freezes corpus publication; stargazers 404 on both intake repos" Retrieval Hint: Commit range 45570db8e..dd25854d4