create-flowdular 0.3.1 → 0.4.0

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 (92) hide show
  1. package/README.md +1 -1
  2. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +25 -0
  3. package/agent-template/.agents/skills/integration-adapter/SKILL.md +116 -0
  4. package/agent-template/.agents/skills/module-new/SKILL.md +17 -2
  5. package/agent-template/.agents/skills/module-update/SKILL.md +14 -2
  6. package/agent-template/.agents/skills/release-eject-pr/SKILL.md +23 -26
  7. package/agent-template/.agents/skills/spec-interview/SKILL.md +16 -0
  8. package/agent-template/.agents/skills/translations-i18n/SKILL.md +2 -1
  9. package/agent-template/.agents/skills/ux-design/SKILL.md +4 -4
  10. package/agent-template/.ai/agents/sandbox/backend-engineer.md +9 -0
  11. package/agent-template/.ai/blueprints/agentic-module/README.md +15 -0
  12. package/agent-template/.ai/blueprints/agentic-module/allowed-paths.yaml +14 -0
  13. package/agent-template/.ai/blueprints/agentic-module/blueprint.json +19 -0
  14. package/agent-template/.ai/blueprints/agentic-module/gates.yaml +24 -0
  15. package/agent-template/.ai/blueprints/agentic-module/input.schema.json +18 -0
  16. package/agent-template/.ai/blueprints/agentic-module/plan.schema.json +35 -0
  17. package/agent-template/.ai/blueprints/agentic-module/required-files.yaml +28 -0
  18. package/agent-template/.ai/blueprints/agentic-module/spec-requirements.yaml +33 -0
  19. package/agent-template/.ai/blueprints/agentic-module/steps.yaml +68 -0
  20. package/agent-template/.ai/platform-capabilities.md +16 -11
  21. package/agent-template/.ai/references/catalog/migrations/0005_catalog_list_indexes.down.sql +3 -0
  22. package/agent-template/.ai/references/catalog/migrations/0005_catalog_list_indexes.up.sql +11 -0
  23. package/agent-template/.ai/references/catalog/module.json +12 -2
  24. package/agent-template/.ai/references/catalog/package.json +2 -2
  25. package/agent-template/.ai/references/catalog/spec/module.yaml +26 -5
  26. package/agent-template/.ai/references/catalog/src/agent/tools.ts +19 -10
  27. package/agent-template/.ai/references/catalog/src/api/endpoints.ts +150 -10
  28. package/agent-template/.ai/references/catalog/src/api/list-cursor.ts +83 -0
  29. package/agent-template/.ai/references/catalog/src/client/CatalogView.tsrx +505 -159
  30. package/agent-template/.ai/references/catalog/src/client/api.ts +124 -36
  31. package/agent-template/.ai/references/catalog/src/client/contribution.tsrx +5 -0
  32. package/agent-template/.ai/references/catalog/src/client/state.ts +169 -3
  33. package/agent-template/.ai/references/catalog/src/domain/lists.ts +7 -0
  34. package/agent-template/.ai/references/catalog/src/domain/types.ts +20 -0
  35. package/agent-template/.ai/references/catalog/src/platform.ts +20 -0
  36. package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +143 -8
  37. package/agent-template/.ai/references/catalog/src/services/database-repository.ts +104 -17
  38. package/agent-template/.ai/references/catalog/src/services/item-export.ts +81 -0
  39. package/agent-template/.ai/references/catalog/src/services/migration.ts +27 -1
  40. package/agent-template/.ai/references/catalog/src/services/repository.ts +31 -2
  41. package/agent-template/.ai/references/catalog/tests/agent-tools.test.ts +6 -5
  42. package/agent-template/.ai/references/catalog/tests/client-state.test.ts +124 -0
  43. package/agent-template/.ai/references/catalog/tests/endpoints.test.ts +269 -0
  44. package/agent-template/.ai/references/catalog/tests/export.test.ts +134 -0
  45. package/agent-template/.ai/references/catalog/tests/idempotency.test.ts +15 -14
  46. package/agent-template/.ai/references/catalog/tests/list.test.ts +217 -0
  47. package/agent-template/.ai/references/catalog/tests/migrations.test.ts +58 -2
  48. package/agent-template/.ai/references/catalog/tests/module.test.ts +2 -1
  49. package/agent-template/.ai/references/catalog/tests/support/database.ts +14 -0
  50. package/agent-template/.ai/references/catalog/translations/en.json +35 -4
  51. package/agent-template/.ai/references/catalog/translations/pl.json +35 -4
  52. package/agent-template/.ai/references/catalog.provenance.json +34 -26
  53. package/agent-template/.ai/rules/flowdular.md +2 -1
  54. package/agent-template/.ai/skills/README.md +1 -0
  55. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +25 -0
  56. package/agent-template/.ai/skills/integration-adapter/SKILL.md +121 -0
  57. package/agent-template/.ai/skills/module-new/SKILL.md +17 -2
  58. package/agent-template/.ai/skills/module-update/SKILL.md +14 -2
  59. package/agent-template/.ai/skills/release-eject-pr/SKILL.md +23 -26
  60. package/agent-template/.ai/skills/spec-interview/SKILL.md +16 -0
  61. package/agent-template/.ai/skills/translations-i18n/SKILL.md +2 -1
  62. package/agent-template/.ai/skills/ux-design/SKILL.md +4 -4
  63. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +25 -0
  64. package/agent-template/.claude/skills/integration-adapter/SKILL.md +116 -0
  65. package/agent-template/.claude/skills/module-new/SKILL.md +17 -2
  66. package/agent-template/.claude/skills/module-update/SKILL.md +14 -2
  67. package/agent-template/.claude/skills/release-eject-pr/SKILL.md +23 -26
  68. package/agent-template/.claude/skills/spec-interview/SKILL.md +16 -0
  69. package/agent-template/.claude/skills/translations-i18n/SKILL.md +2 -1
  70. package/agent-template/.claude/skills/ux-design/SKILL.md +4 -4
  71. package/agent-template/AGENTS.md +2 -1
  72. package/agent-template/CLAUDE.md +2 -1
  73. package/agent-template/docs/agent-contract.md +1 -1
  74. package/agent-template/docs/cli.md +8 -3
  75. package/agent-template/docs/configuration.md +29 -0
  76. package/agent-template/docs/design-system.md +112 -13
  77. package/agent-template/docs/module-distribution.md +10 -3
  78. package/agent-template/docs/modules.md +51 -1
  79. package/agent-template/docs/operations.md +2 -0
  80. package/agent-template/docs/sandbox.md +76 -1
  81. package/assets/flowdular-banner.webp +0 -0
  82. package/package.json +1 -1
  83. package/template/default/flowdular.json +2 -0
  84. package/template/default/modules/example/package.json +1 -1
  85. package/template/default/modules/example/src/client/NotesView.tsrx +12 -16
  86. package/template/default/modules/example/tests/module.test.ts +3 -2
  87. package/template/default/modules/example/translations/pl.json +3 -1
  88. package/template/default/package.json +1 -1
  89. package/template/default/platform/package.json +1 -1
  90. package/template/default/platform/src/generated/modules.client.ts +4 -0
  91. package/template/default/platform/src/generated/modules.server.ts +68 -8
  92. package/assets/flowdular-banner.png +0 -0
