Skip to content

audit(envelope): sweep all list-consuming get() callers — zero remaining mismatches (FM-ENVELOPE-AUDIT)#78

Merged
keyxmakerx merged 1 commit into
mainfrom
claude/cordinator-dispatch-caller-table-lk1kj3
Jul 12, 2026
Merged

audit(envelope): sweep all list-consuming get() callers — zero remaining mismatches (FM-ENVELOPE-AUDIT)#78
keyxmakerx merged 1 commit into
mainfrom
claude/cordinator-dispatch-caller-table-lk1kj3

Conversation

@keyxmakerx

Copy link
Copy Markdown
Owner

Cites: 2026-05-21-core-tenets §T-O1 (verify before claim), §T-O2 (consumer-verified wire contract), §T-B4 (footgun-catalog docs); dispatches/foundry/FM-ENVELOPE-AUDIT.md; reports/coordinator/2026-07-11-r13-post-merge-review.md §1; prior art PR #77 (FM-CAL-BACKCATALOG-FIX — the fix pattern replicated); conventions RC-7 (this body IS the review artifact)
Security implication: none — read-path shape audit + test/docs only; no auth/token/trust-boundary surface touched
Consumer-verified: every list handler read from source and quoted file:line in the table below (e.g. /entity-types envelope at internal/plugins/syncapi/api_handler.go:249-251; /maps bare array at map_api_handler.go:81)
Foundry compatibility: n/a — no Foundry-runtime code changed; the added file is a headless node --test suite plus a CLAUDE.md convention note
Mockup: n/a

What this changes

Executes FM-ENVELOPE-AUDIT, the follow-up flagged by PR #77's re-verify item 5. PR #77 fixed the calendar back-catalog silently importing zero events because Chronicle wraps list responses in {"data":[…],"total":N} while the module expected a bare array. This PR audits every remaining list-consuming get() caller against the real Chronicle handler shape and confirms the bug class does not recur.

Verdict: zero remaining mismatches. Every list caller already unwraps defensively (result?.data || result || [], _normalizeArray(), _coerceArray(), or an Array.isArray(x) ? x : (x?.data ?? []) guard). No production code changed. The PR adds regression pins (tools/test-envelope-audit.mjs) that stub the REAL Chronicle shapes for the two callers #77 flagged plus the least-defensive one, and documents the two-shape convention in CLAUDE.md.

Why

PR #77 flagged two callers as possible same-class bugs (map-sync.mjs:415 /maps, actor-sync.mjs:631 /entity-types) and left the wider sweep to this dispatch. Per T-O2 you verify the consumer against the producer's source before claiming a surface — doing so shows both flags are false alarms (details below). The lasting value is the caller table + the pins + the documented convention, so the next contributor can't reintroduce the #77 no-op.


Caller table — the review artifact

Legend — Shape: ENV = envelope {data,total}, BARE = bare JSON array, OBJ = object whose list lives in a named field. Verdict: OK = caller handles the real shape.

Envelope endpoints — {"data":[…],"total":N}

Module file:line Endpoint Chronicle handler file:line Shape Consumption Verdict
actor-sync.mjs:182 /entities?type_id= api_handler.go:345-347 ListEntities ENV result?.data || [] OK
actor-sync.mjs:780 /entities?type_id= api_handler.go:345-347 ENV result?.data || [] OK
journal-sync.mjs:191 /entities?per_page= api_handler.go:345-347 ENV Array.isArray?…:result.entities/result.data OK
sync-dashboard.mjs:370 /entities?per_page= api_handler.go:345-347 ENV _normalizeArray(result,'entities') OK
actor-sync.mjs:631 /entity-types api_handler.go:249-251 ListEntityTypes ENV result?.data || result || [] OK — #77 flag: false alarm (already unwraps)
import-wizard.mjs:268 /entity-types api_handler.go:249-251 ENV Array.isArray?types:(types?.data??types?.entity_types??[]) OK
sync-dashboard.mjs:359 /entity-types api_handler.go:249-251 ENV _normalizeArray(raw,'entity_types') OK
sync-dashboard.mjs:581 /entity-types api_handler.go:249-251 ENV _normalizeArray(raw,'entity_types') OK
sync-manager.mjs:456 /systems api_handler.go:1067-1069 ListSystems ENV result.data || [] OK (see LOW-1)
import-wizard.mjs:295 /systems api_handler.go:1067-1069 ENV Array.isArray?systems:(systems?.data??[]) OK
sync-manager.mjs:423 /addons (getAddons) api_handler.go:1198-1200 ListAddons ENV Array.isArray?addons:(addons?.data??[]) OK
import-wizard.mjs:279 /addons (getAddons) api_handler.go:1198-1200 ENV Array.isArray?addons:(addons?.data??[]) OK
import-wizard.mjs:287 /tags (getTags) tag_api_handler.go:72-74 ListTags ENV result unused — connectivity probe only OK (result not consumed)
calendar-sync.mjs:1383 /calendar/events calendar_api_handler.go:335-338 ListEvents ENV unwrapped in PR #77 OK (already fixed)

