@gmickel/gno 1.25.1 → 1.26.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/README.md +10 -4
- package/assets/skill/SKILL.md +29 -17
- package/browser-extension/artifacts/{gno-browser-clipper-v1.25.1.zip → gno-browser-clipper-v1.26.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +146 -4
- package/spec/db/schema.sql +1 -1
- package/spec/output-schemas/project-profile-apply.schema.json +209 -0
- package/spec/output-schemas/project-profile-command.schema.json +160 -0
- package/spec/output-schemas/query-diagnose.schema.json +7 -1
- package/spec/output-schemas/setup-profile-result.schema.json +87 -0
- package/spec/project-profile.schema.json +301 -0
- package/src/cli/commands/collection/add.ts +39 -45
- package/src/cli/commands/collection/remove.ts +28 -28
- package/src/cli/commands/collection/rename.ts +55 -73
- package/src/cli/commands/context/add.ts +37 -26
- package/src/cli/commands/context/rm.ts +47 -20
- package/src/cli/commands/init.ts +55 -125
- package/src/cli/commands/models/use.ts +43 -38
- package/src/cli/commands/profile-apply.ts +334 -0
- package/src/cli/commands/profile.ts +409 -0
- package/src/cli/commands/setup-activation.ts +205 -54
- package/src/cli/commands/setup-profile.ts +223 -0
- package/src/cli/commands/setup.ts +3 -0
- package/src/cli/program.ts +110 -9
- package/src/config/index.ts +3 -0
- package/src/config/project-profile.ts +367 -0
- package/src/config/saver.ts +16 -7
- package/src/config/types.ts +42 -0
- package/src/core/config-mutation.ts +138 -76
- package/src/core/config-write-lock.ts +89 -0
- package/src/core/context-identity.ts +16 -0
- package/src/core/context-resolver.ts +2 -12
- package/src/core/folder-setup-planning.ts +6 -21
- package/src/core/folder-setup.ts +30 -2
- package/src/core/path-rules.ts +53 -0
- package/src/core/project-affinity-surface.ts +102 -7
- package/src/core/project-profile-apply-state.ts +268 -0
- package/src/core/project-profile-apply-validation.ts +95 -0
- package/src/core/project-profile-apply.ts +408 -0
- package/src/core/project-profile-canonical.ts +71 -0
- package/src/core/project-profile-diff.ts +302 -0
- package/src/core/project-profile-discovery.ts +519 -0
- package/src/core/project-profile-file.ts +37 -0
- package/src/core/project-profile-parser.ts +98 -0
- package/src/core/project-profile.ts +490 -0
- package/src/ingestion/walker.ts +85 -44
- package/src/llm/cache.ts +21 -0
- package/src/serve/config-sync.ts +2 -2
- package/src/serve/resident-runtime.ts +1 -0
- package/src/serve/routes/api.ts +1 -0
- package/src/store/migrations/021-multi-context-identity.ts +37 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +83 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +0 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only project profile CLI composition.
|
|
3
|
+
*
|
|
4
|
+
* All outputs are deterministic and redact machine-local paths. This module
|
|
5
|
+
* never saves config, opens an index, or downloads a model.
|
|
6
|
+
*
|
|
7
|
+
* @module src/cli/commands/profile
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// node:fs/promises provides realpath; Bun has no equivalent for canonical
|
|
11
|
+
// collection-path comparison.
|
|
12
|
+
import { realpath } from "node:fs/promises";
|
|
13
|
+
|
|
14
|
+
import type { Config } from "../../config/types";
|
|
15
|
+
import type {
|
|
16
|
+
ProjectProfileDesiredState,
|
|
17
|
+
ProjectProfileDiagnostic,
|
|
18
|
+
} from "../../core/project-profile";
|
|
19
|
+
import type { ProjectProfileDiff } from "../../core/project-profile-diff";
|
|
20
|
+
import type {
|
|
21
|
+
ProjectProfileDiscoveryDependencies,
|
|
22
|
+
ProjectProfileDiscoverySummary,
|
|
23
|
+
} from "../../core/project-profile-discovery";
|
|
24
|
+
|
|
25
|
+
import { getModelsCachePath } from "../../app/constants";
|
|
26
|
+
import { createDefaultConfig, loadConfig, type LoadResult } from "../../config";
|
|
27
|
+
import { compileProjectProfileYaml } from "../../core/project-profile";
|
|
28
|
+
import { buildProjectProfileDiff } from "../../core/project-profile-diff";
|
|
29
|
+
import {
|
|
30
|
+
discoverProjectProfile,
|
|
31
|
+
type ProjectProfileDiscoveryDiagnostic,
|
|
32
|
+
} from "../../core/project-profile-discovery";
|
|
33
|
+
import {
|
|
34
|
+
ProjectProfileFileError,
|
|
35
|
+
readProjectProfileFile,
|
|
36
|
+
} from "../../core/project-profile-file";
|
|
37
|
+
import { ModelCache } from "../../llm/cache";
|
|
38
|
+
|
|
39
|
+
export const PROJECT_PROFILE_COMMAND_SCHEMA_VERSION = "1.0" as const;
|
|
40
|
+
|
|
41
|
+
export type ProjectProfileCommandName = "check" | "show" | "diff";
|
|
42
|
+
|
|
43
|
+
export interface ProjectProfileCommandDiagnostic {
|
|
44
|
+
code: string;
|
|
45
|
+
severity: "error" | "warning";
|
|
46
|
+
path: string;
|
|
47
|
+
message: string;
|
|
48
|
+
remediation: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ProjectProfileCommandResult {
|
|
52
|
+
schemaVersion: typeof PROJECT_PROFILE_COMMAND_SCHEMA_VERSION;
|
|
53
|
+
command: ProjectProfileCommandName;
|
|
54
|
+
status: "valid" | "invalid" | "not_found" | "disabled";
|
|
55
|
+
valid: boolean;
|
|
56
|
+
discovery: ProjectProfileDiscoverySummary;
|
|
57
|
+
profile: {
|
|
58
|
+
fingerprint: string;
|
|
59
|
+
desiredState: ProjectProfileDesiredState | null;
|
|
60
|
+
} | null;
|
|
61
|
+
diff: ProjectProfileDiff | null;
|
|
62
|
+
diagnostics: ProjectProfileCommandDiagnostic[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ProjectProfileCommandOutcome {
|
|
66
|
+
result: ProjectProfileCommandResult;
|
|
67
|
+
exitCode: 0 | 1 | 2;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ProjectProfileCommandOptions {
|
|
71
|
+
command: ProjectProfileCommandName;
|
|
72
|
+
path?: string;
|
|
73
|
+
cwd?: string;
|
|
74
|
+
channel?: "local" | "remote";
|
|
75
|
+
configPath?: string;
|
|
76
|
+
offline?: boolean;
|
|
77
|
+
discoveryDependencies?: Partial<ProjectProfileDiscoveryDependencies>;
|
|
78
|
+
loadConfigFn?: (path?: string) => Promise<LoadResult<Config>>;
|
|
79
|
+
readProfileFn?: (path: string) => Promise<string>;
|
|
80
|
+
canonicalizePath?: (path: string) => Promise<string>;
|
|
81
|
+
isModelAvailableOffline?: (
|
|
82
|
+
modelUri: string,
|
|
83
|
+
modelType: "embed" | "rerank" | "expand" | "gen"
|
|
84
|
+
) => Promise<boolean>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const compareCodeUnits = (left: string, right: string): number =>
|
|
88
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
89
|
+
|
|
90
|
+
const remediationForCompilerDiagnostic = (
|
|
91
|
+
diagnostic: ProjectProfileDiagnostic
|
|
92
|
+
): string => {
|
|
93
|
+
switch (diagnostic.code) {
|
|
94
|
+
case "MODEL_PRESET_NOT_FOUND":
|
|
95
|
+
return "Configure the named local preset alias or select a configured alias.";
|
|
96
|
+
case "MODEL_PRESET_UNAVAILABLE_OFFLINE":
|
|
97
|
+
return "Cache every model in the named preset before retrying with --offline.";
|
|
98
|
+
case "MODEL_CACHE_CHECK_FAILED":
|
|
99
|
+
return "Repair local model-cache metadata and retry; no download was attempted.";
|
|
100
|
+
case "MIGRATION_REQUIRED":
|
|
101
|
+
case "UNSUPPORTED_SCHEMA_MAJOR":
|
|
102
|
+
case "UNSUPPORTED_SCHEMA_MINOR":
|
|
103
|
+
return "Migrate .gno/index.yml to the supported schemaVersion.";
|
|
104
|
+
case "PATH_NOT_FOUND":
|
|
105
|
+
return "Create the referenced repository-relative path or update the profile.";
|
|
106
|
+
case "SYMLINK_ESCAPE":
|
|
107
|
+
return "Replace the escaping symlink with a path contained by the profile root.";
|
|
108
|
+
case "CONTEXT_FILE_INVALID":
|
|
109
|
+
case "CONTEXT_FILE_UNREADABLE":
|
|
110
|
+
return "Replace the context file with readable UTF-8 text.";
|
|
111
|
+
case "UNSAFE_PATH":
|
|
112
|
+
return "Use a repository-relative path without traversal, expansion, or runtime state.";
|
|
113
|
+
default:
|
|
114
|
+
return "Repair the reported profile field and rerun gno profile check.";
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const compilerDiagnostic = (
|
|
119
|
+
issue: ProjectProfileDiagnostic
|
|
120
|
+
): ProjectProfileCommandDiagnostic => ({
|
|
121
|
+
...issue,
|
|
122
|
+
remediation: remediationForCompilerDiagnostic(issue),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export const discoveryDiagnostic = (
|
|
126
|
+
issue: ProjectProfileDiscoveryDiagnostic
|
|
127
|
+
): ProjectProfileCommandDiagnostic => ({ ...issue });
|
|
128
|
+
|
|
129
|
+
export const sortDiagnostics = (
|
|
130
|
+
diagnostics: ProjectProfileCommandDiagnostic[]
|
|
131
|
+
): ProjectProfileCommandDiagnostic[] =>
|
|
132
|
+
diagnostics.sort(
|
|
133
|
+
(left, right) =>
|
|
134
|
+
compareCodeUnits(left.path, right.path) ||
|
|
135
|
+
compareCodeUnits(left.code, right.code) ||
|
|
136
|
+
compareCodeUnits(left.message, right.message)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const resultWithoutProfile = (
|
|
140
|
+
command: ProjectProfileCommandName,
|
|
141
|
+
status: "invalid" | "not_found" | "disabled",
|
|
142
|
+
discovery: ProjectProfileDiscoverySummary,
|
|
143
|
+
diagnostics: ProjectProfileCommandDiagnostic[]
|
|
144
|
+
): ProjectProfileCommandResult => ({
|
|
145
|
+
schemaVersion: PROJECT_PROFILE_COMMAND_SCHEMA_VERSION,
|
|
146
|
+
command,
|
|
147
|
+
status,
|
|
148
|
+
valid: false,
|
|
149
|
+
discovery,
|
|
150
|
+
profile: null,
|
|
151
|
+
diff: null,
|
|
152
|
+
diagnostics: sortDiagnostics(diagnostics),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const loadProfileConfig = async (
|
|
156
|
+
options: ProjectProfileCommandOptions
|
|
157
|
+
): Promise<
|
|
158
|
+
| {
|
|
159
|
+
ok: true;
|
|
160
|
+
config: Config;
|
|
161
|
+
diagnostics: ProjectProfileCommandDiagnostic[];
|
|
162
|
+
}
|
|
163
|
+
| { ok: false; diagnostic: ProjectProfileCommandDiagnostic }
|
|
164
|
+
> => {
|
|
165
|
+
const loaded = await (options.loadConfigFn ?? loadConfig)(options.configPath);
|
|
166
|
+
if (!loaded.ok) {
|
|
167
|
+
if (loaded.error.code === "NOT_FOUND") {
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
config: createDefaultConfig(),
|
|
171
|
+
diagnostics: [
|
|
172
|
+
{
|
|
173
|
+
code: "CONFIG_NOT_FOUND",
|
|
174
|
+
severity: "warning",
|
|
175
|
+
path: "config",
|
|
176
|
+
message:
|
|
177
|
+
"No local GNO config exists; diff uses an empty current state.",
|
|
178
|
+
remediation:
|
|
179
|
+
"Run gno profile diff, then apply the profile in a later explicit step.",
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
diagnostic: {
|
|
187
|
+
code: "CONFIG_INVALID",
|
|
188
|
+
severity: "error",
|
|
189
|
+
path: "config",
|
|
190
|
+
message: "The selected local GNO config could not be loaded safely.",
|
|
191
|
+
remediation: "Repair the local config and rerun gno profile check.",
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
config: loaded.value,
|
|
198
|
+
diagnostics: loaded.warnings.map((warning) => ({
|
|
199
|
+
code: "CONFIG_WARNING",
|
|
200
|
+
severity: "warning",
|
|
201
|
+
path: warning.path,
|
|
202
|
+
message: warning.message,
|
|
203
|
+
remediation: "Repair the warned local config entry before applying.",
|
|
204
|
+
})),
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const defaultReadProfile = readProjectProfileFile;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Execute one read-only profile command and return a classified exit code.
|
|
212
|
+
*/
|
|
213
|
+
export async function runProjectProfileCommand(
|
|
214
|
+
options: ProjectProfileCommandOptions
|
|
215
|
+
): Promise<ProjectProfileCommandOutcome> {
|
|
216
|
+
const discovery = await discoverProjectProfile(
|
|
217
|
+
{
|
|
218
|
+
channel: options.channel ?? "local",
|
|
219
|
+
cwd: options.cwd,
|
|
220
|
+
rootOverride: options.path,
|
|
221
|
+
},
|
|
222
|
+
options.discoveryDependencies
|
|
223
|
+
);
|
|
224
|
+
const discoveryDiagnostics = discovery.diagnostics.map(discoveryDiagnostic);
|
|
225
|
+
if (discovery.summary.status !== "found") {
|
|
226
|
+
const status =
|
|
227
|
+
discovery.summary.status === "not_found"
|
|
228
|
+
? "not_found"
|
|
229
|
+
: discovery.summary.status === "disabled"
|
|
230
|
+
? "disabled"
|
|
231
|
+
: "invalid";
|
|
232
|
+
return {
|
|
233
|
+
result: resultWithoutProfile(
|
|
234
|
+
options.command,
|
|
235
|
+
status,
|
|
236
|
+
discovery.summary,
|
|
237
|
+
discoveryDiagnostics
|
|
238
|
+
),
|
|
239
|
+
exitCode: discoveryDiagnostics.some(
|
|
240
|
+
(diagnostic) => diagnostic.code === "PROFILE_DISCOVERY_FAILED"
|
|
241
|
+
)
|
|
242
|
+
? 2
|
|
243
|
+
: 1,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (!(discovery.profilePath && discovery.profileRoot)) {
|
|
247
|
+
return {
|
|
248
|
+
result: resultWithoutProfile(
|
|
249
|
+
options.command,
|
|
250
|
+
"invalid",
|
|
251
|
+
discovery.summary,
|
|
252
|
+
[
|
|
253
|
+
...discoveryDiagnostics,
|
|
254
|
+
{
|
|
255
|
+
code: "PROFILE_DISCOVERY_FAILED",
|
|
256
|
+
severity: "error",
|
|
257
|
+
path: ".gno/index.yml",
|
|
258
|
+
message: "Discovery returned no trusted local profile identity.",
|
|
259
|
+
remediation: "Rerun with an exact readable project root.",
|
|
260
|
+
},
|
|
261
|
+
]
|
|
262
|
+
),
|
|
263
|
+
exitCode: 2,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const configResult = await loadProfileConfig(options);
|
|
268
|
+
if (!configResult.ok) {
|
|
269
|
+
return {
|
|
270
|
+
result: resultWithoutProfile(
|
|
271
|
+
options.command,
|
|
272
|
+
"invalid",
|
|
273
|
+
discovery.summary,
|
|
274
|
+
[...discoveryDiagnostics, configResult.diagnostic]
|
|
275
|
+
),
|
|
276
|
+
exitCode: 1,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let yaml: string;
|
|
281
|
+
try {
|
|
282
|
+
yaml = await (options.readProfileFn ?? defaultReadProfile)(
|
|
283
|
+
discovery.profilePath
|
|
284
|
+
);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
const invalidFile = error instanceof ProjectProfileFileError ? error : null;
|
|
287
|
+
return {
|
|
288
|
+
result: resultWithoutProfile(
|
|
289
|
+
options.command,
|
|
290
|
+
"invalid",
|
|
291
|
+
discovery.summary,
|
|
292
|
+
[
|
|
293
|
+
...discoveryDiagnostics,
|
|
294
|
+
{
|
|
295
|
+
code: invalidFile?.code ?? "PROFILE_READ_FAILED",
|
|
296
|
+
severity: "error",
|
|
297
|
+
path: ".gno/index.yml",
|
|
298
|
+
message:
|
|
299
|
+
invalidFile?.message ??
|
|
300
|
+
"The selected project profile could not be read.",
|
|
301
|
+
remediation: invalidFile
|
|
302
|
+
? "Reduce or repair .gno/index.yml and retry."
|
|
303
|
+
: "Repair local file permissions and retry.",
|
|
304
|
+
},
|
|
305
|
+
]
|
|
306
|
+
),
|
|
307
|
+
exitCode: invalidFile ? 1 : 2,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const modelCache = options.offline
|
|
312
|
+
? new ModelCache(getModelsCachePath())
|
|
313
|
+
: null;
|
|
314
|
+
const offlineAvailability =
|
|
315
|
+
options.isModelAvailableOffline ??
|
|
316
|
+
(modelCache ? modelCache.isCachedReadOnly.bind(modelCache) : undefined);
|
|
317
|
+
const compiled = await compileProjectProfileYaml(yaml, {
|
|
318
|
+
profileRoot: discovery.profileRoot,
|
|
319
|
+
config: configResult.config,
|
|
320
|
+
...(offlineAvailability
|
|
321
|
+
? { isModelAvailableOffline: offlineAvailability }
|
|
322
|
+
: {}),
|
|
323
|
+
});
|
|
324
|
+
if (!compiled.ok) {
|
|
325
|
+
return {
|
|
326
|
+
result: resultWithoutProfile(
|
|
327
|
+
options.command,
|
|
328
|
+
"invalid",
|
|
329
|
+
discovery.summary,
|
|
330
|
+
[
|
|
331
|
+
...discoveryDiagnostics,
|
|
332
|
+
...configResult.diagnostics,
|
|
333
|
+
...compiled.diagnostics.map(compilerDiagnostic),
|
|
334
|
+
]
|
|
335
|
+
),
|
|
336
|
+
exitCode: 1,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let diff: ProjectProfileDiff | null = null;
|
|
341
|
+
const diagnostics = [
|
|
342
|
+
...discoveryDiagnostics,
|
|
343
|
+
...configResult.diagnostics,
|
|
344
|
+
...compiled.value.diagnostics.map(compilerDiagnostic),
|
|
345
|
+
];
|
|
346
|
+
if (options.command === "diff") {
|
|
347
|
+
const built = await buildProjectProfileDiff({
|
|
348
|
+
desiredState: compiled.value.desiredState,
|
|
349
|
+
expectedCollectionRoot: compiled.value.resolvedPaths.collectionRoot,
|
|
350
|
+
profileBinding: {
|
|
351
|
+
path: discovery.profilePath,
|
|
352
|
+
fingerprint: compiled.value.fingerprint,
|
|
353
|
+
collection: compiled.value.desiredState.collection.name,
|
|
354
|
+
},
|
|
355
|
+
config: configResult.config,
|
|
356
|
+
canonicalizePath: options.canonicalizePath ?? realpath,
|
|
357
|
+
});
|
|
358
|
+
diff = built.diff;
|
|
359
|
+
diagnostics.push(...built.diagnostics);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
result: {
|
|
364
|
+
schemaVersion: PROJECT_PROFILE_COMMAND_SCHEMA_VERSION,
|
|
365
|
+
command: options.command,
|
|
366
|
+
status: "valid",
|
|
367
|
+
valid: true,
|
|
368
|
+
discovery: discovery.summary,
|
|
369
|
+
profile: {
|
|
370
|
+
fingerprint: compiled.value.fingerprint,
|
|
371
|
+
desiredState:
|
|
372
|
+
options.command === "check" ? null : compiled.value.desiredState,
|
|
373
|
+
},
|
|
374
|
+
diff,
|
|
375
|
+
diagnostics: sortDiagnostics(diagnostics),
|
|
376
|
+
},
|
|
377
|
+
exitCode: 0,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function formatProjectProfileResult(
|
|
382
|
+
result: ProjectProfileCommandResult,
|
|
383
|
+
options: { json: boolean }
|
|
384
|
+
): string {
|
|
385
|
+
if (options.json) {
|
|
386
|
+
return JSON.stringify(result, null, 2);
|
|
387
|
+
}
|
|
388
|
+
const lines = [
|
|
389
|
+
`Profile ${result.status}: ${result.discovery.profile ?? "(not found)"}`,
|
|
390
|
+
];
|
|
391
|
+
if (result.profile) {
|
|
392
|
+
lines.push(`Fingerprint: ${result.profile.fingerprint}`);
|
|
393
|
+
}
|
|
394
|
+
if (result.command === "show" && result.profile?.desiredState) {
|
|
395
|
+
lines.push(Bun.YAML.stringify(result.profile.desiredState).trimEnd());
|
|
396
|
+
}
|
|
397
|
+
if (result.diff) {
|
|
398
|
+
lines.push(`Diff: ${result.diff.status}`);
|
|
399
|
+
for (const change of result.diff.changes) {
|
|
400
|
+
lines.push(` ${change.action} ${change.field}: ${change.summary}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
for (const issue of result.diagnostics) {
|
|
404
|
+
lines.push(
|
|
405
|
+
`${issue.severity === "error" ? "Error" : "Warning"} ${issue.code}: ${issue.message} ${issue.remediation}`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
return lines.join("\n");
|
|
409
|
+
}
|