Skip to content

MOO-72 Commit 8: production hardening, feature flags, concurrency limits, version provenance - #18

Merged
OwenTanzer merged 2 commits into
mainfrom
moo72-commit8-production-hardening
Jul 30, 2026
Merged

MOO-72 Commit 8: production hardening, feature flags, concurrency limits, version provenance#18
OwenTanzer merged 2 commits into
mainfrom
moo72-commit8-production-hardening

Conversation

@OwenTanzer

Copy link
Copy Markdown
Owner

Summary

Code-and-docs portion of MOO-72 Commit 8 ("Production hardening, final domain cutover, and rollback controls"). Per the plan, this PR deliberately excludes the live infrastructure steps (promoting Railway, DNS cutover, Moopertonic Hub link, reaffirming ALLOWED_OWNERS=*) — those are real production/external-system changes that get proposed and confirmed individually after this merges, not bundled into a single approval.

  • Feature flags: FILE_LAYER_ENABLED, FUNCTION_LAYER_ENABLED, DEGRADED_ANALYSIS_ENABLED (gates the file layer's real pyan3→tree-sitter degrade path specifically — there's no separate "renderer fallback" subsystem to gate), EXPERIMENTAL_INTERACTIONS_ENABLED (currently gates no behavior — none exists yet, shipped as a documented no-op hook, default false). Surfaced via a new authenticated GET /api/capabilities so the UI can hide/disable a disabled layer's affordance instead of leaving it clickable behind a 503, and in /readyz's authenticated detail for operators.
  • Concurrency limiter (server/lib/concurrency-limiter.js): a server-wide cap (MAX_CONCURRENT_ANALYSES, default 4, explicitly documented as not load-tested — a conservative starting point, not evidence-based) shared across all five analysis routes, acquired only around the actual expensive work — after the cache-miss check, and for the file layer specifically only by the in-flight registry's operation creator, never a subscriber joining someone else's already-running work. A 503 at capacity carries both a Retry-After header and a retryAfterMs body field; the client (src/state/serverRequest.js) previously only read Retry-After for a 429, silently dropping this delay for a 503 — fixed.
  • Two previously-hardcoded resource limits now configurable: GITHUB_FETCH_CONCURRENCY (was a hardcoded default param) and PYAN3_MAX_BUFFER_BYTES (same).
  • Version/commit provenance: one shared source (scripts/generate-build-info.mjs, run as part of npm run build) consumed by both the server (/healthz) and the UI (a small corner badge) — the server runs directly via node server/index.js, never processed by Vite, so a define-only injection would never have reached it. Env-override → git rev-parse HEAD (caught) → "unknown", never an uncaught build failure.
  • docs/deployment.md (new): confirmed current state of auth/allowlist/rate-limiting/secrets, two distinct rollback controls (release rollback via a temporary git worktree vs. domain rollback), and the DNS/Hub cutover runbook as a recorded procedure for later.

Test plan

  • Full unit suite: 705/706 passing (1 expected symlink-permission skip), real run, ~19.5s.
  • New/extended tests: concurrency limiter (real counter, release-on-throw, creator-vs-subscriber composition with InFlightRegistry), feature-flag config validation, /api/capabilities//readyz featureFlags check, readBuildInfo/generate-build-info.mjs's pure computeBuildInfo (env-override, git-fallback, uncaught-failure-proof, dirty detection), client 503-retry parsing.
  • Manual/real, against a locally spawned server with a real GitHub token: FILE_LAYER_ENABLED=false/api/graph/file returns a real 503, /api/capabilities reflects it, /healthz reports real version/commitSha/dirty.
  • Manual/real: MAX_CONCURRENT_ANALYSES=1 with two genuinely concurrent real repository-analysis requests (psf/requests, octocat/Hello-World) → one succeeds with a real graph, the other gets a real 503 with Retry-After: 2 and retryAfterMs: 2000.
  • Live infrastructure cutover steps (Railway promotion, DNS, Hub link) — explicitly out of scope for this PR, to follow individually.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

https://claude.ai/code/session_01MAoVCjfAo9e7jWCRrMwyvq

…its, version provenance, rollback runbook)

Adds feature flags (FILE_LAYER_ENABLED, FUNCTION_LAYER_ENABLED,
DEGRADED_ANALYSIS_ENABLED, EXPERIMENTAL_INTERACTIONS_ENABLED) surfaced via
GET /api/capabilities so the UI can hide/disable a disabled layer instead
of leaving a still-clickable control that just 503s. Adds a server-wide
concurrency limiter shared across all five analysis routes, acquired only
around the actual expensive work (creator-only for the file layer's
in-flight-deduplicated pyan3 runs), with a 503+Retry-After response the
client now parses correctly (previously only 429 was). Makes GitHub fetch
concurrency and pyan3's subprocess buffer size configurable instead of
hardcoded. Adds a single shared version/commit-provenance source
(scripts/generate-build-info.mjs) consumed by both the server (/healthz)
and the UI (a small corner badge), since the server runs outside Vite and
a define-only injection would never reach it. Documents the confirmed
current state of auth/allowlist/rate-limiting, two distinct rollback
controls (release vs. domain), and the DNS/Hub cutover runbook in
docs/deployment.md -- the live infrastructure steps themselves are
explicitly out of this commit's scope, to be proposed individually later.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAoVCjfAo9e7jWCRrMwyvq
@linear-code

linear-code Bot commented Jul 30, 2026

Copy link
Copy Markdown

MOO-72

