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
4 changes: 2 additions & 2 deletions csharp/03-nullability-and-the-type-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ public static class UserParser
if (dto is null) return new Result<User, string>.Err("dto was null");
if (string.IsNullOrWhiteSpace(dto.Email)) return new Result<User, string>.Err("email required");

var id = UserId.From(dto.Id); // validated factory mints the brand (3.7)
var id = UserId.From(dto.Id); // validated factory generates the brand (3.7)
var roles = dto.Roles ?? []; // null from the wire collapses to empty, here (3.4)
return new Result<User, string>.Ok(new User(id, dto.Email, roles));
}
}
```

The boundary receives a nullable `UserDto?` and either returns a fully non-null `User` or a typed failure — no `!`, no `default!`, no `dynamic` (3.1, 3.3, 3.5). The one place a `null` from the wire is handled is explicit and local (3.4). `UserId` is a branded value type minted by a validating factory (3.7), and every public field is a `record` property carrying value semantics (3.8). The interior never sees an absence value it did not ask for.
The boundary receives a nullable `UserDto?` and either returns a fully non-null `User` or a typed failure — no `!`, no `default!`, no `dynamic` (3.1, 3.3, 3.5). The one place a `null` from the wire is handled is explicit and local (3.4). `UserId` is a branded value type generated by a validating factory (3.7), and every public field is a `record` property carrying value semantics (3.8). The interior never sees an absence value it did not ask for.

## Rules

Expand Down
2 changes: 1 addition & 1 deletion csharp/05-methods-and-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public string Describe() // good — bloc

**Reasoning, step by step:**
1. A call with three or more positional arguments is a row of unlabelled values at the call site — `Settle(amount, rate, account, true)` — where a reader cannot tell which `decimal` is which and a transposed pair compiles silently. Collecting them into an immutable `record` gives every argument a name at the call site, lets the caller use object-initializer or `with` syntax, and makes adding a field a non-breaking change instead of another positional slot (chapter [06](./06-types-and-data-modeling.md)).
2. The record also becomes the home for the validation and the defaults: `required` members force the caller to supply what matters, `init` defaults fill the rest, and the parse-don't-validate boundary (chapter [03](./03-nullability-and-the-type-system.md)) can mint a proven options value once. Two well-named parameters need no ceremony; the threshold is three, where the loss of labelling starts to cost correctness.
2. The record also becomes the home for the validation and the defaults: `required` members force the caller to supply what matters, `init` defaults fill the rest, and the parse-don't-validate boundary (chapter [03](./03-nullability-and-the-type-system.md)) can generate a proven options value once. Two well-named parameters need no ceremony; the threshold is three, where the loss of labelling starts to cost correctness.

**Worked example:**
```csharp
Expand Down
2 changes: 1 addition & 1 deletion csharp/06-types-and-data-modeling.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public readonly record struct ExpiryMonth
: new Result<ExpiryMonth, string>.Err("month out of range");
}
```
**Enforcement:** review of boundary modules; the private constructor makes the factory the only mint.
**Enforcement:** review of boundary modules; the private constructor makes the factory the only generator.

### 6.5 — Force construction-time completeness with `required` and `init`, not multi-step setters.

Expand Down
2 changes: 1 addition & 1 deletion ruby/05-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def tax_for(subtotal, jurisdiction:)
end
```

`Money` is minted in exactly one place — `Money.parse`, the parse-constructor that validates the integer-cents value before returning an immutable value object. Nothing else creates a `Money`. The public method sits above the two helpers it calls, so the file reads top-down (5.4). Guard clauses assert the preconditions and leave the happy path flush left (5.3). The keyword argument `now:` keeps the call site legible without a positional slot that callers can transpose (5.4 rule). A postcondition pair verifies the sum from both of its parts before return (5.12). Each method holds one level of abstraction — `settle_order` orchestrates named steps and touches no arithmetic itself (5.2). The helpers reach a `Money` only through `Money.parse`, the single parse-constructor that validates (per chapter 03).
`Money` is generated in exactly one place — `Money.parse`, the parse-constructor that validates the integer-cents value before returning an immutable value object. Nothing else creates a `Money`. The public method sits above the two helpers it calls, so the file reads top-down (5.4). Guard clauses assert the preconditions and leave the happy path flush left (5.3). The keyword argument `now:` keeps the call site legible without a positional slot that callers can transpose (5.4 rule). A postcondition pair verifies the sum from both of its parts before return (5.12). Each method holds one level of abstraction — `settle_order` orchestrates named steps and touches no arithmetic itself (5.2). The helpers reach a `Money` only through `Money.parse`, the single parse-constructor that validates (per chapter 03).

