Landing blocker for #61 and #63: the deployment checkout is 12 commits behind and `merge --ff-only` refuses while tracked paths are dirty. Discard is not available here (workspace policy: no `checkout`/`stash` on shared state), so the dirt is committed. - `agent/APPEND_SYSTEM.md`: the point-of-use / one-work-log / PR-link doctrine, already byte-identical to `origin/main` (`cmp` against the trunk blob: no difference). No new prose — the commit exists so the trunk merge can proceed. - `agent/config.yml` and `agent/managed-skills/**`: the machine-live tier the heartbeat owns (AGENTS.md § Live-sync tiers). `ALLOW_SECRETS=1` was used, deliberately and after auditing the staged diff by hand: the only credential-shaped strings in it are two copies of `AKIA0000000000000EXAMPLE` — the inert all-zero AWS example id — and they appear inside the two skill pages that teach that exact placeholder shape. This checkout is still running the pre-#61 hook; the merged gate (value-scoped placeholder filter, `71601f7`) exempts this value, so the refusal dies with this landing. No `--no-verify` was used: the gate ran and its verdict is recorded here. Refs #61
61 lines
2.7 KiB
Markdown
61 lines
2.7 KiB
Markdown
---
|
|
name: pdf-widget-layer-census
|
|
description: "Census a real-world \"flat\" PDF before assuming it needs field injection: Quartz/Preview exports often already carry an AcroForm widget layer under underscore rules; also audits pdf-lib flatten residue."
|
|
---
|
|
|
|
# Real-PDF census (widget-first, then judge)
|
|
|
|
A PDF that *looks* flat (`pdftotext` shows `____` fill rules) may already contain a
|
|
full AcroForm widget layer — macOS Preview/Quartz exports write widgets with
|
|
transparent rects under drawn underscores. Injecting fields into such a file
|
|
duplicates/shadows widgets. Always census before touching the pipeline.
|
|
|
|
## Steps
|
|
|
|
1. **Surface facts** (host):
|
|
```bash
|
|
pdfinfo file.pdf # "Form: AcroForm" hints at a widget layer
|
|
pdftotext -layout file.pdf - # label lines + underscore rules = intended fields
|
|
```
|
|
2. **Widget census** — authoritative check. With the franksign sidecar image,
|
|
the image ENTRYPOINT *is* `inject_fields.py`; override it:
|
|
```bash
|
|
podman run --rm --entrypoint python -v ~/Downloads:/in:z \
|
|
localhost/franksign-pdf-field-sidecar:local -c "
|
|
import pymupdf
|
|
d = pymupdf.open('/in/file.pdf')
|
|
for p in d.pages():
|
|
for w in p.widgets():
|
|
print(w.field_name, w.field_type_string, [round(v) for v in w.rect])
|
|
print('widgets:', sum(1 for p in d.pages() for _ in p.widgets()),
|
|
'annots:', sum(1 for p in d.pages() for _ in p.annots()))
|
|
"
|
|
```
|
|
3. **Fill test**: if widgets exist, `fillTemplate()` (pdf-lib) consumes the file
|
|
AS-IS — spec.json + sidecar are unnecessary for it. Flatten-verify below.
|
|
|
|
## Flatten audit gotchas
|
|
|
|
- `pdfinfo` prints `Form: AcroForm` even on correctly flattened pdf-lib output —
|
|
pdf-lib `flatten()` removes every *widget/annotation* but leaves the field
|
|
*dictionary*. Truth = MuPDF widget enumeration (0), not the pdfinfo line.
|
|
- `describeForm(bytes, specJson)` in `packages/pdf` parses the SPEC, not the
|
|
PDF, for the field list — pass a spec or skip it; calling it spec-less throws
|
|
`JSON Parse error`.
|
|
|
|
## Decision rule
|
|
|
|
- widgets > 0 → the widget layer IS the spec (names/types/rects, flip y to
|
|
bottom-left origin); NEVER run `inject_fields.py` over it.
|
|
- widgets = 0 → genuine flat form: sidecar injection with a spec (position
|
|
detection = issue #14 territory: underscore-run rects, label-derived names).
|
|
- Missing signature widget is normal ("please sign" is prose); franksign
|
|
decision 19: the typed full name is the signature.
|
|
|
|
## Privacy
|
|
|
|
Real documents are personal: scratch copies in /tmp, placeholder fill values,
|
|
destroy scratch (`rm /tmp/*` is fine OUTSIDE the repo; repo deletions still go
|
|
to `.AGENTTRASH/`). Never commit the document, a spike script touching it, or
|
|
its extracted text into the vault.
|