Skip to content

PROD-2315: handle cross-kind same-name model collision (clear error + attributed cascade)#187

Open
5PK wants to merge 3 commits into
mainfrom
PROD-2315-cross-kind-model-collision
Open

PROD-2315: handle cross-kind same-name model collision (clear error + attributed cascade)#187
5PK wants to merge 3 commits into
mainfrom
PROD-2315-cross-kind-model-collision

Conversation

@5PK

@5PK 5PK commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

A model that shares a referenceName across instances but is a different kind (content vs component/module) can never be synced — Agility forbids content and component models sharing a reference name. Previously the CLI fell through to create, ate a raw server 409, and one collision cascaded into ~8 misleading secondary failures. This PR makes the failure clear and attributes/collapses the cascade.

Not a regression (confirmed against 13.20 by the reporter): 13.20 silently dropped the model, 13.21 loudly 409s — "silently wrong → loudly wrong." On both, the model never syncs.

Tier 1 — clear error instead of a blind 409 (model-pusher.ts)

  • Detect the cross-kind collision up front (same reference name, both kinds known and differing) and fail it with an actionable message — "'LinkCard' is a component/module model on the source, but a content model with that reference name already exists on the target — rename one, then re-sync"without attempting the guaranteed-409 create. Reported as a conflict under --preflight.
  • Surface the server's real error message for any create failure in the ERROR SUMMARY (was push-log-only).
  • Type-unknown pulls are unaffected (falls back to name-only matching).

Tier 2 — attribute + group the cascade

  • New blocked-model registry in state (mirrors failedContentRegistry), cleared at sync start; model-pusher registers each collision.
  • Containers whose model is blocked now say "blocked by model conflict 'LinkCard': …" instead of the generic "Target model mapping not found".
  • Content items skipped due to a blocked model are registered with the collision reason, so referencing pages inherit the real cause (via the existing getFailedContent path).
  • ERROR SUMMARY pulls blocked dependents out of the raw failure list and renders one line per blocking model with the affected items beneath. Net: 1 collision → 1 root failure + 1 grouped block line (was 1 → ~9 scattered).

Decisions (per discussion)

relabel + dedupe · continue the run (no hard halt) · exit-code behavior left to PROD-2310 · collision-only for now (not generalized to other failed-model causes).

Testing

  • Unit tests: collision → clear failure + no saveModel call, server-error surfacing, type-unknown fallback, same-kind adopt (PROD-2211), preflight conflict; blocked-model registry; failed-content registry carries the blocking model. Updated the existing PROD-2211 type-blind-lookup test to the new (no-blind-create) behavior.
  • Cross-pusher grouping verified via manual end-to-end (a real cross-kind collision with dependents on the test instance pair).

🤖 Generated with Claude Code

5PK and others added 3 commits July 15, 2026 13:49
…lind 409

When a model shares a referenceName across instances but is a different KIND
(content vs component/module), Agility forbids the collision, so a create can
never succeed. Previously model-pusher fell through to create and ate a raw
server 409, and the actionable detail lived only in the push-log file — the
ERROR SUMMARY showed a generic "Failed to create model". One collision also
cascaded into every dependent container/content/page.

Tier 1:
- Detect the cross-kind collision up front (same referenceName, both types
  known and differing) and fail the model with an actionable message
  ("... is a component model on the source, but a content model with that
  reference name already exists on the target — rename one, then re-sync"),
  skipping the guaranteed-409 saveModel call. Reported as a conflict under
  --preflight.
- Surface the server's actual error message for ANY create failure in the
  ERROR SUMMARY (createNewModel now returns the reason) instead of only the
  generic message; the detail was previously push-log-only.

Type-unknown pulls are unaffected: modelTypeMatches falls back to name-only,
so collision detection only fires when both kinds are known and differ.

NOT in this change (Tier 2, follow-up): suppressing the dependent
container/content/page cascade. The root model error is now clear; secondary
"mapping not found" failures still appear.

Updated the PROD-2211 type-blind-lookup test: a same-name different-type model
is no longer blindly created (saveModel not called) but flagged as a collision
failure. Added unit tests for collision failure, type-unknown fallback,
same-kind adopt, preflight conflict, and server-error surfacing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tier 1 made the root model collision a clear failure, but one collision still
cascaded into ~8 misleading secondary failures ("Target model mapping not
found", "No content mapping ... model may have changed") across the dependent
container, its content items, and every page referencing them.

Tier 2 attributes those dependents to the real cause and collapses them:

- New blocked-model registry in state (mirrors failedContentRegistry): keyed by
  reference name with a by-ID lookup; cleared at sync start. model-pusher
  registers each cross-kind collision.
- container-pusher: a container whose model mapping is missing because the model
  is blocked now reports "blocked by model conflict '<model>': <reason>" instead
  of the generic "Target model mapping not found", tagged for grouping.
- content-pusher: content items skipped because their model is blocked are
  registered in the failed-content registry with the collision reason, so any
  referencing page inherits the real cause (process-page already consults that
  registry), and are tagged for grouping.
- process-page/process-sitemap: thread the blocking model through the page
  failure so page failures group with the rest.
- push.ts ERROR SUMMARY: failures tagged blockedBy are pulled out of the raw
  "Sync Failures" list and rendered as one line per blocking model
  ("<model>: N dependent item(s) not synced — <reason>") with the affected
  items listed beneath. Net effect: 1 collision -> 1 root failure + 1 grouped
  block line, instead of 1 -> 9 scattered failures.

Decisions (per discussion): relabel + dedupe; continue the run (no hard halt);
exit-code behavior left to PROD-2310; collision-only (does not generalize to
other failed-model causes yet).

Tests: blocked-model registry unit tests; failed-content registry carries the
blocking model; model-pusher collision test asserts the block is registered.
Cross-pusher grouping is covered by the manual end-to-end verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live verification surfaced that content models report contentDefinitionTypeID 0
(content item) — the ticket assumed 1. modelKindName only mapped 1 -> "content",
so the message read "is a type 0 model on the source" instead of "content".
Map both 0 (item) and 1 (list) to "content"; 2 stays "component/module".

Verified end-to-end (source content LinkCard -> target component LinkCard):
model push reports 1 failed with "…is a content model on the source, but a
component/module model…"; the container + 3 content items are collapsed into a
single "Blocked by model conflicts (4)" summary line — no raw 409, no cascade.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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