** This file was created with an agent. **
Status: Historical retrospective. Written 2026-02-25 during the (now merged) implementation of the
VideoproofContextualActor, and lightly updated afterwards to keep file/line references current. The friction points and recommendations below are still open at the time of filing underdocs/planning/.
This analysis was written during the implementation of the VideoproofContextualActor
for the TypeRoof shell. An AI agent (goose) successfully created the actor model,
renderer, and registration — with iterative fixes — touching only 3 files and adding
~370 lines of new code. This document examines why the architecture made that feasible
and where friction remains.
TypeRoof’s architecture is built on a small number of powerful, composable patterns. These patterns are highly regular — once an agent understands one actor, it can create another by following the same structural template. The metamodel system provides type-driven automation that eliminates boilerplate UI code. Together, these properties make the codebase unusually navigable for an AI agent.
The main friction points are: implicit registration contracts spread across multiple files, CSS class conventions that are undocumented, and business logic that must be manually ported from legacy functional code into the actor pattern.
Every actor in TypeRoof follows the same structural template:
Model = _BaseActorModel.createClass(name, ...mixins, ['field', FieldType], ...)
Renderer extends _BaseComponent { constructor, initTemplate, update }
This regularity means:
videoproof-array.mjs gave the agent
a complete blueprint for creating videoproof-contextual.mjs.Strength score: ★★★★★ This is the single strongest architectural feature for agent-friendliness.
createClass PatternThe metamodel uses a declarative, data-driven approach to model definition:
const MyModel = _AbstractStructModel.createClass(
'MyModel'
, ['fieldName', FieldTypeModel]
, ['otherField', OtherTypeModel]
);
Why this helps agents:
createClass
call immediately knows every field, its type, and its name....genericActorMixin, ...typographyActorMixin etc.
are spread into the field list. The agent can reuse them without understanding
their internals.PadModeOrEmptyModel is unambiguous — it’s an enum-or-empty.Strength score: ★★★★★
The genericTypeToUIElement function in type-driven-ui.mjs maps model types
to UI widgets automatically:
| Model Type | UI Widget |
|---|---|
_AbstractEnumModel |
UISelectInput (dropdown) |
_AbstractNumberModel |
UINumberInput |
StringModel |
UILineOfTextInput |
BooleanModel |
UICheckboxInput |
ColorModel |
Color chooser |
*OrEmptyModel |
Above + empty/toggle variant |
This is enormously powerful for agent work because:
PadModeModel) work without any UI code — the
abstract parent type match handles it.The only requirement is that the field name appears in
REGISTERED_GENERIC_KEYMOMENT_FIELDS — which was the one fix needed.
Strength score: ★★★★☆ (one star lost for the registration requirement — see friction)
All actor types are registered in one file (available-actors.mjs) with a
predictable structure:
initAvailableActorTypes arraygetActorWidgetSetup switchgetActorTreeNodeType (leaf or container)isTypographicActorTypeKey setgetActorTypeKeySpecificWidgets listWhile this is 6 touch points (see friction), each one is trivially predictable from the existing entries. The agent copied the VideoproofArrayV2 pattern and changed the names.
Strength score: ★★★★☆
The clean separation means:
_getWords function is pure)This allowed the agent to work in phases: model first, then renderer, then registration — each phase independently verifiable.
Strength score: ★★★★★
affixed-line-breaks.mjs exports both fixGridLineBreaks and
fixContextualLineBreaks — clearly named, clearly separated. Switching
from grid to contextual rendering was a single import change.
Strength score: ★★★★☆
REGISTERED_GENERIC_KEYMOMENT_FIELDS GateProblem: Adding padMode and customPad fields to the model was not
enough to get UI. The field names also had to be added to a hardcoded
FreezableSet in motion-stage.mjs (line 421; formerly
stage-and-actors.mjs, renamed since this analysis was written).
Why this caused friction:
_defineGenericWidgets → isAllowedFieldName
→ REGISTERED_GENERIC_KEYMOMENT_FIELDS chain to find the gate.Impact: This was the primary debugging issue in the implementation. The model was correct, the type-driven UI mapping was correct, but the field names weren’t in the allowlist.
Recommendation:
genericTypeToUIElement, it should appear in the
key moment editor without explicit registration.createClass pattern documentation
stating: “New fields also need registration in
REGISTERED_GENERIC_KEYMOMENT_FIELDS.”Problem: Adding a new actor type requires changes in 6 separate locations
within available-actors.mjs. Missing any one causes silent failures or
incomplete behavior.
Why this caused friction:
Recommendation:
registerActorType({
key: 'VideoproofContextual',
label: 'Videoproof Contextual',
Model: VideoproofContextualActorModel,
Renderer: VideoproofContextualActorRenderer,
treeNodeType: 'leaf',
isTypographic: true,
specificWidgets: ['FontSelect'],
charGroupsData: charGroupsData,
});
Problem: The contextual renderer needed fixed-lines in addition to
fixed-line-breaks for proper inline text flow. This was discovered only
by reading CSS comments (“used in contextual/kerning mode”).
Why this caused friction:
.fixed-line-breaks and .fixed-lines is not
obvious from the names alone.fixed-line-breaks (copying from the array
actor), which produced incorrect rendering.Recommendation:
data-layout="contextual" or data-line-break-mode="contextual".fixContextualLineBreaks function add/remove the
required CSS classes itself, rather than requiring them in the template.Problem: The legacy videoproof-contextual.mjs uses a functional style
with closures and passed-in callbacks (getCharsForKey, fixLineBreaks,
showExtended, extendedCharGroups). The shell version uses a different
API (getCharsForSelectUI, getExtendedChars, model-driven properties).
Why this caused friction:
getCharsForKey →
getCharsForSelectUI).getExtendedChars function).Recommendation:
The single most important property is pattern regularity — when every actor follows the same structural template, an agent needs to understand the pattern once and can then apply it N times. TypeRoof excels here.
How to strengthen:
createClass declarations, type-to-UI mappings, and mixin composition are
all declarative. Declarative code is easier for agents because:
How to strengthen:
Consistent naming helps agents navigate:
*Model = data model class*Renderer / *ActorRenderer = rendering component*Mixin = composable field set*OrEmptyModel = optional/nullable variantfix*LineBreaks = line-break algorithmHow to strengthen:
actor_renderer-{type}
is already good).The business logic functions (_getWords, _formatAuto, _kernPaddingGen)
are pure — no side effects, no global state. This matches the project’s
coding style guidelines and makes them:
How to strengthen:
The metamodel ensures each type is defined once:
PadModeModel is defined once with its enum values.CharGroupModel is defined once and imported by both array and contextual actors.type-driven-ui.mjs.This prevents inconsistencies that would confuse an agent.
| Architectural Property | Agent-Friendliness | Notes |
|---|---|---|
| Actor pattern regularity | ★★★★★ | One example = complete blueprint |
Declarative createClass |
★★★★★ | Self-documenting schema |
| Type-driven UI generation | ★★★★☆ | Powerful but gated by field registration |
| Model/Renderer separation | ★★★★★ | Clean phases of work |
| Centralized registration | ★★★★☆ | Predictable but 6 touch points |
| Pure business logic | ★★★★★ | Easy to port and adapt |
| CSS conventions | ★★★☆☆ | Implicit, discovered by reading CSS |
| Legacy → Shell migration | ★★★☆☆ | API differences require interpretation |
| Error feedback | ★★☆☆☆ | Silent failures when registration incomplete |
| Documentation | ★★★☆☆ | Code is readable but guides are sparse |
Replace the 6 scattered registration points with a single declarative registration call per actor type. This eliminates the most common class of agent (and human) errors.
Instead of maintaining REGISTERED_GENERIC_KEYMOMENT_FIELDS manually,
derive it from the keymoment model’s field list, filtering by fields that
have a genericTypeToUIElement mapping. This eliminates the “invisible gate”
problem entirely.
Add a brief comment block in each renderer’s TEMPLATE explaining which CSS classes are required and what they do. Alternatively, create a CSS architecture document mapping class combinations to visual behaviors.
Generated: 2026-02-25, during implementation of VideoproofContextualActor
Original branch: feature/videoproof-contextual (since merged)
Files touched: 3 (1 new, 2 modified)
Lines added: ~370 new + ~36 modified
Filed under docs/planning/ 2026-07-16; stage-and-actors.mjs → motion-stage.mjs references updated.