@adula/kit 0.2.0-alpha.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +12 -2
  2. package/build/agent/AGENTS.template.md +2 -2
  3. package/build/agent/capabilities.md +54 -7
  4. package/build/agent/skills/adula-frontend-design/SKILL.md +1 -1
  5. package/build/agent/skills/idea-review/SKILL.md +26 -1
  6. package/build/agent/skills/module-review/SKILL.md +22 -0
  7. package/build/agent/skills/perf-review/SKILL.md +24 -0
  8. package/build/agent/skills/schema-review/SKILL.md +22 -0
  9. package/build/agent/skills/security-review/SKILL.md +24 -0
  10. package/build/agent/skills/ui-review/SKILL.md +22 -0
  11. package/build/commands/capabilities.d.ts +4 -0
  12. package/build/commands/capabilities.js +35 -4
  13. package/build/commands/doctor.js +37 -1
  14. package/build/commands/gaps.js +16 -6
  15. package/build/commands/install.js +19 -5
  16. package/build/commands/main.d.ts +4 -2
  17. package/build/commands/main.js +2 -0
  18. package/build/commands/module_add.js +2 -2
  19. package/build/commands/resource.js +1 -1
  20. package/build/commands/resource_snapshot.d.ts +15 -0
  21. package/build/commands/resource_snapshot.js +61 -0
  22. package/build/database/migrations/1770000000004_kit_collaboration.d.ts +5 -0
  23. package/build/database/migrations/1770000000004_kit_collaboration.js +10 -0
  24. package/build/database/migrations/1770000000005_kit_assignments.d.ts +5 -0
  25. package/build/database/migrations/1770000000005_kit_assignments.js +10 -0
  26. package/build/database/migrations/1770000000006_kit_messaging.d.ts +5 -0
  27. package/build/database/migrations/1770000000006_kit_messaging.js +10 -0
  28. package/build/database/migrations/1770000000007_kit_webhooks.d.ts +5 -0
  29. package/build/database/migrations/1770000000007_kit_webhooks.js +10 -0
  30. package/build/database/migrations/1770000000008_kit_imports.d.ts +5 -0
  31. package/build/database/migrations/1770000000008_kit_imports.js +10 -0
  32. package/build/database/migrations/1770000000010_kit_workflows.d.ts +5 -0
  33. package/build/database/migrations/1770000000010_kit_workflows.js +10 -0
  34. package/build/database/migrations/1770000000011_kit_managed_assignments.d.ts +5 -0
  35. package/build/database/migrations/1770000000011_kit_managed_assignments.js +10 -0
  36. package/build/database/migrations/1770000000012_kit_role_keys.d.ts +5 -0
  37. package/build/database/migrations/1770000000012_kit_role_keys.js +10 -0
  38. package/build/database/migrations/1770000000013_kit_notification_targets.d.ts +5 -0
  39. package/build/database/migrations/1770000000013_kit_notification_targets.js +10 -0
  40. package/build/database/migrations/1770000000014_kit_upload_grants.d.ts +5 -0
  41. package/build/database/migrations/1770000000014_kit_upload_grants.js +10 -0
  42. package/build/database/migrations/1770000000015_kit_inbound_webhooks.d.ts +5 -0
  43. package/build/database/migrations/1770000000015_kit_inbound_webhooks.js +10 -0
  44. package/build/index.d.ts +31 -2
  45. package/build/index.js +17 -2
  46. package/build/src/admin/contracts.d.ts +7 -0
  47. package/build/src/admin/contracts.js +34 -12
  48. package/build/src/admin/controller.d.ts +2 -0
  49. package/build/src/admin/controller.js +52 -1
  50. package/build/src/admin/presentation.d.ts +6 -0
  51. package/build/src/admin/record_title.d.ts +14 -0
  52. package/build/src/admin/record_title.js +50 -0
  53. package/build/src/admin/resource_service.d.ts +184 -2
  54. package/build/src/admin/resource_service.js +713 -48
  55. package/build/src/attachments/attachment_service.d.ts +12 -0
  56. package/build/src/attachments/attachment_service.js +28 -2
  57. package/build/src/attachments/upload_grants.d.ts +44 -0
  58. package/build/src/attachments/upload_grants.js +105 -0
  59. package/build/src/auth/ability.d.ts +1 -1
  60. package/build/src/auth/ability.js +4 -1
  61. package/build/src/auth/actor_store.js +6 -1
  62. package/build/src/auth/conditions.d.ts +15 -0
  63. package/build/src/auth/conditions.js +36 -0
  64. package/build/src/auth/sql.js +6 -2
  65. package/build/src/collaboration/assignments.d.ts +129 -0
  66. package/build/src/collaboration/assignments.js +333 -0
  67. package/build/src/collaboration/record_collaboration.d.ts +86 -0
  68. package/build/src/collaboration/record_collaboration.js +348 -0
  69. package/build/src/commands/agent_assets.js +5 -0
  70. package/build/src/commands/capabilities.d.ts +19 -0
  71. package/build/src/commands/capabilities.js +179 -0
  72. package/build/src/commands/doctor.d.ts +31 -0
  73. package/build/src/commands/doctor.js +114 -0
  74. package/build/src/commands/gap_report.d.ts +50 -2
  75. package/build/src/commands/gap_report.js +102 -4
  76. package/build/src/commands/generator.js +3 -3
  77. package/build/src/commands/snapshot.d.ts +28 -0
  78. package/build/src/commands/snapshot.js +48 -0
  79. package/build/src/commands/source_markers.d.ts +10 -1
  80. package/build/src/commands/source_markers.js +36 -4
  81. package/build/src/core/administration_guard.js +3 -1
  82. package/build/src/core/message_templates.d.ts +83 -0
  83. package/build/src/core/message_templates.js +293 -0
  84. package/build/src/core/module_seed.d.ts +15 -0
  85. package/build/src/core/module_seed.js +31 -0
  86. package/build/src/core/notifications.d.ts +18 -1
  87. package/build/src/core/notifications.js +27 -1
  88. package/build/src/core/roles.d.ts +27 -1
  89. package/build/src/core/roles.js +133 -5
  90. package/build/src/database/schema.d.ts +45 -0
  91. package/build/src/database/schema.js +290 -0
  92. package/build/src/eslint/index.js +26 -0
  93. package/build/src/events/outbox.d.ts +1 -0
  94. package/build/src/events/outbox.js +1 -1
  95. package/build/src/events/record_mutation.d.ts +11 -0
  96. package/build/src/events/record_mutation.js +15 -2
  97. package/build/src/integrations/imports.d.ts +74 -0
  98. package/build/src/integrations/imports.js +333 -0
  99. package/build/src/integrations/inbound_webhooks.d.ts +94 -0
  100. package/build/src/integrations/inbound_webhooks.js +276 -0
  101. package/build/src/integrations/openapi.d.ts +39 -0
  102. package/build/src/integrations/openapi.js +323 -0
  103. package/build/src/integrations/print.d.ts +37 -0
  104. package/build/src/integrations/print.js +124 -0
  105. package/build/src/integrations/webhooks.d.ts +98 -0
  106. package/build/src/integrations/webhooks.js +298 -0
  107. package/build/src/resource/define_resource.d.ts +1 -0
  108. package/build/src/resource/define_resource.js +21 -1
  109. package/build/src/resource/registry.d.ts +3 -0
  110. package/build/src/resource/registry.js +42 -0
  111. package/build/src/resource/types.d.ts +67 -1
  112. package/build/src/resource/values.js +2 -1
  113. package/build/src/services/settings.d.ts +13 -1
  114. package/build/src/services/settings.js +9 -2
  115. package/build/src/workflows/define_workflow.d.ts +124 -0
  116. package/build/src/workflows/define_workflow.js +123 -0
  117. package/build/src/workflows/engine.d.ts +141 -0
  118. package/build/src/workflows/engine.js +752 -0
  119. package/build/stubs/resource_contract.txt +103 -41
  120. package/package.json +7 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @adula/kit
