@kensio/skills 1.13.1

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.
@@ -0,0 +1,238 @@
1
+ ---
2
+ name: part-factory-test-data
3
+ description: Build test data with @kensio/part-factory, keeping in the factory everything a test does not care about, passing dependencies at call time so factories stay independent, and choosing between StaticFactory, DynamicFactory, VariantFactory, MappedFactory and AsyncMappedFactory. Use when writing test fixtures or builders, when a test file is full of object literals, when a shared event, message or payload shape is being hand-written, when a required field is about to be made optional to ease test setup, and when tempted to wrap a factory in a helper function that applies overrides.
4
+ license: Apache-2.0
5
+ metadata:
6
+ version: "1.13.1"
7
+ ---
8
+
9
+ # Building test data with Part Factory
10
+
11
+ [Part Factory](https://partfactory.dev/) (`@kensio/part-factory`) builds typed objects for tests. A
12
+ factory holds the defaults, and `make(overrides)` returns an object with the overrides applied down
13
+ through the nested structure.
14
+
15
+ The package README is the authority on the API. This skill covers what to put in a factory, where
16
+ factories should live, and which one to reach for.
17
+
18
+ It serves the `isolated-testing-style` skill. Factories are what make building state inside each
19
+ test cheap enough that nobody reaches for a shared fixture in the first place.
20
+
21
+ ## Say only what the test is about
22
+
23
+ A factory defines every value the test ignores. The test can state only the values it does. That is
24
+ the whole point of one.
25
+
26
+ Without it, a test opens with twenty lines of construction and it is unclear which of them the
27
+ assertions actually depend on. With it, the lines that are there are the lines that matter:
28
+
29
+ ```typescript
30
+ it("charges VAT on the order total", () => {
31
+ // Given an order whose lines add up to a total the assertion depends on.
32
+ const order = orderFactory.make({ lines: [{ price: 1000 }, { price: 2000 }] });
33
+
34
+ // When it is priced.
35
+ const priced = priceOrder(order);
36
+
37
+ // Then VAT is a fifth of that total.
38
+ expect(priced.vat).toBe(600);
39
+ });
40
+ ```
41
+
42
+ The prices are written down because the assertion is arithmetic on them. The order id, the customer
43
+ id and the dates stay with the factory, because the test would read the same whatever they were. The
44
+ rule is that simple: **if an assertion depends on a value, put it in the test. Otherwise let the
45
+ factory supply it.**
46
+
47
+ This also removes a pressure that quietly damages production types. When constructing an object by
48
+ hand is painful, the tempting fix is to mark its required fields optional so tests can skip them,
49
+ weakening the type for every caller in order to serve the tests. A factory makes the setup cheap.
50
+ The type can go on saying what is actually required.
51
+
52
+ ## Overrides are a deep partial
53
+
54
+ Overrides merge into the defaults, all the way down the nested structure. That is what makes "state
55
+ only what matters" possible. A test can set one field three levels deep and leave its siblings
56
+ alone.
57
+
58
+ Two consequences worth knowing:
59
+
60
+ - Arrays override by index, so `{ lines: [{ price: 1000 }] }` replaces the first default line and
61
+ leaves any others in place.
62
+ - An empty array leaves the defaults in place. To assert on emptiness, build the case some other way
63
+ rather than expecting `{ lines: [] }` to do it.
64
+
65
+ ## Which factory
66
+
67
+ - **`StaticFactory`** when the defaults are fixed values. The simplest thing that works, and the
68
+ right default choice.
69
+ - **`DynamicFactory`** when the defaults have to be generated fresh for each object. Pair it with
70
+ [`@faker-js/faker`](https://fakerjs.dev/). This is what gives tests their isolation. A random
71
+ email or a UUID means two tests cannot collide, so tearing down is unnecessary.
72
+ - **`VariantFactory`** for a named variation of a base factory, when the variation is a concept the
73
+ tests talk about. `closedOfferFactory` reads better in ten tests than
74
+ `offerFactory.make({ closesAt: aMinuteAgo })` written ten times.
75
+ - **`MappedFactory`** when the output shape differs from the parts you want to override.
76
+ - **`AsyncMappedFactory`** when producing the value means awaiting something, such as inserting a
77
+ row or signing a payload.
78
+
79
+ ```typescript
80
+ import { DynamicFactory } from "@kensio/part-factory";
81
+ import { faker } from "@faker-js/faker";
82
+
83
+ export const customerFactory = new DynamicFactory<Customer>(() => ({
84
+ id: faker.string.uuid(),
85
+ email: faker.internet.email(),
86
+ name: faker.person.fullName(),
87
+ }));
88
+ ```
89
+
90
+ ## Add a variant only for a shared meaning
91
+
92
+ A variant earns its name when several tests mean the same thing by it. One test that needs an
93
+ unusual value should write that value down inline, and leave the factories alone.
94
+
95
+ The failure mode is a directory of `cancelledOrderWithRefundAndNoAddressFactory` names, each used
96
+ once, where reading a test means going to find out what its factory actually sets. Writing the field
97
+ in the test is shorter and says more.
98
+
99
+ ## Reach for MappedFactory only when the map is a real transformation
100
+
101
+ `MappedFactory` earns its place when the thing you want to override has a different shape from the
102
+ thing you want back. Good cases:
103
+
104
+ - Parts to an encoded form body: `{ email, password }` mapped to a
105
+ `application/x-www-form-urlencoded` string.
106
+ - Front matter parts to a file as written on disk: `{ title, tags, body }` mapped to the YAML block
107
+ plus the markdown underneath.
108
+ - Components to a formatted identifier: ARN parts mapped to the ARN string.
109
+
110
+ If the mapping function is copying fields across into an object of the same shape, you wanted a
111
+ `DynamicFactory`. An identity map adds a type parameter, a second function and a layer of
112
+ indirection, and buys little.
113
+
114
+ ## Pass dependencies at call time
115
+
116
+ A factory that needs something from the outside world (a store, a client, a configured host)
117
+ declares it as a third type parameter and receives it as the second argument to `make`:
118
+
119
+ ```typescript
120
+ export const storedOrderFactory = new AsyncMappedFactory<
121
+ OrderParts,
122
+ Order,
123
+ { orders: OrderStore }
124
+ >(
125
+ () => ({ total: faker.number.int({ min: 100, max: 10_000 }) }),
126
+ async (parts, { orders }) => orders.insert({ id: faker.string.uuid(), ...parts }),
127
+ );
128
+
129
+ // In a test, which decides what to hand it.
130
+ const order = await storedOrderFactory.make({ total: 5000 }, { orders });
131
+ ```
132
+
133
+ Dependencies are given at call time rather than held by the factory, and are used as they are given,
134
+ never fetched or awaited. That is what keeps a factory shareable. It holds no state of its own,
135
+ reaches for no ambient state, and cannot depend on what another factory did first. A factory built
136
+ this way can be used in every test file in the codebase and still stand on its own.
137
+
138
+ Keep each dependency as narrow as the factory actually needs. An `OrderStore` is a dependency. The
139
+ whole application never is. A wide dependency is how state starts leaking between tests that were
140
+ supposed to be independent.
141
+
142
+ ## Call the factory directly
143
+
144
+ ```typescript
145
+ // Anti-pattern. This is make(overrides) with extra steps.
146
+ export function makeCustomer(overrides: Partial<Customer> = {}): Customer {
147
+ return { ...customerFactory.make(), ...overrides };
148
+ }
149
+ ```
150
+
151
+ `make(overrides)` already is that function, and it does the job better. Its overrides are partial
152
+ all the way down the nested structure, where the spread above replaces a nested object whole.
153
+
154
+ A wrapper that does anything more than pass overrides through is a signal to read and never to
155
+ write. It usually means one of two things:
156
+
157
+ - **The factory is the wrong shape.** The wrapper is computing something the defaults should be
158
+ computing, or deriving one field from another. Move that into a `DynamicFactory` defaults
159
+ function, which receives the overrides, or into a `MappedFactory` map.
160
+ - **The output type is fighting you.** The wrapper is casting, widening or filling in a field the
161
+ type demands but the test ignores. Fix the type, or use `MappedFactory` so the parts and the
162
+ output are allowed to differ.
163
+
164
+ The same goes for a wrapper that exists to pass a dependency. Declare it on the factory instead.
165
+
166
+ ## Factories belong beside the type they construct
167
+
168
+ A library that defines an event, a message or a payload shape should export a factory for it.
169
+ Otherwise every consumer hand-rolls the literal, and every copy drifts.
170
+
171
+ The worked example is an AWS Lambda function URL invocation event, payload format 2.0. It is around
172
+ thirty lines, of which two matter to any given test.
173
+
174
+ ```json
175
+ {
176
+ "version": "2.0",
177
+ "routeKey": "$default",
178
+ "rawPath": "/upload",
179
+ "rawQueryString": "part=3",
180
+ "headers": { "host": "abc.lambda-url.eu-west-2.on.aws", "user-agent": "..." },
181
+ "queryStringParameters": { "part": "3" },
182
+ "cookies": ["session=abc"],
183
+ "requestContext": {
184
+ "http": { "method": "POST", "path": "/upload", "sourceIp": "127.0.0.1" }
185
+ },
186
+ "body": "...",
187
+ "isBase64Encoded": false
188
+ }
189
+ ```
190
+
191
+ Hand-writing that in three test files gives three copies that drift as the shape changes. It also
192
+ carries a real trap. The path is in the event twice, as `rawPath` and as `requestContext.http.path`,
193
+ and the query string is in it twice, as `rawQueryString` and as `queryStringParameters`. A test that
194
+ sets one and not the other passes against a handler reading the field the test set, and fails in
195
+ production against the same handler reading the other one.
196
+
197
+ A `MappedFactory` removes both problems. The parts are what a test cares about, and the map is what
198
+ fills the event in consistently:
199
+
200
+ ```typescript
201
+ import { MappedFactory } from "@kensio/part-factory";
202
+
203
+ interface FunctionUrlRequestParts {
204
+ method: string;
205
+ path: string;
206
+ query: Record<string, string>;
207
+ body?: string;
208
+ }
209
+
210
+ export const functionUrlEventFactory = new MappedFactory<
211
+ FunctionUrlRequestParts,
212
+ FunctionUrlEvent
213
+ >(
214
+ () => ({ method: "GET", path: "/", query: {} }),
215
+ (parts) => ({
216
+ version: "2.0",
217
+ routeKey: "$default",
218
+ rawPath: parts.path,
219
+ rawQueryString: new URLSearchParams(parts.query).toString(),
220
+ queryStringParameters: parts.query,
221
+ // ... and the rest, filled in once.
222
+ requestContext: {
223
+ http: { method: parts.method, path: parts.path, sourceIp: "127.0.0.1" },
224
+ },
225
+ body: parts.body,
226
+ isBase64Encoded: false,
227
+ }),
228
+ );
229
+
230
+ // In a test, the two lines that matter are the two lines written.
231
+ const event = functionUrlEventFactory.make({ method: "POST", path: "/upload" });
232
+ ```
233
+
234
+ The path can no longer disagree with itself, because there is one place it is set.
235
+
236
+ Export the factory from the package that owns the type, from a test-support entry point so it does
237
+ not ship in the production bundle. Consumers then get a correct event with one call, and a change to
238
+ the shape is made once.
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: skill-template
3
+ description: Scaffold a new agent skill, writing the SKILL.md and its frontmatter to the Agent Skills specification, then wrapping it as a plugin in this repo with package.json, plugin.json and a marketplace entry. Use when the user asks to "add a new skill", "create a skill", "write a SKILL.md" or "start a new plugin", and when checking whether an existing skill is portable between agents.
4
+ license: Apache-2.0
5
+ metadata:
6
+ version: "1.13.1"
7
+ ---
8
+
9
+ # Skill template
10
+
11
+ A skill is a directory with a `SKILL.md` in it. That directory is the artefact, and every agent
12
+ reads it. The plugin folder around it in this repository is packaging for one of them.
13
+
14
+ Write the skill first, following the [specification](https://agentskills.io/specification). Then
15
+ wrap it.
16
+
17
+ ## The skill directory
18
+
19
+ ```
20
+ <skill-name>/
21
+ ├── SKILL.md
22
+ ├── references/ # optional, loaded only when linked to
23
+ ├── scripts/ # optional, anything the skill runs
24
+ └── assets/ # optional, templates and data files
25
+ ```
26
+
27
+ Those three subdirectory names come from the specification. An agent that supports skills at all
28
+ knows this shape, whether it reads the directory from `.agents/skills/`, `.claude/skills/`,
29
+ `.github/skills/` or a plugin.
30
+
31
+ **Every path a skill mentions is relative to its own directory.** The directory gets copied out on
32
+ its own, unzipped somewhere unrelated, and installed under a name this repository never sees. A
33
+ command written as `node skills/<skill-name>/scripts/check.mjs` works here and nowhere else. Write
34
+ `node scripts/check.mjs`. `pnpm validate:skills` fails the build on any path that reaches outside
35
+ the skill, which is the check that caught this after it had already shipped once.
36
+
37
+ ## Frontmatter
38
+
39
+ ```markdown
40
+ ---
41
+ name: <skill-name>
42
+ description: <what it does, then when to use it. Include the words and phrases a user would actually type>
43
+ license: Apache-2.0
44
+ metadata:
45
+ version: "0.0.0"
46
+ ---
47
+ ```
48
+
49
+ - `name` is lowercase, hyphenated, at most 64 characters, and matches the containing directory.
50
+ - `description` is the _only_ thing an agent sees when deciding whether to load the skill. It
51
+ carries the whole triggering burden. State what the skill does, then when to use it, in third
52
+ person. Concrete trigger phrases beat abstract summaries. 1024 characters is the ceiling.
53
+ - `license` and `metadata` travel with the directory. A copy in someone's `.agents/skills/` has no
54
+ package.json beside it, and these are then the only record of what it is and where it came from.
55
+ The release sets `metadata.version`. Never edit that number by hand.
56
+ - The specification allows two more keys. `compatibility` states an environment requirement, such as
57
+ a binary the scripts need. `allowed-tools` restricts the tools the skill may use, and support for
58
+ it varies between agents. Any other key fails validation.
59
+
60
+ Keep `SKILL.md` under 500 lines. It is instructions for an agent, and documentation for a human
61
+ belongs in the README. Push detail into `references/` and link to it. The body stays cheap to load
62
+ and the details are read only when needed.
63
+
64
+ ## Wrapping it as a plugin
65
+
66
+ Claude Code installs skills as plugins, so each one here has a plugin folder around it:
67
+
68
+ ```
69
+ plugins/<skill-name>/
70
+ ├── package.json # npm package: @kensio/<skill-name>
71
+ ├── README.md # for humans arriving from npm or GitHub
72
+ ├── .claude-plugin/
73
+ │ └── plugin.json # name, version, description, author
74
+ └── skills/
75
+ └── <skill-name>/ # the skill directory above
76
+ ```
77
+
78
+ 1. Create `plugins/<skill-name>/` following that layout.
79
+ 2. Copy `package.json` from an existing plugin, then set `name` to `@kensio/<skill-name>` and
80
+ `repository.directory` to `plugins/<skill-name>`.
81
+ 3. Copy `.claude-plugin/plugin.json`, then set `name` and `description`.
82
+ 4. Set the `version` in both files, and in the `SKILL.md` frontmatter, to whatever the other plugins
83
+ currently carry. Versions move in lockstep across the whole repo and the release workflow is what
84
+ changes them. Never pick a new number by hand.
85
+ 5. Write `skills/<skill-name>/SKILL.md`.
86
+ 6. Add an entry to `.claude-plugin/marketplace.json` with a matching `name`, a `source` of
87
+ `"./plugins/<skill-name>"`, and a description.
88
+ 7. Run `pnpm check`.
89
+
90
+ Anything under `skills/` ships, because that is what `package.json` lists in `files`. A script the
91
+ skill runs belongs there too, and never at the plugin root.
92
+
93
+ **A plugin folder must be self-contained.** Never reference files outside it with `../`. Plugins are
94
+ copied, zipped and installed standalone, and those paths will not resolve.
95
+
96
+ Nothing else needs telling about the new folder. `scripts/set-version.mjs`,
97
+ `scripts/publish-npm.mjs` and `scripts/build-zips.mjs` all read the `plugins/` directory, so a new
98
+ skill is versioned, bundled into `@kensio/skills`, zipped onto the release and published without
99
+ being listed anywhere else.
100
+
101
+ **A brand new package still needs one manual first publish.** npm trusted publishing cannot create a
102
+ package that does not exist, because the trusted publisher is configured against a package already
103
+ on the registry. The release reports the commands and carries on. See "npm publishing" in the
104
+ repository README.
105
+
106
+ ## Prose
107
+
108
+ `pnpm check` runs `pnpm prose`, which fails the build on em dashes, semicolons, and five sentence
109
+ shapes measured against Django, Effective Go, the Rust Book and the Python docs. A new `SKILL.md`
110
+ and `README.md` have to pass it.
111
+
112
+ Load the `technical-prose-style` skill before writing either one. It carries the rules, the
113
+ before-and-after examples, and the evidence for each. Checking a single file while drafting:
114
+
115
+ ```bash
116
+ node plugins/technical-prose-style/skills/technical-prose-style/scripts/prose-check.mjs plugins/<skill-name>
117
+ ```
118
+
119
+ ## Releasing
120
+
121
+ Releasing is automatic. Merging to `main` releases, and the version comes from the pull request
122
+ title. `fix:` for a patch, `feat:` for a minor, `feat!:` or a `BREAKING CHANGE` footer for a major.
123
+ A `docs:` or `chore:` title releases nothing.
124
+
125
+ Every plugin is set to the new version together. A released version means the same commit wherever
126
+ it was installed from.