@agentxm/workspace-lint 0.28.4-bootstrap.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 (100) hide show
  1. package/LICENSE +110 -0
  2. package/dist/src/catalog/index.d.ts +31 -0
  3. package/dist/src/catalog/index.js +60 -0
  4. package/dist/src/catalog/workspace/agents-detected-declared.d.ts +23 -0
  5. package/dist/src/catalog/workspace/agents-detected-declared.js +61 -0
  6. package/dist/src/catalog/workspace/agents-recognized.d.ts +23 -0
  7. package/dist/src/catalog/workspace/agents-recognized.js +64 -0
  8. package/dist/src/catalog/workspace/axm-skill-compatible.d.ts +4 -0
  9. package/dist/src/catalog/workspace/axm-skill-compatible.js +51 -0
  10. package/dist/src/catalog/workspace/configured-but-not-installed.d.ts +4 -0
  11. package/dist/src/catalog/workspace/configured-but-not-installed.js +88 -0
  12. package/dist/src/catalog/workspace/desired-state-reconcilable.d.ts +4 -0
  13. package/dist/src/catalog/workspace/desired-state-reconcilable.js +96 -0
  14. package/dist/src/catalog/workspace/display-paths.d.ts +7 -0
  15. package/dist/src/catalog/workspace/display-paths.js +4 -0
  16. package/dist/src/catalog/workspace/helpers/empty.d.ts +18 -0
  17. package/dist/src/catalog/workspace/helpers/empty.js +16 -0
  18. package/dist/src/catalog/workspace/helpers/finding.d.ts +10 -0
  19. package/dist/src/catalog/workspace/helpers/finding.js +11 -0
  20. package/dist/src/catalog/workspace/helpers/source-categorize.d.ts +16 -0
  21. package/dist/src/catalog/workspace/helpers/source-categorize.js +51 -0
  22. package/dist/src/catalog/workspace/hook-ownership-ambiguous.d.ts +4 -0
  23. package/dist/src/catalog/workspace/hook-ownership-ambiguous.js +25 -0
  24. package/dist/src/catalog/workspace/initialized.d.ts +22 -0
  25. package/dist/src/catalog/workspace/initialized.js +58 -0
  26. package/dist/src/catalog/workspace/instructions-agent-supported.d.ts +4 -0
  27. package/dist/src/catalog/workspace/instructions-agent-supported.js +38 -0
  28. package/dist/src/catalog/workspace/instructions-gitignore-current.d.ts +4 -0
  29. package/dist/src/catalog/workspace/instructions-gitignore-current.js +49 -0
  30. package/dist/src/catalog/workspace/instructions-source-present.d.ts +4 -0
  31. package/dist/src/catalog/workspace/instructions-source-present.js +41 -0
  32. package/dist/src/catalog/workspace/instructions-target-current.d.ts +4 -0
  33. package/dist/src/catalog/workspace/instructions-target-current.js +51 -0
  34. package/dist/src/catalog/workspace/instructions-target-stale.d.ts +9 -0
  35. package/dist/src/catalog/workspace/instructions-target-stale.js +41 -0
  36. package/dist/src/catalog/workspace/instructions-target-unowned.d.ts +10 -0
  37. package/dist/src/catalog/workspace/instructions-target-unowned.js +59 -0
  38. package/dist/src/catalog/workspace/knowledge-state-valid.d.ts +5 -0
  39. package/dist/src/catalog/workspace/knowledge-state-valid.js +65 -0
  40. package/dist/src/catalog/workspace/lockfile-valid.d.ts +5 -0
  41. package/dist/src/catalog/workspace/lockfile-valid.js +63 -0
  42. package/dist/src/catalog/workspace/managed-file-unowned.d.ts +4 -0
  43. package/dist/src/catalog/workspace/managed-file-unowned.js +25 -0
  44. package/dist/src/catalog/workspace/mcps-agent-drift.d.ts +4 -0
  45. package/dist/src/catalog/workspace/mcps-agent-drift.js +139 -0
  46. package/dist/src/catalog/workspace/mcps-agent-orphaned.d.ts +4 -0
  47. package/dist/src/catalog/workspace/mcps-agent-orphaned.js +64 -0
  48. package/dist/src/catalog/workspace/mcps-no-secret-literal.d.ts +4 -0
  49. package/dist/src/catalog/workspace/mcps-no-secret-literal.js +67 -0
  50. package/dist/src/catalog/workspace/mcps-shared-target-compatible.d.ts +4 -0
  51. package/dist/src/catalog/workspace/mcps-shared-target-compatible.js +58 -0
  52. package/dist/src/catalog/workspace/mcps-transport-exclusivity.d.ts +4 -0
  53. package/dist/src/catalog/workspace/mcps-transport-exclusivity.js +67 -0
  54. package/dist/src/catalog/workspace/packs-declarations-valid.d.ts +19 -0
  55. package/dist/src/catalog/workspace/packs-declarations-valid.js +110 -0
  56. package/dist/src/catalog/workspace/packs-dependencies-resolved.d.ts +5 -0
  57. package/dist/src/catalog/workspace/packs-dependencies-resolved.js +49 -0
  58. package/dist/src/catalog/workspace/projections-current.d.ts +15 -0
  59. package/dist/src/catalog/workspace/projections-current.js +35 -0
  60. package/dist/src/catalog/workspace/settings-keys-recognized.d.ts +17 -0
  61. package/dist/src/catalog/workspace/settings-keys-recognized.js +90 -0
  62. package/dist/src/catalog/workspace/settings-schema-valid.d.ts +27 -0
  63. package/dist/src/catalog/workspace/settings-schema-valid.js +68 -0
  64. package/dist/src/catalog/workspace/skills-artifacts-correct.d.ts +28 -0
  65. package/dist/src/catalog/workspace/skills-artifacts-correct.js +143 -0
  66. package/dist/src/catalog/workspace/skills-declarations-valid.d.ts +27 -0
  67. package/dist/src/catalog/workspace/skills-declarations-valid.js +102 -0
  68. package/dist/src/catalog/workspace/skills-integrity-valid.d.ts +28 -0
  69. package/dist/src/catalog/workspace/skills-integrity-valid.js +95 -0
  70. package/dist/src/catalog/workspace/skills-lockfile-aligned.d.ts +5 -0
  71. package/dist/src/catalog/workspace/skills-lockfile-aligned.js +64 -0
  72. package/dist/src/catalog/workspace/source-endpoints-aligned.d.ts +5 -0
  73. package/dist/src/catalog/workspace/source-endpoints-aligned.js +75 -0
  74. package/dist/src/catalog/workspace-fixtures/fixture-state.d.ts +8 -0
  75. package/dist/src/catalog/workspace-fixtures/fixture-state.js +120 -0
  76. package/dist/src/catalog/workspace-fixtures/interpret-ops.d.ts +11 -0
  77. package/dist/src/catalog/workspace-fixtures/interpret-ops.js +9 -0
  78. package/dist/src/catalog/workspace-read-model/lint-workspace.d.ts +191 -0
  79. package/dist/src/catalog/workspace-read-model/lint-workspace.js +651 -0
  80. package/dist/src/catalog/workspace-read-model/manifest-json.d.ts +14 -0
  81. package/dist/src/catalog/workspace-read-model/manifest-json.js +27 -0
  82. package/dist/src/catalog/workspace.d.ts +58 -0
  83. package/dist/src/catalog/workspace.js +164 -0
  84. package/dist/src/catalog-contexts.d.ts +106 -0
  85. package/dist/src/catalog-contexts.js +95 -0
  86. package/dist/src/cli.d.ts +239 -0
  87. package/dist/src/cli.js +1077 -0
  88. package/dist/src/index.d.ts +10 -0
  89. package/dist/src/index.js +21 -0
  90. package/dist/src/json-schema.d.ts +121 -0
  91. package/dist/src/json-schema.js +99 -0
  92. package/dist/src/run/errors.d.ts +23 -0
  93. package/dist/src/run/errors.js +21 -0
  94. package/dist/src/run/settings.d.ts +63 -0
  95. package/dist/src/run/settings.js +139 -0
  96. package/dist/src/run/staged-workspace.d.ts +31 -0
  97. package/dist/src/run/staged-workspace.js +273 -0
  98. package/dist/src/workspace-context.d.ts +129 -0
  99. package/dist/src/workspace-context.js +12 -0
  100. package/package.json +55 -0
