Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions objectstack.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { defineStack } from '@objectstack/spec';
import * as objects from './src/objects/index.js';
import { allHooks } from './src/objects/hooks.js';
import * as actions from './src/actions/index.js';
import { allFlows, flowFunctions } from './src/flows/index.js';
import * as profiles from './src/profiles/index.js';
import { ClmPositions, ClmSharingRules } from './src/sharing/index.js';
import { registerClmPositionBindings, type BindHostContext } from './src/security/index.js';
Expand Down Expand Up @@ -58,18 +60,39 @@ export default defineStack({
// that edition. On the enterprise edition the same metadata widens to the
// manager chain. The app states what it MEANS; the edition supplies it.
//
// `automation` is the flow engine: it registers `flows`, runs the
// `contract_intake` screen flow (F1, card 05) and resolves `functions`.
// Measured as required on 17.3.0: without the token the flow-typed
// `launch_contract` action is refused at dispatch (503, "flow action
// unavailable"), because the automation service is never resolved.
//
// `triggers` is deliberately NOT declared yet. It installs the record-change
// and schedule PROVIDERS that launch `record_change` / `schedule` flows;
// this card's flow is a `screen` flow launched by its action, and F2 / F6
// are hooks, not flows — so nothing here is ever fired by a trigger. The
// token arrives with the first card that authors a flow a trigger launches
// (F5 in card 06, the daily jobs in card 09), the same way `sharing` and
// `hierarchy-security` arrived with card 04: measured, then declared.
//
// The rest arrive with the cards that need them (DESIGN.md §06:
// `automation` + `triggers` + `approvals` + `messaging` for F1–F15,
// `analytics` for §09). Capability expansion stays tight — a card names the
// token it adds (AGENTS.md).
requires: ['ui', 'auth', 'sharing', 'hierarchy-security'],
// `approvals` + `messaging` for F5–F15, `analytics` for §09). Capability
// expansion stays tight — a card names the token it adds (AGENTS.md).
requires: ['ui', 'auth', 'sharing', 'hierarchy-security', 'automation'],

objects: Object.values(objects),
// Lifecycle hooks (numbering, type-derived stamps, the state machines and
// the display_name mirrors). A metadata `Hook` is only registered from
// here — `hooks` is a top-level stack key, not an object key.
hooks: allHooks,

// Automation (DESIGN.md §06, card 05): the intake screen flow (F1) and the
// action that launches it. The routing (F2) and the deviation gate (F6)
// are hooks above. `functions` holds the one callable a `script` node
// names — the refusal that fails a run with a message.
flows: allFlows,
actions: Object.values(actions),
functions: flowFunctions,

// Security (DESIGN.md §04, card 04): seven positions, five permission sets,
// the sharing rules. Positions and sets are containers — the join rows that
// make a position grant anything are written by `onEnable` below.
Expand Down
95 changes: 95 additions & 0 deletions src/actions/contract.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { Action } from '@objectstack/spec/ui';
import { P } from '@objectstack/spec';

/**
* "Launch Contract" — the button behind F1 (DESIGN.md §05 "发起 = screen flow
* 动作"), bound to the `contract_intake` flow.
*
* ## Why the action carries params at all
*
* The flat screens of a flow have no record picker (measured on the pinned
* console — see the flow's header), so the two pickers the intake needs are
* collected HERE, by the action's param dialog: field-backed lookup params
* render the real pickers and inherit the fields' `lookupFilters` (active
* types only; blocked parties hidden). The console posts them as the flow's
* `params`, which the engine seeds into the input variables of the same name.
*
* ## The headless half
*
* `ai.exposed` publishes this action to the MCP surface and to the
* `POST /api/v1/actions/clm_contract/launch_contract` route — DESIGN.md §07's
* conversational intake for M2. Measured on 17.3.0: MCP does not mint one
* tool per action; it ships the generic pair `list_actions` / `run_action`,
* and an exposed action appears in `list_actions` (with its params) for
* `run_action` to call by name. The REST route takes the params under a
* `params` wrapper — a bare top-level body is refused
* "Invalid action params: Action param \"contract_type\" is required". Over those doors the params gate (ADR-0104)
* refuses any key the action does not declare, so EVERY flow input is
* declared below; the ones a person answers on the flow's screens are
* `visible: false`, which keeps them out of the console dialog (and out of
* its submission) while leaving them declared for a headless caller. A
* headless call that supplies `title` completes without a screen; one that
* supplies fewer inputs pauses on the first screen it needs.
*
* `list_toolbar` only: the action creates a record, so it has no record to
* sit on. Declaring a record-scoped location would make the console demand a
* selected row first.
*/

type ActionParam = NonNullable<Action['params']>[number];

/** A flow input a headless caller may pass and the screens collect from a person. */
const headless = (name: string, type: ActionParam['type'], label: string): ActionParam => ({
name,
type,
label,
visible: P`false`,
});

export const LaunchContractAction: Action = {
name: 'launch_contract',
label: 'Launch Contract',
objectName: 'clm_contract',
icon: 'rocket',
type: 'flow',
target: 'contract_intake',
locations: ['list_toolbar'],
params: [
// The two pickers a flat screen cannot render.
{ field: 'contract_type', label: 'Contract type', required: true, helpText: 'The workflow this contract runs: which fields are asked, whether legal reviews it, how it is executed.' },
{ field: 'party', label: 'Counterparty', required: false, helpText: 'Pick an existing counterparty, or leave empty to create one on the next step.' },
// Everything else is answered on the flow's screens; declared here for
// the headless doors only.
headless('title', 'text', 'Title'),
headless('our_entity', 'text', 'Our signing entity'),
headless('department', 'text', 'Requesting department'),
headless('amount', 'number', 'Contract amount'),
headless('currency_code', 'text', 'Currency (ISO 4217, lowercase)'),
headless('is_amount_estimated', 'boolean', 'Amount is an estimate'),
headless('start_date', 'date', 'Start date'),
headless('end_date', 'date', 'End date'),
headless('term_months', 'number', 'Term (months)'),
headless('summary', 'text', 'Summary'),
headless('governing_law', 'text', 'Governing law'),
headless('payment_terms', 'text', 'Payment terms'),
headless('confidentiality_term_months', 'number', 'Confidentiality term (months)'),
headless('auto_renew', 'boolean', 'Auto-renews'),
headless('parent_contract', 'text', 'Parent contract (record id)'),
headless('new_party_name', 'text', 'New counterparty: name'),
headless('new_party_kind', 'text', 'New counterparty: kind'),
headless('new_party_registration_no', 'text', 'New counterparty: registration / tax ID'),
headless('new_party_contact_name', 'text', 'New counterparty: contact name'),
headless('new_party_contact_email', 'text', 'New counterparty: contact email'),
headless('draft_from_template', 'boolean', 'Draft version 1 from the type template'),
headless('first_version_file', 'text', 'Version 1: uploaded file id'),
headless('submit_now', 'boolean', 'Submit now'),
],
successMessage: 'Contract launched.',
refreshAfter: true,
ai: {
exposed: true,
category: 'flow',
description:
'Launches a contract: creates the contract draft and its first version from the given contract type, counterparty (an existing record id, or a new one by name) and the core terms plus whichever optional fields the type asks for, then optionally submits it into legal review or approval. Refuses a blocked counterparty, naming it and why.',
},
};
2 changes: 2 additions & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Actions bound to flows and to object surfaces (DESIGN.md §05, §06).
export { LaunchContractAction } from './contract.actions.js';
Loading