create-flowdular 0.2.6 → 0.3.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 (103) hide show
  1. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
  2. package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
  3. package/agent-template/.agents/skills/cli-extension/SKILL.md +1 -1
  4. package/agent-template/.agents/skills/deploy-operate/SKILL.md +114 -0
  5. package/agent-template/.agents/skills/module-new/SKILL.md +29 -0
  6. package/agent-template/.agents/skills/module-update/SKILL.md +9 -1
  7. package/agent-template/.agents/skills/spec-interview/SKILL.md +114 -0
  8. package/agent-template/.agents/skills/ux-design/SKILL.md +34 -3
  9. package/agent-template/.ai/README.md +2 -1
  10. package/agent-template/.ai/agents/sandbox/business-manager.md +5 -1
  11. package/agent-template/.ai/blueprints/author-spec/README.md +1 -1
  12. package/agent-template/.ai/blueprints/author-spec/spec-requirements.yaml +44 -0
  13. package/agent-template/.ai/blueprints/author-spec/steps.yaml +5 -5
  14. package/agent-template/.ai/blueprints/author-spec/templates/module.yaml +99 -3
  15. package/agent-template/.ai/blueprints/edit-module/gates.yaml +4 -0
  16. package/agent-template/.ai/blueprints/edit-module/required-files.yaml +9 -0
  17. package/agent-template/.ai/blueprints/new-module/gates.yaml +4 -0
  18. package/agent-template/.ai/blueprints/new-module/spec-requirements.yaml +2 -2
  19. package/agent-template/.ai/blueprints/release/gates.yaml +4 -0
  20. package/agent-template/.ai/platform-capabilities.md +128 -0
  21. package/agent-template/.ai/policies/capabilities.yaml +130 -3
  22. package/agent-template/.ai/policies/path-ownership.yaml +5 -2
  23. package/agent-template/.ai/policies/task-budgets.yaml +5 -3
  24. package/agent-template/.ai/references/catalog/module.json +4 -4
  25. package/agent-template/.ai/references/catalog/package.json +2 -2
  26. package/agent-template/.ai/references/catalog/spec/module.yaml +5 -3
  27. package/agent-template/.ai/references/catalog/src/platform.ts +2 -0
  28. package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +89 -1
  29. package/agent-template/.ai/references/catalog/src/services/data-classes.ts +47 -0
  30. package/agent-template/.ai/references/catalog/src/services/database-repository.ts +98 -1
  31. package/agent-template/.ai/references/catalog/src/services/repository.ts +22 -1
  32. package/agent-template/.ai/references/catalog/tests/data-classes.test.ts +157 -0
  33. package/agent-template/.ai/references/catalog.provenance.json +12 -10
  34. package/agent-template/.ai/rules/flowdular.md +4 -0
  35. package/agent-template/.ai/skills/README.md +10 -0
  36. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -2
  37. package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
  38. package/agent-template/.ai/skills/business-agent-design/SKILL.md +0 -1
  39. package/agent-template/.ai/skills/cli-extension/SKILL.md +1 -1
  40. package/agent-template/.ai/skills/deploy-operate/SKILL.md +119 -0
  41. package/agent-template/.ai/skills/module-new/SKILL.md +29 -3
  42. package/agent-template/.ai/skills/module-update/SKILL.md +9 -3
  43. package/agent-template/.ai/skills/perf-audit/SKILL.md +0 -1
  44. package/agent-template/.ai/skills/release-eject-pr/SKILL.md +0 -1
  45. package/agent-template/.ai/skills/spec-interview/SKILL.md +120 -0
  46. package/agent-template/.ai/skills/test-hardening/SKILL.md +1 -0
  47. package/agent-template/.ai/skills/ux-design/SKILL.md +34 -3
  48. package/agent-template/.ai/skills/variables/SKILL.md +0 -2
  49. package/agent-template/.ai/skills/workflow-development/SKILL.md +0 -1
  50. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
  51. package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
  52. package/agent-template/.claude/skills/cli-extension/SKILL.md +1 -1
  53. package/agent-template/.claude/skills/deploy-operate/SKILL.md +114 -0
  54. package/agent-template/.claude/skills/module-new/SKILL.md +29 -0
  55. package/agent-template/.claude/skills/module-update/SKILL.md +9 -1
  56. package/agent-template/.claude/skills/spec-interview/SKILL.md +114 -0
  57. package/agent-template/.claude/skills/ux-design/SKILL.md +34 -3
  58. package/agent-template/AGENTS.md +4 -0
  59. package/agent-template/CLAUDE.md +4 -0
  60. package/agent-template/docs/adr/0003-module-settings.md +1 -1
  61. package/agent-template/docs/adr/0006-agentic-workflows.md +24 -21
  62. package/agent-template/docs/agent-contract.md +2 -2
  63. package/agent-template/docs/cli-extensions.md +82 -0
  64. package/agent-template/docs/cli.md +195 -0
  65. package/agent-template/docs/configuration.md +593 -36
  66. package/agent-template/docs/design-system.md +185 -31
  67. package/agent-template/docs/getting-started.md +118 -0
  68. package/agent-template/docs/module-distribution.md +96 -0
  69. package/agent-template/docs/module-web-surfaces.md +221 -0
  70. package/agent-template/docs/modules.md +216 -0
  71. package/agent-template/docs/operations.md +545 -0
  72. package/agent-template/docs/sandbox.md +212 -0
  73. package/agent-template/platform/scripts/build.mjs +11 -0
  74. package/dist/bin.js +29 -0
  75. package/package.json +1 -1
  76. package/template/default/.dockerignore +14 -0
  77. package/template/default/.env.example +96 -0
  78. package/template/default/README.md +37 -1
  79. package/template/default/flowdular.json +15 -4
  80. package/template/default/infra/README.md +116 -0
  81. package/template/default/infra/docker/Dockerfile +37 -0
  82. package/template/default/infra/docker/compose.yaml +158 -0
  83. package/template/default/infra/docker/postgres/10-roles.sh +31 -0
  84. package/template/default/infra/docker/postgres/tls-init.sh +28 -0
  85. package/template/default/infra/kubernetes/database-secret.example.yaml +15 -0
  86. package/template/default/infra/kubernetes/deployment.yaml +211 -0
  87. package/template/default/infra/kubernetes/kustomization.yaml +9 -0
  88. package/template/default/infra/kubernetes/secrets.example.yaml +52 -0
  89. package/template/default/infra/kubernetes/service.yaml +13 -0
  90. package/template/default/modules/example/module.json +2 -1
  91. package/template/default/modules/example/package.json +1 -1
  92. package/template/default/modules/example/spec/module.yaml +1 -1
  93. package/template/default/modules/example/src/services/database-repository.ts +2 -12
  94. package/template/default/package.json +3 -2
  95. package/template/default/platform/octane.config.ts +99 -9
  96. package/template/default/platform/package.json +1 -1
  97. package/template/default/platform/src/generated/modules.client.ts +26 -2
  98. package/template/default/platform/src/generated/modules.server.ts +241 -10
  99. package/template/default/platform/src/server/health.ts +47 -0
  100. package/template/default/platform/src/server/metrics.ts +100 -0
  101. package/template/default/platform/src/server/storage.ts +172 -0
  102. package/template/default/platform/src/server/tracing.ts +85 -0
  103. package/template/default/specs/application.yaml +15 -0
