Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
174 commits
Select commit Hold shift + click to select a range
265cbe3
Correct nine section boundaries in the L40 fixture, measured three ways
gordon2 Jul 25, 2026
36fa212
Write down what the language signals cost, and leave the detector open
gordon2 Jul 25, 2026
ea4a52e
Add internal/doc: work out what a manual contains, for free
gordon2 Jul 25, 2026
8ccb045
Add the registry: locations, devices, documents, and the language map
gordon2 Jul 25, 2026
2fc1a18
Upload a manual and be told what it contains, before spending anything
gordon2 Jul 25, 2026
f715399
Teach extern about pdftohtml, which is how headings will be found
gordon2 Jul 25, 2026
d71f441
Add a fifth language signal: which alphabet the text uses
gordon2 Jul 26, 2026
2647816
Detect text columns by counting crossings, not by looking for whitespace
gordon2 Jul 26, 2026
4cc98ee
Add a column-layout fixture, and record per-page provenance
gordon2 Jul 26, 2026
8777099
Write down how manuals are laid out, and where the seam goes
gordon2 Jul 26, 2026
986488d
Read the language codes manuals actually print, not just two-letter ones
gordon2 Jul 26, 2026
5230789
Name a language per column, not per page
gordon2 Jul 26, 2026
3df6de4
Write the contract for regions before touching shipped tables
gordon2 Jul 26, 2026
be59265
Say what the app actually does now
gordon2 Jul 26, 2026
7f3eca2
Read a manual's text where it sits on the page, not just in order
gordon2 Jul 26, 2026
b8538c2
Admit 'repertoire' as a language signal in the two shipped tables
gordon2 Jul 26, 2026
0e3743e
Store a language that is part of a page, not the whole of it
gordon2 Jul 26, 2026
25e41e3
Record what building regions actually settled, and what it disproved
gordon2 Jul 26, 2026
f2d2f2f
Say what pdftohtml is actually for now
gordon2 Jul 26, 2026
e79025c
Time the whole probe instead of adding up its parts
gordon2 Jul 26, 2026
75f4b4f
Store the regions, so a column manual's languages survive a restart
gordon2 Jul 26, 2026
a98f9ac
Do not claim which way round the sqlc offset bug is
gordon2 Jul 26, 2026
3d0caa3
Stop reading a page of postal addresses as the contents table
gordon2 Jul 26, 2026
029e246
Say which half of the index bug is fixed and which is not
gordon2 Jul 26, 2026
8b2ea68
Show the gate what the regions know, and count in characters
gordon2 Jul 26, 2026
1675a0f
Count the gate's languages in characters on screen, not in pages
gordon2 Jul 26, 2026
edd5084
Read the font each run is set in, from two signals that disagree
gordon2 Jul 26, 2026
d1da7d9
Write the contract for conversion before building it
gordon2 Jul 26, 2026
898918f
Turn a region's runs into blocks a person can read
gordon2 Jul 26, 2026
1036c7a
Correct the contract where implementing it proved it wrong
gordon2 Jul 26, 2026
84b26e5
Read the ruled lines a page draws, and the cells they enclose
gordon2 Jul 26, 2026
38f2464
Record that a table's cell dividers are being read as language columns
gordon2 Jul 26, 2026
296fca2
State the funnel geometrically, not by which letters a language uses
gordon2 Jul 26, 2026
c5265af
Stop reading a table's cell dividers as language boundaries
gordon2 Jul 26, 2026
cf42d9a
Read a table across its rows, not down its cell columns
gordon2 Jul 26, 2026
7f1c3db
Prove the table walk reads through the region's own filter
gordon2 Jul 26, 2026
89de947
Admit that the free probe now pays for reading a table's lines
gordon2 Jul 26, 2026
7200545
Find the pictures on a page, which are not the images in the file
gordon2 Jul 26, 2026
292f244
Record that the pictures in a manual are not the images in the file
gordon2 Jul 26, 2026
30f507e
Correct where the figures are, which was the opposite of what I recorded
gordon2 Jul 27, 2026
aacf685
Store what a conversion produced: readable blocks, and the pictures
gordon2 Jul 27, 2026
60bef00
Save a conversion: blocks and figures, all of it or none
gordon2 Jul 27, 2026
aa2908b
Prove the conversion tables converge, and that the delete is why
gordon2 Jul 27, 2026
7134cab
Assemble a manual into what one household can read
gordon2 Jul 27, 2026
bb6d850
Record that "every language" means every language of a page
gordon2 Jul 27, 2026
fb6b274
Convert a manual after the gate, and serve what came back
gordon2 Jul 27, 2026
86a57b5
Test the conversion job, the approval, and what a language's pictures…
gordon2 Jul 27, 2026
ab0e698
Mirror the conversion endpoints in both hand-written contracts
gordon2 Jul 27, 2026
c13566c
Record what wiring conversion to the pipeline cost, and what it settled
gordon2 Jul 27, 2026
b78ee22
Read a converted manual, and say where right-to-left breaks
gordon2 Jul 27, 2026
ad177c4
Match the formatter on the one line it would have changed
gordon2 Jul 27, 2026
2068155
Record that right-to-left text is extracted backwards
gordon2 Jul 27, 2026
9ff3a57
Verify a conversion against pdftotext, the free second opinion
gordon2 Jul 27, 2026
9d8fa1b
Measure every threshold against both manuals, and add manualbox verify
gordon2 Jul 27, 2026
ca82de1
Drop the measurement scratch harness and satisfy the linter
gordon2 Jul 27, 2026
3d9f2c8
Make the clean-text join test exercise the guard it is about
gordon2 Jul 27, 2026
831eb90
Say which pages the invented-text findings are actually on
gordon2 Jul 27, 2026
24f96e6
Check a conversion against the other extraction, for nothing
gordon2 Jul 27, 2026
5d03304
Read the clip, so a shape's box is what the page paints
gordon2 Jul 27, 2026
169704b
Hold the fixtures to the numbers the clip actually produces
gordon2 Jul 27, 2026
a0919c2
Pin the clip's composition with a transform that does not cancel
gordon2 Jul 27, 2026
ec46b92
Drop the scratch figure-dump harness
gordon2 Jul 27, 2026
f98551f
Say that the clip is read, where the doc said it never would be
gordon2 Jul 27, 2026
3d7bdef
Trim a figure off the prose it reached over, not off its own callouts
gordon2 Jul 27, 2026
917ca20
Hold the figure tests to what the untrimmed drawings measure
gordon2 Jul 27, 2026
7ff2c18
Name the sequential manual's one changed figure correctly
gordon2 Jul 27, 2026
23f5232
Say what the measurement shows, and no more
gordon2 Jul 27, 2026
76e51ec
Trim only what a line of text actually reaches past
gordon2 Jul 27, 2026
45e6109
Merge candidate boxes that overlap, so a fragment is not a picture
gordon2 Jul 27, 2026
9d4bce5
Pin the merge with tests, and record what it moved
gordon2 Jul 27, 2026
8c7298a
Correct two eye counts that were counting boxes, not drawings
gordon2 Jul 27, 2026
c763baf
Remove the scratch measurement hook
gordon2 Jul 27, 2026
519a3ea
Fix the order the merge sees, so a page gives the same pictures twice
gordon2 Jul 27, 2026
5dd0237
Bound the sweep by a twentieth, on the measured spread
gordon2 Jul 27, 2026
289cf68
Pin the page the fault was reported on
gordon2 Jul 27, 2026
7094b85
Stop serving a piece of a drawing as its own picture
gordon2 Jul 27, 2026
f8db183
Add the scratch harness that measures the tokeniser on both real manuals
gordon2 Jul 28, 2026
bca1091
Index the blocks, and answer which manual says X
gordon2 Jul 28, 2026
915042d
Pin the index with tests, and find out what the trigger really prevents
gordon2 Jul 28, 2026
8d456fa
Write down what search measures, and mirror the endpoint in both cont…
gordon2 Jul 28, 2026
5325a0c
Remove the measurement harness, and say search works now
gordon2 Jul 28, 2026
b7ed55c
Surface search: a box, hits that name the manual, and a way in
gordon2 Jul 28, 2026
c9ff011
Print the snippet as it arrived, and check the screen in a browser
gordon2 Jul 28, 2026
7a9838a
Find page furniture where the whole document is in view
gordon2 Jul 29, 2026
6c36dde
Pin the furniture rule and its two guards with hermetic tests
gordon2 Jul 29, 2026
cd0bcde
Hold the counts to what the furniture pass measures, on both manuals
gordon2 Jul 29, 2026
9849961
Keep the furniture out of the table the reader and the index read
gordon2 Jul 29, 2026
7adae02
Pin that coverage cannot hide a runaway furniture rule, and drop the …
gordon2 Jul 29, 2026
f4ac571
Rename a shadowed identifier the linter caught
gordon2 Jul 29, 2026
96456fe
Say what the tab count actually is, where four files say 110
gordon2 Jul 29, 2026
18a3d25
Stop serving the printed tab and the folio as things to read
gordon2 Jul 29, 2026
ca54d0c
Grow a figure's box onto the labels its leader lines point at
gordon2 Jul 30, 2026
ddc6092
Ask the clipped check which box, because it had one box for two quest…
gordon2 Jul 30, 2026
797fe68
Say what a conversion actually produces, where nine numbers predate t…
gordon2 Jul 30, 2026
dd3c1ba
Say that the columns manual's two claims are false, because that is t…
gordon2 Jul 30, 2026
10b1c91
Cover growToLabels with hand-built geometry, no poppler and no PDF
gordon2 Jul 30, 2026
35b9c12
Put each hermetic growth case on a mutation that only it catches
gordon2 Jul 30, 2026
05d9fee
Sweep the growth rule over both whole documents, and count what is left
gordon2 Jul 30, 2026
d68b577
Quote the shipped numbers, not the prototype's, in three swept series
gordon2 Jul 30, 2026
e7c8011
Say inside the range, not nearer than it, in the parts-list test too
gordon2 Jul 30, 2026
4c3a637
Refute an attribute that reads the crop, where a fixture cannot
gordon2 Jul 30, 2026
d4041f1
Pin the grown crop on the page it was reported on, and the zero elsew…
gordon2 Jul 30, 2026
efe3dca
Drop the growth census this duplicates, and point at what refutes the…
gordon2 Jul 30, 2026
f3566ca
Say reached rather than taken, because 11 of page 521's 34 arrive cut
gordon2 Jul 30, 2026
9d3aaea
Read a contents page as the list of entries it is
gordon2 Jul 30, 2026
5d904ea
Record the contents-page reading where the reasoning is kept
gordon2 Jul 30, 2026
c8cd71a
Read right-to-left text in the order it is written
gordon2 Jul 31, 2026
cf14373
Test the visual-to-logical repair on the strings it was measured on
gordon2 Jul 31, 2026
0b57c6f
Make the control line two runs, so a wrong direction verdict shows
gordon2 Jul 31, 2026
94c274e
Keep the space that separates a left-to-right island from the word be…
gordon2 Jul 31, 2026
d058f61
Say "reversed" and mean it: the right-to-left check needs a reversal
gordon2 Jul 31, 2026
5d6cd7f
Hebrew is findable typed forwards, and the one block that is not is p…
gordon2 Jul 31, 2026
ccd59ee
Report the reversal, not the page it was found on
gordon2 Jul 31, 2026
b7cd149
Let the region's language decide direction, not one line's characters
gordon2 Jul 31, 2026
5fec87d
The right-to-left check reads zero, and now asserts it
gordon2 Jul 31, 2026
0e1adf7
Write the bidi controls as escapes, which staticcheck asks for and th…
gordon2 Jul 31, 2026
566cee3
Say at the Kind that a zero here is the goal
gordon2 Jul 31, 2026
eb36025
Keep a left-to-right island in order when it spans several runs
gordon2 Jul 31, 2026
7110472
Pin the URL fixed, and a second run-order defect it uncovered
gordon2 Jul 31, 2026
36d87de
Drop a stale paragraph the block-count rewrite left above its own table
gordon2 Jul 31, 2026
8573c28
Anchor a run-level island at both ends, or a list marker freezes in it
gordon2 Jul 31, 2026
0dd0d64
The list structure is back, and the third run-order defect is not
gordon2 Jul 31, 2026
7dfa142
Do not repair a run that is already in the order it is read
gordon2 Jul 31, 2026
9948ffe
Glued words are 6, and the digits-at-the-edge limit is real but misju…
gordon2 Jul 31, 2026
991412d
Give the digits-at-the-edge limit the reason measurement supports
gordon2 Jul 31, 2026
89af8f7
Derive one folio offset per document, or refuse to answer
gordon2 Aug 1, 2026
0d17670
Pin the folio offset both real manuals produce, and its margin
gordon2 Aug 1, 2026
19132da
Turn a contents entry into a jump to the page it names
gordon2 Aug 1, 2026
06d1fdc
Look at a contents link in a real browser, and click it
gordon2 Aug 1, 2026
dcbe0a1
Record the folio mapping: what it is, what it refuses, and what it re…
gordon2 Aug 1, 2026
28a23e0
Keep the diff to the folio change
gordon2 Aug 1, 2026
6f0dbf8
Assert the one thing a client cannot recover from: absent is not zero
gordon2 Aug 1, 2026
feade0f
Say why the reader resynchronises its opened page
gordon2 Aug 1, 2026
95ab465
Serve a section title once, where the section starts
gordon2 Aug 1, 2026
83b2a50
Find a page's columns from the page, not from what furniture left behind
gordon2 Aug 1, 2026
21ee5d0
Test the one thing 628 real pages cannot say: consecutive is not "ever"
gordon2 Aug 1, 2026
3beb82b
Lay a page out the way the page is laid out
gordon2 Aug 1, 2026
663509f
Read the two columns of a page that has no second column
gordon2 Aug 2, 2026
4f16196
Stop a table the reading-order check cannot see from closing over
gordon2 Aug 2, 2026
2c44d18
Read a right-to-left region's columns right to left
gordon2 Aug 2, 2026
f7171e8
Pin the pages that had two columns and no second column
gordon2 Aug 2, 2026
1de8310
Assert the table break in the reading-order check without a fixture
gordon2 Aug 2, 2026
4ff00e6
Record the pages that have two columns and no second column
gordon2 Aug 2, 2026
b106377
Correct the count of welded blocks and name the two shapes still welded
gordon2 Aug 2, 2026
ff5da01
Format the new tests the way the linter wants them
gordon2 Aug 2, 2026
83b7d7b
Pin the blocks two columns give back, which the agent's last commits …
gordon2 Aug 2, 2026
acacc0a
Offer the pages no language owns as a second scope at the gate
gordon2 Aug 8, 2026
af52f0d
Pin the neutral set's contents, and that opting out is today's conver…
gordon2 Aug 8, 2026
d37ea72
Cover the gate offer and the approve seam hermetically
gordon2 Aug 8, 2026
b45756b
Offer the extra scope on the gate screen, and make Import actually work
gordon2 Aug 8, 2026
0abdcd8
Close the four gaps mutation testing found
gordon2 Aug 8, 2026
b2092ae
Record the measurement in the three docs that carry the reasoning
gordon2 Aug 8, 2026
8778647
Keep the diff to the change: undo make fmt's churn on untouched files
gordon2 Aug 8, 2026
315220a
Measure a plate merge on shared labels, and record why it is refused
gordon2 Aug 9, 2026
140927c
Record in conversion.md why the plate merge is refused
gordon2 Aug 9, 2026
835a4ac
Carry a figure's callout labels as text, and stop growing the crop on…
gordon2 Aug 9, 2026
1483d14
Take a callout label out of the block flow, marked rather than deleted
gordon2 Aug 9, 2026
61d1112
Store a figure's callout labels, additively, without touching doc_blocks
gordon2 Aug 9, 2026
36c8f62
Draw the labels beside the picture, and pin what the claim rule still…
gordon2 Aug 9, 2026
1776dbe
Give the label container a width, or a strip collapses it to a point
gordon2 Aug 9, 2026
5b459ae
Cover the two verify changes hermetically, each with its control
gordon2 Aug 9, 2026
99c3d4d
Close the two gaps mutation testing found
gordon2 Aug 9, 2026
6140fff
Record the measurement in the two docs that carry the reasoning
gordon2 Aug 9, 2026
b22faea
Remove a scratch measurement that should never have been committed
gordon2 Aug 9, 2026
123ae72
Move the three verify pins this change moved, each with its sequence
gordon2 Aug 9, 2026
eb8e2fd
Say on maxLabelGrowth itself that it is no longer the shipped setting
gordon2 Aug 9, 2026
e5be702
Replace the grown-box attribution count with the invariant that repla…
gordon2 Aug 9, 2026
0e86411
Turn the reported-fault test from grown crops to carried labels
gordon2 Aug 9, 2026
482f1c2
Carry all of a wrapped label's lines, or the reader gets half of one
gordon2 Aug 9, 2026
b966d3c
Record what refuses the stranded labels, having measured the two alte…
gordon2 Aug 9, 2026
a3a13ab
Say which total the full-stop signal was measured against
gordon2 Aug 9, 2026
3532d4b
Move the block total the wrapped-label tail moved, and pin the mutati…
gordon2 Aug 9, 2026
857cbbe
Crop the band the page prints, and keep the label text as its descrip…
gordon2 Aug 9, 2026
7502bbe
Record the band, and correct three things this file had wrong
gordon2 Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ jobs:
run: npm run typecheck
working-directory: web

