truecopy について
truecopyは、文書から表の行を抽出し、読み取りの信頼性に関する兆候を報告するTypeScriptライブラリとCLIです。もっともらしい誤読をそのまま受け入れず、抽出データを確認・検証したい開発者向けです。
主な機能
- 01PDF、Word、ODT、CSV、TSV、貼り付けテキストから表を読み取り
- 02警告と構造化された検出結果を報告
- 03列境界と文書レイアウトを説明
- 04セルのページ上の位置を追跡
- 05表記に応じた数値・日付の解析
- 06スキーマまたはStandard Schemaで行を検証
活用シーン
- 01明細書などの文書から行を抽出
- 02表の抽出結果が不確かな理由を確認
- 03疑わしいセルの元の位置を特定
- 04後続処理の前に抽出レコードを検証
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
4掲載時のツール一覧 · 1 日前に更新
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
| from | integer | いいえ | 返す最初の行。1 は先頭を表します。行番号はそのまま保持されます。 |
| path | string | はい | このマシン上のファイルの絶対パス。ここで読み取られ、アップロードされません。 |
| limit | integer | いいえ | 返す行数。最大 2000 行。デフォルトは 200 です。残りは数と名称が示され、黙って破棄されることはありません。 |
| maximumPages | integer | いいえ | 開くページ数。デフォルトは 40 です。これは読み取りが際限なく続くのを防ぐ上限であり、文書のページ数を示すものではありません。年次報告書では値を増やしてください。 |
詳細
truecopy
TypeScriptでPDFから表を抽出し、抽出が誤っているときに見抜きます。
ドキュメントを行に変換し、信頼できない読み取りは拒否します。PDF、WordやOpenDocumentファイル、貼り付けたテキスト、CSV、OCRページなど、すべてを1つのエンジンで読み取ります。
読み間違えたドキュメントから、もっともらしい表を返すパーサーは、何も返さないパーサーよりも厄介です。正しく見える表は誰も確認しないからです。truecopyは、あなたが作成するあらゆるリーダーの周辺を担うパイプラインです。入力されたバイトを受け取り、誤読を検出するチェックを行い、判断内容を確認する手段を提供します。そして、読み取りの誠実さが失われたその日に失敗する適合性テストキットも備えています。
今すぐ、最も厄介なドキュメントで試す
npx -y -p truecopy -p pdfjs-dist truecopy a-statement.pdf
インストールもプロジェクトもコードも不要です。エンジン名がコマンドに含まれているのは、エンジンがオプションのピア依存関係であり、npmが自動では取得しないためです。貼り付けたテキストやCSVだけを読むプロジェクトでは、PDFリーダーをダウンロードする必要はありません。その場合は npx truecopy the-file.csv だけで実行できます。PDFでエンジンを省略すると、推測するのではなく no-engine と表示されます。抽出結果、各列の内容、値が入力されている割合を出力し、その後で保証できなかったことを自動的に表示します。
page 1 - cut at 68, 229, 356, 411, 490
what this reading cannot vouch for:
- column 5 of page 1 is filled on only 2% of its rows - the cut may have invented it
- the pages disagree on how many columns there are (6 on 2, 7 on 1)
npm install truecopy
2行で行を取得
import { readTable } from 'truecopy';
const { rows, warnings, findings } = await readTable(file);
// rows [['02/05/2026', 'CARTE AMAZON', '12,40'], …]
// warnings ['column 3 of page 2 is filled on only 8% of its rows - the cut may have invented it']
// findings [{ code: 'thin-column', page: 2, column: 3, shareFilled: 0.08, message: '…' }]
kindOf も、しきい値も、役割もありません。セルだけが必要なら、rows を分割代入して進めてください。
file はドロップされたファイルそのものです。どの定規でページを測ったかは、区切りには関係ありません。そのため、同じ2行のコードでPDF、そのPDFからコピーした表、CSV、TSVを読み取れます。
| ドロップされたもの | 列の開始位置 |
|---|---|
| 各項目の x(ポイント単位) | |
| スペース区切りで貼り付けられた表 | フィールドが始まる 文字 |
| 区切り文字を使って書かれたファイル | フィールドの インデックス:CSVでは行をそろえられません |
| 文章、またはフィールドが引用符で囲まれたファイル | なし:行全体がそのまま返され、warnings がそのことを伝えます |
最後の行こそが原則であり、例外ではありません。引用符で囲まれたCSVフィールドには区切り文字そのものが含まれる場合があるため、それでも区切ってしまうと、その後のすべての列がずれてしまいます。truecopyは代わりに行を切り分けずに返します。不完全な解析を防ぐことこそ、このライブラリが存在する理由だからです。
explainDocument は定規の種類を示すので、区切りが単なる数値のリストになることはありません。ページ上なら cut at 100, 290、貼り付けられたテキストなら cut at characters 7, 22、CSVなら cut on the delimiter です。CSVでは列そのものがフィールドなので、インデックスを示しても意味がありません。
2つ目のフィールドがあるからこそ、これは単なる別の抽出ツールではありません。warnings が空でも、読み取りが正しいとは限りません。それは、ページの形状からは問題が見つからなかったというだけで、保証される範囲はずっと小さいものです。すべての警告は、ドキュメントについて何も知らずに計算されます。列がまったくないページ、ほとんど空の列、ページごとに異なる区切り方、途中にある空白ページなどです。
findings は、同じリストに code と、それが対象とするページおよび列を加えたものです。人は説明文を読み、プログラムはコードで分岐します。明日には言い換えられるかもしれない英語の文を使って分岐することはありません。
行を信頼できる状態にする必要がある場合(後続の処理がその行に基づいて動作する場合)は、このページの続きでその方法を説明します。
1つのファイルと1つのコマンドで実行できる、完全な読み取り例:
node example/read-a-statement.mjs
example/read-a-statement.mjs は実際のPDFを作成して開き、読み取り結果を出力し、契約に沿って処理します。スタブは使っておらず、手元のドキュメントも必要ありません。サンプルが自分で作成します。
読み取り結果を確認する
読み取りがうまくいかなかったとき、まず必要なのは、より優れたパーサーではありません。問題が起きたのが3つのうちどれかを知ることです。ドキュメントの形式がおかしいのか、列の区切りが間違っているのか、それともルールが間違っているのか。それぞれ必要な修正は異なります。
import { openDocument, explainDocument } from 'truecopy';
const document = await openDocument(file);
console.log(
explainDocument(document, {
signature: {
kindOf: (cell) => (isDate(cell) ? 'date' : isAmount(cell) ? 'amount' : 'text'),
thresholds: {
date: { share: 0.6, emptyIsAnomalyAbove: 0.7 },
amount: { share: 0.6 },
text: { share: 0.6 }
}
},
roles: [
{ role: 'when', kind: 'date', minimum: 0.6, take: 'best' },
{ role: 'money', kind: 'amount', minimum: 0.6, take: 'each' }
]
})
);
truecopy - statement-2026-05.pdf
pdf, 1 page(s), 6 row(s)
page 1 - cut at 100, 290
3 column(s), 6 row(s)
col kind filled role
0 date 83% when
1 text 100% -
2 amount 100% money
1 02/05/2026 | CARTE 28/04 AMAZON EU> | 12,40
2 03/05/2026 | VIR SEPA LOYER MAI | 750,00
3 05/05/2026 | CARTE 03/05 SNCF CONN> | 68,00
4 09/05/2026 | PRLV EDF | 91,32
5 12/05/2026 | CARTE 11/05 LE BOULAN> | 7,90
6 ! | TOTAL DES DEBITS | 929,62 <- column 0 (date) is empty
画像ではなくテキストなので、ターミナル、CIログ、バグレポート、テストの期待値にそのまま使えます。キャンバスもヘッドレスブラウザーも必要ありません。
発行元ごとの設定は不要
表を読み取る一般的な方法は、ある発行元のレイアウトに合うまで設定を調整することです。これはうまくいきますが、2つ目の発行元で行き詰まります。新しい銀行、新しい形式、レイアウト変更のたびに、その作成者にしか理解できない設定のかたまりが増えていきます。
truecopyは表そのものから表を学習します。ヘッダーに何と書かれていても、日付が並ぶ列が日付の列です。ほかの行とは異なるパターンの行は、合計、残高、フッターです。
import { findRowAnomalies } from 'truecopy/signature';
// Which rows fall outside the signature the table itself sets.
const anomalies = findRowAnomalies(rows, signature);
ラベルは読み取らないため、見たことのない発行元にも対応できます。禁止語のリストで認識できるのは、昨日までのドキュメントだけです。
どこで限界に達するか、その実測
ラベルがないと読み取れないのは、判断です。どの行が表に属するのか、どの列に何が入るのか、という判断です。区切りは別の問題で、今も支援が必要なのはこちらです。
見出しラベルを意図的に隠した実際の銀行取引明細で測定しました。ページ全体に広がる x を使う方法では、表の列は5つなのに、12列が提案されました。レターヘッド、住所ブロック、フッターまで、列の開始位置を示す証拠として数えてしまうためです。代わりに、表と同じ形を持つ行(日付と金額が記載された行)に絞って区切ると、5列が提案され、そのうち3列の役割を内容だけから特定できました。
boundariesFromRecurrence が汎用的な解決策であり、readTable でもこれを使っています。行ごとに繰り返し現れる x だけを残します。実際の列の左端は繰り返し現れますが、説明文の中にある単語はそうではないためです。上記の明細では、12列だった区切りを7列まで絞り込み、残りは細い列として分類しました。ヘッダー行や罫線など、より信頼できる手掛かりがページにある場合は、boundariesFromAnchors を使います。
どんな場合でも、自己チェックは残してください。同じ明細では、読み取り結果は誤りでした。それが判明したのは、行の合計が、文書自体に記載されている残高と一致しなかったからです。
これが、このライブラリの実直な姿です。 未知のレイアウトを正しく読み取れるとは約束しません。読み取りに失敗したとき、それを知らせることを約束します。
すべての値に、出どころがわかる情報を持たせる
指し示せない値は、誰にも確認できません。数値のリストを渡された人は、もう一度文書の中を探し回る必要があります。一方、場所を渡されれば、そこへすぐ移動できます。この違いが、確認画面を使い続けるか、使わなくなるかを左右します。
import { cellsOf, placesOf } from 'truecopy/layout';
const cells = cellsOf(page); // cells[3][1] is what the fourth row holds
const places = placesOf(page); // places[3][1] is where to find it
// { page: 1, x: 430, y: 700, width: 30 }
この2つは同じ形式で配置されるため、一緒に読み取れます。何も入らなかったセルには場所がありません。何もない領域を表す長方形ではなく、null です。ReviewableRow.where も、確認を求められる行について同じ情報を持ちます。
高さはわかっていないため、ここではテキスト項目に高さを持たせません。行間をでっち上げると、文書に存在しない長方形を描くことになります。
表記法とドメインは別物です
1 234,50 と 1,234.50 が同じ数量であること、(123,45) と 123,45- がどちらも負の値であること、25/01 がある場所では日、月の順で、別の場所では月、日の順であること。これらは銀行や請求書について何かを示すものではありません。ページ上の組版方法を示すものであり、誰もが書き直し、そのたびに一度は間違える部分です。
import { readNumber, findNumbers, readLeadingDate } from 'truecopy/notation';
readNumber('27800.50'); // 27800.5 - not 2 780 050
readNumber('(123,45)'); // -123.45 - the accounting negative
readNumber("1'234.56"); // 1234.56
// Never starts a match inside another number: a date glued to a figure
// reads as one figure, not as "026 300,00".
findNumbers('30/05/2026 300,00', 2); // [{ value: 300, ... }]
readLeadingDate('25 janv. 2026 GROCERIES', { dateOrder: 'DMY', months });
// { date, length: 13 } - so the caller strips it and keeps the wording
推測するのではなく、null を返します。誰かが判断に使う数値では、拒否すれば訂正が必要になりますが、誤読すれば判断を誤ります。
これらに PDF は必要ありません。 truecopy/notation は文字列を受け取るため、API フィールド、HTML のスクレイピング結果、貼り付けたメールでも、ページと同じように読み取れます。pdfjs-dist が読み込まれることもありません。独自のパターンを書く前に使う価値のあるレイヤーです。というのも、代わりに誰もが書くパターンには、誰にも見えない欠陥があるからです。
import { accentFree } from 'truecopy/notation';
// `\w` is [A-Za-z0-9_] in JavaScript, whatever the document is written in.
/d[ée]tach\w+/.test('détaché'); // false - the suffix is an accented letter
/d[ée]tach\p{L}*/u.test('détaché'); // true
accentFree('Détaché'); // 'detache' - or fold first and compare plainly
ここでは何も失敗しません。パターンはコンパイルされて実行され、もっともらしい数の一致結果を返します。その結果は、ルールの欠陥ではなく、文書についての事実として受け取られてしまいます。
自分の言語で伝えられる拒否
try {
await openDocument(file);
} catch (error) {
if (error instanceof UnreadableDocument) {
// 'empty' | 'too-big' | 'no-text' | 'too-slow' | 'no-engine' | 'not-opened'
showInYourOwnWords(error.reason);
}
}
理由には名前が付いていますが、文面はそうではありません。英語の文章しか同梱しないライブラリでは、それ以外の言語を使うアプリケーションは、独自に拒否メッセージを用意し続けなければなりません。
no-engine と not-opened は意図的に分けてあり、その違いは見た目だけのものではありません。前者は pdfjs-dist をインストールしてください、後者は このファイルは開けません。パスワードを確認してください という意味です。誤った理由を伝えられると、人は存在しないロックを探し、エージェントは文書が保護されていると書いてしまいます。pdfjs-dist はピア依存関係です。そのため、貼り付けたテキストと CSV だけを読み取るプロジェクトが、使うことのない PDF エンジンをダウンロードせずに済みます。
1つの宣言から2つの出力
スキーマを一度記述するだけで、検証とレコード型の両方を得られます。形状を二重に宣言するとずれが生じます。そして、気づかれないままずれるのは、決まって検証のほうです。
import { schemaOf, validate, type RecordOf } from 'truecopy/schema';
export const CAREER = schemaOf({
name: 'career-record',
fields: {
year: { format: 'year', minimum: 1930, maximum: 2030, required: true },
quarters: { format: 'integer', minimum: 0, maximum: 4, required: false },
pay: { format: 'number', minimum: 0, required: false }
},
minimumRecords: 5,
key: 'year'
});
export type CareerRow = RecordOf<typeof CAREER>;
// { year: number; quarters: number | null; pay: number | null }
const violation = validate(CAREER, rows);
// null, or { cause: 'too-few-records', conforming: 1, expected: 5 }
スキーマは純粋なデータです。アクセサーもクロージャーも、JSON として保持できないものは一切ありません。提供したり、バージョン管理したり、ドキュメントを読む人が改善したり、デプロイせずに別の市場向けのものに差し替えたりできます。不足しているものには名前が付けられており、アプリケーションが独自の言葉で説明文を書きます。
その行型をすでに Zod、Valibot、または ArkType で宣言していますか? 二重に宣言する必要はありません。Standard Schema ならどれでも使えます。truecopy は、レコードごとのバリデーターでは分からない部分、つまり、ドキュメントを構成する整形式レコードの数を補います。
import { validateWith } from 'truecopy/schema';
validateWith(myZodSchema, rows, { minimumRecords: 5, key: 'year' });
拒否が重要な理由
研究分野でも別の視点から同じ結論に至り、「キャリブレーションされた棄権」と名付けられました。Zero Hallucination, by Construction(Raduta ら、2026 年)は、エンタープライズ向けドキュメント AI のアーキテクチャを構成する6つのレイヤーの1つとしてこれを位置付けています(「根拠が不十分な場合、システムは推測せずに回答を控える」)。また、生成された内容をソースドキュメントと照合する、証拠に基づく信頼度判定と組み合わせています。これらはレイヤーごとに refuse と selfCheck に対応します。
技術的な理由ではなく、別の理由から、これは依然として珍しいことです。ベンチマークでは、自信のある推測は評価される一方、棄権は誤答とみなされるため、システム全体が推測するように調整されます。決定論的なパーサーに、その悪癖を引き継ぐ言い訳はありません。
リーダーは、次の5つの原則に従って実装されています。
- ドキュメント自身が検証する。 銀行取引明細書には
opening + Σ = closingが記載され、職歴記録には四半期の合計が明記されています。ドキュメントが宣言する内容と読み取った結果を照合することだけが、レイアウトに依存しない検証です。 - もっともらしい表を返すより、拒否するほうがよい。 最悪の欠陥は異常を見逃すことではありません。読み違えたドキュメントから、22件もの異常をでっち上げることです。
- 数を数えることと、測ることは違う。 2回読み取って「より多くの行を見つけたほう」を採用すれば、必ず悪いほうが選ばれます。切り分けが不適切なリーダーは、脚注をデータとして扱ってしまいます。
- リーダーが候補を提示し、人が確認する。 修正は復旧手段ではなく、製品の一部です。
- すべての量化子に上限を設ける。 これらのパターンは、ドキュメント全体を行単位で処理します。
モデルが読み取る場合
セルをレコードに変換するモデルには、目に見える失敗が1つあります。それはレコードを見逃すことです。そして、目に見えない失敗もあります。ドキュメントに一度も印字されていない、もっともらしい値を返すことです。郵便番号から推定した町名、丸められた数値、再構成された日付。どれも抽出に成功したかのように見えます。この失敗には決定論的な対処法があり、より優れたモデルは必要ありません。
モデルには PDF を見せません。 このライブラリが切り分けて番号を付けた行を受け取り、そこから得た行番号を引用してレコードを返します。さらに、すべての値をその行の中だけから検索します。つまり、モデルが事実を生成することはできず、ドキュメントにすでにある事実を指し示すことしかできません。
import { numberedRows, citedText, carriesText, carriesNumber } from 'truecopy/cite';
const prompt = rules + '\n' + numberedRows(page); // what the model sees
const source = citedText(page, record.rows); // what it claims to have read
carriesText(source, record.town); // all its words, in order - never a substring
carriesNumber(source, record.surface, mark); // the page's numbers - never a digit soup
carriesNumber(source, record.postcode, mark, 'punctuation'); // brackets as asides, not as a sign
このように実装する前に、実際のコーパスで、どちらの検索方法も単純な方法と比較して評価しました。連続する部分文字列を使うと、あるドキュメントの111件のレコードのうち104件が拒否されました。レイアウトによって、名前の末尾が数値列の向こう側に配置されるためです。単語はすべて順番どおりに存在するのに、ひと続きになっていません。また、ソースを1つの数字列に平坦化すると、その数値がまったく記載されていない行で、見かけ上の一致が3回見つかりました。保護機能が何の役にも立たないまさにその場面で、強力に見えるガードでした。
あなたが決めるのは仕組みではなくポリシーです。どのフィールドが数値なのか、どの単一フィールドがレコードのアンカーになるのか、また、補助フィールドの検証に失敗した場合は致命的なエラーにせず、そのフィールドを破棄して件数を記録するのか。checkExtraction(truecopy/contract)は計算の部分を担います。返された値の合計と、ドキュメント自身が宣言する合計を照合します。
読み取った後も値が残り続ける場合
一度オフラインで読み取られ、値になった文書を、その後何か月もサイトが提供する場合、閲覧者ごとに抽出をやり直すことはできません。そのような値を裏付けるのは抽出結果ではありません。どこにも保存されていない SENTENCE です。値と一緒に保持した短いアンカーを手掛かりに、表示時に文書内から再び見つけます。テキストを転記することはないため、値が元のテキストからずれることはありません。また、文書が改訂された日にはアンカーが一致しなくなり、その不在が警告になります。
import { anchoredPassage, missingFigures, proofSpan } from 'truecopy/anchor';
// stored once: { value: '9 m', anchor: 'hauteur maximale de 9' }
const passage = anchoredPassage(article, rule.anchor, { units: 'lines', depth: 'outline' });
// A null passage is the alarm: the document was revised since it was read.
if (passage === null) return null; // never a value without its sentence
const quoted = [...passage.heading, passage.line, ...passage.opened].join(' ');
missingFigures(quoted, rule.value, { decimal: mark }); // [] or the figures it does not carry
proofSpan(quoted, rule.value, { decimal: mark }); // how far a reader travels to the proof
単独で引用した段落は正確ですが、何も伝えません。 - under two years of service: one month には、規則の半分に当たる - for technicians と、もう半分に当たる直前の文が抜けています。これだけを引用すると、まるで全員の予告期間が1か月であるかのように読めてしまいます。したがって、引用箇所は段落、その祖先、そしてその段落が始めるリストで構成されます。そして、狭い画面でそのうちどれを省くかは表示上の判断となるため、この3つは別々に扱います。
また、アンカーが一致し続けても、段落の存在が確認できるだけで、そこに何が書かれているかは分かりません。ある計画コードで測定したところ、35 m from the axis と表示された規則の隣に、同じ道路について a minimum setback of 10 metres と記された文がありました。これを検出するのが missingFigures であり、画面で引用が途切れる前に読者が数値にたどり着けるかどうかを判定するのが proofSpan です。
必須にするためのキット
インターフェースは return null で回避できますが、アサーションは回避できません。独自のコーパスを用意して、ゲートにこのキットを組み込みます。
import { checkContract, contractReport, failures, pdfWithText } from 'truecopy/kit';
it('holds the reading contract', async () => {
const results = await checkContract(
myReader,
[
{ name: 'balances', document: aSoundDocument, expected: 'read' },
{ name: 'does not balance', document: aWrongDocument, expected: 'needs-review' }
],
{
referencePdf: pdfWithText([{ word: '2018', x: 50, y: 700 }]),
// YOUR door, not the kit's: what is measured is your reader,
// extraction included.
open: openLikeTheApp,
foreign: [{ name: 'a payslip', document: aPayslip }]
}
);
expect(failures(results)).toEqual([]);
writeFileSync('contract.txt', contractReport(results));
});
6つの規則を個別に挙げます。上記の法則は、読者に何を説明するかを定めたものです。こちらは、コーパスがそれに照らして外部から確認する項目です。
- コーパスが示す判定と、返される判定が一致していること。
- 文書と矛盾する読み取り結果は、決して正しいものとして返さないこと。
- 読み取った内容はすべて、その人が確認できること。
- バイト列からレコードまでの処理が、実際の PDF 上で本当に実行されること(
pdfWithTextは xref テーブルを含む PDF を作成します)。 - 内容のない文書は、空の結果として黙って返すのではなく、拒否すること。
- 別の種類の文書は拒否すること。何と区別すべきかを伝えるコーパスは、何を区別すべきかを知っているのがあなただけなので、あなたが用意します。
contractReport は実行結果をコミットするファイルにまとめます。レコードが26件から24件にひっそり減っても、すべての規則に合格し、アサーションでは何も検出されません。検出するのは差分です。
第5の法則(すべての量指定子に上限を設ける)は、単純なソーススキャンでは誤検出が発生するため、キットには含まれていません。代わりに、独自のゲートで eslint-plugin-regexp とその no-super-linear-backtracking および no-super-linear-move ルールによって担保します。
5つではなく、3つのメソッド
読み取り側が実装するのは read、selfCheck、rowsToReview です。この3つが、実際の処理、第1の法則、そして人が関与する役割を担います。
repair と refuse は任意です。デフォルトの動作は、デフォルトが誤る場合に誤るべき唯一の方向、つまり拒否する方向に寄せています。repair は何も試みません。不誠実な穴埋めより、正直に欠落を示すほうがよいからです。refuse は、レコードを生成しなかった読み取りを拒否します。
実行前に5つのメソッドを必須にするのは壁となり、インターフェースの前に壁があれば、return null が5回書かれることになります。それを検出するために存在するのが kit.ts であり、今ではそのような回避策を最も書きそうなのは、型からコードを生成するモデルです。
メカニズム
それぞれが独立したエントリーポイントでもあるため、利用するプロジェクトはその分のコストを負担します。
| モジュール | truecopy/… | 内容 |
|---|---|---|
open.ts | open | バイト列 → ページ → 行:サイズ上限、ページ上限、期限、エンジンの解放、エンジンの差し替え |
layout.ts | layout | 純粋な幾何学的構造:行、列、セル、位置。エンジンなし、バイト列なし、時間制約なし |
table.ts | table | readTable(file):2 行で済む経路と、保証できないこと |
notation.ts | notation | ページ上の数値と日付の表記方法。意味は扱いません |
classify.ts | classify | 期待される種類の文書かどうか。優先順位も含みます |
columns.ts | columns | 各列に何が含まれるかを、一度だけ数えます |
roles.ts | roles | 各列が何であるかを、その内容から推定します |
records.ts | records | 印刷された行が一つのレコードとは限らない場合に、どの行が同じレコードに属するか |
labels.ts | labels | ラベルが示す値である可能性のあるセル。近いものから順に並べます |
signature.ts | signature | 表自体の型シグネチャに適合しない行 |
schema.ts | schema | 読み取りが満たすべきフィールド、形式、件数 |
cite.ts | cite | モデルが引用した行と、それぞれの値が実際に記載されているかどうか |
anchor.ts | anchor | 保存された値の出典となった箇所を、文書内で再特定します |
explain.ts | explain | 上記すべての判断を文章で説明します |
pattern.ts | pattern | ドメイン知識をデータとして扱い、ReDoS 対策のガードを設けてコンパイルします |
contract.ts | contract | 誠実な読み取りがどのようなものか |
kit.ts | kit | 何がそれを必須にするのか |
優先順位と、順序ではなく明示する理由
銀行の明細書では、取引ラベルに「請求書」という語が記載されていることがあります。順序を誤って読み取ると、請求書として分類され、それに伴うすべての処理で予算が一緒に持ち出されてしまいます。IBAN、あるいは借方/貸方/残高という用語があれば、「請求書」という語を見るまでもなく判断できます。
概念は三つだけです。パターンセット(その中のすべてのパターン)、要件(いずれか一つのセット)、種類(その中のすべての要件。最初に一致したものを採用)。absent は要件を反転させます。そのため、「IBAN がない場合に限り請求書とする」というルールは、別の意味を持つ順序に紛れ込ませず、明示的に記述できます。
ドキュメントについて何も知りません
日付、金額、借方、四半期について、何も知りません。セルの種類と、返してほしい役割を指定すると、このライブラリが数え、割り、比較し、どのルールに違反したかを示します。
これは意図的な設計です。意味づけまで含めて提供するライブラリでは、次のプロジェクトが、他人のために設計された語彙でドキュメントを記述せざるを得なくなります。
市場、発行元、ドキュメントの種類によって変わるものはすべて、分岐ではなく値です。pattern.ts を参照してください。値はバックエンドから提供でき、ドキュメントを読む人が調整でき、バージョン管理でき、デプロイせずに別の市場向けのものに差し替えられます。分岐を変更できるのは、コードを変更できる人だけです。プロファイルには正規表現が含まれますが、JSON は正規表現を扱えません。そのため { source, flags } として渡し、この場所で再構築します。まさにここが危険な箇所であり、ガードを置く場所でもあります。拒否されたパターンは、決して一致しないパターンにコンパイルされるため、機能は縮退してもページは動作し続けます。
リーダーは1つでも、2つでも
プロジェクトでは、リーダーを1つ、または2つ組み込めます。明細書リーダーは、座標がある場合は位置情報を使うリーダーを、座標がない場合は行文法を使うリーダーを実行でき、OCR ページはまさに両方を適用でき、より情報量の多い読み取り結果が優先される場面です。その競合をどう処理するかは利用者の責任です。それ以外のすべては、このライブラリの責任ではありません。
readTable はデフォルトのリーダーであり、最初の1分で費用がかからないようにするものです。これは入口であって、製品そのものではありません。製品は、最終的に処理を担うリーダーを問わず、その周囲にあるパイプラインと保証です。各仕組みを個別に公開しているのは、デフォルトの仕組みを一度に1つずつ置き換えられるようにするためです。
決して行わないこと
明示された制限のないライブラリは、リリースのたびに制限を1つ増やします。以下は今後の計画ではなく、明確な拒否事項です。
- ドキュメントの意味づけを提供することはありません。 日付、金額、借方、四半期、発行元は扱いません。ドメイン知識はデータ(
pattern.ts、schemaOf)として扱います。値ならバージョン管理して別の市場向けのものに差し替えられますが、分岐を変更できるのは、コードを変更できる人だけだからです。 - 発行元ごとの設定を増やすことはありません。 ある銀行で正しく動くまで設定を調整する方法こそ、このライブラリが置き換えようとしているものです。正しく読み取れないレイアウトは、欠陥か、文書化された制限のいずれかであり、プリセットを追加することはありません。
- CSV パーサーになることはありません。 4つの区切り文字で分割しますが、行ごとに同じ個数が繰り返される場合に限ります。引用符で囲まれたファイルは警告とともに全体を返します。引用符付き CSV を中途半端にパースすることは、ここで一貫して反対している、もっともらしいものの誤った読み取りにほかならないためです。本格的な CSV ライブラリを使ってください。この機能は、貼り付けたデータ、エクスポート、OCR の1行を扱うためのものです。
- バリデーションライブラリになることはありません。
schemaOfは読み取りを制限します。対象は形式と、ドキュメントを構成するレコード数です。それより高度な検証には Zod または Valibot を使い、validateWithはそれらを受け取ります。競合することはありません。 - 独自のエンジンを内蔵することはありません。 PDF は
pdfjs-distで処理します。これはオプションのピア依存関係のままです。.docxと.odtは XML の ZIP なので、プラットフォームにすでにある機能で読み取り、依存関係は追加しません。OCR エンジン、スプレッドシートリーダー、旧形式の.docは扱いません。それ以外のものは、バイト列またはテキストとして渡してください。読み取れないバイト列は文字化けに変換せず、名前を示して拒否します。 - 渡されていないファイルシステム、ネットワーク、時計にアクセスすることはありません。 ブラウザー内で動作します。唯一の例外は
bin/truecopy.mjsですが、これはライブラリではなくコマンドです。 - 第2のカットを追加することはありません。 どの左端が繰り返されるかを決める投票方法は1つ、しきい値のセットは1つ、定規は3つです。独自のアルゴリズムが必要な形式には、独自のライブラリが必要です。
要件
Node 20 以降、ESM が必要です。pdfjs-dist はオプションのピア依存関係で、必要に応じて読み込まれ、PDF にのみ使用されます。テキストの読み取り、貼り付け、CSV の場合は何も必要ありません。
pdf.js は意図的にレガシービルドを使用します。モダンビルドはブラウザー以外では実行できないため、パイプライン全体をテストできなくなるからです。**Node ではエンジンを一切渡さないでください。**このビルドは自動的にインポートされます。2 つのコンシューマーで使われる 9 つの取り込みスクリプトは、手動でインポートして pdfjs として渡しています。これは動作しますが、冗長です。このオプションがあるのは、バイト数に制限のあるブラウザーリーダーに対応するためです。この場合に限り、モダンビルドを個別にインポートする価値があります。
ワーカーを使う場合は、URL を自分で解決して渡してください。バンドラーごとに指定方法が異なり、ここで 1 つに決めると、それに縛られてしまうためです。
import workerSrc from 'pdfjs-dist/legacy/build/pdf.worker.mjs?url'; // Vite
await openDocument(file, { workerSrc, maximumPages: 200 });
バッチスクリプトでは上限を引き上げます。それが本来の用途です。DEFAULT_LIMITS(20 MB、40 ページ、30 秒)はブラウザータブを保護するためのもので、年に一度実行する取り込み処理向けではありません。年次報告書を読む 2 つのコンシューマーでは、80~200 MB、600~2000 ページに落ち着きました。maximumPages より先に keepPage を使ってください。長い文書を適切な位置で切り詰められます。
インストール不要でアシスタントから使う
ターミナルを使えるエージェントなら上記のコマンドをすでに使え、モジュールをインポートできるエージェントならライブラリを使えます。どちらも、多くのアシスタントの実態とは異なります。MCP を話すホストが接続できるのはツール呼び出しだけです。そのため、同じパイプラインを、文書がすでに置かれているマシン上で stdio 経由の MCP サーバーとして使えます。
{
"mcpServers": {
"truecopy": {
"command": "npx",
"args": ["-y", "-p", "truecopy", "-p", "pdfjs-dist", "truecopy-mcp"]
}
}
}
ツールは 4 つあり、すべての回答は判定から始まります。reliable、doubtful、unreadable のいずれかです。unreadable はファイルを開けなかった場合、doubtful は読み取りで少なくとも 1 つ問題が見つかった場合で、何がどこで見つかったかは所見に示されます。reliable はページの構造上、問題が見つからなかったことを意味しますが、「この読み取りが正しい」という意味ではありません。回答にはその旨が明記されます。モデルは数値を頼りにする前に判定を読みます。
read_tableは PDF、CSV、TSV、または保存された貼り付け内容を行に変換し、区切り文字、各列が入力されている頻度、行番号、および読み取りで保証できなかったことを示します。1 回あたり最大 200 行です。残りは破棄せず、件数を示します。find_amountsは指定した小数桁数(デフォルトは 2 桁。これによってページ上の金額と参照番号を見分けられます)で記載された数値を一覧表示します。それぞれに、印字どおりの文字と、引用に使える行番号と列番号が付きます。find_datesは日付で始まるセルを一覧表示します。日と月の順序は、推測させず、指定した順序で読み取ります。年のない行を日付として扱うのは、年が指定されている場合に限ります。check_citationsは、モデルが読み取ったと主張するレコードと、参照元の行を受け取り、引用された行に記載のない値を示します。JSON の数値は文書独自の小数点記号を使った数値として、JSON の文字列はその語順のまま照合します。「読み取られたのか、捏造されたのか」に答えるものであり、「完全かどうか」には答えません。その旨も明示します。read_tableとcheck_citationsは読み取ったバイト列のdigestを返します。これを後続の呼び出しで渡すと、間にファイルが変更されていた場合、そのファイルを拒否します。
**アップロード先がないため、何もアップロードされません。**サーバーはファイルがすでに存在する場所で動作し、ネットワーク呼び出しを行わず、渡されていないファイルを開きません。明細書、給与明細、職歴記録は、どこかへ送るには最も不適切な文書です。また、サーバーがそれらを処理するための計算で、このツールがその場で行う処理を超えるものはありません。TRUECOPY_MCP_ROOT を設定すると、シンボリックリンクを検査前に解決したうえで、そのディレクトリ外のファイルは一切開きません。
2026-07-28 版とそれ以前の版に対応しているため、initialize で接続するホストでも、server/discover に直接進むホストでも動作します。npx -y -p truecopy -p pdfjs-dist truecopy mcp を実行すると同じサーバーが起動します。これは MCP registry のエントリ io.github.truecopydev/truecopy で指定されている方法です。
AI が文書を読むよう求められた場合
文書から数値を尋ねられ、明細書、請求書、エクスポートデータを渡されました。重要なのは数値を見落とすことではなく、文書に一度も記載されていない数値を、あたかも正しいかのように返してしまうことです。そこで、次の手順に従ってください。
- 何よりも先に
read_tableを呼び出し、判定を確認します。unreadableは根拠にできるものがないという意味です。その旨を伝えてください。doubtfulは問題が見つかったという意味です。所見には該当するページと列が示されており、そこにある数値は注意が必要な数値です。reliableは問題が見つからなかったという意味で、それが主張できるすべてです。 - 数値には
find_amounts、日付にはfind_datesを使ってください。文章から読み取るのではなく、これらを使います。それぞれ、印字されている行も併せて示されます。 - いずれかの値に基づいて処理が行われる前に、その値の行を
check_citationsに渡して引用を確認してください。引用した行に記載されていない値は、その行から読み取ったものではありません。読み直すか、値を削除してください。 - 空白を埋めてはいけません。合計が記載されていないなら、それは合計が記載されていないということです。各項目を足し合わせる機会ではありません。情報源は文書のみであり、確認結果が指し示すのも文書だけです。
これらのツールを、自作サーバーや別のトランスポートなど、より大きな仕組みから提供する場合は、truecopy/mcp を使ってください。TOOLS は記述子であり、respond はパスを File に変換する方法を受け取り、1 件の JSON-RPC メッセージに応答します。
コーディングエージェント向け
このライブラリは、現在稼働中のすべてのモデルのトレーニングが終わった後に公開されました。そのため、モデルに API を渡さずにこのライブラリ向けのコードを書かせると、API をでっち上げてしまいます。インストールしたものとバージョンを合わせた、次の 2 つの方法があります。
# already on disk once you install the package
cp -r node_modules/truecopy/skills/truecopy ~/.claude/skills/
# or, to follow the repository
claude plugin marketplace add truecopydev/truecopy
claude plugin install truecopy@truecopy
skills/truecopy/SKILL.md はプレーンな Markdown で、フロントマター以外に Claude 固有の指示はありません。そのため、AGENTS.md を読み込む他のエージェントでも利用できます。このパッケージに存在しなくなったエクスポートを指定すると、npm run skill は失敗します。
ドキュメントサイトでは、すべてのページを同じパスに .md を付けた Markdown として提供しています。これは llms.txt からリンクされています。
ゲート
npm run gate
フォーマット、組版上のダッシュ不使用、Lint(警告ゼロ、sonarjs + regexp)、型チェック、ビルド、knip、jscpd、テスト、カバレッジ 100 %、そして上がることだけが許されるラチェット。このキットは、他を測る前にまず自分自身を測ります。
同じコマンドが CI でも実行され、プッシュやプルリクエストのたびにチェックされます。手元で成功すれば、こちらでも成功します。
貢献
CONTRIBUTING.md:変更に必要なことと、変更が取り込まれるまでの手順が記載されています。正しく読めない文書を見つけた場合は、文書そのものではなく、npx truecopy の出力を添えてイシューを作成してください。
RELEASING.md:ここでのバージョン番号が何をもたらし、何をもたらさないかが記載されています。要約すると、リリースはパッチであり、2 桁目は変わらず、公開が取り消されることはありません。
セキュリティについては、SECURITY.md をご覧ください。このライブラリはネットワークへの呼び出しを行わず、ファイルシステムにも触れず、実行時依存関係も含みません。そのため、攻撃対象領域は小さく、具体的な内容がそこに記載されています。
ライセンス
MIT © Florian Mousseau
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。