@@ -37,6 +37,9 @@ One pass, in this order. For each row, write the default from the card into the
37
37
  | Widgets | None. A count belongs on `dashboard.metrics` only when the request asks for it | `widgets[]` |
38
38
  | Settings | None. A number the business may change later is `scope: tenant` with a stated default | `settings[]` |
39
39
  | Agent tools | None. A tool is a later phase and `risk` may only be `read` or `workspace-write` | `agentTools[]` |
40
+ | Outside sources | None. A named public source is `research`, with the entity its findings attach to | `research` |
41
+ | Other systems | None. A named system is one `source` adapter per record kind, run on demand | `adapters[]` |
42
+ | Documents | None. A named document is one `templates[]` entry on the record it describes | `templates[]` |
40
43
  | Reports | None. There is no export, no PDF and no search; a report is a screen or it is out of scope | `outOfScope[]` |
41
44
  | Out of scope | Every item from the card's gap list the request touched, each with its business decision | `outOfScope[]`, `decisions[]` |
42
45
 
@@ -68,6 +71,16 @@ The sandbox renders it as a form and the answers return in the next turn as a `D
68
71
 
69
72
  When the answers come back, copy each one into `decisions[]` with `decidedBy: user` and the answer text, and update whatever the answer changed.
70
73
 
74
+ ### Outside sources, other systems and documents
75
+
76
+ Ask these only when the brief names one; each answer is a decision like any other.
77
+
78
+ - **An outside source** ("check the company in the registry", "compare listing prices"): which sources are trusted (`allowDomains`) or refused (`denyDomains`), which record the findings belong to (`evidenceOwner`, an entity of this spec), and whether the monthly budget differs from the default of 500 queries. Propose `adapter: model-native` outside the sandbox; in a sandbox session write `adapter: recorded`, because the preview refuses a live adapter, and record the adapter the owner will choose after delivery as a decision. Every finding an agent keeps cites its evidence, so write a scenario where a finding without evidence is refused.
79
+ - **Another system** ("pull customers from X", "push invoices to Y"): the system and the operation its documentation names, the direction, which entity the rows become (a source writes through this module's import port `<module id>.<key>`), the field mapping (`rename`, `constant`, `format`, `lookup`), and whether it runs on demand or on a five-field cron. Consent and credentials are the owner's connector instance after delivery, never a spec value. In a sandbox session every adapter names `recorded: adapters/<name>.recorded.json`; the `spec-schema` gate refuses one without it with `SANDBOX_LIVE_ADAPTER_REFUSED`.
80
+ - **A document** ("a risk report", "an offer letter"): its title, the record it describes (`inputEntity`), `pdf` or `docx`, and the sections the body needs. The module renders it through `documents.templates.v1` and stores it as an attachment of that record, so write a scenario where a render without the module's permission on the record is refused.
81
+
82
+ A number the case needs (a score, a premium, a price per square metre) is an `actions[]` entry the module computes, never a value an agent writes.
83
+
71
84
  ## 4. Write the specification
72
85
 
73
86
  `modules/<dir>/spec/module.yaml`, `schemaVersion: 2`, `status: draft`. Keep the v1 keys (`id`, `specVersion`, `name`, `description`, `profile`, `capabilities`, `dependencies`, `tenancy`, `locales`, `invariants`, `permissions`, `dataOwnership`, `acceptanceScenarios`) and add the v2 arrays:
@@ -80,6 +93,9 @@ When the answers come back, copy each one into `decisions[]` with `decidedBy: us
80
93
  - `agentTools[]`: `{ id, permission, description, risk: read|workspace-write }`.
