mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-26 18:45:12 +00:00
* Add poka-yoke skill: make misuse unrepresentable rather than warned against
Mistake-proofing, applied to code. The skill pushes toward devices that make
a wrong action impossible or self-announcing, rather than instructions asking
someone to avoid it, and refuses to accept a comment or a convention as a fix.
The gap it addresses. Given a design, models readily list what to fix and
rarely state what the fix makes impossible. Measured across 591 blind-graded
runs and six model families, responses named the foreclosed set 42% of the
time unprompted and 81% with this skill applied. Assertions were written
before the runs and graded blind to configuration.
Stated with its limits, because they matter: every run was the first turn of
a fresh session, so this measures the ceiling rather than what survives a
long working session; the comparison was against no methodology rather than a
different one, so it does not establish that this particular method caused
the gain; and the skill costs something measurable, making responses somewhat
worse at spotting the specific defect already on the page while better at
changing the shape that allowed it. All of that is in the skill body rather
than omitted.
Bundled, all self-contained, no network access and no dependencies:
scripts/detect_hazards.py standard-library scanner, 42 pattern rules
across 20 hazard shapes, five languages
references/hazard-catalog.md the taxonomy with device per shape
references/lang-*.md Python, TypeScript, Go and Rust patterns
Verified with npm run skill:validate and npm run build. Raw benchmark runs,
the harness and the assertion checklists are public at
https://github.com/rainmanjam/poka-yoke
* Rename HasTable to HasFrom in the TypeScript typestate example
codespell failed the PR: it reads HasTable as a misspelling of hashtable.
The identifier is a legitimate generic parameter on a typestate builder,
QueryBuilder<HasTable, HasWhere>, so this is a false positive, but it is our
file breaking their gate and the fix costs nothing.
HasFrom is also the better name: the flag tracks whether .from() has been
called, not whether a table exists.
Renamed in the upstream repository too, so the two copies do not diverge on
day one. Verified the whole bundle is codespell-clean locally.
* Correct the effect figures in the poka-yoke skill
The submitted numbers (42% -> 81%) could not be reproduced from the upstream
gradings. Recomputed over the six models the sentence describes: 45% -> 80%
across 132 verdicts. Adds the per-scenario breakdown, which is the more useful
claim: the gains are in tasks where nobody asked for a design review.
214 lines
7.6 KiB
Markdown
214 lines
7.6 KiB
Markdown
# Rust and Go Devices
|
|
|
|
Two languages at opposite ends of the expressiveness spectrum. Rust can encode almost any
|
|
invariant in types; Go deliberately cannot, so its devices lean on convention plus tooling.
|
|
Know which one you're in before proposing a device.
|
|
|
|
---
|
|
|
|
# Rust
|
|
|
|
Rust's type system reaches Control for more hazard classes than any other mainstream language.
|
|
The affine type system in particular is the only mainstream answer to use-after-move, and it
|
|
turns use-after-close into a compile error rather than a convention, where Python has context
|
|
managers, TypeScript has scope-bound callbacks, and Go has `defer`, Rust has the compiler.
|
|
|
|
## Contact, newtypes and smart constructors
|
|
|
|
```rust
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct UserId(Uuid);
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Email(String);
|
|
|
|
impl Email {
|
|
// The only way to build one. Private field means no bypass, even in-crate
|
|
// if you put it behind a module boundary.
|
|
pub fn parse(s: &str) -> Result<Self, InvalidEmail> {
|
|
if !s.contains('@') { return Err(InvalidEmail); }
|
|
Ok(Email(s.to_owned()))
|
|
}
|
|
pub fn as_str(&self) -> &str { &self.0 }
|
|
}
|
|
```
|
|
|
|
A private field plus a fallible constructor means possessing an `Email` *is* proof of
|
|
validation. This is the strongest form of parse-don't-validate available anywhere.
|
|
|
|
## Contact, enums make illegal states unrepresentable
|
|
|
|
```rust
|
|
// Each variant carries exactly the data that variant has. There is no
|
|
// "succeeded with an error", because it cannot be written.
|
|
pub enum JobState {
|
|
Queued { enqueued_at: DateTime<Utc> },
|
|
Running { started_at: DateTime<Utc>, worker: WorkerId },
|
|
Succeeded { output: Output },
|
|
Failed { error: JobError, retries: u32 },
|
|
}
|
|
```
|
|
|
|
`match` without a catch-all is exhaustive by default, adding a variant breaks the build
|
|
everywhere it must. Avoid `_ => {}` arms in domain logic for exactly this reason: the wildcard
|
|
is what turns a compile error into a silent fallthrough two releases later.
|
|
|
|
## Motion-step, typestate
|
|
|
|
Ownership makes typestate genuinely practical, since each transition consumes the old state:
|
|
|
|
```rust
|
|
pub struct Draft;
|
|
pub struct Validated;
|
|
|
|
pub struct Order<S> { items: Vec<Item>, _state: PhantomData<S> }
|
|
|
|
impl Order<Draft> {
|
|
pub fn validate(self) -> Result<Order<Validated>, ValidationError> { /* … */ }
|
|
}
|
|
|
|
impl Order<Validated> {
|
|
// submit() does not exist on Order<Draft>. Not "returns an error", does not exist.
|
|
pub fn submit(self) -> Result<OrderId, SubmitError> { /* … */ }
|
|
}
|
|
```
|
|
|
|
The consumed `self` means the draft is gone after validation, so a stale unvalidated copy
|
|
cannot be submitted later.
|
|
|
|
## Fixed-value, make errors impossible to ignore
|
|
|
|
`#[must_use]` on `Result` is built in; add it to your own types where dropping the value is a
|
|
bug. Then set the lints:
|
|
|
|
```toml
|
|
[workspace.lints.clippy]
|
|
unwrap_used = "deny"
|
|
expect_used = "warn" # allow in tests and startup with a reason
|
|
panic = "deny"
|
|
indexing_slicing = "deny" # forces .get() and a real branch
|
|
float_cmp = "deny"
|
|
arithmetic_side_effects = "warn" # forces checked_/saturating_ where overflow matters
|
|
todo = "deny"
|
|
dbg_macro = "deny"
|
|
```
|
|
|
|
`unwrap_used = "deny"` is the highest-value line in that block: it converts every "this can't
|
|
fail" assumption into an explicit decision at review time.
|
|
|
|
## Rust limits
|
|
|
|
- **`unsafe` and `unwrap` are the escape hatches.** Deny both by lint and require a
|
|
`// SAFETY:` comment for each `unsafe` block.
|
|
- **Compile-time only.** Deserialized input still needs `serde` with `deny_unknown_fields`.
|
|
- **Panics bypass the type system.** A device that panics is Warning, not Control.
|
|
- **Typestate has real ergonomic cost.** Reserve it for genuinely dangerous sequences, payments, resource lifecycles, protocol state: not for every builder.
|
|
|
|
---
|
|
|
|
# Go
|
|
|
|
Go rejects most compile-time expressiveness by design. Its devices are therefore fewer, and
|
|
tooling plus data-layer constraints carry more of the load. Say so plainly when you propose a
|
|
device, Control is often not reachable here, and pretending otherwise is worse than
|
|
acknowledging the rung.
|
|
|
|
## Contact, defined types
|
|
|
|
```go
|
|
type UserID string
|
|
type OrderID string
|
|
|
|
func Transfer(from, to UserID) error { ... }
|
|
// Transfer(orderID, userID), compile error, because these are defined types, not aliases.
|
|
```
|
|
|
|
Use `type X string` (a defined type), never `type X = string` (an alias, which gives you
|
|
nothing). This is the one genuine Control-rung contact device Go offers, and it is
|
|
underused.
|
|
|
|
## Contact, functional options instead of boolean flags
|
|
|
|
```go
|
|
type Option func(*Config)
|
|
|
|
func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } }
|
|
func WithRetries(n int) Option { return func(c *Config) { c.Retries = n } }
|
|
|
|
func New(addr string, opts ...Option) (*Client, error) { ... }
|
|
```
|
|
|
|
Every option is named at the call site, `time.Duration` carries its unit in the type, and
|
|
adding an option later doesn't break callers. This replaces both the boolean-flag hazard and
|
|
the implicit-units hazard.
|
|
|
|
## Motion-step, constructors and defer
|
|
|
|
```go
|
|
func NewClient(addr string) (*Client, error) {
|
|
// Fully ready on return. No Connect() to forget.
|
|
}
|
|
|
|
conn, err := pool.Acquire(ctx)
|
|
if err != nil { return err }
|
|
defer conn.Release() // on the line after acquisition, always
|
|
```
|
|
|
|
Put `defer` immediately after the acquisition, before any other statement. Any code between
|
|
the two is a leak on the error path.
|
|
|
|
## Fixed-value, exhaustiveness
|
|
|
|
Go has no exhaustive switch. Use a linter:
|
|
|
|
```yaml
|
|
# .golangci.yml
|
|
version: "2"
|
|
|
|
linters:
|
|
enable:
|
|
- errcheck # unchecked errors: the single most valuable Go linter
|
|
- exhaustive # non-exhaustive switch over typed constants
|
|
- bodyclose # unclosed HTTP response bodies
|
|
- rowserrcheck # unchecked sql.Rows.Err
|
|
- sqlclosecheck
|
|
- contextcheck # context not propagated
|
|
- nilerr # returning nil after a non-nil error
|
|
- noctx # HTTP requests without a context
|
|
- gosec
|
|
settings:
|
|
exhaustive:
|
|
default-signifies-exhaustive: false
|
|
```
|
|
|
|
That is the v2 schema. golangci-lint v2 refuses to run against a v1 file rather than ignoring
|
|
the parts it no longer understands, so run `golangci-lint migrate` over an existing config
|
|
before upgrading.
|
|
|
|
`errcheck` is non-negotiable, Go's error convention is entirely opt-in without it, and
|
|
`_ = doSomething()` is how data loss enters a Go codebase.
|
|
|
|
## Go-specific traps
|
|
|
|
- **Nil maps** accept reads but panic on write. Construct with `make` in the constructor.
|
|
- **Loop variable capture** in goroutines, fixed in Go 1.22+, still present in older
|
|
codebases and vendored code.
|
|
- **`time.Duration` vs bare int**: always take a `Duration`; never an `int` seconds.
|
|
- **Zero values are valid**, so a struct with a missing field looks initialized. Use a
|
|
constructor that returns `(T, error)` and unexported fields to force it.
|
|
- **Slices share backing arrays**, `append` to a sub-slice can mutate the original. Use
|
|
three-index slicing `s[a:b:b]` when handing a slice out.
|
|
- **`context.Context` dropped** across a call boundary silently disables cancellation and
|
|
timeouts. `contextcheck` catches it.
|
|
|
|
## Go limits
|
|
|
|
Go cannot express: exhaustive matching, non-nullable references, immutability, typestate, or
|
|
generic constraints rich enough for units. Its Control-rung devices are essentially defined
|
|
types, unexported fields with constructors, and the database schema.
|
|
|
|
The practical consequence: in Go, **push more invariants into the database and into required
|
|
CI checks** than you would in Rust or TypeScript. `NOT NULL`, `CHECK`, and unique constraints
|
|
are doing work the language declines to do, and `golangci-lint` as a required check is what
|
|
makes the rest hold.
|