@ossy/app 1.40.2 → 3.0.1

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.
Files changed (47) hide show
  1. package/README.md +49 -12
  2. package/cli/build.task.js +49 -10
  3. package/cli/get-platform-files.task.js +21 -15
  4. package/cli/manifest-plugin.js +340 -48
  5. package/package.json +25 -13
  6. package/runtime/merge-shell-slots.js +148 -0
  7. package/runtime/page-runtime.js +36 -18
  8. package/runtime/resolve-app-slots.js +126 -0
  9. package/src/en.translations.json +8 -0
  10. package/src/manifest/action-id-to-tool-name.js +29 -0
  11. package/src/manifest/build-action-input-schema.js +220 -0
  12. package/src/manifest/build-actions-schema.js +36 -0
  13. package/src/manifest/build-capabilities.js +190 -0
  14. package/src/manifest/build-manifest-summary.js +11 -5
  15. package/src/manifest/extract-task-catalog.js +1 -0
  16. package/src/manifest/resolve-page-layout.js +51 -0
  17. package/src/manifest/serialize-package-definition.js +2 -3
  18. package/src/manifest/template-to-json-schema.js +103 -0
  19. package/src/shell/App.jsx +18 -9
  20. package/src/shell/AppSettings.jsx +17 -0
  21. package/src/shell/DevPagesPanel.jsx +17 -13
  22. package/src/shell/HeaderAuthActions.jsx +49 -0
  23. package/src/shell/LanguageList.jsx +83 -0
  24. package/src/shell/LanguageSelect.jsx +73 -0
  25. package/src/shell/ThemeEditor.jsx +19 -34
  26. package/src/shell/ThemeSelect.jsx +101 -0
  27. package/src/shell/WorkspaceAppSettingsSync.jsx +39 -0
  28. package/src/shell/buildSidebarNav.js +113 -0
  29. package/src/shell/index.js +11 -0
  30. package/src/shell/languageCode.js +31 -0
  31. package/src/shell/patchUserAppSettings.js +10 -0
  32. package/src/shell/resolveEndpoints.js +43 -0
  33. package/src/shell/resolveWorkspaceServices.js +20 -0
  34. package/src/shell/themeEditorStyles.js +49 -0
  35. package/src/shell/useCompactShellLayout.js +24 -0
  36. package/src/shell/useShellWorkspace.js +40 -0
  37. package/src/shell-registry/blank.layout.jsx +8 -0
  38. package/src/shell-registry/default.layout.jsx +101 -0
  39. package/src/shell-registry/footer-default.component.jsx +39 -0
  40. package/src/shell-registry/head-default.component.jsx +17 -0
  41. package/src/shell-registry/header-default.component.jsx +50 -0
  42. package/src/shell-registry/logo-logomark.component.jsx +8 -0
  43. package/src/shell-registry/logo-logotype.component.jsx +15 -0
  44. package/src/shell-registry/minimal.layout.jsx +58 -0
  45. package/src/shell-registry/sidebar-default.component.jsx +281 -0
  46. package/src/sv.translations.json +8 -0
  47. package/runtime/resolve-shell-slots.js +0 -112
package/README.md CHANGED
@@ -70,6 +70,12 @@ export default {
70
70
  }
