@ankhorage/devtools 1.10.10 → 1.10.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +1,20 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
4
  import { join, relative, resolve, sep } from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
6
 
7
+ import { generateTemplateCatalog } from './generate-template-catalog.mjs';
7
8
  import { loadOwnerApis } from './owner-api.mjs';
8
9
 
9
- /*** Validate and scaffold one ready authored manifest into the normal Templates variant layout. */
10
+ /*** Scaffold one complete portable template and refresh filesystem discovery. */
10
11
  export async function scaffoldTemplate(input) {
11
12
  assertRecord(input, 'Scaffold input');
12
- for (const field of ['targetDirectory', 'category', 'templateId', 'label', 'description']) {
13
+ for (const field of ['targetDirectory', 'category', 'slug']) {
13
14
  assertNonEmptyString(input[field], field);
14
15
  }
15
- if (
16
- !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(input.templateId) ||
17
- ['default', 'starter'].includes(input.templateId)
18
- ) {
19
- throw new Error('templateId must be a non-reserved kebab-case identifier.');
16
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(input.slug)) {
17
+ throw new Error('slug must be a kebab-case identifier.');
20
18
  }
21
19
  assertRecord(input.manifest, 'manifest');
22
20
 
@@ -30,165 +28,85 @@ export async function scaffoldTemplate(input) {
30
28
  if (input.manifest.metadata?.category !== input.category) {
31
29
  throw new Error('Scaffold category must match manifest.metadata.category.');
32
30
  }
31
+ if (input.manifest.metadata?.slug !== input.slug) {
32
+ throw new Error('Scaffold slug must match manifest.metadata.slug.');
33
+ }
33
34
 
34
35
  const owners = await loadOwnerApis(targetDirectory);
35
36
  const composition = owners.templates.validateTemplateManifest(input.manifest, 'release');
36
- if (composition.status !== 'ready') {
37
- throw new Error(
38
- `Manifest is not release-ready: ${composition.diagnostics.map((item) => item.message).join('; ')}`,
39
- );
40
- }
41
37
  const manifest = owners.templates.assertTemplateManifestReady(composition);
42
- const categoryDirectoryName = input.category.replaceAll('_', '-');
38
+
43
39
  const categoryDirectory = resolve(
44
40
  targetDirectory,
45
- 'src/templates/starter/categories',
46
- categoryDirectoryName,
41
+ 'src/templates/categories',
42
+ input.category.replaceAll('_', '-'),
47
43
  );
48
44
  assertInside(targetDirectory, categoryDirectory);
49
- const variantDirectory = resolve(categoryDirectory, input.templateId);
50
- assertInside(categoryDirectory, variantDirectory);
51
- if (await pathExists(variantDirectory)) {
45
+
46
+ const templateDirectory = resolve(categoryDirectory, input.slug);
47
+ assertInside(categoryDirectory, templateDirectory);
48
+ if (await pathExists(templateDirectory)) {
52
49
  throw new Error(
53
- `Template source already exists: ${relative(targetDirectory, variantDirectory)}`,
50
+ `Template source already exists: ${relative(targetDirectory, templateDirectory)}`,
54
51
  );
55
52
  }
56
53
 
57
- const registryPath = join(categoryDirectory, 'index.ts');
58
- const registrySource = await readFile(registryPath, 'utf8');
59
- const symbol = toPascalCase(input.templateId);
60
- const factoryBase = symbol.endsWith('Starter') ? symbol.slice(0, -'Starter'.length) : symbol;
61
- const factoryName = `create${factoryBase}StarterTemplate`;
62
- const manifestName = `AUTHORED_${toConstantCase(input.templateId)}_MANIFEST`;
63
- const registrySourceUpdated = updateCategoryRegistry(registrySource, {
64
- templateId: input.templateId,
65
- label: input.label,
66
- description: input.description,
67
- factoryName,
68
- });
69
- const files = createTemplateFiles({ manifest, manifestName, factoryName });
70
-
71
- await mkdir(variantDirectory, { recursive: true });
72
- for (const [fileName, contents] of Object.entries(files)) {
73
- await writeFile(join(variantDirectory, fileName), contents);
74
- }
75
- await writeFile(registryPath, registrySourceUpdated);
54
+ const screensDirectory = join(templateDirectory, 'assets', 'screens');
55
+ const imagesDirectory = join(templateDirectory, 'assets', 'images');
56
+ await mkdir(screensDirectory, { recursive: true });
57
+ await mkdir(imagesDirectory, { recursive: true });
58
+ await writeFile(
59
+ join(templateDirectory, 'createAppManifest.ts'),
60
+ createManifestSource(manifest),
61
+ 'utf8',
62
+ );
63
+ await rm(join(categoryDirectory, '.gitkeep'), { force: true });
64
+ await generateTemplateCatalog(targetDirectory, owners.contracts.APP_CATEGORIES);
76
65
 
77
66
  return {
78
67
  targetDirectory,
79
- registryPath: relative(targetDirectory, registryPath),
80
- createdFiles: Object.keys(files).map((fileName) =>
81
- relative(targetDirectory, join(variantDirectory, fileName)),
82
- ),
83
- factoryName,
68
+ templateDirectory: relative(targetDirectory, templateDirectory),
69
+ createdFiles: [relative(targetDirectory, join(templateDirectory, 'createAppManifest.ts'))],
70
+ assetDirectories: [
71
+ relative(targetDirectory, screensDirectory),
72
+ relative(targetDirectory, imagesDirectory),
73
+ ],
84
74
  };
85
75
  }
86
76
 
87
- /*** Create normal manifest, factory, and entrypoint source for one authored starter variant. */
88
- function createTemplateFiles({ manifest, manifestName, factoryName }) {
89
- const manifestSource = `import type { AppManifest } from '@ankhorage/contracts';
90
-
91
- export const ${manifestName} = ${JSON.stringify(manifest, null, 2)} satisfies AppManifest;
92
- `;
93
- const templateSource = `import type { AppManifest } from '@ankhorage/contracts';
77
+ /*** Serialize one complete manifest as the template's canonical default export. */
78
+ function createManifestSource(manifest) {
79
+ return `import type { AppManifest } from '@ankhorage/contracts';
94
80
 
95
- import type { TemplateSeed } from '../../../starter.types';
96
- import { ${manifestName} } from './manifest';
81
+ const manifest = ${JSON.stringify(manifest, null, 2)} satisfies AppManifest;
97
82
 
98
- /*** Create the authored starter while applying the caller's canonical app identity and theme. */
99
- export function ${factoryName}(seed: TemplateSeed): AppManifest {
100
- const theme = seed.theme ?? ${manifestName}.themes[0];
101
- if (theme === undefined) {
102
- throw new Error('The authored template requires one resolved theme.');
103
- }
104
- return {
105
- ...${manifestName},
106
- metadata: {
107
- ...${manifestName}.metadata,
108
- name: seed.appName,
109
- slug: seed.slug,
110
- version: seed.version ?? ${manifestName}.metadata.version,
111
- themeId: theme.id,
112
- },
113
- themes: [theme],
114
- activeThemeId: theme.id,
115
- };
83
+ /*** Create the complete portable application manifest for this template. */
84
+ export default function createAppManifest(): AppManifest {
85
+ return structuredClone(manifest);
116
86
  }
117
87
  `;
118
- return {
119
- 'index.ts': `export { ${factoryName} } from './template';\n`,
120
- 'manifest.ts': manifestSource,
121
- 'template.ts': templateSource,
122
- };
123
- }
124
-
125
- /*** Add one stable import and definition to an existing category registry. */
126
- function updateCategoryRegistry(source, definition) {
127
- const importLine = `import { ${definition.factoryName} } from './${definition.templateId}';`;
128
- if (source.includes(`id: '${definition.templateId}'`) || source.includes(importLine)) {
129
- throw new Error(`Template is already registered: ${definition.templateId}`);
130
- }
131
- const exportMarker = '\nexport const ';
132
- const exportIndex = source.indexOf(exportMarker);
133
- if (exportIndex < 0) {
134
- throw new Error('Category registry does not expose its canonical template array.');
135
- }
136
- const prefixLines = source.slice(0, exportIndex).trimEnd().split('\n');
137
- const relativeImports = [
138
- ...prefixLines.filter((line) => /^import .* from '\.\//u.test(line)),
139
- importLine,
140
- ].sort((left, right) => left.localeCompare(right));
141
- const preservedPrefix = prefixLines.filter((line) => !/^import .* from '\.\//u.test(line));
142
- const withImport = `${[...preservedPrefix, ...relativeImports].join('\n')}\n${source.slice(exportIndex + 1)}`;
143
- const closeMarker = '] satisfies readonly CategoryStarterTemplateDefinition[];';
144
- const closeIndex = withImport.indexOf(closeMarker);
145
- if (closeIndex < 0) {
146
- throw new Error('Category registry is missing its canonical definition-array terminator.');
147
- }
148
- const entry = ` {
149
- id: '${escapeSingleQuoted(definition.templateId)}',
150
- label: '${escapeSingleQuoted(definition.label)}',
151
- description: '${escapeSingleQuoted(definition.description)}',
152
- create: ${definition.factoryName},
153
- },
154
- `;
155
- return `${withImport.slice(0, closeIndex)}${entry}${withImport.slice(closeIndex)}`;
156
- }
157
-
158
- /*** Convert kebab-case identifiers to a PascalCase source symbol. */
159
- function toPascalCase(value) {
160
- return value
161
- .split('-')
162
- .map((segment) => segment[0].toUpperCase() + segment.slice(1))
163
- .join('');
164
- }
165
-
166
- /*** Convert kebab-case identifiers to an uppercase constant name. */
167
- function toConstantCase(value) {
168
- return value.replaceAll('-', '_').toUpperCase();
169
- }
170
-
171
- /*** Escape content placed in generated single-quoted TypeScript strings. */
172
- function escapeSingleQuoted(value) {
173
- return value.replaceAll('\\', '\\\\').replaceAll("'", "\\'");
174
88
  }
175
89
 
176
90
  /*** Assert that a resolved output remains inside its declared owner directory. */
177
- function assertInside(parentDirectory, childPath) {
178
- const relativePath = relative(parentDirectory, childPath);
179
- if (relativePath === '' || relativePath.startsWith(`..${sep}`) || relativePath === '..') {
180
- throw new Error(`Scaffold path escapes its owner directory: ${childPath}`);
91
+ function assertInside(parentPath, childPath) {
92
+ const relativePath = relative(parentPath, childPath);
93
+ if (
94
+ relativePath === '' ||
95
+ relativePath === '..' ||
96
+ relativePath.startsWith(`..${sep}`) ||
97
+ relativePath.startsWith('../')
98
+ ) {
99
+ throw new Error(`Template path escapes its owner directory: ${childPath}`);
181
100
  }
182
101
  }
183
102
 
184
- /*** Return whether a filesystem path already exists. */
185
- async function pathExists(path) {
103
+ /*** Return whether a filesystem path exists. */
104
+ async function pathExists(filePath) {
186
105
  try {
187
- await access(path);
106
+ await access(filePath);
188
107
  return true;
189
- } catch (error) {
190
- if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return false;
191
- throw error;
108
+ } catch {
109
+ return false;
192
110
  }
193
111
  }
194
112
 
@@ -209,8 +127,10 @@ function assertNonEmptyString(value, label) {
209
127
  /*** Run deterministic Templates source scaffolding from one JSON input. */
210
128
  async function main() {
211
129
  const [inputPath] = process.argv.slice(2);
212
- if (!inputPath) throw new Error('Usage: scaffold-template.mjs <scaffold-input.json>');
213
- const input = JSON.parse(await readFile(inputPath, 'utf8'));
130
+ if (!inputPath) {
131
+ throw new Error('Usage: scaffold-template.mjs <scaffold-input.json>');
132
+ }
133
+ const input = JSON.parse(await readFile(resolve(inputPath), 'utf8'));
214
134
  console.log(JSON.stringify(await scaffoldTemplate(input), null, 2));
215
135
  }
216
136
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/devtools",
3
- "version": "1.10.10",
3
+ "version": "1.10.12",
4
4
  "description": "Shared development tools and repository standards for Ankhorage",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ankhorage/devtools#readme",
@@ -105,9 +105,9 @@
105
105
  "version-packages": "bun src/cli/bin/changeset.ts version"
106
106
  },
107
107
  "dependencies": {
108
- "@ankhorage/utility": "^0.2.0",
108
+ "@ankhorage/utility": "^0.3.0",
109
109
  "@changesets/cli": "^3.0.1",
110
- "@eslint/compat": "^2.1.0",
110
+ "@eslint/compat": "^2.1.1",
111
111
  "@eslint/js": "^10.0.1",
112
112
  "eslint": "^10.9.1",
113
113
  "eslint-config-prettier": "^10.1.8",
@@ -1,43 +0,0 @@
1
- # Expo and React Native Applications
2
-
3
- An Expo application is an independently installable application and may use ports-and-adapters
4
- principles for its application behavior. Expo Router route files are framework-owned inbound
5
- adapters, not the home of domain logic.
6
-
7
- ## Structure
8
-
9
- ```text
10
- app/ # Expo Router route tree
11
- src/
12
- app/ # providers and app composition
13
- <domain>/ # application-owned domains
14
- domain/
15
- application/
16
- ports/
17
- platform/ # app-local native/web adapters only
18
- utils/ # app-wide internal utilities
19
- ```
20
-
21
- Keep route files thin: resolve route parameters and navigation context, invoke or render the owning
22
- application boundary, and declare route-specific framework configuration. Substantial UI belongs
23
- with its owning domain or reusable UI package.
24
-
25
- ## Ankhorage ownership
26
-
27
- - Use Contracts for portable authored state.
28
- - Use Runtime for manifest/action/data execution.
29
- - Use ZORA and Surface for reusable UI.
30
- - Use Expo Runtime and provider packages for platform integration.
31
- - Do not copy package behavior into the generated app merely to avoid a public API or release.
32
- - Do not import Studio source or rely on the Studio workspace.
33
-
34
- ## Standalone lifecycle
35
-
36
- Each generated app owns its package manifest, lockfile, installation, validation, build, and
37
- deployment inputs. A parent dashboard may invoke commands with the app as `cwd`, but must not
38
- install it through a hidden shared workspace contract.
39
-
40
- ## Platform variants
41
-
42
- Use `.native`, `.web`, `.ios`, and `.android` variants only when the platform behavior genuinely
43
- differs. Keep the portable contract in the unsuffixed module and concrete behavior at the edge.
@@ -1,120 +0,0 @@
1
- # Ankhorage Ports and Adapters
2
-
3
- Use ports-and-adapters principles for application, engine, service, and hybrid packages. The
4
- Ankhorage package is the hexagon; internal domains are not independent feature packages.
5
-
6
- ## Purpose
7
-
8
- Protect stable package policy from React, Expo, HTTP, Fastify, Bun, filesystem, process, database,
9
- provider SDK, and test-harness details. The useful rule is dependency direction, not a mandatory
10
- folder ceremony.
11
-
12
- ```text
13
- package edge / adapter -> application -> domain
14
- |
15
- v
16
- required ports
17
-
18
- concrete adapter -> required port
19
- ```
20
-
21
- ## Roles
22
-
23
- - **Domain:** pure rules, values, invariants, and deterministic transformations owned by the
24
- package.
25
- - **Application:** command-independent use cases and orchestration of domain behavior.
26
- - **Port:** a capability contract required by application/domain code to reach an external edge.
27
- - **Inbound adapter:** converts CLI, HTTP, UI, Runtime, worker, or test input into an application
28
- invocation.
29
- - **Outbound adapter:** implements a required port using filesystem, process, provider, storage,
30
- network, Expo, or another package.
31
- - **Composition:** selects implementations and wires adapters to application operations.
32
-
33
- Ports belong beside the application/domain code that needs them. Do not create a global
34
- `src/ports/` dumping ground.
35
-
36
- ## Package-level edges
37
-
38
- Ankhorage preserves recognizable package edges:
39
-
40
- ```text
41
- src/app/ React or React Native composition and package-wide UI entrypoints
42
- src/cli/ one package-level Ankh provider
43
- src/host/ Bun/Node/Fastify/filesystem composition and shared host infrastructure
44
- src/platform/ native, web, Expo, or provider-specific implementations
45
- ```
46
-
47
- Internal domain behavior must not migrate into these directories merely because an adapter calls
48
- it. Edges translate and compose; domains own behavior.
49
-
50
- ## Domain-first organization
51
-
52
- Substantial domains may use role directories:
53
-
54
- ```text
55
- src/
56
- projects/
57
- contracts/
58
- domain/
59
- application/
60
- ports/
61
- deploy/
62
- contracts/
63
- domain/
64
- application/
65
- ports/
66
- ```
67
-
68
- Small domains remain flat while their siblings have the same role. Introduce role directories
69
- when definitions, parsers, use cases, adapters, or utilities begin mixing at one level.
70
-
71
- Do not use:
72
-
73
- ```text
74
- src/features/
75
- src/common/
76
- src/core/
77
- src/shared/
78
- src/ports/
79
- ```
80
-
81
- unless a repository has an explicit, narrower meaning that cannot be represented by an owning
82
- domain or package edge.
83
-
84
- ## When a port is justified
85
-
86
- Create a port when at least one is true:
87
-
88
- - more than one real adapter exists or is planned by current architecture;
89
- - deterministic tests need to replace a side effect;
90
- - the dependency is volatile or provider-specific;
91
- - the same application operation is invoked through multiple inbound edges;
92
- - the capability crosses a package, process, storage, network, platform, or credential boundary.
93
-
94
- Do not create a port merely because a function calls another function. Pure utilities, component
95
- composition, value transformations, and React-local presentation state usually do not need ports.
96
-
97
- ## React and React Native
98
-
99
- React/RN UI is an inbound edge. Components may collect input, render state, and invoke application
100
- actions. Provider execution and durable business rules remain outside components and hooks.
101
-
102
- - Keep navigation route modules thin.
103
- - Keep UI-specific transient state near the UI.
104
- - Move reusable business decisions and cross-interface operations into the owning domain or
105
- application layer.
106
- - Inject values, callbacks, or capability interfaces into reusable UI rather than importing
107
- concrete providers.
108
-
109
- ## Composition roots
110
-
111
- Keep wiring explicit and limited to package entrypoints such as app startup, host creation, CLI
112
- provider construction, or a focused factory. Do not use ambient service locators or hidden mutable
113
- registries as dependency injection.
114
-
115
- ## Testing
116
-
117
- - Test domain and application behavior with deterministic inputs and fake ports.
118
- - Test concrete adapters against their real protocol boundary.
119
- - Test user-facing flows through inbound edges only where the integration adds evidence.
120
- - Do not duplicate the full acceptance matrix for every internal refactor.
@@ -1,91 +0,0 @@
1
- # Repository Profiles
2
-
3
- Select one primary profile from the repository's actual ownership and consumers. A package may
4
- also expose secondary edges such as CLI or Expo without changing its primary profile.
5
-
6
- ## Application, engine, or hybrid
7
-
8
- Use for packages that coordinate use cases, state transitions, external systems, or several
9
- delivery mechanisms.
10
-
11
- Typical structure:
12
-
13
- ```text
14
- src/
15
- index.ts
16
- <domain>/
17
- contracts/
18
- domain/
19
- application/
20
- ports/
21
- app/ # optional React/RN edge and composition
22
- cli/ # optional package-level Ankh edge
23
- host/ # optional Bun/Node/HTTP/filesystem edge
24
- platform/ # optional native/web/provider edge
25
- utils/ # internal cross-domain utilities only
26
- ```
27
-
28
- Create role subdirectories only when a domain has enough distinct responsibilities to need them.
29
- Do not pre-create empty `contracts`, `domain`, `application`, or `ports` directories.
30
-
31
- ## Contracts or value library
32
-
33
- Contracts own portable shape and structural validation, not provider execution.
34
-
35
- ```text
36
- src/
37
- index.ts
38
- <domain>/
39
- contracts/
40
- parsers/
41
- constants/
42
- tests colocated with their owner
43
- ```
44
-
45
- - Keep modules deterministic and side-effect free.
46
- - Type definitions and structural parsers change together.
47
- - Provider readiness, network state, filesystem state, and UI behavior stay in their owner.
48
- - Avoid `Record<string, unknown>` escape hatches when a canonical concept can be modeled.
49
-
50
- ## Platform or provider adapter
51
-
52
- These packages deliberately implement an external technology boundary.
53
-
54
- ```text
55
- src/
56
- index.ts
57
- contracts/ # provider-facing public configuration when owned here
58
- planning/ # pure capability/configuration planning
59
- adapters/ # concrete provider/platform implementations
60
- composition/ # factories or provider registration
61
- cli/ # only when this package exposes Ankh commands
62
- ```
63
-
64
- - Depend on portable contracts or ports from lower-level owning packages.
65
- - Do not redefine the application/domain model locally.
66
- - Keep provider SDK values from leaking through portable public contracts.
67
- - Separate build-time planning from runtime execution when both exist.
68
-
69
- ## Tooling package
70
-
71
- Tooling packages may be command-centric but still keep parsing, policy, and side effects distinct.
72
-
73
- ```text
74
- src/
75
- cli/
76
- policy/ # deterministic rules and diagnostics
77
- application/ # command-independent operations
78
- adapters/ # filesystem, process, GitHub, registry, etc.
79
- composition/
80
- index.ts
81
- ```
82
-
83
- The package remains the boundary. Do not create internal pseudo-packages beneath `features/`.
84
-
85
- ## Generated or standalone application
86
-
87
- A generated application is an independently installable and buildable project. It is not a
88
- workspace child of Studio and must not depend on Studio-local source or installation state.
89
-
90
- Use the Expo application profile when applicable. Application-specific domains live under `src/`;
91
- route entrypoints remain thin.
@@ -1,79 +0,0 @@
1
- # UI and Design-System Libraries
2
-
3
- ZORA and Surface are layered reusable UI libraries, not hexagonal applications. From an
4
- application's perspective, they help build the inbound UI adapter; internally they use component
5
- ownership and dependency direction rather than application ports and use cases.
6
-
7
- ## Recommended layers
8
-
9
- ```text
10
- src/
11
- foundation/
12
- theme/
13
- layout/
14
- primitives/
15
- components/
16
- patterns/
17
- registry/
18
- internal/
19
- index.ts
20
- ```
21
-
22
- Use only the layers owned by the package. Surface and ZORA must not duplicate the same abstraction.
23
-
24
- Dependency direction moves from composed UI toward stable foundations:
25
-
26
- ```text
27
- patterns -> components -> primitives/foundation
28
- patterns -> layout -> foundation
29
- components -> theme -> foundation
30
- registry -> component/pattern metadata
31
- ```
32
-
33
- Lower layers do not import higher layers. Components do not import the global registry.
34
-
35
- ## Component ownership
36
-
37
- Colocate artifacts that change with the component:
38
-
39
- ```text
40
- components/
41
- Button/
42
- Button.tsx
43
- Button.types.ts
44
- Button.metadata.ts
45
- Button.test.tsx
46
- ```
47
-
48
- Keep a component as a single file while it has one homogeneous responsibility. Introduce its
49
- directory when tests, metadata, platform variants, styles, or private helpers justify it.
50
-
51
- Component-specific authoring metadata belongs beside the component. Registry composition may be
52
- central, but metadata must not become a parallel model detached from implementation and props.
53
-
54
- ## Patterns
55
-
56
- Patterns are reusable UI solutions, not application use cases. Group substantial collections by
57
- UI capability:
58
-
59
- ```text
60
- patterns/
61
- auth/
62
- content/
63
- navigation/
64
- onboarding/
65
- settings/
66
- ```
67
-
68
- A pattern may accept state, values, errors, and callbacks. It must not execute Supabase, HTTP,
69
- filesystem, deployment, or application authorization behavior.
70
-
71
- ## Platform behavior
72
-
73
- Keep provider and platform execution in the owning adapter package unless it is an unavoidable
74
- peer-backed UI implementation. Prefer injected values/callbacks and platform-neutral component
75
- contracts. Expo Runtime and provider packages own application/platform integration.
76
-
77
- Do not introduce `domain`, `application`, `ports`, or `adapters` for ordinary components. A complex
78
- subsystem such as an editor, canvas, or data grid may use internal model/platform boundaries when
79
- real complexity warrants them.