Skip to content

feat(sdk): fold bitbadges-builder-mcp into the SDK#144

Merged
trevormil merged 54 commits intomainfrom
feat/fold-mcp-into-sdk
Apr 14, 2026
Merged

feat(sdk): fold bitbadges-builder-mcp into the SDK#144
trevormil merged 54 commits intomainfrom
feat/fold-mcp-into-sdk

Conversation

@trevormil
Copy link
Copy Markdown
Collaborator

@trevormil trevormil commented Apr 13, 2026

Summary

  • Migrates the standalone bitbadges-builder-mcp package into bitbadgesjs-sdk/src/mcp/ (78 files). The separate repo is now obsolete.
  • Exposes the full MCP surface as SDK subpath exports: ./mcp, ./mcp/registry, ./mcp/tools, ./mcp/resources, ./mcp/skills, ./mcp/session. Adds typesVersions so consumers on classic moduleResolution: node still resolve the types.
  • Preserves the bitbadges-builder-mcp bin name as a second entry on bitbadgesjs-sdk — existing Claude Desktop configs continue to work after switching the install from npm i -g bitbadges-builder-mcp to npm i -g bitbadgesjs-sdk.
  • Adds a new bitbadges-cli mcp command group (list, call, session, resources) that dispatches the registry in-process — no subprocess, no MCP protocol round-trip. Sessions persist under ~/.bitbadges/sessions/<id>.json so agents can compose a collection across multiple CLI invocations.
  • Includes a separate commit with a 909-line Jest suite for src/core/validate.ts (~90 cases) that was sitting uncommitted locally. Split into its own commit for reviewability.

Why

Two coupled problems:

  1. Circular dep. After wiring MCP into the CLI via file: path, we ended up with SDK → bitbadges-builder-mcpbitbadgesjs-sdk. Publishing was a chicken-and-egg problem, two physical SDK copies lived in the graph, and consumers hit instanceof failures across the boundary.
  2. Distribution. Agents and LLM clients needed one binary/install to reach every tool + resource. Shipping two separate npm packages with tight coupling was noise.

Folding the MCP into the SDK collapses both. One install, one import, one registry.

Companion PRs

  • bitbadges-indexer#92 — rewrites src/routes/ai-builder/ imports from bitbadges-builder-mcp/toolsbitbadgesjs-sdk/mcp/tools.
  • bitbadges-docs#35 — switches the documented install commands + Claude Desktop configs from bitbadges-builder-mcpbitbadgesjs-sdk.
  • bitbadges-frontend#128reviewItems sidebar becomes a thin adapter over the SDK's reviewCollection(). Depends on this PR (for reviewCollection / Finding / ReviewContext) and bitbadges-indexer#92.
  • bitbadgeschain#69 — tracking branch for v30 chain upgrade; includes the bitbadgeschaind mcp cobra delegation that forwards to bitbadges-cli mcp (long-lived, merges on hard fork).
  • bitbadgeschain / feat/v30 has a bitbadgeschaind mcp delegation command plus a .gitignore fix (landed on the existing feature branch, no separate PR).

Test plan

  • bun run build clean (dual ESM + CJS, tsc-alias, madge circular check)
  • bitbadges-cli mcp list --names → 50 tools
  • bitbadges-cli mcp call get_current_timestamp, get_skill_instructions, unknown-tool error path
  • bitbadges-cli mcp resources list --uris → 11 resources
  • bitbadges-cli mcp resources read bitbadges://skills/all — body matches previous MCP resource content
  • Session flow across invocations: set_collection_metadataget_transaction with --session demo, state persists to disk
  • Standalone MCP stdio server via new bin path: node dist/cjs/mcp/index.js responds to tools/list JSON-RPC
  • indexer (linked via bun link bitbadgesjs-sdk) tsc --build ./src clean with the new subpath imports
  • Archive / deprecate the bitbadges-builder-mcp GitHub repo after this PR lands

🤖 Generated with Claude Code

trevormil and others added 2 commits April 13, 2026 09:56
Migrates the standalone bitbadges-builder-mcp package into
bitbadgesjs-sdk/src/mcp/. Removes the file:-dep circular between SDK and
MCP, collapses the two packages into one install, and exposes the full
tool+resource surface as subpath exports (./mcp, ./mcp/tools,
./mcp/resources, ./mcp/skills, ./mcp/session, ./mcp/registry).

The stdio MCP server keeps working through a new bin entry
(bitbadges-builder-mcp -> dist/cjs/mcp/index.js) so existing Claude
Desktop configs continue to resolve by installing bitbadgesjs-sdk.

Adds `bitbadges-cli mcp` subcommand group (list / call / session /
resources) that dispatches directly against the in-process registry
— no subprocess, no protocol. File-backed sessions live under
~/.bitbadges/sessions/<id>.json so incremental builders compose across
invocations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers isValidListId, checkNumbersAreStrings, checkUintRangeFormat,
findAndValidateUintRanges, validateOrderCalculationMethod,
validateApprovals, validateTokenMetadata, validatePermissions,
validateApprovalCriteria, validateMsgConstructorFields,
validateTransaction, validateSubscriptionApproval.

~90 cases. Landed alongside the MCP fold-in since both touch the SDK
working tree; split into its own commit for reviewability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread packages/bitbadgesjs-sdk/src/mcp/tools/utilities/fetchDocs.ts Fixed
Comment thread packages/bitbadgesjs-sdk/src/mcp/tools/utilities/fetchDocs.ts Fixed
Comment thread packages/bitbadgesjs-sdk/src/mcp/tools/utilities/fetchDocs.ts Fixed
Comment thread packages/bitbadgesjs-sdk/src/mcp/tools/utilities/fetchDocs.ts Fixed
trevormil and others added 2 commits April 13, 2026 10:13
Review feedback on cli/commands/mcp.ts — `fs`/`os`/`path` and the
SESSIONS_DIR constant were domain logic leaking into a CLI command
file. Any other consumer (indexer, a future stdio bridge, tests) that
wanted file-backed sessions would have duplicated the same 40 lines.

Moves the persistence layer into src/mcp/session/fileStore.ts beside
the in-memory store, exported through the existing ./mcp/session
subpath:

  - DEFAULT_SESSIONS_DIR (overridable per call)
  - sessionFilePath(id, dir?)
  - loadSessionFromDisk(id, dir?)
  - saveSessionToDisk(id, dir?)
  - listSessionFilesOnDisk(dir?)
  - readSessionFileRaw(id, dir?)
  - resetSessionFile(id, dir?)

