create-flowdular 0.3.2 → 0.4.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 (64) hide show
  1. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +25 -0
  2. package/agent-template/.agents/skills/integration-adapter/SKILL.md +116 -0
  3. package/agent-template/.agents/skills/module-new/SKILL.md +17 -2
  4. package/agent-template/.agents/skills/module-update/SKILL.md +14 -2
  5. package/agent-template/.agents/skills/release-eject-pr/SKILL.md +23 -26
  6. package/agent-template/.agents/skills/spec-interview/SKILL.md +16 -0
  7. package/agent-template/.agents/skills/translations-i18n/SKILL.md +2 -1
  8. package/agent-template/.agents/skills/ux-design/SKILL.md +4 -4
  9. package/agent-template/.ai/agents/sandbox/backend-engineer.md +9 -0
  10. package/agent-template/.ai/blueprints/agentic-module/README.md +15 -0
  11. package/agent-template/.ai/blueprints/agentic-module/allowed-paths.yaml +14 -0
  12. package/agent-template/.ai/blueprints/agentic-module/blueprint.json +19 -0
  13. package/agent-template/.ai/blueprints/agentic-module/gates.yaml +24 -0
  14. package/agent-template/.ai/blueprints/agentic-module/input.schema.json +18 -0
  15. package/agent-template/.ai/blueprints/agentic-module/plan.schema.json +35 -0
  16. package/agent-template/.ai/blueprints/agentic-module/required-files.yaml +28 -0
  17. package/agent-template/.ai/blueprints/agentic-module/spec-requirements.yaml +33 -0
  18. package/agent-template/.ai/blueprints/agentic-module/steps.yaml +68 -0
  19. package/agent-template/.ai/platform-capabilities.md +14 -11
  20. package/agent-template/.ai/references/catalog/module.json +2 -2
  21. package/agent-template/.ai/references/catalog/package.json +2 -2
  22. package/agent-template/.ai/references/catalog/spec/module.yaml +2 -2
  23. package/agent-template/.ai/references/catalog/src/client/CatalogItemForm.tsrx +4 -4
  24. package/agent-template/.ai/references/catalog/src/client/CatalogView.tsrx +28 -27
  25. package/agent-template/.ai/references/catalog.provenance.json +8 -8
  26. package/agent-template/.ai/rules/flowdular.md +2 -1
  27. package/agent-template/.ai/skills/README.md +1 -0
  28. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +25 -0
  29. package/agent-template/.ai/skills/integration-adapter/SKILL.md +121 -0
  30. package/agent-template/.ai/skills/module-new/SKILL.md +17 -2
  31. package/agent-template/.ai/skills/module-update/SKILL.md +14 -2
  32. package/agent-template/.ai/skills/release-eject-pr/SKILL.md +23 -26
  33. package/agent-template/.ai/skills/spec-interview/SKILL.md +16 -0
  34. package/agent-template/.ai/skills/translations-i18n/SKILL.md +2 -1
  35. package/agent-template/.ai/skills/ux-design/SKILL.md +4 -4
  36. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +25 -0
  37. package/agent-template/.claude/skills/integration-adapter/SKILL.md +116 -0
  38. package/agent-template/.claude/skills/module-new/SKILL.md +17 -2
  39. package/agent-template/.claude/skills/module-update/SKILL.md +14 -2
  40. package/agent-template/.claude/skills/release-eject-pr/SKILL.md +23 -26
  41. package/agent-template/.claude/skills/spec-interview/SKILL.md +16 -0
  42. package/agent-template/.claude/skills/translations-i18n/SKILL.md +2 -1
  43. package/agent-template/.claude/skills/ux-design/SKILL.md +4 -4
  44. package/agent-template/AGENTS.md +2 -1
  45. package/agent-template/CLAUDE.md +2 -1
  46. package/agent-template/docs/agent-contract.md +1 -1
  47. package/agent-template/docs/cli.md +8 -3
  48. package/agent-template/docs/configuration.md +29 -0
  49. package/agent-template/docs/design-system.md +112 -13
  50. package/agent-template/docs/module-distribution.md +10 -3
  51. package/agent-template/docs/modules.md +38 -1
  52. package/agent-template/docs/operations.md +2 -0
  53. package/agent-template/docs/sandbox.md +76 -1
  54. package/package.json +1 -1
  55. package/template/default/flowdular.json +2 -0
  56. package/template/default/modules/example/package.json +1 -1
  57. package/template/default/modules/example/src/client/NotesView.tsrx +12 -16
  58. package/template/default/modules/example/tests/module.test.ts +3 -2
  59. package/template/default/modules/example/translations/pl.json +3 -1
  60. package/template/default/package.json +1 -1
  61. package/template/default/platform/package.json +1 -1
  62. package/template/default/platform/scripts/dev.mjs +16 -1
  63. package/template/default/platform/src/generated/modules.client.ts +4 -0
  64. package/template/default/platform/src/generated/modules.server.ts +61 -4
@@ -11,7 +11,9 @@ shared primitives, tokens, and the rules for using them.
11
11
  Variable, IBM Plex Mono) are self-hosted through `@fontsource` packages.
12
12
  - `packages/client`: the application shell (sidebar, topbar, command palette,
13
13
  dashboard, contribution outlets). Shell-only layout lives in
14
- `src/shell/shell.css`.
14
+ `src/shell/shell.css`. Below 1200 px the sidebar starts as its icon rail and
15
+ below 960 px the context rail as its strip; either opens over the workspace
16
+ and leaves the stored preference alone (`src/shell/layout.ts`).
15
17
  - Modules: compose screens from `@flowdular/sdk/ui`. Module CSS may only add
