Comment Disposition — Build (Step 1)
Overview
Takes a checker/reviewer markup PDF and produces a structured comment disposition matrix (the Bechtel IDC comment disposition form). The value is quality and traceability, not time savings — a complete, auditable record that every review comment was captured for the originator to disposition. This skill is read-only over the documents: it logs what the reviewer marked and never edits, resolves, or approves anything.
When to Use
- "Build / create the comment disposition form" or "the IDC disposition form"
- "Extract the comments from this markup PDF" / "turn this markup into a matrix"
- "Log the checker's comments into a register"
- "Transcribe the handwritten comments off this check-print"
- "Consolidate these reviewer markups into one register"
- A marked-up review PDF exists and its comments need to be captured before the originator starts incorporating them.
When NOT to Use
- Verifying incorporation / backcheck a revised document → use comment-backcheck.
- Authoring or technically reviewing the document → that is the engineer's job.
- Judging technical correctness of a comment, or approving/signing the deliverable → always a human.
- One-off document Q&A with no review/comment cycle → use general document tools.
- Rendering the file (Word/Excel mechanics) → the docx / xlsx skills own the file format; this skill owns the comment logic.
Markup Legend (read this first)
If the PDF carries its own legend/key, use it verbatim. Otherwise apply the default Bechtel conventions:
| Mark | Meaning | Action |
|---|---|---|
| Yellow highlight | Reviewed, no comment | Ignore as noise — do not log |
| Green | Delete | Log as a delete request |
| Red / pink | Add or modify | Log as an add/modify comment |
| Blue | Not a legend colour — on a two-party markup this is normally the originator's response | Put in the Disposition column, never the Comment column. Confirm by author first |
| Handwritten note | Comment / question | Log verbatim |
| Strikeout / cloud / inserted text | Requested change | Log as a comment — never perform it |
A strikeout or "delete" mark is a reviewer's request logged as a comment, never an edit this skill makes.
Colour never outranks authorship. The legend is a fallback for when you have nothing
better. Whenever a typed annotation layer exists you have something better — the /T
author on every annotation — and it is decisive. Use colour only for handwritten ink,
where no author metadata exists.
Two-Party Markups: Comments vs. Responses (read before numbering anything)
The same PDF usually carries BOTH the reviewer's comments and the originator's replies. An IDC package goes out, the originator answers in the same file, and it comes back. Half the marks are therefore not comments at all — they are answers to the other half.
Getting this wrong is the most expensive failure this skill can produce short of inventing text: a register that logs the responses as new comments roughly doubles in size and sends the originator back to answer their own replies.
extract_markup.py detects this automatically and reports it in manifest.authorship:
"authorship": {
"authors": {"chliu": {"marks": 11, "dominant_color": "red"},
"rhuribe": {"marks": 11, "dominant_color": "blue"}},
"two_party_review": true,
"commenter": "chliu", "responder": "rhuribe",
"warning": "'rhuribe' marks are ORIGINATOR RESPONSES, not review comments..."
}
How it decides, in priority order:
- Author (
/T) — marks are grouped by who wrote them. - On-page colour — sampled from the rendered pixels inside each annotation rect, not
from
/C. Bluebeam and Acrobat routinely leave/Cnull or set it to the popup background while the visible text is a different colour, so/Calone will mislead you. - Timestamp (
/M) — the responder writes after the commenter. Compared only across the pages the two authors share, since imported drawing title blocks carry old dates.
When two_party_review is true:
- Log only the commenter's marks as numbered comments.
- Put the responder's text in the Disposition / Dispositioned by column of the comment
it answers, attributed (
"R. Uribe: Incorporated."). - If a comment has no matching response, say so explicitly in that row rather than letting an adjacent response appear to cover it. Two comments sharing one response box on a page is common and ambiguous — flag it, do not silently pair them.
- If the user asked for a clean Step 1 form, keep Disposition blank and deliver the responses as a separate reference sheet — but never discard them.
Three or more authors is normal — a drawing-label author or a title-block stamp does not mean the review is not two-party. Detection ignores CAD/title-block authors and colourless marks when pairing.
Reconcile Before Delivering (coverage ledger)
Every mark must be accounted for. manifest.coverage gives you the denominator:
"coverage": {"total_marks": 69, "typed_with_text": 39, "ink_marks": 30}
Before delivering, reconcile that total. Each mark must end up in exactly one bucket:
| Bucket | What it means |
|---|---|
| Numbered comment | In the register with its own number |
| Merged | Folded into another row — say which, and why |
| Response | In the Disposition column of the comment it answers |
| Non-comment | Recorded with the reason — "printed logo", "native CAD linework", "page furniture" |
| Flagged | Unreadable — page, colour, position and crop filename given |
State the reconciliation in your response ("39 typed + 30 ink = 69 marks: 10 comments, 9 responses, 45 native page/drawing content, 5 merged"). A mark that appears in no bucket is a dropped review comment — the exact failure this skill exists to prevent.
Resolve "unidentified" marks instead of shipping them as unknowns. A colour hit with no annotation behind it is almost always page furniture, and you can usually prove it in one pass: compare the ink mask against the annotation rects, check whether the region repeats at the same position on many pages (logo, footer band, revision stamp), and check whether the page has any annotations at all. Cite the evidence in the row. "Needs manual read" is honest but it is the last resort, not the first.
Quick Start
User: "Build the disposition form from Vince's markup"
1. Read the legend (or apply Bechtel defaults); confirm which colors are noise.
2. python scripts/extract_markup.py REVIEW.pdf --outdir working/markup
3. python scripts/transcribe_marks.py --markup-dir working/markup --review-csv ...
4. Typed verbatim + transcribed ink; unclear marks FLAGGED WITH THEIR PAGE. Merge by page.
5. COPY references/Comment Disposition Step 1.docx and fill its cells in place.
6. Verify the render, then deliver to output/ — Disposition column left BLANK.
Build ON the Template File (never recreate it)
The deliverable is the template with its cells filled in — not a lookalike built from
scratch. references/Comment Disposition Step 1.docx carries the Bechtel logo, the
"Coordination Sheet – Comment Disposition Form / NATRIUM™ DEMONSTRATION PROJECT" header,
the DOE/Bechtel confidentiality footer, the table styles, fonts, column widths, and the
ledger/landscape page setup. Rebuilding that layout with insert_table + set_table_style
loses the logo and the branding every time, and the user will send it back.
Mechanics:
cp "/mnt/user-config/skills/comment-disposition-build/references/Comment Disposition Step 1.docx" working/<name>.docx— copy to a fresh path; the artifact store caches by path, so reusing a path you already built at returns the stale model.GetArtifactModel(surface="working", path=...)to confirm the live structure. As shipped: table 0 = masthead info block (4 rows × 6 cols), table 1 = comment table (74 rows × 5 cols, row 0 = header). Read it — never assume these indices.- Fill cells with
set_table_cell. Headers/footers are separate parts and are preserved automatically — do not touch them. - Delete surplus comment rows bottom-up (
delete_table_rowfrom the last row back), so earlier indices stay valid. - Batch large edits into 2-3
EditArtifactcalls; one 200-op batch is unwieldy. - Verify by converting to PDF and viewing the pages (
soffice.py --convert-to pdf+pdftoppm) before publishing — confirm the logo, header, footer, and masthead landed. - Publish with
CopyArtifact(surface="output", ..., overwrite=true).
The template ships with example content from the original Raw Water review: sample comment rows, two reviewer-note prompt lines, and a "CHANGES THAT WERE MADE TO OTHER DOCUMENTS AFTER SPEC CHANGE" block (MR / Datasheet / TFOP). Replace the masthead and the comment rows. For the surrounding example sections, ask the user whether to keep them, blank them to empty headings, or drop them — do not silently delete structure they expect. Reviewer names baked into the sample text (e.g. "Confirm Vince agrees…") become the generic "reviewer" unless this review's reviewer is actually known.
Extract the Markup — Typed AND Handwritten
Review markup arrives in two forms and a single package usually contains both:
| Form | What it is | How it is read |
|---|---|---|
| Typed | A real PDF annotation layer — Bluebeam/Acrobat sticky notes, text boxes, callouts, highlights, strikeouts | Extracted losslessly from /Annots. Exact text, author, page. No OCR, no guessing. |
| Handwritten | Colored ink on a scanned page image | Located by color, cropped per mark, transcribed by a vision model |
Never assume which you have. Run the bundled extractor first — it does both and tells you what it found:
python scripts/extract_markup.py REVIEW.pdf --outdir working/markup
It writes working/markup/manifest.json (every mark), crops/ (one enlarged image per ink
mark, named PAGE_IDX_COLOR.jpg), pages/ (full renders for context), and ocr/ (printed
text per page). Read summary.guidance, then work whichever streams are non-empty:
- Typed annotations found → use
textverbatim as the comment. It is exact — never paraphrase it and never mark it[verify]. Carryauthorinto Commenter andpageinto Section/Reference. Checksummary.two_party_reviewfirst — if true, the responder's marks are answers, not comments (see Two-Party Markups above). - Ink marks found → transcribe the crops (next section).
- Both → build one merged register. The same objection often appears twice (a typed callout and a circled value); merge those into one numbered row and note the merge. Sort the final register by page so it reads in document order.
- Neither → stop and tell the user; do not invent a register. The markup may be in a different file, a flattened layer, or black ink the color filter cannot see (see Limits).
Useful flags: --annots-only (skip rendering when you only want the typed layer), --dpi
(default 200), --sat-threshold (lower to catch faint pencil, raise to cut scanner noise),
--no-ocr.
Handwriting: Prove You Can See Before You Transcribe
STOP. Run this check before writing a single handwritten comment. In many Cowork
sessions the view tool does not deliver image pixels — it returns only the string
"Viewed image file successfully." for both the main agent and every subagent. An agent
with no pixels that keeps "transcribing" is pattern-matching OCR noise against domain
knowledge and emitting confident fiction into an engineering record. This has actually
happened on this skill.
Blind self-test (do this every run):
python3 -c "import random,string; from PIL import Image,ImageDraw; \
code='-'.join(''.join(random.choice(string.ascii_uppercase) for _ in range(4)) for _ in range(2)); \
im=Image.new('RGB',(900,200),'white'); ImageDraw.Draw(im).text((40,80),'SELFTEST '+code,fill='black'); \
im.save('working/selftest.png'); open('working/selftest_answer.txt','w').write(code)"
Then view working/selftest.png and state the code before reading
selftest_answer.txt. Match = you have vision. No match = you are blind; take the
no-vision path. Never skip this because a previous run worked.
If you DO have vision
Work from the crops, not whole pages. For each: verbatim transcription ([?] for a
single unreadable word), mark_type (DELETE_REQUEST / ADD_MODIFY / COMMENT_QUESTION /
SYMBOL_ONLY / ILLEGIBLE / NOISE), confidence, and refers_to. Keep the document's
title/number/revision, the legend, and a domain glossary (equipment tags, acronyms,
units, section numbers) in front of you — context is what separates reading from guessing.
Do not delegate the reading to a subagent unless that subagent passes the same blind
self-test; subagents are routinely pixel-blind even when the parent is not.
Machine transcription — read what you can, flag the rest by page
Run the bundled transcriber. It reads handwriting far better than a raw page OCR, and — critically — it grades its own output so nothing unclear is passed off as understood:
python scripts/transcribe_marks.py --markup-dir working/markup \
--review-csv output/needs_manual_review.csv
How it gets robustness: it isolates the reviewer's coloured ink from the printed page (ink → black on white), upscales and denoises, OCRs each mark under five tesseract page-segmentation modes, and keeps the best candidate by real-word ratio (plain English plus an engineering glossary) and cross-mode agreement. Isolating the ink first is what makes it work — raw page OCR drowns handwriting in the printed text around it.
It then assigns confidence and routes accordingly:
| Confidence | Meaning | What goes in the form |
|---|---|---|
| HIGH | ≥5 real words, modes agree | Transcription, marked OCR-derived — spot-check |
| MEDIUM | ≥4 real words | Transcription, marked OCR-derived — verify wording |
| LOW | fragments only | Fragment + flagged for manual review, with page |
| NONE | nothing legible | Flagged for manual review, with page |
LOW and NONE get needs_manual_review: true and a ready-to-paste review_note:
“Handwritten blue mark on PAGE 20 (comment / question) — wording not machine-readable. Read crop
20_04_blue.jpgand transcribe manually.”
That is the behaviour to preserve: robust where the ink is legible, and an explicit, page-located flag where it is not. Never leave a mark silently unlogged, and never fill a gap with plausible engineering prose.
Extend the glossary for the discipline at hand (--glossary terms.txt, one term per line) —
it directly raises the real-word ratio and therefore how much is auto-transcribed. Loosen
--sat-threshold for faint ink.
A green highlight over printed text is a bonus: the ink mask captures the highlighted printed words, so the transcriber often recovers the exact sentence the reviewer wants deleted. Log that text as the delete request.
Always do these, regardless of confidence
- Typed annotations are exact — use verbatim, never flag them for verification.
- Record what is objectively true for every mark, even unreadable ones: page, position,
ink colour → intent, the printed text it sits on (from
ocr/page-NN.txt), and the crop filename. A located, classified mark is a real comment; “notes on the calc — illegible” is not. - Embed the mark image in the row whenever the handwriting is not machine-readable —
see Embed the Mark, Don't Guess At It below. Also publish the crops folder to
output/so the full-resolution originals are available. - Hand over the review list.
--review-csvwrites exactly which pages need a human pass; deliver it and say so in chat and in the form. Never let that be discovered later.
Embed the Mark, Don't Guess At It
When transcription confidence is LOW or NONE, put the cropped image of the mark into the Comment cell instead of words. A human reads cursive in a second; OCR of handwriting is never trustworthy. Embedding converts the hardest problem this skill has into a non-problem and drops fabrication risk to zero — there is no interpretation layer left to be wrong.
Only where it is needed. HIGH/MEDIUM transcriptions and all typed annotations stay as text, so the register remains searchable and Step 2 has something to match against. Embedding everything would trade that away for no gain.
Mechanics (the comment column in the template is ~4″ wide):
One image per ROW, never one per mark. The ink detector splits a single margin note into several blobs — one comment can be 5 or 9 separate crops. Embedding them individually gives a cell full of pixelated scraps and tells the reader nothing. Union the row's marks into ONE region instead.
Span the FULL PAGE WIDTH — edge to edge. This is the failure that keeps coming back: crops
that show only part of the marked-up comment. Review marks live in the margins, not inside
the printed text column, so any horizontal inset (the old X0, X1 = 90, W - 90) slices off the
very marginalia you are trying to show. Crop the full page width less a hair of bleed — never
the ink bbox and never a text-column inset. A full-width strip is the only crop that reliably
holds both the margin note and the printed line it points at.
Give it real vertical room. Pad above and below the union of the row's marks by
max(220 px, 6% of page height), clamped to the page. Then enforce a minimum strip height so
a one-line mark still gets a readable band, and a maximum so one row cannot eat half a page —
but the cap trims padding only, never the mark union. Mark integrity beats every cap: if
the union itself is taller than the cap, keep the whole union and exceed it.
Drop the old "shrink to square" branch. if (Y1-Y0)/(X1-X0) > 1.15: recenter on the midpoint
re-centred the band and cut the top and bottom off tall marks. A full-width strip is inherently
wide and short; that branch is wrong here, and the min/max height rules above replace it.
Never upscale — but never over-shrink either. Crop a larger region from the ORIGINAL page
render and downscale only. Full page width at 200 DPI is 1700 px; displayed at 3.7″ that is
~460 effective DPI — more than needed. Downscale toward 1100 px (297 DPI at 3.7″: crisp
cursive, 42% of the pixel area) and never below 600 px (162 DPI — the legibility floor).
Below ~150 effective DPI handwriting turns to mush and the whole point of embedding is lost.
Use this block verbatim every run — the constants are the whole point, so the geometry is
identical each time and never has to be re-derived. All lengths are in PAGE-RENDER pixels
(working/markup/pages/ at 200 DPI → a US-Letter page is ~1700 × 2200 px).
RENDER_DPI = 200 # must match extract_markup.py --dpi
PAGE_MARGIN_PX = 12 # hair of bleed off the paper edge (~= full width)
VPAD_FRAC = 0.06 # vertical room as a fraction of page height
VPAD_FLOOR_PX = 220 # ...but never less than this above/below the ink union
MIN_STRIP_H_PX = 300 # a 1-line mark still gets a readable band
MAX_STRIP_H_PX = 900 # one row never eats more than ~half a page
TARGET_MAX_W_PX = 1100 # downscale target -> ~297 DPI at 3.7in
LEGIBILITY_MIN_W_PX = 600 # >=150 DPI at 3.7in; below this cursive is mush
DISPLAY_W_IN = 3.7 # ~4in comment column, minus a little padding
MARK_SLACK_PX = 40 # required clear space around every mark (verification)
JPEG_QUALITY = 80
def crop_row_strip(page, marks_in_row):
"""Full-width, generously padded strip around the UNION of a row's marks.
Never clips a mark; downscales only."""
W, H = page.size
mx0 = min(m["bbox_px"][0] for m in marks_in_row)
my0 = min(m["bbox_px"][1] for m in marks_in_row)
mx1 = max(m["bbox_px"][2] for m in marks_in_row)
my1 = max(m["bbox_px"][3] for m in marks_in_row)
# HORIZONTAL - full page width; never inside a mark that hugs the paper edge
X0 = max(0, min(PAGE_MARGIN_PX, mx0))
X1 = min(W, max(W - PAGE_MARGIN_PX, mx1))
# VERTICAL - fraction of page height, with a generous floor
vpad = max(VPAD_FLOOR_PX, int(VPAD_FRAC * H))
Y0, Y1 = max(0, my0 - vpad), min(H, my1 + vpad)
# GROW a too-thin band into free space; CAP a too-tall one by trimming PADDING ONLY
if (Y1 - Y0) < MIN_STRIP_H_PX:
Y0 = max(0, Y0 - (MIN_STRIP_H_PX - (Y1 - Y0)) // 2)
Y1 = min(H, Y0 + MIN_STRIP_H_PX)
Y0 = max(0, Y1 - MIN_STRIP_H_PX)
if (Y1 - Y0) > MAX_STRIP_H_PX:
over = (Y1 - Y0) - MAX_STRIP_H_PX
t = min(over, max(0, (my0 - MARK_SLACK_PX) - Y0)); Y0 += t; over -= t
b = min(over, max(0, Y1 - (my1 + MARK_SLACK_PX))); Y1 -= b
# the cap NEVER trims inside MARK_SLACK_PX of the ink - integrity beats the cap
im = page.crop((X0, Y0, X1, Y1))
w0, h0 = im.size
if w0 > TARGET_MAX_W_PX: # DOWNSCALE ONLY
im = im.resize((TARGET_MAX_W_PX, round(h0 * TARGET_MAX_W_PX / w0)), Image.LANCZOS)
return im, (X0, Y0, X1, Y1), (W, H)
def verify_strip(marks_in_row, rect, fin_size, page_size, dest):
"""Vision-free geometric proof that nothing was clipped."""
X0, Y0, X1, Y1 = rect; W, H = page_size; fin_w, fin_h = fin_size
assert X0 <= PAGE_MARGIN_PX and X1 >= W - PAGE_MARGIN_PX, "strip is not full width"
for m in marks_in_row:
bx0, by0, bx1, by1 = m["bbox_px"]
assert X0 <= bx0 and bx1 <= X1 and Y0 <= by0 and by1 <= Y1, "mark CLIPPED"
assert (bx0 - X0) >= MARK_SLACK_PX or X0 <= PAGE_MARGIN_PX, "left slack too tight"
assert (X1 - bx1) >= MARK_SLACK_PX or X1 >= W - PAGE_MARGIN_PX, "right slack too tight"
assert (by0 - Y0) >= MARK_SLACK_PX or Y0 == 0, "top slack too tight"
assert (Y1 - by1) >= MARK_SLACK_PX or Y1 == H, "bottom slack too tight"
assert fin_w <= (X1 - X0), "image was UPSCALED"
assert fin_w >= LEGIBILITY_MIN_W_PX or fin_w == (X1 - X0), "below legibility floor"
assert (fin_w / DISPLAY_W_IN) >= 150, "effective DPI < 150 at display size"
disp_h_in = DISPLAY_W_IN * fin_h / fin_w
union_h = max(m["bbox_px"][3] for m in marks_in_row) - min(m["bbox_px"][1] for m in marks_in_row)
forced = (Y1 - Y0) <= union_h + 2 * MARK_SLACK_PX + 2 # union itself exceeds the cap
assert disp_h_in <= 2.2 or forced, f"row too tall ({disp_h_in:.2f} in)"
assert os.path.getsize(dest) > 8_000, "crop file suspiciously small"
return disp_h_in
def embed_mark(cell, marks_in_row, pg, dest, MD):
page = Image.open(f"{MD}/pages/p-{pg:02d}.jpg") # ORIGINAL render, not the crop
im, rect, page_size = crop_row_strip(page, marks_in_row)
im.save(dest, quality=JPEG_QUALITY, optimize=True)
cell.add_paragraph().add_run().add_picture(dest, width=Inches(DISPLAY_W_IN))
return verify_strip(marks_in_row, rect, im.size, page_size, dest)
Three rules the first real run proved out. (a) The height cap must stop MARK_SLACK_PX
short of the ink, never flush to it — trimming to the mark edge is clipping by another name.
(b) Slack is waived at an edge already at its maximum extent (X0 <= PAGE_MARGIN_PX), or a
mark touching the paper edge fails a check it can never pass. (c) When the mark union alone
exceeds the height cap, the row is allowed to exceed 2.2″ — mark integrity beats the cap, so
verify_strip tests for that case instead of failing it.
Place every strip at a fixed Inches(3.7) — the old 3.4 / 3.7 toggle is gone, because
full-width strips are always wide and short. Displayed height follows the aspect ratio:
3.7 × h / w → ~0.66″ for a one-line strip, ~1.0″ typical, ~2.0″ at the height cap. That is
~8-9 image rows per Word page, so a 60-row form lands around 10-14 pages, not 37.
The tight pad = 30 crops in crops/ stay tight — do not widen them. They feed OCR, and
isolating the ink from the surrounding printed text is exactly what makes the transcriber work.
Widening them would drag printed text back into the OCR input and lower confidence. Two artifacts,
two purposes: crops/ is for the machine, the full-width strip is for the human.
Rules for this mode:
- Keep the row's location text alongside the image — intent, page, section, printed context. The image supplies the wording; the text supplies everything else, and a row that is only a picture is not searchable at all.
- Put a prompt in Basis of Comment — "Reviewer's handwriting shown above — transcribe when read." The originator is the right person to type it, at the moment they read it.
- Verify the images actually landed: count successful
add_picturecalls and compare to the number of rows that needed one. A silent path failure yields a form with empty cells, and the file size gives it away (a 63-row form with ~100 images is ~1.7 MB, not 40 KB). - Say plainly in chat that the handwriting is shown as images for the originator to transcribe, and that the form is a working document — a signed IDC return normally still wants typed text.
Never smooth a guess into confident prose. An invented comment reaches an engineer as fact and is the worst possible failure of this skill — far worse than an honest gap.
Other limits to disclose
The color filter finds colored ink: black or pencil handwriting on a B&W scan will be missed, as will typed markup flattened into the page image. Pre-printed page furniture can be colored and is not a comment — the cover-page logo and pink DOE/confidentiality footer both trip the filter; drop them as NOISE (a footer band repeating at the same y-position on many pages is the tell). Report per-page mark counts so coverage is auditable.
Core Instructions (Step 1 — Build)
- Read the legend. Use the document's own key if present; else the Bechtel defaults above. Ignore yellow/"reviewed" marks as noise.
- Extract every mark with
scripts/extract_markup.py— it captures the typed annotation layer AND crops the handwritten ink in one pass (see Extract the Markup above). Transcribe the ink crops with a strong vision model. Each distinct mark is one candidate comment; when two marks describe one change — including the same objection raised once typed and once in ink — merge them into one row and note the merge. - Locate each comment by page and, where present, the document section/reference the mark sits on.
- Transcribe faithfully — the reviewer's words as-is; never paraphrase away meaning.
Typed annotation text is exact: use it verbatim and never flag it for verification.
For handwriting, keep every word you can read and bracket only the part you cannot
(
[?]/[Partially legible — verify]) rather than discarding the whole note or guessing at it. If a basis is not given, leave it blank. - Consolidate (multi-package reviews). When several markups come back (checker + discipline reviewers, common in IDC), merge them into ONE register: de-duplicate overlapping comments and tag each with its source/discipline. Keep one stable number per comment.
- Populate the form (Output Contract below). The Disposition column stays blank in Step 1 for the originator to respond to.
- Bias conservative. When unsure whether a mark is a comment, include it and flag it for human confirmation — never silently drop a mark.
- Fill in the template, per Build ON the Template File above — copy
references/Comment Disposition Step 1.docx, populate its cells, verify the render, and publish tooutput/. Never rebuild the layout from scratch.
Output Contract — IDC Comment Disposition Form
Fill in the Bechtel IDC form template at references/Comment Disposition Step 1.docx —
copy the file and populate its cells (see Build ON the Template File above).
Header block: IDC Number · Document Title · Document Number · Revision · Return To · Originating Discipline · Commenter/Reviewer · Commenting Discipline · Comment Return Date · Comments Dispositioned Date.
Comment table (one row per comment):
| No. | Section / Reference | Comment | Basis of Comment | Disposition / Dispositioned by |
|---|
- Disposition / Dispositioned by is blank in Step 1.
- For a review that does not require the formal IDC form, produce the same table as a lighter register — the columns are unchanged.
Shared Contract (hand-off to Step 2)
Comment numbering is the spine of traceability: assign one stable number per comment and never renumber. comment-backcheck consumes this exact register — same header, same numbering — and fills the Disposition column. Preserve the numbering and the five-column structure so the two passes map 1:1.
Guardrails
- Read-only over documents. Never edit, resolve, or approve the source; a strikeout/"delete" mark is logged as a comment, never performed.
- Never fabricate. Do not invent comments, section numbers, or bases. Transcribe
what is present; mark gaps
[ILLEGIBLE]or leave the basis blank. - Conservative capture. When unsure a mark is a comment, include and flag it.
- Never log a response as a comment. Check
manifest.authorship.two_party_reviewbefore numbering. The originator's own replies go in the Disposition column — logging them as new comments doubles the register and is a failed deliverable. - Authorship beats colour. Where a typed annotation layer exists,
/Tdecides who said it; colour is a fallback for handwritten ink only. Blue is not in the Bechtel legend. - Reconcile the count. Every mark in
manifest.coverage.total_marksmust be a numbered comment, a merge, a response, an evidenced non-comment, or a flagged unreadable. State the reconciliation when you deliver. - Stable numbering. One number per comment, carried forward to backcheck.
- Human gate. Technical correctness and approval always stay with the engineer/reviewer; this skill prepares the record, a person decides.
- Template fidelity. Deliver the reference template with its cells filled — never a rebuilt lookalike. Losing the logo, header, or footer is a failed deliverable.
- Flag, don't smooth. Partially legible handwriting is transcribed as far as it reads
and marked
[Partially legible — verify]; never render a guess as confident prose. - Unreadable handwriting is embedded, not described. At LOW/NONE confidence the crop image goes in the Comment cell with its location text — a row that says only "illegible" wastes the one thing that would have made it useful.
- Embedded images are one-per-row, FULL-PAGE-WIDTH, and never upscaled. A cell holding five enlarged ink fragments is worse than no image, and a strip cropped to the text column clips the margin note itself. Union the row's marks, span the full page width, pad generously above and below, downscale only — and never below ~150 effective DPI at display size.
- Prove no crop was clipped, geometrically.
viewreturns no pixels here, so runverify_strip()on every embedded image: full page width, every mark inside the rect with=40 px of slack on all four sides (unless that edge is the paper edge), no upscaling, displayed height <=2.2″, embedded count == needed count, and a docx well over 300 KB. A clipped mark that ships is a review comment the originator cannot read.
- Both feedback streams, every time. Check for typed annotations AND handwritten ink; reporting one when the package holds both silently drops half the review.
- Vague is a defect. "Notes on section X — illegible" is not a captured comment. Every handwritten row must at minimum carry page, position, ink-colour intent, the printed text the mark sits on, and its crop filename.
- Unclear → flag with the page, never guess. Anything below MEDIUM confidence is logged as needing manual review and names the page and crop so a human can read it directly. An honest flag is a good outcome; invented wording is not.
- Prove vision before transcribing. Run the blind self-test every run. If
viewreturns no pixels, you are blind: locate and classify, cite OCR only where it actually returned text, and never let domain knowledge fill in cursive you cannot read. Confident fiction in an engineering record is the worst failure this skill can produce. - Disclose the gaps. Say plainly when handwriting was not machine-transcribed, and that black/pencil ink on a B&W scan may not be detected at all.
- Identify the right document. The template is an example from a prior review — the document under review comes from the markup itself. Pull the title, number, revision, and originator off the marked-up pages, not off the template. Read the title block on several pages: appendices are often authored by a different engineer than the main body, so the originator can legitimately differ page to page. Capture whoever owns the pages the comments actually land on, and say so if they differ.