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.
- package/README.md +1 -1
- package/agent-template/.agents/skills/agent-tool-design/SKILL.md +25 -0
- package/agent-template/.agents/skills/integration-adapter/SKILL.md +116 -0
- package/agent-template/.agents/skills/module-new/SKILL.md +17 -2
- package/agent-template/.agents/skills/module-update/SKILL.md +14 -2
- package/agent-template/.agents/skills/release-eject-pr/SKILL.md +23 -26
- package/agent-template/.agents/skills/spec-interview/SKILL.md +16 -0
- package/agent-template/.agents/skills/translations-i18n/SKILL.md +2 -1
- package/agent-template/.agents/skills/ux-design/SKILL.md +4 -4
- package/agent-template/.ai/agents/sandbox/backend-engineer.md +9 -0
- package/agent-template/.ai/blueprints/agentic-module/README.md +15 -0
- package/agent-template/.ai/blueprints/agentic-module/allowed-paths.yaml +14 -0
- package/agent-template/.ai/blueprints/agentic-module/blueprint.json +19 -0
- package/agent-template/.ai/blueprints/agentic-module/gates.yaml +24 -0
- package/agent-template/.ai/blueprints/agentic-module/input.schema.json +18 -0
- package/agent-template/.ai/blueprints/agentic-module/plan.schema.json +35 -0
- package/agent-template/.ai/blueprints/agentic-module/required-files.yaml +28 -0
- package/agent-template/.ai/blueprints/agentic-module/spec-requirements.yaml +33 -0
- package/agent-template/.ai/blueprints/agentic-module/steps.yaml +68 -0
- package/agent-template/.ai/platform-capabilities.md +16 -11
- package/agent-template/.ai/references/catalog/migrations/0005_catalog_list_indexes.down.sql +3 -0
- package/agent-template/.ai/references/catalog/migrations/0005_catalog_list_indexes.up.sql +11 -0
- package/agent-template/.ai/references/catalog/module.json +12 -2
- package/agent-template/.ai/references/catalog/package.json +2 -2
- package/agent-template/.ai/references/catalog/spec/module.yaml +26 -5
- package/agent-template/.ai/references/catalog/src/agent/tools.ts +19 -10
- package/agent-template/.ai/references/catalog/src/api/endpoints.ts +150 -10
- package/agent-template/.ai/references/catalog/src/api/list-cursor.ts +83 -0
- package/agent-template/.ai/references/catalog/src/client/CatalogView.tsrx +505 -159
- package/agent-template/.ai/references/catalog/src/client/api.ts +124 -36
- package/agent-template/.ai/references/catalog/src/client/contribution.tsrx +5 -0
- package/agent-template/.ai/references/catalog/src/client/state.ts +169 -3
- package/agent-template/.ai/references/catalog/src/domain/lists.ts +7 -0
- package/agent-template/.ai/references/catalog/src/domain/types.ts +20 -0
- package/agent-template/.ai/references/catalog/src/platform.ts +20 -0
- package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +143 -8
- package/agent-template/.ai/references/catalog/src/services/database-repository.ts +104 -17
- package/agent-template/.ai/references/catalog/src/services/item-export.ts +81 -0
- package/agent-template/.ai/references/catalog/src/services/migration.ts +27 -1
- package/agent-template/.ai/references/catalog/src/services/repository.ts +31 -2
- package/agent-template/.ai/references/catalog/tests/agent-tools.test.ts +6 -5
- package/agent-template/.ai/references/catalog/tests/client-state.test.ts +124 -0
- package/agent-template/.ai/references/catalog/tests/endpoints.test.ts +269 -0
- package/agent-template/.ai/references/catalog/tests/export.test.ts +134 -0
- package/agent-template/.ai/references/catalog/tests/idempotency.test.ts +15 -14
- package/agent-template/.ai/references/catalog/tests/list.test.ts +217 -0
- package/agent-template/.ai/references/catalog/tests/migrations.test.ts +58 -2
- package/agent-template/.ai/references/catalog/tests/module.test.ts +2 -1
- package/agent-template/.ai/references/catalog/tests/support/database.ts +14 -0
- package/agent-template/.ai/references/catalog/translations/en.json +35 -4
- package/agent-template/.ai/references/catalog/translations/pl.json +35 -4
- package/agent-template/.ai/references/catalog.provenance.json +34 -26
- package/agent-template/.ai/rules/flowdular.md +2 -1
- package/agent-template/.ai/skills/README.md +1 -0
- package/agent-template/.ai/skills/agent-tool-design/SKILL.md +25 -0
- package/agent-template/.ai/skills/integration-adapter/SKILL.md +121 -0
- package/agent-template/.ai/skills/module-new/SKILL.md +17 -2
- package/agent-template/.ai/skills/module-update/SKILL.md +14 -2
- package/agent-template/.ai/skills/release-eject-pr/SKILL.md +23 -26
- package/agent-template/.ai/skills/spec-interview/SKILL.md +16 -0
- package/agent-template/.ai/skills/translations-i18n/SKILL.md +2 -1
- package/agent-template/.ai/skills/ux-design/SKILL.md +4 -4
- package/agent-template/.claude/skills/agent-tool-design/SKILL.md +25 -0
- package/agent-template/.claude/skills/integration-adapter/SKILL.md +116 -0
- package/agent-template/.claude/skills/module-new/SKILL.md +17 -2
- package/agent-template/.claude/skills/module-update/SKILL.md +14 -2
- package/agent-template/.claude/skills/release-eject-pr/SKILL.md +23 -26
- package/agent-template/.claude/skills/spec-interview/SKILL.md +16 -0
- package/agent-template/.claude/skills/translations-i18n/SKILL.md +2 -1
- package/agent-template/.claude/skills/ux-design/SKILL.md +4 -4
- package/agent-template/AGENTS.md +2 -1
- package/agent-template/CLAUDE.md +2 -1
- package/agent-template/docs/agent-contract.md +1 -1
- package/agent-template/docs/cli.md +8 -3
- package/agent-template/docs/configuration.md +29 -0
- package/agent-template/docs/design-system.md +112 -13
- package/agent-template/docs/module-distribution.md +10 -3
- package/agent-template/docs/modules.md +51 -1
- package/agent-template/docs/operations.md +2 -0
- package/agent-template/docs/sandbox.md +76 -1
- package/assets/flowdular-banner.webp +0 -0
- package/package.json +1 -1
- package/template/default/flowdular.json +2 -0
- package/template/default/modules/example/package.json +1 -1
- package/template/default/modules/example/src/client/NotesView.tsrx +12 -16
- package/template/default/modules/example/tests/module.test.ts +3 -2
- package/template/default/modules/example/translations/pl.json +3 -1
- package/template/default/package.json +1 -1
- package/template/default/platform/package.json +1 -1
- package/template/default/platform/src/generated/modules.client.ts +4 -0
- package/template/default/platform/src/generated/modules.server.ts +68 -8
- package/assets/flowdular-banner.png +0 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[](https://flowdular.com)
|
|
2
2
|
|
|
3
3
|
# create-flowdular
|
|
4
4
|
|
|
@@ -193,6 +193,31 @@ nothing.
|
|
|
193
193
|
|
|
194
194
|
Declare `settings: defineModuleSettings({...})` (from `@flowdular/sdk/kernel`) by returning it from the composition, keep a reference to `PlatformServerContext.settings` in the tool factory, and read it per call as `settings.get<number>(context.tenantId, '<module>.core', 'key')` at request time, never at boot. Declared settings render in the module's drawer under Administration, Modules automatically.
|
|
195
195
|
|
|
196
|
+
## 6. Research and evidence
|
|
197
|
+
|
|
198
|
+
When the spec declares `research`, agents gather outside facts through `research.core`, and a module's own tools record what the agent concluded. Two rules decide the design.
|
|
199
|
+
|
|
200
|
+
**Evidence ids travel with findings.** `research.search` and `research.fetch` belong to `research.core` (`risk: 'workspace-write'` like `connectors.call`, because `external` asks for a signed grant on every call; `idempotency: 'none'`, behind the harness consent gate `research.consent`, which refuses with `TOOL_NOT_CONSENTED` until an owner turns on `research.core.allowAgents`). Every result the run keeps and every page it reads becomes an evidence row carrying the run id, and `research.fetch` answers its `evidenceId`. A module never registers a tool that opens a URL. The module tool that stores a finding on the `evidenceOwner` record takes the ids in its input:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
additionalProperties: false,
|
|
206
|
+
required: ['recordId', 'finding', 'evidenceIds'],
|
|
207
|
+
properties: {
|
|
208
|
+
recordId: { type: 'string' },
|
|
209
|
+
finding: { type: 'string' },
|
|
210
|
+
evidenceIds: { type: 'array', items: { type: 'string' } },
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The service it calls bounds the list (at least one id, at most a small fixed number), resolves each id with `get(tenantId, id)` on `research.evidence.v1` so an id of another tenant or an invented one refuses the whole call, calls `attach(tenantId, '<module id>', recordId, evidenceIds)`, and only then commits the finding, so a failed attach never leaves a finding without its sources. The tool output echoes the ids, and an agent's `outputSchema` carries them beside each finding, so a reviewer, an approval and a later document can all reach the source.
|
|
216
|
+
|
|
217
|
+
**The model never computes.** A score, a premium, a total or a price per square metre is a module action or tool that runs deterministic code over stored inputs and a versioned rule or table, and answers the value with that version. The agent passes references (record ids, evidence ids, the inputs it read) and never a figure of its own; a tool never stores a number the model supplied as the result, and an `outputSchema` field for a computed value is filled from the action's answer, not from generation.
|
|
218
|
+
|
|
219
|
+
Tests for this section: a finding without evidence and a finding citing another tenant's evidence are refused before any write; the computation answers the same value for the same inputs and names its rule version; a run without research consent sees `TOOL_NOT_CONSENTED` and writes nothing.
|
|
220
|
+
|
|
196
221
|
## Pitfalls
|
|
197
222
|
|
|
198
223
|
- A tool id equal to an endpoint id is a convention, not a requirement; keep them parallel for traceability. A read-by-id tool with no dedicated endpoint reuses the read endpoint id under the same permission.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: integration-adapter
|
|
3
|
+
description: >-
|
|
4
|
+
Add a source or sink adapter for a named external service from its API
|
|
5
|
+
documentation: the connector definition, the port, the mapping, the recorded
|
|
6
|
+
fixture, the consent check and the call and row records.
|
|
7
|
+
---
|
|
8
|
+
# Add an integration adapter
|
|
9
|
+
|
|
10
|
+
An adapter moves records between this module and a service the business already runs (an accounting package, a CRM, a bank feed, a listing portal). A **source** pulls pages from the service and writes them through an import port; a **sink** pushes the pages of a list export to the service. Every call leaves through `connectors.core`, so the egress policy, the sealed credentials, the owner's consent and the call log apply without code of your own.
|
|
11
|
+
|
|
12
|
+
## 1. Read exactly this
|
|
13
|
+
|
|
14
|
+
1. The approved spec: its `adapters[]` entry (`id`, `direction`, `connector`, `operation`, `port`, `schedule`, `mapping`, `recorded`) and the entity the port writes. `pnpm flowdular spec validate` already checked that the id starts with the module id, that a source port belongs to this module or a declared dependency and that `schedule` is a five-field cron; in a sandbox session the `spec-schema` gate also refuses an adapter without `recorded` (`SANDBOX_LIVE_ADAPTER_REFUSED`).
|
|
15
|
+
2. The service's API documentation the brief or the session attachments supply: base URL, authentication, the list or push endpoint, its paging parameters, one example response.
|
|
16
|
+
3. `.ai/platform-capabilities.md`, the Connectors, Import, List export and Background work entries.
|
|
17
|
+
4. `src/adapters/<name>.ts` and the recorded fixture stub, which the scaffold wrote from the spec entry.
|
|
18
|
+
|
|
19
|
+
Anything the documentation does not settle (which field is the natural key, what a missing value means, how deep paging goes) is a spec defect: hand it back, never guess.
|
|
20
|
+
|
|
21
|
+
## 2. The connector definition
|
|
22
|
+
|
|
23
|
+
Use the shipped `http-json` definition (operations `get`, `post`, `put`, `patch`, `delete`, the whole path from the call input) unless the spec names a definition of this module. A definition of your own is registered while the module composes, through `connectors.definitions.v1` (`modules/connectors/src/domain/definitions.ts`):
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
context.capabilities
|
|
27
|
+
.get<ConnectorDefinitionRegistry>(CONNECTORS_DEFINITIONS_CAPABILITY)
|
|
28
|
+
?.register({
|
|
29
|
+
key: 'erp-vendors', // the spec's connector, ^[a-z][a-z0-9-]{0,95}$
|
|
30
|
+
moduleId: 'vendors.core',
|
|
31
|
+
label: 'ERP vendors',
|
|
32
|
+
authKinds: ['bearer'], // what the documentation offers
|
|
33
|
+
operations: [
|
|
34
|
+
{
|
|
35
|
+
key: 'list-vendors', // the spec's operation
|
|
36
|
+
label: 'List vendors',
|
|
37
|
+
method: 'GET',
|
|
38
|
+
path: '/api/v2/vendors', // {name} expands one segment, {+name} a whole path
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
properties: { query: { type: 'object' } },
|
|
43
|
+
},
|
|
44
|
+
outputSchema: { type: 'object' },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
defaultAllowedHosts: ['erp.example.com'], // the API host only
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Declare `connectors.definitions.v1` and `connectors.calls.v1` under `requires` (optional when the module works without the connector) and `connectors.core` with its range under `dependencies` in the spec and `module.json`, plus `@flowdular/sdk/modules/connectors` in `package.json` for the types. The base URL, the credential and the host allowlist are the owner's instance, created after delivery; no key, token or URL of a tenant ever sits in code, a fixture or a log line.
|
|
52
|
+
|
|
53
|
+
## 3. The port
|
|
54
|
+
|
|
55
|
+
- **Source.** The rows land through an import port (`modules/import/src/domain/ports.ts`): `fields`, a `naturalKey` that makes a repeated pull idempotent, per-row outcomes under `create-only`, `update-existing` or `skip-existing`. The spec's `port` is `<module id>.<key>` of this module or a declared dependency. `adapters.core` writes through `import.write.v1` (`modules/import/src/domain/write.ts`), which checks the port's permission on the run's principal and calls the port's own `validate` and `write`; the module never calls its port for an adapter itself.
|
|
56
|
+
- **Sink.** The spec's `port` is a list export id of this module (`defineListExport`, `packages/server/src/export/`, reference `modules/users/src/services/member-export.ts`). `adapters.core` finds it through `exports.lists.v1` and walks its `page` under the run's principal, which must hold the list's permission.
|
|
57
|
+
|
|
58
|
+
## 4. The registration and the mapping
|
|
59
|
+
|
|
60
|
+
Register the adapter while the module composes, sources through `adapters.sources.v1` and sinks through `adapters.sinks.v1` (`modules/adapters/src/domain/registry.ts`), and add both with `optional: true` under `requires`, calling again from `start` when the capability was not there yet (the `exports.lists.v1` pattern in `.ai/references/catalog/src/platform.ts`):
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import fixture from '../adapters/erp-vendors.recorded.json' with { type: 'json' };
|
|
64
|
+
|
|
65
|
+
sources.register('vendors.core', [
|
|
66
|
+
{
|
|
67
|
+
...ERP_VENDORS_ADAPTER, // the scaffolded declaration: id, direction, connector, operation, port, schedule, mapping
|
|
68
|
+
label: 'ERP vendors',
|
|
69
|
+
recorded: fixture, // the parsed fixture, not its path
|
|
70
|
+
input: { path: '/api/v2/vendors', query: { limit: 100 } }, // every call starts from this
|
|
71
|
+
items: 'data', // the record array in the answer; '' is the answer itself
|
|
72
|
+
paging: { kind: 'cursor', param: 'query.cursor', next: 'meta.next_cursor' }, // or { kind: 'page', param: 'query.page', start: 1 }
|
|
73
|
+
mode: 'update-existing',
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A sink names `items` as the input path a batch goes to (`body.records`) and `batchSize` (1 to 200, default 50) instead of `paging` and `mode`. The id must start with the module id, a sink port must be this module's own list, and a malformed cron, mapping, path, paging or fixture throws `ADAPTER_REGISTRATION_INVALID` at boot.
|
|
79
|
+
|
|
80
|
+
The mapping is data: the spec's rules as registered, or the override an owner saves on the Data adapters screen. `from` is a dotted path into the service record (a list column key for a sink), `to` a port field id (a dotted path of the pushed record for a sink):
|
|
81
|
+
|
|
82
|
+
- `rename`: copy the value; a missing or null value leaves the field absent.
|
|
83
|
+
- `constant`: write `value`.
|
|
84
|
+
- `format`: parse with `value` set to `trim`, `lower`, `upper`, `integer`, `decimal`, `boolean`, `iso-date` or `date:<layout>` over `YYYY`, `MM` and `DD`; a value that does not parse refuses the row with `MAPPING_FORMAT_INVALID`.
|
|
85
|
+
- `lookup`: replace the value through the rule's `table`, which the owner fills on the screen; an unmatched value refuses the row with `MAPPING_LOOKUP_UNMATCHED`. The import port contract has no lookup of its own, so a lookup against the target's records is not available.
|
|
86
|
+
|
|
87
|
+
A value that is not text, a number or a boolean, or is longer than 2000 characters, refuses the row with `MAPPING_VALUE_INVALID`.
|
|
88
|
+
|
|
89
|
+
## 5. The run
|
|
90
|
+
|
|
91
|
+
`adapters.core` runs the adapter; the module keeps no run table, job or timer. An owner binds a `connectors.core` instance of the declared definition, checks the mapping with a dry run and enables the adapter. A run is a row claimed by the shared job runner: every call goes through `connectors.calls.v1` with caller `workflow` and `callerRef` set to the run id after `consented` admitted it, each page is tried three times with full jitter and `Retry-After`, the outcomes and the next cursor commit once per page, a process that dies is taken over from the stored cursor, and a failed page keeps its cursor for Resume. A sink stores its row position, re-walks the list to it after a restart (so the list's order must be stable) and pushes under an idempotency key per run chain, row position and slot. A `schedule` runs on its cron in the workspace zone through `adapters.core` itself.
|
|
92
|
+
|
|
93
|
+
## 6. The recorded fixture
|
|
94
|
+
|
|
95
|
+
`recorded` names `adapters/<name>.recorded.json`: `{ adapter, operation, calls: [{ input, body }] }`, where `adapter` and `operation` equal the registration's. A call answers the `body` of the first recorded call whose `input` equals the call input, else of the first whose `input` is contained in it (every key it names, at every depth, with the same value), and `ADAPTER_RECORDED_CALL_MISSING` otherwise. Record one call per page with the exact input the paging produces (`{ path, query: { limit } }`, then `{ path, query: { limit, cursor } }`), and a sink push by the keys that matter (`{ path: '/import' }`). The fixture answers only while the adapter is bound to no instance and the platform is not in production. Write it from the documentation's example responses or the session's sample data, trimmed to a few rows that exercise every mapping rule, including one row each rule refuses. It never holds a credential, a live tenant's data or a response recorded from a production system. In a sandbox session it is the only way the adapter runs.
|
|
96
|
+
|
|
97
|
+
## 7. What the records must show
|
|
98
|
+
|
|
99
|
+
- Consent: a run without the instance's `allowWorkflows` fails with `ADAPTER_CONSENT_MISSING` and calls nothing.
|
|
100
|
+
- Calls: one `connectors.core` call log row per call with the instance, the operation, the caller `workflow`, `callerRef` set to the run id, the outcome, the status, the error class, the duration and the byte counts; never a body.
|
|
101
|
+
- Rows: `adapter_runs` with the adapter, the trigger, the cursor and the counts, and one `adapter_run_rows` outcome per record with its natural key and reason, so a person can answer which record came from where.
|
|
102
|
+
|
|
103
|
+
## 8. Tests
|
|
104
|
+
|
|
105
|
+
Against the recorded fixture, never the network:
|
|
106
|
+
|
|
107
|
+
- the registration composes (`adapters.sources.v1` or `adapters.sinks.v1` accepts it with the fixture);
|
|
108
|
+
- the fixture answers every page the paging asks for, and every mapping rule writes or refuses a row as intended (a dry run through `POST /api/adapters/dry-run` shows it);
|
|
109
|
+
- the port refuses what the module refuses, so a repeated pull updates or skips by the natural key.
|
|
110
|
+
|
|
111
|
+
## Pitfalls
|
|
112
|
+
|
|
113
|
+
- `risk: 'external'` is refused by the runner and the harness; an adapter is a registration `adapters.core` runs through a consented connector, never an external action of its own.
|
|
114
|
+
- The egress policy refuses redirects and private addresses; a documentation example on `http://` or a local host will not run.
|
|
115
|
+
- A page answers at most 1000 records and a run reads at most 1000 pages (`ADAPTER_PAGE_TOO_LARGE`, `ADAPTER_PAGES_EXCEEDED`); set the page size in `input` well below that.
|
|
116
|
+
- A connector `outputSchema` of `{ type: 'object' }` checks nothing; the mapping function is where a changed response is refused.
|
|
@@ -33,6 +33,9 @@ Each spec element maps to files:
|
|
|
33
33
|
| `widgets[]` | a widget component plus a `widgets` entry with its `slot` in `src/client/contribution.tsrx` |
|
|
34
34
|
| `settings[]` | `src/settings.ts` (`defineModuleSettings`) and `settings:` in `src/platform.ts` |
|
|
35
35
|
| `agentTools[]` | `src/agent/tools.ts` and the `context.agentTools.register` call, as a separate `agent-tool-design` phase |
|
|
36
|
+
| `research` | `src/research.ts`, `research-fixtures.json`, `requires` of the capabilities, the evidence attach where the `evidenceOwner` record is written |
|
|
37
|
+
| `adapters[]` | `src/adapters/<name>.ts`, the port, the adapter registration, `adapters/<name>.recorded.json`, as a separate `integration-adapter` phase |
|
|
38
|
+
| `templates[]` | `templates/<name>.md`, `templates` in `package.json` `files`, `src/templates.ts` registered through `documents.templates.v1` in `src/platform.ts` |
|
|
36
39
|
| `permissions[]` | `src/acl/permissions.ts`, the endpoint `access.permission`, the client `scope` |
|
|
37
40
|
| `acceptanceScenarios[]` | `tests/module.test.ts` and its siblings, at least one case each |
|
|
38
41
|
| `outOfScope[]`, `decisions[]` | no code. Read them so you do not rebuild a decision or implement a deferred feature. |
|
|
@@ -52,7 +55,7 @@ pnpm flowdular module new inventory.core --spec modules/inventory/spec/module.ya
|
|
|
52
55
|
pnpm flowdular module new inventory.core --spec modules/inventory/spec/module.yaml --apply
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
`packages/cli/src/module-templates.ts` (`planScaffold`) writes a module the generated composition can import, formatted with the workspace Prettier: `module.json` with `platform.{server,client}` from the capabilities, `package.json` with the `.`, `./client`, `./server`, `./platform` exports and pinned versions, `tsconfig.json` with `types: ["node"]`, the spec copy, `src/index.ts`, `src/acl/permissions.ts` (`X_PERMISSIONS` built from the spec `permissions`), `src/domain/types.ts`, `src/services/{repository,<suffix>-service,index}.ts`, `src/api/endpoints.ts`, and then by capability: `database` gives `src/services/{migration,database-repository}.ts` plus the PostgreSQL `migrations/0001_<snake>_core.{up,down}.sql`, otherwise `src/services/memory-repository.ts`; `api` gives `src/server/{runtime,index}.ts` and `src/platform.ts` with `createServerComposition`; `client` gives `src/client/{index.ts,contribution.tsrx,<Pascal>View.tsrx}` plus `api.ts` and `state.ts` when a read permission exists; `cli` gives `src/cli/{commands.json,index.ts}`; always `tests/module.test.ts` (identity and tenant isolation) and `translations/<locale>.json` (`pl` gets the placeholder `Moduł <name>`).
|
|
58
|
+
`packages/cli/src/module-templates.ts` (`planScaffold`) writes a module the generated composition can import, formatted with the workspace Prettier: `module.json` with `platform.{server,client}` from the capabilities, `package.json` with the `.`, `./client`, `./server`, `./platform` exports and pinned versions, `tsconfig.json` with `types: ["node"]`, the spec copy, `src/index.ts`, `src/acl/permissions.ts` (`X_PERMISSIONS` built from the spec `permissions`), `src/domain/types.ts`, `src/services/{repository,<suffix>-service,index}.ts`, `src/api/endpoints.ts`, and then by capability: `database` gives `src/services/{migration,database-repository}.ts` plus the PostgreSQL `migrations/0001_<snake>_core.{up,down}.sql`, otherwise `src/services/memory-repository.ts`; `api` gives `src/server/{runtime,index}.ts` and `src/platform.ts` with `createServerComposition`; `client` gives `src/client/{index.ts,contribution.tsrx,<Pascal>View.tsrx}` plus `api.ts` and `state.ts` when a read permission exists; `cli` gives `src/cli/{commands.json,index.ts}`; always `tests/module.test.ts` (identity and tenant isolation) and `translations/<locale>.json` (`pl` gets the placeholder `Moduł <name>`). From the optional spec sections it writes `src/research.ts` and `research-fixtures.json` (one example query and page) for `research`, one `src/adapters/<name>.ts` (the id without the module id, dots as hyphens) plus a recorded fixture stub at the declared `recorded` path, else `adapters/<name>.recorded.json`, per adapter, and one `templates/<name>.md` per template body; none of them runs anything.
|
|
56
59
|
|
|
57
60
|
The scaffold is PostgreSQL from the first commit. `database-repository.ts`
|
|
58
61
|
exports `Database<Name>Repository` and `migrate<Name>Database`, takes a
|
|
@@ -164,6 +167,18 @@ Contribution rules (`packages/client/src/contributions.ts`): `navigation[].group
|
|
|
164
167
|
|
|
165
168
|
State and data: `useMemo(() => createXClientState(), [])` per component, `cell<T>()` for typed fields, `const [items] = useValue(state.items)`, `store.act((transaction) => transaction.set(state.items, records), 'inventory/loaded')`. Mutations send `content-type: application/json`, `x-csrf-token`, `credentials: 'same-origin'`. `Kpi.value` is a string. Screen and form pattern: `ux-design`.
|
|
166
169
|
|
|
170
|
+
## 4b. Research, adapters and documents
|
|
171
|
+
|
|
172
|
+
Only when the approved spec declares the section; the spec names every value below.
|
|
173
|
+
|
|
174
|
+
`research`: research belongs to `research.core`. Its capabilities `research.search.v1`, `research.fetch.v1` and `research.evidence.v1` (`modules/research/src/domain/capability.ts`) go under `requires` (optional when the module works without research), `research.core` under `dependencies`, `@flowdular/sdk/modules/research` in `package.json` for the types. Pages are read only inside agent runs through its tools `research.search` and `research.fetch`, which an owner enables with `research.core.allowAgents`; the module never opens a URL itself. The module owns the evidence link: the service method that stores a finding on an `evidenceOwner` record takes `evidenceIds`, checks each with `get(tenantId, id)`, refuses a finding without evidence, and calls `attach(tenantId, '<module id>', recordId, evidenceIds)` before it commits the finding, so a failed attach never leaves a finding without its sources. The record screen lists `list(tenantId, '<module id>', recordId)` and links each entry with `workspaceViewHref('research-evidence') + '?id=' + id`. Tests fake the three capabilities; the sandbox preview answers research from `research-fixtures.json` (`{ queries: { <query>: [{ url, title, snippet, source }] }, pages: { <url>: { title, text } } }`), so replace the scaffolded example with the queries and pages the scenarios need. Nothing reaches the network.
|
|
175
|
+
|
|
176
|
+
`adapters[]`: each adapter is a separate `integration-adapter` phase after the server file set: connector definition or the shipped `http-json`, the import port or list export the spec names, the registration through `adapters.sources.v1` or `adapters.sinks.v1` with its input, paging and recorded fixture, and nothing that runs: `adapters.core` owns the run, the consent check, the schedule and the mapping stored as data.
|
|
177
|
+
|
|
178
|
+
`templates[]`: rendering belongs to `documents.core`. Put `documents.templates.v1` under `requires`, `documents.core` (`^0.3.0`) under `dependencies` and `@flowdular/sdk/modules/documents` in `package.json` `dependencies`. Write the body in `templates/<name>.md` from the sections the spec lists, in the template language (headings 1 to 3, paragraphs with bold, italic, code and links, lists one level deep, quotes, a rule, `---pagebreak---`, pipe tables whose rows repeat between `{{#each items}}` and `{{/each}}` lines, `{{ path | formatter }}` with `money: currency`, `number: 2`, `date`, `datetime`, `upper`, `yesno`), reading only fields of `inputEntity`. `src/templates.ts` exports the definitions `{ key: '<module id>.<name>', title, format, locale, body, inputSchema, layout }`: `inputSchema` is `templateInputSchemaFromFields(<entity fields>)` from `@flowdular/sdk/modules/documents`, and `body` mirrors the `.md` file byte for byte, because the bundled server and the sandbox worker cannot read the file at runtime (the reason migrations are mirrored); a test compares the two and another registers the definitions into `new DocumentTemplateRegistry()` from `@flowdular/sdk/modules/documents/server`, which throws naming the line of an invalid body. `src/platform.ts` registers them while composing: `context.capabilities.get<DocumentTemplates>(DOCUMENTS_TEMPLATES_CAPABILITY)?.register('<module id>', TEMPLATES)`. A module action renders: it checks its own permission on the record first, then calls `render({ tenantId, principal: { accountId, scopes }, ownerModule: '<module id>', recordRef, templateKey, input })`, which answers `{ jobId, status, documentId }`, and reads a queued render later with `status(tenantId, jobId)`. The document is an attachment of the record, which the record screen lists through `documents.attachments.v1`.
|
|
179
|
+
|
|
180
|
+
A number the spec asks for (a score, a total, a price) is computed by a module action from stored inputs with a versioned rule, never taken from an agent's output; `agent-tool-design` covers the tool side.
|
|
181
|
+
|
|
167
182
|
## 5. Tests and local gates
|
|
168
183
|
|
|
169
184
|
`tests/module.test.ts` (vitest): identity, tenant isolation and uniqueness against a `createPgliteTestProvider()` lease, and one denial per endpoint through `route.handler(createContext(request, {}))` (`test-hardening`). Then, from the repository root:
|
|
@@ -195,6 +210,6 @@ One command (`packages/cli/src/runner.ts`, `module enable`): adds the id to `flo
|
|
|
195
210
|
- Module enabled but no navigation: scopes not granted, or `platform.client` missing.
|
|
196
211
|
- Routes 404: `platform.server` missing, no `./platform` export, or `src/platform.ts` absent; `pnpm flowdular module validate` names it (`PLATFORM_*`).
|
|
197
212
|
- `Kpi` typecheck error: `value` must be a string.
|
|
198
|
-
- Register every `translations/*.json` bundle in the client contribution, put all user-facing copy there with matching key sets, and resolve it with `t()` as described by `translations-i18n`.
|
|
213
|
+
- Register every `translations/*.json` bundle in the client contribution, put all user-facing copy there with matching key sets, and resolve it with `t()` as described by `translations-i18n`. A string with a count is a plural family (`<key>.one`, `.other`, plus `.few` and `.many` in `pl`) read as `t(key, { count })`.
|
|
199
214
|
- Every relative import needs its `.ts` or `.tsrx` extension.
|
|
200
215
|
- `module.json` `version`, `spec` `specVersion` and `package.json` `version` are one number.
|
|
@@ -22,7 +22,7 @@ Sandbox facts for an edit session (`packages/sandbox/src/server/sessions.ts`): t
|
|
|
22
22
|
|
|
23
23
|
## 2. Classify the change and use its touch list
|
|
24
24
|
|
|
25
|
-
Change classes: endpoint, table, column, screen, widget, permission, setting, cross-module read, agent tool, business agent, fix.
|
|
25
|
+
Change classes: endpoint, table, column, screen, widget, permission, setting, cross-module read, agent tool, business agent, research, adapter, template, fix.
|
|
26
26
|
|
|
27
27
|
New endpoint:
|
|
28
28
|
|
|
@@ -68,9 +68,21 @@ Agent tool: use `agent-tool-design` as a separate phase; add the approved scenar
|
|
|
68
68
|
|
|
69
69
|
Business agent: use `business-agent-design` as a separate phase; add the approved behavior and refusal scenarios, declare the `agents.core` module and package dependencies, define it in `src/agent/agents.ts`, and register it with `context.agentDefinitions.register(...)`. A code definition owns behavior and a maximum exact tool allowlist. Provider, model, active state, and the reduced enabled tools remain tenant binding data.
|
|
70
70
|
|
|
71
|
+
Research (a `research` section):
|
|
72
|
+
|
|
73
|
+
1. `spec/module.yaml`: the section, `research.search.v1`, `research.fetch.v1` or `research.evidence.v1` under `requires`, `research.core` under `dependencies`, a scenario where a finding without evidence is refused; `specVersion` bump.
|
|
74
|
+
2. `src/research.ts`: the declaration, in the shape `module new` writes (`RESEARCH_CAPABILITIES` and `<CONSTANT>_RESEARCH ... as const satisfies ModuleSpecResearch`), and `research-fixtures.json` with the queries and pages the scenarios need.
|
|
75
|
+
3. The service that stores a finding on the `evidenceOwner` record: an `evidenceIds` input, each id checked with `get(tenantId, id)` on `research.evidence.v1`, `attach(tenantId, '<module id>', recordId, evidenceIds)` before the finding commits; the record screen lists `list(...)` and links `workspaceViewHref('research-evidence') + '?id=' + id`.
|
|
76
|
+
4. `module.json` `requires` and `dependencies`, `package.json` `@flowdular/sdk/modules/research`.
|
|
77
|
+
5. Tests on faked capabilities: a finding without evidence and an evidence id of another tenant are refused.
|
|
78
|
+
|
|
79
|
+
Adapter (an `adapters[]` entry): `spec/module.yaml` with the entry (and `recorded` in a sandbox session) and a `specVersion` bump; `src/adapters/<name>.ts` in the shape `module new` writes; then the connector definition, the port or list export, the run table migration, the job runner in `src/platform.ts`, the tenant setting holding the instance id, `adapters/<name>.recorded.json` and the tests as a separate `integration-adapter` phase.
|
|
80
|
+
|
|
81
|
+
Template (a `templates[]` entry): `spec/module.yaml` with the entry, `documents.templates.v1` under `requires`, `documents.core` (`^0.3.0`) under `dependencies`, a scenario that a render without the module's record permission is refused, `specVersion` bump; `templates/<name>.md` in the template language reading only fields of `inputEntity`; `templates` in `package.json` `files` and `@flowdular/sdk/modules/documents` in `dependencies`; `src/templates.ts` with `{ key: '<module id>.<name>', title, format, locale, body, inputSchema: templateInputSchemaFromFields(<entity fields>), layout }`, the body mirrored byte for byte from the `.md` file with a test comparing them and a test registering them into `new DocumentTemplateRegistry()` (`@flowdular/sdk/modules/documents/server`); `context.capabilities.get<DocumentTemplates>(DOCUMENTS_TEMPLATES_CAPABILITY)?.register('<module id>', TEMPLATES)` in `src/platform.ts`; the action that renders checks its own record permission, then calls `render({ tenantId, principal: { accountId, scopes }, ownerModule: '<module id>', recordRef, templateKey, input })` and reads `status(tenantId, jobId)` for a queued render. `module-new` section 4b carries the language.
|
|
82
|
+
|
|
71
83
|
## 3. Versions and spec
|
|
72
84
|
|
|
73
|
-
Bump `spec/module.yaml` `specVersion`, `module.json` `version` and `package.json` `version` together with `pnpm flowdular module version bump <id> <patch|minor|major> --apply` (patch for a fix, minor for a new endpoint, screen or
|
|
85
|
+
Bump `spec/module.yaml` `specVersion`, `module.json` `version` and `package.json` `version` together with `pnpm flowdular module version bump <id> <patch|minor|major> --apply` (patch for a fix, minor for a new endpoint, screen, column, adapter, template or research section); it also retargets every dependent `^` range that stops matching. A new `context.capabilities.register` id goes under `provides` in `module.json`; a new `context.capabilities.get` id goes under `requires`. Add an acceptance scenario for every new behaviour and an invariant for every new rule; the scenario id matches `^[A-Z][A-Z0-9-]+$`. In the sandbox the business manager leaves the changed spec in `draft` or `in-review`; only the operator approval route records the approved hash and permits implementation.
|
|
74
86
|
|
|
75
87
|
## 4. Gates
|
|
76
88
|
|
|
@@ -33,7 +33,7 @@ The pull request is the unit of a delivery: one session, one branch, one PR, eve
|
|
|
33
33
|
- In the worktree: the copy and the removals, `pnpm install --offline` (fallback `--prefer-offline`), `pnpm flowdular module enable <id> --apply` for each new module with the worktree as `--dir`, the platform typecheck.
|
|
34
34
|
- Guardrails before the commit: `git status --porcelain` in the worktree may list only `modules/<dir>/**` of the session's modules and `pnpm-lock.yaml`. A delivery with a new module may also change `flowdular.json`, `platform/package.json` and `platform/src/generated/**`. The count stays within `sandbox.delivery.maxChangedFiles` or, unset, the `.ai/policies/task-budgets.yaml` figure for the session kind (`new-module` 30, `edit-module` 12, default 18); new packages within `maxNewDependencies` (0). Owners come from `.ai/policies/path-ownership.yaml`; with `crossOwnerChanges.requireReviewer` a cross-owner change asks for a reviewer from each owner in the body. A violation lists the offending paths and stops before anything is committed; the branch is deleted.
|
|
35
35
|
- Commit `sandbox: add|update <module id>` (author from git config) with the session id and the gate summary, `git push -u --force-with-lease <remote> <branch>`, `gh pr create --base <baseBranch> --head <branch> --title "Add|Update <module id>" --body-file <tmp>` (`--reviewer` from `git.reviewers`). A second delivery of the same session updates the branch and keeps the open PR.
|
|
36
|
-
- PR body
|
|
36
|
+
- PR body in the repository template (section 4b): Problem from the brief, Solution from the change and the last review handoff with a spec diff per module, Verification as the gate table, Follow-ups with `Post-merge: pnpm flowdular auth sync-scopes --module <id> --apply` per module and the cross-owner reviewer note, Risks from the guardrails, then the file list grouped as added, modified, removed, and `Session <id>.` No attribution footers, no dashes. Labels from `git.labels` (default `sandbox-delivery`) are added after creation and never fail a delivery.
|
|
37
37
|
- `sync-scopes` does not run in the worktree: it is a runtime action against the deployment database, so it stays the post-merge step. Deploy, run it with `FD_AUTH_DATABASE` pointing at that database, verify the navigation entry appears for an owner.
|
|
38
38
|
- Configuration in `flowdular.json`, all optional and validated by `packages/contracts/schemas/project.schema.json`: `sandbox.delivery { default: 'workspace' | 'git-pr', targets: ['workspace', 'git-pr'], git: { remote: 'origin', baseBranch: 'main', branchPrefix: 'sandbox', provider: 'github' | 'none', mode: 'auto' | 'direct' | 'fork', forkOwner: null, reviewers: [] }, maxChangedFiles }`. Read at request time. `auto` never creates a fork: it uses direct delivery only after GitHub confirms push access and otherwise asks the operator to choose `direct` or `fork`. Only an explicit `fork` choice authorizes fork creation.
|
|
39
39
|
- The screen: "Into this workspace" / "As a pull request", offered only when both are usable here; an unusable target says why. The git plan shows branch, base, changed files against the budget, new packages, owners touched and the guardrail verdict; done shows the PR or compare link. `.flowdular/sandbox/sessions/<id>/delivery.json` keeps the branch and the URL.
|
|
@@ -52,40 +52,37 @@ pnpm audit --prod --audit-level high # what CI runs (.github/workflows
|
|
|
52
52
|
|
|
53
53
|
Branch names: `feat/<module>-<topic>`, `fix/<module>-<topic>`, `core/<package>-<topic>`. Commit one logical change per commit; generated files travel with the command that produced them.
|
|
54
54
|
|
|
55
|
-
## 4.
|
|
55
|
+
## 4. Branches, commits and pull requests (repository rules)
|
|
56
56
|
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
57
|
+
- Branch off `main`: `feat/<scope>-<topic>`, `fix/<scope>-<topic>`, `core/<package>-<topic>`, `docs/<topic>`, `chore/<topic>`, `release/<version>`; the sandbox uses `<branchPrefix>/<module-dir>-<session>`. Never work on `main` or in the operator's checkout; a worktree per branch.
|
|
58
|
+
- One logical change per commit, imperative subject under 72 characters, a body that says why. Generated files travel with the commit of the command that produced them, and the PR names that command.
|
|
59
|
+
- The body follows the template below in that order. Each section is a few sentences or a short list; an empty section says "None." rather than disappearing. No file tables, no design essays, no restating the diff.
|
|
60
|
+
- Labels: exactly one type label (`feat`, `fix`, `docs`, `chore`, `release`) and every area the diff touches (`core` for `packages/**` and `platform/**`, `module` for `modules/**`, `sandbox` for `packages/sandbox/**`, `ci` for `.github/**`, `docs` for `docs/**` and `.ai/**`). `breaking` when a public contract changes, `needs-decision` when a question in the body blocks the merge, `sandbox-delivery` on an eject. `.github/labeler.yml` adds the area labels from paths; the author adds the type.
|
|
61
|
+
- No AI attribution: no AI `Co-Authored-By` line and no `Generated with` footer. No em or en dashes anywhere in commits, PR titles or bodies.
|
|
62
|
+
- Changes to `packages/**` name the consumers that were migrated (`core-extend`). Merge only when every check is green, including the ones that register late.
|
|
62
63
|
|
|
63
64
|
## 4b. Pull request body template
|
|
64
65
|
|
|
65
66
|
```text
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
dashboard KPI. Covers INVENTORY-LIST, INVENTORY-CREATE, INVENTORY-DENY,
|
|
69
|
-
INVENTORY-ISOLATION.
|
|
67
|
+
## Problem
|
|
68
|
+
What is wrong or missing, for whom, and how it shows. One paragraph.
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
## Solution
|
|
71
|
+
What changed and why this shape. Name the spec scenarios covered and the
|
|
72
|
+
commands that produced generated files.
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
## Verification
|
|
75
|
+
What ran and the result: `pnpm verify` (tests), `pnpm build`, a PostgreSQL
|
|
76
|
+
run, a browser look. Name what was not run.
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
## 4c. Pre-flight checklist
|
|
78
|
+
## Follow-ups
|
|
79
|
+
Post-merge steps (sync-scopes, publish, index pin) and work deliberately left
|
|
80
|
+
out, each with its trigger. "None." when there is nothing.
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
- The PR title is under 70 characters and names the module (`inventory.core: stock locations`).
|
|
82
|
+
## Risks
|
|
83
|
+
Migrations, removed files, changed contracts, cross-owner paths, anything a
|
|
84
|
+
reviewer should weigh. "None found." when there is nothing.
|
|
85
|
+
```
|
|
89
86
|
|
|
90
87
|
## 5. Container and tags
|
|
91
88
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -12,6 +12,13 @@ allowedPaths:
|
|
|
12
12
|
- 'src/index.ts'
|
|
13
13
|
- 'migrations/**'
|
|
14
14
|
- 'tests/**'
|
|
15
|
+
- 'preview/**'
|
|
16
|
+
- 'src/preview.ts'
|
|
17
|
+
- 'src/research.ts'
|
|
18
|
+
- 'src/adapters/**'
|
|
19
|
+
- 'research-fixtures.json'
|
|
20
|
+
- 'adapters/**'
|
|
21
|
+
- 'templates/**'
|
|
15
22
|
- 'module.json'
|
|
16
23
|
- 'package.json'
|
|
17
24
|
gates:
|
|
@@ -33,4 +40,6 @@ Permission constants equal the approved spec. All tenant reads and writes use te
|
|
|
33
40
|
|
|
34
41
|
Test observable behavior: successful operations, validation bounds, 401/403, uniqueness, replay and two-tenant isolation. Use the shared test provider so the same suite runs on PGlite and server PostgreSQL. Never use an owner connection to bypass a failing runtime test.
|
|
35
42
|
|
|
43
|
+
Operator sample data comes from the sample-data tool or reference/sample-data.json. Derive tests/fixtures/\*.json and preview/seed.json from its shape with invented names, contacts and identifiers. src/preview.ts exports an idempotent seed({ tenantId, accountId, data, databases }) that writes through the module repository. A spec research section reads research-fixtures.json and each adapter its adapters/<id>.recorded.json; never declare a live adapter.
|
|
44
|
+
|
|
36
45
|
Leave client files to the frontend engineer and tools/business-agent definitions to the agentic engineer. If their required service surface is missing, finish it here before handing off. Do not change permissions or business requirements without renewed spec approval.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# agentic-module
|
|
2
|
+
|
|
3
|
+
The shape of a case module: people open a case, an agent gathers and reads the facts, the module computes what must be exact, a document closes the case and nothing leaves the workspace before a person approves it. An underwriter checking a company, a developer pricing apartments in a district and a finance team screening a vendor all build this shape. It is documentation only: a sandbox session still runs as `new-module@1.0.0` or `edit-module@1.0.0`, and this blueprint names what such a session adds when its spec declares `research`, `adapters` or `templates`.
|
|
4
|
+
|
|
5
|
+
The five steps, each owned by one layer:
|
|
6
|
+
|
|
7
|
+
1. **The agent reads.** A module-owned business agent (`business-agent-design`) reads the case record through the module's own read tools and outside sources through `research.core` (`research.search`, `research.fetch`, behind the owner's `research.core.allowAgents` consent). Data from another system arrives before the run through a source adapter (`integration-adapter`), never through the agent.
|
|
8
|
+
2. **Evidence is cited.** Every finding the agent keeps is stored through a module tool that takes `evidenceIds`, checks them with `research.evidence.v1` and attaches them to the `evidenceOwner` record, so the record lists its sources (`agent-tool-design`, section 6).
|
|
9
|
+
3. **Numbers are computed by an action.** A score, a premium or a price is a module action over stored inputs and a versioned rule; the agent passes references and never writes a figure.
|
|
10
|
+
4. **A document comes at the end.** The spec's `templates[]` entry names the body in `templates/<name>.md` and the record it renders from. The module registers the body through `documents.templates.v1` and renders it for the case record, which keeps the PDF or DOCX as an attachment and names the template version it came from.
|
|
11
|
+
5. **Approval before the result leaves.** A workflow (`workflow-development`) ends with a `human-approval` node before any step that sends the result out: a sink adapter push, a notification to someone outside the workspace, or a shared document.
|
|
12
|
+
|
|
13
|
+
A workflow ties them together: `input` (the case), `agent` (research and findings), `action` (the computation), `validator` or `agent-decision` (every finding cites evidence), `human-approval`, then `action` (the push or the document). In a sandbox session every adapter and the research section run on recorded fixtures only.
|
|
14
|
+
|
|
15
|
+
Files here describe the contract; `blueprint.json` is validated by `pnpm flowdular blueprint validate --all` and the others are checked to exist.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../packages/contracts/schemas/blueprint.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"architecture": "0.2.0",
|
|
5
|
+
"owner": "platform-architecture",
|
|
6
|
+
"status": "draft",
|
|
7
|
+
"id": "agentic-module",
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"risk": "workspace-write",
|
|
10
|
+
"agentRoles": [
|
|
11
|
+
"business-manager",
|
|
12
|
+
"backend-engineer",
|
|
13
|
+
"agentic-engineer",
|
|
14
|
+
"frontend-engineer"
|
|
15
|
+
],
|
|
16
|
+
"executorProfiles": ["sandbox-specialist", "repo-agent"],
|
|
17
|
+
"requiredReviewers": ["reviewer"],
|
|
18
|
+
"requiresApprovedSpec": true
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
schemaVersion: 1
|
|
2
|
+
# The gates of new-module, unchanged; nothing executes this file.
|
|
3
|
+
gates:
|
|
4
|
+
- id: spec-schema
|
|
5
|
+
command: pnpm flowdular spec validate --all --json
|
|
6
|
+
required: true
|
|
7
|
+
- id: module-schema
|
|
8
|
+
command: pnpm flowdular module validate --json
|
|
9
|
+
required: true
|
|
10
|
+
- id: dependencies
|
|
11
|
+
command: 'grep the imports under src/** and compare with package.json dependencies (no CLI command yet)'
|
|
12
|
+
required: true
|
|
13
|
+
- id: typecheck
|
|
14
|
+
command: pnpm --filter @flowdular/module-{module} typecheck
|
|
15
|
+
required: true
|
|
16
|
+
- id: tests
|
|
17
|
+
command: pnpm --filter @flowdular/module-{module} test
|
|
18
|
+
required: true
|
|
19
|
+
- id: format
|
|
20
|
+
command: pnpm format:check
|
|
21
|
+
required: true
|
|
22
|
+
- id: auto-review
|
|
23
|
+
command: run the auto-review skill as a separate read-only phase over the finished change
|
|
24
|
+
required: true
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"required": ["moduleId", "specPath", "caseEntity"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"moduleId": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"pattern": "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+$"
|
|
10
|
+
},
|
|
11
|
+
"specPath": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"pattern": "^modules/[a-z0-9-]+/spec/module\\.yaml$"
|
|
14
|
+
},
|
|
15
|
+
"caseEntity": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
16
|
+
"brief": { "type": "string", "minLength": 8, "maxLength": 20000 }
|
|
17
|
+
}
|
|
18
|
+
}
|