16
18
  module-specific composites built on the tokens (example:
17
19
  `modules/agents/src/client/agents.css`).
@@ -35,9 +37,10 @@ shared primitives, tokens, and the rules for using them.
35
37
  Numbers in tables and KPIs are tabular (`.num`, `ui-kpi__value`).
36
38
  7. Layout containment is owned by the primitives, not by the screen. Children
37
39
  of `ui-view`, `ui-two-col`, `ui-grid-2`, and `ui-kpi-grid` are shrinkable
38
- tracks, long words wrap, and the workspace never scrolls horizontally. Wide
39
- content scrolls inside its own container (`ui-table-wrap`), so one long
40
- value can never push the page sideways.
40
+ tracks, long words wrap (inside a table they end in an ellipsis instead),
41
+ and the workspace never scrolls horizontally. Wide content scrolls inside
42
+ its own container (`ui-table-wrap`), so one long value can never push the
43
+ page sideways.
41
44
  8. `Kpi` is a stat tile. Its value is a number or a short state word; addresses,
42
45
  identifiers, and paths belong in `note` or a `ui-mono` line.
43
46
  9. A screen never splits its width between records and a form. Records own the
@@ -143,13 +146,9 @@ stays open exactly while another page follows, and `summary(page)` reads a
143
146
  `keysetPage` with the page index, size and first row but no page count, because
144
147
  nobody counted the set.
145
148
 
146
- Every column declares a semantic CSS `width`. Give the primary record and its
147
- description the largest share, medium shares to dates and identifiers, and the
148
- smallest share to counts and lifecycle state. In a table with row actions, data
149
- columns normally add up to about 90 percent; the shared 160 px action column
150
- uses the rest. In a read-only table, data columns add up to 100 percent. The
151
- table keeps these widths in loading, empty and populated states and scrolls
152
- horizontally below its minimum readable width.
149
+ Every column declares a `width` and a cell from the typed cells, and a table
150
+ narrower than its columns hides the least important ones before it scrolls.
151
+ The rules are in the Tables section below.
153
152
 
154
153
  The drawer holds one `ui-drawer__form`: fields scroll inside
155
154
  `ui-drawer__body`, and the primary action stays pinned in `ui-drawer__foot`.
@@ -170,6 +169,77 @@ settings are edited in its Drawer under Administration > Modules, as
170
169
  `SettingRow`s inside the drawer's Settings `ui-form__section`; a module
171
170
  without settings shows a one-line empty state there.
172
171
 
