Terminal success/failure closes kept the acceptance-phase operation state,
emitting self-contradicting envelopes (outcome=success with state=processing).
WithOperationTerminalState now closes the envelope at the observed terminal
status and clears timed_out.
WaitSpec.Validate trimmed status values only for checking and never wrote them
back, so padded declarations passed validation, published a padded Schema, and
then fail-closed at runtime as unknown statuses. NormalizeWaitSpec is now the
single canonical form shared by declaration (corecmd.New / AttachContract),
ToolSpec, and validation: trimmed values, duplicate/conflict rejection,
defensive copy.
Also covers the waitTimeoutDuration secs<=0 branch flagged by the coverage
gate.
typedJSONValue marshaled a typed value and then routed the result through
rawJSONValue, which runs json.Valid before decoding. On that path the input is
whatever json.Marshal has just produced, so the validation scan can only ever
succeed: it re-read every marshaled document for nothing.
The decode step is now shared by both entry points. rawJSONValue keeps its
json.Valid check, because it still accepts untrusted input, while typedJSONValue
decodes what it marshaled directly. Across the 1121-tool set this removes about a
third of the Schema Catalog projection work: the internal/app schema suite goes
from 26.0s to 17.2s uninstrumented, and from 291.1s to 241.0s under -race.
The delivered Catalog is byte-for-byte unchanged. check-generated-drift,
check-schema-catalog and check-schema-binary each regenerate the same
source_hash sha256:93b8d44eb163bd2898c78397d22af92d378e3dc4e20f56b33277b51e4342e2e6,
and the two error contracts are preserved: typedJSONValue still rejects a value
json.Marshal cannot encode, and rawJSONValue still rejects invalid JSON.
Completes the wait capability per review guidance ("add corresponding
execution hooks and fallback tests for the modes"):
- contract.WaitSpec restores event/auto modes with event_key,
match_field, and a new resource_query (dotted path into the accepted
result data yielding the identifier events correlate against);
per-mode validation of required fields
- internal/wait adds EventStream (leaf-owned transport) and RunEvent:
correlated-event filtering, the same terminal/pending/unknown mapping
as polling, timed-out pending on deadline during consumption, and an
ErrEventStreamEnded sentinel distinguishing stream termination from
fail-closed status errors
- Spec.WaitEvents hook; validateWaitDecl pairs mode with hooks
(poll<->WaitPoll, event<->WaitEvents, auto<->both; surplus hooks
rejected too)
- the wait phase runs event-first in auto mode and falls back to polling
when the stream ends or the subscription fails, under one deadline
spanning both phases; strict event mode surfaces stream errors
- output.CommandResult gains Data() (deep copy) so the framework can
resolve the resource identifier without exposing mutable state
Changed-code coverage re-verified at 100% (CI cross-package recipe).
Both capabilities were added as flags on an existing leaf, and in both cases
the leaf's published interface stopped describing what the command did:
- `sheet create --values/--sheets/--styles` orchestrates create → probe →
resolve default worksheet → write → read back → optional styles, yet the
leaf still published `interface_mode: mcp` + `create_workspace_sheet`.
- `sheet export --export-format csv` reads `get_range_as_csv` and never
invokes `submit_export_job`, yet the leaf published `submit_export_job`.
Each moves to its own command, declaring the interface it actually uses:
`sheet create-with-data` is `composite` with a reviewed reason and no
`interface_ref`; `sheet export-csv` is `mcp` + `get_range_as_csv`. Both are
pinned in the interface-disposition contract test.
`sheet create` and `sheet export` are restored byte-for-byte to main, so the
compatibility gates see two `command_added` additions instead of four
locked-field changes. The split also removes a user-visible trap: `--range`
without `--export-format csv` used to be silently discarded and the whole
workbook exported; the cross-format flags no longer exist, pinned by
TestSheetExportAndExportCsvFlagsDoNotLeak.
Drops the 8 now-stale mapping-ledger exclusions that covered the flags on
`sheet.create_workspace_sheet` / `sheet.submit_export_job`, shrinking this
branch's exclusion surface. Skill references and CHANGELOG follow the split.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two review findings, plus a same-class defect found by sweeping for it.
1. compatibleInterfaceRefRedirect accepted any mcp tool repointing from any
non-empty interface_ref to any other, as long as no other check for that tool
failed. Schema shape cannot prove two RPCs share business semantics,
permissions, error behaviour, or side effects, so that would have let every
future backend swap bypass the gate it exists to enforce. It is now keyed on
an explicit reviewedInterfaceRefRedirect allowlist of exact tool + old→new
pairs, currently holding only the reviewed
sheet.range_set_style: update_range → set_cell_range migration. Every other
ref change is reported again.
2. sheet export --export-format csv only printed a stderr warning when
get_range_as_csv returned hasMore=true, then wrote --output and reported
success with exit code 0. Automated callers, and anyone not watching stderr,
would treat an incomplete file as a complete export, and an existing target
file was overwritten with truncated data. Truncation now fails before the
write (leaving any existing file untouched) unless --allow-truncated is
passed; with the opt-in the success line states the data is incomplete.
--allow-truncated is registered in the reviewed mapping ledger as a local
policy input.
3. Swept for the same classes and found sheet_dimension.go repeating the
fmt.Sscanf("%d") prefix-parse hole in three places: insert_dimension,
delete_dimension, and update_dimension all accepted --length "3x" as 3, so a
malformed value silently operated on the wrong row/column count — the delete
direction is not rollbackable. All three now use strconv.Atoi. (Checked and
cleared: sheet csv-get also surfaces hasMore, but it has no --output and only
returns the flag in its JSON payload, so it is not the same fail-open shape.)
Tests: allowlist rejection cases (unreviewed target ref, and the same pair on a
tool absent from the allowlist, asserted outside the table so the registration
survives until checkCompatibility runs); truncation fail-closed with a
pre-existing output file asserted byte-for-byte unchanged; --allow-truncated
write-through; and an untruncated read needing no opt-in. Changed-code coverage
stays at 100% (939 statements).
Reverts the interface_mode of sheet create and sheet export from composite back
to mcp with a single interface_ref, matching upstream/main and the existing
convention for multi-tool leaves (doc.create_document declares mcp +
create_document even though it also calls update_document).
Rationale:
- interface_ref is audit / traceability metadata; nothing reads it at runtime
(verified: rebuilding with a bogus interface_ref still routes to the correct
tool). Declaring the primary tool and treating the orchestration as an
implementation detail is the pattern main already uses.
- sheet export was mcp + submit_export_job on main; it already orchestrated
submit_export_job + query_export_job without declaring the poll. Adding an
--export-format csv branch does not change that shape, so it does not warrant
flipping to composite.
- sheet create was a genuine single-RPC command on main (create_workspace_sheet
only). The --values / --sheets / --styles orchestration I added runs after the
document exists; per the doc.create_document precedent it stays mcp.
This takes the sheet schema-compatibility failures from 4 to 0 without a waiver
or an admin override: the declarations now equal main's.
Also updates the wording of the seven new mapping-exclusion reasons for these
two commands (submit_export_job CSV params, create_workspace_sheet
values/sheets/styles) from "Composite ... input" to "Wrapper ... input", so the
reason text no longer collides with the interface_mode value now that both
leaves are mcp. The reasons are otherwise unchanged and still describe where
each value actually goes. range_batch_set_style keeps its "Composite" wording
because it genuinely stays interface_mode=composite.
Removes the two composite entries for these leaves from the interface
disposition contract test.
No execution path changes; targeted sheet suites, the disposition contract test,
and the schema-compat policy tests all pass; check-schema-catalog is green;
sheet-scoped schema-compatibility reports 0 failures.