@wrongstack/plugins 0.307.1 → 0.308.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.
@@ -32,6 +32,11 @@ export interface A11yFinding {
32
32
  rule: A11yRule;
33
33
  severity: 'error' | 'warning';
34
34
  message: string;
35
+ /**
36
+ * Optional scan limitation. Cross-file labels (e.g. `<Label>` in a
37
+ * sibling component) are invisible to this single-file regex walk.
38
+ */
39
+ note?: string;
35
40
  }
36
41
  declare const plugin: Plugin;
37
42
  export default plugin;
@@ -115,15 +115,40 @@ var INPUT_BUTTON = /<input\b[^>]*\btype\s*=\s*["'](submit|button|reset)["'][^>]*
115
115
  var ATTR_ID = /\bid\s*=\s*["']([^"']+)["']/gi;
116
116
  var ATTR_ALT = /\balt\s*=/i;
117
117
  var ATTR_ARIA_LABEL = /\b(?:aria-label|aria-labelledby)\s*=/i;
118
+ var ATTR_ARIA_DESCRIBEDBY = /\baria-describedby\s*=/i;
118
119
  var ATTR_TITLE = /\btitle\s*=/i;
119
120
  var ATTR_PLACEHOLDER = /\bplaceholder\s*=/i;
120
121
  var ATTR_VALUE = /\bvalue\s*=/i;
122
+ var ATTR_ROLE_DECORATIVE = /\brole\s*=\s*["'](?:presentation|none)["']/i;
123
+ var SINGLE_FILE_LABEL_NOTE = "Single-file heuristic: a label declared in a sibling component file is not visible to this scan.";
124
+ function escapeRegExp(value) {
125
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
126
+ }
121
127
  function hasMeaningfulAlt(tag) {
122
128
  const m = ATTR_ALT.exec(tag);
123
129
  ATTR_ALT.lastIndex = 0;
124
130
  if (!m) return false;
125
131
  const valMatch = tag.match(/\balt\s*=\s*["']?([^"'\s>]*)["']?/i);
126
- return valMatch ? valMatch[1].trim().length > 0 : false;
132
+ const alt = valMatch ? valMatch[1].trim() : "";
133
+ if (alt.length > 0) return true;
134
+ return ATTR_ROLE_DECORATIVE.test(tag);
135
+ }
136
+ function hasFieldsetLegendLabel(tag, content) {
137
+ const labelledBy = tag.match(/\baria-labelledby\s*=\s*["']([^"']+)["']/i);
138
+ if (labelledBy?.[1]) {
139
+ for (const id of labelledBy[1].split(/\s+/).filter(Boolean)) {
140
+ const idRe = new RegExp(`\\bid\\s*=\\s*["']${escapeRegExp(id)}["']`, "i");
141
+ if (idRe.test(content)) return true;
142
+ }
143
+ }
144
+ const typeMatch = tag.match(/\btype\s*=\s*["']?([^"'\s>]*)["']?/i);
145
+ const type = typeMatch ? typeMatch[1].toLowerCase() : "text";
146
+ if (type === "checkbox" || type === "radio") {
147
+ return /<fieldset\b[\s\S]*?<legend\b[\s\S]*?<\/legend>[\s\S]*?<input\b[\s\S]*?<\/fieldset>/i.test(
148
+ content
149
+ );
150
+ }
151
+ return false;
127
152
  }
128
153
  async function auditFile(filePath, projectRoot) {
129
154
  let content;
@@ -135,13 +160,14 @@ async function auditFile(filePath, projectRoot) {
135
160
  const findings = [];
136
161
  const lines = content.split(/\r?\n/);
137
162
  const idsByValue = /* @__PURE__ */ new Map();
138
- function add(line, rule, severity, message) {
163
+ function add(line, rule, severity, message, note) {
139
164
  findings.push({
140
165
  file: relative2(projectRoot, filePath),
141
166
  line,
142
167
  rule,
143
168
  severity,
144
- message
169
+ message,
170
+ ...note ? { note } : {}
145
171
  });
146
172
  }
147
173
  for (let i = 0; i < lines.length; i++) {
@@ -169,17 +195,36 @@ async function auditFile(filePath, projectRoot) {
169
195
  continue;
170
196
  }
171
197
  const hasAriaLabel = ATTR_ARIA_LABEL.test(tag);
198
+ const hasDescribedBy = ATTR_ARIA_DESCRIBEDBY.test(tag);
172
199
  const hasTitle = ATTR_TITLE.test(tag);
173
200
  const idMatchLocal = tag.match(/\bid\s*=\s*["']([^"']+)["']/i);
174
201
  const id = idMatchLocal ? idMatchLocal[1] : null;
175
202
  let hasLabelFor = false;
176
203
  if (id) {
177
- const labelForRe = new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*["']${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`, "i");
204
+ const labelForRe = new RegExp(
205
+ `<label\\b[^>]*\\bfor\\s*=\\s*["']${escapeRegExp(id)}["']`,
206
+ "i"
207
+ );
178
208
  hasLabelFor = labelForRe.test(content);
179
209
  }
180
210
  const wrappedInLabel = /<label\b[\s\S]*?<input\b[\s\S]*?<\/label>/i.test(content);
181
- if (!hasAriaLabel && !hasTitle && !hasLabelFor && !wrappedInLabel) {
182
- add(lineNo, "missing-input-label", "error", `<input type="${type}"> is missing an associated label`);
211
+ const hasLegend = hasFieldsetLegendLabel(tag, content);
212
+ const hasPrimaryLabel = hasAriaLabel || hasTitle || hasLabelFor || wrappedInLabel || hasLegend;
213
+ if (!hasPrimaryLabel && hasDescribedBy) {
214
+ add(
215
+ lineNo,
216
+ "low-contrast-placeholder",
217
+ "warning",
218
+ `<input type="${type}"> uses aria-describedby as a description (supplementary, not a primary label)`
219
+ );
220
+ } else if (!hasPrimaryLabel) {
221
+ add(
222
+ lineNo,
223
+ "missing-input-label",
224
+ "error",
225
+ `<input type="${type}"> is missing an associated label`,
226
+ SINGLE_FILE_LABEL_NOTE
227
+ );
183
228
  }
184
229
  if (ATTR_PLACEHOLDER.test(tag)) {
185
230
  add(lineNo, "low-contrast-placeholder", "warning", "<input> uses placeholder text (often low contrast and disappears on input)");
@@ -239,15 +284,24 @@ async function auditPath(rawPath, cfg) {
239
284
  truncated
240
285
  };
241
286
  }
287
+ function truncationWarning(result) {
288
+ if (!result.truncated) return "";
289
+ const unexamined = Math.max(0, result.fileCount - (result.scannedFiles ?? result.fileCount));
290
+ return `partial scan \u2014 ${unexamined} files not examined`;
291
+ }
242
292
  function formatSummary(result) {
293
+ const trunc = truncationWarning(result);
243
294
  if (result.findings.length === 0) {
244
- return `
295
+ const clean = `
245
296
  \u2705 accessibility-auditor: no issues found in ${result.path} (${result.fileCount} file${result.fileCount === 1 ? "" : "s"}).`;
297
+ return trunc ? `${clean}
298
+ \u26A0\uFE0F ${trunc}` : clean;
246
299
  }
247
300
  const lines = result.findings.map((f) => ` - ${f.file}:${f.line} \u2014 ${f.message} (${f.rule})`);
248
301
  return `
249
302
  \u26A0\uFE0F accessibility-auditor: ${result.findings.length} issue(s) in ${result.path} (${result.fileCount} file${result.fileCount === 1 ? "" : "s"}):
250
- ` + lines.join("\n") + "\nConsider adding missing labels/alt text or resolving duplicate ids.";
303
+ ` + lines.join("\n") + "\nConsider adding missing labels/alt text or resolving duplicate ids." + (trunc ? `
304
+ \u26A0\uFE0F ${trunc}` : "");
251
305
  }
252
306
  var plugin = {
253
307
  name: "accessibility-auditor",
@@ -318,9 +372,9 @@ var plugin = {
318
372
  findingCount: result.findings.length,
319
373
  when: (/* @__PURE__ */ new Date()).toISOString()
320
374
  };
321
- if (result.findings.length === 0) return;
375
+ if (result.findings.length === 0 && !result.truncated) return;
322
376
  const summary = formatSummary(result);
323
- if (cfg.severity === "block") {
377
+ if (cfg.severity === "block" && result.findings.length > 0) {
324
378
  return { decision: "block", reason: summary };
325
379
  }
326
380
  return { additionalContext: summary };
@@ -361,6 +415,7 @@ var plugin = {
361
415
  findingCount: result.findings.length,
362
416
  when: (/* @__PURE__ */ new Date()).toISOString()
363
417
  };
418
+ const warning = truncationWarning(result);
364
419
  return {
365
420
  ok: true,
366
421
  path: result.path,
@@ -370,7 +425,8 @@ var plugin = {
370
425
  // that reports few findings must not read as a clean result.
371
426
  truncated: result.truncated,
372
427
  findingCount: result.findings.length,
373
- findings: result.findings
428
+ findings: result.findings,
429
+ ...warning ? { additionalContext: `\u26A0\uFE0F ${warning}`, warning } : {}
374
430
  };
375
431
  }
376
432
  });
@@ -208,6 +208,12 @@ export declare const OFFICIAL_PLUGIN_AUDIT_ENTRIES: readonly [{
208
208
  readonly summary: 'Scans tool output (fetched pages, files) for prompt-injection patterns and warns the model that content is data, not instructions';
209
209
  readonly defaultState: 'active';
210
210
  readonly canDisable: true;
211
+ }, {
212
+ readonly name: 'prompt-firewall';
213
+ readonly risk: 'high';
214
+ readonly summary: 'Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.';
215
+ readonly defaultState: 'inactive';
216
+ readonly canDisable: true;
211
217
  }, {
212
218
  readonly name: 'llm-cache';
213
219
  readonly risk: 'medium';
@@ -226,12 +232,6 @@ export declare const OFFICIAL_PLUGIN_AUDIT_ENTRIES: readonly [{
226
232
  readonly summary: 'Collects session work (commits, edited files, diff) and drafts a pull-request description';
227
233
  readonly defaultState: 'inactive';
228
234
  readonly canDisable: true;
229
- }, {
230
- readonly name: 'prompt-firewall';
231
- readonly risk: 'high';
232
- readonly summary: 'Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.';
233
- readonly defaultState: 'inactive';
234
- readonly canDisable: true;
235
235
  }, {
236
236
  readonly name: 'auto-escalate';
237
237
  readonly risk: 'medium';
package/dist/audit.js CHANGED
@@ -245,6 +245,13 @@ var OFFICIAL_PLUGIN_AUDIT_ENTRIES = [
245
245
  defaultState: "active",
246
246
  canDisable: true
247
247
  },
248
+ {
249
+ name: "prompt-firewall",
250
+ risk: "high",
251
+ summary: "Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.",
252
+ defaultState: "inactive",
253
+ canDisable: true
254
+ },
248
255
  {
249
256
  name: "llm-cache",
250
257
  risk: "medium",
@@ -266,13 +273,6 @@ var OFFICIAL_PLUGIN_AUDIT_ENTRIES = [
266
273
  defaultState: "inactive",
267
274
  canDisable: true
268
275
  },
269
- {
270
- name: "prompt-firewall",
271
- risk: "high",
272
- summary: "Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.",
273
- defaultState: "inactive",
274
- canDisable: true
275
- },
276
276
  {
277
277
  name: "auto-escalate",
278
278
  risk: "medium",
@@ -24,6 +24,15 @@
24
24
  * @public
25
25
  */
26
26
  import type { Plugin } from '@wrongstack/core/types';
27
+ /**
28
+ * Heuristic cyclomatic-complexity formula (per file, not per function):
29
+ * control keywords (if | else if | for | while | switch | catch)
30
+ * + short-circuit / nullish operators (&& || ??)
31
+ * + assignment-or operators (||= &&= ??=)
32
+ * + ternary `?`
33
+ * Optional chaining (`?.`) is not a decision point — `a?.b ?? c` adds 1.
34
+ */
35
+ export declare const COMPLEXITY_FORMULA = "control(if|else if|for|while|switch|catch) + (&& || ?? ||= &&= ??=) + ternary ?; optional chaining ?. is not counted";
27
36
  declare const plugin: Plugin;
28
37
  export default plugin;
29
38
  //# sourceMappingURL=index.d.ts.map
@@ -115,36 +115,65 @@ function countFunctions(content) {
115
115
  for (const _match of content.matchAll(arrowRe)) count++;
116
116
  return count;
117
117
  }
118
+ var COMPLEXITY_FORMULA = "control(if|else if|for|while|switch|catch) + (&& || ?? ||= &&= ??=) + ternary ?; optional chaining ?. is not counted";
118
119
  function countComplexity(content) {
119
120
  const controlRe = /\b(if|else\s+if|for|while|switch|catch)\b/g;
120
- const operatorRe = /[?&|]/g;
121
121
  let complexity = 0;
122
122
  controlRe.lastIndex = 0;
123
123
  for (const _match of content.matchAll(controlRe)) complexity++;
124
- operatorRe.lastIndex = 0;
125
- for (const match of content.matchAll(operatorRe)) {
126
- const ch = match[0];
127
- if (ch === "?") {
124
+ for (let i = 0; i < content.length; i++) {
125
+ const c = content[i];
126
+ const n1 = content[i + 1];
127
+ const n2 = content[i + 2];
128
+ if (c === "?" && n1 === "?" && n2 === "=") {
128
129
  complexity++;
129
- } else {
130
- const next = content[match.index + 1];
131
- if (next === ch) complexity++;
130
+ i += 2;
131
+ continue;
132
+ }
133
+ if (c === "|" && n1 === "|" && n2 === "=" || c === "&" && n1 === "&" && n2 === "=") {
134
+ complexity++;
135
+ i += 2;
136
+ continue;
137
+ }
138
+ if (c === "?" && n1 === "?" || c === "|" && n1 === "|" || c === "&" && n1 === "&") {
139
+ complexity++;
140
+ i += 1;
141
+ continue;
132
142
  }
143
+ if (c === "?" && n1 === ".") {
144
+ i += 1;
145
+ continue;
146
+ }
147
+ if (c === "?") complexity++;
133
148
  }
134
149
  return complexity;
135
150
  }
136
151
  function analyzeFile(filePath, content) {
152
+ if (content.length === 0) {
153
+ return {
154
+ file: relativePath(filePath),
155
+ lines: 0,
156
+ codeLines: 0,
157
+ commentLines: 0,
158
+ blankLines: 0,
159
+ functionCount: 0,
160
+ complexity: 0
161
+ };
162
+ }
137
163
  const lines = content.split(/\r?\n/);
138
164
  let codeLines = 0;
139
165
  let commentLines = 0;
140
166
  let blankLines = 0;
141
167
  let inBlockComment = false;
142
- for (const rawLine of lines) {
143
- const line = rawLine.trim();
168
+ for (let i = 0; i < lines.length; i++) {
169
+ const line = lines[i].trim();
144
170
  if (line.length === 0) {
145
171
  blankLines++;
146
172
  continue;
147
173
  }
174
+ if (i === 0 && line.startsWith("#!")) {
175
+ continue;
176
+ }
148
177
  if (inBlockComment) {
149
178
  commentLines++;
150
179
  if (line.includes("*/")) inBlockComment = false;
@@ -305,7 +334,8 @@ var plugin = {
305
334
  files: state.fileCount,
306
335
  hookInvocations: state.hookInvocationCount,
307
336
  errors: state.errorCount
308
- }
337
+ },
338
+ complexityFormula: COMPLEXITY_FORMULA
309
339
  };
310
340
  }
311
341
  });
@@ -350,5 +380,6 @@ var plugin = {
350
380
  };
351
381
  var code_metrics_default = plugin;
352
382
  export {
383
+ COMPLEXITY_FORMULA,
353
384
  code_metrics_default as default
354
385
  };
package/dist/dep-guard.js CHANGED
@@ -48,10 +48,12 @@ function parseInstallCommands(command) {
48
48
  if (!cleaned) continue;
49
49
  let name = cleaned;
50
50
  let version = null;
51
- const pipMatch = /^([A-Za-z0-9_.-]+)\s*(?:==|>=|<=|~=|!=|>|<)\s*(.+)$/.exec(cleaned);
51
+ const pipMatch = /^([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(==|>=|<=|~=|!=|>|<)\s*(.+)$/.exec(cleaned);
52
52
  if (pipMatch?.[1]) {
53
53
  name = pipMatch[1];
54
- version = pipMatch[2] ?? null;
54
+ const op = pipMatch[2] ?? "";
55
+ const ver = (pipMatch[3] ?? "").trim();
56
+ version = op === "==" || op === "" ? ver || null : `${op}${ver}`;
55
57
  } else {
56
58
  const at = cleaned.lastIndexOf("@");
57
59
  if (at > 0) {
@@ -28,6 +28,10 @@
28
28
  * @public
29
29
  */
30
30
  import type { Plugin } from '@wrongstack/core/types';
31
+ export declare function isInstallCommand(input: {
32
+ toolName?: string | undefined;
33
+ toolInput?: unknown;
34
+ }): boolean;
31
35
  declare const plugin: Plugin;
32
36
  export default plugin;
33
37
  //# sourceMappingURL=index.d.ts.map