@@ -54,10 +54,24 @@ shared primitives, tokens, and the rules for using them.
54
54
  13. Destructive actions close a drawer in their own `ui-form__section--danger`:
55
55
  one title, one line of consequence, one `Button variant="danger"`. They are
56
56
  never mixed into a status row.
57
- 14. Menus (workspace switcher, account menu, session actions) are `ui-menu`
57
+ 14. A screen never writes a bare `select.ui-select`, a bare date input, a bare
58
+ file input, or its own label for any of them: it uses `Select`,
59
+ `DateField`, `DateRangeField`, `DatePicker`, or `FileUpload`, which own the
60
+ label association, the invalid state, and the help or error line. The only
61
+ bare `ui-select` left is the one a primitive renders inside itself (the
62
+ `Pagination` page size), where the row is the label and the control carries
63
+ `aria-label`.
64
+ 15. Menus (workspace switcher, account menu, session actions) are `ui-menu`
58
65
  with `ui-menu__item` rows: 40 px, 10 px inset, the same hover and selected
59
66
  background, a 32 px avatar, two lines of text, and the trailing check
60
67
  pushed to the right edge.
68
+ 16. A surface that opens over the workspace holds the keyboard. `Drawer` and
69
+ `ConfirmDialog` trap Tab inside the open panel, close on Escape, and return
70
+ focus to the control that opened them; the screen only names the first
71
+ field with `autoFocus`, and the panel takes focus when it names none.
72
+ 17. A refusal is stated where the reader meets it, never folded into a label.
73
+ A disabled `TableAction` carries `reason`, and a refused file carries the
74
+ `FileUpload` refusal line; an action label stays the name of the action.
61
75
 
62
76
  ## Screen pattern
63
77
 
@@ -90,6 +104,45 @@ still use the smaller Flowdular `Table` and `TableCard` contract from
90
104
  the TanStack features, row model, header model and cell rendering so every
91
105
  screen keeps the same states, widths and actions.
92
106
 
