Skip to the content.

SF-010 · Local green is not remote green

Status: stable | Family: C · Wrong Evidence | Confidence: high | as of 2026-09-19

One line. Every local gate passes; the published artifact is still inconsistent, because the published artifact is a different object.

Symptom

A repository has good hygiene. Linters, schema checks, link checks and consistency gates all pass locally and in CI. Then someone reads the published state — via API, on the live site, in the registry — and finds disagreements that have existed for weeks.

local gates:  PASS  PASS  PASS
published:    6 inconsistencies, 4 missing attachments, 1 wrong type

The local checks were never wrong. They were answering a different question: “is this working tree internally consistent?” The question that mattered was “does the published state match intent?”

Why it is silent

The two propositions are conflated, and the local one is cheap to verify while the remote one requires a network read, credentials and a comparison against expectations.

A third factor makes it durable: the published object has its own state that the local tree cannot see. Version fields rewritten on deposit, affiliation fields populated from an account profile, types re-mapped by the platform’s own rules, visibility defaulted to non-public. None of these appear in a diff, because they were never in a file.

Minimal reproduction

There is no code reproduction — the reproduction is a comparison.

# local: passes
python check_local.py && echo GREEN

# published: a different claim (illustrative)
curl -s "https://api.example.org/records/<id>" | jq '{type, visibility, fields: (.metadata | keys)}'
# → type: journal-article   (declared as: working-paper)
# → visibility: limited     (assumed: public)

Observed: local GREEN, published disagrees on type and visibility — Expected after fix: a gate that reads the live state and fails on the difference

Self-check

For every external channel — registry, package index, website, profile, store listing — ask:

  1. Is there a check that reads the live state? (Not “is there a check for the local source of it”.)
  2. Does it compare against a declared expectation, or only print what it finds?
  3. Does it check the fields the platform rewrites — type, visibility, ordering, derived fields?

Any channel with no to the first question is unverified. Any with no to the second is SF-005 waiting to happen.

Fix

One read-back check per published channel, comparing live state against a declared expectation.

live = fetch_published(record_id)                 # authoritative for the published claim
exp  = registry[record_id]                        # declared expectation

problems = []
if live["visibility"] != exp["required_visibility"]:
    problems.append(f"visibility {live['visibility']} != {exp['required_visibility']}")
if live["type"] != exp["expected_type"]:
    problems.append(f"type {live['type']} != {exp['expected_type']}")
for field in exp["required_fields"]:
    if not live.get(field):
        problems.append(f"required field empty: {field}")

raise SystemExit(1 if problems else 0)

Design notes:

Negative control Expected result
Change an expectation in the registry so it no longer matches live the check exits non-zero, naming the field
Point it at a deliberately wrong record id exits non-zero (not a silent no-op — see SF-001)

中文要点