create-flowdular 0.2.5 → 0.3.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 (101) hide show
  1. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
  2. package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
  3. package/agent-template/.agents/skills/deploy-operate/SKILL.md +109 -0
  4. package/agent-template/.agents/skills/module-new/SKILL.md +29 -0
  5. package/agent-template/.agents/skills/module-update/SKILL.md +9 -1
  6. package/agent-template/.agents/skills/spec-interview/SKILL.md +114 -0
  7. package/agent-template/.agents/skills/ux-design/SKILL.md +34 -3
  8. package/agent-template/.ai/README.md +2 -1
  9. package/agent-template/.ai/agents/sandbox/business-manager.md +5 -1
  10. package/agent-template/.ai/blueprints/author-spec/README.md +1 -1
  11. package/agent-template/.ai/blueprints/author-spec/spec-requirements.yaml +44 -0
  12. package/agent-template/.ai/blueprints/author-spec/steps.yaml +5 -5
  13. package/agent-template/.ai/blueprints/author-spec/templates/module.yaml +99 -3
  14. package/agent-template/.ai/blueprints/edit-module/gates.yaml +4 -0
  15. package/agent-template/.ai/blueprints/edit-module/required-files.yaml +9 -0
  16. package/agent-template/.ai/blueprints/new-module/gates.yaml +4 -0
  17. package/agent-template/.ai/blueprints/new-module/spec-requirements.yaml +2 -2
  18. package/agent-template/.ai/blueprints/release/gates.yaml +4 -0
  19. package/agent-template/.ai/platform-capabilities.md +128 -0
  20. package/agent-template/.ai/policies/capabilities.yaml +100 -0
  21. package/agent-template/.ai/policies/path-ownership.yaml +5 -2
  22. package/agent-template/.ai/policies/task-budgets.yaml +5 -3
  23. package/agent-template/.ai/references/catalog/module.json +4 -4
  24. package/agent-template/.ai/references/catalog/package.json +2 -2
  25. package/agent-template/.ai/references/catalog/spec/module.yaml +5 -3
  26. package/agent-template/.ai/references/catalog/src/platform.ts +2 -0
  27. package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +89 -1
  28. package/agent-template/.ai/references/catalog/src/services/data-classes.ts +47 -0
  29. package/agent-template/.ai/references/catalog/src/services/database-repository.ts +98 -1
  30. package/agent-template/.ai/references/catalog/src/services/repository.ts +22 -1
  31. package/agent-template/.ai/references/catalog/tests/data-classes.test.ts +157 -0
  32. package/agent-template/.ai/references/catalog.provenance.json +12 -10
  33. package/agent-template/.ai/rules/flowdular.md +4 -0
  34. package/agent-template/.ai/skills/README.md +10 -0
  35. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -2
  36. package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
  37. package/agent-template/.ai/skills/business-agent-design/SKILL.md +0 -1
  38. package/agent-template/.ai/skills/deploy-operate/SKILL.md +114 -0
  39. package/agent-template/.ai/skills/module-new/SKILL.md +29 -3
  40. package/agent-template/.ai/skills/module-update/SKILL.md +9 -3
  41. package/agent-template/.ai/skills/perf-audit/SKILL.md +0 -1
  42. package/agent-template/.ai/skills/release-eject-pr/SKILL.md +0 -1
  43. package/agent-template/.ai/skills/spec-interview/SKILL.md +120 -0
  44. package/agent-template/.ai/skills/test-hardening/SKILL.md +1 -0
  45. package/agent-template/.ai/skills/ux-design/SKILL.md +34 -3
  46. package/agent-template/.ai/skills/variables/SKILL.md +0 -2
  47. package/agent-template/.ai/skills/workflow-development/SKILL.md +0 -1
  48. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
  49. package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
  50. package/agent-template/.claude/skills/deploy-operate/SKILL.md +109 -0
  51. package/agent-template/.claude/skills/module-new/SKILL.md +29 -0
  52. package/agent-template/.claude/skills/module-update/SKILL.md +9 -1
  53. package/agent-template/.claude/skills/spec-interview/SKILL.md +114 -0
  54. package/agent-template/.claude/skills/ux-design/SKILL.md +34 -3
  55. package/agent-template/AGENTS.md +4 -0
  56. package/agent-template/CLAUDE.md +4 -0
  57. package/agent-template/docs/adr/0003-module-settings.md +1 -1
  58. package/agent-template/docs/adr/0006-agentic-workflows.md +24 -21
  59. package/agent-template/docs/agent-contract.md +2 -2
  60. package/agent-template/docs/cli-extensions.md +82 -0
  61. package/agent-template/docs/cli.md +190 -0
  62. package/agent-template/docs/configuration.md +593 -36
  63. package/agent-template/docs/design-system.md +185 -31
  64. package/agent-template/docs/getting-started.md +118 -0
  65. package/agent-template/docs/module-distribution.md +96 -0
  66. package/agent-template/docs/module-web-surfaces.md +221 -0
  67. package/agent-template/docs/modules.md +216 -0
  68. package/agent-template/docs/operations.md +464 -0
  69. package/agent-template/docs/sandbox.md +212 -0
  70. package/agent-template/platform/scripts/build.mjs +10 -0
  71. package/dist/bin.js +65 -1
  72. package/package.json +1 -1
  73. package/template/default/.dockerignore +14 -0
  74. package/template/default/.env.example +94 -0
  75. package/template/default/README.md +37 -1
  76. package/template/default/flowdular.json +15 -4
  77. package/template/default/infra/README.md +116 -0
  78. package/template/default/infra/docker/Dockerfile +37 -0
  79. package/template/default/infra/docker/compose.yaml +158 -0
  80. package/template/default/infra/docker/postgres/10-roles.sh +31 -0
  81. package/template/default/infra/docker/postgres/tls-init.sh +28 -0
  82. package/template/default/infra/kubernetes/database-secret.example.yaml +15 -0
  83. package/template/default/infra/kubernetes/deployment.yaml +211 -0
  84. package/template/default/infra/kubernetes/kustomization.yaml +9 -0
  85. package/template/default/infra/kubernetes/secrets.example.yaml +52 -0
  86. package/template/default/infra/kubernetes/service.yaml +13 -0
  87. package/template/default/modules/example/module.json +2 -1
  88. package/template/default/modules/example/package.json +2 -2
  89. package/template/default/modules/example/spec/module.yaml +1 -1
  90. package/template/default/modules/example/src/services/database-repository.ts +2 -12
  91. package/template/default/package.json +3 -2
  92. package/template/default/platform/octane.config.ts +99 -9
  93. package/template/default/platform/package.json +1 -1
  94. package/template/default/platform/src/generated/modules.client.ts +26 -2
  95. package/template/default/platform/src/generated/modules.server.ts +241 -10
  96. package/template/default/platform/src/server/health.ts +47 -0
  97. package/template/default/platform/src/server/metrics.ts +100 -0
  98. package/template/default/platform/src/server/storage.ts +172 -0
  99. package/template/default/platform/src/server/tracing.ts +85 -0
  100. package/template/default/pnpm-workspace.yaml +1 -0
  101. package/template/default/specs/application.yaml +15 -0