107
+ Sorting, narrowing and paging are opt-in on the same `Table`. A column that
108
+ declares `value` (the comparable, searchable value behind the rendered cell) is
109
+ sortable once the table carries `sorting`, and is scanned by `globalFilter`; a
110
+ column without it stays presentation only. Sorting is uncontrolled by default
111
+ and reported through `onSortingChange`; pass `sortingState` as well to keep it
112
+ in the screen's own state. `pagination` takes `pageIndex`, `pageSize` and
113
+ `onPageChange` and slices `rows` client-side; add `totalRows` when `rows`
114
+ already holds one page because the module paginated in SQL. The page window is
115
+ taken over the rows that survive `globalFilter`, so a narrowed set can never
116
+ page into emptiness. The table never moves the page on its own: it clamps the
117
+ index when the row set shrinks under it and reports that through `onPageChange`
118
+ after the render commits, so the reader keeps their place when they sort or
119
+ filter. A screen that wants the first page after a sort or a new search term
120
+ resets the index itself.
121
+
122
+ A list the module already sorted, narrowed and cut in SQL passes `mode="server"`
123
+ instead. The table then renders `rows` exactly as they arrive and narrows
124
+ nothing: the headers still toggle and report through `onSortingChange`, the page
125
+ still reports through `onPageChange`, and the screen turns both into its next
126
+ request. `pagination.totalRows` stays optional there. With a total the table
127
+ clamps the page index the same way it does client-side; without one the page is
128
+ a keyset page nobody counted, and `pagination.hasMore` says whether another
129
+ follows, so an empty page with nothing behind it walks the reader back one page
130
+ rather than leaving them on a page that does not exist.
131
+
132
+ A row action that is refused carries `reason` beside `disabled`. The label stays
133
+ the name of the action and the reason becomes the button's accessible
134
+ description, so a reader who cannot see the greyed button still hears why.
135
+
136
+ The pager is a separate `Pagination` in the `TableCard` `after` slot, so the
137
+ screen holds one page index and size that both the table and the pager read.
138
+ It takes the row total the screen already knows (after its own filtering) and
139
+ a `summary(range)` the screen translates; `pageRange` is exported for a screen
140
+ that needs the same arithmetic elsewhere. Default page size is 25. A keyset
141
+ page passes `hasMore` in place of `totalRows`: Previous behaves the same, Next
142
+ stays open exactly while another page follows, and `summary(page)` reads a
143
+ `keysetPage` with the page index, size and first row but no page count, because
144
+ nobody counted the set.
145
+
93
146
  Every column declares a semantic CSS `width`. Give the primary record and its
94
147
  description the largest share, medium shares to dates and identifiers, and the
95
148
  smallest share to counts and lifecycle state. In a table with row actions, data
@@ -119,39 +172,130 @@ without settings shows a one-line empty state there.
119
172
 
120
173
  ## Components
121
174
 
