Skip to content

feat: status contract — Skip(reason, kind), warnings in JSON, enrichment counters, resource_logger (zeeker 0.9.0) - #10

Merged
houfu merged 2 commits into
mainfrom
claude/status-contract
Jul 16, 2026
Merged

feat: status contract — Skip(reason, kind), warnings in JSON, enrichment counters, resource_logger (zeeker 0.9.0)#10
houfu merged 2 commits into
mainfrom
claude/status-contract

Conversation

@houfu

@houfu houfu commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Stacked PR — based on claude/build-lifecycle (#9). Merge #9 first; GitHub retargets this PR to main when #9's branch is deleted.

Why

A logging audit across all five data projects found the root ambiguity that every repo's "Build Monitoring Guide" exists to work around: a resource can only raise (FAIL) or return a list (empty = SKIP, rendered "no data returned"), so "up to date" and "my proxy is down" are indistinguishable; --json omits warnings entirely; and enrichment pipelines that update thousands of rows report 0 records. AI monitors were left inferring failure from run duration.

The contract

1. from zeeker import Skipraise Skip("TAILSCALE_PROXY unset — proxy required", kind="blocked") (kinds: up_to_date, blocked, disabled). Caught on every fetch path (sync, async, schema-check sample, --parallel pre-warm — the Skip is cached so fetch_data still runs exactly once). Renders as [SKIP] name <reason> (blocked) (0.1s), lands in --json/--progress-file as skip_reason/skip_kind. Returning [] still works unchanged (up_to_date, "no data returned"). New --fail-on-blocked flag exits 1 on blocked skips (default off). The string-matched skip detection is gone — status is typed end-to-end.

2. Warnings reach machine-readable output. ResourceOutcome.warnings + BuildReport.build_warnings (S3-sync failures included) now appear in the JSON payload; plain mode streams WARN[resource]:/WARN[build]: lines.

3. __zeeker_report__ enrichment counters. A resource sets a module-level dict ({"updated": 50, "notes": "phase2 drained 25"}); the processor consumes it defensively on every outcome path (success/skip/crash, try/finally) into extra_counts — so a "skipped" resource that did 50 rows of enrichment shows updated=50 on its line, in the SUMMARY footer, and in JSON.

4. zeeker_common.buildlog.resource_logger(name) — stdlib-only logger emitting the exact line grammar the data repos adopted this week ({name}: done — k=v, {name}: ABORTED (reason) — … to stderr, {name}: SKIPPED (reason)), with optional JSONL mirroring via ZEEKER_BUILDLOG_JSONL.

Versions: zeeker 0.8.0 → 0.9.0 (also fixes __init__.__version__ which was desynced at 0.6.0), zeeker-common 0.1.0 → 0.2.0.

Review process

Two adversarial reviewers (correctness; API-design/compat) produced 10 findings — all verified real, all fixed, notably:

  • Rich markup injection: a Skip reason containing [socks5h://…] raised MarkupError inside the progress callback, turning a healthy build into a fatal exit-2. All user-controlled strings in TTY rendering are now escaped.
  • Blocked skips advanced the freshness marker: _zeeker_updates.last_updated was bumped during outages, silently losing data for time-based incremental resources. Blocked/disabled skips no longer touch it; up_to_date still does.
  • Retry decorators retried Skip: zeeker_common.retry now passes Skip through instead of burning backoff and resurfacing it as RetryError → FAILED.
  • Downstream-compat docs: zeeker>=0.9.0 requirement and a try/except ImportError shim pattern for repos on older versions.

Tests

uv run pytest: 447 passed, 5 skipped (pre-existing), coverage 78.29%. New suites cover Skip on all paths, blocked exit codes, JSON schema additions, counter consumption/reset/crash-survival, markup safety, and buildlog grammar.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cuh5eJDfVowgpp22s7siE1


Generated by Claude Code

claude added 2 commits July 16, 2026 08:14
…chment counters, resource_logger

Makes build output unambiguous for AI monitoring agents:

- New `zeeker.Skip(reason, kind)` exception (kinds: up_to_date, blocked,
  disabled) lets fetch_data distinguish "up to date" from "proxy down".
  Handled on sync + async paths, cached by AsyncExecutor so the
  single-fetch-per-build guarantee holds (schema-check sample fetch and
  parallel pre-warm both re-observe the same Skip without re-running
  fetch_data). Skip during the schema check is not a schema error.
  fragments_on_skip still runs fragments with context [] on a raised Skip.
- Skip detection now flows through a typed ValidationResult.skipped flag
  set by the processor — the "No data returned" string-matching in
  builder._build_resource_outcome is gone. Returning [] still maps to a
  skip with kind "up_to_date" and no reason.
- New `zeeker build --fail-on-blocked`: exit 1 when any resource skipped
  with kind "blocked" (documented in the exit-codes section).
- ResourceOutcome.warnings + BuildReport.build_warnings now carry
  ValidationResult warnings into --json / --progress-file payloads;
  plain mode streams WARN[resource]/WARN[build] lines, rich mode shows a
  warnings count in Notes and lists them when verbose.
- Optional module-level __zeeker_report__ dict ({"updated": 50, ...,
  "notes": "..."}) read leniently after fetch_data and cleared per build;
  surfaces as ResourceOutcome.extra_counts/notes, appended to success/skip
  lines and aggregated into the SUMMARY footer — enrichment pipelines that
  UPDATE rows are no longer invisible.
- zeeker-common: new zeeker_common.buildlog.resource_logger(name) —
  prefix-consistent info/warn/error/done/aborted/skipped logging (pure
  stdlib, stdout/stderr split) with best-effort JSONL sink via
  ZEEKER_BUILDLOG_JSONL; lazily exported from the package root.
- Version bumps: zeeker 0.8.0 -> 0.9.0 (top-level __version__ synced),
  zeeker-common 0.1.0 -> 0.2.0; uv.lock refreshed.
- Tests: 43 new (packages/zeeker/tests/test_status_contract.py,
  packages/zeeker-common/tests/test_buildlog.py); full suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cuh5eJDfVowgpp22s7siE1
…er, count grammar

Reviewer findings addressed:

- Rich markup injection (helpers.py): skip reasons, error messages,
  __zeeker_report__ count keys, warnings, fatal/FTS errors, post-hook
  output, and tracebacks are now markup-escaped in TTY mode. Previously
  Skip("proxy down [/socks5]") raised MarkupError inside the progress
  callback (which runs inside build_database's try), converting a healthy
  build into a fatal exit-2 error and aborting remaining resources;
  bracketed tokens like [socks5h://...] were silently swallowed as style
  tags. End-to-end regression test drives the TTY callback through a
  real build.

- Blocked/disabled skips no longer advance _zeeker_updates.last_updated
  (builder.py). Skip(kind="blocked") means "could not even check the
  source"; bumping the freshness marker made time-based incremental
  resources permanently miss anything published during an outage.
  up_to_date skips (raised or returned-[]) still bump it, matching the
  legacy returned-[] behavior.

- resource_logger count grammar (zeeker-common buildlog): done()/aborted()
  now render noun-first ("done — 3 new, 2 skipped, 0 failed") to match
  the grammar the data repos' Build Monitoring Guides already parse
  ("(\d+) new"), instead of "new=3" which would have silently broken
  every monitoring regex on migration. Underscores render as spaces;
  the JSONL sink keeps raw keys.

- sync_retry/async_retry no longer retry Skip (zeeker-common retry):
  tenacity's default predicate retried a declared skip with 2-10s
  backoff and resurfaced it as RetryError -> resource FAILED. Skip is
  excluded by class name across the MRO (zeeker-common cannot import
  zeeker), so shimmed Skip classes pass through too.

- Skip raised from fetch_fragments_data is now honored (processor.py):
  it gracefully skips just the fragments phase instead of flipping the
  resource to "failed" and the build to exit 1. Skip in transform_data
  remains out of contract and is documented as such.

- __zeeker_report__ is consumed on every exit path (processor.py
  try/finally): counters set before a fetch_data crash now surface on
  the failed outcome and in --json, and counters set during the
  fragments phase (fetch_fragments_data / transform_data) merge into
  the outcome (summing on key collision) instead of being dropped.

- TTY --verbose no longer prints every warning twice (cli.py):
  echo_warnings is skipped when _emit_rich's verbose loop lists the
  same warnings under the summary table.

- Docs: CLAUDE.md + Skip docstring now state the minimum version
  (zeeker>=0.9.0) for `from zeeker import Skip`, the try/except
  ImportError shim pattern for repos pinned to older zeeker, the
  retry-decorator interaction, where Skip is honored, and the
  blocked/disabled freshness-marker semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cuh5eJDfVowgpp22s7siE1
Base automatically changed from claude/build-lifecycle to main July 16, 2026 15:24
@houfu
houfu merged commit 19f4a72 into main Jul 16, 2026
2 checks passed
@houfu
houfu deleted the claude/status-contract branch July 16, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants