@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,232 @@
1
+ import { CODES, DiagnosticSink } from "./diagnostics.js";
2
+ import { classify } from "./document.js";
3
+ import { extractPage, round2 } from "./text.js";
4
+ /** Square points per square inch: 72 × 72. */
5
+ const POINTS_PER_SQUARE_INCH = 5184;
6
+ /**
7
+ * Below this many characters per square inch a page cannot be read as a
8
+ * document.
9
+ *
10
+ * A conventionally typeset Letter page — 12 pt body, ~1.4 line spacing, one inch
11
+ * margins — carries roughly 3000 characters over 93.5 in², so about 32 chars per
12
+ * square inch. Five is a sixth of that: on Letter it is around 470 characters,
13
+ * six to eight lines. Above it the text layer can carry a document; below it,
14
+ * whatever is there is a stamp, a caption, or a watermark.
15
+ *
16
+ * The inputs are published alongside the label so a caller who disagrees can
17
+ * re-threshold from `characters` and `density` without reverse-engineering this
18
+ * constant.
19
+ */
20
+ const SPARSE_DENSITY = 5;
21
+ /** A page with a handful of glyphs and nothing else is `absent`, not `sparse`. */
22
+ const MINIMUM_GLYPHS = 32;
23
+ const MINIMUM_DENSITY = 0.5;
24
+ export function classifyTextLayer(characters, density) {
25
+ if (characters === 0)
26
+ return "absent";
27
+ if (characters < MINIMUM_GLYPHS && density < MINIMUM_DENSITY)
28
+ return "absent";
29
+ if (density < SPARSE_DENSITY)
30
+ return "sparse";
31
+ return "present";
32
+ }
33
+ /** The document-level roll-up: the label holding at least 80% of pages, else mixed. */
34
+ function rollUp(pages) {
35
+ if (pages.length === 0)
36
+ return "absent";
37
+ const counts = new Map();
38
+ for (const page of pages)
39
+ counts.set(page.textLayer, (counts.get(page.textLayer) ?? 0) + 1);
40
+ for (const label of ["present", "sparse", "absent"])
41
+ if ((counts.get(label) ?? 0) / pages.length >= 0.8)
42
+ return label;
43
+ return "sparse";
44
+ }
45
+ /** Strips C0 controls and bounds a metadata string; attacker-controlled input. */
46
+ function clean(value) {
47
+ if (typeof value !== "string")
48
+ return undefined;
49
+ // eslint-disable-next-line no-control-regex
50
+ const stripped = value.replace(/[\u0000-\u001f\u007f]/g, "").trim();
51
+ return stripped ? stripped.slice(0, 1024) : undefined;
52
+ }
53
+ /**
54
+ * Converts a PDF `D:YYYYMMDDHHmmSS` date to ISO 8601 in UTC.
55
+ *
56
+ * Omitted rather than guessed when it does not parse, and never rendered in the
57
+ * host timezone: that would make the payload machine-dependent, which is the
58
+ * same reasoning the ADF converter applies to its own timestamps.
59
+ */
60
+ export function parsePdfDate(value) {
61
+ if (typeof value !== "string")
62
+ return undefined;
63
+ const match = /^D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?(?:(Z)|([+-])(\d{2})'?(\d{2})'?)?/.exec(value);
64
+ if (!match)
65
+ return undefined;
66
+ const [, year, month, day, hour, minute, second, , sign, offsetHours, offsetMinutes] = match;
67
+ const stamp = Date.UTC(Number(year), Number(month ?? "01") - 1, Number(day ?? "01"), Number(hour ?? "00"), Number(minute ?? "00"), Number(second ?? "00"));
68
+ if (Number.isNaN(stamp))
69
+ return undefined;
70
+ const offset = sign && offsetHours
71
+ ? (Number(offsetHours) * 60 + Number(offsetMinutes ?? "0")) * 60_000 * (sign === "-" ? -1 : 1)
72
+ : 0;
73
+ return new Date(stamp - offset).toISOString();
74
+ }
75
+ /** True when a structure tree carries at least one marked-content leaf. */
76
+ export function hasStructContent(node) {
77
+ if (!node)
78
+ return false;
79
+ if (node.type === "content" && node.id)
80
+ return true;
81
+ return (node.children ?? []).some(hasStructContent);
82
+ }
83
+ /**
84
+ * The document facts that cost nothing: no page is opened.
85
+ *
86
+ * Every command carries this, so a caller can read `document.tagged` off a
87
+ * `to-markdown` payload rather than making a second call. `structured` and
88
+ * `textLayer` are absent here because only a command that walks pages can
89
+ * measure them, and a field nobody measured must not read as a measurement.
90
+ */
91
+ export async function documentSummary(handle) {
92
+ const sink = new DiagnosticSink();
93
+ const { doc } = handle;
94
+ let info = {};
95
+ try {
96
+ const metadata = await handle.within(() => doc.getMetadata());
97
+ info = metadata.info ?? {};
98
+ }
99
+ catch {
100
+ sink.add({
101
+ code: CODES.metadataUnreadable,
102
+ quality: "approximate",
103
+ message: "The document's metadata could not be read; other findings are unaffected",
104
+ });
105
+ }
106
+ // getMarkInfo() returns a Map, not an object — reading `.Marked` on it is
107
+ // always undefined, which would report every document as untagged. The
108
+ // published .d.ts says otherwise and is wrong.
109
+ let tagged;
110
+ try {
111
+ const markInfo = await handle.within(() => doc.getMarkInfo());
112
+ tagged = markInfo instanceof Map && markInfo.get("Marked") === true;
113
+ }
114
+ catch {
115
+ tagged = false;
116
+ }
117
+ if (handle.encrypted)
118
+ sink.add({
119
+ code: CODES.encryptedOpenPassword,
120
+ quality: "exact",
121
+ message: "The document is encrypted but opened without a password, so its restrictions are advisory only",
122
+ });
123
+ return {
124
+ document: {
125
+ pageCount: doc.numPages,
126
+ tagged,
127
+ encrypted: handle.encrypted,
128
+ ...maybe("pdfVersion", clean(info.PDFFormatVersion)),
129
+ ...maybe("title", clean(info.Title)),
130
+ ...maybe("author", clean(info.Author)),
131
+ ...maybe("subject", clean(info.Subject)),
132
+ ...maybe("keywords", clean(info.Keywords)),
133
+ ...maybe("creator", clean(info.Creator)),
134
+ ...maybe("producer", clean(info.Producer)),
135
+ ...maybe("created", parsePdfDate(info.CreationDate)),
136
+ ...maybe("modified", parsePdfDate(info.ModDate)),
137
+ },
138
+ diagnostics: sink.all(),
139
+ };
140
+ }
141
+ /**
142
+ * The page inventory, on top of {@link documentSummary}.
143
+ *
144
+ * `probeStructure` drives whether each page's structure tree is opened. It is on
145
+ * for `inspect`, because `structured` is one of the two fields the command
146
+ * exists to report, and off elsewhere.
147
+ */
148
+ export async function inspectDocument(handle, options = {}) {
149
+ const sink = new DiagnosticSink();
150
+ const { doc } = handle;
151
+ const summary = await documentSummary(handle);
152
+ for (const item of summary.diagnostics)
153
+ sink.add({ ...item, quality: item.quality, severity: item.severity });
154
+ const wanted = options.pages ?? Array.from({ length: doc.numPages }, (_, index) => index + 1);
155
+ const pages = [];
156
+ let structuredPages = 0;
157
+ let probedPages = 0;
158
+ for (const page of wanted) {
159
+ try {
160
+ const extracted = await extractPage(handle, page);
161
+ const density = extracted.width > 0 && extracted.height > 0
162
+ ? (extracted.characters * POINTS_PER_SQUARE_INCH) / (extracted.width * extracted.height)
163
+ : 0;
164
+ const textLayer = classifyTextLayer(extracted.characters, density);
165
+ pages.push({
166
+ page,
167
+ width: extracted.width,
168
+ height: extracted.height,
169
+ rotation: extracted.rotation,
170
+ characters: extracted.characters,
171
+ density: round2(density),
172
+ textLayer,
173
+ });
174
+ if (textLayer === "absent")
175
+ sink.add({
176
+ code: CODES.noTextLayer,
177
+ quality: "unsupported",
178
+ message: "The page carries no text layer; nothing can be extracted from it",
179
+ page,
180
+ remediation: "The page is an image. Recognizing its text needs OCR, which this toolset does not do.",
181
+ });
182
+ if (options.probeStructure) {
183
+ probedPages += 1;
184
+ const proxy = await handle.within(() => doc.getPage(page));
185
+ const tree = await handle.within(() => proxy.getStructTree());
186
+ if (hasStructContent(tree))
187
+ structuredPages += 1;
188
+ proxy.cleanup();
189
+ }
190
+ }
191
+ catch (error) {
192
+ const classified = await classify(error, page);
193
+ sink.add({ ...classified, quality: "unsupported" });
194
+ }
195
+ }
196
+ if (options.probeStructure) {
197
+ if (summary.document.tagged && structuredPages === 0)
198
+ sink.add({
199
+ code: CODES.taggedButEmpty,
200
+ quality: "approximate",
201
+ message: "The document declares /MarkInfo <</Marked true>> but no page carries a usable structure tree",
202
+ remediation: "Conversion will infer structure geometrically, as though the document were untagged.",
203
+ });
204
+ if (structuredPages > 0 && structuredPages < probedPages)
205
+ sink.add({
206
+ code: CODES.structPartial,
207
+ quality: "exact",
208
+ message: `A structure tree is present on ${structuredPages} of ${probedPages} pages`,
209
+ });
210
+ }
211
+ return {
212
+ document: {
213
+ ...summary.document,
214
+ ...(options.probeStructure
215
+ ? {
216
+ structured: structuredPages === 0
217
+ ? "none"
218
+ : structuredPages === probedPages
219
+ ? "struct"
220
+ : "partial",
221
+ }
222
+ : {}),
223
+ ...(pages.length ? { textLayer: rollUp(pages) } : {}),
224
+ },
225
+ pages,
226
+ diagnostics: sink.all(),
227
+ };
228
+ }
229
+ function maybe(key, value) {
230
+ return value === undefined ? {} : { [key]: value };
231
+ }
232
+ //# sourceMappingURL=inspect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspect.js","sourceRoot":"","sources":["../../src/pdf/inspect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import { DiagnosticSink } from "./diagnostics.js";
2
+ import type { PageRuns, PositionedRun } from "./text.js";
3
+ import type { Block } from "./types.js";
4
+ /**
5
+ * Geometric structure inference.
6
+ *
7
+ * A PDF has no paragraphs, no headings, and no lists — only positioned glyph
8
+ * runs — so everything here is inference from geometry and font metrics. The
9
+ * stages are exported as pure functions taking and returning plain data, which
10
+ * is what makes each one testable on its own and lets them be reviewed one at a
11
+ * time.
12
+ *
13
+ * Every sort ends with an explicit `(page, index)` tie-break. A heuristic whose
14
+ * output flips between runs is worse than one that is wrong consistently.
15
+ */
16
+ /** A line: runs sharing a baseline, ordered left to right. */
17
+ export interface Line {
18
+ page: number;
19
+ y: number;
20
+ runs: PositionedRun[];
21
+ text: string;
22
+ /** Left edge, for indent comparisons. */
23
+ x: number;
24
+ /** The modal font size across the line's runs. */
25
+ size: number;
26
+ bold: boolean;
27
+ /** x positions where a gap wider than 1.5em opened — the only table signal. */
28
+ gaps: number[];
29
+ }
30
+ export interface NormalizeResult {
31
+ body: PositionedRun[];
32
+ dropped: number;
33
+ }
34
+ /**
35
+ * Splits out runs no axis-aligned heuristic can place.
36
+ *
37
+ * Text at an arbitrary angle and vertical writing are excluded rather than
38
+ * folded in at the wrong position, and reported. Every stage after this may
39
+ * assume upright, left-to-right runs.
40
+ */
41
+ export declare function normalize(runs: PositionedRun[], sink: DiagnosticSink): NormalizeResult;
42
+ /**
43
+ * Removes text that repeats at the same height on most pages.
44
+ *
45
+ * Requires at least four pages: below that, repetition is not evidence. A bucket
46
+ * that repeats but is *not* near an edge is left alone — that is a section title
47
+ * or a watermark, and dropping it would delete content.
48
+ */
49
+ export declare function removeArtifacts(runs: PositionedRun[], pageCount: number, pageHeight: number, sink: DiagnosticSink): PositionedRun[];
50
+ /**
51
+ * Splits a page's runs into columns, or reports that it could not.
52
+ *
53
+ * Reading a two-column page top to bottom interleaves the columns into text that
54
+ * reads like prose and is not — a degradation indistinguishable from success, so
55
+ * refusing to look is not the safe option. Looking, with a reported failure
56
+ * mode, is.
57
+ *
58
+ * A gutter is a vertical band at least 3% of the page wide containing no run and
59
+ * spanning at least 70% of the content's height. A candidate is rejected when
60
+ * the resulting sides differ by more than 6:1, which is a hanging indent or a
61
+ * figure rather than a gutter.
62
+ */
63
+ export declare function splitColumns(runs: PositionedRun[], pageWidth: number, sink: DiagnosticSink, page: number): PositionedRun[][];
64
+ /** Groups runs into lines and records the wide gaps within each. */
65
+ export declare function buildLines(runs: PositionedRun[]): Line[];
66
+ export interface LayoutOptions {
67
+ /** Outline titles to level, used to pin heading levels where they agree. */
68
+ headingLevels?: Map<string, number>;
69
+ }
70
+ /**
71
+ * Turns one document's pages into blocks.
72
+ *
73
+ * Column splitting, line grouping, and paragraph breaking are per page; the
74
+ * modal body size and the heading size ranking are computed across the whole
75
+ * document, because a per-page ranking makes heading levels drift between pages
76
+ * of the same document.
77
+ */
78
+ export declare function blocksFromLayout(pages: PageRuns[], sink: DiagnosticSink, options?: LayoutOptions): Block[];