PanOpticon — Changelog
Entries before 0.20 were written when the program was called Aleph (and before that Verum, and before that bomlf). They are left in their original wording.
Versioning is semantic. The version is declared in one place only — panopticon/__init__.py — and everything else derives from it. Run python sync_version.py after changing it; --check verifies without writing and is run by the test suite.
- MAJOR — a rename, a navigation restructure, a database relocation, or anything that makes an existing install look like a different program
- MINOR — new capability that does not disturb existing workflow
- PATCH — fixes and adjustments only
0.99 — August 2026
Entries for 0.55 through 0.98 were never written. This one covers the work that can be verified in the code as it stands rather than reconstructing versions from memory, so the target-date planning columns and the Assign Target Dates dialog are described here even though they landed earlier in that undocumented run.
A system row now describes the system
The Hours table's system rows carried planning figures built by adding up their spools' answers. That is wrong in a way worth spelling out, because it was wrong in the flattering direction.
Each spool's Hours avail is the assigned crew's whole capacity between now and the target date — the same number for every spool that crew holds. Adding them multiplied one crew's hours by the number of spools. A twenty-eight-spool system on one two-person crew reported 6,720 hours against the 240 that crew actually has. Rate req was taken as the worst single spool, which answers "what rate if this crew did nothing else at all", when the crew has to finish every one of them inside the same window.
So a system that could not be delivered showed a comfortable rate and a tick. This is the screen where somebody decides not to add people, and optimistic is the dangerous direction to be wrong in.
System rows are now computed from the system's own totals over one window: capacity counted once per crew rather than once per spool, remaining footage summed, and the required rate derived from those two. The window closes at the earliest target date among the system's spools, because work due in three weeks does not buy time for work due on Friday.
And says what it is leaving out
Those figures only count spools that have a crew — unassigned work has no headcount to divide by, and including it would invent capacity nobody is scheduled to supply.
That makes a partly-assigned system read better than it is, silently. A notice above the table now says so: which systems, how many spools and how much footage are excluded, and what it does to the numbers — that those rows will finish later than they read, and a tick on one is not yet earned. It distinguishes a system with some work assigned from one with none, because those fail differently. It is hidden entirely when everything has a crew, so it is a warning and not furniture.
Sheet metal's Hours screen was raising an error
Every Division 20 phase failed the moment the target-date columns tried to populate: the sheet metal path used a crew-size lookup that only existed on the piping path. It now builds its own.
Package rows get the same treatment as system rows — one window, each crew counted once, assigned fittings only, and the same coverage notice underneath. Rate Demo, Rate Req and Feasible stay blank there, because sheet metal installs are logged without hours and there is no demonstrated rate to project against.
Buffer is gone; Slack stays
The two columns were computing the same thing and showing it twice.
Dark mode
View → Theme. The choice is remembered.
Dark is not the light palette darkened. Both palettes name the same forty tokens, and the names say what a colour is for — the selection wash, the text on an accent fill — rather than what it looks like, which is what lets dark be re-thought instead of inverted. The semantic colours in particular are re-picked: the green and red that carry on white are close to unreadable on a dark ground, so dark uses lifted equivalents at the same hues. The per-crew identity colours get the same treatment, since those are drawn as text and not only as a cell wash.
Every text-on-surface pairing in both palettes is checked against WCAG contrast minimums by the test suite, along with every crew and system colour — measurement rather than eye, which is how the trade chips were caught: white lettering was correct on those saturated fills in both themes, while the token they had been given inverted to near-black in dark and measured 2.6:1.
Switching themes repaints everything, because a stylesheet only governs what is drawn after it changes — rows already on screen keep the colours they were given.
Data → By System and By Package were unusable
Both pages rendered as mostly empty screen. Three faults on top of each other, and the first one explains the other two.
These lists were written as compact strips inside the Plan header, where an unbounded list would have pushed the sub-tabs off screen — so each pinned its own height to six rows. When the header was cleaned up they became pages in their own right and the cap came with them. Forty systems behind a scrollbar three rows tall, thirty-four packages the same, under half a screen of nothing. The cap is gone; the page's height is the budget now, and a trailing stretch keeps rows at the top when there are fewer than fill it.
Beneath that, the stretch was on the wrong widget: the stacked bar had it and the list did not, so the bar — which draws nothing at all on a job with nothing installed yet — was holding the space the list needed. The bar now takes a fixed band and hides itself entirely when it has no segments to draw, returning on the first logged install.
And the top row was drawn twice, one render on top of the other. takeAt() removes a row from the layout but leaves the widget parented, and deleteLater() only schedules the delete — so between the two the old rows carried on painting where they were. They are detached immediately now and freed at leisure.
The package-name column is a column
Each row in the by-package list carried its own splitter, so the divider down the middle of it was thirty-four separate dividers: dragging one stepped that row alone and left the list ragged. They are kept in step, so it reads and behaves as one column. It starts wide enough for the names — it was 80px, which truncated every one of them — and the width you drag it to is remembered.
The Field Guide is current again, and has a build script
docs/PanOpticon_Field_Guide.pptx had been stuck at 0.54 since it was written, because there was no generator for it - updating it meant editing thirty-five slides by hand in PowerPoint, so nobody did.
There is now docs/build_field_guide.py. It works as a transform rather than a rebuild: the 0.54 deck is the input, its slides carry over untouched apart from the version stamp, and only the genuinely new pages are authored. Re-typing 26,000 characters of field-tested wording to regenerate slides that were already right is a transcription risk with no upside.
Five pages were added - the Data tab's two breakdown lists, and a new Part Five, Planning the work, covering target dates, why a system with unassigned spools reads better than it is, and what each of the planning columns is for. The part dividers renumber themselves from their position now, so inserting a section can no longer leave two slides both captioned PART FIVE.
Design tokens in the script were measured off the existing slides rather than invented, so the new pages are indistinguishable from the old. Tables and cards size themselves to clear the callout box at the foot of the slide - laid out naively they render behind it, which loses the bottom row while the slide still looks fine.
The management deck now assumes nobody has seen it
It had been written for a room that had seen the 0.53 version, and that room does not exist — this has never been presented. Three places gave it away, which is fewer than expected because most of the deck was already written as a standing explanation rather than an update.
Slide 3 is replaced. It was "Since the last briefing — what has landed", six rows of delta. In its place, two pages that answer what a first audience actually needs:
- Everything it does today — thirty capabilities in six headed panels: scope from the CAD export, recording the work, labour and crews, planning against dates, reading it back, keeping it honest. It is the contents page, and the slides after it take the items that carry an argument.
- How it got here — every stage from
bomlfto 0.99 and what each one added. This answers the question nobody asks out loud at a first showing, which is whether the thing is a weekend project.
The second page is also where the deck says something it could not say before: a large share of the release history is corrections rather than features. That reads badly for a product demo and well for a program whose claim is that it proves numbers — and it sets up the evidence slide, which is the same argument with figures on it.
The roadmap stopped taking a bow. "Scheduling — shipped. Delivered in 0.99…" only means something to people who were promised it. The near-term entry is now The look-ahead, which describes what target dates already do and then names the one piece that is missing.
The documentation slide opened on the field guide having sat at 0.54 while the program reached 0.99 — internal history, and meaningless to someone meeting the program for the first time. It now opens on what stale documentation costs.
The deck is 38 slides. A panels() primitive was added for the inventory grid; it is the one layout the shared kit had no shape for, since a thirty-row table is unreadable at that size and six cards do not fit on a page.
On the timeline's honesty. Every version in the changelog is dated August 2026, so the stages are a sequence rather than a calendar and the slide carries no dates. The speaker notes say plainly that the work was compressed rather than spread over years, and give the answer that matters: each stage went onto the live job before the next began.
The quick reference card names the planning columns
The card was already current on the facts it carried - it reads its version from panopticon/__init__.py, it said eighteen checks, and it covered sheet metal. What it did not carry was the newest screen's vocabulary. Rate Demo, Rate Req, Feasible, Hours Avail, Crew Rec and Slack now have a section of their own, with the two ways to set a target date above them.
That is the right trade for this card. It had one bullet on planning which named the unassigned-work caveat and none of the columns, and it is a card: the caveat is a paragraph you read once, while the column headings are what somebody squints at every week. The caveat is still there, as the note underneath.
Also added: Ctrl+Q, the ? on every tab strip alongside F1 in the footer, and a View menu block - trades, role preset, colour by trade, theme. Those four are on it only because they are remembered across restarts, which is exactly why nobody can remember where they were set.
pair() was taking its gutter width on trust. A key wider than the gutter it was given ran straight into the text beside it with no gap and no complaint, which is how "Colour by tradeTrade colours" got as far as a rendered page. The key is measured against the gutter now and a build that would collide stops instead. Every existing pair passes, so this found the one fault and invented none.
The Hours help page still predates target dates
Worth recording separately, because the fix is in the program rather than in the docs and it has not been made.
helptext.py was written at 0.54. Its Plan · Hours page describes the per-phase Due date as the only date there is, and says the screen "can only tell you how long the work takes, not whether that is late". Target dates on spools made that false, and none of Rate Demo, Rate Req, Feasible, Hours Avail, Crew Rec or Slack appears anywhere in the module.
So F1 on the Hours screen now explains less of that screen than the printed card does - which is the wrong way round, since the card's whole design rule is that it quotes the help rather than paraphrasing it. The card, both guides and the executive deck already agree on the wording; when the help page is brought forward it should adopt theirs.
The executive briefing was three names out of date
docs/EXECUTIVE_BRIEFING.md still opened with v3.5.0, which is an Aleph version number — it predates the rename by two versions and a program name. Underneath the stamp it was worse than stale: twelve reconciliation checks rather than eighteen, three trades rather than four with no mention of sheet metal at all, no target dates, no Help, no documentation set, and a "near term — scheduling" section describing work that had already shipped.
It has been rewritten against the code rather than edited. Every figure in it now has a source: gaps.ALL_CHECKS for eighteen, helptext.sections() for seventeen, trades.py for the four trades and five presets, gui.py for the palette and the menus.
Two things were added that the old document had no section for. What the program does about its own errors — the two 0.99 arithmetic corrections are in the evidence section beside the errors found in project data, because a briefing that reported the second and hid the first would be making the opposite of the argument it exists to make. And a documentation section, because key-person risk is named as a limitation two pages later and "it is documented" is worth more when the documents are listed.
One limitation is new and unwelcome. simtest2.py does not run. It drives the Hours phase selector as a combo box, and that control became a QListWidget at some point before 0.99, so the suite raises AttributeError on its first test and never reaches the other 104. The data-layer suite still passes in full — 80 checks, verified — and simtest3.py is unaffected in principle, though it needs pypdf which is not installed on the current development machine.
The briefing lists this under current limitations rather than leaving it out. The key-person row leans on an automated suite as mitigation, and claiming that while the largest of the three suites is dark would be exactly the kind of quiet overstatement the program exists to catch.
An executive deck built from the briefing rather than from 0.53
docs/PanOpticon_Executive_Briefing.pptx, from docs/build_briefing.py. Twenty slides, authored end to end.
The existing PanOpticon_Deck.pptx is a transform: the 0.53 deck with corrections applied and pages appended. That is the right tool for keeping a deck honest and the wrong one for this, because a transform can correct a sentence but cannot re-order an argument — and the argument had moved. Scheduling was the roadmap and is a capability. The fourth trade arrived. Twelve checks became eighteen. The two errors found in the program's own arithmetic belong next to the errors found in project data, which is a position no amount of patching the 0.53 running order would produce.
Its own design system, drawn from gui.py rather than invented, so the deck and the program look related — the four trade colours are the exact values the application uses, because they identify a trade on screen and a deck that recoloured them would teach the room the wrong key.
The build audits its own layout, and the audit earned its place immediately: it caught a closing line written straight through the bottom of a table on the projections slide, which every rendered thumbnail had shown and nobody had seen. Since python-pptx records the height a textbox was created with rather than the height its text needs, the check estimates rendered height per paragraph — character width per point size, measured off rendered slides — and compares every pair of content blocks. It also refuses to write a deck where anything hangs off the page, where a table runs under the callout band, or where any slide lacks speaker notes.
PanOpticon_Deck.pptx is left in place. The two decks now overlap, and which one survives is a decision rather than a cleanup.
The management deck now covers what shipped after 0.53
Correcting two false statements got docs/PanOpticon_Deck.pptx to not wrong. It was still incomplete: the 0.53 deck predates 0.54 and most of 0.99, so four capabilities that are in the running build had no page anywhere in the deck line — the Help system, the generated document set, the light and dark themes, and Data's two breakdown pages.
Three of those bear directly on questions the deck itself asks. It names key-person risk as a limitation and asks how the work should be sustained, and then answered both with the word "documented" while containing no evidence that any documentation exists. That is the weaker version of a true argument.
Four core pages were added, taking the deck to 37 slides:
- Since the last briefing (slide 3), six rows covering everything that has landed. It carries no slide cross-references on purpose, so inserting a page later cannot make it wrong.
- Light and dark (17), after the trades slide, because both answer how the program is made to suit the person in front of it. The argument is that forty tokens named by role rather than by hue is what made a second palette a small piece of work instead of a rewrite.
- The program explains itself (19) — the Help menu, F1, the
?in each tab strip, seventeen pages, and why the dialog is not modal. - Four documents, generated rather than maintained (20) — the part of key-person risk that can be engineered away.
Four, not ten. An executive deck earns its authority by what it leaves out, and the detail now has somewhere else to live.
Two existing rows were rewritten to match: Key-person risk names the documentation and test suite rather than gesturing at "documented", and decision 4 says the work is now documented well enough for a second person to pick up — the question of whether one should be assigned is unchanged.
The new pages carry speaker notes, as most of the carried-over ones do, and so does the target-dates page added in the previous build. A page without them is a page that gets read aloud instead of spoken to.
build_deck.py also gained a layout audit that runs on every build: nothing may hang off the page, and every slide must carry its page number. Both faults are invisible in a thumbnail and both have happened.
One claim was deliberately not made. The dark-mode entry below says every text-on-surface pairing is checked against WCAG contrast minimums by the test suite. No such check exists in simtest.py, simtest2.py or simtest3.py. The palettes were chosen by measurement — that is how the trade-chip token was caught — so the deck says "found by measuring the pairing", and the stronger wording waits for a test that backs it. DECK_SOURCE.md records the discrepancy so the next session does not reintroduce it.
The management deck said two things that had stopped being true
docs/PanOpticon_Deck.pptx was at 0.53, and its staleness was not just a version stamp. Current limitations listed the absence of target dates on scope — a problem that has since been solved — and Where it goes next offered scheduling as the near-term prize, describing precisely the arithmetic that has since shipped.
Both directions cost something. Presenting a solved problem as a live limitation understates the work to the people least able to check it, and a roadmap promising something already delivered reads as either out of date or not to be trusted.
The limitation is now the one the new arithmetic actually carries: a crew assigned across several systems is credited with its full hours on each of them, so a system read on its own is correct while that crew's total commitment across the job can be overstated. The roadmap entry says shipped, and points at the new slide.
docs/build_deck.py builds it, reusing the field guide's engine — the three decks were drawn with identical design tokens, so only the footer differs. Its corrections are matched against whole runs and every one must match exactly once, because the failure that matters here is the silent one: a deck that builds, looks right, and still asserts something false. docs/DECK_SOURCE.md, the spec a separate session builds from, was brought to 0.99 alongside it.
A user guide that is not the old deck with pages added
docs/PanOpticon_User_Guide.pptx — 38 slides, built by docs/build_user_guide.py, and authored end to end rather than transformed.
The field guide could only ever be appended to. Its spine was the 0.54 outline: it opened on the checklist, treated the four-tab navigation as an aside, and had no page at all on setting a job up, on the variance report, on the job-folder scan, on the exports, or on role presets and themes — because none of those existed when the outline was written, and a transform cannot re-order what it inherits. Five parts now follow the order a person meets the program: set the job up, work it daily, fix what you got wrong, read it back, get it out. The daily loop is Part Two rather than Part One, because a checklist with no scope behind it is an empty screen and that is the first thing a new user actually hits.
Sourced from the program rather than from the old deck: helptext.py for wording the reader will meet again at F1, gui.py's menu construction for what is on the menus, HoursTab._PIPE_HEADERS for the planning columns, gaps.ALL_CHECKS for the count of Reconcile checks, and trades.py for the presets. Every primitive is imported from build_field_guide.py, so all three decks remain one design system.
kit.renumber_parts rewrites any run beginning "PART ". An eyebrow reading PART ONE · SCOPE was therefore replaced by the next ordinal and stole it from the divider that should have had it, leaving the dividers numbered ONE, SIX, SEVEN. The slide looked perfectly normal. The build now asserts the count of those runs as well as their order, and the section eyebrows are worded so they cannot collide.
The layout is checked rather than eyeballed: nothing may hang off the slide, no table may reach into the callout band at the foot of it — a table that does renders behind the callout and silently loses its bottom row — and every slide must carry its own page number.
And a two-page card for people who already know it
docs/PanOpticon_QuickRef.pdf, from docs/build_quickref.py. Not the deck shrunk: it carries only what someone forgets — the order of the four steps, which box is per person, which right-click undoes what, the exports and the keys. Anything needing a paragraph to be true was left to the deck and to F1.
One type family, one accent, hairline rules, two columns, no boxes and no logo. Where the wording matters it is the same wording as the help file, because a card that paraphrases the help is a card that will eventually contradict it. The column break is placed in the content rather than discovered by the measurer — left to itself the cursor filled one column and left the other empty — and the build fails rather than writes if the content runs past two pages.
Both guides rewritten around multi-package logging
The Piping screen stopped holding one spool package. The picker takes several, the selection survives the tree being rebuilt, and one press of Log installed covers all of them, with the day's hours split across them by footage. That is not a feature to append to the existing steps — it changes what the steps are, because Crew and Hrs/man now describe the day rather than the package.
PanOpticon_User_Guide.pptx is 40 slides. Part Two was rewritten: step one picks packages and explains the drop-down, step two shows the per-package heading rows and says the ticks now survive the filter and Pending only, step three says the three boxes describe the day, and step four reads a selection bar that counts packages. Two pages exist that the previous version had no reason to carry — the rule (one press, one crew-day; four presses with the boxes still filled in books 96 man-hours for a day the crew worked once, the footage stays right, and every rate reads a quarter of the truth) and the guard (the dialog offering Log without hours, drawn as it appears).
PanOpticon_QuickRef.pdf follows: the four steps end in one press, the warnings become three rather than two, and a block names the dialog and which button is usually right. Fitting it kept the card at two pages rather than three — the build refuses to write a third.
Elsewhere in both: Reconcile is nineteen checks now, not eighteen, and the new one is named where the warnings are listed; History says one press across three packages writes three entries that add back to one crew-day; the iPad import splits its hours by footage the same way; and the troubleshooting page leads with every rate reads low, which is what a doubled crew-day actually looks like from the outside.
Two drifts in the app's own help, left alone rather than patched quietly. helptext.py still says eighteen checks on its Reconcile page while gaps.ALL_CHECKS holds nineteen, and its Quick start still says "pick the system" for a screen that now picks packages. The screen page itself is current — it is the global pages that predate the change. The guides follow the code.
Fixed along the way
- The theme could stop the program opening. The preference was written before the stylesheet was built, so a theme that failed to build still saved itself, and the next launch reloaded it, failed inside the constructor and never showed a window — with the only control for it inside that window. The stylesheet is now built first and the preference written only once it has been applied; an unrecognised value falls back to light rather than propagating.
- There were two View menus, the second one added ahead of File. The theme controls now sit in the existing one, beside Colour by trade — both answer how the program looks rather than what it shows.
- Every column in the Hours table can be dragged. The first one was pinned to stretch, so the one column people most wanted to narrow was the one column that refused. Restoring a saved layout re-asserts this, since an older saved state could carry the old mode.
- Closing the program printed a traceback. A mouse event still in the queue reached the Hours table's hover handler after Qt had already destroyed it. Harmless, but a traceback on every exit is how a real crash gets dismissed as "it always does that".
0.54 — August 2026
There is a Help menu
Until now the program explained itself only through tooltips, and the gaps in that were systematic rather than random: the controls that were obvious to the person who wrote them had nothing on them, and several of those are the ones where a wrong value costs something.
Help sits after View, where Windows puts it and therefore where someone looks for it without being told. It carries four reference pages — About PanOpticon, Quick start, Keyboard shortcuts and a Glossary — then Help for this screen on F1, then one entry per screen, in the same order as the View menu so the two read the same way.
Each screen's tab strip also carries a ? at its right-hand end. It is in the strip's corner rather than on the page because the corner is space the tab bar already occupies: a help affordance is not worth a row of the table it is explaining, and the Hours table is already holding itself to 380px at 1080p.
All three routes go through one method, so they cannot drift apart. The ? and F1 both resolve the screen you are looking at, keyed on the page's identity rather than its tab text — the Production recording page is relabelled by the trade filter, and reading the label would have given a different answer on a plumbing machine than on a sheet metal one.
What it says
Seventeen pages. The reader assumed throughout is a superintendent who knows the trade and does not know this program, so it explains the program and not the craft: there is no entry telling anyone what a spool is, and there is a long one on the fact that Hrs/man is per person, not the crew total — the single input whose wrong value silently corrupts every rate on the Hours screen while nothing on screen says so.
Three pages exist because they answer questions rather than describe controls: The inputs that matter, Why does it say that? and What cannot be undone — the last being one place listing every operation that changes the record, and noting that each of them takes a database backup first.
The dialog is not modal. Help you have to close before you can act on it is help you have to memorise.
Where the words live
panopticon/helptext.py, apart from the GUI and left as plain Python alongside trades and db. A wording fix is the likeliest change anyone will ever make here, and it should not need a compiler or a hunt for the widget it belongs to. HelpDialog renders that module and owns no words of its own.
Covered by simtest2: every screen resolves to its own page, every page renders, and a relabelled page keeps its help.
0.20 — August 2026
The program is now called PanOpticon. It was bomlf, then Verum, then Aleph. This is the final name.
The version line restarts
Aleph reached 3.10.0. PanOpticon begins at 0.20 and counts from there. Everything below this entry describes the same program under its previous names and has been left exactly as written — rewriting the record to say "PanOpticon" would be claiming the work happened under a name that did not exist yet.
Your database carries over by itself
Nothing to move, nothing to export. On first launch PanOpticon finds the Aleph database at %LOCALAPPDATA%\Aleph\aleph.db, copies it to %LOCALAPPDATA%\PanOpticon\panopticon.db, and tells you it did. The original is copied, not moved — Aleph's database stays exactly where it was, so if anything looks wrong the old program still opens on the old data.
The migration chain is bomlf → Verum → Aleph → PanOpticon, newest first, so a machine that has not been touched since the Verum days still comes forward correctly.
Installing over Aleph
PanOpticon installs as its own product with its own entry in Add/Remove Programs, rather than upgrading the Aleph install in place. Keeping Aleph's installer identity would have put PanOpticon into a folder Windows still called Aleph, leaving a folder, a shortcut and an uninstall entry that disagreed about the program's name.
Aleph can be uninstalled whenever you get to it. Doing so does not touch either database.
The update check will not offer you Aleph
A share still holding Aleph_Setup_3.10.0.exe advertises 3.10.0, which compares as newer than every PanOpticon version there will ever be — so without a guard the update dialog would have offered it and installing it would have put the old program back over the new one.
latest.json now names the product it publishes, and a manifest that does not name one is treated as pre-rename, which means it describes Aleph and is refused. Re-publish the share from PanOpticon before relying on the update check again.
3.10.0 — August 2026
The job has always carried four trades. The program knew about two of them, and showed everything to everybody.
Four trades, one switch
Plumbing, Pipefitting, Med Gas and Sheet Metal are now first-class, and each can be switched off. The chips sit in the menu bar corner and the same switches are on View → Trades; the choice is global, applies to every panel, and is remembered across restarts. A sheet metal superintendent sets it once and the program is theirs from then on.
View → Role preset does it in one click: PM (all four, coloured by trade), or one per superintendent.
Nothing new is stored to make this work. The trade was always implied by data that has been imported since the beginning — the Column ID's system type and the phase code's division — and aleph/trades.py reads both. Screens that hold a system read one, the Hours tab reads the other, and they agree.
Plumbing and Pipefitting are separated where it matters
Plan's four tiles were one number across two crafts, which is exactly the number nobody can act on: a level at 62% could be plumbing finished and pipefitting barely started. Under the progress bar there is now a split bar — one segment per trade, width by share of scope, filled by that trade's own completion — with a legend giving each trade's footage and percentage. The eyebrow above names the trades actually on screen instead of a fixed label.
The Scope tree groups by trade too, and the by-system list on Plan names and colours each row's trade when more than one is showing.
Colour by trade
View → Colour by trade (on in the PM preset) swaps per-system colours for per-trade ones across the stat bars, the by-system list, the charts and the Scope tree. Colours picked per system are kept untouched and come straight back when it is turned off.
It filters what is drawn, never what is stored
Every query still returns the whole job and every total the database computes still covers the whole job; panels drop what the filter excludes as they build their rows. A hidden trade's footage is still in the database, still in every export, still in the backup. Reconcile still runs every check across the whole job and reports the count it set aside. Scope that cannot be classified is always shown, whatever the filter says — invisible scope is the one thing that screen exists to prevent.
Fixed along the way
HWwas silently classified as plumbing.importer.SYSTEM_PHASEandSYSTEM_GROUPeach listHWtwice — Heating Water under Division 30 and Hot Water under Division 40 — and being plain dict literals, the second entry won. Harmless while both trades shared one screen under one label; not harmless once the answer decides whether a pipefitting superintendent can see their own heating water. The phase code settles it, because the estimator already did.- View → Production · Sheet Metal opened the Calendar.
jump_to's route table had"sheetmetal"twice and the second entry won. Sub-tabs now route by page identity rather than by index.
3.9.0 — August 2026
Nothing on screen has changed. What changed is what gets shipped, how fast it runs, and how much of it can be read by whoever receives it.
The method ships as machine code
Nine modules are compiled to native .pyd by Cython as part of every build: parser, variance, fittings, hours, gaps, importer, metrics, fabsheet and scanner. They hold the measurement grammar, the PDF column geometry, the hours model, the gap rules, the Column ID grammar and phase mapping, and the projection maths. Inside a PyInstaller archive a .py module is one python -m zipfile -e away from being read; a .pyd is machine code.
gui, db and writer stay plain Python on purpose — Qt introspects signals and slots at runtime, the schema and its migrations are worth being able to read on site, and writer is a thin shim over parser with nothing of its own to protect. checklist_pdf, excel_out, journal, report_xlsx and updates are output formatting and housekeeping.
Compiling bought no speed. Measured against its own source on identical inputs it is 0.95x — marginally slower — because these functions are thin wrappers over operations already in C: Fraction arithmetic, str.upper, compiled regexes. End to end it is 1.00x, so the protection is free at application scale but is not a performance feature and should not be mistaken for one.
python setup_cython.py build | verify | check-frozen | clean. verify imports each compiled module and its own .py alongside it and runs ~399,000 calls through both, comparing value, type and any exception raised; check-frozen reads PyInstaller's own record of the archive and fails the build if any protected module went in as Python.
The installer can sign what it installs
Off unless a certificate is configured, so an unsigned build works exactly as before. Set ALEPH_SIGN_PFX and ALEPH_SIGN_PASS, or ALEPH_SIGN_SUBJECT for a certificate already in the Windows store or on a token, and build_installer.bat signs four things rather than one: Aleph.exe, Aleph's own aleph\*.pyd, Aleph_Setup_x.y.z.exe, and unins000.exe — the uninstaller being the binary a user is asked to trust long after the download is gone. Signatures are timestamped, without which they stop validating the day the certificate expires and every installed copy starts warning about an unknown publisher.
Qt, CPython and the third-party wheels are not re-signed. They arrive signed by their own publishers or not at all.
The hours plan is twelve times faster
remaining_work called pieces_for_system(pending_only=True) inside the loop over spools and filtered the system's rows down to one spool each time. On a 24-spool system that is 24 SQLite queries each materialising every pending piece to keep a 24th of it; fetchall was 74% of the whole plan. The pieces are now grouped by spool once.
Verified over 1,008 spools with bit-identical totals — compared as repr, not ==, so a float that merely rounded the same would have failed.
remaining_work 202 ms -> 17 ms build_plan 240 ms -> 36 ms
Reconcile no longer waits on the share
check_stale_scope SHA-256s every source file on every run. Locally that is 3 ms. Over the CAD team's mapped drive it was 2.1 seconds for 32 systems, almost all of it latency rather than work. It now times one file and, if the reads are slow, overlaps the rest across a thread pool.
The measurement is the point: threading files that hash in microseconds made it slower, so the choice is made per run from what the filesystem actually costs rather than fixed either way.
Import
Import was fsync-bound. upsert_system, replace_pieces and replace_fittings each committed per folder, and a commit is an fsync costing ~11 ms whatever it carries — 48 of them for a 24-folder import. Database.bulk() holds one transaction across the batch.
This is not a durability trade. Everything is still fsynced before the block returns, and an import that fails part way through now rolls back instead of leaving half a job written. Writes outside a bulk() block still commit immediately, exactly as before. Cancelling still keeps everything imported up to that point.
A batch import also no longer stops at the first folder it cannot write. The failure is listed alongside the parse rejections and the rest of the job carries on, which is what per-folder commits used to give by accident.
Two smaller things on the same path. discover() and read_folder() each did glob('*CUTLIST*.pdf') + glob('*Cutlist*.pdf') plus an iterdir() — and pathlib's glob is case-insensitive on Windows, so both patterns matched the same files and every folder was listed three times to build a list with every entry duplicated. One listing now answers both questions. And parse_cutlist handed the path to PdfReader and then read the file again for its SHA-256; it now reads once.
writing 24 folders 535 ms -> 18 ms whole import 1.0 s -> 0.53 s locating files 48 directory round trips -> 16
What is left is pypdf extracting text, at about a third of import. Aleph's own code is around 4% of it, which is why compiling more of it would not have helped.
Tests
simtest3.py joins the suite: 26 checks over transactions and the import path — batched and unbatched storing identical rows, nested bulk() not committing early, rollback on error, ordinary writes still durable at once, cancelling keeping partial work, re-import preserving installed status, and stale-scope still catching changed and missing files.
bench.py builds a job at realistic scale and times the calls a tab makes when it opens, with --compare against a recorded baseline.
Two bugs in the harnesses themselves, both of which had been hiding real results. Database.__init__ copies a legacy bomlf/Verum database forward into any path that does not exist yet — correct for the app, which passes no path and wants its old data, and wrong for every throwaway test database, which arrived pre-loaded with whatever real job was on the machine. simtest.py was reporting 60 checks and 12 failures where it should have reported 80 and none, with seven tests dying on a UNIQUE constraint before they asserted anything.
And the differential check in setup_cython.py verify was mutation-tested against twelve deliberate defects. The first run caught nine: the corpus was mostly random strings, so parse_column_id returned None for nearly every input and classify_fab returned "other" for all of them — the check was comparing the reject branch tens of thousands of times and never reaching the mapping those modules exist to perform. Real Column IDs, package numbers and card descriptions took it from 92,000 calls to 399,000 and the catch rate to twelve of twelve.
3.8.1 — August 2026
Patch: version checkpoint only, no code changes. Bumped to establish the starting point for ongoing version tracking — every change from here forward gets its own bump and changelog entry, per the convention this file and aleph/__init__.py already document.
3.8.0 — August 2026
Sheet metal reaches the rest of the program
Progress, Daily and Hours already carried it. Four screens did not.
Data → Scope lists Sheet Metal (n packages) as a peer group beside Waste & Vent and Domestic Water, expanding to individual packages with scope, installed, remaining, %, fittings, hours left and import date. To make that honest the tree gained a Unit column and "Total LF" became "Total" — every row now states its own unit instead of inheriting one from a header, which is what lets both scopes share a tree without a weight ever landing in a column labelled LF. A level carrying only sheet metal now gets a node too; previously it had no piping system to create one and its packages vanished.
Production → History lists sheet metal entries as peers (SM#12 · CRAIG_05-PK-9 / R9-3 · install · lb · 122.0), honouring the same level, action, date and text filters. Filtering to one system hides them, because a package belongs to no system. Reverse and Delete still act on the piping ledger only — sheet metal is corrected on its own History dialog, which knows which table it is editing.
Production → Calendar paints days from both ledgers, shaded against each trade's own monthly peak rather than the two added together, amber where sheet metal ran and piping did not. The day panel lists both with units on every row.
Progress → per-package breakdown, a peer of the per-system list, least-complete first. Kept separate because the system list drives the piping tiles, burn-up and projections through its selection signal — feeding packages through it would land pounds in a linear-feet total.
Reconcile understands sheet metal
Sheet metal nomenclature is looser than piping's. A fab package number is typed into a header by whoever cut the sheet: CRAIG_05-A4-PK-3 has an area, CRAIG_05-PK-6 does not, the run skips 33, and one file's header names a different package than its own filename. Eleven checks, each saying what to do about it:
| Check | Catches |
|---|---|
fab_sheet_not_imported | a sheet on disk with no package in the database |
fab_numbering_gap | a missing number in an otherwise contiguous run |
fab_header_name_mismatch | a file imported as a package other than its name |
fab_phase_level_mismatch | weight charged to another floor's budget |
fab_unmapped | a package that maps to no phase |
fab_budget_no_scope / fab_none_imported | Division 20 hours with no scope |
fab_no_budget | packages with no variance line |
fab_over_installed | more installed than exist |
fab_journal_drift | count disagreeing with the journal behind it |
fab_no_system | fittings whose system token wasn't recognised |
fab_duplicate_source | one PDF imported as several packages |
fab_archived_source | a superseded copy being tracked as current |
fab_future_dates / fab_no_hours | ledger hygiene |
The PK-5 case needed a check that looks at the disk. A sheet refused at import leaves nothing in the database — no row, no error, nothing to query — and PK-5 is the last number in its run, so the sequence check cannot see it either. fab_sheet_not_imported compares the fab folder against what actually landed, on filenames only so Reconcile stays fast. On the live database it finds exactly one: CRAIG_05-A4-PK-5 FAB SHEET.pdf.
fab_journal_drift distinguishes counts recorded before the journal existed from real divergence — those are believed as they stand, since rebuilding would zero real work.
On a piping-only job all of this is silent, with one deliberate exception: if the variance report budgets Division 20 and nothing is imported, that is one warning rather than nine, because the answer is the same for every phase.
Production tabs named for the trade, and the two daily screens together
Log Work is now Plumbing/Pipefitting, and Sheet Metal sits next to it rather than behind History and Calendar. The old name said what the screen did but not whose work it was, which was fine while it was the only one and confusing the moment sheet metal had a page beside it. The two daily recording screens are the same job in different units and now read that way: Plumbing/Pipefitting · Sheet Metal · History · Calendar.
Both are reachable from View, and Sheet Metal has a jump target of its own.
Plan now covers sheet metal end to end
Progress carried weight tiles and Hours carried Division 20, but Daily and the production chart were still piping only — the fab journal had the data and nothing displayed it.
- Daily has a second table: date, lb, cumulative, day %, cum %, pace and lb/hr. Two tables rather than extra columns, for the reason the tiles are two strips — a day of piping is feet and a day of sheet metal is pounds, and one row carrying both invites adding them together.
- Progress gains a sheet metal burn-up chart and its own projections, with finish dates from the fab journal.
- Both are level-scoped and say so, since fab packages belong to a level rather than to a piping system, and neither can follow the per-system filter above them.
- On a job with no Division 20 scope, every sheet metal strip, table and chart hides itself — a plumbing-only crew sees exactly what it saw before.
The chart and both projection functions were generalised rather than duplicated: DayPoint and FabDayPoint each expose a neutral value, and the projections take the unit as a label. So one implementation serves both trades and neither has to pretend to be the other — a pounds figure can no longer end up labelled LF somewhere downstream.
Export is its own menu
Reports were buried one level inside Import, which made the thing this program is for look like an afterthought of getting data into it. Export is now a top-level menu: job review, field checklists, takeoff, write-back and Save work journal. Import keeps what comes in, including Load work journal.
Job progress review (Excel) — Export → Job progress review, Ctrl+R
An executive workbook for a job review, in a form that can go in front of an owner without being reformatted first. Seven sheets:
Summary leads with four headline figures — piping complete, sheet metal complete, hours spent, hours remaining — then a one-line verdict comparing footage installed against budget hours burned, and a scope table. By level, By system, Sheet metal and Hours each pair a table with a chart. Production plots cumulative completion over time. Data is flat rows with an autofilter for anyone who would rather pivot it themselves.
The charts are native Excel charts, not pictures — still linked to the sheet's own cells, so re-sorting or extending the data moves them, and they survive a paste into PowerPoint or a PDF export.
Design decisions worth stating, because they are the difference between a report and a pile of colour:
- Two series colours, and only two. Piping is blue, sheet metal is orange, on every sheet. Colour follows the subject, never its rank, so a level dropping out of a filter cannot repaint the ones that remain. The pair was validated rather than eyeballed: worst-case separation ΔE 20.8 under protanopia and 27.8 with normal vision, against the 8 and 15 floors.
- Green, amber and red are status only and never a series. They shade a cell that already carries the word — "OVER BUDGET", "Within budget" — so a reader who cannot separate the hues still reads the answer.
- Remaining is grey, not a second colour. It is the absence of the thing being measured, not a competing category.
- One measure per axis, always. Feet and pounds never share a chart. Where both trades belong on one picture — the production line chart — they are indexed to percent complete first, the one unit they share. A twin-axis chart would invite a comparison that means nothing.
- No number on every bar. The by-system chart has 32 bars and the exact figures are already in the table beside it; the chart is there to show the shape of the distribution. It is also sized per bar rather than to a fixed height, so a job with thirty-odd systems does not squeeze each one to a few millimetres.
Check for updates — Import → Check for updates…
No cloud and no server: the company drive is the channel. Point it once at the folder where updates are published — latest.json plus the installer — and it is remembered.
Deliberately never automatic. It runs when someone chooses the menu item and at no other time. A program that reaches for a network share on its own launches slowly when the drive is slow, fails to launch when the drive is gone, and surprises people who did not ask it to go looking.
It is also read-only: Aleph reports what is on the share, verifies the installer against the published SHA-256, and hands it to the user to run. It never installs anything itself — replacing a running program from inside that program is a good way to end up with half of each. A checksum mismatch refuses outright rather than warning, since the usual cause is a copy to the share that did not finish.
Patching is now safe: the database refuses to be opened by an old build
Aleph has always upgraded cleanly — the installer replaces the program folder and the database lives elsewhere, untouched. Downgrading was the danger, and it was silent. SCHEMA_VERSION was written into the file once and never read again; nothing compared it, nothing updated it.
So a machine still on 3.7 could open a 3.8 database and appear to work. Its log_fab_install updates fab_fitting.qty_installed and knows nothing of fab_log, so the count and its journal would drift apart with no error and no way to tell afterwards which was right.
- The version is now checked on every open, before anything is written. A database from a newer Aleph is refused, with a message naming both versions and pointing at the work journal as the way to move the work across in the meantime. Refusing to open is the anti-corruption mechanism.
- A backup is taken automatically before any upgrade, named
aleph_pre_v1_to_v2_<timestamp>.dbso the one to reach for after a bad patch is obvious. Aleph says so once, plainly, rather than migrating in silence. - The migration rule is written down: additive only. New tables via
CREATE TABLE IF NOT EXISTS, new columns via_ADDED_COLUMNS— both no-ops on a database that already has them, so any newer build over any older database converges without an ordered ladder of steps to get wrong. Renames, drops and constraint changes cannot be expressed this way and need a real migration with its own backup and test. SCHEMA_VERSIONis 2, forfab_log.
Verified against a copy of a real database: a v1 file upgrades and keeps all 1,476 pieces with an intact v1 backup beside it; a v99 file is refused with the file left byte-identical; reopening a current file migrates nothing and takes no backup; a genuinely new file starts at v2.
The installer no longer half-replaces a running Aleph
The --onedir build means Setup replaces the whole program folder. With Aleph open, Windows holds Aleph.exe and the Qt DLLs, and the install could leave old DLLs beside a new exe. Setup now detects a running copy — via a named mutex Aleph claims at startup — offers to close it, and restarts it when finished.
Sheet metal progress is now journalled, like piping
A sheet metal install updated a running count and a date and recorded nothing else. There was no way to say when the third of eight pieces went in, no hours against the weight, and nothing to reverse when a quantity was logged wrong — the count was simply edited, leaving no trace of what it had been. Piping has never worked that way.
fab_log is now the record and qty_installed is a cache of it, exactly as log stands behind pieces.status.
- Crew and Hrs/man on the Sheet Metal screen, the same pair Log Work carries. Hours are attributed to the logging action rather than to each fitting, so a day's labour is counted once, not once per piece ticked.
- History… lists every entry and undoes one by appending a reversal. Nothing is deleted, reversals show in red, and a reversal cannot itself be reversed.
- Logging a fitting that is already complete is refused rather than silently capped at zero — the same double-count guard piping has.
Which closes the gap the Hours tab had. Division 20 could state a required lb/hr and had nothing to check it against, because no hours were ever recorded against sheet metal. Now build_fab_plan reads the phase's own logged production and reports a demonstrated rate, a feasibility verdict and a shortfall — on the same terms as piping, including refusing to guess from fewer than five logged days. Nothing is borrowed from the piping crews' LF/hr, which would be another trade's number wearing these units.
The save file: a portable, append-only work journal
Import → Save work journal… writes two files, and Load work journal… merges them back.
aleph-journal.jsonl is the history — every install, baseline and correction across both trades, one JSON object per line. It is append-only, and every event carries a content hash as its id, which is what makes it safe to hand around: exporting twice produces a byte-identical file, and importing the same file twice applies nothing the second time. Two machines merge by concatenating, sorting and loading.
Pieces are exported by natural key — (spool, item, ordinal) within a system, the same identity replace_pieces matches on, ordinal included because item numbers repeat inside a spool 45 times in this scope. A journal keyed on local row ids would import silently into the wrong pieces, which is worse than failing. Events whose subject is not in the receiving database are reported, not guessed at.
aleph-setup.json is current state — crews, assignments, target dates, colours, settings. Rewritten whole, because assigning a spool twice is one fact changed rather than two facts. Kept in a separate file on purpose: one you may overwrite freely, one you must only append to. Mixing them would put the second rule at the mercy of the first.
Most of the database is deliberately not exported. Pieces, fittings, BOM lines, fab packages and the labour budget all come back identically by re-importing the documents they came from. Only what a person decided is irreplaceable.
Round-tripped end to end: 12 events including reversals, exported from one database and loaded into another holding the same scope and no history — all eight measures matched, and two further loads applied nothing.
Sheet metal weight on Plan → Progress
The progress tiles carried piping only. Sheet metal scope existed, was imported and was tracked, and none of it appeared on the screen people open to answer "how is this level going" — it lived on its own production screen and nowhere else.
Progress now carries both scopes, in two strips. A SHEET METAL row (scope lb, installed, remaining, complete) with its own completion bar sits below the piping row, which gains a PIPING label to match.
- The row is hidden entirely on a level with no Division 20 scope, so a crew that has none never sees four empty tiles.
- Not blended into one percentage. Piping is linear feet off a cut list and sheet metal is pounds off a fab sheet; one merged number would be actionable by nobody, and neither the pipefitters nor the plumbers could find their own scope in it.
- Sheet metal is level-scoped and says so. Fab packages hang off a level, not off a piping system, so when the tiles above are filtered to a subset of systems this row says
not filteredrather than quietly describing a different scope than the row above it.
Division 20 budget hours, allocated by weight and shop effort
The shop-effort ratios have existed since 3.6 and were wired to nothing. Selecting a sheet metal phase in Plan → Hours was not even possible: the phase picker was built from systems, and Division 20 phases hang off fab packages, so an imported and mapped budget could not be selected.
- The phase picker now includes sheet metal phases.
- Budget hours are spread across the fittings still to install in proportion to remaining pounds × the shop-effort ratio for each fitting's fabrication type — a square-to-round of 8 lb draws 2.4x the hours of 8 lb of straight duct. The rate itself still comes from the estimate: budget hours over the phase's whole effort-weighted scope.
- The table becomes a package → fitting tree with Pieces, Weight lb, Type, Effort and Hours allowed. Sheet metal keeps its own column layout, so arranging it no longer overwrites piping's.
- Crews work here too. Assignments reuse the existing table keyed by (package, fitting) instead of (system, spool), so double-click to assign and the staffing tiles behave as they do on piping.
- No demonstrated rate is shown, deliberately. Sheet metal installs are logged by weight with no hours attached, so there is nothing measured to check the required lb/hr against. Borrowing the piping crews' LF/hr would be another trade's number wearing these units. The Hours screen says this rather than leaving a blank tile unexplained.
Fixed: selecting a single spool folder found nothing
Importing one spool by selecting its own folder reported "No files named 'LABEL - PIPE B&T.csv' were found under…" — naming a folder that visibly contained exactly that file. Only selecting a parent folder worked, which made it look like particular spools were unparseable rather than that the chosen folder had never been examined.
discover() walked root.rglob('*'), which yields everything under a folder and never the folder itself, so the selected folder could never qualify. Root is now a candidate like any other. Affected every trade — plumbing, med gas and pipefitting alike — and every spool; it was never about sheet metal.
Verified against the spool tree: single leaf folders in PLUMBING (DOM and DWV), MEDGAS and MPIPE now each discover and import their spool, including the CSV-less welded MP1 package that qualifies on its CUTLIST alone.
Fixed: pointing the piping importer at fab sheets said nothing useful
Selecting a folder of Fab Sheet PDFs on Import → Scope reported that LABEL - PIPE B&T.csv was missing — a file that folder was never going to hold. It now recognises the fab sheets and says to use Import → Fab Sheets… instead. The general "nothing found" message also now states what actually qualifies (a B&T CSV or a CUTLIST PDF) and that a single spool folder is a valid choice.
Fixed: 45% of fab cards recorded the wrong system, or none
The system label was matched against a hand-kept list of literals, and the list was wrong in two ways on this job's 502 cards:
LPSAwas absent entirely — 130 cards recorded no system. Low Pressure Supply Air is the largest single system in the package set.RA-2WGwas absent while bareRAwas present — and since alternation is first-match-wins with\bsatisfied before the hyphen, 96 cards silently recordedRA. Not a blank anyone would notice, but a plausible wrong answer, which is worse.
The interleaving was never the cause. The rotated label does get mangled — 2 S 0 u 2 p 5 p _ ly C R A A ir IG EAST: — but the dash-delimited token survives intact on every card; - LPSA - was sitting there in all 130.
Families are now matched by shape rather than enumerated: [LMH]PSA for the pressure classes and (?:RA|EXH|SA|OA)-\d+WG for the width-gauge systems, specific variants before bare fallbacks, with a trailing guard so an unrecognised variant reads as unparsed rather than being truncated to a shorter system that happens to be a prefix. Every one of the 500 fittings now carries a system: LPSA 130, MPSA 119, RA-2WG 96, EXH-3WG 83, RA-4WG 62, EXH-4WG 10.
Re-import fab sheets after upgrading — systems already stored as RA are corrected on re-import, not retroactively.
Repeated fitting numbers are now classified, not just suffixed
A repeated number is three different situations wanting three different answers, and comparing the cards says which:
- Identical in every respect — one fitting printed twice. The sheet should have said
Qty: 2; the cards are merged into one fitting with the quantities added.CRAIG_05-PK-23'sE23-13is this case: two byte-identical 34.19 lb cards, now one fitting of Qty 2 holding the same 68.38 lb. Keeping them apart put a piece in the fitting list that does not exist as its own item. - Same number, different system — legal here, since a package can span systems (three do). Kept apart and qualified:
E23-13 (EXH-4WG). - Dimensions or weight differ — a real numbering error. Kept apart under a suffix and warned about loudly, because only the shop can say which piece is which.
Weight is preserved in all three.
Package number gaps are reported
A package that was never issued is invisible scope — nothing to count, so nothing to be short of. The fab sheet import now says so: "level 05: packages 6–36 imported, but 33 is not present". Counted per level and area, since CRAIG_05-A4-PK-* and CRAIG_05-PK-* are separate sequences and interleaving them would invent gaps that do not exist.
A refused package now says which sheet is wrong
The TO# collision message named both files and left the reader to work out which was at fault. It now supplies the two facts that settle it: the systems differ, and the refused file is named after a package no sheet claims.
Package CRAIG_05-A4-PK-3 is already imported from
CRAIG_05-A4-PK-3 FAB SHEETS.pdf, butCRAIG_05-A4-PK-5 FAB SHEET.pdfalso claims it. The imported one is MPSA; this sheet is RA-4WG. No sheet claims CRAIG_05-A4-PK-5, which this file is named after — that is most likely the number its header should read.
Fixed: archived copies were imported as current scope
Discovery walked into archive folders. Superseded packages keep the same identifiers and usually the same filenames as the live ones, so each was read twice and whichever copy was visited last won.
On sheet metal that landed on the right answer only because "Archive" sorts before "CRAIG", while the reported weight counted both copies — an import that stored 47,988 lb announced 50,999 lb. On the pipefitting and plumbing trees, where retired spool folders sit in _OLD beside the live ones and hold real CSVs and cut lists, the same walk would bring retired scope in as though it were current.
Archive, _OLD, old and superseded are now skipped by every discovery walk in the program — piping and sheet metal share one definition (scanner.SUPERSEDED_DIRS) so the two importers cannot drift apart on what "archived" means. Pointing an importer straight at an archive folder still works, since the chosen root is never itself treated as superseded.
Sheet metal, before: 38 PDFs discovered, 50,999 lb reported, 47,988 lb stored. After: 35 discovered, 34 imported, 47,987.7 lb reported and 47,988 lb stored — the announced figure and the tracked figure are finally the same number. Piping spool counts are unchanged where no archive exists (29 across the tree, 14 in PLUMBING), and a synthetic tree with _OLD and Archive copies now yields only the live spool.
Fixed: a repeated fitting number killed the whole fab import
A package that lists the same fitting number on two cards — which the Craig East sheets do — crashed the import partway through on UNIQUE(package, fitting_no). The duplicate was detected and warned about and then inserted anyway, so one bad sheet took down the entire run and left the packages after it unimported.
The repeats now import under suffixed numbers (S-2, then S-2 #2). Dropping them would have quietly understated the package — the cards are distinct pieces carrying distinct weight — so every pound stays in scope and every card stays separately installable. The warning now names what the repeats were imported as, and still says to check the sheet.
Verified against the real folder: 38 PDFs, 34 packages, 47,988 lb over 501 fittings, mapping to phase 20.110.6, with 5,259 available hours allocating to 5,259.0000 across the fittings — exact, no drift.
3.7.0 — August 2026
Sheet metal has a screen
Production → Sheet Metal. Weight tiles (scope, installed, remaining, complete), a package tree expanding to individual fittings, and install logging.
- Import Fab Sheets… reads a folder of Fab Sheet PDFs in one pass, with a progress dialog, and reports refused packages and warnings rather than failing quietly. Also on the Import menu.
- Select fittings and Log installed, or double-click one to install a single piece. Quantities are capped at each fitting's remaining count, so a card reading Qty: 8 can never record nine.
- Filter by level or text; hide completed fittings.
- Packages show their phase code, or unmapped in amber if the number could not be derived.
It sits beside Log Work rather than inside the piping checklist, because sheet metal is counted in pounds off fab sheets, not in pieces off a cut list — a different unit and a different daily workflow.
Division 20 phase mapping
Fab package numbers map to sheet metal phase codes: CRAIG_05-A4-PK-3 → level 05 → 20.110.6. Confirmed against the variance report, where Division 20 follows the same level-plus-one pattern as Water, Storm and Pipefitting (Lower Level .1, Level 1B .2 … Level 5 & Penthouse .6, with A-side areas taking an A prefix).
3.6.0 — August 2026
Sheet metal scope, tracked by weight
New reader for MTECH Fab Sheet PDFs. Sheet metal is measured in pounds, not linear feet — weight is the only quantity comparable across a straight, a transition and a square bend, and every fitting card carries it.
- Parses the 2-column card grid: fitting number, weight, quantity, gauge, system (MPSA / RA-4WG / EXH-4WG …) and fabrication type.
- Fittings are classified (straight, transition, bend, tap, cap, square to round, offset) with relative shop-effort ratios, because 8 lb of square-to-round is not 8 lb of straight duct.
- Installed quantity is tracked per fitting and capped at its quantity.
- Re-import matches on (package, fitting number) — never on page position — so a re-issued sheet cannot duplicate fittings the way the piping importer once did.
- Verified against the printed sheets: CRAIG_05-A4-PK-3 reads 3,295.12 lb across 21 fittings and 32 pieces, matching hand arithmetic exactly.
A real data clash is refused rather than merged. The file named CRAIG_05-A4-PK-5 carries TO#: CRAIG_05-A4-PK-3 in its header, so two files claim one package. Importing both silently replaced 21 fittings with 1 and erased 3,295 lb of scope. The importer now blocks the second file and says which two sheets disagree.
Target dates and slack
A due date can be set per phase in Plan → Hours, beside the staffing plan. Every duration on that screen becomes slack:
- Each crew tile shows
· 25d slackor· 4d LATEagainst its own finish date. A missed date overrides an on-budget colour — being under on hours is no comfort if the work lands late. - A phase verdict reads: "16.7 working days of work left at 3 people, finishing Sep 4 — 4 day(s) PAST the Aug 29 due date." Unassigned hours are included and called out, so the date is never flattered by work nobody has been given.
- Slack uses the same working-week model as the durations it is compared against, so the two cannot disagree about what a working day is.
Dates are stored per (kind, ref), so systems and packages can carry their own dates later without a new table.
3.5.0 — August 2026
The header now describes what you are actually looking at
Filtering the by-system list to 11 of 26 systems left the four tiles above it still headlining the whole level — "26 systems / 4,714 LF" — while the list underneath showed 11. Two answers to the same question on one screen.
Everything above the list now follows the filters:
- Scope, Installed, Remaining and Complete recompute for the filtered set, and say "11 of 26 systems" so a narrowed view is never mistaken for the whole job.
- The stacked per-system bar shows only the filtered systems.
- The daily install chart and the Daily table are rebuilt from just those systems' logs.
- Finish projections use the filtered remaining scope — a level-wide finish date next to one system's figures was meaningless.
Chart colour follows the system
Narrow to a single system and the daily bars take that system's colour, so the chart, the stacked bar, its swatch and its row all agree. With several systems on screen the bars are a mixture, so they stay the default — a single colour there would claim something untrue.
3.4.0 — August 2026
Content no longer gets cut off on shorter screens
Measured across every screen: the app could not be made shorter than 859px without clipping. That is taller than a 1366x768 laptop, taller than a 1080p screen at 150% Windows scaling (720px usable), and taller than 1080p at 125% once the taskbar is counted. On any of those the bottom of a page was simply cut off with no way to reach it.
Plan / Hours was the culprit — three stacked rows of 75px stat tiles plus a 260px table floor in one rigid, non-scrolling column, 758px before any data.
Two fixes:
- Every page is scroll-hosted. Content is reachable at any window size instead of vanishing. The inner widget is still resizable, so tables stretch to fill a tall window exactly as before — nothing changes on a large screen; the scrollbar only appears when it is actually needed.
- Reduced the height that gets stacked. Compact stat tiles on Hours (75px → 57px across three rows), and the table floor lowered from 260px to 170px, which no longer has to guarantee usability on its own now that pages scroll.
Window minimum: 859px → 473px. Fits 1366x768, 1080p at 125% and 150%, and a small windowed session.
Window size and position are remembered
Set the window up once; it comes back the same way. A sensible floor (1100x620) stops it being dragged smaller than usable.
3.3.0 — August 2026
Duplicate item numbers are now classified, not lumped together
3.2.0 reported every differing-length repeat as one warning and advised re-importing. That advice was wrong for most of them. A re-import only removes a duplicate the current CAD export does not contain. If the export genuinely repeats an item number, Aleph keeps both pieces — correctly — and re-importing changes nothing, which looks like the fix failing.
The check now reads the current export and splits the finding three ways:
- Left over from an old re-import (warning) — the export does not have the repeat, so it is damage from the pre-3.2.2 bug. Re-import clears it, and the resolution is offered inline.
- Genuinely in the CAD export (informational) — the repeat is real, the scope is right, and re-importing will not and should not remove it. The only cost is that two pieces share an item number on paper. Noted as normal if the detailer numbers items per section rather than per spool.
- Could not be checked (informational) — the CAD folder is not reachable from this machine, so Aleph says so instead of guessing.
The leftover message also names the right menu: scope re-import lives under Data → Scope, not the checklist import under Production, which reads a filled-in checklist PDF and does not touch scope.
Re-import all scope
New button on Data → Reconcile. Re-reads every system from its CAD folder in one pass, with a progress dialog, preserving installed status and logged history, and reporting how many rows were removed, updated and added — plus any system whose folder could not be reached. Fixing hundreds of pieces one system at a time was not practical.
3.2.2 — August 2026
Patch: build only, no application changes.
"Failed to extract PIL\\_avif.cp312-win_amd64.pyd" fixed at the source
All three build scripts now exclude modules Aleph never touches:
PIL._avif/PIL.AvifImagePlugin— a large native AV1 image decoder, pulled in transitively bypdfplumberandreportlab, and the file a--onefilebuild fails to decompress at start-up. Aleph does no image work whatsoever;AvifImagePluginalready guards its import withtry/except ImportError, andPIL.Image.init()skips plugins that fail to load, so removing it changes nothing at runtime. Verified by blocking the modules at import time and running the full GUI, a CAD import, a variance PDF parse and a checklist PDF export.- Unused Qt subsystems —
QtWebEngineCore(a bundled Chromium), QtQml/QtQuick, QtMultimedia, Qt3D, QtCharts, QtSql and others — plus tkinter, matplotlib and numpy. Smaller download, fewer files to unpack, fewer things that can fail to unpack.
build_exe.bat now carries a warning explaining that --onefile re-unpacks the whole app to temp on every launch, which is what causes that error, and pointing at the two --onedir builds where the failure mode does not exist. README documents the same.
3.2.1 — August 2026
Patch, and the most important fix in the 3.x line so far.
Re-import was duplicating every piece below an edited line
Pieces were identified by (spool, item, csv_row). Row position in a CSV is not an identity. When the CAD team inserted or removed a line, every piece below it shifted row and therefore matched nothing: the incoming rows were all inserted as new pending pieces, while the originals survived because installed pieces are never deleted.
The result, on any system re-imported after logging work:
- two copies of every piece — one installed, one pending, identical lengths
- scope LF and piece count doubled
- completion halved: a finished spool reading "3/6 pieces"
Pieces are now matched on (spool, item), with position used only to disambiguate a spool that genuinely repeats an item number. csv_row is carried forward for display order but is no longer part of identity.
Verified: a re-import with every row shifted now reports 3 updated, 0 added, 0 removed — installed status intact, scope unchanged, row numbers refreshed. Genuine repeated item numbers still round-trip at their real count, pieces genuinely dropped from an export are still removed, and installed pieces dropped from an export are still kept.
Cleaning up databases already affected
The duplicate check added in 3.2.0 finds these, and Remove the extra row(s) resolves them — it keeps the installed copy and drops the pending duplicate, then recomputes the cached totals. A spool showing "3/6 pieces" goes back to 3 pieces and complete.
Run Reconcile after upgrading. Existing damage does not repair itself.
3.2.0 — August 2026
Minor: new Reconcile checks, and a correction to an existing one that was giving false reassurance.
Duplicate spool/item rows
- A duplicate check existed but was filed as informational and stated that "each row is still counted once, so totals are correct." That was false. A piece is identified by (spool, item, csv_row), so two export lines carrying the same spool and item number become two separate pieces and both are counted — inflating scope LF, piece count, and every percentage built on them. Found in real Med Gas data: two byte-identical lines in one cut list, overstating that system by 3.5 LF.
- Now reported as a warning with the overstated LF quantified, split into two findings because they have different causes:
- same length — a repeated export line; surplus scope that isn't there
- different lengths — two real pieces sharing one item number; the scope is right but a fitter can't tell the checklist rows apart
- Remove the extra row(s) resolution, which keeps one of each and recomputes the cached scope totals. Installed pieces are never deleted, and a group whose copies are all installed is skipped entirely and stays flagged — resolving it would mean deleting logged history.
- Nothing is auto-removed. A real second piece of the same length that was never renumbered looks identical to a duplicated line, and only the drawing settles it.
Spool numbers reused across systems
- New informational check. Legal — spool numbers only have to be unique within a system, and Aleph keys on (system, spool) throughout — but it makes spool numbers ambiguous on paper, and it is the condition that previously broke budget-hour allocation.
3.1.0 — August 2026
Minor: new capability, no change to existing workflow.
Colour selection is a menu, not a dialog
- The swatch beside each system on Progress is now a button. Clicking it drops a menu with a Previously used row, the full palette, Custom colour… and Reset to automatic.
- Recently used colours are remembered per job, most-recent-first, deduped and capped at nine. On a real job the same handful get reused across systems, and re-finding the exact blue used on the last three was the tedious part.
- The old route — right-click the row — still works and now opens the same menu.
System selection in Log Work is grouped and searchable
- The flat dropdown is replaced by a button that drops a tree grouped Level → kind of system → system, with a filter box. A flat combo is fine for a dozen entries and unusable at sixty.
- Group headings are not selectable, so a heading can never be mistaken for a choice.
- The button shows the level and system kind alongside the ID, so the current selection reads without opening anything.
- Unmapped systems sort last and are no longer the default pick — a blank level sorts first in database order, so a job with one stray riser used to open on that riser every time.
3.0.0 — August 2026
Major, because the program was renamed, its navigation was rebuilt, and its database moved. An existing user opens this and finds a different program.
Renamed to Aleph
- Was Verum, and
bomlfbefore that. Database now at%LOCALAPPDATA%\Aleph\aleph.db. - Migration walks the whole chain —
bomlf→Verum→Aleph— rather than one hop, so a machine sitting on any previous version carries its history forward. Files are copied, never moved, and an existing Aleph database is never overwritten.
Navigation: six tabs to four
- Plan — Progress · Daily · Hours
- Production — Log Work · History · Calendar
- Takeoff
- Data — Scope · Reconcile
- The Dashboard's Systems sub-tab is gone: six of its eight columns duplicated the Scope tree. Its one unique column, Remaining, moved into that tree.
- Scope moved from Plan to Data. It is administrative, not analytical, and the corrections Reconcile asks for are made in Scope — previously a round trip between top-level tabs.
- Sub-tabs are flat; no page contains another tab bar.
Hours and crews
- Named crews with headcounts, assigned per spool. Each crew's tile counts only its own spools, so crews working in parallel get independent finish dates instead of one serial one.
- Days @ crew column: budget man-hours divided by the assigned crew's size. Hours allowed stays man-hours so it still reconciles against the variance report.
- Finish dates from a configurable working week (5-day, 4×10, 6-day).
- Rate needed split into LF/hr, joints/hr and spools/hr.
- Columns are movable, hideable and resizable, and the layout persists.
- Worst-overrun-first ordering with severity shading.
Medical gas
- New Braze connection type, diameter-scaled like a weld. Medical gas copper reads as ordinary solder-grade copper in the cut list, so system type decides the join method — previously every brazed joint was costed as ProPress, the cheapest category available.
MGmapped into the phase-code tables (35.160.x).
Progress display
- Stacked per-system progress bar; segments sum exactly to the headline installed figure.
- Per-system colours, user-assignable by double-clicking a swatch, stored separately from
systemsso a re-import cannot wipe them. - By-system list is collapsible and filterable by level, system kind, text and completion.
Fixes
- Budget hours could be double-counted. Allocation was keyed on spool number alone, but spool numbers are only unique within a system. Two systems in one phase sharing a spool number — which the importer's
(unassigned)fallback guarantees — made the displayed hours exceed the phase budget by a third in testing. Now keyed on(system, spool). - Selecting "Unmapped" showed the entire job. The level filter used a truthiness test, so
""was read as "no filter". Affected the stat tiles, burn-up and daily series, not just the list. - The phase selector showed one phase per level. It was filtered by the level selector, which also made any phase on a blank-level system unreachable. Hours is phase-scoped; the filter was a UI artefact hiding real data.
- The Hours table was clipped on a 1080p screen — 7 rows visible. The level header above it described a different slice of the job (Hours is phase-scoped) and cost 268px. Removed there; now ~18 rows, 21 with the basis text collapsed.
- Red negative figures were dropped from variance PDFs. Parenthesised negatives render fractionally off-baseline; fixed-grid row bucketing put them on a different row and discarded them. Affected 60+ rows per report.
- Version numbers unified (see below).
- App icon added; taskbar identity set so Windows groups it correctly.
Versioning
Three constants previously held versions independently and had drifted: the GUI reported 2.16.0, the CLI reported 1.2.0 and still called the program bomlf. All now derive from aleph/__init__.py, and the test suite fails if any drifts.
On the jump from 2.16.0 to 3.0.0: 2.16.0 was already above 2.0 — semantic versioning reads 2.16 as the sixteenth minor release, not as "two point one six". Setting 2.0 would have moved backwards. 3.0.0 marks the same milestone unambiguously and is greater than everything shipped.
Before 3.0.0
Released as Verum (2.x) and bomlf (1.x), tracked in the project's history rather than here.