Skip to content

feat(mcp/connect): catch slug-length 50-char trap before Connect 500s#347

Merged
jjackson merged 1 commit into
mainfrom
emdash/e2e-leep-paint-vsvc9
May 18, 2026
Merged

feat(mcp/connect): catch slug-length 50-char trap before Connect 500s#347
jjackson merged 1 commit into
mainfrom
emdash/e2e-leep-paint-vsvc9

Conversation

@jjackson
Copy link
Copy Markdown
Owner

Summary

Closes the LearnModule.slug / DeliverUnit.slug 50-char trap that 500'd Phase 4 of leep-paint-collection/20260517-1515 opaquely. Connect's slug columns are SlugField() with the Django default max_length=50; Nova's compile_app derives slugs as module_<index>_<slugified-name>; module names ≥ ~40 chars overflow. Postgres DataError: value too long for type character varying(50) raised inside sync_learn_modules_and_deliver_units falls through program/api/views.py:102's narrow except and surfaces as HTTP 500 with empty body from connect_create_opportunity.

Same shape as the 2026-05-12 short_description 50-char trap, different boundary (slug is server-extracted from CCZ XML, not sent through any serializer). The 2026-05-12 generalized serializer-vs-model length probe (still pending) would NOT have caught this.

Four-layer fix (defense-in-depth)

  • mcp/connect/backends/commcare.tssimulateConnectSync projection now exposes slug_length_limit: 50, max_slug_length, and per-type oversized_slugs[]. New exported SLUG_LENGTH_LIMIT constant for lock-step bumping when the upstream column widens.
  • skills/app-release/SKILL.md § Step 6 — release-time gate extended from collision_count === 0 && per-type > 0 to ALSO require every oversized_slugs.* empty. [BLOCKER] brief lists each offender as <type>: <slug> (<length> chars, in <first_seen_in>). Structural backstop — catches even operator-driven manual Nova edits.
  • skills/pdd-to-learn-app/SKILL.md + skills/pdd-to-deliver-app/SKILL.md — new REQUIRED clause in the architect brief template: module / deliver-unit names must be ≤ 40 chars (Nova prefix + slugify + Connect column limit). Includes removal criterion tied to the upstream commcare-connect fix.
  • docs/learnings/2026-05-17-connect-slug-length-50-char-trap.md + registry update — documents the bug, the prior-framing refutations (CCZ-marker over-strip and time_estimate: NULL were both wrong before Sentry pinned it), the Sentry proof, and the upstream Connect fix needed (SlugField(max_length=255) + migration). docs/learnings/2026-05-12-boundary-probe-registry.md updated with the new shipped probe + annotation on the still-pending generalized probe explaining why it'd miss this class.

Tests

5 new vitest cases in test/mcp/connect/unit/connect-sync-projection.test.ts (boundary projection — every projection exposes the new fields; learn_module + deliver_unit slugs > 50 flagged; empty projection sensible defaults; 50-char slugs do NOT trigger). All 13 tests pass.

Reproducer

leep-paint-collection run 20260517-1515 Phase 4. Module 6 name "Stage 2: Sample Preparation, Drying, Bagging, Shipment" produces slug module_6_stage_2_sample_prep_drying_bagging_shipment (52 chars). Pre-fix: opaque HTTP 500. Post-fix: app-release Step 6 halts with a [BLOCKER] naming the offender + remediation; the architect brief constraint also prevents the long name being emitted in the first place.

Recovery for the affected run

Rename Nova module 6 to a shorter active title (e.g. "Stage 2: Sample Prep + Shipment" → slug module_6_stage_2_sample_prep_shipment = 38 chars), re-build + re-release Learn, resume /ace:run leep-paint-collection/20260517-1515.

Upstream follow-up

Separate Connect PR will bump LearnModule.slug + DeliverUnit.slug to SlugField(max_length=255) + migration. Also worth proposing: extend program/api/views.py:102's except clause to catch DataError / IntegrityError and return HTTP 400 with the offending column name — converts every future column-width trap (any field, any path) from "opaque 500" to "actionable 400."

Test plan

  • npx vitest run test/mcp/connect/unit/connect-sync-projection.test.ts — 13 pass
  • After merge + /ace:update, re-run /ace:run leep-paint-collection/20260517-1515 with a shortened M6 name and confirm Phase 4 proceeds.
  • After merge: a future Nova build with an over-length module name now halts at app-release Step 6 with the new [BLOCKER] brief instead of leaking to Connect.

🤖 Generated with Claude Code

Connect's LearnModule.slug / DeliverUnit.slug are SlugField() with the
Django default max_length=50. Nova's compile_app derives slugs as
module_<index>_<slugified-name>; module names ~40 chars overflow and
trigger Postgres `DataError: value too long for type character
varying(50)` inside Connect's sync_learn_modules_and_deliver_units,
which falls through program/api/views.py:102's narrow except clause
and surfaces as HTTP 500 with empty body from connect_create_opportunity.

Same shape as the 2026-05-12 short_description 50-char trap but at a
different boundary — the slug is server-extracted from CCZ XML, not
sent through any serializer. The 2026-05-12 generalized serializer-vs-
model length probe (still pending) would NOT have caught this.

Four-layer defense-in-depth:

- mcp/connect/backends/commcare.ts: simulateConnectSync projection
  now exposes slug_length_limit (constant 50), max_slug_length, and
  per-type oversized_slugs[]. New exported SLUG_LENGTH_LIMIT constant.
- skills/app-release/SKILL.md § Step 6: gate extended to require every
  oversized_slugs.* array empty; [BLOCKER] brief lists each offender.
  Structural backstop — catches even operator-driven manual Nova edits.
- skills/pdd-to-learn-app + pdd-to-deliver-app SKILL.md: new REQUIRED
  clause in the architect brief template — module/deliver-unit names
  must be ≤ 40 chars. Includes removal criterion tied to upstream fix.
- docs/learnings/2026-05-17-connect-slug-length-50-char-trap.md +
  registry update: documents the bug, the prior-framing refutations,
  the Sentry proof, and the upstream Connect fix needed.

5 new vitest cases. All 13 tests pass.

Reproducer: leep-paint-collection/20260517-1515 Phase 4 — M6 name
"Stage 2: Sample Preparation, Drying, Bagging, Shipment" → slug
module_6_stage_2_sample_prep_drying_bagging_shipment (52 chars).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jjackson jjackson force-pushed the emdash/e2e-leep-paint-vsvc9 branch from a1db7ac to 98abab9 Compare May 18, 2026 13:52
@jjackson jjackson merged commit de1d792 into main May 18, 2026
2 checks 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.

1 participant