#TypeScript#DevOps#开源

Local Development Dependency Readiness Contract: From Open Ports to Available Capabilities

A local service starting successfully does not mean database migrations, authorization scopes, and messaging topics are ready. This article uses versioned capability probes, stability windows, and failure classification to turn "wait and refresh" into a verifiable, reversible development startup gate.

Key Points

  • Topic Focus: TypeScript、DevOps、开源
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

A development script sees that port 5432 is reachable and starts the web application, but the home page fails because migrations are incomplete. A mock identity service returns 200 but lacks the scope required by the project. The object storage process is listening, but its initialization bucket still does not exist. These are not signs of a "slow computer." They mean the team has not defined when a dependency is actually ready. CI/CD Pipeline Design addresses the automated stages after a commit; this article focuses only on the capability conditions that must be met before a developer starts a local workspace from a repository snapshot.

Docker Compose's depends_on can wait for a health check through service_healthy or wait for a one-off task through service_completed_successfully, but the project still defines what the health command proves. A simple curl /health cannot establish that the current database schema, test tenant, or authorization policy matches the code. Risks from dependency image and lockfile upgrades remain the responsibility of Dependency Upgrade and Lockfile Compatibility Gates. The readiness contract determines whether development can safely begin in the environment resolved for this run.

Freeze readinessContract.v1

The repository stores dev.readinessContract.v1 and computes a digest over its canonicalized contents. The contract lists the repository revision, lockfile digest, supported platforms, and, for each dependency, a stable dependencyId, prerequisite edges, connection method, expected protocol, version range, required capabilities, probe timeout, and failure policy. At runtime, an independent readinessRunId records the actual image digest, endpoint, probe version, and result. Variable local paths must not be written back into the contract.

yaml
schema: dev.readinessContract.v1
dependencyId: postgres.primary
requires: [network.compose]
probe: db-schema.v2
accepts: { engine: ">=16 <18", migration: "2026071903" }
stabilityWindow: 10s
freshSamples: 3
deadline: 90s
onUnavailable: block

The only permitted state sequence is BLOCKED -> PROBING -> STABILIZING -> READY. STABILIZING must obtain three samples at separate intervals, with the first and last samples spanning the complete stabilityWindow. Every sample must establish a new connection and include the sampling time, endpoint identity, and current readinessRunId. Any failure or fresh-age violation resets the count to zero and returns the dependency to PROBING. The overall deadline covers both probing and the complete stability window. Late samples from an old run cannot contribute to the count. A timeout or capability mismatch enters FAILED; a dependency that fails after reaching READY enters STALE. An optional email previewer may declare degrade, but the database cannot be temporarily reclassified as optional. An unrecognized mandatory probe returns UNSUPPORTED_PROBE; it must not degrade to a TCP check.

Probes Verify Capabilities Without Damaging the Environment

Probes are read-only by default. PostgreSQL probes read the server version and migration table and execute a read-only query. HTTP probes validate a machine-readable version, scopes, and required fields. When write capability genuinely must be proven, the contract separately declares effectProbe.v1. It uses a restricted identity granted only to the current run and a dedicated probe table, queue, or object prefix with no triggers, sequences, CDC, or business consumers. It must never write to shared business resources. Each effect records an effectId, resource name, and PLANNED -> ACKED -> OBSERVED -> CLEANED state. If the call succeeds but its acknowledgment is lost, it enters UNKNOWN_EFFECT; the system must reconcile and clean it up by effectId, blocking READY until confirmation. Cleanup also requires an explicit acknowledgment. A failure enters CLEANUP_FAILED; the existence of a TTL does not justify falsely reporting completion. Secrets are injected only through credential references.

The scheduler uses the dependency graph to run unrelated probes concurrently. When a prerequisite fails, downstream dependencies are marked BLOCKED_BY instead of producing duplicate errors. Retries are bounded. Results are bound to the contract digest and run ID, so a late READY result from an old process cannot overwrite a new run. After the application starts, it retains a lightweight status channel. If a dependency becomes unavailable again, the UI must clearly show that the capability is unavailable instead of relying on hot reload to conceal a stale connection. SLOs and Error Budgets are appropriate for production targets; this local gate uses fixed fixtures and a development waiting budget.

Prove the Gate Is Not Decorative with Counterexamples

The test environment deliberately injects the following conditions: a listening port that speaks the wrong protocol; a database one migration behind; an HTTP 200 response missing a scope; a failure within the stability window after one successful sample; a delayed sample from an old run; a write effect that was accepted but whose acknowledgment was lost; cleanup that was issued but failed; and a dependency restarting after reaching READY. The expected results are PROTOCOL_MISMATCH, SCHEMA_BEHIND, CAPABILITY_MISSING, a return to STABILIZING, STALE_RUN, UNKNOWN_EFFECT, CLEANUP_FAILED, and STALE, respectively. They must not all be displayed as "service not started."

The blocking gate requires clean startup to succeed 30/30 times across the supported matrix; a 100% detection rate for all eight failure classes; zero false READY results, old samples counted, or writes to shared resources; 100% reconciliation coverage for effects and cleanup; a P95 decision time of no more than 90 seconds for required dependencies; and complete removal of isolated artifacts within 20 seconds after the stop command. It must also run on a machine with an empty cache to prevent existing volumes from producing false passes.

The rollout begins in observe mode, recording differences between the old script and the new contract. It then warns without blocking and finally enables blocking only for stable dependencies. Each stage compares startup success rates, manual retry counts, P95 waiting time, and degraded-mode usage. If false blocks exceed 1%, waiting time worsens by 25%, or the reader crashes, revert to the previous contract digest and restore the old startup entry point. Generated run reports remain available for diagnosis and must not be rewritten as successful. Offline work uses an explicit profile with an expiration time, while the interface continuously identifies missing capabilities. An environment variable must not become a permanent bypass. Infrastructure as Code can ensure that shared resources are declared; this contract covers the final step that determines whether a developer can actually begin coding.

Related

Related Content