ios-agent-mcp 2.0.1 → 2.2.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.
Files changed (45) hide show
  1. package/README.md +74 -7
  2. package/data/knowledge.json +1 -0
  3. package/dist/analyzers/memory.d.ts +12 -0
  4. package/dist/analyzers/memory.js +81 -0
  5. package/dist/analyzers/memory.js.map +1 -0
  6. package/dist/analyzers/performance.d.ts +10 -0
  7. package/dist/analyzers/performance.js +106 -0
  8. package/dist/analyzers/performance.js.map +1 -0
  9. package/dist/analyzers/security.d.ts +2 -0
  10. package/dist/analyzers/security.js +82 -0
  11. package/dist/analyzers/security.js.map +1 -0
  12. package/dist/analyzers/skill.d.ts +34 -0
  13. package/dist/analyzers/skill.js +483 -0
  14. package/dist/analyzers/skill.js.map +1 -0
  15. package/dist/analyzers/testing.d.ts +16 -0
  16. package/dist/analyzers/testing.js +135 -0
  17. package/dist/analyzers/testing.js.map +1 -0
  18. package/dist/analyzers/types.d.ts +19 -1
  19. package/dist/analyzers/types.js +65 -3
  20. package/dist/analyzers/types.js.map +1 -1
  21. package/dist/index.js +162 -5
  22. package/dist/index.js.map +1 -1
  23. package/dist/knowledge-server.d.ts +2 -0
  24. package/dist/knowledge-server.js +44 -0
  25. package/dist/knowledge-server.js.map +1 -0
  26. package/dist/knowledge.d.ts +78 -0
  27. package/dist/knowledge.js +69 -0
  28. package/dist/knowledge.js.map +1 -0
  29. package/dist/report.d.ts +10 -0
  30. package/dist/report.js +40 -0
  31. package/dist/report.js.map +1 -1
  32. package/dist/resources.d.ts +67 -0
  33. package/dist/resources.js +222 -0
  34. package/dist/resources.js.map +1 -0
  35. package/dist/result.d.ts +203 -0
  36. package/dist/result.js +135 -0
  37. package/dist/result.js.map +1 -0
  38. package/dist/scan.d.ts +9 -0
  39. package/dist/scan.js +110 -0
  40. package/dist/scan.js.map +1 -1
  41. package/dist/version.d.ts +1 -0
  42. package/dist/version.js +4 -0
  43. package/dist/version.js.map +1 -0
  44. package/mcp.json +76 -11
  45. package/package.json +19 -10
