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
29 changes: 29 additions & 0 deletions .changeset/20513-named-runtime-strings-state-the-decision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@objectstack/objectql': patch
'@objectstack/service-automation': patch
'@objectstack/runtime': patch
---

Warnings, refusals and hints that cited a tracker number now say what was decided

Clause-②: no

Several runtime strings an author or operator reads sent the reader to an issue-tracker number for
the reason behind them. Each now states that reason in the sentence itself:

- `@objectstack/objectql`: the two data-event warnings. A write that names no single record publishes
no per-record event rather than one with an empty `recordId`; a predicate (`multi: true`) write
publishes its own `data.records.*` event carrying the affected-row count and nothing else, so a
driver result that is not a count publishes no bulk event either.
- `@objectstack/service-automation`: the warning for a pausing node type that never declares
`resumeAuthority`, the generic-route resume refusal (its log line and its error text), and the
refusal of a suspension from a type that declares `supportsPause: false`. An undeclared
`resumeAuthority` resolves to `'service'` (fail-closed), so the generic resume route refuses those
pauses; guessing `'any'` is how a raw resume once walked past an approval decision no service had
recorded.
- `@objectstack/runtime`: the endpoint step's `NOT_IMPLEMENTED` message and its two hints (the
composed runtime always threads the policy context and the execution wiring, because execution is
reachable only past the policy chain), and the endpoint mapping refusals (the publish gate rejects
the same shapes, so a declaration that reaches the runtime check was stored without passing it).

Text only: no error code, field name, status or behaviour changes.
2 changes: 1 addition & 1 deletion packages/objectql/src/engine-data-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ describe('#4639 — predicate writes publish aggregate BulkDataEvents', () => {
expect(published).toHaveLength(0);
const logged = offWarn.mock.calls.map((c) => String(c[0])).join('\n');
expect(logged).toContain('data.records.updated');
expect(logged).toContain('#4639');
expect(logged).toContain('it carries no records and no predicate');
});