## Rules

Expand Down
2 changes: 1 addition & 1 deletion skills/typescript-bun-styleguide/reference/checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Bun runtime additions on top of `typescript-styleguide`. Additive only — never
- Every route declares request AND response schemas; the outbound `.parse()` is a leak tripwire.
- Handlers are thin: parse already-validated input, call one plain domain function, return; no Hono types in domain code. Test via `app.request()`, not a live socket or MSW.
- One centralized `app.onError(mapError)` maps domain errors to `problem+json`; handlers never craft a 5xx; unknown errors map to a generic 500 carrying only the correlation id.
- Every request carries a `correlationId`, minted or accepted once at the edge, propagated through `AsyncLocalStorage` via `store.run`.
- Every request carries a `correlationId`, generated or accepted once at the edge, propagated through `AsyncLocalStorage` via `store.run`.
- Set server timeouts explicitly: `idleTimeout` on the `Bun.serve` config and a per-route `timeout(...)`, kept under the upstream LB idle timeout.
- Rate-limit at the edge with bounded store state (LRU max size or Redis TTL), `429` + `Retry-After`.
- Health endpoints are honest: liveness does no dependency I/O (restart signal); readiness checks dependencies and fails first during drain (traffic signal).
Expand Down
2 changes: 1 addition & 1 deletion skills/typescript-styleguide/reference/checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ One section per chapter. Read on demand for a full audit; the SKILL digest cover
- Prefer optional `?` over `| undefined` in object types (`exactOptionalPropertyTypes` enforces the difference).
- Narrow with the weakest tool that works: discriminant, then `typeof`/`instanceof`/`in`, then a custom guard.
- Unit-test every custom type guard (`x is T`) with positive and negative cases.
- Brand domain primitives in high-rigor modules; mint only through a validating constructor (the one sanctioned `as`).
- Brand domain primitives in high-rigor modules; generate only through a validating constructor (the one sanctioned `as`).
- Put `readonly`/`ReadonlyArray`/`Readonly<T>` in every public signature.
- Constrain every generic; add no gratuitous type parameters; annotate variance (`in`/`out`) on public generic interfaces.
- Write erasable syntax only: no `enum`, runtime `namespace`, parameter properties, or `import =`. Consume a codegen `enum` only at the boundary, convert to a union inside.
Expand Down
8 changes: 4 additions & 4 deletions typescript-bun/03-http-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { mapError } from './error-map.ts'; // the one domain-error
const app = new Hono();

