Dosu LogoDosu Logo
Ask
Join our Discord
Organization avatar
HashHivePublic
EvilBit Labs LLC
DocumentsHashHive
pre-prod-wire-rename-and-schema-version-bump
pre-prod-wire-rename-and-schema-version-bump
Type
External
Status
Published
Created
Jul 18, 2026
Updated
Jul 19, 2026
Updated by
Dosu Bot
Source
docs/solutions/conventions/pre-prod-wire-rename-and-schema-version-bump.md

Drop placeholder wire identifiers cleanly while pre-prod, paired with a schema-version bump#

Context#

PR #153 swapped HashHive's object store from MinIO to SeaweedFS but kept minio as the wire identifier in the /health envelope. The defensive preservation was explicit in the code: a comment said "'minio' is preserved as the wire identifier across the SeaweedFS swap so frontend consumers keep working without a coupled release." Three weeks later, issue #156 AC 4.3 mandated the neutral name (object_store). Two options surfaced:

  • A. Add services.object_store alongside services.minio — dual-write, deprecate the old name later.
  • B. Rename outright, drop the legacy alias, bump the schema version.

The instinct on shipping wire-contract changes is to favor option A. Dual-write is what you do in production. But HashHive is pre-prod — no monitors, no load-balancer probes, no SDK consumers — so the defensive preservation had no live constraint to honor. Option A would carry placeholder vendor naming forward into post-prod indefinitely; option B costs one PR.

The decision was option B. The rename landed in PR #171 alongside a HEALTH_VERSION bump from 1.1.0 to 2.0.0 and a matching info.version bump in the OpenAPI control spec, with a changelog entry documenting the break.

Guidance#

When an earlier PR's defensive back-compat preservation outlives its usefulness AND the project is genuinely pre-prod, drop it cleanly in one PR. Four moves make the change durable:

1. Audit consumers before deciding (cheap, evidence-based)#

Run rg "services\.minio|body\['minio'\]|body\.minio" packages/ (substitute your field name) across the repo. If the only consumers are the test files for the legacy field itself, the back-compat has no live constraint. If a frontend hook, CLI, or agent code reads the old name, the rename is still possible but must update those consumers in the same PR.

Anything outside the repo is harder to audit. Pre-prod posture means "no production consumers exist yet" — note the assumption explicitly in the PR description so a reviewer can challenge it.

2. Rename across the wire-shape mirror in one PR#

The rename touches every corner of the contract atomically. (As of 2026-06-01 the OpenAPI surface is route-as-spec — the shared Zod schema bound into createRoute(...) IS the spec, generated at GET /api/v1/{agent,dashboard,control}/openapi.json; there is no packages/openapi/*.yaml to edit. The *.yaml references in this doc reflect PR #171's state, which predated that migration — see the now-superseded wire-contract-mirror convention.)

  • Backend service code (enum types, probe wiring, log fields, comments describing current behavior).
  • Backend tests (mocks, assertions, regression-guards).
  • Shared schemas (packages/shared/src/schemas/) — if the type didn't live there already, move it now per AGENTS.md "wire shapes live in shared."
  • The OpenAPI binding — the shared Zod schema referenced from the route's createRoute(...) definition (formerly a hand-maintained packages/openapi/*.yaml entry: the required array and properties map).
  • Frontend hooks + components that consume the field.
  • Frontend tests with fixtures (the most common miss; the type-check covers backend but a tsconfig.json that only includes src/**/* won't catch frontend test fixtures).

3. Bump the schema version with a changelog entry#

// packages/backend/src/services/health.ts
/**
 * 2.0.0 (issue #156) — `components.minio` renamed to `components.object_store`
 * across both the rich envelope and the legacy public envelope. No
 * back-compat alias; pre-2.0.0 probes keyed on the old name will see
 * the field absent.
 * 1.1.0 (issue #109) — three-tier status, `components` map, per-component
 * `message`/`detail`/`durationMs`.
 */
export const HEALTH_VERSION = '2.0.0'

Historical note (route-as-spec migration, ADR-0013): the packages/openapi/ directory and its hand-maintained *-api.yaml files no longer exist. The changelog snippet below reflects PR #171's pre-migration state — today the equivalent changelog lives in the TS JSDoc above the version constant (see the HEALTH_VERSION block above) and, when surfaced on the wire, in the route's createRoute(...) description.

# packages/openapi/control-api.yaml (retired — see historical note above)
info:
  description: |
    ## Changelog

    ### 2.0.0 (issue #156)

    Breaking rename in `SystemHealth.components`:
    - `minio` → `object_store`. The previous key was a SeaweedFS-era
      placeholder; the neutral name is vendor-agnostic.
    - `services.minio` is removed from the legacy `/health` envelope —
      no dual-write, no alias.
    - `HEALTH_VERSION` constant bumps to `2.0.0` in lock-step.
  version: 2.0.0

Bumping the version is the durable signal that lets a future consumer gate on the change. Without it, the only evidence of the rename is the git history.

4. Add a regression guard, not just an assertion change#

Mechanical assertion renames (expect(body.services.minio) → expect(body.services.object_store)) prove the new field exists. Symmetric guards prove the old name is gone:

// Asserts the new name is present AND the old name is absent —
// reintroducing the legacy alias becomes a test failure, not a silent
// re-violation of the AC.
expect(body.services.object_store.status).toBe('connected')
expect(body.services.minio).toBeUndefined()

Apply this pattern on every health surface (legacy /health, rich /control/health, rich /dashboard/health) — pinning only the least-used one is inconsistent.

Why This Matters#

  • Pre-prod is the right window. Once external probes, generated SDKs, or third-party monitors exist, dropping a wire identifier requires a deprecation cycle (typically two minor versions with dual-write). Pre-prod has no such constraint — the cost is one atomic PR. Post-prod, the same rename is a quarter of work.
  • Schema versions are the durable signal. HEALTH_VERSION and OpenAPI info.version survive git-history archaeology. Consumers regenerating SDKs three months later see "2.0.0 — breaking rename" in the changelog without needing to read the diff.
  • Symmetric absence guards prevent silent re-introduction. A future PR that "helpfully" re-adds the old field as an alias would pass mechanical assertions on the new field; only the toBeUndefined() guard catches it. This is the same regression-guard discipline as the services.minio.toBeUndefined() test landed in this PR.
  • Defensive preservation has a half-life. PR #153's "preserve minio as wire identifier" comment was correct at the time and wrong three weeks later. Mark such preservations with their justification (PR #156's AC 4.3 cited the rename mandate) so the next maintainer can evaluate whether the constraint still holds.

When to Apply#

  • A field, key, or wire identifier on a public-facing surface has placeholder vendor naming (minio, aws_*, legacy_*).
  • The repo is pre-prod and a rg audit confirms no in-repo consumer reads the old name.
  • The rename is part of a verification sweep or AC closure where the cleanup PR is already on the roadmap.
  • The new name reduces future cognitive load (a maintainer reading object_store shouldn't have to mentally translate to "actually SeaweedFS, or AWS S3 if hosted").

Don't apply when:

  • Production consumers exist (operator scripts, generated SDKs, third-party monitors). Use additive-then-deprecate instead.
  • The "placeholder" is actually a load-bearing internal symbol — renaming would cascade through DB columns, queue names, log queries, or other infrastructure not in the same repo.
  • You can't audit the consumer space. Pre-prod posture is a claim about external state; if you can't verify it, treat the rename as a production-grade contract change with dual-write.

Examples#

The audit that decided it#

$ rg "services\.minio|body\['minio'\]|body\.minio" packages/
packages/backend/src/services/health.ts: ...
packages/backend/tests/unit/health.test.ts: ...
packages/backend/tests/unit/health-service.test.ts: ...
packages/frontend/src/hooks/use-system-health.ts: ...
packages/frontend/src/components/features/system-health-card.tsx: ...
packages/openapi/control-api.yaml: ...

All in-repo. No external grep target. Option B was safe.

The rename diff at a glance#

SurfaceBeforeAfter
ComponentName enum'minio''object_store'
LegacyHealthEnvelope.services.minio{ status, bucket }(renamed to object_store)
OpenAPI required array[database, redis, minio, queues][database, redis, object_store, queues]
HEALTH_VERSION'1.1.0''2.0.0'
OpenAPI info.version1.1.02.0.0
WS broadcast component field'minio''object_store'

What stayed#

# docker/seaweedfs/s3-iam.json — left untouched.
# `minioadmin/minioadmin` is an opaque dev secret, not a wire identifier.
# AC 6.2 explicitly mandates "matching the prior MinIO defaults so
# `just db-seed` and integration tests do not need new env wiring."
identities:
  - name: hashhive-admin
    credentials:
      - accessKey: minioadmin
        secretKey: minioadmin

Audit your "everything has the old name" intuition. Some literals — opaque credentials, third-party SDK class names, library version strings — survive a rename because they aren't wire identifiers in the AC-text sense.

Related#

  • shared-zod-openapi-wire-contract-mirror-2026-05-25.md — the triple-sync pattern this rename followed (Zod schema + OpenAPI spec + route handler + contract test).
  • PR #153 — first half of the swap; preserved minio as wire identifier defensively.
  • PR #171 — second half; dropped the placeholder, bumped HEALTH_VERSION to 2.0.0.
  • AGENTS.md — "Wire shapes live in @hashhive/shared as z.infer from Zod schemas" and "All three surfaces are route-as-spec via @hono/zod-openapi ... there is no separate YAML to keep in sync, and there is no packages/openapi/ directory."
Documents
.impeccable
0001-agent-enrollment-two-token-exchange
0002-docker-supervision-over-pm2
0003-bind-agent-to-enrollment-token
0004-agent-api-revisable-until-1.0
0005-version-public-api-after-1.0
0006-seaweedfs-over-minio
0007-three-api-surfaces
0008-betterauth-over-handrolled-jwt
0009-postgres-task-store-bullmq-orchestration
0010-schema-first-drizzle-zod
0011-pull-advisory-notify-task-distribution
0012-in-memory-websocket-v1-redis-deferred
0013-route-as-spec-hono-zod-openapi
0014-turborepo-bun-over-nx
0015-append-only-telemetry-timescaledb-rrd
0016-server-fanout-bus
0017-adaptive-task-sizing-lease
0018-real-db-integration-test-lane
0019-campaign-archiving-immutable-lifecycle
155-ac-traceability-matrix
155-task-distribution-assignment-spec
156-ac-traceability-matrix
156-object-storage-file-management-spec
160-login-project-selection-ui
2026-05-25-websocket-realtime-infrastructure-requirements
2026-05-29-dashboard-stats-and-read-endpoint-contract-requirements
2026-05-31-openapi-auto-generation-migration-requirements
2026-06-08-results-analysis-export-ui-requirements
2026-06-19-task-distribution-architecture
2026-06-20-campaign-archiving-requirements
2026-06-30-hash-export-import-search-requirements
227-spa-deep-link-refresh-redirect-spec
94-p0-cracker-binary-management-agent-auto-update
96-keyspace-task-distribution-spec
97-task-preemption-spec
99-attack-complexity-state-machine-spec
AGENTS
AGENTS
AI_POLICY
AI_POLICY
ARCHITECTURE
ARCHITECTURE
Agent_Authentication_&_Authorization
Agent_Heartbeat_&_Error_Handling
Agent_List_&_Detail_UI
Base Model Schema System
BullMQ_Queue_Architecture_&_Redis_Integration
CLAUDE
CONTRIBUTING
CONTRIBUTING
Campaign_Creation_Wizard_UI
Campaign_List_&_Detail_UI
Campaign_Orchestration_API
Code Quality Automation with Husky and Kiro Hooks
Core_Flows__HashHive_User_Journeys
Dashboard_&_Real-Time_Monitoring_UI
Dashboard_Stats_API_Endpoint_(GET__api_v1_dashboard_stats)
Development Workflow and Tooling Integration
ESLint and Formatting Configuration
Epic_Brief__HashHive_-_Modern_Distributed_Password_Cracking_Platform
GOTCHAS
GOTCHAS
Health Check Endpoint Enhancements
Login_&_Project_Selection_UI
MERN_proposal
Mixed Hash Type Detection & Automatic Campaign Splitting (#202)
MongoDB Connection Management
Object_Storage_&_File_Management
Project_Selection_&_User_Authentication_API
README
README
README
README
README
README
Real-Time_Events_&_WebSocket_Infrastructure
Resource_Management_API
Resource_Management_UI
Results_API_&_CSV_Export_(Dashboard_Backend)
Results_Analysis_&_Export_UI
SECURITY
SECURITY
STRATEGY
Task_Distribution_&_Assignment
Tech_Plan__HashHive_Architecture
Testing Strategy for Database and Models
advanced_task_scheduler
agent
agent-resource-protocol
agent-server-collaboration-vision
agent-token-rotation
agent_notes
agent_sync_extensions
agent_v1_contract_violations
allow_hybrid_hash_lists
attack
attack_notes
authentication_cleanup
back-compat-credential-format-migration
barrel-plus-subdir-service-split
brute_force_attack_editor
bullmq-jobid-dedup-requires-eviction
bun-test-mock-module-import-order
campaign_list_view
campaign_notes
campaigns
ci_check.prompt
code_review.prompt
contract-test-mocks-mirror-service-not-schema
copilot-instructions
core_algorithm_implementation_guide
crackable_uploads_plan
dashboard-read-endpoint-contract
dashboard-ux
default_config_suggestions
development
development
feat-agent-list-detail-ui
feat-betterauth-project-selection-rbac
feat-campaign-list-detail-ui
feat-masklist-keyspace
form-submit-payload-null-checks-2026-05-19
full_testing_architecture
getbytext-collision-waitfor-timeout
hard_password_attack_strategies
hash_hive_gap_analysis
health_status_screen
implementation_plan
learned_rules_parser_plan
llmstxt.prompt
markov_autogen_plan
new_dictionary_attack_editor
new_mask_attack_editor
openldap-testcontainer-bootstrap-race
performance_tuning.prompt
phase-1-core-infrastructure
phase-2-api-implementation
phase-2-api-implementation-part-1
phase-2-api-implementation-part-2
phase-2-api-implementation-part-3
phase-2b-resource-management
phase-2c-refactor-cleanup
phase-3-e2e-test-coverage-plan
phase-3-step-1
phase-3-step-2
phase-3-step-3
phase-3-step-4
phase-3-step-5
phase-3-web-ui-foundation
phase-4-containerization-deployment
phase-5-task-distribution
phase-6-monitoring-testing-documentation
playwright-shared-seed-user-last-project-id-flake
playwright-visual-baselines
pre-prod-wire-rename-and-schema-version-bump
previous_password_dictionary_attack_editor
projection-narrows-zrecord-wire-shape
psycopg_migration
radix-roving-focus-preventdefault-breaks-keyboard-nav
reactflow-position-by-array-index-2026-05-19
rule_explanation
salvage_templates
security_hardening.prompt
shadcn-radix-non-destructive-add-on-tailwind-v4
shared-zod-openapi-wire-contract-mirror-2026-05-25
spa_to_ssr
summary
template
test-coverage-plan
testcontainers_minio_support
testing
testing
user_flows_notes
v2-upgrade-overview
v2_agent_api_removal
work_next_task.prompt