# node --test, running the TypeScript directly by stripping the types. Needs
# Node 22.18 or newer for that to be on without a flag; NODE_VERSION is "22",
# which setup-node resolves to the latest 22.x.
- name: Test
run: npm test
working-directory: web

- name: Build
run: npm run build
working-directory: web
Expand Down
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ config.local.yaml
/web/node_modules/
/web/dist/
/web/.vite/
# Where `vite build --ssr reader-check.tsx` and `search-check.tsx` put their
# bundles. Both render a screen to HTML on the command line, which is what a
# headless browser is then pointed at to take a screenshot of an authenticated page.
/web/.reader-check/
/web/.search-check/
# Where `vite build --config folio-browser-check.vite.ts` puts its bundle. Unlike
# the two above this one is a whole page a browser opens directly, with a real
# document's conversion JSON compiled into it — which is the other reason it is
# never committed.
/web/.folio-check*/
*.tsbuildinfo

# go
Expand All @@ -38,6 +48,11 @@ coverage.html
/tmp/
/scratch/

# Agent worktrees. Root-anchored for the same reason as /manualbox above: these
# are checkouts of this repository, and `git add -A` will otherwise stage one as
# an embedded repository.
/.claude/worktrees/

# Vite builds the SPA into internal/frontend/dist/app so it can be go:embed-ed.
# It writes into the app/ subdirectory rather than dist/ itself because Vite's
# emptyOutDir wipes its output directory on every build — which silently deleted
Expand Down
464 changes: 464 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ measurements:
| | |
|---|---|
| [ingest.md](docs/design/ingest.md) | How a 560-page, 34-language manual is reduced to the 16 pages you actually read, before any model is called |
| [layouts.md](docs/design/layouts.md) | Manuals are arranged differently — sequential sections or parallel columns — and where the seam between them goes |
| [regions.md](docs/design/regions.md) | Contract for storing a language that occupies part of a page, and what it deliberately leaves unsolved |
| [language-detection.md](docs/design/language-detection.md) | The four signals that say what language a page is in, what each one costs, and why the detector choice is deliberately still open |
| [providers.md](docs/design/providers.md) | Why a subscription CLI or a local model comes before a metered API key, and why a CLI adapter must batch a whole document |
| [privacy.md](docs/design/privacy.md) | What manualbox holds, ranked by how it actually leaks |
| [keys.md](docs/design/keys.md) | Encryption keys: choosing, storing, and recovering them |
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,17 @@ web-build: ## Build the frontend into web/dist
web-typecheck: ## Typecheck the frontend
cd web && npm run typecheck