@OwenTanzer OwenTanzer left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three production-hardening issues to address before merge.

const readsRetryAfter = status === 429 || status === 503;
const retryAfterHeader = readsRetryAfter && headers && typeof headers.get === 'function' ? headers.get('Retry-After') : null;
const headerRetryAfterMs = retryAfterHeader && !Number.isNaN(Number(retryAfterHeader)) ? Number(retryAfterHeader) * 1000 : null;
const bodyRetryAfterMs = typeof safeBody.retryAfterMs === 'number' ? safeBody.retryAfterMs : null;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Honor the response body's explicit retryability contract

The new disabled-layer routes return 503 { retryable: false }, but this mapper only considers diagnostics[0].retryable before defaulting every 5xx to true. As a result, a direct disabled-layer response is exposed to the UI as retryable despite the server explicitly saying otherwise (confirmed with mapServerJsonResponse(false, 503, {retryable:false}, ...), which yields retryable === true). Please let a top-level boolean safeBody.retryable take precedence before the status fallback, and add the disabled-503 case to server-request.test.mjs.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — mapServerJsonResponse now checks typeof safeBody.retryable === 'boolean' after the diagnostic-level check but before the status-based 429/5xx defaults, so an explicit top-level retryable: false (the disabled-layer 503s) is honored instead of being overridden by the "5xx defaults retryable" fallback. Added 3 new cases to server-request.test.mjs: explicit false on a 503 is honored, explicit true still works as before, and a diagnostic-level retryable still takes precedence over a top-level one (diagnostic is more specific). Verified with the full suite (716/717, 1 expected skip), pushed as d8c2189.

Comment thread src/state/capabilitiesClient.js Outdated
* @returns {Promise<{fileLayerEnabled: boolean, functionLayerEnabled: boolean, degradedAnalysisEnabled: boolean, experimentalInteractionsEnabled: boolean} | null>}
*/
export async function fetchCapabilities(serverAuthToken) {
if (cached && cachedForToken === serverAuthToken) return cached;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Do not cache feature flags forever by auth token

The auth token is not a configuration version: an operator can toggle these flags and redeploy without changing the token. This permanent cache means an existing tab never observes that rollout/rollback; most notably, after a layer is re-enabled, a page that cached false keeps refusing to call it until a full reload. Please revalidate capabilities (for example with a short TTL or explicit invalidation tied to deployment/version) so these production rollback controls take effect for long-lived sessions.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added a 30s TTL (CAPABILITIES_CACHE_TTL_MS) to the module-level cache, so a long-lived tab revalidates within a bounded time instead of caching by token forever. Chose 30s as a balance: cheap endpoint, doesn't need real-time push, but a rollback/re-enable becomes visible in-session soon rather than requiring a full reload. Added 5 new tests in capabilities-client.test.mjs (injected now() for determinism): serves from cache within the TTL, refetches once it elapses, always refetches on a different token, and the two existing failure-tolerance behaviors (non-ok response, thrown fetch error) still return null without poisoning the cache. Pushed as d8c2189.


const info = computeBuildInfo({
version: pkg.version,
envCommitSha: process.env.BUILD_COMMIT_SHA,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Consume Railway's provided deployment SHA

Railway provides RAILWAY_GIT_COMMIT_SHA to builds/deployments originating from GitHub, but this Railway-targeted provenance path only reads the custom BUILD_COMMIT_SHA. Whenever git metadata is absent from the remote build context and that custom alias was not configured, /healthz and the UI report unknown even though Railway supplied the exact SHA. Please fall back to process.env.RAILWAY_GIT_COMMIT_SHA before invoking git (while retaining BUILD_COMMIT_SHA as the provider-neutral override), and cover that precedence in the build-info tests. Reference: https://docs.railway.com/variables/reference

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — precedence is now explicit BUILD_COMMIT_SHA override > Railway's own RAILWAY_GIT_COMMIT_SHA > git rev-parse HEAD > "unknown". The explicit override still wins over Railway's variable deliberately (an operator setting it directly is a conscious choice that shouldn't be silently outranked), but it's no longer required just to get real provenance out of a standard GitHub-connected Railway deployment. Added 3 tests to generate-build-info.test.mjs covering the precedence: RAILWAY_GIT_COMMIT_SHA used when no override is set, BUILD_COMMIT_SHA still wins when both are set, and the git-rev-parse fallback when neither env var is set. Also updated docs/deployment.md's provenance section to describe the corrected precedence and cited the Railway variable reference doc. Pushed as d8c2189.

…che, read RAILWAY_GIT_COMMIT_SHA

- mapServerJsonResponse now honors an explicit top-level `retryable`
  boolean (e.g. the disabled-layer 503s) before falling back to the
  status-based default, so a server that explicitly says "don't retry"
  is no longer overridden.
- fetchCapabilities now expires its cache after 30s instead of caching
  forever by token -- the auth token isn't a configuration version, so a
  long-lived tab could never observe an operator's flag toggle/redeploy.
- generate-build-info.mjs now reads RAILWAY_GIT_COMMIT_SHA as a fallback
  between the explicit BUILD_COMMIT_SHA override and git rev-parse, so a
  Railway build with no git metadata in the remote build context still
  gets real provenance instead of "unknown".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAoVCjfAo9e7jWCRrMwyvq
@OwenTanzer
OwenTanzer merged commit 7719075 into main Jul 30, 2026
3 checks passed
@OwenTanzer
OwenTanzer deleted the moo72-commit8-production-hardening branch July 30, 2026 09:52
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.

1 participant