create-flowdular 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/agent-template/.agents/skills/agent-tool-design/SKILL.md +203 -0
- package/agent-template/.agents/skills/auth-security-review/SKILL.md +90 -0
- package/agent-template/.agents/skills/auto-review/SKILL.md +103 -0
- package/agent-template/.agents/skills/bug-hunt/SKILL.md +104 -0
- package/agent-template/.agents/skills/business-agent-design/SKILL.md +182 -0
- package/agent-template/.agents/skills/cli-extension/SKILL.md +108 -0
- package/agent-template/.agents/skills/core-extend/SKILL.md +99 -0
- package/agent-template/.agents/skills/database-adapter/SKILL.md +198 -0
- package/agent-template/.agents/skills/database-adapter/references/first-run-and-matrix.md +105 -0
- package/agent-template/.agents/skills/migration-authoring/SKILL.md +161 -0
- package/agent-template/.agents/skills/module-new/SKILL.md +171 -0
- package/agent-template/.agents/skills/module-update/SKILL.md +91 -0
- package/agent-template/.agents/skills/perf-audit/SKILL.md +98 -0
- package/agent-template/.agents/skills/release-eject-pr/SKILL.md +107 -0
- package/agent-template/.agents/skills/spec-approval/SKILL.md +106 -0
- package/agent-template/.agents/skills/test-hardening/SKILL.md +79 -0
- package/agent-template/.agents/skills/translations-i18n/SKILL.md +78 -0
- package/agent-template/.agents/skills/ux-design/SKILL.md +92 -0
- package/agent-template/.agents/skills/variables/SKILL.md +156 -0
- package/agent-template/.agents/skills/workflow-development/SKILL.md +192 -0
- package/agent-template/.ai/README.md +62 -0
- package/agent-template/.ai/agents/README.md +27 -0
- package/agent-template/.ai/agents/module-executor.md +36 -0
- package/agent-template/.ai/agents/reviewer.md +23 -0
- package/agent-template/.ai/agents/sandbox/agentic-engineer.md +31 -0
- package/agent-template/.ai/agents/sandbox/backend-engineer.md +36 -0
- package/agent-template/.ai/agents/sandbox/business-manager.md +23 -0
- package/agent-template/.ai/agents/sandbox/frontend-engineer.md +27 -0
- package/agent-template/.ai/agents/sandbox/ux-designer.md +23 -0
- package/agent-template/.ai/agents/spec-author.md +29 -0
- package/agent-template/.ai/blueprints/add-migration/README.md +5 -0
- package/agent-template/.ai/blueprints/add-migration/allowed-paths.yaml +23 -0
- package/agent-template/.ai/blueprints/add-migration/blueprint.json +14 -0
- package/agent-template/.ai/blueprints/add-migration/examples/invalid/input-destructive.json +6 -0
- package/agent-template/.ai/blueprints/add-migration/examples/invalid/plan-unnumbered-file.json +9 -0
- package/agent-template/.ai/blueprints/add-migration/examples/valid/input.json +6 -0
- package/agent-template/.ai/blueprints/add-migration/examples/valid/plan.json +9 -0
- package/agent-template/.ai/blueprints/add-migration/gates.yaml +30 -0
- package/agent-template/.ai/blueprints/add-migration/input.schema.json +23 -0
- package/agent-template/.ai/blueprints/add-migration/plan.schema.json +54 -0
- package/agent-template/.ai/blueprints/add-migration/required-files.yaml +18 -0
- package/agent-template/.ai/blueprints/add-migration/spec-requirements.yaml +13 -0
- package/agent-template/.ai/blueprints/add-migration/steps.yaml +62 -0
- package/agent-template/.ai/blueprints/author-spec/README.md +5 -0
- package/agent-template/.ai/blueprints/author-spec/allowed-paths.yaml +7 -0
- package/agent-template/.ai/blueprints/author-spec/blueprint.json +14 -0
- package/agent-template/.ai/blueprints/author-spec/examples/invalid/input-missing-outcome.json +5 -0
- package/agent-template/.ai/blueprints/author-spec/examples/valid/input.json +6 -0
- package/agent-template/.ai/blueprints/author-spec/gates.yaml +13 -0
- package/agent-template/.ai/blueprints/author-spec/input.schema.json +20 -0
- package/agent-template/.ai/blueprints/author-spec/plan.schema.json +14 -0
- package/agent-template/.ai/blueprints/author-spec/required-files.yaml +6 -0
- package/agent-template/.ai/blueprints/author-spec/spec-requirements.yaml +35 -0
- package/agent-template/.ai/blueprints/author-spec/steps.yaml +28 -0
- package/agent-template/.ai/blueprints/author-spec/templates/module.yaml +45 -0
- package/agent-template/.ai/blueprints/bug-fix/README.md +5 -0
- package/agent-template/.ai/blueprints/bug-fix/allowed-paths.yaml +29 -0
- package/agent-template/.ai/blueprints/bug-fix/blueprint.json +14 -0
- package/agent-template/.ai/blueprints/bug-fix/examples/invalid/input-no-symptom.json +4 -0
- package/agent-template/.ai/blueprints/bug-fix/examples/invalid/plan-no-test.json +8 -0
- package/agent-template/.ai/blueprints/bug-fix/examples/valid/input.json +6 -0
- package/agent-template/.ai/blueprints/bug-fix/examples/valid/plan.json +8 -0
- package/agent-template/.ai/blueprints/bug-fix/gates.yaml +30 -0
- package/agent-template/.ai/blueprints/bug-fix/input.schema.json +25 -0
- package/agent-template/.ai/blueprints/bug-fix/plan.schema.json +53 -0
- package/agent-template/.ai/blueprints/bug-fix/required-files.yaml +7 -0
- package/agent-template/.ai/blueprints/bug-fix/spec-requirements.yaml +7 -0
- package/agent-template/.ai/blueprints/bug-fix/steps.yaml +51 -0
- package/agent-template/.ai/blueprints/core-extend/README.md +5 -0
- package/agent-template/.ai/blueprints/core-extend/allowed-paths.yaml +49 -0
- package/agent-template/.ai/blueprints/core-extend/blueprint.json +14 -0
- package/agent-template/.ai/blueprints/core-extend/examples/invalid/input-unknown-package.json +5 -0
- package/agent-template/.ai/blueprints/core-extend/examples/invalid/plan-missing-gates.json +7 -0
- package/agent-template/.ai/blueprints/core-extend/examples/valid/input.json +6 -0
- package/agent-template/.ai/blueprints/core-extend/examples/valid/plan.json +10 -0
- package/agent-template/.ai/blueprints/core-extend/gates.yaml +16 -0
- package/agent-template/.ai/blueprints/core-extend/input.schema.json +54 -0
- package/agent-template/.ai/blueprints/core-extend/plan.schema.json +39 -0
- package/agent-template/.ai/blueprints/core-extend/required-files.yaml +36 -0
- package/agent-template/.ai/blueprints/core-extend/spec-requirements.yaml +17 -0
- package/agent-template/.ai/blueprints/core-extend/steps.yaml +54 -0
- package/agent-template/.ai/blueprints/edit-module/README.md +9 -0
- package/agent-template/.ai/blueprints/edit-module/allowed-paths.yaml +27 -0
- package/agent-template/.ai/blueprints/edit-module/blueprint.json +20 -0
- package/agent-template/.ai/blueprints/edit-module/examples/invalid/input-unknown-change.json +5 -0
- package/agent-template/.ai/blueprints/edit-module/examples/invalid/plan-touches-platform.json +15 -0
- package/agent-template/.ai/blueprints/edit-module/examples/valid/input.json +5 -0
- package/agent-template/.ai/blueprints/edit-module/examples/valid/plan.json +25 -0
- package/agent-template/.ai/blueprints/edit-module/gates.yaml +30 -0
- package/agent-template/.ai/blueprints/edit-module/input.schema.json +31 -0
- package/agent-template/.ai/blueprints/edit-module/plan.schema.json +65 -0
- package/agent-template/.ai/blueprints/edit-module/required-files.yaml +80 -0
- package/agent-template/.ai/blueprints/edit-module/spec-requirements.yaml +15 -0
- package/agent-template/.ai/blueprints/edit-module/steps.yaml +115 -0
- package/agent-template/.ai/blueprints/new-module/README.md +7 -0
- package/agent-template/.ai/blueprints/new-module/allowed-paths.yaml +27 -0
- package/agent-template/.ai/blueprints/new-module/blueprint.json +20 -0
- package/agent-template/.ai/blueprints/new-module/examples/invalid/input-spec-outside-modules.json +4 -0
- package/agent-template/.ai/blueprints/new-module/examples/invalid/plan-unknown-gate.json +8 -0
- package/agent-template/.ai/blueprints/new-module/examples/valid/input.json +5 -0
- package/agent-template/.ai/blueprints/new-module/examples/valid/plan.json +22 -0
- package/agent-template/.ai/blueprints/new-module/gates.yaml +30 -0
- package/agent-template/.ai/blueprints/new-module/input.schema.json +21 -0
- package/agent-template/.ai/blueprints/new-module/plan.schema.json +58 -0
- package/agent-template/.ai/blueprints/new-module/required-files.yaml +73 -0
- package/agent-template/.ai/blueprints/new-module/spec-requirements.yaml +30 -0
- package/agent-template/.ai/blueprints/new-module/steps.yaml +138 -0
- package/agent-template/.ai/blueprints/release/README.md +5 -0
- package/agent-template/.ai/blueprints/release/allowed-paths.yaml +19 -0
- package/agent-template/.ai/blueprints/release/blueprint.json +14 -0
- package/agent-template/.ai/blueprints/release/examples/invalid/input-bad-version.json +4 -0
- package/agent-template/.ai/blueprints/release/examples/invalid/plan-bad-branch.json +7 -0
- package/agent-template/.ai/blueprints/release/examples/valid/input.json +5 -0
- package/agent-template/.ai/blueprints/release/examples/valid/plan.json +20 -0
- package/agent-template/.ai/blueprints/release/gates.yaml +20 -0
- package/agent-template/.ai/blueprints/release/input.schema.json +24 -0
- package/agent-template/.ai/blueprints/release/plan.schema.json +46 -0
- package/agent-template/.ai/blueprints/release/required-files.yaml +19 -0
- package/agent-template/.ai/blueprints/release/spec-requirements.yaml +8 -0
- package/agent-template/.ai/blueprints/release/steps.yaml +47 -0
- package/agent-template/.ai/blueprints/security-review/README.md +5 -0
- package/agent-template/.ai/blueprints/security-review/allowed-paths.yaml +6 -0
- package/agent-template/.ai/blueprints/security-review/blueprint.json +14 -0
- package/agent-template/.ai/blueprints/security-review/examples/invalid/input-unknown-kind.json +4 -0
- package/agent-template/.ai/blueprints/security-review/examples/invalid/plan-finding-without-scenario.json +14 -0
- package/agent-template/.ai/blueprints/security-review/examples/valid/input.json +4 -0
- package/agent-template/.ai/blueprints/security-review/examples/valid/plan.json +19 -0
- package/agent-template/.ai/blueprints/security-review/gates.yaml +22 -0
- package/agent-template/.ai/blueprints/security-review/input.schema.json +20 -0
- package/agent-template/.ai/blueprints/security-review/plan.schema.json +65 -0
- package/agent-template/.ai/blueprints/security-review/required-files.yaml +6 -0
- package/agent-template/.ai/blueprints/security-review/spec-requirements.yaml +9 -0
- package/agent-template/.ai/blueprints/security-review/steps.yaml +38 -0
- package/agent-template/.ai/examples/README.md +8 -0
- package/agent-template/.ai/examples/bad/client-imports-server/README.md +20 -0
- package/agent-template/.ai/examples/bad/client-imports-server/api.ts +12 -0
- package/agent-template/.ai/examples/bad/missing-acl/README.md +23 -0
- package/agent-template/.ai/examples/bad/missing-acl/endpoints.ts +12 -0
- package/agent-template/.ai/examples/bad/tenant-from-body/README.md +19 -0
- package/agent-template/.ai/examples/bad/tenant-from-body/endpoints.ts +33 -0
- package/agent-template/.ai/examples/client-contribution/CustomerListView.tsrx +34 -0
- package/agent-template/.ai/examples/client-contribution/README.md +11 -0
- package/agent-template/.ai/examples/client-contribution/contribution.tsrx +48 -0
- package/agent-template/.ai/examples/client-contribution/index.ts +20 -0
- package/agent-template/.ai/examples/client-contribution/permissions.ts +8 -0
- package/agent-template/.ai/examples/customer-cli-extension/README.md +14 -0
- package/agent-template/.ai/examples/customer-cli-extension/commands.json +17 -0
- package/agent-template/.ai/examples/customer-cli-extension/index.ts +36 -0
- package/agent-template/.ai/examples/module-create/task-packet.json +11 -0
- package/agent-template/.ai/guides/application-development.md +97 -0
- package/agent-template/.ai/policies/capabilities.yaml +164 -0
- package/agent-template/.ai/policies/model-routing.yaml +72 -0
- package/agent-template/.ai/policies/path-ownership.yaml +65 -0
- package/agent-template/.ai/policies/task-budgets.yaml +37 -0
- package/agent-template/.ai/references/catalog/LICENSE +21 -0
- package/agent-template/.ai/references/catalog/migrations/0001_catalog_core.down.sql +2 -0
- package/agent-template/.ai/references/catalog/migrations/0001_catalog_core.up.sql +21 -0
- package/agent-template/.ai/references/catalog/migrations/0002_catalog_history.down.sql +3 -0
- package/agent-template/.ai/references/catalog/migrations/0002_catalog_history.up.sql +20 -0
- package/agent-template/.ai/references/catalog/migrations/0003_catalog_history_service_actors.down.sql +3 -0
- package/agent-template/.ai/references/catalog/migrations/0003_catalog_history_service_actors.up.sql +36 -0
- package/agent-template/.ai/references/catalog/migrations/0004_catalog_idempotency_ledger.down.sql +3 -0
- package/agent-template/.ai/references/catalog/migrations/0004_catalog_idempotency_ledger.up.sql +19 -0
- package/agent-template/.ai/references/catalog/migrations/README.md +3 -0
- package/agent-template/.ai/references/catalog/module.json +27 -0
- package/agent-template/.ai/references/catalog/package.json +49 -0
- package/agent-template/.ai/references/catalog/spec/module.yaml +86 -0
- package/agent-template/.ai/references/catalog/src/acl/permissions.ts +6 -0
- package/agent-template/.ai/references/catalog/src/agent/tools.ts +164 -0
- package/agent-template/.ai/references/catalog/src/api/endpoints.ts +243 -0
- package/agent-template/.ai/references/catalog/src/client/CatalogHistoryDrawer.tsrx +123 -0
- package/agent-template/.ai/references/catalog/src/client/CatalogItemForm.tsrx +190 -0
- package/agent-template/.ai/references/catalog/src/client/CatalogView.tsrx +473 -0
- package/agent-template/.ai/references/catalog/src/client/api.ts +111 -0
- package/agent-template/.ai/references/catalog/src/client/contribution.tsrx +61 -0
- package/agent-template/.ai/references/catalog/src/client/index.ts +18 -0
- package/agent-template/.ai/references/catalog/src/client/navigation-copy.ts +9 -0
- package/agent-template/.ai/references/catalog/src/client/state.ts +24 -0
- package/agent-template/.ai/references/catalog/src/domain/types.ts +32 -0
- package/agent-template/.ai/references/catalog/src/domain/variables.ts +111 -0
- package/agent-template/.ai/references/catalog/src/index.ts +31 -0
- package/agent-template/.ai/references/catalog/src/platform.ts +35 -0
- package/agent-template/.ai/references/catalog/src/server/index.ts +4 -0
- package/agent-template/.ai/references/catalog/src/server/runtime.ts +86 -0
- package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +306 -0
- package/agent-template/.ai/references/catalog/src/services/database-repository.ts +440 -0
- package/agent-template/.ai/references/catalog/src/services/index.ts +4 -0
- package/agent-template/.ai/references/catalog/src/services/migration.ts +171 -0
- package/agent-template/.ai/references/catalog/src/services/repository.ts +36 -0
- package/agent-template/.ai/references/catalog/src/services/target-idempotency.ts +59 -0
- package/agent-template/.ai/references/catalog/tests/agent-tools.test.ts +277 -0
- package/agent-template/.ai/references/catalog/tests/endpoints.test.ts +320 -0
- package/agent-template/.ai/references/catalog/tests/idempotency.test.ts +297 -0
- package/agent-template/.ai/references/catalog/tests/migrations.test.ts +149 -0
- package/agent-template/.ai/references/catalog/tests/module.test.ts +271 -0
- package/agent-template/.ai/references/catalog/tests/support/database.ts +76 -0
- package/agent-template/.ai/references/catalog/translations/en.json +101 -0
- package/agent-template/.ai/references/catalog/translations/pl.json +101 -0
- package/agent-template/.ai/references/catalog/tsconfig.json +15 -0
- package/agent-template/.ai/references/catalog/vitest.config.ts +16 -0
- package/agent-template/.ai/references/catalog.provenance.json +55 -0
- package/agent-template/.ai/rules/flowdular.md +86 -0
- package/agent-template/.ai/skills/README.md +36 -0
- package/agent-template/.ai/skills/agent-tool-design/SKILL.md +209 -0
- package/agent-template/.ai/skills/auth-security-review/SKILL.md +96 -0
- package/agent-template/.ai/skills/auto-review/SKILL.md +112 -0
- package/agent-template/.ai/skills/bug-hunt/SKILL.md +110 -0
- package/agent-template/.ai/skills/business-agent-design/SKILL.md +188 -0
- package/agent-template/.ai/skills/cli-extension/SKILL.md +114 -0
- package/agent-template/.ai/skills/core-extend/SKILL.md +104 -0
- package/agent-template/.ai/skills/database-adapter/SKILL.md +204 -0
- package/agent-template/.ai/skills/database-adapter/references/first-run-and-matrix.md +105 -0
- package/agent-template/.ai/skills/migration-authoring/SKILL.md +167 -0
- package/agent-template/.ai/skills/module-new/SKILL.md +180 -0
- package/agent-template/.ai/skills/module-update/SKILL.md +100 -0
- package/agent-template/.ai/skills/perf-audit/SKILL.md +105 -0
- package/agent-template/.ai/skills/release-eject-pr/SKILL.md +113 -0
- package/agent-template/.ai/skills/spec-approval/SKILL.md +112 -0
- package/agent-template/.ai/skills/test-hardening/SKILL.md +86 -0
- package/agent-template/.ai/skills/translations-i18n/SKILL.md +85 -0
- package/agent-template/.ai/skills/ux-design/SKILL.md +97 -0
- package/agent-template/.ai/skills/variables/SKILL.md +164 -0
- package/agent-template/.ai/skills/workflow-development/SKILL.md +199 -0
- package/agent-template/.claude/skills/agent-tool-design/SKILL.md +203 -0
- package/agent-template/.claude/skills/auth-security-review/SKILL.md +90 -0
- package/agent-template/.claude/skills/auto-review/SKILL.md +103 -0
- package/agent-template/.claude/skills/bug-hunt/SKILL.md +104 -0
- package/agent-template/.claude/skills/business-agent-design/SKILL.md +182 -0
- package/agent-template/.claude/skills/cli-extension/SKILL.md +108 -0
- package/agent-template/.claude/skills/core-extend/SKILL.md +99 -0
- package/agent-template/.claude/skills/database-adapter/SKILL.md +198 -0
- package/agent-template/.claude/skills/database-adapter/references/first-run-and-matrix.md +105 -0
- package/agent-template/.claude/skills/migration-authoring/SKILL.md +161 -0
- package/agent-template/.claude/skills/module-new/SKILL.md +171 -0
- package/agent-template/.claude/skills/module-update/SKILL.md +91 -0
- package/agent-template/.claude/skills/perf-audit/SKILL.md +98 -0
- package/agent-template/.claude/skills/release-eject-pr/SKILL.md +107 -0
- package/agent-template/.claude/skills/spec-approval/SKILL.md +106 -0
- package/agent-template/.claude/skills/test-hardening/SKILL.md +79 -0
- package/agent-template/.claude/skills/translations-i18n/SKILL.md +78 -0
- package/agent-template/.claude/skills/ux-design/SKILL.md +92 -0
- package/agent-template/.claude/skills/variables/SKILL.md +156 -0
- package/agent-template/.claude/skills/workflow-development/SKILL.md +192 -0
- package/agent-template/AGENTS.md +77 -0
- package/agent-template/CLAUDE.md +77 -0
- package/agent-template/docs/adr/0001-development-reload.md +16 -0
- package/agent-template/docs/adr/0002-durable-agent-execution.md +21 -0
- package/agent-template/docs/adr/0003-module-settings.md +22 -0
- package/agent-template/docs/adr/0004-enterprise-access-and-audit.md +36 -0
- package/agent-template/docs/adr/0005-sandbox-runtime-and-coding-agents.md +81 -0
- package/agent-template/docs/adr/0006-agentic-workflows.md +1702 -0
- package/agent-template/docs/adr/0007-module-owned-agents.md +429 -0
- package/agent-template/docs/adr/0008-database-adapter-contract.md +90 -0
- package/agent-template/docs/agent-contract.md +45 -0
- package/agent-template/docs/configuration.md +122 -0
- package/agent-template/docs/database-adapters.md +346 -0
- package/agent-template/docs/design-system.md +217 -0
- package/agent-template/docs/modules.md +146 -0
- package/agent-template/platform/scripts/build.mjs +38 -0
- package/agent-template/rulesync.jsonc +11 -0
- package/dist/bin.js +3 -1
- package/package.json +3 -2
- package/template/default/.prettierignore +9 -0
- package/template/default/README.md +12 -0
- package/template/default/flowdular.json +3 -3
- package/template/default/modules/example/package.json +1 -1
- package/template/default/package.json +6 -2
- package/template/default/platform/octane.config.ts +17 -6
- package/template/default/platform/package.json +3 -2
- package/template/default/platform/scripts/dev.mjs +39 -6
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Adapter setup and delivery matrix
|
|
2
|
+
|
|
3
|
+
Read this reference when adding a driver adapter, exposing database setup, or
|
|
4
|
+
preparing an adapter-backed module for eject or a pull request.
|
|
5
|
+
|
|
6
|
+
## Adapter descriptor
|
|
7
|
+
|
|
8
|
+
A platform adapter descriptor owns the complete first-run contract:
|
|
9
|
+
|
|
10
|
+
- an open `adapterId` and `dialectId`;
|
|
11
|
+
- a machine-readable configuration schema;
|
|
12
|
+
- UI hints such as labels, descriptions, groups, order, and input kind;
|
|
13
|
+
- an explicit list of secret fields;
|
|
14
|
+
- configuration validation without opening module databases;
|
|
15
|
+
- a connectivity probe with bounded time and sanitized errors;
|
|
16
|
+
- optional provisioning for infrastructure the adapter actually owns;
|
|
17
|
+
- connection creation that returns the shared adapter contract;
|
|
18
|
+
- advertised database capabilities.
|
|
19
|
+
|
|
20
|
+
The registry is open. Core code resolves a descriptor by id and negotiates its
|
|
21
|
+
capabilities rather than switching over known adapter ids, so an adapter package
|
|
22
|
+
can join without changing module business ports or the setup screen.
|
|
23
|
+
|
|
24
|
+
Configuration UI is generated by the platform from descriptor metadata. A
|
|
25
|
+
module never implements database setup UI and never asks for a DSN.
|
|
26
|
+
|
|
27
|
+
## First run
|
|
28
|
+
|
|
29
|
+
The embedded PostgreSQL is the zero-configuration local default: PGlite runs in
|
|
30
|
+
process, so a workstation starts with nothing installed and still exercises the
|
|
31
|
+
same forced row-level security a deployment enforces. Its data root comes from
|
|
32
|
+
the platform provider, not from each module.
|
|
33
|
+
|
|
34
|
+
Before selecting another adapter, setup:
|
|
35
|
+
|
|
36
|
+
1. validates descriptor configuration and required secrets;
|
|
37
|
+
2. probes the target with a strict timeout and no secret echo;
|
|
38
|
+
3. lists every enabled database-owning module;
|
|
39
|
+
4. verifies that each module declares an implementation for the target
|
|
40
|
+
`dialectId` and that the adapter supplies its required capabilities;
|
|
41
|
+
5. refuses activation with the exact incompatible modules and capabilities;
|
|
42
|
+
6. provisions only after explicit operator confirmation when provisioning
|
|
43
|
+
changes external state;
|
|
44
|
+
7. runs migrations through a migration lease before runtime leases are served;
|
|
45
|
+
8. verifies readiness under the non-bypass runtime role.
|
|
46
|
+
|
|
47
|
+
Secret values live in environment-backed or encrypted platform secret storage.
|
|
48
|
+
`flowdular.json`, module manifests, setup responses, logs, audit metadata, run
|
|
49
|
+
snapshots, and pull request descriptions contain only secret references or
|
|
50
|
+
redacted presence state.
|
|
51
|
+
|
|
52
|
+
## Switching an adapter with existing data
|
|
53
|
+
|
|
54
|
+
Changing an adapter id never points modules at an empty database and never
|
|
55
|
+
copies storage files. The only supported switch is an explicit operation:
|
|
56
|
+
|
|
57
|
+
1. stop writes and acquire an export snapshot;
|
|
58
|
+
2. export through module-owned portable records, preserving ids and versions;
|
|
59
|
+
3. provision and migrate the target;
|
|
60
|
+
4. import in dependency-safe batches under tenant context;
|
|
61
|
+
5. verify counts, checksums, referential expectations, and module invariants;
|
|
62
|
+
6. probe reads and writes through the target runtime role;
|
|
63
|
+
7. activate the target only after verification succeeds;
|
|
64
|
+
8. retain the source for rollback until the operator closes the window.
|
|
65
|
+
|
|
66
|
+
A failed import or verification leaves the source active. There is no silent
|
|
67
|
+
fallback to another database, because that would split writes.
|
|
68
|
+
|
|
69
|
+
## Module declarations
|
|
70
|
+
|
|
71
|
+
Each database-owning module declares:
|
|
72
|
+
|
|
73
|
+
- supported open dialect ids;
|
|
74
|
+
- the required capability set, such as transactions, transactional DDL,
|
|
75
|
+
schema introspection, returning values, or an isolation level;
|
|
76
|
+
- its module-owned persistence implementation and migrations;
|
|
77
|
+
- one adapter-neutral repository behavior suite.
|
|
78
|
+
|
|
79
|
+
The platform compatibility check consumes these declarations before first run,
|
|
80
|
+
adapter change, sandbox eject, and deployment validation.
|
|
81
|
+
|
|
82
|
+
## Delivery matrix
|
|
83
|
+
|
|
84
|
+
Every turn runs against a real PostgreSQL, because the embedded one starts in
|
|
85
|
+
process. The sandbox and the test suites use `createTestDatabaseProvider()` from
|
|
86
|
+
`@flowdular/sdk/database-testing`, which brings the `coreloom_runtime` and
|
|
87
|
+
`coreloom_background` roles and forced row-level security with it.
|
|
88
|
+
|
|
89
|
+
A target run covers tenant A and B fixtures, operations without tenant context,
|
|
90
|
+
forged cross-tenant inserts, direct row-security bypass probes, concurrent
|
|
91
|
+
writes, and cleanup. Where a module polls across tenants, it also proves the
|
|
92
|
+
background role reads only the routing columns.
|
|
93
|
+
|
|
94
|
+
Before eject or a pull request the matrix additionally runs the same suite
|
|
95
|
+
against a server PostgreSQL when one is configured, using a unique schema and a
|
|
96
|
+
role that holds neither `SUPERUSER` nor `BYPASSRLS`. Test credentials are
|
|
97
|
+
short-lived and never enter the transcript. If that target is unavailable the
|
|
98
|
+
session may continue editing, and delivery states the exact missing target.
|
|
99
|
+
|
|
100
|
+
Server selection is explicit: `FD_TEST_DATABASE_ADAPTER=postgresql`,
|
|
101
|
+
`FD_TEST_POSTGRES_URL` (migrator), `FD_TEST_POSTGRES_RUNTIME_URL` and
|
|
102
|
+
`FD_TEST_POSTGRES_BACKGROUND_URL`. All three URLs are required. Missing or
|
|
103
|
+
unreachable configuration never falls back to PGlite. `.github/workflows/ci.yml`
|
|
104
|
+
runs this matrix, including auth and agents. Keep role separation intact in
|
|
105
|
+
fixtures: even the migration owner must supply tenant context for tenant data.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: migration-authoring
|
|
3
|
+
description: >-
|
|
4
|
+
Add an immutable PostgreSQL module migration through @flowdular/sdk/database,
|
|
5
|
+
with the namespaced v2 ledger, safe schema adoption, forced row-level
|
|
6
|
+
security, and tenant isolation tests.
|
|
7
|
+
---
|
|
8
|
+
# Author a database migration
|
|
9
|
+
|
|
10
|
+
An adapter conversion is a separate phase using `database-adapter`. For this phase, read
|
|
11
|
+
`packages/database/src/migrations.ts` and the converted `modules/profile`
|
|
12
|
+
migration as the reference. Flowdular has a migration runner. Do not add
|
|
13
|
+
constructor-owned `DatabaseSync.exec()` guards around it.
|
|
14
|
+
|
|
15
|
+
## 1. Source layout and compatibility
|
|
16
|
+
|
|
17
|
+
Migration SQL is checked in and immutable after release:
|
|
18
|
+
|
|
19
|
+
- `migrations/NNNN_<module>_<name>.up.sql` holds the PostgreSQL source and is
|
|
20
|
+
byte-exact. Never move, renumber or reformat a released file.
|
|
21
|
+
- `src/services/migration.ts` mirrors every `.up.sql` file as a literal and
|
|
22
|
+
exports `databaseMigrations: readonly DatabaseMigration[]`, one entry per
|
|
23
|
+
file. No code translates or rewrites SQL.
|
|
24
|
+
- `.down.sql` documents a reverse operation. Flowdular never executes it.
|
|
25
|
+
|
|
26
|
+
## 2. What the v2 runner guarantees
|
|
27
|
+
|
|
28
|
+
`runDatabaseMigrations(database, namespace, databaseMigrations)` uses the
|
|
29
|
+
namespaced `_coreloom_migrations_v2` ledger. A row records namespace, migration
|
|
30
|
+
id, dialect id, checksum, and applied time. The checksum covers the selected
|
|
31
|
+
dialect's exact SQL.
|
|
32
|
+
|
|
33
|
+
Before applying outstanding work, the runner checks every existing ledger row.
|
|
34
|
+
It refuses checksum drift, a missing script, duplicate ids, a wrong ledger
|
|
35
|
+
dialect, partial adoption, and adapters without transactional DDL. It opens one
|
|
36
|
+
serializable transaction, takes a transaction advisory lock, and commits DDL
|
|
37
|
+
plus ledger rows together. Dry run writes nothing.
|
|
38
|
+
|
|
39
|
+
The runner selects scripts by the provider's open `dialectId`. Do not add a core
|
|
40
|
+
switch over known dialects. A new driver advertises capabilities and a module
|
|
41
|
+
opts into it by supplying reviewed SQL and tests.
|
|
42
|
+
|
|
43
|
+
## 3. Explicit adoption proof
|
|
44
|
+
|
|
45
|
+
Every migration that may predate v2 defines `inspectExisting(database)`. Use
|
|
46
|
+
adapter-owned, capability-checked schema introspection such as `hasTable`,
|
|
47
|
+
`hasColumn`, and `hasIndex` with fixed identifiers.
|
|
48
|
+
|
|
49
|
+
Return `complete` only when every table, column, index, constraint, data effect,
|
|
50
|
+
and security policy exists. Return `absent` only when none exists. Return
|
|
51
|
+
`partial` for every mixed state. If an adapter cannot inspect a required object,
|
|
52
|
+
extend its introspection capability or supply a narrow module-owned inspection.
|
|
53
|
+
Never guess `complete` and never parse another dialect's catalog directly in
|
|
54
|
+
shared code.
|
|
55
|
+
|
|
56
|
+
## 4. SQL rules shared by dialects
|
|
57
|
+
|
|
58
|
+
- Tenant tables carry `tenant_id TEXT NOT NULL`.
|
|
59
|
+
- Tenant uniqueness and lookup indexes start with `tenant_id`; ordered indexes
|
|
60
|
+
end with `id` for stable results.
|
|
61
|
+
- IDs are text UUIDs generated by the service. Times are integer milliseconds.
|
|
62
|
+
- Money is integer minor units plus a currency code, never floating point.
|
|
63
|
+
- Enums and ranges have database check constraints.
|
|
64
|
+
- Cross-module foreign keys do not exist. Use the owner's capability or API.
|
|
65
|
+
- Additive changes are the default. Destructive or locking changes need an
|
|
66
|
+
operator-approved rollout, compatibility window, and rollback plan.
|
|
67
|
+
|
|
68
|
+
Write PostgreSQL directly: its types, conflict syntax, indexes and policies.
|
|
69
|
+
Never rewrite placeholders or DDL text.
|
|
70
|
+
|
|
71
|
+
## 5. PostgreSQL row-level security
|
|
72
|
+
|
|
73
|
+
Every PostgreSQL tenant table includes:
|
|
74
|
+
|
|
75
|
+
```sql
|
|
76
|
+
ALTER TABLE inventory_locations ENABLE ROW LEVEL SECURITY;
|
|
77
|
+
ALTER TABLE inventory_locations FORCE ROW LEVEL SECURITY;
|
|
78
|
+
CREATE POLICY inventory_locations_tenant_policy ON inventory_locations
|
|
79
|
+
USING (tenant_id = current_setting('coreloom.tenant_id', true))
|
|
80
|
+
WITH CHECK (tenant_id = current_setting('coreloom.tenant_id', true));
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The runtime role is not a superuser and has no `BYPASSRLS`. DDL and policy
|
|
84
|
+
ownership use `purpose: 'migration'`. Runtime repository calls use
|
|
85
|
+
`database.transaction(operation, { tenantId, access })`; the adapter sets
|
|
86
|
+
transaction-local `coreloom.tenant_id` on the pinned connection. Queries still
|
|
87
|
+
include `WHERE tenant_id = ...` as defense in depth.
|
|
88
|
+
|
|
89
|
+
## 6. Add one migration
|
|
90
|
+
|
|
91
|
+
Scaffold instead of writing from memory:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pnpm flowdular migration new <name> --module <id> # dry run
|
|
95
|
+
pnpm flowdular migration new <name> --module <id> --apply # writes both files
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The scaffold emits the `.up.sql` and `.down.sql` pair with the tenant table, its
|
|
99
|
+
index, and the `ENABLE` + `FORCE` + policy block already correct. Replace the
|
|
100
|
+
placeholder columns with the real schema; keep the row-security block unless the
|
|
101
|
+
table is not tenant owned.
|
|
102
|
+
|
|
103
|
+
1. Never renumber released files. The scaffold picks the next id.
|
|
104
|
+
2. Mirror every `.up.sql` byte for byte and append one `DatabaseMigration` with
|
|
105
|
+
the same id.
|
|
106
|
+
3. Add exact `inspectExisting` logic with `migrationObjectState` and
|
|
107
|
+
`postgresTenantTableState` from `@flowdular/sdk/database`. Pass thunks, never
|
|
108
|
+
ready promises: adoption runs inside a transaction pinned to one connection,
|
|
109
|
+
and eager promises issue overlapping queries on it.
|
|
110
|
+
4. Extend repository SQL, row mapping, service validation, endpoint, client,
|
|
111
|
+
approved spec scenario, and all three module versions.
|
|
112
|
+
|
|
113
|
+
### Column types that bite
|
|
114
|
+
|
|
115
|
+
PostgreSQL returns `BIGINT` as a string. Normalize every integer read in the
|
|
116
|
+
repository through a local `integer()` helper; a raw value concatenates where it
|
|
117
|
+
should add, and `enabled === 1` is false against `'1'`.
|
|
118
|
+
|
|
119
|
+
Widen timestamps, durations and sequences to `BIGINT`. Leave boolean flags,
|
|
120
|
+
counters and version columns `INTEGER`, or every comparison against them has to
|
|
121
|
+
be normalized too.
|
|
122
|
+
|
|
123
|
+
Money is integer minor units in `BIGINT` plus a currency code.
|
|
124
|
+
|
|
125
|
+
Never test against a workspace database. `createTestDatabaseProvider()` from
|
|
126
|
+
`@flowdular/sdk/database-testing` gives the suite its own PGlite by default or an
|
|
127
|
+
isolated PostgreSQL schema in server CI. Tenant fixture reads and writes still
|
|
128
|
+
need transaction-local tenant context, including on the migration connection.
|
|
129
|
+
|
|
130
|
+
## 7. Tests and gates
|
|
131
|
+
|
|
132
|
+
`pnpm flowdular migration verify` checks the ledger and every module's migration
|
|
133
|
+
set: a missing script, a tenant table without forced row security, and a policy
|
|
134
|
+
lost to a table rebuild.
|
|
135
|
+
|
|
136
|
+
The migration suite proves byte parity and id order, empty apply, complete
|
|
137
|
+
adoption without row loss, partial refusal, checksum refusal before later SQL,
|
|
138
|
+
dry run without writes, and a clean second start.
|
|
139
|
+
|
|
140
|
+
Every gate runs against a real PostgreSQL, because the embedded one starts in
|
|
141
|
+
process. Cover tenant A and B isolation, a forged tenant `WITH CHECK` refusal, a
|
|
142
|
+
missing-context `TENANT_CONTEXT_REQUIRED` test, and direct row-security bypass
|
|
143
|
+
probes under a role that holds neither `SUPERUSER` nor `BYPASSRLS`. Where the
|
|
144
|
+
module polls across tenants, prove the background role reads only the routing
|
|
145
|
+
columns.
|
|
146
|
+
|
|
147
|
+
## Refuse
|
|
148
|
+
|
|
149
|
+
- Editing, moving, reordering, or removing released migration bytes.
|
|
150
|
+
- A PostgreSQL tenant table without enabled and forced RLS plus both policy
|
|
151
|
+
clauses.
|
|
152
|
+
- A runtime role with superuser or `BYPASSRLS`.
|
|
153
|
+
- DDL through a runtime lease or request data through `executeScript()`.
|
|
154
|
+
- Automatic SQL translation or a closed core switch over known dialects.
|
|
155
|
+
- Forcing past checksum or partial-adoption refusal.
|
|
156
|
+
- A production-support claim based on a fake driver alone.
|
|
157
|
+
|
|
158
|
+
## Verification
|
|
159
|
+
|
|
160
|
+
Run the module typecheck and tests, module validation,
|
|
161
|
+
`pnpm flowdular migration verify`, and `pnpm verify`.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: module-new
|
|
3
|
+
description: >-
|
|
4
|
+
Create a Flowdular module from an approved spec, from scaffold to enabled and
|
|
5
|
+
granted, with the file set and APIs .ai/references/catalog uses.
|
|
6
|
+
---
|
|
7
|
+
# Create a module
|
|
8
|
+
|
|
9
|
+
The reference module is `.ai/references/catalog` (in a sandbox session: `reference/example-module`). When this skill and the code disagree, the code wins; tell the operator.
|
|
10
|
+
|
|
11
|
+
Two ways to land the same module: the sandbox (a brief, specialist turns, gates after every turn, preview, eject) or the direct path (this skill in your own coding tool, the gates by hand, `pnpm verify`, a pull request). The sections below mark the differences.
|
|
12
|
+
|
|
13
|
+
## 1. Preconditions
|
|
14
|
+
|
|
15
|
+
- `pnpm flowdular doctor --json` reports `status: healthy` (repository root only; the sandbox runs gates for you).
|
|
16
|
+
- `modules/<dir>/spec/module.yaml` exists, validates (`pnpm flowdular spec validate --all --json`) and has `status: approved`. `pnpm flowdular module new` refuses a draft with `A module can only be created from an approved specification.` In the sandbox the operator approves the spec after the business manager's turn, and the orchestrator runs the scaffold itself. Outside the sandbox, a host agent may record approval only after an explicit current user request and only through `spec-approval`.
|
|
17
|
+
- Ids: module id and every permission id match `^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$`. `inventory.core` lives in `modules/inventory` as `@flowdular/module-inventory` (`packages/cli/src/module-scaffold.ts`, `packageSuffix`). Module environment variables use the upper-case directory (`modules/auth` reads `FD_AUTH_SECURE_COOKIE`); the database is platform-owned, so a module never gets one of its own.
|
|
18
|
+
|
|
19
|
+
## 2. Scaffold
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm flowdular module new inventory.core --spec modules/inventory/spec/module.yaml # dry run, lists files
|
|
23
|
+
pnpm flowdular module new inventory.core --spec modules/inventory/spec/module.yaml --apply
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`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>`).
|
|
27
|
+
|
|
28
|
+
The scaffold is PostgreSQL from the first commit. `database-repository.ts`
|
|
29
|
+
exports `Database<Name>Repository` and `migrate<Name>Database`, takes a
|
|
30
|
+
`DatabaseHandle`, wraps every async method in
|
|
31
|
+
`database.transaction(..., { tenantId, access })`, binds `$1`, `$2` parameters,
|
|
32
|
+
and normalizes integer columns through a local `integer()` helper because
|
|
33
|
+
PostgreSQL returns `BIGINT` as a string. `migrations/0001_<snake>_core.up.sql`
|
|
34
|
+
creates the table with forced row-level security and a tenant policy. The
|
|
35
|
+
runtime takes `context.databases`, acquires a `migration` lease, runs the
|
|
36
|
+
migrations, releases it, then acquires the runtime lease requiring
|
|
37
|
+
`DATABASE_DIALECT_IDS.postgresql`. `tests/module.test.ts` runs on
|
|
38
|
+
`createPgliteTestProvider()` and asserts tenant isolation. The business repository port
|
|
39
|
+
stays async and database-agnostic, and persistence stays on
|
|
40
|
+
`@flowdular/sdk/database`.
|
|
41
|
+
|
|
42
|
+
The first entity is the middle segment of the first permission id (`inventory.locations.read` gives `locations`, table `inventory_locations`, type `InventoryLocation`); it gets the list endpoint (`.read`), the create endpoint (`.manage`), the table, the view and the tests. Every other permission becomes a constant in `X_PERMISSIONS` only; its entity is `module-update` work. A directory that already holds `spec/module.yaml` and `translations/**` (the business manager's files) is extended, and those files win over the scaffold's; a directory with sources is refused. In the sandbox the orchestrator runs the scaffold once the spec is approved.
|
|
43
|
+
|
|
44
|
+
What is still yours after the scaffold: the real fields of the entity beyond `name`, validation bounds and stable error codes, uniqueness rules and their indexes, further endpoints and entities, screen columns and the drawer form, tests beyond identity and isolation. A business agent is a separate phase using `business-agent-design`; the scaffold does not invent one.
|
|
45
|
+
|
|
46
|
+
## 3. Server file set
|
|
47
|
+
|
|
48
|
+
Copy the HTTP and domain layout of `.ai/references/catalog/src`, and the persistence shape of `modules/profile/src/services/database-repository.ts`. Keep three layers: an async database-agnostic repository port, module-owned persistence and PostgreSQL SQL, and the platform-owned driver provider. Exact endpoint signatures:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// src/api/endpoints.ts
|
|
52
|
+
import {
|
|
53
|
+
defineEndpoint,
|
|
54
|
+
HttpProblem,
|
|
55
|
+
jsonResponse,
|
|
56
|
+
problemResponse,
|
|
57
|
+
readJsonObject,
|
|
58
|
+
requiredInteger,
|
|
59
|
+
requiredString,
|
|
60
|
+
} from '@flowdular/sdk/server';
|
|
61
|
+
import type { AuthRuntime } from '@flowdular/sdk/modules/auth/server';
|
|
62
|
+
import {
|
|
63
|
+
endpointIdentityFromContext,
|
|
64
|
+
principalFromContext,
|
|
65
|
+
sessionMutationDenial,
|
|
66
|
+
} from '@flowdular/sdk/modules/auth/server';
|
|
67
|
+
|
|
68
|
+
const create = defineEndpoint({
|
|
69
|
+
id: 'inventory.locations.create',
|
|
70
|
+
path: '/api/inventory/locations',
|
|
71
|
+
methods: ['POST'],
|
|
72
|
+
access: { kind: 'permission', permission: INVENTORY_PERMISSIONS.manage },
|
|
73
|
+
resolveIdentity: endpointIdentityFromContext,
|
|
74
|
+
handler: async ({ octane }) => {
|
|
75
|
+
const denial = sessionMutationDenial(octane, auth);
|
|
76
|
+
if (denial) return denial;
|
|
77
|
+
try {
|
|
78
|
+
const value = await readJsonObject(octane.request);
|
|
79
|
+
const input = { code: requiredString(value, 'code', { max: 32 }) };
|
|
80
|
+
const tenantId = principalFromContext(octane)!.tenantId;
|
|
81
|
+
return jsonResponse(
|
|
82
|
+
{ location: await runtime.service().create(tenantId, input) },
|
|
83
|
+
201,
|
|
84
|
+
);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return failure(error);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`createXRoutes(auth: AuthRuntime, runtime: XRuntime)` returns `[list.serverRoute, create.serverRoute] as const`. `src/platform.ts` (the scaffold writes this; extend it):
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import type {
|
|
96
|
+
PlatformServerComposition,
|
|
97
|
+
PlatformServerContext,
|
|
98
|
+
} from '@flowdular/sdk/modules/auth/server';
|
|
99
|
+
export function createServerComposition(
|
|
100
|
+
context: PlatformServerContext,
|
|
101
|
+
): PlatformServerComposition {
|
|
102
|
+
const runtime = createInventoryRuntime({ databases: context.databases });
|
|
103
|
+
return {
|
|
104
|
+
routes: createInventoryRoutes(context.auth, runtime),
|
|
105
|
+
dispose: () => runtime.dispose(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`PlatformServerContext` also carries `databases: DatabaseProvider`, `settings: ModuleSettingsRuntime`, `agentTools: PlatformToolRegistry`, `agentDefinitions: PlatformAgentRegistry`, and `capabilities: PlatformCapabilityRegistry`. The runtime shares one lazy database initialization, uses separate migration and runtime leases, and releases the runtime lease from `dispose()`; `prepare()` remains read-only. A composition may return module settings, lifecycle hooks, agent registrations, and typed cross-module capabilities as described in the focused skills.
|
|
111
|
+
|
|
112
|
+
Schema: write PostgreSQL SQL in `migrations/0001_inventory_core.up.sql` and mirror it byte for byte in `databaseMigrations` as `sql: { postgresql: ... }` with an `inspectExisting` built from `postgresTenantTableState(...)`. Tenant tables enable and force row-level security with an `<table>_tenant_policy` whose `USING` and `WITH CHECK` compare `tenant_id` with `current_setting('coreloom.tenant_id', true)`; the runtime role has no superuser or `BYPASSRLS`. Repository operations use `database.transaction(..., { tenantId, access })` and retain explicit tenant predicates. Details in `migration-authoring` and `database-adapter`.
|
|
113
|
+
|
|
114
|
+
Errors: `{ error: { code, message } }`; service errors `class XServiceError extends Error { constructor(readonly code: string, message: string, readonly status = 400) }`; a `failure(error)` helper routes them to `jsonResponse(..., error.status)` and everything else to `problemResponse(error, 'The <module> operation failed.')`.
|
|
115
|
+
|
|
116
|
+
## 4. Client file set
|
|
117
|
+
|
|
118
|
+
`src/client/{index.ts,contribution.tsrx,api.ts,state.ts,XView.tsrx,XForm.tsrx}`. The canonical entry:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// src/client/index.ts
|
|
122
|
+
import type {
|
|
123
|
+
ModuleClientContext,
|
|
124
|
+
ModuleClientContribution,
|
|
125
|
+
} from '@flowdular/sdk/client';
|
|
126
|
+
import { createInventoryClientContribution as canonicalContribution } from './contribution.tsrx';
|
|
127
|
+
export function createClientContribution(
|
|
128
|
+
context: ModuleClientContext,
|
|
129
|
+
): ModuleClientContribution {
|
|
130
|
+
return canonicalContribution({ csrfToken: context.csrfToken });
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Contribution rules (`packages/client/src/contributions.ts`): `navigation[].group` in `Workspace`, `Operations`, `Agents`, `Administration`, `Development` (`Agents` only with an `agents.core` dependency, `Development` is owner-only in the shell); `widgets[].slot` in `WORKSPACE_SLOTS` (`dashboard.metrics`, `dashboard.main`, `dashboard.aside`, `topbar.actions`); `glyph` an `ICON_PATHS` key (`packages/ui/src/icons/Icon.tsrx`, list in `ux-design`); ids `<module>.navigation`, `<module>.dashboard.<name>`, view id equals the URL slug; `accountMenu` for personal screens (`modules/profile`). Duplicate ids or a navigation entry pointing at a missing view throw at boot.
|
|
135
|
+
|
|
136
|
+
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`.
|
|
137
|
+
|
|
138
|
+
## 5. Tests and local gates
|
|
139
|
+
|
|
140
|
+
`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:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pnpm --filter @flowdular/module-inventory typecheck # tsrx-tsc --noEmit -p tsconfig.json
|
|
144
|
+
pnpm --filter @flowdular/module-inventory test # vitest run
|
|
145
|
+
pnpm flowdular module validate --json
|
|
146
|
+
pnpm flowdular spec validate --all --json
|
|
147
|
+
pnpm format:check
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`module validate` (`packages/cli/src/module-validate.ts`) also checks the composition contract: `PLATFORM_SERVER_ENTRY_MISSING` and `PLATFORM_EXPORT_MISSING` (no `src/platform.ts` or `./platform` export behind `platform.server`), `PLATFORM_CLIENT_ENTRY_MISSING` and `PLATFORM_CLIENT_EXPORT_MISSING`, `PACKAGE_NAME_MISMATCH`, `SPEC_ID_MISMATCH`, `TRANSLATION_FILE_MISSING`, `TRANSLATION_KEYS_MISMATCH`, and the warnings `SPEC_VERSION_DRIFT` and `LOCALE_NOT_IN_PROJECT`.
|
|
151
|
+
|
|
152
|
+
Sandbox gates (`packages/sandbox/src/server/gates.ts`): `spec-schema` and `module-schema` once per session workspace; `dependencies`, `typecheck`, `tests` (`vitest run --passWithNoTests`, so no tests still passes) and `format` (`prettier --check .`) once per draft module with the module's own binaries. The session workspace is a real pnpm workspace that installs what each draft `package.json` declares, so an undeclared import fails the `dependencies` gate, which runs after every turn that changed files. A driver with a shell may run the same commands from the module directory; the sandbox runs them again after the turn and feeds a failure back into the fix prompt.
|
|
153
|
+
|
|
154
|
+
## 6. Enable
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pnpm flowdular module enable inventory.core --apply
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
One command (`packages/cli/src/runner.ts`, `module enable`): adds the id to `flowdular.json` `modules.enabled`, adds the package to `platform/package.json`, runs `pnpm install` when the package is not linked, regenerates `platform/src/generated/modules.{server,client}.ts`, and then runs `auth sync-scopes` for the module, reporting the grant as `scopes` in the result; a failed grant is `MODULE_SCOPES_SYNC_FAILED` with the module already enabled. Never edit those files by hand. `pnpm flowdular auth sync-scopes --module inventory.core --apply` stays the way to re-grant later (a new permission, a new owner, a deployment database): it grants the spec's `permissions[].id` to the owners of every tenant (`modules/auth/src/services/auth-service.ts`, `grantModuleScopes`). Members never receive new scopes automatically; a module bundled with the platform also adds its scopes to `BUNDLED_MODULE_SCOPES` and, for read scopes, `MEMBER_SCOPES` in `modules/auth/src/acl/scopes.ts` (a core change). The sandbox eject runs enable for each new module and sync-scopes for each module (`packages/sandbox/src/server/delivery/local.ts`).
|
|
161
|
+
|
|
162
|
+
## Pitfalls
|
|
163
|
+
|
|
164
|
+
- 415 on every POST: the client did not send `content-type: application/json` (`readJsonObject`).
|
|
165
|
+
- 403 `CSRF_REJECTED` or `ORIGIN_REQUIRED`: `x-csrf-token` missing or the request is not same-origin.
|
|
166
|
+
- Module enabled but no navigation: scopes not granted, or `platform.client` missing.
|
|
167
|
+
- Routes 404: `platform.server` missing, no `./platform` export, or `src/platform.ts` absent; `pnpm flowdular module validate` names it (`PLATFORM_*`).
|
|
168
|
+
- `Kpi` typecheck error: `value` must be a string.
|
|
169
|
+
- 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`.
|
|
170
|
+
- Every relative import needs its `.ts` or `.tsrx` extension.
|
|
171
|
+
- `module.json` `version`, `spec` `specVersion` and `package.json` `version` are one number.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: module-update
|
|
3
|
+
description: >-
|
|
4
|
+
Change an existing module (endpoint, table, screen, permission, widget) with
|
|
5
|
+
the fixed touch list per change class and the version bump rules.
|
|
6
|
+
---
|
|
7
|
+
# Update an existing module
|
|
8
|
+
|
|
9
|
+
## 1. Read first
|
|
10
|
+
|
|
11
|
+
Read the whole module before changing it: `spec/module.yaml`, `src/index.ts`, `src/acl/permissions.ts`, `src/api/endpoints.ts`, `src/services/*`, `src/client/*`, `tests/`. Keep every exported name in `src/index.ts`, `src/server/index.ts` and `src/client/index.ts` stable: other modules import them (`modules/users` uses `AuthRuntime` from `@flowdular/sdk/modules/auth/server`), and the generated composition imports `createServerComposition` and `createClientContribution`.
|
|
12
|
+
|
|
13
|
+
Sandbox facts for an edit session (`packages/sandbox/src/server/sessions.ts`): the module is copied to `workspace/modules/<dir>` and a pristine copy to `base/modules/<dir>`; the diff shown to the operator and the eject plan compare the two. The workspace is a pnpm workspace of its own (declared dependencies install for real; a `package.json` change triggers a reinstall that counts as the `dependencies` gate). The business manager updates the spec before implementation. The operator approves the exact spec hash for every affected module; editing that spec, requesting changes, or adding another module reopens its approval gate. A sandbox specialist never writes `status: approved`; a host agent may invoke approval only after an explicit current user request through `spec-approval`. Delivery checks the recorded hash again.
|
|
14
|
+
|
|
15
|
+
## 2. Classify the change and use its touch list
|
|
16
|
+
|
|
17
|
+
Change classes: endpoint, table, column, screen, widget, permission, setting, cross-module read, agent tool, business agent, fix.
|
|
18
|
+
|
|
19
|
+
New endpoint:
|
|
20
|
+
|
|
21
|
+
1. `src/services/<name>-service.ts`: the method with validation and a stable error code.
|
|
22
|
+
2. `src/services/repository.ts` and `database-repository.ts`: the async interface method and SQL with `$1`, `$2` parameters, `WHERE tenant_id = $1` on every tenant-owned query, inside `database.transaction(..., { tenantId, access })`.
|
|
23
|
+
3. `src/api/endpoints.ts`: `defineEndpoint` with `access`, `resolveIdentity: endpointIdentityFromContext`, `sessionMutationDenial(octane, auth)` first on mutations, `readJsonObject` plus `requiredString`/`requiredInteger`/`optionalString`; add the route to the returned tuple and its id to `endpoints`.
|
|
24
|
+
4. `src/client/api.ts`: the fetch (`content-type: application/json`, `x-csrf-token`, `credentials: 'same-origin'`).
|
|
25
|
+
5. `tests/module.test.ts`: service rule tests plus a 401 and a 403 through `route.handler(createContext(...))`, and a tenant isolation case.
|
|
26
|
+
6. `spec/module.yaml`: an acceptance scenario, `specVersion` bump.
|
|
27
|
+
|
|
28
|
+
New column or table:
|
|
29
|
+
|
|
30
|
+
1. Write `migrations/000N_<module>_<name>.up.sql` first and its documented reverse in `.down.sql`. Never edit, reorder, or remove a migration that shipped. A new table uses `CREATE TABLE IF NOT EXISTS`; a new column uses `ALTER TABLE ... ADD COLUMN` once under the ledger.
|
|
31
|
+
2. `src/services/migration.ts`: append `X_MIGRATION_00N` mirroring the `.up.sql` file byte for byte and append its `{ id, sql: { postgresql: X_MIGRATION_00N }, inspectExisting }` entry to `databaseMigrations`. Build `inspectExisting` from `postgresTenantTableState(...)` so a mixed state returns `partial`.
|
|
32
|
+
3. `database-repository.ts`: extend the row interface and `fromRow`, the `INSERT`, `UPDATE` and `SELECT` lists, and the `integer()` normalization for a new integer column. Migrations run from the runtime's `migration` lease, not from the repository. Add the migration tests required by `migration-authoring`.
|
|
33
|
+
4. `src/domain/types.ts`, service, endpoint validation, client form and table column.
|
|
34
|
+
5. Tests against the module's `tests/support/database.ts` provider for the new rule; `spec/module.yaml` invariant or scenario, `specVersion` bump.
|
|
35
|
+
|
|
36
|
+
New permission:
|
|
37
|
+
|
|
38
|
+
1. `spec/module.yaml` `permissions`: the new `{ id, description }`.
|
|
39
|
+
2. `src/acl/permissions.ts`: the constant with the same string.
|
|
40
|
+
3. Endpoint `access.permission` and client `scope` on the navigation entry, widget or `canManage` flag.
|
|
41
|
+
4. After eject or enable: `pnpm flowdular auth sync-scopes --module <id> --apply` grants it to owners. Members and bundled defaults require a core change in `modules/auth/src/acl/scopes.ts` (`BUNDLED_MODULE_SCOPES`, `MEMBER_SCOPES`); say so in the handoff instead of editing another module.
|
|
42
|
+
|
|
43
|
+
New screen or widget:
|
|
44
|
+
|
|
45
|
+
1. `src/client/XView.tsrx` (and `XForm.tsrx` for a drawer) following the pattern in `ux-design`.
|
|
46
|
+
2. `src/client/contribution.tsrx`: a `views` entry, a `navigation` entry with a unique id, `viewId`, `group`, `glyph` from `ICON_PATHS`, `scope`, `order`; or a `widgets` entry with a `WORKSPACE_SLOTS` slot. Widget state is its own store instance.
|
|
47
|
+
3. `src/client/index.ts`: re-export the view.
|
|
48
|
+
4. Add user-facing copy to every declared `translations/*.json` bundle and resolve it with fully qualified `t()` keys. Navigation copy uses getters because contributions exist before bundles are registered.
|
|
49
|
+
|
|
50
|
+
New setting:
|
|
51
|
+
|
|
52
|
+
1. `src/settings.ts`: `export const X_MODULE_SETTINGS = defineModuleSettings({ moduleId: '<module>.core', settings: { key: { type: 'string' | 'number' | 'boolean', defaultValue, visibility: 'private' | 'shared', client: boolean, scope: 'tenant' | 'platform', labelKey, label, descriptionKey, description, min?, max?, enum?, secret? } } })` from `@flowdular/sdk/kernel` (`packages/kernel/src/module-settings.ts`; setting keys match `^[a-z][a-zA-Z0-9]*$`). `labelKey` and `descriptionKey` are fully qualified module translation keys present in every locale. Keep the English literals as compatibility fallbacks; values, ids and secrets are never translated.
|
|
53
|
+
2. `src/platform.ts`: return `settings: X_MODULE_SETTINGS` next to `routes`; the platform declares it at boot and Administration, Modules renders it in the module's drawer (`modules/system/src/client/ModuleSettingsSection.tsrx`, behind `system.settings.read` and `system.settings.manage`; the API is `GET /api/settings` and `POST /api/settings/update` in `modules/system/src/server/endpoints.ts`).
|
|
54
|
+
3. Read it live where it is used: `context.settings.get<number>(tenantId, '<module>.core', 'key')` at request time, never cached at boot; pass `context.settings` into the runtime or service that needs it (`modules/agents/src/settings.ts`, `agentSettings`, shows the pattern with an environment fallback).
|
|
55
|
+
4. `spec/module.yaml`: an invariant or scenario naming the setting and its bounds; `specVersion` bump. Cross-module reads of a setting need `visibility: 'shared'` and a declared dependency.
|
|
56
|
+
|
|
57
|
+
Cross-module read: import the other module's runtime or service type from its public entry (`@flowdular/module-<x>` or `@flowdular/module-<x>/server`), declare `{ "id": "<x>.core", "range": "^0.1.0" }` in `module.json` `dependencies` and the spec, and add the package to `package.json`. Never open its database or import from its `src/` path.
|
|
58
|
+
|
|
59
|
+
Agent tool: use `agent-tool-design` as a separate phase; add the approved scenario, `src/agent/tools.ts`, the `context.agentTools.register(...)` call, target-side idempotency for writes, and denial tests.
|
|
60
|
+
|
|
61
|
+
Business agent: use `business-agent-design` as a separate phase; add the approved behavior and refusal scenarios, declare the `agents.core` module and package dependencies, define it in `src/agent/agents.ts`, and register it with `context.agentDefinitions.register(...)`. A code definition owns behavior and a maximum exact tool allowlist. Provider, model, active state, and the reduced enabled tools remain tenant binding data.
|
|
62
|
+
|
|
63
|
+
## 3. Versions and spec
|
|
64
|
+
|
|
65
|
+
Bump `spec/module.yaml` `specVersion`, `module.json` `version` and `package.json` `version` together (patch for a fix, minor for a new endpoint, screen or column). Add an acceptance scenario for every new behaviour and an invariant for every new rule; the scenario id matches `^[A-Z][A-Z0-9-]+$`. In the sandbox the business manager leaves the changed spec in `draft` or `in-review`; only the operator approval route records the approved hash and permits implementation.
|
|
66
|
+
|
|
67
|
+
## 4. Gates
|
|
68
|
+
|
|
69
|
+
Sandbox: the role's gates run after the turn. Repository root:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pnpm --filter @flowdular/module-<dir> typecheck
|
|
73
|
+
pnpm --filter @flowdular/module-<dir> test
|
|
74
|
+
pnpm flowdular spec validate --all --json
|
|
75
|
+
pnpm flowdular module validate --json
|
|
76
|
+
pnpm format:check
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The `dependencies` gate (sandbox) scans imports under `src/`; declare any new package before you import it.
|
|
80
|
+
|
|
81
|
+
## 5. Landing
|
|
82
|
+
|
|
83
|
+
Sandbox: eject runs the gates per module, copies added and changed files over the workspace copy and removes the files the session deleted (`packages/sandbox/src/server/delivery/steps.ts`, `removeModuleFiles`), runs `pnpm install`, `auth sync-scopes` for the module, and a platform typecheck; any failed step stops the delivery there. `module enable` runs only for a new module. A session may carry several modules (`modules[]` in `session.json`); each is diffed against its own base and delivered in the same eject. Repository root: `pnpm verify`, then a PR (`release-eject-pr`).
|
|
84
|
+
|
|
85
|
+
## Pitfalls
|
|
86
|
+
|
|
87
|
+
- Renaming `createServerComposition`, `createClientContribution` or a permission constant breaks the platform typecheck or another module.
|
|
88
|
+
- Editing an applied `.up.sql` file, even only its whitespace, changes its checksum and blocks startup. Add a new numbered migration.
|
|
89
|
+
- `ORDER BY` on a new list must be covered by a `(tenant_id, <column>, id)` index.
|
|
90
|
+
- A new `Tag` tone or `Icon` name must exist in `@flowdular/sdk/ui`; there is no fallback warning.
|
|
91
|
+
- Editing `platform/**`, `flowdular.json`, or another module from a module change is out of scope; hand off with the exact core change needed.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: perf-audit
|
|
3
|
+
description: >-
|
|
4
|
+
Find the hot paths of a module or platform package, state their cost, and
|
|
5
|
+
change only what a measurement justifies.
|
|
6
|
+
---
|
|
7
|
+
# Performance audit
|
|
8
|
+
|
|
9
|
+
Measure first. A micro-rewrite without a number is not a performance change and does not belong in the diff.
|
|
10
|
+
|
|
11
|
+
## 1. Inventory the hot paths
|
|
12
|
+
|
|
13
|
+
Server (per request):
|
|
14
|
+
|
|
15
|
+
- Queries are asynchronous and pooled, so the cost is round trips, not a blocked event loop. A query inside a loop, an N+1 read after a list, or one transaction per row multiplies the round trip by the row count; do the work in one statement. A list endpoint that returns a tenant's whole table is still O(rows) per request, so paginate or filter in SQL, never in JavaScript after the rows arrive.
|
|
16
|
+
- A lease or a transaction held longer than the work needs starves the pool. Open the transaction around the statements it protects and release it; never hold one across a fetch, an agent call, or a sleep.
|
|
17
|
+
- `list(tenantId)` orders by a column: the index must cover `(tenant_id, <order column>, id)` (`.ai/references/catalog/src/services/migration.ts` has `catalog_items_tenant_sku_idx`). Without it PostgreSQL adds a sort node over the tenant's rows on every call.
|
|
18
|
+
- `readJsonObject` caps bodies at 16 KB and reads the whole text once; do not raise the cap for one field, add an endpoint.
|
|
19
|
+
- `defineEndpoint` allocates a request id and a Set of permissions per request through `endpointIdentityFromContext` (`new Set(principal.scopes)`); this is fine at current sizes and not a target.
|
|
20
|
+
- The runtime checks the migration ledger once, behind a short `purpose: 'migration'` lease, and then holds one runtime lease for the repository (`src/server/runtime.ts` shares a single initialization promise). Acquiring a lease or building a repository per request adds a ledger read and a pool acquisition to every request.
|
|
21
|
+
|
|
22
|
+
Client (per render):
|
|
23
|
+
|
|
24
|
+
- `items.filter(...)` and `toLocaleLowerCase` run on every render in `CatalogView.tsrx`. With a few hundred rows this is invisible; past that, derive once with `store.derive((get) => ...)` from `segment-state` or filter in the effect that loads data.
|
|
25
|
+
- One store per component (`useMemo(() => createXClientState(), [])`) is the intended shape; a shared module-level store would leak between screens.
|
|
26
|
+
- Widgets in `dashboard.metrics` each fetch on mount. Five widgets are five requests on the dashboard; a widget that needs a count should not load the whole list once an endpoint can count.
|
|
27
|
+
|
|
28
|
+
Bundle:
|
|
29
|
+
|
|
30
|
+
- `packages/ui/src/index.ts` imports the Plex fonts and `styles/index.css` at the top, so every consumer of `@flowdular/sdk/ui` pulls them once. A module must not import fonts or global CSS again.
|
|
31
|
+
- Module CSS is allowed only for module-specific composites (`modules/agents/src/client/agents.css`).
|
|
32
|
+
|
|
33
|
+
Agent runtime (`modules/agents`, `packages/harness`):
|
|
34
|
+
|
|
35
|
+
- Bounds that exist: `maxSteps` 1 to 32 and `timeoutMs` 250 to 86400000 per agent definition (`modules/agents/src/services/agent-service.ts`), worker concurrency `FD_AGENT_WORKER_CONCURRENCY` (1 to 16, default 2) and lease `FD_AGENT_WORKER_LEASE_MS` (`modules/agents/src/server/runtime.ts`).
|
|
36
|
+
- Tool calls have a deadline (`timeoutMs`, default 30 seconds, bounded from 250 to 600000 ms) and serialized output is capped at 32 KB by the harness. A list tool must still page or limit rows so useful data fits inside that cap.
|
|
37
|
+
|
|
38
|
+
## 2. Measure
|
|
39
|
+
|
|
40
|
+
- Server: a vitest `bench` or a script against the module's `tests/support/database.ts` provider seeded with 10k rows for one tenant and 10k for another; time `list(tenantId)` before and after an index. `await database.query({ text: 'EXPLAIN (ANALYZE, BUFFERS) SELECT ...' })` shows an `Index Scan` or the `Seq Scan` plus `Sort` pair that means the index is not covering the order.
|
|
41
|
+
- Client: count renders with a counter in the component during development, or `store.stats()` for commit counts. Remove the instrumentation before the handoff.
|
|
42
|
+
- Bundle: `pnpm --filter @flowdular/platform build` prints chunk sizes.
|
|
43
|
+
|
|
44
|
+
Record the number, the input size and the machine in the handoff or PR body.
|
|
45
|
+
|
|
46
|
+
## 2b. Bench recipe
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// tests/list.bench.ts (vitest bench; run with: pnpm --filter @flowdular/module-catalog exec vitest bench)
|
|
50
|
+
import { bench, describe } from 'vitest';
|
|
51
|
+
import { CatalogService } from '../src/services/catalog-service.ts';
|
|
52
|
+
import { createCatalogTestDatabase } from './support/database.ts';
|
|
53
|
+
|
|
54
|
+
const database = await createCatalogTestDatabase();
|
|
55
|
+
const service = new CatalogService(database.repository);
|
|
56
|
+
for (let index = 0; index < 10_000; index += 1) {
|
|
57
|
+
for (const tenant of ['tenant-a', 'tenant-b']) {
|
|
58
|
+
await service.create(tenant, {
|
|
59
|
+
sku: `SKU-${index}`,
|
|
60
|
+
name: `Item ${index}`,
|
|
61
|
+
kind: 'product',
|
|
62
|
+
unit: 'each',
|
|
63
|
+
basePriceMinor: index,
|
|
64
|
+
currency: 'EUR',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('catalog list', () => {
|
|
70
|
+
bench('list one tenant (10k of 20k rows)', async () => {
|
|
71
|
+
await service.list('tenant-a');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Keep bench files out of `tests/**/*.test.ts` so the `tests` gate does not run them; name them `*.bench.ts`. Delete the file or keep it only when the number is worth tracking.
|
|
77
|
+
|
|
78
|
+
## 2c. Report template
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
Path: GET /api/catalog/items -> CatalogService.list -> DatabaseCatalogRepository.list
|
|
82
|
+
Complexity: O(rows of tenant) time and space per request; ORDER BY covered by catalog_items_tenant_sku_idx
|
|
83
|
+
Measurement: 10k rows per tenant, embedded PGlite, Node 24: 3.1 ms per call before, 3.0 ms after (no change)
|
|
84
|
+
Decision: no code change; add pagination when a tenant exceeds ~50k items
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 3. Change only what the number justifies
|
|
88
|
+
|
|
89
|
+
Allowed without a benchmark: adding a missing covering index; moving a filter from JavaScript into the SQL `WHERE`; removing a duplicate fetch. Everything else (loop style, hoisting, memoization of cheap values, replacing `Array.prototype` calls) needs a before and after measurement in the same environment.
|
|
90
|
+
|
|
91
|
+
Complexity to state in the review: for each new data structure and loop on a request or render path, its time and space in terms of rows, tenants, or items. Unbounded growth (a Map keyed by tenant that is never pruned, a list of listeners never detached) is a defect even when each entry is small.
|
|
92
|
+
|
|
93
|
+
## Pitfalls
|
|
94
|
+
|
|
95
|
+
- `LIKE` or `=` against `lower(column)` cannot use a plain `(tenant_id, column)` index; store a normalized column (`sku_normalized`) as `.ai/references/catalog` does, or add an expression index on `lower(column)`.
|
|
96
|
+
- `ORDER BY lower(name)` (`Flowdular/official-modules`, `modules/parties`) cannot use the `(tenant_id, name, id)` index for the sort; acceptable at current sizes, name it if parties grow.
|
|
97
|
+
- A `Kpi` that shows `items.length` after loading the full list is O(rows) network per dashboard load.
|
|
98
|
+
- Never change behaviour in a performance commit; keep the functional tests green and add none that assert internal call counts.
|