feat(schema): TTL fast-path skips per-invocation /api/schema/ fetch#35
Merged
Conversation
Resolves #34. Before: every `nsc` invocation fetched `/api/schema/` from NetBox so the cached CommandModel could be hash-compared, even when the cache was seconds old. NetBox access logs showed a `GET /api/schema/` between every PATCH from agent-driven bulk updates — pure waste. After: `Defaults.schema_refresh` (already a config field, never wired) controls a TTL fast-path: - `daily` (new default) — trust the cache for 24h between live fetches - `weekly` — trust for 7 days - `manual` — trust indefinitely - `on-hash-change` — legacy behaviour, fetch every invocation Adds `nsc --refresh-schema <subcmd>` to force a one-shot live fetch bypassing the TTL. The SKILL.md gains a "Performance" section guiding agents to prefer one filtered `list --all` over N `get` calls and one NDJSON `--apply` over shell loops. Behaviour change: the default policy moves from `on-hash-change` to `daily`. Users who pinned `schema_refresh: on-hash-change` in their config keep the legacy behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tch, partial writes Adversarial review of #35 surfaced four P1s and two P2s. Address all: P1.1 mtime-based freshness is fragile. A `touch`, backup-restore, or `cp -p` could fake freshness, and a clock that jumped forward then back could keep a stale entry "fresh" indefinitely. Replace with an explicit `<hash>.meta.json` sidecar carrying `{"fetched_at": <epoch>}`. Reject entries whose sidecar is more than 60s in the future. Pre-existing cache files without a sidecar are treated as stale (one-time refetch on upgrade). P1.2 fast path skipped hash validation. `_find_fresh_cached` and `_find_any_cached` previously did `CommandModel.model_validate_json` on the raw cache file without verifying that the file's embedded `schema_hash` matches the filename. A tampered or copied JSON file would be trusted on the DAILY path. Both helpers now route through `CacheStore.load`, which already enforces the hash check. P1.3 cache writes were non-atomic. `CacheStore.save` did `Path.write_text` directly; a concurrent reader could observe a partial file. Now writes via temp-file + `os.replace`. Same for the sidecar. `prune_orphans` deletes the sidecar alongside its cache file. P1.4 default-flip had no migration note. Add Unreleased section to CHANGELOG.md documenting the `on-hash-change -> daily` change, the `--refresh-schema` escape hatch, and the one-time refetch on upgrade. P2.5 `docs/architecture/schema-loading.md` listed `on-hash-change` as the default and referenced a nonexistent `nsc refresh` command. Rewritten to reflect the TTL fast-path, sidecar layout, hash validation, and `--refresh-schema`. P2.6 added an end-to-end CLI test exercising the brittle wire-through that unit tests for `force_refresh=True` could not prove: Typer parsing -> `_extract_global_overrides` -> `CLIOverrides` -> `build_runtime_context` -> `resolve_command_model`. Proves DAILY default uses the fast-path on a warm cache and `--refresh-schema` actually bypasses it. New tests: - sidecar-missing forces refetch under DAILY (upgrade path) - future-dated sidecar (>60s) is rejected - fast path rejects hash mismatch on cache file content - `CacheStore.save` writes a sidecar with a numeric `fetched_at` near now - `load_fetched_at` returns None when missing or corrupt 608 tests pass; ruff + ruff format + mypy --strict clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 7, 2026
Merged
thomaschristory
added a commit
that referenced
this pull request
May 7, 2026
… self-heals (#42) Resolves #39. Before: when `_build_and_cache` fetched `/api/schema/` and the live hash matched an existing cache file, it returned the cached CommandModel without bumping the `<hash>.meta.json` sidecar. An aged-out sidecar — or a legacy cache from before #35 — therefore never gained proof of freshness, so the TTL fast-path failed on every subsequent invocation and `nsc` refetched `/api/schema/` between every command, exactly the behaviour #34 was supposed to eliminate. After: `CacheStore.touch_fetched_at` rewrites just the sidecar (no re-serialize of the cache file), and `_build_and_cache` calls it on the cache-hit-by-hash branch. First post-upgrade invocation refetches once, seeds the sidecar, and the fast path is active from there. Tests cover four `touch_fetched_at` cases (refresh, legacy seed, no-cache no-op, invalid hash) and two end-to-end scenarios via `resolve_command_model` (legacy cache without sidecar, aged cache with unchanged hash). Co-authored-by: Thomas Christory <tchristory@partner.auchan.fr> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GET /api/schema/between every PATCH that surfaced in the issue logs.Defaults.schema_refreshconfig field as a real TTL fast-path:daily(new default, 24h),weekly(7d),manual(∞),on-hash-change(legacy, fetch every call).nsc --refresh-schema <subcmd>for one-shot forced refresh bypassing the TTL.list --allover Ngetcalls and one NDJSON--applyover shell loops, plus documents the new TTL behaviour.Behaviour change
Default policy moves from
on-hash-change→daily. Users with explicitdefaults.schema_refreshin their config keep their setting. The previous default was the source of the wasted round-trips; the new default matches whatkubectldoes for discovery cache.Test plan
tests/schema/test_source_ttl.pycovering: fresh/stale cache under daily, manual ignores age, force_refresh bypass, on-hash-change preserved, no-cache bootstrap, schema-override wins, default constant, and per-policy TTL values.test_cache_hit_skips_rebuildstill asserts legacy fetch-every-time when the policy ison-hash-change.just lintpasses (ruff + ruff format + mypy --strict).just test— 600 passed, 1 skipped.nsc --helpshows the new--refresh-schemaflag.🤖 Generated with Claude Code