Skip to content

feat(api): DTPR icon composition — symbols, shapes, composed icons, MCP get_icon_url#270

Merged
pichot merged 11 commits intomainfrom
feat/icon-composition-api
Apr 18, 2026
Merged

feat(api): DTPR icon composition — symbols, shapes, composed icons, MCP get_icon_url#270
pichot merged 11 commits intomainfrom
feat/icon-composition-api

Conversation

@pichot
Copy link
Copy Markdown
Member

@pichot pichot commented Apr 18, 2026

Summary

Moves DTPR icon composition into the api/ Cloudflare Workers app as a first-class capability. Structural schema gains symbol_id on elements and shape on categories; IconSchema is dropped; category_ids: string[] collapses to category_id: string. Build pipeline pre-bakes every (element × variant) composed SVG to R2 — the runtime hot path is a plain R2 point-read with long CDN TTL, and on-the-fly composition remains as a miss-fallback so authors can iterate on new symbols/context colors between builds.

New REST endpoints under /api/v2/: shapes/:shape.svg, schemas/:version/symbols/:symbol_id.svg, and schemas/:version/elements/:id/icon[.variant].svg. New MCP tool get_icon_url. Release content ported via the migration to emit the new fields + copy 64 unique symbol SVGs into the beta release, yielding 168 pre-baked composed SVGs per build. Plan implemented end-to-end in 9 units; 355 tests passing (323 Workers + 32 CLI).

Test plan

  • pnpm --filter api typecheck passes
  • pnpm --filter api test passes (355 tests)
  • pnpm --filter api schema:build ai@2026-04-16-beta emits 64 symbols + 168 composed icons to dist/
  • Spot-check a composed icon response in a preview deploy: curl returns image/svg+xml with correct Cache-Control and DTPR-Content-Hash headers
  • Confirm icon_miss_fallback log line appears when the R2 object is absent and disappears after r2-upload

🤖 Generated with Claude Code

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 18, 2026

Too many files changed for review. (217 files found, 100 file limit)

pichot added 8 commits April 18, 2026 15:24
- Add symbol_id to ElementSchema with [a-zA-Z0-9_-] whitelist
- Add ShapeTypeEnum + required shape field to CategorySchema
- Collapse category_ids: string[] to singular category_id
- Drop IconSchema and rule 14 (ICON_URL_EMPTY, ICON_FORMAT_EMPTY)
- Cascade singular category_id through validator, REST, MCP, search
- Rewrite schema tests for new shape; update all fixtures
- api/src/icons/shapes.ts: SHAPES record with 4 shape templates
  (circle, hexagon, octagon, rounded-square), getShapeSvgFragment()
  with parameterized fill/stroke, UnknownShapeError
- api/src/icons/color.ts: parseHex, relativeLuminance, contrastRatio,
  innerColorForShape per WCAG 2.1 (threshold 0.179)
- Pure modules, no I/O or Worker globals
- api/src/icons/compositor.ts: composeIcon({ shape, symbolSvg, variant })
  produces 36x36 composed SVG; stripOuterSvg rejects BOM/XML-prolog/
  leading-comment wrapper at runtime
- Variant matrix: default (outline), dark (inverted), colored (WCAG-picked
  inner color)
- Deterministic: pure string composition, no randomness or timestamps
- 19 tests incl. 4 golden-file parity checks (one per shape primitive)
…ory_id

- transform-element: emit symbol_id (v1 symbol filename stem) and singular
  category_id (first ai__* from v1 list); drop icon block
- transform-category: emit shape from AI_CATEGORY_SHAPE_MAP (copied from
  studio/lib/icon-shapes.ts)
- port script: copy unique symbol SVGs from app/public/dtpr-icons/symbols
  into release symbols/ directory; fail on missing source file
- Regenerate ai@2026-04-16-beta: 11 categories + 75 elements + 64 unique
  symbol SVGs

Migration test coverage: symbol_id derivation, singular category_id picking,
per-shape emission, ELEMENT_NO_SYMBOL warning path
…ntent_hash coverage

Build pipeline:
- yaml-reader: load release symbols/ directory into SchemaVersionSource
- json-emitter: materialize shape + icon_variants onto emitted elements
- json-emitter: pre-bake (element x variant) composed SVGs via composeIcon
- json-emitter: include sorted symbol + composed-icon sha256 maps in
  content_hash so byte changes without YAML changes force a new hash

