Skip to content

Commit 1bd66ff

Browse files
btraversclaude
andcommitted
chore: enable all @unthrown/oxlint rules and fix the docs build
Docs build (root cause of the Package / ci Build / Bundle Size failures): the TSDoc on TypedWorkflowHandle's queries/signals/updates properties held inline code spans containing a raw `|` (e.g. AsyncResult<T, QueryValidationError | WorkflowExecutionNotFoundError>). typedoc renders member docs into markdown table cells without escaping pipes inside code spans, so markdown-it split the row at the pipe, the broken span left `Promise<T>` as a raw HTML-looking `<T>` element, and VitePress's Vue compiler failed with "Element is missing end tag" at api/client/index.md. Fixed at the source by rewording the three doc comments so no code span carries a pipe; generated files untouched. Lint: turn on unthrown/no-throw and unthrown/prefer-ensure, so all six rules are now at "error". prefer-ensure surfaced 0 violations. no-throw surfaced 148: - 76 in test files, handled by a config override turning only no-throw off for **/*.spec.ts and **/__tests__/** (tests throw to simulate defects and assert rejections; the other five rules stay on). - 72 in source, each carrying a targeted oxlint-disable naming its category — none could be restructured to Err(...) without changing semantics: - 33 sanctioned ValidationError / ContractMisuseError / ApplicationFailure sites (CLAUDE.md rule 2 exception — Temporal's terminal-failure semantics require the throw) - 12 client-side technical faults thrown inside makeAsyncResult work thunks (RuntimeClientError/TechnicalError — the throw IS the defect-channel routing) - 6 defect-cause / workflow-sandbox rethrows (unmodeled failures kept on the defect channel) - 1 cancellation rethrow (CancelledFailure must propagate) - 7 declaration-time fail-fast config errors (worker startup / contract definition / test setup) - 13 example-domain throws in plain Promise helpers wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...)) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e5e80b4 commit 1bd66ff

23 files changed

Lines changed: 94 additions & 12 deletions

.oxlintrc.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@
55
"rules": {
66
"unthrown/no-ambiguous-error-type": "error",
77
"unthrown/no-catch-all-pattern": "error",
8+
"unthrown/no-throw": "error",
89
"unthrown/no-unhandled-result": "error",
9-
"unthrown/prefer-async-result": "error"
10-
}
10+
"unthrown/prefer-async-result": "error",
11+
"unthrown/prefer-ensure": "error"
12+
},
13+
"overrides": [
14+
{
15+
"files": ["**/*.spec.ts", "**/__tests__/**"],
16+
"rules": {
17+
"unthrown/no-throw": "off"
18+
}
19+
}
20+
]
1121
}

examples/order-processing-worker/src/application/workflows.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export const processOrder = declareWorkflow({
150150
if (paymentResult.isDefect()) {
151151
// Unmodeled failure (a bug, not an anticipated outcome) — rethrow at
152152
// the edge so Temporal surfaces the Workflow Task failure.
153+
// oxlint-disable-next-line unthrown/no-throw -- defect-cause rethrow at the edge: an unmodeled failure must surface as a Workflow Task failure
153154
throw paymentResult.cause;
154155
}
155156
const failure = paymentResult.error;
@@ -169,6 +170,7 @@ export const processOrder = declareWorkflow({
169170
// Rethrow as this workflow's own declared contract error: the
170171
// execution fails with `ApplicationFailure(type: "PaymentDeclined")`
171172
// and the typed client rehydrates it into a `ContractError`.
173+
// oxlint-disable-next-line unthrown/no-throw -- sanctioned ApplicationFailure model: `throw context.errors.X(...)` is how a workflow fails terminally with a typed contract error (CLAUDE.md rule 2 exception)
172174
throw context.errors.PaymentDeclined({ reason: failure.data.reason }, { cause: failure });
173175
}
174176
case "@temporal-contract/ActivityError":
@@ -244,6 +246,7 @@ export const processOrder = declareWorkflow({
244246
// Cancellation must propagate — swallowing it here would leave the
245247
// workflow running after a cancel request.
246248
if (isCancellation(error)) {
249+
// oxlint-disable-next-line unthrown/no-throw -- cancellation rethrow: swallowing a CancelledFailure would leave the workflow running after a cancel request
247250
throw error;
248251
}
249252
// Non-critical: log but continue

examples/order-processing-worker/src/domain/usecases/create-shipment.usecase.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ export class CreateShipmentUseCase {
1212
async execute(orderId: string, customerId: string): Promise<ShippingResult> {
1313
// Business validation
1414
if (!orderId || orderId.trim() === "") {
15+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
1516
throw new Error("Order ID is required");
1617
}
1718

1819
if (!customerId || customerId.trim() === "") {
20+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
1921
throw new Error("Customer ID is required");
2022
}
2123

examples/order-processing-worker/src/domain/usecases/process-payment.usecase.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ export class ProcessPaymentUseCase {
1212
async execute(customerId: string, amount: number): Promise<PaymentOutcome> {
1313
// Business validation
1414
if (amount <= 0) {
15+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
1516
throw new Error("Payment amount must be positive");
1617
}
1718

1819
if (!customerId || customerId.trim() === "") {
20+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
1921
throw new Error("Customer ID is required");
2022
}
2123

examples/order-processing-worker/src/domain/usecases/purge-expired-orders.usecase.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class PurgeExpiredOrdersUseCase {
1111
async execute(olderThanDays: number): Promise<number> {
1212
// Business validation
1313
if (!Number.isInteger(olderThanDays) || olderThanDays <= 0) {
14+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
1415
throw new Error("olderThanDays must be a positive integer");
1516
}
1617

examples/order-processing-worker/src/domain/usecases/refund-payment.usecase.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class RefundPaymentUseCase {
1111
async execute(transactionId: string): Promise<void> {
1212
// Business validation
1313
if (!transactionId || transactionId.trim() === "") {
14+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
1415
throw new Error("Transaction ID is required");
1516
}
1617

examples/order-processing-worker/src/domain/usecases/release-inventory.usecase.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class ReleaseInventoryUseCase {
1111
async execute(reservationId: string): Promise<void> {
1212
// Business validation
1313
if (!reservationId || reservationId.trim() === "") {
14+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
1415
throw new Error("Reservation ID is required");
1516
}
1617

examples/order-processing-worker/src/domain/usecases/reserve-inventory.usecase.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ export class ReserveInventoryUseCase {
1212
async execute(items: OrderItem[]): Promise<InventoryReservation> {
1313
// Business validation
1414
if (!items || items.length === 0) {
15+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
1516
throw new Error("At least one item is required");
1617
}
1718

1819
for (const item of items) {
1920
if (item.quantity <= 0) {
21+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
2022
throw new Error(`Invalid quantity for product ${item.productId}`);
2123
}
2224
}

examples/order-processing-worker/src/domain/usecases/send-notification.usecase.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@ export class SendNotificationUseCase {
1111
async execute(customerId: string, subject: string, message: string): Promise<void> {
1212
// Business validation
1313
if (!customerId || customerId.trim() === "") {
14+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
1415
throw new Error("Customer ID is required");
1516
}
1617

1718
if (!subject || subject.trim() === "") {
19+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
1820
throw new Error("Subject is required");
1921
}
2022

2123
if (!message || message.trim() === "") {
24+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
2225
throw new Error("Message is required");
2326
}
2427

examples/order-processing-worker/src/infrastructure/adapters/payment.adapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class MockPaymentAdapter implements PaymentPort {
5656
logger.info(`✅ Refund successful`);
5757
} else {
5858
logger.error(`❌ Refund failed`);
59+
// oxlint-disable-next-line unthrown/no-throw -- known-technical precondition throw in a plain (non-Result) domain helper, wrapped once at the activity boundary via fromPromise(..., qualifyFailure(...))
5960
throw new Error("Payment processor rejected refund request");
6061
}
6162
}

0 commit comments

Comments
 (0)