Skip to content

fix(err-06): return IpcError from every command that can emit a conflict code - #283

Open
entelecheia wants to merge 1 commit into
mainfrom
fix/err-06-typed-conflict-commands
Open

fix(err-06): return IpcError from every command that can emit a conflict code#283
entelecheia wants to merge 1 commit into
mainfrom
fix/err-06-typed-conflict-commands

Conversation

@entelecheia

Copy link
Copy Markdown
Member

Closes ERR-06, one of the two follow-ups the Codex adversarial review raised on
Phase 3 (#279).

The gap

Three commands reached a contract-code emitter and flattened its typed error
back to String:

Command Emitter Code lost
today_apply_plan_result today_mutate today_conflict
task_calendar_set_sync today_mutate today_conflict
update_frontmatter_field assert_expected_revision document_conflict

A conflict on those paths arrived at the frontend as a plain string, so
isTodayConflict and the document_conflict branch returned false.

No caller branches on them, so nothing regressed and Phase 3's "display-only, no
frontend branch" justification was factually correct at the time. The point is
the boundary: it was drawn by what the frontend happened to branch on today,
which gives a future author no compile-time signal that the advertised recovery
is unavailable there.

The guard

no_code_emitting_path_flattens_its_error_to_string scans the emitter-calling
modules for map_err(|x| x.to_string()) and names the enclosing function. It
matches only the closure's own argument, so map_err(|_| "literal".to_string())
(minting a fresh error) and an incidental .to_string() elsewhere on the line do
not trip it - the first draft did, and reported four false positives.

Proven rather than assumed: dropping the allowlist makes it report
web_actions.rs:574 in apply_receipt() flattens task_transition(.

No IpcResult<T> alias was added. The inventory test covers the intent without
introducing a second way to spell the return type.

Deliberately out of scope

apply_receipt is allowlisted with its reason in the test: it is an internal
helper, not a command, and web_actions_apply consumes its Err as the reason
string on a retry marker, so the value never crosses IPC and no frontend branch
can read a code.

Worth flagging separately: web_actions.rs:860 treats a today_conflict as
skip-and-retry-next-run, while :574 marks a task_conflict as retry-needed.
Same class of event, two treatments, in one file. Reconciling them is a behavior
change, so it is left alone here.

Verification

cargo test --lib 1220 passed / 0 failed, clippy -D warnings and fmt --check
clean, pnpm typecheck exit 0, vitest 1392 passed, eslint clean on the touched
file.

Non-contract errors inside these commands ride From<String> unchanged, so their
text stays byte-identical; tests that asserted a string prefix on a contract
code now assert err.code, which is what the contract was for.

update_frontmatter_field's TypeScript wrapper called invoke directly with no
funnel. Now that the command rejects with an object rather than a string, a
caller doing String(err) would have rendered [object Object], so it routes
through normalizeIpcError like saveDocument does.

ERR-04's count moves 1128 -> 1124 (three commands plus one test helper), still
inside the [1118, 1138] band Phase 3 recorded.

ERR-05 is untouched and stays open.

…ict code

Three commands reached a contract-code emitter and flattened its typed error
back to String with `.map_err(|e| e.to_string())`, so a conflict arrived at the
frontend as a plain string and `isTodayConflict` / the document_conflict branch
returned false:

- today_apply_plan_result (today_ai.rs) -> today_mutate, today_conflict
- task_calendar_set_sync (today_calendar.rs) -> today_mutate, today_conflict
- update_frontmatter_field (document.rs) -> assert_expected_revision,
  document_conflict

No caller branched on them, so nothing regressed. The point is the boundary: it
had been drawn by what the frontend happened to branch on today, which gives a
future author no compile-time signal that the advertised recovery is missing.

Non-contract errors inside these commands ride From<String> unchanged, so their
text stays byte-identical. Tests that asserted a string prefix on a contract
code now assert err.code instead, which is what the contract was for.

`update_frontmatter_field`'s TypeScript wrapper called invoke directly with no
funnel. Now that the command rejects with a { code, message } object rather
than a string, a caller doing String(err) would have rendered "[object
Object]", so it routes through normalizeIpcError like saveDocument does.

Adds `no_code_emitting_path_flattens_its_error_to_string`: it scans the
emitter-calling modules for `map_err(|x| x.to_string())` and reports the
enclosing function. It matches only the closure's own argument, so
`map_err(|_| "literal".to_string())` (minting an error) and an incidental
`.to_string()` elsewhere on the line do not trip it. Proven by dropping the
allowlist and watching it name web_actions.rs:574 in apply_receipt().

apply_receipt is allowlisted with its reason: it is an internal helper, not a
command, and web_actions_apply consumes its Err as a retry-marker string that
never crosses IPC. Worth noting for later - web_actions.rs:860 treats a
today_conflict as skip-and-retry-next-run while :574 marks a task_conflict as
retry-needed. Same class of event, two treatments. Changing that is a behavior
change, so it is left alone here.

ERR-04's count moves 1128 -> 1124 (three commands plus one test helper), still
inside the [1118, 1138] band Phase 3 recorded.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 607993ad25

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +124 to +128
const SOURCES: &[(&str, &str)] = &[
("web_actions.rs", include_str!("web_actions.rs")),
("today_store.rs", include_str!("today_store.rs")),
("today_calendar.rs", include_str!("today_calendar.rs")),
("today_lifecycle.rs", include_str!("today_lifecycle.rs")),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Discover emitter-calling modules automatically

When a new command calls an existing conflict-code emitter from any Rust module not manually listed in SOURCES, this test never examines that command, so it can still flatten the error to String while the purported ERR-06 regression guard passes. Because adding a caller does not require changing EMITTERS, there is no test or compiler signal reminding the author to extend this separate file list; derive the scanned sources/callers automatically or make the inventory exhaustive and self-validating.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant