ecs: an architecture standard (layout, naming, exports, checker, diagrams) for the new ECS samples, and how to distribute it
I maintainer di solito rispondono entro 1 giorno
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 8/100
- Tipo di issue
- Funzionalità
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Stack tecnologico
- eslint, markdown, typescript, vite
- Ambito
- developer-experience, documentation, tooling
Direzione di ricerca
This is a design proposal, not a task with a code target. Read section 5 and ask the maintainers for a decision on distribution (a docs/architecture.md, a published checker package, and agent rules) before any code. The checker and diagram code it describes lives in the author's own project, so there is no file in this repo to start from. Done means the maintainers have chosen a home for each of the three parts.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Proposal: an architecture standard for @adobe/ecs applications, with a checker, diagrams and agent rules
This comes from building a non-trivial application (several hundred source files, worked on by one human and coding agents) on
@adobe/ecs 0.3. Everything below is in daily use there and enforced in CI by the build and test scripts.
The ask
- Adopt the standard below in the new ECS sample projects (
ecs-todo-vanillajs,ecs-particles-webgpu,
ecs-transfers-canvas, ...), so they show one consistent way to organize an ECS application. - Decide how the standard, its checker and its agent rules reach other people building on the new ECS. Section 5 has a
concrete recommendation; we would like maintainers' opinion on it, since this is the part with the most options.
1. Why
An ECS application has no natural folder structure: state is global, systems are free functions, and the library gives no
guidance on what may depend on what. Left alone, apps drift into one flat folder of components and systems, DOM and GPU code
mixed into simulation, import cycles, and files that export whatever they like. That hurts people and coding agents alike
(an agent cannot tell what is public or where new code goes). We found that a small set of structural rules, enforced by a
script rather than described in prose, fixed this and kept it fixed through a lot of churn. The rules are generic: nothing
in them depends on what the application does.
2. The standard
2.1 Features, layers, components
src/
index.ts the app's entry points, re-exported (no side effects on import)
<feature>/ a vertical slice; holds only layer folders (a group of features holds only features)
elements/ custom elements that know nothing of the ECS (DOM only)
model/ data, rules and systems that make sense headless; what is persisted and inspected
ports/ glue to outside sources independent of this feature's markup: device input, network, storage, timers
view/ systems that render the model (DOM binding, GPU drawing) and connect functions for the feature's widgets
A feature is a vertical slice; each layer is a component: a folder with an index.ts and a README.md whose exports are
its public API and nothing else. The rule is recursive: a layer made of parts holds each part as a component of its own.
Layers every feature may omit; a feature's components are always its layers.
Layers by what they depend on (elements ← view → model ← ports):
| layer | may import | may not |
|---|---|---|
elements |
elements |
any @adobe/* (plain custom elements: properties in, DOM events out) |
model |
model |
DOM, network, timers, globalThis, Date.now, performance.now, @adobe/ecs-gpu, UI libraries. Time is the clock resource. It runs headless; a test drives it through its public systems |
ports |
model, ports |
@adobe/ecs-gpu; hardcoded selectors of a widget (it is handed targets and predicates) |
view |
model, elements, view |
(ports and view never import each other) |
Features form a DAG: a feature may extend another through its public interface; no feature cycles. A feature is only about its
own subject (input-device code does not belong in a rendering feature).
2.2 What is public, and how a component looks
- Public declarations live at the component's root, one per file, named
<kind>.<name>.ts(<name>kebab-case, the file named
for its export:component.todo-title.tsexportstodoTitle). Kinds:type,constant,component,resource,
archetype,index,function,system,rule,finalizer,element. - A root file's export is its first declaration and its only export. After the imports come the doc comment and the one
export, typed as its interface (export const clearCompleted: System = System.create(...)); constants and helpers it needs
come beneath it. A reader gets the whole public interface from the top of the file. Only what the export's statement needs at
module load (anIndexit names) may sit above it. - Anything else is private: a folder with no
index.ts(shared/,systems/, or named for what it holds). Shared private
code sits at the lowest common ancestor of its users; if something outside needs it, it becomes a public component. index.tsre-exports and nothing else (export * from './<root file>.js', plus named re-exports of parts a parent passes on).
Components import each other only throughindex.ts, never a deep path, never into a private folder; a part never imports its parent.- A public export nothing outside its component uses is a violation (move it to
shared/), unless its file says
// component-check: unused-export-ok <reason>. This keeps public surfaces honest: the check removes dead API. README.mdfor each layer:# <name>, a concise summary paragraph, then only as much as comprehension needs.- Render systems (
System.render, whose implementation needs a helper module such as a shader) are a wrapper
system.x.tsthat re-exports the implementation typed asSystem, plus a foldersystem.x/x.tsholding the implementation
and its helpers. - Keys are top-level
Symbol()constants with adeclare module '@adobe/ecs'entry in the file that declares them; archetypes are
PascalCaseas constarrays; entity references are components of their own (type: Entity), never fields of structs.
2.3 systems, apps and plugins
- Every
modelandviewexportssystems: readonly System[](what the world adds each frame; empty if none) from
constant.systems.ts; a part's systems are spread into its parent's. An app lists layers'systems; a headless run lists the
models'. A system the world adds only throughsystemsis private; systems run byworld.runor ordered against by others are
exported by name. The checker fails a system that nosystemslist reaches (unlisted-system) and an app that leaves a used
layer's systems out (app-misses-systems), the two commonest ways to ship a system that silently never runs. - A model system never names a view system. Order them with the view system's
before/after, or with a phase (an empty
system, as the ECS guide describes: aninputphase, aneditedphase): systems written apartduring: [phase]and what must see their
result runsafter: [phase]. This also decides where sync points land, and we found real frame-late bugs by examining it. - Plugins: what an app adds and removes as a unit is
{ systems, connect?(world, config) → stop }. Aportslayer exports its
plugin; the values a connection needs (canvas, root element, predicates) are one named config object, declared by
declaration merging (interface PluginConfig { canvas: HTMLCanvasElement }), andstartPlugins(world, plugins, config)asks
for every name its plugins ask for, once, as a compile error if one is missing or typed two ways. (About 115 lines; written to be moved into the ECS, which is why it is called out here.)
2.4 Data kinds
Persistence traits (ecs-persistence) name where data lives: document (shared, persisted), settings (user's, local), tab
(this tab's copy, restored on refresh, inherited by a duplicated tab), plus presence and session (neither persisted). A feature marks
its own data with the trait; nothing is saved per component. Rule that paid off: persist only independent data; derived data is
re-derived on load.
2.5 Smaller rules that are cheap to adopt
- Read with a query, not an
Index, unless the system looks up by key or reads a set from inside another system. - Commands, not row assignment, for anything a changes-finalizer must see (undo, dirty tracking).
- Hot paths get a benchmark in a
perftest/folder and are recorded per machine (pnpm perf --record); allocation-free per frame.
3. Enforcement
All of it is checked, not requested.
scripts/check-components/(about 550 lines with its graph analysis, plus a 375-line test file): one pure function from{ path → text }to
violations, each with a fix message written for an agent or a newcomer. Rules:src-root-files,index-is-only-re-exports,
index-exports-root-files,feature-holds-only-layers,folder-holds-only-folders,layer-name,layer-in-feature,
layer-needs-index,readme,root-file-name,root-file-exports-its-name,root-export-first,system-typed,
model-is-headless,layer-rank,deep-import,nested-import,part-imports-parent,import-cycle,feature-cycle,
systems-export,systems-spread,unlisted-system,app-misses-systems,unused-export. It parses with the TypeScript-capable
parser Vite ships (parseSync), so there is no extra dependency in a Vite project. Parts are heuristic where noted (a system
counts as listed if its name appears in asystemslist's text; aportsselector in a string is found by review).- Architecture diagrams, generated (
scripts/diagrams/, about 240 lines + tests):src/ARCHITECTURE.mddraws the features as
a DAG (core at the bottom, arrows mean "uses", transitive edges removed, apps in yellow), every feature with several layers gets
its ownARCHITECTURE.md, and a layer with three or more parts gets a parts diagram in its README. Each node links down to the
next level. The diagrams are Mermaid, generated from the imports, between<!-- architecture:start/end -->markers, so
they cannot be wrong; descriptions are written by people (the>line, at most 64 characters, enforced).pnpm buildand
pnpm testfail when they are stale. Rendered on GitHub as is. - Wiring:
pnpm build= typecheck + component check + diagram check + bundle; two vitest tests run the same scripts, so a
plainpnpm testcatches it too.
Experience: the violation messages mattered more than the rules. "x is exported by a/model but nothing outside imports it. Fix:
move it into a private folder..." let an agent fix 20 violations unprompted. Agents also reliably follow the structure once the
check exists, and drift fast when it is only prose.
4. What we would change before others adopt it
- Make layers, kinds and the folder names configuration (
elements|model|ports|viewas defaults), not constants in the script. - Replace text matching in
unlisted-systemwith real symbol resolution (TS program), and letsystemslists be found by
convention or config. - Split into a CLI + library with a stable report format (JSON), and stable rule ids with per-rule enable/disable.
- Decide the status of
elements(plain custom elements) for samples that do not use DOM components. - Port the
Plugin/PluginConfig/startPluginstrio into@adobe/ecsproper (it is generic).
5. Distribution: what we recommend
Three separate things want distributing, and they have different best homes:
| what | recommended home | why |
|---|---|---|
| The standard (prose) | one docs/architecture.md in adobe/data/ecs, linked from the ECS README and each sample's README |
a single human-readable source of truth; everything else derives from or links to it |
| The checker and diagram generator | a published package, e.g. @adobe/ecs-architecture: a CLI (ecs-architecture check, ecs-architecture diagrams [--check]) and a library, run via pnpm exec, devDependency, config in package.json or ecs-architecture.config.json |
tooling in a versioned package beats a copied script: bug fixes and new rules arrive by upgrade; the repo's own samples use it, so it is tested continuously. A cross-file import-graph check does not fit ESLint (per-file) well; per-file rules (naming, export-first) could later be an ESLint plugin sharing the same core |
| Agent rules and skills | (a) AGENTS.md at the repo root of each sample and in the scaffold (the cross-tool convention read by Codex, Cursor, Copilot, Claude Code and others): a short block with the layer rules, the commands to run and "run the check after moving or exporting anything", pointing at docs/architecture.md; (b) a Claude Code plugin (.claude-plugin/plugin.json, containing skills and path-scoped rules, installable from a marketplace, e.g. /plugin marketplace add adobe/data) with an ecs-architecture skill: how to place new code, how to read and fix a violation, how to add a feature |
AGENTS.md reaches every tool with no install; a plugin gives Claude Code users skills that are versioned and updated with the library instead of copied into .claude/ and left to rot. Keep the substance in the docs and the checker's messages, so each wrapper stays thin |
Plus one more piece that makes adoption cheap: create-ecs-app (pnpm create @adobe/ecs-app) that scaffolds the structure
(one feature with model and view, the systems wiring, the scripts, AGENTS.md, CI). The sample projects can double as its
templates, which keeps them honest.
Suggested order: (1) docs page + apply the structure to one sample, by hand, to find what is awkward in a small app (we expect
elements and ports to be empty in several); (2) extract the checker into a package, config-driven, and use it in all samples
in CI; (3) agent files and the plugin; (4) the scaffold.
Trade-offs we know of: a published checker is another package to maintain (but it is the part that carries the value); a Claude
plugin is vendor-specific (hence AGENTS.md first, and keeping both thin); strict rules cost newcomers a first hour (the fix
messages are written to make that hour short, and every rule has an escape hatch in the file that needs it).
6. Open questions for maintainers
- Is
adobe/data/ecsthe right home for the checker (a package besideecs,ecs-gpu,ecs-persistence), or should it be a
separate repo? - Are
elements | model | ports | viewthe right layer names for the ECS's recommended style, and<kind>.<name>.tsthe right
file naming? (They are not essential; consistency is.) - Should
Plugin/PluginConfig/startPluginslive in@adobe/ecs? - Should the standard require the one-export-per-file rule, or only recommend it? It is the rule with most mechanical value (public
surface, tree-shaking, agent navigation) and the most friction.
7. Reference implementation
We have a working implementation of the checker (with its test file), the diagram generator (with tests) and the rule documents
for agents, and can contribute it as the starting point for the package and for the sample changes if maintainers want it.
- Lingua principale
- TypeScript
- Stelle
- 12
- Fork
- 8
- Merge medio
- 11h 2m
- PR unite (30g)
- 23
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di adobe/data
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
ecs: let kernels issue commands through helpers (component lists, symbol-keyed values, void helpers)Aperta
Difficoltà 5/5 Più di una settimana Idoneità per principianti 15/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 30/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 22/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 15/100
I maintainer di solito rispondono entro 1 giorno
Issue simili
-
Add: Yichang TVApertacheck:passed streams:add
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
I maintainer di solito rispondono entro 1 giorno
-
vtselect: data race in Tempo search tags APIs for invalid TraceQL queriesForse già presa @Shubham19032004 l’ha presa oggi. Apertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
VictoriaMetrics/VictoriaTraces#304 ·
I maintainer di solito rispondono entro 1 giorno
-
test: claim-conflict drop leaves both facts active and is never re-proposed (untested review cell)Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 83/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
microsoft/vscode-pull-request-github#9159 ·
I maintainer di solito rispondono entro 1 giorno
-
[Feature]: Usage page should show API-equivalent cost and say when a connected plan shows info onlyApertaenhancement from-x
Difficoltà 2/5 1-3 ore Idoneità per principianti 66/100
nightly-labs/openbot#4867 ·
I maintainer di solito rispondono entro 1 giorno