Validator rules:
- symbol-refs: SYMBOL_NOT_FOUND (with nearest-id fix_hint),
  SYMBOL_MALFORMED_WRAPPER (BOM / xml prolog / leading comment),
  SYMBOL_ACTIVE_CONTENT (script, event handlers, external refs,
  foreignObject)
- variant-reserved: RESERVED_VARIANT_TOKEN for context value ids
  colliding with 'default' or 'dark'
- color-contrast: LOW_CONTRAST_CONTEXT_COLOR warning (< 4.5:1)

Build output on ai@2026-04-16-beta: 64 symbols + 168 composed icons
across 75 elements.
- store/keys.ts: symbolKey(), composedIconKey()
- store/r2-loader.ts: loadSymbolSvg, loadComposedIconSvg using
  cachedText and the existing beta-aware cache options
- store/index.ts: inline-bundle fallback for both + singular loadCategory
- store/inline-bundles.ts: symbols and composedIcons maps on InlineBundle
- scripts/r2-upload.ts: .svg content-type mapping; walker already handles
  the nested icons/<element_id>/<variant>.svg tree
- GET /api/v2/shapes/:shape.svg — bundled shape primitive,
  always immutable cache
- GET /api/v2/schemas/:version/symbols/:symbol_id.svg — release-pinned
  symbol from R2 (with inline-bundle fallback); beta caches 1h,
  stable is immutable
- responses.ts: setIconCacheHeaders helper with prebaked flag so
  Unit 8 can diverge on fallback path
- error-handler: Cache-Control: no-store on all error responses so
  beta's short TTL never poisons the CDN cache on a 4xx
- Explicit per-route timeouts for all icon routes (shape, symbol,
  composed) in app.ts
- GET /api/v2/schemas/:version/elements/:id/icon.svg (default variant)
- GET /api/v2/schemas/:version/elements/:id/icon.:variant.svg
  (both routes served by a single :icon_variant handler due to Hono's
  literal-dot-before-param handling)
- Hot path: R2 point-read of pre-baked bytes with long beta TTL (1h)
  / stable immutable cache
- Miss-fallback: load element + category + symbol, composeIcon on the
  fly, short TTL (60s); no R2 write-back
- icon_miss_fallback observability log on every miss
- resolveComposeVariant maps URL variant to ComposeVariant union;
  unknown variant → 404 with valid_variants list
- Byte-parity test confirms build-time and runtime compositors produce
  identical bytes for the same inputs
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 18, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
dtpr-docs 0fcb527 Apr 18 2026, 02:02 PM

pichot added 2 commits April 18, 2026 15:28
- mcp/tools.ts: get_icon_url tool returns { url, content_type, variant,
  valid_variants } so MCP clients resolve composed icon URLs without
  building paths by hand
- Validates variant against the element's icon_variants; unknown variant
  returns error envelope listing valid options
- icon_variants, symbol_id, shape surface automatically via get_element
  and list_elements (when explicitly requested in fields); default
  list_elements projection stays tight
- Seed fixtures extended with context on ai__decision and a
  MaterializedElement shape so MCP responses mirror real release JSON
- brainstorm: 2026-04-17-dtpr-icon-composition-brainstorm
- plan: 2026-04-17-002-feat-dtpr-icon-composition-plan (the blueprint
  this branch implements)
- stub: 2026-04-17-001-feat-content-release-terminology-rename
  (deferred follow-up)
@pichot pichot force-pushed the feat/icon-composition-api branch from 63cca9e to 90fa520 Compare April 18, 2026 13:28
Element no longer carries an icon field; category_ids/symbol refs
collapsed to singular category_id + symbol_id, categories gained
shape, and SchemaVersionSource now requires a symbols map. Update
types, deriveElementDisplay (iconUrl/iconAlt now come via options,
default to HEXAGON_FALLBACK + title), and test fixtures to match,
which also unblocks validateDatachain's happy-path runtime check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pichot pichot merged commit e7106f4 into main Apr 18, 2026
6 of 7 checks passed
@pichot pichot deleted the feat/icon-composition-api branch April 18, 2026 14:01
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