ecs: an architecture standard (layout, naming, exports, checker, diagrams) for the new ECS samples, and how to distribute it

Đang mở
#257 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@krisnye đang làm issue này rồi.

Từ ngày 3/11/2026.

  • #261 của @krisnye — đang mở

Đánh giá

Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức phù hợp với người mới
8/100
Loại issue
Tính năng
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
eslint, markdown, typescript, vite

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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

  1. 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.
  2. 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.ts exports todoTitle). 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 (an Index it 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.ts re-exports and nothing else (export * from './<root file>.js', plus named re-exports of parts a parent passes on).
    Components import each other only through index.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.md for 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.ts that re-exports the implementation typed as System, plus a folder system.x/x.ts holding the implementation
    and its helpers.
  • Keys are top-level Symbol() constants with a declare module '@adobe/ecs' entry in the file that declares them; archetypes are
    PascalCase as const arrays; entity references are components of their own (type: Entity), never fields of structs.
2.3 systems, apps and plugins
  • Every model and view exports systems: 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 through systems is private; systems run by world.run or ordered against by others are
    exported by name. The checker fails a system that no systems list 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: an input phase, an edited phase): systems written apart during: [phase] and what must see their
    result runs after: [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 }. A ports layer 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 }), and startPlugins(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 a systems list's text; a ports selector in a string is found by review).
  • Architecture diagrams, generated (scripts/diagrams/, about 240 lines + tests): src/ARCHITECTURE.md draws 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 own ARCHITECTURE.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 build and
    pnpm test fail 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
    plain pnpm test catches 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|view as defaults), not constants in the script.
  • Replace text matching in unlisted-system with real symbol resolution (TS program), and let systems lists 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/startPlugins trio into @adobe/ecs proper (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/ecs the right home for the checker (a package beside ecs, ecs-gpu, ecs-persistence), or should it be a
    separate repo?
  • Are elements | model | ports | view the right layer names for the ECS's recommended style, and <kind>.<name>.ts the right
    file naming? (They are not essential; consistency is.)
  • Should Plugin/PluginConfig/startPlugins live 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.

Ngôn ngữ chính
TypeScript
Star
12
Fork
8
Merge trung bình
11 giờ 2 phút
Pull request đã merge (30 ngày)
23

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của adobe/data

Tất cả issue của adobe/data

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.