audit(envelope): sweep all list-consuming get() callers — zero remaining mismatches (FM-ENVELOPE-AUDIT)#78
Merged
Conversation
… 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
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:linein the table below (e.g./entity-typesenvelope atinternal/plugins/syncapi/api_handler.go:249-251;/mapsbare array atmap_api_handler.go:81)Foundry compatibility: n/a — no Foundry-runtime code changed; the added file is a headless
node --testsuite plus a CLAUDE.md convention noteMockup: 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-consumingget()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 anArray.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 inCLAUDE.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}file:linefile:lineactor-sync.mjs:182/entities?type_id=api_handler.go:345-347ListEntitiesresult?.data || []actor-sync.mjs:780/entities?type_id=api_handler.go:345-347result?.data || []journal-sync.mjs:191/entities?per_page=api_handler.go:345-347Array.isArray?…:result.entities/result.datasync-dashboard.mjs:370/entities?per_page=api_handler.go:345-347_normalizeArray(result,'entities')actor-sync.mjs:631/entity-typesapi_handler.go:249-251ListEntityTypesresult?.data || result || []import-wizard.mjs:268/entity-typesapi_handler.go:249-251Array.isArray?types:(types?.data??types?.entity_types??[])sync-dashboard.mjs:359/entity-typesapi_handler.go:249-251_normalizeArray(raw,'entity_types')sync-dashboard.mjs:581/entity-typesapi_handler.go:249-251_normalizeArray(raw,'entity_types')sync-manager.mjs:456/systemsapi_handler.go:1067-1069ListSystemsresult.data || []import-wizard.mjs:295/systemsapi_handler.go:1067-1069Array.isArray?systems:(systems?.data??[])sync-manager.mjs:423/addons(getAddons)api_handler.go:1198-1200ListAddonsArray.isArray?addons:(addons?.data??[])import-wizard.mjs:279/addons(getAddons)api_handler.go:1198-1200Array.isArray?addons:(addons?.data??[])import-wizard.mjs:287/tags(getTags)tag_api_handler.go:72-74ListTagscalendar-sync.mjs:1383/calendar/eventscalendar_api_handler.go:335-338ListEventsBare-array endpoints — raw JSON array
file:linefile:linemap-sync.mjs:415/mapsmap_api_handler.go:81ListMaps ([]Map)Array.isArray?result:(result?.data||[])import-wizard.mjs:793/mapsmap_api_handler.go:81Array.isArray?result:(result?.data??result?.maps??[])sync-dashboard.mjs:498/mapsmap_api_handler.go:81_normalizeArray(raw,'maps')map-sync.mjs:908/maps/:id/{markers,drawings,tokens,layers,fog}map_api_handler.go:686/142/286/484/583_coerceArray(fog object-guarded)map-sync.mjs:1052/maps/:id/{kind}(poll)_coerceArray(fog object-guarded)item-sync.mjs:116/entities/:id/relationsapi_handler.go:851ListEntityRelations(relations||[]).filtershop-widget.mjs:154/entities/:id/relationsapi_handler.go:851(relations||[]).filtersync-manager.mjs:251/membersapi_handler.go:235ListMembersresult?.data || result || []+Array.isArrayguardnote-sync.mjs:87/notes(getNotes)note_api_handler.go:49ListNotesresult?.data || result || []sync-dashboard.mjs:936/notes(getNotes)note_api_handler.go:49result?.data || result || []Object endpoints — list lives in a named field (NOT
{data,total})file:linefile:linegeneric-adapter.mjs:46/systems/:id/character-fieldsapi_handler.go:1139GetCharacterFields (*CharacterFieldsResponse).fieldsresp.fieldssync-dashboard.mjs:1114/systems/:id/character-fieldsapi_handler.go:1139.fieldsArray.isArray(resp.fields)item-sync.mjs:406/systems/:id/item-fieldsapi_handler.go:1173/1167GetItemFields.fieldsresult?.fieldsjournal-sync.mjs:924/entities/:id/permissionsapi_handler.go:857-860/920GetEntityPermissions.permissionspermsData.permissionssync-manager.mjs:517/sync/pullsync_handler.go:172PullMappings (SyncPullResponse,model.go:236).mappingsresult.mappingsDead / not-a-list (accounted for, no risk)
api-client.mjs:495getRelationTypes()/relations/types(ENV,api_handler.go:1210-1212)/sync/mappings(ENV,sync_handler.go:51-53)/sync/mappings; no list-consuming GET/entities/:id,/maps/:id,/media/:id,/calendar,/sync/lookupCoverage: enumerated via
grep -rnoE "\.(get\|getNotes\|getAddons\|getTags\|getRelationTypes)\((\/…|'/…'|"/…")"across **all** receiver names (this._api,this.api,api,apiClient,_api) inscripts/`. 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 readsresult?.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, servicemaps/service.go:200). The caller readsArray.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)
sync-manager.mjs:456/systemsreadsresult.data || [](envelope-only; no bare fallback). Correct today because/systemsreturns 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
tools/test-envelope-audit.mjsCLAUDE.mdThe pins (not fixes) cover:
actor-sync/entity-typesenvelope →_resolveCharacterTypeIdresolves;sync-manager/systemsenvelope →_detectSystemmatches;sync-manager/addonsenvelope →_fetchAddonsdetects (complementstest-pc-claiming.mjs, which only stubbed the bare shape);map-syncsub-resources bare +.data→_coerceArraytolerant.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
Regression pins verified genuine: temporarily reverting
actor-sync'sresult?.data || resultunwrap toresultfails the/entity-typesenvelope pin; restoring passes.Test plan
node --test tools/test-*.mjspasses locally (617/617)node tools/check-package-descriptor.mjspasses (OK, 0 warnings)node --test) will run on pushTenet self-check
tools/,CLAUDE.md)CLAUDE.mdconvention note + the header comment intest-envelope-audit.mjsserve 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