diff --git a/FIX_LOCUS_164.md b/FIX_LOCUS_164.md new file mode 100644 index 0000000..4260dfe --- /dev/null +++ b/FIX_LOCUS_164.md @@ -0,0 +1,325 @@ +# 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) + +```json +// 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) + +```json +// 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 +// rust/crates/rusty-claude-cli/src/json_envelope.rs (new file) + +pub fn wrap_v2_success(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, +} +``` + +### 4b. Per-Verb Migration Pattern + +```rust +// 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 + +--- + +## 9. Related + +- **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.**