@maci0/dsh-perf-review 0.10.0

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.
package/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ dsh-perf-review
2
+
3
+ The bundled perf-review skill includes material adapted from the perf-review,
4
+ webperf-review, concurrency, resource, database, and cache reviews in
5
+ maci0/gauntlet (https://github.com/maci0/gauntlet), identified in this
6
+ repository as AGPL-3.0 material. The distributed package is AGPL-3.0-only.
7
+
8
+ The original MIT license notice for the DSH adapter is retained in LICENSE-MIT.
9
+ Copyright (c) 2026 dsh-perf-review contributors.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # dsh-perf-review
2
+
3
+ "Make it faster" is a guess. This skill makes the agent measure first.
4
+
5
+ Type `/perf-review` and it profiles the hot path you name, ranks the bottlenecks it finds by user-visible impact, changes one thing, and reports the p50/p95 delta. If it cannot measure a win, it says so instead of shipping one.
6
+
7
+ The whole plugin is one skill: instructions the agent follows, not a profiler. It brings the method and the output format; your existing tools (`perf`, `hyperfine`, `lighthouse`, `heaptrack`, devtools) do the measuring.
8
+
9
+ ## What you get
10
+
11
+ - **A resident performance engineer.** The agent gets a role and a success metric (perceived speed) plus hard rules: profile before changing, name the tool and the scenario and the baseline, benchmark every change that claims speed with p50/p95, keep behaviour identical unless a tradeoff is explicit and measured.
12
+ - **A triage order.** Unbounded growth (no pagination, no bound) beats N+1 and redundant work, which beats hot-path allocations and per-iteration compilation, which beats cold-path issues. With no benchmark target, it fixes only categorically safe wins and skips anything whose benefit needs numbers to prove.
13
+ - **Checklists for both halves.** Frontend: FPS, long tasks, input delay, layout thrash, forced reflow, giant unwindowed lists, work that belongs on a worker, compositor-friendly animation, then render-blocking critical path and deferred bytes. Backend: CPU and allocation profiles, cache misses, SoA over AoS, arena reuse, batched I/O, auto-vectorization blockers checked before anyone reaches for intrinsics.
14
+ - **Deterministic perf tests, not flaky ones.** Every claimed win has to leave a test that still passes on a loaded machine: retired instructions or work counters first, then CPU time, then hardware-counter ratios. Wall clock is for the product-level p50/p95 and a coarse bound only; a wall-clock gate needs medians and a tolerance band, and says so.
15
+ - **Runtime currency check.** It notes the version the code actually runs on and checks recent releases for speedups that touch the hot paths found, and recommends an upgrade only where a measured path gains.
16
+ - **Named ownership boundaries.** It judges whether a cache should exist and owns app-side query call sites; it never owns schema or migrations. It will not trade correctness, accessibility, or content for speed.
17
+ - **A fixed report shape.** Bottlenecks ranked by user-visible impact with confidence (confirmed / likely / potential), changes made with measured deltas, remaining hot paths, and what it refused to do because it was unmeasured or would not help.
18
+
19
+ ## Install
20
+
21
+ > **Install it as a bundle.** `dsh plugin add …` mounts the row from the
22
+ > package's own patch layer, which is what the settings editor can write to. A
23
+ > row added with `--patch` is an overlay: it disappears at the next start.
24
+
25
+ ```sh
26
+ dsh plugin --profile web add @maci0/dsh-perf-review@0.10.0
27
+ ```
28
+
29
+ This installs the public npm package; no GitHub token or `~/.secrets` setup is needed.
30
+ The version is pinned. To upgrade, run the same command with a newer version,
31
+ then restart `dsh web` (bundle layers compose at boot).
32
+
33
+ The package declares `dsh.bundle.patch`, so the CLI appends it to `dsh.profile.bundles` and its shipped `cordis.patch.yml` supplies the row. Do not paste that row into `~/.dsh/profiles/web/cordis.patch.yml` as well: the bundle layer already applies it, and a second row registers the plugin twice.
34
+
35
+ Uninstall with the package name:
36
+
37
+ ```sh
38
+ dsh plugin --profile web remove @maci0/dsh-perf-review
39
+ ```
40
+
41
+ That drops the dependency and the bundle layer with it; nothing else to edit.
42
+
43
+ ## Use it
44
+
45
+ Type the lag, not the fix:
46
+
47
+ ```
48
+ /perf-review the file tree stalls for a beat when I expand a folder, profile it and tell me what to change
49
+ ```
50
+
51
+ The agent then states the user-visible lag it is attacking, shows profile evidence (hot function, % time, scenario), proposes the smallest change that hits that hot path, implements it, re-runs the same scenario before and after, and keeps or reverts on the numbers.
52
+
53
+ Give it a repo path, a trace, or a running local URL and it works from what exists. For a browser measurement it uses static files or an already-listening local URL. It never installs tools, never starts a server to get a measurement, and never hits a remote host. With no codebase in reach it first lists the exact files, traces, and benchmarks it needs.
54
+
55
+ ## Configure
56
+
57
+ No config fields. The plugin reads one bundled skill and mounts it; behaviour is the skill text itself.
58
+
59
+ | Frontmatter key | Effect |
60
+ |---|---|
61
+ | `name` | Skill id: `perf-review`, so the composer exposes `/perf-review`. |
62
+ | `description` | What the model sees when deciding to load the skill. |
63
+
64
+ Both invocation policies are always on (the provider emits `invocation: { modelInvocable: true, userInvocable: true }`), and any other frontmatter key is parsed and ignored.
65
+
66
+ ## How it works
67
+
68
+ `index.js` is plain JavaScript, no build step. It hooks `ctx.skills.registerProvider()`, resolves its `skills/` directory with `fileURLToPath`, and reads the one bundled `skills/perf-review/SKILL.md` directly. Candidate summaries carry `rank: BUNDLED_SKILL_RANK`, so a project or user skill of the same name still takes precedence.
69
+
70
+ Frontmatter is parsed with `yaml`, the same parser the harness's own filesystem skill provider uses, so plain scalars, `|`/`|-`/`>-` block scalars, and nested maps read as YAML says they do. An invalid skill name or a description-less file is skipped with a warning, never fatal. A missing root or a refused `SKILL.md` is reported as an **incomplete observation**, not an empty catalog, so the registry cannot cache a failed read as "no skills here".
71
+
72
+ Runtime dependencies: `@deepseek-ai/dsh-skill` and `yaml`, both declared in `package.json`.
73
+
74
+ ## Limits
75
+
76
+ - It is a skill plus instructions, not a profiler. Every number comes from a tool you already have; if nothing can measure the path, the change does not ship.
77
+ - It does not own schema, indexes, or migrations, and it does not own caching correctness, only whether a cache should exist at all.
78
+ - It adds no model tool, no slash command, and no browser half. A skill needs none of that.
79
+ - The composer exposes user-invocable skills as `/<name>` on its own; this package does not draw UI.
80
+
81
+ ## Development
82
+
83
+ ```sh
84
+ bun install --frozen-lockfile
85
+ bun test # tests/*.test.js: 60 tests, no build step
86
+ ```
87
+
88
+ dsh loads plugins on Node `^22.19.0 || >=24.0.0`; development and tests run on bun. Tests cover the frontmatter parser, discovery, the provider's `list`/`get` contract, abort handling, incomplete-root reporting, and a real Cordis composition that mounts and disposes the provider.
89
+
90
+ For local development, install the checkout as a bundle:
91
+
92
+ ```sh
93
+ dsh plugin --profile <name> add <path-to-checkout>
94
+ ```
95
+
96
+ ## Licence
97
+
98
+ AGPL-3.0-only. The bundled skill includes material adapted from
99
+ [maci0/gauntlet](https://github.com/maci0/gauntlet). See `LICENSE` and `NOTICE`.
100
+ The original MIT notice for the DSH adapter is retained in `LICENSE-MIT`.
@@ -0,0 +1,10 @@
1
+ # The dsh-perf-review bundle patch: applied automatically when a profile lists
2
+ # this bundle (`dsh plugin add`/`update` appends the package to
3
+ # dsh.profile.bundles). Users override this row from their profile's own
4
+ # cordis.patch.yml (live-watched; dsh.profile.bundles is frozen at boot) with a
5
+ # `- id: perf-review` row, which replaces the row's whole `config`.
6
+ # Do not also insert this same row into the profile patch: insert does not
7
+ # dedupe ids, and a second row would register the plugin twice.
8
+ - insert:
9
+ - id: perf-review
10
+ name: '@maci0/dsh-perf-review'
package/icon.svg ADDED
@@ -0,0 +1,5 @@
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M8 23.5a10 10 0 1 1 20 0" stroke="#F2AF63" stroke-width="2.4" stroke-linecap="round"/>
3
+ <path d="M18 23.5L24.2 15" stroke="#C47B2B" stroke-width="2.2" stroke-linecap="round"/>
4
+ <circle cx="18" cy="23.5" r="2.1" fill="#C47B2B"/>
5
+ </svg>
package/index.js ADDED
@@ -0,0 +1,381 @@
1
+ /**
2
+ * dsh-perf-review: performance review skill for DeepSeek Harness.
3
+ *
4
+ * One capability: the bundled `perf-review` skill becomes a `ctx.skills`
5
+ * provider, so it loads through the `skill` tool and appears as
6
+ * `/perf-review` in the composer. No settings, no tool, no command, no
7
+ * browser half: a skill needs none of that.
8
+ *
9
+ * Skill content: the user's perf prompt, plus the hot-path/SIMD/data-layout
10
+ * material from gauntlet's perf-review and the critical-path/delivery
11
+ * material from its webperf-review (maci0/gauntlet), plus fix-order and
12
+ * ownership-boundary lines from the adjacent concurrency/resource/db/cache
13
+ * reviews.
14
+ *
15
+ * `package.json` declares `dsh.bundle.patch`, so `dsh plugin add` mounts this
16
+ * package as a profile layer and cordis.patch.yml supplies the row.
17
+ */
18
+
19
+ import { readFile } from 'node:fs/promises'
20
+ import { basename, dirname, join } from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+ import { BUNDLED_SKILL_RANK, isSkillName } from '@deepseek-ai/dsh-skill'
23
+
24
+ /** Plugin name as it appears in the loader. */
25
+ export const name = 'perf-review'
26
+
27
+ /** Service this plugin needs; `ctx.skills` is ready when `apply` runs. */
28
+ export const inject = ['skills']
29
+
30
+ /**
31
+ * Read and parse one skill file. Shared by discovery and direct loads so a
32
+ * single file enforces the name/description/frontmatter rules everywhere.
33
+ */
34
+ async function readSkillFile(path, onWarn, entryName, signal) {
35
+ if (signal?.aborted) return undefined
36
+
37
+ let source
38
+ try {
39
+ source = await readFile(path, { encoding: 'utf8', signal })
40
+ } catch (error) {
41
+ // An aborted read is the caller withdrawing, not a broken skill.
42
+ if (!signal?.aborted) onWarn?.(`cannot read ${path}: ${error instanceof Error ? error.message : String(error)}`)
43
+ return undefined
44
+ }
45
+
46
+ let parsed
47
+ try {
48
+ // The flat reader covers every header this package ships. Its refusal is
49
+ // what selects the real YAML parser, so the fallback stays the contract.
50
+ parsed = parseFrontmatter(source)
51
+ if (parsed === undefined) parsed = await parseFrontmatterWithYaml(source)
52
+ } catch (error) {
53
+ onWarn?.(`skipping ${path}: ${error instanceof Error ? error.message : String(error)}`)
54
+ return undefined
55
+ }
56
+
57
+ const fallback = entryName ?? basename(path)
58
+ const skillName = String(parsed.data.name ?? fallback).trim()
59
+ const description = String(parsed.data.description ?? '').trim()
60
+ if (!isSkillName(skillName)) {
61
+ onWarn?.(`skipping ${path}: "${skillName}" is not a valid kebab-case skill name`)
62
+ return undefined
63
+ }
64
+ if (description === '') {
65
+ onWarn?.(`skipping ${path}: frontmatter has no description`)
66
+ return undefined
67
+ }
68
+
69
+ return {
70
+ name: skillName,
71
+ description,
72
+ content: parsed.body.trim(),
73
+ path,
74
+ directory: dirname(path),
75
+ }
76
+ }
77
+
78
+ /** `key: value` at column zero, with nothing but horizontal space around the colon. */
79
+ const ENTRY = /^([^\s:#][^\s:#]*?)[ \t]*:([ \t]+[^\r\n]*|)$/
80
+ /** A plain scalar with no leading indicator, no `#`, no `: ` mapping, no reserved start. */
81
+ const PLAIN = /^[^\s!&*\-?{}[\],#|>@`"'%:][^#:]*$/
82
+ /** A double-quoted scalar with no backslash escape in it. */
83
+ const SIMPLE_DOUBLE = /^[^\\]*$/
84
+ /** A single-quoted scalar with no `''` escape in it. */
85
+ const SIMPLE_SINGLE = /^[^']*$/
86
+ /** The block scalar header this reader reads: style plus an optional strip flag. */
87
+ const BLOCK_HEADER = /^([|>])(-)?$/
88
+ /** A plain scalar YAML types as an integer: `0x10`, `0o17`, `+5`, `-0`, `007`, `1_000`. */
89
+ const TYPED_INT = /^[-+]?(?:0[xX][0-9a-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+|[0-9][0-9_]*)$/
90
+ /** A plain scalar YAML types as a special float: `.inf`, `.nan`, either sign, any case. */
91
+ const TYPED_SPECIAL = /^[-+]?\.(?:inf|nan)$/i
92
+ /** A plain scalar YAML types as a float: a leading `+`, leading zeros, a `.5`/`1.` body. */
93
+ const TYPED_FLOAT = /^(?:[-+]?[0-9][0-9_]*\.[0-9_]*|[-+]?\.[0-9][0-9_]*)$/i
94
+ /** The decimal scalars this reader converts itself, with no YAML-only spelling. */
95
+ const SAFE_INT = /^-?(?:0|[1-9]\d*)$/
96
+ const SAFE_FLOAT = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][-+]?\d+)?$/
97
+ /** Keys YAML resolves to a non-string: `null` becomes `''` and `True` becomes `'true'`. */
98
+ const RESOLVED_KEY = /^(?:~|null|Null|NULL|true|True|TRUE|false|False|FALSE)$/
99
+ /** Keys that would not survive `data[key] = value` on an object literal. */
100
+ const UNSAFE_KEY = new Set(['__proto__'])
101
+ /** A key this reader can prove `yaml` resolves to the same string. */
102
+ const SAFE_KEY = /^[A-Za-z_][A-Za-z0-9_.-]*$/
103
+ /** Code points JS `trim` strips but YAML counts as content: indentation is unprovable. */
104
+ const JS_ONLY_SPACE = /[\u000B\u000C\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/
105
+
106
+ /** The delimiter line, exactly as the pre-change reader tested it. */
107
+ const DELIMITER = /^---[ \t]*$/
108
+
109
+ /** Split a data block into lines. `parseFrontmatter` refuses any `\r`, so `\n` is the only break. */
110
+ function toLines(text) {
111
+ return text.split('\n')
112
+ }
113
+
114
+ /**
115
+ * Split one document into its leading frontmatter block and the body, the way
116
+ * the pre-change reader did: line by line, so the string handed to `yaml` is
117
+ * byte-identical to what it parsed before the fast path existed.
118
+ * @param {string} source - the file's contents.
119
+ * @returns {{ block: string, body: string, present: boolean }} the parts.
120
+ */
121
+ function splitDocument(source) {
122
+ const text = String(source).replace(/^\uFEFF/, '')
123
+ const lines = text.split(/\r?\n/)
124
+ if (lines[0] === undefined || !DELIMITER.test(lines[0])) return { block: '', body: text, present: false }
125
+ let closing = -1
126
+ for (let index = 1; index < lines.length; index += 1) {
127
+ if (DELIMITER.test(lines[index] ?? '')) {
128
+ closing = index
129
+ break
130
+ }
131
+ }
132
+ if (closing === -1) return { block: '', body: text, present: false }
133
+ return { block: lines.slice(1, closing).join('\n'), body: lines.slice(closing + 1).join('\n'), present: true }
134
+ }
135
+
136
+ /**
137
+ * Count the spaces a line starts with. YAML indentation is spaces; a tab or a
138
+ * code point JS treats as blank is not this reader's to interpret.
139
+ * @param {string} line - one line of the block.
140
+ * @returns {number} the number of leading spaces.
141
+ */
142
+ function leadingSpaces(line) {
143
+ let count = 0
144
+ while (count < line.length && line.charCodeAt(count) === 32) count += 1
145
+ return count
146
+ }
147
+
148
+ /**
149
+ * Read one block scalar: same-indent lines only, chomping as YAML defines it.
150
+ * @returns the scalar and the index one past the block, or `undefined` when the
151
+ * block has a deeper-indented line, an interior blank line, or no content.
152
+ */
153
+ function readBlockScalar(lines, header, headerValue) {
154
+ const style = headerValue[0]
155
+ const modifier = headerValue.slice(1)
156
+ const first = lines[header + 1]
157
+ if (first === undefined) return undefined
158
+ // Indentation is spaces only: a tab is a YAML parse error there, so a
159
+ // tab-led or unindented first line leaves the block to `yaml`.
160
+ const indent = leadingSpaces(first)
161
+ if (indent === 0) return undefined
162
+
163
+ const content = []
164
+ let index = header + 1
165
+ let closed = false
166
+ for (; index < lines.length; index += 1) {
167
+ const line = lines[index]
168
+ if (leadingSpaces(line) < indent) { closed = true; break }
169
+ if (line.length === indent) {
170
+ // An interior blank line folds differently; only a trailing run may stay.
171
+ if (index + 1 < lines.length && lines[index + 1].length >= indent) return undefined
172
+ content.push('')
173
+ continue
174
+ }
175
+ if (leadingSpaces(line) > indent) return undefined
176
+ if (line.charCodeAt(indent) === 9) return undefined
177
+ content.push(line.slice(indent, line.length))
178
+ }
179
+ // A block that runs to the end of the document is closed there.
180
+ if (!closed && index >= lines.length) closed = true
181
+ if (!closed) return undefined
182
+ if (content.length === 0) return undefined
183
+
184
+ let last = content.length
185
+ while (last > 0 && content[last - 1] === '') last -= 1
186
+ const kept = content.slice(0, last)
187
+ if (kept.length === 0) return undefined
188
+
189
+ const body = style === '|' ? kept.join('\n') : kept.join(' ')
190
+ if (modifier.includes('-')) return { value: body, next: index }
191
+ return { value: `${body}\n`, next: index }
192
+ }
193
+
194
+ /**
195
+ * Read one scalar value.
196
+ * @returns the value, or `undefined` when it needs the real YAML parser.
197
+ */
198
+ function readScalar(raw) {
199
+ // A `#` needs YAML's comment rules (one only after whitespace) to read: refuse.
200
+ if (raw.includes('#')) return undefined
201
+ // `.inf` / `.nan` are YAML's special floats, in any case and either sign.
202
+ if (TYPED_SPECIAL.test(raw)) return undefined
203
+ if (raw === '' || raw === '~' || raw === 'null' || raw === 'Null' || raw === 'NULL') return { value: null }
204
+ if (raw === 'true' || raw === 'True' || raw === 'TRUE') return { value: true }
205
+ if (raw === 'false' || raw === 'False' || raw === 'FALSE') return { value: false }
206
+ if (/[0-9]/.test(raw)) {
207
+ // A digit anywhere means YAML may type this scalar; only the spellings this
208
+ // reader converts identically may pass, every other form goes to `yaml`.
209
+ if (SAFE_INT.test(raw) || SAFE_FLOAT.test(raw)) return { value: Number(raw) }
210
+ // Any other exponent spelling is YAML's floatExp, whose mantissa may be
211
+ // `.5`, `1.`, or zero-padded (`01e9`, `00e0`), none of which this reader
212
+ // converts, so it must not claim the block.
213
+ if (/[eE]/.test(raw)) return undefined
214
+ if (TYPED_INT.test(raw) || TYPED_FLOAT.test(raw) || /^[-+]/.test(raw) || raw.includes('_')) return undefined
215
+ }
216
+
217
+ const first = raw.charCodeAt(0)
218
+ if (first === 34) {
219
+ if (!SIMPLE_DOUBLE.test(raw.slice(1))) return undefined
220
+ const closing = raw.indexOf('"', 1)
221
+ if (closing === -1 || raw.slice(closing + 1).trim() !== '') return undefined
222
+ return { value: raw.slice(1, closing) }
223
+ }
224
+ if (first === 39) {
225
+ if (!SIMPLE_SINGLE.test(raw.slice(1))) return undefined
226
+ const closing = raw.indexOf("'", 1)
227
+ if (closing === -1 || raw.slice(closing + 1).trim() !== '') return undefined
228
+ return { value: raw.slice(1, closing) }
229
+ }
230
+ if (!PLAIN.test(raw)) return undefined
231
+ return { value: raw }
232
+ }
233
+
234
+ /**
235
+ * Read a flat block of `key: value` entries.
236
+ * @returns the mapping, or `undefined` when any line needs the real YAML parser.
237
+ */
238
+ function parseFlatBlock(block) {
239
+ // `trim`/`trimStart` in this reader would measure indentation through these
240
+ // and YAML would not: the real parser has to decide.
241
+ if (JS_ONLY_SPACE.test(block)) return undefined
242
+ const lines = toLines(block)
243
+ const data = {}
244
+ const seen = new Set()
245
+ for (let index = 0; index < lines.length; index += 1) {
246
+ const line = lines[index]
247
+ if (line === '' || line.charCodeAt(0) === 35) continue
248
+ const entry = ENTRY.exec(line)
249
+ if (entry === null) return undefined
250
+
251
+ const key = entry[1].trimEnd()
252
+ if (key !== entry[1]) return undefined
253
+ // Only a key that is provably its own string: that excludes quoted keys,
254
+ // flow keys, keys starting with an indicator (`@a`, `|a`, `[a]`), typed
255
+ // keys (`0x10`), and the words YAML resolves to `null`/`true`/`false`.
256
+ if (!SAFE_KEY.test(key)) return undefined
257
+ if (RESOLVED_KEY.test(key)) return undefined
258
+ // `yaml` rejects a duplicate key and a `__proto__` key does not survive a
259
+ // plain object assignment; both need the real parser.
260
+ if (seen.has(key) || UNSAFE_KEY.has(key)) return undefined
261
+ seen.add(key)
262
+ const raw = entry[2].replace(/^[ \t]+/, '').replace(/[ \t]+$/, '')
263
+ if (raw === '') {
264
+ // A value on following lines is a nested map or a sequence.
265
+ const next = lines[index + 1]
266
+ if (next !== undefined && next.trimStart() !== '' && next.charCodeAt(0) !== 35) return undefined
267
+ data[key] = null
268
+ continue
269
+ }
270
+ if (raw.charCodeAt(0) === 124 || raw.charCodeAt(0) === 62) {
271
+ // Only clip and strip chomping are read here; `+` keeps every trailing
272
+ // line break, which this line-based reader does not count.
273
+ if (BLOCK_HEADER.exec(raw) === null) return undefined
274
+ const scalar = readBlockScalar(lines, index, raw)
275
+ if (scalar === undefined) return undefined
276
+ data[key] = scalar.value
277
+ index = scalar.next - 1
278
+ continue
279
+ }
280
+ const scalar = readScalar(raw)
281
+ if (scalar === undefined) return undefined
282
+ data[key] = scalar.value
283
+ }
284
+ return data
285
+ }
286
+
287
+ /**
288
+ * Parse leading frontmatter from a markdown document, for the flat subset only.
289
+ * @param {string} source - full file contents.
290
+ * @returns {{ data: Record<string, unknown>, body: string }|undefined} the read,
291
+ * or `undefined` when the block needs the real YAML parser.
292
+ */
293
+ export function parseFrontmatter(source) {
294
+ // `\r` is a line break to YAML and not to this reader: hand the whole file,
295
+ // CRLF included, to the real parser instead of claiming the block.
296
+ if (String(source).includes('\r')) return undefined
297
+ const { block, body, present } = splitDocument(source)
298
+ if (!present) return { data: {}, body }
299
+ const data = parseFlatBlock(block)
300
+ if (data === undefined) return undefined
301
+ return { data, body }
302
+ }
303
+
304
+ /**
305
+ * Parse leading `---` frontmatter with `yaml`, the parser the harness's own
306
+ * filesystem skill provider uses. Frontmatter with no closing `---` is not
307
+ * frontmatter, and the body is then the whole source. The extraction is the
308
+ * pre-change reader's line-by-line split, so `yaml` sees the same bytes.
309
+ * @param {string} source - full file contents.
310
+ * @returns {Promise<{ data: Record<string, unknown>, body: string }>} the read.
311
+ */
312
+ export async function parseFrontmatterWithYaml(source) {
313
+ const { block, body, present } = splitDocument(source)
314
+ if (!present) return { data: {}, body }
315
+ const { parse: parseYaml } = await import('yaml')
316
+ const parsed = parseYaml(block)
317
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
318
+ // Empty or non-mapping frontmatter: no keys, but the body still loads.
319
+ return { data: {}, body }
320
+ }
321
+ return { data: parsed, body }
322
+ }
323
+ /**
324
+ * Read this package's one bundled skill. No directory walk: the package ships
325
+ * exactly one `SKILL.md`, and `get()` already loads a locator directly.
326
+ * @returns `{ candidates, complete }`; `complete: false` means re-read, not "no skills".
327
+ */
328
+ export async function discoverSkills(skillsDir, onWarn, signal) {
329
+ if (signal?.aborted) return { candidates: [], complete: false }
330
+ const skill = await readSkillFile(join(skillsDir, name, 'SKILL.md'), onWarn, name, signal)
331
+ // A failed read is an incomplete observation, so the registry re-reads
332
+ // instead of caching a broken skill as an empty catalog.
333
+ return skill === undefined ? { candidates: [], complete: false } : { candidates: [skill], complete: true }
334
+ }
335
+
336
+ /** Summary for one bundled skill. Every SKILL.md this package ships is invocable by both the model and the user. */
337
+ function summaryOf(skill) {
338
+ return {
339
+ path: skill.path,
340
+ name: skill.name,
341
+ description: skill.description,
342
+ invocation: { modelInvocable: true, userInvocable: true },
343
+ source: 'bundled',
344
+ provider: 'perf-review',
345
+ resourceBase: { kind: 'directory', path: skill.directory },
346
+ }
347
+ }
348
+
349
+ /** Build the provider the skill registry mounts. */
350
+ export function createSkillProvider({ skillsDir, onWarn }) {
351
+ return {
352
+ name: 'perf-review',
353
+ async list(lookup) {
354
+ const { candidates, complete } = await discoverSkills(skillsDir, onWarn, lookup?.signal)
355
+ const skills = candidates.map((skill) => ({ ...summaryOf(skill), rank: BUNDLED_SKILL_RANK, locator: skill.path }))
356
+ // Array shorthand on a complete read; an explicit observation otherwise, so the registry cannot cache a failed read as an empty catalog.
357
+ return complete ? skills : { candidates: skills, complete: false }
358
+ },
359
+ async get(candidate, lookup) {
360
+ if (typeof candidate.locator !== 'string') return undefined
361
+ // Read the locator directly: one file instead of a full re-discovery.
362
+ // The directory name is the fallback identity a name-less SKILL.md is
363
+ // listed under; the name check keeps a stale candidate (path reused by
364
+ // another skill) from loading under the wrong identity.
365
+ const skill = await readSkillFile(candidate.locator, onWarn, basename(dirname(candidate.locator)), lookup?.signal)
366
+ if (skill === undefined || skill.name !== candidate.name) return undefined
367
+ return { ...summaryOf(skill), content: skill.content }
368
+ },
369
+ }
370
+ }
371
+
372
+ /** Mount the plugin: skills provider only. `inject` above already made the fiber wait for `ctx.skills`. */
373
+ export function apply(ctx) {
374
+ const warn = (message) => {
375
+ if (ctx.logger?.warn) ctx.logger.warn(`[perf-review] ${message}`)
376
+ else console.warn(`[perf-review] ${message}`)
377
+ }
378
+ ctx.skills.registerProvider(() =>
379
+ createSkillProvider({ skillsDir: fileURLToPath(new URL('./skills', import.meta.url)), onWarn: warn }),
380
+ )
381
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Performance review",
4
+ "description": "Perceived speed first: profile before changing, and keep behavior the same."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "性能审查",
4
+ "description": "体感速度优先:先测量再改,并保持行为不变。"
5
+ }
6
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@maci0/dsh-perf-review",
3
+ "version": "0.10.0",
4
+ "publishConfig": {
5
+ "access": "public",
6
+ "registry": "https://registry.npmjs.org/"
7
+ },
8
+ "type": "module",
9
+ "main": "index.js",
10
+ "description": "Performance review skill for DeepSeek Harness: perceived speed above all: profile before changing, benchmark every claim, keep behavior identical.",
11
+ "license": "AGPL-3.0-only",
12
+ "exports": {
13
+ ".": "./index.js",
14
+ "./cordis.patch.yml": "./cordis.patch.yml",
15
+ "./locale/*.json": "./locale/*.json",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "dsh": {
19
+ "bundle": {
20
+ "patch": "./cordis.patch.yml"
21
+ },
22
+ "compatibility": {
23
+ "dsh": ">=0.2.0-rc.2 <0.3.0"
24
+ }
25
+ },
26
+ "files": [
27
+ "icon.svg",
28
+ "locale/*.json",
29
+ "index.js",
30
+ "skills",
31
+ "cordis.patch.yml",
32
+ "README.md",
33
+ "LICENSE",
34
+ "LICENSE-MIT",
35
+ "NOTICE"
36
+ ],
37
+ "scripts": {
38
+ "test": "bun test",
39
+ "test:node": "node --test tests/*.test.*"
40
+ },
41
+ "engines": {
42
+ "node": "^22.19.0 || >=24.0.0"
43
+ },
44
+ "dependencies": {
45
+ "@deepseek-ai/dsh-skill": "0.2.1-alpha.1",
46
+ "yaml": "2.9.1"
47
+ },
48
+ "devDependencies": {
49
+ "@deepseek-ai/cordis": "4.0.5-alpha.1",
50
+ "@deepseek-ai/dsh-scope": "0.2.1-alpha.1"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/maci0/dsh-perf-review.git"
55
+ },
56
+ "icon": "./icon.svg"
57
+ }