@noir-ai/skills 1.15.0 → 1.16.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/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/dist/index.js
CHANGED
|
@@ -108,6 +108,306 @@ function discoverAll(opts = {}) {
|
|
|
108
108
|
|
|
109
109
|
// src/quality.ts
|
|
110
110
|
import { ARTIFACT_TYPES } from "@noir-ai/core";
|
|
111
|
+
|
|
112
|
+
// src/residue.ts
|
|
113
|
+
var RESIDUE = [
|
|
114
|
+
{
|
|
115
|
+
token: "workflow/<task",
|
|
116
|
+
why: "Names a state file of the removed workflow plugin, which nothing in this repository reads."
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
token: "noir-workflow.mode",
|
|
120
|
+
why: "Names the mode flag of the removed workflow plugin, which nothing in this repository reads."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
token: "noir-workflow",
|
|
124
|
+
why: "Names the removed workflow plugin as a plugin or a path, so a reader following it finds nothing."
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
token: "@uiigateway",
|
|
128
|
+
why: "Names a package from the project this one was extracted from, which no consumer can resolve."
|
|
129
|
+
},
|
|
130
|
+
// 'ClickUp'/'clickup' were forbidden while the predecessor plugin was being
|
|
131
|
+
// ported, because the ClickUp REST precedent lived inside it. ClickUp now
|
|
132
|
+
// ships as a first-class integration, so the token names a legitimate
|
|
133
|
+
// subject and is deliberately absent from this list.
|
|
134
|
+
{
|
|
135
|
+
token: "<EXTREMELY-IMPORTANT",
|
|
136
|
+
why: "Copied rhetorical markup from the upstream tool, which carries no instruction a reader can follow."
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
token: "SUBAGENT-STOP",
|
|
140
|
+
why: "Copied rhetorical markup from the upstream tool, which carries no instruction a reader can follow."
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
token: "plugins/noir-workflow",
|
|
144
|
+
why: "Names the removed plugin path, which no longer exists in this repository."
|
|
145
|
+
}
|
|
146
|
+
];
|
|
147
|
+
var FORBIDDEN_RESIDUE = RESIDUE.map((entry) => entry.token);
|
|
148
|
+
var RESIDUE_RULES = RESIDUE.map((entry) => ({
|
|
149
|
+
id: `residue-${entry.token.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase()}`,
|
|
150
|
+
tier: "fail",
|
|
151
|
+
pattern: new RegExp(`(?<![\\w-])${escapeForRegExp(entry.token)}(?![\\w-])`),
|
|
152
|
+
rationale: entry.why,
|
|
153
|
+
fix: "Remove the reference. If it named a mechanism, name the mechanism that replaced it.",
|
|
154
|
+
appliesTo: "both"
|
|
155
|
+
}));
|
|
156
|
+
function escapeForRegExp(literal2) {
|
|
157
|
+
return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/hygiene.ts
|
|
161
|
+
var HYGIENE_EXEMPT_MARKERS = {
|
|
162
|
+
code: "// noir-hygiene: exempt",
|
|
163
|
+
markdown: "<!-- noir-hygiene: exempt -->"
|
|
164
|
+
};
|
|
165
|
+
var MAX_COMMENT_BLOCK_LINES = 12;
|
|
166
|
+
var MARKER = String.raw`(?:^[ \t]*|[ \t])(?:\/\/|#|\/\*|\*)`;
|
|
167
|
+
var VARIATION_SELECTOR = String.fromCharCode(65039);
|
|
168
|
+
var PICTOGRAPH = String.raw`[\u{1F300}-\u{1FAFF}]`;
|
|
169
|
+
var DECORATION_EMOJI = `[\u26A0\u2705\u{1F389}\u2728\u{1F525}\u{1F680}\u{1F9E0}\u{1F4A1}\u{1F527}\u{1F4CC}]${VARIATION_SELECTOR}?`;
|
|
170
|
+
var BACKTICK = String.fromCharCode(96);
|
|
171
|
+
var QUOTE_OR_BACKTICK = `[${BACKTICK}'"]`;
|
|
172
|
+
var MARKER_WORDS = ["TODO", "FIXME"];
|
|
173
|
+
var REASON_WORDS = [
|
|
174
|
+
"because",
|
|
175
|
+
"since",
|
|
176
|
+
"until",
|
|
177
|
+
"once",
|
|
178
|
+
"when",
|
|
179
|
+
"unless",
|
|
180
|
+
"blocked",
|
|
181
|
+
"otherwise",
|
|
182
|
+
"revisit"
|
|
183
|
+
];
|
|
184
|
+
var OWNER = String.raw`(?:@[A-Za-z0-9_-]|\([A-Za-z][A-Za-z0-9_-]*\)|#\d+)`;
|
|
185
|
+
var DECORATIVE_BANNER = {
|
|
186
|
+
id: "decorative-banner",
|
|
187
|
+
tier: "fail",
|
|
188
|
+
pattern: new RegExp(String.raw`${MARKER}[ \t]*[-=*_~]{4,}`, "m"),
|
|
189
|
+
rationale: "A divider drawn in punctuation marks a section for the person writing the file, not for the reader: it carries no information, and it makes the file read as machine output.",
|
|
190
|
+
fix: "Delete the divider. If the label names a section a reader needs, make it a heading or name it in the declaration below.",
|
|
191
|
+
appliesTo: "both"
|
|
192
|
+
};
|
|
193
|
+
var WORKFLOW_NARRATION = {
|
|
194
|
+
id: "workflow-narration",
|
|
195
|
+
tier: "fail",
|
|
196
|
+
pattern: new RegExp(
|
|
197
|
+
String.raw`${MARKER}[ \t]*(?:step[ \t]*\d+[ \t]*[:.)]|(?:first|second|third|then|next|finally|lastly)[ \t]*[,:](?![ \t]*(?:which|that|who)\b)(?![^\n]*\b(?:so|because|since)\b))`,
|
|
198
|
+
"im"
|
|
199
|
+
),
|
|
200
|
+
rationale: "Numbered narration restates what the code already says in the order it already says it, and it goes stale the moment that order changes.",
|
|
201
|
+
fix: "Drop the step markers. Keep only what the code cannot show: why this order, which invariant is held, or what breaks otherwise.",
|
|
202
|
+
appliesTo: "code"
|
|
203
|
+
};
|
|
204
|
+
var DECORATIVE_EMOJI_IN_COMMENT = {
|
|
205
|
+
id: "decorative-emoji-comment",
|
|
206
|
+
tier: "fail",
|
|
207
|
+
pattern: new RegExp(
|
|
208
|
+
String.raw`${MARKER}[^\n]*(?<!${QUOTE_OR_BACKTICK})${DECORATION_EMOJI}(?!${VARIATION_SELECTOR}?${QUOTE_OR_BACKTICK})`,
|
|
209
|
+
"mu"
|
|
210
|
+
),
|
|
211
|
+
rationale: "A decorative emoji in a comment adds no information a reader needs, and it is the clearest single sign that the text was generated rather than written.",
|
|
212
|
+
fix: "Remove the emoji and let the sentence carry the emphasis. If it marked a state, name the state in a word.",
|
|
213
|
+
appliesTo: "code"
|
|
214
|
+
};
|
|
215
|
+
var DECORATIVE_PICTOGRAPH = {
|
|
216
|
+
id: "decorative-pictograph-comment",
|
|
217
|
+
tier: "fail",
|
|
218
|
+
pattern: new RegExp(
|
|
219
|
+
String.raw`${MARKER}[ \t]*(?!${DECORATION_EMOJI})${PICTOGRAPH}${VARIATION_SELECTOR}?`,
|
|
220
|
+
"mu"
|
|
221
|
+
),
|
|
222
|
+
rationale: "A comment that opens with a pictograph is decoration: the pictograph marks nothing a reader can act on, and it reads as a generated flourish.",
|
|
223
|
+
fix: "Remove the pictograph. If it stood for a state or a result, write that in words.",
|
|
224
|
+
appliesTo: "code"
|
|
225
|
+
};
|
|
226
|
+
var DECORATIVE_EMOJI_IN_PROSE = {
|
|
227
|
+
id: "decorative-emoji-doc",
|
|
228
|
+
tier: "fail",
|
|
229
|
+
pattern: new RegExp(
|
|
230
|
+
String.raw`^[ \t]{0,3}(?:#{1,6}[ \t]+)?(?:[-*+][ \t]+)?(?:${DECORATION_EMOJI}|${PICTOGRAPH}${VARIATION_SELECTOR}?)`,
|
|
231
|
+
"mu"
|
|
232
|
+
),
|
|
233
|
+
rationale: "A heading or a bullet that opens with a decorative emoji spends a reader attention on the emoji and none of it on the point of the line.",
|
|
234
|
+
fix: "Remove the emoji and put the point of the line in its words.",
|
|
235
|
+
appliesTo: "markdown"
|
|
236
|
+
};
|
|
237
|
+
var IRREGULAR_SCRIPT_SOURCES = [
|
|
238
|
+
String.raw`\u{0370}-\u{03ff}`,
|
|
239
|
+
// Greek
|
|
240
|
+
String.raw`\u{1f00}-\u{1fff}`,
|
|
241
|
+
// Greek extended
|
|
242
|
+
String.raw`\u{0400}-\u{052f}`,
|
|
243
|
+
// Cyrillic and its supplement
|
|
244
|
+
String.raw`\u{2de0}-\u{2dff}`,
|
|
245
|
+
// Cyrillic extended A
|
|
246
|
+
String.raw`\u{a640}-\u{a69f}`,
|
|
247
|
+
// Cyrillic extended B
|
|
248
|
+
String.raw`\u{0590}-\u{05ff}`,
|
|
249
|
+
// Hebrew
|
|
250
|
+
String.raw`\u{0600}-\u{06ff}`,
|
|
251
|
+
// Arabic
|
|
252
|
+
String.raw`\u{0750}-\u{077f}`,
|
|
253
|
+
// Arabic supplement
|
|
254
|
+
String.raw`\u{fb1d}-\u{fb4f}`,
|
|
255
|
+
// Hebrew presentation forms
|
|
256
|
+
String.raw`\u{fb50}-\u{fdff}`,
|
|
257
|
+
// Arabic presentation forms A
|
|
258
|
+
String.raw`\u{fe70}-\u{feff}`,
|
|
259
|
+
// Arabic presentation forms B, and the byte-order mark
|
|
260
|
+
String.raw`\u{0900}-\u{097f}`,
|
|
261
|
+
// Devanagari
|
|
262
|
+
String.raw`\u{0e00}-\u{0e7f}`,
|
|
263
|
+
// Thai
|
|
264
|
+
String.raw`\u{1100}-\u{11ff}`,
|
|
265
|
+
// Hangul Jamo
|
|
266
|
+
String.raw`\u{2e80}-\u{2fdf}`,
|
|
267
|
+
// CJK radicals
|
|
268
|
+
String.raw`\u{3000}-\u{30ff}`,
|
|
269
|
+
// CJK symbols and punctuation, Hiragana, Katakana
|
|
270
|
+
String.raw`\u{3100}-\u{318f}`,
|
|
271
|
+
// Bopomofo, Hangul compatibility Jamo
|
|
272
|
+
String.raw`\u{31c0}-\u{31ef}`,
|
|
273
|
+
// CJK strokes and Katakana extensions
|
|
274
|
+
String.raw`\u{3200}-\u{33ff}`,
|
|
275
|
+
// Enclosed and compatibility CJK
|
|
276
|
+
String.raw`\u{3400}-\u{4dbf}`,
|
|
277
|
+
// CJK unified ideographs extension A
|
|
278
|
+
String.raw`\u{4e00}-\u{9fff}`,
|
|
279
|
+
// CJK unified ideographs
|
|
280
|
+
String.raw`\u{ac00}-\u{d7af}`,
|
|
281
|
+
// Hangul syllables
|
|
282
|
+
String.raw`\u{f900}-\u{faff}`,
|
|
283
|
+
// CJK compatibility ideographs
|
|
284
|
+
String.raw`\u{ff00}-\u{ffef}`,
|
|
285
|
+
// Fullwidth and halfwidth forms
|
|
286
|
+
String.raw`\u{200b}-\u{200f}`,
|
|
287
|
+
// Zero-width space, joiners and the bidirectional marks
|
|
288
|
+
String.raw`\u{202a}-\u{202e}`,
|
|
289
|
+
// Bidirectional embeddings and overrides
|
|
290
|
+
String.raw`\u{2066}-\u{2069}`,
|
|
291
|
+
// Bidirectional isolates
|
|
292
|
+
String.raw`\u{fffd}`,
|
|
293
|
+
// the replacement character
|
|
294
|
+
String.raw`\u{e000}-\u{f8ff}`,
|
|
295
|
+
// Private use area
|
|
296
|
+
String.raw`\u{f0000}-\u{ffffd}`,
|
|
297
|
+
// Private use planes 15 and 16
|
|
298
|
+
String.raw`\u{100000}-\u{10fffd}`
|
|
299
|
+
// Private use plane 17
|
|
300
|
+
];
|
|
301
|
+
var IRREGULAR_SCRIPT = `[${IRREGULAR_SCRIPT_SOURCES.join("")}]`;
|
|
302
|
+
var IRREGULAR_SCRIPT_IN_TEXT = {
|
|
303
|
+
id: "no-irregular-script",
|
|
304
|
+
tier: "fail",
|
|
305
|
+
pattern: new RegExp(IRREGULAR_SCRIPT, "u"),
|
|
306
|
+
rationale: "Generated text can leak a token from another script, or carry the bytes a mis-decoding turned into a replacement or invisible character. Either way the reader is shown something the writer never meant to say.",
|
|
307
|
+
fix: "Rewrite the text in the project language, and delete the invisible character. Keep a character from another script only in a deliberate fixture, which states its own exemption with the marker above.",
|
|
308
|
+
appliesTo: "both"
|
|
309
|
+
};
|
|
310
|
+
var LONG_COMMENT_BLOCK = {
|
|
311
|
+
id: "long-comment-block",
|
|
312
|
+
tier: "warn",
|
|
313
|
+
pattern: new RegExp(
|
|
314
|
+
String.raw`(?:^[ \t]*(?:\/\/|\/\*|\*|#[ \t])(?![ \t]*[-=*_~]{4})[ \t]*\S[^\n]*(?:\n|$)){${MAX_COMMENT_BLOCK_LINES},}`,
|
|
315
|
+
"m"
|
|
316
|
+
),
|
|
317
|
+
rationale: `A run of ${MAX_COMMENT_BLOCK_LINES} or more comment lines with no break between them is usually narration or a restatement of the code below it, which a reader skips and which drifts out of date.`,
|
|
318
|
+
fix: "Cut the block to the part a reader cannot get from the code, and move reference detail into the document that owns it.",
|
|
319
|
+
appliesTo: "code"
|
|
320
|
+
};
|
|
321
|
+
var BARE_TODO = {
|
|
322
|
+
id: "bare-todo",
|
|
323
|
+
tier: "warn",
|
|
324
|
+
pattern: new RegExp(
|
|
325
|
+
String.raw`${MARKER}[ \t]*(?:${MARKER_WORDS.join("|")})\b(?![^\n]*${OWNER})(?![^\n]*\b(?:${REASON_WORDS.join("|")})\b)`,
|
|
326
|
+
"im"
|
|
327
|
+
),
|
|
328
|
+
rationale: "A TODO with no owner and no reason cannot be acted on: no one knows who will do it, or what would make it unnecessary.",
|
|
329
|
+
fix: 'Name an owner or a tracked issue and the condition that retires the marker, such as "revisit once the v2 client lands".',
|
|
330
|
+
appliesTo: "code"
|
|
331
|
+
};
|
|
332
|
+
var HYGIENE_RULES = [
|
|
333
|
+
DECORATIVE_BANNER,
|
|
334
|
+
WORKFLOW_NARRATION,
|
|
335
|
+
DECORATIVE_EMOJI_IN_COMMENT,
|
|
336
|
+
DECORATIVE_PICTOGRAPH,
|
|
337
|
+
DECORATIVE_EMOJI_IN_PROSE,
|
|
338
|
+
IRREGULAR_SCRIPT_IN_TEXT,
|
|
339
|
+
LONG_COMMENT_BLOCK,
|
|
340
|
+
BARE_TODO,
|
|
341
|
+
...RESIDUE_RULES
|
|
342
|
+
];
|
|
343
|
+
function checkHygiene(text, kind) {
|
|
344
|
+
const findings = collectFindings(text, kind);
|
|
345
|
+
const exemptAt = exemptionLine(text, kind);
|
|
346
|
+
if (exemptAt !== 0 && exemptAt < (findings[0]?.line ?? Number.POSITIVE_INFINITY)) return [];
|
|
347
|
+
return findings;
|
|
348
|
+
}
|
|
349
|
+
function collectFindings(text, kind) {
|
|
350
|
+
const lineStarts = lineStartOffsets(text);
|
|
351
|
+
const findings = [];
|
|
352
|
+
for (const rule of HYGIENE_RULES) {
|
|
353
|
+
if (rule.appliesTo !== "both" && rule.appliesTo !== kind) continue;
|
|
354
|
+
let reportedLine = 0;
|
|
355
|
+
for (const match of text.matchAll(globalPattern(rule.pattern))) {
|
|
356
|
+
const line = lineNumberAt(lineStarts, match.index ?? 0);
|
|
357
|
+
if (line === reportedLine) continue;
|
|
358
|
+
reportedLine = line;
|
|
359
|
+
findings.push({
|
|
360
|
+
id: rule.id,
|
|
361
|
+
tier: rule.tier,
|
|
362
|
+
line,
|
|
363
|
+
text: lineTextAt(text, lineStarts, line),
|
|
364
|
+
rationale: rule.rationale,
|
|
365
|
+
fix: rule.fix
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return findings.sort((a, b) => a.line - b.line);
|
|
370
|
+
}
|
|
371
|
+
function exemptionLine(text, kind) {
|
|
372
|
+
const marker = HYGIENE_EXEMPT_MARKERS[kind];
|
|
373
|
+
const lines = text.split("\n");
|
|
374
|
+
for (let i = 0; i < lines.length; i++) if ((lines[i] ?? "").trim() === marker) return i + 1;
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
function lineStartOffsets(text) {
|
|
378
|
+
const starts = [0];
|
|
379
|
+
for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) starts.push(i + 1);
|
|
380
|
+
return starts;
|
|
381
|
+
}
|
|
382
|
+
function lineNumberAt(lineStarts, index) {
|
|
383
|
+
let low = 0;
|
|
384
|
+
let high = lineStarts.length - 1;
|
|
385
|
+
while (low < high) {
|
|
386
|
+
const mid = Math.ceil((low + high) / 2);
|
|
387
|
+
if ((lineStarts[mid] ?? 0) <= index) low = mid;
|
|
388
|
+
else high = mid - 1;
|
|
389
|
+
}
|
|
390
|
+
return low + 1;
|
|
391
|
+
}
|
|
392
|
+
function lineTextAt(text, lineStarts, line) {
|
|
393
|
+
const start = lineStarts[line - 1] ?? 0;
|
|
394
|
+
const end = text.indexOf("\n", start);
|
|
395
|
+
return text.slice(start, end === -1 ? text.length : end).trim();
|
|
396
|
+
}
|
|
397
|
+
function globalPattern(pattern) {
|
|
398
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
399
|
+
return new RegExp(pattern.source, flags);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/quality.ts
|
|
403
|
+
function bodyOf(md) {
|
|
404
|
+
return md.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
405
|
+
}
|
|
406
|
+
function authoredBody(body) {
|
|
407
|
+
if (body.startsWith("\r\n")) return body.slice(2);
|
|
408
|
+
if (body.startsWith("\n")) return body.slice(1);
|
|
409
|
+
return body;
|
|
410
|
+
}
|
|
111
411
|
var MAX_BODY_LINES = 500;
|
|
112
412
|
var MIN_FULL_BODY_LINES = 20;
|
|
113
413
|
var WHEN_SECTION = /^## When to use$/im;
|
|
@@ -142,23 +442,24 @@ function looksLikeWhenDescription(description) {
|
|
|
142
442
|
}
|
|
143
443
|
function lintWarnings(skill) {
|
|
144
444
|
const warnings = [];
|
|
145
|
-
const
|
|
445
|
+
const file = skill.skillMd;
|
|
446
|
+
const body = bodyOf(file);
|
|
146
447
|
const bodyLines = body.split("\n").length;
|
|
147
|
-
if (bodyLines < MIN_FULL_BODY_LINES
|
|
448
|
+
if (bodyLines < MIN_FULL_BODY_LINES) {
|
|
148
449
|
warnings.push("thin-body: full playbook body is under 20 lines");
|
|
149
450
|
}
|
|
150
451
|
const hasExample = /```/.test(body) || /\bexample:?\b/i.test(body) || /\be\.g\.\b/i.test(body);
|
|
151
452
|
if (!hasExample) {
|
|
152
453
|
warnings.push("no-example: no concrete code fence or worked example in the body");
|
|
153
454
|
}
|
|
154
|
-
if (/\b(I|we|you)\s+(will|should|can|need|must|do|write|create|implement|run|build)\b/i.test(
|
|
455
|
+
if (/\b(I|we|you)\s+(will|should|can|need|must|do|write|create|implement|run|build)\b/i.test(file)) {
|
|
155
456
|
warnings.push("first-person: narration addresses the reader instead of imperative steps");
|
|
156
457
|
}
|
|
157
|
-
if (/\b(?:wait|sleep|retry|backoff|limit|cap)\s+[a-z]*\s*(\d{1,4})\b/i.test(
|
|
458
|
+
if (/\b(?:wait|sleep|retry|backoff|limit|cap)\s+[a-z]*\s*(\d{1,4})\b/i.test(file) && !/because|to (avoid|prevent|give|let)/i.test(file)) {
|
|
158
459
|
warnings.push("voodoo-constant: numeric threshold without a stated reason");
|
|
159
460
|
}
|
|
160
|
-
const hasVersionPin = /as of \d{4}|\bversion \d+\.\d+\.\d+\b|"v\d+\.\d+"/i.test(
|
|
161
|
-
const hasLegacySection = /^## Legacy|^## Old patterns/i.test(
|
|
461
|
+
const hasVersionPin = /as of \d{4}|\bversion \d+\.\d+\.\d+\b|"v\d+\.\d+"/i.test(file);
|
|
462
|
+
const hasLegacySection = /^## Legacy|^## Old patterns/i.test(file);
|
|
162
463
|
if (hasVersionPin && !hasLegacySection) {
|
|
163
464
|
warnings.push("time-sensitive: version/date pin outside a Legacy section");
|
|
164
465
|
}
|
|
@@ -202,6 +503,56 @@ function artifactPathDrift(skill) {
|
|
|
202
503
|
}
|
|
203
504
|
return [...drifts];
|
|
204
505
|
}
|
|
506
|
+
function splitFencedBlocks(body) {
|
|
507
|
+
const prose = [];
|
|
508
|
+
const code = [];
|
|
509
|
+
let closing = null;
|
|
510
|
+
for (const line of body.split("\n")) {
|
|
511
|
+
let boundary = false;
|
|
512
|
+
if (closing === null) {
|
|
513
|
+
const open = line.match(/^[ \t]{0,3}(`{3,}|~{3,})/);
|
|
514
|
+
const run = open?.[1];
|
|
515
|
+
if (run) {
|
|
516
|
+
const char = run[0] ?? "";
|
|
517
|
+
closing = new RegExp(`^[ \\t]{0,3}${char}{${run.length},}[ \\t]*$`);
|
|
518
|
+
boundary = true;
|
|
519
|
+
}
|
|
520
|
+
} else if (closing.test(line)) {
|
|
521
|
+
closing = null;
|
|
522
|
+
boundary = true;
|
|
523
|
+
}
|
|
524
|
+
const isCode = closing !== null && !boundary;
|
|
525
|
+
prose.push(isCode ? "" : line);
|
|
526
|
+
code.push(isCode ? line : "");
|
|
527
|
+
}
|
|
528
|
+
return { prose: prose.join("\n"), code: code.join("\n") };
|
|
529
|
+
}
|
|
530
|
+
function exemptionLine2(body) {
|
|
531
|
+
const marker = HYGIENE_EXEMPT_MARKERS.markdown;
|
|
532
|
+
const lines = body.split("\n");
|
|
533
|
+
for (let i = 0; i < lines.length; i++) {
|
|
534
|
+
if ((lines[i] ?? "").trim() === marker) return i + 1;
|
|
535
|
+
}
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
function hygieneFindings(body) {
|
|
539
|
+
const text = authoredBody(body);
|
|
540
|
+
const { prose, code } = splitFencedBlocks(text);
|
|
541
|
+
const findings = [...checkHygiene(prose, "markdown"), ...checkHygiene(code, "code")];
|
|
542
|
+
const seen = /* @__PURE__ */ new Set();
|
|
543
|
+
const ordered = findings.filter((f) => {
|
|
544
|
+
const key = `${f.id}:${f.line}`;
|
|
545
|
+
if (seen.has(key)) return false;
|
|
546
|
+
seen.add(key);
|
|
547
|
+
return true;
|
|
548
|
+
}).sort((a, b) => a.line - b.line);
|
|
549
|
+
const exemptAt = exemptionLine2(text);
|
|
550
|
+
if (exemptAt !== 0 && exemptAt < (ordered[0]?.line ?? Number.POSITIVE_INFINITY)) return [];
|
|
551
|
+
return ordered;
|
|
552
|
+
}
|
|
553
|
+
function hygieneMessage(finding) {
|
|
554
|
+
return `${finding.id} (line ${finding.line}): ${finding.rationale} ${finding.fix}`;
|
|
555
|
+
}
|
|
205
556
|
|
|
206
557
|
// src/compiler.ts
|
|
207
558
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
@@ -218,9 +569,6 @@ function parseFrontmatter(md) {
|
|
|
218
569
|
}
|
|
219
570
|
return fm;
|
|
220
571
|
}
|
|
221
|
-
function bodyOf(md) {
|
|
222
|
-
return md.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
223
|
-
}
|
|
224
572
|
function validateSkill(skill) {
|
|
225
573
|
const errors = [];
|
|
226
574
|
const warnings = [];
|
|
@@ -253,6 +601,11 @@ function validateSkill(skill) {
|
|
|
253
601
|
if (!r.content.trim()) errors.push(`reference "${r.name}" is empty`);
|
|
254
602
|
}
|
|
255
603
|
for (const d of artifactPathDrift(skill)) errors.push(d);
|
|
604
|
+
for (const finding of hygieneFindings(body)) {
|
|
605
|
+
const message = hygieneMessage(finding);
|
|
606
|
+
if (finding.tier === "fail") errors.push(message);
|
|
607
|
+
else warnings.push(message);
|
|
608
|
+
}
|
|
256
609
|
warnings.push(...lintWarnings(skill));
|
|
257
610
|
return { ok: errors.length === 0, errors, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
258
611
|
}
|
|
@@ -642,12 +995,18 @@ function loadEvalSuites(dir = EVALS_DIR) {
|
|
|
642
995
|
}
|
|
643
996
|
return suites;
|
|
644
997
|
}
|
|
645
|
-
function evaluateSuite(suite) {
|
|
646
|
-
return suite.evals.map((e) =>
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
998
|
+
function evaluateSuite(suite, candidates) {
|
|
999
|
+
return suite.evals.map((e) => {
|
|
1000
|
+
if (candidates && !(e.id in candidates)) {
|
|
1001
|
+
return { id: e.id, pass: false, failures: [`no candidate output for eval "${e.id}"`] };
|
|
1002
|
+
}
|
|
1003
|
+
const output = candidates ? candidates[e.id] ?? e.expected_output : e.expected_output;
|
|
1004
|
+
return {
|
|
1005
|
+
id: e.id,
|
|
1006
|
+
pass: e.assertions ? runAssertions(output, e.assertions).pass : true,
|
|
1007
|
+
failures: e.assertions ? runAssertions(output, e.assertions).failures : []
|
|
1008
|
+
};
|
|
1009
|
+
});
|
|
651
1010
|
}
|
|
652
1011
|
|
|
653
1012
|
// src/registry.ts
|
|
@@ -684,43 +1043,26 @@ function buildRegistry() {
|
|
|
684
1043
|
function registryByCategory(category) {
|
|
685
1044
|
return buildRegistry().filter((e) => e.category === category);
|
|
686
1045
|
}
|
|
687
|
-
|
|
688
|
-
// src/residue.ts
|
|
689
|
-
var FORBIDDEN_RESIDUE = [
|
|
690
|
-
"workflow/<task",
|
|
691
|
-
// predecessor state file
|
|
692
|
-
"noir-workflow.mode",
|
|
693
|
-
// predecessor mode flag
|
|
694
|
-
"noir-workflow",
|
|
695
|
-
// predecessor plugin name (as a plugin/path reference)
|
|
696
|
-
"@uiigateway",
|
|
697
|
-
// predecessor Angular specifics
|
|
698
|
-
// NOTE: 'ClickUp'/'clickup' were forbidden during the predecessor-port era (the
|
|
699
|
-
// ClickUp REST precedent lived in the deleted noir-workflow plugin). ClickUp is
|
|
700
|
-
// now reintroduced as a first-class Noir integration (skills/integrations/noir-clickup),
|
|
701
|
-
// so the token is no longer residue — it is the integration's legitimate subject.
|
|
702
|
-
"<EXTREMELY-IMPORTANT",
|
|
703
|
-
// Superpowers rhetoric
|
|
704
|
-
"SUBAGENT-STOP",
|
|
705
|
-
// Superpowers rhetoric
|
|
706
|
-
"plugins/noir-workflow"
|
|
707
|
-
// predecessor path
|
|
708
|
-
];
|
|
709
1046
|
export {
|
|
710
1047
|
BUILTIN_DIR,
|
|
711
1048
|
EVALS_DIR,
|
|
712
1049
|
FORBIDDEN_RESIDUE,
|
|
1050
|
+
HYGIENE_EXEMPT_MARKERS,
|
|
1051
|
+
HYGIENE_RULES,
|
|
713
1052
|
INTEGRATIONS_DIR,
|
|
714
1053
|
IntegrationAuthSchema,
|
|
715
1054
|
IntegrationDeclarationSchema,
|
|
716
1055
|
IntegrationMcpSchema,
|
|
717
1056
|
IntegrationSddSchema,
|
|
718
1057
|
MAX_BODY_LINES,
|
|
1058
|
+
MAX_COMMENT_BLOCK_LINES,
|
|
719
1059
|
MIN_FULL_BODY_LINES,
|
|
720
1060
|
NOIR_NAMESPACE,
|
|
1061
|
+
RESIDUE_RULES,
|
|
721
1062
|
bodyOf,
|
|
722
1063
|
buildRegistry,
|
|
723
1064
|
chainedReferences,
|
|
1065
|
+
checkHygiene,
|
|
724
1066
|
compileIntegration,
|
|
725
1067
|
compileSkill,
|
|
726
1068
|
discoverAll,
|