71
71
  ```
72
72
 
73
+ Optional flags:
74
+
75
+ | Key | Default | Purpose |
76
+ |-----|---------|---------|
77
+ | `enablePushInvalidation` | `false` | App-wide SSE cache invalidation. Prefer `<PushInvalidationSubscriber />` on specific pages instead — see [@ossy/sdk-react README](../sdk-react/README.md#push-invalidation). |
78
+
73
79
  Config is loaded at build time and merged with request-time settings (e.g. user theme preference from cookies).
74
80
 
75
81
  ## API routes
@@ -94,40 +100,71 @@ Build output: `build/.ossy/api.generated.json` — a registry of `[{ id, path, m
94
100
 
95
101
  ## Background tasks (`*.task.js`)
96
102
 
97
- For background job processors, create `*.task.js` files in `src/`. Each file exports a `metadata` object with a `type` field and a default handler function.
103
+ Task modules export `metadata` (id, optional `triggers` / `schedule`) and a `run` function. They are registered by `TaskService` at server startup.
104
+
105
+ **Sync invoke:** `POST /actions` with `{ action, payload }` awaits the primary task at `{feature}/tasks/{intent}` derived from the action id.
106
+
107
+ **Async:** changestream events and cron schedules dispatch matching tasks without blocking the caller.
108
+
109
+ Build output includes task entries in the manifest for server registration. Async execution does **not** use a separate polling worker — see ADR 0003.
110
+
111
+ Task ids use the canonical form `@ossy/{feature}/tasks/{intent}`. Action ids (`@ossy/{feature}/actions/{intent}`) map to their primary task via ADR 0003.
112
+
113
+ ## Schemas (`*.schema.js`)
114
+
115
+ Resource document schemas are POJOs default-exported from `*.schema.js` in `src/` or installed feature packages. The build discovers them, validates canonical ids (`@{provider}/{feature}/schema/{concept}`), and merges them into `manifest.schemas`.
98
116
 
99
117
  ```js
100
- // src/send-email.task.js
101
- export const metadata = {
102
- type: 'send-email',
118
+ // packages/booking/src/service.schema.js
119
+ export default {
120
+ id: '@ossy/booking/schema/service',
121
+ name: 'Service',
122
+ fields: [{ name: 'name', type: 'text', required: true }],
103
123
  }
124
+ ```
125
+
126
+ Forms reference a schema via `metadata.schemaId` (not `templateId`).
127
+
128
+ ## Forms (`*.form.js`)
129
+
130
+ Form modules export intent metadata only — field definitions live on the schema:
104
131
 
105
- export default async function handle({ sdk, job }) {
106
- // process the job
132
+ ```js
133
+ export const metadata = {
134
+ id: '@ossy/booking/form/service',
135
+ schemaId: '@ossy/booking/schema/service',
107
136
  }
108
137
  ```
109
138
 
110
- The worker polls the job queue every 3 seconds and lazy-imports the handler module the first time a job of that type arrives.
139
+ ## Build manifest
140
+
141
+ `app build` writes `build/manifest.json` plus derived catalogs:
142
+
143
+ | Output | Purpose |
144
+ |--------|---------|
145
+ | `manifest.json` | Pages, actions, tasks, `schemas`, components, layouts, `taskCatalog`, `taskGraphEdges`, … |
146
+ | `build/capabilities.json` | Automation UI catalog — actions, tasks, schemas, edges |
147
+ | `build/actions.schema.json` | JSON Schema for agent/MCP tool inputs per action |
111
148
 
112
- Build output: `build/.ossy/tasks.generated.json` a registry of `[{ type, module }]` entries. Run the worker with `node build/worker.js`. Requires `OSSY_WORKSPACE_ID`, `OSSY_API_URL`, and `OSSY_API_TOKEN` environment variables.
149
+ Action and task metadata are validated at build time via `@ossy/schema`. Task triggers reference schema ids (`type: '@ossy/platform/schema/file'`) and lifecycle events (`Created`, `Patched`, …).
113
150
 
114
151
  ## Components (`*.component.jsx`)
115
152
 
116
153
  Register injectable UI via `*.component.jsx` in `src/` or feature packages. Components are bundled separately and resolved into slots at runtime.
117
154
 
118
- **Shell chrome** — the app assigns placement in `*.layout.jsx`:
155
+ **App chrome** — the app assigns placement in `*.layout.jsx`:
119
156
 
120
157
  ```jsx
121
158
  export const slots = {
122
- 'shell:header': 'app-header',
123
- 'shell:sidebar': 'app-sidebar',
159
+ 'app:header': 'app-header',
160
+ 'app:sidebar': 'app-sidebar',
124
161
  }
125
162
  ```
126
163
 
127
164
  **Resource / input UI** — feature packages use `metadata.id` as the slot key:
128
165
 
129
166
  ```jsx
130
- export const metadata = { id: 'resource:booking/service/form' }
167
+ export const metadata = { id: '@ossy/booking/form/service' }
131
168
  export default function ServiceForm() { … }
132
169
  ```
133
170
 
package/cli/build.task.js CHANGED
@@ -13,7 +13,7 @@ import getPlatformFiles, {
13
13
  PAGE_FILE_PATTERN,
14
14
  API_FILE_PATTERN,
15
15
  TASK_FILE_PATTERN,
16
- RESOURCE_FILE_PATTERN,
16
+ SCHEMA_FILE_PATTERN,
17
17
  COMPONENT_FILE_PATTERN,
18
18
  AGGREGATE_FILE_PATTERN,
19
19
  INTEGRATION_FILE_PATTERN,
@@ -21,13 +21,14 @@ import getPlatformFiles, {
21
21
  EMAIL_FILE_PATTERN,
22
22
  ACTION_FILE_PATTERN,
23
23
  E2E_FILE_PATTERN,
24
+ FLOW_FILE_PATTERN,
24
25
  LAYOUT_FILE_PATTERN,
25
26
  TRANSLATIONS_FILE_PATTERN,
26
27
  } from './get-platform-files.task.js'
27
28
  import { manifestPlugin } from './manifest-plugin.js'
28
29
  import { mergeAndEmitTranslations } from './merge-translations.task.js'
29
30
 
30
- export { PAGE_FILE_PATTERN, API_FILE_PATTERN, TASK_FILE_PATTERN, RESOURCE_FILE_PATTERN, COMPONENT_FILE_PATTERN, AGGREGATE_FILE_PATTERN, INTEGRATION_FILE_PATTERN, STARTUP_FILE_PATTERN, EMAIL_FILE_PATTERN, ACTION_FILE_PATTERN, E2E_FILE_PATTERN, LAYOUT_FILE_PATTERN, TRANSLATIONS_FILE_PATTERN }
31
+ export { PAGE_FILE_PATTERN, API_FILE_PATTERN, TASK_FILE_PATTERN, SCHEMA_FILE_PATTERN, COMPONENT_FILE_PATTERN, AGGREGATE_FILE_PATTERN, INTEGRATION_FILE_PATTERN, STARTUP_FILE_PATTERN, EMAIL_FILE_PATTERN, ACTION_FILE_PATTERN, E2E_FILE_PATTERN, FLOW_FILE_PATTERN, LAYOUT_FILE_PATTERN, TRANSLATIONS_FILE_PATTERN }
31
32
 
32
33
  // Generated entry stubs live under `build/.ossy/entries/`. Putting them
33
34
  // inside `build/` means they're cleaned automatically with every build,
@@ -126,10 +127,8 @@ function generateTaskStub ({ stubAbs, sourceAbs }) {
126
127
  ].join('\n')
127
128
  }
128
129
 
129
- // Resources are pure data (POJOs default-exported from `*.resource.js`).
130
- // The stub only pass-through re-exports the default so the manifest plugin
131
- // can read it via the same `await import(chunkUrl)` path as the other kinds.
132
- function generateResourceStub ({ stubAbs, sourceAbs }) {
130
+ // Schemas are pure data (POJOs default-exported from `*.schema.js`).
131
+ function generateSchemaStub ({ stubAbs, sourceAbs }) {
133
132
  const importPath = relImport(stubAbs, sourceAbs)
134
133
  return [
135
134
  '// Generated by @ossy/app — do not edit',
@@ -166,7 +165,22 @@ function generateAggregateStub ({ stubAbs, sourceAbs }) {
166
165
  // Integrations are server-side only. The stub re-exports `id`, `credentials`,
167
166
  // and `connect` so the manifest plugin can validate them and the platform
168
167
  // server can call `connect({ env })` at startup.
169
- function generateIntegrationStub ({ stubAbs, sourceAbs }) {
168
+ //
169
+ // grafana-metrics mutates a package singleton (`metrics`). A relative re-export
170
+ // gets bundled into its own copy, so TaskService recordings never reach
171
+ // connect() — re-export via @ossy/observability to keep one module instance.
172
+ function generateIntegrationStub ({ stubAbs, sourceAbs, packageName }) {
173
+ const base = path.basename(sourceAbs)
174
+ if (packageName === '@ossy/observability' && base === 'grafana-metrics.integration.js') {
175
+ return [
176
+ '// Generated by @ossy/app — do not edit',
177
+ "import { grafanaMetricsIntegration } from '@ossy/observability'",
178
+ 'export const id = grafanaMetricsIntegration.id',
179
+ 'export const credentials = grafanaMetricsIntegration.credentials',
180
+ 'export const connect = grafanaMetricsIntegration.connect',
181
+ '',
182
+ ].join('\n')
183
+ }
170
184
  const importPath = relImport(stubAbs, sourceAbs)
171
185
  return [
172
186
  '// Generated by @ossy/app — do not edit',
@@ -208,6 +222,16 @@ function generateActionStub ({ stubAbs, sourceAbs }) {
208
222
  ].join('\n')
209
223
  }
210
224
 
225
+ // Forms are intent metadata (schemaId reference). Field schema lives on the resource.
226
+ function generateFormStub ({ stubAbs, sourceAbs }) {
227
+ const importPath = relImport(stubAbs, sourceAbs)
228
+ return [
229
+ '// Generated by @ossy/app — do not edit',
230
+ `export { metadata } from '${importPath}'`,
231
+ '',
232
+ ].join('\n')
233
+ }
234
+
211
235
  // E2e tests are Playwright test modules. The stub re-exports `id`, `feature`,
212
236
  // `requires`, and the default test function so the manifest plugin can validate
213
237
  // them and the e2e runner can import and execute them.
@@ -220,6 +244,15 @@ function generateE2eStub ({ stubAbs, sourceAbs }) {
220
244
  ].join('\n')
221
245
  }
222
246
 
247
+ function generateFlowStub ({ stubAbs, sourceAbs }) {
248
+ const importPath = relImport(stubAbs, sourceAbs)
249
+ return [
250
+ '// Generated by @ossy/app — do not edit',
251
+ `export { metadata, default } from '${importPath}'`,
252
+ '',
253
+ ].join('\n')
254
+ }
255
+
223
256
  // Layouts are React components that wrap page renders. The stub re-exports
224
257
  // everything (including required `id`) plus the
225
258
  // default component so the manifest plugin can validate them and the server
@@ -237,14 +270,16 @@ function generateLayoutStub ({ stubAbs, sourceAbs }) {
237
270
  function stubFor (kind, args) {
238
271
  if (kind === 'page') return generatePageStub(args)
239
272
  if (kind === 'api') return generateApiStub(args)
240
- if (kind === 'resource') return generateResourceStub(args)
273
+ if (kind === 'schema') return generateSchemaStub(args)
241
274
  if (kind === 'component') return generateComponentStub(args)
242
275
  if (kind === 'aggregate') return generateAggregateStub(args)
243
276
  if (kind === 'integration') return generateIntegrationStub(args)
244
277
  if (kind === 'startup') return generateStartupStub(args)
245
278
  if (kind === 'email') return generateEmailStub(args)
246
279
  if (kind === 'action') return generateActionStub(args)
280
+ if (kind === 'form') return generateFormStub(args)
247
281
  if (kind === 'e2e') return generateE2eStub(args)
282
+ if (kind === 'flow') return generateFlowStub(args)
248
283
  if (kind === 'layout') return generateLayoutStub(args)
249
284
  return generateTaskStub(args)
250
285
  }
@@ -294,14 +329,16 @@ export async function build (cliArgs = []) {
294
329
  ...platformFiles.pages,
295
330
  ...platformFiles.apis,
296
331
  ...platformFiles.tasks,
297
- ...platformFiles.resources,
332
+ ...platformFiles.schemas,
298
333
  ...platformFiles.components,
299
334
  ...platformFiles.aggregates,
300
335
  ...platformFiles.integrations,
301
336
  ...platformFiles.startups,
302
337
  ...platformFiles.emails,
303
338
  ...platformFiles.actions,
339
+ ...platformFiles.forms,
304
340
  ...platformFiles.e2es,
341
+ ...platformFiles.flows,
305
342
  ...platformFiles.layouts,
306
343
  ]
307
344
 
@@ -313,7 +350,7 @@ export async function build (cliArgs = []) {
313
350
  const effectiveSrcDir = packageSrcDir || srcDir
314
351
  const inputName = entryNameFromSource(sourcePath, effectiveSrcDir, packageName)
315
352
  const stubAbs = path.join(stubDir, stubFileNameFor(kind, sourcePath, effectiveSrcDir, packageName))
316
- fs.writeFileSync(stubAbs, stubFor(kind, { stubAbs, sourceAbs: sourcePath }), 'utf8')
353
+ fs.writeFileSync(stubAbs, stubFor(kind, { stubAbs, sourceAbs: sourcePath, packageName, packageSrcDir }), 'utf8')
317
354
  stubInputMap[inputName] = stubAbs
318
355
  entriesByStub.set(stubAbs, { kind, sourcePath, packageName, packageSrcDir })
319
356
  }
@@ -350,6 +387,8 @@ export async function build (cliArgs = []) {
350
387
  if (id === '@ossy/platform' || id.startsWith('@ossy/platform/')) return true
351
388
  if (id === '@ossy/event-store' || id.startsWith('@ossy/event-store/')) return true
352
389
  if (id === '@ossy/observability' || id.startsWith('@ossy/observability/')) return true
390
+ // Shared in-memory outbox — email integration appends, dev-inbox API reads.
391
+ if (id === '@ossy/email/local-outbox') return true
353
392
  return false
354
393
  },
355
394
  plugins: [
@@ -4,19 +4,21 @@ import path from 'node:path'
4
4
  export const PAGE_FILE_PATTERN = /\.page\.(jsx?|tsx?)$/
5
5
  export const API_FILE_PATTERN = /\.api\.(mjs|cjs|js)$/
6
6
  export const TASK_FILE_PATTERN = /\.task\.(mjs|cjs|js)$/
7
- export const RESOURCE_FILE_PATTERN = /\.resource\.(mjs|cjs|js)$/
7
+ export const SCHEMA_FILE_PATTERN = /\.schema\.(mjs|cjs|js)$/
8
8
  export const COMPONENT_FILE_PATTERN = /\.component\.(jsx?|tsx?)$/
9
9
  export const AGGREGATE_FILE_PATTERN = /\.aggregate\.(mjs|cjs|js)$/
10
10
  export const INTEGRATION_FILE_PATTERN = /\.integration\.(mjs|cjs|js)$/
11
11
  export const STARTUP_FILE_PATTERN = /\.startup\.(mjs|cjs|js)$/
12
12
  export const EMAIL_FILE_PATTERN = /\.email\.(jsx?|tsx?)$/
13
13
  export const ACTION_FILE_PATTERN = /\.action\.(mjs|cjs|js)$/
14
+ export const FORM_FILE_PATTERN = /\.form\.(mjs|cjs|js)$/
14
15
  export const E2E_FILE_PATTERN = /\.e2e\.(mjs|cjs|js)$/
16
+ export const FLOW_FILE_PATTERN = /\.flow\.(mjs|cjs|js)$/
15
17
  export const LAYOUT_FILE_PATTERN = /\.layout\.(jsx?|tsx?)$/
16
18
  export const TRANSLATIONS_FILE_PATTERN = /\.translations\.json$/
17
19
 
18
20
  /**
19
- * @typedef {'page' | 'api' | 'task' | 'resource' | 'component' | 'aggregate' | 'integration' | 'startup' | 'email' | 'action' | 'e2e' | 'layout'} EntryKind
21
+ * @typedef {'page' | 'api' | 'task' | 'schema' | 'component' | 'aggregate' | 'integration' | 'startup' | 'email' | 'action' | 'form' | 'e2e' | 'flow' | 'layout'} EntryKind
20
22
  *
21
23
  * @typedef {object} PlatformEntry
22
24
  * @property {EntryKind} kind Which bucket this entry lives in.
@@ -28,15 +30,17 @@ export const TRANSLATIONS_FILE_PATTERN = /\.translations\.json$/
28
30
  * @property {PlatformEntry[]} pages Discovered `*.page.{jsx,tsx,...}` entries.
29
31
  * @property {PlatformEntry[]} apis Discovered `*.api.{js,mjs,cjs}` entries.
30
32
  * @property {PlatformEntry[]} tasks Discovered `*.task.{js,mjs,cjs}` entries.
31
- * @property {PlatformEntry[]} resources Discovered `*.resource.{js,mjs,cjs}` entries.
33
+ * @property {PlatformEntry[]} schemas Discovered `*.schema.{js,mjs,cjs}` entries (ADR 0006).
32
34
  * @property {PlatformEntry[]} components Discovered `*.component.{jsx,tsx,...}` entries.
33
35
  * @property {PlatformEntry[]} aggregates Discovered `*.aggregate.{js,mjs,cjs}` entries (installed packages only).
34
36
  * @property {PlatformEntry[]} integrations Discovered `*.integration.{js,mjs,cjs}` entries.
35
37
  * @property {PlatformEntry[]} startups Discovered `*.startup.{js,mjs,cjs}` entries.
36
38
  * @property {PlatformEntry[]} emails Discovered `*.email.{jsx,tsx,...}` entries.
37
39
  * @property {PlatformEntry[]} actions Discovered `*.action.{js,mjs,cjs}` entries.
40
+ * @property {PlatformEntry[]} forms Discovered `*.form.{js,mjs,cjs}` entries.
38
41
  * @property {PlatformEntry[]} e2es Discovered `*.e2e.{js,mjs,cjs}` entries (installed packages only).
39
- * @property {PlatformEntry[]} layouts Discovered `*.layout.{jsx,tsx,...}` entries (app root `src/` only — never from installed packages).
42
+ * @property {PlatformEntry[]} flows Discovered `*.flow.{js,mjs,cjs}` entries (installed packages only).
43
+ * @property {PlatformEntry[]} layouts Discovered `*.layout.{jsx,tsx,...}` entries (app and installed packages).
40
44
  */
41
45
  export function discoverFilesByPattern (srcDir, filePattern) {
42
46
  const dir = path.resolve(srcDir)
@@ -58,14 +62,16 @@ function classifyFile (absPath) {
58
62
  if (PAGE_FILE_PATTERN.test(base)) return 'page'
59
63
  if (API_FILE_PATTERN.test(base)) return 'api'
60
64
  if (TASK_FILE_PATTERN.test(base)) return 'task'
61
- if (RESOURCE_FILE_PATTERN.test(base)) return 'resource'
65
+ if (SCHEMA_FILE_PATTERN.test(base)) return 'schema'
62
66
  if (COMPONENT_FILE_PATTERN.test(base)) return 'component'
63
67
  if (AGGREGATE_FILE_PATTERN.test(base)) return 'aggregate'
64
68
  if (INTEGRATION_FILE_PATTERN.test(base)) return 'integration'
65
69
  if (STARTUP_FILE_PATTERN.test(base)) return 'startup'
66
70
  if (EMAIL_FILE_PATTERN.test(base)) return 'email'
67
71
  if (ACTION_FILE_PATTERN.test(base)) return 'action'
72
+ if (FORM_FILE_PATTERN.test(base)) return 'form'
68
73
  if (E2E_FILE_PATTERN.test(base)) return 'e2e'
74
+ if (FLOW_FILE_PATTERN.test(base)) return 'flow'
69
75
  if (LAYOUT_FILE_PATTERN.test(base)) return 'layout'
70
76
  return null
71
77
  }
@@ -167,16 +173,17 @@ export function discoverInstalledPackageEntries (projectRoot) {
167
173
  ...discoverFilesByPattern(packageSrcDir, PAGE_FILE_PATTERN),
168
174
  ...discoverFilesByPattern(packageSrcDir, API_FILE_PATTERN),
169
175
  ...discoverFilesByPattern(packageSrcDir, TASK_FILE_PATTERN),
170
- ...discoverFilesByPattern(packageSrcDir, RESOURCE_FILE_PATTERN),
176
+ ...discoverFilesByPattern(packageSrcDir, SCHEMA_FILE_PATTERN),
171
177
  ...discoverFilesByPattern(packageSrcDir, COMPONENT_FILE_PATTERN),
172
178
  ...discoverFilesByPattern(packageSrcDir, AGGREGATE_FILE_PATTERN),
173
179
  ...discoverFilesByPattern(packageSrcDir, INTEGRATION_FILE_PATTERN),
174
180
  ...discoverFilesByPattern(packageSrcDir, STARTUP_FILE_PATTERN),
175
181
  ...discoverFilesByPattern(packageSrcDir, EMAIL_FILE_PATTERN),
176
182
  ...discoverFilesByPattern(packageSrcDir, ACTION_FILE_PATTERN),
183
+ ...discoverFilesByPattern(packageSrcDir, FORM_FILE_PATTERN),
177
184
  ...discoverFilesByPattern(packageSrcDir, E2E_FILE_PATTERN),
178
- // Layouts are intentionally excluded: *.layout.jsx is an app-level
179
- // primitive only. Feature packages must never define layouts.
185
+ ...discoverFilesByPattern(packageSrcDir, FLOW_FILE_PATTERN),
186
+ ...discoverFilesByPattern(packageSrcDir, LAYOUT_FILE_PATTERN),
180
187
  ]
181
188
  for (const sourcePath of files) {
182
189
  const stat = fs.statSync(sourcePath)
@@ -222,21 +229,20 @@ export function discoverInstalledPackageEntries (projectRoot) {
222
229
  * @returns {Promise<PlatformFiles>}
223
230
  */
224
231
  export default async function getPlatformFiles (srcDir) {
225
- const out = { pages: [], apis: [], tasks: [], resources: [], components: [], aggregates: [], integrations: [], startups: [], emails: [], actions: [], e2es: [], layouts: [] }
226
- const bucketByKind = { page: 'pages', api: 'apis', task: 'tasks', resource: 'resources', component: 'components', aggregate: 'aggregates', integration: 'integrations', startup: 'startups', email: 'emails', action: 'actions', e2e: 'e2es', layout: 'layouts' }
232
+ const out = { pages: [], apis: [], tasks: [], schemas: [], components: [], aggregates: [], integrations: [], startups: [], emails: [], actions: [], forms: [], e2es: [], flows: [], layouts: [] }
233
+ const bucketByKind = { page: 'pages', api: 'apis', task: 'tasks', schema: 'schemas', component: 'components', aggregate: 'aggregates', integration: 'integrations', startup: 'startups', email: 'emails', action: 'actions', form: 'forms', e2e: 'e2es', flow: 'flows', layout: 'layouts' }
227
234
 
228
235
  // Aggregates and e2e tests are only discovered from installed packages — never
229
236
  // from the local `src/` — to avoid pulling in per-project duplicates of the
230
237
  // same aggregate class or test suite that is canonically owned by the feature
231
238
  // package.
232
- // Layouts are the opposite restriction: they are only discovered from the
233
- // app's own `src/`, never from installed packages. Feature packages are pure
234
- // content (pages, actions, tasks, etc.) and must never define layouts.
239
+ // Layouts are discovered from the app's `src/` and from installed packages
240
+ // (`@ossy/app` ships default layouts via `ossy.src`).
235
241
  const localFiles = [
236
242
  ...discoverFilesByPattern(srcDir, PAGE_FILE_PATTERN),
237
243
  ...discoverFilesByPattern(srcDir, API_FILE_PATTERN),
238
244
  ...discoverFilesByPattern(srcDir, TASK_FILE_PATTERN),
239
- ...discoverFilesByPattern(srcDir, RESOURCE_FILE_PATTERN),
245
+ ...discoverFilesByPattern(srcDir, SCHEMA_FILE_PATTERN),
240
246
  ...discoverFilesByPattern(srcDir, COMPONENT_FILE_PATTERN),
241
247
  ...discoverFilesByPattern(srcDir, INTEGRATION_FILE_PATTERN),
242
248
  ...discoverFilesByPattern(srcDir, STARTUP_FILE_PATTERN),
@@ -248,7 +254,7 @@ export default async function getPlatformFiles (srcDir) {
248
254
  const stat = fs.statSync(sourcePath)
249
255
  if (!stat.isFile() || stat.size === 0) continue
250
256
  const kind = classifyFile(sourcePath)
251
- if (!kind || kind === 'aggregate' || kind === 'e2e') continue
257
+ if (!kind || kind === 'aggregate' || kind === 'e2e' || kind === 'flow') continue
252
258
  out[bucketByKind[kind]].push({ kind, sourcePath })
253
259
  }
254
260