Inline authoring
In plain words
Section titled “In plain words”Feature registration is written inside the defineFeature callback. The calls
to r.entity, r.screen, r.nav, and the other registrar methods remain
visible to tools without executing the app. Large entity and screen values may
live in schema/ and be imported into feature.ts; the standard scaffold uses
that layout.
This keeps the source usable by the designer, code transformations, and AI authoring tools. Do not hide registration behind a function that conditionally builds a different feature graph at runtime. Handler bodies and pure helpers belong in their own files.
written out in plain sight hidden behind a builder"fields: title, severity" vs buildTicket(options) -> ???a tool can read it directly a tool has to run code to find outWhy it exists
Section titled “Why it exists”A Kumiko app feature is written inline. Entities, handlers, hooks,
events, screens, they live as literal expressions inside the
defineFeature callback, not behind a factory function that constructs
them at runtime. This looks like a stylistic choice. It is a contract,
and it has a specific reason.
// Visible registration, the standard app-feature shapeexport default defineFeature("incidents", (r) => { r.entity("incident", incidentEntity);
r.writeHandler(resolveIncident); r.queryHandler(listOpenIncidents);});// Factory, what app features must NOT look likeexport function createIncidentsFeature(opts: { auditEnabled?: boolean }) { return defineFeature("incidents", (r) => { r.entity("incident", buildIncidentEntity(opts)); if (opts.auditEnabled) r.requires("audit"); });}The first form leaves the registrar calls visible. The second makes the feature shape depend on runtime options.
What reads the source
Section titled “What reads the source”Three classes of tooling depend on the inline form, and each of them fails on the factory version.
The AST patcher. When Kumiko’s designer adds a field to an entity, it
parses the source file, finds the r.entity("incident", incidentEntity) call,
and follows regular source references into the schema module. A factory call
whose result depends on runtime options has no stable shape for the patcher to
edit.
The designer. A non-developer rendering a feature in the schema
designer sees one entity per r.entity call. The designer reads the
source AST, not the runtime registry, because the runtime registry only
exists after a build. A factory function hides the entity behind one
or more layers of indirection that the designer cannot follow without
running the code.
The AI feature builder. An LLM generating a feature edit needs the input to be the same shape as the output. If the codebase is full of factories, every example becomes “decode the factory’s options object, imagine what it produces, edit the imagined version”. Generating the code becomes generating the inputs to a factory you have to construct in your head. Inline removes the layer.
The same property reappears in three places because it is one property: the source must be the canonical representation. Anything that hides the actual feature shape behind a runtime construction breaks every tool that reads source.
What goes inline, what doesn’t
Section titled “What goes inline, what doesn’t”The contract applies to what defines the feature’s shape. Registrar calls
remain in feature.ts. Entity and screen definitions may be imported from the
feature’s schema/ directory, translation keys from i18n.ts, and handlers
from handlers/. The referenced values must still be regular, inspectable
source rather than the result of runtime branching.
What does not have to be inline:
- Handler bodies. The function that runs when
incidents:resolveis called is a regular TypeScript function. It can live inhandlers/resolve.write.ts, do whatever it needs to do, import what it needs to import. The patcher and the designer do not read handler bodies. - Helper functions used inside handlers. A
slugify, a date calculation, a domain-specific validation, those are normal code. - Anything used at boot but not defining the feature shape. Schema
helpers that return Zod schemas, constants used in multiple field
definitions, enum-like
as constarrays.
The line is whether the shape of the feature, what entities, what fields, what handlers exist, can be read by walking the source. As long as that property holds, everything else is unconstrained.
Bundled features are exempt
Section titled “Bundled features are exempt”A bundled feature like audit or secrets is a function that returns a
feature definition: createAuditFeature(opts). That is exactly the
factory pattern app features may not use. Bundled features get a pass
because they are not designer- or AI-editable. A user adding a field to
their incidents entity is editing app code. Nobody adds a field to the
audit feature through the designer.
The split keeps the contract honest. Bundled features need configuration options, which transport to use for email, which storage backend to use for files, and forcing them inline would mean every app re-declares those options. App features have no equivalent need: their shape is their definition.
Migrating a factory-style app feature
Section titled “Migrating a factory-style app feature”Move the defineFeature call to feature.ts, keep the registrar calls visible,
and move entity or screen constants into schema/. Runtime options that select
providers belong at app composition or in a bundled-feature factory. Business
rules belong in handlers or lib/. The feature still runs before this cleanup,
but source-based tools cannot safely edit a graph they cannot resolve.
What this gets you
Section titled “What this gets you”Inline authoring is the contract that makes everything else work. The schema-as-data property in entities, the boot-time validation of the feature graph, the designer’s ability to render a feature, the AI builder’s ability to edit one, every property compounds on top of the source being readable by tools.
The cost is some explicit registration and a fixed folder structure. In return, tools can read and edit the same source that reviewers approve.
See also
Section titled “See also”- Schemas as data, the property that inline authoring exists to preserve.
- Bundled features vs. custom, why bundled features are exempt from the inline contract.