172
+ ## Tables
173
+
174
+ A table reads at any width: a cell never breaks a word, a narrow card hides
175
+ the columns that matter least and lists them under the row, and the table
176
+ scrolls only when even its essential columns do not fit.
177
+
178
+ **Cells.** A column's `cell` returns one of the typed cells, so every table
179
+ truncates, titles and aligns the same way. Inside `ui-table` text wraps only at
180
+ spaces; a value without one (an email, an id, an action code, a timestamp)
181
+ stays whole and ends in an ellipsis with the full value as its title.
182
+
183
+ | Cell | Use |
184
+ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
185
+ | `CellText` | One line of text: `value`, `strong` for the record's name, `mono` for a code or a path, `wrap` for prose (a description, a message, a list) that wraps at spaces up to two lines, `muted` for a description in the smaller secondary ink, `title` when the hover should say more than the value |
186
+ | `CellStack` | A primary line over a secondary one (a name over its id, a type over its identifier, a state over its note): `primary`, `secondary`, `primaryMono`, `secondaryMono`; a line may be another cell |
187
+ | `CellTime` | A timestamp: `value` as an ISO string, a `YYYY-MM-DD` day or epoch milliseconds, `kind` datetime (default) or date, `timeZone` for a value read in a zone of its own (a schedule's). Compact (`16 Sep, 19:39`, the year only when it is not this year), the hour cycle of the locale, a `<time datetime>` with the full timestamp and seconds as its title |
188
+ | `CellCode` | An identifier in monospace: `value`, `max` (16) past which it keeps its head and tail (`4783b5b2…40d8`), `copy` (true) for the copy button that shows on row hover and focus |
189
+ | `CellTag` | A state or a value with meaning: `label`, `tone`, `dot`, `mono`; the label ends in an ellipsis instead of being cut |
190
+ | `CellNumber` | A count or an amount: `value`, `options` for `Intl.NumberFormat`; a string `value` arrives formatted (`1.2K`, `<$0.01`, `3/5`) and shows as it came; the column declares `numeric` for the right alignment |
191
+ | `CellMuted` | The placeholder for a missing value: "None", "Never"; `title` for the hover reading, such as why the value is missing |
192
+
193
+ **Widths.** `width` stays required. Give a bounded column a px width: a time
194
+ 170 px, a status or a tag 120 to 140 px, a number 100 to 140 px, an id or a
195
+ version 120 to 200 px. Give `auto` to the one column that takes the rest,
196
+ usually the record's identity or its description. Percentages still work and
197
+ count as flexible columns.
198
+
199
+ **Priority.** `TableColumn.priority` is 1 (default), 2 or 3:
200
+
201
+ - 1: the identity, the state and the main value. Always visible. The first
202
+ column is always 1.
203
+ - 2: secondary ids, counts and timestamps.
204
+ - 3: details and descriptions.
205
+
206
+ The table wrapper is a size container, and the table computes from the declared
207
+ widths where each column stops fitting: px columns as declared, 200 px per
208
+ flexible column, the selection and action columns, and 28 px for the expand
209
+ button once something hides, rounded up to a 40 px step between 480 and 1600
210
+ px (`table-steps.css`). Columns hide one at a time, priority 3 before 2 and the
211
+ last declared first; each hides below the width it and every column still
212
+ beside it need. The table scrolls only below its minimum, counted from priority
213
+ 1 alone at 120 px per flexible column, plus the selection column, the folded
214
+ action column and the expand button. The container queries decide; the only
215
+ measurement is the action column, which grows to its widest row of buttons so a
216
+ longer translation is never cut.
217
+
218
+ **Row expansion.** While any column is hidden, every row starts with an expand
219
+ button (`aria-expanded`). An expanded row lists exactly the columns hidden at
220
+ that width as label and value pairs, rendered by the same cells, so nothing is
221
+ lost and nothing is cut. Widening the card brings the columns back and empties
222
+ the list.
223
+
224
+ **Row actions.** Up to two actions are compact buttons. Two actions fold into
225
+ one More button when the card is narrower than three times the action column
226
+ or than the narrowest table beside it; more than two actions always sit in the
227
+ More menu, and the column then keeps the width of that one button. The menu
228
+ opens over the page, arrows, Home and End move between items, Enter or Space
229
+ runs one, and Escape or Tab closes it and returns focus to the button. A refused
230
+ action stays in the menu, announced as unavailable, with its `reason`.
231
+
232
+ **Clickable rows.** A table with `onSelect` makes its first cell a button, so
233
+ Tab reaches the row and Enter or Space opens it. That column holds text, never
234
+ a control; a `CellCode` there leaves out its copy button.
235
+
236
+ **Headers** stay one line and end in an ellipsis with the label as their title.
237
+
238
+ **Copy and locale.** The expand, collapse, More, copy and copied labels and the
239
+ locale for `CellTime` and `CellNumber` come from `TableLocaleContext`, which the
240
+ shell provides from its own `shell.table.*` bundle. A screen passes none of
241
+ them; outside the shell the English defaults and the host locale apply.
242
+
173
243
  ## Components
174
244
 
175
245
  | Component | Use |
@@ -179,10 +249,11 @@ without settings shows a one-line empty state there.
179
249
  | `VariableTextarea` | Multiline template field: `value`, `onInput`, `variables` (scope-filtered `VariableDefinition[]`), `sampleValues`, `label`, `name`; a `braces` menu inserts `{{ key }}` and tokens highlight as pills (error pill when unknown). Presentational, never fetches |
180
250
  | `VariableInput` | Single-line variant of `VariableTextarea` with the same props |
181
251
  | `VariableSelect` | Native select that stores either a literal option value or one allowed `{{ key }}` token. Takes scope-filtered `variables`, `sampleValues`, literal `options`, `value`, `onInput`, `name`, `label`, and native required/disabled state. It preserves keyboard, validation, accessibility, and `FormData` semantics and never fetches or resolves data |
252
+ | `CellText` | One of the typed table cells (`CellText`, `CellStack`, `CellTime`, `CellCode`, `CellTag`, `CellNumber`, `CellMuted`); see Tables |
182
253
  | `Tag` | Status and metadata: `tone` neutral, success, warning, danger, info, ink; `dot` adds a state dot; `mono` |
183
254
  | `Kpi` | Stat tile: `label`, `value`, `unit`, `badge`, `note`, `href`, `linkLabel` |
184
255
  | `Chart` | Token-driven Chart.js wrapper on a client-only canvas: `type` area, bar, line; `data`, `series` (`key`, `label`, `token`), `xKey`, `height`, `title`, `xTickFormatter`; series colors come from `--chart-1..5`; shows an EmptyState for empty or all-zero data |
185
- | `Table` | The one data table, backed by `@octanejs/tanstack-table`: `columns` (`key`, `header`, required `width`, `numeric`, `value`, `cell`), `rows`, `rowKey`, `status` idle/loading, `loadingLabel`, `empty` and `emptyFiltered` picked by `filtered`, `sorting` with `sortingState` and `onSortingChange`, `globalFilter`, `pagination`, `mode` client (default) or server, `actions(row): TableAction[]` (`id`, `label`, `icon`, `tone`, `disabled`, `reason`), `actionsLabel`, optional stable `actionsWidth` (160 px by default, 280 px for two actions), `onSelect` with `selectedKey`, `selection` (`selectedKeys`, `onSelectionChange`, `label`, `rowLabel`, `selectable`, `clearLabel`) with `bulkActions: TableBulkAction[]` (`id`, `label`, `icon`, `tone`, `disabled`, `reason`, `onSelect(keys)`) and `selectionSummary(count)`, `caption`; fixed layout and `colgroup` keep columns stable across states. Selection is a leading checkbox column; the header checkbox toggles the rows on screen and reads mixed while some are selected; the bulk bar sits above the table only while something is selected and its clear button empties the whole set through `onSelectionChange`. The screen owns the selected keys and resets them on a page, sort or filter change |
256
+ | `Table` | The one data table, backed by `@octanejs/tanstack-table`: `columns` (`key`, `header`, required `width`, `numeric`, `value`, `cell`), `rows`, `rowKey`, `status` idle/loading, `loadingLabel`, `empty` and `emptyFiltered` picked by `filtered`, `sorting` with `sortingState` and `onSortingChange`, `globalFilter`, `pagination`, `mode` client (default) or server, `actions(row): TableAction[]` (`id`, `label`, `icon`, `tone`, `disabled`, `reason`), `actionsLabel`, optional minimum `actionsWidth` (160 px default, grows to fit its buttons), `onSelect` with `selectedKey`, `selection` (`selectedKeys`, `onSelectionChange`, `label`, `rowLabel`, `selectable`, `clearLabel`) with `bulkActions: TableBulkAction[]` (`id`, `label`, `icon`, `tone`, `disabled`, `reason`, `onSelect(keys)`) and `selectionSummary(count)`, `caption`; fixed layout and `colgroup` keep columns stable across states. Selection is a leading checkbox column; the header checkbox toggles the rows on screen and reads mixed while some are selected; the bulk bar sits above the table only while something is selected and its clear button empties the whole set through `onSelectionChange`. The screen owns the selected keys and resets them on a page, sort or filter change |
186
257
  | `TableCard` | The record card around `Table`: `title`, `count`, the `head`, `search`, and `filters` head slots, `before` and `after` around the table, `note` with `noteIcon` as the footer |
187
258
  | `Pagination` | The pager for the `TableCard` `after` slot: `pageIndex`, `pageSize`, `totalRows` or `hasMore` for a keyset page nobody counted, `onPageChange`, `pageSizes` with `onPageSizeChange` (the type couples the pair, so one cannot arrive without the other), `label`, `previousLabel`, `nextLabel`, `pageSizeLabel`, and `summary(range)` that the screen translates |
188
259
  | `Select` | The labelled native select: required `id`, `label`, `options` (`value`, `label`, `disabled`), `value`, `onChange`, `placeholder`, `name` (defaults to `id`), `required`, `disabled`, `autoFocus`, `invalid`, `help`, `error` |
@@ -191,6 +262,7 @@ without settings shows a one-line empty state there.
191
262
  | `DatePicker` | The calendar picker: required `id`, `label`, `openLabel`, `previousMonthLabel`, `nextMonthLabel`, `value` (`from`, `to`), `onChange`, `mode` single (default) or range, `kind`, `locale`, `min`, `max`, `presets` with `presetsLabel`, `fromLabel` and `toLabel` for a range, `name`, `required`, `disabled`, `autoFocus`, `invalid`, `help`, `error`; one popover holds the presets and the month grid |
192
263
  | `FileUpload` | The file control: required `id`, `label`, `value`, `onChange`, `refusal(reason, file)`, `chosen(file)`, `clearLabel`, plus `accept`, `maxBytes`, `hint`, `busy` with `busyLabel`, `error`, `name`, `required`, `disabled`, `autoFocus`; it refuses an unaccepted type and a file over `maxBytes` and hands the screen back `null` |
193
264
  | `Tabs` | Accessible tablist: required `id`, `items` (`id`, `label`, `disabled`), `active`, `onChange`, `label`; arrows, Home and End move roving focus, an `active` that names no enabled tab selects the first enabled one, and the caller renders the panel |
265
+ | `SortableList` | Reorderable list: required `id`, `label`, `items` (`id`, `label`), `onReorder(ids)`, `handleLabel(item)`, `instructions`, `announce(event)`, `renderItem(item, index)`, `disabled`; a grip per item drags with a pointer or picks up, moves and drops from the keyboard, with a polite live region |
194
266
  | `ToastHost` | Renders the toast queue: `label`, `closeLabel`, optional `store`. Raise toasts with `toasts.success/error/info(message)`; `createToastStore` makes a scoped queue |
195
267
  | `PageHeader` | Every view starts with it: `eyebrow`, `title`, `description`; children render as right-side actions |
196
268
  | `EmptyState` | `icon`, `title`, children, optional `code` |
@@ -269,6 +341,20 @@ aria-labelledby={id + '-tab-' + active}>`. Arrow keys move focus and selection
269
341
  over enabled tabs and wrap, Home and End jump to the ends, and only the active
270
342
  tab is in the tab order, so Tab leaves the list for the panel.
271
343
 
344
+ `SortableList` renders an ordered list whose order the screen owns: it never
345
+ reorders `items` itself and calls `onReorder` once per committed drop with the
346
+ whole new order, never for a cancel or an order that did not change. Each item
347
+ has a grip button named by `handleLabel` and described by `instructions`. From
348
+ the keyboard, Space or Enter picks the item up, ArrowUp and ArrowDown move it,
349
+ Space or Enter drops it, and Escape or moving focus away puts it back; focus
350
+ stays on the moved grip. With a pointer, the grip drags the item, a line marks
351
+ where it lands, releasing commits, and a cancelled pointer or Escape puts it
352
+ back. Every step reaches a polite live region through `announce`, which gets
353
+ the kind (`lifted`, `moved`, `dropped`, `cancelled`), the item label and its
354
+ position of the total, so the screen supplies the words. `disabled` makes every
355
+ grip inert, which is the loading and denied reading; an empty `items` renders an
356
+ empty list, so the screen shows its own empty state instead.
357
+
272
358
  `Toast` is a transient confirmation of something the reader just did, never a
273
359
  state a screen must keep showing: a failure that blocks work stays in `Alert`.
274
360
  A screen that raises toasts renders one `ToastHost`; the region keeps its place
@@ -295,7 +381,7 @@ Icon names (`packages/ui/src/icons/Icon.tsrx`): `dashboard`, `parties`, `catalog
295
381
  `flask`, `activity`, `plug`, `search`, `chevron-down`, `chevron-left`,
296
382
  `chevron-right`, `chevrons-up-down`, `sort`, `calendar`,
297
383
  `plus`, `panel-left`, `check`, `filter`, `download`, `more`, `external`,
298
- `alert`, `x`, `sign-out`, `refresh`, `help`, `info`, `key`, `settings`, `braces`. An unknown name renders
384
+ `alert`, `x`, `sign-out`, `refresh`, `help`, `info`, `key`, `settings`, `braces`, `copy`. An unknown name renders
299
385
  `modules` without a warning; a new icon is one 24x24 stroke path added there.
300
386
 
301
387
  ## Classes
@@ -346,7 +432,20 @@ Rendered by components, not written by hand: `ui-page-head*`, `ui-search`,
346
432
  `__head`, `__month`, `__step`, `__grid`, `__day` (+`--outside`, `--between`,
347
433
  `--on`)), `ui-fileupload` (+`__control`, `__input`, `__clear`),
