From 4c18caabc86645b70072fd4e829df15aa275976d Mon Sep 17 00:00:00 2001 From: lauren Date: Sat, 17 Oct 2026 11:56:01 -0700 Subject: [PATCH] fix(pstack): make a brand with a type guard, not as The branded-type example built its brand with `as`, which the skill's "No `as` casts" row tells agents to avoid. parseAgentId now narrows through an isAgentId type guard and returns the input with no cast, and the Branded types row says a brand comes from a verifying type guard, never from `as`. --- .../claude-code/skills/typescript-best-practices/SKILL.md | 2 +- .../typescript-best-practices/references/patterns.md | 8 ++++++-- pstack/skills/typescript-best-practices/SKILL.md | 2 +- .../typescript-best-practices/references/patterns.md | 8 ++++++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/harness/pstack/claude-code/skills/typescript-best-practices/SKILL.md b/harness/pstack/claude-code/skills/typescript-best-practices/SKILL.md index 2c0279d9a..e1e5aa7c9 100644 --- a/harness/pstack/claude-code/skills/typescript-best-practices/SKILL.md +++ b/harness/pstack/claude-code/skills/typescript-best-practices/SKILL.md @@ -12,7 +12,7 @@ Apply the **type-system-discipline** principle skill first. | Rule | Summary | |------|---------| | Discriminated unions | Model variants with a `kind` literal discriminant so impossible states can't be represented. No optional-field bags. | -| Branded types | Brand primitives with `& { readonly __brand: "X" }` so they can't be mixed up. Validate once at the boundary. | +| Branded types | Brand primitives with `& { readonly __brand: "X" }` so they can't be mixed up. Validate once at the boundary. A brand comes from a verifying type guard (`input is AgentId`), never from `as`. | | Constructive modeling | Build the shape so the illegal value can't be constructed. `[T, ...T[]]` for non-empty, `[T, T][]` for even length, `start` plus `duration` for a range. Not a runtime guard, not a wish for refinement types. | | Simplest total type | Keep `T[]` while every operation on it stays total. Strengthen to `NonEmpty` only where the loose type forces `!`, a cast, or a "should never happen" throw. | | `unknown` over `any` | External data is `unknown`. | diff --git a/harness/pstack/claude-code/skills/typescript-best-practices/references/patterns.md b/harness/pstack/claude-code/skills/typescript-best-practices/references/patterns.md index c21d876b7..f8dd99b63 100644 --- a/harness/pstack/claude-code/skills/typescript-best-practices/references/patterns.md +++ b/harness/pstack/claude-code/skills/typescript-best-practices/references/patterns.md @@ -9,9 +9,13 @@ Brand primitives so they can't be mixed up. Validate once at the boundary. Downs ```ts type AgentId = string & { readonly __brand: "AgentId" }; +function isAgentId(input: string): input is AgentId { + return isUUID(input); +} + function parseAgentId(input: string): AgentId { - if (!isUUID(input)) throw new Error(`Invalid agent id: ${input}`); - return input as AgentId; + if (!isAgentId(input)) throw new Error(`Invalid agent id: ${input}`); + return input; } function focusAgent(id: AgentId): void { diff --git a/pstack/skills/typescript-best-practices/SKILL.md b/pstack/skills/typescript-best-practices/SKILL.md index 2c0279d9a..e1e5aa7c9 100644 --- a/pstack/skills/typescript-best-practices/SKILL.md +++ b/pstack/skills/typescript-best-practices/SKILL.md @@ -12,7 +12,7 @@ Apply the **type-system-discipline** principle skill first. | Rule | Summary | |------|---------| | Discriminated unions | Model variants with a `kind` literal discriminant so impossible states can't be represented. No optional-field bags. | -| Branded types | Brand primitives with `& { readonly __brand: "X" }` so they can't be mixed up. Validate once at the boundary. | +| Branded types | Brand primitives with `& { readonly __brand: "X" }` so they can't be mixed up. Validate once at the boundary. A brand comes from a verifying type guard (`input is AgentId`), never from `as`. | | Constructive modeling | Build the shape so the illegal value can't be constructed. `[T, ...T[]]` for non-empty, `[T, T][]` for even length, `start` plus `duration` for a range. Not a runtime guard, not a wish for refinement types. | | Simplest total type | Keep `T[]` while every operation on it stays total. Strengthen to `NonEmpty` only where the loose type forces `!`, a cast, or a "should never happen" throw. | | `unknown` over `any` | External data is `unknown`. | diff --git a/pstack/skills/typescript-best-practices/references/patterns.md b/pstack/skills/typescript-best-practices/references/patterns.md index c21d876b7..f8dd99b63 100644 --- a/pstack/skills/typescript-best-practices/references/patterns.md +++ b/pstack/skills/typescript-best-practices/references/patterns.md @@ -9,9 +9,13 @@ Brand primitives so they can't be mixed up. Validate once at the boundary. Downs ```ts type AgentId = string & { readonly __brand: "AgentId" }; +function isAgentId(input: string): input is AgentId { + return isUUID(input); +} + function parseAgentId(input: string): AgentId { - if (!isUUID(input)) throw new Error(`Invalid agent id: ${input}`); - return input as AgentId; + if (!isAgentId(input)) throw new Error(`Invalid agent id: ${input}`); + return input; } function focusAgent(id: AgentId): void {