@@ -0,0 +1,483 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ /**
4
+ * Tools an agent definition may name.
5
+ *
6
+ * A typo here is invisible at runtime — an unknown tool name is not granted and
7
+ * not reported, so the agent silently lacks a capability its prompt assumes.
8
+ */
9
+ const KNOWN_TOOLS = new Set([
10
+ "Agent",
11
+ "Bash",
12
+ "BashOutput",
13
+ "Edit",
14
+ "ExitPlanMode",
15
+ "Glob",
16
+ "Grep",
17
+ "KillShell",
18
+ "NotebookEdit",
19
+ "Read",
20
+ "Task",
21
+ "TodoWrite",
22
+ "WebFetch",
23
+ "WebSearch",
24
+ "Write",
25
+ ]);
26
+ /** Tools that mutate the working tree. The whole point of a read-only agent. */
27
+ const WRITE_TOOLS = new Set(["Edit", "Write", "NotebookEdit"]);
28
+ /**
29
+ * Mirror targets. Kept in sync with scripts/sync-mirrors.sh.
30
+ *
31
+ * These are only compared when the repository demonstrably uses the mirror
32
+ * pattern — see `lintMirrors`. A project with an unrelated CLAUDE.md must not
33
+ * be told it has drifted from a SKILL.md it never mirrored.
34
+ */
35
+ const MIRROR_CANDIDATES = [
36
+ "AGENTS.md",
37
+ "CLAUDE.md",
38
+ "CONVENTIONS.md",
39
+ "GEMINI.md",
40
+ "replit.md",
41
+ ".clinerules",
42
+ ".continuerules",
43
+ ".cursorrules",
44
+ ".kilocoderules",
45
+ ".roorules",
46
+ ".rules",
47
+ ".windsurfrules",
48
+ ".aiassistant/rules/ios-skill.md",
49
+ ".amazonq/rules/ios-skill.md",
50
+ ".augment/rules/ios-skill.md",
51
+ ".continue/rules/ios-skill.md",
52
+ ".cursor/rules/ios-skill.md",
53
+ ".github/copilot-instructions.md",
54
+ ".junie/guidelines.md",
55
+ ".kilocode/rules/ios-skill.md",
56
+ ".roo/rules/ios-skill.md",
57
+ ".tabnine/guidelines/ios-skill.md",
58
+ ".trae/rules/ios-skill.md",
59
+ ".windsurf/rules/ios-skill.md",
60
+ ];
61
+ const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
62
+ const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
63
+ /** Agent Skills caps the trigger description; past this it is silently truncated. */
64
+ const MAX_DESCRIPTION = 1024;
65
+ /**
66
+ * Parse a leading YAML frontmatter block.
67
+ *
68
+ * Deliberately not a YAML parser: it reads top-level `key: value` pairs and
69
+ * ignores nested structure. That covers every key these checks care about
70
+ * without taking a dependency, and nested keys are simply not reported on.
71
+ */
72
+ function parseFrontmatter(text) {
73
+ const lines = text.split("\n");
74
+ if (lines[0] !== "---")
75
+ return null;
76
+ const end = lines.indexOf("---", 1);
77
+ if (end === -1)
78
+ return null;
79
+ const entries = new Map();
80
+ for (let index = 1; index < end; index += 1) {
81
+ const match = /^([A-Za-z][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(lines[index]);
82
+ if (match) {
83
+ entries.set(match[1], { value: match[2].trim(), line: index + 1 });
84
+ }
85
+ }
86
+ return {
87
+ block: lines.slice(1, end).join("\n"),
88
+ entries,
89
+ bodyLine: end + 2,
90
+ };
91
+ }
92
+ /** Strip a leading frontmatter block and the blank lines after it. */
93
+ function stripFrontmatter(text) {
94
+ const lines = text.split("\n");
95
+ if (lines[0] !== "---")
96
+ return text;
97
+ const end = lines.indexOf("---", 1);
98
+ if (end === -1)
99
+ return text;
100
+ let start = end + 1;
101
+ while (start < lines.length && lines[start].trim() === "")
102
+ start += 1;
103
+ return lines.slice(start).join("\n");
104
+ }
105
+ async function readIfPresent(path) {
106
+ try {
107
+ return await readFile(path, "utf8");
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ async function exists(path) {
114
+ try {
115
+ await stat(path);
116
+ return true;
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
122
+ /** Split a `tools:` value, tolerating both `A, B` and `[A, B]` forms. */
123
+ function parseToolList(value) {
124
+ return value
125
+ .replace(/^\[|\]$/g, "")
126
+ .split(",")
127
+ .map((entry) => entry.trim().replace(/^["']|["']$/g, ""))
128
+ .filter((entry) => entry.length > 0);
129
+ }
130
+ /**
131
+ * Does this description claim the agent cannot write?
132
+ *
133
+ * Matched against the description because that is the contract the main agent
134
+ * reads when it decides to delegate. If the prose promises read-only and the
135
+ * frontmatter grants Edit, the delegation decision was made on a false premise.
136
+ */
137
+ function claimsReadOnly(description) {
138
+ return /read[- ]only|never edits?|does not (?:edit|change|modify)|no write tools/i.test(description);
139
+ }
140
+ function checkSkillFrontmatter(relativePath, text, findings) {
141
+ const frontmatter = parseFrontmatter(text);
142
+ if (!frontmatter) {
143
+ findings.push({
144
+ file: relativePath,
145
+ line: 1,
146
+ severity: "blocker",
147
+ rule: "skill-missing-frontmatter",
148
+ message: `${relativePath} has no YAML frontmatter block.`,
149
+ consequence: "Agent Skills loaders read `name` and `description` from frontmatter to decide when to load the skill. Without it the skill never triggers.",
150
+ fix: "Add a `---` fenced block at the very top of the file with at least `name`, `description`, `version`, and `license`.",
151
+ excerpt: text.split("\n")[0] ?? "",
152
+ });
153
+ return;
154
+ }
155
+ for (const key of ["name", "description", "version", "license"]) {
156
+ if (!frontmatter.entries.has(key)) {
157
+ findings.push({
158
+ file: relativePath,
159
+ line: 1,
160
+ severity: "blocker",
161
+ rule: "skill-frontmatter-missing-key",
162
+ message: `${relativePath} frontmatter is missing \`${key}\`.`,
163
+ consequence: key === "description"
164
+ ? "The description is the trigger. With none, the skill is never selected for a task."
165
+ : `Loaders that require \`${key}\` reject the skill outright.`,
166
+ fix: `Add \`${key}:\` to the frontmatter block.`,
167
+ excerpt: "",
168
+ });
169
+ }
170
+ }
171
+ const name = frontmatter.entries.get("name");
172
+ if (name && !KEBAB.test(name.value)) {
173
+ findings.push({
174
+ file: relativePath,
175
+ line: name.line,
176
+ severity: "serious",
177
+ rule: "skill-name-not-kebab-case",
178
+ message: `Skill name \`${name.value}\` is not lowercase-kebab-case.`,
179
+ consequence: "Skill names are used as invocation identifiers. Mixed case or spaces make the skill unaddressable by name.",
180
+ fix: "Rename to lowercase words separated by hyphens.",
181
+ excerpt: `name: ${name.value}`,
182
+ });
183
+ }
184
+ const version = frontmatter.entries.get("version");
185
+ if (version && !SEMVER.test(version.value.replace(/^["']|["']$/g, ""))) {
186
+ findings.push({
187
+ file: relativePath,
188
+ line: version.line,
189
+ severity: "serious",
190
+ rule: "skill-version-not-semver",
191
+ message: `Version \`${version.value}\` is not semantic versioning.`,
192
+ consequence: "Consumers cannot tell a breaking change from a patch, and release tooling that sorts versions misorders them.",
193
+ fix: "Use MAJOR.MINOR.PATCH, for example `2.0.0`.",
194
+ excerpt: `version: ${version.value}`,
195
+ });
196
+ }
197
+ const description = frontmatter.entries.get("description");
198
+ if (description) {
199
+ if (description.value.length > MAX_DESCRIPTION) {
200
+ findings.push({
201
+ file: relativePath,
202
+ line: description.line,
203
+ severity: "serious",
204
+ rule: "skill-description-too-long",
205
+ message: `Description is ${description.value.length} characters; the limit is ${MAX_DESCRIPTION}.`,
206
+ consequence: "Loaders truncate past the limit, so the trailing trigger conditions are silently dropped and the skill stops matching those tasks.",
207
+ fix: "Trim to the triggering conditions. Move detail into the body, which is not length-limited.",
208
+ excerpt: `${description.value.slice(0, 80)}…`,
209
+ });
210
+ }
211
+ else if (description.value.length < 40) {
212
+ findings.push({
213
+ file: relativePath,
214
+ line: description.line,
215
+ severity: "minor",
216
+ rule: "skill-description-too-short",
217
+ message: "Description is too short to trigger reliably.",
218
+ consequence: "The description is matched against the task. A few words give the loader almost nothing to match on, so the skill loads inconsistently.",
219
+ fix: "State what the skill does and the specific conditions under which it should load.",
220
+ excerpt: `description: ${description.value}`,
221
+ });
222
+ }
223
+ }
224
+ const allowed = frontmatter.entries.get("allowed-tools");
225
+ if (allowed) {
226
+ for (const tool of parseToolList(allowed.value)) {
227
+ if (!KNOWN_TOOLS.has(tool)) {
228
+ findings.push({
229
+ file: relativePath,
230
+ line: allowed.line,
231
+ severity: "serious",
232
+ rule: "skill-unknown-tool",
233
+ message: `\`allowed-tools\` names an unknown tool \`${tool}\`.`,
234
+ consequence: "An unrecognized tool name is not granted and not reported. The skill runs without a capability its instructions assume it has.",
235
+ fix: `Correct the spelling, or remove \`${tool}\` if it is not a real tool.`,
236
+ excerpt: `allowed-tools: ${allowed.value}`,
237
+ });
238
+ }
239
+ }
240
+ }
241
+ }
242
+ async function lintAgents(root, findings) {
243
+ const directory = join(root, ".claude", "agents");
244
+ let files;
245
+ try {
246
+ files = (await readdir(directory)).filter((name) => name.endsWith(".md")).sort();
247
+ }
248
+ catch {
249
+ return 0;
250
+ }
251
+ for (const file of files) {
252
+ const relativePath = join(".claude", "agents", file);
253
+ const text = await readFile(join(directory, file), "utf8");
254
+ const frontmatter = parseFrontmatter(text);
255
+ if (!frontmatter) {
256
+ findings.push({
257
+ file: relativePath,
258
+ line: 1,
259
+ severity: "blocker",
260
+ rule: "agent-missing-frontmatter",
261
+ message: `${file} has no YAML frontmatter.`,
262
+ consequence: "Without frontmatter the file is not registered as a subagent at all. It looks defined but can never be invoked.",
263
+ fix: "Add a `---` block with `name`, `description`, and `tools`.",
264
+ excerpt: text.split("\n")[0] ?? "",
265
+ });
266
+ continue;
267
+ }
268
+ const stem = basename(file, ".md");
269
+ const name = frontmatter.entries.get("name");
270
+ if (!name) {
271
+ findings.push({
272
+ file: relativePath,
273
+ line: 1,
274
+ severity: "serious",
275
+ rule: "agent-missing-name",
276
+ message: `${file} frontmatter has no \`name\`.`,
277
+ consequence: "The subagent cannot be addressed by name.",
278
+ fix: `Add \`name: ${stem}\`.`,
279
+ excerpt: "",
280
+ });
281
+ }
282
+ else {
283
+ if (!KEBAB.test(name.value)) {
284
+ findings.push({
285
+ file: relativePath,
286
+ line: name.line,
287
+ severity: "serious",
288
+ rule: "agent-name-not-kebab-case",
289
+ message: `Agent name \`${name.value}\` is not lowercase-kebab-case.`,
290
+ consequence: "Names outside this form are not reliably resolvable at invocation.",
291
+ fix: "Rename to lowercase words separated by hyphens.",
292
+ excerpt: `name: ${name.value}`,
293
+ });
294
+ }
295
+ if (name.value !== stem) {
296
+ findings.push({
297
+ file: relativePath,
298
+ line: name.line,
299
+ severity: "serious",
300
+ rule: "agent-name-filename-mismatch",
301
+ message: `Agent is named \`${name.value}\` but the file is \`${file}\`.`,
302
+ consequence: "Documentation and delegation prompts reference one identifier while the loader registers the other, so the delegation silently fails to resolve.",
303
+ fix: `Rename the file to \`${name.value}.md\`, or change \`name\` to \`${stem}\`.`,
304
+ excerpt: `name: ${name.value}`,
305
+ });
306
+ }
307
+ }
308
+ const description = frontmatter.entries.get("description");
309
+ if (!description || description.value === "") {
310
+ findings.push({
311
+ file: relativePath,
312
+ line: 1,
313
+ severity: "serious",
314
+ rule: "agent-missing-description",
315
+ message: `${file} has no \`description\`.`,
316
+ consequence: "The description is what the main agent reads to decide whether to delegate. With none, the subagent is never chosen.",
317
+ fix: "Add a description stating when to use this agent, not just what it is.",
318
+ excerpt: "",
319
+ });
320
+ }
321
+ else if (!/\b(use|when|after|before)\b/i.test(description.value)) {
322
+ findings.push({
323
+ file: relativePath,
324
+ line: description.line,
325
+ severity: "minor",
326
+ rule: "agent-description-lacks-trigger",
327
+ message: `${file} describes what the agent is, but not when to use it.`,
328
+ consequence: "Delegation is decided by matching the task against this text. A description with no triggering condition rarely matches.",
329
+ fix: 'Add an explicit trigger — "Use when …", "Use after …".',
330
+ excerpt: `description: ${description.value.slice(0, 80)}…`,
331
+ });
332
+ }
333
+ const tools = frontmatter.entries.get("tools");
334
+ if (!tools || tools.value === "") {
335
+ findings.push({
336
+ file: relativePath,
337
+ line: 1,
338
+ severity: "serious",
339
+ rule: "agent-missing-tools",
340
+ message: `${file} does not declare \`tools\`.`,
341
+ consequence: "An agent with no tool list inherits every tool the main agent has. A reviewer meant to be read-only silently gains Edit and Write.",
342
+ fix: "Declare the minimum tools this agent needs, for example `tools: Read, Grep, Glob`.",
343
+ excerpt: "",
344
+ });
345
+ continue;
346
+ }
347
+ const granted = parseToolList(tools.value);
348
+ for (const tool of granted) {
349
+ if (!KNOWN_TOOLS.has(tool)) {
350
+ findings.push({
351
+ file: relativePath,
352
+ line: tools.line,
353
+ severity: "serious",
354
+ rule: "agent-unknown-tool",
355
+ message: `${file} grants an unknown tool \`${tool}\`.`,
356
+ consequence: "An unrecognized name is not granted and not reported. The agent's prompt assumes a capability it does not have, and fails at the moment it tries to use it.",
357
+ fix: `Correct the spelling, or drop \`${tool}\`.`,
358
+ excerpt: `tools: ${tools.value}`,
359
+ });
360
+ }
361
+ }
362
+ if (description && claimsReadOnly(description.value)) {
363
+ const violations = granted.filter((tool) => WRITE_TOOLS.has(tool));
364
+ if (violations.length > 0) {
365
+ findings.push({
366
+ file: relativePath,
367
+ line: tools.line,
368
+ severity: "blocker",
369
+ rule: "agent-read-only-holds-write-tool",
370
+ message: `${file} is described as read-only but is granted ${violations.join(", ")}.`,
371
+ consequence: "The main agent delegates to it believing it cannot change code. A reviewer that can edit will fix what it was supposed to report, which destroys the separation of duties the review depends on.",
372
+ fix: `Remove ${violations.join(", ")} from \`tools\`, or stop describing the agent as read-only.`,
373
+ excerpt: `tools: ${tools.value}`,
374
+ });
375
+ }
376
+ }
377
+ }
378
+ return files.length;
379
+ }
380
+ /**
381
+ * Compare mirrors against SKILL.md's body.
382
+ *
383
+ * Only runs when at least one candidate already matches byte-for-byte. That
384
+ * proves the repository uses the generated-mirror pattern; without the check, a
385
+ * project with a hand-written CLAUDE.md would be told all 24 mirrors are stale.
386
+ */
387
+ async function lintMirrors(root, skillText, findings) {
388
+ const body = stripFrontmatter(skillText).trimEnd();
389
+ const present = [];
390
+ for (const candidate of MIRROR_CANDIDATES) {
391
+ const text = await readIfPresent(join(root, candidate));
392
+ if (text === null)
393
+ continue;
394
+ present.push({ path: candidate, matches: text.trimEnd() === body });
395
+ }
396
+ if (present.length === 0 || !present.some((entry) => entry.matches)) {
397
+ return { compared: 0, skipped: true };
398
+ }
399
+ for (const entry of present) {
400
+ if (entry.matches)
401
+ continue;
402
+ findings.push({
403
+ file: entry.path,
404
+ line: 0,
405
+ severity: "serious",
406
+ rule: "mirror-out-of-sync",
407
+ message: `${entry.path} does not match the body of SKILL.md.`,
408
+ consequence: "Agents reading this file get different rules from agents reading SKILL.md. The divergence is invisible until two agents disagree about the same codebase.",
409
+ fix: "Regenerate with `./scripts/sync-mirrors.sh`, then commit the result.",
410
+ excerpt: "",
411
+ });
412
+ }
413
+ return { compared: present.length, skipped: false };
414
+ }
415
+ const REFERENCE = /`((?:docs|patterns|checklists|templates|scripts|samples)\/[^`\s]+)`/g;
416
+ /** Flag backtick-quoted repository paths that do not resolve. */
417
+ async function lintReferences(root, sources, findings) {
418
+ let count = 0;
419
+ for (const source of sources) {
420
+ const text = await readIfPresent(join(root, source));
421
+ if (text === null)
422
+ continue;
423
+ const lines = text.split("\n");
424
+ for (let index = 0; index < lines.length; index += 1) {
425
+ for (const match of lines[index].matchAll(REFERENCE)) {
426
+ const reference = match[1];
427
+ if (reference.includes("*"))
428
+ continue;
429
+ count += 1;
430
+ if (!(await exists(join(root, reference.replace(/\/$/, ""))))) {
431
+ findings.push({
432
+ file: source,
433
+ line: index + 1,
434
+ severity: "serious",
435
+ rule: "broken-doc-reference",
436
+ message: `References \`${reference}\`, which does not exist.`,
437
+ consequence: "An agent told to consult this path finds nothing and proceeds without the rules it was supposed to apply — silently, because a missing file is not an error it reports.",
438
+ fix: `Create ${reference}, or correct the path.`,
439
+ excerpt: lines[index].trim(),
440
+ });
441
+ }
442
+ }
443
+ }
444
+ }
445
+ return count;
446
+ }
447
+ /** Lint a skill repository's metadata: frontmatter, agents, mirrors, references. */
448
+ export async function lintSkill(root) {
449
+ const findings = [];
450
+ const skillPath = (await exists(join(root, "SKILL.md"))) ? "SKILL.md" : null;
451
+ const skillText = skillPath ? await readFile(join(root, skillPath), "utf8") : null;
452
+ if (skillText === null) {
453
+ findings.push({
454
+ file: "SKILL.md",
455
+ line: 0,
456
+ severity: "blocker",
457
+ rule: "skill-file-missing",
458
+ message: "No SKILL.md at the repository root.",
459
+ consequence: "SKILL.md is the entry point every Agent Skills loader looks for. Without it there is no skill to load, whatever else the repository contains.",
460
+ fix: "Create SKILL.md with YAML frontmatter (`name`, `description`, `version`, `license`) and the instruction body beneath it.",
461
+ excerpt: "",
462
+ });
463
+ }
464
+ else {
465
+ checkSkillFrontmatter("SKILL.md", skillText, findings);
466
+ }
467
+ const agentCount = await lintAgents(root, findings);
468
+ const mirrors = skillText
469
+ ? await lintMirrors(root, skillText, findings)
470
+ : { compared: 0, skipped: true };
471
+ const referencedPaths = await lintReferences(root, ["SKILL.md", "README.md"], findings);
472
+ return {
473
+ findings,
474
+ checked: {
475
+ skillFile: skillPath,
476
+ agentCount,
477
+ mirrorsCompared: mirrors.compared,
478
+ mirrorCheckSkipped: mirrors.skipped,
479
+ referencedPaths,
480
+ },
481
+ };
482
+ }
483
+ //# sourceMappingURL=skill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill.js","sourceRoot":"","sources":["../../src/analyzers/skill.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { Finding, SourceFile } from "./types.js";
2
+ /**
3
+ * Test-suite quality.
4
+ *
5
+ * This is the one analyzer that runs ONLY on test files — the inverse of every
6
+ * other rule here, which skips them. A flaky or vacuous test is worse than a
7
+ * missing one: it costs the same to run, and it reports success either way.
8
+ */
9
+ export declare function analyzeTesting(file: SourceFile): Finding[];
10
+ /**
11
+ * Project-level: is anything tested at all?
12
+ *
13
+ * Reported once per scan rather than per file, because "no tests" is a property
14
+ * of the project and repeating it would drown the file-level findings.
15
+ */
16
+ export declare function analyzeTestCoverage(files: SourceFile[]): Finding[];
@@ -0,0 +1,135 @@
1
+ import { eachLine, withoutStringLiterals } from "./types.js";
2
+ const TESTING_DOC = "checklists/testing.md";
3
+ const MOCKING_DOC = "docs/testing/mocking-strategy.md";
4
+ const TEST_FILE = /Tests?\.swift$|Tests?\/|Spec\.swift$/i;
5
+ /**
6
+ * Test-suite quality.
7
+ *
8
+ * This is the one analyzer that runs ONLY on test files — the inverse of every
9
+ * other rule here, which skips them. A flaky or vacuous test is worse than a
10
+ * missing one: it costs the same to run, and it reports success either way.
11
+ */
12
+ export function analyzeTesting(file) {
13
+ const findings = [];
14
+ if (!TEST_FILE.test(file.path))
15
+ return findings;
16
+ const push = (line, excerpt, rule, severity, message, consequence, fix, doc = TESTING_DOC) => findings.push({
17
+ file: file.path,
18
+ line,
19
+ severity,
20
+ rule,
21
+ message,
22
+ consequence,
23
+ fix,
24
+ doc,
25
+ excerpt: excerpt.trim(),
26
+ });
27
+ const lines = file.content.split("\n");
28
+ eachLine(file, (line, number) => {
29
+ const index = number - 1;
30
+ // Sleeping to wait for async work.
31
+ if (/Thread\.sleep|usleep\s*\(|sleep\s*\(\s*\d/.test(line) ||
32
+ /Task\.sleep\s*\(/.test(line)) {
33
+ push(number, line, "test-sleeps", "serious", "Test waits by sleeping.", "The duration is guesswork. It passes on a fast machine, fails under CI load, and the fix people reach for is a longer sleep — so the suite gets slower and stays flaky. A flaky test teaches the team to re-run until green, which is worse than no test.", "Await the work directly, or gate it: an actor with a continuation, `XCTestExpectation` with `await fulfillment(of:)`, or a controllable clock.");
34
+ }
35
+ // waitForExpectations with a long timeout is the same problem.
36
+ if (/waitForExpectations\s*\(\s*timeout:\s*(\d+)/.test(line)) {
37
+ const timeout = Number(/timeout:\s*(\d+)/.exec(line)?.[1] ?? 0);
38
+ if (timeout >= 5) {
39
+ push(number, line, "long-test-timeout", "minor", `Expectation timeout of ${timeout}s.`, "A timeout this long is usually covering for a race rather than a slow operation. It turns a deterministic failure into a slow, intermittent one.", "Reduce it and make the wait deterministic. If the work genuinely takes seconds, it belongs behind a protocol seam with a fake.");
40
+ }
41
+ }
42
+ // A test with no assertion cannot fail for the reason it claims.
43
+ if (/^\s*func\s+test\w*\s*\(/.test(line)) {
44
+ let depth = 0;
45
+ let started = false;
46
+ let body = "";
47
+ for (let cursor = index; cursor < Math.min(index + 60, lines.length); cursor += 1) {
48
+ const current = lines[cursor];
49
+ depth += (current.match(/\{/g) ?? []).length;
50
+ depth -= (current.match(/\}/g) ?? []).length;
51
+ if (!started && depth > 0)
52
+ started = true;
53
+ body += `${current}\n`;
54
+ if (started && depth <= 0)
55
+ break;
56
+ }
57
+ if (!/XCTAssert|XCTFail|XCTUnwrap|#expect|#require|\.expect\(/.test(body) &&
58
+ !/throws/.test(line)) {
59
+ push(number, line, "test-without-assertion", "serious", "Test contains no assertion.", "It passes as long as nothing throws, so it reports success whether the behaviour is right or wrong. It costs CI time and buys a false sense of coverage.", "Assert the outcome, or delete the test. A test that cannot fail is not a test.");
60
+ }
61
+ }
62
+ // XCTAssert with an await inside — does not compile, but people write it.
63
+ //
64
+ // String literals are blanked first: the assertion MESSAGE regularly
65
+ // contains the word "await" ("set before the await, not after it"), and
66
+ // matching that reports a defect in correct code.
67
+ if (/XCTAssert\w*\s*\([^)]*\bawait\b/.test(withoutStringLiterals(line))) {
68
+ push(number, line, "await-inside-xctassert", "blocker", "`await` inside an XCTAssert autoclosure.", "XCTAssert takes an autoclosure, which cannot contain an await — this does not compile.", "Hoist it: `let value = try await subject.run()` then assert on `value`.");
69
+ }
70
+ // Real network in a test.
71
+ if (/URLSession\.shared|URLSession\(configuration:\s*\.default\)/.test(line) &&
72
+ !/mock|stub|fake/i.test(file.path)) {
73
+ push(number, line, "network-in-test", "serious", "Test uses a live URLSession.", "The suite now depends on the network, a server, and its data. It fails offline, fails in CI sandboxes, and fails when someone else changes a fixture — none of which are bugs in the code under test.", "Inject the dependency behind a protocol and pass a fake. If a screen cannot be tested without the network, the seam is missing.", MOCKING_DOC);
74
+ }
75
+ // Force-unwrap in a test turns a failed assertion into a crashed run.
76
+ if (/\btry!\s/.test(line)) {
77
+ push(number, line, "force-try-in-test", "minor", "`try!` in a test.", "A throw crashes the whole test run instead of failing one test, so you lose every other result in the suite and the report says nothing about which case broke.", "Mark the test `throws` and use `try`, or `XCTUnwrap`.");
78
+ }
79
+ // Order-dependent state.
80
+ if (/^\s*(static\s+var|static\s+let)\s+\w+/.test(line) && !/\blet\b.*=\s*"/.test(line)) {
81
+ push(number, line, "shared-mutable-test-state", "minor", "Static mutable state in a test case.", "XCTest does not guarantee test order and may run classes in parallel. State that survives between tests makes results depend on execution order, which is the hardest kind of flake to reproduce.", "Move it into `setUp()` as instance state, so each test gets a fresh value.");
82
+ }
83
+ // Asserting on a bare Bool loses the values on failure.
84
+ if (/XCTAssertTrue\s*\(\s*\w+\s*==\s*/.test(line)) {
85
+ push(number, line, "assert-true-on-equality", "minor", "`XCTAssertTrue(a == b)` hides the values.", "On failure the report says only 'expected true'. `XCTAssertEqual` prints both sides, which is usually the whole diagnosis.", "Use `XCTAssertEqual(a, b)`.");
86
+ }
87
+ });
88
+ return findings;
89
+ }
90
+ /**
91
+ * Project-level: is anything tested at all?
92
+ *
93
+ * Reported once per scan rather than per file, because "no tests" is a property
94
+ * of the project and repeating it would drown the file-level findings.
95
+ */
96
+ export function analyzeTestCoverage(files) {
97
+ const testFiles = files.filter((file) => TEST_FILE.test(file.path));
98
+ const sourceFiles = files.filter((file) => !TEST_FILE.test(file.path));
99
+ if (sourceFiles.length === 0)
100
+ return [];
101
+ if (testFiles.length === 0) {
102
+ return [
103
+ {
104
+ file: "(project)",
105
+ line: 0,
106
+ severity: "serious",
107
+ rule: "no-tests",
108
+ message: `No test files found alongside ${sourceFiles.length} source files.`,
109
+ consequence: "Every change is verified by hand or not at all, and no refactor can be proven behaviour-preserving.",
110
+ fix: "Add a test target. Start with the view models — they are pure logic once dependencies cross a protocol boundary.",
111
+ doc: TESTING_DOC,
112
+ excerpt: "",
113
+ },
114
+ ];
115
+ }
116
+ // A ratio, not a coverage percentage — this counts files, not lines.
117
+ const ratio = testFiles.length / sourceFiles.length;
118
+ if (ratio < 0.1) {
119
+ return [
120
+ {
121
+ file: "(project)",
122
+ line: 0,
123
+ severity: "minor",
124
+ rule: "sparse-tests",
125
+ message: `${testFiles.length} test file(s) for ${sourceFiles.length} source files.`,
126
+ consequence: "Most of the codebase has no automated check, so regressions surface in review or in production rather than in CI.",
127
+ fix: "Prioritize view models and pure functions — the parts that are cheapest to test and most likely to hold a bug.",
128
+ doc: TESTING_DOC,
129
+ excerpt: "",
130
+ },
131
+ ];
132
+ }
133
+ return [];
134
+ }
135
+ //# sourceMappingURL=testing.js.map