@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.
- package/README.md +12 -2
- package/build/agent/AGENTS.template.md +2 -2
- package/build/agent/capabilities.md +54 -7
- package/build/agent/skills/adula-frontend-design/SKILL.md +1 -1
- package/build/agent/skills/idea-review/SKILL.md +26 -1
- package/build/agent/skills/module-review/SKILL.md +22 -0
- package/build/agent/skills/perf-review/SKILL.md +24 -0
- package/build/agent/skills/schema-review/SKILL.md +22 -0
- package/build/agent/skills/security-review/SKILL.md +24 -0
- package/build/agent/skills/ui-review/SKILL.md +22 -0
- package/build/commands/capabilities.d.ts +4 -0
- package/build/commands/capabilities.js +35 -4
- package/build/commands/doctor.js +37 -1
- package/build/commands/gaps.js +16 -6
- package/build/commands/install.js +19 -5
- package/build/commands/main.d.ts +4 -2
- package/build/commands/main.js +2 -0
- package/build/commands/module_add.js +2 -2
- package/build/commands/resource.js +1 -1
- package/build/commands/resource_snapshot.d.ts +15 -0
- package/build/commands/resource_snapshot.js +61 -0
- package/build/database/migrations/1770000000004_kit_collaboration.d.ts +5 -0
- package/build/database/migrations/1770000000004_kit_collaboration.js +10 -0
- package/build/database/migrations/1770000000005_kit_assignments.d.ts +5 -0
- package/build/database/migrations/1770000000005_kit_assignments.js +10 -0
- package/build/database/migrations/1770000000006_kit_messaging.d.ts +5 -0
- package/build/database/migrations/1770000000006_kit_messaging.js +10 -0
- package/build/database/migrations/1770000000007_kit_webhooks.d.ts +5 -0
- package/build/database/migrations/1770000000007_kit_webhooks.js +10 -0
- package/build/database/migrations/1770000000008_kit_imports.d.ts +5 -0
- package/build/database/migrations/1770000000008_kit_imports.js +10 -0
- package/build/database/migrations/1770000000010_kit_workflows.d.ts +5 -0
- package/build/database/migrations/1770000000010_kit_workflows.js +10 -0
- package/build/database/migrations/1770000000011_kit_managed_assignments.d.ts +5 -0
- package/build/database/migrations/1770000000011_kit_managed_assignments.js +10 -0
- package/build/database/migrations/1770000000012_kit_role_keys.d.ts +5 -0
- package/build/database/migrations/1770000000012_kit_role_keys.js +10 -0
- package/build/database/migrations/1770000000013_kit_notification_targets.d.ts +5 -0
- package/build/database/migrations/1770000000013_kit_notification_targets.js +10 -0
- package/build/database/migrations/1770000000014_kit_upload_grants.d.ts +5 -0
- package/build/database/migrations/1770000000014_kit_upload_grants.js +10 -0
- package/build/database/migrations/1770000000015_kit_inbound_webhooks.d.ts +5 -0
- package/build/database/migrations/1770000000015_kit_inbound_webhooks.js +10 -0
- package/build/index.d.ts +31 -2
- package/build/index.js +17 -2
- package/build/src/admin/contracts.d.ts +7 -0
- package/build/src/admin/contracts.js +34 -12
- package/build/src/admin/controller.d.ts +2 -0
- package/build/src/admin/controller.js +52 -1
- package/build/src/admin/presentation.d.ts +6 -0
- package/build/src/admin/record_title.d.ts +14 -0
- package/build/src/admin/record_title.js +50 -0
- package/build/src/admin/resource_service.d.ts +184 -2
- package/build/src/admin/resource_service.js +713 -48
- package/build/src/attachments/attachment_service.d.ts +12 -0
- package/build/src/attachments/attachment_service.js +28 -2
- package/build/src/attachments/upload_grants.d.ts +44 -0
- package/build/src/attachments/upload_grants.js +105 -0
- package/build/src/auth/ability.d.ts +1 -1
- package/build/src/auth/ability.js +4 -1
- package/build/src/auth/actor_store.js +6 -1
- package/build/src/auth/conditions.d.ts +15 -0
- package/build/src/auth/conditions.js +36 -0
- package/build/src/auth/sql.js +6 -2
- package/build/src/collaboration/assignments.d.ts +129 -0
- package/build/src/collaboration/assignments.js +333 -0
- package/build/src/collaboration/record_collaboration.d.ts +86 -0
- package/build/src/collaboration/record_collaboration.js +348 -0
- package/build/src/commands/agent_assets.js +5 -0
- package/build/src/commands/capabilities.d.ts +19 -0
- package/build/src/commands/capabilities.js +179 -0
- package/build/src/commands/doctor.d.ts +31 -0
- package/build/src/commands/doctor.js +114 -0
- package/build/src/commands/gap_report.d.ts +50 -2
- package/build/src/commands/gap_report.js +102 -4
- package/build/src/commands/generator.js +3 -3
- package/build/src/commands/snapshot.d.ts +28 -0
- package/build/src/commands/snapshot.js +48 -0
- package/build/src/commands/source_markers.d.ts +10 -1
- package/build/src/commands/source_markers.js +36 -4
- package/build/src/core/administration_guard.js +3 -1
- package/build/src/core/message_templates.d.ts +83 -0
- package/build/src/core/message_templates.js +293 -0
- package/build/src/core/module_seed.d.ts +15 -0
- package/build/src/core/module_seed.js +31 -0
- package/build/src/core/notifications.d.ts +18 -1
- package/build/src/core/notifications.js +27 -1
- package/build/src/core/roles.d.ts +27 -1
- package/build/src/core/roles.js +133 -5
- package/build/src/database/schema.d.ts +45 -0
- package/build/src/database/schema.js +290 -0
- package/build/src/eslint/index.js +26 -0
- package/build/src/events/outbox.d.ts +1 -0
- package/build/src/events/outbox.js +1 -1
- package/build/src/events/record_mutation.d.ts +11 -0
- package/build/src/events/record_mutation.js +15 -2
- package/build/src/integrations/imports.d.ts +74 -0
- package/build/src/integrations/imports.js +333 -0
- package/build/src/integrations/inbound_webhooks.d.ts +94 -0
- package/build/src/integrations/inbound_webhooks.js +276 -0
- package/build/src/integrations/openapi.d.ts +39 -0
- package/build/src/integrations/openapi.js +323 -0
- package/build/src/integrations/print.d.ts +37 -0
- package/build/src/integrations/print.js +124 -0
- package/build/src/integrations/webhooks.d.ts +98 -0
- package/build/src/integrations/webhooks.js +298 -0
- package/build/src/resource/define_resource.d.ts +1 -0
- package/build/src/resource/define_resource.js +21 -1
- package/build/src/resource/registry.d.ts +3 -0
- package/build/src/resource/registry.js +42 -0
- package/build/src/resource/types.d.ts +67 -1
- package/build/src/resource/values.js +2 -1
- package/build/src/services/settings.d.ts +13 -1
- package/build/src/services/settings.js +9 -2
- package/build/src/workflows/define_workflow.d.ts +124 -0
- package/build/src/workflows/define_workflow.js +123 -0
- package/build/src/workflows/engine.d.ts +141 -0
- package/build/src/workflows/engine.js +752 -0
- package/build/stubs/resource_contract.txt +103 -41
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @adula/kit
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1
|
+
# adula-kit capabilities (1.2.0)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
## Resource definition
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
23
|
+
Field options: required, unique (partial, active rows), sortable, searchable (generated tsvector), filterable, permissionLevel, sequence, column.
|
|
10
24
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -1,9 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 = '
|
|
12
|
+
static description = 'Generate the capability catalog (kit features plus this project registry)';
|
|
13
|
+
static options = { startApp: true };
|
|
6
14
|
async run() {
|
|
7
|
-
|
|
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);
|
package/build/commands/doctor.js
CHANGED
|
@@ -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'))
|
package/build/commands/gaps.js
CHANGED
|
@@ -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
|
|
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,
|
|
27
|
-
this.logger.info(`${
|
|
28
|
-
for (const
|
|
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 {
|
package/build/commands/main.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import Resource from './resource.js';
|
|
1
2
|
import Doctor from './doctor.js';
|
|
2
|
-
import
|
|
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
|
|
8
|
+
}): Promise<typeof Resource | typeof Doctor | typeof ModuleAdd | typeof Ui | null>;
|
package/build/commands/main.js
CHANGED
|
@@ -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'),
|
|
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
|
|
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
|
+
}
|