Write an admission rule¶
The rule is where the application refuses a write: one that fails the application's schema, duplicates something already on the board, or adds nothing. It is the first of the decisions the control component makes, and the only one the application writes itself.
The signature¶
from blackboard import Accept, Reject
def rule(proposed, reader):
...
return Accept() # or Reject("why")
It runs on every proposed write of both kinds that the control component puts to it, before the board sequences anything. With no rule supplied, every one of them is accepted. Where it does not apply names the writes it is never asked about.
Telling the two kinds apart¶
from blackboard import ProposedContribution, ProposedPremiseWrite
def rule(proposed, reader):
if isinstance(proposed, ProposedContribution):
return validate_bundle(proposed.content)
if isinstance(proposed, ProposedPremiseWrite):
return Accept() if proposed.premise != "window" else Reject("window is fixed")
return Accept()
ProposedContribution carries writer, level and content.
ProposedPremiseWrite carries writer, premise, value and
expected_version. Both name the writer, so a rule can refuse a write for
who made it as well as for what it carries.
Validating content¶
def rule(proposed, reader):
if not isinstance(proposed, ProposedContribution):
return Accept()
content = proposed.content
if not isinstance(content, dict) or "findings" not in content:
return Reject("a contribution carries findings")
return Accept()
The reason reaches the writing agent, so write it for whoever has to fix the caller.
Reading the board¶
The rule receives a BoardReader, so it can refuse a write for what the board
already holds rather than for the write alone.
def refuse_duplicates(proposed, reader):
if isinstance(proposed, ProposedContribution):
for existing in reader.read_level(proposed.level):
if existing.content == proposed.content:
return Reject("a duplicate of a contribution already on the board")
return Accept()
The check-then-act window¶
The rule runs without the control component's lock, so two writes judged at the same moment both see the board as it was before either write landed. A duplicate-refusing rule therefore bounds concurrent duplicates rather than preventing them.
A premise write closes that window itself, because it names the version it expects to replace. A level write has no equivalent, so a rule that has to be exact rather than approximate needs a unique constraint in the store that the board writes to.
Where it does not apply¶
Some writes are settled before the rule is asked, so no rule sees them.
A premise's opening value bypasses admission. It reaches the board and takes a sequence number like any other write, but it is the application's own input rather than a proposal from a writer, so there is nothing for the rule to judge.
A write naming a region nobody declared raises UndeclaredRegionError, and one
naming a region declared as the other region kind raises RegionKindError.
Both raise before the rule runs, so a rule never has to check that the region
it is judging exists.
A write to a closed run comes back Rejected with the cause RUN_CLOSED, and
a write by a registered agent to a level that its writes_to does not name
comes back with the cause NOT_PERMITTED. Both are decided before the rule
runs, and ADMISSION is the only cause a rule produces.
The names¶
AdmissionRule is the type of the callable that create_model takes: it
receives a
ProposedWrite and a BoardReader, and answers Accept or Reject.
ProposedWrite is ProposedContribution | ProposedPremiseWrite, so a rule that
cares which kind it was given branches on isinstance. A contribution names a
level; a premise write names a premise and the expected_version it means
to replace; and both name the writer.
A rule never sees a write to a region nobody declared. That raises
UndeclaredRegionError before the rule is reached, because the application
declared the regions and a name outside them is its own mistake rather than
something for the rule to judge.