@objectstack/types 17.3.0 → 17.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,170 @@
1
1
  # @objectstack/types
2
2
 
3
+ ## 17.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3d3f60e: An approval decision that lands while its flow run strands now says so in fields, not only in prose.
8
+
9
+ `POST /api/v1/approvals/requests/{id}/reject` — and its sibling decision doors — could produce three coexisting outcomes from one call: the caller read HTTP 500, the request row **was** in its terminal status and had left the pending inbox, and the workflow run was stranded. A caller reading 500 has one honest inference available — "the rejection did not happen" — and it was the wrong one, so scripts and operators retried or escalated against a decision that was already durable. The only carrier of the truth was English prose in `error`, so finding the affected run meant regexing a run id out of a sentence, and nothing said whether that run could be repaired at all.
10
+
11
+ The 500 stays. A recorded decision whose flow never advances is still a failure and is still reported as one; the door does not become atomic and no decision is ever rolled back. What changed is that it stops discarding what the engine already said:
12
+
13
+ - **The `RESUME_FAILED` body gains four fields**, additively — `finalized` (always `true`: the decision stands), `decision`, `runId`, and `repairable`. Existing consumers see the same `code`, the same `error` and the same status.
14
+ - **`repairable` carries the engine's own discriminator** — `AutomationResult.status === 'stranded'`, the state stamped on exactly the exit that journals a repair snapshot. `false` is the answer for every other failure, including a lost run: absence of the signal is not repairability, and a repair verb that would refuse is worse than no promise.
15
+ - **`serviceResume` carries `status`** through to the door. It previously read only `success` / `code` / `error`, and the stranded exit reports a `status` and no `code` at all — so the platform's own repairability signal died one line before the envelope was built.
16
+
17
+ `@objectstack/types` gains `strandedDecisionFailure` / `strandedDecisionDetails` and the `StrandedDecisionDetails` type — the constructor and its recogniser in one module, so the producing service and the REST door cannot drift. A `RESUME_FAILED` raised without that carrier answers exactly the body it always did; the door never synthesises the envelope.
18
+
19
+ ### Patch Changes
20
+
21
+ - 088f761: `createHostImporter` now loads the `import` build of an ALIASED dual-published package, instead of silently keeping its `require` build.
22
+
23
+ An alias declaration — `{"dependencies": {"foo": "npm:bar@1"}}` — installs a package whose manifest is named `bar` under the key `foo`. On the path where CommonJS resolution SUCCEEDS, the importer re-decides only the CONDITION (it asks the package which entry an `import()` gets, so the caller's ESM chain and this load share one instance). That re-decision recognised the package root by walking up from the resolved entry until it found a manifest named after the DECLARATION KEY — `foo` — while an aliased install's manifest is named `bar`. The walk therefore never matched, the re-decision produced nothing, and the load fell back to whatever the CommonJS resolver had answered: the `require` condition.
24
+
25
+ For an aliased dual publish that left the process holding two live copies of one package — the CommonJS build behind the host importer, the `import` build in the caller's own chain — which is exactly the split the condition re-decision exists to remove: a plugin registry, a singleton kernel, a module-level cache, one copy each.
26
+
27
+ The expectation now comes from the host's own declaration (`npm:name@range`, aliased `workspace:name@range`), the same reading the ESM-only fallback finder has used since it learned about aliases. Nothing about the check's strictness moves: an alias naming one package still does not license a directory holding another, and a non-aliased declaration is still verified against its key. Declarations that name a LOCATION rather than a package (`link:`, `file:`) carry no name to expect, so they keep today's behaviour unchanged.
28
+
29
+ Measured population for the behaviour change: zero aliased declarations exist across this workspace's 875 dependency declarations, and 867 of 867 installed declarations already match their key — no ordinary, non-aliased install reaches this path.
30
+ - c5d6803: Published `.js.map` files no longer embed the complete original source text (`sourcesContent`) — comments included. `sourcemap: true` was esbuild shorthand, and esbuild's own default for `sourcesContent` is `true`; nobody had decided to publish every package's full source (including `@internal`/test-only comments) to npm inside its source maps, it fell out of a default nobody had looked at. Measured before this change: 55 of 57 publishable packages shipped embedded source text, and maps were roughly half of `@objectstack/spec`'s published bytes.
31
+
32
+ `sourcesContent: false` is now set at one shared place (`scripts/tsup-drop-sources-content.mjs`, wired into every `tsup.config.ts` via tsup's `esbuildOptions` hook — most packages build through the repo-root config directly and pick this up with no config change of their own). `mappings` are untouched, so stack-trace positions still resolve correctly to the original file/line/column; only the embedded source text is gone.
33
+
34
+ `@objectstack/cli` (built with `tsc`, not `tsup`) never embedded source text to begin with — its maps' `sources` entries point at `src/**` paths that are not part of the published tarball either way. That is not a defect unique to `cli`: every `tsup`-built package's `sources` entries are `../src/**`-relative paths that are equally outside `files: ["dist", …]`, and were merely masked by the embedded content that just stopped shipping. Shipping `src/**` in `files[]` to make `sources` resolve was rejected — it would put most of the removed bytes straight back. So `cli`'s maps are left exactly as `tsc` emits them: this is now the fleet-consistent shape (accurate `mappings`, non-resolving-but-honest `sources` labels, no embedded text), not an outlier.
35
+
36
+ A new gate, `pnpm check:sourcemap-no-sources-content`, sweeps every built, non-private package's `dist/**/*.map` and fails if any of them carries a non-empty `sourcesContent` array — so a future `tsup.config.ts` that skips the shared hook, or a toolchain upgrade that changes esbuild's default back, is caught rather than silently re-publishing source text.
37
+ - Updated dependencies [fe0d9a4]
38
+ - Updated dependencies [ecd2158]
39
+ - Updated dependencies [f2b5e46]
40
+ - Updated dependencies [ed7243d]
41
+ - Updated dependencies [6ba0db4]
42
+ - Updated dependencies [625b0c3]
43
+ - Updated dependencies [233222e]
44
+ - Updated dependencies [07f40e5]
45
+ - Updated dependencies [ceb4877]
46
+ - Updated dependencies [e9fcd6b]
47
+ - Updated dependencies [90e7e6d]
48
+ - Updated dependencies [2bdabe6]
49
+ - Updated dependencies [ca326b5]
50
+ - Updated dependencies [8f404a5]
51
+ - Updated dependencies [68437d4]
52
+ - Updated dependencies [abb140c]
53
+ - Updated dependencies [8333a6c]
54
+ - Updated dependencies [3e3ecb0]
55
+ - Updated dependencies [3030369]
56
+ - Updated dependencies [d5d8d50]
57
+ - Updated dependencies [e08892d]
58
+ - Updated dependencies [ae05f2e]
59
+ - Updated dependencies [b548e43]
60
+ - Updated dependencies [c463d03]
61
+ - Updated dependencies [64bd6a3]
62
+ - Updated dependencies [13c48c2]
63
+ - Updated dependencies [132742f]
64
+ - Updated dependencies [85a2459]
65
+ - Updated dependencies [50dc214]
66
+ - Updated dependencies [e89fa92]
67
+ - Updated dependencies [e9fcd6b]
68
+ - Updated dependencies [8976ea1]
69
+ - Updated dependencies [56fe8c2]
70
+ - Updated dependencies [acabd24]
71
+ - Updated dependencies [ab50c8f]
72
+ - Updated dependencies [6491463]
73
+ - Updated dependencies [89cf4d6]
74
+ - Updated dependencies [21c5dcb]
75
+ - Updated dependencies [6d4d5d3]
76
+ - Updated dependencies [ed5d557]
77
+ - Updated dependencies [bca21f7]
78
+ - Updated dependencies [e9fcd6b]
79
+ - Updated dependencies [1a7a7c9]
80
+ - Updated dependencies [e9fcd6b]
81
+ - Updated dependencies [ef3a138]
82
+ - Updated dependencies [68d5dfd]
83
+ - Updated dependencies [3e21cf0]
84
+ - Updated dependencies [4cfc93b]
85
+ - Updated dependencies [efd6b43]
86
+ - Updated dependencies [859ded3]
87
+ - Updated dependencies [fa125f3]
88
+ - Updated dependencies [74628d9]
89
+ - Updated dependencies [a646120]
90
+ - Updated dependencies [6f1ce7d]
91
+ - Updated dependencies [7778115]
92
+ - Updated dependencies [2c753fe]
93
+ - Updated dependencies [52804cd]
94
+ - Updated dependencies [3f89967]
95
+ - Updated dependencies [53cf263]
96
+ - Updated dependencies [21aabbc]
97
+ - Updated dependencies [9c270bb]
98
+ - Updated dependencies [76c8c5a]
99
+ - Updated dependencies [a84e1ce]
100
+ - Updated dependencies [bf1054a]
101
+ - Updated dependencies [d8d2776]
102
+ - Updated dependencies [222dc0f]
103
+ - Updated dependencies [e9fcd6b]
104
+ - Updated dependencies [32c917d]
105
+ - Updated dependencies [f9a3c32]
106
+ - Updated dependencies [f502898]
107
+ - Updated dependencies [af7edfe]
108
+ - Updated dependencies [b60f48b]
109
+ - Updated dependencies [c78c918]
110
+ - Updated dependencies [cf9bda4]
111
+ - Updated dependencies [784cb92]
112
+ - Updated dependencies [7629f4d]
113
+ - Updated dependencies [51df9fd]
114
+ - Updated dependencies [a7da4de]
115
+ - Updated dependencies [de0bcdd]
116
+ - Updated dependencies [70f7d6d]
117
+ - Updated dependencies [c677cda]
118
+ - Updated dependencies [554a160]
119
+ - Updated dependencies [f7da71e]
120
+ - Updated dependencies [7f745c3]
121
+ - Updated dependencies [5eb24f8]
122
+ - Updated dependencies [2a3decc]
123
+ - Updated dependencies [cc00df2]
124
+ - Updated dependencies [f4e6adf]
125
+ - Updated dependencies [ee4a59b]
126
+ - Updated dependencies [4db3c61]
127
+ - Updated dependencies [5ca314a]
128
+ - Updated dependencies [e0af1a8]
129
+ - Updated dependencies [414c1fc]
130
+ - Updated dependencies [22c0279]
131
+ - Updated dependencies [0db2947]
132
+ - Updated dependencies [92b5d7f]
133
+ - Updated dependencies [613bfbd]
134
+ - Updated dependencies [abae16a]
135
+ - Updated dependencies [094b8fd]
136
+ - Updated dependencies [c7aca0d]
137
+ - Updated dependencies [c1d8f98]
138
+ - Updated dependencies [8e0b297]
139
+ - Updated dependencies [5f7fa1d]
140
+ - Updated dependencies [87f0ccc]
141
+ - Updated dependencies [aedbaef]
142
+ - Updated dependencies [c5d6803]
143
+ - Updated dependencies [10d05bb]
144
+ - Updated dependencies [69602e5]
145
+ - Updated dependencies [c3ce76c]
146
+ - Updated dependencies [7936b29]
147
+ - Updated dependencies [46803fa]
148
+ - Updated dependencies [c2a336c]
149
+ - Updated dependencies [9f890d3]
150
+ - Updated dependencies [0bb2318]
151
+ - Updated dependencies [f7db8f4]
152
+ - Updated dependencies [1ecee3e]
153
+ - Updated dependencies [9408b7f]
154
+ - Updated dependencies [e9fcd6b]
155
+ - Updated dependencies [9bcd9be]
156
+ - Updated dependencies [b398ad2]
157
+ - Updated dependencies [99261a7]
158
+ - Updated dependencies [81b426f]
159
+ - Updated dependencies [001af1c]
160
+ - Updated dependencies [fb77aa5]
161
+ - Updated dependencies [581d8f8]
162
+ - Updated dependencies [f81afe3]
163
+ - Updated dependencies [40a44b9]
164
+ - Updated dependencies [7a7fb03]
165
+ - Updated dependencies [8fd246d]
166
+ - @objectstack/spec@17.4.0
167
+
3
168
  ## 17.3.0
4
169
 
5
170
  ### Minor Changes
package/dist/index.d.mts CHANGED
@@ -475,8 +475,10 @@ declare function _resetEnvDeprecationWarnings(): void;
475
475
  * "Do not ship driver internals to clients" is a property of the HTTP
476
476
  * boundary, not of one router, so the predicate lives here — the package both
477
477
  * `@objectstack/rest` and `@objectstack/runtime` already depend on — and each
478
- * boundary applies it in its own envelope. One heuristic, one place to widen
479
- * when a new dialect's phrasing shows up.
478
+ * boundary applies it in its own envelope. One heuristic, one place and
479
+ * since #16019 a FROZEN one: a phrasing it does not recognise is closed by the
480
+ * producer declaring its fault, never by a new row here (the ruling is
481
+ * recorded on {@link DIALECT_LEAK_PHRASINGS}).
480
482
  *
481
483
  * Deliberately a *heuristic over the message*, not a driver taxonomy: these
482
484
  * errors arrive as plain `Error`s from a half-dozen dialects with no shared
@@ -1340,6 +1342,118 @@ declare function fieldsFromZodIssues(issues: Array<{
1340
1342
  message: string;
1341
1343
  }>;
1342
1344
 
1345
+ /**
1346
+ * The machine-readable half of a `RESUME_FAILED` — a decision that is durably
1347
+ * recorded whose flow run could not be resumed (#13807).
1348
+ *
1349
+ * ## The condition
1350
+ *
1351
+ * An approval decision finalises: the `sys_approval_request` row flips to its
1352
+ * terminal status, the audit action is written, the record's mirrored status
1353
+ * field advances — and only THEN is the owning flow run resumed. When that
1354
+ * resume fails the writes are already durable, so the outcome stands and the
1355
+ * run is stranded. `@objectstack/plugin-approvals` throws rather than
1356
+ * answering `resumed: false`, deliberately: a recorded decision whose flow
1357
+ * never advances is #4420's zombie half-state, and the contract
1358
+ * (`ApprovalDecisionResult`) declares the throw intentional.
1359
+ *
1360
+ * ⛔ This module does NOT change that posture. The maintainer ruled on
1361
+ * 2026-09-04 (decision batch #37, option B) that the door **keeps its status
1362
+ * code** — the 500-class `RESUME_FAILED` — because the effect landing while
1363
+ * the run strands is still a failure. What the ruling changed is that the
1364
+ * throw must be *truthful*: the facts a caller needs were being discarded.
1365
+ *
1366
+ * ## What was being discarded, measured
1367
+ *
1368
+ * Three states coexist after such a call: the caller reads 500, the request
1369
+ * IS in its terminal status, and the run is stranded. A caller — human,
1370
+ * script, or agent — reads 500 as "the rejection did not happen" and retries
1371
+ * or escalates. It did happen. Before this module the only carrier of that
1372
+ * fact was English prose in `error`, so an operator had to regex the run id
1373
+ * out of a sentence, and nothing said whether the run was repairable at all.
1374
+ *
1375
+ * Meanwhile the engine already knew. `AutomationResult.status: 'stranded'`
1376
+ * (`@objectstack/spec`, `automation-service.ts`) is stamped on exactly the
1377
+ * exit that journals a repair snapshot — the shape-4 name from #13937 — and
1378
+ * it is distinct from `'failed'` on purpose: `'failed'` says the run ran and
1379
+ * was rejected, `'stranded'` says a recorded continuation stopped mid-flight
1380
+ * and an operator has something to repair. It had a producer and, until this
1381
+ * module, **zero consumers**: the approvals door read only
1382
+ * `success` / `code` / `error` off the resume result and dropped it one line
1383
+ * before the envelope was built.
1384
+ *
1385
+ * ## Why it lives in `@objectstack/types`
1386
+ *
1387
+ * Same Home rule as {@link ValidationFailureDetails} one file over: the
1388
+ * PRODUCER is `@objectstack/plugin-approvals` and the CONSUMER is the REST
1389
+ * door in `@objectstack/rest`, and rest cannot import a plugin. Both already
1390
+ * depend on this package, so the shared declaration adds no dependency edge —
1391
+ * and keeping the constructor and the reader in ONE module is what stops the
1392
+ * two sides from drifting into a stringly-typed agreement about a property
1393
+ * name.
1394
+ *
1395
+ * ⛔ Deliberately NOT a tolerant reader. There is no alias chain and no prose
1396
+ * parsing: a body either carries the four facts the producer attached, or the
1397
+ * response is exactly what it was before. A `RESUME_FAILED` raised by
1398
+ * something that never had a decision to report (a test double, a future
1399
+ * caller) must not be dressed up as one.
1400
+ */
1401
+ /**
1402
+ * The four facts a stranded decision publishes alongside its `code` and
1403
+ * `error`. Every field is present or the whole envelope is absent — a partial
1404
+ * one would let a consumer branch on `finalized === undefined` and read it as
1405
+ * "the decision did not stand", which is the exact misreading this exists to
1406
+ * end.
1407
+ */
1408
+ interface StrandedDecisionDetails {
1409
+ /**
1410
+ * Always `true`. The decision reached a terminal state and is durable; the
1411
+ * 5xx is about the run, never about the decision. Spelled as a literal
1412
+ * rather than omitted so a consumer reads a fact instead of an absence.
1413
+ */
1414
+ finalized: true;
1415
+ /**
1416
+ * Which outcome was recorded — `'approve'` / `'reject'` for a decision, and
1417
+ * the sibling doors on the same path for the rest (`'revise'` on a
1418
+ * send-back, `'resubmit'`). Free-form by design: the vocabulary belongs to
1419
+ * the producing service, not to this recogniser.
1420
+ */
1421
+ decision: string;
1422
+ /** The stranded run. The one identifier an operator needs to act. */
1423
+ runId: string;
1424
+ /**
1425
+ * Whether the engine says this run can still be repaired — derived from the
1426
+ * engine's own discriminator (`AutomationResult.status === 'stranded'`),
1427
+ * never from the message text and never assumed.
1428
+ *
1429
+ * `false` is the honest answer for every other exit, including the ones
1430
+ * that report no status at all (a lost run, an engine that predates the
1431
+ * discriminator). ⛔ Absence of the signal is not repairability: promising a
1432
+ * repair verb that will refuse is worse than promising nothing.
1433
+ */
1434
+ repairable: boolean;
1435
+ }
1436
+ /**
1437
+ * Structured details for a thrown stranded-decision failure, or `undefined`
1438
+ * when `err` is not one.
1439
+ *
1440
+ * Callers use the `undefined` result as the predicate and the returned object
1441
+ * as the payload, so the two can never disagree — the same contract
1442
+ * `validationFailureDetails` keeps one module over. Every field is validated:
1443
+ * a malformed carrier answers `undefined` rather than putting a half-envelope
1444
+ * on the wire.
1445
+ */
1446
+ declare function strandedDecisionDetails(err: unknown): StrandedDecisionDetails | undefined;
1447
+ /**
1448
+ * The CONSTRUCTOR for the shape {@link strandedDecisionDetails} recognises —
1449
+ * kept in the same module so the two can never drift.
1450
+ *
1451
+ * The message stays the producer's own, unchanged: the prose is what a human
1452
+ * reads in a log, the details are what a machine reads on the wire, and this
1453
+ * ruling added the second without touching the first.
1454
+ */
1455
+ declare function strandedDecisionFailure(message: string, details: StrandedDecisionDetails): Error;
1456
+
1343
1457
  /**
1344
1458
  * The one home for Postgres' `«sub-object» "x" of relation "y" …` phrasing
1345
1459
  * (#6615).
@@ -1931,4 +2045,4 @@ interface RuntimePlugin {
1931
2045
  onStart?: (ctx: RuntimeContext) => void | Promise<void>;
1932
2046
  }
1933
2047
 
1934
- export { DRIVER_TARGETED_TABLE, type EnvelopeResponse, GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION, type GlobalUniqueAttestation, type GlobalUniqueFinding, type IKernel, INTERNAL_ERROR_MESSAGE, type KeysetPageQuery, type KeysetWalk, type KeysetWalkOptions, PLATFORM_OWNER_EMAIL_ENV, type RuntimeContext, type RuntimePlugin, SERVER_FAULT_LOG_PREFIX, type ServerFaultLogInput, type ServerFaultProvenance, type ServerFaultRequest, type ThrownHttpError, VALIDATION_FAILED_STATUS, type ValidationFailureDetails, _resetEnvDeprecationWarnings, buildGlobalUniqueStopMessage, collectConfiguredLocales, collectGlobalUniques, declareTargetedTable, declaredIndexUniqueIsGlobal, declaredUserMessage, declaresServerFault, demotedDeclaredCode, describeFaultRequest, describeGlobalUniqueFinding, emitDegradedBootBanner, fieldUniqueIsGlobal, fieldsFromZodIssues, globalUniqueFindingId, isEmailVerifiedUserRow, isMcpServerEnabled, isMissingTableError, isModuleNotFoundError, isPlatformOwnedObject, isRelationSubObjectPhrase, isSchemaAlreadyExistsError, isServerFault, isUnbackedConflictTargetError, isUniqueViolationError, keysetWalk, logServerFault, looksLikeInternalErrorLeak, matchMissingColumnOfRelation, postureGatesGlobalUniques, readEnvWithDeprecation, recordGlobalUniqueAttestation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveOrgMembershipLimit, resolvePlatformOwnerEmail, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, resolveThrownHttpError, sendError, sendOk, serverFaultLogMessage, serverFaultLogMeta, serverFaultProvenance, stampSearchPinyinEnabled, targetedTableOf, unconfirmedGlobalUniques, uniqueViolationColumn, validationFailure, validationFailureDetails };
2048
+ export { DRIVER_TARGETED_TABLE, type EnvelopeResponse, GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION, type GlobalUniqueAttestation, type GlobalUniqueFinding, type IKernel, INTERNAL_ERROR_MESSAGE, type KeysetPageQuery, type KeysetWalk, type KeysetWalkOptions, PLATFORM_OWNER_EMAIL_ENV, type RuntimeContext, type RuntimePlugin, SERVER_FAULT_LOG_PREFIX, type ServerFaultLogInput, type ServerFaultProvenance, type ServerFaultRequest, type StrandedDecisionDetails, type ThrownHttpError, VALIDATION_FAILED_STATUS, type ValidationFailureDetails, _resetEnvDeprecationWarnings, buildGlobalUniqueStopMessage, collectConfiguredLocales, collectGlobalUniques, declareTargetedTable, declaredIndexUniqueIsGlobal, declaredUserMessage, declaresServerFault, demotedDeclaredCode, describeFaultRequest, describeGlobalUniqueFinding, emitDegradedBootBanner, fieldUniqueIsGlobal, fieldsFromZodIssues, globalUniqueFindingId, isEmailVerifiedUserRow, isMcpServerEnabled, isMissingTableError, isModuleNotFoundError, isPlatformOwnedObject, isRelationSubObjectPhrase, isSchemaAlreadyExistsError, isServerFault, isUnbackedConflictTargetError, isUniqueViolationError, keysetWalk, logServerFault, looksLikeInternalErrorLeak, matchMissingColumnOfRelation, postureGatesGlobalUniques, readEnvWithDeprecation, recordGlobalUniqueAttestation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveOrgMembershipLimit, resolvePlatformOwnerEmail, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, resolveThrownHttpError, sendError, sendOk, serverFaultLogMessage, serverFaultLogMeta, serverFaultProvenance, stampSearchPinyinEnabled, strandedDecisionDetails, strandedDecisionFailure, targetedTableOf, unconfirmedGlobalUniques, uniqueViolationColumn, validationFailure, validationFailureDetails };
package/dist/index.d.ts CHANGED
@@ -475,8 +475,10 @@ declare function _resetEnvDeprecationWarnings(): void;
475
475
  * "Do not ship driver internals to clients" is a property of the HTTP
476
476
  * boundary, not of one router, so the predicate lives here — the package both
477
477
  * `@objectstack/rest` and `@objectstack/runtime` already depend on — and each
478
- * boundary applies it in its own envelope. One heuristic, one place to widen
479
- * when a new dialect's phrasing shows up.
478
+ * boundary applies it in its own envelope. One heuristic, one place and
479
+ * since #16019 a FROZEN one: a phrasing it does not recognise is closed by the
480
+ * producer declaring its fault, never by a new row here (the ruling is
481
+ * recorded on {@link DIALECT_LEAK_PHRASINGS}).
480
482
  *
481
483
  * Deliberately a *heuristic over the message*, not a driver taxonomy: these
482
484
  * errors arrive as plain `Error`s from a half-dozen dialects with no shared
@@ -1340,6 +1342,118 @@ declare function fieldsFromZodIssues(issues: Array<{
1340
1342
  message: string;
1341
1343
  }>;
1342
1344
 
1345
+ /**
1346
+ * The machine-readable half of a `RESUME_FAILED` — a decision that is durably
1347
+ * recorded whose flow run could not be resumed (#13807).
1348
+ *
1349
+ * ## The condition
1350
+ *
1351
+ * An approval decision finalises: the `sys_approval_request` row flips to its
1352
+ * terminal status, the audit action is written, the record's mirrored status
1353
+ * field advances — and only THEN is the owning flow run resumed. When that
1354
+ * resume fails the writes are already durable, so the outcome stands and the
1355
+ * run is stranded. `@objectstack/plugin-approvals` throws rather than
1356
+ * answering `resumed: false`, deliberately: a recorded decision whose flow
1357
+ * never advances is #4420's zombie half-state, and the contract
1358
+ * (`ApprovalDecisionResult`) declares the throw intentional.
1359
+ *
1360
+ * ⛔ This module does NOT change that posture. The maintainer ruled on
1361
+ * 2026-09-04 (decision batch #37, option B) that the door **keeps its status
1362
+ * code** — the 500-class `RESUME_FAILED` — because the effect landing while
1363
+ * the run strands is still a failure. What the ruling changed is that the
1364
+ * throw must be *truthful*: the facts a caller needs were being discarded.
1365
+ *
1366
+ * ## What was being discarded, measured
1367
+ *
1368
+ * Three states coexist after such a call: the caller reads 500, the request
1369
+ * IS in its terminal status, and the run is stranded. A caller — human,
1370
+ * script, or agent — reads 500 as "the rejection did not happen" and retries
1371
+ * or escalates. It did happen. Before this module the only carrier of that
1372
+ * fact was English prose in `error`, so an operator had to regex the run id
1373
+ * out of a sentence, and nothing said whether the run was repairable at all.
1374
+ *
1375
+ * Meanwhile the engine already knew. `AutomationResult.status: 'stranded'`
1376
+ * (`@objectstack/spec`, `automation-service.ts`) is stamped on exactly the
1377
+ * exit that journals a repair snapshot — the shape-4 name from #13937 — and
1378
+ * it is distinct from `'failed'` on purpose: `'failed'` says the run ran and
1379
+ * was rejected, `'stranded'` says a recorded continuation stopped mid-flight
1380
+ * and an operator has something to repair. It had a producer and, until this
1381
+ * module, **zero consumers**: the approvals door read only
1382
+ * `success` / `code` / `error` off the resume result and dropped it one line
1383
+ * before the envelope was built.
1384
+ *
1385
+ * ## Why it lives in `@objectstack/types`
1386
+ *
1387
+ * Same Home rule as {@link ValidationFailureDetails} one file over: the
1388
+ * PRODUCER is `@objectstack/plugin-approvals` and the CONSUMER is the REST
1389
+ * door in `@objectstack/rest`, and rest cannot import a plugin. Both already
1390
+ * depend on this package, so the shared declaration adds no dependency edge —
1391
+ * and keeping the constructor and the reader in ONE module is what stops the
1392
+ * two sides from drifting into a stringly-typed agreement about a property
1393
+ * name.
1394
+ *
1395
+ * ⛔ Deliberately NOT a tolerant reader. There is no alias chain and no prose
1396
+ * parsing: a body either carries the four facts the producer attached, or the
1397
+ * response is exactly what it was before. A `RESUME_FAILED` raised by
1398
+ * something that never had a decision to report (a test double, a future
1399
+ * caller) must not be dressed up as one.
1400
+ */
1401
+ /**
1402
+ * The four facts a stranded decision publishes alongside its `code` and
1403
+ * `error`. Every field is present or the whole envelope is absent — a partial
1404
+ * one would let a consumer branch on `finalized === undefined` and read it as
1405
+ * "the decision did not stand", which is the exact misreading this exists to
1406
+ * end.
1407
+ */
1408
+ interface StrandedDecisionDetails {
1409
+ /**
1410
+ * Always `true`. The decision reached a terminal state and is durable; the
1411
+ * 5xx is about the run, never about the decision. Spelled as a literal
1412
+ * rather than omitted so a consumer reads a fact instead of an absence.
1413
+ */
1414
+ finalized: true;
1415
+ /**
1416
+ * Which outcome was recorded — `'approve'` / `'reject'` for a decision, and
1417
+ * the sibling doors on the same path for the rest (`'revise'` on a
1418
+ * send-back, `'resubmit'`). Free-form by design: the vocabulary belongs to
1419
+ * the producing service, not to this recogniser.
1420
+ */
1421
+ decision: string;
1422
+ /** The stranded run. The one identifier an operator needs to act. */
1423
+ runId: string;
1424
+ /**
1425
+ * Whether the engine says this run can still be repaired — derived from the
1426
+ * engine's own discriminator (`AutomationResult.status === 'stranded'`),
1427
+ * never from the message text and never assumed.
1428
+ *
1429
+ * `false` is the honest answer for every other exit, including the ones
1430
+ * that report no status at all (a lost run, an engine that predates the
1431
+ * discriminator). ⛔ Absence of the signal is not repairability: promising a
1432
+ * repair verb that will refuse is worse than promising nothing.
1433
+ */
1434
+ repairable: boolean;
1435
+ }
1436
+ /**
1437
+ * Structured details for a thrown stranded-decision failure, or `undefined`
1438
+ * when `err` is not one.
1439
+ *
1440
+ * Callers use the `undefined` result as the predicate and the returned object
1441
+ * as the payload, so the two can never disagree — the same contract
1442
+ * `validationFailureDetails` keeps one module over. Every field is validated:
1443
+ * a malformed carrier answers `undefined` rather than putting a half-envelope
1444
+ * on the wire.
1445
+ */
1446
+ declare function strandedDecisionDetails(err: unknown): StrandedDecisionDetails | undefined;
1447
+ /**
1448
+ * The CONSTRUCTOR for the shape {@link strandedDecisionDetails} recognises —
1449
+ * kept in the same module so the two can never drift.
1450
+ *
1451
+ * The message stays the producer's own, unchanged: the prose is what a human
1452
+ * reads in a log, the details are what a machine reads on the wire, and this
1453
+ * ruling added the second without touching the first.
1454
+ */
1455
+ declare function strandedDecisionFailure(message: string, details: StrandedDecisionDetails): Error;
1456
+
1343
1457
  /**
1344
1458
  * The one home for Postgres' `«sub-object» "x" of relation "y" …` phrasing
1345
1459
  * (#6615).
@@ -1931,4 +2045,4 @@ interface RuntimePlugin {
1931
2045
  onStart?: (ctx: RuntimeContext) => void | Promise<void>;
1932
2046
  }
1933
2047
 
1934
- export { DRIVER_TARGETED_TABLE, type EnvelopeResponse, GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION, type GlobalUniqueAttestation, type GlobalUniqueFinding, type IKernel, INTERNAL_ERROR_MESSAGE, type KeysetPageQuery, type KeysetWalk, type KeysetWalkOptions, PLATFORM_OWNER_EMAIL_ENV, type RuntimeContext, type RuntimePlugin, SERVER_FAULT_LOG_PREFIX, type ServerFaultLogInput, type ServerFaultProvenance, type ServerFaultRequest, type ThrownHttpError, VALIDATION_FAILED_STATUS, type ValidationFailureDetails, _resetEnvDeprecationWarnings, buildGlobalUniqueStopMessage, collectConfiguredLocales, collectGlobalUniques, declareTargetedTable, declaredIndexUniqueIsGlobal, declaredUserMessage, declaresServerFault, demotedDeclaredCode, describeFaultRequest, describeGlobalUniqueFinding, emitDegradedBootBanner, fieldUniqueIsGlobal, fieldsFromZodIssues, globalUniqueFindingId, isEmailVerifiedUserRow, isMcpServerEnabled, isMissingTableError, isModuleNotFoundError, isPlatformOwnedObject, isRelationSubObjectPhrase, isSchemaAlreadyExistsError, isServerFault, isUnbackedConflictTargetError, isUniqueViolationError, keysetWalk, logServerFault, looksLikeInternalErrorLeak, matchMissingColumnOfRelation, postureGatesGlobalUniques, readEnvWithDeprecation, recordGlobalUniqueAttestation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveOrgMembershipLimit, resolvePlatformOwnerEmail, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, resolveThrownHttpError, sendError, sendOk, serverFaultLogMessage, serverFaultLogMeta, serverFaultProvenance, stampSearchPinyinEnabled, targetedTableOf, unconfirmedGlobalUniques, uniqueViolationColumn, validationFailure, validationFailureDetails };
2048
+ export { DRIVER_TARGETED_TABLE, type EnvelopeResponse, GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION, type GlobalUniqueAttestation, type GlobalUniqueFinding, type IKernel, INTERNAL_ERROR_MESSAGE, type KeysetPageQuery, type KeysetWalk, type KeysetWalkOptions, PLATFORM_OWNER_EMAIL_ENV, type RuntimeContext, type RuntimePlugin, SERVER_FAULT_LOG_PREFIX, type ServerFaultLogInput, type ServerFaultProvenance, type ServerFaultRequest, type StrandedDecisionDetails, type ThrownHttpError, VALIDATION_FAILED_STATUS, type ValidationFailureDetails, _resetEnvDeprecationWarnings, buildGlobalUniqueStopMessage, collectConfiguredLocales, collectGlobalUniques, declareTargetedTable, declaredIndexUniqueIsGlobal, declaredUserMessage, declaresServerFault, demotedDeclaredCode, describeFaultRequest, describeGlobalUniqueFinding, emitDegradedBootBanner, fieldUniqueIsGlobal, fieldsFromZodIssues, globalUniqueFindingId, isEmailVerifiedUserRow, isMcpServerEnabled, isMissingTableError, isModuleNotFoundError, isPlatformOwnedObject, isRelationSubObjectPhrase, isSchemaAlreadyExistsError, isServerFault, isUnbackedConflictTargetError, isUniqueViolationError, keysetWalk, logServerFault, looksLikeInternalErrorLeak, matchMissingColumnOfRelation, postureGatesGlobalUniques, readEnvWithDeprecation, recordGlobalUniqueAttestation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveOrgMembershipLimit, resolvePlatformOwnerEmail, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, resolveThrownHttpError, sendError, sendOk, serverFaultLogMessage, serverFaultLogMeta, serverFaultProvenance, stampSearchPinyinEnabled, strandedDecisionDetails, strandedDecisionFailure, targetedTableOf, unconfirmedGlobalUniques, uniqueViolationColumn, validationFailure, validationFailureDetails };
package/dist/index.js CHANGED
@@ -77,6 +77,8 @@ __export(index_exports, {
77
77
  serverFaultLogMeta: () => serverFaultLogMeta,
78
78
  serverFaultProvenance: () => serverFaultProvenance,
79
79
  stampSearchPinyinEnabled: () => stampSearchPinyinEnabled,
80
+ strandedDecisionDetails: () => strandedDecisionDetails,
81
+ strandedDecisionFailure: () => strandedDecisionFailure,
80
82
  targetedTableOf: () => targetedTableOf,
81
83
  unconfirmedGlobalUniques: () => unconfirmedGlobalUniques,
82
84
  uniqueViolationColumn: () => uniqueViolationColumn,
@@ -501,6 +503,24 @@ function demotedDeclaredCode(thrown) {
501
503
  return thrown.declaredCode !== void 0 && thrown.declaredCode !== thrown.code ? thrown.declaredCode : void 0;
502
504
  }
503
505
 
506
+ // src/stranded-decision.ts
507
+ var CARRIER = "strandedDecision";
508
+ function strandedDecisionDetails(err) {
509
+ const carried = err?.[CARRIER];
510
+ if (!carried || typeof carried !== "object") return void 0;
511
+ const d = carried;
512
+ if (d.finalized !== true) return void 0;
513
+ if (typeof d.decision !== "string" || d.decision === "") return void 0;
514
+ if (typeof d.runId !== "string" || d.runId === "") return void 0;
515
+ if (typeof d.repairable !== "boolean") return void 0;
516
+ return { finalized: true, decision: d.decision, runId: d.runId, repairable: d.repairable };
517
+ }
518
+ function strandedDecisionFailure(message, details) {
519
+ const err = new Error(message);
520
+ err[CARRIER] = details;
521
+ return err;
522
+ }
523
+
504
524
  // src/relation-sub-object.ts
505
525
  function matchMissingColumnOfRelation(message) {
506
526
  return MISSING_COLUMN_OF_RELATION.exec(message)?.[1];
@@ -933,6 +953,8 @@ function postureGatesGlobalUniques(posture) {
933
953
  serverFaultLogMeta,
934
954
  serverFaultProvenance,
935
955
  stampSearchPinyinEnabled,
956
+ strandedDecisionDetails,
957
+ strandedDecisionFailure,
936
958
  targetedTableOf,
937
959
  unconfirmedGlobalUniques,
938
960
  uniqueViolationColumn,