122
- | Component | Use |
123
- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
124
- | `Button` | Actions: `variant` primary, secondary (default), ghost, danger; `size` sm, md, lg; `block` |
125
- | `FormField` | Label + control + help or error. Put `ui-input`, `ui-select`, or `ui-textarea` inside |
126
- | `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 |
127
- | `VariableInput` | Single-line variant of `VariableTextarea` with the same props |
128
- | `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 |
129
- | `Tag` | Status and metadata: `tone` neutral, success, warning, danger, info, ink; `dot` adds a state dot; `mono` |
130
- | `Kpi` | Stat tile: `label`, `value`, `unit`, `badge`, `note`, `href`, `linkLabel` |
131
- | `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 |
132
- | `Table` | The one data table, backed by `@octanejs/tanstack-table`: `columns` (`key`, `header`, required `width`, `numeric`, `cell`), `rows`, `rowKey`, `status` idle/loading, `loadingLabel`, `empty` and `emptyFiltered` picked by `filtered`, `actions(row): TableAction[]`, `actionsLabel`, optional stable `actionsWidth` (160 px by default, 280 px for two actions), `onSelect` with `selectedKey`, `caption`; fixed layout and `colgroup` keep columns stable across states |
133
- | `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 |
134
- | `PageHeader` | Every view starts with it: `eyebrow`, `title`, `description`; children render as right-side actions |
135
- | `EmptyState` | `icon`, `title`, children, optional `code` |
136
- | `Alert` | Inline message: `tone` danger (default), warning, info |
137
- | `Drawer` | Editor panel over the records: `open`, `title`, `subtitle`, `width` md/lg, `onClose` |
138
- | `SearchField` | Filter control for a panel head: `value`, `placeholder`, `label`, `onInput` |
139
- | `CheckGrid` | Grouped multi-select for scopes, tools, and long option sets: `groups` (`label`, `options` of `value`, `label`, `hint`), `value`, `mono`, `disabled`, `onChange` |
140
- | `ScopeSummary` | Read-only summary of `module.entity.action` scopes: one row per module, one chip per entity with its actions: `scopes`, `labels` (module id to display name) |
141
- | `SettingRow` | One setting: `label`, `description` (node, one line), `scopeLabel` (small neutral tag), `status` (`ok`, `message` of the last save), children as the control cluster |
142
- | `Avatar` | Initials from `name`: `square` for organizations, round for people; `large` in profile and account headers |
143
- | `Switch` | Boolean setting that applies on its own (no form submit): `checked`, `label` as the accessible name, `disabled`, `onChange` |
144
- | `ConfirmDialog` | One question before an irreversible action: `open`, `title`, children, `confirmLabel`, `tone` danger (default) or primary, `busy`, `onConfirm`, `onCancel` |
145
- | `Icon` | Stroke icon by `name` from `ICON_PATHS`; `size` 18 default, 16 in controls, 14 in `Button size="sm"`; `strokeWidth` 1.75 default |
146
- | `BrandMark` | The weave: `size`, `signature` (copper weft, large brand moments only), `tone` brand, current, inverse |
175
+ | Component | Use |
176
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
177
+ | `Button` | Actions: `variant` primary, secondary (default), ghost, danger; `size` sm, md, lg; `block` |
178
+ | `FormField` | Label + control + help or error. Put `ui-input`, `ui-select`, or `ui-textarea` inside |
179
+ | `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
+ | `VariableInput` | Single-line variant of `VariableTextarea` with the same props |
181
+ | `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 |
182
+ | `Tag` | Status and metadata: `tone` neutral, success, warning, danger, info, ink; `dot` adds a state dot; `mono` |
183
+ | `Kpi` | Stat tile: `label`, `value`, `unit`, `badge`, `note`, `href`, `linkLabel` |
184
+ | `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 |
186
+ | `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
+ | `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
+ | `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` |
189
+ | `DateField` | Native date or datetime control: required `id`, `label`, ISO `value`, `onChange`, `kind` date (default) or datetime, `locale`, `min`, `max`, `name`, `required`, `disabled`, `invalid`, `help`, `error`, `describedBy` for a message a group around the field owns; the reading beside the input repeats the value in the reader's locale |
190
+ | `DateRangeField` | Two `DateField`s with one answer: required `id`, `legend`, `fromLabel`, `toLabel`, `value` (`from`, `to`), `onChange`, `kind`, `locale`, `min`, `max`, `required`, `disabled`, `reversedMessage`, `help`, `error`; each side bounds the other and a reversed range reports instead of swapping |
191
+ | `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
+ | `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
+ | `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 |
194
+ | `ToastHost` | Renders the toast queue: `label`, `closeLabel`, optional `store`. Raise toasts with `toasts.success/error/info(message)`; `createToastStore` makes a scoped queue |
195
+ | `PageHeader` | Every view starts with it: `eyebrow`, `title`, `description`; children render as right-side actions |
196
+ | `EmptyState` | `icon`, `title`, children, optional `code` |
197
+ | `Alert` | Inline message: `tone` danger (default), warning, info |
198
+ | `Drawer` | Editor panel over the records: `open`, `title`, `subtitle`, `width` md/lg, `onClose`; traps Tab and restores focus to the opener |
199
+ | `SearchField` | Filter control for a panel head: `value`, `placeholder`, `label`, `onInput` |
200
+ | `CheckGrid` | Grouped multi-select for scopes, tools, and long option sets: `groups` (`label`, `options` of `value`, `label`, `hint`), `value`, `mono`, `disabled`, `onChange` |
201
+ | `ScopeSummary` | Read-only summary of `module.entity.action` scopes: one row per module, one chip per entity with its actions: `scopes`, `labels` (module id to display name) |
202
+ | `SettingRow` | One setting: `label`, `description` (node, one line), `scopeLabel` (small neutral tag), `status` (`ok`, `message` of the last save), children as the control cluster |
203
+ | `Avatar` | Initials from `name`: `square` for organizations, round for people; `large` in profile and account headers |
204
+ | `Switch` | Boolean setting that applies on its own (no form submit): `checked`, `label` as the accessible name, `disabled`, `onChange` |
205
+ | `ConfirmDialog` | One question before an irreversible action: `open`, `title`, children, `confirmLabel`, `tone` danger (default) or primary, `busy`, `onConfirm`, `onCancel`; traps Tab and restores focus to the opener |
206
+ | `Icon` | Stroke icon by `name` from `ICON_PATHS`; `size` 18 default, 16 in controls, 14 in `Button size="sm"`; `strokeWidth` 1.75 default |
207
+ | `BrandMark` | The weave: `size`, `signature` (copper weft, large brand moments only), `tone` brand, current, inverse |
147
208
 
148
209
  `Drawer` takes one child, a `ui-drawer__form` (fields in `ui-drawer__body`, actions in `ui-drawer__foot`) or a plain `ui-drawer__body`; it closes on Escape and on the scrim. `SearchField` carries no visible label, so pass `label` as its accessible name. `FormField` renders `error` in place of `help` and marks it `role="alert"`. `SettingRow` is presentation only: the caller owns the draft value, the save call, and passes the result back as `status`. `ScopeSummary` is the read side of `CheckGrid`; both keep the first-seen module order, and `summarizeScopes` is exported for callers that need the grouping without the markup.
149
210
 
