@frihet/mcp-server 1.9.0-beta.1 → 1.12.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 (125) hide show
  1. package/README.md +263 -13
  2. package/dist/__tests__/d4b-hr-payroll-onboarding-tools.test.d.ts +13 -0
  3. package/dist/__tests__/d4b-hr-payroll-onboarding-tools.test.d.ts.map +1 -0
  4. package/dist/__tests__/d4b-hr-payroll-onboarding-tools.test.js +420 -0
  5. package/dist/__tests__/d4b-hr-payroll-onboarding-tools.test.js.map +1 -0
  6. package/dist/__tests__/einvoice-day4-tools.test.d.ts +16 -0
  7. package/dist/__tests__/einvoice-day4-tools.test.d.ts.map +1 -0
  8. package/dist/__tests__/einvoice-day4-tools.test.js +377 -0
  9. package/dist/__tests__/einvoice-day4-tools.test.js.map +1 -0
  10. package/dist/__tests__/einvoice-tools.test.d.ts +1 -1
  11. package/dist/__tests__/einvoice-tools.test.js +25 -12
  12. package/dist/__tests__/einvoice-tools.test.js.map +1 -1
  13. package/dist/__tests__/fiscal-tools.test.d.ts +5 -4
  14. package/dist/__tests__/fiscal-tools.test.d.ts.map +1 -1
  15. package/dist/__tests__/fiscal-tools.test.js +8 -26
  16. package/dist/__tests__/fiscal-tools.test.js.map +1 -1
  17. package/dist/__tests__/openai-profile.test.d.ts +10 -0
  18. package/dist/__tests__/openai-profile.test.d.ts.map +1 -0
  19. package/dist/__tests__/openai-profile.test.js +132 -0
  20. package/dist/__tests__/openai-profile.test.js.map +1 -0
  21. package/dist/__tests__/recurring-tools.test.d.ts +14 -7
  22. package/dist/__tests__/recurring-tools.test.d.ts.map +1 -1
  23. package/dist/__tests__/recurring-tools.test.js +195 -9
  24. package/dist/__tests__/recurring-tools.test.js.map +1 -1
  25. package/dist/__tests__/team-tools.test.d.ts +18 -0
  26. package/dist/__tests__/team-tools.test.d.ts.map +1 -0
  27. package/dist/__tests__/team-tools.test.js +228 -0
  28. package/dist/__tests__/team-tools.test.js.map +1 -0
  29. package/dist/__tests__/time-tools.test.d.ts +9 -7
  30. package/dist/__tests__/time-tools.test.d.ts.map +1 -1
  31. package/dist/__tests__/time-tools.test.js +96 -9
  32. package/dist/__tests__/time-tools.test.js.map +1 -1
  33. package/dist/client-interface.d.ts +236 -0
  34. package/dist/client-interface.d.ts.map +1 -1
  35. package/dist/client.d.ts +245 -1
  36. package/dist/client.d.ts.map +1 -1
  37. package/dist/client.js +266 -5
  38. package/dist/client.js.map +1 -1
  39. package/dist/index.js +6 -6
  40. package/dist/index.js.map +1 -1
  41. package/dist/openai-profile.d.ts +4 -1
  42. package/dist/openai-profile.d.ts.map +1 -1
  43. package/dist/openai-profile.js +105 -1
  44. package/dist/openai-profile.js.map +1 -1
  45. package/dist/tools/accountingClose.d.ts +20 -0
  46. package/dist/tools/accountingClose.d.ts.map +1 -0
  47. package/dist/tools/accountingClose.js +119 -0
  48. package/dist/tools/accountingClose.js.map +1 -0
  49. package/dist/tools/audit_gl.d.ts +19 -0
  50. package/dist/tools/audit_gl.d.ts.map +1 -0
  51. package/dist/tools/audit_gl.js +86 -0
  52. package/dist/tools/audit_gl.js.map +1 -0
  53. package/dist/tools/bank_rules.d.ts +23 -0
  54. package/dist/tools/bank_rules.d.ts.map +1 -0
  55. package/dist/tools/bank_rules.js +96 -0
  56. package/dist/tools/bank_rules.js.map +1 -0
  57. package/dist/tools/einvoice.d.ts +70 -3
  58. package/dist/tools/einvoice.d.ts.map +1 -1
  59. package/dist/tools/einvoice.js +421 -3
  60. package/dist/tools/einvoice.js.map +1 -1
  61. package/dist/tools/fiscal.d.ts +3 -2
  62. package/dist/tools/fiscal.d.ts.map +1 -1
  63. package/dist/tools/fiscal.js +4 -22
  64. package/dist/tools/fiscal.js.map +1 -1
  65. package/dist/tools/gestoria.d.ts +28 -0
  66. package/dist/tools/gestoria.d.ts.map +1 -0
  67. package/dist/tools/gestoria.js +248 -0
  68. package/dist/tools/gestoria.js.map +1 -0
  69. package/dist/tools/hr.d.ts +24 -0
  70. package/dist/tools/hr.d.ts.map +1 -0
  71. package/dist/tools/hr.js +219 -0
  72. package/dist/tools/hr.js.map +1 -0
  73. package/dist/tools/igic.d.ts +19 -0
  74. package/dist/tools/igic.d.ts.map +1 -0
  75. package/dist/tools/igic.js +102 -0
  76. package/dist/tools/igic.js.map +1 -0
  77. package/dist/tools/impuesto_sociedades.d.ts +18 -0
  78. package/dist/tools/impuesto_sociedades.d.ts.map +1 -0
  79. package/dist/tools/impuesto_sociedades.js +64 -0
  80. package/dist/tools/impuesto_sociedades.js.map +1 -0
  81. package/dist/tools/onboard_vies.d.ts +22 -0
  82. package/dist/tools/onboard_vies.d.ts.map +1 -0
  83. package/dist/tools/onboard_vies.js +74 -0
  84. package/dist/tools/onboard_vies.js.map +1 -0
  85. package/dist/tools/onboarding.d.ts +19 -0
  86. package/dist/tools/onboarding.d.ts.map +1 -0
  87. package/dist/tools/onboarding.js +63 -0
  88. package/dist/tools/onboarding.js.map +1 -0
  89. package/dist/tools/payroll.d.ts +20 -0
  90. package/dist/tools/payroll.d.ts.map +1 -0
  91. package/dist/tools/payroll.js +73 -0
  92. package/dist/tools/payroll.js.map +1 -0
  93. package/dist/tools/permissions.d.ts +19 -0
  94. package/dist/tools/permissions.d.ts.map +1 -0
  95. package/dist/tools/permissions.js +52 -0
  96. package/dist/tools/permissions.js.map +1 -0
  97. package/dist/tools/portal_domain.d.ts +19 -0
  98. package/dist/tools/portal_domain.d.ts.map +1 -0
  99. package/dist/tools/portal_domain.js +85 -0
  100. package/dist/tools/portal_domain.js.map +1 -0
  101. package/dist/tools/recurring.d.ts +10 -5
  102. package/dist/tools/recurring.d.ts.map +1 -1
  103. package/dist/tools/recurring.js +178 -6
  104. package/dist/tools/recurring.js.map +1 -1
  105. package/dist/tools/register-all.d.ts +2 -2
  106. package/dist/tools/register-all.d.ts.map +1 -1
  107. package/dist/tools/register-all.js +41 -3
  108. package/dist/tools/register-all.js.map +1 -1
  109. package/dist/tools/shared.d.ts +313 -1
  110. package/dist/tools/shared.d.ts.map +1 -1
  111. package/dist/tools/shared.js +256 -0
  112. package/dist/tools/shared.js.map +1 -1
  113. package/dist/tools/team.d.ts +17 -0
  114. package/dist/tools/team.d.ts.map +1 -0
  115. package/dist/tools/team.js +141 -0
  116. package/dist/tools/team.js.map +1 -0
  117. package/dist/tools/time.d.ts +9 -8
  118. package/dist/tools/time.d.ts.map +1 -1
  119. package/dist/tools/time.js +54 -9
  120. package/dist/tools/time.js.map +1 -1
  121. package/dist/tools/webhooks.d.ts.map +1 -1
  122. package/dist/tools/webhooks.js +27 -1
  123. package/dist/tools/webhooks.js.map +1 -1
  124. package/package.json +12 -5
  125. package/scripts/audit-mcp-refs.mjs +331 -0
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+ // Audit cross-repo refs to @frihet/mcp-server tool count + version.
3
+ // Source of truth = this repo's package.json + actual registerTool count.
4
+ // Run from anywhere; flags any sister-repo file with stale numbers.
5
+ //
6
+ // Usage:
7
+ // node scripts/audit-mcp-refs.mjs # check (exit 1 if stale)
8
+ // node scripts/audit-mcp-refs.mjs --fix # auto-replace stale numbers
9
+ // node scripts/audit-mcp-refs.mjs --json # machine-readable
10
+ // node scripts/audit-mcp-refs.mjs --repo <name> # limit to one repo
11
+ // node scripts/audit-mcp-refs.mjs --allow-dirty # bypass worktree-clean guard
12
+ //
13
+ // Exit codes:
14
+ // 0 = clean (or --fix succeeded)
15
+ // 1 = stale refs found
16
+ // 2 = invalid --repo argument
17
+ // 3 = sister repo dirty (use --allow-dirty to override)
18
+ //
19
+ // Limitations:
20
+ // - grep-based, not AST. False positives possible — extend SAFE_PATTERNS
21
+ // or add "// mcp-refs:ok" annotation to skip a line.
22
+ // - Sister repos must be cloned at ~/Documents/<repo-name>.
23
+ //
24
+ // Whitelist: lines matching SAFE_PATTERNS skip the tool-count check.
25
+ // Inline: append "// mcp-refs:ok" or "# mcp-refs:ok" to ignore one line.
26
+
27
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
28
+ import { execSync } from 'node:child_process';
29
+ import { resolve, join, dirname } from 'node:path';
30
+ import { fileURLToPath } from 'node:url';
31
+ import { homedir } from 'node:os';
32
+
33
+ const __dirname = dirname(fileURLToPath(import.meta.url));
34
+ const SELF = resolve(__dirname, '..');
35
+ const HOME = homedir();
36
+ const ARGS = process.argv.slice(2);
37
+ const FIX = ARGS.includes('--fix');
38
+ const JSON_OUT = ARGS.includes('--json');
39
+ const REPO_FILTER = ARGS.includes('--repo') ? ARGS[ARGS.indexOf('--repo') + 1] : null;
40
+ const ALLOW_DIRTY = ARGS.includes('--allow-dirty');
41
+
42
+ // === SOURCE OF TRUTH ===
43
+ const pkg = JSON.parse(readFileSync(join(SELF, 'package.json'), 'utf8'));
44
+ const VERSION = pkg.version;
45
+
46
+ // Real tool count = registerTool calls in src/tools/*.ts minus meta-tools in register-all.ts
47
+ const toolDir = join(SELF, 'src/tools');
48
+ let total = 0;
49
+ let metaCount = 0;
50
+ for (const f of readdirSync(toolDir)) {
51
+ if (!f.endsWith('.ts')) continue;
52
+ const txt = readFileSync(join(toolDir, f), 'utf8');
53
+ const matches = (txt.match(/registerTool/g) || []).length;
54
+ if (f === 'register-all.ts') metaCount = matches;
55
+ else total += matches;
56
+ }
57
+ const TOOL_COUNT = total;
58
+
59
+ // === TARGETS ===
60
+ const REPOS = {
61
+ 'frihet-mcp': {
62
+ root: SELF,
63
+ files: [
64
+ 'server.json',
65
+ 'package.json',
66
+ 'README.md',
67
+ 'CHANGELOG.md',
68
+ 'skill/SKILL.md',
69
+ 'src/index.ts',
70
+ 'workers/remote-mcp/src/index.ts',
71
+ 'workers/remote-mcp/public/releases.json',
72
+ ],
73
+ },
74
+ 'Frihet-ERP': {
75
+ root: join(HOME, 'Documents/Frihet-ERP'),
76
+ files: [
77
+ 'CLAUDE.md',
78
+ 'apps/erp/public/llms.txt',
79
+ 'packages/manifest/src/data/product.ts',
80
+ 'packages/manifest/src/data/comparisons.ts',
81
+ 'packages/manifest/src/emit/schema-org.ts',
82
+ 'packages/ui/src/manifestBrowser/data.json',
83
+ 'docs/dev/mcp-tools-coverage.md',
84
+ ],
85
+ },
86
+ 'Frihet-Saas-Website': {
87
+ root: join(HOME, 'Documents/Frihet-Saas-Website'),
88
+ files: [
89
+ 'public/.well-known/llms.txt',
90
+ 'public/.well-known/llms-full.txt',
91
+ 'public/.well-known/agents.json',
92
+ 'src/data/comparisons.json',
93
+ 'src/data/schema-org.json',
94
+ 'src/i18n/es.json',
95
+ 'src/layouts/Base.astro',
96
+ ],
97
+ },
98
+ 'frihet-docs': {
99
+ root: join(HOME, 'Documents/frihet-docs'),
100
+ files: [
101
+ 'docs/desarrolladores/mcp-server.md',
102
+ 'static/.well-known/jsonld',
103
+ ],
104
+ },
105
+ };
106
+
107
+ // Tool-count nouns across 17 langs
108
+ const TOOL_NOUNS = [
109
+ 'tool', 'tools',
110
+ 'herramienta', 'herramientas',
111
+ 'outil', 'outils',
112
+ 'Werkzeug', 'Werkzeuge',
113
+ 'strumento', 'strumenti',
114
+ 'ferramenta', 'ferramentas',
115
+ 'verktyg',
116
+ 'tyokalu', 'tyokalua', 'työkalu', 'työkalua',
117
+ 'gereedschap', 'gereedschappen',
118
+ 'narzedzie', 'narzedzi', 'narzędzie', 'narzędzi',
119
+ 'instrument', 'instrumente',
120
+ 'εργαλείο', 'εργαλεία',
121
+ 'araç', 'araçlar', 'araclar',
122
+ 'eszköz', 'eszközök',
123
+ 'ツール',
124
+ ];
125
+ const TOOL_NOUN_RE = TOOL_NOUNS.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
126
+ // e.g. "94 tools", "94 herramientas"
127
+ const TOOL_COUNT_RE = new RegExp(`\\b(\\d{1,4})[\\s_-]+(${TOOL_NOUN_RE})\\b`, 'gi');
128
+
129
+ // Files whose tool-count entries are entirely historical/narrative — skip count checks.
130
+ // These files record past release totals as changelog entries (not current-state claims).
131
+ // Version checks are still enforced. Extend when adding new changelog-style files.
132
+ const HISTORY_FILES = new Set([
133
+ 'CHANGELOG.md',
134
+ ]);
135
+
136
+ // Lines containing any of these phrases are NOT checked for tool-count drift.
137
+ // (different concept than MCP tool count)
138
+ const SAFE_PATTERNS = [
139
+ /55\+?\s+(herramientas|tools)/i, // Gemini in-app function tools
140
+ /\d+\s+(?:tools?|herramientas)\s+copiloto/i, // Gemini copiloto tools
141
+ /(?:function[- ]tools?|function[- ]calls?)/i, // Gemini function tools
142
+ /\bGemini\b/i, // Gemini-related lines
143
+ /\d+\s+(?:tipos? de webhook|webhook events?|tipos webhook)/i, // webhook event types
144
+ /\d+\s+(?:tipos? de evento|event types?)/i, // event type counts
145
+ /\bnpm\s+install\b/i, // version pin lines
146
+ /node[- ]?modules/i,
147
+ /mcp-refs:ok/i, // inline annotation
148
+ // Historical / changelog / release-note context
149
+ /\bdelta\b/i,
150
+ /\+\d+\s+(tools?|herramientas?)/i, // "+N tools" delta
151
+ /Wave\s+\d/i, // wave N references
152
+ /\bnotes?\b\s*[:=]/i, // notes field in JSON/release entries
153
+ /history|hist[oó]rico|previous|earlier|legacy|deprecated|prior|former/i,
154
+ /(?:Banking|POS|Stay|Fiscal|Time|Recurring|Team|Invoices|Expenses|Clients|Products|Quotes|CRM|Deposits|Vendors|Webhooks|Einvoice|Intelligence|EInvoice)\s*\(\d+\s+(tools?|herramientas?)\)/i,
155
+ // ES families (frihet-docs)
156
+ /(?:Facturas?|Gastos?|Clientes?|Productos?|Presupuestos?|Proveedores?|Inteligencia|Anticipos?|Dep[oó]sitos?|CRM|Webhooks?|E[- ]?facturas?|Banca|TPV|Alojamientos?|Fiscal|Tiempo|Recurrentes?|Equipo)\s*\(\d+\s+(tools?|herramientas?)\)/i,
157
+ // Generic: markdown section header with parenthesized count → category breakdown
158
+ /^#{2,5}\s+.*\(\d+\s+(?:tools?|herramientas?)\)/i,
159
+ // Generic: list item with parenthesized count
160
+ /^\s*[*\-+]\s+.*\(\d+\s+(?:tools?|herramientas?)\)\s*[—:]/i,
161
+ // Counts of resources/prompts (separate concept from tools)
162
+ /\d+\s+(resources?|recursos?|prompts?)/i,
163
+ ];
164
+
165
+ // Version pattern: catches "v1.5.4", "1.7.0-beta.1", "@frihet/mcp-server@1.6.0", etc.
166
+ // Only flagged when line context contains MCP markers.
167
+ const VERSION_RE = /v?(\d+\.\d+\.\d+(?:-[a-z]+\.\d+)?)/g;
168
+ const MCP_CONTEXT_RE = /(@frihet\/mcp-server|frihet-mcp|servidor\s+mcp|mcp\s+server|mcp\.frihet\.io)/i;
169
+
170
+ // Validate --repo argument
171
+ if (REPO_FILTER && !Object.keys(REPOS).includes(REPO_FILTER)) {
172
+ console.error(`Unknown repo: ${REPO_FILTER}`);
173
+ console.error(`Valid: ${Object.keys(REPOS).join(', ')}`);
174
+ process.exit(2);
175
+ }
176
+
177
+ // Worktree-clean guard for sister repos when --fix is active.
178
+ // Skip self-repo guard (caller likely on dev branch in frihet-mcp itself).
179
+ function isDirty(root) {
180
+ try {
181
+ const out = execSync('git status --porcelain', { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
182
+ return out.trim().length > 0;
183
+ } catch {
184
+ return false; // not a git repo → don't block
185
+ }
186
+ }
187
+
188
+ if (FIX && !ALLOW_DIRTY) {
189
+ const dirty = [];
190
+ for (const [name, cfg] of Object.entries(REPOS)) {
191
+ if (REPO_FILTER && name !== REPO_FILTER) continue;
192
+ if (name === 'frihet-mcp') continue; // skip self
193
+ if (existsSync(cfg.root) && isDirty(cfg.root)) dirty.push(name);
194
+ }
195
+ if (dirty.length) {
196
+ console.error(`Refusing --fix: sister repos dirty: ${dirty.join(', ')}`);
197
+ console.error(`Commit/stash there first, or pass --allow-dirty.`);
198
+ process.exit(3);
199
+ }
200
+ }
201
+
202
+ const findings = [];
203
+
204
+ for (const [repoName, cfg] of Object.entries(REPOS)) {
205
+ if (REPO_FILTER && repoName !== REPO_FILTER) continue;
206
+ if (!existsSync(cfg.root)) {
207
+ findings.push({ repo: repoName, severity: 'warn', msg: `repo dir not found: ${cfg.root}` });
208
+ continue;
209
+ }
210
+ for (const rel of cfg.files) {
211
+ const abs = join(cfg.root, rel);
212
+ if (!existsSync(abs)) {
213
+ findings.push({ repo: repoName, file: rel, severity: 'warn', msg: 'file missing' });
214
+ continue;
215
+ }
216
+ const lines = readFileSync(abs, 'utf8').split('\n');
217
+
218
+ // History files: skip tool-count checks entirely (entries are historical narrative).
219
+ const isHistoryFile = HISTORY_FILES.has(rel);
220
+
221
+ lines.forEach((line, idx) => {
222
+ // Skip safe-pattern lines for tool-count check
223
+ const safeLine = isHistoryFile || SAFE_PATTERNS.some((re) => re.test(line));
224
+
225
+ if (!safeLine) {
226
+ TOOL_COUNT_RE.lastIndex = 0;
227
+ let m;
228
+ while ((m = TOOL_COUNT_RE.exec(line)) !== null) {
229
+ const n = parseInt(m[1], 10);
230
+ if (n === TOOL_COUNT) continue;
231
+ // Heuristic: only flag if number is in MCP context OR it's an obviously MCP-related file.
232
+ // All files inside frihet-mcp repo are MCP-related by definition.
233
+ const mcpFile = repoName === 'frihet-mcp'
234
+ || /llms\.txt|llms-full\.txt|server\.json|releases\.json|mcp[-_]server|skill\/SKILL|jsonld|agents\.json|manifestBrowser|schema-org|comparisons|product\.ts|emit\/schema-org/i.test(rel);
235
+ if (!mcpFile && !MCP_CONTEXT_RE.test(line)) continue;
236
+ findings.push({
237
+ repo: repoName,
238
+ file: rel,
239
+ line: idx + 1,
240
+ severity: 'fail',
241
+ kind: 'tool-count',
242
+ found: n,
243
+ expected: TOOL_COUNT,
244
+ snippet: line.trim().slice(0, 120),
245
+ });
246
+ }
247
+ }
248
+
249
+ // Version check — only if line matches MCP context
250
+ if (MCP_CONTEXT_RE.test(line)) {
251
+ VERSION_RE.lastIndex = 0;
252
+ let v;
253
+ while ((v = VERSION_RE.exec(line)) !== null) {
254
+ const ver = v[1];
255
+ // Only flag versions that look like @frihet/mcp-server (semver with optional prerelease, 0.x or 1.x for now)
256
+ if (!/^\d+\.\d+\.\d+/.test(ver)) continue;
257
+ if (ver === VERSION) continue;
258
+ // Skip schema URL versions (e.g., "2025-12-11")
259
+ if (/\d{4}-\d{2}-\d{2}/.test(line) && !line.includes('@frihet/mcp-server')) continue;
260
+ findings.push({
261
+ repo: repoName,
262
+ file: rel,
263
+ line: idx + 1,
264
+ severity: 'fail',
265
+ kind: 'version',
266
+ found: ver,
267
+ expected: VERSION,
268
+ snippet: line.trim().slice(0, 120),
269
+ });
270
+ }
271
+ }
272
+ });
273
+
274
+ if (FIX) {
275
+ let txt = readFileSync(abs, 'utf8');
276
+ let mutated = false;
277
+ // Replace tool-count: only on flagged file lines
278
+ const fileFails = findings.filter((f) => f.repo === repoName && f.file === rel && f.kind === 'tool-count');
279
+ for (const fail of fileFails) {
280
+ // Replace exact pattern "N tools/herramientas" → "TOOL_COUNT $noun"
281
+ const re = new RegExp(`\\b${fail.found}([\\s_-]+(?:${TOOL_NOUN_RE}))\\b`, 'gi');
282
+ const newTxt = txt.replace(re, `${TOOL_COUNT}$1`);
283
+ if (newTxt !== txt) { txt = newTxt; mutated = true; }
284
+ }
285
+ const verFails = findings.filter((f) => f.repo === repoName && f.file === rel && f.kind === 'version');
286
+ for (const fail of verFails) {
287
+ const re = new RegExp(fail.found.replace(/\./g, '\\.'), 'g');
288
+ const newTxt = txt.replace(re, VERSION);
289
+ if (newTxt !== txt) { txt = newTxt; mutated = true; }
290
+ }
291
+ if (mutated) {
292
+ writeFileSync(abs, txt);
293
+ findings.push({ repo: repoName, file: rel, severity: 'fixed', msg: 'auto-replaced' });
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ if (JSON_OUT) {
300
+ console.log(JSON.stringify({
301
+ sot: { version: VERSION, toolCount: TOOL_COUNT, metaCount },
302
+ findings,
303
+ }, null, 2));
304
+ } else {
305
+ console.log(`SoT: @frihet/mcp-server@${VERSION} · ${TOOL_COUNT} tools (+${metaCount} meta)\n`);
306
+ const fails = findings.filter((f) => f.severity === 'fail');
307
+ const warns = findings.filter((f) => f.severity === 'warn');
308
+ const fixed = findings.filter((f) => f.severity === 'fixed');
309
+ if (fails.length === 0 && warns.length === 0 && fixed.length === 0) {
310
+ console.log('OK — all refs match SoT.');
311
+ } else {
312
+ if (fails.length) {
313
+ console.log(`STALE (${fails.length}):`);
314
+ for (const f of fails) {
315
+ console.log(` ${f.repo}/${f.file}:${f.line} [${f.kind}] found=${f.found} expected=${f.expected}`);
316
+ console.log(` ${f.snippet}`);
317
+ }
318
+ }
319
+ if (warns.length) {
320
+ console.log(`\nWARN (${warns.length}):`);
321
+ for (const w of warns) console.log(` ${w.repo}${w.file ? '/' + w.file : ''}: ${w.msg}`);
322
+ }
323
+ if (fixed.length) {
324
+ console.log(`\nFIXED (${fixed.length}):`);
325
+ for (const f of fixed) console.log(` ${f.repo}/${f.file}: ${f.msg}`);
326
+ }
327
+ }
328
+ }
329
+
330
+ const exitFail = findings.some((f) => f.severity === 'fail');
331
+ process.exit(exitFail && !FIX ? 1 : 0);