it('a by-id delete still takes the PER-RECORD path even with multi: true', async () => {
Expand Down
11 changes: 6 additions & 5 deletions packages/objectql/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7075,9 +7075,10 @@ export class ObjectQL implements IObjectQLEngine {
if (!recordId) {
this.logger.warn(
`No data.record.${action} event published for '${object}': the write names no single record, ` +
`and DataEvent.recordId is required — refusing to publish an off-contract event. ` +
`A predicate write publishes data.records.${action} instead (#4639), so reaching this ` +
`means a single-id write whose driver returned no usable primary key (#4626)`,
`and DataEvent.recordId is required — refusing to publish an off-contract event rather than ` +
`fabricate one with an empty recordId. A predicate (multi: true) write publishes its own ` +
`data.records.${action} event, carrying the affected-row count, instead — so reaching this ` +
`means a single-id write whose driver returned no usable primary key, which is a driver defect`,
{ object },
);
return;
Expand Down Expand Up @@ -7169,8 +7170,8 @@ export class ObjectQL implements IObjectQLEngine {
this.logger.warn(
`No data.records.${action} event published for '${object}': the driver's multi-row result is ` +
`not an affected-row count (IDataDriver.updateMany/deleteMany are contracted to resolve ` +
`a number). The count is the only thing a bulk event states, so publishing one here would ` +
`assert something unverified (#4639)`,
`a number). The count is the only thing a bulk event states — it carries no records and no ` +
`predicate — so publishing one here would assert something unverified`,
{ object },
);
return;
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/src/api-endpoint-step.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ describe('a match with no wiring answers an honest 501', () => {
// "matched but not executed" must never read as "executed and empty".
expect(body.error.message).toContain('showcase_tasks');
expect(body.error.message).toContain('no wiring');
expect(String(body.error.hint)).toContain('#5040');
expect(String(body.error.hint)).toContain('execution is reachable only on the far side of the policy chain');
});

it('passes the request coordinates through untouched', async () => {
Expand Down Expand Up @@ -229,7 +229,7 @@ describe('the policy chain runs between the match and the answer', () => {
expect(answer?.status).toBe(501);
const hint = String((answer!.body as { error: { hint: unknown } }).error.hint);
expect(hint).toContain('enforced');
expect(hint).toContain('#5040');
expect(hint).toContain('supplies it together with the policy context');
});

it('never puts the cacheTtlSeconds header on the 501 — but the verdict still carries it', async () => {
Expand Down
9 changes: 5 additions & 4 deletions packages/runtime/src/api-endpoint-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,9 @@ export async function runAppEndpointStep(
// about what ran is worse than no report.
return notImplemented(match, method, path,
'This request reached the step without a policy context, so authRequired / rateLimit / cacheTtlSeconds '
+ 'were not evaluated — and nothing was executed either. The composed runtime always threads one '
+ '(#5040 E5b), so reaching this answer means a host mounted the step by hand and omitted it.');
+ 'were not evaluated — and nothing was executed either. The composed runtime always threads one, '
+ 'because execution is reachable only on the far side of the policy chain, so reaching this answer '
+ 'means a host mounted the step by hand and omitted it.');
}

const verdict = await applyEndpointPolicies({ ...input.policy, endpoint: match.endpoint, method });
Expand All @@ -263,7 +264,7 @@ export async function runAppEndpointStep(
return notImplemented(match, method, path,
'Policies (authRequired / rateLimit / cacheTtlSeconds) were enforced and this request passed them, but no '
+ 'execution wiring was supplied, so the target was not run. The composed runtime always supplies '
+ 'it (#5040 E5b).');
+ 'it together with the policy context; only a host that mounts the step by hand can leave it out.');
}

const { request, deps, executionContext, environmentId, dataDriver } = input.execution;
Expand Down Expand Up @@ -344,7 +345,7 @@ function notImplemented(
httpStatus: 501,
message:
`Declarative endpoint '${match.endpoint.name}' claims ${method} ${path}, but the caller of the `
+ 'endpoint step supplied no wiring to serve it with (#5040).',
+ 'endpoint step supplied no wiring to serve it with, so nothing was executed.',
extra: { hint },
});
}
3 changes: 2 additions & 1 deletion packages/runtime/src/api-mapping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ describe('a declaration this runtime cannot serve is refused, never ignored', ()
});
const error = expectRefusal(
rejectionOf(applyInputMapping(endpoint, { price: '3' })),
'inputMapping[1].transform', 'convertToInt', 'showcase_inquiries', '#5040',
'inputMapping[1].transform', 'convertToInt', 'showcase_inquiries',
'rejected at publish rather than parsed and ignored',
);
// The prescription, not just the verdict: an author has to be told what
// to do instead, or the refusal is only half a signal.
Expand Down
7 changes: 4 additions & 3 deletions packages/runtime/src/api-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@ function reject(message: string, hint: string): EndpointMappingRejection {
const PATH_HINT =
"`source` and `target` are dot-separated field paths ('user.profile.email'). An empty path, an empty "
+ "segment ('a..b') and the JavaScript prototype keys (__proto__, prototype, constructor) are refused; "
+ 'the publish gate rejects the same shapes (#5040 E7).';
+ 'the publish gate rejects the same shapes, so a declaration that reaches this check was stored without '
+ 'passing that gate (for example through a direct metadata register() call).';

/**
* Whether this runtime can serve a key's declaration AT ALL — data-independent,
Expand Down Expand Up @@ -263,8 +264,8 @@ export function mappingDeclarationRejection(
`Endpoint '${endpoint.name}' declares ${at}.transform ('${entry.transform}'), which this runtime `
+ 'does not execute.',
'A mapping entry moves and renames fields by dot path; there is no transformation-function '
+ "registry in this runtime, so `transform` is rejected at publish (#5040 §3.4, E7) rather than "
+ 'parsed and ignored. Drop the key, or shape the value where it is produced.',
+ "registry in this runtime, so `transform` is rejected at publish rather than parsed and "
+ 'ignored. Drop the key, or shape the value where it is produced.',
);
}

Expand Down
16 changes: 10 additions & 6 deletions packages/services/service-automation/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3274,9 +3274,10 @@ export class AutomationEngine implements IAutomationService {
this.resumeAuthorityOmissionWarned.add(descriptor.type);
this.logger.warn(
`[automation] node type '${descriptor.type}' declares supportsPause but never declares ` +
`resumeAuthority, so the #3801 resume gate REFUSES every pause it creates on the generic route ` +
`(POST /automation/:name/runs/:runId/resume) — an unclaimed pause is fail-closed since #5561, ` +
`because the opposite guess is how #3823 walked past an unrecorded approval decision. ` +
`resumeAuthority, so the resume-authority gate REFUSES every pause it creates on the generic route ` +
`(POST /automation/:name/runs/:runId/resume) — an undeclared resumeAuthority resolves to ` +
`'service', fail-closed, because guessing 'any' is how a raw resume once walked past an approval ` +
`decision no service had recorded. ` +
`Declare it on the descriptor: 'any' if that route IS the intended door (a screen's collected ` +
`inputs, a signal wait's external producer), or 'service' if resuming is the tail of a decision ` +
`some service must authorize and record first. Declaring 'any' is what RESTORES the generic ` +
Expand Down Expand Up @@ -6266,14 +6267,16 @@ export class AutomationEngine implements IAutomationService {
const why = declared === 'service'
? `which is resumable only through its owning service (resumeAuthority: 'service')`
: `whose type never declares resumeAuthority, so it is closed to the generic route until it does ` +
`(#5561) — declare resumeAuthority: 'any' on its descriptor if this route IS the intended door`;
`(an undeclared resumeAuthority resolves to 'service', fail-closed) — declare ` +
`resumeAuthority: 'any' on its descriptor if this route IS the intended door`;
this.logger.warn(`[automation] refused resume of run '${runId}': parked on ${nodeType} node ${at}, ${why}`);

// The fix, identical in both the direct and the linked-run phrasing —
// what has to change is a descriptor, not the call that just failed.
const undeclaredFix =
`and that node type never declares resumeAuthority, so the generic resume route is closed to the ` +
`pauses it creates (#5561). If that route IS the intended door — a screen's collected inputs, a ` +
`pauses it creates: an undeclared resumeAuthority resolves to 'service', fail-closed. If that ` +
`route IS the intended door — a screen's collected inputs, a ` +
`signal wait's external producer — declare resumeAuthority: 'any' on its action descriptor; declare ` +
`'service' if resuming is the tail of a decision some service must authorize and record first`;
return {
Expand Down Expand Up @@ -6409,7 +6412,8 @@ export class AutomationEngine implements IAutomationService {
`node type '${nodeType}' suspended the run but its action descriptor declares ` +
`supportsPause: false, so the pause is refused — a run that paused here could not be ` +
`continued on the generic resume route anyway: a type that declares no pause declares no ` +
`resumeAuthority either, and an unclaimed pause is fail-closed since #5561. Declare ` +
`resumeAuthority either, and an undeclared resumeAuthority resolves to 'service', which the ` +
`generic route refuses. Declare ` +
`supportsPause: true on the descriptor together with the resumeAuthority the pauses need ` +
`('any' if POST /automation/:name/runs/:runId/resume is the intended door, 'service' if ` +
`resuming is the tail of a decision some service must authorize and record first) — or stop ` +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ describe('resumeAuthority omission warning (#5561)', () => {
// one field that restores their resume route.
expect(line).toContain("'any'");
expect(line).toContain("'service'");
expect(line).toContain('#3801');
expect(line).toContain('#3823');
expect(line).toContain("an undeclared resumeAuthority resolves to 'service', fail-closed");
expect(line).toContain('walked past an approval decision no service had recorded');
expect(line).toContain('REFUSES');
expect(line).toContain("Declaring 'any' is what RESTORES the generic route");
expect(line).not.toContain('changes no behaviour');
Expand Down
13 changes: 1 addition & 12 deletions scripts/doc-authoring-prose-id.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -408,8 +408,6 @@
"#3617": 2,
"#4371": 1,
"#4419": 1,
"#4626": 1,
"#4639": 2,
"#4769": 2,
"#4797": 2,
"#5158": 2,
Expand Down Expand Up @@ -835,12 +833,6 @@
"#11519": 1,
"#5933": 1
},
"packages/runtime/src/api-endpoint-step.ts": {
"#5040": 3
},
"packages/runtime/src/api-mapping.ts": {
"#5040": 2
},
"packages/runtime/src/app-plugin.ts": {
"#8686": 1
},
Expand Down Expand Up @@ -957,13 +949,10 @@
"#3017": 1,
"#3528": 1,
"#3760": 1,
"#3801": 1,
"#3823": 1,
"#4045": 1,
"#4277": 1,
"#4414": 1,
"#5393": 4,
"#5561": 4
"#5393": 4
},
"packages/services/service-automation/src/plugin.ts": {
"#1928": 1,
Expand Down
Loading