81
94
  - `outOfScope[]`: plain sentences, each naming the gap and the decision taken instead.
82
95
  - `decisions[]`: `{ id, question, answer, decidedBy: user|default }`; ids match `^[A-Z][A-Z0-9-]+$`, for example `D-UNIQUE-SKU`.
96
+ - `research`: `{ adapter: model-native|searxng|firecrawl|connector|recorded, allowDomains?, denyDomains?, monthlyQueryBudget?, evidenceOwner }`; domains are lower-case host names and `evidenceOwner` names an entity.
97
+ - `adapters[]`: `{ id, direction: source|sink, connector, operation, port, schedule?, mapping[], recorded? }`. `id` starts with the module id (`sales.core.crm-customers`); `connector` and `operation` are connector definition and operation keys (`^[a-z][a-z0-9-]*$`); a source `port` is an import port of this module or a declared dependency; `schedule` is a five-field cron or `null`; a mapping entry is `{ from?, to, transform: rename|constant|format|lookup, value? }` where only `constant` omits `from` and only `rename` omits `value`; `recorded` is `adapters/<name>.recorded.json` and required in a sandbox session.
98
+ - `templates[]`: `{ id, title, inputEntity, format: pdf|docx, body }`; `inputEntity` names an entity and `body` is `templates/<name>.md`.
83
99
 
84
100
  Put the primary entity's read and manage permissions first: the scaffold builds that entity and later permissions become constants only. Every `acceptanceScenarios[]` entry stays observable (given, when, then) and covers success, denial and the cross-tenant case, because each one becomes at least one test. The schema rejects unknown keys.
85
101
 
@@ -14,6 +14,7 @@ Flowdular loads translations at runtime. The shell owns locale selection and the
14
14
  - A module contribution imports `translations/en.json` and every declared locale, then returns `translations: { en, pl }` with its `moduleId`.