211
+ `Select`, `DateField` and `DateRangeField` own their own label, so they go
212
+ straight into a form row and not inside a `FormField`. Each requires an `id`:
213
+ it binds the label to the control and keeps the browser's association stable
214
+ across renders, and `name` defaults to it for `FormData`. `invalid` marks the
215
+ control without occupying the message line; `error` does both and replaces
216
+ `help`.
217
+
218
+ A `DateField` holds the ISO value the platform stores and the form submits
219
+ (`YYYY-MM-DD`, or `YYYY-MM-DDTHH:mm` for `kind="datetime"`), because a native
220
+ date input picks its own display format and no page can change it. The reading
221
+ beside the input repeats that value in the reader's locale: pass `locale` from
222
+ `activeLocale()`, and use the exported `formatDateValue` for the same reading
223
+ in a table cell. `DateRangeField` bounds each side by the other, so the picker
224
+ cannot produce a reversed range, and shows `reversedMessage` when one arrives
225
+ from the screen's own state; `dateRangeReversed` is exported for the same check
226
+ before a submit. Its one help or error line describes both inputs through
227
+ `describedBy`, because a fieldset's own `aria-describedby` never reaches the
228
+ controls inside it.
229
+
230
+ `DatePicker` is the calendar over those same native inputs: the trigger opens
231
+ one popover holding the presets and the month grid, and a range is picked in
232
+ that one calendar, first end then second, ordered on the way out so the picker
233
+ can never make a reversed range. `mode="single"` reads and writes `value.from`
234
+ and mirrors it into `to`, because a single day is a range whose ends match and
235
+ one shape keeps one keyboard model. Arrows move by day and week, PageUp and
236
+ PageDown by month, Home and End to the ends of the week, Enter or Space selects,
237
+ and Escape closes the calendar and stops there, so a picker inside a `Drawer`
238
+ never dismisses the drawer with the same keystroke. The four standard windows
239
+ come from `datePresetRange('today' | 'last-7-days' | 'last-30-days' |
240
+ 'this-month')`, which the screen pairs with its own translated labels;
241
+ 'this-month' is the month so far. With `kind="datetime"` the calendar answers
242
+ about the day only and keeps the time the value already carried, clamping the
243
+ result into `min` and `max`. Use `DateField` for a plain date a reader types,
244
+ `DateRangeField` for two dates in two labelled fields, and `DatePicker` when the
245
+ reader picks from a calendar or takes a window in one click.
246
+
247
+ `FileUpload` owns the two refusals a browser cannot state: a content type the
248
+ `accept` list does not cover, and a file over `maxBytes`. It detects them and
249
+ the screen supplies the words through `refusal(reason, file)`, the same way
250
+ every other primitive takes its copy. A refused file is never handed on: the
251
+ control reports `null`, empties the native list so the same file can be chosen
252
+ again, and shows the refusal in place of the reading. It re-reads the file it
253
+ holds whenever the limits change, so a ceiling that arrives after the drawer
254
+ opened still refuses what it must, and the screen keeps one `File | null` and no
255
+ check of its own. The native input stays the focus stop, so the picker opens
256
+ from the keyboard.
257
+
258
+ `Drawer` and `ConfirmDialog` hold the keyboard while they are open. Tab wraps at
259
+ both ends of the panel, Escape closes, and closing returns focus to the control
260
+ that opened the panel. Focus moves in on open: to the control the screen marked
261
+ `autoFocus` if there is one, otherwise to the first focusable control in the
262
+ panel, otherwise to the panel itself. A drawer whose first field is a `Select`
263
+ marks it `autoFocus`; the screen never moves focus by hand.
264
+
265
+ `Tabs` renders only the tablist. The caller renders the panel and wires it to
266
+ the tab: the tabs are `<id>-tab-<item.id>` and point at `<id>-panel-<item.id>`,
267
+ so a panel is `<div id={id + '-panel-' + active} role="tabpanel"
268
+ aria-labelledby={id + '-tab-' + active}>`. Arrow keys move focus and selection
269
+ over enabled tabs and wrap, Home and End jump to the ends, and only the active
270
+ tab is in the tab order, so Tab leaves the list for the panel.
271
+
272
+ `Toast` is a transient confirmation of something the reader just did, never a
273
+ state a screen must keep showing: a failure that blocks work stays in `Alert`.
274
+ A screen that raises toasts renders one `ToastHost`; the region keeps its place
275
+ in the DOM while empty, because a live region added with its first message is
276
+ not announced. `toasts.success`, `.error` and `.info` raise them from anywhere,
277
+ the queue keeps the four newest and dismisses each after five seconds, and
278
+ `createToastStore` makes a scoped queue for a surface or a test. The entry
279
+ animation follows the global reduced-motion rule in `base.css`.
280
+
281
+ The five states read the same way on every one of these: `Select`, `DateField`,
282
+ `DateRangeField` and `DatePicker` are loading when the screen disables them,
283
+ empty with an empty `value` (and a `placeholder` on `Select`), in error through
284
+ `error` or `invalid`, populated with a value, and denied by not being rendered
285
+ at all. `FileUpload` is loading while `busy`, empty with no `value` and its
286
+ `hint` on the line, in error through `error` or a refusal it made itself,
287
+ populated with the `chosen` reading beside a remove button, and denied the same
288
+ way: the screen does not render it.
289
+ `Table` keeps its own loading row, empty state and filtered-empty message, and
290
+ `Pagination` reads "1 of 1" over an empty set rather than disappearing.
291
+ `ToastHost` has no loading or denied state: it is empty or it carries toasts.
292
+
150
293
  Icon names (`packages/ui/src/icons/Icon.tsrx`): `dashboard`, `parties`, `catalog`,
151
294
  `user`, `users`, `shield`, `code`, `modules`, `file-text`, `play`, `bot`,
