diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index d7954e7..8b4237a 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -1493,14 +1493,28 @@ Region-based extraction finds all 23 expected copy fragments, and full-page 144-DPI visual QA finds no clipping or overlap. Preserve it as the verified Run 60 press baseline. -The operator has now authorized a new press-layout task over the designated -accepted Canopy Ledger edition. Do not rewrite its copy. Build a reusable -random-seed-driven newspaper layout script with bounded typography and press -variation; the same source and seed must reproduce the same layout choices. -Use ImageGen for story-specific woodcuts. Reserve printer's ornaments and -fleurons for reusable decoration rather than narrative illustration. Preserve -the source identity plus generation and visual provenance with the finished -artifact. This task temporarily precedes the Delvehold adapter. +The historical press-layout and style/flow seed split are complete. The +reference corpus runs through 1914, and +`notes/historical-newspaper-layout-grammar.md` preserves the derived grammar. +Manifest v2 is +`output/tex/canopy-ledger-run-61-style-1847-flow-723.manifest.toml`. It binds +`style_seed=1847`, `flow_seed=723`, frozen source SHA-256 +`8f195140f52a40ba705d6a66987146a1500ae89668e5d82b01d1792eb3afb882`. +The final artifact is +`output/pdf/canopy-ledger-run-61-style-1847-flow-723.pdf`, SHA-256 +`a457f4b1702a7a52480e42fce0039f0dbe677ff32958e98f1c412b346237190c`. + +An identical seed pair produces byte-identical TeX and now byte-identical PDF. +Changing only `flow_seed` preserves the exact manifest style section while +materially changing a plausible layout; changing only `style_seed` preserves +flow topology. Cut-first and cut-midstory templates both pass visual inspection. +The failed split-card template was deleted. Frozen copy and assets, actual PDF +`/BM /Multiply`, and visual provenance remain intact. Layout is closed. + +Future advertisements remain economic content. The layout may reserve typed +slots, but only the economic layer may own advertiser identity, goods, price, +claims, campaign continuity, and payment. Unfilled slots collapse unless the +edition explicitly records unsold space; presentation cannot invent a campaign. Run 54 exposed one separate upstream publicity defect: a public strategic activity propagates its channel to private `MemberMemory` outcomes, which is @@ -1512,11 +1526,10 @@ materializing a `NewsIssue`. 1. Preserve immutable accepted Run 60 and Run 59; do not rewrite, recompose, or resimulate either edition. 2. Preserve the delivered Run 60 PDF and its frozen-copy verification as the - baseline. Typeset the operator-designated accepted Canopy Ledger with the - reusable seeded press-layout machinery described above; verify the artifact - without changing accepted copy. -3. After the press-layout task, advance the Delvehold-owned adapter against - Ghostlight's admitted generic consumer contract. Defer multiple publications + earlier baseline, plus the final Run 61 style/flow artifact, v2 manifest, + grammar, hashes, frozen copy, and art assets recorded above. +3. Advance the Delvehold-owned adapter + against Ghostlight's admitted generic consumer contract. Defer multiple publications until separately authorized, or follow the operator's next directed work. Preserve immutable Runs 60 through 63 and do not replay newspaper work for cache telemetry. diff --git a/notes/ghostlight-current-system-map.md b/notes/ghostlight-current-system-map.md index 1378a48..e14faeb 100644 --- a/notes/ghostlight-current-system-map.md +++ b/notes/ghostlight-current-system-map.md @@ -1691,48 +1691,89 @@ write world state. #### Seeded TeX press projection - **Owner:** the operator-designated frozen newsroom Markdown owns masthead, - edition label, sections, story order, headlines, decks, bylines, and every - paragraph. `tools/typeset_newspaper.py` owns presentation only: parsing that - copy, choosing bounded seed-driven style, emitting TeX and its manifest, and - invoking LuaLaTeX. Woodcut bitmaps and their adjacent prompt sidecars own - illustration provenance, not facts or copy. -- **Inputs:** one frozen Markdown edition, one required integer seed, zero or - more ordered wordless woodcut PNGs, output/TeX/manifest paths, and an optional - LuaLaTeX engine or `--no-compile`. The current Run 61 projection uses seed - 1847 and the `boundary-rail-exchange` and `sinkroot-gauge-gallery` cuts. Their - matching prompt files preserve the exact fresh ImageGen requests and record - grayscale, 72-percent threshold, and trim preparation. -- **Outputs:** escaped TeX, a `ghostlight.seeded_tex_press.v1` TOML manifest, and - optionally a compiled PDF. The manifest records source/output/TeX paths, - seed, selected style values, ordered woodcut paths, and SHA-256 digests for - source, TeX, tool, woodcuts, engine, and compiled output. These are press - artifacts, not another newsroom composition or correction surface. -- **Derived state:** the parsed `Edition`, seeded `Style`, page grouping, - ornament choice, image placement and widths, TeX markup, and PDF pagination - are presentation projections. The same seed deterministically selects the - same bounded style. Exact portable PDF reproduction additionally depends on - the named system fonts; source, woodcut, engine, tool, TeX, and output bytes - are bound by the manifest. + edition label, article identities, lead designation, sections, headlines, + decks, bylines, and every paragraph. `tools/typeset_newspaper.py` owns + presentation only through two independent decisions. `style_seed` owns the + durable house body: sheet geometry, type, stock, ink, rules, gutters, and + ornaments. `flow_seed` owns the per-issue display template, secondary-story + grouping, which eligible secondary closes page one, cut-first versus + cut-midstory placement, cut scale, and page closure. Both apply the pre-WWI + mass-circulation grammar distilled in + `notes/historical-newspaper-layout-grammar.md`; neither owns copy. Woodcut + bitmaps and their adjacent prompt sidecars own illustration provenance, not + facts. A future economic layer owns advertiser identity, goods, prices, + claims, campaign continuity, and payment; the press may only place an already + admitted ad module in the shared page grid. +- **Inputs:** one frozen Markdown edition, required independent style and flow + seeds, zero or more article-indexed wordless woodcut PNGs, + output/TeX/manifest paths, and an optional LuaLaTeX engine or `--no-compile`. + The current Run 61 projection uses style seed 1847 and flow seed 723 and binds + `boundary-rail-exchange` to article 0 and + `sinkroot-gauge-gallery` to article 1. Their matching prompt files preserve + the exact fresh ImageGen requests and record grayscale, 72-percent threshold, + and trim preparation. No ad module is currently admitted. +- **Outputs:** escaped TeX, a `ghostlight.seeded_tex_press.v2` TOML manifest, and + optionally a compiled PDF. Manifest v2 records independent `[style]` and + `[flow]` projections and binds both seeds, source/output/TeX paths, + article-indexed woodcuts, and SHA-256 digests for source, TeX, tool, woodcuts, + engine, and compiled output. The current artifact is + `output/pdf/canopy-ledger-run-61-style-1847-flow-723.pdf`, bound by + `output/tex/canopy-ledger-run-61-style-1847-flow-723.manifest.toml` with output + SHA-256 `a457f4b1702a7a52480e42fce0039f0dbe677ff32958e98f1c412b346237190c`. + These are press artifacts, not another newsroom composition or correction + surface. +- **Derived state:** the parsed `Edition`, house `Style`, per-issue `Flow`, + two-page story grouping, continuous four-column reading flow, unbreakable + story-header modules, story-local cut placement and widths, TeX markup, and + PDF pagination are presentation projections. Flow templates include + `display-plate` and `display-band`; inside-story placement includes + `cut-first` and `cut-midstory`. TikZ/PDF multiply blending makes monochrome cut + paper disappear into the seeded stock. The same style seed preserves the + bounded house body across issues while a different flow seed may change + grouping and placement without changing that body. Exact portable PDF + reproduction additionally depends on the named system fonts; source, + woodcut, engine, tool, TeX, and output bytes are bound by manifest v2. - **Forbidden writers:** the press, TeX compiler, manifest, woodcuts, and prompt sidecars cannot add, remove, summarize, reorder, correct, or reinterpret - newsroom copy; change bylines or story priority; write newsroom checkpoints - or world state; or promote an illustration into evidence. A successful PDF - compile cannot admit missing or altered copy. + newsroom copy; change bylines, lead designation, or newsroom-owned narrative + function; write newsroom checkpoints + or world state; promote an illustration into evidence; fabricate an + advertiser, offer, price, claim, payment, or campaign; or fill an unadmitted + ad slot. Flow may choose which eligible secondary closes page one, but it + cannot rewrite article text, change the lead, reassign an indexed cut, detach + that cut from its story, or omit it. A successful PDF compile cannot admit + missing or altered copy. - **Shared path:** every press run parses the frozen source, derives one style - from the seed, writes TeX and manifest, then either stops at `--no-compile` or - compiles that exact TeX in a temporary directory and copies the resulting PDF - to the named output. Woodcut order alone selects lead and later placement. + from `style_seed` and one issue flow from `flow_seed`, verifies that all frozen + fields survive, writes TeX, compiles that exact TeX when requested, and then + writes manifest v2. Each woodcut is keyed to its owning article index and + remains attached to that story's display field in either cut-first or + cut-midstory form; accepted copy then flows continuously through the common + four-column grid. There is no detached image bank or bottom-art reservation. + Any future admitted ad module must use this same grid without changing + article-internal copy order or flow-selected grouping. - **Cut line:** the live press path contains no model rewrite, editorial summarizer, copy-fitting loop, or second content owner. Press variation is bounded to page geometry, typography, color, rules, fleurons, and illustration - sizing/placement; the former fixed ReportLab front-page renderer is not live. -- **Verification layer:** admission requires source-to-TeX/PDF copy completeness - and order, a same-source/seed derivation check, successful halt-on-error - compilation, rendered-page inspection for clipping and overlap, and matching - manifest/woodcut prompt provenance. LuaLaTeX produces a two-page PDF with - embedded Times New Roman faces. The parser flushes the final article paragraph - at end of file; extracted copy includes the closing `Marshal Eryn Tal` - paragraph, and rendered inspection shows no clipping or overlap. + sizing/placement. The former fixed ReportLab renderer, three-column lead plus + one-column rail, paired story-minipage layout, and detached image bank are not + live. Minipages now protect headers only; story bodies remain in continuous + column flow. The single-seed manifest v1 and prior + `output/pdf/canopy-ledger-run-61-seed-1847.pdf` artifact are not current press + authority. +- **Verification layer:** admission requires source-to-TeX/PDF field completeness + and article-internal order, exact same-pair TeX derivation, independent style- + and flow-seed variation checks, successful halt-on-error compilation, rendered-page + inspection for clipping and overlap, one attached rendering for every indexed + cut, and matching manifest/woodcut prompt provenance. The current manifest v2 + records `display-plate`, `cut-first`, and page-one secondary article 2. Its + final TeX uses four-column `multicols`, article-indexed cuts, `needspace` plus + header-only minipages, and TikZ multiply scopes. LuaLaTeX produces a two-page + PDF with embedded Times New Roman faces; `SOURCE_DATE_EPOCH=0` and + `FORCE_SOURCE_DATE=1` make the same admitted inputs byte-reproducible under the + bound engine. Extracted copy includes the closing `Marshal Eryn Tal` + paragraph. Rendered inspection confirms continuous reading order, attached + cuts blended into the stock, unstranded headers, and no clipping or overlap. #### Acceptance-driver per-wave newspaper recovery diff --git a/notes/historical-newspaper-layout-grammar.md b/notes/historical-newspaper-layout-grammar.md new file mode 100644 index 0000000..5c6c62d --- /dev/null +++ b/notes/historical-newspaper-layout-grammar.md @@ -0,0 +1,148 @@ +# Historical Newspaper Layout Grammar + +This is a working press grammar, not a costume catalogue. It distills recurring +page structures from representative nineteenth-century newspapers into rules +the seeded TeX press can actually enforce. + +## Reference set + +- *Pall Mall Gazette*, first issue, 1865: a deliberately spacious evening + review using two wide columns, double masthead rules, restrained italic + headlines, and long uninterrupted reading measure. + +- *Evening Star* (Washington), 28 February 1880 and 17 June 1890: metropolitan + sheets with a broad nameplate, six or more narrow continuous columns, dense + vertical packing, and advertisements/notices inhabiting the same column grid + as news. + + +- *The Sun* (New York), 30 May 1885: a uniform multi-column field with modest + story heads and almost no unowned white space. + +- University of Illinois, *American Newspapers, 1800–1860*: physical and + production context for five-to-eight-column sheets, full-height columns, + front-page advertising, and the migration of timely news to inside pages. + +- Robinson, *Newspapers and Advertising*: nineteenth-century advertising moves + from small text-heavy insertions intermingled on front and back pages toward + larger display advertising and more consistent classified pages late in the + century. + +- *New-York Tribune*, 16 April 1912: a page-wide disaster head, central map and + photographic display spanning several columns, flanking report columns, and + dense continuation below the illustration. + +- *Richmond Times-Dispatch*, 17 April 1912: a page-wide head and ship image + followed by modular three-column packages, lists, subheads, and boxed matter. + +- *St. Louis Globe-Democrat*, 17 April 1912: a large central illustrated package + surrounded by independent column-width reports and subordinate display heads. + +- *The Daily News* (St. John's), 14 August 1914: narrow continuous news columns + coexist with multi-column war heads and boxed display advertisements occupying + ordinary grid modules. + + +Reference scans used for visual comparison live in disposable +`tmp/newspaper/references/`; the URLs above remain the durable provenance. + +## Families + +The press selects a family before it varies details. + +### Evening review + +- Two wide columns. +- Smaller physical sheet and lower information density. +- Long-form reading dominates scanning. +- Sparse display hierarchy; italics, rules, and small capitals do more work + than large headlines. + +### Metropolitan sheet + +- Four to eight narrow columns depending on sheet size and date. +- The nameplate spans the page, while issue data, price, notices, or subscription + terms may flank or bracket it. +- Stories, notices, and advertisements inhabit one shared vertical grid. +- Story heads are usually modest. Late-century sensational sheets may spend + several columns on a display head or illustration, but the surrounding text + closes around that expenditure. + +The Canopy Ledger currently uses a compact four-column metropolitan sheet. Its +small accepted copy body does not earn a six-to-eight-column broadsheet. + +### Mass-circulation display sheet, 1895–1914 + +- The underlying narrow-column grid survives, but a major event may spend + several columns on one head, deck stack, map, photograph, or illustration. +- Display packages remain attached to their story. Copy resumes directly below + them or rises in adjacent columns; the illustration does not become a detached + gallery object. +- Secondary stories become more visibly modular through short rules, boxed + lists, subheads, and column-spanning heads. +- A large central image may organize the entire page, with flanking text fields + and subordinate modules closing around it. +- Display advertisements become visually distinct boxed modules while small ads + and notices continue to occupy ordinary column measure. + +The accepted Run 61 special issue uses this family. Its crisis head and cut earn +a page-wide display package; accepted story copy then returns immediately to the +four-column grid. + +## Composition rules + +1. **The page is a column economy.** Every occupied region begins and ends on + column or half-column lines. Large vacant regions require an owned reason + such as an absent paid advertisement; they are not a centering technique. +2. **Build fields, not isolated boxes.** A story field contains its head, deck, + byline, cut, caption when present, and body measure. Adjacent fields share a + top line or establish an intentional stepped hierarchy. +3. **Cuts spend story measure.** A narrative cut is assigned to one story and + placed inside that story's contiguous field. It may span multiple columns, + with the story flowing above, beside, or below it. It may not be deposited at + a page edge after unrelated columns have ended. +4. **Ink blends with stock.** Monochrome cuts use multiply compositing (or its + alpha-equivalent against a flat stock color) so white image paper disappears + and black linework shares the page ink. +5. **Reading order remains typographic.** Column traversal, rules, heads, and + continuation labels must make the next fragment unambiguous. Geometry may + not reorder accepted copy. +6. **Density follows the edition.** Type size, page dimensions, column count, + and cut expenditure are solved together from copy volume. A short edition + becomes a smaller sheet or fewer columns; it does not become a broadsheet + with a desert under it. +7. **Hierarchy is rationed.** One lead field may spend a multi-column head and + large cut. Secondary heads stay within their field. Masthead display does not + authorize every story to shout. +8. **Rules join related matter.** Column rules and horizontal rules articulate + the grid. Ornaments close or divide small matter; they do not substitute for + missing content. +9. **Advertisements are economic content.** The press may reserve typed ad + slots by column width and depth. Only the economic layer may fill them with + advertiser identity, goods, price, claims, campaign continuity, and payment. + Unfilled slots collapse unless the edition explicitly records unsold space. +10. **House and issue variation are separate.** `style_seed` owns the durable + publication body: sheet, grid, fonts, type scale, stock, ink, rules, gutter, + and ornament vocabulary. `flow_seed` owns one issue's imposition: display + template, story grouping, page-one closure, cut span and placement, page + breaks, and continuation rhythm. Neither may detach art, rewrite copy, + fabricate ads, or create barren pages. +11. **A house remains recognizable across flows.** Holding `style_seed` fixed + must preserve typography, stock, ink, rules, sheet geometry, and base grid + while materially different `flow_seed` values produce distinct plausible + readings of the same admitted content. Holding `flow_seed` fixed while the + style changes must preserve its story/cut topology. + +## Current Run 61 imposition + +- House style seed `1847`; issue flow seed `723`. The manifest records both as + independent inputs under `ghostlight.seeded_tex_press.v2`. +- Page one: full-width nameplate and crisis head; a centered page-wide Boundary + Rail cut; lead and secondary copy flowing continuously through four columns. +- Page two: a compact repeated nameplate and Sinkroot head; the centered gauge + cut immediately below its owning head; Sinkroot and Dispatches copy flowing + continuously through the same four-column grid. +- Secondary story headers are unbreakable column modules, so a section label or + head cannot strand at the foot of the preceding column. +- Both cuts use PDF multiply blending so their white image stock disappears into + the seeded paper stock. No image bank or detached bottom art reservation exists. diff --git a/output/pdf/canopy-ledger-run-61-seed-1847.pdf b/output/pdf/canopy-ledger-run-61-style-1847-flow-723.pdf similarity index 79% rename from output/pdf/canopy-ledger-run-61-seed-1847.pdf rename to output/pdf/canopy-ledger-run-61-style-1847-flow-723.pdf index f1137d1..e271ac3 100644 Binary files a/output/pdf/canopy-ledger-run-61-seed-1847.pdf and b/output/pdf/canopy-ledger-run-61-style-1847-flow-723.pdf differ diff --git a/output/tex/canopy-ledger-run-61-seed-1847.manifest.toml b/output/tex/canopy-ledger-run-61-style-1847-flow-723.manifest.toml similarity index 57% rename from output/tex/canopy-ledger-run-61-seed-1847.manifest.toml rename to output/tex/canopy-ledger-run-61-style-1847-flow-723.manifest.toml index 83b6c54..4a64958 100644 --- a/output/tex/canopy-ledger-run-61-seed-1847.manifest.toml +++ b/output/tex/canopy-ledger-run-61-style-1847-flow-723.manifest.toml @@ -1,23 +1,24 @@ -schema = "ghostlight.seeded_tex_press.v1" +schema = "ghostlight.seeded_tex_press.v2" source = "F:/Projects/Ghostlight/acceptance-run61-newspaper.md" source_sha256 = "8f195140f52a40ba705d6a66987146a1500ae89668e5d82b01d1792eb3afb882" -output = "F:/Projects/Ghostlight/output/pdf/canopy-ledger-run-61-seed-1847.pdf" -tex = "F:/Projects/Ghostlight/output/tex/canopy-ledger-run-61-seed-1847.tex" -tex_sha256 = "c25e7390b2ca3ba68d455962401cc7e7bb77ce99caf4610f4565a380ca10a0ff" +output = "F:/Projects/Ghostlight/output/pdf/canopy-ledger-run-61-style-1847-flow-723.pdf" +tex = "F:/Projects/Ghostlight/output/tex/canopy-ledger-run-61-style-1847-flow-723.tex" +tex_sha256 = "002a1a6271ee7a746e86d6632037809683e4a39510a91b4c90ee63c10e5d7717" tool = "F:/Projects/Ghostlight/tools/typeset_newspaper.py" -tool_sha256 = "3090b99546c12d2cb9afa44ec1afa2270cb7474aadd3a242a7dafeab37e931d7" -seed = 1847 +tool_sha256 = "b3481086d82f87dce8ab4302139d7343bcaad79180f57984b07246ab383456f6" +style_seed = 1847 +flow_seed = 723 engine = "C:/Users/Meta/AppData/Local/Programs/MiKTeX/miktex/bin/x64/lualatex.exe" engine_sha256 = "e2bce0b59faf2d6501d0d03d8d13a5b93621285d7839438a19911ef963d46bfa" -output_sha256 = "529de5202c5c948f3e1339f8663184cc933794a0d85c1e42f30cd84a7f95c6c2" +output_sha256 = "a457f4b1702a7a52480e42fce0039f0dbe677ff32958e98f1c412b346237190c" [style] -width = 9.8 -height = 14.0 -margin = 0.38 -body = 8.6 -leading = 1.04 -gutter = 10.5 +width = 9.6 +height = 10.9 +margin = 0.34 +body = 7.7 +leading = 1.03 +gutter = 9.5 rule = 0.36 masthead = 39 lead = 29 @@ -26,13 +27,20 @@ text = "Times New Roman" paper = "EFE1C2" ink = "21170F" fleuron = "cross" -lead_cut = 0.82 -inside_cut = 0.54 -rules = "double" +rules = "heavy-thin" + +[flow] +template = "display-plate" +inside_layout = "cut-first" +page_one_secondary = 2 +lead_cut = 0.97 +inside_cut = 0.78 [[woodcuts]] +article_index = 0 path = "F:/Projects/Ghostlight/assets/newspaper/woodcuts/boundary-rail-exchange.png" sha256 = "87666118f63d55c797cece58eb5b09ff0984cecfe9c5e29e69932c830508bf87" [[woodcuts]] +article_index = 1 path = "F:/Projects/Ghostlight/assets/newspaper/woodcuts/sinkroot-gauge-gallery.png" sha256 = "496e5b83e296e9c4479c9b00eca01ac34ed610de2aa11f8a9353ac1dc5f3cc95" diff --git a/output/tex/canopy-ledger-run-61-seed-1847.tex b/output/tex/canopy-ledger-run-61-style-1847-flow-723.tex similarity index 65% rename from output/tex/canopy-ledger-run-61-seed-1847.tex rename to output/tex/canopy-ledger-run-61-style-1847-flow-723.tex index cc45a0c..5ae3aea 100644 --- a/output/tex/canopy-ledger-run-61-seed-1847.tex +++ b/output/tex/canopy-ledger-run-61-style-1847-flow-723.tex @@ -1,60 +1,55 @@ \documentclass{article} -\usepackage[paperwidth=9.8in,paperheight=14.0in,margin=0.38in]{geometry} -\usepackage{fontspec,graphicx,xcolor,microtype,multicol,ragged2e,fancyhdr} +\usepackage[paperwidth=9.6in,paperheight=10.9in,margin=0.34in]{geometry} +\usepackage{fontspec,graphicx,xcolor,microtype,multicol,ragged2e,fancyhdr,tikz,needspace} \setmainfont{Times New Roman}\newfontfamily\displayface{Times New Roman}\newfontfamily\sectionface{Times New Roman} \definecolor{paper}{HTML}{EFE1C2}\definecolor{ink}{HTML}{21170F}\pagecolor{paper}\color{ink} -\setlength{\columnsep}{10.5pt}\setlength{\columnseprule}{0.36pt}\setlength{\parindent}{1em}\setlength{\parskip}{0pt}\setlength{\emergencystretch}{1em} -\pagestyle{fancy}\fancyhf{}\renewcommand{\headrulewidth}{0pt}\fancyfoot[C]{\displayface\fontsize{6.5}{7}\selectfont THE CANOPY LEDGER · SEED 1847 · \thepage} -\newcommand{\pressrules}{\hrule height 1.1pt\vspace{2pt}\hrule height .35pt}\newcommand{\fleuron}{\rule{18pt}{.35pt}\hspace{5pt}\raisebox{-1pt}{\rule{1pt}{7pt}}\hspace{-4pt}\rule{7pt}{1pt}\hspace{5pt}\rule{18pt}{.35pt}} +\setlength{\columnsep}{9.5pt}\setlength{\columnseprule}{0.36pt}\setlength{\parindent}{1em}\setlength{\parskip}{0pt}\setlength{\emergencystretch}{1em} +\pagestyle{fancy}\fancyhf{}\renewcommand{\headrulewidth}{0pt}\fancyfoot[C]{\displayface\fontsize{6.5}{7}\selectfont THE CANOPY LEDGER · HOUSE 1847 · FLOW 723 · \thepage} +\newcommand{\pressrules}{\hrule height 1.7pt\vspace{1pt}\hrule height .25pt}\newcommand{\fleuron}{\rule{18pt}{.35pt}\hspace{5pt}\raisebox{-1pt}{\rule{1pt}{7pt}}\hspace{-4pt}\rule{7pt}{1pt}\hspace{5pt}\rule{18pt}{.35pt}} \newcommand{\masthead}[1]{{\displayface\bfseries\fontsize{39}{41}\selectfont\centering\MakeUppercase{#1}\par}} \newcommand{\leadhead}[1]{{\displayface\bfseries\fontsize{29}{30.5}\selectfont\centering #1\par}} \newcommand{\storyhead}[1]{{\displayface\bfseries\fontsize{14}{15}\selectfont\RaggedRight #1\par\smallskip}} \newcommand{\deck}[1]{{\displayface\itshape\fontsize{8}{9.2}\selectfont\RaggedRight #1\par\smallskip}} \newcommand{\byline}[1]{{\displayface\bfseries\fontsize{6.8}{7.5}\selectfont\MakeUppercase{By #1}\par\smallskip}} -\AtBeginDocument{\fontsize{8.6}{8.94}\selectfont\justifying} +\AtBeginDocument{\fontsize{7.7}{7.93}\selectfont\justifying} \begin{document}\thispagestyle{fancy} {\displayface\fontsize{7}{8}\selectfont CURRENT EDITION\hfill SOURCE: acceptance-run61-newspaper.md\par}\smallskip\pressrules\smallskip \masthead{The Canopy Ledger}\smallskip\pressrules\medskip \leadhead{The Cordon Holds. So Do the Severed Hands.}\smallskip {\displayface\itshape\fontsize{9.2}{10.5}\selectfont\centering Roots are bending toward live mana conduits, the Exchange remains isolated, and severed hands lie undisturbed at the scene.\par} {\displayface\bfseries\fontsize{7}{8}\selectfont\centering BY MERA QUILL\par}\smallskip -\noindent\includegraphics[width=0.82\textwidth]{F:/Projects/Ghostlight/assets/newspaper/woodcuts/boundary-rail-exchange.png}\par\smallskip -\begin{multicols}{3}\textbf{Boundary Rail Exchange —} The Boundary Rail Exchange remains isolated as roots bend toward active mana conduits, with its warning procedure maintained and severed hands left undisturbed at the scene. That last fact is the one procedure cannot make tidy: at the Exchange, severed hands remain while roots bend toward live power. +\begin{center}\begin{tikzpicture}\begin{scope}[blend mode=multiply]\node[inner sep=0] {\includegraphics[width=0.97\linewidth]{F:/Projects/Ghostlight/assets/newspaper/woodcuts/boundary-rail-exchange.png}};\end{scope}\end{tikzpicture}\end{center}\smallskip +\begin{multicols}{4}\textbf{Boundary Rail Exchange —} The Boundary Rail Exchange remains isolated as roots bend toward active mana conduits, with its warning procedure maintained and severed hands left undisturbed at the scene. That last fact is the one procedure cannot make tidy: at the Exchange, severed hands remain while roots bend toward live power. Boundary Exchange Workers erected a reinforced conduit exclusion cordon on Sept. 20. Eight hours later, Vesa Thorn marked a safe withdrawal route and widened the warning cordon. By early Sept. 21, Orin Pell had recorded that the roots were bending toward active mana conduits and that the Exchange must remain isolated. The sequence is emergency restraint: workers established a cordon, retreat was made clearer, and isolation continued after the roots’ movement toward active conduits was recorded. -But a cordon and warning procedure do two things at once: they mark a boundary around the conduits, while the hands remain undisturbed at the Exchange. No operational warning answers who lost them, who controls their recovery and examination, or whose authority could safely reopen the Exchange while the roots still lean toward active mana. Until those questions are answered, the severed hands are not incidental debris beside a safety procedure. They are the bodily account the procedure has not yet settled.\end{multicols}\vfill\begin{center}\fleuron\end{center} +But a cordon and warning procedure do two things at once: they mark a boundary around the conduits, while the hands remain undisturbed at the Exchange. No operational warning answers who lost them, who controls their recovery and examination, or whose authority could safely reopen the Exchange while the roots still lean toward active mana. Until those questions are answered, the severed hands are not incidental debris beside a safety procedure. They are the bodily account the procedure has not yet settled. +\par\medskip \Needspace{18\baselineskip}\noindent\begin{minipage}{\linewidth}\hrule height .45pt\smallskip {\sectionface\fontsize{7.2}{8}\selectfont\MakeUppercase{Courts \& Councils}}\par\smallskip \storyhead{The Witnesses Copied the Ledger Before the Curator Found His Voice} +\deck{Witnesses secured separately recorded accounts before Curator Oren Vale stated an intention to conduct interviews and issue a rejection of interference—without saying whose interference was at issue.} +\byline{Aven Tarl}\end{minipage}\par\smallskip\textbf{Contested Seed-Vault —} The Seed-Vault Witnesses created and retained an independent copy of the witness ledger, with accounts recorded separately, at 22:00 on Sept. 20. That is the accomplished fact in this dispute: the people whose testimony is at stake created their own record before the curator’s stated course could take effect. + +Eight hours later, Curator Oren Vale publicly stated an intention to conduct separate formal witness interviews and issue an in-person statement rejecting seizure, substituted testimony and unsupported custody claims. It is an admirable catalogue of abuses to oppose, and a revealing one. Vale did not name the person or authority behind any seizure, substitution or custody claim, leaving the public with all the ceremonial language of vigilance and none of the discourtesy of identifying whom vigilance is required against. + +Vale’s interviews and proposed rejection remain a promised course. The witnesses’ copy already exists in their keeping. Whatever title eventually arrives to assert custody, the contest now begins from the embarrassing position that the witnesses acted first—and that their dignity is safer in a duplicated ledger than in anyone’s reassurance. +\end{multicols} +\vfill\begin{center}\fleuron\end{center} \clearpage\thispagestyle{fancy}{\displayface\fontsize{7}{8}\selectfont CURRENT EDITION\hfill PAGE 2\par}\smallskip\pressrules\smallskip {\displayface\bfseries\fontsize{18}{19}\selectfont\centering The Canopy Ledger\par}\smallskip\pressrules\medskip -\noindent -\begin{minipage}[t]{\dimexpr(\textwidth-2\columnsep)/3\relax}\begin{minipage}[t]{\linewidth}{\sectionface\fontsize{7.2}{8}\selectfont\MakeUppercase{Guilds \& Trade}}\par\smallskip \storyhead{Sinkroot Wins Scrutiny, Not Yet Safety, as Deep-Current Dispute Settles} +\Needspace{18\baselineskip}\noindent\begin{minipage}{\linewidth}\hrule height .45pt\smallskip {\sectionface\fontsize{7.2}{8}\selectfont\MakeUppercase{Guilds \& Trade}}\par\smallskip \storyhead{Sinkroot Wins Scrutiny, Not Yet Safety, as Deep-Current Dispute Settles} \deck{Residents have secured immediate scrutiny of measured deep-current turbulence and proposed interim feeding, but the public rail shows neither a shutdown nor restitution—and a petition for a stay is not a stay.} -\byline{Ossan Reed} - -\textbf{Sinkroot Gauge Gallery —} Immediate public scrutiny of Sinkroot’s measured deep-current turbulence and proposed interim feeding has been settled after residents established a dated public report and petition. That is a material gain for the Sinkroot Residential Roll: readings and the proposed response can no longer be treated as private workshop weather. It is not, on the present public account, proof that the turbulence has stopped, that a temporary stay has taken effect, or that any household has received restitution. +\byline{Ossan Reed}\end{minipage}\par\smallskip\begin{center}\begin{tikzpicture}\begin{scope}[blend mode=multiply]\node[inner sep=0] {\includegraphics[width=0.78\linewidth]{F:/Projects/Ghostlight/assets/newspaper/woodcuts/sinkroot-gauge-gallery.png}};\end{scope}\end{tikzpicture}\end{center}\begin{multicols}{4}\textbf{Sinkroot Gauge Gallery —} Immediate public scrutiny of Sinkroot’s measured deep-current turbulence and proposed interim feeding has been settled after residents established a dated public report and petition. That is a material gain for the Sinkroot Residential Roll: readings and the proposed response can no longer be treated as private workshop weather. It is not, on the present public account, proof that the turbulence has stopped, that a temporary stay has taken effect, or that any household has received restitution. Hours earlier, the residential roll established a dated turbulence report and petitioned for disclosure, restitution and a temporary stay. The order matters. First the residential roll made a record; then scrutiny became immediate. Discussion first and measurements later is a familiar arrangement, but a dated gauge reading is harder to sweep into the sump than a reassurance. The unresolved mechanical question is interim feeding: what is being fed, through which arrangement, under what measured conditions, and who remains nearest the consequences if the turbulence persists. The settled scrutiny makes that proposal public business, but the available account does not specify what residents may halt, which thresholds would compel a shutdown, or who must answer if the proposed feed worsens the current. A petition can restrain an official’s sleep; only an operational restraint can restrain a system. -Sinkroot is not alone in reaching for instruments before promises. Gaugewatch Households have retained a dated calibrated counterflow report and a hearing petition at Gaugewatch Gallery. Two residential populations now possess measurements and written demands. That may be the beginning of leverage, but nobody should mistake paperwork for pressure relief: until the readings produce safer feeding, an enforceable stop or paid restitution, the households are still carrying the load while the rail records the dispute.\end{minipage}\end{minipage}\hspace{\columnsep}% -\begin{minipage}[t]{\dimexpr(\textwidth-2\columnsep)/3\relax}\begin{minipage}[t]{\linewidth}{\sectionface\fontsize{7.2}{8}\selectfont\MakeUppercase{Courts \& Councils}}\par\smallskip \storyhead{The Witnesses Copied the Ledger Before the Curator Found His Voice} -\deck{Witnesses secured separately recorded accounts before Curator Oren Vale stated an intention to conduct interviews and issue a rejection of interference—without saying whose interference was at issue.} -\byline{Aven Tarl} - -\textbf{Contested Seed-Vault —} The Seed-Vault Witnesses created and retained an independent copy of the witness ledger, with accounts recorded separately, at 22:00 on Sept. 20. That is the accomplished fact in this dispute: the people whose testimony is at stake created their own record before the curator’s stated course could take effect. - -Eight hours later, Curator Oren Vale publicly stated an intention to conduct separate formal witness interviews and issue an in-person statement rejecting seizure, substituted testimony and unsupported custody claims. It is an admirable catalogue of abuses to oppose, and a revealing one. Vale did not name the person or authority behind any seizure, substitution or custody claim, leaving the public with all the ceremonial language of vigilance and none of the discourtesy of identifying whom vigilance is required against. - -Vale’s interviews and proposed rejection remain a promised course. The witnesses’ copy already exists in their keeping. Whatever title eventually arrives to assert custody, the contest now begins from the embarrassing position that the witnesses acted first—and that their dignity is safer in a duplicated ledger than in anyone’s reassurance.\end{minipage}\end{minipage}\hspace{\columnsep}% -\begin{minipage}[t]{\dimexpr(\textwidth-2\columnsep)/3\relax}\begin{minipage}[t]{\linewidth}{\sectionface\fontsize{7.2}{8}\selectfont\MakeUppercase{Dispatches}}\par\smallskip \storyhead{Grain-Passage Proposal Announced; the Bridge Remains the Weapon} +Sinkroot is not alone in reaching for instruments before promises. Gaugewatch Households have retained a dated calibrated counterflow report and a hearing petition at Gaugewatch Gallery. Two residential populations now possess measurements and written demands. That may be the beginning of leverage, but nobody should mistake paperwork for pressure relief: until the readings produce safer feeding, an enforceable stop or paid restitution, the households are still carrying the load while the rail records the dispute.\par\medskip \Needspace{18\baselineskip}\noindent\begin{minipage}{\linewidth}\hrule height .45pt\smallskip {\sectionface\fontsize{7.2}{8}\selectfont\MakeUppercase{Dispatches}}\par\smallskip \storyhead{Grain-Passage Proposal Announced; the Bridge Remains the Weapon} \deck{The commune has proposed the same witnessed accounting terms to the claimant, the courts and the refusing legion. None has yet proved that grain or its carriers can cross safely.} -\byline{Lysa Fen} - -\textbf{Occupied River Toll Bridge —} The River Commune Bridge Assembly has publicly proposed opening witnessed grain-passage negotiations with the Veiled Claimant and the Rival Bloodline Courts, while proposing to send the same accounting offer to the Refusing Border Legion. That symmetry is the commune’s sharpest available defense: no throne, bloodline or uniform is offered private terms in the proposal. It is not yet safe passage. At an occupied crossing, every contender can praise procedure; the test is whether the proposal produces passage. +\byline{Lysa Fen}\end{minipage}\par\smallskip\textbf{Occupied River Toll Bridge —} The River Commune Bridge Assembly has publicly proposed opening witnessed grain-passage negotiations with the Veiled Claimant and the Rival Bloodline Courts, while proposing to send the same accounting offer to the Refusing Border Legion. That symmetry is the commune’s sharpest available defense: no throne, bloodline or uniform is offered private terms in the proposal. It is not yet safe passage. At an occupied crossing, every contender can praise procedure; the test is whether the proposal produces passage. In a public statement, the legion proposed issuing and publishing an order barring seizure of any toll, seed-vault, child or ledger under disputed command. The statement also proposed requesting authenticated legal grounds and directing patrols to secure the camp and approaches without advancing. Read from the bridge rather than the camp, the useful words are “without advancing”; the dangerous ones are “secure” and “approaches.” A proposed non-seizure order restrains soldiers on paper only if issued and followed. It does not establish who controls the road, who may halt a carrier, or whether refusal survives the first disputed wagon. -Marshal Eryn Tal has committed a witnessed summons and conference-procedure order, copied by canopy-wire. That gives the coming contest witnesses and a public trail. But the next meaningful fact will not be another signature. It will be grain crossing with its carriers and commune negotiators intact—and an accounting no claimant, court or legion can alter after the gates close.\end{minipage}\end{minipage}\vfill\begin{center}\includegraphics[width=0.54\textwidth]{F:/Projects/Ghostlight/assets/newspaper/woodcuts/sinkroot-gauge-gallery.png}\end{center}\smallskip\begin{center}\fleuron\end{center} +Marshal Eryn Tal has committed a witnessed summons and conference-procedure order, copied by canopy-wire. That gives the coming contest witnesses and a public trail. But the next meaningful fact will not be another signature. It will be grain crossing with its carriers and commune negotiators intact—and an accounting no claimant, court or legion can alter after the gates close.\end{multicols} +\vfill\begin{center}\fleuron\end{center} \end{document} diff --git a/state/map.yaml b/state/map.yaml index eaf7fb3..32de7ea 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -53,6 +53,7 @@ hard_boundaries: - Prompt text is projection, not truth storage. - A consumer-supplied ontology or fixed seed may bypass Session Zero world generation; it may never bypass WorldKernel validation, closed mutation authority, or atomic CultCache commit. - Delvehold retains quantitative economy authority. Ghostlight string resource handles and strategic resource effects must not impersonate quantities, prices, recipes, production capacity, inventories, orders, contracts, or conservation semantics. + - Newspaper layout may reserve typed advertisement slots, but only the economic layer may supply advertiser identity, goods, price, claims, campaign continuity, or payment. Unfilled slots collapse unless the edition explicitly records unsold space; the compositor cannot invent an ad campaign to fill a page. - Canonical agent state and per-agent perceived state must remain distinct. - Characters act from local knowledge, local perception, local pressure, and bounded affordances. - Aetheria prose must support tonal range and ordinary life, not only crisis pressure. Wit, warmth, domesticity, ritual, boredom, wonder, and quiet dread are valid state-grounded modes when they fit the scene. @@ -194,7 +195,6 @@ live_architecture: - Institution, faction, and consumer decision models active_subgoals: - - Typeset the operator-designated accepted Canopy Ledger edition without changing its copy: build a reusable random-seed-driven newspaper layout script with bounded typography and press variation, use ImageGen for story-specific woodcuts, and reserve printer's ornaments and fleurons for reusable decoration. Preserve source identity and make a given source-plus-seed reproducible. - Connect a Delvehold-owned adapter to the admitted generic consumer contract: shared compiler/consumer WorldSeed admission, externally authoritative institution-or-Gestalt snapshots, attributed proposal acknowledgement, and committed-event newspaper composition over typed CultNet, without consumer-specific IDs or schemas in Ghostlight. - Extend strategic/offscreen named-person individuation beyond the proven emerge-then-act path so people plot across sustained political waves, acquire relationships, fold, and return without granting a cell, arena, or model commit authority. - Pressure the proven 200-cell dispatcher with authored consumer hierarchies and measure provider cost; the active budget ceiling is 240 while provider concurrency remains separately capped at 32. @@ -327,5 +327,6 @@ current_status: summary: Ghostlight's generic consumer boundary admits sparse WorldSeeds, runs the hundreds-class parallel cell machinery used by Dungeon, and projects grounded newspapers from committed public events. Local acceptance proves 1,000 subjects across 200 cells and two-wave durable named-person emergence. Exact-source provider run 38 at `596e50679244b5d42b4cfdf70d723f815751b8ff` resumed the preserved world at revision 8 without replaying compilation or four committed locality elaborations, recovered 51 of 52 strategic-wave cells on pulse three and the final cell on pulse four, then committed all six waves through revision 14. Pushed commit `3a69306f3004e4af2cd57bba612a6ad8d1b8132f` implements the mapped clock-consequence owner and is verified locally: world clocks own declared consequence text and exact observable scope; a bounded agent may propose legacy scope binding, while `WorldKernel` alone admits it and materializes stable threshold-crossing events and public news through the common mutation path. Binding, emitted-event ancestry, mutation receipts, and the next newspaper boundary persist atomically. Exact-source run 39 resumed run 38 without replaying its six committed waves, bound clock consequences, committed six new waves through revision 21, and emitted Issues 7 through 12 plus a final digest. This completes the mechanical provider acceptance path; all inherited wave 1-6 checkpoint hashes match run 38 exactly and live services were unchanged. Independent blind editorial review returns FAIL: only Issue 7 approaches spicy, while Issues 8-12 are dominated by plans, filings, inspections, warnings, and abstract relationship deltas without betrayal, scandal, public reaction, sustained reversal, or material lived cost. That run-39 diagnosis is superseded for the current phase: run 51 closes the world and reconciliation mechanics, and the active defect belongs to newsroom narrative selection and self-contained semantic provenance over immutable facts. Independent grounding review also returns FAIL: Issue 9 inflates a planned expedited agenda into completion, while Issue 8 assigns unsupported feminine pronouns to Sela Venn and Eryn Tal. Other grounding mechanics pass: 208/208 citations resolve, no future events appear, no replay occurred, the player remained unchanged, all 116/116 manifest entries verify, the archive is exact, and root independently confirmed the live service PIDs and invocation IDs remained unchanged. Editorial and grounding acceptance are terminally failed for run 39. Eight titled elaborators use the generic agent harness under deterministic weighted scheduling, bounded parallelism, and Luna/Terra/Sol routing. Workers remain proposal-only; deterministic admission binds exact frozen schedules, dispatch, receipts, conflicts, and semantic verification, and `WorldKernel` alone owns atomic CultCache persistence. Destination reconciliation is a private transactional workbench, provider schema compatibility is lowered once at the shared model-connector boundary, and durable elaboration and strategic-pulse checkpoints bind to their canonical mutation proofs. The generic ModelAgent harness remains consumer-neutral transport. Provider run 23 remains stopped; earlier failed runs and checkpoint archives remain immutable provenance. The live daemons remain outside the acceptance body. Dungeon play remains regression pressure, the Delvehold game-side adapter remains unimplemented, and unpublished Mars and Kalsa destination previews remain unapproved. provider_acceptance_checkpoint: Exact pushed commit `139b8a6` completed Run 60 under `character-newsroom.v10` without world or per-wave replay. Its acceptance witness and terminal status bind the completed page and exact resume lineage. The independently dispatched blind reviewer, shown only the reader-facing fantasy newspaper, read it as a real left-populist civic broadsheet and did not immediately identify procedural generation, so the narrative reverse-Turing criterion passes. The review's substantive criticism is that the three stories feel like three rewrites of one briefing; accepted Run 60 remains immutable. The run used 96,902 prompt tokens and reported zero cache hits. Its delivered typeset artifact is `output/pdf/canopy-ledger-run-60-front-page.pdf`; frozen-source and visual verification remain accepted. Exact pushed `bc2ca39` then produced frozen-fact newsroom-only Run 61 Issues 13, 15, 16, 17, and 18 without world or wave model replay. Provider output proves `special_issue` convergence in Issues 13 and 15, independent `general_issue` beats in Issues 16 through 18, and assignment-selector cache hits. Blind review of Issues 13, 15, 16, and 17 passes both distinct recurring voices and issue-shape diversity, recognizing Ossan, Mera, Corin, and Lysa's biases plus modest cross-issue evolution. Run 62 exercised v12 without world or wave replay and failed only because two lead articles proposed invalid datelines; recurring reporter calls still recorded zero cache hits. Run 63 used exact pushed v13 `a52d57b`; Issues 17 and 18 completed before root stopped the final weekly recomposition, both repeated bylines again recorded zero cache hits, and live services remained unchanged. Preserve Runs 61 through 63 as stopped or completed immutable evidence. Copy defects remain later deadline-correction work rather than a reason to reopen accepted issue artifacts. implementation_checkpoint: Exact pushed v13 `a52d57b` keeps one stable reporter action schema, places a common stable house charter before recurring identity and dynamic assignment, and projects deterministic `allowed_datelines` from cited records while retaining copy-desk factual authority and deterministic cited-place admission; 480 library tests and the all-target check pass. Run 63 falsifies reporter-cache pursuit as a useful acceptance target for sporadic independent filings. The proposed v14 cache-floor prompt growth was removed. Append-only agent contexts remain the natural cache beneficiary; accepted Run 60 remains immutable. - next_action: Typeset the operator-designated accepted Canopy Ledger without rewriting its copy. Build a reusable random-seed-driven newspaper layout script whose bounded typography and press variation are reproducible for the same source and seed; use ImageGen for story-specific woodcuts and printer's ornaments or fleurons only for reusable decoration. Verify the produced press artifact and preserve its source and visual provenance. Resume the Delvehold-owned adapter after this operator-directed press-layout task. + presentation_checkpoint: The historical press-layout and seed-ownership split are complete. `notes/historical-newspaper-layout-grammar.md` remains the durable grammar. Manifest v2 at `output/tex/canopy-ledger-run-61-style-1847-flow-723.manifest.toml` independently binds `style_seed=1847`, `flow_seed=723`, frozen source SHA-256 `8f195140f52a40ba705d6a66987146a1500ae89668e5d82b01d1792eb3afb882`, and the final artifact `output/pdf/canopy-ledger-run-61-style-1847-flow-723.pdf`, SHA-256 `a457f4b1702a7a52480e42fce0039f0dbe677ff32958e98f1c412b346237190c`. An identical seed pair produces byte-identical TeX and PDF; changing only flow preserves the exact style section while materially changing a plausible layout, and changing only style preserves flow topology. Cut-first and cut-midstory templates pass visual inspection; the failed split-card template is deleted. Frozen copy/assets, actual PDF `/BM /Multiply`, and economic-layer advertising authority remain invariant. + next_action: Advance the Delvehold-owned adapter against Ghostlight's admitted generic consumer contract, unless a newer operator directive supersedes it. Preserve the completed historical layout grammar, manifest-v2 seed ownership, final PDF and hashes, frozen copy/assets, multiply blending, and economic-layer ownership of future advertising campaigns. gestalt_population: Gestalt Personas own shared population baselines; persistent member deltas own named individuality; WorldKernel materialization and approval-gated fission preserve lineage and identity. Materialisation/folding changes only representation and the exact member delta, advances resolution epoch, and retires the old cover; it has no Gestalt-wide effect authority. Selected actionable member exceptions are mandatory Projector perspective owners, so aggregation cannot silently erase their decision. Presence projection is limited to nearby active leaves and exact-location dormant members, includes reciprocal player relationship evidence, and may propose an earned cast without committing it. Simulation cells are derived covers only. diff --git a/tools/typeset_newspaper.py b/tools/typeset_newspaper.py index 414e50e..e733ecf 100644 --- a/tools/typeset_newspaper.py +++ b/tools/typeset_newspaper.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -"""Emit and compile a reproducible seed-varied TeX newspaper.""" +"""Emit a TeX newspaper with independent house-style and issue-flow seeds.""" from __future__ import annotations -import argparse, hashlib, html, random, re, shutil, subprocess, tempfile +import argparse, hashlib, html, os, random, re, shutil, subprocess, tempfile from dataclasses import asdict, dataclass from pathlib import Path @@ -18,7 +18,12 @@ class Edition: class Style: seed: int; width: float; height: float; margin: float; body: float; leading: float gutter: float; rule: float; masthead: int; lead: int; display: str; text: str - paper: str; ink: str; fleuron: str; lead_cut: float; inside_cut: float; rules: str + paper: str; ink: str; fleuron: str; rules: str + +@dataclass(frozen=True) +class Flow: + seed: int; template: str; inside_layout: str; page_one_secondary: int + lead_cut: float; inside_cut: float def plain(s: str) -> str: s=s.strip() @@ -53,15 +58,24 @@ def parse(path: Path) -> Edition: if not mast or not ed or not out: raise ValueError("source lacks masthead, edition, or articles") return Edition(mast,ed,tuple(out)) -def choose(seed: int) -> Style: +def choose_style(seed: int) -> Style: r=random.Random(seed) - return Style(seed,r.choice((9.8,10.0,10.2)),r.choice((13.7,14.0,14.3)),r.choice((.38,.42,.46)), - r.choice((8.6,8.8,9.0)),r.choice((1.04,1.07,1.10)),r.choice((9.,10.5,12.)), + return Style(seed,r.choice((9.6,9.8,10.0)),r.choice((10.7,10.9,11.1)),r.choice((.34,.38,.42)), + r.choice((7.7,7.9,8.1)),r.choice((1.03,1.05,1.07)),r.choice((8.,9.5,11.)), r.choice((0.,.24,.36)),r.choice((36,39,42)),r.choice((25,27,29)), r.choice(("Georgia","Times New Roman")),"Times New Roman", r.choice(("F2E5C7","F5E9CE","EFE1C2")),r.choice(("17120D","21170F","1A1510")), - r.choice(("diamond","cross","leaf")),r.choice((.78,.82,.86)),r.choice((.44,.50,.54)), - r.choice(("double","heavy-thin","triple"))) + r.choice(("diamond","cross","leaf")),r.choice(("double","heavy-thin","triple"))) + +def choose_flow(seed: int, e: Edition, cuts: dict[int, Path]) -> Flow: + r=random.Random(seed); remaining=list(range(1,len(e.articles))) + without_cuts=[index for index in remaining if index not in cuts] + secondary=r.choice(without_cuts or remaining) if remaining else -1 + template=r.choice(("display-plate","display-band")) + lead_cut=r.choice((.90,.97)) if template=="display-plate" else r.choice((.78,.82,.86)) + inside_layout=r.choice(("cut-first","cut-midstory")) + inside_cut=r.choice((.62,.70,.78)) if inside_layout=="cut-first" else r.choice((.44,.50,.56)) + return Flow(seed,template,inside_layout,secondary,lead_cut,inside_cut) def esc(s: str) -> str: m={"\\":r"\textbackslash{}","&":r"\&","%":r"\%","$":r"\$","#":r"\#","_":r"\_","{":r"\{","}":r"\}","~":r"\textasciitilde{}","^":r"\textasciicircum{}"} @@ -75,12 +89,17 @@ def inline(s: str) -> str: else: out.append(esc(p)) return "".join(out) -def article(a: Article, lead=False) -> str: - if lead: - return "\n\n".join(inline(p) for p in a.paragraphs) +def story_body(a: Article) -> str: + return "\n\n".join(inline(p) for p in a.paragraphs) + +def story_header(a: Article) -> str: sec=(r"{\sectionface\fontsize{7.2}{8}\selectfont\MakeUppercase{"+esc(a.section)+r"}}\par\smallskip " if a.section else "") head=r"\storyhead{"+esc(a.headline)+"}\n" - return sec+head+r"\deck{"+esc(a.deck)+"}\n"+r"\byline{"+esc(a.author)+"}\n\n"+"\n\n".join(inline(p) for p in a.paragraphs) + return (r"\Needspace{18\baselineskip}\noindent\begin{minipage}{\linewidth}\hrule height .45pt\smallskip "+sec+head+ + r"\deck{"+esc(a.deck)+"}\n"+r"\byline{"+esc(a.author)+r"}\end{minipage}\par\smallskip") + +def article(a: Article) -> str: + return story_header(a)+story_body(a) def rules(kind: str) -> str: return {"double":r"\hrule height 1.1pt\vspace{2pt}\hrule height .35pt","heavy-thin":r"\hrule height 1.7pt\vspace{1pt}\hrule height .25pt","triple":r"\hrule height .3pt\vspace{1pt}\hrule height 1.1pt\vspace{1pt}\hrule height .3pt"}[kind] @@ -91,18 +110,25 @@ def ornament(kind: str) -> str: def tex_path(p: Path) -> str: return p.resolve().as_posix() -def build(e: Edition, s: Style, cuts: list[Path], source: Path) -> str: - leadcut="" - if cuts: - leadcut=r"\noindent\includegraphics[width="+f"{s.lead_cut:.2f}"+r"\textwidth]{"+tex_path(cuts[0])+"}" - leadcut+=r"\par" +def press_cut(path: Path, width: float) -> str: + return (r"\begin{center}\begin{tikzpicture}\begin{scope}[blend mode=multiply]" + r"\node[inner sep=0] {\includegraphics[width="+f"{width:.2f}"+r"\linewidth]{"+tex_path(path)+r"}};" + r"\end{scope}\end{tikzpicture}\end{center}") + +def build(e: Edition, s: Style, f: Flow, cuts: dict[int, Path], source: Path) -> str: + lead=e.articles[0] + remaining=list(enumerate(e.articles[1:],1)) + rail=next((pair for pair in remaining if pair[0]==f.page_one_secondary),None) + paired=[pair for pair in remaining if pair != rail] + leadcut=press_cut(cuts[0],f.lead_cut) if 0 in cuts else "" + rail_text=(r"\par\medskip "+article(rail[1])) if rail else "" pre=rf"""\documentclass{{article}} \usepackage[paperwidth={s.width}in,paperheight={s.height}in,margin={s.margin}in]{{geometry}} -\usepackage{{fontspec,graphicx,xcolor,microtype,multicol,ragged2e,fancyhdr}} +\usepackage{{fontspec,graphicx,xcolor,microtype,multicol,ragged2e,fancyhdr,tikz,needspace}} \setmainfont{{{s.text}}}\newfontfamily\displayface{{{s.display}}}\newfontfamily\sectionface{{{s.display}}} \definecolor{{paper}}{{HTML}}{{{s.paper}}}\definecolor{{ink}}{{HTML}}{{{s.ink}}}\pagecolor{{paper}}\color{{ink}} \setlength{{\columnsep}}{{{s.gutter}pt}}\setlength{{\columnseprule}}{{{s.rule}pt}}\setlength{{\parindent}}{{1em}}\setlength{{\parskip}}{{0pt}}\setlength{{\emergencystretch}}{{1em}} -\pagestyle{{fancy}}\fancyhf{{}}\renewcommand{{\headrulewidth}}{{0pt}}\fancyfoot[C]{{\displayface\fontsize{{6.5}}{{7}}\selectfont {esc(e.masthead.upper())} · SEED {s.seed} · \thepage}} +\pagestyle{{fancy}}\fancyhf{{}}\renewcommand{{\headrulewidth}}{{0pt}}\fancyfoot[C]{{\displayface\fontsize{{6.5}}{{7}}\selectfont {esc(e.masthead.upper())} · HOUSE {s.seed} · FLOW {f.seed} · \thepage}} \newcommand{{\pressrules}}{{{rules(s.rules)}}}\newcommand{{\fleuron}}{{{ornament(s.fleuron)}}} \newcommand{{\masthead}}[1]{{{{\displayface\bfseries\fontsize{{{s.masthead}}}{{{s.masthead+2}}}\selectfont\centering\MakeUppercase{{#1}}\par}}}} \newcommand{{\leadhead}}[1]{{{{\displayface\bfseries\fontsize{{{s.lead}}}{{{s.lead+1.5}}}\selectfont\centering #1\par}}}} @@ -113,66 +139,93 @@ def build(e: Edition, s: Style, cuts: list[Path], source: Path) -> str: \begin{{document}}\thispagestyle{{fancy}} {{\displayface\fontsize{{7}}{{8}}\selectfont {esc(e.edition.upper())}\hfill SOURCE: {esc(source.name)}\par}}\smallskip\pressrules\smallskip \masthead{{{esc(e.masthead)}}}\smallskip\pressrules\medskip -\leadhead{{{esc(e.articles[0].headline)}}}\smallskip -{{\displayface\itshape\fontsize{{9.2}}{{10.5}}\selectfont\centering {esc(e.articles[0].deck)}\par}} -{{\displayface\bfseries\fontsize{{7}}{{8}}\selectfont\centering BY {esc(e.articles[0].author.upper())}\par}}\smallskip +\leadhead{{{esc(lead.headline)}}}\smallskip +{{\displayface\itshape\fontsize{{9.2}}{{10.5}}\selectfont\centering {esc(lead.deck)}\par}} +{{\displayface\bfseries\fontsize{{7}}{{8}}\selectfont\centering BY {esc(lead.author.upper())}\par}}\smallskip {leadcut}\smallskip -\begin{{multicols}}{{3}}{article(e.articles[0],True)}\end{{multicols}}\vfill\begin{{center}}\fleuron\end{{center}} +\begin{{multicols}}{{4}}{story_body(lead)} +{rail_text} +\end{{multicols}} +\vfill\begin{{center}}\fleuron\end{{center}} """ - pages=[pre]; rest=list(e.articles[1:]); page=2; ci=1 - while rest: - group,rest=rest[:3],rest[3:] - bottomcut="" - if ci str: return hashlib.sha256(path.read_bytes()).hexdigest() -def manifest(path: Path, src: Path, out: Path, tex: Path, cuts: list[Path], style: Style, engine: Path | None): +def verify_copy(e: Edition, rendered: str) -> None: + haystack=rendered.casefold(); missing=[] + for index,a in enumerate(e.articles): + fields=[("headline",esc(a.headline)),("deck",esc(a.deck)),("author",esc(a.author))] + fields += [(f"paragraph-{n}",inline(p)) for n,p in enumerate(a.paragraphs,1)] + missing += [(index,label) for label,value in fields if value.casefold() not in haystack] + if missing: raise ValueError(f"press projection omitted frozen copy: {missing}") + +def manifest(path: Path, src: Path, out: Path, tex: Path, cuts: dict[int, Path], style: Style, flow: Flow, engine: Path | None): tool=Path(__file__).resolve() - rows=['schema = "ghostlight.seeded_tex_press.v1"',f'source = "{src.resolve().as_posix()}"',f'source_sha256 = "{digest(src)}"',f'output = "{out.resolve().as_posix()}"',f'tex = "{tex.resolve().as_posix()}"',f'tex_sha256 = "{digest(tex)}"',f'tool = "{tool.as_posix()}"',f'tool_sha256 = "{digest(tool)}"',f"seed = {style.seed}"] + rows=['schema = "ghostlight.seeded_tex_press.v2"',f'source = "{src.resolve().as_posix()}"',f'source_sha256 = "{digest(src)}"',f'output = "{out.resolve().as_posix()}"',f'tex = "{tex.resolve().as_posix()}"',f'tex_sha256 = "{digest(tex)}"',f'tool = "{tool.as_posix()}"',f'tool_sha256 = "{digest(tool)}"',f"style_seed = {style.seed}",f"flow_seed = {flow.seed}"] if engine: rows += [f'engine = "{engine.resolve().as_posix()}"',f'engine_sha256 = "{digest(engine)}"'] if out.is_file(): rows.append(f'output_sha256 = "{digest(out)}"') rows += ["","[style]"] for k,v in asdict(style).items(): if k!="seed": rows.append(f'{k} = "{v}"' if isinstance(v,str) else f"{k} = {v}") + rows += ["","[flow]"] + for k,v in asdict(flow).items(): + if k!="seed": rows.append(f'{k} = "{v}"' if isinstance(v,str) else f"{k} = {v}") rows += ["","[[woodcuts]]"] if cuts else [] - for index,c in enumerate(cuts): - if index: rows.append("[[woodcuts]]") - rows += [f'path = "{c.resolve().as_posix()}"',f'sha256 = "{digest(c)}"'] + for position,(article_index,c) in enumerate(sorted(cuts.items())): + if position: rows.append("[[woodcuts]]") + rows += [f'article_index = {article_index}',f'path = "{c.resolve().as_posix()}"',f'sha256 = "{digest(c)}"'] path.parent.mkdir(parents=True,exist_ok=True); path.write_text("\n".join(rows)+"\n",encoding="utf-8") def compile(engine: Path, tex: Path, out: Path): out.parent.mkdir(parents=True,exist_ok=True) with tempfile.TemporaryDirectory(prefix="ghostlight-tex-") as td: - p=subprocess.run([str(engine),"-interaction=nonstopmode","-halt-on-error",f"-output-directory={td}",str(tex.resolve())],capture_output=True,text=True) + env=os.environ.copy(); env["SOURCE_DATE_EPOCH"]="0"; env["FORCE_SOURCE_DATE"]="1" + p=subprocess.run([str(engine),"-interaction=nonstopmode","-halt-on-error",f"-output-directory={td}",str(tex.resolve())],capture_output=True,text=True,env=env) built=Path(td)/(tex.stem+".pdf") if p.returncode or not built.exists(): raise RuntimeError((p.stdout+p.stderr)[-6000:]) shutil.copy2(built,out) def main(): - ap=argparse.ArgumentParser(description=__doc__); ap.add_argument("source",type=Path); ap.add_argument("output",type=Path); ap.add_argument("--seed",type=int,required=True); ap.add_argument("--woodcut",type=Path,action="append",default=[]); ap.add_argument("--tex-output",type=Path); ap.add_argument("--manifest",type=Path); ap.add_argument("--engine",type=Path); ap.add_argument("--no-compile",action="store_true"); a=ap.parse_args() - src=a.source.resolve(); out=a.output.resolve(); cuts=[p.resolve() for p in a.woodcut] - for p in [src,*cuts]: + ap=argparse.ArgumentParser(description=__doc__); ap.add_argument("source",type=Path); ap.add_argument("output",type=Path); ap.add_argument("--style-seed",type=int,required=True); ap.add_argument("--flow-seed",type=int,required=True); ap.add_argument("--woodcut",action="append",default=[],metavar="ARTICLE_INDEX=PATH"); ap.add_argument("--tex-output",type=Path); ap.add_argument("--manifest",type=Path); ap.add_argument("--engine",type=Path); ap.add_argument("--no-compile",action="store_true"); a=ap.parse_args() + src=a.source.resolve(); out=a.output.resolve(); cuts={} + for spec in a.woodcut: + try: raw_index,raw_path=spec.split("=",1); article_index=int(raw_index) + except ValueError as exc: raise ValueError("--woodcut requires ARTICLE_INDEX=PATH") from exc + if article_index in cuts: raise ValueError(f"duplicate woodcut for article {article_index}") + cuts[article_index]=Path(raw_path).resolve() + for p in [src,*cuts.values()]: if not p.is_file(): raise FileNotFoundError(p) - tx=(a.tex_output or Path("output/tex")/(out.stem+".tex")).resolve(); mf=(a.manifest or tx.with_suffix(".manifest.toml")).resolve(); st=choose(a.seed); ed=parse(src) - tx.parent.mkdir(parents=True,exist_ok=True); tx.write_text(build(ed,st,cuts,src),encoding="utf-8") + tx=(a.tex_output or Path("output/tex")/(out.stem+".tex")).resolve(); mf=(a.manifest or tx.with_suffix(".manifest.toml")).resolve(); ed=parse(src); st=choose_style(a.style_seed); fl=choose_flow(a.flow_seed,ed,cuts) + if any(index < 0 or index >= len(ed.articles) for index in cuts): raise ValueError("woodcut article index is outside the edition") + rendered=build(ed,st,fl,cuts,src); verify_copy(ed,rendered) + tx.parent.mkdir(parents=True,exist_ok=True); tx.write_text(rendered,encoding="utf-8") eng=None if not a.no_compile: eng=a.engine or Path(shutil.which("lualatex") or "") if not eng.is_file(): raise FileNotFoundError("LuaLaTeX not found; pass --engine") compile(eng,tx,out) - manifest(mf,src,out,tx,cuts,st,eng) - print(f"typeset={out} tex={tx} manifest={mf} seed={st.seed} articles={len(ed.articles)} pages={1+(len(ed.articles)-1+2)//3}") + manifest(mf,src,out,tx,cuts,st,fl,eng) + print(f"typeset={out} tex={tx} manifest={mf} style_seed={st.seed} flow_seed={fl.seed} articles={len(ed.articles)} pages={1+(max(0,len(ed.articles)-2)+1)//2}") if __name__=="__main__": main()