2
2
 
3
- Experimental 0.2.0-alpha.4 resource framework for AdonisJS 7 and PostgreSQL 17. Node 24 or later is required. MIT licensed. The target is the complete approved 1.0 scope; it has not passed acceptance.
3
+ Resource framework for AdonisJS 7 and PostgreSQL 17, version 1.0.0 (accepted by the owner, ADR 028). Node 24 or later is required. MIT licensed. The public API in `api/kit-api.json` follows semantic versioning.
4
4
 
5
5
  ## Install into a consumer
6
6
 
@@ -28,4 +28,14 @@ Doctor measures the deployment's `storage/uploads` directory and warns above 5 d
28
28
 
29
29
  ## Current limitations
30
30
 
31
- Attachment ownership, upload/download, storage migration, authentication lifecycle/rate limiting, generic resource UI and core administration have local integration consumers and tests. Real OAuth/SMTP, full Tuyau field contracts, human/performance acceptance, XState workflows, later business features, staging/offsite restoration and a genuine minor-version consumer upgrade remain open. Submission envelopes are durable records in pending_definition state, not running workflows. See [implementation status](https://github.com/adulash/adula-kit/blob/main/docs/implementation-status.md), [gaps](https://github.com/adulash/adula-kit/blob/main/KIT_GAPS.md) and [1.0 acceptance](https://github.com/adulash/adula-kit/blob/main/docs/acceptance-1.0.md). Version 0.2.0-alpha.4 targets alpha; the owner-authorized latest alias remains on 0.2.0-alpha.1.
31
+ Phases 3 and 4 (collaboration, assignments, templates and realtime notifications,
32
+ CSV import, printing, webhooks, API tokens/OpenAPI, amend-by-copy and versioned
33
+ XState workflows) are implemented with PostgreSQL tests; see the
34
+ [business features guide](https://github.com/adulash/adula-kit/blob/main/docs/business-features.md).
35
+
36
+ - Two-factor authentication is not included; it is planned for 2.0 (ADR 027).
37
+ - Impersonation was reviewed by automated adversarial and black-box tests only; its
38
+ human security review is deferred to 2.0 (ADR 027).
39
+ - XLSX import is not available (GAP-006); CSV is.
40
+
41
+ See [implementation status](https://github.com/adulash/adula-kit/blob/main/docs/implementation-status.md), [gaps](https://github.com/adulash/adula-kit/blob/main/KIT_GAPS.md) and [1.0 acceptance](https://github.com/adulash/adula-kit/blob/main/docs/acceptance-1.0.md).
@@ -16,11 +16,11 @@ Structure
16
16
  8. Deletes are soft. Changeable lists come from lookups. Approvable documents use `submittable: true`.
17
17
  9. Notify via notify(), number via sequence, configure via settings. Nothing else.
18
18
 
19
- Security 10. Every route passes the authorize middleware. Every transformer uses `serialize` (explicit pick). 11. Never write crypto, sessions, or auth flows; use kit. 2FA and impersonation changes need a human review. 12. Never delete or weaken a test to make the build pass.
19
+ Security 10. Every route passes the authorize middleware. Every transformer uses `serialize` (explicit pick). `systemSave` and `rehome` skip role rules: use them only in listeners and module services after an explicit authorization, never in a controller or route (enforced by lint). 11. Never write crypto, sessions, or auth flows; use kit. 2FA and impersonation changes need a human review. 12. Never delete or weaken a test to make the build pass.
20
20
 
21
21
  UI 13. Before designing or changing any interface, read and apply `.agents/skills/adula-frontend-design/SKILL.md` (the kit's bundled frontend-design skill for business applications). At project kickoff, request the company's identity before the first design: name, logo, colors, fonts and brand guidelines when available. Reuse supplied identity, ask only for missing information, and preserve it in project-owned docs/design-identity.md. Fill the resource definition before writing a page. Override only via pages/<resource>/. 14. Compose interfaces from the project-owned shadcn/ui components in inertia/components/ui/. Use Button, Input, Select, Table, Card and the other registry primitives instead of hand-styled native controls or homemade equivalents. Semantic HTML for structure, text and form submission is allowed. Add components only via `node ace adula:ui add`, never `shadcn add`. 15. All forms (including create/edit) and row/record detail views use shadcn Dialog (modal) by default. Use ResourceSurface for resource forms/details; do not choose a standalone page, Sheet or custom overlay unless the user explicitly requests that alternative. View dialogs close on outside click; edit dialogs ask before closing by default. Honor system preferences for Gregorian, Hijri or both calendars and reduced motion. RTL by default; Arabic labels; English identifiers. Installation terminal instructions are English. Preserve explicit user exceptions and project-owned customizations.
22
22
 
23
- Boundaries 16. Never modify node_modules/@adula and never use patch-package (enforced by lint). Do not re-implement a kit service under another name (checked in review). 17. A limitation is recorded in KIT_GAPS.md (template inside) and reported to the developer — never worked around. 18. Only packages already in package.json. Ask before adding one. 19. New module or workflow → run the idea-review skill first. Field changes do not need it. 20. Before finishing: `npm run typecheck && npm test && node ace adula:doctor`.
23
+ Boundaries 16. Never modify node_modules/@adula and never use patch-package (enforced by lint). Do not re-implement a kit service under another name (checked in review). 17. A limitation is recorded in KIT_GAPS.md (template inside) and reported to the developer — never worked around. Describe the kit capability, not this project, and reproduce it on a new application from create-app. The developer submits it through the prefilled form link printed by `node ace adula:gaps report`, then records `Issue: #<number>`; never open kit issues through the API. 18. Only packages already in package.json. Ask before adding one. 19. New module or workflow → run the idea-review skill first. Field changes do not need it. 20. Before finishing: `npm run typecheck && npm test && node ace adula:doctor`.
24
24
  <!-- adula-kit:end -->
25
25
 
26
26
  ## Project rules
@@ -1,13 +1,60 @@
1
- # Current capabilities
1
+ # adula-kit capabilities (1.2.0)
2
2
 
3
- Version 0.2.0-alpha.4, experimental alpha. Resource definitions and registry; six flat condition operators (typed limits documented in KIT_GAPS.md); CASL evaluation with structural ltree scope; JSON/Inertia resource CRUD; field contracts; keyset pagination; sequences/lookups/settings; soft deletion; optimistic concurrency; atomic inline writes/activity/outbox; deduplicated database listeners; org moves; attachment ownership/storage migration; saved views (queries limited to fields the author may query); attachment fields with per-field `accept`/`maxSize` limits and daily pruning of unbound uploads; case-insensitive e-mail identity with ownership proof before OAuth linking; `adula:gaps report`; core administration services; resource/module commands; copied shadcn UI and installation diagnostics.
3
+ Generated by `node ace adula:capabilities`. Read this before proposing a module; anything not listed here is not provided by the kit.
4
4
 
5
- The reference consumer wires authentication/session lifecycle, admin screens, authorized attachments, optional MCP, Redis actor caching, a queue worker, one scheduler and backup/restore drills. Educational modules are isolated test fixtures, not installed application features. The agent ships business frontend-design and idea-review skills, company identity intake and modal defaults. Real SMTP receipt/recovery and isolated offsite local/source-S3 attachment recovery are documented in the source repository's confirmed-achievements ledger. OAuth is optional and externally unverified. Source-S3 recovery is integrated into the operational backup commands and verified directly; local Docker backup/recovery also passed; natural scheduled operation and staging remain pending; each consumer must verify its own configuration.
5
+ ## Resource definition
6
6
 
7
- This is not the 1.0 capability catalog. Planned features are listed in docs/implementation-status.md of the adula-kit source repository; do not assume them.
7
+ | Field type | Storage |
8
+ |---|---|
9
+ | string | varchar |
10
+ | text | text |
11
+ | integer | integer |
12
+ | money | bigint minor units, decimal string in JSON |
13
+ | boolean | boolean |
14
+ | date | date (YYYY-MM-DD) |
15
+ | datetime | timestamptz |
16
+ | json | jsonb |
17
+ | attachment | attachments row id; per-field accept/maxSize |
18
+ | belongsTo | foreign key, preloaded, restrict on delete |
19
+ | user | users foreign key, restrict on delete; choices are active members of the record unit or its ancestors; related as { id, fullName } |
20
+ | hasMany | child resource; inline rows saved with the parent |
21
+ | lookup | lookups group key |
8
22
 
9
- Core user invitations are available through `UserInvitations` and the `core.users/invite` permission. New standalone projects include an Arabic invitation Dialog, SMTP mail and single-use 24-hour acceptance links; recipients set their own passwords and receive no automatic roles. Configure SMTP and APP_URL, then assign business roles after acceptance. Existing consumers must merge project-owned routes/pages deliberately.
23
+ Field options: required, unique (partial, active rows), sortable, searchable (generated tsvector), filterable, permissionLevel, sequence, column.
10
24
 
11
- Administrative mutations protect the last active administrator and the acting administrator's own access. Repeated installation repairs missing bootstrap grants. The reference settings screen controls Gregorian, Umm al-Qura Hijri or dual-calendar presentation, edit-dialog dismissal confirmation and restrained transitions. Storage and API dates stay canonical. Permission actions require confirmation and report server outcomes.
25
+ Resource options: scoped (required), scope.from (the unit follows a required belongsTo parent), title (fields that name the record in relations, pickers, tasks and approvals), submittable (docStatus, submit/cancel/amend-by-copy), version (optimistic locking, default with submittable), customFields, list / form / show / serialize / hidden, actions, hooks.beforeSave / hooks.afterSave.
12
26
 
13
- The separate @adula/create-app package creates a complete application from an empty directory, including local service provisioning, fresh migrations, an administrator and company identity intake. Node/npm and Docker for the default service mode are prerequisites; existing PostgreSQL/Redis can be used instead. The packages are published on npm under the experimental `alpha` tag: `npm create @adula/app@alpha my-app`.
27
+ Role rule conditions: $eq, $ne, $in, $lt, $gt, $like on scalar fields; unsupported conditions are refused. Organization scope is always added with AND. `$actor.id` names the signed-in user on user fields, createdBy and updatedBy ($eq, $ne, $in), bound per request.
28
+
29
+ Workflow steps: condition, update, notify, approval, decision, delay, http, end. Workflow and step names: use lower-case letters, digits and underscores, starting with a letter (for example release_approval, notify_approved).
30
+
31
+ ## Services
32
+
33
+ - **Resources and authorization:** `defineResource`, `ResourceRegistry`, `ResourceService`, `createResourceController`, `buildAbility`, `accessibleBy`, `ActorStore`, `packedResourceRules`
34
+ - **Records and collaboration:** `RecordCollaboration`, `followerListeners`, `Assignments`, `SavedViews`, `logActivity`
35
+ - **Documents and workflows:** `defineWorkflow`, `WorkflowEngine`, `workflowListeners`
36
+ - **Notifications and messages:** `notify`, `notifyWithTemplate`, `MessageTemplates`, `deliverNotificationMail`, `listenForNotifications`, `NotificationsAdmin`
37
+ - **Integration:** `Webhooks`, `signWebhook`, `InboundWebhooks`, `openApiDocument`, `ImportBatches`, `renderPrintHtml`, `htmlToPdf`
38
+ - **Security:** `UserInvitations`, `UsersAdmin`, `RolesAdmin`
39
+ - **Data and operations:** `sequence`, `Settings`, `SettingsAdmin`, `publishOutbox`, `consumeEvent`, `recordMutation`, `moveOrgUnit`, `migrateStorage`, `verifyBackup`, `runtimeHealth`
40
+
41
+ ## Extension points
42
+
43
+ - Resource hooks (beforeSave, afterSave) inside the save transaction
44
+ - ResourceService.systemSave for module-decided writes (validator, hooks and audit, no role rules); ResourceService.rehome after a parent moves
45
+ - Assignment closing notes (Assignments closeNote: optional or required); managed assignments (managed: true) closed by module code with Assignments.close
46
+ - Page override: inertia/pages/<resource>/{index,form,show}.tsx replaces the generated page
47
+ - Create links with defaults: /resources/<resource>/create?defaults[field]=value
48
+ - Domain events <module>.<resource>.{created,updated,deleted,submitted,cancelled,amended} with idempotent listeners
49
+ - Module workflows (Module.workflows) with versioned definitions
50
+ - Message templates edited per deployment
51
+ - Outgoing webhooks and the bearer-token /api/v1 API
52
+ - Signed inbound webhooks raising inbound.<source>.<event> through the outbox
53
+
54
+ ## Outside the kit
55
+
56
+ - Dynamic fields or a field editor screen
57
+ - Runtime plugins
58
+ - A visual workflow editor
59
+ - Multi-tenant SaaS in one database (one deployment per organization)
60
+ - XLSX import until a maintained parser passes the dependency rule (GAP-006)
@@ -46,7 +46,7 @@ Install missing registry components with `node ace adula:ui add <component>`. Ne
46
46
 
47
47
  Every form and every row/record detail view opens in a shadcn Dialog unless the user explicitly requests another presentation. This includes create/edit forms and custom page overrides. Do not infer a page or Sheet exception from screen size, form length or personal design preference.
48
48
 
49
- For resource routes, use `ResourcePage`, which selects the modal surface by default. For a custom form/detail override, wrap its content in `ResourceSurface` with a title, description and the list's `backHref`. It keeps direct URLs usable and returns to the list when dismissed. Use `presentation="page"` only for a user-requested page exception. Keep embedded line-item fields within their parent's form, not separate nested forms. Search and filter controls may stay in the list toolbar; their form element is submission structure, not a separate record form.
49
+ For resource routes, use `ResourcePage`, which selects the modal surface by default. For a custom form/detail override, wrap its content in `ResourceSurface` with a title, description and the list's `backHref`. It keeps direct URLs usable and returns to the list when dismissed. From a list, open records with `openRecord(resource, id, 'show' | 'edit')` (exported by `resource-surface`): the dialog opens over the mounted list, and closing keeps its query, loaded rows and scroll. Use `presentation="page"` only for a user-requested page exception. Keep embedded line-item fields within their parent's form, not separate nested forms. Search and filter controls may stay in the list toolbar; their form element is submission structure, not a separate record form.
50
50
 
51
51
  Give every Dialog a visible DialogTitle and a useful DialogDescription. Keep long content scrollable within the viewport, Arabic RTL alignment, an accessible close control, focus containment and Escape dismissal. Use shadcn components inside the modal too. Keep validation and conflict messages inside it, preserve typed values on errors, and keep unauthorized actions absent. Do not replace server authorization with UI checks.
52
52
 
@@ -3,4 +3,29 @@ name: idea-review
3
3
  description: Translate a new module or workflow into existing kit resources and declared extension points before writing code.
4
4
  ---
5
5
 
6
- Read docs/decisions, the resource registry, capabilities and KIT_GAPS.md. Identify existing entities before proposing new ones. Produce a compact module blueprint with owned tables, explicit scope, dependencies, relationships, events, field visibility and acceptance tests. Classify each requirement as implemented now, a declared extension, or outside the kit. Do not design a dynamic field editor, runtime plugin loader or visual workflow editor. A user's already-approved plan authorizes its specified reference modules; do not ask to approve them again.
6
+ Use for a new module or workflow only; field changes do not need it.
7
+
8
+ 1. Run `node ace adula:capabilities` and read the output: kit field types, services,
9
+ workflow steps, extension points, what is outside the kit, and this project's
10
+ registered modules, resources and workflows. Read docs/decisions and KIT_GAPS.md.
11
+ 2. Find existing entities with the same meaning; reference them with belongsTo.
12
+ Never duplicate a table owned by another module.
13
+ 3. Produce a compact blueprint:
14
+ - module name, `dependsOn`, owned resources with `scoped` stated explicitly;
15
+ - fields with kit types, required/unique/searchable/filterable, permission levels,
16
+ hidden fields, lookups (groups and keys) and sequences;
17
+ - submittable documents and their workflow as steps (condition, update, notify,
18
+ approval, delay, http, end) with approvers by role;
19
+ - events consumed and emitted, listeners (idempotent) and any webhooks;
20
+ - role rules per action, including conditions from the six operators;
21
+ - acceptance tests: 403/404/uniqueness/scope from the generator plus the
22
+ business rules.
23
+ 4. Classify each requirement: **now** (kit capability), **extension** (hooks, page
24
+ override, listener, workflow), or **outside the kit** (record it in KIT_GAPS.md
25
+ with Needed by, Tried, Blocked because, Proposed kit change, Workaround).
26
+ 5. Never design a dynamic field editor, runtime plugin loader, visual workflow editor
27
+ or a second tenant model. An already-approved plan authorizes its modules; do not
28
+ ask to approve them again.
29
+
30
+ After implementation, run the reviewers: module-review, security-review,
31
+ schema-review, ui-review and perf-review.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: module-review
3
+ description: Review a module's boundaries, dependencies, events and scope before merging.
4
+ ---
5
+
6
+ Check, citing file and line for every finding:
7
+
8
+ - The module lives in `app/modules/<name>` and is registered in `start/modules.ts`
9
+ after every module it depends on; `dependsOn` lists each module whose resources
10
+ it references. `ResourceRegistry.register` must accept the order.
11
+ - It writes only its own tables. Cross-module effects happen in idempotent
12
+ listeners (`consumeEvent` + processed_events), never by importing another
13
+ module's controllers or services. Heavy listeners dispatch jobs.
14
+ - Every resource declares `scoped`. Central (`scoped: false`) resources are
15
+ justified; scoped ones rely on the kit's organization scope, not custom filters.
16
+ - Documents that need approval use `submittable: true` and a module workflow; the
17
+ operational status is a lookup separate from `docStatus`.
18
+ - No kit service is re-implemented under another name (notify, sequence, settings,
19
+ assignments, comments, imports, webhooks, workflows).
20
+ - Educational fixtures are not imported by the application.
21
+
22
+ Verdict: accept, or a numbered list of blocking findings.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: perf-review
3
+ description: Review queries, pagination and background work against the performance budget.
4
+ ---
5
+
6
+ Budget (plan section 13): list with two relations p95 < 300 ms, form save < 200 ms,
7
+ cached Ability build < 5 ms, on 100,000 seeded rows and 50 concurrent users.
8
+
9
+ Check, citing file and line:
10
+
11
+ - Lists go through `ResourceService.list`: keyset pagination, limit ≤ 100, selected
12
+ columns only, belongsTo preloaded in one query per relation; no query in a loop.
13
+ - Custom queries have supporting indexes (foreign keys, `(org_unit_id, deleted_at)`,
14
+ sort columns used by saved views); `EXPLAIN` shows no sequential scan on large
15
+ tables for the common filters.
16
+ - Save paths keep one transaction: record, lines, activity, field changes and outbox.
17
+ Anything slow (mail, HTTP, PDF, imports) runs in the worker, never inline.
18
+ - Listeners are idempotent and cheap or dispatch jobs; workflows do not poll.
19
+ - Caches (Ability, lookups, settings) are invalidated by the kit's revision rules,
20
+ not by time alone.
21
+ - Run `pnpm test:medical` with `K6_BINARY` on staging-like hardware before release
22
+ and record the measured p95 values; never relax the thresholds.
23
+
24
+ Verdict: accept, or findings with the measurement or query plan.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: schema-review
3
+ description: Review migrations and resource definitions for integrity and evolution.
4
+ ---
5
+
6
+ Check, citing file and line:
7
+
8
+ - Migrations are additive (expand/contract): no drop or rename of a column still
9
+ read by the previous release; kit migrations are never edited after release.
10
+ - Standard columns come from the generator (`id`, `org_unit_id` when scoped,
11
+ `created_by`, `updated_by`, timestamps, `deleted_at`, `version`, `doc_status`,
12
+ `amended_from_id`); they are not hand-edited.
13
+ - Every foreign key is indexed and `restrict` on delete (cascade only inside the
14
+ same module); `(org_unit_id, deleted_at)` exists for scoped tables.
15
+ - Unique rules are partial (`WHERE deleted_at IS NULL`); searchable fields produce
16
+ the generated tsvector with a GIN index.
17
+ - Money is bigint minor units; dates use `date`, instants `timestamptz`; changeable
18
+ lists are lookups, numbering uses sequences.
19
+ - The definition's `form`, `list`, `show` and `serialize` match the columns, and
20
+ generated contract tests cover 403/404/uniqueness/scope.
21
+
22
+ Verdict: accept, or blocking findings with the corrected migration.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: security-review
3
+ description: Review authorization, serialization and sensitive flows of a change.
4
+ ---
5
+
6
+ Check, citing file and line:
7
+
8
+ - Every route passes authentication and the resource goes through ResourceService
9
+ (or `resources.access` for record features). Custom queries use `accessibleBy`;
10
+ nothing reads records by id without the scope and Ability checks.
11
+ - Responses use the kit serializers; hidden and `permissionLevel` fields never leave
12
+ the server, including through search, sort, filters, exports, prints, history,
13
+ notifications, webhooks and OpenAPI.
14
+ - Role rules use only the six operators; nothing widens scope with an allow rule.
15
+ - No hand-written crypto, sessions, password or token handling. Secrets are sealed
16
+ with the application encryption; tokens and recovery codes are stored hashed.
17
+ - Outgoing HTTP (webhooks, workflow steps) targets HTTPS and cannot reach private
18
+ addresses in production; payloads carry identifiers, not record fields.
19
+ - Uploads and imports are size-limited and parsed by the kit; errors never echo
20
+ secrets. Rate limits cover authentication, second factor and token creation.
21
+ - 2FA, impersonation and authentication changes are flagged for human review
22
+ (AGENTS rule 11); map the change to ASVS 4.0.3 V2/V3/V4 items.
23
+
24
+ Verdict: accept, or blocking findings with the exploit scenario for each.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: ui-review
3
+ description: Review pages and components for RTL, the component system and data access.
4
+ ---
5
+
6
+ Check, citing file and line:
7
+
8
+ - Components come from `inertia/components/ui` (installed by `node ace adula:ui add`);
9
+ no `shadcn add` in modules, no hand-styled native controls or homemade dialogs.
10
+ - Forms and record details use the shadcn Dialog (ResourceSurface) unless the user
11
+ asked otherwise; destructive actions confirm; edits guard against closing.
12
+ - RTL: `dir="rtl"`, logical properties (`ms-`, `ps-`, `text-start`), arrows follow
13
+ the reading direction, Latin digits in amounts per the UI preference, Arabic
14
+ labels and English identifiers. Dates honor the calendar preference.
15
+ - Generated pages are overridden only through `pages/<resource>/<mode>.tsx`.
16
+ - Heavy sections are deferred props or fetched after render; lists paginate with
17
+ the scroll prop; no page renders data the server did not serialize for the user.
18
+ - Accessible names exist for icon buttons, dialogs and regions; errors use
19
+ `role="alert"`; focus stays inside dialogs.
20
+ - Company identity from docs/design-identity.md is applied, not invented.
21
+
22
+ Verdict: accept, or findings with screenshots or selectors.
@@ -2,5 +2,9 @@ import { BaseCommand } from '@adonisjs/core/ace';
2
2
  export default class Capabilities extends BaseCommand {
3
3
  static commandName: string;
4
4
  static description: string;
5
+ static options: {
6
+ startApp: boolean;
7
+ };
8
+ write: boolean;
5
9
  run(): Promise<void>;
6
10
  }
@@ -1,9 +1,40 @@
1
- import { BaseCommand } from '@adonisjs/core/ace';
2
- import { readFile } from 'node:fs/promises';
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { BaseCommand, flags } from '@adonisjs/core/ace';
8
+ import { writeFile } from 'node:fs/promises';
9
+ import { capabilityCatalog } from '../src/commands/capabilities.js';
3
10
  export default class Capabilities extends BaseCommand {
4
11
  static commandName = 'adula:capabilities';
5
- static description = 'Print implemented capabilities and current acceptance boundary';
12
+ static description = 'Generate the capability catalog (kit features plus this project registry)';
13
+ static options = { startApp: true };
6
14
  async run() {
7
- this.logger.log(await readFile(new URL('../agent/capabilities.md', import.meta.url), 'utf8'));
15
+ let registry;
16
+ try {
17
+ ;
18
+ ({ registry } = await this.app.import('#start/modules'));
19
+ }
20
+ catch {
21
+ this.logger.warning('start/modules could not be loaded; printing kit capabilities only');
22
+ }
23
+ // Imported lazily: main.ts lists this command itself.
24
+ const { getMetaData } = await import('./main.js');
25
+ const metadata = await getMetaData();
26
+ const catalog = capabilityCatalog({
27
+ registry,
28
+ commands: metadata.map((command) => `${command.commandName} — ${command.description}`),
29
+ });
30
+ if (this.write) {
31
+ await writeFile(this.app.makePath('capabilities.md'), catalog);
32
+ this.logger.success('capabilities.md');
33
+ }
34
+ else
35
+ this.logger.log(catalog);
8
36
  }
9
37
  }
38
+ __decorate([
39
+ flags.boolean({ description: 'Write capabilities.md at the project root' })
40
+ ], Capabilities.prototype, "write", void 0);
@@ -1,9 +1,11 @@
1
1
  import { BaseCommand } from '@adonisjs/core/ace';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { diagnose } from '../src/commands/doctor.js';
3
+ import { diagnose, diagnoseResourceSnapshots, diagnoseOutbox, diagnoseWorkflowRoles, } from '../src/commands/doctor.js';
4
+ import { runtimeHealth } from '../src/core/health.js';
4
5
  import { diagnoseAttachments } from '../src/attachments/doctor.js';
5
6
  import { Settings } from '../src/services/settings.js';
6
7
  import { MigrationRunner } from '@adonisjs/lucid/migration';
8
+ import { readFile } from 'node:fs/promises';
7
9
  export default class Doctor extends BaseCommand {
8
10
  static commandName = 'adula:doctor';
9
11
  static description = 'Check kit ownership, installation and backup readiness';
@@ -12,6 +14,7 @@ export default class Doctor extends BaseCommand {
12
14
  const { default: db } = await import('@adonisjs/lucid/services/db');
13
15
  const findings = await diagnose(fileURLToPath(this.app.appRoot), new Settings(db.connection().getWriteClient()), this.app.inProduction, process.env);
14
16
  findings.push(await diagnoseAttachments(db.connection().getWriteClient()));
17
+ findings.push(diagnoseOutbox(await runtimeHealth(db.connection().getWriteClient())));
15
18
  const migrations = await new MigrationRunner(db, this.app, { direction: 'up' }).getList();
16
19
  const missing = migrations.filter((entry) => entry.status === 'corrupt');
17
20
  const pending = migrations.filter((entry) => entry.status === 'pending');
@@ -24,6 +27,39 @@ export default class Doctor extends BaseCommand {
24
27
  ? `${pending.length} pending migrations; run migration:run after the deployment backup`
25
28
  : 'Migration files and database history agree',
26
29
  });
30
+ const sources = [];
31
+ for (const entry of pending)
32
+ for (const extension of ['.ts', '.js'])
33
+ try {
34
+ const file = `${entry.name}${extension}`;
35
+ sources.push({ file, source: await readFile(this.app.makePath(file), 'utf8') });
36
+ break;
37
+ }
38
+ catch { }
39
+ let registry;
40
+ try {
41
+ ;
42
+ ({ registry } = await this.app.import('#start/modules'));
43
+ }
44
+ catch { }
45
+ if (registry) {
46
+ // Before the 1.1 migrations run, roles have no key column yet (an unfinished upgrade).
47
+ const keyed = await db.connection().getWriteClient().schema.hasColumn('roles', 'key');
48
+ const roles = await db.from('roles').select(keyed ? ['key', 'name'] : ['name']);
49
+ findings.push(diagnoseWorkflowRoles(registry.workflows(), roles.map((role) => ({
50
+ key: role.key ? String(role.key) : null,
51
+ name: String(role.name),
52
+ }))));
53
+ }
54
+ if (registry)
55
+ findings.push(diagnoseResourceSnapshots(sources, (name) => {
56
+ try {
57
+ return registry.get(name);
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }));
27
63
  for (const finding of findings)
28
64
  this.logger.log(`${finding.status.toUpperCase()} ${finding.check}: ${finding.message}`);
29
65
  if (findings.some((finding) => finding.status === 'fail'))
@@ -10,7 +10,7 @@ import { gapReport } from '../src/commands/gap_report.js';
10
10
  /** Collects KIT_GAPS.md for reporting; nothing leaves the machine without explicit confirmation. */
11
11
  export default class Gaps extends BaseCommand {
12
12
  static commandName = 'adula:gaps';
13
- static description = 'Show the project gap report with private names masked before it is sent';
13
+ static description = 'Show the project gap report with private names masked, and links that open the kit gap form prefilled';
14
14
  static options = { startApp: false };
15
15
  async run() {
16
16
  if (this.action !== 'report')
@@ -23,16 +23,26 @@ export default class Gaps extends BaseCommand {
23
23
  catch {
24
24
  throw new Error('KIT_GAPS.md does not exist; adula:install creates it');
25
25
  }
26
- const { masked, titles } = gapReport(content);
27
- this.logger.info(`${titles.length} gap(s) recorded in KIT_GAPS.md`);
28
- for (const title of titles)
29
- this.logger.log(` ${title}`);
26
+ const { masked, gaps } = gapReport(content);
27
+ this.logger.info(`${gaps.length} gap(s) recorded in KIT_GAPS.md`);
28
+ for (const gap of gaps)
29
+ this.logger.log(` ${gap.title}${gap.issue ? ` (reported: ${gap.issue})` : ''}`);
30
30
  if (!this.yes) {
31
- const confirmed = await this.prompt.confirm('Show the full masked report? Nothing is sent anywhere by this command.');
31
+ const confirmed = await this.prompt.confirm('Show the full masked report and the issue links? Nothing is sent anywhere by this command.');
32
32
  if (!confirmed)
33
33
  return;
34
34
  }
35
35
  this.logger.log(masked);
36
+ const pending = gaps.filter((gap) => gap.url);
37
+ if (!pending.length)
38
+ return;
39
+ this.logger.info('Review each link in a browser, submit the form, then write the issue number in the entry as "Issue: #<number>".');
40
+ for (const gap of pending) {
41
+ this.logger.log(`\n${gap.title}`);
42
+ if (gap.missing.length)
43
+ this.logger.warning(`Complete before submitting: ${gap.missing.join(', ')}`);
44
+ this.logger.log(gap.url);
45
+ }
36
46
  }
37
47
  }
38
48
  __decorate([
@@ -1,7 +1,9 @@
1
1
  import { BaseCommand } from '@adonisjs/core/ace';
2
2
  import { readFile, writeFile, access } from 'node:fs/promises';
3
3
  import { Settings } from '../src/services/settings.js';
4
+ import { seedModules } from '../src/core/module_seed.js';
4
5
  import { syncAgentAssets } from '../src/commands/agent_assets.js';
6
+ import { GAPS_TEMPLATE } from '../src/commands/gap_report.js';
5
7
  import { KIT_VERSION } from '../src/version.js';
6
8
  import { fileURLToPath } from 'node:url';
7
9
  export default class Install extends BaseCommand {
@@ -32,10 +34,12 @@ export default class Install extends BaseCommand {
32
34
  let role = await trx('roles').where('name', 'administrator').first();
33
35
  if (!role) {
34
36
  const [created] = await trx('roles')
35
- .insert({ name: 'administrator', permission_level: 1 })
37
+ .insert({ name: 'administrator', key: 'administrator', permission_level: 1 })
36
38
  .returning('*');
37
39
  role = created;
38
40
  }
41
+ else if (!role.key && !(await trx('roles').where('key', 'administrator').first()))
42
+ await trx('roles').where('id', role.id).update({ key: 'administrator' });
39
43
  // Repair a missing bootstrap grant even when the role already exists.
40
44
  // The transaction lock makes repeated/concurrent installation idempotent.
41
45
  if (!(await trx('role_rules')
@@ -55,12 +59,22 @@ export default class Install extends BaseCommand {
55
59
  .ignore();
56
60
  await new Settings(trx).set('kit.version', KIT_VERSION);
57
61
  });
62
+ // Module lookups and default roles: added when missing, never overwritten.
63
+ let registry;
64
+ try {
65
+ ;
66
+ ({ registry } = await this.app.import('#start/modules'));
67
+ }
68
+ catch (error) {
69
+ this.logger.warning(`Module defaults skipped: ${error.message}`);
70
+ }
71
+ if (registry) {
72
+ const seeded = await seedModules(knex, registry, user.id);
73
+ this.logger.info(`Module defaults: ${seeded.lookups} lookup rows added; roles created: ${seeded.created.join(', ') || 'none'}; adopted by name: ${seeded.adopted.join(', ') || 'none'}`);
74
+ }
58
75
  for (const [name, content] of [
59
76
  ['CLAUDE.md', 'Read and follow AGENTS.md.\nThe project rules are maintained there.\n'],
60
- [
61
- 'KIT_GAPS.md',
62
- '# Kit gaps\n\nRecord Needed by, Tried, Blocked because, Proposed kit change, Workaround.\n',
63
- ],
77
+ ['KIT_GAPS.md', GAPS_TEMPLATE],
64
78
  ]) {
65
79
  const path = this.app.makePath(name);
66
80
  try {
@@ -1,6 +1,8 @@
1
+ import Resource from './resource.js';
1
2
  import Doctor from './doctor.js';
2
- import Capabilities from './capabilities.js';
3
+ import ModuleAdd from './module_add.js';
4
+ import Ui from './ui.js';
3
5
  export declare function getMetaData(): Promise<import("@adonisjs/core/types/ace").CommandMetaData[]>;
4
6
  export declare function getCommand(meta: {
5
7
  commandName: string;
6
- }): Promise<typeof Capabilities | typeof Doctor | null>;
8
+ }): Promise<typeof Resource | typeof Doctor | typeof ModuleAdd | typeof Ui | null>;
@@ -1,4 +1,5 @@
1
1
  import Resource from './resource.js';
2
+ import ResourceSnapshot from './resource_snapshot.js';
2
3
  import Doctor from './doctor.js';
3
4
  import Install from './install.js';
4
5
  import Capabilities from './capabilities.js';
@@ -9,6 +10,7 @@ import StorageMigrate from './storage_migrate.js';
9
10
  import Gaps from './gaps.js';
10
11
  const commands = [
11
12
  Resource,
13
+ ResourceSnapshot,
12
14
  Doctor,
13
15
  Install,
14
16
  Capabilities,
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { BaseCommand, args, flags } from '@adonisjs/core/ace';
8
8
  import { mkdir, readFile, writeFile, access } from 'node:fs/promises';
9
9
  import { identifier } from '../src/resource/define_resource.js';
10
- import { appendMarkedItem } from '../src/commands/source_markers.js';
10
+ import { appendMarkedItem, moduleSource } from '../src/commands/source_markers.js';
11
11
  export default class ModuleAdd extends BaseCommand {
12
12
  static commandName = 'adula:module:add';
13
13
  static description = 'Create and register an application-owned module';
@@ -44,7 +44,7 @@ export default class ModuleAdd extends BaseCommand {
44
44
  'tests',
45
45
  ])
46
46
  await mkdir(this.app.makePath('app/modules', this.name, directory), { recursive: true });
47
- await writeFile(this.app.makePath('app/modules', this.name, 'module.ts'), `import type { Module } from '@adula/kit'\n// adula:imports\nexport default { name: '${this.name}', label: { ar: '${this.name}', en: '${this.name}' }, reference: ${Boolean(this.reference)}, dependsOn: [], resources: [/* adula:resources */] } satisfies Module\n`, { flag: 'wx' });
47
+ await writeFile(this.app.makePath('app/modules', this.name, 'module.ts'), moduleSource(this.name, { reference: Boolean(this.reference), typed: true }), { flag: 'wx' });
48
48
  await writeFile(indexPath, next);
49
49
  this.logger.success(`Created ${this.name}. Fill bilingual labels and declare dependencies before adding resources.`);
50
50
  }
@@ -18,7 +18,7 @@ export default class Resource extends BaseCommand {
18
18
  }
19
19
  const files = await generateResource(fileURLToPath(this.app.appRoot), this.name, this.module);
20
20
  files.forEach((file) => this.logger.success(file));
21
- this.logger.info('Set bilingual labels, fill the resource definition, migrate, and run the generated security contract.');
21
+ this.logger.info('Set bilingual labels (label: the plural list heading; recordLabel: the singular record noun) and fill the resource definition. The generated migration embeds the definition as scaffolded: after filling the definition, run `node ace adula:resource:snapshot <name>` to refresh the pending migration, and update the model, factory and contract fixture, before migration:run. adula:doctor reports a pending migration that differs. Then run the generated security contract.');
22
22
  }
23
23
  }
24
24
  __decorate([
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ /**
3
+ * Rewrites the definition embedded in a resource's generated create-migration from the
4
+ * current resource, while that migration has not run yet (#20). A migration that already
5
+ * ran is never edited: change the table with a new expand migration instead.
6
+ */
7
+ export default class ResourceSnapshot extends BaseCommand {
8
+ static commandName: string;
9
+ static description: string;
10
+ static options: {
11
+ startApp: boolean;
12
+ };
13
+ name: string;
14
+ run(): Promise<void>;
15
+ }