15
15
  - Use fully qualified keys with `t()`, for example `t('catalog.items.title')`. In `.tsrx`, import from `@flowdular/sdk/client`. In plain `.ts` helpers, import from `@flowdular/sdk/client/i18n` so tests do not pull the TSRX shell entry.
16
16
  - Navigation and account-menu labels use getters. Contributions are created before their bundles are registered, so eager `label: t(...)` can paint a raw key.
17
+ - A count is a plural family: `items.count.one` and `items.count.other` in `en`, `items.count.one`, `.few`, `.many` and `.other` in `pl`, read as `t('catalog.items.count', { count })` with a number. The runtime picks the member `Intl.PluralRules` selects, falls back to `.other` and then the plain key, and writes `{count}` in the active locale's number format; never choose `.one` or `.other` in code.
17
18
  - Locale-sensitive dates, numbers and currency use `activeLocale()` with `Intl.DateTimeFormat` or `Intl.NumberFormat`.
18
19
  - The personal locale selector lives in Profile and applies immediately. The tenant default remains an Administration setting and is the fallback when the browser has no personal choice.
19
20
 
@@ -61,7 +62,7 @@ pnpm --filter @flowdular/module-<dir> test
61
62
  pnpm format:check
62
63
  ```
63
64
 
64
- `module validate` rejects a missing locale file, mismatched locale key sets, and a static `t('module.key')` whose module bundle does not contain the key. A dynamic key cannot be proven statically, so test its complete value set.
65
+ `module validate` rejects a missing locale file, mismatched locale key sets (a plural family counts as its base key), a plural family missing a category its locale selects for whole numbers (`TRANSLATION_PLURAL_INCOMPLETE`), and a static `t('module.key')` whose module bundle does not contain the key. Tests compare locales with `translationKeys(bundle)` from `@flowdular/sdk/contracts`. A dynamic key cannot be proven statically, so test its complete value set.
65
66
 
66
67
  When a raw key appears in the UI, check in this order:
67
68
 
@@ -37,11 +37,11 @@ div.ui-view
37
37
  Drawer open title subtitle onClose form keyed by 'form-' + formSession
38
38
  ```
39
39
 
40
- `TableCard` is the record card and `Table` is the only table in the product: never hand-roll `table.ui-table` again, and never rebuild the head, the loading row or the empty state that these already own. `actions(row)` returns `TableAction[]`; the component renders visible compact buttons in its narrow trailing column. Do not build a dropdown or module-owned action markup. Fixed column widths apply through loading, empty and populated states. A cell returns nodes: `span.ui-cell` (`<b>` primary, `<small>` secondary), `ui-mono` for an identifier, `Tag` for state, `numeric: true` on the column for tabular figures.
40
+ `TableCard` is the record card and `Table` is the only table in the product: never hand-roll `table.ui-table` again, and never rebuild the head, the loading row or the empty state that these already own. `actions(row)` returns `TableAction[]`; the component renders up to two as compact buttons and folds more than two, or two in a narrow card, into its own More menu. Do not build a dropdown or module-owned action markup. Fixed column widths apply through loading, empty and populated states. A cell returns a typed cell and never raw text: `CellText` (`strong` for the name, `mono` for a code, `wrap` for prose), `CellStack` for a name over its id, `CellTime` for a timestamp, `CellCode` for an identifier, `CellTag` for state, `CellNumber` with `numeric: true` on the column, `CellMuted` for a missing value.
41
41
 
42
42
  The shared `Table` is backed by the official `@octanejs/tanstack-table` adapter. A module never imports TanStack directly. It supplies the Flowdular columns, rows and actions above, while `@flowdular/sdk/ui` owns the features, row model, header model and cell rendering.
43
43
 
44
- Every column declares `width`. Primary identity and descriptions get the largest share, dates and identifiers a medium share, and counts or status the smallest. For a table with actions, data widths normally add up to about 90 percent because the shared action column is 160 px. Without actions they add up to 100 percent. Do not leave all columns unspecified: equal distribution wastes space and weakens the hierarchy.
44
+ Every column declares `width`: px for a bounded column (a time 170, a status or tag 120 to 140, a number 100 to 140, an id 120 to 200) and `auto` for the one column that takes the rest. Every column also gets a `priority` for the reader: 1 (default) for the identity, the state and the main value, 2 for secondary ids, counts and timestamps, 3 for details and descriptions. A narrow card hides 3, then 2, and each row expands to list what is hidden; the first column is always 1. Rules and numbers: `docs/design-system.md`, section Tables.
45
45
 
46
46
  Drawer form: `form.ui-drawer__form > div.ui-drawer__body > div.ui-form > div.ui-form__row > FormField label required help` wrapping a native `input.ui-input`, `select.ui-select` or `textarea.ui-textarea`; `Alert` inside the body for the submit error; `div.ui-drawer__foot` with `<small>` for the constraint and `div.ui-form__actions` (Cancel, primary submit with `disabled={busy}` and a progressive label `Creating…`). `Drawer width="lg"` when rows have two columns or an editor.
47
47
 
@@ -60,7 +60,7 @@ Read-only master-detail (runs, playground) keeps `ui-two-col` (+ `--wide-aside`)
60
60
  - `Button`: `variant` primary, secondary (default), ghost, danger; `size` sm, md, lg; `type` button, submit; `block`; `disabled`; `onClick`.
61
61
  - `FormField`: `label`, `required`, `help`, `error`; one control child with `ui-input`, `ui-select` or `ui-textarea`.
62
62
  - `SearchField`: `value`, `placeholder`, `label` (accessible name), `onInput(value)`.
63
- - `Table`: `columns: TableColumn<Row>[]` (`key`, `header`, required `width`, `cell(row)`, `numeric`, `value(row)` for the comparable and searchable value behind the cell), `rows`, `rowKey(row)`, `status`, `loadingLabel`, `empty`, `emptyFiltered`, `filtered`, `sorting` with `sortingState` and `onSortingChange`, `globalFilter`, `pagination` (`pageIndex`, `pageSize`, `onPageChange`, `totalRows` when the module paged in SQL), `actions(row): TableAction[]`, `actionsLabel`, optional stable `actionsWidth` (160 px default, 280 px for two actions), `onSelect(row)`, `selectedKey`, `caption`. Only a column with `value` is sortable and searched. Multi-row selection: `selection` (`selectedKeys: ReadonlySet<string>`, `onSelectionChange(keys)`, `label` of the header checkbox, `rowLabel(row)`, optional `selectable(row)` and `clearLabel`) adds a leading checkbox column whose header toggles the rows on screen; `bulkActions: TableBulkAction[]` (`id`, `label`, `icon`, `tone`, `disabled`, `reason`, `onSelect(keys)`) render in the bar above the table while something is selected, and `selectionSummary(count)` translates "N selected". The screen keeps the keys in its own state and resets them on a page, sort or filter change; the header checkbox touches only the rows on screen, while the clear button empties the whole set through `onSelectionChange`.
63
+ - `Table`: `columns: TableColumn<Row>[]` (`key`, `header`, required `width`, `priority`, `cell(row)`, `numeric`, `value(row)` for the comparable and searchable value behind the cell), `rows`, `rowKey(row)`, `status`, `loadingLabel`, `empty`, `emptyFiltered`, `filtered`, `sorting` with `sortingState` and `onSortingChange`, `globalFilter`, `pagination` (`pageIndex`, `pageSize`, `onPageChange`, `totalRows` when the module paged in SQL), `actions(row): TableAction[]`, `actionsLabel`, optional minimum `actionsWidth` (160 px default, 280 px for two actions; the column grows to its widest buttons), `onSelect(row)`, `selectedKey`, `caption`. Only a column with `value` is sortable and searched. Multi-row selection: `selection` (`selectedKeys: ReadonlySet<string>`, `onSelectionChange(keys)`, `label` of the header checkbox, `rowLabel(row)`, optional `selectable(row)` and `clearLabel`) adds a leading checkbox column whose header toggles the rows on screen; `bulkActions: TableBulkAction[]` (`id`, `label`, `icon`, `tone`, `disabled`, `reason`, `onSelect(keys)`) render in the bar above the table while something is selected, and `selectionSummary(count)` translates "N selected". The screen keeps the keys in its own state and resets them on a page, sort or filter change; the header checkbox touches only the rows on screen, while the clear button empties the whole set through `onSelectionChange`.
64
64
  - `TableCard`: every `Table` prop plus `title`, `count`, `head`, `search`, `filters`, `before`, `after`, `note`, `noteIcon`.