152
- `flask`, `activity`, `plug`, `search`, `chevron-down`, `chevrons-up-down`,
295
+ `flask`, `activity`, `plug`, `search`, `chevron-down`, `chevron-left`,
296
+ `chevron-right`, `chevrons-up-down`, `sort`, `calendar`,
153
297
  `plus`, `panel-left`, `check`, `filter`, `download`, `more`, `external`,
154
- `alert`, `x`, `sign-out`, `refresh`, `help`, `key`, `settings`, `braces`. An unknown name renders
298
+ `alert`, `x`, `sign-out`, `refresh`, `help`, `info`, `key`, `settings`, `braces`. An unknown name renders
155
299
  `modules` without a warning; a new icon is one 24x24 stroke path added there.
156
300
 
157
301
  ## Classes
@@ -172,8 +316,8 @@ Icon names (`packages/ui/src/icons/Icon.tsrx`): `dashboard`, `parties`, `catalog
172
316
  with `__section-head` (`b` title, `small` description) and
173
317
  `__section--danger`), `ui-control-row` (a growing control beside one fixed
174
318
  button), `ui-choices` (a short wrapping row of `ui-checkbox`), `ui-input`
175
- (+`--error`), `ui-select`, `ui-textarea` (+`--error`), `ui-checkbox`,
176
- `ui-label`, `ui-help` (+`--error`)
319
+ (+`--error`), `ui-select` (+`--error`), `ui-textarea` (+`--error`),
320
+ `ui-checkbox`, `ui-label`, `ui-help` (+`--error`)
177
321
  - Drawer: `ui-drawer__form` (scrolling body plus pinned footer),
178
322
  `ui-drawer__body`, `ui-drawer__foot`
179
323
  - Settings rows inside a `ui-card` or a drawer `ui-form__section`, rendered
@@ -183,7 +327,8 @@ Icon names (`packages/ui/src/icons/Icon.tsrx`): `dashboard`, `parties`, `catalog
183
327
  - Buttons: `ui-btn` with `--primary`, `--secondary`, `--ghost`, `--danger`,
184
328
  `--sm`, `--lg`, `--block`, and `--icon` for an icon-only square button
185
329
  (the `Button` component renders all but `--icon`)