# `node --test`, which runs the TypeScript directly by stripping the types. No test
# framework and no new dependency: the alternative was adding a runner to assert
# rules that are a few lines each.
.PHONY: web-test
web-test: ## Run frontend tests
cd web && npm test

## ---- quality ----

.PHONY: check
check: test lint web-typecheck ## Everything CI runs
check: test lint web-typecheck web-test ## Everything CI runs

.PHONY: test
test: ## Run Go tests with race detector
Expand Down
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

Self-hosted. Free. MIT. Single binary, SQLite, no external services required, and **no API key needed** to get real value out of it.

> ⚠️ Early development. The server runs, you can create an account and sign in, and background
> jobs report live progress — but **you cannot add devices or manuals yet**. That is M1, the next
> milestone. See the [Roadmap](#roadmap).
> ⚠️ Early development. You can create an account, add devices, and upload a manual — manualbox
> reads it locally and tells you what it contains, in which languages, before anything is
> converted or sent anywhere. Approve it and the pages you read are converted to blocks, and
> `GET /api/v1/search?q=` finds the sentence you need across every manual in the house.
> **It stops there for now:** the reader screen, the search screen and export are still to
> come. See the [Roadmap](#roadmap).

## Try it

Expand Down Expand Up @@ -75,7 +78,7 @@ Then it gets out of the way: notifications where you already look, and a calenda
| | |
|---|---|
| **M0** ✅ | Skeleton: config, SQLite + migrations, blob store, job queue, auth, API, frontend shell, Docker, CI |
| **M1** | Registry, document pipeline (convert → language-segment → index), reader, full-text search, **export** — see [ingest design](docs/design/ingest.md) |
| **M1** | Registry, document probe and language map ✅, conversion ✅, full-text search ✅, then the reader and **export** — see [ingest](docs/design/ingest.md), [conversion](docs/design/conversion.md) and [search](docs/design/search.md) |
| **M2** | Maintenance: schedules, battery charge cycles, service log, notifications, ICS calendar feed |
| **M3** | Translation: per-block, glossary, translation memory, side-by-side, post-editing |
| **M4** | Extraction: maintenance plans with citations, printable per-device cheat sheets, error-code lookup |
Expand All @@ -101,8 +104,10 @@ Optional external binaries, used when present and degraded gracefully when not (
| `tesseract` | OCR for scans and photos | `brew install tesseract tesseract-lang` |

Design decisions, with the measurements behind them, are in `docs/design/`:
[ingest](docs/design/ingest.md) · [providers](docs/design/providers.md) ·
[privacy](docs/design/privacy.md) · [keys](docs/design/keys.md).
[ingest](docs/design/ingest.md) · [layouts](docs/design/layouts.md) ·
[language detection](docs/design/language-detection.md) ·
[providers](docs/design/providers.md) · [privacy](docs/design/privacy.md) ·
[keys](docs/design/keys.md).

Conventions and the things that have already caused bugs here:
[CONTRIBUTING.md](CONTRIBUTING.md).
Expand Down
41 changes: 31 additions & 10 deletions cmd/manualbox/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import (
"github.com/gordon2/manualbox/internal/config"
"github.com/gordon2/manualbox/internal/db"
"github.com/gordon2/manualbox/internal/extern"
"github.com/gordon2/manualbox/internal/ingest"
"github.com/gordon2/manualbox/internal/jobs"
"github.com/gordon2/manualbox/internal/logging"
"github.com/gordon2/manualbox/internal/registry"
"github.com/gordon2/manualbox/internal/store"
)

Expand Down Expand Up @@ -68,6 +70,8 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
return cmdServe(ctx, rest, stdout, stderr)
case "doctor":
return cmdDoctor(ctx, rest, stdout)
case "verify":
return cmdVerify(ctx, rest, stdout, stderr)
case "version", "--version", "-v":
fmt.Fprintf(stdout, "manualbox %s (%s)\n", version, commit)
return nil
Expand All @@ -89,6 +93,7 @@ Usage:
Commands:
serve Run the web server and background workers
doctor Report configuration and which optional tools are available
verify Convert a PDF and report what is wrong with the conversion
version Print the version
help Show this help

Expand All @@ -98,6 +103,10 @@ Flags (serve, doctor):
Flags (doctor):
-redact Replace your home directory with ~, for pasting into a bug report

Flags (verify):
-limit <n> How many findings of each kind to print (default 20)
-all Print every finding

Configuration comes from defaults, then the config file, then MANUALBOX_*
environment variables. Run "manualbox doctor" to see what was resolved.
`)
Expand Down Expand Up @@ -197,19 +206,31 @@ func cmdServe(ctx context.Context, args []string, stdout, stderr io.Writer) erro
queue := jobs.NewQueue(database, log)
defer queue.Broker().Close()

registryService := registry.New(database, registry.Options{Logger: log})
ingestService := ingest.New(ingest.Deps{
Config: cfg,
Registry: registryService,
Store: blobs,
Jobs: queue,
Logger: log,
})

pool := jobs.NewPool(queue, cfg.Jobs, log)
// M1 registers the real conversion, OCR, translation, and extraction
// handlers here. Until then the pool runs with none, and a job of an unknown
// kind fails with a clear message rather than hanging.
// The document pipeline's handlers. Conversion, OCR, translation, and
// extraction register here as they land; a job of an unknown kind fails with a
// clear message rather than hanging.
ingestService.Register(pool)

server := api.New(api.Deps{
Config: cfg,
DB: database,
Store: blobs,
Auth: authService,
Jobs: queue,
Logger: log,
Version: version,
Config: cfg,
DB: database,
Store: blobs,
Auth: authService,
Jobs: queue,
Registry: registryService,
Ingest: ingestService,
Logger: log,
Version: version,
})

httpServer := &http.Server{
Expand Down
121 changes: 121 additions & 0 deletions cmd/manualbox/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package main

import (
"context"
"errors"
"flag"
"fmt"
"io"
"sort"
"text/tabwriter"
"time"

"github.com/gordon2/manualbox/internal/doc"
"github.com/gordon2/manualbox/internal/verify"
)

// cmdVerify converts a PDF and reports what is wrong with the conversion.
//
// It exists to be run on a manual the fixtures do not contain, which is where
// this check earns its keep: the two measured documents are the two whose defects
// are already written down. Nothing is stored and nothing is uploaded — the same
// stance doctor takes — so it is safe to point at a file and read the answer.
//
// The document is converted for EVERY language it holds rather than for the
// household's, because coverage is measured against all the text on a page and a
// page of a parallel-columns manual holds five languages. See [verify.Check].
func cmdVerify(ctx context.Context, args []string, stdout, stderr io.Writer) error {
fs := flag.NewFlagSet("verify", flag.ContinueOnError)
fs.SetOutput(stderr)
limit := fs.Int("limit", 20, "how many findings of each kind to print")
all := fs.Bool("all", false, "print every finding rather than the first few of each kind")
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() != 1 {
return errors.New("usage: manualbox verify [-limit n] [-all] <file.pdf>")
}
path := fs.Arg(0)

start := time.Now()
res, err := doc.Analyze(ctx, path)
if err != nil {
return err
}
langs := res.Languages()
names := make([]string, 0, len(langs))
for i := range langs {
names = append(names, langs[i].Lang)
}
fmt.Fprintf(stdout, "%d pages, %d language(s): %s\nprobed in %v\n",
res.Info.Pages, len(langs), join(names), time.Since(start).Round(time.Millisecond))

start = time.Now()
conv, err := verify.ConvertAll(ctx, path, res)
if err != nil {
return err
}
fmt.Fprintf(stdout, "%s\nconverted in %v\n", conv.Summary(),
time.Since(start).Round(time.Millisecond))
for _, n := range conv.Notes {
fmt.Fprintf(stdout, " note: %s\n", n)
}

start = time.Now()
rep, err := verify.Check(ctx, path, conv)
if err != nil {
return err
}
fmt.Fprintf(stdout, "\n%s\nchecked in %v\n\n", rep.Summary(),
time.Since(start).Round(time.Millisecond))
for _, n := range rep.Notes {
fmt.Fprintf(stdout, " note: %s\n", n)
}

tw := tabwriter.NewWriter(stdout, 0, 8, 2, ' ', 0)
fmt.Fprintf(tw, " kind\tfindings\tpages\n")
kinds := rep.Kinds()
for _, k := range verify.AllKinds {
if kinds[k] == 0 {
continue
}
fmt.Fprintf(tw, " %s\t%d\t%d\n", k, kinds[k], rep.PagesFlagged(k))
}
tw.Flush()

// The worst pages by coverage, whether or not they were reported: the
// measurement is what a person reads this for, and a document whose worst page
// scores 0.99 has been told something by that number.
cov := make([]verify.PageCoverage, len(rep.Coverage))
copy(cov, rep.Coverage)
sort.Slice(cov, func(a, b int) bool { return cov[a].Ratio < cov[b].Ratio })
fmt.Fprintf(stdout, "\nmedian coverage %.3f; least covered pages:\n", rep.MedianCoverage())
for i := 0; i < len(cov) && i < 5; i++ {
fmt.Fprintf(stdout, " page %d: %.3f (%d block characters against %d from pdftotext)\n",
cov[i].Page, cov[i].Ratio, cov[i].Blocks, cov[i].Text)
}

shown := make(map[verify.Kind]int, len(kinds))
fmt.Fprintln(stdout)
for i := range rep.Findings {
f := &rep.Findings[i]
if !*all {
if shown[f.Kind] >= *limit {
continue
}
shown[f.Kind]++
}
fmt.Fprintf(stdout, "%s: %s\n", f.Kind, f.Detail)
if f.Sample != "" {
fmt.Fprintf(stdout, " %s\n", f.Sample)
}
}
if !*all {
for k, n := range kinds {
if n > shown[k] {
fmt.Fprintf(stdout, "… %d more %s finding(s); -all prints them\n", n-shown[k], k)
}
}
}
return nil
}
Loading
Loading