@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.
- package/LICENSE +110 -0
- package/dist/src/catalog/index.d.ts +31 -0
- package/dist/src/catalog/index.js +60 -0
- package/dist/src/catalog/workspace/agents-detected-declared.d.ts +23 -0
- package/dist/src/catalog/workspace/agents-detected-declared.js +61 -0
- package/dist/src/catalog/workspace/agents-recognized.d.ts +23 -0
- package/dist/src/catalog/workspace/agents-recognized.js +64 -0
- package/dist/src/catalog/workspace/axm-skill-compatible.d.ts +4 -0
- package/dist/src/catalog/workspace/axm-skill-compatible.js +51 -0
- package/dist/src/catalog/workspace/configured-but-not-installed.d.ts +4 -0
- package/dist/src/catalog/workspace/configured-but-not-installed.js +88 -0
- package/dist/src/catalog/workspace/desired-state-reconcilable.d.ts +4 -0
- package/dist/src/catalog/workspace/desired-state-reconcilable.js +96 -0
- package/dist/src/catalog/workspace/display-paths.d.ts +7 -0
- package/dist/src/catalog/workspace/display-paths.js +4 -0
- package/dist/src/catalog/workspace/helpers/empty.d.ts +18 -0
- package/dist/src/catalog/workspace/helpers/empty.js +16 -0
- package/dist/src/catalog/workspace/helpers/finding.d.ts +10 -0
- package/dist/src/catalog/workspace/helpers/finding.js +11 -0
- package/dist/src/catalog/workspace/helpers/source-categorize.d.ts +16 -0
- package/dist/src/catalog/workspace/helpers/source-categorize.js +51 -0
- package/dist/src/catalog/workspace/hook-ownership-ambiguous.d.ts +4 -0
- package/dist/src/catalog/workspace/hook-ownership-ambiguous.js +25 -0
- package/dist/src/catalog/workspace/initialized.d.ts +22 -0
- package/dist/src/catalog/workspace/initialized.js +58 -0
- package/dist/src/catalog/workspace/instructions-agent-supported.d.ts +4 -0
- package/dist/src/catalog/workspace/instructions-agent-supported.js +38 -0
- package/dist/src/catalog/workspace/instructions-gitignore-current.d.ts +4 -0
- package/dist/src/catalog/workspace/instructions-gitignore-current.js +49 -0
- package/dist/src/catalog/workspace/instructions-source-present.d.ts +4 -0
- package/dist/src/catalog/workspace/instructions-source-present.js +41 -0
- package/dist/src/catalog/workspace/instructions-target-current.d.ts +4 -0
- package/dist/src/catalog/workspace/instructions-target-current.js +51 -0
- package/dist/src/catalog/workspace/instructions-target-stale.d.ts +9 -0
- package/dist/src/catalog/workspace/instructions-target-stale.js +41 -0
- package/dist/src/catalog/workspace/instructions-target-unowned.d.ts +10 -0
- package/dist/src/catalog/workspace/instructions-target-unowned.js +59 -0
- package/dist/src/catalog/workspace/knowledge-state-valid.d.ts +5 -0
- package/dist/src/catalog/workspace/knowledge-state-valid.js +65 -0
- package/dist/src/catalog/workspace/lockfile-valid.d.ts +5 -0
- package/dist/src/catalog/workspace/lockfile-valid.js +63 -0
- package/dist/src/catalog/workspace/managed-file-unowned.d.ts +4 -0
- package/dist/src/catalog/workspace/managed-file-unowned.js +25 -0
- package/dist/src/catalog/workspace/mcps-agent-drift.d.ts +4 -0
- package/dist/src/catalog/workspace/mcps-agent-drift.js +139 -0
- package/dist/src/catalog/workspace/mcps-agent-orphaned.d.ts +4 -0
- package/dist/src/catalog/workspace/mcps-agent-orphaned.js +64 -0
- package/dist/src/catalog/workspace/mcps-no-secret-literal.d.ts +4 -0
- package/dist/src/catalog/workspace/mcps-no-secret-literal.js +67 -0
- package/dist/src/catalog/workspace/mcps-shared-target-compatible.d.ts +4 -0
- package/dist/src/catalog/workspace/mcps-shared-target-compatible.js +58 -0
- package/dist/src/catalog/workspace/mcps-transport-exclusivity.d.ts +4 -0
- package/dist/src/catalog/workspace/mcps-transport-exclusivity.js +67 -0
- package/dist/src/catalog/workspace/packs-declarations-valid.d.ts +19 -0
- package/dist/src/catalog/workspace/packs-declarations-valid.js +110 -0
- package/dist/src/catalog/workspace/packs-dependencies-resolved.d.ts +5 -0
- package/dist/src/catalog/workspace/packs-dependencies-resolved.js +49 -0
- package/dist/src/catalog/workspace/projections-current.d.ts +15 -0
- package/dist/src/catalog/workspace/projections-current.js +35 -0
- package/dist/src/catalog/workspace/settings-keys-recognized.d.ts +17 -0
- package/dist/src/catalog/workspace/settings-keys-recognized.js +90 -0
- package/dist/src/catalog/workspace/settings-schema-valid.d.ts +27 -0
- package/dist/src/catalog/workspace/settings-schema-valid.js +68 -0
- package/dist/src/catalog/workspace/skills-artifacts-correct.d.ts +28 -0
- package/dist/src/catalog/workspace/skills-artifacts-correct.js +143 -0
- package/dist/src/catalog/workspace/skills-declarations-valid.d.ts +27 -0
- package/dist/src/catalog/workspace/skills-declarations-valid.js +102 -0
- package/dist/src/catalog/workspace/skills-integrity-valid.d.ts +28 -0
- package/dist/src/catalog/workspace/skills-integrity-valid.js +95 -0
- package/dist/src/catalog/workspace/skills-lockfile-aligned.d.ts +5 -0
- package/dist/src/catalog/workspace/skills-lockfile-aligned.js +64 -0
- package/dist/src/catalog/workspace/source-endpoints-aligned.d.ts +5 -0
- package/dist/src/catalog/workspace/source-endpoints-aligned.js +75 -0
- package/dist/src/catalog/workspace-fixtures/fixture-state.d.ts +8 -0
- package/dist/src/catalog/workspace-fixtures/fixture-state.js +120 -0
- package/dist/src/catalog/workspace-fixtures/interpret-ops.d.ts +11 -0
- package/dist/src/catalog/workspace-fixtures/interpret-ops.js +9 -0
- package/dist/src/catalog/workspace-read-model/lint-workspace.d.ts +191 -0
- package/dist/src/catalog/workspace-read-model/lint-workspace.js +651 -0
- package/dist/src/catalog/workspace-read-model/manifest-json.d.ts +14 -0
- package/dist/src/catalog/workspace-read-model/manifest-json.js +27 -0
- package/dist/src/catalog/workspace.d.ts +58 -0
- package/dist/src/catalog/workspace.js +164 -0
- package/dist/src/catalog-contexts.d.ts +106 -0
- package/dist/src/catalog-contexts.js +95 -0
- package/dist/src/cli.d.ts +239 -0
- package/dist/src/cli.js +1077 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +21 -0
- package/dist/src/json-schema.d.ts +121 -0
- package/dist/src/json-schema.js +99 -0
- package/dist/src/run/errors.d.ts +23 -0
- package/dist/src/run/errors.js +21 -0
- package/dist/src/run/settings.d.ts +63 -0
- package/dist/src/run/settings.js +139 -0
- package/dist/src/run/staged-workspace.d.ts +31 -0
- package/dist/src/run/staged-workspace.js +273 -0
- package/dist/src/workspace-context.d.ts +129 -0
- package/dist/src/workspace-context.js +12 -0
- package/package.json +55 -0
package/dist/src/cli.js
ADDED
|
@@ -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
|