@noir-ai/skills 1.15.0 → 1.16.0-beta.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.
- package/README.md +1 -1
- package/builtin/noir-code-hygiene/SKILL.md +199 -0
- package/builtin/noir-code-hygiene/references/examples.md +248 -0
- package/dist/index.d.ts +87 -10
- package/dist/index.js +379 -37
- package/dist/index.js.map +1 -1
- package/evals/noir-code-hygiene/evals.json +30 -0
- package/integrations/noir-clickup/SKILL.md +7 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @noir-ai/skills
|
|
2
2
|
|
|
3
|
-
The native `noir-*` skill pack (
|
|
3
|
+
The native `noir-*` skill pack (27 builtins plus 1 integration) and a copy-and-validate compiler. `noir init` / `noir sync` emit it idempotently for hosts with a skill surface: Claude uses `.claude/skills/`; Cursor uses `.cursor/rules/*.mdc`. There is no plugin or marketplace.
|
|
4
4
|
|
|
5
5
|
Part of the **[Noir](https://github.com/agaaaptr/noir#readme)** toolkit — the discipline, context, and memory layer for any agentic CLI.
|
|
6
6
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: noir-code-hygiene
|
|
3
|
+
description: Use when writing or reviewing comments, docstrings, summaries, or documents — keep every line carrying something a reader can act on. Use when the user says "clean this up", "this reads like machine output", or asks for a comment sweep. Do NOT use for layout (indentation, quoting, line length); this is about what the text says.
|
|
4
|
+
metadata:
|
|
5
|
+
category: meta
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
license: MIT
|
|
8
|
+
compatibility: claude · agents-md · gemini · cursor · opencode
|
|
9
|
+
references:
|
|
10
|
+
- examples.md
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# noir-code-hygiene
|
|
14
|
+
|
|
15
|
+
Text that reads as machine-generated fails in a small number of ways, and every
|
|
16
|
+
one of them is a sentence a reader cannot act on. The ten defects below are
|
|
17
|
+
those ways, each as a Tell (what it looks like), a Why (what it costs a reader)
|
|
18
|
+
and a Fix (what to write instead). Four of them — restating, stale text, jargon,
|
|
19
|
+
unstated assumptions — are judgements a person makes; the rest are mechanical
|
|
20
|
+
enough that the quality gate checks them itself.
|
|
21
|
+
|
|
22
|
+
## When to use
|
|
23
|
+
|
|
24
|
+
- Writing or reviewing a comment, docstring, summary, or document that a reader will meet without the context its author had.
|
|
25
|
+
- Sweeping a file, a diff, or a report for text that reads as generated.
|
|
26
|
+
- The user says "clean this up", "this reads like machine output", or asks for a comment sweep.
|
|
27
|
+
- Reviewing your own output before handing it back: comments and summaries are where these defects concentrate.
|
|
28
|
+
- **Do NOT use:** for layout (indentation, quoting, line length), or as a substitute for a formatter or a type checker. This is about what the text says, not how it is drawn.
|
|
29
|
+
|
|
30
|
+
## Procedure
|
|
31
|
+
|
|
32
|
+
1. **Read the text as somebody who did not write it.** For each sentence, ask what a reader learns that the code, the diff, or the line above does not already say. A sentence that teaches nothing is the one to delete.
|
|
33
|
+
2. **Delete before rewriting.** Most defects end at deletion: a divider, a restated line, an empty label, a fact that is no longer true. Rewriting a comment that should not exist only makes the noise longer.
|
|
34
|
+
3. **Keep what the code cannot say.** The reason a value is what it is, the invariant a caller depends on, the condition that would break the order, the precondition. When only a restatement would be left, the comment is finished.
|
|
35
|
+
4. **Name things in the reader's terms.** Replace a codename, or a shorthand that resolves only against a planning document, with the mechanism it stood for. State the path, the command, or the precondition instead of assuming the reader knows it.
|
|
36
|
+
5. **Run the gate.** `noir skills lint` over a skill body, `noir doctor` over a repository. Fix every fail-tier finding; read a warn-tier one as a question about the line rather than a rule to satisfy.
|
|
37
|
+
|
|
38
|
+
## Tell / Why / Fix
|
|
39
|
+
|
|
40
|
+
### Decorative separators and banners
|
|
41
|
+
|
|
42
|
+
**Tell:** a line of punctuation used as a divider — a run of `=`, `-`, `*`, `_`,
|
|
43
|
+
or `~` characters, with or without a label inside it — or a heading whose only
|
|
44
|
+
job is to sit above another heading.
|
|
45
|
+
|
|
46
|
+
**Why:** the divider marks a section for whoever wrote the file, not for a reader. It carries nothing, and it is the fastest way to make a file read as machine-written.
|
|
47
|
+
|
|
48
|
+
**Fix:** delete it. If the label named something a reader needs, make it a heading in a document, or name it in the declaration the divider sat above.
|
|
49
|
+
|
|
50
|
+
### Restating the obvious
|
|
51
|
+
|
|
52
|
+
**Tell:** a comment that says what the line below does in the words the code already uses — a getter described as getting, a counter described as counting, a paragraph that repeats the sentence before it.
|
|
53
|
+
|
|
54
|
+
**Why:** it doubles the reading cost, and it is the first thing to go stale: the code changes and the echo of it does not. A reader who learns the comments are redundant skips the one that matters.
|
|
55
|
+
|
|
56
|
+
**Fix:** delete it. Keep a comment only when it carries something the code does not — why this value, what the caller relies on, which invariant holds.
|
|
57
|
+
|
|
58
|
+
### Workflow narration
|
|
59
|
+
|
|
60
|
+
**Tell:** a comment that walks a reader through the file in the order it runs, with ordinals as markers, or a summary that lists a function's statements in sequence.
|
|
61
|
+
|
|
62
|
+
**Why:** the code already gives the order, and it gives the true one. The narration goes stale the moment the order changes, and a reader takes it as a promise about behavior.
|
|
63
|
+
|
|
64
|
+
**Fix:** drop the markers. Keep only what the code cannot show: why this order, which invariant the order holds, or what breaks when it changes.
|
|
65
|
+
|
|
66
|
+
### Empty labels
|
|
67
|
+
|
|
68
|
+
**Tell:** a heading, a comment, or a field that names a category and stops — a lone label above nothing, a section with no content under it, a marker recorded with no owner and no reason.
|
|
69
|
+
|
|
70
|
+
**Why:** a label promises information. A reader pays to open it, finds nothing, and learns to skip the next one.
|
|
71
|
+
|
|
72
|
+
**Fix:** delete it, or say the thing: what is set up and by whom, what the note adds, what would retire the marker.
|
|
73
|
+
|
|
74
|
+
### Stale comments
|
|
75
|
+
|
|
76
|
+
**Tell:** a comment that described the code before an edit — a parameter that no longer exists, a step that no longer happens, a docstring for behavior that has changed, a stated limit the constant beside it contradicts.
|
|
77
|
+
|
|
78
|
+
**Why:** worse than no comment at all. A wrong comment is trusted, and the reader who acts on it investigates the wrong thing.
|
|
79
|
+
|
|
80
|
+
**Fix:** when the code changes, read the comment above it in the same edit. Delete what no longer holds, rewrite what changed meaning. A comment is part of the change, not a leftover from it.
|
|
81
|
+
|
|
82
|
+
### Decorative icons and emoji
|
|
83
|
+
|
|
84
|
+
**Tell:** an emoji or pictograph used as a bullet, as a heading prefix, as a status flourish in a comment, or as a symbol standing in for a word — a check mark for done, a warning sign for careful.
|
|
85
|
+
|
|
86
|
+
**Why:** the glyph takes attention and returns none of it, it renders differently or not at all across terminals and fonts, and it announces the text as generated. In output a screen reader reads aloud, it is noise in the middle of a sentence.
|
|
87
|
+
|
|
88
|
+
**Fix:** remove it and let the words carry the meaning. A glyph that is part of a program's own output — a status badge, a CLI label — belongs inside the string that prints it, not in the text around the code.
|
|
89
|
+
|
|
90
|
+
### Unexpected characters from another script
|
|
91
|
+
|
|
92
|
+
**Tell:** a character from a writing system this project does not write in — Han,
|
|
93
|
+
Kana, Hangul, Cyrillic, Greek, Arabic, Hebrew, Thai, Devanagari — or a fullwidth
|
|
94
|
+
form, a zero-width space, a byte-order mark, a bidirectional override, or the
|
|
95
|
+
replacement character a bad decode leaves behind. It turns up inside a word or a
|
|
96
|
+
string, where no author would have typed it.
|
|
97
|
+
|
|
98
|
+
**Why:** a model can leak a token from another script into generated text, and
|
|
99
|
+
bytes that were decoded wrongly arrive as an invisible character or a
|
|
100
|
+
replacement mark. Either way the reader is shown something nobody meant to
|
|
101
|
+
write, and inside a string it can change what the code does.
|
|
102
|
+
|
|
103
|
+
**Fix:** rewrite the text in the project's language and delete the invisible
|
|
104
|
+
character; a deliberate fixture — a test that measures how a wide glyph is laid
|
|
105
|
+
out, say — states its own exemption with the marker instead of the text keeping
|
|
106
|
+
the character. `noir doctor`'s hygiene check reports these as failures.
|
|
107
|
+
|
|
108
|
+
### Verbosity
|
|
109
|
+
|
|
110
|
+
**Tell:** three sentences where one does; a preamble that restates the request; a summary of a summary; a dozen comment lines above a four-line function; a document that explains the same decision twice.
|
|
111
|
+
|
|
112
|
+
**Why:** every extra sentence is a cost the reader pays before reaching the one that matters. Length also hides the defects above, because prose that repeats itself is prose nobody re-reads closely.
|
|
113
|
+
|
|
114
|
+
**Fix:** cut to what the reader cannot get elsewhere, and move reference detail into the document that owns it, linked, instead of inlined.
|
|
115
|
+
|
|
116
|
+
### Internal jargon
|
|
117
|
+
|
|
118
|
+
**Tell:** a label that resolves only against a planning document — a task code, a milestone codename, a bare section citation, an abbreviation defined once in a note nobody keeps.
|
|
119
|
+
|
|
120
|
+
**Why:** no reader of this file can resolve it, and it ties the source to a document that will be deleted. The code outlives the plan, so the shorthand ends up naming nothing at all.
|
|
121
|
+
|
|
122
|
+
**Fix:** name the mechanism, the behavior, or the condition in self-contained words. When a code is the only handle that exists, give the fact it stood for beside it.
|
|
123
|
+
|
|
124
|
+
### Unstated assumptions
|
|
125
|
+
|
|
126
|
+
**Tell:** an instruction or a comment that works only if the reader already knows something the text does not say — the path, the file that has to exist first, the command, the environment, or the choice made silently between two options.
|
|
127
|
+
|
|
128
|
+
**Why:** the reader either guesses, and a wrong guess surfaces as a bug somewhere else, or stops to ask. Both cost more than the sentence that would have prevented it.
|
|
129
|
+
|
|
130
|
+
**Fix:** state it — the path, the command, the precondition, the reason for the choice. When the answer changes what the text should say, ask rather than pick one silently and leave the reader to find out.
|
|
131
|
+
|
|
132
|
+
## Worked example
|
|
133
|
+
|
|
134
|
+
The defects a pattern can catch are the easy half. This pair is the other half:
|
|
135
|
+
neither snippet trips a rule, and the first one still wastes a reader's time.
|
|
136
|
+
|
|
137
|
+
Before:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
/** Fetches the user. */
|
|
141
|
+
// NOTE: 30 second timeout because the gateway is slow.
|
|
142
|
+
// Added during the checkout rewrite — see the planning note for context.
|
|
143
|
+
// Gets the user by id from the client.
|
|
144
|
+
export async function fetchUser(id: string): Promise<User> {
|
|
145
|
+
return client.get(`/users/${id}`, { timeoutMs: 5000 });
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
After:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
/**
|
|
153
|
+
* Loads a user. A healthy gateway answers in well under a second, so five
|
|
154
|
+
* seconds is the point past which the request is a hang rather than a slow
|
|
155
|
+
* read — the caller hears about it instead of waiting on nothing.
|
|
156
|
+
*/
|
|
157
|
+
export async function fetchUser(id: string): Promise<User> {
|
|
158
|
+
return client.get(`/users/${id}`, { timeoutMs: 5000 });
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The behavior is identical. What changed: the name restated as a sentence is
|
|
163
|
+
gone, the timeout figure that contradicted the constant beside it is gone, and
|
|
164
|
+
the two facts a reader cannot get from the expression — why the timeout exists
|
|
165
|
+
and why it is this value — are all that is left. The codename went with them.
|
|
166
|
+
|
|
167
|
+
## Enforcement
|
|
168
|
+
|
|
169
|
+
Two commands run the same rules, so this guidance and the gate agree:
|
|
170
|
+
|
|
171
|
+
- `noir skills lint` validates each shipped skill body. A fail-tier pattern is an error and exits non-zero; a warn-tier pattern is a warning and does not.
|
|
172
|
+
- `noir doctor` scans the repository's own text — the root documents, `docs/` outside the planning corpus, `.claude/skills`, each package's `src` and `test` trees, and `scripts/` — reporting fail-tier findings in a failing row and warn-tier findings in a warning row. It exits non-zero whenever a fail-tier finding is present, so a repository's continuous integration can call it as its gate.
|
|
173
|
+
|
|
174
|
+
The rules themselves are one table in the skills package, and each entry carries its tier, the reason the shape is noise, and the fix. Read the table when a line's status is unclear rather than working from memory.
|
|
175
|
+
|
|
176
|
+
**The exemption marker.** Some files have to name what the rules forbid: the rule table, its fixtures, and the examples file this skill ships. Such a file states its own exemption with the marker the rules honour — the text `noir-hygiene: exempt` alone on a line above its first finding, written as a source comment in code and as an HTML comment in a document. The marker is a statement about the whole file, not a way to silence one line, so it belongs only where the prohibited shape is the subject. A file that carries it to keep a defect is worse off than the defect: the gate stops reporting it, and the reason it was written is now invisible.
|
|
177
|
+
|
|
178
|
+
## Reference
|
|
179
|
+
|
|
180
|
+
[examples.md](references/examples.md) carries a worked before/after pair for every defect above, the mechanical shapes the gate catches included — a divider, numbered narration, a decorative emoji, a marker nobody can act on.
|
|
181
|
+
|
|
182
|
+
## Verification
|
|
183
|
+
|
|
184
|
+
- [ ] Every sentence kept carries something the code, the diff, or the line above does not.
|
|
185
|
+
- [ ] No divider, no restatement, no ordinal marker, no empty label survives the pass.
|
|
186
|
+
- [ ] Every fact stated beside the code is true of the code as it stands now.
|
|
187
|
+
- [ ] Every codename or shorthand is replaced by the mechanism it stood for.
|
|
188
|
+
- [ ] The path, the command, and the precondition are stated rather than assumed.
|
|
189
|
+
- [ ] `noir skills lint` (skill body) or `noir doctor` (repository) reports no fail-tier finding.
|
|
190
|
+
|
|
191
|
+
## Notes
|
|
192
|
+
|
|
193
|
+
- The tiers differ in what to do with a finding, not in whether the shape is real. A fail-tier pattern is never the right thing to write, so it is removed. A warn-tier one is a mechanical threshold too, but the shape is sometimes the right choice — a marker is sometimes the right note, a block sometimes the right length — so the tier asks a reviewer to look at the line rather than forbidding it.
|
|
194
|
+
- Deleting is the most common fix. A file gets clearer by getting shorter, and a shorter file has fewer places to be wrong.
|
|
195
|
+
- The standard covers what an agent reports back, not only what it writes into a file: a summary that narrates its own steps, or that repeats the request before answering it, is the same defect in a different medium.
|
|
196
|
+
|
|
197
|
+
## When done → next skill
|
|
198
|
+
|
|
199
|
+
→ `noir-verifying` to confirm the sweep left nothing broken. Or `noir-writing-skills` when the text being cleaned is a skill body.
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
<!-- Worked examples for the noir-code-hygiene skill.
|
|
2
|
+
|
|
3
|
+
This file exists to show the shapes the rules forbid, so it must contain
|
|
4
|
+
them. It therefore declares the exemption the rules honour with the marker
|
|
5
|
+
below, alone on its line and above the first finding.
|
|
6
|
+
|
|
7
|
+
The pairs follow the same shape as the skill: what arrives first, then the
|
|
8
|
+
same thing after the pass. The code is the same both times; what changes is
|
|
9
|
+
what the text says about it. -->
|
|
10
|
+
<!-- noir-hygiene: exempt -->
|
|
11
|
+
|
|
12
|
+
# Worked examples
|
|
13
|
+
|
|
14
|
+
One pair per defect. The first snippet in each pair is what a draft or a
|
|
15
|
+
generator tends to produce; the second is the same code after the pass the
|
|
16
|
+
skill describes. Read the pair for the defect you met rather than the whole
|
|
17
|
+
file.
|
|
18
|
+
|
|
19
|
+
## Decorative separators and banners
|
|
20
|
+
|
|
21
|
+
Before:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// ============================================================
|
|
25
|
+
// ====================== Users ===============================
|
|
26
|
+
// ============================================================
|
|
27
|
+
|
|
28
|
+
/** Loads a user. The timeout bounds a hung request, not a healthy one. */
|
|
29
|
+
export async function fetchUser(id: string): Promise<User> {
|
|
30
|
+
return client.get(`/users/${id}`, { timeoutMs: 5000 });
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
After:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
/** Loads a user. The timeout bounds a hung request, not a healthy one. */
|
|
38
|
+
export async function fetchUser(id: string): Promise<User> {
|
|
39
|
+
return client.get(`/users/${id}`, { timeoutMs: 5000 });
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Workflow narration
|
|
44
|
+
|
|
45
|
+
Before:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
async function sync(): Promise<void> {
|
|
49
|
+
// Step 1: read the config.
|
|
50
|
+
const cfg = await readConfig();
|
|
51
|
+
// Step 2: resolve the profile named on the command line.
|
|
52
|
+
const profile = pick(cfg, name);
|
|
53
|
+
// Step 3: start the daemon.
|
|
54
|
+
await start(profile);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
After:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
async function sync(): Promise<void> {
|
|
62
|
+
// The profile is resolved against the config's profile table, so the config
|
|
63
|
+
// has to be read first — a profile name is not self-describing.
|
|
64
|
+
const cfg = await readConfig();
|
|
65
|
+
const profile = pick(cfg, name);
|
|
66
|
+
await start(profile);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Restating the obvious
|
|
71
|
+
|
|
72
|
+
Before:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// Increments the retry counter.
|
|
76
|
+
retries++;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
After:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// Per request, not per host: a retry storm against one endpoint must not
|
|
83
|
+
// exhaust the budget the other endpoints share.
|
|
84
|
+
retries++;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Empty labels
|
|
88
|
+
|
|
89
|
+
Before:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// TODO: handle the timeout case.
|
|
93
|
+
export async function fetchUser(id: string): Promise<User> {
|
|
94
|
+
return client.get(`/users/${id}`, { timeoutMs: 5000 });
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
After:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// The client rejects rather than returning a partial page once the request
|
|
102
|
+
// budget is gone, so callers must handle a rejected promise. Revisit once the
|
|
103
|
+
// export job needs partial results.
|
|
104
|
+
export async function fetchUser(id: string): Promise<User> {
|
|
105
|
+
return client.get(`/users/${id}`, { timeoutMs: 5000 });
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Stale comments
|
|
110
|
+
|
|
111
|
+
Before:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// Backs off for 30 seconds between retries, matching the gateway's window.
|
|
115
|
+
const BACKOFF_MS = 250;
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
After:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// The gateway rate-limits per second, so a longer backoff only delays the
|
|
122
|
+
// request the user is already waiting on.
|
|
123
|
+
const BACKOFF_MS = 250;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Decorative icons and emoji
|
|
127
|
+
|
|
128
|
+
Before:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// ✅ The second read is served from cache.
|
|
132
|
+
// 🚀 Ready to ship.
|
|
133
|
+
export function read(id: string): User | undefined {
|
|
134
|
+
return cache.get(id);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
After:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// Read twice and the second read makes no request, so a caller may treat a
|
|
142
|
+
// hit as free.
|
|
143
|
+
export function read(id: string): User | undefined {
|
|
144
|
+
return cache.get(id);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Unexpected characters from another script
|
|
149
|
+
|
|
150
|
+
Before:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// The retry 次数 is capped so a flapping upstream cannot hold the request open.
|
|
154
|
+
const MAX_RETRIES = 3;
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
After:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// The retry count is capped so a flapping upstream cannot hold the request open.
|
|
161
|
+
const MAX_RETRIES = 3;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Verbosity
|
|
165
|
+
|
|
166
|
+
Before:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
/**
|
|
170
|
+
* Loads a user.
|
|
171
|
+
* This function takes an id and returns the user it names.
|
|
172
|
+
* It calls the client, which performs the HTTP request.
|
|
173
|
+
* If the request fails, the error is propagated to the caller.
|
|
174
|
+
* The caller decides what to do with the error.
|
|
175
|
+
* The id is validated before the request is made.
|
|
176
|
+
* An invalid id therefore never reaches the client.
|
|
177
|
+
* The validation is a format check only.
|
|
178
|
+
* It does not confirm that the user exists.
|
|
179
|
+
* A missing user produces the client's own error.
|
|
180
|
+
* The result is not cached.
|
|
181
|
+
* A second call repeats the request.
|
|
182
|
+
*/
|
|
183
|
+
export async function fetchUser(id: string): Promise<User> {
|
|
184
|
+
assertId(id);
|
|
185
|
+
return client.get(`/users/${id}`);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
After:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
/**
|
|
193
|
+
* Loads a user. The id is checked for shape, not for existence, so a
|
|
194
|
+
* well-formed id that names nobody fails with the client's own error.
|
|
195
|
+
*/
|
|
196
|
+
export async function fetchUser(id: string): Promise<User> {
|
|
197
|
+
assertId(id);
|
|
198
|
+
return client.get(`/users/${id}`);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Internal jargon
|
|
203
|
+
|
|
204
|
+
Before:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// Part of the checkout rework — see the planning note, section 4.
|
|
208
|
+
const flags = parseFlags(raw);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
After:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
// Flags arrive as a comma-separated list from the runner's environment: a flag
|
|
215
|
+
// set for a preview deploy is not set on the production runner.
|
|
216
|
+
const flags = parseFlags(raw);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Unstated assumptions
|
|
220
|
+
|
|
221
|
+
Before:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Restart it after the migration.
|
|
225
|
+
sudo systemctl restart api
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
After:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# Run on the application host, not the database host. The API reads the schema
|
|
232
|
+
# at boot and keeps it, so it serves the old one until it restarts.
|
|
233
|
+
sudo systemctl restart api
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Which tier each shape lands in
|
|
237
|
+
|
|
238
|
+
The divider, the ordinal narration, the decorative emoji and a character from
|
|
239
|
+
another script are mechanical, and the gate fails on them — through
|
|
240
|
+
`noir skills lint` for a skill body and through `noir doctor` for a repository.
|
|
241
|
+
The marker with no owner and the long comment block are warnings instead: a
|
|
242
|
+
marker is sometimes the right note and a block is sometimes the right length, so
|
|
243
|
+
those rules ask whether the line earns its place rather than forbidding the
|
|
244
|
+
shape.
|
|
245
|
+
|
|
246
|
+
Restating, stale text, jargon and unstated assumptions have no pattern behind
|
|
247
|
+
them at all, so only a reader catches them. That is why the skill covers them
|
|
248
|
+
beside the rules the gate can see: the pass is one pass, not two.
|
package/dist/index.d.ts
CHANGED
|
@@ -245,6 +245,62 @@ declare function discoverAll(opts?: {
|
|
|
245
245
|
integrations: IntegrationSkill[];
|
|
246
246
|
};
|
|
247
247
|
|
|
248
|
+
type HygieneTier = 'fail' | 'warn';
|
|
249
|
+
/** The two kinds of text a rule can be written for: source, or prose. */
|
|
250
|
+
type HygieneKind = 'code' | 'markdown';
|
|
251
|
+
interface HygieneRule {
|
|
252
|
+
id: string;
|
|
253
|
+
tier: HygieneTier;
|
|
254
|
+
/** Anchored: a pattern that matches ordinary writing is the failure this
|
|
255
|
+
* rule source exists to prevent, so every pattern below is reviewed against
|
|
256
|
+
* plain prose and against string literals. */
|
|
257
|
+
pattern: RegExp;
|
|
258
|
+
rationale: string;
|
|
259
|
+
fix: string;
|
|
260
|
+
appliesTo: HygieneKind | 'both';
|
|
261
|
+
}
|
|
262
|
+
interface HygieneFinding {
|
|
263
|
+
/** The id of the rule that matched, exactly as `HYGIENE_RULES` declares it. */
|
|
264
|
+
id: string;
|
|
265
|
+
tier: HygieneTier;
|
|
266
|
+
/** 1-based number of the line the pattern matched. */
|
|
267
|
+
line: number;
|
|
268
|
+
/** The matched line, trimmed of surrounding whitespace. */
|
|
269
|
+
text: string;
|
|
270
|
+
rationale: string;
|
|
271
|
+
fix: string;
|
|
272
|
+
}
|
|
273
|
+
/** The line that exempts a file from every rule. A file that must keep what the
|
|
274
|
+
* rules flag — the rule source, the token table, a document that states the
|
|
275
|
+
* ban — carries the line matching its kind near the top. */
|
|
276
|
+
declare const HYGIENE_EXEMPT_MARKERS: Readonly<Record<HygieneKind, string>>;
|
|
277
|
+
/** A run of this many consecutive comment lines counts as a comment block. A
|
|
278
|
+
* blank comment line is a paragraph break and a divider is reported on its
|
|
279
|
+
* own, so neither one extends a block. */
|
|
280
|
+
declare const MAX_COMMENT_BLOCK_LINES = 12;
|
|
281
|
+
/** Every rule the gate enforces: the deterministic patterns first, then the
|
|
282
|
+
* judgement calls, then the tokens this project must never ship again. */
|
|
283
|
+
declare const HYGIENE_RULES: readonly HygieneRule[];
|
|
284
|
+
/** Checks `text` against every rule that applies to `kind`, and returns the
|
|
285
|
+
* findings in reading order: one per rule per line.
|
|
286
|
+
*
|
|
287
|
+
* A file is exempt from every rule when it carries the marker
|
|
288
|
+
* `HYGIENE_EXEMPT_MARKERS` declares for its kind on a line above the first
|
|
289
|
+
* finding — the rule source itself, the token table, and any guidance or
|
|
290
|
+
* fixture file that has to name what the rules forbid. Such a file states its
|
|
291
|
+
* own exemption, so no consumer keeps a list of paths to skip. A marker placed
|
|
292
|
+
* at or below the first finding exempts nothing.
|
|
293
|
+
*
|
|
294
|
+
* Pure and deterministic: each pattern is recompiled per call, so no
|
|
295
|
+
* `lastIndex` state escapes and the same text always gives the same findings. */
|
|
296
|
+
declare function checkHygiene(text: string, kind: HygieneKind): HygieneFinding[];
|
|
297
|
+
|
|
298
|
+
/** The SKILL.md body — the markdown after the YAML frontmatter block. The
|
|
299
|
+
* frontmatter is metadata the host reads on its own; the body is the playbook
|
|
300
|
+
* a reader loads. The validator and the two body-structure lint rules both
|
|
301
|
+
* measure this string, so it lives here (single source of truth) rather than
|
|
302
|
+
* in compiler.ts. */
|
|
303
|
+
declare function bodyOf(md: string): string;
|
|
248
304
|
/** The max body length the canon recommends (Anthropic: "under 500 lines").
|
|
249
305
|
* SKILL.md is a navigator, not a repository — split to references/ past this. */
|
|
250
306
|
declare const MAX_BODY_LINES = 500;
|
|
@@ -289,13 +345,13 @@ declare function looksLikeWhenDescription(description: string): boolean;
|
|
|
289
345
|
declare function lintWarnings(skill: BuiltinSkill): string[];
|
|
290
346
|
|
|
291
347
|
declare function parseFrontmatter(md: string): SkillFrontmatter;
|
|
292
|
-
declare function bodyOf(md: string): string;
|
|
293
348
|
declare function validateSkill(skill: BuiltinSkill): ValidationResult;
|
|
294
349
|
/**
|
|
295
350
|
* `lintSkill` — the soft quality gate. Errors = `validateSkill` errors (a
|
|
296
|
-
* skill that fails validation is broken
|
|
297
|
-
*
|
|
298
|
-
*
|
|
351
|
+
* skill that fails validation is broken, the hygiene fail tier included);
|
|
352
|
+
* warnings = the hygiene warn tier plus the `quality.ts` style rules (thin
|
|
353
|
+
* body, no examples, first-person narration, …). A skill can validate clean
|
|
354
|
+
* yet still carry lint warnings the author should resolve.
|
|
299
355
|
*/
|
|
300
356
|
declare function lintSkill(skill: BuiltinSkill): {
|
|
301
357
|
name: string;
|
|
@@ -393,6 +449,15 @@ interface EvalSuite {
|
|
|
393
449
|
skill_name: string;
|
|
394
450
|
evals: SkillEval[];
|
|
395
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Candidate answers keyed by eval id — what a model (or a stub standing in for
|
|
454
|
+
* one) actually produced. An id present here is the text the assertions run
|
|
455
|
+
* against. An id absent is a failure for that eval: the run recorded no answer,
|
|
456
|
+
* and asserting the golden `expected_output` instead would report a pass the
|
|
457
|
+
* harness cannot vouch for. Pass no source at all to assert `expected_output`,
|
|
458
|
+
* which is how suites that predate candidate output keep working.
|
|
459
|
+
*/
|
|
460
|
+
type CandidateOutputs = Record<string, string>;
|
|
396
461
|
/** Parse + validate a raw `evals.json` payload. Throws on malformed shape. */
|
|
397
462
|
declare function parseEvalSuite(raw: unknown): EvalSuite;
|
|
398
463
|
/** Run a list of assertions against `output`. Returns pass + per-assertion failures. */
|
|
@@ -408,11 +473,13 @@ declare const EVALS_DIR: string;
|
|
|
408
473
|
* bug, not a skip).
|
|
409
474
|
*/
|
|
410
475
|
declare function loadEvalSuites(dir?: string): EvalSuite[];
|
|
411
|
-
/** Evaluate one suite: for each eval, check its assertions against
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
|
|
476
|
+
/** Evaluate one suite: for each eval, check its assertions against the
|
|
477
|
+
* candidate output supplied for that eval. With no candidate source at all,
|
|
478
|
+
* each eval is checked against `expected_output` (the directive the skill
|
|
479
|
+
* should produce); with a source that omits an eval, that eval fails as
|
|
480
|
+
* unrecorded. Returns pass/fail per eval with the assertion failures. This is
|
|
481
|
+
* the offline core the vitest runner drives. */
|
|
482
|
+
declare function evaluateSuite(suite: EvalSuite, candidates?: CandidateOutputs): Array<{
|
|
416
483
|
id: string;
|
|
417
484
|
pass: boolean;
|
|
418
485
|
failures: string[];
|
|
@@ -552,6 +619,16 @@ declare function buildRegistry(): SkillRegistryEntry[];
|
|
|
552
619
|
/** Convenience: registry filtered to a single category (CLI grouping). */
|
|
553
620
|
declare function registryByCategory(category: string): SkillRegistryEntry[];
|
|
554
621
|
|
|
622
|
+
/** The forbidden tokens, in the order they are declared. */
|
|
555
623
|
declare const FORBIDDEN_RESIDUE: readonly string[];
|
|
624
|
+
/** One fail-tier rule per forbidden token. A token matches only when it stands
|
|
625
|
+
* alone — neither side is a word character or a hyphen — so `noir-workflow`
|
|
626
|
+
* does not fire inside a longer identifier such as `noir-workflow-engine-`,
|
|
627
|
+
* which names this repository's own workflow engine rather than the removed
|
|
628
|
+
* plugin. It still fires on `plugins/noir-workflow/` and `noir-workflow.mode`,
|
|
629
|
+
* which name the plugin itself; a token that contains another
|
|
630
|
+
* (`noir-workflow.mode`) is reported by both rules, so the narrower token is
|
|
631
|
+
* never the only thing matching a line. */
|
|
632
|
+
declare const RESIDUE_RULES: readonly HygieneRule[];
|
|
556
633
|
|
|
557
|
-
export { BUILTIN_DIR, type BuiltinReference, type BuiltinSkill, type CompileTarget, type CompiledIntegration, type CompiledSkill, EVALS_DIR, type EmitSummary, type EmittedFile, type EvalAssertion, type EvalSuite, FORBIDDEN_RESIDUE, INTEGRATIONS_DIR, IntegrationAuthSchema, type IntegrationDeclaration$1 as IntegrationDeclaration, IntegrationDeclarationSchema, IntegrationMcpSchema, IntegrationSddSchema, type IntegrationSkill, MAX_BODY_LINES, MIN_FULL_BODY_LINES, NOIR_NAMESPACE, type SkillConflict, type SkillEval, type SkillFrontmatter, type SkillRegistryEntry, type ValidationResult, bodyOf, buildRegistry, chainedReferences, compileIntegration, compileSkill, discoverAll, discoverBuiltin, discoverIntegrations, emitSkillsToDir, evaluateSuite, isWhatWhenDescription, lintSkill, lintWarnings, loadEvalSuites, looksLikeWhenDescription, missingSections, parseEvalSuite, parseFrontmatter, parseIntegration, registryByCategory, runAssertions, runtimeEmitsHostMcp, validateIntegration, validateSkill, withinLineBudget };
|
|
634
|
+
export { BUILTIN_DIR, type BuiltinReference, type BuiltinSkill, type CandidateOutputs, type CompileTarget, type CompiledIntegration, type CompiledSkill, EVALS_DIR, type EmitSummary, type EmittedFile, type EvalAssertion, type EvalSuite, FORBIDDEN_RESIDUE, HYGIENE_EXEMPT_MARKERS, HYGIENE_RULES, type HygieneFinding, type HygieneKind, type HygieneRule, type HygieneTier, INTEGRATIONS_DIR, IntegrationAuthSchema, type IntegrationDeclaration$1 as IntegrationDeclaration, IntegrationDeclarationSchema, IntegrationMcpSchema, IntegrationSddSchema, type IntegrationSkill, MAX_BODY_LINES, MAX_COMMENT_BLOCK_LINES, MIN_FULL_BODY_LINES, NOIR_NAMESPACE, RESIDUE_RULES, type SkillConflict, type SkillEval, type SkillFrontmatter, type SkillRegistryEntry, type ValidationResult, bodyOf, buildRegistry, chainedReferences, checkHygiene, compileIntegration, compileSkill, discoverAll, discoverBuiltin, discoverIntegrations, emitSkillsToDir, evaluateSuite, isWhatWhenDescription, lintSkill, lintWarnings, loadEvalSuites, looksLikeWhenDescription, missingSections, parseEvalSuite, parseFrontmatter, parseIntegration, registryByCategory, runAssertions, runtimeEmitsHostMcp, validateIntegration, validateSkill, withinLineBudget };
|