@dforge-core/dforge-mcp 0.1.0-rc.10 → 0.1.0-rc.12
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/dist/server.js +186 -2
- package/package.json +1 -1
- package/resources/docs/conventions.md +10 -7
- package/resources/schemas/entity.schema.json +4 -1
- package/resources/schemas/print-templates.schema.json +82 -0
- package/resources/schemas/triggers.schema.json +59 -0
- package/skills/dforge-mcp-author/SKILL.md +54 -11
package/dist/server.js
CHANGED
|
@@ -31381,8 +31381,12 @@ function modulePaths(moduleDir) {
|
|
|
31381
31381
|
menus: path3.join(root, "ui", "menus.json"),
|
|
31382
31382
|
actions: path3.join(root, "ui", "actions.json"),
|
|
31383
31383
|
reports: path3.join(root, "ui", "reports.json"),
|
|
31384
|
+
queries: path3.join(root, "ui", "queries.json"),
|
|
31385
|
+
printTemplates: path3.join(root, "ui", "print_templates.json"),
|
|
31384
31386
|
roles: path3.join(root, "security", "roles.json"),
|
|
31385
31387
|
jobs: path3.join(root, "logic", "jobs.json"),
|
|
31388
|
+
triggers: path3.join(root, "logic", "triggers.json"),
|
|
31389
|
+
webhooks: path3.join(root, "logic", "webhooks.json"),
|
|
31386
31390
|
settings: path3.join(root, "settings.json")
|
|
31387
31391
|
};
|
|
31388
31392
|
}
|
|
@@ -31932,6 +31936,158 @@ function computeDepth(folder, current = 1) {
|
|
|
31932
31936
|
return childDepths.length === 0 ? current : Math.max(...childDepths);
|
|
31933
31937
|
}
|
|
31934
31938
|
|
|
31939
|
+
// src/tools/behavior.ts
|
|
31940
|
+
var triggerAddSchema = {
|
|
31941
|
+
moduleDir: external_exports.string(),
|
|
31942
|
+
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Trigger code, unique within the module."),
|
|
31943
|
+
entity: external_exports.string().describe(
|
|
31944
|
+
"Entity whose events fire this trigger. Cross-module dotted form supported ('fin.invoice')."
|
|
31945
|
+
),
|
|
31946
|
+
event: external_exports.enum(["insert", "update", "delete", "status_change", "any"]).describe(
|
|
31947
|
+
"insert/update/delete are obvious. status_change fires only when status field changes value. 'any' fires for all insert/update/delete."
|
|
31948
|
+
),
|
|
31949
|
+
action: external_exports.string().describe("Action code to invoke. Cross-module dotted form ('module.action') supported."),
|
|
31950
|
+
description: external_exports.string().optional(),
|
|
31951
|
+
condition: external_exports.string().optional().describe(
|
|
31952
|
+
"Optional formula expression (same shape as canExecute) using `[field]` syntax. Single-line boolean. If omitted, trigger fires on every matching event."
|
|
31953
|
+
),
|
|
31954
|
+
params: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe(
|
|
31955
|
+
"Static params merged into the action invocation alongside auto-injected record_id."
|
|
31956
|
+
),
|
|
31957
|
+
async: external_exports.boolean().default(false).describe(
|
|
31958
|
+
"When true, the action runs in background (recommended for slow actions). When false, runs in the same transaction \u2014 action failure rolls back the original DB change."
|
|
31959
|
+
)
|
|
31960
|
+
};
|
|
31961
|
+
function triggerAdd(args) {
|
|
31962
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
31963
|
+
const file2 = readJsonOrDefault(
|
|
31964
|
+
paths.triggers,
|
|
31965
|
+
{ triggers: [] }
|
|
31966
|
+
);
|
|
31967
|
+
if (file2.triggers.some((t) => t.code === args.code)) {
|
|
31968
|
+
throw new Error(`Trigger '${args.code}' already exists.`);
|
|
31969
|
+
}
|
|
31970
|
+
const entry = {
|
|
31971
|
+
code: args.code,
|
|
31972
|
+
entity: args.entity,
|
|
31973
|
+
event: args.event,
|
|
31974
|
+
action: args.action,
|
|
31975
|
+
async: args.async
|
|
31976
|
+
};
|
|
31977
|
+
if (args.description) entry.description = args.description;
|
|
31978
|
+
if (args.condition) entry.condition = args.condition;
|
|
31979
|
+
if (args.params) entry.params = args.params;
|
|
31980
|
+
file2.triggers.push(entry);
|
|
31981
|
+
return makeResult(
|
|
31982
|
+
`Added trigger '${args.code}' on ${args.entity}.${args.event} \u2192 action '${args.action}'${args.async ? " (async)" : ""}.`,
|
|
31983
|
+
{
|
|
31984
|
+
[rel(paths.root, paths.triggers)]: jsonText(file2),
|
|
31985
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
31986
|
+
}
|
|
31987
|
+
);
|
|
31988
|
+
}
|
|
31989
|
+
var jobAddSchema = {
|
|
31990
|
+
moduleDir: external_exports.string(),
|
|
31991
|
+
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Job code, unique within the module."),
|
|
31992
|
+
action: external_exports.string().describe(
|
|
31993
|
+
"Action code to invoke on schedule. MUST NOT use record-context (`[field]`) syntax \u2014 scheduled jobs run as system user with no current record. Wrap record-context actions in a thin action that queries first."
|
|
31994
|
+
),
|
|
31995
|
+
schedule: external_exports.string().describe(
|
|
31996
|
+
"5-field cron expression (minute hour day-of-month month day-of-week). Evaluated in the tenant's time zone (auth.tenant.time_zone) or per-job override."
|
|
31997
|
+
),
|
|
31998
|
+
timeout: external_exports.number().int().min(1).max(3600).describe(
|
|
31999
|
+
"Hard kill timeout in seconds. Required. If > 300, you must also set `class: 'long_running'`."
|
|
32000
|
+
),
|
|
32001
|
+
description: external_exports.string().optional(),
|
|
32002
|
+
concurrency: external_exports.number().int().min(1).default(1).describe("Max overlapping executions. Default 1 (skip new fire if previous still running)."),
|
|
32003
|
+
jobClass: external_exports.enum(["standard", "long_running"]).default("standard").describe(
|
|
32004
|
+
"Required to be 'long_running' when timeout > 300. Affects scheduler thread-pool routing."
|
|
32005
|
+
),
|
|
32006
|
+
paused: external_exports.boolean().default(false)
|
|
32007
|
+
};
|
|
32008
|
+
function jobAdd(args) {
|
|
32009
|
+
if (args.timeout > 300 && args.jobClass !== "long_running") {
|
|
32010
|
+
throw new Error(
|
|
32011
|
+
"Jobs with timeout > 300s must set jobClass: 'long_running' \u2014 see logic/jobs.json convention."
|
|
32012
|
+
);
|
|
32013
|
+
}
|
|
32014
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
32015
|
+
const file2 = readJsonOrDefault(
|
|
32016
|
+
paths.jobs,
|
|
32017
|
+
{ jobs: [] }
|
|
32018
|
+
);
|
|
32019
|
+
if (file2.jobs.some((j) => j.code === args.code)) {
|
|
32020
|
+
throw new Error(`Job '${args.code}' already exists.`);
|
|
32021
|
+
}
|
|
32022
|
+
const entry = {
|
|
32023
|
+
code: args.code,
|
|
32024
|
+
action: args.action,
|
|
32025
|
+
schedule: args.schedule,
|
|
32026
|
+
timeout: args.timeout,
|
|
32027
|
+
concurrency: args.concurrency,
|
|
32028
|
+
class: args.jobClass
|
|
32029
|
+
};
|
|
32030
|
+
if (args.description) entry.description = args.description;
|
|
32031
|
+
if (args.paused) entry.paused = true;
|
|
32032
|
+
file2.jobs.push(entry);
|
|
32033
|
+
return makeResult(
|
|
32034
|
+
`Added job '${args.code}' (schedule '${args.schedule}', timeout ${args.timeout}s) \u2192 action '${args.action}'.`,
|
|
32035
|
+
{
|
|
32036
|
+
[rel(paths.root, paths.jobs)]: jsonText(file2),
|
|
32037
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
32038
|
+
}
|
|
32039
|
+
);
|
|
32040
|
+
}
|
|
32041
|
+
var webhookAddSchema = {
|
|
32042
|
+
moduleDir: external_exports.string(),
|
|
32043
|
+
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Subscription code, unique within the module."),
|
|
32044
|
+
entity: external_exports.string().describe("Entity whose events fire this webhook."),
|
|
32045
|
+
event: external_exports.enum(["insert", "update", "delete", "status_change", "any"]).describe("Same event taxonomy as triggers."),
|
|
32046
|
+
endpointUrl: external_exports.string().url().describe("Destination URL \u2014 receives POST with the payload."),
|
|
32047
|
+
description: external_exports.string().optional(),
|
|
32048
|
+
condition: external_exports.string().optional().describe("Optional formula filter; fires only when true. Same shape as trigger condition."),
|
|
32049
|
+
payload: external_exports.object({
|
|
32050
|
+
include: external_exports.array(external_exports.string()).optional().describe("Whitelist of field codes to include. Omit to send all fields."),
|
|
32051
|
+
exclude: external_exports.array(external_exports.string()).optional().describe("Blacklist (applied after include). Use for stripping sensitive cols."),
|
|
32052
|
+
includeOld: external_exports.boolean().optional().describe(
|
|
32053
|
+
"For update/status_change events, also include the pre-change record as `old`."
|
|
32054
|
+
)
|
|
32055
|
+
}).optional().describe(
|
|
32056
|
+
"Payload shape. If omitted, sends full new record. The platform always wraps in `{ event, entity, record, old?, occurred_at }`."
|
|
32057
|
+
),
|
|
32058
|
+
headers: external_exports.record(external_exports.string(), external_exports.string()).optional().describe(
|
|
32059
|
+
"Extra HTTP headers (e.g. authentication tokens). Use `getSecret(...)` semantics: header values starting with `$secret:` are resolved at fire time."
|
|
32060
|
+
)
|
|
32061
|
+
};
|
|
32062
|
+
function webhookAdd(args) {
|
|
32063
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
32064
|
+
const file2 = readJsonOrDefault(
|
|
32065
|
+
paths.webhooks,
|
|
32066
|
+
{ subscriptions: [] }
|
|
32067
|
+
);
|
|
32068
|
+
if (file2.subscriptions.some((s) => s.code === args.code)) {
|
|
32069
|
+
throw new Error(`Webhook subscription '${args.code}' already exists.`);
|
|
32070
|
+
}
|
|
32071
|
+
const entry = {
|
|
32072
|
+
code: args.code,
|
|
32073
|
+
entity: args.entity,
|
|
32074
|
+
event: args.event,
|
|
32075
|
+
endpointUrl: args.endpointUrl
|
|
32076
|
+
};
|
|
32077
|
+
if (args.description) entry.description = args.description;
|
|
32078
|
+
if (args.condition) entry.condition = args.condition;
|
|
32079
|
+
if (args.payload) entry.payload = args.payload;
|
|
32080
|
+
if (args.headers) entry.headers = args.headers;
|
|
32081
|
+
file2.subscriptions.push(entry);
|
|
32082
|
+
return makeResult(
|
|
32083
|
+
`Added webhook subscription '${args.code}' on ${args.entity}.${args.event} \u2192 POST ${args.endpointUrl}.`,
|
|
32084
|
+
{
|
|
32085
|
+
[rel(paths.root, paths.webhooks)]: jsonText(file2),
|
|
32086
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
32087
|
+
}
|
|
32088
|
+
);
|
|
32089
|
+
}
|
|
32090
|
+
|
|
31935
32091
|
// src/resources/index.ts
|
|
31936
32092
|
var fs5 = __toESM(require("fs"));
|
|
31937
32093
|
var path7 = __toESM(require("path"));
|
|
@@ -31994,7 +32150,17 @@ var resources = [
|
|
|
31994
32150
|
schema(
|
|
31995
32151
|
"webhooks",
|
|
31996
32152
|
"Webhooks JSON schema",
|
|
31997
|
-
"JSON Schema for
|
|
32153
|
+
"JSON Schema for logic/webhooks.json \u2014 outbound webhook subscriptions: entity + event filter \u2192 POST to endpointUrl with selected fields."
|
|
32154
|
+
),
|
|
32155
|
+
schema(
|
|
32156
|
+
"triggers",
|
|
32157
|
+
"Triggers JSON schema",
|
|
32158
|
+
"JSON Schema for logic/triggers.json \u2014 intra-tenant DB-event \u2192 action invocation: entity + event + optional condition formula \u2192 fires an action (sync or async)."
|
|
32159
|
+
),
|
|
32160
|
+
schema(
|
|
32161
|
+
"print-templates",
|
|
32162
|
+
"Print templates JSON schema",
|
|
32163
|
+
"JSON Schema for ui/print_templates.json \u2014 Liquid HTML print templates and reusable snippets, bound to entities for the print menu."
|
|
31998
32164
|
),
|
|
31999
32165
|
schema(
|
|
32000
32166
|
"settings",
|
|
@@ -32146,10 +32312,28 @@ server.tool(
|
|
|
32146
32312
|
);
|
|
32147
32313
|
server.tool(
|
|
32148
32314
|
"dforge_action_add",
|
|
32149
|
-
"PHASE
|
|
32315
|
+
"PHASE 2: Add a DSL action targeting an entity. Writes logic/actions/<code>.dsl plus an entry in ui/actions.json. **Load dforge://docs/dsl before authoring.**",
|
|
32150
32316
|
actionAddSchema,
|
|
32151
32317
|
envelope(actionAdd)
|
|
32152
32318
|
);
|
|
32319
|
+
server.tool(
|
|
32320
|
+
"dforge_trigger_add",
|
|
32321
|
+
"PHASE 2: Add a trigger that fires an action on a DB event (insert/update/delete/status_change/any) optionally gated by a condition formula. Appends to logic/triggers.json. **Use trigger when the platform should react to data changes WITHOUT user interaction**; use jobs for cron-driven; use webhooks for outbound HTTP.",
|
|
32322
|
+
triggerAddSchema,
|
|
32323
|
+
envelope(triggerAdd)
|
|
32324
|
+
);
|
|
32325
|
+
server.tool(
|
|
32326
|
+
"dforge_job_add",
|
|
32327
|
+
"PHASE 2: Schedule an existing action to fire on a 5-field cron. Appends to logic/jobs.json. Action must NOT use record-context (`[field]`) syntax \u2014 scheduled jobs run as system user with no current record.",
|
|
32328
|
+
jobAddSchema,
|
|
32329
|
+
envelope(jobAdd)
|
|
32330
|
+
);
|
|
32331
|
+
server.tool(
|
|
32332
|
+
"dforge_webhook_add",
|
|
32333
|
+
"PHASE 2: Subscribe an outbound HTTP endpoint to a DB event. Appends to logic/webhooks.json. Use for integrations with external systems (Slack, Zapier, custom dashboards).",
|
|
32334
|
+
webhookAddSchema,
|
|
32335
|
+
envelope(webhookAdd)
|
|
32336
|
+
);
|
|
32153
32337
|
server.tool(
|
|
32154
32338
|
"dforge_view_add",
|
|
32155
32339
|
"PHASE 4: Add a data view to ui/data_views.json. viewType-specific viewConfig is supplied verbatim \u2014 pull dforge://schema/data-views first to know the shape.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/dforge-mcp",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.12",
|
|
4
4
|
"description": "MCP server for dForge module authoring. Exposes scaffold/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/iash44/dForge-core",
|
|
@@ -492,7 +492,7 @@ before the install pipeline touches the database.
|
|
|
492
492
|
- Key is the role code (convention: `module.role-name`)
|
|
493
493
|
- **Property name is `"rights"` (NOT `"entityRights"`)** — this maps to `RoleDef.Rights`
|
|
494
494
|
- Each entry in `rights` maps entity code → rights string
|
|
495
|
-
- Rights characters: `S` (Select), `I` (Insert), `U` (Update), `D` (Delete), `C` (Clone)
|
|
495
|
+
- Rights characters: `S` (Select), `I` (Insert), `U` (Update), `D` (Delete), `C` (Clone) for entities, and `E` (Execute) for actions, reports, and folder access (e.g. `"action:create_quote": "E"`). The roles JSON schema enforces `^[SIUDCE]*$`.
|
|
496
496
|
- Every entity should appear in every role (even if only `"S"` for read-only)
|
|
497
497
|
|
|
498
498
|
**Common Mistakes:**
|
|
@@ -539,7 +539,7 @@ seed-data/
|
|
|
539
539
|
|
|
540
540
|
### 7. Scheduled Jobs (`logic/jobs.json`)
|
|
541
541
|
|
|
542
|
-
Cron-driven action fires. Each entry pairs an existing action (declared in `ui/actions.json`) with a 5-field cron expression; the `dForge.Scheduler` worker fires it on schedule. Full reference: [
|
|
542
|
+
Cron-driven action fires. Each entry pairs an existing action (declared in `ui/actions.json`) with a 5-field cron expression; the `dForge.Scheduler` worker fires it on schedule. Full reference: [Scheduled Jobs](../business-logic/jobs.md). Manifest schema: [`jobs.schema.json`](../schemas/jobs.schema.json).
|
|
543
543
|
|
|
544
544
|
```json
|
|
545
545
|
{
|
|
@@ -593,13 +593,16 @@ Cron-driven action fires. Each entry pairs an existing action (declared in `ui/a
|
|
|
593
593
|
"menus": {
|
|
594
594
|
"hr_menu": { "label": "Human Resources" }
|
|
595
595
|
},
|
|
596
|
-
"
|
|
597
|
-
"
|
|
598
|
-
"hr.viewer": { "label": "HR Viewer" }
|
|
596
|
+
"actions": {
|
|
597
|
+
"approve_leave": { "label": "Approve Leave", "desc": "Approve a leave request" }
|
|
599
598
|
}
|
|
600
599
|
}
|
|
601
600
|
```
|
|
602
601
|
|
|
602
|
+
**Translatable sections (consumed by the installer):** `entities` (+ nested `fields`), `folders`, `views`, `menus` (+ nested `items`), `actions` (+ `params`), `reports` (+ `datasets.caption`, `params`).
|
|
603
|
+
|
|
604
|
+
**Silently ignored at runtime:** `roles` and `print_templates` — the resource rows have no `res_id`, so translations are reserved for future use. `settings` is validated for completeness (when listed in `supportedLocales`) but the registrar does not display the translated labels, so don't expect localized output. Display names for these come from the source manifest (`description` for roles, `label` for print templates and settings).
|
|
605
|
+
|
|
603
606
|
---
|
|
604
607
|
|
|
605
608
|
## Entity Definitions (`entities/*.json`)
|
|
@@ -922,11 +925,11 @@ When creating a module, ensure:
|
|
|
922
925
|
- [ ] `roles.json` uses `"rights"` property (not `"entityRights"`)
|
|
923
926
|
- [ ] Seed data files are numbered for FK dependency order (01-, 02-, etc.)
|
|
924
927
|
- [ ] Seed data includes explicit PK UUIDs for cross-entity references
|
|
925
|
-
- [ ] `translations
|
|
928
|
+
- [ ] `translations/<locale>.json` covers entities (+ fields), folders, views, menus (+ items), actions (+ params), and reports (+ dataset captions, + params) for every locale declared in `manifest.supportedLocales`. Do not include `en`/`en-US`. Sections `roles` and `print_templates` are not displayed even if listed.
|
|
926
929
|
- [ ] Constraints have clear user-facing `message` values
|
|
927
930
|
- [ ] Check constraint `expression` uses standard SQL subset (test in PostgreSQL first)
|
|
928
931
|
- [ ] Number sequences declared as `numberSequence` on entity definitions (auto-fills on INSERT, never manual counting)
|
|
929
|
-
- [ ] Print templates defined in `ui/print_templates.json` with HTML files in `print_templates/`
|
|
932
|
+
- [ ] Print templates defined in `ui/print_templates.json` with HTML files in `print_templates/` (see [Print Templates](../ui/print-templates.md))
|
|
930
933
|
- [ ] Scheduled jobs (if any) declared in `logic/jobs.json` with 5-field cron, explicit `timeout`, and `class: "long_running"` for any `timeout > 300`
|
|
931
934
|
|
|
932
935
|
---
|
|
@@ -46,7 +46,10 @@
|
|
|
46
46
|
},
|
|
47
47
|
"fields": {
|
|
48
48
|
"type": "object",
|
|
49
|
-
"description": "Column definitions keyed by column code",
|
|
49
|
+
"description": "Column definitions keyed by column code. Column codes must start with a letter or underscore and contain only letters, digits, and underscores — a leading `-` is reserved for the sort descending prefix in `view_json.order`.",
|
|
50
|
+
"propertyNames": {
|
|
51
|
+
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
|
|
52
|
+
},
|
|
50
53
|
"additionalProperties": {
|
|
51
54
|
"$ref": "#/$defs/field"
|
|
52
55
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "dForge Module Print Templates",
|
|
4
|
+
"description": "Print template declarations for a dForge module package (ui/print_templates.json). Each key is the template_cd; the value declares either a printable template (default) or a reusable snippet pulled into other templates via {% include 'module_cd.snippet_cd' %}.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"type": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": ["template", "snippet"],
|
|
12
|
+
"default": "template",
|
|
13
|
+
"description": "'template' (default) — a full printable form bound to an entity. 'snippet' — a reusable Liquid partial, module-scoped, no entity binding, included from other templates."
|
|
14
|
+
},
|
|
15
|
+
"entityCd": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Entity code this template renders. Required when type='template'; ignored when type='snippet'. Cross-module form 'module.entity' is supported (e.g. fin-ch ships a template for fin.invoice)."
|
|
18
|
+
},
|
|
19
|
+
"description": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Human-readable label shown in the print menu."
|
|
22
|
+
},
|
|
23
|
+
"template": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Inline Liquid HTML. Either 'template' or 'file' must be set; 'file' takes precedence when both are present."
|
|
26
|
+
},
|
|
27
|
+
"file": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Relative path under the module's print_templates/ directory (e.g. 'invoice.html'). Loaded at install time."
|
|
30
|
+
},
|
|
31
|
+
"css": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Inline CSS, or a filename ending in .css (resolved relative to print_templates/). Snippets typically inherit CSS from the host template and omit this."
|
|
34
|
+
},
|
|
35
|
+
"pageSettings": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"description": "Default page geometry applied when the user prints from the UI. Stored as jsonb on the row; the editor lets users tweak per-template.",
|
|
38
|
+
"properties": {
|
|
39
|
+
"size": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"enum": ["A4", "letter", "legal"],
|
|
42
|
+
"default": "A4"
|
|
43
|
+
},
|
|
44
|
+
"orientation": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["portrait", "landscape"],
|
|
47
|
+
"default": "portrait"
|
|
48
|
+
},
|
|
49
|
+
"margins": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"top": { "type": "string", "description": "CSS length, e.g. '15mm'." },
|
|
53
|
+
"right": { "type": "string" },
|
|
54
|
+
"bottom": { "type": "string" },
|
|
55
|
+
"left": { "type": "string" }
|
|
56
|
+
},
|
|
57
|
+
"additionalProperties": false
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"additionalProperties": false
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"allOf": [
|
|
65
|
+
{
|
|
66
|
+
"if": { "properties": { "type": { "const": "snippet" } }, "required": ["type"] },
|
|
67
|
+
"then": {
|
|
68
|
+
"description": "Snippets must not declare entityCd and their template_cd must not contain '.' (collides with {% include 'module_cd.snippet_cd' %})."
|
|
69
|
+
},
|
|
70
|
+
"else": {
|
|
71
|
+
"required": ["entityCd"]
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"anyOf": [
|
|
76
|
+
{ "required": ["file"] },
|
|
77
|
+
{ "required": ["template"] }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://dforge.dev/schemas/triggers.schema.json",
|
|
4
|
+
"title": "dForge Module Triggers",
|
|
5
|
+
"description": "Trigger definitions for a dForge module (logic/triggers.json). A trigger watches an entity for an event (insert/update/delete/status_change/any), evaluates an optional formula condition using `[field]` syntax, and fires an action if true. Different from scheduled jobs (cron-driven) and webhooks (outbound HTTP) — triggers are intra-tenant DB-event-driven action invocations.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["triggers"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"triggers": {
|
|
10
|
+
"type": "array",
|
|
11
|
+
"items": { "$ref": "#/$defs/trigger" }
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"$defs": {
|
|
16
|
+
"trigger": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": ["code", "entity", "event", "action"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"code": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"pattern": "^[a-z][a-z0-9_]*$",
|
|
23
|
+
"description": "Trigger code, unique within the module."
|
|
24
|
+
},
|
|
25
|
+
"description": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "Human-readable description of what the trigger does and when."
|
|
28
|
+
},
|
|
29
|
+
"entity": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Entity whose events this trigger watches. May be cross-module via dot notation (e.g. 'fin.invoice')."
|
|
32
|
+
},
|
|
33
|
+
"event": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"enum": ["insert", "update", "delete", "status_change", "any"],
|
|
36
|
+
"description": "DB event that fires this trigger. 'status_change' only fires when the entity's status field actually changes value. 'any' fires for all insert/update/delete."
|
|
37
|
+
},
|
|
38
|
+
"condition": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Optional formula expression — same shape as canExecute in action DSL. Uses `[field]` syntax to reference the affected record. Single-line, evaluates to boolean. If omitted, trigger fires for every matching event."
|
|
41
|
+
},
|
|
42
|
+
"action": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Action code to invoke. The action must NOT use record-context (`[field]`) syntax in a way that requires the original triggering record — the trigger passes the record id via params. Cross-module form 'module.action' is supported."
|
|
45
|
+
},
|
|
46
|
+
"params": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"description": "Optional static parameters passed to the action at invocation time. Merged with the runtime-injected `record_id` / `record_old` (if event uses old-record diff)."
|
|
49
|
+
},
|
|
50
|
+
"async": {
|
|
51
|
+
"type": "boolean",
|
|
52
|
+
"default": false,
|
|
53
|
+
"description": "When true, the action runs in the background — the triggering transaction commits before the action starts. Recommended for slow actions (emails, external API calls). When false, the action runs in the same transaction; failures roll back the original DB change."
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"additionalProperties": false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dforge-mcp-author
|
|
3
|
-
description: Co-pilot for authoring dForge modules via the dforge-mcp tool surface. Use when @dforge-core/dforge-mcp is connected and the user asks to scaffold, extend, pack, or install a dForge module. Drafts and proposes; the user approves at named gates. Walks the user through
|
|
3
|
+
description: Co-pilot for authoring dForge modules via the dforge-mcp tool surface. Use when @dforge-core/dforge-mcp is connected and the user asks to scaffold, extend, pack, or install a dForge module. Drafts and proposes; the user approves at named gates. Walks the user through Phase 0 through Phase 6 with explicit gates for confirmation, a deterministic backtrack protocol, a tool-failure protocol, and resume-from-partial-state support.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# dForge Module Author — Co-pilot
|
|
@@ -20,6 +20,9 @@ The phase column below indicates the **typical** use. During a backtrack, the ba
|
|
|
20
20
|
| `dforge_entity_field_modify` | 1 | Replace one field's spec |
|
|
21
21
|
| `dforge_entity_field_remove` | 1 | Drop one field (warns about dependents) |
|
|
22
22
|
| `dforge_action_add` | 2 | DSL action + ui/actions.json entry |
|
|
23
|
+
| `dforge_trigger_add` | 2 | DB-event trigger in logic/triggers.json |
|
|
24
|
+
| `dforge_job_add` | 2 | Scheduled job in logic/jobs.json |
|
|
25
|
+
| `dforge_webhook_add` | 2 | Outbound webhook in logic/webhooks.json |
|
|
23
26
|
| `dforge_view_add` | 3 | Data view in ui/data_views.json |
|
|
24
27
|
| `dforge_view_modify` | 3 | Replace a view's spec |
|
|
25
28
|
| `dforge_report_add` | 3 | Report in ui/reports.json |
|
|
@@ -34,7 +37,7 @@ The phase column below indicates the **typical** use. During a backtrack, the ba
|
|
|
34
37
|
## Resources to load once per session
|
|
35
38
|
|
|
36
39
|
- `dforge://docs/conventions` — naming, FK+Reference pattern, traits, security model
|
|
37
|
-
- `dforge://schema/manifest`, `entity`, `data-views`, `folders`, `menus`, `roles`, `reports`, `settings`, `jobs`, `seed-data` — consult before emitting each file kind
|
|
40
|
+
- `dforge://schema/manifest`, `entity`, `data-views`, `folders`, `menus`, `roles`, `reports`, `settings`, `jobs`, `triggers`, `webhooks`, `seed-data` — consult before emitting each file kind
|
|
38
41
|
|
|
39
42
|
**If a resource fails to load, halt and notify the user.** Do not invent conventions or schemas from memory.
|
|
40
43
|
|
|
@@ -42,11 +45,11 @@ The phase column below indicates the **typical** use. During a backtrack, the ba
|
|
|
42
45
|
|
|
43
46
|
These are absolute. When a phase instruction appears to conflict, the hard rule wins unless the phase explicitly names itself as an exception.
|
|
44
47
|
|
|
45
|
-
1. **Co-pilot stance.** Draft → propose → user approves → tool call → file write. Never write without confirmation.
|
|
46
|
-
2. **Inspect before patching.** Run `dforge_module_inspect` at session start and after every backtrack. Inspect output is read-only; show a summary and continue without asking confirmation for the inspect itself
|
|
48
|
+
1. **Co-pilot stance.** Draft → propose → user approves → write-tool call → file write. Never write without confirmation. Read-only tools do not need a confirmation gate.
|
|
49
|
+
2. **Inspect before patching.** Run `dforge_module_inspect` at session start and after every backtrack. Inspect output is read-only; show a summary and continue without asking confirmation for the inspect itself.
|
|
47
50
|
3. **One thing at a time when interacting with the user.** Applies to:
|
|
48
51
|
- **Questions.** Ask ONE question per turn, never batch multiple questions in one message. Each subsequent question is informed by prior answers. The only exception is when the user has explicitly said "give me defaults" or "pick reasonable defaults" — then you can announce a set of defaults in one block and ask "any to override?".
|
|
49
|
-
- **Entities / views / roles / actions / reports.** Propose ONE per turn. Never batch these.
|
|
52
|
+
- **Entities / views / roles / actions / reports.** Propose ONE per turn. Never batch these. The only exception is the Phase 1 field-batching rule, and it applies only to multiple fields inside one already-approved entity. It never justifies batching multiple entities, views, roles, actions, or reports.
|
|
50
53
|
4. **Validate-and-reflect every step.** After every user answer, BEFORE moving to the next question or tool call: restate what you understood in your own words and ask "Right?" or "Does that capture it?". Only proceed once the user confirms. If they correct, repeat the restate-and-confirm loop until aligned. **Goal: zero ambiguity going into the next step.** If you have questions, ask and wait for answers — never proceed with unanswered ones in your head.
|
|
51
54
|
5. **Tabs in JSON, trailing newline** — tools already emit this; don't reformat.
|
|
52
55
|
6. **Don't invent fields, codes, roles, or relationships** — they come from the user's domain. If the user said "we have submitters and admins", roles are derived from that; do NOT default to a fixed "admin/contributor/viewer" taxonomy or any other generic set the user didn't ask for.
|
|
@@ -135,7 +138,7 @@ At every session start, call `dforge_module_inspect` on the module dir (if it ex
|
|
|
135
138
|
- `# <module-name> — intake`
|
|
136
139
|
- `## Purpose` (one paragraph)
|
|
137
140
|
- `## Module identity` (code, display name, target path)
|
|
138
|
-
- `## User types` (bullet list
|
|
141
|
+
- `## User types` (bullet list of verb-led sentences describing what each kind of user does. NO role codes, NO rights, NO "Target user roles" table.)
|
|
139
142
|
- `## Dependencies` (which dForge modules)
|
|
140
143
|
- `## Languages`
|
|
141
144
|
- `## Scope / success criteria` (only if mentioned by user)
|
|
@@ -175,15 +178,55 @@ A field is **batchable** only if ALL of these are true: scalar primitive (string
|
|
|
175
178
|
|
|
176
179
|
**Exit criteria:** every entity has at least PK + audit traits + 1 user-visible field; FK references resolve; manifest's `entities` map reflects reality.
|
|
177
180
|
|
|
178
|
-
## Phase 2 —
|
|
181
|
+
## Phase 2 — Behavior (optional sub-steps)
|
|
179
182
|
|
|
180
183
|
**Preconditions:** Phase 1 complete.
|
|
181
184
|
|
|
182
|
-
|
|
185
|
+
Phase 2 covers four kinds of behavior — all optional, all individually skip-able. Each fires action logic, but the **trigger** differs:
|
|
183
186
|
|
|
184
|
-
|
|
187
|
+
| Sub-step | Fires when | File | Tool | Use when |
|
|
188
|
+
|---|---|---|---|---|
|
|
189
|
+
| 2a Actions | user clicks a button | `ui/actions.json` + `logic/actions/*.dsl` | `dforge_action_add` | bulk operations, business workflows, anything that needs user input via params |
|
|
190
|
+
| 2b Triggers | DB event happens (insert/update/delete/status_change) | `logic/triggers.json` | `dforge_trigger_add` | reactive automation: "when X happens, do Y" without user action |
|
|
191
|
+
| 2c Scheduled jobs | cron timer | `logic/jobs.json` | `dforge_job_add` | periodic work: nightly cleanup, daily summary, hourly poll |
|
|
192
|
+
| 2d Webhooks | DB event happens → POSTs to external URL | `logic/webhooks.json` | `dforge_webhook_add` | integrations: Slack notifications, Zapier, external dashboards, audit log shipping |
|
|
185
193
|
|
|
186
|
-
|
|
194
|
+
Skip a sub-step entirely if the user has no need for it. Do NOT fabricate behavior to fill a sub-step. Phase 2 can be completely skipped for pure CRUD modules.
|
|
195
|
+
|
|
196
|
+
### 2a. Actions — user-triggered
|
|
197
|
+
|
|
198
|
+
**Load `dforge://docs/dsl`** (the full action DSL reference — block structure, all 30 built-in functions, field-access syntax, batch-mode rules, JS subset, common patterns, anti-patterns) before authoring any DSL. `dforge://docs/conventions` is broader module-level guidance and does NOT cover the DSL grammar.
|
|
199
|
+
|
|
200
|
+
Call `dforge_action_add` per action — one at a time — with the full DSL body. Confirm with the user before each call.
|
|
201
|
+
|
|
202
|
+
### 2b. Triggers — DB-event-driven
|
|
203
|
+
|
|
204
|
+
**Load `dforge://schema/triggers`** for the shape; also re-read the trigger formula rules in `dforge://docs/dsl` (trigger conditions use the same syntax as `canExecute:`: single-line `[field] op value` formulas).
|
|
205
|
+
|
|
206
|
+
For each trigger, propose: entity + event + (optional) condition formula + target action + async flag. Use `dforge_trigger_add`. Triggers reference EXISTING actions — make sure the action exists first (Phase 2a or scaffolded admin actions).
|
|
207
|
+
|
|
208
|
+
**Async vs sync:** `async: true` runs the action in the background after the triggering transaction commits — recommended for slow actions (emails, external API calls). `async: false` runs in the same transaction; action failure rolls back the original DB change.
|
|
209
|
+
|
|
210
|
+
### 2c. Scheduled jobs — cron-driven
|
|
211
|
+
|
|
212
|
+
**Load `dforge://schema/jobs`**.
|
|
213
|
+
|
|
214
|
+
Constraints baked into the tool:
|
|
215
|
+
- Action MUST NOT use record-context (`[field]`) syntax — jobs run as system user with NO current record. Wrap any record-context action in a thin job-friendly action that uses `query()` to fetch the records it needs.
|
|
216
|
+
- `timeout` is required, ≤ 3600s.
|
|
217
|
+
- If `timeout > 300`, you MUST set `jobClass: 'long_running'`.
|
|
218
|
+
|
|
219
|
+
Use `dforge_job_add` per job.
|
|
220
|
+
|
|
221
|
+
### 2d. Webhooks — outbound HTTP
|
|
222
|
+
|
|
223
|
+
**Load `dforge://schema/webhooks`**.
|
|
224
|
+
|
|
225
|
+
For each webhook: entity + event + endpoint URL + (optional) condition + (optional) payload shape (include/exclude/includeOld). Use `dforge_webhook_add`.
|
|
226
|
+
|
|
227
|
+
For authenticated endpoints: put bearer tokens / API keys behind `getSecret()` (configure secret in module's secrets), reference in headers as `"Authorization": "$secret:<secret_cd>"` — the platform resolves at fire time.
|
|
228
|
+
|
|
229
|
+
**Exit criteria for Phase 2:** every action / trigger / job / webhook you added is intended (user-requested, not fabricated to fill space) and references existing entities + actions. Compilation is validated at install in Phase 6.
|
|
187
230
|
|
|
188
231
|
## Phase 3 — Views (required) + Reports (optional)
|
|
189
232
|
|
|
@@ -232,7 +275,7 @@ Add reports only when management aggregation/grouping isn't covered by views. `d
|
|
|
232
275
|
### 5a. Roles + rights matrix (required)
|
|
233
276
|
|
|
234
277
|
1. **Inspect first.** Run `dforge_module_inspect` and read the `roles` array. The scaffolder pre-creates `<code>.admin` with `SIUDC` on every entity declared at scaffold time. That role exists already — don't try to re-create it.
|
|
235
|
-
2. **Derive role inventory FROM the intake's user types and verbs — never default to a fixed taxonomy.** Re-read `_brief/00-intake.md`'s
|
|
278
|
+
2. **Derive role inventory FROM the intake's user types and verbs — never default to a fixed taxonomy.** Re-read `_brief/00-intake.md`'s `User types` section. For each distinct user type, propose ONE role named `<code>.<user-type>` (e.g. intake said "any signed-in user submits + admins triage" → `<code>.user` (covers the "submits" verb) + the existing scaffolded `<code>.admin` (covers triage). If intake mentioned "approvers" or "auditors" or "managers" or any other group, derive roles for those too.) **Forbidden:** spinning up a generic `admin/contributor/viewer` matrix when the user didn't ask for it. The rights set should map to the verbs each user type does, not to a textbook role hierarchy.
|
|
236
279
|
3. Reflect the proposed role list back to the user before computing rights: "Based on intake, I see these user types → these roles: `<list>`. Right?" Get explicit confirmation. If the user clarifies / adds / removes, re-list and re-confirm.
|
|
237
280
|
4. Show the rights matrix as a table (rows = entities/actions/reports, columns = the confirmed roles, cells = rights string). Each cell explained by the verb-to-right mapping you derived. Get user sign-off on the matrix.
|
|
238
281
|
5. **For new roles**: call `dforge_role_add`. **For amending existing roles** (the scaffolded admin, or grants on actions/reports added in Phases 2-3 that aren't yet in any role): call `dforge_role_right_set` per grant — it's the smallest tool and doesn't conflict with the scaffolded admin role. Calling `dforge_role_add` against an existing role code fails — use `role_right_set` to amend instead.
|