cli/commands/mcp.ts now imports these helpers. The file keeps `fs`
only for the genuinely CLI-local concern of reading --args-file. No
behavior change — `~/.bitbadges/sessions/<id>.json` stays the default
location, same format, same fall-through semantics on missing files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CodeQL flagged four issues on the regex-based HTML sanitizer used by
fetch_docs:

1. `<script[^>]*>...</script>` doesn't match `</script >` (trailing
   whitespace before the closing `>`) — script bypass.
2. A single `.replace()` pass over `<script...>` leaves nested forms
   like `<scr<script>ipt>` as `<script>` after removing the inner
   match — partial sanitization.
3. Same two issues for `<style>`.
4. Sequential `.replace()` calls for HTML entities double-unescape —
   `&amp;lt;` goes to `&lt;` to `<` instead of staying as `&lt;`.

Replaces the ad-hoc regexes with:

- `stripTagBlock(html, tag)` — tempered-greedy regex with permissive
  `</tag\s*>` closing match, run in a loop until the output is stable
  so overlapping bypasses are fully eliminated.
- `decodeHtmlEntities(input)` — single-pass lookup table over
  `&(nbsp|amp|lt|gt|quot|apos);` so no decoded character can be
  reinterpreted as part of another entity.

Verified against CodeQL's four cases plus the nested-prefix and
`&amp;lt;` double-unescape cases. Behavior for normal input (the only
path this tool actually hits — bitbadges.io docs pages) is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ecks

Adds a single SDK entry point so CLI, MCP, indexer, and frontend all run
the same deterministic review set. Ports ~40 frontend UX checks into
src/core/review-ux/ and exposes reviewCollection() + Finding type with
stable machine codes plus English fallback messages (localization is
handled by callers via the finding code).

CLI: sdk review now supports --json / --human / --strict with exit 0/1/2.
MCP: new review_collection tool; audit_collection / verify_standards
marked deprecated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds optional localeKey (+ per-slot overrides) to Finding so frontend
adapters can prefer the richly-edited review_*_title/detail/fix strings
in en/common.json over the SDK's terser messageEn/recommendationEn.
Every ported UX check now points at its legacy key base.

Also ports the last orphaned checks that had locale entries but no SDK
coverage:
- forceful_invariant_not_set (pre-fix critical for invariant-only case)
- credit_token_transfers_allowed (credit-token skill)
- addresslist_transfers_allowed (address-list skill)

And fixes a small firing-condition bug in forceful_transfers_allowed so
the invariant-only branch actually triggers, switching localeKeyDetail
conditionally across the 3 state branches.

+4 tests, 1010/1010 pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nd-aids

Routes every indexer auto-fix repair to its correct home:
- Wrong-shape hallucinations (PathMetadata.image at wrong level, array
  collectionMetadata, partial approvalAmounts/maxNumTransfers, empty
  amountTrackerId with non-zero limits) now throw clearly in
  validate.ts instead of being silently coerced
- amountTrackerId is auto-filled in sessionState.addApproval from the
  approvalId when a non-zero limit exists, preventing silent tracker
  cross-contamination across approvals
- Opinionated semantic guesses (predetermined_order unset/conflict)
  surface as new review items review.ux.predetermined_order_unset and
  review.ux.predetermined_order_conflict instead of silent defaults

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes the ensureAmountTrackerIds helper from sessionState.addApproval.
The validator in validate.ts already throws clearly when a non-zero
limit is set without an amountTrackerId — silent auto-fill even through
the MCP tool path was still a band-aid over a producer bug.

Pure throw-only posture: the AI agent sees the real error and learns to
set amountTrackerId explicitly instead of relying on an implicit default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
"MCP" is the wire protocol of the stdio transport — one presentation
layer among several. Calling the CLI subcommand, the source directory,
and the library subpath exports "mcp" overstates the protocol's
relevance and confuses consumers that reach the handlers in-process
(CLI, indexer library use, chain binary delegation). The consistent
brand elsewhere is "BitBadges Builder", so drop the `-mcp` suffix.

Scope:
- `src/mcp/` → `src/builder/` (78 files moved via git mv)
- `src/cli/commands/mcp.ts` → `src/cli/commands/builder.ts`
- Identifier: `mcpCommand` → `builderCommand`
- Commander root: `bitbadges-cli mcp ...` → `bitbadges-cli builder ...`
- package.json subpath exports and typesVersions: `./mcp/*` → `./builder/*`
- package.json bin: `bitbadges-builder-mcp` → `bitbadges-builder`
- `sdk skills` alias slug: `mcp-builder-skills` → `builder-skills`
  (coordinated with the bitbadges-docs rename on `feat/mcp-in-sdk`)
- fetchDocs topic map: 'builder'/'builder tools' added, URLs pointed at
  /builder-tools; old 'mcp'/'mcp tools' keys retained for back-compat
  lookups until docs deploys
