From 8474ea276a0c748890b0a46001985df34ffd5498 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 05:17:01 +0000 Subject: [PATCH 1/2] docs(skills): teach one naming rule, every package-authored metadata name starts with its namespace State the rule once in objectstack-platform beside the object rule, with the four exceptions (fields, object extensions, container views, translations) and what main enforces today. Rename every bare non-object name the skills' examples and evals show or generate to the crm_ prefix (or the snippet's own package namespace), cross-references included. Token ratchets are paid by same-file deletion of restatements. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cw4B24PbS5MZbX1r5j --- ...0805-skills-namespace-prefix-every-name.md | 12 ++++++ skills/README.md | 7 +++- skills/objectstack-ai/SKILL.md | 6 +-- .../evals/skills-tools-knowledge.json | 12 +++--- skills/objectstack-automation/SKILL.md | 10 ++--- .../evals/approvals/test-revise-loop.md | 2 +- .../evals/flows-triggers-approvals.json | 14 +++---- .../references/examples-flows.md | 4 +- .../references/integration-recipes.md | 2 +- .../state-machines-and-approvals.md | 16 ++++---- skills/objectstack-data/SKILL.md | 6 +-- .../hooks-security-seeds-datasources.json | 2 +- .../objectstack-data/references/data-hooks.md | 16 ++++---- .../references/examples-objects.md | 4 +- skills/objectstack-data/rules/security.md | 7 ++-- skills/objectstack-data/rules/validation.md | 4 +- skills/objectstack-i18n/SKILL.md | 2 +- .../evals/bundles-locales-coverage.json | 12 +++--- skills/objectstack-platform/SKILL.md | 10 +++-- .../evals/config-plugins-ops.json | 6 +-- .../evals/report-period-export.json | 4 +- .../evals/views-apps-actions-pages.json | 8 ++-- skills/objectstack-ui/rules/actions.md | 18 ++++----- skills/objectstack-ui/rules/dashboards.md | 37 +++++++++---------- skills/objectstack-ui/rules/navigation.md | 8 ++-- skills/objectstack-ui/rules/pages.md | 7 ++-- .../references/examples-upgrade.md | 4 +- skills/objectui/rules/protocol.md | 2 +- 28 files changed, 128 insertions(+), 114 deletions(-) create mode 100644 .changeset/30805-skills-namespace-prefix-every-name.md diff --git a/.changeset/30805-skills-namespace-prefix-every-name.md b/.changeset/30805-skills-namespace-prefix-every-name.md new file mode 100644 index 00000000000..217aa75edef --- /dev/null +++ b/.changeset/30805-skills-namespace-prefix-every-name.md @@ -0,0 +1,12 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): the authoring skills teach one naming rule, every metadata name a package authors starts with its namespace + +Clause-②: no +Metadata-protocol: none + +- **Before.** `objectstack-platform` stated the prefix rule for objects only, and claimed `os lint` warns on bare app, page, dashboard, flow, action, report and dataset names whenever a namespace is set. The examples across the skills named flows, actions, hooks, pages, dashboards, reports, datasets, queues, positions, permission sets, assignment rules, agents and skills bare (`lead_conversion`, `sales_rep`, `support_intake`). +- **Now.** `objectstack-platform` states the rule once, beside the object rule: every metadata name a package authors starts with its namespace (`crm_send_email`, `crm_sales_rep`). The exceptions are fields, object extensions, container views and translations (`_`). It also says what is enforced today: `defineStack` refuses a bare object name, `os build` refuses a bare doc name, and `os lint`'s `naming/namespace-prefix` warns only on a name one package declares twice. Every example and eval that showed or generated a bare non-object name now carries `crm_` (or its package's namespace where the snippet has one), cross-references included. +- Connector ids, the inbound api flow whose name is part of its webhook URL, and the datasource whose name keys its password variable keep their names. No schema, key or behaviour changes. diff --git a/skills/README.md b/skills/README.md index 2f255bd7e30..14be5e05e67 100644 --- a/skills/README.md +++ b/skills/README.md @@ -95,7 +95,12 @@ carries `SKILL.md` alone — `gen:skill-refs` only visits skills listed in its ## Conventions enforced across skills - **Zod first.** Never invent types — read `node_modules/@objectstack/spec/src/**/*.zod.ts`. -- **Short object names** (`account`, `task`); no `namespace`, no `tableName`. +- **Every non-object name carries the namespace** (`crm_send_email`, `crm_sales_rep`); + a snippet with no manifest uses `crm`. Exempt: fields, object extensions, + container views, translations (`_`). The one rule lives in + `objectstack-platform/SKILL.md`. +- **Short object names in snippets** (`account`, `task`); no object-level + `namespace`, no `tableName`. - **CEL for all expressions** — predicates, conditions, schedules. Use the `F\`\``, `P\`\``, `cel\`\``, `cron\`\``, `tmpl\`\`` tagged templates from `@objectstack/spec`. Legacy `OLD` / `NEW` are not CEL — use diff --git a/skills/objectstack-ai/SKILL.md b/skills/objectstack-ai/SKILL.md index bf691e87887..acf4002ffeb 100644 --- a/skills/objectstack-ai/SKILL.md +++ b/skills/objectstack-ai/SKILL.md @@ -94,7 +94,7 @@ copilot claiming abilities it did not have. import { defineSkill } from '@objectstack/spec'; export default defineSkill({ - name: 'case_management', + name: 'crm_case_management', label: 'Case Management', description: 'Triage, query, and escalate support cases.', instructions: ` @@ -104,8 +104,8 @@ export default defineSkill({ - Escalated cases must summarise the actions already taken. `, // Every name resolves: two platform tools (rung 2) plus one action tool - // materialised from this stack's own `escalate_case` Action (rung 3). - tools: ['query_records', 'get_record', 'action_escalate_case'], + // materialised from this stack's own `crm_escalate_case` Action (rung 3). + tools: ['query_records', 'get_record', 'action_crm_escalate_case'], triggerConditions: [ { field: 'objectName', operator: 'eq', value: 'support_case' }, ], diff --git a/skills/objectstack-ai/evals/skills-tools-knowledge.json b/skills/objectstack-ai/evals/skills-tools-knowledge.json index 3631cb7eb29..4f0f2af6c20 100644 --- a/skills/objectstack-ai/evals/skills-tools-knowledge.json +++ b/skills/objectstack-ai/evals/skills-tools-knowledge.json @@ -3,11 +3,11 @@ "evals": [ { "id": 1, - "prompt": "Add `src/skills/case-triage.skill.ts` for our support app: the assistant should be able to query and read support cases and run our existing `escalate_case` Action on a case. Only activate it when the user is working on the `support_case` object.", - "expected_output": "Emits a `defineSkill` with a snake_case `name`, `label`, `instructions`, and a `tools` list where every name resolves: platform tools `query_records` / `get_record` plus the action tool `action_escalate_case` materialised from the stack's own Action. Routing is a `triggerConditions` entry `{ field: 'objectName', operator: 'eq', value: 'support_case' }` (a string value for `eq`). No `permissions` key on the skill (removed in 17) and no `*.agent.ts` file.", + "prompt": "Add `src/skills/case-triage.skill.ts` for our support app: the assistant should be able to query and read support cases and run our existing `crm_escalate_case` Action on a case. Only activate it when the user is working on the `support_case` object.", + "expected_output": "Emits a `defineSkill` with a namespace-prefixed snake_case `name`, `label`, `instructions`, and a `tools` list where every name resolves: platform tools `query_records` / `get_record` plus the action tool `action_crm_escalate_case` materialised from the stack's own Action. Routing is a `triggerConditions` entry `{ field: 'objectName', operator: 'eq', value: 'support_case' }` (a string value for `eq`). No `permissions` key on the skill (removed in 17) and no `*.agent.ts` file.", "files": [], "assertions": { - "must_contain": ["defineSkill", "tools", "action_escalate_case", "triggerConditions"], + "must_contain": ["defineSkill", "tools", "action_crm_escalate_case", "triggerConditions"], "must_not_contain": ["permissions:", "defineAgent("] } }, @@ -33,8 +33,8 @@ }, { "id": 4, - "prompt": "Write a `*.tool.ts` so the assistant can call our `close_case` action on a support case.", - "expected_output": "Does not author a tool record: the default path is to opt the Action in with `ai: { exposed: true, description: '<40+ chars>' }` on the object's action, which materialises `action_close_case`, and to name that tool in a skill's `tools`. `defineTool` is only for presentation refinement and cannot make anything executable (no `handler` / `implementation`; `ToolSchema` is strict).", + "prompt": "Write a `*.tool.ts` so the assistant can call our `crm_close_case` action on a support case.", + "expected_output": "Does not author a tool record: the default path is to opt the Action in with `ai: { exposed: true, description: '<40+ chars>' }` on the object's action, which materialises `action_crm_close_case`, and to name that tool in a skill's `tools`. `defineTool` is only for presentation refinement and cannot make anything executable (no `handler` / `implementation`; `ToolSchema` is strict).", "files": [], "assertions": { "must_contain": ["exposed", "description", "action_", "tools"], @@ -43,7 +43,7 @@ }, { "id": 5, - "prompt": "Create `src/ai/support-bot.agent.ts` with defineAgent({ name: 'support_bot', tools: ['query_records', 'action_close_case'], knowledge: ['support_kb'] }) and register it under agents.", + "prompt": "Create `src/ai/support-bot.agent.ts` with defineAgent({ name: 'crm_support_bot', tools: ['query_records', 'action_crm_close_case'], knowledge: ['support_kb'] }) and register it under agents.", "expected_output": "Refuses the agent: the `agent` type is closed to third parties (a stack-authored agent is filtered out, refused by `loadAgent()` and 404s on chat), and `agent.tools` / `agent.knowledge` are protocol-17 tombstones. Re-declares the two tools in a `defineSkill` (registered under `skills`) with a `surface`, and restricts retrieval at the knowledge-source level (`aiExposed`, `where`) rather than on an agent. The runtime's `ask` / `build` agents pick the skill up by surface.", "files": [], "assertions": { diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index d1729444fc6..d040de3b9ab 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -292,7 +292,7 @@ write them right the first time: ## Assignment rules — route a new record to a queue or a person -The third automation family: `assignmentRules` on the stack routes each inserted +`assignmentRules` on the stack routes each inserted row of an `ownership: 'user'` object whose `owner_id` and `queue` are both still empty — once, after every `beforeInsert` hook, never on update, never refusing the insert (ADR-0142 D5). Entries are ordered; the first whose `criteria` (CEL over @@ -303,12 +303,12 @@ insert (ADR-0142 D5). Entries are ordered; the first whose `criteria` (CEL over import { defineAssignmentRule } from '@objectstack/spec/automation'; export const caseIntake = defineAssignmentRule({ - name: 'case_intake', + name: 'crm_case_intake', object: 'crm_case', entries: [ { name: 'urgent', criteria: 'record.priority == "urgent"', - assignTo: { type: 'queue', value: 'escalations' } }, // no strategy: parked for a claim - { name: 'rest', assignTo: { type: 'queue', value: 'support_intake' }, + assignTo: { type: 'queue', value: 'crm_escalations' } }, // no strategy: parked for a claim + { name: 'rest', assignTo: { type: 'queue', value: 'crm_support_intake' }, strategy: 'least_loaded', load: 'record.status != "closed"' }, // or 'round_robin'; `load` only with least_loaded ], }); @@ -316,7 +316,7 @@ export const caseIntake = defineAssignmentRule({ `strategy` is for a queue target only; `assignTo: { type: 'user', value: '' }` names one owner. Queues are declared in `queues` (objectstack-data); an approval -for the same people is `approvers: [{ type: 'queue', value: 'support_intake' }]` +for the same people is `approvers: [{ type: 'queue', value: 'crm_support_intake' }]` ([Approver Types](./references/state-machines-and-approvals.md#approver-types)). ⛔ Never a `beforeInsert` hook that picks an owner, nor a round-robin counter field. diff --git a/skills/objectstack-automation/evals/approvals/test-revise-loop.md b/skills/objectstack-automation/evals/approvals/test-revise-loop.md index 42634aadeb3..7d703ac17f8 100644 --- a/skills/objectstack-automation/evals/approvals/test-revise-loop.md +++ b/skills/objectstack-automation/evals/approvals/test-revise-loop.md @@ -11,7 +11,7 @@ This file states only what a grader scores, so the rule has one home. ## Scenario -> Build an autolaunched flow `budget_approval` on the `project` object: when +> Build an autolaunched flow `crm_budget_approval` on the `project` object: when > `budget` increases past 100000, route to a manager for approval. The manager > can **approve**, **reject**, or **send the record back to the submitter for > revision**; after the submitter reworks and resubmits, it returns to the diff --git a/skills/objectstack-automation/evals/flows-triggers-approvals.json b/skills/objectstack-automation/evals/flows-triggers-approvals.json index 714548fb312..38e833b6e54 100644 --- a/skills/objectstack-automation/evals/flows-triggers-approvals.json +++ b/skills/objectstack-automation/evals/flows-triggers-approvals.json @@ -43,8 +43,8 @@ }, { "id": 3, - "prompt": "Build an autolaunched flow `budget_approval` on `project`: when `budget` increases past 100000, route to a manager who can approve, reject, or send it back to the submitter for revision; a resubmission returns to the manager. Cap it at two send-backs.", - "expected_output": "The ADR-0044 shape: an `approval` node (`type: 'approval'`, `approvers: [{ type: 'position', value: 'manager' }]`, `lockRecord: true`, `maxRevisions: 2`) with `approve` / `reject` out-edges plus a `revise` out-edge to an `approval_revise` node, and a resubmit edge back into the approval node typed `type: 'back'` — what legalises the cycle for `registerFlow`. The flow lives in `flows` (no `approvals` collection).", + "prompt": "Build an autolaunched flow `crm_budget_approval` on `project`: when `budget` increases past 100000, route to a manager who can approve, reject, or send it back to the submitter for revision; a resubmission returns to the manager. Cap it at two send-backs.", + "expected_output": "The ADR-0044 shape: an `approval` node (`type: 'approval'`, `approvers: [{ type: 'position', value: 'crm_manager' }]`, `lockRecord: true`, `maxRevisions: 2`) with `approve` / `reject` out-edges plus a `revise` out-edge to an `approval_revise` node, and a resubmit edge back into the approval node typed `type: 'back'` — what legalises the cycle for `registerFlow`. The flow lives in `flows` (no `approvals` collection).", "files": [], "assertions": { "must_contain": [ @@ -120,8 +120,8 @@ }, { "id": 7, - "prompt": "An expense approval only holders of `expense.approve_request` may start; route to the `finance_manager` position; the submitter must never approve their own expense.", - "expected_output": "Flow-level `requiredPermissions: ['expense.approve_request']`, checked at every door that starts the flow (never a run-user positions check inside it); the `approval` node names `approvers: [{ type: 'position', value: 'finance_manager' }]` with `excludeSubmitter: true`, and `onEmptyApprovers` is anything but `auto_approve`, refused beside it.", + "prompt": "An expense approval only holders of `expense.approve_request` may start; route to the `crm_finance_manager` position; the submitter must never approve their own expense.", + "expected_output": "Flow-level `requiredPermissions: ['expense.approve_request']`, checked at every door that starts the flow (never a run-user positions check inside it); the `approval` node names `approvers: [{ type: 'position', value: 'crm_finance_manager' }]` with `excludeSubmitter: true`, and `onEmptyApprovers` is anything but `auto_approve`, refused beside it.", "files": [], "assertions": { "must_contain": [ @@ -155,7 +155,7 @@ }, { "id": 9, - "prompt": "Build a screen flow `convert_lead` that asks for a company name and a contact name, creates a `crm_account` and a linked `crm_contact`, and ends on a screen linking to both new records.", + "prompt": "Build a screen flow `crm_convert_lead` that asks for a company name and a contact name, creates a `crm_account` and a linked `crm_contact`, and ends on a screen linking to both new records.", "expected_output": "A `type: 'screen'` flow: a `screen` collecting `company_name` and `contact_name`; two `create_record` nodes with `outputVariable` (`account`, `contact`), the contact linked by `account: { dialect: 'cel', source: 'account.id' }`; a last `screen` of two `type: 'record_link'` fields with `reference` (`crm_account`, `crm_contact`) and `defaultValue: { dialect: 'cel', source: 'account.id' }` / `'contact.id'`, no `required`, `placeholder` or `options`. The `end` carries no `message` (refused when completed), the `description` no markdown link — the links are the `record_link` fields, never the object field type.", "files": [], "assertions": { @@ -190,7 +190,7 @@ }, { "id": 11, - "prompt": "When a `contractor` is marked `onboarded`, grant its linked user (`contractor.user_id`) the `contractor_portal` permission set; run the flow as system so the grant always lands.", + "prompt": "When a `contractor` is marked `onboarded`, grant its linked user (`contractor.user_id`) the `crm_contractor_portal` permission set; run the flow as system so the grant always lands.", "expected_output": "Not as system: a system-context insert into `sys_user_permission_set` (as into `sys_user_position`, `sys_invitation` or `sys_member`) is refused with `PERMISSION_DENIED` — elevation does not lift a governed identity write, a system subflow included. The flow runs `runAs: 'user'`: its `create_record` on `sys_user_permission_set` is judged by the delegated-administration gate as the user whose write fired it, recorded as `granted_by`; a user the gate would not let grant the set is refused — the rule, not a defect.", "files": [], "assertions": { @@ -210,7 +210,7 @@ { "id": 12, "prompt": "Spread new `crm_case` records evenly across the support team; a `refund_requested` case needs sign-off from the support-leads queue.", - "expected_output": "An assignment rule, not a flow or hook: `defineAssignmentRule({ name: 'case_intake', object: 'crm_case', entries: [{ name: 'all', assignTo: { type: 'queue', value: 'support_team' }, strategy: 'round_robin' }] })` in `assignmentRules` — evaluated once per insert on a still-ownerless row, never on update; no counter field. The sign-off is an `approval` node (`type: 'approval'`) with `approvers: [{ type: 'queue', value: 'support_leads' }]`: the queue's members join the slate at node entry; an empty queue adds nobody.", + "expected_output": "An assignment rule, not a flow or hook: `defineAssignmentRule({ name: 'crm_case_intake', object: 'crm_case', entries: [{ name: 'all', assignTo: { type: 'queue', value: 'crm_support_team' }, strategy: 'round_robin' }] })` in `assignmentRules` — evaluated once per insert on a still-ownerless row, never on update; no counter field. The sign-off is an `approval` node (`type: 'approval'`) with `approvers: [{ type: 'queue', value: 'crm_support_leads' }]`: the queue's members join the slate at node entry; an empty queue adds nobody.", "files": [], "assertions": { "must_contain": [ diff --git a/skills/objectstack-automation/references/examples-flows.md b/skills/objectstack-automation/references/examples-flows.md index 794a67a6258..56df2385f27 100644 --- a/skills/objectstack-automation/references/examples-flows.md +++ b/skills/objectstack-automation/references/examples-flows.md @@ -16,7 +16,7 @@ import { defineFlow } from '@objectstack/spec'; export const EscalateOverdueCasesFlow = defineFlow({ - name: 'escalate_overdue_cases', + name: 'crm_escalate_overdue_cases', label: 'Escalate Overdue Cases', type: 'schedule', status: 'active', @@ -82,7 +82,7 @@ day, a threshold is never missed. ```typescript { - name: 'renewal_alert', + name: 'crm_renewal_alert', label: 'Renewal Alert', type: 'schedule', runAs: 'system', // a sweep has no trigger user — elevate explicitly diff --git a/skills/objectstack-automation/references/integration-recipes.md b/skills/objectstack-automation/references/integration-recipes.md index ebf396cdc2c..2b4acdaa8f1 100644 --- a/skills/objectstack-automation/references/integration-recipes.md +++ b/skills/objectstack-automation/references/integration-recipes.md @@ -41,7 +41,7 @@ export const StripeConnector = defineConnector({ }); // crm_account gains `stripe_available` (currency) and `stripe_synced_at` (datetime). export const StripeBalanceSweep = defineFlow({ - name: 'stripe_balance_sweep', label: 'Stripe balance → house account', + name: 'crm_stripe_balance_sweep', label: 'Stripe balance → house account', type: 'schedule', runAs: 'system', nodes: [ { id: 'start', type: 'start', label: 'Nightly 03:00', diff --git a/skills/objectstack-automation/references/state-machines-and-approvals.md b/skills/objectstack-automation/references/state-machines-and-approvals.md index f50b872cbc0..6be40965075 100644 --- a/skills/objectstack-automation/references/state-machines-and-approvals.md +++ b/skills/objectstack-automation/references/state-machines-and-approvals.md @@ -72,7 +72,7 @@ is one diagram a reviewer (or AI) can read end-to-end. // A record-triggered flow: high-value opportunities need manager sign-off, // and director sign-off too when the amount clears 500k. { - name: 'opportunity_discount_approval', + name: 'crm_opportunity_discount_approval', label: 'Opportunity Discount Approval', type: 'record_change', nodes: [ @@ -95,7 +95,7 @@ is one diagram a reviewer (or AI) can read end-to-end. type: 'approval', label: 'Sales Manager Review', config: { - approvers: [{ type: 'position', value: 'sales_manager' }], + approvers: [{ type: 'position', value: 'crm_sales_manager' }], behavior: 'first_response', lockRecord: true, approvalStatusField: 'approval_status', @@ -111,7 +111,7 @@ is one diagram a reviewer (or AI) can read end-to-end. type: 'approval', label: 'Sales Director Sign-off', config: { - approvers: [{ type: 'position', value: 'sales_director' }], + approvers: [{ type: 'position', value: 'crm_sales_director' }], behavior: 'unanimous', approvalStatusField: 'approval_status', }, @@ -178,7 +178,7 @@ Three pieces author it: ```typescript { id: 'manager_review', type: 'approval', label: 'Manager Review', - config: { approvers: [{ type: 'position', value: 'manager' }], lockRecord: true, maxRevisions: 2 }, + config: { approvers: [{ type: 'position', value: 'crm_manager' }], lockRecord: true, maxRevisions: 2 }, }, { id: 'wait_revision', type: 'approval_revise', label: 'Awaiting Revision' }, // …among the approval's edges… @@ -250,7 +250,7 @@ an id that does not exist gets. |:-------|:------------| | `user` | A specific user id (`value` = user id) | | `position` | Holders of a position — `value` = the position machine name, resolved via `sys_user_position` (ADR-0090 D3) | -| `org_membership_level` | The **org-membership tier** — `value` is one of `owner`/`admin`/`delegated_admin`/`member`. **NOT** a position: `{ type: 'org_membership_level', value: 'sales_manager' }` matches nobody; use `position`. Spelled `role` before ADR-0090 D3 — that spelling is deprecated, still resolves, and is removed in the next major | +| `org_membership_level` | The **org-membership tier** — `value` is one of `owner`/`admin`/`delegated_admin`/`member`. **NOT** a position: `{ type: 'org_membership_level', value: 'crm_sales_manager' }` matches nobody; use `position`. Spelled `role` before ADR-0090 D3 — that spelling is deprecated, still resolves, and is removed in the next major | | `team` | Members of a flat `sys_team` | | `department` | A department + all descendant departments | | `manager` | The submitter's manager (`sys_user.manager_id`) | @@ -308,7 +308,7 @@ The full "previous approver picks the next step's approvers" loop, end to end import { defineFlow } from '@objectstack/spec'; export const DynamicApprovalFlow = defineFlow({ - name: 'dynamic_approval', + name: 'crm_dynamic_approval', label: 'Dynamic Approval', type: 'autolaunched', status: 'active', @@ -362,7 +362,7 @@ Object-hook `ctx` is a different vocabulary — see **objectstack-data** | Field | Purpose | |:------|:--------| -| `approvers` | Who may act (≥ 1 — see Approver Types above). Each approver may carry an optional **`group`** label (e.g. `{ type: 'position', value: 'auditor', group: 'finance' }`) — with `behavior: 'per_group'`, approvers sharing a label form one group; unlabelled approvers each form their own | +| `approvers` | Who may act (≥ 1 — see Approver Types above). Each approver may carry an optional **`group`** label (e.g. `{ type: 'position', value: 'crm_auditor', group: 'finance' }`) — with `behavior: 'per_group'`, approvers sharing a label form one group; unlabelled approvers each form their own | | `behavior` | `first_response` (first approver decides), `unanimous` (all must approve), `quorum` (`minApprovals` of N — M-of-N collective sign-off), or `per_group` (EACH approver `group` must reach `minApprovals` — one-from-each-group sign-off, 会签). In every mode a single rejection finalizes the node as `rejected`. Default `first_response` | | `minApprovals` | Approvals required — total for `quorum`, per group for `per_group`. Omitted ⇒ ALL resolvable approvers under `quorum`, `1` per group; clamped at runtime so a misconfiguration can never deadlock | | `lockRecord` | Lock the triggering record from edits while pending. Default `true` | @@ -371,7 +371,7 @@ Object-hook `ctx` is a different vocabulary — see **objectstack-data** | `excludeSubmitter` | `true` drops the run's submitter (`submitter_id`) from the resolved slate, whatever approver type produced them — segregation of duties; a slate it empties takes the `onEmptyApprovers` route, never a silent pass. `onEmptyApprovers: 'auto_approve'` is **refused** beside it (it would approve exactly the request this exists to stop): use `admin_rescue`, `fail` or `fallback`. Default `false` | | `decisionOutputs` | Decision outputs a decision may carry: bare keys (free text) **or typed declarations** `{ key, label?, type: 'text'\|'user'\|'department'\|'position'\|'team', multiple?, required? }`, enforced at the decision — see *Recording a decision* | | `escalation` | Optional per-node SLA — `{ enabled, timeoutHours, action: reassign\|auto_approve\|auto_reject\|notify, escalateTo?, notifySubmitter }`. `timeoutHours` is **calendar (wall-clock) hours** — nights, weekends and holidays count; the platform ships no business-hours calendar. `escalateTo` is a **position machine name** (expanded to its holders via `sys_user_position`, ADR-0090 D3) or a specific user id — never a membership tier. `reassign` without `escalateTo` degrades to notify (linted) | -| `maxRevisions` | Send-backs per run before auto-reject (ADR-0044) — see *Send-back for revision* above | +| `maxRevisions` | Send-backs per run before auto-reject (ADR-0044) | ### Branching, side-effects & rejection diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index ee2f16edafa..bc3cdbcfa9d 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -409,9 +409,9 @@ organization adds users and teams through `sys_queue_member` rows: import { defineQueue } from '@objectstack/spec/security'; export const supportIntake = defineQueue({ - name: 'support_intake', // the one name the `queue` column, a rule, an approver and a sharing recipient use + name: 'crm_support_intake', // the one name the `queue` column, a rule, an approver and a sharing recipient use label: 'Support Intake', - members: [{ type: 'position', value: 'service_agent' }], + members: [{ type: 'position', value: 'crm_service_agent' }], }); // defineStack({ queues: [supportIntake], assignmentRules: [...] }) — routing rows in: objectstack-automation ``` @@ -429,7 +429,7 @@ picklist, a round-robin counter field, or a hook that picks an owner. --- -→ Seed Data & Fixtures (`defineSeed()`) moved verbatim to [references/seeds.md](./references/seeds.md) (Quick start · `Seed` fields · Import modes · `externalId` selection · Relationship references · Dynamic values (CEL) · Seed best practices). +→ Seed Data & Fixtures (`defineSeed()`) moved verbatim to [references/seeds.md](./references/seeds.md). --- diff --git a/skills/objectstack-data/evals/hooks-security-seeds-datasources.json b/skills/objectstack-data/evals/hooks-security-seeds-datasources.json index e1abb388de4..cd8ef423aea 100644 --- a/skills/objectstack-data/evals/hooks-security-seeds-datasources.json +++ b/skills/objectstack-data/evals/hooks-security-seeds-datasources.json @@ -74,7 +74,7 @@ { "id": 8, "prompt": "Web-form leads arrive ownerless; inside sales holds them until a rep takes one, and two reps must never own the same lead.", - "expected_output": "`queues: [defineQueue({ name: 'inside_sales', label: 'Inside Sales', members: [{ type: 'position', value: 'inside_sales_rep' }] })]` and an `assignmentRules` entry `assignTo: { type: 'queue', value: 'inside_sales' }` with no `strategy` park each lead in the injected `queue` column; a rep claims by an ordinary by-id `{ owner_id: }` update, the loser gets `409 CONCURRENT_UPDATE`.", + "expected_output": "`queues: [defineQueue({ name: 'crm_inside_sales', label: 'Inside Sales', members: [{ type: 'position', value: 'crm_inside_sales_rep' }] })]` and an `assignmentRules` entry `assignTo: { type: 'queue', value: 'crm_inside_sales' }` with no `strategy` park each lead in the injected `queue` column; a rep claims by an ordinary by-id `{ owner_id: }` update, the loser gets `409 CONCURRENT_UPDATE`.", "files": [], "assertions": { "must_contain": ["defineQueue", "type: 'position'", "type: 'queue'", "CONCURRENT_UPDATE"], diff --git a/skills/objectstack-data/references/data-hooks.md b/skills/objectstack-data/references/data-hooks.md index 5cd76cd243f..bc07085d25f 100644 --- a/skills/objectstack-data/references/data-hooks.md +++ b/skills/objectstack-data/references/data-hooks.md @@ -85,7 +85,7 @@ import { defineHook, HookContext } from '@objectstack/spec/data'; const myHook = defineHook({ // Required: Unique identifier (snake_case) - name: 'my_validation_hook', + name: 'crm_validation_hook', // Required: Target object(s) object: 'account', // string | string[] | '*' @@ -513,7 +513,7 @@ is **not** in the partial patch) and flip that position to `filled`: import { defineHook } from '@objectstack/spec/data'; const fillPositionOnHire = defineHook({ - name: 'fill_position_on_hire', + name: 'crm_fill_position_on_hire', object: 'candidate', events: ['afterUpdate'], body: { @@ -769,7 +769,7 @@ handler: async (ctx: HookContext) => { ```typescript const protectStrategicAccounts = defineHook({ - name: 'protect_strategic_accounts', + name: 'crm_protect_strategic_accounts', object: 'account', events: ['beforeDelete'], body: { @@ -793,7 +793,7 @@ const protectStrategicAccounts = defineHook({ ```typescript const createAuditTrail = defineHook({ - name: 'audit_trail', + name: 'crm_audit_trail', object: ['account', 'contact', 'opportunity'], events: ['afterInsert', 'afterUpdate', 'afterDelete'], async: false, // must run inside the transaction @@ -818,7 +818,7 @@ const createAuditTrail = defineHook({ ```typescript const cascadeAccountUpdate = defineHook({ - name: 'cascade_account_updates', + name: 'crm_cascade_account_updates', object: 'account', events: ['afterUpdate'], handler: async (ctx) => { @@ -846,9 +846,9 @@ const cascadeAccountUpdate = defineHook({ ```typescript const maskSensitiveData = defineHook({ - name: 'mask_pii', + name: 'crm_mask_pii', object: ['contact', 'lead'], - events: ['afterFind'], // fires for findOne too — no separate afterFindOne + events: ['afterFind'], handler: async (ctx) => { // Exempt the engine's own elevated reads (`isSystem`) — internal writes // and self-reads must see the real values. @@ -894,7 +894,7 @@ these at startup: no `register*Hook` boilerplate, and the declarative fields ```typescript // objectstack.config.ts export default defineStack({ - hooks: [taskHook, { name: 'h', object: 'account', events: ['beforeInsert'], handler: 'normalize' }], + hooks: [taskHook, { name: 'crm_h', object: 'account', events: ['beforeInsert'], handler: 'normalize' }], functions: { normalize: async (ctx) => { /* ... */ } }, }); ``` diff --git a/skills/objectstack-data/references/examples-objects.md b/skills/objectstack-data/references/examples-objects.md index 18867a37ad6..1520ebb4284 100644 --- a/skills/objectstack-data/references/examples-objects.md +++ b/skills/objectstack-data/references/examples-objects.md @@ -111,14 +111,12 @@ Declared `indexes` are a separate decision — see ### Lifecycle Hooks -Implement business logic at data operation lifecycle points: - ```typescript import { defineHook, HookContext } from '@objectstack/spec/data'; export default defineHook({ - name: 'account_defaults', + name: 'crm_account_defaults', object: 'account', events: ['beforeInsert'], handler: async (ctx: HookContext) => { diff --git a/skills/objectstack-data/rules/security.md b/skills/objectstack-data/rules/security.md index dc6ade1c63d..a4237ee7637 100644 --- a/skills/objectstack-data/rules/security.md +++ b/skills/objectstack-data/rules/security.md @@ -9,14 +9,13 @@ Grant CRUD access per object with boolean bits on a permission set: import { definePermissionSet } from '@objectstack/spec'; export const salesUser = definePermissionSet({ - name: 'sales_user', + name: 'crm_sales_user', objects: { account: { allowRead: true, allowCreate: true, allowEdit: true }, contact: { allowRead: true }, }, }); -// Register it on the stack root under `permissions`: // defineStack({ permissions: [salesUser], ... }) ``` @@ -167,8 +166,8 @@ rowLevelSecurity: [ ## Positions grant only what they name; portal users are `external` - A position distributes **only** the sets its `permissionSets` names - (ADR-0131 D3/D4): `{ name: 'sales_rep', label: 'Sales Rep', permissionSets: - ['sales_user'] }`. A set that merely shares the position's name is reported, + (ADR-0131 D3/D4): `{ name: 'crm_sales_rep', label: 'Sales Rep', permissionSets: + ['crm_sales_user'] }`. A set that merely shares the position's name is reported, never granted; a name matching no set confers nothing. - A portal or partner user is an organization member on the `external` membership tier (closed list `owner`, `admin`, `delegated_admin`, `member`, diff --git a/skills/objectstack-data/rules/validation.md b/skills/objectstack-data/rules/validation.md index 977bb6cb91f..2219f1593ed 100644 --- a/skills/objectstack-data/rules/validation.md +++ b/skills/objectstack-data/rules/validation.md @@ -248,13 +248,13 @@ With an `otherwise` branch: Calling an external API, hitting another object, or running arbitrary code is **not** a validation type. Implement it as a `beforeInsert` / `beforeUpdate` -lifecycle hook and throw on failure — the typed, supported extension point: +lifecycle hook and throw on failure: ```typescript import { defineHook, HookContext } from '@objectstack/spec/data'; const taxIdCheck = defineHook({ - name: 'tax_id_external_check', + name: 'crm_tax_id_external_check', object: 'account', events: ['beforeInsert', 'beforeUpdate'], handler: async (ctx: HookContext) => { diff --git a/skills/objectstack-i18n/SKILL.md b/skills/objectstack-i18n/SKILL.md index ec6f31df5a9..a21bbfae6c3 100644 --- a/skills/objectstack-i18n/SKILL.md +++ b/skills/objectstack-i18n/SKILL.md @@ -89,7 +89,7 @@ const en: TranslationData = { details: { label: 'Details' }, }, _actions: { - complete: { + todo_complete: { label: 'Complete', confirmText: 'Mark this task as completed?', successMessage: 'Task completed', diff --git a/skills/objectstack-i18n/evals/bundles-locales-coverage.json b/skills/objectstack-i18n/evals/bundles-locales-coverage.json index 01e05b88d13..1ca9099fd04 100644 --- a/skills/objectstack-i18n/evals/bundles-locales-coverage.json +++ b/skills/objectstack-i18n/evals/bundles-locales-coverage.json @@ -3,8 +3,8 @@ "evals": [ { "id": 1, - "prompt": "Add Simplified Chinese for the `support_case` object: its label and plural, the `status` field with options New / Open / Closed, the `close` action, and a welcome message that greets the user by name.", - "expected_output": "A `TranslationData` per locale under `objects.support_case` — `label`, `pluralLabel`, `fields.status.label`, `fields.status.options` keyed by the STORED option value (`new`, `open`, `closed`, never the display label), `_actions.close.label` — assembled with `defineTranslationBundle({ en, 'zh-CN': zhCN })` and registered under `defineStack({ translations })`. The message uses double-brace interpolation, `'welcome.user': '欢迎,{{userName}}!'`.", + "prompt": "Add Simplified Chinese for the `support_case` object: its label and plural, the `status` field with options New / Open / Closed, the `crm_close` action, and a welcome message that greets the user by name.", + "expected_output": "A `TranslationData` per locale under `objects.support_case` — `label`, `pluralLabel`, `fields.status.label`, `fields.status.options` keyed by the STORED option value (`new`, `open`, `closed`, never the display label), `_actions.crm_close.label` — assembled with `defineTranslationBundle({ en, 'zh-CN': zhCN })` and registered under `defineStack({ translations })`. The message uses double-brace interpolation, `'welcome.user': '欢迎,{{userName}}!'`.", "files": [], "assertions": { "must_contain": ["defineTranslationBundle", "objects", "pluralLabel", "options", "_actions", "{{"], @@ -53,8 +53,8 @@ }, { "id": 6, - "prompt": "Add zh-CN for the `pipeline_by_stage` report, the `sales_manager` permission set, the `regional_lead` position and the `deal_stage` picklist's option labels.", - "expected_output": "Top-level groups keyed by name: `reports.pipeline_by_stage` and `positions.regional_lead` (`label`, `description`), `permissions.sales_manager` (the same, spelled as the stack's `permissions[]`, not the camelCase name), `picklists.deal_stage.options` by stored value.", + "prompt": "Add zh-CN for the `crm_pipeline_by_stage` report, the `crm_sales_manager` permission set, the `crm_regional_lead` position and the `crm_deal_stage` picklist's option labels.", + "expected_output": "Top-level groups keyed by name: `reports.crm_pipeline_by_stage` and `positions.crm_regional_lead` (`label`, `description`), `permissions.crm_sales_manager` (the same, spelled as the stack's `permissions[]`, not the camelCase name), `picklists.crm_deal_stage.options` by stored value.", "files": [], "assertions": { "must_contain": ["reports", "permissions", "positions", "picklists"], @@ -63,8 +63,8 @@ }, { "id": 7, - "prompt": "In zh-CN the `contact` quick-create form still shows its English heading and the placeholder its `contact_details` section sets on `email`; the `home` page's `overview` tab and the `manager_review` step of the `expense_claim` flow are English too.", - "expected_output": "Each key spelled as its source: `objects.contact._views.quick_create.title` (a form's own heading), `objects.contact._sections.contact_details.fields.email.placeholder` (that form's override; `helpText` there, `help` at `fields.email`), `pages.home.components.{tabs_id}.items.overview.label` (by the item's `value`), `flows.expense_claim.approvals.manager_review.label` (the node id). Not a view `label`, an object `_tabs` key or a screen `title`.", + "prompt": "In zh-CN the `contact` quick-create form still shows its English heading and the placeholder its `contact_details` section sets on `email`; the `crm_home` page's `overview` tab and the `manager_review` step of the `crm_expense_claim` flow are English too.", + "expected_output": "Each key spelled as its source: `objects.contact._views.quick_create.title` (a form's own heading), `objects.contact._sections.contact_details.fields.email.placeholder` (that form's override; `helpText` there, `help` at `fields.email`), `pages.crm_home.components.{tabs_id}.items.overview.label` (by the item's `value`), `flows.crm_expense_claim.approvals.manager_review.label` (the node id). Not a view `label`, an object `_tabs` key or a screen `title`.", "files": [], "assertions": { "must_contain": ["_views", "title", "_sections", "fields", "items", "approvals"], diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index c2b312bd1dc..dc06fb5fd6a 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -118,7 +118,9 @@ to a new protocol major — never to silence a mismatch. **Object naming:** The object `name` is the canonical identifier and equals the physical table name. Embed any domain prefix directly in the name (e.g. `name: 'crm_account'`); the object-level `namespace` *field* is retired (ADR-0129 D3) and refused at load. Never set `tableName`, and never add a `sys_metadata` object to a runtime manifest. -**`manifest.namespace` (ADR-0048):** Optional, but **enforced once set**. When a package declares `manifest.namespace: 'crm'`, every `object.name` must start with `crm_` or `defineStack` errors (`validateNamespacePrefix` in `@objectstack/spec`); the legacy `__` double-underscore form is rejected, and `sys_`-prefixed names are platform-reserved and exempt. The namespace is also a package-ownership key — installing two packages that both claim `crm` fails with `NamespaceConflictError` (downgrade to a warning with `OS_METADATA_COLLISION=warn`). `os lint` additionally emits a non-fatal `naming/namespace-prefix` warning for bare-named UI/automation items (app, page, dashboard, flow, action, report, dataset) when a namespace is set. +**`manifest.namespace` (ADR-0048):** Optional, but **enforced once set**. When a package declares `manifest.namespace: 'crm'`, every `object.name` must start with `crm_` or `defineStack` errors (`validateNamespacePrefix` in `@objectstack/spec`); the legacy `__` double-underscore form is rejected, and `sys_`-prefixed names are platform-reserved and exempt. The namespace is also a package-ownership key — installing two packages that both claim `crm` fails with `NamespaceConflictError` (downgrade to a warning with `OS_METADATA_COLLISION=warn`). + +**One rule for every name:** every metadata name a package authors starts with its namespace — flows, actions, hooks, apps, pages, dashboards, reports, datasets, queues, positions, permission sets, skills (`crm_send_email`, `crm_sales_rep`) — so no two installed packages share one. Exceptions: fields, object extensions and container views (derived from a prefixed object), and translations (`_`). Today `defineStack` refuses only a bare object name and `os build` a bare doc name; `os lint`'s `naming/namespace-prefix` warns only when one package declares an app, page, dashboard, flow, action, report or dataset name twice. **Customizing an installed package (ADR-0146):** Customize an installed base `hotcrm` in its paired writable `hotcrm.custom`, which depends on it: a same-named item there with `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` (its `manifest.id`/`manifest.version`) replaces the base's document whole. Reads honour it only from the pair (the row's `package_id`), for a view, dashboard, report, email_template, translation, flow, action, page, app, dataset, doc, book or mapping, never judging `basedOn`. An email template is one item per (name, locale): an override names a locale the base ships; one it does not ship is added without `overrides`. Then no `?package`, `?package=hotcrm`, the layered read and both lists serve it, and removing the pair's item (`DELETE …?package=hotcrm.custom`) serves the base's again; the env-wide list holds one item per name one code package ships (a dependent's stays under its own `?package=`); a pre-D11 unbound Setup row wins over the base's until `os migrate packageless-overlays` moves it, and once movable as a declared override, its package-less save, publish, reset and rollback are refused (D11); an email-template row at a locale the base does not ship moves as the pair's own member and keeps that write. The base's address serves the pair's added locale too (both lists, the layered read with `code` null and `effective` the pair's, the execution list, a send in that locale); one a dependent adds under the base's name is served only under its own `?package=`. An undeclared pair row or a dependent's row at a base-shipped locale is an orphan in the layered read's `overlay`, `effective` = the base's. An override may narrow a public form's anonymous intake, never re-open one any honoured body withdrew. `GET /packages/hotcrm.custom/export` carries it with `overrides`. @@ -382,7 +384,7 @@ an entry carries no `password`: demoStaff: [ { name: 'Legal Lead', email: 'lead@example.com' }, { name: 'Legal Counsel 1', email: 'counsel1@example.com', manager: 'lead@example.com', - position: 'legal_counsel', permissionSets: ['contracts_editor'] }], + position: 'crm_legal_counsel', permissionSets: ['crm_contracts_editor'] }], ``` `name` is what a seed reference resolves by; `position` / `permissionSets` @@ -396,11 +398,11 @@ existing account on the boot banner. --- -→ Part 2 — Plugin Development & Kernel Extension: [references/plugin-development.md](./references/plugin-development.md) (Quick Reference — Detailed Rules · ObjectKernel vs LiteKernel · Plugin Interface — Quick Overview · PluginContext API · Complete Plugin Example · Using Plugins · Testing Plugins · Well-Known Plugin Names & Services · MetadataPlugin Runtime Boundary · Health Monitoring · Feature Flags); its detailed rules stay at [rules/plugin-lifecycle.md](./rules/plugin-lifecycle.md) · [rules/service-registry.md](./rules/service-registry.md) · [references/plugin-hooks.md](./references/plugin-hooks.md). +→ Part 2 — Plugin Development & Kernel Extension: [references/plugin-development.md](./references/plugin-development.md); its detailed rules stay at [rules/plugin-lifecycle.md](./rules/plugin-lifecycle.md) · [rules/service-registry.md](./rules/service-registry.md) · [references/plugin-hooks.md](./references/plugin-hooks.md). --- -→ Part 3 — Operations: CLI, Testing, Deployment: [references/operations.md](./references/operations.md) (Daily-loop commands · Build & runtime · Verify your work · Ports & networking · Data & migrations · Environments & deploy · Shipping a plugin · Testing pattern · Deployment targets · Health & observability · Common ops pitfalls). +→ Part 3 — Operations: CLI, Testing, Deployment: [references/operations.md](./references/operations.md). --- diff --git a/skills/objectstack-platform/evals/config-plugins-ops.json b/skills/objectstack-platform/evals/config-plugins-ops.json index 338f01b8faf..d576807c12b 100644 --- a/skills/objectstack-platform/evals/config-plugins-ops.json +++ b/skills/objectstack-platform/evals/config-plugins-ops.json @@ -4,7 +4,7 @@ { "id": 1, "prompt": "Write `objectstack.config.ts` for our CRM app: objects, views and flows under src/, namespace `crm`, protocol 18.", - "expected_output": "`defineStack` with a `manifest` of `id`, `version`, `type: 'app'`, `name`, `namespace: 'crm'`, `engines: { protocol: '^18' }`; barrel imports, `objects: Object.values(objects)` (likewise `views` / `flows`); prefixed object names (`crm_account`), no object-level `namespace`, no `tableName`; no driver in `plugins:` (the CLI resolves it); no `workflows` / `approvals` keys (refused).", + "expected_output": "`defineStack` with a `manifest` of `id`, `version`, `type: 'app'`, `name`, `namespace: 'crm'`, `engines: { protocol: '^18' }`; barrel imports, `objects: Object.values(objects)` (likewise `views` / `flows`); prefixed object and flow names (`crm_account`, `crm_convert_lead`), no object-level `namespace`, no `tableName`; no driver in `plugins:` (the CLI resolves it); no `workflows` / `approvals` keys (refused).", "files": [], "assertions": { "must_contain": [ @@ -147,8 +147,8 @@ }, { "id": 9, - "prompt": "Add a `support_intake` queue staffed by the `service_agent` position and route every new `crm_case` into it (`requires: ['automation', 'triggers']`).", - "expected_output": "Two stack collections: `queues: [defineQueue({ name: 'support_intake', label: 'Support Intake', members: [{ type: 'position', value: 'service_agent' }] })]` and `assignmentRules: [defineAssignmentRule(...)]` with one entry, `assignTo: { type: 'queue', value: 'support_intake' }`; plus `'sharing'` in `requires`: the keys need `@objectstack/plugin-sharing`'s ownership-queue service (`queue` is the job queue), else the boot is refused.", + "prompt": "Add a `crm_support_intake` queue staffed by the `crm_service_agent` position and route every new `crm_case` into it (`requires: ['automation', 'triggers']`).", + "expected_output": "Two stack collections: `queues: [defineQueue({ name: 'crm_support_intake', label: 'Support Intake', members: [{ type: 'position', value: 'crm_service_agent' }] })]` and `assignmentRules: [defineAssignmentRule(...)]` with one entry, `assignTo: { type: 'queue', value: 'crm_support_intake' }`; plus `'sharing'` in `requires`: the keys need `@objectstack/plugin-sharing`'s ownership-queue service (`queue` is the job queue), else the boot is refused.", "files": [], "assertions": { "must_contain": [ diff --git a/skills/objectstack-ui/evals/report-period-export.json b/skills/objectstack-ui/evals/report-period-export.json index dc3544f7e57..cfc922284ef 100644 --- a/skills/objectstack-ui/evals/report-period-export.json +++ b/skills/objectstack-ui/evals/report-period-export.json @@ -4,8 +4,8 @@ "evals": [ { "id": 1, - "prompt": "The opportunity_metrics dataset (base object opportunity) declares the amount_sum measure and the stage and close_date dimensions (close_date is type: 'date', dateGranularity: 'quarter'). Add a pipeline-by-stage summary report that opens on the current quarter, lets the viewer pick another period, and lets users who may already export opportunities download the table as CSV or Excel. Emit the metadata.", - "expected_output": "One non-joined defineReport bound to opportunity_metrics (rows: ['stage'], values: ['amount_sum']) with `dateRange: { dimension: 'close_date', defaultRange: 'this_quarter', allowCustomRange: true }` — the dataset's `type: 'date'` dimension and the dashboard's own preset names, never a second preset table or a hand-rolled `runtimeFilter` window on close_date — and `exportOptions: { formats: ['csv', 'xlsx'] }` (optionally `includeHeaders` / `fileNamePrefix`). Who may download is the base object's existing permission-set `allowExport`, named in words: no report permission key is invented, and neither key is placed on a `joined` container (refused).", + "prompt": "The crm_opportunity_metrics dataset (base object opportunity) declares the amount_sum measure and the stage and close_date dimensions (close_date is type: 'date', dateGranularity: 'quarter'). Add a pipeline-by-stage summary report that opens on the current quarter, lets the viewer pick another period, and lets users who may already export opportunities download the table as CSV or Excel. Emit the metadata.", + "expected_output": "One non-joined defineReport bound to crm_opportunity_metrics (rows: ['stage'], values: ['amount_sum']) with `dateRange: { dimension: 'close_date', defaultRange: 'this_quarter', allowCustomRange: true }` — the dataset's `type: 'date'` dimension and the dashboard's own preset names, never a second preset table or a hand-rolled `runtimeFilter` window on close_date — and `exportOptions: { formats: ['csv', 'xlsx'] }` (optionally `includeHeaders` / `fileNamePrefix`). Who may download is the base object's existing permission-set `allowExport`, named in words: no report permission key is invented, and neither key is placed on a `joined` container (refused).", "files": [], "assertions": { "must_contain": ["defineReport", "dateRange", "dimension: 'close_date'", "defaultRange: 'this_quarter'", "exportOptions", "'csv'", "'xlsx'", "allowExport"], diff --git a/skills/objectstack-ui/evals/views-apps-actions-pages.json b/skills/objectstack-ui/evals/views-apps-actions-pages.json index ffa3a759e36..9e506e0161c 100644 --- a/skills/objectstack-ui/evals/views-apps-actions-pages.json +++ b/skills/objectstack-ui/evals/views-apps-actions-pages.json @@ -23,8 +23,8 @@ }, { "id": 3, - "prompt": "Add a Convert action on `lead` — only for qualified, not-yet-converted leads, from the record header and each list row, launching the `lead_conversion` flow — and a bulk 'Add to campaign' that asks which campaign, for the selected rows.", - "expected_output": "Two `defineAction` items. Convert: `type: 'flow'`, `target: 'lead_conversion'`, `locations: ['record_header', 'list_item']`, `visible: P`record.status == \"qualified\" && record.is_converted == false`` (bare CEL, never brace-wrapped), `confirmText`, `successMessage`, `refreshAfter: true`. Bulk: `type: 'script'`, `locations: ['list_toolbar']`, a field-backed `params: [{ field: 'campaign_id', objectOverride: 'campaign', required: true }]`, and a `body` that reads `input.selectedIds`.", + "prompt": "Add a Convert action on `lead` — only for qualified, not-yet-converted leads, from the record header and each list row, launching the `crm_lead_conversion` flow — and a bulk 'Add to campaign' that asks which campaign, for the selected rows.", + "expected_output": "Two `defineAction` items. Convert: `type: 'flow'`, `target: 'crm_lead_conversion'`, `locations: ['record_header', 'list_item']`, `visible: P`record.status == \"qualified\" && record.is_converted == false`` (bare CEL, never brace-wrapped), `confirmText`, `successMessage`, `refreshAfter: true`. Bulk: `type: 'script'`, `locations: ['list_toolbar']`, a field-backed `params: [{ field: 'campaign_id', objectOverride: 'campaign', required: true }]`, and a `body` that reads `input.selectedIds`.", "files": [], "assertions": { "must_contain": ["defineAction", "locations", "record_header", "list_toolbar", "visible", "successMessage", "selectedIds"], @@ -73,7 +73,7 @@ }, { "id": 8, - "prompt": "Make the Reassign Lead toast read 'Lead NAME reassigned'; after Convert's lead_conversion flow completes, open the opportunity it created.", + "prompt": "Make the Reassign Lead toast read 'Lead NAME reassigned'; after Convert's crm_lead_conversion flow completes, open the opportunity it created.", "expected_output": "`successMessage: 'Lead {{ record.name }} reassigned'` (a `${…}` hole fails `os validate`); Convert gets `onSuccess: { navigate }` ending `${result.output.opportunity_id}`, an `isOutput` flow variable.", "files": [], "assertions": { @@ -104,7 +104,7 @@ { "id": 11, "prompt": "Add a 'Log Call' action on `contact` that asks for the call notes and the outcome in a dialog, and make the dialog's button read Log Call instead of Confirm.", - "expected_output": "One `defineAction` with field-backed `params` (`call_notes`, `call_outcome`) and `submitText: 'Log Call'` beside them — the verb on the param dialog's submit button, an I18nLabel translated at `objects.contact._actions.log_call.submitText`. Not `confirmText` (the message of a param-less action's confirm dialog, refused beside `params`), not `submitLabel` / `confirmLabel` (refused, naming `submitText`), and never blank.", + "expected_output": "One `defineAction` with field-backed `params` (`call_notes`, `call_outcome`) and `submitText: 'Log Call'` beside them — the verb on the param dialog's submit button, an I18nLabel translated at `objects.contact._actions.crm_log_call.submitText`. Not `confirmText` (the message of a param-less action's confirm dialog, refused beside `params`), not `submitLabel` / `confirmLabel` (refused, naming `submitText`), and never blank.", "files": [], "assertions": { "must_contain": ["params", "submitText: 'Log Call'"], diff --git a/skills/objectstack-ui/rules/actions.md b/skills/objectstack-ui/rules/actions.md index 6f1520dbe08..fba3afc81ee 100644 --- a/skills/objectstack-ui/rules/actions.md +++ b/skills/objectstack-ui/rules/actions.md @@ -2,7 +2,7 @@ ## Where Actions Appear (`locations`) -`locations` is an array — an action can live in multiple surfaces: +`locations` is an array of surfaces: | Value | Surface | |:-----------------|:--------| @@ -31,7 +31,7 @@ import { defineAction } from '@objectstack/spec/ui'; import { P } from '@objectstack/spec'; export const ReassignLeadAction = defineAction({ - name: 'reassign_lead', + name: 'crm_reassign_lead', label: 'Reassign Lead', objectName: 'lead', type: 'api', @@ -57,17 +57,17 @@ import { defineAction } from '@objectstack/spec/ui'; import { P } from '@objectstack/spec'; export const ConvertLeadAction = defineAction({ - name: 'convert_lead', + name: 'crm_convert_lead', label: 'Convert Lead', objectName: 'lead', icon: 'arrow-right-circle', type: 'flow', - target: 'lead_conversion', // name of the flow + target: 'crm_lead_conversion', // name of the flow locations: ['record_header', 'list_item'], visible: P`record.status == "qualified" && record.is_converted == false`, confirmText: 'Are you sure you want to convert this lead?', successMessage: 'Lead converted successfully!', - onSuccess: { navigate: '/apps/crm/opportunity/record/${result.output.opportunity_id}' }, + onSuccess: { navigate: '/apps/crm_enterprise/opportunity/record/${result.output.opportunity_id}' }, refreshAfter: true, }); ``` @@ -79,7 +79,7 @@ export const ConvertLeadAction = defineAction({ import { defineAction } from '@objectstack/spec/ui'; export const AddToCampaignAction = defineAction({ - name: 'create_campaign', + name: 'crm_create_campaign', label: 'Add to Campaign', objectName: 'lead', icon: 'send', @@ -131,7 +131,7 @@ in a new tab and relative ones navigate in place. import { defineAction } from '@objectstack/spec/ui'; export const PrintA3Action = defineAction({ - name: 'print_a3', + name: 'crm_print_a3', label: 'Print Summary Sheet (A3)', type: 'url', target: '/print/a3?id=${record.id}', // interpolated at click @@ -151,11 +151,11 @@ auth (the tab carries no in-app session). ```typescript export const OpenInvoicePdfAction = defineAction({ - name: 'open_invoice_pdf', + name: 'crm_open_invoice_pdf', label: 'Open PDF', objectName: 'invoice', type: 'script', - target: 'open_invoice_pdf', // registered handler + target: 'crm_open_invoice_pdf', // registered handler opensInNewTab: true, newTabUrl: '/api/v1/invoice/{recordId}/pdf', locations: ['record_header'], diff --git a/skills/objectstack-ui/rules/dashboards.md b/skills/objectstack-ui/rules/dashboards.md index f90b7f6fef5..d7165d45aaf 100644 --- a/skills/objectstack-ui/rules/dashboards.md +++ b/skills/objectstack-ui/rules/dashboards.md @@ -49,7 +49,7 @@ filter; keep `filter` on the widget when binding a dataset. id: 'revenue_by_region', type: 'bar', title: 'Revenue by Region', - dataset: 'sales', + dataset: 'crm_sales', dimensions: ['region'], values: ['revenue'], layout: { x: 0, y: 0, w: 6, h: 4 }, @@ -143,7 +143,7 @@ dataset, so Level B only surfaces in Studio previews and hand-coded react-page import type { Dashboard } from '@objectstack/spec/ui'; export const SalesDashboard: Dashboard = { - name: 'sales_dashboard', + name: 'crm_sales_dashboard', label: 'Sales Performance', columns: 12, gap: 4, @@ -152,9 +152,9 @@ export const SalesDashboard: Dashboard = { header: { showTitle: true, actions: [ - { label: 'New Opportunity', icon: 'Plus', actionType: 'modal', actionUrl: 'create_opportunity' }, + { label: 'New Opportunity', icon: 'Plus', actionType: 'modal', actionUrl: 'crm_create_opportunity' }, { label: 'Forecast', icon: 'TrendingUp', actionType: 'url', actionUrl: '/reports/forecast' }, - { label: 'Export', icon: 'Download', actionType: 'script', actionUrl: 'export_dashboard_pdf' }, + { label: 'Export', icon: 'Download', actionType: 'script', actionUrl: 'crm_export_dashboard_pdf' }, ], }, @@ -171,7 +171,7 @@ export const SalesDashboard: Dashboard = { { id: 'total_pipeline_value', type: 'metric', title: 'Total Pipeline', - dataset: 'opportunity_metrics', values: ['total_amount'], + dataset: 'crm_opportunity_metrics', values: ['total_amount'], filter: { stage: { $nin: ['closed_won', 'closed_lost'] } }, layout: { x: 0, y: 0, w: 3, h: 2 }, colorVariant: 'success', // the accent only @@ -187,7 +187,7 @@ export const SalesDashboard: Dashboard = { { id: 'revenue_vs_last_year', type: 'line', title: 'Revenue — This Year vs Last', - dataset: 'order_metrics', dimensions: ['closed_at'], values: ['total_sum'], + dataset: 'crm_order_metrics', dimensions: ['closed_at'], values: ['total_sum'], filter: { closed_at: { $gte: '{current_year_start}', $lte: '{current_year_end}' } }, compareTo: { kind: 'previousYear' }, layout: { x: 3, y: 0, w: 9, h: 4 }, @@ -214,9 +214,9 @@ vocabularies. ```typescript // One control, two objects: re-target on one widget, opt the other out. -{ id: 'revenue_by_region', dataset: 'order_metrics', values: ['total_sum'], +{ id: 'revenue_by_region', dataset: 'crm_order_metrics', values: ['total_sum'], filterBindings: { region: 'sales_region' } }, -{ id: 'projects_at_risk', dataset: 'project_metrics', values: ['project_count'], +{ id: 'projects_at_risk', dataset: 'crm_project_metrics', values: ['project_count'], filterBindings: { task_status: false } }, ``` @@ -259,14 +259,14 @@ compareTo: { kind: 'previousYear', dimension: 'close_date' } // several — reuses the original `filter` shape and replaces only the date-bound clauses. ```typescript -// Metric — WoW delta (binds the task_metrics dataset; filter = runtimeFilter) -{ id: 'done_this_week', type: 'metric', dataset: 'task_metrics', values: ['task_count'], +// Metric — WoW delta (binds the crm_task_metrics dataset; filter = runtimeFilter) +{ id: 'done_this_week', type: 'metric', dataset: 'crm_task_metrics', values: ['task_count'], filter: { assignee: '{current_user_id}', status: 'done', completed_at: { $gte: '{week_start}' } }, compareTo: { kind: 'previousPeriod' } } // Bar — YoY overlay on a stable category set -{ id: 'headcount_by_dept', type: 'bar', dataset: 'employee_metrics', +{ id: 'headcount_by_dept', type: 'bar', dataset: 'crm_employee_metrics', dimensions: ['department'], values: ['headcount'], filter: { status: { $ne: 'terminated' } }, compareTo: { kind: 'previousYear' } } @@ -286,7 +286,7 @@ import { defineDataset, type DashboardWidget } from '@objectstack/spec/ui'; // In the dataset (Guides → Analytics Datasets): defineDataset({ - name: 'contract_metrics', label: 'Contract Metrics', object: 'contract', + name: 'crm_contract_metrics', label: 'Contract Metrics', object: 'contract', dimensions: [{ name: 'signed_date', field: 'signed_date', type: 'date', dateGranularity: 'month' }], measures: [{ name: 'signed_count', aggregate: 'count' }], }); @@ -304,7 +304,7 @@ defineDataset({ // The widget just selects the dimension by name: const signedByMonth: DashboardWidget = { id: 'signed_by_month', type: 'line', - dataset: 'contract_metrics', dimensions: ['signed_date'], values: ['signed_count'], + dataset: 'crm_contract_metrics', dimensions: ['signed_date'], values: ['signed_count'], filter: { signed_date: { $gte: '{12_months_ago}' } }, compareTo: { kind: 'previousYear' } }; ``` @@ -361,8 +361,7 @@ behind it; **drill-to-record** opens one record. * **Escape hatch — "Open in list →".** The drawer header offers a link to the object's *full* list page (sort / bulk-select / export / shareable URL), scoped by the same drill filter. The in-place drawer is the default (peek - without losing the dashboard); the escape hatch escalates when the user wants - the full surface. + without losing the dashboard). * **`metric` / `chart` widgets are not click-drillable** in the dataset form (they render the aggregate only; `compareTo` still applies). Surface the detail through a `table` / `pivot` widget instead. @@ -380,14 +379,14 @@ records, click a row to open one, or "Open in list →" for the full list page. import { defineReport } from '@objectstack/spec/ui'; // ADR-0021: a report binds a `dataset` and selects `rows` (dimensions) + -// `values` (measures) BY NAME. The `opportunity_metrics` dataset defines the +// `values` (measures) BY NAME. The `crm_opportunity_metrics` dataset defines the // object, the `amount_sum` measure, and the `forecast_category` + `close_date` // (dateGranularity: 'quarter') dimensions — see Guides → Analytics Datasets. export const PipelineCoverageReport = defineReport({ - name: 'pipeline_coverage_by_quarter', + name: 'crm_pipeline_coverage_by_quarter', label: 'Pipeline Coverage (Quarter)', type: 'matrix', - dataset: 'opportunity_metrics', + dataset: 'crm_opportunity_metrics', rows: ['forecast_category'], // down axis columns: ['close_date'], // across axis (ADR-0021 D2) — matrix pivots rows × columns values: ['amount_sum'], // measures placed in the cells @@ -437,7 +436,7 @@ widgets can compose without hand-rolling each query. Register under import { defineCube } from '@objectstack/spec/data'; export const opportunityCube = defineCube({ - name: 'opportunity', + name: 'crm_opportunity', title: 'Opportunities', sql: 'opportunity', // underlying object name (snake_case) public: true, diff --git a/skills/objectstack-ui/rules/navigation.md b/skills/objectstack-ui/rules/navigation.md index 6698cd905f5..520e2138f28 100644 --- a/skills/objectstack-ui/rules/navigation.md +++ b/skills/objectstack-ui/rules/navigation.md @@ -32,8 +32,8 @@ export const CrmApp = App.create({ { id: 'nav_pipeline', type: 'object', objectName: 'opportunity', viewName: 'pipeline_kanban', label: 'Sales Pipeline', icon: 'columns-3' }, // One-off slice on the bare data surface (Run Modes, below): { id: 'nav_my_open', type: 'object', objectName: 'opportunity', filters: { owner_id: '{current_user_id}', status: 'open' }, label: 'My Open Deals', icon: 'user-check' }, - { id: 'nav_dash', type: 'dashboard', dashboardName: 'sales_dashboard', label: 'Sales Dashboard', icon: 'chart-bar' }, - { id: 'nav_report', type: 'report', reportName: 'opportunities_by_stage', label: 'Opps by Stage', icon: 'bar-chart-3' }, + { id: 'nav_dash', type: 'dashboard', dashboardName: 'crm_sales_dashboard', label: 'Sales Dashboard', icon: 'chart-bar' }, + { id: 'nav_report', type: 'report', reportName: 'crm_opportunities_by_stage', label: 'Opps by Stage', icon: 'bar-chart-3' }, // No `reportName` = the app's report index; the console adds none itself: { id: 'nav_reports', type: 'report', label: 'Reports', icon: 'library' }, ], @@ -72,7 +72,7 @@ navigation pointing at objects. Escalate only on explicit signals: author a view for it; a slice graduates to a named view only when it is curated and reused. Values support `{current_user_id}` / `{current_org_id}`. Never treat it as security: the surface shows what row-level permissions - allow. (Canonical rules: objectui ADR-0055, "parameterized bare data + allow. (objectui ADR-0055: "parameterized bare data surface".) - **Interface page** — persona split ("sales reps see…", customer portal, 给业务部门的简化界面); capability narrowing ("users must not change views", @@ -102,7 +102,7 @@ its own, but new pages define `columns`/`sort`/`filterBy` on the page. import { definePage } from '@objectstack/spec/ui'; export const TaskWorkbenchPage = definePage({ - name: 'task_workbench', + name: 'crm_task_workbench', label: 'Task Workbench', type: 'list', object: 'task', diff --git a/skills/objectstack-ui/rules/pages.md b/skills/objectstack-ui/rules/pages.md index 68fbee9cca2..cfc4cd73338 100644 --- a/skills/objectstack-ui/rules/pages.md +++ b/skills/objectstack-ui/rules/pages.md @@ -101,7 +101,7 @@ a list prints as shown via the view's `allowPrinting`. import { definePage } from '@objectstack/spec/ui'; export const InvoicePrintPage = definePage({ - name: 'invoice_print', label: 'Invoice', type: 'record', object: 'invoice', + name: 'crm_invoice_print', label: 'Invoice', type: 'record', object: 'invoice', print: { paperSize: 'A4', margins: { top: 15, right: 12, bottom: 15, left: 12 }, repeatHeader: true, repeatFooter: true, pageNumbers: true }, regions: [ @@ -149,7 +149,7 @@ on unknown tags / missing required props / forbidden constructs (event handlers, ```typescript export const ReleaseNotesPage = definePage({ - name: 'release_notes', label: 'Release Notes', type: 'home', kind: 'html', + name: 'crm_release_notes', label: 'Release Notes', type: 'home', kind: 'html', source: `