Bare-array endpoints — raw JSON array

Module file:line Endpoint Chronicle handler file:line Shape Consumption Verdict
map-sync.mjs:415 /maps map_api_handler.go:81 ListMaps ([]Map) BARE Array.isArray?result:(result?.data||[]) OK — #77 flag: false alarm (shape is BARE, not envelope; handles both)
import-wizard.mjs:793 /maps map_api_handler.go:81 BARE Array.isArray?result:(result?.data??result?.maps??[]) OK
sync-dashboard.mjs:498 /maps map_api_handler.go:81 BARE _normalizeArray(raw,'maps') OK
map-sync.mjs:908 /maps/:id/{markers,drawings,tokens,layers,fog} map_api_handler.go:686/142/286/484/583 BARE (fog OBJ) _coerceArray (fog object-guarded) OK
map-sync.mjs:1052 /maps/:id/{kind} (poll) same as above BARE _coerceArray (fog object-guarded) OK
item-sync.mjs:116 /entities/:id/relations api_handler.go:851 ListEntityRelations BARE (relations||[]).filter OK
shop-widget.mjs:154 /entities/:id/relations api_handler.go:851 BARE (relations||[]).filter OK
sync-manager.mjs:251 /members api_handler.go:235 ListMembers BARE result?.data || result || [] + Array.isArray guard OK
note-sync.mjs:87 /notes (getNotes) note_api_handler.go:49 ListNotes BARE (getNotes normalizes) result?.data || result || [] OK
sync-dashboard.mjs:936 /notes (getNotes) note_api_handler.go:49 BARE result?.data || result || [] OK

Object endpoints — list lives in a named field (NOT {data,total})

Module file:line Endpoint Chronicle handler file:line Shape Consumption Verdict
generic-adapter.mjs:46 /systems/:id/character-fields api_handler.go:1139 GetCharacterFields (*CharacterFieldsResponse) OBJ .fields resp.fields OK
sync-dashboard.mjs:1114 /systems/:id/character-fields api_handler.go:1139 OBJ .fields Array.isArray(resp.fields) OK
item-sync.mjs:406 /systems/:id/item-fields api_handler.go:1173/1167 GetItemFields OBJ .fields result?.fields OK
journal-sync.mjs:924 /entities/:id/permissions api_handler.go:857-860/920 GetEntityPermissions OBJ .permissions permsData.permissions OK
sync-manager.mjs:517 /sync/pull sync_handler.go:172 PullMappings (SyncPullResponse, model.go:236) OBJ .mappings result.mappings OK

Dead / not-a-list (accounted for, no risk)

Symbol Endpoint Note
api-client.mjs:495 getRelationTypes() /relations/types (ENV, api_handler.go:1210-1212) Helper defined but no caller — dead code, cannot mismatch
GET /sync/mappings (ENV, sync_handler.go:51-53) Module only POSTs to /sync/mappings; no list-consuming GET
single-object gets /entities/:id, /maps/:id, /media/:id, /calendar, /sync/lookup Return one object; consumed as objects, not lists

