@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.
- package/README.md +74 -28
- package/dist/agent/audit/index.d.ts +6 -6
- package/dist/agent/audit/index.js +6 -17
- package/dist/agent/audit/index.js.map +1 -1
- package/dist/archive/run.d.ts +14 -7
- package/dist/archive/run.js +4 -1
- package/dist/archive/run.js.map +1 -1
- package/dist/archive/sets.d.ts +26 -1
- package/dist/archive/sets.js +108 -0
- package/dist/archive/sets.js.map +1 -1
- package/dist/atomic-write.d.ts +22 -0
- package/dist/atomic-write.js +42 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/binary-kind.d.ts +13 -0
- package/dist/binary-kind.js +25 -0
- package/dist/binary-kind.js.map +1 -0
- package/dist/cli.js +48 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/archive.js +11 -5
- package/dist/commands/archive.js.map +1 -1
- package/dist/commands/jira.js +1 -21
- package/dist/commands/jira.js.map +1 -1
- package/dist/commands/pdf.d.ts +29 -0
- package/dist/commands/pdf.js +345 -0
- package/dist/commands/pdf.js.map +1 -0
- package/dist/contract/registry.js +65 -1
- package/dist/contract/registry.js.map +1 -1
- package/dist/contract/schemas/index.js +2 -0
- package/dist/contract/schemas/index.js.map +1 -1
- package/dist/contract/schemas/pdf.d.ts +2 -0
- package/dist/contract/schemas/pdf.js +292 -0
- package/dist/contract/schemas/pdf.js.map +1 -0
- package/dist/jira/adf/to-markdown.js +2 -32
- package/dist/jira/adf/to-markdown.js.map +1 -1
- package/dist/markdown-stringify.d.ts +35 -0
- package/dist/markdown-stringify.js +44 -0
- package/dist/markdown-stringify.js.map +1 -0
- package/dist/pdf/attachments.d.ts +50 -0
- package/dist/pdf/attachments.js +229 -0
- package/dist/pdf/attachments.js.map +1 -0
- package/dist/pdf/diagnostics.d.ts +110 -0
- package/dist/pdf/diagnostics.js +137 -0
- package/dist/pdf/diagnostics.js.map +1 -0
- package/dist/pdf/document.d.ts +159 -0
- package/dist/pdf/document.js +272 -0
- package/dist/pdf/document.js.map +1 -0
- package/dist/pdf/forms.d.ts +9 -0
- package/dist/pdf/forms.js +101 -0
- package/dist/pdf/forms.js.map +1 -0
- package/dist/pdf/inspect.d.ts +42 -0
- package/dist/pdf/inspect.js +232 -0
- package/dist/pdf/inspect.js.map +1 -0
- package/dist/pdf/layout.d.ts +78 -0
- package/dist/pdf/layout.js +524 -0
- package/dist/pdf/layout.js.map +1 -0
- package/dist/pdf/outline.d.ts +30 -0
- package/dist/pdf/outline.js +127 -0
- package/dist/pdf/outline.js.map +1 -0
- package/dist/pdf/read.d.ts +62 -0
- package/dist/pdf/read.js +193 -0
- package/dist/pdf/read.js.map +1 -0
- package/dist/pdf/struct.d.ts +35 -0
- package/dist/pdf/struct.js +226 -0
- package/dist/pdf/struct.js.map +1 -0
- package/dist/pdf/text.d.ts +71 -0
- package/dist/pdf/text.js +166 -0
- package/dist/pdf/text.js.map +1 -0
- package/dist/pdf/to-markdown.d.ts +22 -0
- package/dist/pdf/to-markdown.js +289 -0
- package/dist/pdf/to-markdown.js.map +1 -0
- package/dist/pdf/types.d.ts +205 -0
- package/dist/pdf/types.js +2 -0
- package/dist/pdf/types.js.map +1 -0
- package/dist/pdf/validate.d.ts +51 -0
- package/dist/pdf/validate.js +136 -0
- package/dist/pdf/validate.js.map +1 -0
- package/dist/serve/pdf-tools.d.ts +2 -0
- package/dist/serve/pdf-tools.js +196 -0
- package/dist/serve/pdf-tools.js.map +1 -0
- package/dist/serve/tools.d.ts +2 -17
- package/dist/serve/tools.js +4 -0
- package/dist/serve/tools.js.map +1 -1
- package/dist/serve/types.d.ts +22 -0
- package/dist/serve/types.js +2 -0
- package/dist/serve/types.js.map +1 -0
- package/dist/usage/providers/cursor.d.ts +2 -0
- package/dist/usage/providers/cursor.js +570 -0
- package/dist/usage/providers/cursor.js.map +1 -0
- package/dist/usage/providers/index.js +2 -0
- package/dist/usage/providers/index.js.map +1 -1
- 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[];
|