@notedge/panduck 0.0.0 → 0.0.1

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.
@@ -1,188 +1,188 @@
1
- import type { Cli, ParsedOptions } from "@vmz/commander";
2
-
3
- import {
4
- assertReadableInput,
5
- buildBlockedReport,
6
- createContext,
7
- resolveFormats,
8
- validateIoPaths,
9
- } from "../context.js";
10
- import { emitReport } from "../diagnostics.js";
11
- import { ExitCode } from "../exit-codes.js";
12
- import { findFormat, isOperationReady } from "../format-registry.js";
13
- import { CliOptionError, readSharedOptions, readString } from "../options.js";
14
- import { hasPipeline } from "../pipeline.js";
15
- import { configureConsoleBackend } from "../console-backend.js";
16
- import { createReport } from "../report.js";
17
-
18
- export function registerPlanCommand(cli: Cli): void {
19
- cli.command("plan", "cli.cmd.plan")
20
- .option("--to <format>", "cli.opt.to")
21
- .option("--from <format>", "cli.opt.from")
22
- .option("--profile <name>", "cli.opt.profile")
23
- .option("--config <file>", "cli.opt.config")
24
- .option("--budget <limit>", "cli.opt.budget")
25
- .option("--json", "cli.opt.json")
26
- .action((options: ParsedOptions) => runPlan(options));
27
- }
28
-
29
- async function runPlan(options: ParsedOptions): Promise<number> {
30
- try {
31
- const shared = readSharedOptions(options);
32
- const inputPath = options._[0] ?? readString(options, "input");
33
- if (!inputPath) {
34
- return ExitCode.InvalidArgs;
35
- }
36
- const ctx = createContext();
37
- configureConsoleBackend(ctx.bindings, shared.logFile);
38
- const resolved = resolveFormats(inputPath, undefined, shared);
39
-
40
- if (resolved.ambiguous) {
41
- const report = buildBlockedReport(
42
- ctx,
43
- "plan",
44
- inputPath,
45
- resolved,
46
- "ambiguous input format",
47
- ExitCode.InputUnsupported,
48
- shared,
49
- );
50
- emitReport(report, shared.diagnostics, shared.json);
51
- return ExitCode.InputUnsupported;
52
- }
53
-
54
- if (!resolved.to) {
55
- const report = buildBlockedReport(
56
- ctx,
57
- "plan",
58
- inputPath,
59
- resolved,
60
- "target format is required for plan",
61
- ExitCode.InvalidArgs,
62
- shared,
63
- );
64
- emitReport(report, shared.diagnostics, shared.json);
65
- return ExitCode.InvalidArgs;
66
- }
67
-
68
- try {
69
- await assertReadableInput(inputPath);
70
- } catch {
71
- const report = buildBlockedReport(
72
- ctx,
73
- "plan",
74
- inputPath,
75
- resolved,
76
- "input is not readable",
77
- ExitCode.InputUnsupported,
78
- shared,
79
- );
80
- emitReport(report, shared.diagnostics, shared.json);
81
- return ExitCode.InputUnsupported;
82
- }
83
-
84
- const source = resolved.from ? findFormat(ctx.registry, resolved.from) : undefined;
85
- const target = findFormat(ctx.registry, resolved.to);
86
- if (!target) {
87
- const report = buildBlockedReport(
88
- ctx,
89
- "plan",
90
- inputPath,
91
- resolved,
92
- `unknown target format ${resolved.to}`,
93
- ExitCode.InputUnsupported,
94
- shared,
95
- );
96
- emitReport(report, shared.diagnostics, shared.json);
97
- return ExitCode.InputUnsupported;
98
- }
99
-
100
- const routeReady = hasPipeline(ctx.bindings, resolved.from, resolved.to);
101
-
102
- if (!routeReady) {
103
- if (source && !isOperationReady(source, "read")) {
104
- const report = createReport({
105
- operation: "plan",
106
- toolVersion: ctx.toolVersion,
107
- status: "blocked",
108
- inputs: [{ path: inputPath, format: source.format }],
109
- detection: { format: source.format, confidence: "hint", hints: resolved.hints },
110
- pipeline: {
111
- reader: source.format,
112
- writer: target.format,
113
- stages: ["detect", "read", "ir", "write", "publish"],
114
- blocked_reason: `reader state is ${source.reader.state}`,
115
- },
116
- coverage: target.coverage,
117
- diagnostics: [
118
- {
119
- owner: "panduck",
120
- severity: "error",
121
- message: `source format ${source.format} is not ready for read`,
122
- code: "panduck.reader.not_ready",
123
- },
124
- ],
125
- determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
126
- statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.InputUnsupported },
127
- });
128
- emitReport(report, shared.diagnostics, shared.json);
129
- return ExitCode.InputUnsupported;
130
- }
131
-
132
- if (!isOperationReady(target, "write")) {
133
- const report = createReport({
134
- operation: "plan",
135
- toolVersion: ctx.toolVersion,
136
- status: "blocked",
137
- inputs: [{ path: inputPath, format: resolved.from }],
138
- detection: { format: resolved.from, confidence: "hint", hints: resolved.hints },
139
- pipeline: {
140
- reader: source?.format,
141
- writer: target.format,
142
- stages: ["detect", "read", "ir", "write", "publish"],
143
- blocked_reason: `writer state is ${target.writer.state}`,
144
- },
145
- coverage: target.coverage,
146
- diagnostics: [
147
- {
148
- owner: "panduck",
149
- severity: "error",
150
- message: `target format ${target.format} is not ready for write`,
151
- code: "panduck.writer.not_ready",
152
- },
153
- ],
154
- determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
155
- statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.InputUnsupported },
156
- });
157
- emitReport(report, shared.diagnostics, shared.json);
158
- return ExitCode.InputUnsupported;
159
- }
160
- }
161
-
162
- const report = createReport({
163
- operation: "plan",
164
- toolVersion: ctx.toolVersion,
165
- status: routeReady ? "success" : "success_with_loss",
166
- inputs: [{ path: inputPath, format: resolved.from }],
167
- detection: { format: resolved.from, confidence: resolved.from ? "hint" : undefined, hints: resolved.hints },
168
- pipeline: {
169
- reader: source?.format ?? resolved.from,
170
- writer: target.format,
171
- stages: ["detect", "read", "ir", "write", "publish"],
172
- },
173
- coverage: source?.coverage ?? target.coverage,
174
- budgets: shared.budgets,
175
- determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
176
- statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.Success },
177
- });
178
- emitReport(report, shared.diagnostics, shared.json);
179
- return ExitCode.Success;
180
- } catch (error) {
181
- if (error instanceof CliOptionError) {
182
- console.error(error.message);
183
- return error.exitCode;
184
- }
185
- console.error(error instanceof Error ? error.message : String(error));
186
- return ExitCode.InvalidArgs;
187
- }
188
- }
1
+ import type { Cli, ParsedOptions } from "@vmz/commander";
2
+
3
+ import {
4
+ assertReadableInput,
5
+ buildBlockedReport,
6
+ createContext,
7
+ resolveFormats,
8
+ validateIoPaths,
9
+ } from "../context.js";
10
+ import { emitReport } from "../diagnostics.js";
11
+ import { ExitCode } from "../exit-codes.js";
12
+ import { findFormat, isOperationReady } from "../format-registry.js";
13
+ import { CliOptionError, readSharedOptions, readString } from "../options.js";
14
+ import { hasPipeline } from "../pipeline.js";
15
+ import { configureConsoleBackend } from "../console-backend.js";
16
+ import { createReport } from "../report.js";
17
+
18
+ export function registerPlanCommand(cli: Cli): void {
19
+ cli.command("plan", "cli.cmd.plan")
20
+ .option("--to <format>", "cli.opt.to")
21
+ .option("--from <format>", "cli.opt.from")
22
+ .option("--profile <name>", "cli.opt.profile")
23
+ .option("--config <file>", "cli.opt.config")
24
+ .option("--budget <limit>", "cli.opt.budget")
25
+ .option("--json", "cli.opt.json")
26
+ .action((options: ParsedOptions) => runPlan(options));
27
+ }
28
+
29
+ async function runPlan(options: ParsedOptions): Promise<number> {
30
+ try {
31
+ const shared = readSharedOptions(options);
32
+ const inputPath = options._[0] ?? readString(options, "input");
33
+ if (!inputPath) {
34
+ return ExitCode.InvalidArgs;
35
+ }
36
+ const ctx = createContext();
37
+ configureConsoleBackend(ctx.bindings, shared.logFile);
38
+ const resolved = resolveFormats(inputPath, undefined, shared);
39
+
40
+ if (resolved.ambiguous) {
41
+ const report = buildBlockedReport(
42
+ ctx,
43
+ "plan",
44
+ inputPath,
45
+ resolved,
46
+ "ambiguous input format",
47
+ ExitCode.InputUnsupported,
48
+ shared,
49
+ );
50
+ emitReport(report, shared.diagnostics, shared.json);
51
+ return ExitCode.InputUnsupported;
52
+ }
53
+
54
+ if (!resolved.to) {
55
+ const report = buildBlockedReport(
56
+ ctx,
57
+ "plan",
58
+ inputPath,
59
+ resolved,
60
+ "target format is required for plan",
61
+ ExitCode.InvalidArgs,
62
+ shared,
63
+ );
64
+ emitReport(report, shared.diagnostics, shared.json);
65
+ return ExitCode.InvalidArgs;
66
+ }
67
+
68
+ try {
69
+ await assertReadableInput(inputPath);
70
+ } catch {
71
+ const report = buildBlockedReport(
72
+ ctx,
73
+ "plan",
74
+ inputPath,
75
+ resolved,
76
+ "input is not readable",
77
+ ExitCode.InputUnsupported,
78
+ shared,
79
+ );
80
+ emitReport(report, shared.diagnostics, shared.json);
81
+ return ExitCode.InputUnsupported;
82
+ }
83
+
84
+ const source = resolved.from ? findFormat(ctx.registry, resolved.from) : undefined;
85
+ const target = findFormat(ctx.registry, resolved.to);
86
+ if (!target) {
87
+ const report = buildBlockedReport(
88
+ ctx,
89
+ "plan",
90
+ inputPath,
91
+ resolved,
92
+ `unknown target format ${resolved.to}`,
93
+ ExitCode.InputUnsupported,
94
+ shared,
95
+ );
96
+ emitReport(report, shared.diagnostics, shared.json);
97
+ return ExitCode.InputUnsupported;
98
+ }
99
+
100
+ const routeReady = hasPipeline(ctx.bindings, resolved.from, resolved.to);
101
+
102
+ if (!routeReady) {
103
+ if (source && !isOperationReady(source, "read")) {
104
+ const report = createReport({
105
+ operation: "plan",
106
+ toolVersion: ctx.toolVersion,
107
+ status: "blocked",
108
+ inputs: [{ path: inputPath, format: source.format }],
109
+ detection: { format: source.format, confidence: "hint", hints: resolved.hints },
110
+ pipeline: {
111
+ reader: source.format,
112
+ writer: target.format,
113
+ stages: ["detect", "read", "ir", "write", "publish"],
114
+ blocked_reason: `reader state is ${source.reader.state}`,
115
+ },
116
+ coverage: target.coverage,
117
+ diagnostics: [
118
+ {
119
+ owner: "panduck",
120
+ severity: "error",
121
+ message: `source format ${source.format} is not ready for read`,
122
+ code: "panduck.reader.not_ready",
123
+ },
124
+ ],
125
+ determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
126
+ statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.InputUnsupported },
127
+ });
128
+ emitReport(report, shared.diagnostics, shared.json);
129
+ return ExitCode.InputUnsupported;
130
+ }
131
+
132
+ if (!isOperationReady(target, "write")) {
133
+ const report = createReport({
134
+ operation: "plan",
135
+ toolVersion: ctx.toolVersion,
136
+ status: "blocked",
137
+ inputs: [{ path: inputPath, format: resolved.from }],
138
+ detection: { format: resolved.from, confidence: "hint", hints: resolved.hints },
139
+ pipeline: {
140
+ reader: source?.format,
141
+ writer: target.format,
142
+ stages: ["detect", "read", "ir", "write", "publish"],
143
+ blocked_reason: `writer state is ${target.writer.state}`,
144
+ },
145
+ coverage: target.coverage,
146
+ diagnostics: [
147
+ {
148
+ owner: "panduck",
149
+ severity: "error",
150
+ message: `target format ${target.format} is not ready for write`,
151
+ code: "panduck.writer.not_ready",
152
+ },
153
+ ],
154
+ determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
155
+ statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.InputUnsupported },
156
+ });
157
+ emitReport(report, shared.diagnostics, shared.json);
158
+ return ExitCode.InputUnsupported;
159
+ }
160
+ }
161
+
162
+ const report = createReport({
163
+ operation: "plan",
164
+ toolVersion: ctx.toolVersion,
165
+ status: routeReady ? "success" : "success_with_loss",
166
+ inputs: [{ path: inputPath, format: resolved.from }],
167
+ detection: { format: resolved.from, confidence: resolved.from ? "hint" : undefined, hints: resolved.hints },
168
+ pipeline: {
169
+ reader: source?.format ?? resolved.from,
170
+ writer: target.format,
171
+ stages: ["detect", "read", "ir", "write", "publish"],
172
+ },
173
+ coverage: source?.coverage ?? target.coverage,
174
+ budgets: shared.budgets,
175
+ determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
176
+ statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.Success },
177
+ });
178
+ emitReport(report, shared.diagnostics, shared.json);
179
+ return ExitCode.Success;
180
+ } catch (error) {
181
+ if (error instanceof CliOptionError) {
182
+ console.error(error.message);
183
+ return error.exitCode;
184
+ }
185
+ console.error(error instanceof Error ? error.message : String(error));
186
+ return ExitCode.InvalidArgs;
187
+ }
188
+ }
@@ -1,16 +1,16 @@
1
- import type { PanduckBindings } from "../types.js";
2
-
3
- /** Install a native JSON lines log sink when bindings expose the hook. */
4
- export function configureConsoleBackend(
5
- bindings: PanduckBindings | null,
6
- logFile?: string,
7
- ): void {
8
- if (!logFile) {
9
- return;
10
- }
11
- const install = bindings?.installLoggerLogFile ?? bindings?.installConsoleLogFile;
12
- if (!install) {
13
- return;
14
- }
15
- install(logFile);
16
- }
1
+ import type { PanduckBindings } from "../types.js";
2
+
3
+ /** Install a native JSON lines log sink when bindings expose the hook. */
4
+ export function configureConsoleBackend(
5
+ bindings: PanduckBindings | null,
6
+ logFile?: string,
7
+ ): void {
8
+ if (!logFile) {
9
+ return;
10
+ }
11
+ const install = bindings?.installLoggerLogFile ?? bindings?.installConsoleLogFile;
12
+ if (!install) {
13
+ return;
14
+ }
15
+ install(logFile);
16
+ }
@@ -1,98 +1,98 @@
1
- import { access } from "node:fs/promises";
2
- import { constants } from "node:fs";
3
-
4
- import type { PanduckBindings } from "../types.js";
5
- import { loadPanduckNode } from "../node/load.js";
6
- import { getFormatRegistry, findFormat, type FormatRecord } from "./format-registry.js";
7
- import { extensionHints, inferTargetFromPath, isStdinPath, normalizeFormatId, pathsOverlap } from "./paths.js";
8
- import { ExitCode } from "./exit-codes.js";
9
- import type { SharedCliOptions } from "./options.js";
10
- import { createReport, type PanduckReport } from "./report.js";
11
-
12
- export type CliContext = {
13
- bindings: PanduckBindings | null;
14
- toolVersion: string;
15
- registry: FormatRecord[];
16
- };
17
-
18
- export function loadBindings(): PanduckBindings | null {
19
- try {
20
- return loadPanduckNode();
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- export function createContext(): CliContext {
27
- const bindings = loadBindings();
28
- return {
29
- bindings,
30
- toolVersion: bindings?.panduckVersion() ?? "0.0.0",
31
- registry: getFormatRegistry(bindings),
32
- };
33
- }
34
-
35
- /** Resolved source/target format ids for a CLI invocation. */
36
- export type ResolvedFormats = {
37
- from?: string;
38
- to?: string;
39
- ambiguous: boolean;
40
- hints: string[];
41
- };
42
-
43
- export function resolveFormats(
44
- inputPath: string,
45
- outputPath: string | undefined,
46
- shared: SharedCliOptions,
47
- ): ResolvedFormats {
48
- const hints = extensionHints(inputPath);
49
- const from = shared.from ? normalizeFormatId(shared.from) : hints.length === 1 ? hints[0] : undefined;
50
- const to =
51
- shared.to
52
- ? normalizeFormatId(shared.to)
53
- : outputPath && outputPath !== "-"
54
- ? inferTargetFromPath(outputPath)
55
- : undefined;
56
- const ambiguous = !shared.from && hints.length > 1;
57
- return { from, to, ambiguous, hints };
58
- }
59
-
60
- export async function assertReadableInput(path: string): Promise<void> {
61
- if (isStdinPath(path)) {
62
- return;
63
- }
64
- await access(path, constants.R_OK);
65
- }
66
-
67
- export function buildBlockedReport(
68
- ctx: CliContext,
69
- operation: string,
70
- inputPath: string,
71
- resolved: ResolvedFormats,
72
- reason: string,
73
- exitCode: number,
74
- shared: SharedCliOptions,
75
- ): PanduckReport {
76
- return createReport({
77
- operation,
78
- toolVersion: ctx.toolVersion,
79
- status: "blocked",
80
- inputs: [{ path: inputPath, format: resolved.from }],
81
- detection: {
82
- format: resolved.from,
83
- confidence: resolved.ambiguous ? "ambiguous" : resolved.from ? "hint" : undefined,
84
- hints: resolved.hints,
85
- },
86
- pipeline: { blocked_reason: reason },
87
- diagnostics: [{ owner: "panduck", severity: "error", message: reason }],
88
- determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
89
- statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: exitCode },
90
- });
91
- }
92
-
93
- export function validateIoPaths(inputPath: string, outputPath: string | undefined): number | null {
94
- if (outputPath && pathsOverlap(inputPath, outputPath)) {
95
- return ExitCode.InvalidArgs;
96
- }
97
- return null;
98
- }
1
+ import { access } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+
4
+ import type { PanduckBindings } from "../types.js";
5
+ import { loadPanduckNode } from "../node/load.js";
6
+ import { getFormatRegistry, findFormat, type FormatRecord } from "./format-registry.js";
7
+ import { extensionHints, inferTargetFromPath, isStdinPath, normalizeFormatId, pathsOverlap } from "./paths.js";
8
+ import { ExitCode } from "./exit-codes.js";
9
+ import type { SharedCliOptions } from "./options.js";
10
+ import { createReport, type PanduckReport } from "./report.js";
11
+
12
+ export type CliContext = {
13
+ bindings: PanduckBindings | null;
14
+ toolVersion: string;
15
+ registry: FormatRecord[];
16
+ };
17
+
18
+ export function loadBindings(): PanduckBindings | null {
19
+ try {
20
+ return loadPanduckNode();
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ export function createContext(): CliContext {
27
+ const bindings = loadBindings();
28
+ return {
29
+ bindings,
30
+ toolVersion: bindings?.panduckVersion() ?? "0.0.0",
31
+ registry: getFormatRegistry(bindings),
32
+ };
33
+ }
34
+
35
+ /** Resolved source/target format ids for a CLI invocation. */
36
+ export type ResolvedFormats = {
37
+ from?: string;
38
+ to?: string;
39
+ ambiguous: boolean;
40
+ hints: string[];
41
+ };
42
+
43
+ export function resolveFormats(
44
+ inputPath: string,
45
+ outputPath: string | undefined,
46
+ shared: SharedCliOptions,
47
+ ): ResolvedFormats {
48
+ const hints = extensionHints(inputPath);
49
+ const from = shared.from ? normalizeFormatId(shared.from) : hints.length === 1 ? hints[0] : undefined;
50
+ const to =
51
+ shared.to
52
+ ? normalizeFormatId(shared.to)
53
+ : outputPath && outputPath !== "-"
54
+ ? inferTargetFromPath(outputPath)
55
+ : undefined;
56
+ const ambiguous = !shared.from && hints.length > 1;
57
+ return { from, to, ambiguous, hints };
58
+ }
59
+
60
+ export async function assertReadableInput(path: string): Promise<void> {
61
+ if (isStdinPath(path)) {
62
+ return;
63
+ }
64
+ await access(path, constants.R_OK);
65
+ }
66
+
67
+ export function buildBlockedReport(
68
+ ctx: CliContext,
69
+ operation: string,
70
+ inputPath: string,
71
+ resolved: ResolvedFormats,
72
+ reason: string,
73
+ exitCode: number,
74
+ shared: SharedCliOptions,
75
+ ): PanduckReport {
76
+ return createReport({
77
+ operation,
78
+ toolVersion: ctx.toolVersion,
79
+ status: "blocked",
80
+ inputs: [{ path: inputPath, format: resolved.from }],
81
+ detection: {
82
+ format: resolved.from,
83
+ confidence: resolved.ambiguous ? "ambiguous" : resolved.from ? "hint" : undefined,
84
+ hints: resolved.hints,
85
+ },
86
+ pipeline: { blocked_reason: reason },
87
+ diagnostics: [{ owner: "panduck", severity: "error", message: reason }],
88
+ determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
89
+ statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: exitCode },
90
+ });
91
+ }
92
+
93
+ export function validateIoPaths(inputPath: string, outputPath: string | undefined): number | null {
94
+ if (outputPath && pathsOverlap(inputPath, outputPath)) {
95
+ return ExitCode.InvalidArgs;
96
+ }
97
+ return null;
98
+ }
@@ -1,24 +1,24 @@
1
- import type { DiagnosticsMode } from "./options.js";
2
- import type { PanduckReport } from "./report.js";
3
-
4
- export function emitHuman(message: string): void {
5
- console.error(message);
6
- }
7
-
8
- export function emitJson(value: unknown): void {
9
- console.log(JSON.stringify(value, null, 2));
10
- }
11
-
12
- export function emitReport(report: PanduckReport, mode: DiagnosticsMode, jsonRequested: boolean): void {
13
- if (jsonRequested || mode === "json") {
14
- emitJson(report);
15
- return;
16
- }
17
- if (mode === "silent") {
18
- return;
19
- }
20
- emitHuman(`${report.operation}: ${report.status}`);
21
- for (const item of report.diagnostics ?? []) {
22
- emitHuman(`[${item.owner}] ${item.severity}: ${item.message}`);
23
- }
24
- }
1
+ import type { DiagnosticsMode } from "./options.js";
2
+ import type { PanduckReport } from "./report.js";
3
+
4
+ export function emitHuman(message: string): void {
5
+ console.error(message);
6
+ }
7
+
8
+ export function emitJson(value: unknown): void {
9
+ console.log(JSON.stringify(value, null, 2));
10
+ }
11
+
12
+ export function emitReport(report: PanduckReport, mode: DiagnosticsMode, jsonRequested: boolean): void {
13
+ if (jsonRequested || mode === "json") {
14
+ emitJson(report);
15
+ return;
16
+ }
17
+ if (mode === "silent") {
18
+ return;
19
+ }
20
+ emitHuman(`${report.operation}: ${report.status}`);
21
+ for (const item of report.diagnostics ?? []) {
22
+ emitHuman(`[${item.owner}] ${item.severity}: ${item.message}`);
23
+ }
24
+ }