@notedge/panduck 0.0.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.
@@ -0,0 +1,84 @@
1
+ import { access, mkdtemp, rm } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import type { Cli, ParsedOptions } from "@vmz/commander";
7
+
8
+ import { loadBindings } from "../context.js";
9
+ import { emitHuman, emitJson } from "../diagnostics.js";
10
+ import { readSharedOptions } from "../options.js";
11
+
12
+ type DoctorCheck = {
13
+ id: string;
14
+ ok: boolean;
15
+ message: string;
16
+ };
17
+
18
+ export function registerDoctorCommand(cli: Cli): void {
19
+ cli.command("doctor", "cli.cmd.doctor")
20
+ .option("--json", "cli.opt.json")
21
+ .action((options: ParsedOptions) => runDoctor(options));
22
+ }
23
+
24
+ async function runDoctor(options: ParsedOptions): Promise<number> {
25
+ const shared = readSharedOptions(options);
26
+ const checks: DoctorCheck[] = [];
27
+
28
+ checks.push({
29
+ id: "node",
30
+ ok: Number(process.versions.node.split(".")[0]) >= 20,
31
+ message: `node ${process.version}`,
32
+ });
33
+
34
+ const bindings = loadBindings();
35
+ if (bindings) {
36
+ checks.push({
37
+ id: "native",
38
+ ok: true,
39
+ message: `panduck-napi ${bindings.panduckVersion()}; formats=${bindings.supportedFormats().join(",")}`,
40
+ });
41
+ } else {
42
+ checks.push({
43
+ id: "native",
44
+ ok: false,
45
+ message: "native bindings are not installed for this platform",
46
+ });
47
+ }
48
+
49
+ try {
50
+ const dir = await mkdtemp(join(tmpdir(), "panduck-doctor-"));
51
+ await access(dir, constants.W_OK);
52
+ await rm(dir, { recursive: true, force: true });
53
+ checks.push({ id: "tempdir", ok: true, message: tmpdir() });
54
+ } catch (error) {
55
+ checks.push({
56
+ id: "tempdir",
57
+ ok: false,
58
+ message: error instanceof Error ? error.message : String(error),
59
+ });
60
+ }
61
+
62
+ const ok = checks.every((check) => check.ok);
63
+ const payload = {
64
+ schema_version: "panduck.doctor/v1",
65
+ status: ok ? "ok" : "issues",
66
+ checks,
67
+ };
68
+
69
+ if (shared.json || options.json === true) {
70
+ emitJson(payload);
71
+ } else if (ok) {
72
+ emitHuman("Panduck environment looks healthy");
73
+ for (const check of checks) {
74
+ emitHuman(` ok ${check.id}: ${check.message}`);
75
+ }
76
+ } else {
77
+ emitHuman("Panduck environment has issues");
78
+ for (const check of checks) {
79
+ emitHuman(` ${check.ok ? "ok" : "fail"} ${check.id}: ${check.message}`);
80
+ }
81
+ }
82
+
83
+ return ok ? 0 : 2;
84
+ }
@@ -0,0 +1,50 @@
1
+ import type { Cli, ParsedOptions } from "@vmz/commander";
2
+
3
+ import { createContext } from "../context.js";
4
+ import { emitJson } from "../diagnostics.js";
5
+ import { findFormat, type OperationKind } from "../format-registry.js";
6
+ import { readSharedOptions, readString } from "../options.js";
7
+
8
+ export function registerFormatsCommand(cli: Cli): void {
9
+ cli.command("formats", "cli.cmd.formats")
10
+ .option("--operation <kind>", "cli.opt.operation")
11
+ .option("--json", "cli.opt.json")
12
+ .action((options: ParsedOptions) => runFormats(options));
13
+ }
14
+
15
+ function runFormats(options: ParsedOptions): number {
16
+ const shared = readSharedOptions(options);
17
+ const ctx = createContext();
18
+ const formatId = options._[0] ?? readString(options, "format");
19
+ const operation = readString(options, "operation") as OperationKind | undefined;
20
+ const selected = formatId ? findFormat(ctx.registry, formatId) : undefined;
21
+ if (formatId && !selected) {
22
+ console.error(`unknown format: ${formatId}`);
23
+ return 2;
24
+ }
25
+
26
+ const records = selected ? [selected] : ctx.registry;
27
+ const payload = records.map((record) => {
28
+ if (operation === "read") {
29
+ return { format: record.format, reader: record.reader, container: record.container, coverage: record.coverage };
30
+ }
31
+ if (operation === "write") {
32
+ return { format: record.format, writer: record.writer, container: record.container, coverage: record.coverage };
33
+ }
34
+ return record;
35
+ });
36
+
37
+ if (shared.json || options.json === true) {
38
+ emitJson({
39
+ schema_version: "panduck.formats/v1",
40
+ formats: payload,
41
+ conversions: ctx.bindings?.supportedConversions?.() ?? [],
42
+ });
43
+ return 0;
44
+ }
45
+
46
+ for (const record of records) {
47
+ console.log(`${record.format}\tread=${record.reader.state}\twrite=${record.writer.state}`);
48
+ }
49
+ return 0;
50
+ }
@@ -0,0 +1,231 @@
1
+ import { stat } from "node:fs/promises";
2
+
3
+ import type { Cli, ParsedOptions } from "@vmz/commander";
4
+
5
+ import {
6
+ assertReadableInput,
7
+ buildBlockedReport,
8
+ createContext,
9
+ resolveFormats,
10
+ } from "../context.js";
11
+ import { emitReport } from "../diagnostics.js";
12
+ import { ExitCode } from "../exit-codes.js";
13
+ import { CliOptionError, parseInspectStage, readSharedOptions, readString } from "../options.js";
14
+ import { configureConsoleBackend } from "../console-backend.js";
15
+ import { createReport } from "../report.js";
16
+ import { isStdinPath } from "../paths.js";
17
+
18
+ export function registerInspectCommand(cli: Cli): void {
19
+ cli.command("inspect", "cli.cmd.inspect")
20
+ .option("--from <format>", "cli.opt.from")
21
+ .option("--stage <stage>", "cli.opt.stage")
22
+ .option("--path <layout-path>", "cli.opt.path")
23
+ .option("--budget <limit>", "cli.opt.budget")
24
+ .option("--json", "cli.opt.json")
25
+ .action((options: ParsedOptions) => runInspect(options));
26
+ }
27
+
28
+ async function runInspect(options: ParsedOptions): Promise<number> {
29
+ try {
30
+ const shared = readSharedOptions(options);
31
+ const inputPath = options._[0] ?? readString(options, "input");
32
+ if (!inputPath) {
33
+ return ExitCode.InvalidArgs;
34
+ }
35
+ const stage = parseInspectStage(readString(options, "stage"));
36
+ const layoutPath = readString(options, "path");
37
+ const ctx = createContext();
38
+ configureConsoleBackend(ctx.bindings, shared.logFile);
39
+ const resolved = resolveFormats(inputPath, undefined, shared);
40
+
41
+ if (resolved.ambiguous) {
42
+ const report = buildBlockedReport(
43
+ ctx,
44
+ "inspect",
45
+ inputPath,
46
+ resolved,
47
+ "ambiguous input format",
48
+ ExitCode.InputUnsupported,
49
+ shared,
50
+ );
51
+ emitReport(report, shared.diagnostics, shared.json);
52
+ return ExitCode.InputUnsupported;
53
+ }
54
+
55
+ try {
56
+ await assertReadableInput(inputPath);
57
+ } catch {
58
+ const report = buildBlockedReport(
59
+ ctx,
60
+ "inspect",
61
+ inputPath,
62
+ resolved,
63
+ "input is not readable",
64
+ ExitCode.InputUnsupported,
65
+ shared,
66
+ );
67
+ emitReport(report, shared.diagnostics, shared.json);
68
+ return ExitCode.InputUnsupported;
69
+ }
70
+
71
+ const size = isStdinPath(inputPath) ? null : (await stat(inputPath)).size;
72
+
73
+ if (stage === "decode" && resolved.from === "docx" && ctx.bindings?.inspectDecode) {
74
+ try {
75
+ const decode = ctx.bindings.inspectDecode(inputPath, layoutPath);
76
+ const report = createReport({
77
+ operation: "inspect",
78
+ toolVersion: ctx.toolVersion,
79
+ status: "success",
80
+ inputs: [{ path: inputPath, format: decode.format }],
81
+ detection: {
82
+ format: decode.format,
83
+ outer: decode.outer,
84
+ inner: decode.inner,
85
+ confidence: "verified",
86
+ hints: resolved.hints,
87
+ },
88
+ pipeline: { stages: ["decode"] },
89
+ decodedParts: decode.parts.map((part) => ({
90
+ path: part.path,
91
+ compression_method: part.compressionMethod,
92
+ compressed_size: part.compressedSize,
93
+ uncompressed_size: part.uncompressedSize,
94
+ decoded_size: part.decodedSize,
95
+ })),
96
+ budgets: shared.budgets,
97
+ determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
98
+ statusPolicy: {
99
+ loss: shared.loss,
100
+ strict: shared.strict,
101
+ exit_code: ExitCode.Success,
102
+ },
103
+ });
104
+ if (size !== null) {
105
+ report.budgets = { ...report.budgets, input_bytes: size };
106
+ }
107
+ emitReport(report, shared.diagnostics, shared.json);
108
+ return ExitCode.Success;
109
+ } catch (error) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ const report = buildBlockedReport(
112
+ ctx,
113
+ "inspect",
114
+ inputPath,
115
+ resolved,
116
+ message,
117
+ ExitCode.IrOrWriterFailure,
118
+ shared,
119
+ );
120
+ emitReport(report, shared.diagnostics, shared.json);
121
+ return ExitCode.IrOrWriterFailure;
122
+ }
123
+ }
124
+
125
+ if (stage === "index" && resolved.from === "docx" && ctx.bindings?.inspectIndex) {
126
+ try {
127
+ const index = ctx.bindings.inspectIndex(inputPath);
128
+ const report = createReport({
129
+ operation: "inspect",
130
+ toolVersion: ctx.toolVersion,
131
+ status: "success",
132
+ inputs: [{ path: inputPath, format: index.format }],
133
+ detection: {
134
+ format: index.format,
135
+ outer: index.outer,
136
+ inner: index.inner,
137
+ confidence: "verified",
138
+ hints: resolved.hints,
139
+ },
140
+ pipeline: { stages: ["index"] },
141
+ parts: index.parts,
142
+ budgets: shared.budgets,
143
+ determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
144
+ statusPolicy: {
145
+ loss: shared.loss,
146
+ strict: shared.strict,
147
+ exit_code: ExitCode.Success,
148
+ },
149
+ });
150
+ if (size !== null) {
151
+ report.budgets = { ...report.budgets, input_bytes: size };
152
+ }
153
+ emitReport(report, shared.diagnostics, shared.json);
154
+ return ExitCode.Success;
155
+ } catch (error) {
156
+ const message = error instanceof Error ? error.message : String(error);
157
+ const report = buildBlockedReport(
158
+ ctx,
159
+ "inspect",
160
+ inputPath,
161
+ resolved,
162
+ message,
163
+ ExitCode.IrOrWriterFailure,
164
+ shared,
165
+ );
166
+ emitReport(report, shared.diagnostics, shared.json);
167
+ return ExitCode.IrOrWriterFailure;
168
+ }
169
+ }
170
+
171
+ const report = createReport({
172
+ operation: "inspect",
173
+ toolVersion: ctx.toolVersion,
174
+ status: stage === "probe" ? "success" : "partial",
175
+ inputs: [{ path: inputPath, format: resolved.from }],
176
+ detection: {
177
+ format: resolved.from,
178
+ confidence: resolved.from ? "hint" : undefined,
179
+ hints: resolved.hints,
180
+ },
181
+ pipeline: {
182
+ stages: [stage],
183
+ blocked_reason:
184
+ stage === "probe"
185
+ ? undefined
186
+ : "deeper inspect stages require Acorn/Oak integration",
187
+ },
188
+ diagnostics:
189
+ stage === "probe"
190
+ ? []
191
+ : [
192
+ {
193
+ owner: "panduck",
194
+ severity: "warning",
195
+ message: `inspect stage ${stage} is not implemented yet`,
196
+ code: "panduck.inspect.not_implemented",
197
+ },
198
+ ],
199
+ budgets: shared.budgets,
200
+ determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
201
+ statusPolicy: {
202
+ loss: shared.loss,
203
+ strict: shared.strict,
204
+ exit_code: stage === "probe" ? ExitCode.Success : ExitCode.IrOrWriterFailure,
205
+ },
206
+ });
207
+
208
+ if (layoutPath) {
209
+ report.diagnostics?.push({
210
+ owner: "acorn",
211
+ severity: "warning",
212
+ message: `layout path ${layoutPath} indexing is not implemented yet`,
213
+ code: "acorn.layout.not_implemented",
214
+ });
215
+ }
216
+
217
+ if (size !== null) {
218
+ report.budgets = { ...report.budgets, input_bytes: size };
219
+ }
220
+
221
+ emitReport(report, shared.diagnostics, shared.json);
222
+ return stage === "probe" ? ExitCode.Success : ExitCode.IrOrWriterFailure;
223
+ } catch (error) {
224
+ if (error instanceof CliOptionError) {
225
+ console.error(error.message);
226
+ return error.exitCode;
227
+ }
228
+ console.error(error instanceof Error ? error.message : String(error));
229
+ return ExitCode.InvalidArgs;
230
+ }
231
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }