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
@@ -87,6 +87,7 @@ describe('catalog migrations', () => {
87
87
  it('declares forced PostgreSQL row security for every tenant table', () => {
88
88
  for (const migration of databaseMigrations) {
89
89
  const sql = migration.sql.postgresql ?? '';
90
+ if (!sql.includes('CREATE TABLE')) continue;
90
91
  expect(sql).toContain('ENABLE ROW LEVEL SECURITY');
91
92
  expect(sql).toContain('FORCE ROW LEVEL SECURITY');
92
93
  expect(sql).toContain("current_setting('coreloom.tenant_id', true)");
@@ -110,9 +111,9 @@ describe('catalog migrations', () => {
110
111
  transaction.execute({
111
112
  text: `INSERT INTO catalog_items
112
113
  (id, tenant_id, sku, sku_normalized, name, kind, unit,
113
- base_price_minor, currency, status, created_at)
114
+ base_price_minor, currency, status, created_at, updated_at)
114
115
  VALUES ('item-1', 'tenant-a', 'SKU-1', 'sku-1', 'Bolt', 'product',
115
- 'pcs', 500, 'EUR', 'active', 1)`,
116
+ 'pcs', 500, 'EUR', 'active', 1, 1)`,
116
117
  }),
117
118
  { tenantId: 'tenant-a', access: 'write' },
118
119
  );
@@ -139,6 +140,61 @@ describe('catalog migrations', () => {
139
140
  ).toEqual([{ sku: 'SKU-1' }]);
140
141
  });
141
142
 
143
+ it('adds the update time and the list indexes, and refuses a half-applied schema', async () => {
144
+ await apply();
145
+ const schema = lease.database.schema;
146
+ expect(await schema.hasColumn('catalog_items', 'updated_at')).toBe(true);
147
+ expect(await schema.hasIndex('catalog_items_tenant_name_idx')).toBe(true);
148
+ expect(await schema.hasIndex('catalog_items_tenant_updated_idx')).toBe(
149
+ true,
150
+ );
151
+
152
+ await lease.database.execute({
153
+ text: 'DROP INDEX catalog_items_tenant_name_idx',
154
+ });
155
+ await lease.database.execute({
156
+ text: `DELETE FROM ${DATABASE_MIGRATION_LEDGER} WHERE namespace = 'catalog.core'`,
157
+ });
158
+ expect((await status()).at(-1)).toMatchObject({
159
+ id: '0005_catalog_list_indexes',
160
+ state: 'partial',
161
+ });
162
+ });
163
+
164
+ it('backfills the update time of rows that predate it', async () => {
165
+ await runDatabaseMigrations(
166
+ lease.database,
167
+ 'catalog.core',
168
+ databaseMigrations.slice(0, 4),
169
+ );
170
+ await lease.database.transaction(
171
+ (transaction) =>
172
+ transaction.execute({
173
+ text: `INSERT INTO catalog_items
174
+ (id, tenant_id, sku, sku_normalized, name, kind, unit,
175
+ base_price_minor, currency, status, created_at)
176
+ VALUES ('item-1', 'tenant-a', 'SKU-1', 'sku-1', 'Bolt', 'product',
177
+ 'pcs', 500, 'EUR', 'active', 1234)`,
178
+ }),
179
+ { tenantId: 'tenant-a', access: 'write' },
180
+ );
181
+ expect((await apply()).at(-1)).toMatchObject({
182
+ id: '0005_catalog_list_indexes',
183
+ action: 'applied',
184
+ });
185
+ expect(
186
+ (
187
+ await lease.database.transaction(
188
+ (transaction) =>
189
+ transaction.query<{ updated_at: string | number }>({
190
+ text: 'SELECT updated_at FROM catalog_items',
191
+ }),
192
+ { tenantId: 'tenant-a', access: 'read' },
193
+ )
194
+ ).rows.map((row) => Number(row.updated_at)),
195
+ ).toEqual([1234]);
196
+ });
197
+
142
198
  it('runs clean on a second migration pass', async () => {
143
199
  await apply();
144
200
 
@@ -13,6 +13,7 @@ import {
13
13
  CatalogServiceError,
14
14
  } from '../src/services/catalog-service.ts';
15
15
  import {
16
+ allItems,
16
17
  closeCatalogTestDatabases,
17
18
  createCatalogTestDatabase,
18
19
  type CatalogTestDatabase,
@@ -161,7 +162,7 @@ describe('catalog.core', () => {
161
162
  },
162
163
  TEST_ACTOR,
163
164
  );
164
- expect(await service.list('tenant-b')).toEqual([]);
165
+ expect(await allItems(service, 'tenant-b')).toEqual([]);
165
166
  });
166
167
 
167
168
  it('keeps lifecycle revisions append-only and tenant-scoped', async () => {
@@ -4,6 +4,20 @@ import {
4
4
  DatabaseCatalogRepository,
5
5
  migrateCatalogDatabase,
6
6
  } from '../../src/services/database-repository.ts';
7
+ import {
8
+ FIRST_LIST_PAGE,
9
+ type CatalogService,
10
+ } from '../../src/services/catalog-service.ts';
11
+ import type { CatalogItem } from '../../src/domain/types.ts';
12
+
13
+ /** The first page at the ceiling in SKU order: every item of a small test tenant. */
14
+ export async function allItems(
15
+ service: CatalogService,
16
+ tenantId: string,
17
+ ): Promise<readonly CatalogItem[]> {
18
+ return (await service.listPage(tenantId, { ...FIRST_LIST_PAGE, sort: 'sku' }))
19
+ .items;
20
+ }
7
21
 
8
22
  const TENANT_TABLES = [
9
23
  'catalog_items',
@@ -7,6 +7,16 @@
7
7
  "drawer.editTitle": "Edit catalog item",
8
8
  "action.edit": "Edit",
9
9
  "action.history": "History",
10
+ "action.archiveSelected": "Archive selected",
11
+ "action.restoreSelected": "Restore selected",
12
+ "action.archiveRefused": "Select at least one active item.",
13
+ "action.restoreRefused": "Select at least one archived item.",
14
+ "action.close": "Close",
15
+ "archiveMany.title": "Archive selected items",
16
+ "archiveMany.description": "Archive {count} selected items? Archived items can be restored later.",
17
+ "common.messages": "Messages",
18
+ "denied.title": "Catalog access denied",
19
+ "denied.description": "Your account may not read catalog items in this workspace.",
10
20
  "delete.title": "Delete catalog item",
11
21
  "delete.description": "Delete {name} permanently? This cannot be undone.",
12
22
  "delete.drawerDescription": "This permanently removes the archived catalog item.",
@@ -17,7 +27,13 @@
17
27
  "error.delete": "Could not delete the catalog item.",
18
28
  "error.history": "Could not load catalog item history.",
19
29
  "error.load": "Could not load catalog items.",
30
+ "error.archiveMany": "Could not archive the selected items.",
31
+ "error.restoreMany": "Could not restore the selected items.",
20
32
  "error.request": "The catalog operation failed.",
33
+ "export.action": "Export CSV",
34
+ "export.started": "The catalog export started. It appears on the Exports screen once the file is ready.",
35
+ "export.link": "Open Exports",
36
+ "export.error": "Could not start the export.",
21
37
  "form.cancel": "Cancel",
22
38
  "form.currency": "Currency",
23
39
  "form.kind": "Kind",
@@ -37,27 +53,42 @@
37
53
  "status.active": "Active",
38
54
  "status.archived": "Archived",
39
55
  "filters.label": "Filters",
40
- "filters.includeArchived": "Include archived items",
56
+ "filters.kind": "Kind",
57
+ "filters.allKinds": "All kinds",
58
+ "filters.status": "Status",
59
+ "filters.allStatuses": "All statuses",
41
60
  "lifecycle.title": "Lifecycle",
42
61
  "lifecycle.description": "Archive an item before deleting it permanently.",
43
62
  "lifecycle.history": "View history",
44
63
  "lifecycle.archive": "Archive item",
45
64
  "lifecycle.restore": "Restore item",
46
65
  "lifecycle.delete": "Delete item",
66
+ "notice.archivedMany": "Archived {updated}, missing {missing}, refused {refused}.",
67
+ "notice.restoredMany": "Restored {updated}, missing {missing}, refused {refused}.",
47
68
  "page.action.new": "New item",
48
69
  "page.action.refresh": "Refresh",
49
70
  "page.description": "Products and services with stable tenant-scoped SKUs.",
50
71
  "page.eyebrow": "Commercial master data",
51
72
  "page.title": "Catalog",
73
+ "pagination.label": "Catalog pages",
74
+ "pagination.previous": "Previous",
75
+ "pagination.next": "Next",
76
+ "pagination.size": "Items per page",
77
+ "pagination.summary": "Page {page} · from item {from}",
52
78
  "price.fallback": "{amount} {currency} minor",
79
+ "selection.label": "Select all items on screen",
80
+ "selection.rowLabel": "Select {name}",
81
+ "selection.clear": "Clear selection",
82
+ "selection.summary": "{count} selected",
53
83
  "table.caption": "Catalog items",
54
- "table.column.item": "Item",
84
+ "table.column.name": "Name",
85
+ "table.column.sku": "SKU",
55
86
  "table.column.kind": "Kind",
56
87
  "table.column.price": "Price",
57
88
  "table.column.status": "Status",
89
+ "table.column.updated": "Updated",
58
90
  "table.column.actions": "Actions",
59
- "table.count.one": "1 item",
60
- "table.count.other": "items: {count}",
91
+ "table.count.page": "{count} on this page",
61
92
  "table.empty.hint": "Create the first product or service.",
62
93
  "table.empty.title": "No catalog items yet",
63
94
  "table.emptyFiltered.hint": "No item in this workspace matches the current filter.",
@@ -7,6 +7,16 @@
7
7
  "drawer.editTitle": "Edytuj pozycję katalogu",
8
8
  "action.edit": "Edytuj",
9
9
  "action.history": "Historia",
10
+ "action.archiveSelected": "Archiwizuj zaznaczone",
11
+ "action.restoreSelected": "Przywróć zaznaczone",
12
+ "action.archiveRefused": "Zaznacz co najmniej jedną aktywną pozycję.",
13
+ "action.restoreRefused": "Zaznacz co najmniej jedną zarchiwizowaną pozycję.",
14
+ "action.close": "Zamknij",
15
+ "archiveMany.title": "Archiwizuj zaznaczone pozycje",
16
+ "archiveMany.description": "Zarchiwizować zaznaczone pozycje ({count})? Zarchiwizowane pozycje można później przywrócić.",
17
+ "common.messages": "Komunikaty",
18
+ "denied.title": "Brak dostępu do katalogu",
19
+ "denied.description": "Twoje konto nie może odczytywać pozycji katalogu w tej przestrzeni roboczej.",
10
20
  "delete.title": "Usuń pozycję katalogu",
11
21
  "delete.description": "Usunąć trwale pozycję {name}? Tej operacji nie można cofnąć.",
12
22
  "delete.drawerDescription": "Ta operacja trwale usuwa zarchiwizowaną pozycję katalogu.",
@@ -17,7 +27,13 @@
17
27
  "error.delete": "Nie udało się usunąć pozycji katalogu.",
18
28
  "error.history": "Nie udało się wczytać historii pozycji katalogu.",
19
29
  "error.load": "Nie udało się wczytać pozycji katalogu.",
30
+ "error.archiveMany": "Nie udało się zarchiwizować zaznaczonych pozycji.",
31
+ "error.restoreMany": "Nie udało się przywrócić zaznaczonych pozycji.",
20
32
  "error.request": "Operacja na katalogu nie powiodła się.",
33
+ "export.action": "Eksportuj CSV",
34
+ "export.started": "Eksport katalogu wystartował. Pojawi się na ekranie Eksporty, gdy plik będzie gotowy.",
35
+ "export.link": "Otwórz Eksporty",
36
+ "export.error": "Nie udało się rozpocząć eksportu.",
21
37
  "form.cancel": "Anuluj",
22
38
  "form.currency": "Waluta",
23
39
  "form.kind": "Rodzaj",
@@ -37,27 +53,42 @@
37
53
  "status.active": "Aktywna",
38
54
  "status.archived": "Zarchiwizowana",
39
55
  "filters.label": "Filtry",
40
- "filters.includeArchived": "Pokaż zarchiwizowane pozycje",
56
+ "filters.kind": "Rodzaj",
57
+ "filters.allKinds": "Wszystkie rodzaje",
58
+ "filters.status": "Status",
59
+ "filters.allStatuses": "Wszystkie statusy",
41
60
  "lifecycle.title": "Cykl życia",
42
61
  "lifecycle.description": "Zarchiwizuj pozycję przed jej trwałym usunięciem.",
43
62
  "lifecycle.history": "Zobacz historię",
44
63
  "lifecycle.archive": "Archiwizuj pozycję",
45
64
  "lifecycle.restore": "Przywróć pozycję",
46
65
  "lifecycle.delete": "Usuń pozycję",
66
+ "notice.archivedMany": "Zarchiwizowano {updated}, brak {missing}, odrzucono {refused}.",
67
+ "notice.restoredMany": "Przywrócono {updated}, brak {missing}, odrzucono {refused}.",
47
68
  "page.action.new": "Nowa pozycja",
48
69
  "page.action.refresh": "Odśwież",
49
70
  "page.description": "Produkty i usługi ze stałym SKU w obrębie przestrzeni roboczej.",
50
71
  "page.eyebrow": "Dane podstawowe sprzedaży",
51
72
  "page.title": "Katalog",
73
+ "pagination.label": "Strony katalogu",
74
+ "pagination.previous": "Poprzednia",
75
+ "pagination.next": "Następna",
76
+ "pagination.size": "Pozycji na stronie",
77
+ "pagination.summary": "Strona {page} · od pozycji {from}",
52
78
  "price.fallback": "{amount} {currency} w groszach",
79
+ "selection.label": "Zaznacz wszystkie pozycje na ekranie",
80
+ "selection.rowLabel": "Zaznacz {name}",
81
+ "selection.clear": "Wyczyść zaznaczenie",
82
+ "selection.summary": "Zaznaczono: {count}",
53
83
  "table.caption": "Pozycje katalogu",
54
- "table.column.item": "Pozycja",
84
+ "table.column.name": "Nazwa",
85
+ "table.column.sku": "SKU",
55
86
  "table.column.kind": "Rodzaj",
56
87
  "table.column.price": "Cena",
57
88
  "table.column.status": "Status",
89
+ "table.column.updated": "Zmieniono",
58
90
  "table.column.actions": "Akcje",
59
- "table.count.one": "1 pozycja",
60
- "table.count.other": "pozycje: {count}",
91
+ "table.count.page": "Na tej stronie: {count}",
61
92
  "table.empty.hint": "Dodaj pierwszy produkt lub usługę.",
62
93
  "table.empty.title": "Nie ma jeszcze pozycji katalogu",
63
94
  "table.emptyFiltered.hint": "Żadna pozycja w tej przestrzeni roboczej nie pasuje do filtra.",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "id": "catalog.core",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "repository": "Flowdular/official-modules",
5
- "sourceCommit": "be391ac460e53e06a812fc9f0ea1260e2daa058f",
6
- "artifactSha256": "7c645f660f44bb4c4540d32dd188d643aafb067f4c505a2722a7f2605a7b54b8",
5
+ "sourceCommit": "6565e2067b029264de98f59cfc9fa7eeb5645b54",
6
+ "artifactSha256": "5c32abd8f6df0c1d3ec0f694d270de3b24ef2e62888454bc89564a67f59c7828",
7
7
  "files": {
8
8
  "LICENSE": "155d722071bad9d0d832482e06fd5a47f7034389cb63aabcb39f4282aa3499fc",
9
9
  "migrations/0001_catalog_core.down.sql": "0962a1edf0bde959cd4facccf77bd5b634d6cc25f125a0c79a61770c8871093f",
@@ -14,43 +14,51 @@
14
14
  "migrations/0003_catalog_history_service_actors.up.sql": "7caae787ca2fc1e331ccbfda4df5982beec76136e8dc486aab5532ccd338a722",
15
15
  "migrations/0004_catalog_idempotency_ledger.down.sql": "8ba72d8edc4d25ecf6d041888cf321690c25afdaf3723812ddd7d391d75370aa",
16
16
  "migrations/0004_catalog_idempotency_ledger.up.sql": "41e86ba2051159c6cc9672bfae17c1d02051da975f1c43c566a2b66592673cb0",
17
+ "migrations/0005_catalog_list_indexes.down.sql": "3dd547d738e75513abef502afc4bf9b51851ec9405b6b719c6134b897c5b1776",
18
+ "migrations/0005_catalog_list_indexes.up.sql": "dc3ece30b93fba0c91be58f772d1ae421cfbba49439e8c72a06aead6720f5db5",
17
19
  "migrations/README.md": "2a15fd001713c2552a3c82b186246aa5fec136f43143cc2d9aa15bd491a09d41",
18
- "module.json": "f2c66d18e3d54ef1ad8dcc3d26d1ee39a2982463dd3bc3fb26b159039933ed86",
19
- "package.json": "3e05cf299b5cc5d948e4b3569bd93f2648a30b22f4f14d26bb31548a79529092",
20
- "spec/module.yaml": "633a719d765d9779e2bf4ae0a608251767a62cbb4e15305dc13afcb1e52f14c9",
20
+ "module.json": "a4ac79385b396842f96d6d190b615565d09a9ea7a4f761de589ee3312b0b8639",
21
+ "package.json": "716dd452c7eaf14b5eeb80c1dba98e71c6f43ee33bd5fd03a2bf90801571d5f5",
22
+ "spec/module.yaml": "6a2cf7d9df6d2cf5cf2ea6517a79faab7bf184fab917485811fbfaad5b31787b",
21
23
  "src/acl/permissions.ts": "3375521a5a229736ffac5a948140ec347dc7a7e4fba80e778c0baaf9e0622590",
22
- "src/agent/tools.ts": "3204d20886d2b864482adc1ed3303c653aebd207e16c4fce0c67a4979ea8db07",
23
- "src/api/endpoints.ts": "34f1036e759353550be2acf67c6fa254afc790942d09489db2e76a082df5c3a3",
24
+ "src/agent/tools.ts": "3403db7954906dc2c79227ba0420b5cb4bfb57063b7cfc285e6e556f16be9266",
25
+ "src/api/endpoints.ts": "205ef36d058f246f99b77336e7246a23aaca611c696a1f6f09758f66e8a93a29",
26
+ "src/api/list-cursor.ts": "dcb763edc0128a3aac95e9ca3c5158611a7f2c6a4c71484438c89b7f4d79bb0d",
24
27
  "src/client/CatalogHistoryDrawer.tsrx": "6cdba5249be81e70da703cb34c1a2210943ce5e5fb596ee77cf7952e2acf3ee3",
25
28
  "src/client/CatalogItemForm.tsrx": "7dafa4149975d9e7d5784c8a5391b5b64c2405991aaead713d94b61c41168c2c",
26
- "src/client/CatalogView.tsrx": "7c507931f33afea8414b002e3ad815740c8db75016c53d8a81effc6427bd958d",
27
- "src/client/api.ts": "5f404f6b25777bc8fe2eab5a35424593abc8e879fb83fa2ae4d637fc23fdce4b",
28
- "src/client/contribution.tsrx": "e95f755882005ee090c99a119ce2dda2ec70420daed4ce06021193905858c62f",
29
+ "src/client/CatalogView.tsrx": "bc1e543690e07d52b51626832f36afe3fde87bf2dc45642324507df8384d7303",
30
+ "src/client/api.ts": "eaa55af81d26aa0190d19b3102c363c1ee25b2bdc05436446ac95dcde62aea75",
31
+ "src/client/contribution.tsrx": "6cbf79e6188bdf24b9cec4a06ae621e03efd5d0c76ad72b28828eee9155ab893",
29
32
  "src/client/index.ts": "03eb0664cdafccb66cb25f2e1b07e6959c2dfa9bdbfffc3e7edf9b535ba4949a",
30
33
  "src/client/navigation-copy.ts": "9a8757e64f4001a93e2bc2861c686a19f791f9d67a396d9e658023b1ec4cea8f",
31
- "src/client/state.ts": "bc054bc269026b6abf60d99a2fc5dd94eb8a488fc1ce4c81a1ae1065d9db576b",
32
- "src/domain/types.ts": "4b26be80954d792b09a3a34d29d845b9f6119640be505daba325fec820e06ef6",
34
+ "src/client/state.ts": "61601f6037c4bb8a709b69b10c1a37e59b45b0c941240c436f7861c2b034dcc1",
35
+ "src/domain/lists.ts": "83749967f99fa0c4f44abc0c614108b844321e50ce6408553215e6233ad6e350",
36
+ "src/domain/types.ts": "374efeb24c4f7d41e3c660336e721fa362418579837d44a46724076d03287a0b",
33
37
  "src/domain/variables.ts": "131130e57838235b1f3fcb799da44baeae16522bb346d522827e022305c110b5",
34
38
  "src/index.ts": "5b27943018db9a18651d62add7d6f8397d7c492a204381fae74547b99b91990d",
35
- "src/platform.ts": "b5d809b071af812877f3fb09cce3ced4cdaed8f62611c3eb6d5fcaf6cd7333cb",
39
+ "src/platform.ts": "f048082a33c1c688e4952750c762c8b65a4626ff52cc9c4eaca2156014bac631",
36
40
  "src/server/index.ts": "90eae107d863418124e5d10c4d1fe8a119bd14da605b36de708c18d8500c4b5e",
37
41
  "src/server/runtime.ts": "5f4389a6adf8265f6737c489baf686f63cc3550662103d28b2da9800dd9f5b63",
38
- "src/services/catalog-service.ts": "51f1dcd7c0335f7b417fb45ba700c0098e7193246ee91659c27cab8608282693",
42
+ "src/services/catalog-service.ts": "66e61dfbd9797d31e6814c18efcd21cfcb7b220f7df22677868f486f3fa0e9d1",
39
43
  "src/services/data-classes.ts": "ea09b1a7def59fee43d57ca7dab6fdb4d646b51df2cacb60af96676c43602b33",
40
- "src/services/database-repository.ts": "e6ff61ba197899d5618a02ce743c90d4bf09ae64dcfd1bd3a9985b18a2d23e80",
44
+ "src/services/database-repository.ts": "1b72ce91a2f85f6572825766781a806d94fe0d2abb91872ee9acae3b1e70db2e",
41
45
  "src/services/index.ts": "a08aefe5101814538666f861536287fc382e6c655d62ba9b9ef4bbfbc6ede4af",
42
- "src/services/migration.ts": "89c8e333482c80bc819deba0cf4f4e37e94137933af0f6503f4f9ebcb4f5f371",
43
- "src/services/repository.ts": "98d27a40ff02137ec0c6d8799d9a853d40a76eab2564dd500806c4e84f6dc3a3",
46
+ "src/services/item-export.ts": "b2a4613b43a98d0d41699481bb5e85a770f9a60cb2241579b065effb6936c80b",
47
+ "src/services/migration.ts": "57c9a857d4074973e14b5e3a1c4548a92ad0faca371419e7daf2334f3ef701ab",
48
+ "src/services/repository.ts": "9f560033875d05654148b4a11897465ee4eefa0d7fa701439e8f562da9fba1db",
44
49
  "src/services/target-idempotency.ts": "7111b56a30b691eaaaa64f5691895ac94b6e0735b0079794835bbbeba5102cb0",
45
- "tests/agent-tools.test.ts": "3cdda30b5454d58c6e8791a98e3daf6fe58b9852cd5c437b3614c3f6123baa91",
50
+ "tests/agent-tools.test.ts": "7762c14bc353ee3085df46a91cf370eb5c25b2edb7096f0a4b0e033f220ea726",
51
+ "tests/client-state.test.ts": "ea146c793b14d0f9bc5e36d92e92894ced6531b8fcbd11c8be1486b62313b8c3",
46
52
  "tests/data-classes.test.ts": "bea08e4d1d7380ccdebc211374b74a6034714b192fd98bfb90a2218067b35658",
47
- "tests/endpoints.test.ts": "24d05c7dd52820f90c777a1b814529f5188a9a5a9daeaacda07314f17b7483cc",
48
- "tests/idempotency.test.ts": "f2e0896635bcd95271b66181daa2c084411913cdf864ae8b4814f2beeb8c2943",
49
- "tests/migrations.test.ts": "47d3d741eddde8677be287730d2f96af556b4b7bf0d8378f9a12a7309af17d78",
50
- "tests/module.test.ts": "01bedfa1540470cc87c808c75120f6ede7c3b8e3d81e340d2076af0a66f1c7ab",
51
- "tests/support/database.ts": "f108cf6390da7669f8935301fac125a7e5653f06ba75327f4bdc28fdd409ba73",
52
- "translations/en.json": "406a40b77e3778e83cd4d30e7b3aee8f89c75baf7bc52f1befc6d16f3b877ab3",
53
- "translations/pl.json": "db6325b4980d978e1a3098758b5259a68e32469baa09d194b9d6746462776d27",
53
+ "tests/endpoints.test.ts": "a2d700f4c3eb6fbbef5c57ed5591017a27c98e17fe84fba8426fcd3a2820b210",
54
+ "tests/export.test.ts": "de83066272cee5f7f5736d636d7b9b97dab09778ba661816e401bf926b826402",
55
+ "tests/idempotency.test.ts": "faea4a2cfa04e81b7155399cba807af4c1ba4448cdfeab733d1afe801d27734f",
56
+ "tests/list.test.ts": "97605b50020841cbdac62f9d44b65f0f7dce9c4c4972af6409afeb6b9b66e59a",
57
+ "tests/migrations.test.ts": "fb796aecedb997f6395c2c15e45bf4abfae4677953fa8cb420c94e9cfba8222f",
58
+ "tests/module.test.ts": "af961a5bcddc41d304d14841f8810976f43f46b46ab7814142fdb921a773e92a",
59
+ "tests/support/database.ts": "4ff6d093ca2e80baab3dea476018661473d6474db63d67655f1edc667b8472ac",
60
+ "translations/en.json": "a86d70e02ccf46a2a132d5bee0de7c528c0eb56a85c269a78e0c2e7e0de24e37",
61
+ "translations/pl.json": "8157d83467e9dd8d9700e2fb188f1a730073f805d2a8b3f0ba98f02f2ca9a264",
54
62
  "tsconfig.json": "140bb4775df421146563d13d2f78ff74cb3c2decab6737db32938339248a92ba",
55
63
  "vitest.config.ts": "d13725b13c3712ec91c1646e9226fb172eec2599f72f4e6608bece9912e2bd81"
56
64
  }
@@ -62,7 +62,8 @@ Do not load the whole skill catalog into the task context.
62
62
  network/git, or touch a DB outside their module tests.
63
63
  12. Keep handoffs short and factual. No AI attribution footers or em/en dashes.
64
64
  Sandbox final line: `HANDOFF: <allowed-role> - <why>` or
65
- `HANDOFF: none - <why>`, never your own role.
65
+ `HANDOFF: none - <why>`, never your own role. Branches, commits, PR body,
66
+ labels: `release-eject-pr` section 4.
66
67
  13. Flow: request, `spec-interview`, approval, `module-new`/`module-update`,
67
68
  `auto-review`. Implement from the approved spec and its touch list; do not
68
69
  scan `modules/` or `packages/`. First lookup is `.ai/platform-capabilities.md`.
@@ -21,6 +21,7 @@ Flowdular is an agentic foundation framework: the platform is the foundation, an
21
21
  | `cli-extension` | Module CLI commands through `commands.json` and `defineCliExtension`, with the runner's approval rules. |
22
22
  | `agent-tool-design` | Register module tools through the live composition registry with tenant, permission, input, output, and audit bounds. |
23
23
  | `business-agent-design` | Ship a module-owned business agent with an exact tool ceiling, tenant binding, revisions, and access tests. |
24
+ | `integration-adapter` | Add a source or sink adapter for a named service: connector, port, mapping, recorded fixture, consent, call log. |
24
25
  | `variables` | Variable-aware fields and templates: the `{{ }}` contract, the scope mask, server-side resolution, adding a source. |
25
26
  | `workflow-development` | Build, publish, invoke, simulate, and test typed durable workflows and their module integration capability. |
26
27
  | `release-eject-pr` | Sandbox eject sequence, repository gates, branch and PR conventions, post-merge scope grant. |
@@ -198,6 +198,31 @@ nothing.
198
198
 
199
199
  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.
200
200
 
201
+ ## 6. Research and evidence
202
+
203
+ 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.
204
+
205
+ **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:
206
+
207
+ ```ts
208
+ inputSchema: {
209
+ type: 'object',
210
+ additionalProperties: false,
211
+ required: ['recordId', 'finding', 'evidenceIds'],
212
+ properties: {
213
+ recordId: { type: 'string' },
214
+ finding: { type: 'string' },
215
+ evidenceIds: { type: 'array', items: { type: 'string' } },
216
+ },
217
+ },
218
+ ```
219
+
220
+ 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.
221
+
222
+ **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.
223
+
224
+ 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.
225
+
201
226
  ## Pitfalls
202
227
 
203
228
  - 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,121 @@
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
+ roles:
8
+ - backend-engineer
9
+ - module-executor
10
+ when: An approved spec declares adapters[], or a brief asks to pull records from or push records to a named service.
11
+ ---
12
+
13
+ # Add an integration adapter
14
+
15
+ 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.
16
+
17
+ ## 1. Read exactly this
18
+
19
+ 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`).
20
+ 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.
21
+ 3. `.ai/platform-capabilities.md`, the Connectors, Import, List export and Background work entries.
22
+ 4. `src/adapters/<name>.ts` and the recorded fixture stub, which the scaffold wrote from the spec entry.
23
+
24
+ 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.
25
+
26
+ ## 2. The connector definition
27
+
28
+ 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`):
29
+
30
+ ```ts
31
+ context.capabilities
32
+ .get<ConnectorDefinitionRegistry>(CONNECTORS_DEFINITIONS_CAPABILITY)
33
+ ?.register({
34
+ key: 'erp-vendors', // the spec's connector, ^[a-z][a-z0-9-]{0,95}$
35
+ moduleId: 'vendors.core',
36
+ label: 'ERP vendors',
37
+ authKinds: ['bearer'], // what the documentation offers
38
+ operations: [
39
+ {
40
+ key: 'list-vendors', // the spec's operation
41
+ label: 'List vendors',
42
+ method: 'GET',
43
+ path: '/api/v2/vendors', // {name} expands one segment, {+name} a whole path
44
+ inputSchema: {
45
+ type: 'object',
46
+ additionalProperties: false,
47
+ properties: { query: { type: 'object' } },
48
+ },
49
+ outputSchema: { type: 'object' },
50
+ },
51
+ ],
52
+ defaultAllowedHosts: ['erp.example.com'], // the API host only
53
+ });
54
+ ```
55
+
56
+ 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.
57
+
58
+ ## 3. The port
59
+
60
+ - **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.
61
+ - **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.
62
+
63
+ ## 4. The registration and the mapping
64
+
65
+ 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`):
66
+
67
+ ```ts
68
+ import fixture from '../adapters/erp-vendors.recorded.json' with { type: 'json' };
69
+
70
+ sources.register('vendors.core', [
71
+ {
72
+ ...ERP_VENDORS_ADAPTER, // the scaffolded declaration: id, direction, connector, operation, port, schedule, mapping
73
+ label: 'ERP vendors',
74
+ recorded: fixture, // the parsed fixture, not its path
75
+ input: { path: '/api/v2/vendors', query: { limit: 100 } }, // every call starts from this
76
+ items: 'data', // the record array in the answer; '' is the answer itself
77
+ paging: { kind: 'cursor', param: 'query.cursor', next: 'meta.next_cursor' }, // or { kind: 'page', param: 'query.page', start: 1 }
78
+ mode: 'update-existing',
79
+ },
80
+ ]);
81
+ ```
82
+
83
+ 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.
84
+
85
+ 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):
86
+
87
+ - `rename`: copy the value; a missing or null value leaves the field absent.
88
+ - `constant`: write `value`.
89
+ - `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`.
90
+ - `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.
91
+
92
+ A value that is not text, a number or a boolean, or is longer than 2000 characters, refuses the row with `MAPPING_VALUE_INVALID`.
93
+
94
+ ## 5. The run
95
+
96
+ `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.
97
+
98
+ ## 6. The recorded fixture
99
+
100
+ `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.
101
+
102
+ ## 7. What the records must show
103
+
104
+ - Consent: a run without the instance's `allowWorkflows` fails with `ADAPTER_CONSENT_MISSING` and calls nothing.
105
+ - 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.
106
+ - 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.
107
+
108
+ ## 8. Tests
109
+
110
+ Against the recorded fixture, never the network:
111
+
112
+ - the registration composes (`adapters.sources.v1` or `adapters.sinks.v1` accepts it with the fixture);
113
+ - 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);
114
+ - the port refuses what the module refuses, so a repeated pull updates or skips by the natural key.
115
+
116
+ ## Pitfalls
117
+
118
+ - `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.
119
+ - The egress policy refuses redirects and private addresses; a documentation example on `http://` or a local host will not run.
120
+ - 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.
121
+ - A connector `outputSchema` of `{ type: 'object' }` checks nothing; the mapping function is where a changed response is refused.
@@ -39,6 +39,9 @@ Each spec element maps to files:
39
39
  | `widgets[]` | a widget component plus a `widgets` entry with its `slot` in `src/client/contribution.tsrx` |
40
40
  | `settings[]` | `src/settings.ts` (`defineModuleSettings`) and `settings:` in `src/platform.ts` |
41
41
  | `agentTools[]` | `src/agent/tools.ts` and the `context.agentTools.register` call, as a separate `agent-tool-design` phase |
42
+ | `research` | `src/research.ts`, `research-fixtures.json`, `requires` of the capabilities, the evidence attach where the `evidenceOwner` record is written |
43
+ | `adapters[]` | `src/adapters/<name>.ts`, the port, the adapter registration, `adapters/<name>.recorded.json`, as a separate `integration-adapter` phase |
44
+ | `templates[]` | `templates/<name>.md`, `templates` in `package.json` `files`, `src/templates.ts` registered through `documents.templates.v1` in `src/platform.ts` |
42
45
  | `permissions[]` | `src/acl/permissions.ts`, the endpoint `access.permission`, the client `scope` |
43
46
  | `acceptanceScenarios[]` | `tests/module.test.ts` and its siblings, at least one case each |
44
47
  | `outOfScope[]`, `decisions[]` | no code. Read them so you do not rebuild a decision or implement a deferred feature. |
@@ -58,7 +61,7 @@ pnpm flowdular module new inventory.core --spec modules/inventory/spec/module.ya
58
61
  pnpm flowdular module new inventory.core --spec modules/inventory/spec/module.yaml --apply
59
62
  ```
60
63
 
61
- `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>`).
64
+ `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.
62
65
 
63
66
  The scaffold is PostgreSQL from the first commit. `database-repository.ts`
64
67
  exports `Database<Name>Repository` and `migrate<Name>Database`, takes a
@@ -170,6 +173,18 @@ Contribution rules (`packages/client/src/contributions.ts`): `navigation[].group
170
173
 
171
174
  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`.
172
175
 
176
+ ## 4b. Research, adapters and documents
177
+
178
+ Only when the approved spec declares the section; the spec names every value below.
179
+
180
+ `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.
181
+
182
+ `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.
183
+
184
+ `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`.
185
+
186
+ 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.
187
+
173
188
  ## 5. Tests and local gates
174
189
 
175
190
  `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:
@@ -201,6 +216,6 @@ One command (`packages/cli/src/runner.ts`, `module enable`): adds the id to `flo
201
216
  - Module enabled but no navigation: scopes not granted, or `platform.client` missing.
202
217
  - Routes 404: `platform.server` missing, no `./platform` export, or `src/platform.ts` absent; `pnpm flowdular module validate` names it (`PLATFORM_*`).
203
218
  - `Kpi` typecheck error: `value` must be a string.
204
- - 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`.
219
+ - 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 })`.
205
220
  - Every relative import needs its `.ts` or `.tsrx` extension.
206
221
  - `module.json` `version`, `spec` `specVersion` and `package.json` `version` are one number.