348
434
  `ui-table-action` (the wrapper carrying a refused action's title),
435
+ `ui-table__lead` with `ui-table__toggle` and `ui-table__open` (the first cell's
436
+ expand and open buttons), `ui-table__hide-*` (a column hidden below that width),
437
+ `ui-table__reveal-*` (the expand button, details list and detail shown below
438
+ that width), `ui-table--fold-*` (the action fold step),
439
+ `ui-table__placeholder-body` (the loading and empty row content),
440
+ `ui-table__details` (+`-list`), `ui-table__detail`,
441
+ `ui-table-more` (+`--always`), `ui-table-menu` (+`__scrim`, `__label`),
442
+ `ui-cell-text` (+`--strong`, `--mono`, `--muted`, `--wrap`), `ui-cell-stack`
443
+ (+`__primary`, `__secondary`, `__line--mono`), `ui-cell-time`, `ui-cell-code`
444
+ (+`__value`, `__copy`, `__copy--done`), `ui-cell-number`, `ui-cell-muted`,
445
+ `ui-tag__label`,
349
446
  `ui-tabs` (+`__tab`, `__tab--on`),
447
+ `ui-sortable` (+`__item`, `__item--lifted`, `__item--dragging`,
448
+ `__item--drop-before`, `__item--drop-after`, `__handle`, `__body`),
350
449
  `ui-toasts` with `ui-toast` (+`--success`, `--error`, `--info`, `__message`,
351
450
  `__close`), `ui-varfield` (+`__control`, `__highlight`, `__input`, `__pill`
352
451
  (+`--error`), `__trigger`, `__menu`, `__value`, `__empty`; the overlay layer
@@ -8,8 +8,8 @@ release artifacts. The landing is in [Flowdular/landing](https://github.com/Flow
8
8
  ```sh
9
9
  pnpm flowdular module search expenses
10
10
  pnpm flowdular module info expenses.core
11
- pnpm flowdular module install expenses.core@0.8.0
12
- pnpm flowdular module install expenses.core@0.8.0 --apply
11
+ pnpm flowdular module install expenses.core@0.8.1
12
+ pnpm flowdular module install expenses.core@0.8.1 --apply
13
13
  pnpm flowdular module enable expenses.core --apply
14
14
  pnpm flowdular module validate --locked
15
15
  ```
@@ -31,7 +31,7 @@ The installer resolves a consistent dependency closure, including diamond
31
31
  constraints, within a bounded search budget. Existing workspace/package versions
32
32
  are preserved. Registry and runtime validation share semver semantics, including
33
33
  pre-1.0 caret ranges. Every module declares `platformApi` as a range
34
- (`^0.1.0`); the current platform API is `0.1.3` and `module search --compatible`
34
+ (`^0.1.0`); the current platform API is `PLATFORM_API_VERSION` in `packages/contracts/src/index.ts` and `module search --compatible`
35
35
  filters releases by it. A release that declares `requires` is resolved together
36
36
  with the newest compatible release providing each required capability.
37
37
 
@@ -93,4 +93,11 @@ authentication. It skips already-published identical tarballs and stops if a
93
93
  version exists with different bytes. No npm publication is performed by packing,
94
94
  smoke testing or the default publication preview.
95
95
 
96
+ The `SDK consumer smoke` workflow (`.github/workflows/sdk-release.yml`, job
97
+ `consumer`) runs the pack and the smoke on every pull request and push to main
98
+ that touches `packages`, `modules` or `scripts`. It never publishes: its token
99
+ can only read the repository, and the packed tarballs are kept as an Actions
100
+ artifact only after a merge to main. `pnpm verify` is not repeated there, the
101
+ CI workflow owns it.
102
+
96
103
  The SDK is assembled from private internal workspaces. Import UI from `@flowdular/sdk/ui` and styles from `@flowdular/sdk/ui/styles`; use `@flowdular/sdk/server`, `client`, `contracts` or `modules/<name>` for other surfaces. There is no root SDK barrel, so browser imports do not load server entrypoints. See [npm publication](https://github.com/flowdular/flowdular/blob/main/docs/npm-publication.md).
@@ -39,6 +39,18 @@ spec instead of scanning the repository. Every section is optional and a
39
39
  `agentTools[]`: `id`, `permission`, `description`, `risk`.
40
40
  - `outOfScope[]` records what is deliberately not built; `decisions[]` records
41
41
  each interview question, its answer and whether a user or a default decided.
42
+ - `research`: `adapter` (`model-native`, `searxng`, `firecrawl`, `connector`
43
+ or `recorded`),
44
+ `allowDomains`, `denyDomains`, `monthlyQueryBudget`, and `evidenceOwner`, the
45
+ entity whose records evidence attaches to through `research.core`.
46
+ - `adapters[]`: `id` (prefixed with the module id), `direction` (`source` or
47
+ `sink`), `connector` and `operation` (a `connectors.core` definition and
48
+ operation key), `port` (an import port id for a source, a list export id for a
49
+ sink), `schedule` (a five-field cron or `null`), `mapping[]` (`from`, `to`,
50
+ `transform` of `rename`, `constant`, `format` or `lookup`, `value`) and
51
+ `recorded` (`adapters/<name>.recorded.json`).
52
+ - `templates[]`: `id`, `title`, `inputEntity`, `format` (`pdf` or `docx`) and
53
+ `body` (`templates/<name>.md`).
42
54
 
43
55
  Validation is more than the schema: an action permission must exist in
44
56
  `permissions`, every `entity` must name an entity, screen `columns` and
@@ -53,6 +65,19 @@ at least one entity (`SPEC_ACTION_PERMISSION_UNKNOWN`, `SPEC_ENTITY_UNKNOWN`,
53
65
  `SPEC_DUPLICATE_ID`). A client without a list screen, or a stored entity with no
54
66
  tenant-unique field, is a warning.
55
67
 
68
+ The three optional sections have checks of their own. `research.evidenceOwner`
69
+ and `templates[].inputEntity` must name an entity (`SPEC_ENTITY_UNKNOWN`). An
70
+ adapter id must start with the module id (`SPEC_ADAPTER_ID_NAMESPACE`); a source
71
+ adapter's `port` must belong to this module or a declared dependency
72
+ (`SPEC_ADAPTER_PORT_UNKNOWN`); `schedule` must be a cron `automations.core`
73
+ accepts: minute, hour, day of month, month and day of week, each `*`, a number,
74
+ a three letter month or weekday name, a list, a range or a step
75
+ (`SPEC_ADAPTER_SCHEDULE_INVALID`); and a mapping needs `from` unless it is a
76
+ `constant` and `value` unless it is a `rename` (`SPEC_ADAPTER_MAPPING_INVALID`).
77
+ `recorded` stays optional here; a sandbox session refuses a live adapter in its
78
+ own `spec-schema` gate (`SANDBOX_LIVE_ADAPTER_REFUSED`, see
79
+ [sandbox.md](sandbox.md)).
80
+
56
81
  ### 2. Scaffold
57
82
 
58
83
  ```bash
@@ -82,6 +107,15 @@ lifecycle field is set by the service), and the columns of the first `list`
82
107
  screen become the table view. Further entities are the implementing agent's
83
108
  work.
84
109
 
110
+ The optional sections scaffold declarations and fixtures, nothing that runs:
111
+ `src/research.ts` and `research-fixtures.json` (one example query and page in
112
+ the shape the recorded research adapter reads) for `research`; per adapter one
113
+ `src/adapters/<name>.ts` (the id without the module id, dots turned into
114
+ hyphens) and one recorded fixture stub at the declared `recorded` path, else
115
+ `adapters/<name>.recorded.json`; and one `templates/<name>.md` per template
116
+ body, with `templates` added to the package `files`. Implementing them follows the `module-new` and `integration-adapter`
117
+ skills.
118
+
85
119
  Files are written through the workspace Prettier, so the format gate passes
86
120
  without a rewrite. A directory that already holds `spec/module.yaml` or
87
121
  `translations/**` is extended, not rejected, and a failed run leaves nothing
@@ -119,7 +153,10 @@ pnpm --filter @flowdular/module-<dir> test
119
153
  `module validate` checks more than the schema: `platform.server` requires
120
154
  `src/platform.ts` and a `./platform` export, `platform.client` requires
121
155
  `src/client/index.ts` and a `./client` export, and every declared locale needs a
122
- `translations/<locale>.json` with the same key set as the others (an error).
156
+ `translations/<locale>.json` with the same key set as the others (an error). A
157
+ plural family such as `count.one`/`count.other` in `en` and
158
+ `count.one`/`count.few`/`count.many`/`count.other` in `pl` counts as one key,
159
+ and each locale must carry the categories its plural rules select.
123
160
  `module.json` version drift against `specVersion`, or a locale missing from
124
161
  `flowdular.json`, is reported as a warning.
125
162
 
@@ -356,6 +356,8 @@ variable falls back to the default and logs one warning about it.
356
356
  ## Production checklist
357
357
 
358
358
  - `pnpm verify` and `pnpm build` pass on the commit being shipped.
359
+ The container image only builds; it does not run the tests again, so a tag
360
+ must point at a commit whose CI verify passed.
359
361
  - `FD_ENV=production`, `FD_DATABASE_ADAPTER=postgresql`, separate
360
362
  `FD_DATABASE_URL` and `FD_DATABASE_MIGRATOR_URL` roles, neither `SUPERUSER`
361
363
  nor `BYPASSRLS` on the runtime role.
@@ -65,6 +65,79 @@ so a workspace can change them.
65
65
  When the work is done, eject it into `modules/` and enable it, or open a pull
66
66
  request with the gate evidence attached.
67
67
 
68
+ ## Sample data and recorded adapters
69
+
70
+ A session can carry a sample of the data the module will really see: attach a
71
+ CSV, JSON or plain text file (`.csv`, `.json`, `.txt`) to the brief or the
72
+ composer, within the attachment limits (5 MB per file, 10 per session). The file
73
+ stays in the session directory, is never sent to the connected application, and
74
+ is deleted with the session.
75
+
76
+ Every turn reads the sample through the read-only sandbox tool `sample-data`.
77
+ Without input it lists each sample file with its columns, row count and a
78
+ parsed preview of the first 20 rows; `{ "name": "customers.csv" }` previews one
79
+ file. CSV is parsed with quoted fields and a detected `,`, `;` or tab delimiter,
80
+ JSON from a top-level array or the first array inside an object, text as lines.
81
+ A value is cut at 200 characters and a row at 40 columns; one preview stays
82
+ under 32 KB and the listing under 64 KB, listing a file that does not fit
83
+ without its rows. The BYOK driver offers the tool to the model. The local
84
+ `claude` and `codex` drivers have no tool channel, so the same listing is
85
+ written to `reference/sample-data.json` for the turn.
86
+
87
+ The backend engineer derives the module's fixtures from the sample:
88
+ `tests/fixtures/*.json` for the tests and `preview/seed.json` for the preview. It
89
+ keeps the shape and replaces real names, contacts and identifiers with invented
90
+ values, because fixtures ship with the module. The role may write `preview/**`,
91
+ `src/preview.ts`, `research-fixtures.json` and `adapters/**`.
92
+
93
+ ### Seeding the preview
94
+
95
+ When a draft module has both `preview/seed.json` (at most 1 MB) and
96
+ `src/preview.ts` exporting `seed`, the preview calls it once the generation has
97
+ started:
98
+
99
+ ```ts
100
+ export async function seed(context: {
101
+ readonly tenantId: string; // the preview workspace
102
+ readonly accountId: string; // the preview account
103
+ readonly data: unknown; // parsed preview/seed.json
104
+ readonly databases: DatabaseProvider; // the preview's own provider
105
+ }): Promise<void>;
106
+ ```
107
+
108
+ `seed()` writes through the module's own repository, as the server composition
109
+ does. The sandbox keeps a hash of both files in the session data directory and
110
+ calls `seed()` again only when one of them changes, so it must be idempotent
111
+ (upsert by the natural key). A failure is reported as the preview error of that
112
+ module and never stops the preview. The preview does not seed through an
113
+ `import.ports.v1` port: only `import.core` can read its port registry (the
114
+ public capability registers ports), a port write needs a full principal and a
115
+ job, and most drafts do not compose `import.core`.
116
+
117
+ ### Recorded adapters
118
+
119
+ A draft spec with a `research` section previews through `research.core`. The
120
+ preview composes it from the platform modules ahead of the drafts and holds
121
+ `research.core.adapter` at `recorded`, the adapter chain at the recorded adapter
122
+ alone (`searchOrder` at `recorded` with `recordedEnabled` on, `fetchOrder` at
123
+ `direct`) and `research.core.recordedFixturesPath` at the absolute path of the
124
+ module's `research-fixtures.json` for every workspace. A chain holding the
125
+ recorded adapter reads pages from the same file before it looks at the fetch
126
+ order, so nothing reaches the network. Changing any of them answers
127
+ `409 SANDBOX_LIVE_ADAPTER_REFUSED`. When several drafts declare research, the
128
+ first one in session order supplies the path. An entry of the spec's `adapters`
129
+ section reads the fixture its `recorded` field names, by convention
130
+ `adapters/<id>.recorded.json`.
131
+
132
+ A session declares only recorded adapters; an owner connects a live search or
133
+ connector instance after delivery. When a draft spec sets `research.adapter` to
134
+ `model-native`, `searxng`, `firecrawl` or `connector`, or lists an adapter
135
+ without `recorded`, the `spec-schema` gate fails with
136
+ `SANDBOX_LIVE_ADAPTER_REFUSED` and names the file and field, so the turn goes
137
+ back to its specialist and delivery stops. The preview refuses to compose such a
138
+ session with the same code before a worker starts. A `research` section without
139
+ `adapter` is previewed on the recorded fixtures.
140
+
68
141
  ## Deliver as a pull request
69
142
 
70
143
  The eject route (`POST /sandbox/api/sessions/:id/eject`) takes `target:
@@ -81,7 +154,9 @@ request time (`delivery/configuration.ts`): `targets`, `default`,
81
154
  `maxChangedFiles` and `git` with `remote`
82
155
  (`origin`), `repository` (`owner/name`, derived from the remote when null),
83
156
  `baseBranch` (`main`), `branchPrefix` (`sandbox`), `provider` (`github` or
84
- `none`), `mode` (`auto`, `direct` or `fork`), `forkOwner` and `reviewers`.
157
+ `none`), `mode` (`auto`, `direct` or `fork`), `forkOwner`, `reviewers` and
158
+ `labels` (added to the pull request after creation, default
159
+ `sandbox-delivery`; a label the repository lacks never fails a delivery).
85
160
 
86
161
  Operator settings live in the sandbox configuration
87
162
  (`.flowdular/sandbox/config.json`, `GitHubDeliveryConfiguration` in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-flowdular",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "Scaffold a Flowdular application: the platform, one example module and the secrets a fresh install needs.",
6
6
  "license": "MIT",
@@ -12,6 +12,7 @@
12
12
  "system.core",
13
13
  "auth.core",
14
14
  "access.core",
15
+ "adapters.core",
15
16
  "reports.core",
16
17
  "metering.core",
17
18
  "agents.core",
@@ -26,6 +27,7 @@
26
27
  "import.core",
27
28
  "notifications.core",
28
29
  "profile.core",
30
+ "research.core",
29
31
  "sandbox.core",
30
32
  "search.core",
31
33
  "users.core",
@@ -16,7 +16,7 @@
16
16
  "dependencies": {
17
17
  "octane": "0.1.51",
18
18
  "segment-state": "0.2.1",
19
- "@flowdular/sdk": "0.3.2"
19
+ "@flowdular/sdk": "0.4.1"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@tsrx/typescript-plugin": "0.3.120",
@@ -2,6 +2,9 @@ import { useEffect, useMemo } from 'octane';
2
2
  import {
3
3
  Alert,
4
4
  Button,
5
+ CellStack,
6
+ CellText,
7
+ CellTime,
5
8
  Drawer,
6
9
  FormField,
7
10
  Icon,
@@ -31,20 +34,18 @@ function columns(): readonly TableColumn<Note>[] {
31
34
  {
32
35
  key: 'note',
33
36
  header: t('example.table.column.note'),
34
- width: '70%',
35
- cell: (note) => <span class="ui-cell">
36
- <b>{note.title}</b>
37
- <span class="ui-cell__muted">{note.body}</span>
38
- </span>,
37
+ width: 'auto',
38
+ cell: (note) =>
39
+ <CellStack
40
+ primary={note.title}
41
+ secondary={<CellText value={note.body} muted />}
42
+ />,
39
43
  },
40
44
  {
41
45
  key: 'created',
42
46
  header: t('example.table.column.created'),
43
- width: '30%',
44
- cell: (note) => new Intl.DateTimeFormat(activeLocale(), {
45
- dateStyle: 'medium',
46
- timeStyle: 'short',
47
- }).format(new Date(note.createdAt)),
47
+ width: '170px',
48
+ cell: (note) => <CellTime value={note.createdAt} />,
48
49
  },
49
50
  ];
50
51
  }
@@ -152,12 +153,7 @@ export function NotesView(props: NotesViewProps) @{
152
153
  }
153
154
  <TableCard
154
155
  title={t('example.table.title')}
155
- count={t(
156
- notes.length === 1
157
- ? 'example.table.count.one'
158
- : 'example.table.count.other',
159
- { count: notes.length },
160
- )}
156
+ count={t('example.table.count', { count: notes.length })}
161
157
  search={<SearchField
162
158
  value={query}
163
159
  placeholder={t('example.table.search.placeholder')}
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { describe, expect, it } from 'vitest';
3
+ import { translationKeys } from '@flowdular/sdk/contracts';
3
4
  import manifest from '../module.json' with { type: 'json' };
4
5
  import translationsEn from '../translations/en.json' with { type: 'json' };
5
6
  import translationsPl from '../translations/pl.json' with { type: 'json' };
@@ -28,8 +29,8 @@ describe('example.core manifest', () => {
28
29
 
29
30
  describe('example.core translations', () => {
30
31
  it('ships the same key set in every declared locale', () => {
31
- expect(Object.keys(translationsPl).sort()).toEqual(
32
- Object.keys(translationsEn).sort(),
32
+ expect(translationKeys(translationsPl)).toEqual(
33
+ translationKeys(translationsEn),
33
34
  );
34
35
  expect(manifest.locales).toEqual(['en', 'pl']);
35
36
  });
@@ -9,7 +9,9 @@
9
9
  "page.action.new": "Nowa notatka",
10
10
  "table.title": "Notatki",
11
11
  "table.count.one": "{count} notatka",
12
- "table.count.other": "{count} notatek",
12
+ "table.count.few": "{count} notatki",
13
+ "table.count.many": "{count} notatek",
14
+ "table.count.other": "{count} notatki",
13
15
  "table.column.note": "Notatka",
14
16
  "table.column.created": "Utworzono",
15
17
  "table.search.placeholder": "Szukaj notatek",
@@ -25,7 +25,7 @@
25
25
  "devDependencies": {
26
26
  "@tsrx/prettier-plugin": "0.3.120",
27
27
  "prettier": "3.6.2",
28
- "flowdular": "0.3.2",
28
+ "flowdular": "0.4.1",
29
29
  "rulesync": "16.21.0"
30
30
  }
31
31
  }
@@ -15,7 +15,7 @@
15
15
  "@octanejs/vite-plugin": "0.1.51",
16
16
  "octane": "0.1.51",
17
17
  "pg": "8.23.0",
18
- "@flowdular/sdk": "0.3.2"
18
+ "@flowdular/sdk": "0.4.1"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@octanejs/app-core": "0.0.47",
@@ -10,6 +10,14 @@ import {
10
10
  } from '@flowdular/sdk/dev-console';
11
11
 
12
12
  const appRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
13
+ /* `pnpm dev -- --port 4396 --host 0.0.0.0` overrides vite.config.ts, so a
14
+ second application runs beside the first one. */
15
+ const flag = (name) => {
16
+ const index = process.argv.indexOf(name);
17
+ return index === -1 ? undefined : process.argv[index + 1];
18
+ };
19
+ const port = Number(flag('--port'));
20
+ const host = flag('--host');
13
21
  const verbose =
14
22
  process.argv.includes('--verbose') ||
15
23
  process.argv.includes('-v') ||
@@ -24,6 +32,11 @@ try {
24
32
  configFile: resolve(appRoot, 'vite.config.ts'),
25
33
  customLogger: createOctaneLogger(verbose, color),
26
34
  clearScreen: false,
35
+ ...(Number.isInteger(port) && port > 0
36
+ ? { server: { port, strictPort: true, ...(host ? { host } : {}) } }
37
+ : host
38
+ ? { server: { host } }
39
+ : {}),
27
40
  });
28
41
  await server.listen();
29
42
  } catch (error) {
@@ -37,7 +50,9 @@ printReady({
37
50
  lines: [
38
51
  [
39
52
  'local',
40
- server.resolvedUrls?.local?.[0] ?? 'http://localhost:4310/',
53
+ server.resolvedUrls?.local?.[0] ??
54
+ server.resolvedUrls?.network?.[0] ??
55
+ 'the address vite.config.ts sets',
41
56
  'info',
42
57
  ],
43
58
  ['diagnostics', verbose ? 'verbose' : 'quiet · use --verbose', 'muted'],