Skip to content

feat: add promoted template edit contracts - #3407

Merged
jrusso1020 merged 2 commits into
mainfrom
fix/template-editor-contracts
Aug 22, 2026
Merged

feat: add promoted template edit contracts#3407
jrusso1020 merged 2 commits into
mainfrom
fix/template-editor-contracts

Conversation

@jrusso1020

Copy link
Copy Markdown
Collaborator

Summary

  • add TEMPLATE.md ownership/editability contracts to all nine promoted templates
  • backfill declared variables for ChatGPT, Claude, HeyGen promo, and Slack templates
  • enforce contract presence and variables in registry tests
  • keep ChatGPT, Claude, Slack, and iOS host chrome protected from subject-brand restyling

Validation

  • bun test packages/cli/src/registry/registryBlocks.test.ts
  • CLI typecheck and formatting
  • all nine templates passed static lint and browser runtime/layout/contrast checks
  • all nine passed declared-edit vs host-mutation contract evaluation

Notes

  • the unrelated webm-transparency compiled artifact was explicitly excluded

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head c25c2088f4fa8e3f0aadbbfd7d3c449267f9829e.

The ownership split is coherent across all nine promoted templates: editable content/brand slots are represented by root data-composition-variables, while each TEMPLATE.md explicitly protects host chrome, typography, palette, geometry, motion, and timing. ChatGPT, Claude, Slack/iOS, notes, messages, and share-sheet contracts correctly treat the referenced website as subject content rather than owner of the surrounding product UI. The four templates that needed declarations now read those defaults through the runtime variable API without moving structural or motion code.

Registry coverage pins one packaged TEMPLATE.md, one composition, and a non-empty root declaration for every promoted identity; the full lint/runtime/catalog/Windows/regression suite is green.

Rollout note: this approval covers source head c25. The internal promotion PR must update its source pin to the final merged HyperFrames SHA before publishing artifacts.

Verdict: APPROVE
Reasoning: The source contracts draw the requested ownership boundary clearly, variable wiring stays inside declared content/brand slots, protected host identity remains byte-owned by the templates, and exact-head CI is fully green.