- LLM-facing resource strings (recipes.ts, frontendDocs.ts, workflows.ts,
  registry.ts, tools/builders/*, core/validate.ts) rewritten
  "MCP tool" → "builder tool", "MCP server" → "builder", etc.

Kept intentionally (protocol-true references):
- `@modelcontextprotocol/sdk` imports
- File headers in `src/builder/server.ts` and `src/builder/index.ts`
  clarified as "Model Context Protocol (MCP) stdio transport/server" —
  the directory is named for purpose, the file explains the transport
- `src/builder/tools/registry.ts` uses "MCP" only where describing the
  content block wire shape returned over the stdio transport

Verified:
- `npm run build` clean (dual ESM+CJS, tsc-alias, madge no circulars)
- `bitbadges-cli builder list --names | wc -l` → 51
- `bitbadges-cli builder resources list --uris | wc -l` → 11
- `bitbadges-cli builder call get_current_timestamp` round-trips
- `node dist/cjs/builder/index.js` stdio JSON-RPC tools/list OK
- `bitbadges-cli mcp ...` now fails with "unknown command 'mcp'"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follows the prior mcp → builder rename. "build" and "builder" were two
separate top-level command groups that both meant "build a transaction"
to users — just from different audiences (deterministic templates vs.
AI tool registry). Fold everything into one builder hierarchy so the
full build + review workflow lives in one place.

Layout:

  bitbadges-cli builder templates <name> [flags]  # 18 deterministic templates
  bitbadges-cli builder tools list                # tool registry discovery
  bitbadges-cli builder tools call <tool> --args  # fine-grained registry calls
  bitbadges-cli builder review <input>            # reviewCollection(), grouped findings
  bitbadges-cli builder explain <input>           # interpretTransaction / interpretCollection
  bitbadges-cli builder validate <input>          # pure validateTransaction()
  bitbadges-cli builder resources list|read       # static MCP resources
  bitbadges-cli builder session list|show|reset   # persisted session scratch pad

Changes:
- src/cli/commands/build.ts renamed to templates.ts. Top-level
  `build` command renamed to `templates`, export is now
  `templatesCommand`. 18 template commands unchanged — only the
  wrapping container moved.
- builder.ts attaches `templatesCommand` as a subcommand, wraps the
  prior `list` / `call` commands under a new `tools` subcommand.
- builder.ts adds top-level `review`, `explain`, `validate` commands.
  `review` is a faithful port of `sdk review` (supports numeric
  collection IDs with API fetch). `explain` auto-detects tx vs
  collection shape and routes to interpretTransaction or
  interpretCollection. `validate` wraps validateTransaction for a
  fast low-level structure check.
- New `ensureTxWrapper` helper normalizes loose inputs — bare
  `{typeUrl, value}` Msgs get wrapped in `{messages: [msg]}` so both
  review and validate accept piped template output cleanly.
- cli/index.ts no longer registers the old buildCommand top-level;
  `builder templates` is the canonical path.

Auto-review on template output:
- `builder templates vault --backing-coin USDC` now pipes stdout JSON
  + stderr review findings by default. The stdout stream stays pure
  JSON for downstream sign/broadcast pipelines.
- `--dry-run` flag removed (subsumed by the default review).
- New `--json-only` flag suppresses the automatic review for
  pipelines that want zero stderr noise.
- Auto-review is called with `selectedSkills: []` so the reviewer's
  skill fan-out (which defaults to "run every protocol check") is
  disabled — the user just built with a specific template, so only
  cross-cutting audit/ux checks should fire. A full skill-union
  review stays one step away via `builder review <file>`.

Verified end-to-end:
- `npm run build` clean (589 files, tsc dual build, tsc-alias,
  madge circular check pass)
- `builder templates vault --backing-coin USDC --creator bb1test`
  → JSON on stdout, 0 critical / 5 warning / 5 info on stderr,
  verdict `warn`
- `… --json-only` → pure JSON on stdout, empty stderr
- `builder templates vault --json-only | builder review -` → 6
  critical / 8 warning / 5 info (full skill fan-out as expected
  for an explicit review invocation)
- `builder templates vault --json-only | builder explain -` →
  prose transaction summary
- `builder templates vault --json-only | builder validate -` →
  5 actionable static validation issues
- `builder tools list --names | wc -l` → 51
- `builder tools call get_current_timestamp` round-trips
- `build vault` (old top-level) → commander suggests `builder`
- `builder list` (old flat command) → `unknown command 'list'`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous review output was one wall of unformatted text with no visual
separation between the stderr review section and the stdout JSON dump.
When both streams hit the terminal, the user couldn't tell where the
review ended and the transaction JSON began.

Adds src/cli/utils/terminal.ts with:
  - `makeColor(stream)` — narrow ANSI helper with TTY detection
    (honours NO_COLOR env var, respects `TERM=dumb`, inspects the
    specific stream's isTTY since stderr/stdout can differ per
    redirection)
  - `rule(ch, width, label?)` — horizontal rules with optional label
  - `renderReview(result, { stream, title })` — single renderer used
    by both `builder templates` auto-review and `builder review`, so
    they stay in lockstep. Wraps long text to the stream's column
    width, groups findings by severity, bold-tinted sigils
    (■ critical / ▲ warning / ● info), gray code slugs, summary
    bar with each count tinted only when non-zero
  - `renderJsonBoundary(stream)` — the "─── Transaction JSON (stdout)
    ───" rule that fires only when stdout AND stderr are both TTYs
    (interactive use); suppressed when piped so it never lands in a
    redirected JSON file

Updates:
  - templates.ts `emit()` now calls renderReview for auto-review and
    renderJsonBoundary when appropriate
  - builder review command uses renderReview (shared path)
  - builder validate gets the same treatment inline — rule headers,
    sigils, tinted summary, TTY-aware

Output file paths strip ANSI defensively before writing, so
`--output-file` always produces plain-text files even if rendered
from a TTY session.

Verified:
  - `script -q -c "builder templates vault …" /dev/null` → ANSI
    rendering with boundary rule between review + JSON
  - `builder templates vault … 2>/tmp/err >/tmp/out` → plain-text
    review in /tmp/err, no boundary rule, clean JSON in /tmp/out
  - `… --json-only 2>/tmp/err >/tmp/out` → /tmp/err empty (unchanged)
  - `builder validate -` on piped input → plain-text rule sections
    with bold sigils and summary
  - `npm run build` clean (589 files, madge pass)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two changes to the templates auto-review path, both driven by the
"build → review" UX pass:

1. Order flip: JSON now emits BEFORE the review. The user's final
   eyeline after a template run lands on the review verdict instead
   of a wall of JSON, and in interactive terminals scrolling up
   shows the build output above the critique. Works cleanly with
   pipes too — stdout JSON flushes first, stderr review after.

2. Auto-validate: before running `reviewCollection`, auto-review
   now also runs `validateTransaction` from core/validate.ts. These
   are both static, local, pre-broadcast checks, but they were
   previously split across `builder validate` and the review path.
   Folding them into one emit path means templates can't silently
   produce JSON that the chain would reject.

   In practice this surfaces genuine template bugs. Running the
   `vault` template today fires 4 structural errors (all in
   buildVault's output — PathMetadata.image doesn't exist,
   canUpdateAutoApproveAllIncomingTransfers missing, etc.) that the
   design-level reviewCollection never caught. Fixing those is
   out of scope for this commit — the change here just makes them
   visible on every run.

Implementation notes:
- emit() structure: JSON output → optional --explain → validate →
  review. Both checks are skipped by --json-only.
- New `renderValidate()` helper local to templates.ts. Kept separate
  from the richer `renderReview()` because the shapes differ
  (ValidationIssue vs Finding). Both share the same color palette
  and rule style via `makeColor`/`rule` in utils/terminal.ts.
- Removed the pre-JSON boundary rule (`renderJsonBoundary`): with
  the order flipped, the review's own "━━━ Auto-Review ━━━" header
  is the natural visual separator.

Decision recorded in the commit so future readers know why templates
do both validate+review and not simulate: simulation is a network +
auth concern with 100-1000x higher latency and failure modes
orthogonal to JSON correctness. Draw the line at static checks;
simulation is opt-in via a separate path.

Verified:
- `builder templates vault --backing-coin USDC` → JSON on stdout,
  Auto-Validate (4 errors/1 warning) + Auto-Review (5 warn/5 info)
  on stderr in that order
- `--json-only` → pure JSON, empty stderr (unchanged)
- `npm run build` clean

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The auto-validate layer added in 0f976b8 surfaced 41 structural
errors spread across 5 templates (vault, smart-account, subscription,
crowdfund, auction, prediction-market). All five failed for the same
set of shared-helper bugs. This commit fixes them at the source.

core/builders/shared.ts:

- `alwaysLockedPermission()` previously returned just
  `{permanentlyPermittedTimes: [], permanentlyForbiddenTimes: FOREVER}`
  and was used for every permission type. The validator correctly
  rejects that for permission shapes that require extra scoping
  fields. Adds two new scoped variants:
    - `alwaysLockedTokenIdsPermission()` — for
      canUpdateValidTokenIds / canUpdateTokenMetadata, returns the
      base permission plus `tokenIds: FOREVER` so the validator's
      TokenIdsActionPermission check passes.
    - `alwaysLockedCollectionApprovalPermission()` — for
      canUpdateCollectionApprovals, returns the base permission plus
      fromListId/toListId/initiatedByListId = 'All', transferTimes,
      tokenIds, approvalId, amountTrackerId, challengeTrackerId.
  `emptyPermissions()` and `frozenPermissions()` now call the
  correct variant for each field.

- `defaultBalances()` was emitting userPermissions without
  `canUpdateAutoApproveAllIncomingTransfers`. The SDK constructor
  requires it as `[]`. Added.

- `buildAliasPath()` was setting `metadata: {uri, customData, image}`
  on both the path and its denomUnits. PathMetadata only has
  `{uri, customData}`; the `image` field doesn't exist on-chain.
  Drops `image` from the proto emission and now sets placeholder
  URIs (`ipfs://METADATA_ALIAS_<denom>` and
  `ipfs://METADATA_ALIAS_<denom>_UNIT`) so the path has a valid
  shape and downstream metadata tooling has something to substitute.
  The `image` parameter is kept in the function signature for
  back-compat; a follow-up commit will wire it through the metadata
  placeholder surface.

core/builders/smart-account.ts:

- Was calling `alwaysLockedPermission()` for canUpdateCollectionApprovals
  directly. Switched to `alwaysLockedCollectionApprovalPermission()`.

core/builders/auction.ts:

- `initiatedByListId: ''` on the mint-to-winner approval — empty
  string is an invalid list ID. Changed to `'All'` with a comment
  explaining the tradeoff (users who want a specific bidder list
  can patch the approval).

core/builders/crowdfund.ts:

- Three approvals were falling back to `params.crowdfunder || ''`
  for toListId / initiatedByListId. Empty string fails validation.
  Changed fallback to `'All'` so the template produces valid JSON
  without the flag; the reviewer still surfaces the missing
  crowdfunder as a warning.

Verified with the auto-validate sweep after each fix:

  vault                0 errors (was 4)
  smart-account        0 errors (was 7)
  subscription         0 errors (was 3)
  crowdfund            0 errors (was 17)
  auction              0 errors (was 10)
  prediction-market    0 errors (was 10)
  [12 other templates] 0 errors

Design-level warnings from reviewCollection are unchanged — those
remain real advisories about permission neutrality, missing
noForcefulPostMintTransfers, etc. This commit only addresses
structural validation, not the review findings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ad section

User caught that the earlier metadata.image workarounds (both in SDK
builders and indexer prompts) were masking a root-cause issue: we've
been teaching LLMs to emit `metadata.image` on PathMetadata when the
proto has EXACTLY `{ uri, customData }`. Every downstream consumer
was then hot-patching the invalid field back out. This commit fixes
the teaching at source and removes the hot-patches.

Root-cause fixes:

- core/verify-standards.ts — the verifier was telling callers
  "Set metadata.image to <BITBADGES_DEFAULT_IMAGE>" as a fix when the
  image was missing. That's invalid proto. Rewritten to check
  metadata.uri and instruct callers to set a placeholder URI and put
  the image inside the off-chain JSON at that URI. Added a new
  explicit error case when `metadata.image` appears at all, with the
  clear message "PathMetadata only has { uri, customData }". New
  helper `isFakeOrMissingUri` that knows `ipfs://METADATA_*`
  placeholders are valid.

- core/review-ux/metadata.ts — the `review.ux.alias_paths_missing_images`
  check was reading `pathMeta?.image`, `unitMeta?.metadata?.image`,
  `_metadataForUpload?.image` — i.e. every wrong location we'd ever
  stuffed an image into. Now it reads ONLY the metadataPlaceholders
  sidecar keyed by path/unit metadata.uri, which is where images are
  supposed to live. Recommendation text rewritten.

- builder/resources/skillInstructions.ts — removed 6 sites where the
  skill prompt taught LLMs to put `image` on alias path / denomUnit /
  wrapper path metadata. Replaced with explicit guidance that
  PathMetadata is `{ uri, customData }` only and that images belong
  in the off-chain JSON registered via metadataPlaceholders. Example
  JSON blobs in the skill docs now show the placeholder URI pattern.

- builder/tools/session/addAliasPath.ts +
  builder/tools/session/addCosmosWrapperPath.ts — these had zod
  schemas + JSON-schema inputSchemas that declared
  `metadata.image` REQUIRED, AND runtime handlers that propagated
  `pathImage` onto `unit.metadata.image`. Both rewritten: the
  schemas drop `image`, the handlers strip any incoming `image`
  field and fill a placeholder URI (`ipfs://METADATA_ALIAS_<denom>` /
  `ipfs://METADATA_WRAPPER_<denom>`) when one isn't provided. Tool
  descriptions updated to point callers at the off-chain JSON flow.

Indexer side (committed separately on bitbadges-indexer):

- routes/ai-builder/core/promptBuilder.ts — the export-mode prompt
  Rules block was ambiguous about whether image/name/description go
  on the on-chain metadata proto or in the metadataPlaceholders
  sidecar. Rewritten to be explicit: on-chain metadata is ONLY
  `{ uri, customData }`, everything else lives in the sidecar.

New templates.ts feature — "Metadata To Upload" section:

Templates walk their emitted tx body and surface every placeholder
URI (`ipfs://METADATA_*`) in a new stderr section alongside the
auto-validate / auto-review output. For each placeholder:
- the URI
- what it's for (Collection / Token / Approval / Alias path /
  Denom unit / Wrapper path)
- which field on the proto references it
- which fields the off-chain JSON needs (name/description/image)
- a PROVIDED / NEEDED badge keyed off `data._meta.metadataPlaceholders`

Gives CLI users a concrete TODO list: here's what you still have to
upload before broadcast. Honoring the "build + review = static"
philosophy, the section is informational only — no network calls.

Verified:
  - `npm run build` clean (589 files, madge pass)
  - 12/12 templates still produce 0 structural errors after the
    skillInstructions + session-tool rewrites
  - `builder templates vault --backing-coin USDC` shows two
    NEEDED placeholders (`ipfs://METADATA_ALIAS_uvault` and
    `ipfs://METADATA_ALIAS_uvault_UNIT`) with the expected field list

Known follow-up: the `metadataPlaceholders()` / `singleTokenMetadata()`
helpers in shared.ts still use `DEFAULT_METADATA_URI` and stuff
name/description/image into customData as JSON. That's the old
shape. A follow-up commit will rewire them to emit
`ipfs://METADATA_COLLECTION` / `ipfs://METADATA_TOKEN_<range>`
placeholders so collection/token metadata also surfaces in the
Metadata To Upload section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
custom-2fa permission shape (Gap B)

Two compatibility gaps surfaced by the indexer/frontend audit:

Gap A (moderate) — Silent metadata loss in alias/wrapper session tools
─────────────────────────────────────────────────────────────────────
The earlier metadata.image purge (14bc772) made addAliasPath /
addCosmosWrapperPath strip any inbound `image` field from PathMetadata.
Correct on the proto side, but the strip went into the void: there was
no way for an LLM (or any other caller) to attach an image to an alias
path's placeholder URI, so images were lost silently.

This commit adds first-class top-level params to both tools:

  addAliasPath / addCosmosWrapperPath now accept:
    pathName, pathDescription, pathImage
    denomUnitName, denomUnitDescription, denomUnitImage

The handlers route those (plus any legacy fields the caller stuffed
nested-on-metadata for backwards compat) into the session's
metadataPlaceholders sidecar keyed by the path's auto-generated
ipfs://METADATA_ALIAS_<denom> / ipfs://METADATA_WRAPPER_<denom>
placeholder URI. Same routing pattern as setCollectionMetadata —
content into the sidecar, only { uri, customData } onto the proto.

This restores the AI builder flow's ability to attach images to alias
paths without touching the on-chain proto. The Metadata To Upload
section in templates auto-review will now render those entries as
PROVIDED instead of NEEDED when the caller passes the new params.

Gap B (low) — custom-2fa template still using bare permission helper
─────────────────────────────────────────────────────────────────────
`core/builders/custom-2fa.ts` was using the bare alwaysLockedPermission()
for canUpdateValidTokenIds, which the validator now rejects (needs
tokenIds field per the cf8c379 fix). Switched to
alwaysLockedTokenIdsPermission(). custom-2fa was the only template still
hand-rolling permissions outside of emptyPermissions/frozenPermissions.

Verified:
  - npm run build clean (589 files, madge pass)
  - All 12 templates still produce 0 structural errors
  - frontend tsc --noEmit clean against the linked SDK

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tection

Four related false-positive fixes driven by the vault form sidebar:

1. Unbacking approval detection: previously gated on
   fromListId.startsWith('!') || fromListId === 'All'. The vault form
   emits withdrawal approvals with fromListId: 'AllWithoutMint', which
   failed both checks. Switch to structural matching using the canonical
   backing address from invariants.cosmosCoinBackedPath.address —
   fromListId/toListId that contains or equals the backing address is
   the unambiguous signal. Also recognize vault-deposit / vault-withdraw
   approval ID prefixes as explicit hints.

2. Soulbound detection: previously excluded backed-minting approvals
   from the transfer-path count, so any smart-token collection that
   used backing/unbacking for its transfer flow got flagged as
   soulbound. Now backed-minting approvals count as transfer paths —
   users CAN move tokens out via the backing address, so they are not
   soulbound.

3. Metadata URI placeholder violations: verify-standards flagged
   aliasPathsToAdd[*].metadata.uri as "missing or fake placeholder"
   when the vault form was in pre-apply state. The frontend stores
   alias path metadata in the WithDetails shape —
   { uri: '', customData: '', metadata: { name, image, description } } —
   with the uri left blank until the metadata auto-apply flow uploads
   the inline content to IPFS on submit. Added isPreApplyMetadata()
   detection that suppresses the uri check when a nested metadata.metadata
   object with inline name/image/description exists. Same fix for the
   denomUnits[*].metadata.uri check.

4. review.ux.alias_paths_missing_images: the existing check only looked
   at metadataPlaceholders sidecar entries. Now also looks at the
   WithDetails nested inline image (pathMeta.metadata.image /
   unitMeta.metadata.image) so frontend pre-apply state with an inline
   image is recognized as "has image".

Tests: 1144/1144 core green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mint noise

Swap the recursive stringify/bigintify hack for the proper class-based
pipeline the SDK already has:

  new MsgUniversalUpdateCollection(value).convert(BigIntify)

The class knows which fields are numeric via each nested class's
`getNumberFieldNames()` and only converts those — addresses, URIs,
denoms, IDs, approvalIds stay as strings. No more false conversions of
any-string-that-looks-numeric.

normalizeForReview() moved to its own file (`core/review-normalize.ts`)
so `auditCollection`, `verifyStandardsCompliance`, and `reviewCollection`
can all call it without a circular dependency. The function is
idempotent — running it twice is a no-op, so pipelines that stack
(reviewCollection → verifyStandardsCompliance) work naturally.

Downstream checks now assume bigint numeric fields. Rewrote ~35 strict
string comparisons in audit.ts / verify-standards.ts / review-ux/*.ts
to use bigint literals (`=== 1n`, `!== 0n`, `=== MAX_UINT64` where
`MAX_UINT64 = 18446744073709551615n`). Removed every `String(x) === '0'`
defensive wrapper — no longer needed.

Also:
- Removed the "mint approval is PUBLIC" audit finding. Approvals are
  typically gated by claim plugins, merkle challenges, or out-of-band
  allowlists; flagging All+All was noise on every public-mint template.
- Collection metadata URI placeholder check now detects the frontend
  WithDetails shape (nested metadata.metadata with inline
  name/image/description) and suppresses the "URI is placeholder"
  warning when the auto-apply flow will populate the URI on submit.

Tests: 1144/1144 core green. Review spec updated to pass bigint
ranges since runUxChecks callers bypass reviewCollection's upfront
normalization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Msg class .convert(BigIntify) pipeline is clean for numeric
fields but the nested proto classes (PathMetadata, CollectionMetadata,
CollectionApproval) only keep proto-spec fields. The frontend stores
WithDetails inline content on those objects:

  aliasPathsToAdd[i].metadata.metadata.{name,image,description}
  collectionMetadata.metadata.{name,image,description}
  collectionApprovals[i].details.{name,description,image}

These are dropped when we wrap in MsgUniversalUpdateCollection to
get numeric conversion, so review checks that read them false-fire
with "URI is missing or placeholder" / "alias paths missing display
images" / "approval rules are missing names" on the frontend vault
and subscription forms, which are exactly in that pre-apply state.

normalizeForReview now walks the raw (pre-conversion) input and
reattaches those dropped fields onto the converted result. The
numeric conversion still runs; only the WithDetails sidecar fields
are preserved. Non-mutating to the input; mutates the converted
output in place.

Also: branch the `review.ux.forceful_transfers_allowed` message on
whether the finding is firing because of forceful override approvals
(count > 0) or because the noForcefulPostMintTransfers invariant is
unset (count = 0). Previously both cases produced "Currently 0
non-mint approval(s) have forceful transfer overrides enabled..."
which reads as nonsense when count is 0.

Tests: 1144/1144 core green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ddress list transfers

Four related review-finding adjustments:

1. Drop `review.ux.addresslist_transfers_allowed` entirely. Address list
   tokens can legitimately have transfer approvals — the check was
   flagging every such collection as a warning with no real signal.

2. Downgrade `review.ux.forceful_transfers_allowed` from critical to
   warning and rewrite the message. Forceful transfer overrides are
   legitimate for many standard flows (subscription revoke, auction
   seller-forced settlement, prediction market resolution, credit
   tokens, etc.) — firing as critical on every collection that uses
   them produced unactionable noise. The finding now lists the
   specific approval IDs so the user can verify each one, and the
   wording reframes it as "verify these are intentional" rather than
   "this is dangerous."

3. Move quest/reward-escrow funding check from verify-standards
   (critical) to review-ux (warning). The user can fund mint escrow
   at any time post-creation by sending coins to the escrow address,
   so blocking a build on unfunded escrow is wrong. The new check
   also applies to ANY mint approval that pays out coins (not just
   Quest) — Subscription, Crowdfund, Auction, Bounty, etc. share the
   same flow. It cross-references required denoms from coinTransfers
   against mintEscrowCoinsToTransfer to report exactly which denoms
   still need funding.

4. Reword `review.ux.amount_scaling_with_approver_funds` detail. The
   old copy said "expected for prediction markets and credit tokens
   but dangerous for bids or offers" which read as judgemental — the
   new copy asks the user to verify the per-use amount under scaling
   makes sense for their specific use case.

Tests: 1143/1143 core green (dropped one obsolete address-list test).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…noise

Two noisy findings removed after debug investigation:

1. `review.ux.mint_escrow_unfunded`: the check read
   `value.mintEscrowCoinsToTransfer` from the collection WIP state to
   decide whether the escrow was funded. But the frontend never
   populates that field on the WIP — `withCoinsToEscrow` is tracked
   separately in TxTimelineContext and only attached to the Msg at the
   final broadcast step (CreateTxMsgUniversalUpdateCollection.tsx:106).
   That meant the warning fired on every quest / subscription /
   crowdfund / auction form regardless of what the user actually set.
   Combined with the fact that mint escrow can be topped up at any
   time post-creation, this was pure noise — removed.

2. `review.audit.directed transfer channel`: the audit info finding
   fired on any approval with specific from/to lists and "All"
   initiator. The message ("Allows transfers from X to Y, initiated
   by anyone. Verify this directed channel is intentional.") restated
   what the approval definition already says without adding signal.
   Fired on every smart-token backing/unbacking pair and similar
   patterns. Removed.

Tests: 1143/1143 core green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…backing supply

Four related changes to the permission / forceful analysis:

1. Forceful check outcomes cleaned up into four distinct cases:
     hasOverrides + !invariantBlocksForceful  → warning (verify intent)
     hasOverrides + invariantBlocksForceful   → critical (on-chain fail)
     !hasOverrides + !invariantBlocksForceful → info (future could enable)
     !hasOverrides + invariantBlocksForceful  → silent (locked)
   Previously the warning and mismatch would double-fire because both
   conditions matched simultaneously when the invariant was set. Now
   they are mutually exclusive and the mismatch message gets a fuller
   explanation + lists the specific approval IDs causing the conflict.

2. The "future approvals could enable forceful" info finding now
   detects when canUpdateCollectionApprovals is permanently forbidden
   with a blanket All/All/All scope and appends a note that the
   finding is redundant in that case (no future approvals can be
   added anyway).

3. New critical audit finding:
   `review.audit.transferability.post_mint_transfer_approvals_can_be_modified`
   Mirrors the existing "Mint approvals can be modified - UNLIMITED
   SUPPLY RISK" finding but for the transferability side — fires when
   canUpdateCollectionApprovals is permitted/neutral without a
   blanket lock AND the collection has post-mint transfer approvals.

4. New critical audit finding:
   `review.audit.supply.backing_approvals_can_be_modified`
   Same supply-risk concern as mutable mint approvals, but for
   backing approvals (allowBackedMinting: true). Backing approvals
   are the smart-token mint source; fromListId is the backing
   address, not "Mint", so the existing mint check missed them.
   Now covered explicitly with its own supply-risk finding.

Tests: 1143/1143 core green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "blanket permanent lock on canUpdateCollectionApprovals" detection
previously only matched on from/to/initiatedBy list IDs, missing the
other five scope fields. A CollectionApprovalPermission has 8 scope
fields total:

  fromListId          === 'All'
  toListId            === 'All'
  initiatedByListId   === 'All'
  transferTimes       covers [1 → MAX_UINT]
  tokenIds            covers [1 → MAX_UINT]
  approvalId          === 'All'
  amountTrackerId     === 'All'
  challengeTrackerId  === 'All'
  (plus permanentlyForbiddenTimes covers forever)

A partial match (e.g. the three list IDs set to All but tokenIds
scoped to a single range) does NOT permanently lock transferability —
it only freezes a subset. New `isCollectionApprovalBlanketLocked`
helper in review-ux/shared.ts walks all 8 fields + the time range;
`isTransferabilityPermanentlyLocked` is the convenience wrapper.
audit.ts has an equivalent inline check for parity.

Message text cleanup:

- Drop "(All/All/All forbidden)" from the forceful_transfers_not_locked
  suffix — the "permanently locked" phrasing is sufficient and the
  parenthetical was misleading given only 3 of 8 fields were checked.
- Drop "Lock canUpdateCollectionApprovals with a blanket 'All/All/All'
  permanently-forbidden scope" from the post-mint transferability
  recommendation. Now reads "Permanently lock canUpdateCollectionApprovals".
- Drop "including introducing forceful transfer approvals that move
  tokens without holder consent" from the post-mint transferability
  detail — it was conflating two separate concerns.

Severity upgrade:

- `review.ux.forceful_transfers_not_locked` upgraded from `info` to
  `warning` when case B fires (no overrides now, but invariant could
  never be added later). This is a real trust assumption worthy of
  more than an info. Stays at `info` in the special case where
  transferability is already permanently locked, since no future
  approval can be added anyway.

Tests: 1143/1143 core green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… false

The case B finding (no current forceful approvals + invariant does not
block them) previously said "The noForcefulPostMintTransfers invariant
is not set" — which is a lie when the form explicitly set the invariant
to `false` (it IS set, just to false). Now distinguishes the two:

  explicit false: "This collection has noForcefulPostMintTransfers
                   set to false, which permits forceful post-mint
                   transfers."
  unset:          "The noForcefulPostMintTransfers invariant is not
                   set, which by default permits forceful post-mint
                   transfers."

Title rephrased from "Forceful transfers not locked at creation" to
"Forceful transfers permitted post-mint" — the old title implied the
user forgot to lock something, when the unlocked state is often
intentional (vault, subscription, etc.).

Recommendation text also reframed so "this is intentional for your
design" is a first-class option, not an afterthought.

No logic change — the 4-case outcome table stays:
  invariant=true  + no overrides   → silent
  invariant=true  + has overrides  → critical (mismatch)
  invariant=false + no overrides   → warning (this finding)
  invariant=false + has overrides  → warning (verify each intentional)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The four forceful-transfer outcomes are now:

  hasOverrides + !invariantBlocks   → critical (was warning)
  hasOverrides + invariantBlocks    → critical (unchanged — on-chain fail)
  !hasOverrides + !invariantBlocks  → critical (was warning)
                                      except when transferability is
                                      permanently locked via a blanket
                                      canUpdateCollectionApprovals
                                      forbidden entry → info
  !hasOverrides + invariantBlocks   → silent (unchanged — safe state)

Rationale: forceful transfers are an immutable decision at creation
time. The invariant can't be toggled later, and existing approvals
with override flags can only be removed via another update. Holders
depend on the current state being stable, so any non-safe case is a
critical sign-off point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ctions

BitBadges has two minting approaches — standard mint (fromListId:
"Mint" approvals) and 1:1 IBC backing (allowBackedMinting approvals
at the backing address with cosmosCoinBackedPath set). Many review
findings were phrased assuming the standard-mint flow, which read
oddly for smart-token collections where tokens enter circulation
via IBC deposits.

Key terminology swaps in review item text:
  minting               → creating new tokens / token creation
  mint approvals        → token-creation approvals
  post-mint             → once in circulation / after creation
  minted tokens         → newly created tokens

Specific findings updated:

  audit.ts
    - "No post-mint transfer approval found (soulbound)" retitled
      "Tokens are non-transferable / soulbound". Detail no longer
      says "soulbound after minting" — now "cannot leave their
      initial holder once they are in circulation".
    - "Missing autoApproveAllIncomingTransfers for mint collection"
      retitled "Recipients cannot receive newly created tokens".
    - "Mint approval X has NO supply limits" — detail now says
      "create unlimited new tokens" instead of "mint unlimited".
      Title keeps "Mint approval" since it literally references
      fromListId: "Mint".
    - "Mint approvals can be modified — UNLIMITED SUPPLY RISK"
      detail rewrites "mint approvals" → "token-creation (Mint)
      approvals" and "mint limits" → "creation limits".
    - "Backing approvals can be modified" detail drops the awkward
      "smart-token mint source" phrasing.
    - "Token ID creation is not locked" recommendation drops
      "locked mint approvals" in favor of "locked token-creation
      approvals".

  review-ux/approvals.ts
    - all_includes_mint detail: "allows minting new tokens" →
      "allows new tokens to be created from the Mint address";
      "post-mint transfer approvals" → "approvals that should
      only govern tokens already in circulation".
    - auto_approve_disabled_on_mintable detail: "mint approvals"
      → "approvals with fromListId: Mint"; "minted tokens" →
      "created tokens"; "minting will silently fail" →
      "creation flow will silently fail".
    - forceful_transfers_not_locked title: "Forceful transfers
      permitted post-mint" → "Tokens can be moved without holder
      consent".

  review-ux/skills.ts
    - no_mint_approvals finding retitled "No token-creation
      approvals configured". Detail explicitly notes smart tokens
      using IBC backing or wrapper paths are excluded.

  verify-standards.ts
    - common mint-approval autoApprove check: "mint approvals"
      → "token-creation (Mint) approvals"; "minted tokens" →
      "newly created tokens".

Other mint-centric text kept unchanged where the finding is
literally about the "Mint" list ID (e.g. approvals that require
overridesFromOutgoingApprovals because they send from Mint) —
the technical term is accurate and a plain-English rewrite would
lose the field-level specificity a developer needs.

Tests: 1143/1143 core green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…h branch

Case B of the forceful check (no current override approvals + invariant
does not block them) previously used the title "Tokens can be moved
without holder consent". That's accurate for case A (real overrides
present), but for case B where the count is 0, no tokens can currently
be moved forcefully — the finding is about future risk, not current
state.

Retitled to "Forceful transfers are not permanently blocked" which is
forward-looking and matches the detail text (which talks about "any
approval the manager adds later could introduce forceful transfers").

Case A keeps "Forceful transfer overrides enabled" since real approvals
with override flags exist there.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…fund templates

Mirror the frontend security cleanup (b38538d0c) in the SDK's own
template builders and their protocol validator.

Changes match the same rules the frontend registries now follow:

  * Mint approvals keep `overridesFromOutgoingApprovals: true` (required
    by the token standard) but drop `overridesToIncomingApprovals: true`
    (redundant — recipients auto-approve via defaultBalances).
  * Non-mint approvals (settlement / redeem / refund / burn) drop BOTH
    override flags. The holder self-initiates their own burn and
    auto-approves via `autoApproveSelfInitiatedOutgoingTransfers: true`
    on defaultBalances. Previously the outgoing override let any third
    party initiate the burn AND redirect the payout via
    `overrideToWithInitiator: true` on the coinTransfer — a theft
    vulnerability on pm-redeem, pm-settle-*, and crowdfund-refund,
    plus a griefing vector on auction-burn and crowdfund-burn.
  * No `requireFromEqualsInitiatedBy: true` added — the default behavior
    is correct (self-initiated self-burns auto-approve; third parties
    have no path unless the holder grants an explicit user-level
    outgoing approval). Leaves flows open-ended.

Files:

  core/builders/crowdfund.ts
    - deposit-refund (Mint):   incoming override → false
    - deposit-progress (Mint): incoming override → false
    - refund (!Mint):          both overrides → false
    - invariant noForcefulPostMintTransfers → true

  core/builders/auction.ts
    - mint-to-winner (Mint):   already had incoming: false, no change
    - auction-burn (!Mint):    already had no approvalCriteria (no overrides)
    - invariant noForcefulPostMintTransfers → true

  core/builders/prediction-market.ts
    - pairedMint (Mint):       incoming override → false
    - preRedeem (!Mint):       both overrides → false; requireFromEqualsInitiatedBy
                                removed (leave open-ended)
    - settlementApproval (!Mint): both overrides → false (4 variants:
                                  settle-yes / settle-no / settle-push-yes /
                                  settle-push-no)
    - invariant noForcefulPostMintTransfers → true

  core/crowdfunds.ts (protocol validator)
    - Removed the requirement that deposit-refund and deposit-progress
      approvals set `overridesToIncomingApprovals: true`. Comment
      explains the rationale — the collection-level incoming override
      is redundant with defaultBalances auto-approve and no longer
      required by the protocol.

Tests: 1143/1143 core green including builders.spec.ts which exercises
every template through verifyStandardsCompliance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…a (single source of truth)

Collapse the placeholder sidecar onto a single canonical location:
`tx.messages[i].value._meta.metadataPlaceholders`. Every producer
writes there, every consumer reads from there, no fallback paths,
no top-level copy anywhere.

Prior state carried the sidecar in two parallel shapes (top-level
`tx.metadataPlaceholders` for the AI builder flow, per-msg
`msg._meta.metadataPlaceholders` for CLI templates). Both had to be
kept in sync at every producer, and every consumer had to know which
one to check first. The split caused image auto-apply bugs on multi-
hop data flows where the shapes drifted.

SDK changes:
- buildMsg() now writes `msg.value._meta.metadataPlaceholders`
  instead of `msg._meta.metadataPlaceholders` so the sidecar is
  scoped inside the value (same scope as other per-msg _meta fields
  on intent/bid/listing/recurring-payment/pm-*-intent builders).
- review-normalize.ts `reattachInlineMetadata` now preserves `_meta`
  through the MsgUniversalUpdateCollection proto-class round-trip
  (proto classes strip unknown fields, so the review pipeline would
  otherwise drop `_meta` between normalize and check).
- review-ux/metadata.ts three checks (placeholder_images,
  unnamed_approvals, alias_paths_missing_images) now read from
  `value?._meta?.metadataPlaceholders`. Fixtures in review.spec.ts
  updated to match.
- sessionState.ts drops the top-level `metadataPlaceholders` field
  from the SessionTransaction interface and every writer. New
  `getMsgPlaceholders(session)` helper lazily creates
  `messages[0].value._meta.metadataPlaceholders` on first use; all
  four writer sites (setCollectionMetadata, setTokenMetadata,
  addApproval, setApprovalMetadata) route through it. export/import
  no longer carry a sibling field — the sidecar rides inside
  `messages` automatically.
- tools/session/addAliasPath + addCosmosWrapperPath rewired through
  getMsgPlaceholders.
- CLI templates.ts writes `data.value._meta.metadataPlaceholders`
  and renders from the same location. CLI builder.ts `preview`
  upload stops lifting to top-level — just POSTs the tx as-is.
  doctor-probe fixture moved onto per-msg shape.
- Prompt resources (masterPrompt.ts, examplesDocs.ts) rewritten to
  show placeholder sidecars inside `messages[0].value._meta` in
  every example JSON block.

Also landed in this commit (from the same working session, all
interconnected with the review + update-flow work):
- Auction post-settlement validator: treat missing mint-to-winner
  approval as a valid post-settlement state (autoDeletionOptions.
  afterOneUse removes it after first use) instead of a hard error.
- Auction builder now mirrors custom-2fa single-NFT pattern — token
  metadata inherits collection metadata automatically via
  METADATA_TOKEN_DEFAULT with tokenIds narrowed to [1,1].
- buildAliasPath returns {path, placeholders} so smart-account,
  vault, credit-token, prediction-market all emit per-msg sidecars
  for their alias path metadata URIs. Previous behavior silently
  dropped the image arg.
- verify-standards.ts takes optional onChainCollection to overlay
  create-only fields (defaultBalances, invariants) on update txs so
  verifyCommonMintRules doesn't false-positive on update flows.
  isUpdateTransaction() short-circuit for when onChainCollection is
  absent. review.ts threads ctx.onChainCollection through.
- Security fixes for auction/PM/crowdfund override flags on non-Mint
  approvals (now rely on defaultBalances auto-approve + self-
  initiated outgoing defaults).
- KNOWN_SYSTEM_APPROVAL_PREFIXES for unnamed_approvals check — AI
  builder uses `smart-token-backing_<hex>` style IDs that the old
  fixed ID list missed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…owdfund skill text

Port the skill-doc generator from bitbadges-builder-mcp/scripts/gen-skill-docs.ts
to bitbadgesjs-sdk/scripts/gen-skill-docs.ts with the new import path
(src/builder/resources/skillInstructions.ts). Output destination unchanged
(../bitbadges-docs/x-tokenization/examples/skills).

Also tighten the auction / PM / crowdfund skill summary text to reflect the
override-flag drops on non-mint approvals that landed earlier this session:
- Auction: noForcefulPostMintTransfers locked in invariants, burn approval
  has no approvalCriteria at all (relies on defaultBalances + burn destination),
  post-settlement auto-deletion of mint-to-winner is documented as a valid
  state per the validator fix.
- PM: noForcefulPostMintTransfers documented, non-mint approvals
  (redeem, settlement, transferable) must not set override flags.
- Crowdfund: noForcefulPostMintTransfers documented, refund approval
  must not set override flags. Deposit/success/progress Mint-side approvals
  keep overridesFromOutgoingApprovals: true as the chain requires, with
  overridesToIncomingApprovals: false.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@trevormil trevormil merged commit 4419357 into main Apr 14, 2026
4 checks passed
@trevormil trevormil deleted the feat/fold-mcp-into-sdk branch April 14, 2026 18:58
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