65
65
  - `Pagination`: the pager for the `TableCard` `after` slot: `pageIndex`, `pageSize`, `totalRows` (after the screen's own filtering), `onPageChange(pageIndex)`, `pageSizes` with `onPageSizeChange(pageSize)` (both or neither), `label`, `previousLabel`, `nextLabel`, `pageSizeLabel`, `summary(range)` that the screen translates. It reads "1 of 1" over an empty set, so it is rendered unconditionally.
66
66
  - `Select`: the labelled native select: required `id`, `label`, `options` (`value`, `label`, `disabled`), `value`, `onChange(value)`, `placeholder`, `name` (defaults to `id`), `required`, `disabled`, `invalid`, `help`, `error`.
@@ -83,7 +83,7 @@ Read-only master-detail (runs, playground) keeps `ui-two-col` (+ `--wide-aside`)
83
83
  - `Icon`: `name`, `size` (18 default, 16 in controls, 14 in `Button size="sm"`), `strokeWidth`.
84
84
  - `BrandMark`: `size`, `signature`, `tone`; brand moments only.
85
85
 
86
- Icon keys (`ICON_PATHS`, `packages/ui/src/icons/Icon.tsrx`): `dashboard`, `parties`, `catalog`, `user`, `users`, `shield`, `code`, `modules`, `file-text`, `play`, `bot`, `flask`, `activity`, `plug`, `search`, `chevron-down`, `chevron-left`, `chevron-right`, `chevrons-up-down`, `sort`, `calendar`, `plus`, `panel-left`, `check`, `filter`, `download`, `more`, `external`, `alert`, `x`, `sign-out`, `refresh`, `help`, `info`, `key`, `settings`, `braces`. An unknown name renders `modules` silently, so check the list.
86
+ Icon keys (`ICON_PATHS`, `packages/ui/src/icons/Icon.tsrx`): `dashboard`, `parties`, `catalog`, `user`, `users`, `shield`, `code`, `modules`, `file-text`, `play`, `bot`, `flask`, `activity`, `plug`, `search`, `chevron-down`, `chevron-left`, `chevron-right`, `chevrons-up-down`, `sort`, `calendar`, `plus`, `panel-left`, `check`, `filter`, `download`, `more`, `external`, `alert`, `x`, `sign-out`, `refresh`, `help`, `info`, `key`, `settings`, `braces`, `copy`. An unknown name renders `modules` silently, so check the list.
87
87
 
88
88
  ## 5. Classes a module writes by hand (`packages/ui/src/styles/components.css`)
89
89
 
@@ -53,7 +53,8 @@ Do not load the whole skill catalog into the task context.
53
53
  network/git, or touch a DB outside their module tests.
54
54
  12. Keep handoffs short and factual. No AI attribution footers or em/en dashes.
55
55
  Sandbox final line: `HANDOFF: <allowed-role> - <why>` or
56
- `HANDOFF: none - <why>`, never your own role.
56
+ `HANDOFF: none - <why>`, never your own role. Branches, commits, PR body,
57
+ labels: `release-eject-pr` section 4.
57
58
  13. Flow: request, `spec-interview`, approval, `module-new`/`module-update`,
58
59
  `auto-review`. Implement from the approved spec and its touch list; do not
59
60
  scan `modules/` or `packages/`. First lookup is `.ai/platform-capabilities.md`.
@@ -53,7 +53,8 @@ Do not load the whole skill catalog into the task context.
53
53
  network/git, or touch a DB outside their module tests.
54
54
  12. Keep handoffs short and factual. No AI attribution footers or em/en dashes.
55
55
  Sandbox final line: `HANDOFF: <allowed-role> - <why>` or
56
- `HANDOFF: none - <why>`, never your own role.
56
+ `HANDOFF: none - <why>`, never your own role. Branches, commits, PR body,
57
+ labels: `release-eject-pr` section 4.
57
58
  13. Flow: request, `spec-interview`, approval, `module-new`/`module-update`,
58
59
  `auto-review`. Implement from the approved spec and its touch list; do not
59
60
  scan `modules/` or `packages/`. First lookup is `.ai/platform-capabilities.md`.
@@ -25,7 +25,7 @@ Use the already selected task skill. Consult `.ai/references/catalog` for implem
25
25
  14. Build UI only from `@flowdular/sdk/ui` components, `ui-*` classes and tokens (`docs/design-system.md`). No hardcoded colors, fonts or sizes; never restyle a `ui-*` class; `glyph` and `Icon name` are `ICON_PATHS` keys. Modules use the shared `Table` and `TableCard`, which are backed by the official Octane TanStack adapter; they never import `@octanejs/tanstack-table` directly. A missing primitive becomes a module-local component on tokens, flagged as a promotion candidate. Guard UX: no visual artifacts. A card head is one line (title left; search and the `Filters` dropdown right), filters live inside `Filters` not loose in the head, form rows top-align so a `help` line never drops its neighbour, a field is labelled once, no decorative tags, long values use `ui-mono`/`ui-table-wrap`, adjacent top-level nodes go in a fragment. Look at the rendered screen before handing off and fix any alignment, wrapping, padding or duplicated-label glitch.
26
26
  15. One screen, form, table or stateful region per named component. Records own the page; create and edit happen in a `Drawer`. Every screen shows loading, empty, error, populated and denied.
27
27
  16. Tests live in `tests/*.test.ts`: identity, tenant isolation, uniqueness, one 401 and one 403 per endpoint, each validation bound. Repository behavior uses `createTestDatabaseProvider()` from `@flowdular/sdk/database-testing`: PGlite locally and isolated server PostgreSQL in CI. Open one provider per test file, migrate once, and truncate module tables between cases (`modules/profile/tests/support/database.ts`). Tenancy tests use two tenants, prove `TENANT_CONTEXT_REQUIRED` without a transaction tenant id, prove RLS prevents cross-tenant reads and writes under the non-bypass runtime role, and cover `WITH CHECK`. Tenant fixture work also supplies tenant context on a migration connection. The sandbox `tests` gate passes with zero tests, so an empty suite is a defect.
28
- 17. Translations are live. Every module contribution registers all declared `translations/*.json` bundles, user-facing copy uses fully qualified `t('<module>.<key>')` keys, navigation labels are lazy getters, locale-aware formatting uses `activeLocale()`, and every locale has the same key set. `module validate` rejects missing files, key drift, and missing static translation keys.
28
+ 17. Translations are live. Every module contribution registers all declared `translations/*.json` bundles, user-facing copy uses fully qualified `t('<module>.<key>')` keys, navigation labels are lazy getters, locale-aware formatting uses `activeLocale()`, and every locale has the same key set, where a plural family (`<key>.one`/`.other`, plus `.few`/`.many` in `pl`, read as `t(key, { count })`) counts as one key. `module validate` rejects missing files, key drift, and missing static translation keys.
29
29
  18. Numbered migration SQL is immutable source. `migrations/000N_<module>_<name>.up.sql` and `.down.sql` are PostgreSQL and the only schema source; there is no dialect subdirectory. `src/services/migration.ts` mirrors every `.up.sql` byte for byte as `databaseMigrations: readonly DatabaseMigration[]` with `sql: { postgresql: ... }`, and the runtime calls `runDatabaseMigrations` through its provider lease. The namespaced `_coreloom_migrations_v2` ledger records the checksum, adopts only an explicit complete `inspectExisting` result (`postgresTenantTableState` is the standard check), and refuses drift, duplicates, or partial schema. Add a new numbered, additive migration instead of changing existing bytes. A tenant table's migration includes enabled and forced RLS plus a tenant policy, with the policy behavior covered by tests.
30
30
  19. Module CLI commands live in `src/cli/commands.json` and `src/cli/index.ts`, metadata-identical, inside the module namespace.
31
31
  20. Passwords, session tokens and provider credentials never leave `auth.core` (or the `agents.core` vault) and never appear in logs, audit metadata or responses.
@@ -184,11 +184,16 @@ pnpm typecheck # every workspace package
184
184
  pnpm test # every workspace package
185
185
  pnpm validate # spec, blueprint and module validation
186
186
  pnpm format:check # Prettier
187
- pnpm verify # typecheck + test + validate + format:check
187
+ pnpm verify # verify:static, then test
188
+ pnpm verify:static # rules, reference, capabilities, platform API, typecheck, validate, format
188
189
  ```
189
190
 
190
- `pnpm verify` is the gate CI runs and the gate a pull request is expected to
191
- pass.
191
+ `pnpm verify` is the gate a pull request is expected to pass. CI runs the same
192
+ gate split for speed: `verify:static` in one job and the test suites in three
193
+ parallel shards (`node scripts/test-shards.mjs --shard <n>/3`), balanced by the
194
+ per-package seconds in `scripts/test-weights.json`. A new package without a
195
+ weight counts as 30 seconds; refresh the file from a CI run when the shards
196
+ drift apart.
192
197
 
193
198
  ## Official module distribution
194
199
 
@@ -645,6 +645,35 @@ alike, sends `content-disposition: attachment` with
645
645
  `cache-control: private, no-store`, and is limited to 600 reads a minute per
646
646
  caller.
647
647
 
648
+ ## Documents (`documents.core`)
649
+
650
+ | Variable | Default | Purpose |
651
+ | ------------------------ | ------- | ------------------------------------------------------------ |
652
+ | `FD_DOCUMENTS_OCR_URL` | unset | https URL of the OCR service a scan or an image is sent to |
653
+ | `FD_DOCUMENTS_OCR_TOKEN` | unset | Bearer token sent to that service, printable ASCII, 4096 max |
654
+
655
+ Reading text out of a stored document needs no configuration: a PDF text layer,
656
+ DOCX, XLSX, PPTX, CSV and plain text are read in process, at most 200 pages and
657
+ 2 MiB of text per document, and the text is kept in `documents_text` until the
658
+ document is deleted. A PDF without a text layer and an image need OCR, which is
659
+ a deployment seam: without `FD_DOCUMENTS_OCR_URL` such a document answers
660
+ `unscanned`.
661
+
662
+ With the URL set, the document bytes are posted to it (by the text runner for a
663
+ stored document, within the call for bytes a module hands over) with the
664
+ document's content type, `accept: application/json` and
665
+ `authorization: Bearer <FD_DOCUMENTS_OCR_TOKEN>` when a token is set. The answer
666
+ is JSON, either `{ "pages": ["page one", "page two"] }` or `{ "text": "..." }`
667
+ with pages separated by a form feed, at most 8 MiB, within 60 seconds; a
668
+ redirect, another status or another shape leaves the document `unscanned` with
669
+ the reason `DOCUMENT_OCR_FAILED`, and a manager can retry it from the Text tab.
670
+ The call follows the connectors egress rules through `connectors.egress.v1`:
671
+ the host is resolved and checked for public addresses on every call and the
672
+ connection is pinned to those addresses, so OCR needs `connectors.core` in the
673
+ composition. The module refuses to boot when the URL is not an https URL on
674
+ port 443 to a public host name without credentials, or when the token is not
675
+ printable ASCII. Neither the token nor the document bytes are logged.
676
+
648
677
  ## Sandbox
649
678
 
650
679
  | Variable | Default | Purpose |
@@ -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.7.1
12
- pnpm flowdular module install expenses.core@0.7.1 --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
 
@@ -147,6 +184,19 @@ A running `pnpm dev` picks the change up live: the octane plugin reloads server
147
184
  routes when the generated composition changes and the client hot-reloads, so no
148
185
  rebuild is needed. `pnpm dev` and `pnpm build` run the sync automatically.
149
186
 
187
+ ### Per-workspace activation
188
+
189
+ Enabling composes a module into the application; whether a given workspace
190
+ uses it is an owner's decision made in Administration, Modules. Every composed
191
+ module is active until an owner deactivates it there, and the change applies to
192
+ that workspace alone: its endpoints answer 403 `MODULE_INACTIVE`, and the shell
193
+ hides the module's navigation, views, widgets and command search. `system.core`,
194
+ `auth.core`, `users.core` and `profile.core` cannot be deactivated, and neither
195
+ can a module another active module depends on; the refusal names the
196
+ dependents. Each change is audited. The state lives in `system.core` and is
197
+ published to other modules as the `system.modules.v1` capability; see
198
+ `.ai/platform-capabilities.md`.
199
+
150
200
  ## Versions, ranges and capabilities
151
201
 
152
202
  A module carries one version in three places: `module.json` `version`,
@@ -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.