Coverage: enumerated via grep -rnoE "\.(get\|getNotes\|getAddons\|getTags\|getRelationTypes)\((\/…|'/…'|"/…")" across **all** receiver names (this._api, this.api, api, apiClient, _api) in scripts/`. 28 list-consuming call sites, all OK.


Both #77 flags resolved (false alarms)

  • actor-sync.mjs:631 /entity-types — the endpoint does return an envelope (api_handler.go:249-251), but the caller already reads result?.data || result || [], so it unwraps correctly. No bug.
  • map-sync.mjs:415 /maps — the fix(calendar-sync): back-catalog envelope, date-push indexing, reentrant guard (FM-CAL-BACKCATALOG-FIX) #77 note hypothesised a {data} envelope; the real handler returns a bare []Map (map_api_handler.go:81, service maps/service.go:200). The caller reads Array.isArray(result) ? result : (result?.data || []) and handles both regardless. No bug.

This is exactly the T-O2 payoff: one flag had the wrong shape, one had the wrong verdict — both only visible by reading the producer's source.

Stop-and-flag observations (tabled per dispatch §3 — NOT fixed here)

  • LOW-1 — sync-manager.mjs:456 /systems reads result.data || [] (envelope-only; no bare fallback). Correct today because /systems returns an envelope, so it is not a mismatch — but it is the one list caller that would silently break if that endpoint ever flipped to bare, unlike every peer which accepts both. Optional symmetry hardening (Array.isArray(result) ? result : (result?.data ?? [])); left for a follow-up to keep this PR a pure audit.

What's in the diff

File Change
tools/test-envelope-audit.mjs new — 8 regression pins stubbing the REAL Chronicle shapes for the #77-flagged callers + the least-defensive one; each pin verified by temporary revert
CLAUDE.md convention note documenting the two list-response shapes and the mandatory defensive-unwrap rule (T-B4 footgun catalog)

The pins (not fixes) cover: actor-sync /entity-types envelope → _resolveCharacterTypeId resolves; sync-manager /systems envelope → _detectSystem matches; sync-manager /addons envelope → _fetchAddons detects (complements test-pc-claiming.mjs, which only stubbed the bare shape); map-sync sub-resources bare + .data_coerceArray tolerant.

Adversarial pass

An independent per-endpoint verification fan-out re-derived each handler shape and each caller verdict from source. 8 endpoint groups completed (all envelope endpoints + the multi-caller bare ones: /entities, /entity-types, /systems, /addons, /tags, /maps, /entities/:id/relations, /members) — every one returned zero mismatches, matching the table. The remaining groups (object-shaped /systems/:id/*, /entities/:id/permissions, /sync/pull; bare /notes, /maps/:id/*) were hand-verified above and are not in the {data,total} envelope class.

Tests

node --test tools/test-envelope-audit.mjs   → 8/8 pass
node --test tools/test-*.mjs (full CI suite) → 617/617 pass
node tools/check-package-descriptor.mjs      → OK (0 warnings)
node -c tools/test-envelope-audit.mjs        → parses

Regression pins verified genuine: temporarily reverting actor-sync's result?.data || result unwrap to result fails the /entity-types envelope pin; restoring passes.

Test plan

  • node --test tools/test-*.mjs passes locally (617/617)
  • node tools/check-package-descriptor.mjs passes (OK, 0 warnings)
  • No Foundry-runtime code changed (headless test + docs only) — no live smoke required
  • CI (descriptor check + node --test) will run on push

Tenet self-check

  • T-B1 security: n/a — read-path shape audit; no signed-URL / auth-token / trust-boundary surface touched
  • T-B2 plugin isolation: all changes stay inside this module (tools/, CLAUDE.md)
  • T-B3 production UI: n/a — no UI change
  • T-B4 dual-audience docs: the CLAUDE.md convention note + the header comment in test-envelope-audit.mjs serve both humans and AI sessions (footgun catalog)

Stop-and-flag

No tenet violation found. Audit conclusion is a negative result (zero mismatches) reached by consumer-verified reading per T-O2; the two #77 flags are documented false alarms. LOW-1 is tabled, not fixed, per the dispatch's "do not fix non-list shape bugs — table them."

🤖 Generated with Claude Code

https://claude.ai/code/session_01BgwKzUUUY26C1oFjxY4Hfu


Generated by Claude Code

… callers

FM-ENVELOPE-AUDIT — the follow-up sweep flagged by PR #77 re-verify item 5.
Audited every list-consuming get() caller in scripts/ against the real
Chronicle handler shape (consumer-verified, file:line). Verdict: zero
remaining mismatches — every list caller already unwraps defensively.

Adds tools/test-envelope-audit.mjs: regression pins (not fixes) that stub
the REAL Chronicle shapes for the two callers #77 flagged plus the
least-defensive envelope caller:
  - actor-sync /entity-types (envelope) → _resolveCharacterTypeId resolves
  - sync-manager /systems (envelope) → _detectSystem matches
  - sync-manager /addons (envelope) → _fetchAddons detects addon
  - map-sync sub-resources (bare + .data) → _coerceArray tolerant
Each pin verified by temporary revert of its unwrap.

Documents the two-shape list convention in CLAUDE.md (T-B4 footgun catalog).
No production code changed. Full suite 617/617; descriptor check OK.

Cites: 2026-05-21-core-tenets §T-O1, §T-O2, §T-B4

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BgwKzUUUY26C1oFjxY4Hfu
@keyxmakerx
keyxmakerx merged commit deb818b into main Jul 12, 2026
1 check passed
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.

2 participants