@@ -4,6 +4,7 @@ import type {
4
4
  } from '@flowdular/sdk/modules/auth/server';
5
5
  import { platformVariableRegistry } from '@flowdular/sdk/kernel';
6
6
  import { registerCatalogVariableSource } from './domain/variables.ts';
7
+ import { catalogDataClasses } from './services/data-classes.ts';
7
8
  import {
8
9
  catalogAgentTools,
9
10
  createCatalogRoutes,
@@ -28,6 +29,7 @@ export function createServerComposition(
28
29
  platformVariableRegistry(context.capabilities),
29
30
  tools,
30
31
  );
32
+ context.dataClasses.declare(catalogDataClasses(() => runtime.service()));
31
33
  return {
32
34
  routes: createCatalogRoutes(context.auth, runtime),
33
35
  dispose: () => runtime.dispose(),
@@ -2,6 +2,8 @@ import { randomUUID } from 'node:crypto';
2
2
  import {
3
3
  normalizeActor,
4
4
  type Actor,
5
+ type DataClassExportSink,
6
+ type DataClassExportSummary,
5
7
  type HistoryPage,
6
8
  type HistoryRequest,
7
9
  } from '@flowdular/sdk/kernel';
@@ -11,7 +13,11 @@ import type {
11
13
  CreateCatalogItemInput,
12
14
  UpdateCatalogItemInput,
13
15
  } from '../domain/types.ts';
14
- import { DuplicateSkuError, type CatalogRepository } from './repository.ts';
16
+ import {
17
+ DuplicateSkuError,
18
+ type CatalogRepository,
19
+ type ExportCursor,
20
+ } from './repository.ts';
15
21
  import {
16
22
  canonicalDigest,
17
23
  TargetIdempotencyConflictError,
@@ -22,6 +28,9 @@ export interface CatalogIdempotencyRequest {
22
28
  readonly operationId: string;
23
29
  }
24
30
 
31
+ /** Rows one export read carries; the whole tenant is walked page by page. */
32
+ export const EXPORT_PAGE = 200;
33
+
25
34
  export class CatalogServiceError extends Error {
26
35
  constructor(
27
36
  readonly code: string,
@@ -271,6 +280,60 @@ export class CatalogService {
271
280
  });
272
281
  }
273
282
 
283
+ async exportItemsTo(
284
+ tenantId: string,
285
+ sink: DataClassExportSink,
286
+ ): Promise<DataClassExportSummary> {
287
+ const trustedTenantId = bounded(tenantId, 'tenantId', 1, 128);
288
+ return await exportPaged(
289
+ (cursor) =>
290
+ this.repository.listItemsForExport(
291
+ trustedTenantId,
292
+ cursor,
293
+ EXPORT_PAGE,
294
+ ),
295
+ (item) => item.createdAt,
296
+ (item) => ({
297
+ id: item.id,
298
+ sku: item.sku,
299
+ name: item.name,
300
+ kind: item.kind,
301
+ unit: item.unit,
302
+ basePriceMinor: item.basePriceMinor,
303
+ currency: item.currency,
304
+ status: item.status,
305
+ createdAt: new Date(item.createdAt).toISOString(),
306
+ }),
307
+ sink,
308
+ );
309
+ }
310
+
311
+ async exportHistoryTo(
312
+ tenantId: string,
313
+ sink: DataClassExportSink,
314
+ ): Promise<DataClassExportSummary> {
315
+ const trustedTenantId = bounded(tenantId, 'tenantId', 1, 128);
316
+ return await exportPaged(
317
+ (cursor) =>
318
+ this.repository.listHistoryForExport(
319
+ trustedTenantId,
320
+ cursor,
321
+ EXPORT_PAGE,
322
+ ),
323
+ (entry) => entry.occurredAt,
324
+ (entry) => ({
325
+ id: entry.id,
326
+ recordId: entry.recordId,
327
+ version: entry.version,
328
+ action: entry.action,
329
+ actor: entry.actor,
330
+ changes: entry.changes,
331
+ occurredAt: new Date(entry.occurredAt).toISOString(),
332
+ }),
333
+ sink,
334
+ );
335
+ }
336
+
274
337
  private async item(tenantId: string, id: string): Promise<CatalogItem> {
275
338
  const item = await this.repository.find(
276
339
  tenantId,
@@ -304,3 +367,28 @@ export class CatalogService {
304
367
  return item;
305
368
  }
306
369
  }
370
+
371
+ async function exportPaged<T extends { readonly id: string }>(
372
+ page: (after: ExportCursor | null) => Promise<readonly T[]>,
373
+ at: (record: T) => number,
374
+ row: (record: T) => Record<string, unknown>,
375
+ sink: DataClassExportSink,
376
+ ): Promise<DataClassExportSummary> {
377
+ let cursor: ExportCursor | null = null;
378
+ let rows = 0;
379
+ let from: Date | null = null;
380
+ let to: Date | null = null;
381
+ for (;;) {
382
+ const records = await page(cursor);
383
+ for (const record of records) {
384
+ await sink.write(row(record));
385
+ rows += 1;
386
+ from ??= new Date(at(record));
387
+ to = new Date(at(record));
388
+ }
389
+ if (records.length < EXPORT_PAGE) break;
390
+ const last = records[records.length - 1]!;
391
+ cursor = { at: at(last), id: last.id };
392
+ }
393
+ return { rows, from, to };
394
+ }
@@ -0,0 +1,47 @@
1
+ import type { DataClassDeclaration } from '@flowdular/sdk/kernel';
2
+ import type { CatalogService } from './catalog-service.ts';
3
+
4
+ /** The class ids are `catalog.core.<key>`. */
5
+ export const CATALOG_DATA_CLASS_KEYS = {
6
+ items: 'items',
7
+ history: 'history',
8
+ idempotencyLedger: 'idempotency-ledger',
9
+ } as const;
10
+
11
+ /**
12
+ * What this module holds, for the workspace's data class catalogue. Items and
13
+ * their history are master data: no retention period, no sweep, no erasure. A
14
+ * row leaves only when a person deletes it. The idempotency ledger is the
15
+ * replay guard of the mutating tools and workflow actions; it stays out of the
16
+ * export because the item it points at is already in the items class.
17
+ */
18
+ export function catalogDataClasses(
19
+ service: () => Promise<CatalogService>,
20
+ ): readonly DataClassDeclaration[] {
21
+ return [
22
+ {
23
+ key: CATALOG_DATA_CLASS_KEYS.items,
24
+ label: 'Catalog items',
25
+ defaultRetentionDays: null,
26
+ exportable: true,
27
+ export: async ({ tenantId, sink }) =>
28
+ (await service()).exportItemsTo(tenantId, sink),
29
+ },
30
+ {
31
+ key: CATALOG_DATA_CLASS_KEYS.history,
32
+ label: 'Catalog item history',
33
+ defaultRetentionDays: null,
34
+ exportable: true,
35
+ export: async ({ tenantId, sink }) =>
36
+ (await service()).exportHistoryTo(tenantId, sink),
37
+ },
38
+ {
39
+ key: CATALOG_DATA_CLASS_KEYS.idempotencyLedger,
40
+ label: 'Catalog idempotency ledger',
41
+ defaultRetentionDays: null,
42
+ exportable: false,
43
+ excludedReason:
44
+ 'Operational replay ledger of mutating tools and workflow actions; the catalog items it points at are exported by catalog.core.items.',
45
+ },
46
+ ];
47
+ }
@@ -11,13 +11,20 @@ import {
11
11
  import {
12
12
  diffFields,
13
13
  type Actor,
14
+ type HistoryEntry,
14
15
  type HistoryPage,
15
16
  type HistoryQuery,
17
+ type RecordChanges,
16
18
  type TrackedFields,
19
+ type UserActor,
17
20
  } from '@flowdular/sdk/kernel';
18
21
  import type { CatalogItem } from '../domain/types.ts';
19
22
  import { databaseMigrations } from './migration.ts';
20
- import { DuplicateSkuError, type CatalogRepository } from './repository.ts';
23
+ import {
24
+ DuplicateSkuError,
25
+ type CatalogRepository,
26
+ type ExportCursor,
27
+ } from './repository.ts';
21
28
  import {
22
29
  canonicalDigest,
23
30
  TargetIdempotencyCorruptionError,
@@ -36,6 +43,18 @@ const COLUMNS = `id, tenant_id, sku, name, kind, unit, base_price_minor,
36
43
  const LIST = `SELECT ${COLUMNS} FROM catalog_items
37
44
  WHERE tenant_id = $1 ORDER BY sku_normalized, id`;
38
45
 
46
+ const LIST_FOR_EXPORT = `SELECT ${COLUMNS} FROM catalog_items
47
+ WHERE tenant_id = $1
48
+ AND ($2::bigint IS NULL OR (created_at, id) > ($2::bigint, $3::text))
49
+ ORDER BY created_at, id LIMIT $4`;
50
+
51
+ const LIST_HISTORY_FOR_EXPORT = `SELECT id, record_id, version, action, actor_kind,
52
+ actor_id, actor_label, run_id, configured_by_json, changes_json, occurred_at
53
+ FROM ${HISTORY_TABLE}
54
+ WHERE tenant_id = $1
55
+ AND ($2::bigint IS NULL OR (occurred_at, id) > ($2::bigint, $3::text))
56
+ ORDER BY occurred_at, id LIMIT $4`;
57
+
39
58
  const FIND = `SELECT ${COLUMNS} FROM catalog_items WHERE tenant_id = $1 AND id = $2`;
40
59
 
41
60
  const INSERT = `INSERT INTO catalog_items
@@ -74,6 +93,20 @@ interface CatalogItemRow {
74
93
  created_at: number | bigint | string;
75
94
  }
76
95
 
96
+ interface HistoryRow {
97
+ id: string;
98
+ record_id: string;
99
+ version: number | bigint | string;
100
+ action: string;
101
+ actor_kind: Actor['kind'];
102
+ actor_id: string;
103
+ actor_label: string;
104
+ run_id: string | null;
105
+ configured_by_json: string | null;
106
+ changes_json: string;
107
+ occurred_at: number | bigint | string;
108
+ }
109
+
77
110
  interface IdempotencyRow {
78
111
  operation_id: string;
79
112
  input_digest: string;
@@ -119,6 +152,36 @@ function fromRow(row: CatalogItemRow): CatalogItem {
119
152
  };
120
153
  }
121
154
 
155
+ function actorFromRow(row: HistoryRow): Actor {
156
+ const identity = { id: row.actor_id, label: row.actor_label };
157
+ if (row.actor_kind === 'user') return { kind: 'user', ...identity };
158
+ if (row.actor_kind === 'agent' && row.run_id !== null) {
159
+ return { kind: 'agent', ...identity, runId: row.run_id };
160
+ }
161
+ if (row.actor_kind === 'service' && row.configured_by_json !== null) {
162
+ return {
163
+ kind: 'service',
164
+ ...identity,
165
+ configuredBy: JSON.parse(row.configured_by_json) as UserActor,
166
+ };
167
+ }
168
+ throw new Error(
169
+ `The catalog history table returned an invalid actor for "${row.id}".`,
170
+ );
171
+ }
172
+
173
+ function historyFromRow(row: HistoryRow): HistoryEntry {
174
+ return {
175
+ id: row.id,
176
+ recordId: row.record_id,
177
+ version: integer(row.version, 'version'),
178
+ action: row.action,
179
+ actor: actorFromRow(row),
180
+ changes: JSON.parse(row.changes_json) as RecordChanges,
181
+ occurredAt: integer(row.occurred_at, 'timestamp'),
182
+ };
183
+ }
184
+
122
185
  /* The fields a history version reports on. Identity, tenancy and creation time
123
186
  cannot change and are never part of a diff. */
124
187
  function tracked(item: CatalogItem): TrackedFields {
@@ -159,6 +222,40 @@ export class DatabaseCatalogRepository implements CatalogRepository {
159
222
  return result.rows.map(fromRow);
160
223
  }
161
224
 
225
+ async listItemsForExport(
226
+ tenantId: string,
227
+ after: ExportCursor | null,
228
+ limit: number,
229
+ ): Promise<readonly CatalogItem[]> {
230
+ await this.readyPromise;
231
+ const result = await this.database.transaction(
232
+ (transaction) =>
233
+ transaction.query<CatalogItemRow>({
234
+ text: LIST_FOR_EXPORT,
235
+ parameters: [tenantId, after?.at ?? null, after?.id ?? null, limit],
236
+ }),
237
+ { access: 'read', tenantId },
238
+ );
239
+ return result.rows.map(fromRow);
240
+ }
241
+
242
+ async listHistoryForExport(
243
+ tenantId: string,
244
+ after: ExportCursor | null,
245
+ limit: number,
246
+ ): Promise<readonly HistoryEntry[]> {
247
+ await this.readyPromise;
248
+ const result = await this.database.transaction(
249
+ (transaction) =>
250
+ transaction.query<HistoryRow>({
251
+ text: LIST_HISTORY_FOR_EXPORT,
252
+ parameters: [tenantId, after?.at ?? null, after?.id ?? null, limit],
253
+ }),
254
+ { access: 'read', tenantId },
255
+ );
256
+ return result.rows.map(historyFromRow);
257
+ }
258
+
162
259
  async find(tenantId: string, id: string): Promise<CatalogItem | null> {
163
260
  await this.readyPromise;
164
261
  const result = await this.database.transaction(
@@ -1,4 +1,9 @@
1
- import type { Actor, HistoryPage, HistoryQuery } from '@flowdular/sdk/kernel';
1
+ import type {
2
+ Actor,
3
+ HistoryEntry,
4
+ HistoryPage,
5
+ HistoryQuery,
6
+ } from '@flowdular/sdk/kernel';
2
7
  import type { CatalogItem } from '../domain/types.ts';
3
8
  import type { TargetIdempotencyRequest } from './target-idempotency.ts';
4
9
 
@@ -9,9 +14,25 @@ export class DuplicateSkuError extends Error {
9
14
  }
10
15
  }
11
16
 
17
+ /** Keyset position of an export page: the record time and id of its last row. */
18
+ export interface ExportCursor {
19
+ readonly at: number;
20
+ readonly id: string;
21
+ }
22
+
12
23
  /** The database-agnostic business port. No driver type crosses it. */
13
24
  export interface CatalogRepository {
14
25
  list(tenantId: string): Promise<readonly CatalogItem[]>;
26
+ listItemsForExport(
27
+ tenantId: string,
28
+ after: ExportCursor | null,
29
+ limit: number,
30
+ ): Promise<readonly CatalogItem[]>;
31
+ listHistoryForExport(
32
+ tenantId: string,
33
+ after: ExportCursor | null,
34
+ limit: number,
35
+ ): Promise<readonly HistoryEntry[]>;
15
36
  find(tenantId: string, id: string): Promise<CatalogItem | null>;
16
37
  create(
17
38
  item: CatalogItem,
@@ -0,0 +1,157 @@
1
+ import { createDataClassRegistry, type Actor } from '@flowdular/sdk/kernel';
2
+ import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest';
3
+ import {
4
+ CatalogService,
5
+ EXPORT_PAGE,
6
+ } from '../src/services/catalog-service.ts';
7
+ import { catalogDataClasses } from '../src/services/data-classes.ts';
8
+ import {
9
+ closeCatalogTestDatabases,
10
+ createCatalogTestDatabase,
11
+ type CatalogTestDatabase,
12
+ } from './support/database.ts';
13
+
14
+ const ACTOR: Actor = { kind: 'user', id: 'account-1', label: 'Ada' };
15
+
16
+ let database: CatalogTestDatabase;
17
+ let service: CatalogService;
18
+
19
+ beforeEach(async () => {
20
+ database = await createCatalogTestDatabase();
21
+ service = new CatalogService(database.repository);
22
+ });
23
+
24
+ afterEach(() => database.dispose());
25
+ afterAll(closeCatalogTestDatabases);
26
+
27
+ function declarations() {
28
+ return catalogDataClasses(() => Promise.resolve(service));
29
+ }
30
+
31
+ function declared(key: string) {
32
+ const declaration = declarations().find((entry) => entry.key === key);
33
+ if (!declaration) throw new Error(`No data class ${key}.`);
34
+ return declaration;
35
+ }
36
+
37
+ async function exported(key: string, tenantId: string) {
38
+ const rows: Record<string, unknown>[] = [];
39
+ const summary = await declared(key).export!({
40
+ tenantId,
41
+ sink: { write: async (row) => void rows.push(row) },
42
+ });
43
+ return { rows, summary };
44
+ }
45
+
46
+ function create(tenantId: string, sku: string) {
47
+ return service.create(
48
+ tenantId,
49
+ {
50
+ sku,
51
+ name: `Item ${sku}`,
52
+ kind: 'product',
53
+ unit: 'pcs',
54
+ basePriceMinor: 1_000,
55
+ currency: 'PLN',
56
+ },
57
+ ACTOR,
58
+ );
59
+ }
60
+
61
+ describe('catalog data classes', () => {
62
+ it('declares one class per owned table the platform registry accepts', () => {
63
+ const registry = createDataClassRegistry();
64
+ registry.declare('catalog.core', declarations());
65
+ registry.seal();
66
+
67
+ const entry = registry
68
+ .list()
69
+ .find((module) => module.moduleId === 'catalog.core');
70
+ expect(entry?.classes.map((declaration) => declaration.key)).toEqual([
71
+ 'items',
72
+ 'history',
73
+ 'idempotency-ledger',
74
+ ]);
75
+ for (const key of ['items', 'history']) {
76
+ const declaration = declared(key);
77
+ expect(declaration.exportable).toBe(true);
78
+ expect(declaration.export).toBeTypeOf('function');
79
+ expect(declaration.defaultRetentionDays).toBeNull();
80
+ expect(declaration.sweep).toBeUndefined();
81
+ expect(declaration.erase).toBeUndefined();
82
+ }
83
+ const ledger = declared('idempotency-ledger');
84
+ expect(ledger.exportable).toBe(false);
85
+ expect(ledger.excludedReason).toBeTruthy();
86
+ expect(ledger.export).toBeUndefined();
87
+ expect(ledger.sweep).toBeUndefined();
88
+ });
89
+
90
+ it('exports the items and history of one tenant and none of another', async () => {
91
+ const kept = await create('tenant-a', 'A-1');
92
+ const archived = await create('tenant-a', 'A-2');
93
+ await service.archive('tenant-a', archived.id, ACTOR);
94
+ await create('tenant-b', 'B-1');
95
+
96
+ const items = await exported('items', 'tenant-a');
97
+ expect(items.rows.map((row) => row.sku)).toEqual(['A-1', 'A-2']);
98
+ expect(items.rows[0]).toEqual({
99
+ id: kept.id,
100
+ sku: 'A-1',
101
+ name: 'Item A-1',
102
+ kind: 'product',
103
+ unit: 'pcs',
104
+ basePriceMinor: 1_000,
105
+ currency: 'PLN',
106
+ status: 'active',
107
+ createdAt: new Date(kept.createdAt).toISOString(),
108
+ });
109
+ expect(items.summary).toEqual({
110
+ rows: 2,
111
+ from: new Date(kept.createdAt),
112
+ to: new Date(archived.createdAt),
113
+ });
114
+
115
+ const history = await exported('history', 'tenant-a');
116
+ expect(
117
+ history.rows.map((row) => [row.recordId, row.action, row.version]),
118
+ ).toEqual([
119
+ [kept.id, 'created', 1],
120
+ [archived.id, 'created', 1],
121
+ [archived.id, 'archived', 2],
122
+ ]);
123
+ expect(history.rows[2]).toMatchObject({
124
+ actor: ACTOR,
125
+ changes: { status: { from: 'active', to: 'archived' } },
126
+ });
127
+ expect(history.summary.rows).toBe(3);
128
+ expect(history.summary.from).toBeInstanceOf(Date);
129
+ expect(history.summary.to).toBeInstanceOf(Date);
130
+ });
131
+
132
+ it('walks past one page and stays inside the tenant', async () => {
133
+ const total = EXPORT_PAGE + 1;
134
+ for (let index = 0; index < total; index += 1) {
135
+ await create('tenant-a', `P-${String(index).padStart(3, '0')}`);
136
+ }
137
+ await create('tenant-b', 'B-1');
138
+
139
+ const items = await exported('items', 'tenant-a');
140
+ expect(items.summary.rows).toBe(total);
141
+ expect(new Set(items.rows.map((row) => row.id)).size).toBe(total);
142
+ expect(items.rows.some((row) => row.sku === 'B-1')).toBe(false);
143
+
144
+ const history = await exported('history', 'tenant-a');
145
+ expect(history.summary.rows).toBe(total);
146
+ expect(new Set(history.rows.map((row) => row.id)).size).toBe(total);
147
+ });
148
+
149
+ it('reports an empty tenant without writing a row', async () => {
150
+ await create('tenant-b', 'B-1');
151
+ for (const key of ['items', 'history']) {
152
+ const { rows, summary } = await exported(key, 'tenant-empty');
153
+ expect(rows).toEqual([]);
154
+ expect(summary).toEqual({ rows: 0, from: null, to: null });
155
+ }
156
+ });
157
+ });
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "id": "catalog.core",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "repository": "Flowdular/official-modules",
5
- "sourceCommit": "8ed1e780ba7405d0bdf5082882b92d130b72d081",
6
- "artifactSha256": "9010086822aa5f4d13176e465022fd2118c5dd0cb157200b31c2940fec1f3513",
5
+ "sourceCommit": "b8281b38498b1e3164e6619ec9e8fe922ad7dc10",
6
+ "artifactSha256": "6c98785c10fac14cb1a913c6462cb42e18fe824a7d0dd7620746605fd351441d",
7
7
  "files": {
8
8
  "LICENSE": "155d722071bad9d0d832482e06fd5a47f7034389cb63aabcb39f4282aa3499fc",
9
9
  "migrations/0001_catalog_core.down.sql": "0962a1edf0bde959cd4facccf77bd5b634d6cc25f125a0c79a61770c8871093f",
@@ -15,9 +15,9 @@
15
15
  "migrations/0004_catalog_idempotency_ledger.down.sql": "8ba72d8edc4d25ecf6d041888cf321690c25afdaf3723812ddd7d391d75370aa",
16
16
  "migrations/0004_catalog_idempotency_ledger.up.sql": "41e86ba2051159c6cc9672bfae17c1d02051da975f1c43c566a2b66592673cb0",
17
17
  "migrations/README.md": "2a15fd001713c2552a3c82b186246aa5fec136f43143cc2d9aa15bd491a09d41",
18
- "module.json": "66afd7635404b565e8b86f7c5bc485c85c9d568144858bfcff8a4a9429f76081",
19
- "package.json": "08b76b72f6fd7a8e3b08113c135899f6010b5fbbea8ca6be0b68b599afe75dad",
20
- "spec/module.yaml": "2df3b7c2cbd20f5f5950b469b092f85ef140875bf789a69da04fb6dc68ae0ff8",
18
+ "module.json": "1b512c9b97bdec2155103f521a6545e558e50491f90c74a93d33cc74511d2125",
19
+ "package.json": "67d50dc95723d21bb44bbab54ae14fe89641c52b9f62ba065908f67ebcf0ed7a",
20
+ "spec/module.yaml": "ec931c78f351c7fc53f43fdd5ec9cd1a46db485f642648329bf1612883a3c958",
21
21
  "src/acl/permissions.ts": "3375521a5a229736ffac5a948140ec347dc7a7e4fba80e778c0baaf9e0622590",
22
22
  "src/agent/tools.ts": "3204d20886d2b864482adc1ed3303c653aebd207e16c4fce0c67a4979ea8db07",
23
23
  "src/api/endpoints.ts": "34f1036e759353550be2acf67c6fa254afc790942d09489db2e76a082df5c3a3",
@@ -32,16 +32,18 @@
32
32
  "src/domain/types.ts": "4b26be80954d792b09a3a34d29d845b9f6119640be505daba325fec820e06ef6",
33
33
  "src/domain/variables.ts": "131130e57838235b1f3fcb799da44baeae16522bb346d522827e022305c110b5",
34
34
  "src/index.ts": "5b27943018db9a18651d62add7d6f8397d7c492a204381fae74547b99b91990d",
35
- "src/platform.ts": "3bdc15a5749cceeca4e86388fad13aebcbc151bb4226852d46ed7845248c4efe",
35
+ "src/platform.ts": "b5d809b071af812877f3fb09cce3ced4cdaed8f62611c3eb6d5fcaf6cd7333cb",
36
36
  "src/server/index.ts": "90eae107d863418124e5d10c4d1fe8a119bd14da605b36de708c18d8500c4b5e",
37
37
  "src/server/runtime.ts": "5f4389a6adf8265f6737c489baf686f63cc3550662103d28b2da9800dd9f5b63",
38
- "src/services/catalog-service.ts": "e5ffa703f6a87e4e2e762f5beb87952be40b3b5847d10b373033a9b01fa4a378",
39
- "src/services/database-repository.ts": "b513a9459fb3fc1362e9dcd98c10a104b02dbb748ae23c59c6768c2f0e7a1466",
38
+ "src/services/catalog-service.ts": "51f1dcd7c0335f7b417fb45ba700c0098e7193246ee91659c27cab8608282693",
39
+ "src/services/data-classes.ts": "ea09b1a7def59fee43d57ca7dab6fdb4d646b51df2cacb60af96676c43602b33",
40
+ "src/services/database-repository.ts": "e6ff61ba197899d5618a02ce743c90d4bf09ae64dcfd1bd3a9985b18a2d23e80",
40
41
  "src/services/index.ts": "a08aefe5101814538666f861536287fc382e6c655d62ba9b9ef4bbfbc6ede4af",
41
42
  "src/services/migration.ts": "89c8e333482c80bc819deba0cf4f4e37e94137933af0f6503f4f9ebcb4f5f371",
42
- "src/services/repository.ts": "df0f1364cb05031972993c873faf6bcb7fbe4c92d05a04ae980497d9a732dd7f",
43
+ "src/services/repository.ts": "98d27a40ff02137ec0c6d8799d9a853d40a76eab2564dd500806c4e84f6dc3a3",
43
44
  "src/services/target-idempotency.ts": "7111b56a30b691eaaaa64f5691895ac94b6e0735b0079794835bbbeba5102cb0",
44
45
  "tests/agent-tools.test.ts": "3cdda30b5454d58c6e8791a98e3daf6fe58b9852cd5c437b3614c3f6123baa91",
46
+ "tests/data-classes.test.ts": "bea08e4d1d7380ccdebc211374b74a6034714b192fd98bfb90a2218067b35658",
45
47
  "tests/endpoints.test.ts": "24d05c7dd52820f90c777a1b814529f5188a9a5a9daeaacda07314f17b7483cc",
46
48
  "tests/idempotency.test.ts": "f2e0896635bcd95271b66181daa2c084411913cdf864ae8b4814f2beeb8c2943",
47
49
  "tests/migrations.test.ts": "47d3d741eddde8677be287730d2f96af556b4b7bf0d8378f9a12a7309af17d78",
@@ -63,6 +63,10 @@ Do not load the whole skill catalog into the task context.
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
65
  `HANDOFF: none - <why>`, never your own role.
66
+ 13. Flow: request, `spec-interview`, approval, `module-new`/`module-update`,
67
+ `auto-review`. Implement from the approved spec and its touch list; do not
68
+ scan `modules/` or `packages/`. First lookup is `.ai/platform-capabilities.md`.
69
+ What the spec lacks is a spec defect to report, never a guess.
66
70
 
67
71
  Detailed recipes: `docs/agent-contract.md` (lookup only).
68
72
  Reference module: `.ai/references/catalog`; visual contract: `docs/design-system.md`.
@@ -4,6 +4,7 @@ Flowdular is an agentic foundation framework: the platform is the foundation, an
4
4
 
5
5
  | Skill | One line |
6
6
  | ----------------------- | --------------------------------------------------------------------------------------------------------------------- |
7
+ | `spec-interview` | Interview a request into a v2 spec: capability card defaults, the questions protocol, decisions and out of scope. |
7
8
  | `module-new` | Create a module from an approved spec: scaffold, what the scaffold lacks, server and client file sets, enable, grant. |
8
9
  | `module-update` | Change an existing module with a fixed touch list per change class and the version bump rules. |
9
10
  | `spec-approval` | Record explicit user approval of an exact current module spec without letting an agent approve its own work. |
@@ -23,9 +24,18 @@ Flowdular is an agentic foundation framework: the platform is the foundation, an
23
24
  | `variables` | Variable-aware fields and templates: the `{{ }}` contract, the scope mask, server-side resolution, adding a source. |
24
25
  | `workflow-development` | Build, publish, invoke, simulate, and test typed durable workflows and their module integration capability. |
25
26
  | `release-eject-pr` | Sandbox eject sequence, repository gates, branch and PR conventions, post-merge scope grant. |
27
+ | `deploy-operate` | Container build, production env keys, migrations at rollout, health and readiness, backup and restore, rollback. |
26
28
 
27
29
  Choose one skill per task phase: the most specific matching entry above. For ordinary module work use `module-new` or `module-update`. Finish the phase before switching; do not recursively load other SKILL.md files mentioned in a skill. Consult relevant code and supporting references only as needed. `spec-approval` is host-only and requires an explicit user approval instruction.
28
30
 
31
+ The normal path for a module is one phase per step:
32
+
33
+ ```text
34
+ request -> spec-interview -> operator approval (spec-approval) -> module-new -> auto-review
35
+ ```
36
+
37
+ An existing module takes the same path with `module-update` in place of `module-new`. `spec-interview` reads `.ai/platform-capabilities.md` and asks for the decisions it cannot infer; implementation reads the approved spec and the touch list instead of scanning the repository, and reports anything the spec lacks as a spec defect.
38
+
29
39
  Where they are read:
30
40
 
31
41
  - Sandbox: copies are available in `reference/skills/`, but each turn receives exactly one Task skill selected by role, blueprint and request. An explicit `$skill-name` takes precedence only if installed and eligible for that role. Missing matches never load the whole catalog.
@@ -5,7 +5,6 @@ description: >-
5
5
  the real harness, permission, idempotency, audit, and test contract.
6
6
  roles:
7
7
  - agentic-engineer
8
- - backend-engineer
9
8
  - module-executor
10
9
  when: A brief asks to expose a module operation as a tool to business agents or workflows.
11
10
  ---
@@ -125,7 +124,7 @@ Dependencies: `package.json` gets `"@flowdular/sdk/harness": "workspace:*"`. You
125
124
  - A mutating tool with `idempotency: 'required'` is executable only after the target module implements a durable ledger and the definition declares `idempotencyProtection: 'target-ledger'`. The harness derives a stable key from the durable run id and deterministic tool-call ordinal. The target ledger binds `(tenant, tool id, key)` to a canonical input hash and the first result. A replay returns that result without another mutation; the same key with another tool or input fails closed. Provider tool-call ids are audit metadata only. Never add the declaration before the target migration, repository transaction, and crash-recovery test exist.
126
125
  - Each call has a deadline (`tool.timeoutMs`, default 30 s, range 250 to 600000) and the harness caps serialized output at 32 KB (`boundToolOutput`), marking `truncated`. Still page or limit your rows so one call cannot dominate the run window.
127
126
  - Events per call land in the run's persisted audit chain: `tool.started`, then `tool.completed` (metadata `tool`, `outputCharacters`, `truncated`) on success, `tool.failed` (reason) on error, or `tool.denied` (reason) when not granted or input-invalid.
128
- - Runs are enqueued by `POST /api/agent-runs` behind `agents.runs.execute`, claimed by `AgentWorker` with a lease, observed through `GET /api/agent-runs` and the SSE stream. The registered tool ids surface in `GET /api/agents` `tools`, which the Agents form reads to build the allowed-tools grid. Never make a tool block on user input.
127
+ - Runs are enqueued by `POST /api/agent-runs` behind `agents.runs.execute`, claimed by `AgentWorker` with a lease, observed through `GET /api/agent-runs` and the SSE stream. The registered tool ids surface in `GET /api/agents/context` `tools`, which the Agents form reads to build the allowed-tools grid. Never make a tool block on user input.
129
128
 
130
129
  ## 4. Deliverables for a module
131
130
 
@@ -26,7 +26,7 @@ Check every route in `src/api/endpoints.ts` against `.ai/references/catalog/src/
26
26
 
27
27
  ## 2. Scope model
28
28
 
29
- `modules/auth/src/acl/scopes.ts`: `AUTH_SCOPES`, `PLATFORM_SCOPES` (`system.workspace.access` gates the shell in `platform/src/App.tsrx`; `system.settings.read` and `system.settings.manage` guard `GET /api/settings` and `POST /api/settings/update` in `modules/auth/src/server/settings-endpoints.ts`), `BUNDLED_MODULE_SCOPES`, `OWNER_SCOPES` (all of them), `MEMBER_SCOPES` (read scopes plus `agents.runs.execute`). Sign-up creates an owner with `OWNER_SCOPES`; member creation copies `OWNER_SCOPES` or `MEMBER_SCOPES` by role (`modules/auth/src/services/auth-service.ts`). A module's scopes reach existing owners through `pnpm flowdular module enable <id> --apply` (which runs the grant) or `pnpm flowdular auth sync-scopes --module <id> --apply` for a re-grant. Navigation in the `Development` group is owner-only in the client (`packages/client/src/shell/navigation.ts`); the server permission stays authoritative.
29
+ `modules/auth/src/acl/scopes.ts`: `AUTH_SCOPES`, `PLATFORM_SCOPES` (`system.workspace.access` gates the shell in `platform/src/App.tsrx`; `system.settings.read` and `system.settings.manage` guard `GET /api/settings` (`system.settings.list`) and `POST /api/settings/update` (`system.settings.update`) in `modules/system/src/server/endpoints.ts`, from `SYSTEM_PERMISSIONS` in `modules/system/src/acl/permissions.ts`), `BUNDLED_MODULE_SCOPES`, `OWNER_SCOPES` (all of them), `MEMBER_SCOPES` (read scopes plus `agents.runs.execute`). Sign-up creates an owner with `OWNER_SCOPES`; member creation copies `OWNER_SCOPES` or `MEMBER_SCOPES` by role (`modules/auth/src/services/auth-service.ts`). A module's scopes reach existing owners through `pnpm flowdular module enable <id> --apply` (which runs the grant) or `pnpm flowdular auth sync-scopes --module <id> --apply` for a re-grant. Navigation in the `Development` group is owner-only in the client (`packages/client/src/shell/navigation.ts`); the server permission stays authoritative.
30
30
 
31
31
  Review question: does every new scope appear in the spec `permissions`, in `src/acl/permissions.ts`, on the endpoint, and on the client contribution that exposes it?
32
32
 
@@ -6,7 +6,6 @@ description: >-
6
6
  automation delivered by a module, not for sandbox coding specialists.
7
7
  roles:
8
8
  - agentic-engineer
9
- - backend-engineer
10
9
  - module-executor
11
10
  when: A module should provide a ready business agent that tenants can configure and run.
12
11
  ---