@cairn-tool/cairn 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +74 -28
  2. package/dist/agent/audit/index.d.ts +6 -6
  3. package/dist/agent/audit/index.js +6 -17
  4. package/dist/agent/audit/index.js.map +1 -1
  5. package/dist/archive/run.d.ts +14 -7
  6. package/dist/archive/run.js +4 -1
  7. package/dist/archive/run.js.map +1 -1
  8. package/dist/archive/sets.d.ts +26 -1
  9. package/dist/archive/sets.js +108 -0
  10. package/dist/archive/sets.js.map +1 -1
  11. package/dist/atomic-write.d.ts +22 -0
  12. package/dist/atomic-write.js +42 -0
  13. package/dist/atomic-write.js.map +1 -0
  14. package/dist/binary-kind.d.ts +13 -0
  15. package/dist/binary-kind.js +25 -0
  16. package/dist/binary-kind.js.map +1 -0
  17. package/dist/cli.js +48 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/commands/archive.js +11 -5
  20. package/dist/commands/archive.js.map +1 -1
  21. package/dist/commands/jira.js +1 -21
  22. package/dist/commands/jira.js.map +1 -1
  23. package/dist/commands/pdf.d.ts +29 -0
  24. package/dist/commands/pdf.js +345 -0
  25. package/dist/commands/pdf.js.map +1 -0
  26. package/dist/contract/registry.js +65 -1
  27. package/dist/contract/registry.js.map +1 -1
  28. package/dist/contract/schemas/index.js +2 -0
  29. package/dist/contract/schemas/index.js.map +1 -1
  30. package/dist/contract/schemas/pdf.d.ts +2 -0
  31. package/dist/contract/schemas/pdf.js +292 -0
  32. package/dist/contract/schemas/pdf.js.map +1 -0
  33. package/dist/jira/adf/to-markdown.js +2 -32
  34. package/dist/jira/adf/to-markdown.js.map +1 -1
  35. package/dist/markdown-stringify.d.ts +35 -0
  36. package/dist/markdown-stringify.js +44 -0
  37. package/dist/markdown-stringify.js.map +1 -0
  38. package/dist/pdf/attachments.d.ts +50 -0
  39. package/dist/pdf/attachments.js +229 -0
  40. package/dist/pdf/attachments.js.map +1 -0
  41. package/dist/pdf/diagnostics.d.ts +110 -0
  42. package/dist/pdf/diagnostics.js +137 -0
  43. package/dist/pdf/diagnostics.js.map +1 -0
  44. package/dist/pdf/document.d.ts +159 -0
  45. package/dist/pdf/document.js +272 -0
  46. package/dist/pdf/document.js.map +1 -0
  47. package/dist/pdf/forms.d.ts +9 -0
  48. package/dist/pdf/forms.js +101 -0
  49. package/dist/pdf/forms.js.map +1 -0
  50. package/dist/pdf/inspect.d.ts +42 -0
  51. package/dist/pdf/inspect.js +232 -0
  52. package/dist/pdf/inspect.js.map +1 -0
  53. package/dist/pdf/layout.d.ts +78 -0
  54. package/dist/pdf/layout.js +524 -0
  55. package/dist/pdf/layout.js.map +1 -0
  56. package/dist/pdf/outline.d.ts +30 -0
  57. package/dist/pdf/outline.js +127 -0
  58. package/dist/pdf/outline.js.map +1 -0
  59. package/dist/pdf/read.d.ts +62 -0
  60. package/dist/pdf/read.js +193 -0
  61. package/dist/pdf/read.js.map +1 -0
  62. package/dist/pdf/struct.d.ts +35 -0
  63. package/dist/pdf/struct.js +226 -0
  64. package/dist/pdf/struct.js.map +1 -0
  65. package/dist/pdf/text.d.ts +71 -0
  66. package/dist/pdf/text.js +166 -0
  67. package/dist/pdf/text.js.map +1 -0
  68. package/dist/pdf/to-markdown.d.ts +22 -0
  69. package/dist/pdf/to-markdown.js +289 -0
  70. package/dist/pdf/to-markdown.js.map +1 -0
  71. package/dist/pdf/types.d.ts +205 -0
  72. package/dist/pdf/types.js +2 -0
  73. package/dist/pdf/types.js.map +1 -0
  74. package/dist/pdf/validate.d.ts +51 -0
  75. package/dist/pdf/validate.js +136 -0
  76. package/dist/pdf/validate.js.map +1 -0
  77. package/dist/serve/pdf-tools.d.ts +2 -0
  78. package/dist/serve/pdf-tools.js +196 -0
  79. package/dist/serve/pdf-tools.js.map +1 -0
  80. package/dist/serve/tools.d.ts +2 -17
  81. package/dist/serve/tools.js +4 -0
  82. package/dist/serve/tools.js.map +1 -1
  83. package/dist/serve/types.d.ts +22 -0
  84. package/dist/serve/types.js +2 -0
  85. package/dist/serve/types.js.map +1 -0
  86. package/dist/usage/providers/cursor.d.ts +2 -0
  87. package/dist/usage/providers/cursor.js +570 -0
  88. package/dist/usage/providers/cursor.js.map +1 -0
  89. package/dist/usage/providers/index.js +2 -0
  90. package/dist/usage/providers/index.js.map +1 -1
  91. package/package.json +2 -1
@@ -0,0 +1,229 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { binaryKind } from "../binary-kind.js";
5
+ import { writeAtomically } from "../atomic-write.js";
6
+ import { CODES, DiagnosticSink } from "./diagnostics.js";
7
+ /**
8
+ * Embedded files: inventory, and extraction under `--extract`.
9
+ *
10
+ * Two rules shape the module, and both are security properties rather than
11
+ * conveniences.
12
+ *
13
+ * **Binary never reaches stdout.** `--extract` is the only way bytes leave this
14
+ * command. A command that emits UTF-8 under one flag and a binary blob under
15
+ * another is a contract no consumer can code against.
16
+ *
17
+ * **Extraction is planned before anything is written.** An embedded file's
18
+ * stored name is attacker-controlled and will contain `../` eventually. Every
19
+ * destination is resolved and checked over the whole set first, so a single
20
+ * refused path means nothing is written at all — the same plan-then-write shape
21
+ * `src/agent/install` uses, and deliberately not `archive extract`, which
22
+ * sanitizes with `path.basename` alone and has no collision handling.
23
+ *
24
+ * pdf.js already strips the path — a `/F` of `../../etc/evil.csv` arrives as
25
+ * `filename: "evil.csv"` with `rawFilename` preserved. That is not treated as
26
+ * sufficient. The sanitization here is this command's own, and the payload
27
+ * reports both names so a caller can see what was renamed.
28
+ */
29
+ /**
30
+ * Total decoded attachment bytes an inventory will hold at once.
31
+ *
32
+ * The inventory fetches content because size and SHA-256 are what make it
33
+ * actionable, and the document is already resident so a fetch is decompression
34
+ * rather than I/O. The cap bounds a document that claims a thousand large
35
+ * embedded files, and it reports rather than truncating silently.
36
+ */
37
+ export const MAX_ATTACHMENT_BYTES = 128 * 1024 * 1024;
38
+ /** A name is refused outright rather than repaired into something surprising. */
39
+ const RESERVED = new Set([".", "..", ""]);
40
+ /**
41
+ * Windows device names, which are not writable files on that platform whatever
42
+ * extension follows. Refused on every platform so an extraction behaves the same
43
+ * everywhere rather than only failing on the host that happens to care.
44
+ */
45
+ const DEVICE = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i;
46
+ /**
47
+ * The basename this command is willing to write, or null when there is none.
48
+ *
49
+ * Deliberately stricter than "strip the directory": a name that still carries a
50
+ * separator, a NUL, or a drive letter after basename is a name whose author was
51
+ * trying something, and guessing at their intent is how a traversal gets through.
52
+ */
53
+ export function sanitizeName(candidate) {
54
+ const trimmed = candidate.replace(/\0/g, "").trim();
55
+ if (RESERVED.has(trimmed))
56
+ return null;
57
+ // Both separators, because a Windows-authored name reaching a POSIX host keeps
58
+ // its backslashes and `path.basename` here would not treat them as separators.
59
+ const base = trimmed.split(/[/\\]/).pop() ?? "";
60
+ if (RESERVED.has(base))
61
+ return null;
62
+ if (/^[A-Za-z]:/.test(base))
63
+ return null;
64
+ if (DEVICE.test(base))
65
+ return null;
66
+ // Anything left is a plain file name, `.gitignore` included: a leading dot is
67
+ // a real name, and `.` and `..` were already refused above.
68
+ return base || null;
69
+ }
70
+ /** `name-2.csv`, `name-3.csv`, … — never an overwrite. */
71
+ function disambiguate(name, taken, directory) {
72
+ const extension = path.extname(name);
73
+ const stem = name.slice(0, name.length - extension.length);
74
+ let candidate = name;
75
+ let counter = 2;
76
+ while (taken.has(candidate.toLowerCase()) || fs.existsSync(path.join(directory, candidate))) {
77
+ candidate = `${stem}-${counter}${extension}`;
78
+ counter += 1;
79
+ }
80
+ return candidate;
81
+ }
82
+ export async function readAttachments(handle, options = {}) {
83
+ const sink = new DiagnosticSink();
84
+ const entries = await handle.within(() => handle.doc.getAttachments());
85
+ if (!entries || entries.size === 0)
86
+ return { attachments: [], diagnostics: sink.all() };
87
+ const keys = [...entries.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
88
+ const attachments = [];
89
+ const contents = new Map();
90
+ let budget = 0;
91
+ for (const id of keys) {
92
+ const entry = entries.get(id) ?? {};
93
+ const rawFilename = entry.rawFilename ?? entry.filename ?? id;
94
+ const filename = entry.filename ?? id;
95
+ const attachment = {
96
+ id,
97
+ filename,
98
+ rawFilename,
99
+ ...(entry.description ? { description: entry.description } : {}),
100
+ };
101
+ if (budget >= MAX_ATTACHMENT_BYTES) {
102
+ sink.add({
103
+ code: CODES.attachmentBudgetReached,
104
+ quality: "approximate",
105
+ construct: filename,
106
+ message: `Attachment budget of ${MAX_ATTACHMENT_BYTES} bytes reached; '${filename}' was listed without size or hash`,
107
+ remediation: "Extract the earlier attachments first, or read the document in parts.",
108
+ });
109
+ attachments.push(attachment);
110
+ continue;
111
+ }
112
+ let content;
113
+ try {
114
+ content = await handle.within(() => handle.doc.getAttachmentContent(id));
115
+ }
116
+ catch {
117
+ content = null;
118
+ }
119
+ if (!content) {
120
+ sink.add({
121
+ code: CODES.attachmentUnreadable,
122
+ quality: "approximate",
123
+ construct: filename,
124
+ message: `Attachment '${filename}' could not be decoded; it is listed without size or hash`,
125
+ });
126
+ attachments.push(attachment);
127
+ continue;
128
+ }
129
+ const buffer = Buffer.from(content.buffer, content.byteOffset, content.byteLength);
130
+ budget += buffer.byteLength;
131
+ attachment.bytes = buffer.byteLength;
132
+ attachment.sha256 = crypto.createHash("sha256").update(buffer).digest("hex");
133
+ const kind = binaryKind(buffer);
134
+ if (kind)
135
+ attachment.binary = kind;
136
+ contents.set(id, content);
137
+ attachments.push(attachment);
138
+ }
139
+ if (!options.extract)
140
+ return { attachments, diagnostics: sink.all() };
141
+ // ---- Planning. Nothing below this line writes until every entry is resolved.
142
+ const directory = path.resolve(options.extract);
143
+ const real = fs.existsSync(directory) ? fs.realpathSync(directory) : directory;
144
+ if (fs.existsSync(real) && !fs.statSync(real).isDirectory())
145
+ throw new Error(`--extract is not a directory: ${options.extract}`);
146
+ const taken = new Set();
147
+ const plan = [];
148
+ let refused = false;
149
+ for (const attachment of attachments) {
150
+ const content = contents.get(attachment.id);
151
+ if (!content)
152
+ continue;
153
+ const safe = sanitizeName(attachment.filename) ?? sanitizeName(attachment.rawFilename);
154
+ if (!safe) {
155
+ sink.add({
156
+ code: CODES.attachmentPathRefused,
157
+ severity: "error",
158
+ quality: "unsupported",
159
+ construct: attachment.rawFilename,
160
+ message: `Attachment '${attachment.rawFilename}' has no name that can be safely written`,
161
+ remediation: "Nothing was extracted. Inspect the document before trusting its file names.",
162
+ });
163
+ refused = true;
164
+ continue;
165
+ }
166
+ if (safe !== attachment.rawFilename)
167
+ sink.add({
168
+ code: CODES.attachmentNameSanitized,
169
+ quality: "approximate",
170
+ construct: attachment.rawFilename,
171
+ message: `Stored name '${attachment.rawFilename}' was sanitized to '${safe}' before writing`,
172
+ });
173
+ const chosen = disambiguate(safe, taken, real);
174
+ if (chosen !== safe)
175
+ sink.add({
176
+ code: CODES.attachmentNameCollided,
177
+ quality: "exact",
178
+ construct: safe,
179
+ message: `'${safe}' already exists or was already claimed; written as '${chosen}'`,
180
+ });
181
+ const destination = path.resolve(real, chosen);
182
+ // Belt and braces: `chosen` carries no separator by construction, so this
183
+ // cannot fail — which is exactly why it is cheap to assert rather than trust.
184
+ const relative = path.relative(real, destination);
185
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
186
+ sink.add({
187
+ code: CODES.attachmentPathRefused,
188
+ severity: "error",
189
+ quality: "unsupported",
190
+ construct: chosen,
191
+ message: `Destination for '${chosen}' escapes ${options.extract}`,
192
+ remediation: "Nothing was extracted.",
193
+ });
194
+ refused = true;
195
+ continue;
196
+ }
197
+ taken.add(chosen.toLowerCase());
198
+ plan.push({ attachment, content, destination });
199
+ }
200
+ if (refused)
201
+ return { attachments, diagnostics: sink.all() };
202
+ // The collision check above ran during planning, so a file appearing in the
203
+ // target between then and now would still be replaced. That window is left
204
+ // open deliberately: closing it means either writing some files and refusing
205
+ // others — which breaks the all-or-nothing property that makes a refusal
206
+ // safe to act on — or holding the directory locked, which a local extraction
207
+ // tool has no business doing.
208
+ fs.mkdirSync(real, { recursive: true });
209
+ for (const item of plan) {
210
+ writeAtomically(item.destination, item.content);
211
+ item.attachment.written = item.destination;
212
+ }
213
+ return { attachments, diagnostics: sink.all() };
214
+ }
215
+ /** The human and llm rendering: one line per embedded file. */
216
+ export function formatAttachments(attachments) {
217
+ if (attachments.length === 0)
218
+ return "no embedded files\n";
219
+ const width = Math.max(...attachments.map((item) => item.filename.length));
220
+ const lines = attachments.map((item) => {
221
+ const size = item.bytes === undefined ? " ?" : String(item.bytes).padStart(9);
222
+ const hash = item.sha256 ? ` ${item.sha256.slice(0, 12)}` : " ".repeat(13);
223
+ const flags = [item.binary ?? "", item.written ? `-> ${item.written}` : ""].filter(Boolean);
224
+ const suffix = flags.length ? ` ${flags.join(" ")}` : "";
225
+ return `${size}${hash} ${item.filename.padEnd(width)}${suffix}`;
226
+ });
227
+ return `${lines.join("\n")}\n`;
228
+ }
229
+ //# sourceMappingURL=attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.js","sourceRoot":"","sources":["../../src/pdf/attachments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,110 @@
1
+ import type { MappingQuality, PdfDiagnostic } from "./types.js";
2
+ /**
3
+ * Every `AP###` code, grouped by where the condition is detected.
4
+ *
5
+ * The range follows the detection site, never the command the user typed — the
6
+ * same rule the `AB` and `AD` families follow, which is why `AP020` is emitted
7
+ * by both `pdf text` and `pdf validate`. A retired code is never reused: a
8
+ * consumer may still be suppressing it.
9
+ *
10
+ * `tests/unit/diagnostic-codes.test.ts` fails on a code emitted here that
11
+ * `docs/formats/diagnostic-codes.md` does not document, *and* on a documented
12
+ * code nothing emits. Add both halves in the same change.
13
+ */
14
+ export declare const CODES: {
15
+ readonly failure: "AP001";
16
+ readonly notAPdf: "AP002";
17
+ readonly tooLarge: "AP003";
18
+ readonly notRegularFile: "AP004";
19
+ readonly timedOut: "AP005";
20
+ readonly emptyInput: "AP006";
21
+ readonly leadingBytesIgnored: "AP007";
22
+ readonly passwordRequired: "AP010";
23
+ readonly passwordIncorrect: "AP011";
24
+ readonly tooManyPages: "AP012";
25
+ readonly pageRangeInvalid: "AP013";
26
+ readonly pageUnreadable: "AP020";
27
+ readonly contentUndecodable: "AP021";
28
+ readonly noTextLayer: "AP050";
29
+ readonly destinationUnresolved: "AP080";
30
+ readonly outlineTooDeep: "AP081";
31
+ readonly unparseable: "AP100";
32
+ readonly xrefReconstructed: "AP101";
33
+ readonly fontSubstituted: "AP110";
34
+ readonly filterUnsupported: "AP111";
35
+ readonly metadataUnreadable: "AP112";
36
+ readonly encryptedOpenPassword: "AP113";
37
+ readonly taggedButEmpty: "AP114";
38
+ readonly structPartial: "AP115";
39
+ readonly parserWarning: "AP120";
40
+ readonly conversionPath: "AP200";
41
+ readonly readingOrderUncertain: "AP201";
42
+ readonly tableFlattened: "AP202";
43
+ readonly rotatedTextDropped: "AP203";
44
+ readonly artifactsRemoved: "AP205";
45
+ readonly paragraphSpansPages: "AP206";
46
+ readonly pageSubsetConverted: "AP208";
47
+ readonly columnsDetected: "AP210";
48
+ readonly headingLevelsCollapsed: "AP211";
49
+ readonly listNumberingLost: "AP213";
50
+ readonly hyphenationRejoined: "AP214";
51
+ readonly figureTextOnly: "AP216";
52
+ readonly unknownRole: "AP219";
53
+ readonly cellSpanDropped: "AP220";
54
+ readonly headingLevelInferred: "AP224";
55
+ readonly listOrderingInferred: "AP225";
56
+ readonly inlineStyleInferred: "AP230";
57
+ readonly ligaturesExpanded: "AP231";
58
+ readonly controlCharactersStripped: "AP232";
59
+ readonly attachmentUnreadable: "AP300";
60
+ readonly attachmentNameSanitized: "AP301";
61
+ readonly attachmentNameCollided: "AP302";
62
+ readonly attachmentPathRefused: "AP303";
63
+ readonly attachmentBudgetReached: "AP304";
64
+ readonly formXfa: "AP311";
65
+ readonly formFieldPageUnresolved: "AP312";
66
+ };
67
+ export interface DiagnosticInput {
68
+ code: string;
69
+ message: string;
70
+ quality: MappingQuality;
71
+ page?: number;
72
+ construct?: string;
73
+ remediation?: string;
74
+ /** Overrides the quality-derived severity. Use only to refuse. */
75
+ severity?: DiagnosticSeverityOverride;
76
+ }
77
+ type DiagnosticSeverityOverride = PdfDiagnostic["severity"];
78
+ /**
79
+ * Builds a finding, deriving severity from quality unless overridden.
80
+ *
81
+ * Optional fields are spread conditionally so an absent one never serializes as
82
+ * `undefined` — the payload is published and a null-ish key is a contract
83
+ * change nobody meant to make.
84
+ */
85
+ export declare function diagnostic(input: DiagnosticInput): PdfDiagnostic;
86
+ /**
87
+ * Collects findings, deduplicating and ordering them.
88
+ *
89
+ * Deliberately a copy of `src/jira/adf/diagnostics.ts`'s sink rather than a
90
+ * shared generic. The two differ in the one place that matters: this one orders
91
+ * by *numeric* page before the construct, because a bytewise sort puts page 10
92
+ * before page 2 and a reader scanning a 300-page document's findings would be
93
+ * reading them out of order. Parameterizing a shared sink on a key function and
94
+ * a comparator is more type noise than the twenty-five duplicated lines remove.
95
+ */
96
+ export declare class DiagnosticSink {
97
+ private readonly items;
98
+ private readonly seen;
99
+ add(input: DiagnosticInput): void;
100
+ get length(): number;
101
+ /**
102
+ * A copy, ordered by code, then page, then construct.
103
+ *
104
+ * The construct comparison is byte comparison, never `localeCompare`: that is
105
+ * ICU-build and locale dependent, so a differently configured CI runner would
106
+ * reorder the payload.
107
+ */
108
+ all(): PdfDiagnostic[];
109
+ }
110
+ export {};
@@ -0,0 +1,137 @@
1
+ import { severityFor } from "../mapping-quality.js";
2
+ /**
3
+ * Every `AP###` code, grouped by where the condition is detected.
4
+ *
5
+ * The range follows the detection site, never the command the user typed — the
6
+ * same rule the `AB` and `AD` families follow, which is why `AP020` is emitted
7
+ * by both `pdf text` and `pdf validate`. A retired code is never reused: a
8
+ * consumer may still be suppressing it.
9
+ *
10
+ * `tests/unit/diagnostic-codes.test.ts` fails on a code emitted here that
11
+ * `docs/formats/diagnostic-codes.md` does not document, *and* on a documented
12
+ * code nothing emits. Add both halves in the same change.
13
+ */
14
+ export const CODES = {
15
+ // AP0xx invocation, input bounds, and opening the document
16
+ failure: "AP001",
17
+ notAPdf: "AP002",
18
+ tooLarge: "AP003",
19
+ notRegularFile: "AP004",
20
+ timedOut: "AP005",
21
+ emptyInput: "AP006",
22
+ leadingBytesIgnored: "AP007",
23
+ passwordRequired: "AP010",
24
+ passwordIncorrect: "AP011",
25
+ tooManyPages: "AP012",
26
+ pageRangeInvalid: "AP013",
27
+ // AP02x the page tree and per-page decoding
28
+ pageUnreadable: "AP020",
29
+ contentUndecodable: "AP021",
30
+ // AP05x the text layer
31
+ noTextLayer: "AP050",
32
+ // AP08x the outline
33
+ destinationUnresolved: "AP080",
34
+ outlineTooDeep: "AP081",
35
+ // AP1xx structural integrity, reported by validate
36
+ unparseable: "AP100",
37
+ xrefReconstructed: "AP101",
38
+ fontSubstituted: "AP110",
39
+ filterUnsupported: "AP111",
40
+ metadataUnreadable: "AP112",
41
+ encryptedOpenPassword: "AP113",
42
+ taggedButEmpty: "AP114",
43
+ structPartial: "AP115",
44
+ parserWarning: "AP120",
45
+ // AP2xx conversion to Markdown
46
+ conversionPath: "AP200",
47
+ readingOrderUncertain: "AP201",
48
+ tableFlattened: "AP202",
49
+ rotatedTextDropped: "AP203",
50
+ artifactsRemoved: "AP205",
51
+ paragraphSpansPages: "AP206",
52
+ pageSubsetConverted: "AP208",
53
+ columnsDetected: "AP210",
54
+ headingLevelsCollapsed: "AP211",
55
+ listNumberingLost: "AP213",
56
+ hyphenationRejoined: "AP214",
57
+ figureTextOnly: "AP216",
58
+ unknownRole: "AP219",
59
+ cellSpanDropped: "AP220",
60
+ headingLevelInferred: "AP224",
61
+ listOrderingInferred: "AP225",
62
+ inlineStyleInferred: "AP230",
63
+ ligaturesExpanded: "AP231",
64
+ controlCharactersStripped: "AP232",
65
+ // AP3xx embedded files and form fields
66
+ attachmentUnreadable: "AP300",
67
+ attachmentNameSanitized: "AP301",
68
+ attachmentNameCollided: "AP302",
69
+ attachmentPathRefused: "AP303",
70
+ attachmentBudgetReached: "AP304",
71
+ formXfa: "AP311",
72
+ formFieldPageUnresolved: "AP312",
73
+ };
74
+ /**
75
+ * Builds a finding, deriving severity from quality unless overridden.
76
+ *
77
+ * Optional fields are spread conditionally so an absent one never serializes as
78
+ * `undefined` — the payload is published and a null-ish key is a contract
79
+ * change nobody meant to make.
80
+ */
81
+ export function diagnostic(input) {
82
+ const { code, message, quality, severity, page, construct, remediation } = input;
83
+ return {
84
+ code,
85
+ severity: severity ?? severityFor(quality),
86
+ message,
87
+ quality,
88
+ ...(page !== undefined ? { page } : {}),
89
+ ...(construct ? { construct } : {}),
90
+ ...(remediation ? { remediation } : {}),
91
+ };
92
+ }
93
+ /**
94
+ * Collects findings, deduplicating and ordering them.
95
+ *
96
+ * Deliberately a copy of `src/jira/adf/diagnostics.ts`'s sink rather than a
97
+ * shared generic. The two differ in the one place that matters: this one orders
98
+ * by *numeric* page before the construct, because a bytewise sort puts page 10
99
+ * before page 2 and a reader scanning a 300-page document's findings would be
100
+ * reading them out of order. Parameterizing a shared sink on a key function and
101
+ * a comparator is more type noise than the twenty-five duplicated lines remove.
102
+ */
103
+ export class DiagnosticSink {
104
+ items = [];
105
+ seen = new Set();
106
+ add(input) {
107
+ const key = `${input.code} ${input.construct ?? ""} ${input.page ?? ""}`;
108
+ if (this.seen.has(key))
109
+ return;
110
+ this.seen.add(key);
111
+ this.items.push(diagnostic(input));
112
+ }
113
+ get length() {
114
+ return this.items.length;
115
+ }
116
+ /**
117
+ * A copy, ordered by code, then page, then construct.
118
+ *
119
+ * The construct comparison is byte comparison, never `localeCompare`: that is
120
+ * ICU-build and locale dependent, so a differently configured CI runner would
121
+ * reorder the payload.
122
+ */
123
+ all() {
124
+ return [...this.items].sort((a, b) => {
125
+ if (a.code !== b.code)
126
+ return a.code < b.code ? -1 : 1;
127
+ const pageA = a.page ?? 0;
128
+ const pageB = b.page ?? 0;
129
+ if (pageA !== pageB)
130
+ return pageA - pageB;
131
+ const left = a.construct ?? "";
132
+ const right = b.construct ?? "";
133
+ return left < right ? -1 : left > right ? 1 : 0;
134
+ });
135
+ }
136
+ }
137
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/pdf/diagnostics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,159 @@
1
+ import type { PdfDiagnostic } from "./types.js";
2
+ /**
3
+ * The single place a PDF is opened.
4
+ *
5
+ * Everything hostile about the format is contained here: one hardened option
6
+ * set, one console capture, one deadline, one teardown, and one place a pdf.js
7
+ * exception becomes an `AP###`. Every other module in `src/pdf` receives an
8
+ * already-open handle and never imports pdfjs itself.
9
+ */
10
+ /**
11
+ * The minimum of pdf.js this project uses, written out by hand.
12
+ *
13
+ * Same discipline as `src/sqlite.ts`: `tsconfig` sets `declaration: true`, so a
14
+ * pdfjs type appearing in an exported signature would put that package's types
15
+ * into cairn's published `.d.ts`. pdfjs's own definitions are also JSDoc-derived
16
+ * and `any`-heavy, which would leak straight through.
17
+ */
18
+ export interface PdfTextItem {
19
+ str?: string;
20
+ dir?: string;
21
+ transform?: number[];
22
+ width?: number;
23
+ height?: number;
24
+ fontName?: string;
25
+ hasEOL?: boolean;
26
+ /** Marked-content items carry these two instead. */
27
+ type?: string;
28
+ id?: string;
29
+ }
30
+ export interface PdfTextStyle {
31
+ fontFamily?: string;
32
+ ascent?: number;
33
+ descent?: number;
34
+ vertical?: boolean;
35
+ }
36
+ export interface PdfStructNode {
37
+ role?: string;
38
+ type?: string;
39
+ id?: string;
40
+ children?: PdfStructNode[];
41
+ }
42
+ export interface PdfViewport {
43
+ width: number;
44
+ height: number;
45
+ rotation: number;
46
+ convertToViewportPoint(x: number, y: number): number[];
47
+ }
48
+ export interface PdfPageHandle {
49
+ getViewport(params: {
50
+ scale: number;
51
+ }): PdfViewport;
52
+ getTextContent(params?: {
53
+ includeMarkedContent?: boolean;
54
+ }): Promise<{
55
+ items: PdfTextItem[];
56
+ styles: Record<string, PdfTextStyle>;
57
+ }>;
58
+ getStructTree(): Promise<PdfStructNode | null>;
59
+ getXfa(): Promise<unknown>;
60
+ cleanup(): boolean;
61
+ }
62
+ /**
63
+ * One embedded file, as `getAttachments()` describes it.
64
+ *
65
+ * `content` is declared optional because pdf.js does not populate it: the
66
+ * lookup is metadata only and the bytes come from `getAttachmentContent`. That
67
+ * is what makes `pdf attachments`'s inventory cheap and its extraction the
68
+ * opt-in.
69
+ */
70
+ export interface PdfAttachmentEntry {
71
+ /** Basename pdf.js derived from the file spec. Already path-stripped. */
72
+ filename?: string;
73
+ /** The stored name verbatim, traversal and all. */
74
+ rawFilename?: string;
75
+ description?: string;
76
+ content?: Uint8Array;
77
+ }
78
+ export interface PdfPageHandleExtras {
79
+ getXfa(): Promise<unknown>;
80
+ }
81
+ export interface PdfDocumentHandle {
82
+ numPages: number;
83
+ getPage(page: number): Promise<PdfPageHandle>;
84
+ getMetadata(): Promise<{
85
+ info?: Record<string, unknown>;
86
+ metadata?: unknown;
87
+ }>;
88
+ getOutline(): Promise<any[] | null>;
89
+ getDestination(id: string): Promise<any[] | null>;
90
+ getPageIndex(ref: unknown): Promise<number>;
91
+ getMarkInfo(): Promise<unknown>;
92
+ getPermissions(): Promise<Set<number> | null>;
93
+ /**
94
+ * A `Map`, like `getMarkInfo`, `getFieldObjects`, and `getJSActions` — never a
95
+ * plain object. `Object.entries()` on any of the four silently yields nothing.
96
+ */
97
+ getAttachments(): Promise<Map<string, PdfAttachmentEntry> | null>;
98
+ /** Bytes for one attachment, keyed by its `getAttachments()` map key. */
99
+ getAttachmentContent(id: string): Promise<Uint8Array | null>;
100
+ /** A `Map` keyed by fully-qualified field name; each value is one array of widgets. */
101
+ getFieldObjects(): Promise<Map<string, unknown[]> | null>;
102
+ /** True when the form is XFA-only, so `getFieldObjects` has nothing to report. */
103
+ isPureXfa: boolean;
104
+ cleanup(): Promise<unknown>;
105
+ }
106
+ /** What every command works against. */
107
+ export interface OpenDocument {
108
+ doc: PdfDocumentHandle;
109
+ /** Warnings pdf.js emitted while this document was open, in emission order. */
110
+ notices: readonly string[];
111
+ /** True when a password was supplied and accepted, or none was needed. */
112
+ encrypted: boolean;
113
+ /** Races a pdfjs promise against the deadline. */
114
+ within<T>(work: () => Promise<T>): Promise<T>;
115
+ }
116
+ export interface OpenOptions {
117
+ /** Wall-clock budget in milliseconds for the whole operation. */
118
+ timeoutMs: number;
119
+ /** Refuse a document with more pages than this before touching any of them. */
120
+ maxPages: number;
121
+ }
122
+ /** Raised when the wall-clock budget expires. Classified as `AP005`. */
123
+ export declare class DeadlineExceeded extends Error {
124
+ readonly ms: number;
125
+ constructor(ms: number);
126
+ }
127
+ /** Raised when the install is broken, rather than the document. */
128
+ export declare class PdfjsUnavailableError extends Error {
129
+ constructor(cause: string);
130
+ }
131
+ /**
132
+ * Opens a document, runs `body`, and tears down — in that order, always.
133
+ *
134
+ * A scoped-resource function rather than an open/close pair so a caller cannot
135
+ * forget the teardown. On 6.3.289 pdf.js uses the main-thread fake worker in
136
+ * Node (no worker thread is spawned, verified), so a leaked handle does not hang
137
+ * the process — but `destroy()` still releases the parsed object graph, and a
138
+ * future version spawning a real worker would make it load-bearing.
139
+ */
140
+ export declare function withDocument<T>(data: Uint8Array, options: OpenOptions, body: (handle: OpenDocument) => Promise<T>): Promise<T>;
141
+ /** Raised before any page is touched, when the document is larger than allowed. */
142
+ export declare class PageBudgetExceeded extends Error {
143
+ readonly pages: number;
144
+ readonly limit: number;
145
+ constructor(pages: number, limit: number);
146
+ }
147
+ /**
148
+ * Maps a thrown value onto a finding.
149
+ *
150
+ * Matched by class where pdf.js exports one, never by `error.name`: a name is a
151
+ * string that has drifted before, and the classes are exported precisely so a
152
+ * consumer can branch on them.
153
+ *
154
+ * The distinction between `AP002` and `AP100` is worth keeping: `AP002` means
155
+ * the bytes never had a `%PDF-` header, decided by `read.ts` before pdf.js ran,
156
+ * and `AP100` means it looked like a PDF and could not be parsed. That is the
157
+ * difference between "wrong file" and "damaged file".
158
+ */
159
+ export declare function classify(error: unknown, page?: number): Promise<PdfDiagnostic>;