claw-code/FIX_LOCUS_164.md
YeonGyu-Kim afc792f1a5 docs: add FIX_LOCUS_164.md — JSON envelope contract migration strategy
Cycle #77 deliverable. Escalates #164 from pinpoint to fix-locus cycle.

Documents:
- 100% divergence across all 14 JSON-emitting verbs (not a partial drift)
- Two envelope shapes: current flat vs. documented nested
- Phased migration: dual-mode → default bump → deprecation (3 phases)
- Shared wrapper helper pattern (json_envelope.rs)
- Per-verb migration template (before/after code)
- Error classification remapping table (cli_parse → parse, etc.)
- 6 acceptance criteria + 3 risk categories
- Rollout timeline: Phase 1 ~6 dev-days, v3.0 cutoff at ~8 months

Ready for author review + pilot implementation decision (which 3 verbs lead).
2026-04-23 04:34:57 +09:00

11 KiB
Raw Blame History

Fix-Locus #164 — JSON Envelope Contract Migration

Status: 📋 Proposed (2026-04-23, cycle #77). Escalated from pinpoint #164 after gaebal-gajae review recognized product-wide scope.

Class: Contract migration (not a patch). Affects EVERY --output-format json command.

Bundle: Typed-error family — joins #102 + #121 + #127 + #129 + #130 + #245 + #164. Contract-level implementation of §4.44 typed-error envelope.


1. Scope — What This Migration Affects

Every JSON-emitting verb. Audit across the 14 documented verbs:

Verb Current top-level keys Schema-conformant?
doctor checks, has_failures, kind, message, report, summary No (kind=verb-id, flat)
status config_load_error, kind, model, ..., workspace No
version git_sha, kind, message, target, version No
sandbox active, ..., kind, ...supported No
help kind, message No (minimal)
agents action, agents, count, kind, summary, working_directory No
mcp action, config_load_error, ..., kind, servers No
skills action, kind, skills, summary No
system-prompt kind, message, sections No
dump-manifests error, hint, kind, type No (emits error envelope for success)
bootstrap-plan kind, phases No
acp aliases, ..., kind, ...tracking No
export file, kind, markdown, messages, session_id No
state error, hint, kind, type No (emits error envelope for success)

All 14 verbs diverge from SCHEMAS.md. The gap is 100%, not a partial drift.


2. The Two Envelope Shapes

2a. Current Binary Shape (Flat Top-Level)

// Success example (claw doctor --output-format json)
{
  "kind": "doctor",          // verb identity
  "checks": [...],
  "summary": {...},
  "has_failures": false,
  "report": "...",
  "message": "..."
}

// Error example (claw doctor foo --output-format json)
{
  "error": "unrecognized argument...",   // string, not object
  "hint": "Run `claw --help` for usage.",
  "kind": "cli_parse",        // error classification (overloaded)
  "type": "error"             // not in schema
}

Properties:

  • Flat top-level
  • kind field is overloaded (verb-id in success, error-class in error)
  • No common wrapper metadata (timestamp, exit_code, schema_version)
  • error is a string, not a structured object

2b. Documented Schema Shape (Nested, Wrapped)

// Success example (per SCHEMAS.md)
{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "doctor",
  "exit_code": 0,
  "output_format": "json",
  "schema_version": "1.0",
  "data": {
    "checks": [...],
    "summary": {...},
    "has_failures": false
  }
}

// Error example (per SCHEMAS.md)
{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "doctor",
  "exit_code": 1,
  "output_format": "json",
  "schema_version": "1.0",
  "error": {
    "kind": "parse",           // enum, nested
    "operation": "parse_args",
    "target": "subcommand `doctor`",
    "retryable": false,
    "message": "unrecognized argument...",
    "hint": "Run `claw --help` for usage."
  }
}

Properties:

  • Common metadata wrapper (timestamp, command, exit_code, output_format, schema_version)
  • data (payload) vs. error (failure) as sibling fields, never coexisting
  • kind in error is the enum from §4.44 (filesystem/auth/session/parse/runtime/mcp/delivery/usage/policy/unknown)
  • error is a structured object with operation/target/retryable

3. Migration Strategy — Phased Rollout

Principle: Don't break downstream consumers mid-migration. Support both shapes during overlap, then deprecate.

Phase 1 — Dual-Envelope Mode (Opt-In)

Deliverables:

  • New flag: --envelope-version=2.0 (or --schema-version=2.0)
  • When flag set: emit new (schema-conformant) envelope
  • When flag absent: emit current (flat) envelope
  • SCHEMAS.md: add "Legacy (v1.0)" section documenting current flat shape alongside v2.0

Implementation:

  • Single envelope_version parameter in CliOutputFormat enum
  • Every verb's JSON writer checks version, branches accordingly
  • Shared wrapper helper: wrap_v2(payload, command, exit_code)

Consumer impact: Opt-in. Existing consumers unchanged. New consumers can opt in.

Timeline estimate: ~2 days for 14 verbs + shared wrapper + tests.

Phase 2 — Default Version Bump

Deliverables:

  • Default changes from v1.0 → v2.0
  • New flag: --legacy-envelope to opt back into flat shape
  • Migration guide added to SCHEMAS.md and CHANGELOG
  • Release notes: "Breaking change in envelope, pre-migration opt-in available via --legacy-envelope"

Consumer impact: Existing consumers must add --legacy-envelope OR update to v2.0 schema. Grace period = "until Phase 3."

Timeline estimate: Immediately after Phase 1 ships.

Phase 3 — Flat-Shape Deprecation

Deliverables:

  • --legacy-envelope flag prints deprecation warning to stderr
  • SCHEMAS.md "Legacy v1.0" section marked DEPRECATED
  • v3.0 release (future): remove flag entirely, binary only emits v2.0

Consumer impact: Full migration required by v3.0.

Timeline estimate: Phase 3 after ~6 months of Phase 2 usage.


4. Implementation Details

4a. Shared Wrapper Helper

// rust/crates/rusty-claude-cli/src/json_envelope.rs (new file)

pub fn wrap_v2_success<T: Serialize>(command: &str, data: T) -> Value {
    serde_json::json!({
        "timestamp": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
        "command": command,
        "exit_code": 0,
        "output_format": "json",
        "schema_version": "2.0",
        "data": data,
    })
}

pub fn wrap_v2_error(command: &str, error: StructuredError) -> Value {
    serde_json::json!({
        "timestamp": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
        "command": command,
        "exit_code": 1,
        "output_format": "json",
        "schema_version": "2.0",
        "error": {
            "kind": error.kind,
            "operation": error.operation,
            "target": error.target,
            "retryable": error.retryable,
            "message": error.message,
            "hint": error.hint,
        },
    })
}

pub struct StructuredError {
    pub kind: &'static str,   // enum from §4.44
    pub operation: String,
    pub target: String,
    pub retryable: bool,
    pub message: String,
    pub hint: Option<String>,
}

4b. Per-Verb Migration Pattern

// Before (current flat shape):
match output_format {
    CliOutputFormat::Json => {
        serde_json::to_string_pretty(&DoctorOutput {
            kind: "doctor",
            checks,
            summary,
            has_failures,
            message,
            report,
        })
    }
    CliOutputFormat::Text => render_text(&data),
}

// After (v2.0 with v1.0 fallback):
match (output_format, envelope_version) {
    (CliOutputFormat::Json, 2) => {
        json_envelope::wrap_v2_success("doctor", DoctorData { checks, summary, has_failures })
    }
    (CliOutputFormat::Json, 1) => {
        // Legacy flat shape (with deprecation warning at Phase 3)
        serde_json::to_value(&LegacyDoctorOutput { kind: "doctor", ...})
    }
    (CliOutputFormat::Text, _) => render_text(&data),
}

4c. Error Classification Migration

Current error kind values (found in binary):

  • cli_parse, no_managed_sessions, unknown, missing_credentials, session_not_found

Target v2.0 enum (per §4.44):

  • filesystem, auth, session, parse, runtime, mcp, delivery, usage, policy, unknown

Migration table:

Current kind v2.0 error.kind
cli_parse parse
no_managed_sessions session (with operation: "list_sessions")
missing_credentials auth
session_not_found session (with operation: "resolve_session")
unknown unknown

5. Acceptance Criteria

  1. Schema parity: Every --output-format json command emits v2.0 envelope shape exactly per SCHEMAS.md
  2. Success/error symmetry: Success envelopes have data field; error envelopes have error object; never both
  3. kind semantic unification: data.kind = verb identity (when present); error.kind = enum from §4.44. No overloading.
  4. Common metadata: timestamp, command, exit_code, output_format, schema_version present in ALL envelopes
  5. Dual-mode support: --envelope-version=1|2 flag allows opt-in/opt-out during migration
  6. Tests: Per-verb golden test fixtures for both v1.0 and v2.0 envelopes
  7. Documentation: SCHEMAS.md documents both versions with deprecation timeline

6. Risks

6a. Breaking Change Risk

Phase 2 (default version bump) WILL break consumers that depend on flat-shape envelope. Mitigations:

  • Dual-mode flag allows opt-in testing before default change
  • Long grace period (Phase 3 deprecation ~6 months post-Phase 2)
  • Clear migration guide + example consumer code

6b. Implementation Risk

14 verbs to migrate. Each verb has its own success shape (checks, agents, phases, etc.). Payload structure stays the same; only the wrapper changes. Mechanical but high-volume.

Estimated diff size: ~200 lines per verb × 14 verbs = ~2,800 lines (mostly boilerplate).

Mitigation: Start with doctor, status, version as pilot. If pattern works, batch remaining 11.

6c. Error Classification Remapping Risk

Changing kind: "cli_parse" to error.kind: "parse" is a breaking change even within the error envelope. Consumers doing response["kind"] == "cli_parse" will break.

Mitigation: Document explicitly in migration guide. Provide sed script if needed.


7. Deliverables Summary

Item Phase Effort
json_envelope.rs shared helper Phase 1 1 day
14 verb migrations (pilot 3 + batch 11) Phase 1 2 days
--envelope-version flag Phase 1 0.5 day
Dual-mode tests (golden fixtures) Phase 1 1 day
SCHEMAS.md updates (v1.0 + v2.0) Phase 1 0.5 day
Default version bump Phase 2 0.5 day
Deprecation warnings Phase 3 0.5 day
Migration guide doc Phase 1 0.5 day

Total estimate: ~6 developer-days for Phase 1 (the core work). Phases 2/3 are cheap follow-ups.


8. Rollout Timeline (Proposed)

  • Week 1: Phase 1 — dual-mode support + pilot migration (3 verbs)
  • Week 2: Phase 1 completion — remaining 11 verbs + full test coverage
  • Week 3: Stabilization period, gather consumer feedback
  • Month 2: Phase 2 — default version bump
  • Month 8: Phase 3 — deprecation warnings
  • v3.0 release: Remove --legacy-envelope flag, v1.0 shape no longer supported

  • ROADMAP #164: The originating pinpoint (this document is its fix-locus)
  • ROADMAP §4.44: Typed-error contract (defines the error.kind enum this migration uses)
  • SCHEMAS.md: The envelope schema this migration makes reality
  • Typed-error family: #102, #121, #127, #129, #130, #245, #164

Cycle #77 locus doc. Ready for author review + pilot implementation decision.