WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLANT EXTRACTION RULE PACKS: one producer's drawing habits, kept out of
|
||||
the generic reader and loaded only where the plant knowledge says so.
|
||||
|
||||
WHY THIS EXISTS
|
||||
---------------
|
||||
`ticketqc.parse_design_summary` reads a design summary with rules that are
|
||||
true of precast drawings generally -- an f'c line, a strand callout, a mesh
|
||||
callout. Every text note it cannot place lands in the spec's `unresolved`
|
||||
list verbatim, which is what stops a narrow rule set from being a lossy one.
|
||||
|
||||
But a real sheet carries a second population of notes that are not general at
|
||||
all. They are one producer's house style: the title of a standard detail, the
|
||||
label above a legend, a standing instruction that appears on every sheet that
|
||||
producer draws. The generic reader cannot place them and should not try --
|
||||
they are not facts about precast, they are facts about an office.
|
||||
|
||||
Left alone they are not harmless. Seven of them on one sheet turn `unresolved`
|
||||
into a list nobody reads, and the day a genuinely unread value lands there it
|
||||
is the eighth line of a list the reader has already learned to skim. A signal
|
||||
that always fires is not a signal -- the same argument `.setup-opts` makes for
|
||||
a check that always FAILs, and the same one `checks.muted` makes for a row
|
||||
that is permanently NOT RUN.
|
||||
|
||||
WHAT A PACK MAY DO, AND WHAT IT MAY NEVER DO
|
||||
--------------------------------------------
|
||||
A pack classifies a note the generic rules did not match. It may say:
|
||||
|
||||
* `note` -- prose that belongs in the spec's `notes` list. An
|
||||
instruction, a condition, a standing rule. It is CARRIED,
|
||||
verbatim, into a field a person reads.
|
||||
* `furniture` -- drawing furniture that carries no data at all: the bare
|
||||
title of a legend or a detail. It STAYS in `unresolved`,
|
||||
with the pack's reason in place of the blanket one.
|
||||
|
||||
A pack may NEVER remove a note from the spec, and there is no third verdict
|
||||
that makes one disappear. `furniture` is not an ignore list: the line is still
|
||||
written out, still readable, still countable -- only the sentence beside it
|
||||
changes, from "no rule matched this" to "this is a legend title, it carries no
|
||||
data". An exclusion the code chose for itself is the one failure this project
|
||||
has already paid for (see `vibe/scope.py`), and nothing here may repeat it.
|
||||
|
||||
WHEN UNSURE, A PACK SAYS `note`, NEVER `furniture`
|
||||
--------------------------------------------------
|
||||
The two errors are not symmetric, in the same way `vibe/scope.py`'s two are
|
||||
not. A note wrongly called `note` is copied into a field a person reads and
|
||||
costs them one line of attention. A note wrongly called `furniture` has been
|
||||
declared meaningless by a regex, and the reader is told there is nothing
|
||||
there. So a rule that is not certain writes `note`, and a title that carries
|
||||
any condition beyond its own name (`STRIPPING & BLOCKING DETAIL < 25'`) is a
|
||||
note, not furniture -- the condition is the part that could matter.
|
||||
|
||||
WHICH PACKS LOAD IS DECLARED, NEVER INFERRED
|
||||
--------------------------------------------
|
||||
From the PLANT layer, `extract.rule_packs`. Not from `plant.id`, even though
|
||||
that key exists and would be enough to guess with: which house style a job is
|
||||
drawn in is a thing somebody knows and can write down, and a guess that is
|
||||
right today is a guess that goes wrong silently the first time a job is drawn
|
||||
to another producer's standard on this plant's template.
|
||||
|
||||
extract:
|
||||
rule_packs: [mc]
|
||||
|
||||
Undeclared means NO PACK LOADS and every note behaves exactly as it did
|
||||
before this module existed -- unmatched notes land in `unresolved` under the
|
||||
generic reason. That is the honest default: a machine that has been told
|
||||
nothing about an office's habits should claim to recognise nothing.
|
||||
|
||||
A declared name this module does not know is REFUSED, loudly, and no pack
|
||||
loads at all. Same discipline as `tools/setup_machine.py`'s `.setup-opts`
|
||||
check: an opt file that has gone quiet about a line nobody honours is worse
|
||||
than no opt file, because the reader believes something is in force that is
|
||||
not. `problems` comes back beside the packs and the caller prints it.
|
||||
|
||||
Nothing here imports a module named in knowledge. `PACKS` is a whitelist of
|
||||
names this package ships; a name outside it is an error, never an import.
|
||||
|
||||
IronPython 2.7: no f-strings, no pathlib, no typing, no dataclasses.
|
||||
"""
|
||||
|
||||
# Declared name -> module basename inside this package. A WHITELIST: the
|
||||
# knowledge layer chooses among these, it never names a module to import.
|
||||
PACKS = {
|
||||
'mc': 'mc',
|
||||
}
|
||||
|
||||
KEY = 'extract.rule_packs'
|
||||
|
||||
# What a pack may return from `classify`. `note` is carried into the spec's
|
||||
# notes; `furniture` stays in unresolved with a better reason. There is no
|
||||
# verdict that drops a line -- see the module docstring.
|
||||
NOTE = 'note'
|
||||
FURNITURE = 'furniture'
|
||||
KINDS = (NOTE, FURNITURE)
|
||||
|
||||
|
||||
class Placement(object):
|
||||
"""One pack's verdict on one note.
|
||||
|
||||
`rule` is the pack-qualified id of the rule that fired (`mc/legend-title`)
|
||||
so a reader of the spec can find the line of code that made the claim, and
|
||||
argue with it. A placement that cannot say which rule made it is not
|
||||
accepted -- an unattributable classification is exactly the kind nobody
|
||||
can correct.
|
||||
"""
|
||||
|
||||
__slots__ = ('kind', 'rule', 'reason')
|
||||
|
||||
def __init__(self, kind, rule, reason):
|
||||
if kind not in KINDS:
|
||||
raise ValueError('unknown placement kind: %r' % (kind,))
|
||||
if not rule:
|
||||
raise ValueError('a placement must name the rule that made it')
|
||||
self.kind = kind
|
||||
self.rule = rule
|
||||
self.reason = reason
|
||||
|
||||
def __repr__(self):
|
||||
return 'Placement(%r, %r)' % (self.kind, self.rule)
|
||||
|
||||
|
||||
def load(know, get=None):
|
||||
"""(packs, problems) for the rule packs this knowledge declares.
|
||||
|
||||
`packs` is a list of imported pack modules, in declared order. `problems`
|
||||
is a list of one-line strings naming anything refused; when it is
|
||||
non-empty `packs` is EMPTY -- a half-loaded rule set would classify some
|
||||
notes and not others, and the reader could not tell which sentences to
|
||||
trust. All or nothing, and say why.
|
||||
"""
|
||||
if get is None:
|
||||
from vibe import knowledge as K
|
||||
get = K.get
|
||||
declared = get(know, KEY, None) or []
|
||||
if isinstance(declared, str):
|
||||
declared = [declared]
|
||||
|
||||
problems = []
|
||||
names = []
|
||||
for raw in declared:
|
||||
name = (raw or '').strip().lower()
|
||||
if not name:
|
||||
problems.append('an empty name in %s -- remove the entry or '
|
||||
'name a pack' % (KEY,))
|
||||
elif name not in PACKS:
|
||||
problems.append("%s names '%s', which this build does not ship. "
|
||||
'Known packs: %s'
|
||||
% (KEY, name, ', '.join(sorted(PACKS)) or '(none)'))
|
||||
elif name in names:
|
||||
problems.append("%s names '%s' twice" % (KEY, name))
|
||||
else:
|
||||
names.append(name)
|
||||
|
||||
if problems:
|
||||
return [], problems
|
||||
|
||||
packs = []
|
||||
for name in names:
|
||||
packs.append(__import__('vibe.extractrules.' + PACKS[name],
|
||||
globals(), locals(), ['classify'], 0))
|
||||
return packs, []
|
||||
|
||||
|
||||
def classify(text, packs):
|
||||
"""The first pack that recognises `text`, or None.
|
||||
|
||||
First, not best: packs are tried in the order they were declared, and the
|
||||
reader can see that order in the job file. Nothing here merges two packs'
|
||||
opinions -- a note that two house styles both claim is a fact about the
|
||||
declaration, not something this function should quietly average.
|
||||
"""
|
||||
for pack in (packs or []):
|
||||
placed = pack.classify(text)
|
||||
if placed is not None:
|
||||
return placed
|
||||
return None
|
||||
|
||||
|
||||
def apply_to(entries, packs):
|
||||
"""Split `unresolved` entries into (notes, unresolved) using the packs.
|
||||
|
||||
`entries` are the dicts `ticketqc.unresolved_in_view` returns -- each
|
||||
carries at least 'text' and 'reason'. Every entry comes back in exactly
|
||||
one of the two lists and none is dropped: `len(notes) + len(unresolved)`
|
||||
always equals `len(entries)`, which `test_extractrules` asserts directly
|
||||
because it is the property the whole module rests on.
|
||||
|
||||
A `note` gains 'rule' so the spec says which rule claimed it. A
|
||||
`furniture` keeps its place in unresolved and only its 'reason' changes.
|
||||
"""
|
||||
notes = []
|
||||
unresolved = []
|
||||
for entry in (entries or []):
|
||||
placed = classify(entry.get('text') or '', packs)
|
||||
if placed is None:
|
||||
unresolved.append(entry)
|
||||
continue
|
||||
out = dict(entry)
|
||||
out['rule'] = placed.rule
|
||||
if placed.reason:
|
||||
out['reason'] = placed.reason
|
||||
if placed.kind == NOTE:
|
||||
notes.append(out)
|
||||
else:
|
||||
unresolved.append(out)
|
||||
return notes, unresolved
|
||||
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The `mc` pack: one producer's design-summary habits.
|
||||
|
||||
Loaded only when the plant layer declares it (`extract.rule_packs: [mc]`) --
|
||||
see `vibe/extractrules/__init__.py` for why that is declared and not inferred
|
||||
from `plant.id`.
|
||||
|
||||
WHERE THESE RULES COME FROM
|
||||
---------------------------
|
||||
Every rule below was written against text observed on a real sheet, and each
|
||||
one says which. They are not a model of how this producer draws; they are a
|
||||
record of lines that actually landed in `unresolved` and what they turned out
|
||||
to be. A rule with no observation behind it does not belong here -- this file
|
||||
is evidence, and a guess mixed into evidence is indistinguishable from it
|
||||
afterwards.
|
||||
|
||||
Observed on `WPA1.0` (sample_job, 8" horizontal wall, read 2026-09-01):
|
||||
all seven notes the generic reader could not place, listed in the order they
|
||||
sit on the sheet.
|
||||
|
||||
WHAT IS DELIBERATELY NOT HERE
|
||||
------------------------------
|
||||
`STRIPPING & BLOCKING DETAIL < 25'` is NOT treated as the same thing as the
|
||||
bare `STRIPPING & BLOCKING DETAIL` beside it. The bare one is the title of a
|
||||
detail drawn on the sheet and carries no data. The other one carries a
|
||||
CONDITION -- which detail applies below twenty-five feet -- and a condition is
|
||||
the part that could change what gets built. So the bare title is furniture and
|
||||
the conditional one is a note, and the rule that separates them tests for
|
||||
anything trailing the title rather than matching both with one loose pattern.
|
||||
That asymmetry is the module's standing rule (`__init__`: when unsure, note).
|
||||
|
||||
Nothing here encodes a NUMBER. `*USE MC STANDARD REINFORCEMENT ...` names a
|
||||
standard this office keeps; what that standard says is plant knowledge and
|
||||
belongs in the plant knowledge file, looked up by name, never frozen into a
|
||||
regex in this file. The rule's whole job is to carry the sentence to a field a
|
||||
person reads.
|
||||
|
||||
IronPython 2.7: no f-strings, no pathlib, no typing, no dataclasses.
|
||||
"""
|
||||
import re
|
||||
|
||||
from vibe.extractrules import Placement, NOTE, FURNITURE
|
||||
|
||||
NAME = 'mc'
|
||||
|
||||
|
||||
def _rule(rule_id, pattern, kind, reason):
|
||||
return (rule_id, re.compile(pattern, re.IGNORECASE), kind, reason)
|
||||
|
||||
|
||||
# Order matters and is not alphabetical: a more specific rule must come before
|
||||
# the looser one it would otherwise be swallowed by. The conditional detail
|
||||
# title is tested before the bare one for exactly that reason.
|
||||
RULES = (
|
||||
# ---------------------------------------------------------- instructions
|
||||
_rule('mc/standard-reinforcement',
|
||||
r"^\*?\s*USE\s+MC\s+STANDARD\s+REINFORCEMENT\b",
|
||||
NOTE,
|
||||
'a standing instruction naming this plant\'s standard reinforcement '
|
||||
'for cut-down panels and openings -- prose, not a number; what the '
|
||||
'standard says is plant knowledge, looked up by name'),
|
||||
|
||||
_rule('mc/conditional-bar',
|
||||
r"^#\s*\d+\s+BAR\b.*\bONLY\s+NEEDED\s+WHEN\b",
|
||||
NOTE,
|
||||
'a conditional reinforcing instruction -- the bar is required only '
|
||||
'when the condition in the sentence holds, so the condition is the '
|
||||
'value and it stays as prose'),
|
||||
|
||||
_rule('mc/pick-instruction',
|
||||
r"^USE\s+FOR\b.*\bPICK\b",
|
||||
NOTE,
|
||||
'a handling instruction saying which pick this detail is for -- '
|
||||
'prose, and it qualifies a detail rather than stating a number'),
|
||||
|
||||
_rule('mc/detail-title-conditional',
|
||||
r"^STRIPPING\s*&\s*BLOCKING\s+DETAIL\b\s*\S+",
|
||||
NOTE,
|
||||
'a standard-detail title carrying a CONDITION (which detail applies, '
|
||||
'and when) -- the title alone would be furniture, the condition is '
|
||||
'not'),
|
||||
|
||||
# ------------------------------------------------------------- furniture
|
||||
_rule('mc/detail-title',
|
||||
r"^STRIPPING\s*&\s*BLOCKING\s+DETAIL\s*$",
|
||||
FURNITURE,
|
||||
'the bare title of a standard detail drawn on the sheet -- names '
|
||||
'the detail, states nothing about this design'),
|
||||
|
||||
_rule('mc/legend-title',
|
||||
r"^STRAND\s+PATTERN\s*$",
|
||||
FURNITURE,
|
||||
'the title above the strand-pattern legend -- the legend\'s DATA is '
|
||||
'the dimension chain under it, which is read as geometry, not as '
|
||||
'this text'),
|
||||
|
||||
_rule('mc/legend-label',
|
||||
r"^STRAND\s*$",
|
||||
FURNITURE,
|
||||
'a bare column label in the strand legend -- a heading over values '
|
||||
'read elsewhere, with no value of its own'),
|
||||
)
|
||||
|
||||
|
||||
def classify(text):
|
||||
"""A Placement for `text`, or None when this pack does not recognise it.
|
||||
|
||||
`text` arrives already whitespace-normalised by the caller (one space
|
||||
between words, no leading or trailing run), so every pattern here can
|
||||
assume single spaces and anchor at both ends where it means to.
|
||||
"""
|
||||
probe = (text or '').strip()
|
||||
if not probe:
|
||||
return None
|
||||
for rule_id, pattern, kind, reason in RULES:
|
||||
if pattern.search(probe):
|
||||
return Placement(kind, rule_id, reason)
|
||||
return None
|
||||
Reference in New Issue
Block a user