diff --git a/.changeset/20751-services-strings-stage4-state-the-decision.md b/.changeset/20751-services-strings-stage4-state-the-decision.md new file mode 100644 index 00000000000..d5b7d11efb9 --- /dev/null +++ b/.changeset/20751-services-strings-stage4-state-the-decision.md @@ -0,0 +1,15 @@ +--- +'@objectstack/service-automation': patch +'@objectstack/plugin-audit': patch +--- + +Automation refusals, prescriptions, log lines and run-object field help, and the activity type help, no longer cite tracker numbers; each one states the decision behind it in words + +Clause-②: no + +Some strings these two packages show to flow authors, operators and administrators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + +- `@objectstack/service-automation`: the refusal for a `fieldValues` write map says a runtime alias for it was rejected by design, so the node keeps one strict `fields` key; the refusal for a screen field's `visibleIf` says a predicate under any other key is never read, so the field always shows, and a `required` field meant to stay hidden then blocks the screen from ever being submitted; the undeclared-config-key refusal says the built-in node types were reconciled so that every key their executors read is declared; the unknown-function error in a flow value expression says such a name is refused rather than evaluated to null, which would write the field as undefined; the inert-connector warning says entries without a `provider` are catalog descriptors, while an entry that names a `provider` is a connector instance that provider's installed executor materializes; the `sys_automation_run` field help says the paused node's type decides who may continue a run (an approval pause only through its owning service), that rows written before run history recorded its trigger were not backfilled, and that a finished run's bounded step log keeps its per-node detail across a restart; three bridge debug lines say what each bridge provides. The bulk-intent guidance, the degraded-connector dispatch error and retry lines, the user-less `runAs` warning and refusal, the unclaimed-branch warning, the script-function and node-config refusals and the `sys_flow_dispatch` description drop their citations. +- `@objectstack/plugin-audit`: the `sys_activity` `type` help, whose English text all four shipped locale bundles carry, says the vocabulary is open by decision, not a gap awaiting enforcement. + +Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. diff --git a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts index b0db4ce282e..92d0d2cc0fe 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts @@ -117,8 +117,8 @@ export const SysActivity = ObjectSchema.create({ + 'vocabulary, not a closed enum: metadata authors may contribute their own values ' + '(sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an ' + 'undeclared value is stored verbatim rather than rejected. Consumers must render ' - + 'an unknown value instead of assuming this list is exhaustive (maintainer ruling ' - + '2026-08-24, #11507).', + + 'an unknown value instead of assuming this list is exhaustive: the vocabulary is ' + + 'open by decision, not a gap awaiting enforcement.', required: true, readonly: true, searchable: true, diff --git a/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts index 7c4575273dc..305f981652b 100644 --- a/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts @@ -122,7 +122,7 @@ export const enObjects: NonNullable = { }, type: { label: "Type", - help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive (maintainer ruling 2026-08-24, #11507).", + help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive: the vocabulary is open by decision, not a gap awaiting enforcement.", options: { created: "created", updated: "updated", diff --git a/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts index a1e96820b12..5f7589ec20b 100644 --- a/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts @@ -122,7 +122,7 @@ export const esESObjects: NonNullable = { }, type: { label: "Tipo", - help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive (maintainer ruling 2026-08-24, #11507).", + help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive: the vocabulary is open by decision, not a gap awaiting enforcement.", options: { created: "Creado", updated: "Actualizado", diff --git a/packages/plugins/plugin-audit/src/translations/es-ES.source-hashes.generated.ts b/packages/plugins/plugin-audit/src/translations/es-ES.source-hashes.generated.ts index b3e645d2a0d..a013415bcb1 100644 --- a/packages/plugins/plugin-audit/src/translations/es-ES.source-hashes.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/es-ES.source-hashes.generated.ts @@ -19,7 +19,7 @@ export const esESGeneratedSourceHashes: Readonly> = { "objects.sys_activity.fields.actor_id.label": "b155813f8a7f06e3", - "objects.sys_activity.fields.type.help": "bc8b10c89aacd494", + "objects.sys_activity.fields.type.help": "5d8df777db464241", "objects.sys_activity.fields.url.label": "2203b0b9f72534f7", "objects.sys_audit_log.fields.actor.label": "b155813f8a7f06e3", }; diff --git a/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts index e600352bd82..b741e95be48 100644 --- a/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts @@ -122,7 +122,7 @@ export const jaJPObjects: NonNullable = { }, type: { label: "タイプ", - help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive (maintainer ruling 2026-08-24, #11507).", + help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive: the vocabulary is open by decision, not a gap awaiting enforcement.", options: { created: "作成", updated: "更新", diff --git a/packages/plugins/plugin-audit/src/translations/ja-JP.source-hashes.generated.ts b/packages/plugins/plugin-audit/src/translations/ja-JP.source-hashes.generated.ts index bee58465af7..908cf67d9dd 100644 --- a/packages/plugins/plugin-audit/src/translations/ja-JP.source-hashes.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/ja-JP.source-hashes.generated.ts @@ -18,6 +18,6 @@ */ export const jaJPGeneratedSourceHashes: Readonly> = { - "objects.sys_activity.fields.type.help": "bc8b10c89aacd494", + "objects.sys_activity.fields.type.help": "5d8df777db464241", "objects.sys_activity.fields.url.label": "2203b0b9f72534f7", }; diff --git a/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts index c0b8c7eef4a..50d960c0dbc 100644 --- a/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts @@ -122,7 +122,7 @@ export const zhCNObjects: NonNullable = { }, type: { label: "类型", - help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive (maintainer ruling 2026-08-24, #11507).", + help: "Activity kind. The declared options are the platform BUILT-IN set of an open vocabulary, not a closed enum: metadata authors may contribute their own values (sanctioned channel: `activityMilestones[].type`, ADR-0052 §5b.2), and an undeclared value is stored verbatim rather than rejected. Consumers must render an unknown value instead of assuming this list is exhaustive: the vocabulary is open by decision, not a gap awaiting enforcement.", options: { created: "已创建", updated: "已更新", diff --git a/packages/plugins/plugin-audit/src/translations/zh-CN.source-hashes.generated.ts b/packages/plugins/plugin-audit/src/translations/zh-CN.source-hashes.generated.ts index 0a3f00cf1cb..def45595778 100644 --- a/packages/plugins/plugin-audit/src/translations/zh-CN.source-hashes.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/zh-CN.source-hashes.generated.ts @@ -18,6 +18,6 @@ */ export const zhCNGeneratedSourceHashes: Readonly> = { - "objects.sys_activity.fields.type.help": "bc8b10c89aacd494", + "objects.sys_activity.fields.type.help": "5d8df777db464241", "objects.sys_activity.fields.url.label": "2203b0b9f72534f7", }; diff --git a/packages/services/service-automation/src/builtin/config-unknown-keys.test.ts b/packages/services/service-automation/src/builtin/config-unknown-keys.test.ts index 5d42633825a..78d53633465 100644 --- a/packages/services/service-automation/src/builtin/config-unknown-keys.test.ts +++ b/packages/services/service-automation/src/builtin/config-unknown-keys.test.ts @@ -94,7 +94,7 @@ describe('unknown node config keys are rejected (#4277)', () => { // …and this particular key has a documented incident, so it also carries // its tombstone (the UNKNOWN_KEY_GUIDANCE pattern). expect(msg).toContain('visibleWhen'); - expect(msg).toContain('#3528'); + expect(msg).toContain('blocks the screen from ever being submitted'); // The flow is NOT registered. await expect(engine.getFlow('f')).resolves.toBeNull(); }); diff --git a/packages/services/service-automation/src/builtin/connector-nodes.ts b/packages/services/service-automation/src/builtin/connector-nodes.ts index 9c8fc3804ad..817b75c77f1 100644 --- a/packages/services/service-automation/src/builtin/connector-nodes.ts +++ b/packages/services/service-automation/src/builtin/connector-nodes.ts @@ -69,7 +69,7 @@ export function registerConnectorNodes(engine: AutomationEngine, ctx: PluginCont success: false, error: `connector_action '${node.id}': connector '${cfg.connectorId}' is degraded — ${degraded}. ` - + `Dispatch is unavailable until its upstream recovers; the platform retries automatically (#3017).`, + + `Dispatch is unavailable until its upstream recovers; the platform retries automatically.`, }; } return { diff --git a/packages/services/service-automation/src/builtin/decision-branch-routing.test.ts b/packages/services/service-automation/src/builtin/decision-branch-routing.test.ts index ae6edf032de..169d6380ae0 100644 --- a/packages/services/service-automation/src/builtin/decision-branch-routing.test.ts +++ b/packages/services/service-automation/src/builtin/decision-branch-routing.test.ts @@ -207,7 +207,7 @@ describe('decision branch routing (#4414)', () => { // branch and every out-edge label. expect(warnings.some((w) => w.msg.includes('no out-edge carries that label') - && w.msg.includes('#4414') + && w.msg.includes('The branch selection is IGNORED') && w.meta?.branchLabel === 'Yes — already converted' && (w.meta?.outEdges as Array<{ label: string | null }>) .map((e) => e.label).includes('Yes') diff --git a/packages/services/service-automation/src/builtin/parse-config.ts b/packages/services/service-automation/src/builtin/parse-config.ts index 4a37b8d3233..c2472a2abaa 100644 --- a/packages/services/service-automation/src/builtin/parse-config.ts +++ b/packages/services/service-automation/src/builtin/parse-config.ts @@ -109,7 +109,7 @@ export function parseNodeConfig( `${nodeType} '${nodeId}': config does not satisfy the ${nodeType} contract — ${issues || 'invalid config'}. ` + `The config is metadata, so re-running changes nothing; fix the node in the flow definition. ` + `The declared contract is the node type's configSchema (the Studio form) and the ` + - `${nodeType} config Zod in @objectstack/spec/automation (#4277).`, + `${nodeType} config Zod in @objectstack/spec/automation.`, ), }; } diff --git a/packages/services/service-automation/src/builtin/screen-nodes.ts b/packages/services/service-automation/src/builtin/screen-nodes.ts index 409b2419316..3f29f606d59 100644 --- a/packages/services/service-automation/src/builtin/screen-nodes.ts +++ b/packages/services/service-automation/src/builtin/screen-nodes.ts @@ -333,7 +333,7 @@ export function registerScreenNodes(engine: AutomationEngine, ctx: PluginContext success: false, error: `script node '${node.id}': no function named '${target}' is registered. ` + - `Register it via \`defineStack({ functions: { '${target}': fn } })\`, or fix the name (#1870).`, + `Register it via \`defineStack({ functions: { '${target}': fn } })\`, or fix the name.`, }; } diff --git a/packages/services/service-automation/src/builtin/template.ts b/packages/services/service-automation/src/builtin/template.ts index bb05ebc9fd2..869dd5ba3b6 100644 --- a/packages/services/service-automation/src/builtin/template.ts +++ b/packages/services/service-automation/src/builtin/template.ts @@ -189,7 +189,7 @@ function unknownFunctionError(name: string, expr: string): FlowExpressionFunctio `flow value expression: unknown function '${name}' in '${expr}'. ` + `Value expressions support round, floor, ceil, abs, min, max (1:1 with the CEL stdlib) ` + `and the whole-token date macros NOW() / TODAY().${hint} ` + - `(Before #11060 this name was silently rewritten to null and the field was written undefined.)`, + `An unknown function is refused here rather than evaluated to null, which would write the field as undefined.`, ); } diff --git a/packages/services/service-automation/src/connector-degrade-cause.test.ts b/packages/services/service-automation/src/connector-degrade-cause.test.ts index 6cb5cb28449..ccd37272a54 100644 --- a/packages/services/service-automation/src/connector-degrade-cause.test.ts +++ b/packages/services/service-automation/src/connector-degrade-cause.test.ts @@ -182,7 +182,7 @@ describe('#5636 — the degrade announcement is ONE record, cause in meta', () = expect(record.msg).toContain("'gh_mcp'"); expect(record.msg).toContain("provider 'fake'"); expect(record.msg).toContain('instance registered degraded (no actions)'); - expect(record.msg).toContain('attempt 1 (#3017)'); + expect(record.msg).toContain('retrying with backoff, attempt 1.'); // Not a validation rejection → `error`, not `issues`; and the full text // survives, newlines escaped by the logger's JSON.stringify. expect(record.issues).toBeUndefined(); @@ -271,7 +271,7 @@ describe('#5636 — a husk-registration rejection reports its issues, on one lin }; expect(record.msg).not.toContain('\n'); expect(record.msg).toContain("'gh_mcp'"); - expect(record.msg).toContain('#3017'); + expect(record.msg).toContain('stays absent from the connector registry until a retry succeeds'); // The facts a reader came for. const issues = record.issues; expect(Array.isArray(issues)).toBe(true); @@ -373,7 +373,7 @@ describe('#5636 — what the interpolated rendering cost, measured', () => { expect(before.length - beforeKept.length, 'lines the buffer drops').toBeGreaterThan(1); const after = await captureStream('stdout', async () => { - log.warn(`[Automation] could not register degraded husk for 'gh_mcp' (#3017).`, { + log.warn(`[Automation] could not register degraded husk for 'gh_mcp'.`, { issues: [{ code: 'invalid_value', path: 'type', message: 'Invalid option' }], }); }); @@ -393,7 +393,7 @@ describe('#5636 — what the interpolated rendering cost, measured', () => { const before = await captureStream('stderr', async () => { log.error( `[Automation] connector instance 'gh_mcp' (provider 'fake') upstream unavailable — ` + - `instance registered degraded (no actions); retrying with backoff, attempt 1 (#3017): ${MULTILINE_UPSTREAM}`, + `instance registered degraded (no actions); retrying with backoff, attempt 1: ${MULTILINE_UPSTREAM}`, ); }); expect(before).toHaveLength(3); @@ -404,7 +404,7 @@ describe('#5636 — what the interpolated rendering cost, measured', () => { const after = await captureStream('stderr', async () => { log.error( `[Automation] connector instance 'gh_mcp' (provider 'fake') upstream unavailable — ` + - `instance registered degraded (no actions); retrying with backoff, attempt 1 (#3017).`, + `instance registered degraded (no actions); retrying with backoff, attempt 1.`, undefined, { error: MULTILINE_UPSTREAM }, ); diff --git a/packages/services/service-automation/src/connector-descriptor-audit.test.ts b/packages/services/service-automation/src/connector-descriptor-audit.test.ts index 0056c13e539..f3bce826135 100644 --- a/packages/services/service-automation/src/connector-descriptor-audit.test.ts +++ b/packages/services/service-automation/src/connector-descriptor-audit.test.ts @@ -131,7 +131,7 @@ describe('kernel:ready audit (wiring)', () => { const warnings = auditWarnings(warnSpy); expect(warnings).toHaveLength(1); expect(warnings[0]).toContain('crm_billing'); - expect(warnings[0]).toContain('#2612'); + expect(warnings[0]).toContain('catalog descriptors (descriptor-only contract)'); await kernel.shutdown(); }); diff --git a/packages/services/service-automation/src/degraded-register-cause.test.ts b/packages/services/service-automation/src/degraded-register-cause.test.ts index 339ec4f62ec..7709ec109a1 100644 --- a/packages/services/service-automation/src/degraded-register-cause.test.ts +++ b/packages/services/service-automation/src/degraded-register-cause.test.ts @@ -209,7 +209,7 @@ describe('#5660 — the degrade REGISTRATION is one record, cause in meta', () = // Self-sufficient without the reason: what the state costs and what // happens next, so the message alone is a usable record. expect(record.msg).toContain('no actions and no handlers'); - expect(record.msg).toContain('#3017'); + expect(record.msg).toContain('the materializer retries with backoff'); // …and none of the foreign text is in it. expect(record.msg).not.toContain(CAUSE_LINE_FACT); expect(record.msg).not.toContain(HINT_LINE_FACT); @@ -404,7 +404,7 @@ describe('#5660 — what the interpolated rendering cost, measured', () => { log.warn( 'Connector registered DEGRADED: gh_mcp (origin: declarative) — no actions and no handlers ' + 'until its upstream is reachable; a connector_action dispatching to it fails with the stored ' + - 'reason, and the materializer retries with backoff (#3017).', + 'reason, and the materializer retries with backoff.', { degradedReason: MULTILINE_UPSTREAM, error: MULTILINE_UPSTREAM }, ); }); diff --git a/packages/services/service-automation/src/engine.ts b/packages/services/service-automation/src/engine.ts index d599cce4fb9..7372f2a8f4f 100644 --- a/packages/services/service-automation/src/engine.ts +++ b/packages/services/service-automation/src/engine.ts @@ -178,37 +178,39 @@ interface ConfigSchemaNode { */ const BULK_INTENT_GUIDANCE: Record = { bulk: 'Bulk intent is `multi: true` — the data engine\'s own word for it (`options.multi`), so the concept ' + - 'keeps one name from node config to driver call (#5393). Without it the write must name one row by ' + + 'keeps one name from node config to driver call. Without it the write must name one row by ' + 'scalar `id`; a predicate write is refused by the engine rather than silently widened.', all: 'Bulk intent is `multi: true` — the data engine\'s own word for it (`options.multi`), so the concept ' + - 'keeps one name from node config to driver call (#5393). Without it the write must name one row by ' + + 'keeps one name from node config to driver call. Without it the write must name one row by ' + 'scalar `id`; a predicate write is refused by the engine rather than silently widened.', multiple: 'Bulk intent is `multi: true` — the data engine\'s own word for it (`options.multi`), so the ' + - 'concept keeps one name from node config to driver call (#5393). Without it the write must name one row ' + + 'concept keeps one name from node config to driver call. Without it the write must name one row ' + 'by scalar `id`; a predicate write is refused by the engine rather than silently widened.', options: 'This is the NODE config, not the data engine\'s options bag — declare `multi: true` at the top ' + 'level of `config`, never `options: { multi: true }`. Translating that declaration into `options.multi` ' + - 'on the engine call is the executor\'s job (#5393).', + 'on the engine call is the executor\'s job.', }; const FLOW_NODE_UNKNOWN_KEY_GUIDANCE: Record> = { create_record: { fieldValues: 'The write map is `fields` — `fieldValues` was an AI-authoring dialect that never had a ' + - 'runtime reader; the fix is the authoring source + this rejection, not a runtime alias ' + - '(#2419, rejected by design).', + 'runtime reader; the fix is the authoring source + this rejection. A runtime alias for it was ' + + 'rejected by design: the node keeps one strict `fields` key rather than two spellings.', }, update_record: { fieldValues: 'The write map is `fields` — `fieldValues` was an AI-authoring dialect that never had a ' + - 'runtime reader (#2419, rejected by design).', + 'runtime reader, and a runtime alias for it was rejected by design: the node keeps one strict ' + + '`fields` key rather than two spellings.', ...BULK_INTENT_GUIDANCE, }, delete_record: BULK_INTENT_GUIDANCE, screen: { visibleIf: 'The visibility predicate is `visibleWhen` (bare CEL, re-evaluated client-side as the ' + - 'user types — #3528, the incident this whole check descends from).', + 'user types). A predicate under any other key is never read, so the field always shows — and a ' + + '`required` field meant to stay hidden then blocks the screen from ever being submitted.', }, }; import { runIsUnscopedUserMode, flowTouchesData } from './runtime-identity.js'; @@ -4021,7 +4023,7 @@ export class AutomationEngine implements IAutomationService { this.logger.warn( `Connector registered DEGRADED: ${parsed.name} (origin: ${origin}) — no actions and no handlers ` + `until its upstream is reachable; a connector_action dispatching to it fails with the stored ` + - `reason, and the materializer retries with backoff (#3017).`, + `reason, and the materializer retries with backoff.`, cause === undefined ? { degradedReason: reason } : { degradedReason: reason, ...describeThrownForLog(cause) }, @@ -6064,11 +6066,11 @@ export class AutomationEngine implements IAutomationService { if (runIsUnscopedUserMode(runContext) && flowTouchesData(flow)) { this.logger.warn( `[runAs] flow '${flow.name}' executes with runAs:'user' but its trigger resolved no user ` + - `— its data operations will be REFUSED (#3760). Running them would execute UNSCOPED ` + + `— its data operations will be REFUSED. Running them would execute UNSCOPED ` + `(elevated, RLS-bypassing) rather than restricted, which is the fail-open ADR-0049 ` + `forbids. Declare runAs:'system' to make the elevation explicit and intended, or arrange ` + `for the trigger to supply a user. Note a user-less trigger is NOT only a schedule: a ` + - `record-change flow fired by a system write carries no user either (ADR-0049, #1888).`, + `record-change flow fired by a system write carries no user either (ADR-0049).`, ); } @@ -10263,12 +10265,13 @@ export class AutomationEngine implements IAutomationService { } if (violations.length > 0) { throw new Error( - `Flow '${flowName}' rejected: ${violations.length} undeclared config key(s) (#4277).\n` + + `Flow '${flowName}' rejected: ${violations.length} undeclared config key(s).\n` + violations.map((v) => ` - ${v}`).join('\n') + `\nAn undeclared key is never read, so it can only be a typo or dead config — fix the ` + `flow's metadata (rename or remove the key). If an executor genuinely reads this key, ` + - `declare it on the node type's descriptor configSchema instead; read-but-undeclared ` + - `keys are exactly the drift the #4045 reconciliation closed.`, + `declare it on the node type's descriptor configSchema instead: the built-in node types were ` + + `reconciled so every key their executors read is declared, and a read-but-undeclared key is ` + + `exactly the drift that reconciliation removed.`, ); } } @@ -11320,7 +11323,7 @@ export class AutomationEngine implements IAutomationService { `in this record's meta. The branch selection is IGNORED and every out-edge is ` + `evaluated instead, so unconditional siblings run regardless of the decision. ` + `Make an out-edge's \`label\` match the branch, or mark the fallback edge ` + - `\`isDefault: true\`. (#4414)`, + `\`isDefault: true\`.`, { branchLabel, outEdges: allOutEdges.map(e => ({ id: e.id, label: e.label ?? null })), diff --git a/packages/services/service-automation/src/plugin.ts b/packages/services/service-automation/src/plugin.ts index 30d4b604b0a..d6d14f0d429 100644 --- a/packages/services/service-automation/src/plugin.ts +++ b/packages/services/service-automation/src/plugin.ts @@ -1140,7 +1140,10 @@ export class AutomationServicePlugin implements Plugin { if (ql?.registry && typeof ql.registry.getObject === 'function') { this.engine.setObjectSchemaResolver((objectName) => parseObjectFieldSchema((ql.registry!.getObject!(objectName) as { fields?: unknown } | undefined)?.fields)); - ctx.logger.debug('[Automation] object-schema resolver bridged to objectql.registry (#1928 condition checks)'); + ctx.logger.debug( + '[Automation] object-schema resolver bridged to objectql.registry ' + + '(flow conditions are checked against object fields at registration)', + ); } } catch { ctx.logger.debug('[Automation] objectql registry not present — flow-condition checks limited to syntax'); @@ -1178,7 +1181,10 @@ export class AutomationServicePlugin implements Plugin { ...(tenantId ? { tenantId } : {}), }; }); - ctx.logger.debug('[Automation] runAs:user grant resolver bridged to @objectstack/core resolveUserAuthzGrants (#3356)'); + ctx.logger.debug( + '[Automation] runAs:user grant resolver bridged to @objectstack/core resolveUserAuthzGrants ' + + '(a user-mode run carries the triggering user\'s positions and permission sets)', + ); // #3475 — bridge the lookup expander for record-change flow // templates. Re-reads the relations a flow declares in its start @@ -1205,7 +1211,10 @@ export class AutomationServicePlugin implements Plugin { const full = await expandQl.findOne!(objectName, query); return full && typeof full === 'object' ? (full as Record) : undefined; }); - ctx.logger.debug('[Automation] record-change lookup expander bridged (#3475)'); + ctx.logger.debug( + '[Automation] record-change lookup expander bridged ' + + '(a lookup the start node declares in expand is read with the run\'s own identity)', + ); } } else { ctx.logger.debug('[Automation] objectql not present — runAs:user runs keep the trigger-supplied identity'); @@ -1670,12 +1679,14 @@ export class AutomationServicePlugin implements Plugin { ctx.logger.warn( `[Automation] ${inert.length} declarative connector(s) declare actions but are not registered ` + `in the connector registry — the connector_action node cannot dispatch them: ${inert.join(', ')}. ` + - `Declarative \`connectors:\` entries are catalog descriptors (descriptor-only contract, #2612); ` + - `runtime connectors are contributed by plugins via engine.registerConnector() — e.g. ` + + `Declarative \`connectors:\` entries without a \`provider\` are catalog descriptors ` + + `(descriptor-only contract); runtime connectors are contributed by plugins via ` + + `engine.registerConnector() — e.g. ` + `@objectstack/connector-rest, @objectstack/connector-slack, @objectstack/connector-openapi, ` + `@objectstack/connector-mcp. Install/instantiate the matching connector plugin, or mark a ` + `deliberate catalog-only entry with \`enabled: false\` to silence this warning. ` + - `Declarative provider-bound connector instances are tracked in #2977 (ADR-0097).`, + `An entry that names a \`provider\` is a connector instance instead: that provider's ` + + `installed executor materializes it into a live connector (ADR-0097).`, ); } @@ -1817,7 +1828,7 @@ export class AutomationServicePlugin implements Plugin { // the live connector is already the desired one: cancel it. if (this.degradedInstances.delete(name)) { ctx.logger.info( - `[Automation] connector instance '${name}' reverted to its live configuration; pending retry cancelled (#3017)`, + `[Automation] connector instance '${name}' reverted to its live configuration; pending retry cancelled`, ); } continue; @@ -2060,7 +2071,7 @@ export class AutomationServicePlugin implements Plugin { // cause goes there. ctx.logger.warn( `[Automation] could not register degraded husk for '${info.name}' — the instance stays absent from the ` + - `connector registry until a retry succeeds (#3017).`, + `connector registry until a retry succeeds.`, describeThrownForLog(err), ); } @@ -2072,7 +2083,7 @@ export class AutomationServicePlugin implements Plugin { (info.hasLive ? 'the previously-materialized connector keeps serving' : 'instance registered degraded (no actions)') + - `; retrying with backoff, attempt ${attempts} (#3017).`, + `; retrying with backoff, attempt ${attempts}.`, undefined, describeThrownForLog(info.cause), ); @@ -2129,7 +2140,7 @@ export class AutomationServicePlugin implements Plugin { (timer as unknown as { unref?: () => void }).unref?.(); this.declarativeRetryTimer = timer; ctx.logger.info( - `[Automation] ${this.degradedInstances.size} degraded connector instance(s); next retry in ${delay}ms (#3017)`, + `[Automation] ${this.degradedInstances.size} degraded connector instance(s); next retry in ${delay}ms`, ); } diff --git a/packages/services/service-automation/src/runtime-identity.ts b/packages/services/service-automation/src/runtime-identity.ts index 27a0301001f..4bed142c70c 100644 --- a/packages/services/service-automation/src/runtime-identity.ts +++ b/packages/services/service-automation/src/runtime-identity.ts @@ -110,7 +110,7 @@ export class UnscopedRunDataAccessError extends Error { `'user' but no trigger user could be resolved, so the operation would execute UNSCOPED ` + `(elevated, RLS-bypassing) rather than restricted to a user. Declare \`runAs: 'system'\` on the ` + `flow to make the elevation explicit and intended, or arrange for the trigger to supply a user ` + - `(a write made with a system context carries none). (ADR-0049, #1888, #3760)`, + `(a write made with a system context carries none). (ADR-0049)`, ); this.name = 'UnscopedRunDataAccessError'; // #3863 — a guard refusal, so a `fault` edge must not route it. Elevation diff --git a/packages/services/service-automation/src/sys-automation-run.object.ts b/packages/services/service-automation/src/sys-automation-run.object.ts index bb97a4daca9..b0bd1b4c557 100644 --- a/packages/services/service-automation/src/sys-automation-run.object.ts +++ b/packages/services/service-automation/src/sys-automation-run.object.ts @@ -166,7 +166,7 @@ export const SysAutomationRun = ObjectSchema.create({ label: 'Node Type', required: false, maxLength: 255, - description: 'Registry type of the node a suspended run paused at (approval / screen / wait / …). Keys the resume authorization gate (#3801) — captured at suspend time rather than re-read from a flow that may have been republished since. Null on rows written before the gate shipped, and on terminal history rows — except the one class of terminal row that carries a restorable consumed suspension (a run whose resume consumed its pause and then failed downstream), which keeps the paused node\'s type so a restore re-arms the gate.', + description: 'Registry type of the node a suspended run paused at (approval / screen / wait / …). Keys the resume authorization gate, which decides from the paused node\'s type who may continue the run (an approval pause only through its owning service) — captured at suspend time rather than re-read from a flow that may have been republished since. Null on rows written before the gate shipped, and on terminal history rows — except the one class of terminal row that carries a restorable consumed suspension (a run whose resume consumed its pause and then failed downstream), which keeps the paused node\'s type so a restore re-arms the gate.', group: 'State', }), @@ -231,7 +231,7 @@ export const SysAutomationRun = ObjectSchema.create({ label: 'Trigger Type', required: false, maxLength: 255, - description: 'What fired this run — the runtime trigger event (record-after-update / schedule / api / time_relative / manual / …). Null on rows written before #7533, which is NOT the same as "no trigger": every run has one.', + description: 'What fired this run — the runtime trigger event (record-after-update / schedule / api / time_relative / manual / …). Null on rows written before run history recorded its trigger (they were not backfilled), which is NOT the same as "no trigger": every run has one.', group: 'Trigger', }), @@ -285,7 +285,7 @@ export const SysAutomationRun = ObjectSchema.create({ label: 'Trigger Record', required: false, maxLength: 255, - description: 'Id of the record that fired this run — the correlation from a run back to its cause, and the reason the run log is usable as an audit trail for record_change flows. Null for record-less trigger kinds and for rows written before #7533.', + description: 'Id of the record that fired this run — the correlation from a run back to its cause, and the reason the run log is usable as an audit trail for record_change flows. Null for record-less trigger kinds and for rows written before run history recorded its trigger (they were not backfilled).', group: 'Trigger', }), @@ -321,7 +321,7 @@ export const SysAutomationRun = ObjectSchema.create({ steps_json: Field.textarea({ label: 'Steps', required: false, - description: 'JSON step log: for a paused run, the steps executed so far (resume state); for a terminal history row, the bounded per-node step log (durable run detail, #2585).', + description: 'JSON step log: for a paused run, the steps executed so far (resume state); for a terminal history row, the bounded per-node step log, so a finished run\'s per-node detail survives a restart.', group: 'State', }), diff --git a/packages/services/service-automation/src/sys-flow-dispatch.object.ts b/packages/services/service-automation/src/sys-flow-dispatch.object.ts index 187633229a3..b4e657df5d5 100644 --- a/packages/services/service-automation/src/sys-flow-dispatch.object.ts +++ b/packages/services/service-automation/src/sys-flow-dispatch.object.ts @@ -83,7 +83,7 @@ export const SysFlowDispatch = ObjectSchema.create({ retention: { maxAge: '30d' }, }, description: - 'Idempotency ledger for trigger dispatches (#10220): one row per claimed dispatch key — (flow, record, matched-window) for a time-relative sweep, (flow, tick-window) for a scheduled flow — so a re-scan, a rebuilt kernel or an operator replay never re-launches a flow for a window it already delivered.', + 'Idempotency ledger for trigger dispatches: one row per claimed dispatch key — (flow, record, matched-window) for a time-relative sweep, (flow, tick-window) for a scheduled flow — so a re-scan, a rebuilt kernel or an operator replay never re-launches a flow for a window it already delivered.', displayNameField: 'id', nameField: 'id', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) highlightFields: ['id', 'dispatched_at', 'outcome'], diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json index 747d5d03c6a..e765765f0ad 100644 --- a/scripts/doc-authoring-prose-id.baseline.json +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -73,21 +73,6 @@ "packages/plugins/plugin-audit/src/audit-writers.ts": { "#5226": 1 }, - "packages/plugins/plugin-audit/src/objects/sys-activity.object.ts": { - "#11507": 1 - }, - "packages/plugins/plugin-audit/src/translations/en.objects.generated.ts": { - "#11507": 1 - }, - "packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts": { - "#11507": 1 - }, - "packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts": { - "#11507": 1 - }, - "packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts": { - "#11507": 1 - }, "packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts": { "#5876": 1, "#8552": 2 @@ -188,48 +173,5 @@ "#5222": 1, "#5333": 1, "#7598": 1 - }, - "packages/services/service-automation/src/builtin/connector-nodes.ts": { - "#3017": 1 - }, - "packages/services/service-automation/src/builtin/parse-config.ts": { - "#4277": 1 - }, - "packages/services/service-automation/src/builtin/screen-nodes.ts": { - "#1870": 1 - }, - "packages/services/service-automation/src/builtin/template.ts": { - "#11060": 1 - }, - "packages/services/service-automation/src/engine.ts": { - "#1888": 1, - "#2419": 2, - "#3017": 1, - "#3528": 1, - "#3760": 1, - "#4045": 1, - "#4277": 1, - "#4414": 1, - "#5393": 4 - }, - "packages/services/service-automation/src/plugin.ts": { - "#1928": 1, - "#2612": 1, - "#2977": 1, - "#3017": 4, - "#3356": 1, - "#3475": 1 - }, - "packages/services/service-automation/src/runtime-identity.ts": { - "#1888": 1, - "#3760": 1 - }, - "packages/services/service-automation/src/sys-automation-run.object.ts": { - "#2585": 1, - "#3801": 1, - "#7533": 2 - }, - "packages/services/service-automation/src/sys-flow-dispatch.object.ts": { - "#10220": 1 } }