— Magi

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strengths

  • TEMPLATE.md contracts are added for all 9 promoted templates with a consistent Surface ownership / Editable slots / Protected structure, and registryBlocks.test.ts:32-55 enforces presence of exactly one contract file plus at least one non-empty data-composition-variables declaration per template.
  • The data-composition-variables JSON on <html> is the actual machine-readable enforcement surface (checked downstream by hyperframes-internal#624 and experiment-framework#47354), not just prose — good separation between human doc and machine contract.

Important

  • packages/cli/src/registry/registryBlocks.test.ts:13-23 — the set of "promoted" templates is a hand-maintained array (promotedTemplateIds) with no link to the registry metadata (no promoted: true flag or similar in registry-item.json, confirmed by grepping tags on ai-chat-reveal/registry-item.json, which only has free-form tags). This list has to stay in lockstep with the independent list hyperframes-internal's promote-oss-templates.ts promotes. If a template is added to the internal promotion list but someone forgets to add its id here, this suite silently stops enforcing the contract on it — no CI signal on either side. Recommend deriving "promoted" from a field on registry-item.json (single source of truth) instead of two independently maintained lists in two repos.
  • chatgpt-exchange.html / claude-exchange.html declare search/thinking/prompt etc. as editable string content variables, but the values flow into el(cls, html, tag)n.innerHTML = html sinks (e.g. mkTool building `<div>${label}</div>` from CONFIG.search/CONFIG.thinking). That means an "editable content" value can structurally alter the DOM (tag injection) unless something else blocks it. I verified experiment-framework#47354's gate (template_editor_gate.py:534-535) hard-rejects any string default containing </>, which closes this specific vector — but that protection lives entirely in EF, one repo away from where the sink is. Worth a code comment in the two .html files noting these are innerHTML sinks and depend on the delivery gate's </> ban, so a future template author doesn't add a similar sink without that same protection in mind.

Notes

  • Contract markdown isn't cross-validated against the actual variable list (test only checks 3 heading strings are present + declarations.length > 0), so a TEMPLATE.md could claim a slot is protected/editable inconsistent with what data-composition-variables actually declares. Low risk since the real enforcement point is the HTML declaration, not the prose, but the prose is what the editor-agent's system prompt is told to read first.
  • CI is green on all required checks; mergeStateStatus: BLOCKED is purely REVIEW_REQUIRED, not a failing check.

Verdict: APPROVE
Reasoning: Core mechanism (declared-variable contract) is sound and tested; findings above are process/robustness improvements, not correctness bugs in this diff.

— Vai

@jrusso1020

Copy link
Copy Markdown
Collaborator Author

Addressed the review hardening on head e22d6e3. Contract coverage is now derived from the existing ad-template registry tag instead of a handwritten ID list, so newly promoted templates automatically enter the gate. The ChatGPT and Claude innerHTML helpers now document their dependency on the downstream angle-bracket rejection. Focused registry tests, formatting, lint, typecheck, and pre-commit checks pass.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed exact head e22d6e35013a4906e540bd2982c51e3d80fbd1cc.

The prior process gap is closed: contract coverage now derives the promoted roster from the shared ad-template registry tag instead of a second hand-maintained ID list. That is the same metadata the internal promotion layer now validates. The innerHTML sinks in ChatGPT/Claude are explicitly documented beside the code and remain protected by the downstream angle-bracket gate.

The ownership contracts and variable wiring are otherwise unchanged from the approved head. All 8 required contexts are green at this exact SHA.

Verdict: APPROVE
Reasoning: The source ownership boundary remains sound, and promoted-template coverage is now single-sourced from registry metadata rather than a duplicated roster.

— Magi

@jrusso1020
jrusso1020 merged commit dac8f9f into main Aug 22, 2026
58 checks passed
@jrusso1020
jrusso1020 deleted the fix/template-editor-contracts branch August 22, 2026 01:39

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE at e22d6e35 — independent adversarial pass.

Scope of this PR (narrow reading): adds a TEMPLATE.md ownership contract to 9 promoted templates, wires data-composition-variables + window.__hyperframes.getVariables() into 4 templates (chatgpt-exchange, claude-exchange, heygen-avatar-promo-card, slack-notification-ad), and adds a registry test that requires (a) an ad-template-tagged block, (b) a TEMPLATE.md file entry, and (c) data-composition-variables.length > 0 on <html>. The catalog-parity / main-reachable pin gate / export terminal-status fix claims from the stack summary live in siblings #624 (HFI) and #47354 (EF), not here.

No blockers. Non-blocking notes for follow-ups:

  • P2 (defense-in-depth): XSS surface expansion. chatgpt-exchange.html, claude-exchange.html, and slack-notification-ad.html feed variable content into n.innerHTML via the new content() helper. Before this PR, CONFIG was author-baked at bundle time; after this PR, values arrive from variables[id], expanding the injection surface from author-only to any producer that can set the variables map. Safety now rests on an off-PR delivery gate ("rejects angle brackets") that is not exercised in this PR's tests. heygen-avatar-promo-card correctly uses textContent — worth normalizing the other three the same way, or adding an in-template escape.
  • nit: presence-only registry-test assertion. expect(declarations.length).toBeGreaterThan(0) passes for [{}] — no shape assertion on id/type/role/label/default. Mutation-escapable; a schema check on each declaration would harden the contract runtime code relies on.
  • nit: tag-only contract filter. manifest.tags?.includes('ad-template') is the sole gate. A partial drift (a promoted template loses the tag) exits the check silently; the outer promotedManifests.length > 0 only guards against ALL templates losing it. Consider a "TEMPLATE.md present ⇔ ad-template tag present" cross-check.
  • nit: helper-name drift. chatgpt/claude/slack use content(); heygen-avatar-promo-card uses text() + a trivial color() alias. Not a bug, but future authors copy-pasting between templates will fork on the name.
  • nit: empty-string coerces to fallback. variables[id].trim() ? variables[id] : fallback prevents a producer from legitimately blanking a slot. Probably intentional for length-lock reasons — worth documenting in TEMPLATE.md.

Review by Via

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE at e22d6e35 — independent adversarial pass.

Scope of this PR (narrow reading): adds a TEMPLATE.md ownership contract to 9 promoted templates, wires data-composition-variables + window.__hyperframes.getVariables() into 4 templates (chatgpt-exchange, claude-exchange, heygen-avatar-promo-card, slack-notification-ad), and adds a registry test that requires (a) an ad-template-tagged block, (b) a TEMPLATE.md file entry, and (c) data-composition-variables.length > 0 on <html>. The catalog-parity / main-reachable pin gate / export terminal-status fix claims from the stack summary live in siblings #624 (HFI) and #47354 (EF), not here.

No blockers. Non-blocking notes for follow-ups:

  • P2 (defense-in-depth): XSS surface expansion. chatgpt-exchange.html, claude-exchange.html, and slack-notification-ad.html feed variable content into n.innerHTML via the new content() helper. Before this PR, CONFIG was author-baked at bundle time; after this PR, values arrive from variables[id], expanding the injection surface from author-only to any producer that can set the variables map. Safety now rests on an off-PR delivery gate ("rejects angle brackets") that is not exercised in this PR's tests. heygen-avatar-promo-card correctly uses textContent — worth normalizing the other three the same way, or adding an in-template escape.
  • nit: presence-only registry-test assertion. expect(declarations.length).toBeGreaterThan(0) passes for [{}] — no shape assertion on id/type/role/label/default. Mutation-escapable; a schema check on each declaration would harden the contract runtime code relies on.
  • nit: tag-only contract filter. manifest.tags?.includes('ad-template') is the sole gate. A partial drift (a promoted template loses the tag) exits the check silently; the outer promotedManifests.length > 0 only guards against ALL templates losing it. Consider a "TEMPLATE.md present ⇔ ad-template tag present" cross-check.
  • nit: helper-name drift. chatgpt/claude/slack use content(); heygen-avatar-promo-card uses text() + a trivial color() alias. Not a bug, but future authors copy-pasting between templates will fork on the name.
  • nit: empty-string coerces to fallback. variables[id].trim() ? variables[id] : fallback prevents a producer from legitimately blanking a slot. Probably intentional for length-lock reasons — worth documenting in TEMPLATE.md.

Review by Via

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.

3 participants