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
|
||||
Reference in New Issue
Block a user