Release Notes

@@ -319,8 +319,7 @@ grounding the AI assistant reads about a package. 2. **Namespace-prefixed filename.** The filename stem becomes the doc `name` (`^[a-z][a-z0-9_]*$`) and must start with the package namespace (`crm_…`). `os build` refuses a bare `user_guide` - (`docs/namespace-prefix`) — hygiene, not uniqueness: two packages - coexist on one bare name (ADR-0048 §3.4). + (`docs/namespace-prefix`). 3. **Title** resolves: frontmatter `title:` → first `#` heading → `name`. Optional frontmatter `description:` is a one-line summary the docs portal shows under the title — add it on index/overview docs. diff --git a/skills/objectstack-upgrade/references/examples-upgrade.md b/skills/objectstack-upgrade/references/examples-upgrade.md index d7c99d703c9..f0c39a1fb98 100644 --- a/skills/objectstack-upgrade/references/examples-upgrade.md +++ b/skills/objectstack-upgrade/references/examples-upgrade.md @@ -110,10 +110,10 @@ export const Lead = ObjectSchema.create({ // Agent capability is reached through skills — there is no inline tool list. export const SupportBot = defineAgent({ - name: 'support_bot', + name: 'crm_support_bot', label: 'Support Bot', role: 'Front-line support triage', instructions: 'Answer support questions and open cases when needed.', - skills: ['case_management'], + skills: ['crm_case_management'], }); ``` diff --git a/skills/objectui/rules/protocol.md b/skills/objectui/rules/protocol.md index 110644aff0e..3d4dbe83c61 100644 --- a/skills/objectui/rules/protocol.md +++ b/skills/objectui/rules/protocol.md @@ -293,7 +293,7 @@ form-supported type (`select`, `lookup`, `date`, `file`, `image`, `richtext`, ```json { - "name": "approve", + "name": "crm_approve", "params": [ { "name": "comment", "type": "textarea", "required": true }, { "name": "attachments", "type": "file", "multiple": true, "accept": ["application/pdf"] }, From b31889fc631363764677964823fa20391c9e7e07 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 05:50:08 +0000 Subject: [PATCH 2/2] docs(skills): rename the upgrade decision row's skill and tighten the action lint wording The objectstack-upgrade "Open decisions" row for `agent.tools` still named the skill `case_management` in both cells while the example below it declares `crm_case_management`; both cells now match. The platform rule's enforcement sentence now says an action warns only when one package declares its name twice on one object, which is how the lint keys actions. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cw4B24PbS5MZbX1r5j --- skills/objectstack-platform/SKILL.md | 2 +- skills/objectstack-upgrade/references/examples-upgrade.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index dc06fb5fd6a..f289bd5b14a 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -120,7 +120,7 @@ to a new protocol major — never to silence a mismatch. **`manifest.namespace` (ADR-0048):** Optional, but **enforced once set**. When a package declares `manifest.namespace: 'crm'`, every `object.name` must start with `crm_` or `defineStack` errors (`validateNamespacePrefix` in `@objectstack/spec`); the legacy `__` double-underscore form is rejected, and `sys_`-prefixed names are platform-reserved and exempt. The namespace is also a package-ownership key — installing two packages that both claim `crm` fails with `NamespaceConflictError` (downgrade to a warning with `OS_METADATA_COLLISION=warn`). -**One rule for every name:** every metadata name a package authors starts with its namespace — flows, actions, hooks, apps, pages, dashboards, reports, datasets, queues, positions, permission sets, skills (`crm_send_email`, `crm_sales_rep`) — so no two installed packages share one. Exceptions: fields, object extensions and container views (derived from a prefixed object), and translations (`_`). Today `defineStack` refuses only a bare object name and `os build` a bare doc name; `os lint`'s `naming/namespace-prefix` warns only when one package declares an app, page, dashboard, flow, action, report or dataset name twice. +**One rule for every name:** every metadata name a package authors starts with its namespace — flows, actions, hooks, apps, pages, dashboards, reports, datasets, queues, positions, permission sets, skills (`crm_send_email`, `crm_sales_rep`) — so no two installed packages share one. Exceptions: fields, object extensions and container views (derived from a prefixed object), and translations (`_`). Today `defineStack` refuses only a bare object name and `os build` a bare doc name; `os lint`'s `naming/namespace-prefix` warns only when one package declares an app, page, dashboard, flow, report or dataset name twice, or an action name twice on one object. **Customizing an installed package (ADR-0146):** Customize an installed base `hotcrm` in its paired writable `hotcrm.custom`, which depends on it: a same-named item there with `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` (its `manifest.id`/`manifest.version`) replaces the base's document whole. Reads honour it only from the pair (the row's `package_id`), for a view, dashboard, report, email_template, translation, flow, action, page, app, dataset, doc, book or mapping, never judging `basedOn`. An email template is one item per (name, locale): an override names a locale the base ships; one it does not ship is added without `overrides`. Then no `?package`, `?package=hotcrm`, the layered read and both lists serve it, and removing the pair's item (`DELETE …?package=hotcrm.custom`) serves the base's again; the env-wide list holds one item per name one code package ships (a dependent's stays under its own `?package=`); a pre-D11 unbound Setup row wins over the base's until `os migrate packageless-overlays` moves it, and once movable as a declared override, its package-less save, publish, reset and rollback are refused (D11); an email-template row at a locale the base does not ship moves as the pair's own member and keeps that write. The base's address serves the pair's added locale too (both lists, the layered read with `code` null and `effective` the pair's, the execution list, a send in that locale); one a dependent adds under the base's name is served only under its own `?package=`. An undeclared pair row or a dependent's row at a base-shipped locale is an orphan in the layered read's `overlay`, `effective` = the base's. An override may narrow a public form's anonymous intake, never re-open one any honoured body withdrew. `GET /packages/hotcrm.custom/export` carries it with `overrides`. diff --git a/skills/objectstack-upgrade/references/examples-upgrade.md b/skills/objectstack-upgrade/references/examples-upgrade.md index f0c39a1fb98..fbd80057c9d 100644 --- a/skills/objectstack-upgrade/references/examples-upgrade.md +++ b/skills/objectstack-upgrade/references/examples-upgrade.md @@ -70,7 +70,7 @@ _N sites, M conversions. Ported into sources from `os migrate meta --out`._ | Item | Site | Options | Recommendation | Blocking? | |:--|:--|:--|:--|:--| -| `agent.tools` → which skill | `src/ai/support-bot.ts:12` | `case_management` · new skill | `case_management` | no — parses without it | +| `agent.tools` → which skill | `src/ai/support-bot.ts:12` | `crm_case_management` · new skill | `crm_case_management` | no — parses without it | ## 4 · Pending, per deployment