@@ -0,0 +1,1077 @@
1
+ /**
2
+ * Lint runner — the reusable core of `axm lint`.
3
+ *
4
+ * The `axm lint` CLI command file is a thin surface over flag parsing and
5
+ * rendering; the logic that evaluates rule catalogs, renders findings, and
6
+ * detects publish-gate drift lives in this module.
7
+ *
8
+ * Lint engine entry points:
9
+ *
10
+ * - {@link evaluateAllCatalogs} — concurrent evaluation of the three
11
+ * v1 rule catalogs against pre-built contexts.
12
+ * - {@link summarizeEvaluations} — group, count, and derive the exit
13
+ * category from raw `Evaluated<*>` lists.
14
+ * - {@link detectPublishGateDrift} — compute whether the configured
15
+ * `LintConfig` weakens any `skill/*` / `pack/*` platform-default-`error`
16
+ * rule (task 5.7).
17
+ * - {@link renderFindingsText} — finding-first human renderer (task
18
+ * 5.6).
19
+ * - {@link toLintJsonDocument} — `--json` document shape (task 5.6).
20
+ * - {@link resolveLintExitCategory} — exit-code contract evaluator (task
21
+ * 5.9).
22
+ *
23
+ * Lint-intent → canonical `Operation` adapter composition happens in the CLI
24
+ * handler (`packages/cli/src/root/lint/handler.ts`), which re-resolves each
25
+ * intent's `source` via the `resolveConfigured*` helpers and hands the
26
+ * resulting canonical Operation to the per-extension plan-step builder. The
27
+ * runner here stays accessor-free.
28
+ *
29
+ * @experimental This API is unstable and may change without notice.
30
+ * @packageDocumentation
31
+ */
32
+ import * as Effect from "effect/Effect";
33
+ import { composePath } from "@agentxm/registry-protocol/unstable/lint/compose-path";
34
+ import { platformCanonicalLintConfig } from "@agentxm/registry-protocol/unstable/lint/config";
35
+ import { evaluateContexts } from "@agentxm/registry-protocol/unstable/lint/evaluate";
36
+ import { CATALOG_GROUP_ORDER, lintCatalogsForView, } from "./catalog-contexts.js";
37
+ // -----------------------------------------------------------------------------
38
+ // Evaluation
39
+ // -----------------------------------------------------------------------------
40
+ /**
41
+ * Evaluate every rule catalog against its contexts, concurrently.
42
+ *
43
+ * Catalogs run in parallel; findings stay in stable catalog order inside each
44
+ * group, and groups render in {@link CATALOG_GROUP_ORDER}.
45
+ *
46
+ * @experimental This API is unstable and may change without notice.
47
+ */
48
+ export const evaluateAllCatalogs = (args) => Effect.gen(function* () {
49
+ const catalogs = lintCatalogsForView(args.view);
50
+ const [skill, pack, subagent, mcpServer, hook, rule, knowledge, workspace] = yield* Effect.all([
51
+ evaluateContexts(catalogs.skill, args.contexts.skill, args.config),
52
+ evaluateContexts(catalogs.pack, args.contexts.pack, args.config),
53
+ evaluateContexts(catalogs.subagent, args.contexts.subagent, args.config),
54
+ evaluateContexts(catalogs["mcp-server"], args.contexts["mcp-server"], args.config),
55
+ evaluateContexts(catalogs.hook, args.contexts.hook, args.config),
56
+ evaluateContexts(catalogs.rule, args.contexts.rule, args.config),
57
+ evaluateContexts(catalogs.knowledge, args.contexts.knowledge, args.config),
58
+ evaluateContexts(catalogs.workspace, args.contexts.workspace, args.config),
59
+ ], { concurrency: "unbounded" });
60
+ return {
61
+ skill,
62
+ pack,
63
+ subagent,
64
+ "mcp-server": mcpServer,
65
+ hook,
66
+ rule,
67
+ knowledge,
68
+ workspace,
69
+ };
70
+ });
71
+ // -----------------------------------------------------------------------------
72
+ // Summary
73
+ // -----------------------------------------------------------------------------
74
+ const severityOrder = (s) => {
75
+ switch (s) {
76
+ case "error":
77
+ return 0;
78
+ case "warning":
79
+ return 1;
80
+ case "info":
81
+ return 2;
82
+ }
83
+ };
84
+ const flattenEvaluated = (group, evaluated) => {
85
+ const out = [];
86
+ for (const entry of evaluated) {
87
+ const displayRoot = entry.context.displayRoot;
88
+ for (const finding of entry.findings) {
89
+ out.push({
90
+ group,
91
+ ruleDescription: entry.rule.description,
92
+ displayRoot,
93
+ path: composePath(displayRoot, finding.location),
94
+ finding,
95
+ });
96
+ }
97
+ }
98
+ return out;
99
+ };
100
+ /**
101
+ * Flatten a {@link GroupEvaluations} record into a single `RenderedFinding[]`
102
+ * in stable group-then-catalog order.
103
+ *
104
+ * @experimental This API is unstable and may change without notice.
105
+ */
106
+ export const collectRenderedFindings = (evaluations) => CATALOG_GROUP_ORDER.flatMap((group) => flattenEvaluated(group, evaluations[group]));
107
+ /**
108
+ * Count findings by severity across every group.
109
+ *
110
+ * @experimental This API is unstable and may change without notice.
111
+ */
112
+ export const countFindings = (findings) => {
113
+ let errors = 0;
114
+ let warnings = 0;
115
+ let infos = 0;
116
+ for (const f of findings) {
117
+ switch (f.finding.severity) {
118
+ case "error":
119
+ errors += 1;
120
+ break;
121
+ case "warning":
122
+ warnings += 1;
123
+ break;
124
+ case "info":
125
+ infos += 1;
126
+ break;
127
+ }
128
+ }
129
+ return { total: findings.length, errors, warnings, infos };
130
+ };
131
+ /**
132
+ * Derive a full {@link LintSummary} (findings, counts, exit category, drift
133
+ * banner rule ids) from raw evaluations + the configured severity overrides.
134
+ *
135
+ * @experimental This API is unstable and may change without notice.
136
+ */
137
+ export const summarizeEvaluations = (evaluations, config) => {
138
+ const findings = collectRenderedFindings(evaluations);
139
+ const counts = countFindings(findings);
140
+ return {
141
+ findings,
142
+ counts,
143
+ exitCategory: exitCategoryFromCounts(counts),
144
+ driftBanner: detectPublishGateDrift(config),
145
+ };
146
+ };
147
+ const exitCategoryFromCounts = (counts) => {
148
+ if (counts.errors > 0) {
149
+ return "errors";
150
+ }
151
+ if (counts.warnings > 0) {
152
+ return "warnings";
153
+ }
154
+ return "clean";
155
+ };
156
+ // -----------------------------------------------------------------------------
157
+ // Exit-code contract (task 5.9)
158
+ // -----------------------------------------------------------------------------
159
+ /**
160
+ * Translate a {@link LintExitCategory} + `--strict` into the exit-code
161
+ * policy.
162
+ *
163
+ * | Category | --strict=false | --strict=true |
164
+ * | ------------ | -------------- | ------------- |
165
+ * | `"clean"` | `0` | `0` |
166
+ * | `"warnings"` | `0` | non-zero |
167
+ * | `"errors"` | non-zero | non-zero |
168
+ *
169
+ * The return value is a discriminated enum; the CLI handler maps the `"fail"`
170
+ * branch to its platform exit-code primitive.
171
+ *
172
+ * @experimental This API is unstable and may change without notice.
173
+ */
174
+ export const resolveLintExitCategory = (args) => {
175
+ switch (args.category) {
176
+ case "errors":
177
+ return "fail";
178
+ case "warnings":
179
+ return args.strict ? "fail" : "success";
180
+ case "clean":
181
+ return "success";
182
+ }
183
+ };
184
+ // -----------------------------------------------------------------------------
185
+ // Drift banner (task 5.7)
186
+ // -----------------------------------------------------------------------------
187
+ /**
188
+ * Identify every configured `lint.rules` entry that weakens a platform-canonical
189
+ * `error`-severity `skill/*` or `pack/*` rule.
190
+ *
191
+ * The publish gate runs the `skill/*` and `pack/*` catalogs against
192
+ * {@link platformCanonicalLintConfig}; any workspace override that lowers a
193
+ * rule in those namespaces from `error` to `off | info | warn` creates a
194
+ * publish-gate divergence the user should know about (they'll see `error`
195
+ * findings from the registry that don't appear locally).
196
+ *
197
+ * WorkspaceMutations-only rule weakenings (`workspace/*`) do NOT trigger the banner —
198
+ * those never reach publish.
199
+ *
200
+ * Returns the rule ids that trigger the banner, in catalog order, so the
201
+ * renderer can produce stable deterministic output.
202
+ *
203
+ * @experimental This API is unstable and may change without notice.
204
+ */
205
+ export const detectPublishGateDrift = (config) => {
206
+ void config;
207
+ return [];
208
+ };
209
+ const defaultHumanReporter = "grouped";
210
+ const compareRenderedFindings = (left, right) => {
211
+ const byPath = left.path.localeCompare(right.path);
212
+ if (byPath !== 0) {
213
+ return byPath;
214
+ }
215
+ const bySeverity = severityOrder(left.finding.severity) - severityOrder(right.finding.severity);
216
+ if (bySeverity !== 0) {
217
+ return bySeverity;
218
+ }
219
+ const byRuleId = left.finding.ruleId.localeCompare(right.finding.ruleId);
220
+ if (byRuleId !== 0) {
221
+ return byRuleId;
222
+ }
223
+ return left.finding.message.localeCompare(right.finding.message);
224
+ };
225
+ const pluralize = (n, singular, plural) => n === 1 ? singular : plural;
226
+ const splitSentences = (message) => {
227
+ const out = [];
228
+ let remaining = message.trim();
229
+ while (remaining.length > 0) {
230
+ const match = /^(.+?\.(?=\s+[A-Z`]))\s+(.+)$/.exec(remaining);
231
+ if (match === null) {
232
+ out.push(remaining);
233
+ break;
234
+ }
235
+ const head = match[1];
236
+ const tail = match[2];
237
+ if (head === undefined || tail === undefined) {
238
+ out.push(remaining);
239
+ break;
240
+ }
241
+ out.push(head);
242
+ remaining = tail;
243
+ }
244
+ return out;
245
+ };
246
+ const splitDetailClause = (message) => {
247
+ const marker = " Detail: ";
248
+ const index = message.indexOf(marker);
249
+ if (index === -1) {
250
+ return {
251
+ lead: message,
252
+ detail: undefined,
253
+ trailing: [],
254
+ };
255
+ }
256
+ const lead = message.slice(0, index);
257
+ const rest = message.slice(index + marker.length);
258
+ const sentences = splitSentences(rest);
259
+ const detail = sentences[0];
260
+ return {
261
+ lead,
262
+ detail,
263
+ trailing: sentences.slice(1),
264
+ };
265
+ };
266
+ const parseFindingMessage = (message) => {
267
+ const detailSplit = splitDetailClause(message);
268
+ const leadSentences = splitSentences(detailSplit.lead);
269
+ const title = leadSentences[0] ?? message.trim();
270
+ const details = detailSplit.detail === undefined ? [] : [detailSplit.detail];
271
+ return {
272
+ title,
273
+ details,
274
+ helps: [...leadSentences.slice(1), ...detailSplit.trailing],
275
+ };
276
+ };
277
+ const dirnamePosix = (path) => {
278
+ if (path === "." || path === "..") {
279
+ return path;
280
+ }
281
+ const index = path.lastIndexOf("/");
282
+ if (index <= 0) {
283
+ return path;
284
+ }
285
+ return path.slice(0, index);
286
+ };
287
+ const groupDisplayPath = (entry) => {
288
+ switch (entry.finding.ruleId) {
289
+ case "workspace/skills-managed":
290
+ return dirnamePosix(entry.path);
291
+ default:
292
+ return entry.path;
293
+ }
294
+ };
295
+ const bucketForFinding = (entry, parsed) => {
296
+ switch (entry.finding.ruleId) {
297
+ case "workspace/lockfile-valid":
298
+ if (parsed.title.startsWith("Lockfile is missing required field `")) {
299
+ return "missing-required-field";
300
+ }
301
+ if (parsed.title.startsWith("The lockfile is not valid YAML.")) {
302
+ return "invalid-yaml";
303
+ }
304
+ return "validation";
305
+ case "workspace/skills-managed":
306
+ if (parsed.title.includes("but it is not managed by this workspace.")) {
307
+ return "unmanaged";
308
+ }
309
+ return "managed";
310
+ case "workspace/skills-artifacts-correct":
311
+ return "artifact-state";
312
+ case "workspace/skills-lockfile-aligned":
313
+ if (parsed.title.includes("missing from the lockfile.")) {
314
+ return "missing";
315
+ }
316
+ if (parsed.title.includes("listed in the lockfile but not in settings.skills.")) {
317
+ return "orphan";
318
+ }
319
+ if (parsed.title.includes("lockfile version does not match the declared version.")) {
320
+ return "version";
321
+ }
322
+ return "alignment";
323
+ case "workspace/skills-integrity-valid":
324
+ return "integrity";
325
+ case "workspace/instructions-target-current":
326
+ return "target-health";
327
+ default:
328
+ return entry.finding.ruleId;
329
+ }
330
+ };
331
+ const parseHumanFinding = (entry) => {
332
+ const parsed = parseFindingMessage(entry.finding.message);
333
+ return {
334
+ path: groupDisplayPath(entry),
335
+ bucket: bucketForFinding(entry, parsed),
336
+ severity: entry.finding.severity,
337
+ ruleId: entry.finding.ruleId,
338
+ title: parsed.title,
339
+ details: parsed.details,
340
+ helps: parsed.helps,
341
+ fixable: false,
342
+ };
343
+ };
344
+ const matchSingleQuoted = (message) => {
345
+ const match = /'([^']+)'/.exec(message);
346
+ return match?.[1];
347
+ };
348
+ const uniqueStrings = (values) => {
349
+ const out = [];
350
+ const seen = new Set();
351
+ for (const value of values) {
352
+ if (seen.has(value)) {
353
+ continue;
354
+ }
355
+ seen.add(value);
356
+ out.push(value);
357
+ }
358
+ return out;
359
+ };
360
+ const sortStrings = (values) => [...values].sort();
361
+ const uniquePaths = (findings) => uniqueStrings(findings.map((finding) => finding.path));
362
+ const mergedRuleHelps = (findings, autofixHelp) => {
363
+ if (findings.every((finding) => finding.fixable)) {
364
+ return [autofixHelp];
365
+ }
366
+ const helps = uniqueStrings(findings.flatMap((finding) => finding.helps));
367
+ return helps.length > 0
368
+ ? helps
369
+ : findings.some((finding) => finding.fixable)
370
+ ? [autofixHelp]
371
+ : [];
372
+ };
373
+ const summarizeSkillByDetail = (finding) => {
374
+ const name = matchSingleQuoted(finding.title);
375
+ const detail = finding.details[0];
376
+ if (name === undefined) {
377
+ return detail ?? finding.title;
378
+ }
379
+ if (detail === undefined) {
380
+ return name;
381
+ }
382
+ return `${name}: ${detail}`;
383
+ };
384
+ const compressDetails = (details, limit = 10) => {
385
+ if (details.length <= limit) {
386
+ return details;
387
+ }
388
+ const remaining = details.length - limit;
389
+ return [...details.slice(0, limit), `... and ${remaining} more`];
390
+ };
391
+ const previewList = (values, limit = 3) => {
392
+ if (values.length === 0) {
393
+ return "";
394
+ }
395
+ if (values.length <= limit) {
396
+ return values.join(", ");
397
+ }
398
+ const remaining = values.length - limit;
399
+ return `${values.slice(0, limit).join(", ")}, ... and ${remaining} more`;
400
+ };
401
+ const groupFindingsByPath = (findings, extract) => {
402
+ const grouped = new Map();
403
+ for (const finding of findings) {
404
+ const current = grouped.get(finding.path);
405
+ const value = extract(finding);
406
+ if (current === undefined) {
407
+ grouped.set(finding.path, [value]);
408
+ }
409
+ else {
410
+ current.push(value);
411
+ }
412
+ }
413
+ return [...grouped.entries()].sort(([left], [right]) => left.localeCompare(right));
414
+ };
415
+ const coalesceFullDiagnostic = (findings) => {
416
+ const [first] = findings;
417
+ if (first === undefined) {
418
+ return {
419
+ severity: "info",
420
+ ruleId: "",
421
+ title: "",
422
+ details: [],
423
+ helps: [],
424
+ fixable: false,
425
+ paths: [],
426
+ };
427
+ }
428
+ if (findings.length === 1) {
429
+ return {
430
+ severity: first.severity,
431
+ ruleId: first.ruleId,
432
+ title: first.title,
433
+ details: first.details,
434
+ helps: first.helps,
435
+ fixable: first.fixable,
436
+ paths: [first.path],
437
+ };
438
+ }
439
+ const allHelps = uniqueStrings(findings.flatMap((finding) => finding.helps));
440
+ const paths = uniquePaths(findings);
441
+ switch (`${first.ruleId}:${first.bucket}`) {
442
+ case "workspace/lockfile-valid:missing-required-field": {
443
+ const fields = findings.flatMap((finding) => {
444
+ const match = /Lockfile is missing required field `([^`]+)`\./.exec(finding.title);
445
+ return match?.[1] === undefined ? [] : [match[1]];
446
+ });
447
+ return {
448
+ severity: first.severity,
449
+ ruleId: first.ruleId,
450
+ title: "Lockfile is missing required fields.",
451
+ details: compressDetails(fields),
452
+ helps: [
453
+ "Regenerate the workspace lockfile from workspace settings by reinstalling the declared extensions.",
454
+ ],
455
+ fixable: false,
456
+ paths,
457
+ };
458
+ }
459
+ case "workspace/skills-managed:unmanaged": {
460
+ const names = findings.flatMap((finding) => {
461
+ const name = matchSingleQuoted(finding.title);
462
+ return name === undefined ? [] : [name];
463
+ });
464
+ return {
465
+ severity: first.severity,
466
+ ruleId: first.ruleId,
467
+ title: `${names.length} ${pluralize(names.length, "skill is", "skills are")} present here but not managed by this workspace.`,
468
+ details: compressDetails(names),
469
+ helps: [
470
+ "Review ownership before changing each unowned skill.",
471
+ "Use `axm adopt @owner/skills/<name>` to transfer an existing canonical package into AXM ownership.",
472
+ ],
473
+ fixable: false,
474
+ paths,
475
+ };
476
+ }
477
+ case "workspace/skills-artifacts-correct:enabled-missing":
478
+ case "workspace/skills-artifacts-correct:disabled-present":
479
+ case "workspace/skills-artifacts-correct:inconsistent":
480
+ case "workspace/skills-artifacts-correct:artifact-state":
481
+ return {
482
+ severity: first.severity,
483
+ ruleId: first.ruleId,
484
+ title: `${findings.length} ${pluralize(findings.length, "skill is", "skills are")} inconsistent across the declared agents.`,
485
+ details: compressDetails(findings.map(summarizeSkillByDetail)),
486
+ helps: mergedRuleHelps(findings, "Run `axm sync` to reconcile the declared agent artifacts."),
487
+ fixable: findings.some((finding) => finding.fixable),
488
+ paths,
489
+ };
490
+ case "workspace/skills-lockfile-aligned:missing":
491
+ case "workspace/skills-lockfile-aligned:orphan":
492
+ case "workspace/skills-lockfile-aligned:version":
493
+ case "workspace/skills-lockfile-aligned:alignment":
494
+ return {
495
+ severity: first.severity,
496
+ ruleId: first.ruleId,
497
+ title: "Skill declarations and lockfile entries are out of sync.",
498
+ details: compressDetails(findings.map(summarizeSkillByDetail)),
499
+ helps: ["Run `axm sync` to reconcile desired skills with accepted resolutions."],
500
+ fixable: true,
501
+ paths,
502
+ };
503
+ case "workspace/skills-integrity-valid:integrity":
504
+ return {
505
+ severity: first.severity,
506
+ ruleId: first.ruleId,
507
+ title: "Skills listed in the lockfile are missing their installed sources.",
508
+ details: compressDetails(findings.map(summarizeSkillByDetail)),
509
+ helps: mergedRuleHelps(findings, "Run `axm sync` to restore the affected skills."),
510
+ fixable: findings.some((finding) => finding.fixable),
511
+ paths,
512
+ };
513
+ default:
514
+ return {
515
+ severity: first.severity,
516
+ ruleId: first.ruleId,
517
+ title: `${findings.length} findings reported for this rule.`,
518
+ details: compressDetails(findings.map((finding) => finding.details.length === 0
519
+ ? finding.title
520
+ : `${finding.title}: ${finding.details.join("; ")}`)),
521
+ helps: allHelps,
522
+ fixable: findings.some((finding) => finding.fixable),
523
+ paths,
524
+ };
525
+ }
526
+ };
527
+ const coalesceGroupedDiagnostic = (findings) => {
528
+ const [first] = findings;
529
+ if (first === undefined) {
530
+ return {
531
+ severity: "info",
532
+ ruleId: "",
533
+ title: "",
534
+ details: [],
535
+ helps: [],
536
+ fixable: false,
537
+ paths: [],
538
+ };
539
+ }
540
+ if (findings.length === 1) {
541
+ return {
542
+ severity: first.severity,
543
+ ruleId: first.ruleId,
544
+ title: first.title,
545
+ details: first.details,
546
+ helps: first.helps,
547
+ fixable: first.fixable,
548
+ paths: [first.path],
549
+ };
550
+ }
551
+ const paths = uniquePaths(findings);
552
+ const allHelps = uniqueStrings(findings.flatMap((finding) => finding.helps));
553
+ switch (`${first.ruleId}:${first.bucket}`) {
554
+ case "workspace/lockfile-valid:missing-required-field": {
555
+ const fields = sortStrings(uniqueStrings(findings.flatMap((finding) => {
556
+ const match = /Lockfile is missing required field `([^`]+)`\./.exec(finding.title);
557
+ return match?.[1] === undefined ? [] : [match[1]];
558
+ })));
559
+ return {
560
+ severity: first.severity,
561
+ ruleId: first.ruleId,
562
+ title: "Lockfile is missing fields required by the current schema.",
563
+ details: [`Missing fields include: ${previewList(fields, 4)}`],
564
+ helps: [
565
+ "Fix: Regenerate the workspace lockfile from workspace settings by reinstalling the declared extensions.",
566
+ ],
567
+ fixable: false,
568
+ paths,
569
+ };
570
+ }
571
+ case "workspace/skills-managed:unmanaged": {
572
+ const perPath = groupFindingsByPath(findings, (finding) => matchSingleQuoted(finding.title) ?? finding.title);
573
+ return {
574
+ severity: first.severity,
575
+ ruleId: first.ruleId,
576
+ title: `Unmanaged skills are present in ${paths.length} ${pluralize(paths.length, "skill directory", "skill directories")}.`,
577
+ details: compressDetails(perPath.map(([path, names]) => {
578
+ const sorted = sortStrings(uniqueStrings(names));
579
+ return `${path}: ${sorted.length} unmanaged ${pluralize(sorted.length, "skill", "skills")} (${previewList(sorted, 3)})`;
580
+ }), 8),
581
+ helps: [
582
+ "Review ownership before changing each unowned skill.",
583
+ "Use `axm adopt @owner/skills/<name>` to transfer an existing canonical package into AXM ownership.",
584
+ ],
585
+ fixable: false,
586
+ paths,
587
+ };
588
+ }
589
+ case "workspace/skills-artifacts-correct:enabled-missing":
590
+ case "workspace/skills-artifacts-correct:disabled-present":
591
+ case "workspace/skills-artifacts-correct:inconsistent":
592
+ case "workspace/skills-artifacts-correct:artifact-state":
593
+ return {
594
+ severity: first.severity,
595
+ ruleId: first.ruleId,
596
+ title: `${findings.length} ${pluralize(findings.length, "skill is", "skills are")} inconsistent across the declared agents.`,
597
+ details: compressDetails(findings.map(summarizeSkillByDetail)),
598
+ helps: mergedRuleHelps(findings, "Run `axm sync` to reconcile the declared agent artifacts."),
599
+ fixable: findings.some((finding) => finding.fixable),
600
+ paths,
601
+ };
602
+ case "workspace/skills-lockfile-aligned:missing":
603
+ case "workspace/skills-lockfile-aligned:orphan":
604
+ case "workspace/skills-lockfile-aligned:version":
605
+ case "workspace/skills-lockfile-aligned:alignment":
606
+ return {
607
+ severity: first.severity,
608
+ ruleId: first.ruleId,
609
+ title: "`settings.skills` and the lockfile are out of sync.",
610
+ details: compressDetails(findings.map(summarizeSkillByDetail)),
611
+ helps: ["Run `axm sync` to reconcile desired skills with accepted resolutions."],
612
+ fixable: true,
613
+ paths,
614
+ };
615
+ case "workspace/skills-integrity-valid:integrity":
616
+ return {
617
+ severity: first.severity,
618
+ ruleId: first.ruleId,
619
+ title: "Skills listed in the lockfile are missing their installed sources.",
620
+ details: compressDetails(findings.map(summarizeSkillByDetail)),
621
+ helps: mergedRuleHelps(findings, "Run `axm sync` to restore the affected skills."),
622
+ fixable: findings.some((finding) => finding.fixable),
623
+ paths,
624
+ };
625
+ default:
626
+ return {
627
+ severity: first.severity,
628
+ ruleId: first.ruleId,
629
+ title: findings.length === 1
630
+ ? first.title
631
+ : `${findings.length} related findings were reported.`,
632
+ details: compressDetails(findings.map((finding) => {
633
+ const detail = finding.details.length === 0
634
+ ? finding.title
635
+ : `${finding.title}: ${finding.details.join("; ")}`;
636
+ return paths.length === 1 ? detail : `${finding.path}: ${detail}`;
637
+ })),
638
+ helps: allHelps,
639
+ fixable: findings.some((finding) => finding.fixable),
640
+ paths,
641
+ };
642
+ }
643
+ };
644
+ /**
645
+ * Repairs whose desired state is fully determined by authoritative local state,
646
+ * keyed by rule. Naming the operation is a reporting concern, so it lives here
647
+ * rather than in the rule, which states the intrinsic fact alone.
648
+ */
649
+ const DETERMINED_REPAIRS = {
650
+ "workspace/instructions-target-current": "Fix: Run `axm lint --fix` to regenerate the instruction files from their canonical source.",
651
+ "workspace/instructions-target-stale": "Fix: Run `axm lint --fix` to remove AXM-owned instruction files the configuration no longer needs.",
652
+ };
653
+ const withDeterminedRepair = (diagnostic) => {
654
+ const repair = DETERMINED_REPAIRS[diagnostic.ruleId];
655
+ if (repair === undefined)
656
+ return diagnostic;
657
+ return {
658
+ ...diagnostic,
659
+ helps: diagnostic.helps.includes(repair) ? diagnostic.helps : [...diagnostic.helps, repair],
660
+ fixable: true,
661
+ };
662
+ };
663
+ const joinList = (values) => {
664
+ if (values.length === 0) {
665
+ return "";
666
+ }
667
+ if (values.length === 1) {
668
+ return values[0] ?? "";
669
+ }
670
+ if (values.length === 2) {
671
+ const first = values[0] ?? "";
672
+ const second = values[1] ?? "";
673
+ return `${first} and ${second}`;
674
+ }
675
+ const head = values.slice(0, -1).join(", ");
676
+ const tail = values[values.length - 1] ?? "";
677
+ return `${head}, and ${tail}`;
678
+ };
679
+ const formatFullOverviewSentence = (args) => {
680
+ const parts = [];
681
+ if (args.counts.errors > 0) {
682
+ parts.push(`${args.counts.errors} ${pluralize(args.counts.errors, "error", "errors")}`);
683
+ }
684
+ if (args.counts.warnings > 0) {
685
+ parts.push(`${args.counts.warnings} ${pluralize(args.counts.warnings, "warning", "warnings")}`);
686
+ }
687
+ if (args.counts.infos > 0) {
688
+ parts.push(`${args.counts.infos} ${pluralize(args.counts.infos, "info", "infos")}`);
689
+ }
690
+ const locations = `${args.locationCount} ${pluralize(args.locationCount, "location", "locations")}`;
691
+ const base = `Found ${joinList(parts)} in ${locations}.`;
692
+ if (args.fixableCount === 0) {
693
+ return base;
694
+ }
695
+ return `${base} ${args.fixableCount} ${pluralize(args.fixableCount, "finding can", "findings can")} be auto-fixed.`;
696
+ };
697
+ const formatGroupedOverviewSentence = (args) => {
698
+ const parts = [`${args.diagnosticCount} ${pluralize(args.diagnosticCount, "issue", "issues")}.`];
699
+ if (args.fixableCount > 0) {
700
+ parts.push(`${args.fixableCount} ${pluralize(args.fixableCount, "can", "can")} be fixed automatically.`);
701
+ }
702
+ const manualCount = args.diagnosticCount - args.fixableCount;
703
+ if (manualCount > 0) {
704
+ parts.push(`${manualCount} ${pluralize(manualCount, "needs", "need")} manual attention.`);
705
+ }
706
+ return parts.join(" ");
707
+ };
708
+ const buildFullDiagnostics = (parsed) => {
709
+ const pathOrder = [];
710
+ const byPath = new Map();
711
+ for (const entry of parsed) {
712
+ const current = byPath.get(entry.path);
713
+ if (current === undefined) {
714
+ byPath.set(entry.path, [entry]);
715
+ pathOrder.push(entry.path);
716
+ }
717
+ else {
718
+ current.push(entry);
719
+ }
720
+ }
721
+ const blocks = [];
722
+ pathOrder.forEach((path, index) => {
723
+ const pathEntries = byPath.get(path);
724
+ if (pathEntries === undefined) {
725
+ return;
726
+ }
727
+ const groups = new Map();
728
+ const groupOrder = [];
729
+ for (const entry of pathEntries) {
730
+ const key = `${severityOrder(entry.severity)}:${entry.ruleId}:${entry.bucket}`;
731
+ const current = groups.get(key);
732
+ if (current === undefined) {
733
+ groups.set(key, [entry]);
734
+ groupOrder.push(key);
735
+ }
736
+ else {
737
+ current.push(entry);
738
+ }
739
+ }
740
+ blocks.push({
741
+ kind: "pathGroup",
742
+ path,
743
+ diagnostics: groupOrder.flatMap((key) => {
744
+ const grouped = groups.get(key);
745
+ return grouped === undefined ? [] : [withDeterminedRepair(coalesceFullDiagnostic(grouped))];
746
+ }),
747
+ });
748
+ if (index < pathOrder.length - 1) {
749
+ blocks.push({ kind: "blank" });
750
+ }
751
+ });
752
+ return blocks;
753
+ };
754
+ const groupedBucketKey = (entry) => {
755
+ switch (entry.ruleId) {
756
+ case "workspace/skills-managed":
757
+ return `${entry.ruleId}:${entry.bucket}`;
758
+ default:
759
+ return `${entry.path}:${entry.ruleId}:${entry.bucket}`;
760
+ }
761
+ };
762
+ const buildGroupedDiagnostics = (parsed) => {
763
+ const groups = new Map();
764
+ const order = [];
765
+ for (const entry of parsed) {
766
+ const key = groupedBucketKey(entry);
767
+ const current = groups.get(key);
768
+ if (current === undefined) {
769
+ groups.set(key, [entry]);
770
+ order.push(key);
771
+ }
772
+ else {
773
+ current.push(entry);
774
+ }
775
+ }
776
+ return order.flatMap((key) => {
777
+ const grouped = groups.get(key);
778
+ return grouped === undefined ? [] : [withDeterminedRepair(coalesceGroupedDiagnostic(grouped))];
779
+ });
780
+ };
781
+ const appendDiagnosticSection = (blocks, title, diagnostics, note) => {
782
+ if (diagnostics.length === 0) {
783
+ return;
784
+ }
785
+ if (blocks.length > 0) {
786
+ blocks.push({ kind: "blank" });
787
+ }
788
+ blocks.push(note === undefined ? { kind: "section", title } : { kind: "section", title, note });
789
+ diagnostics.forEach((diagnostic, index) => {
790
+ blocks.push({ kind: "diagnostic", diagnostic });
791
+ if (index < diagnostics.length - 1) {
792
+ blocks.push({ kind: "blank" });
793
+ }
794
+ });
795
+ };
796
+ const buildSectionedDiagnostics = (args) => {
797
+ const manual = args.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
798
+ const warnings = args.diagnostics.filter((diagnostic) => diagnostic.severity === "warning");
799
+ const infos = args.diagnostics.filter((diagnostic) => diagnostic.severity === "info");
800
+ appendDiagnosticSection(args.blocks, "Requires manual attention", manual);
801
+ appendDiagnosticSection(args.blocks, "Warnings", warnings);
802
+ appendDiagnosticSection(args.blocks, "Information", infos);
803
+ };
804
+ const makeSummaryDiagnostic = (diagnostic) => ({
805
+ ...diagnostic,
806
+ details: diagnostic.paths.length === 1
807
+ ? ["1 affected location"]
808
+ : [
809
+ `${diagnostic.paths.length} affected ${pluralize(diagnostic.paths.length, "location", "locations")}`,
810
+ ],
811
+ helps: [],
812
+ });
813
+ const toFullLintHumanBlocks = (args) => {
814
+ const blocks = [];
815
+ const { summary } = args;
816
+ if (summary.findings.length === 0) {
817
+ blocks.push(summary.driftBanner.length === 0
818
+ ? { kind: "empty", message: "No findings." }
819
+ : { kind: "empty", message: "No local findings." });
820
+ }
821
+ else {
822
+ const parsed = [...summary.findings].sort(compareRenderedFindings).map(parseHumanFinding);
823
+ const locationCount = uniqueStrings(parsed.map((finding) => finding.path)).length;
824
+ const fixableCount = 0;
825
+ blocks.push({
826
+ kind: "overview",
827
+ message: formatFullOverviewSentence({
828
+ counts: summary.counts,
829
+ locationCount,
830
+ fixableCount,
831
+ }),
832
+ counts: summary.counts,
833
+ notes: [],
834
+ });
835
+ if (summary.driftBanner.length > 0) {
836
+ blocks.push({ kind: "blank" });
837
+ blocks.push({
838
+ kind: "driftBanner",
839
+ title: "The registry will still block publish on these rules:",
840
+ ruleIds: summary.driftBanner,
841
+ });
842
+ }
843
+ const diagnostics = buildFullDiagnostics(parsed);
844
+ if (diagnostics.length > 0) {
845
+ blocks.push({ kind: "blank" });
846
+ blocks.push(...diagnostics);
847
+ }
848
+ }
849
+ if (summary.findings.length === 0 && summary.driftBanner.length > 0) {
850
+ blocks.push({ kind: "blank" });
851
+ blocks.push({
852
+ kind: "driftBanner",
853
+ title: "The registry will still block publish on these rules:",
854
+ ruleIds: summary.driftBanner,
855
+ });
856
+ }
857
+ return blocks;
858
+ };
859
+ const toGroupedLintHumanBlocks = (args) => {
860
+ const blocks = [];
861
+ const { summary } = args;
862
+ if (summary.findings.length === 0) {
863
+ blocks.push(summary.driftBanner.length === 0
864
+ ? { kind: "empty", message: "No findings." }
865
+ : { kind: "empty", message: "No local findings." });
866
+ }
867
+ else {
868
+ const parsed = [...summary.findings].sort(compareRenderedFindings).map(parseHumanFinding);
869
+ const diagnostics = buildGroupedDiagnostics(parsed);
870
+ const fixableCount = diagnostics.filter((diagnostic) => diagnostic.fixable).length;
871
+ blocks.push({
872
+ kind: "overview",
873
+ message: formatGroupedOverviewSentence({
874
+ diagnosticCount: diagnostics.length,
875
+ fixableCount,
876
+ }),
877
+ counts: summary.counts,
878
+ notes: [],
879
+ });
880
+ if (summary.driftBanner.length > 0) {
881
+ blocks.push({ kind: "blank" });
882
+ blocks.push({
883
+ kind: "driftBanner",
884
+ title: "The registry will still block publish on these rules:",
885
+ ruleIds: summary.driftBanner,
886
+ });
887
+ }
888
+ buildSectionedDiagnostics({ blocks, diagnostics });
889
+ }
890
+ if (summary.findings.length === 0 && summary.driftBanner.length > 0) {
891
+ blocks.push({ kind: "blank" });
892
+ blocks.push({
893
+ kind: "driftBanner",
894
+ title: "The registry will still block publish on these rules:",
895
+ ruleIds: summary.driftBanner,
896
+ });
897
+ }
898
+ if (summary.findings.length > 0) {
899
+ blocks.push({ kind: "blank" });
900
+ blocks.push({
901
+ kind: "footer",
902
+ message: "More output: `axm lint --details` | `axm lint --json`",
903
+ });
904
+ }
905
+ return blocks;
906
+ };
907
+ const toSummaryLintHumanBlocks = (args) => {
908
+ const blocks = [];
909
+ const { summary } = args;
910
+ if (summary.findings.length === 0) {
911
+ blocks.push(summary.driftBanner.length === 0
912
+ ? { kind: "empty", message: "No findings." }
913
+ : { kind: "empty", message: "No local findings." });
914
+ }
915
+ else {
916
+ const parsed = [...summary.findings].sort(compareRenderedFindings).map(parseHumanFinding);
917
+ const diagnostics = buildGroupedDiagnostics(parsed).map(makeSummaryDiagnostic);
918
+ const fixableCount = diagnostics.filter((diagnostic) => diagnostic.fixable).length;
919
+ blocks.push({
920
+ kind: "overview",
921
+ message: formatGroupedOverviewSentence({
922
+ diagnosticCount: diagnostics.length,
923
+ fixableCount,
924
+ }),
925
+ counts: summary.counts,
926
+ notes: [],
927
+ });
928
+ if (summary.driftBanner.length > 0) {
929
+ blocks.push({ kind: "blank" });
930
+ blocks.push({
931
+ kind: "driftBanner",
932
+ title: "The registry will still block publish on these rules:",
933
+ ruleIds: summary.driftBanner,
934
+ });
935
+ }
936
+ buildSectionedDiagnostics({ blocks, diagnostics });
937
+ }
938
+ if (summary.findings.length === 0 && summary.driftBanner.length > 0) {
939
+ blocks.push({ kind: "blank" });
940
+ blocks.push({
941
+ kind: "driftBanner",
942
+ title: "The registry will still block publish on these rules:",
943
+ ruleIds: summary.driftBanner,
944
+ });
945
+ }
946
+ return blocks;
947
+ };
948
+ export const toLintHumanBlocks = (args) => {
949
+ switch (args.reporter ?? defaultHumanReporter) {
950
+ case "full":
951
+ return toFullLintHumanBlocks(args);
952
+ case "summary":
953
+ return toSummaryLintHumanBlocks(args);
954
+ case "grouped":
955
+ return toGroupedLintHumanBlocks(args);
956
+ }
957
+ };
958
+ export const renderFindingsText = (args) => {
959
+ const lines = [];
960
+ for (const block of toLintHumanBlocks(args)) {
961
+ switch (block.kind) {
962
+ case "overview":
963
+ lines.push(block.message);
964
+ for (const note of block.notes) {
965
+ lines.push(note);
966
+ }
967
+ break;
968
+ case "driftBanner":
969
+ lines.push(`DRIFT: ${block.title}`);
970
+ for (const id of block.ruleIds) {
971
+ lines.push(` - ${id}`);
972
+ }
973
+ break;
974
+ case "section": {
975
+ const label = block.note !== undefined ? `${block.title} (${block.note})` : block.title;
976
+ lines.push(label);
977
+ break;
978
+ }
979
+ case "diagnostic": {
980
+ const location = block.diagnostic.paths.length === 1
981
+ ? (block.diagnostic.paths[0] ?? "")
982
+ : block.diagnostic.paths.length > 1
983
+ ? `(${block.diagnostic.paths.length} locations)`
984
+ : "(workspace)";
985
+ lines.push(` [${block.diagnostic.severity}] ${location}`);
986
+ lines.push(` rule: ${block.diagnostic.ruleId}${block.diagnostic.fixable ? " (auto-fixable)" : ""}`);
987
+ lines.push(` ${block.diagnostic.title}`);
988
+ for (const detail of block.diagnostic.details) {
989
+ lines.push(` - ${detail}`);
990
+ }
991
+ for (const help of block.diagnostic.helps) {
992
+ lines.push(` ${help}`);
993
+ }
994
+ break;
995
+ }
996
+ case "pathGroup":
997
+ lines.push(block.path);
998
+ for (const diagnostic of block.diagnostics) {
999
+ lines.push(` [${diagnostic.severity}] ${diagnostic.ruleId}${diagnostic.fixable ? " (auto-fixable)" : ""}: ${diagnostic.title}`);
1000
+ for (const detail of diagnostic.details) {
1001
+ lines.push(` - ${detail}`);
1002
+ }
1003
+ for (const help of diagnostic.helps) {
1004
+ lines.push(` ${help}`);
1005
+ }
1006
+ }
1007
+ break;
1008
+ case "blank":
1009
+ lines.push("");
1010
+ break;
1011
+ case "empty":
1012
+ lines.push(block.message);
1013
+ break;
1014
+ case "footer":
1015
+ lines.push(block.message);
1016
+ break;
1017
+ }
1018
+ }
1019
+ return lines;
1020
+ };
1021
+ // -----------------------------------------------------------------------------
1022
+ // JSON document (task 5.6)
1023
+ // -----------------------------------------------------------------------------
1024
+ const toJsonFinding = (entry) => {
1025
+ const base = {
1026
+ group: entry.group,
1027
+ kind: entry.finding.kind,
1028
+ ruleId: entry.finding.ruleId,
1029
+ severity: entry.finding.severity,
1030
+ message: entry.finding.message,
1031
+ displayRoot: entry.displayRoot,
1032
+ path: entry.path,
1033
+ subject: entry.path,
1034
+ authority: entry.finding.location?.file ?? entry.displayRoot,
1035
+ observed: entry.finding.message,
1036
+ expected: entry.ruleDescription,
1037
+ };
1038
+ if (entry.finding.location === undefined) {
1039
+ return base;
1040
+ }
1041
+ const loc = {
1042
+ file: entry.finding.location.file,
1043
+ ...(entry.finding.location.line !== undefined ? { line: entry.finding.location.line } : {}),
1044
+ ...(entry.finding.location.column !== undefined
1045
+ ? { column: entry.finding.location.column }
1046
+ : {}),
1047
+ };
1048
+ return { ...base, location: loc };
1049
+ };
1050
+ /**
1051
+ * Build the `--json` document from a {@link LintSummary}.
1052
+ *
1053
+ * @experimental This API is unstable and may change without notice.
1054
+ */
1055
+ export const toLintJsonDocument = (args) => {
1056
+ const { summary } = args;
1057
+ return {
1058
+ input: args.input,
1059
+ ...(args.axmSkillCompatibility === undefined
1060
+ ? {}
1061
+ : { axmSkillCompatibility: args.axmSkillCompatibility }),
1062
+ findings: summary.findings.map(toJsonFinding),
1063
+ summary: {
1064
+ total: summary.counts.total,
1065
+ errors: summary.counts.errors,
1066
+ warnings: summary.counts.warnings,
1067
+ infos: summary.counts.infos,
1068
+ exitCategory: summary.exitCategory,
1069
+ },
1070
+ driftBanner: summary.driftBanner,
1071
+ };
1072
+ };
1073
+ // -----------------------------------------------------------------------------
1074
+ // Re-exports that CLI callers use alongside the runner
1075
+ // -----------------------------------------------------------------------------
1076
+ export { platformCanonicalLintConfig };
1077
+ //# sourceMappingURL=cli.js.map