app.use('*', async (c, next) => { // 3.6 — correlate before any handler runs
const correlationId = c.req.header('x-request-id') ?? randomUUID(); // accept inbound or mint
const correlationId = c.req.header('x-request-id') ?? randomUUID(); // accept inbound or generate
await runWithRequestContext({ correlationId }, next); // store.run wraps the rest of the request (ch. 06)
});
app.onError(mapError); // 3.5 — handlers never craft 5xx; one map owns error → problem+json
Expand All @@ -35,7 +35,7 @@ app.get(
export default { fetch: app.fetch, idleTimeout: 30 }; // 3.7 — Bun.serve picks this up; idleTimeout is the slowloris guard
```

Every byte is parsed before the handler sees it and every reply field is schema-checked on the way out (3.3); the handler is glue over `getUser`, a function the unit tests call with no HTTP in sight (3.4); failures route through one `onError` (3.5); a correlation id is minted or accepted once and threaded through `AsyncLocalStorage` so every downstream log line carries it (3.6); the server idle timeout is set explicitly on the exported `Bun.serve` config (3.7). This is the Hono-on-`Bun.serve` idiom (3.1) the rest of the chapter justifies rule by rule.
Every byte is parsed before the handler sees it and every reply field is schema-checked on the way out (3.3); the handler is glue over `getUser`, a function the unit tests call with no HTTP in sight (3.4); failures route through one `onError` (3.5); a correlation id is generated or accepted once and threaded through `AsyncLocalStorage` so every downstream log line carries it (3.6); the server idle timeout is set explicitly on the exported `Bun.serve` config (3.7). This is the Hono-on-`Bun.serve` idiom (3.1) the rest of the chapter justifies rule by rule.

## Rules

Expand Down Expand Up @@ -128,14 +128,14 @@ app.onError((err, c) => {
### 3.6 — Every request carries a correlation id.

**Reasoning, step by step:**
1. A middleware registered with `app.use('*', ...)` either accepts an inbound `x-request-id` (so a trace spans services) or mints a fresh UUID when none arrives, via `randomUUID` from `node:crypto`. The id is decided once, at the very front of the request, before any handler or domain call.
1. A middleware registered with `app.use('*', ...)` either accepts an inbound `x-request-id` (so a trace spans services) or generates a fresh UUID when none arrives, via `randomUUID` from `node:crypto`. The id is decided once, at the very front of the request, before any handler or domain call.
2. It propagates through `AsyncLocalStorage` ([06](./06-logging.md)), which works unchanged on Bun, not by threading a parameter through every function. The middleware binds it with `store.run` wrapping the request continuation — `await store.run(ctx, next)` — the scoped form [06 §6.2](./06-logging.md) mandates over the mid-handler accessor it bans for leaking context into whatever runs next on the loop. Any code in the request's async context — domain function, repository, error handler — then reads the id from the store, so correlation survives `await` boundaries without polluting signatures.
3. Every log line during the request carries that id, which makes the one boundary log (3.5) joinable to all that led to it — parity with the kotlin-jvm correlation contract ([kotlin-jvm 06](../kotlin-jvm/06-logging.md)): one id, set at the edge, on every line, across the whole call tree.
4. The canonical log key is `correlationId`, set once here at the edge and read everywhere downstream ([06 §6.2](./06-logging.md)) — one name for the id in the child logger, the `AsyncLocalStorage` store, and every line, so a trace joins without reconciling synonyms.

```ts
app.use('*', async (c, next) => {
const correlationId = c.req.header('x-request-id') ?? randomUUID(); // accept inbound or mint
const correlationId = c.req.header('x-request-id') ?? randomUUID(); // accept inbound or generate
await runWithRequestContext({ correlationId }, next); // store.run wraps the request continuation, the scoped form ch. 06 §6.2 mandates
});
```
Expand Down
2 changes: 1 addition & 1 deletion typescript-bun/05-serialization-and-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,6 @@ const stored = await s3.file(`thumbs/${id}`).bytes(); // s3.file (S3
## Cross-references

- zod at the boundary, `z.infer` as the single source of type truth: [core 10.7](../typescript/10-api-design.md). Boundary route rule, `unknown` inward, `any` banned, `undefined` over `null`: [core 03's §3.2, §3.5, §3.6](../typescript/03-the-type-system.md).
- Branded `Cents`, integer minor units, the parse-mint constructor: [core 05](../typescript/05-functions.md).
- Branded `Cents`, integer minor units, the parse-generate constructor: [core 05](../typescript/05-functions.md).
- Null-versus-absent and time types, JVM parity: [kotlin-jvm serialization](../kotlin-jvm/05-serialization.md).
- Parse every boundary, crash-only boot, dependency justification: BUN-3, BUN-1, BUN-4 ([README](./README.md)). Rows parsed at the edge: [persistence](./04-persistence.md). HTTP body limits and handlers: [HTTP services](./03-http-services.md).
2 changes: 1 addition & 1 deletion typescript-bun/06-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ log().info(`order ${orderId} placed with ${itemCount} items`); // bad — data m
**Reasoning, step by step:**
1. Every log line for a request needs the same cross-cutting context: a correlation id, the principal, the tenant. Threading a `logger` or `ctx` parameter through every function to achieve that pollutes signatures all the way down and breaks the moment one layer forgets to pass it.
2. `AsyncLocalStorage` from `node:async_hooks` is the answer, and the direct analog of SLF4J's MDC in [kotlin-jvm/06-logging.md](../kotlin-jvm/06-logging.md) §6.6. Bun ships it through its `node:async_hooks` compatibility, and it works for this pattern: the store follows the async call graph across every `await`, timer, and microtask — the store set before an `await` is the store seen after it. Where the JVM bridges MDC across coroutine suspensions with `MDCContext`, here no bridging is needed.
3. Bind the store once at the boundary with `runWithRequestContext(ctx, fn)` and read it through a `log()` accessor that does `root.child(store.getStore())`. Correlation id and principal then appear on every line in that request's async subtree, no parameter passed. The canonical context key is `correlationId` — one name in the store, the child logger, and every line, so a trace joins downstream without reconciling synonyms ([03 §3.6](./03-http-services.md) mints it at the edge). Use `store.run` to scope it; never `enterWith` mid-handler, which leaks the context into whatever runs next on the loop.
3. Bind the store once at the boundary with `runWithRequestContext(ctx, fn)` and read it through a `log()` accessor that does `root.child(store.getStore())`. Correlation id and principal then appear on every line in that request's async subtree, no parameter passed. The canonical context key is `correlationId` — one name in the store, the child logger, and every line, so a trace joins downstream without reconciling synonyms ([03 §3.6](./03-http-services.md) generates it at the edge). Use `store.run` to scope it; never `enterWith` mid-handler, which leaks the context into whatever runs next on the loop.

```ts
app.use('*', (c, next) => runWithRequestContext({ correlationId: c.req.header('x-request-id') ?? randomUUID(), principal: c.get('principal') }, next));
Expand Down
2 changes: 1 addition & 1 deletion typescript-bun/08-build-and-distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ await Bun.build({

**Reasoning, step by step:**
1. Supply-chain integrity runs in both directions, and both directions need a guarantee. Outbound: a consumer installing your package wants proof the tarball was built from the source it claims and not swapped by a compromised token. Inbound: your build wants proof the dependency tree it resolves is the exact one that was reviewed.
2. Outbound is npm provenance plus 2FA. `bun publish` carries the tarball, the access flag, and 2FA (`--auth-type`, `--otp`), but as of this writing it does not emit a provenance attestation — there is no `--provenance` flag. So the publish step in CI runs `npm publish --provenance` (from a CI runner with an OIDC identity), which attaches a signed, verifiable link from the artifact back to the commit and workflow that produced it. This is a registry-tooling choice, not a workflow regression: `bun` builds, installs, and audits the package; the one publish call that mints the attestation goes through `npm` until Bun ships provenance. 2FA on the publish step means a stolen token alone cannot push a release, and a laptop cannot mint provenance — which is the point: publishing moves to CI (BUN-4). When `bun publish` gains `--provenance`, the call swaps and the rest of the pipeline is unchanged.
2. Outbound is npm provenance plus 2FA. `bun publish` carries the tarball, the access flag, and 2FA (`--auth-type`, `--otp`), but as of this writing it does not emit a provenance attestation — there is no `--provenance` flag. So the publish step in CI runs `npm publish --provenance` (from a CI runner with an OIDC identity), which attaches a signed, verifiable link from the artifact back to the commit and workflow that produced it. This is a registry-tooling choice, not a workflow regression: `bun` builds, installs, and audits the package; the one publish call that generates the attestation goes through `npm` until Bun ships provenance. 2FA on the publish step means a stolen token alone cannot push a release, and a laptop cannot generate provenance — which is the point: publishing moves to CI (BUN-4). When `bun publish` gains `--provenance`, the call swaps and the rest of the pipeline is unchanged.
3. Inbound is the committed lockfile, no exceptions. `bun.lock` pins every transitive dependency to an exact version and integrity hash, so `bun install` resolves the reviewed tree and not a freshly-floated one. An uncommitted or `.gitignore`d lockfile means every install is an unreviewed code change ([../security.md](../security.md), BUN-4).

```jsonc
Expand Down
4 changes: 2 additions & 2 deletions typescript/03-the-type-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ parseUser(42);

**Reasoning, step by step:**
1. An assertion is an unproven claim to the compiler: "trust me, this is a `T`." The compiler stops checking and believes you. When you are wrong, the failure surfaces far from the lie.
2. Reach for the proven alternatives first: `satisfies` checks a value against a type *without* widening it, and a guard or a parse (zod) establishes the type with a runtime check the compiler can see. When an assertion is genuinely unavoidable, such as minting a brand after validation (3.9) or narrowing a value the compiler cannot follow, it carries a comment stating why the claim holds.
2. Reach for the proven alternatives first: `satisfies` checks a value against a type *without* widening it, and a guard or a parse (zod) establishes the type with a runtime check the compiler can see. When an assertion is genuinely unavoidable, such as generating a brand after validation (3.9) or narrowing a value the compiler cannot follow, it carries a comment stating why the claim holds.

**Worked example:**
```ts
Expand Down Expand Up @@ -149,7 +149,7 @@ expect(isUser({id: 'u1'})).toBe(false); // negative space is man

**Reasoning, step by step:**
1. In a structural type system every `string` is interchangeable, so `UserId`, `OrderId`, and a raw email are one type and the compiler will happily pass one where another is meant. A brand attaches a phantom tag: `type UserId = string & {readonly __brand: 'UserId'}`. It costs nothing at runtime (the field never exists) but makes the values nominally distinct.
2. A branded value can only be *created* through a parsing constructor that validates and mints it. That constructor is the single place an `as` is sanctioned (3.4), because the value is proven the line before.
2. A branded value can only be *created* through a parsing constructor that validates and generates it. That constructor is the single place an `as` is sanctioned (3.4), because the value is proven the line before.

**Worked example:**
```ts
Expand Down
Loading