186
- - Bits: `ui-kbd`, `ui-dot` (+`--muted`), `ui-note`, `ui-menu` (+`__label`,
330
+ - Bits: `ui-visually-hidden` (text only a screen reader reads), `ui-kbd`,
331
+ `ui-dot` (+`--muted`), `ui-note`, `ui-menu` (+`__label`,
187
332
  `__section` for a static identity or connection block, `__item`,
188
333
  `__item--active`, `__item--danger`, `__sep`), `ui-icon`
189
334
 
@@ -194,6 +339,15 @@ Rendered by components, not written by hand: `ui-page-head*`, `ui-search`,
194
339
  `__note`, `__link`), `ui-avatar` (+`--sq`, `--lg`), `ui-checks` (+`__group`,
195
340
  `__head`, `__count`, `__all`, `__items`, `__option`), `ui-scopes` (+`__row`,
196
341
  `__chip`), `ui-drawer-slot`, `ui-drawer` (+`__panel`, `__panel--lg`, `__head`,
342
+ `__close`), `ui-table__sort` with `ui-table__sort-icon` (+`--on`, `--asc`),
343
+ `ui-pagination` (+`__summary`, `__size`, `__pages`), `ui-datefield`
344
+ (+`__reading`), `ui-daterange` (+`__row`), `ui-datepicker` (+`__row`,
345
+ `__trigger`, `__scrim`, `__popover`, `__presets`, `__preset`, `__calendar`,
346
+ `__head`, `__month`, `__step`, `__grid`, `__day` (+`--outside`, `--between`,
347
+ `--on`)), `ui-fileupload` (+`__control`, `__input`, `__clear`),
348
+ `ui-table-action` (the wrapper carrying a refused action's title),
349
+ `ui-tabs` (+`__tab`, `__tab--on`),
350
+ `ui-toasts` with `ui-toast` (+`--success`, `--error`, `--info`, `__message`,
197
351
  `__close`), `ui-varfield` (+`__control`, `__highlight`, `__input`, `__pill`
198
352
  (+`--error`), `__trigger`, `__menu`, `__value`, `__empty`; the overlay layer
199
353
  that highlights `{{ key }}` tokens, rendered by `VariableTextarea` and
@@ -0,0 +1,118 @@
1
+ # Getting started
2
+
3
+ Run Flowdular on your machine, seed a demo workspace, and sign in.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 22.22.2 or newer
8
+ - pnpm 11.17.0
9
+ - No database server: the platform ships an embedded PostgreSQL and keeps it
10
+ under `.flowdular/data`
11
+
12
+ ## Install and check the workspace
13
+
14
+ ```bash
15
+ pnpm install
16
+ pnpm flowdular doctor
17
+ ```
18
+
19
+ `doctor` reports workspace health (configuration, enabled modules, generated
20
+ composition, guardrail files). Add `--json` for a machine-readable envelope.
21
+
22
+ ## Seed a local demo
23
+
24
+ `pnpm flowdular setup` opens an interactive wizard. Choose a local demo, configure PostgreSQL, or check the existing configuration. Local initialization requires confirmation and a stopped application.
25
+
26
+ For scripts and CI, `setup quick` is a destructive local reset. It prints its full plan first and
27
+ writes only after a typed confirmation:
28
+
29
+ ```bash
30
+ pnpm flowdular setup quick # dry run, prints the plan
31
+ pnpm flowdular setup quick --apply --confirm reset-local-auth # resets and seeds
32
+ ```
33
+
34
+ Stop `pnpm dev` before applying it. Quick setup is blocked outside development
35
+ and test, and must never point at a deployed database.
36
+
37
+ It creates two demo tenants (Operations Demo, Finance Demo) and two logins:
38
+
39
+ | Account | Password | Role |
40
+ | ------------------- | ---------------- | -------------------------- |
41
+ | `admin@example.com` | `Owner!23456789` | Owner of both demo tenants |
42
+ | `user@example.com` | `Member!2345678` | Reduced scope member |
43
+
44
+ ## Run the platform
45
+
46
+ ```bash
47
+ pnpm dev
48
+ ```
49
+
50
+ Open `http://localhost:4310`. Vite HMR covers TSRX, TypeScript and styles. The
51
+ launcher keeps tool warnings quiet; use `pnpm dev -- --verbose` for full
52
+ diagnostics. `pnpm dev` runs `module sync` first, so a composition change is
53
+ picked up without a manual step.
54
+
55
+ The first visit opens the `auth.core` sign-in flow. The session lives in an
56
+ HttpOnly cookie and carries the scopes of the selected tenant membership. On a
57
+ clean database the sign-up wizard is available: workspace name plus a unique
58
+ workspace id (the first URL segment, `/{workspace}/{view}`), then the
59
+ administrator account, then an optional email confirmation step. A bookmark
60
+ pointing at another workspace you belong to switches the session on load.
61
+
62
+ `Development` navigation is visible only to tenant owners; server permissions
63
+ stay authoritative either way.
64
+
65
+ ## Where local state lives
66
+
67
+ The database and the development vault keys live under `.flowdular/data`. Every
68
+ module shares one embedded PostgreSQL in `.flowdular/data/pglite`, which
69
+ `FD_DATABASE_PGLITE_DIRECTORY` can redirect. Point `FD_DATABASE_ADAPTER` at
70
+ `postgresql` and give it `FD_DATABASE_URL` to run against a real server instead.
71
+ See [configuration.md](configuration.md).
72
+
73
+ ## Migrating preserved state from `.octane-erp`
74
+
75
+ `setup migrate-state` is an isolated compatibility bridge for workspaces that
76
+ still hold state under the old directory:
77
+
78
+ ```bash
79
+ pnpm flowdular setup migrate-state
80
+ # Stop the platform, the sandbox, and anything holding those files open first.
81
+ pnpm flowdular setup migrate-state --apply --confirm migrate-legacy-state
82
+ ```
83
+
84
+ The dry run lists every source, destination and collision. Apply copies the
85
+ vault key files, refuses symbolic links and existing destination files, verifies
86
+ every copied file and never removes the legacy source directory. SQLite database
87
+ files are reported by name and left in place: the PostgreSQL and PGlite adapters
88
+ cannot read them, so start a fresh workspace on the current adapter; that data
89
+ does not carry over.
90
+
91
+ ## Agents in a local workspace
92
+
93
+ `agents.core` ships enabled: reusable agent definitions, an isolated playground
94
+ and durable run history. Enqueue returns once the run is committed, so execution
95
+ continues across navigation, a closed browser or a sign-out. Agents reach
96
+ platform data only through tools registered against approved API endpoints or
97
+ CLI capabilities. The default local provider is a simulation: it calls no
98
+ external model and no network service, so nothing leaves your machine until you
99
+ bind a real provider under Providers.
100
+
101
+ ## Landing site
102
+
103
+ The public website lives in [Flowdular/landing](https://github.com/Flowdular/landing): one
104
+ server-rendered page, no session and no module composition, so marketing work
105
+ never reaches the product. The platform serves the workspace itself at `/`.
106
+
107
+ ```bash
108
+ git clone https://github.com/Flowdular/landing.git
109
+ cd landing
110
+ pnpm install
111
+ pnpm dev # http://127.0.0.1:4330
112
+ ```
113
+
114
+ ## Next
115
+
116
+ - Build a module: [modules.md](modules.md)
117
+ - Build one by chat: [sandbox.md](sandbox.md)
118
+ - Every command: [cli.md](cli.md)
@@ -0,0 +1,96 @@
1
+ # Installing official modules
2
+
3
+ Core is maintained in [Flowdular/flowdular](https://github.com/Flowdular/flowdular).
4
+ [Flowdular/official-modules](https://github.com/Flowdular/official-modules) owns
5
+ expenses, parties and catalog, including their source, reviews and immutable
6
+ release artifacts. The landing is in [Flowdular/landing](https://github.com/Flowdular/landing).
7
+
8
+ ```sh
9
+ pnpm flowdular module search expenses
10
+ pnpm flowdular module info expenses.core
11
+ pnpm flowdular module install expenses.core@0.7.1
12
+ pnpm flowdular module install expenses.core@0.7.1 --apply
13
+ pnpm flowdular module enable expenses.core --apply
14
+ pnpm flowdular module validate --locked
15
+ ```
16
+
17
+ Without `--apply`, installation and updates return a plan and write nothing.
18
+ Installation downloads source into the first configured module root and writes
19
+ `flowdular.modules.lock.json`. It does not install npm dependencies, run downloaded
20
+ scripts, activate modules, grant scopes or touch a database. Enablement links npm
21
+ packages with install lifecycle scripts disabled, generates composition and runs
22
+ its existing scope-grant flow. Review the source before enabling it.
23
+
24
+ `module update <id[@version]> [--apply]` requires an installer-managed module and
25
+ refuses local source changes, removed or edited historical migrations, incompatible
26
+ versions and downgrades. Additional source files count as local edits. Generated
27
+ `dist`, `node_modules` and Git metadata do not. `module validate --locked` requires
28
+ a lock and checks source hashes as well as ordinary manifest/dependency validation.
29
+
30
+ The installer resolves a consistent dependency closure, including diamond
31
+ constraints, within a bounded search budget. Existing workspace/package versions
32
+ are preserved. Registry and runtime validation share semver semantics, including
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`
35
+ filters releases by it. A release that declares `requires` is resolved together
36
+ with the newest compatible release providing each required capability.
37
+
38
+ ## Trust and recovery
39
+
40
+ The default catalog is the official repository's `registry/index.json`. HTTPS
41
+ artifact URLs must point to the declared immutable commit in that same repository.
42
+ Downloads have time and size limits. Source bundles are bounded JSON records of
43
+ regular files, validated for path escapes, duplicate names, digest mismatches,
44
+ identity, portable dependencies and stale/missing review evidence. No archive
45
+ extraction or install lifecycle scripts execute during source installation.
46
+
47
+ A checksum does not authenticate an arbitrary publisher. The trust root is the
48
+ configured official repository over HTTPS. An operator can explicitly supply
49
+ `--registry /absolute/path/index.json` for an offline catalog; artifacts must stay
50
+ inside that catalog directory. Third-party remote registries are not supported.
51
+ A review report is an assessment, not a guarantee or a defense against a malicious
52
+ publisher. Official release CI also executes the checks independently.
53
+
54
+ Concurrent installs share an exclusive transaction directory. Recover an
55
+ interrupted operation with `module recover` and `module recover --apply` after
56
+ its owner process has exited. Recovery restores the previous source and module
57
+ lock and refuses to discard conflicting edits. Keep the transaction directory
58
+ when a conflict is reported. Source recovery never rolls back database migrations.
59
+ Installation and updates are host/operator capabilities and do not expand sandbox
60
+ agents' network, filesystem or tool permissions.
61
+
62
+ ## SDK publication and consumer checks
63
+
64
+ ```sh
65
+ pnpm release:pack
66
+ pnpm release:smoke
67
+ # Also install and test actual source artifacts from the official repo:
68
+ node scripts/smoke-sdk.mjs release-artifacts/sdk /path/to/official-modules/registry/local-index.json
69
+ ```
70
+
71
+ `release-artifacts/sdk/sdk.json` lists exactly `@flowdular/sdk`, `flowdular`, `create-flowdular` and `@flowdular/sandbox`, with versions, tarballs
72
+ and SHA-256 digests. Publish those tarballs with `npm publish <tarball> --access public`.
73
+ Publish all SDK dependencies before consumers install the starter. The three
74
+ business modules and the landing are not in this npm publication set. Keep release
75
+ artifacts outside runtime state directories; creating both `.flowdular` and legacy
76
+ `.coreloom` state would correctly stop the application.
77
+
78
+ The smoke test creates a separate project and resolves SDK dependencies from
79
+ packed artifacts, without aliases or symlinks into core sources. With a module
80
+ catalog it installs all module source artifacts, enables their source composition,
81
+ checks the lock, typechecks and executes the consumer's tests. Its composition
82
+ check does not grant tenant scopes; auth CLI tests cover that separate boundary.
83
+
84
+ Sandbox examples use `.ai/references/catalog`, generated from a reviewed official
85
+ artifact. `pnpm reference:check` verifies every file against the pinned provenance
86
+ record. Do not edit the generated reference. To update it, build the CLI and run
87
+ `scripts/module-reference.mjs` with `--artifact`, `--sha256`, `--source-commit` and
88
+ `--apply`. Skills and sandbox preparation refer to that offline snapshot.
89
+
90
+ `pnpm release:publish` previews the exact publication set after validating every
91
+ artifact digest. The operator can then use `pnpm release:publish --apply` after npm
92
+ authentication. It skips already-published identical tarballs and stops if a
93
+ version exists with different bytes. No npm publication is performed by packing,
94
+ smoke testing or the default publication preview.
95
+
96
+ 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).