create-flowdular 0.2.6 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.agents/skills/cli-extension/SKILL.md +1 -1
- package/agent-template/.agents/skills/deploy-operate/SKILL.md +114 -0
- package/agent-template/.agents/skills/module-new/SKILL.md +29 -0
- package/agent-template/.agents/skills/module-update/SKILL.md +9 -1
- package/agent-template/.agents/skills/spec-interview/SKILL.md +114 -0
- package/agent-template/.agents/skills/ux-design/SKILL.md +34 -3
- package/agent-template/.ai/README.md +2 -1
- package/agent-template/.ai/agents/sandbox/business-manager.md +5 -1
- package/agent-template/.ai/blueprints/author-spec/README.md +1 -1
- package/agent-template/.ai/blueprints/author-spec/spec-requirements.yaml +44 -0
- package/agent-template/.ai/blueprints/author-spec/steps.yaml +5 -5
- package/agent-template/.ai/blueprints/author-spec/templates/module.yaml +99 -3
- package/agent-template/.ai/blueprints/edit-module/gates.yaml +4 -0
- package/agent-template/.ai/blueprints/edit-module/required-files.yaml +9 -0
- package/agent-template/.ai/blueprints/new-module/gates.yaml +4 -0
- package/agent-template/.ai/blueprints/new-module/spec-requirements.yaml +2 -2
- package/agent-template/.ai/blueprints/release/gates.yaml +4 -0
- package/agent-template/.ai/platform-capabilities.md +128 -0
- package/agent-template/.ai/policies/capabilities.yaml +130 -3
- package/agent-template/.ai/policies/path-ownership.yaml +5 -2
- package/agent-template/.ai/policies/task-budgets.yaml +5 -3
- package/agent-template/.ai/references/catalog/module.json +4 -4
- package/agent-template/.ai/references/catalog/package.json +2 -2
- package/agent-template/.ai/references/catalog/spec/module.yaml +5 -3
- package/agent-template/.ai/references/catalog/src/platform.ts +2 -0
- package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +89 -1
- package/agent-template/.ai/references/catalog/src/services/data-classes.ts +47 -0
- package/agent-template/.ai/references/catalog/src/services/database-repository.ts +98 -1
- package/agent-template/.ai/references/catalog/src/services/repository.ts +22 -1
- package/agent-template/.ai/references/catalog/tests/data-classes.test.ts +157 -0
- package/agent-template/.ai/references/catalog.provenance.json +12 -10
- package/agent-template/.ai/rules/flowdular.md +4 -0
- package/agent-template/.ai/skills/README.md +10 -0
- package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -2
- package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.ai/skills/business-agent-design/SKILL.md +0 -1
- package/agent-template/.ai/skills/cli-extension/SKILL.md +1 -1
- package/agent-template/.ai/skills/deploy-operate/SKILL.md +119 -0
- package/agent-template/.ai/skills/module-new/SKILL.md +29 -3
- package/agent-template/.ai/skills/module-update/SKILL.md +9 -3
- package/agent-template/.ai/skills/perf-audit/SKILL.md +0 -1
- package/agent-template/.ai/skills/release-eject-pr/SKILL.md +0 -1
- package/agent-template/.ai/skills/spec-interview/SKILL.md +120 -0
- package/agent-template/.ai/skills/test-hardening/SKILL.md +1 -0
- package/agent-template/.ai/skills/ux-design/SKILL.md +34 -3
- package/agent-template/.ai/skills/variables/SKILL.md +0 -2
- package/agent-template/.ai/skills/workflow-development/SKILL.md +0 -1
- package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.claude/skills/cli-extension/SKILL.md +1 -1
- package/agent-template/.claude/skills/deploy-operate/SKILL.md +114 -0
- package/agent-template/.claude/skills/module-new/SKILL.md +29 -0
- package/agent-template/.claude/skills/module-update/SKILL.md +9 -1
- package/agent-template/.claude/skills/spec-interview/SKILL.md +114 -0
- package/agent-template/.claude/skills/ux-design/SKILL.md +34 -3
- package/agent-template/AGENTS.md +4 -0
- package/agent-template/CLAUDE.md +4 -0
- package/agent-template/docs/adr/0003-module-settings.md +1 -1
- package/agent-template/docs/adr/0006-agentic-workflows.md +24 -21
- package/agent-template/docs/agent-contract.md +2 -2
- package/agent-template/docs/cli-extensions.md +82 -0
- package/agent-template/docs/cli.md +195 -0
- package/agent-template/docs/configuration.md +593 -36
- package/agent-template/docs/design-system.md +185 -31
- package/agent-template/docs/getting-started.md +118 -0
- package/agent-template/docs/module-distribution.md +96 -0
- package/agent-template/docs/module-web-surfaces.md +221 -0
- package/agent-template/docs/modules.md +216 -0
- package/agent-template/docs/operations.md +545 -0
- package/agent-template/docs/sandbox.md +212 -0
- package/agent-template/platform/scripts/build.mjs +11 -0
- package/dist/bin.js +29 -0
- package/package.json +1 -1
- package/template/default/.dockerignore +14 -0
- package/template/default/.env.example +96 -0
- package/template/default/README.md +37 -1
- package/template/default/flowdular.json +15 -4
- package/template/default/infra/README.md +116 -0
- package/template/default/infra/docker/Dockerfile +37 -0
- package/template/default/infra/docker/compose.yaml +158 -0
- package/template/default/infra/docker/postgres/10-roles.sh +31 -0
- package/template/default/infra/docker/postgres/tls-init.sh +28 -0
- package/template/default/infra/kubernetes/database-secret.example.yaml +15 -0
- package/template/default/infra/kubernetes/deployment.yaml +211 -0
- package/template/default/infra/kubernetes/kustomization.yaml +9 -0
- package/template/default/infra/kubernetes/secrets.example.yaml +52 -0
- package/template/default/infra/kubernetes/service.yaml +13 -0
- package/template/default/modules/example/module.json +2 -1
- package/template/default/modules/example/package.json +1 -1
- package/template/default/modules/example/spec/module.yaml +1 -1
- package/template/default/modules/example/src/services/database-repository.ts +2 -12
- package/template/default/package.json +3 -2
- package/template/default/platform/octane.config.ts +99 -9
- package/template/default/platform/package.json +1 -1
- package/template/default/platform/src/generated/modules.client.ts +26 -2
- package/template/default/platform/src/generated/modules.server.ts +241 -10
- package/template/default/platform/src/server/health.ts +47 -0
- package/template/default/platform/src/server/metrics.ts +100 -0
- package/template/default/platform/src/server/storage.ts +172 -0
- package/template/default/platform/src/server/tracing.ts +85 -0
- package/template/default/specs/application.yaml +15 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowdular/module-catalog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"octane": "0.1.51",
|
|
18
18
|
"segment-state": "0.2.0",
|
|
19
|
-
"@flowdular/sdk": "0.
|
|
19
|
+
"@flowdular/sdk": "0.3.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@tsrx/typescript-plugin": "0.3.120",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
schemaVersion: 1
|
|
2
2
|
id: catalog.core
|
|
3
|
-
specVersion: 0.
|
|
3
|
+
specVersion: 0.7.1
|
|
4
4
|
status: approved
|
|
5
5
|
name: Product Catalog Core
|
|
6
6
|
description: Provides tenant-scoped products and services with stable SKUs, units, prices, currencies, and lifecycle state.
|
|
@@ -12,9 +12,9 @@ capabilities:
|
|
|
12
12
|
- translations
|
|
13
13
|
dependencies:
|
|
14
14
|
- id: system.core
|
|
15
|
-
range: 0.
|
|
15
|
+
range: ^0.7.0
|
|
16
16
|
- id: auth.core
|
|
17
|
-
range: 0.
|
|
17
|
+
range: ^0.13.0
|
|
18
18
|
tenancy: required
|
|
19
19
|
locales:
|
|
20
20
|
- en
|
|
@@ -30,6 +30,7 @@ invariants:
|
|
|
30
30
|
- One tenant-scoped durable target ledger is shared by mutating agent tools and workflow actions; each entry binds an idempotency key to the exact tool identifier or versioned workflow action identifier, canonical input digest, and first persisted result in the same transaction as the catalog mutation.
|
|
31
31
|
- Replaying the same idempotency key for the same operation and canonical input returns the original result without another catalog item or history row, while reuse for another operation or input fails with a stable conflict.
|
|
32
32
|
- Idempotency ledger entries remain after a catalog item is deleted, and audit metadata records identifiers, input digests, and outcomes without raw tool input, credentials, secrets, or result payloads.
|
|
33
|
+
- catalog.core declares three data classes to the platform registry while it composes and performs their export itself on its own lease under its own tenant transaction. catalog.core.items and catalog.core.history are master data, and each export walks every row of one workspace by keyset in bounded pages, and neither class carries a retention period, a sweep, or an erasure, so a row leaves only when a person deletes it. catalog.core.idempotency-ledger is operational, is never swept, and is excluded from the export with a stated reason because the item it points at is already exported.
|
|
33
34
|
permissions:
|
|
34
35
|
- id: catalog.items.read
|
|
35
36
|
description: Read products and services in the active tenant.
|
|
@@ -38,6 +39,7 @@ permissions:
|
|
|
38
39
|
dataOwnership:
|
|
39
40
|
- catalog.core owns SKU, item identity, kind, unit, base price, currency, and lifecycle status.
|
|
40
41
|
- catalog.core owns the idempotency ledger for its mutating agent tools and workflow actions; callers supply the key but do not maintain a second target ledger.
|
|
42
|
+
- catalog.core declares the data classes catalog.core.items (catalog_items), catalog.core.history (catalog_items_history_v2, which holds every row of the superseded catalog_items_history) and catalog.core.idempotency-ledger (catalog_idempotency_ledger); items and history are exported per workspace, the ledger is excluded with a stated reason.
|
|
41
43
|
- Sales and purchasing modules reference catalog item identifiers and snapshot commercial terms when required by their specs.
|
|
42
44
|
acceptanceScenarios:
|
|
43
45
|
- id: CATALOG-LIST
|
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"repository": "Flowdular/official-modules",
|
|
5
|
-
"sourceCommit": "
|
|
6
|
-
"artifactSha256": "
|
|
5
|
+
"sourceCommit": "be391ac460e53e06a812fc9f0ea1260e2daa058f",
|
|
6
|
+
"artifactSha256": "7c645f660f44bb4c4540d32dd188d643aafb067f4c505a2722a7f2605a7b54b8",
|
|
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": "
|
|
19
|
-
"package.json": "
|
|
20
|
-
"spec/module.yaml": "
|
|
18
|
+
"module.json": "f2c66d18e3d54ef1ad8dcc3d26d1ee39a2982463dd3bc3fb26b159039933ed86",
|
|
19
|
+
"package.json": "3e05cf299b5cc5d948e4b3569bd93f2648a30b22f4f14d26bb31548a79529092",
|
|
20
|
+
"spec/module.yaml": "633a719d765d9779e2bf4ae0a608251767a62cbb4e15305dc13afcb1e52f14c9",
|
|
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": "
|
|
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": "
|
|
39
|
-
"src/services/
|
|
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": "
|
|
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.
|