Design decisions¶
Explanation: the load-bearing choices, each with the alternative it was made against. One entry a decision, grouped by subject. Where a decision rests on a wire measurement, the basis is in Viewdata encoding rather than restated here.
Addressing¶
Page addresses are strings¶
Decision: PageAddress wraps a validated non-empty ASCII digit string, and is
the framework’s only notion of what a page is called.
Because: the page number is the one name everyone shares — the reader keying it,
the terminal displaying it, the service answering it, and a note on paper.
Rejected: being generic over an application-supplied reference type, as the
original code was; a string makes history, the back key and cross-service links
ordinary operations on a value that carries nothing about what it names.
A frame letter is not part of an address¶
Decision: the b in 82489493b names a continuation frame and is not part of a
PageAddress.
Because: it appears only on screen, and a reader never keys it.
Rejected: folding the letter into the address; it would carry something no
request ever names.
Routing¶
Patterns, not a table of numbers¶
Decision: a route is a pattern of literal digits and named fields compiled to an
anchored regex, 82{post_id:int}.
Because: a page number’s fields are the service’s own identifiers, and a service
has as many pages as it has posts.
Rejected: a dictionary of numbers; it cannot hold a page per post, and cannot be
read backwards by address_for to keep the numbering in one place.
Distinct, not prefix-free¶
Decision: page numbers need only be distinct, so fields may vary in width and
stay short.
Because: a viewdata request is terminated — *8# and *82489493# are
unambiguously different — because the reader presses a terminator.
Rejected: requiring prefix-free numbers as a URL router must; the terminator
removes the ambiguity a URL has no way to.
Most literal wins¶
Decision: candidates are tried by how many characters of the pattern are fixed
digits, most first, so 90 beats 9{n:int}.
Because: a table whose meaning changed when someone reordered it would be no use.
Rejected: matching in registration order; the meaning would then shift whenever
someone tidied the list.
Fields must be separable¶
Decision: all but the last of adjacent fields must have a width known in advance,
and two bare int fields adjacent are refused at registration.
Because: a page number has no separators, so adjacent fields can be told apart
only by a fixed width.
Rejected: matching adjacent variable-width fields; the split is unrecoverable at
match time, so it is refused when the route is declared instead.
The session¶
A missing page returns None¶
Decision: respond returns None for a page that is not there, rather than a
notice.
Because: a page that exists is somewhere the reader has gone and enters the
history; a page that does not is something said to a reader who has not moved.
Rejected: returning a notice page; it blurs “gone somewhere” into “told
something”, which the session must tell apart.
A raised handler costs its page, not the call¶
Decision: the session catches a handler’s exception, logs it with its traceback,
and shows Sextile.failed without moving the reader.
Because: a session is a telephone call, and ending it over the service’s own
fault means dialling back in over minutes of a slow line.
Rejected: letting the exception end the session; the fault was the service’s, not
the reader’s.
Failed is not not-found¶
Decision: Sextile.failed is a page of its own, distinct from the not-found
notice.
Because: one says the reader asked for something not here, the other that the
service could not build something that is.
Rejected: reusing the not-found notice; it sends the reader away thinking they
mistyped and hides the fault from whoever could fix it.
The board has a finite number of lines¶
Decision: serve caps live callers at max_connections (64 by default) and
turns a caller over the ceiling away with the application’s busy frame — an
on_busy hook, like the not-found and timeout notices — rather than holding the
line.
Because: nothing else stops one caller opening connections without limit and
locking everyone out; the idle timeout releases a silent caller, not one who
keeps a line active.
Rejected: an unbounded server, or a silent TCP refusal; the first is a denial of
service anyone can mount, the second leaves a caller at a dead line rather than a
busy signal they can read.
Pages and frames¶
A page is a type, with choices per frame¶
Decision: a handler returns a Page of PageFrames, each carrying its own
choices, not bare frames.
Because: frame b of a listing offers a different nine destinations from frame a,
and the session consults the showing frame and nothing else.
Rejected: bare frames with the choices on the page; the choices differ frame to
frame, so they belong to the frame.
Choices are kept apart from moves¶
Decision: a PageFrame keeps choices and moves as two mappings.
Because: a move names no destination — it steps between frames of the page
already showing.
Rejected: putting frame movement among the choices; a menu then appeared to
offer eleven, which two existing tests caught at once.
A service is a list of routes¶
Decision: a service is Sextile(pages=[...]), given as data.
Because: it makes registration order unobservable, which was the root of four
separate defects.
Rejected: registering pages imperatively through decorators and constructor
order; each defect was that order showing through.
Listing words are stated once¶
Decision: a page’s title, detail, label and keywords sit on its PageRoute, and
a page given no title is not advertised.
Because: a service naming each page in its menu, again where listed, and again in
its guide keeps three copies that drift.
Rejected: naming a page separately in each place it appears; the copies go out of
step, as an earlier service’s did.
Layout¶
Furniture is separate from parts¶
Decision: PageLayout splits a page into furniture round the edge and a list of
parts down the middle.
Because: the earlier shape coupled the furniture to being a homogeneous sequence,
so a grid, form or masthead had to draw its own.
Rejected: the coupled template; six pages had come to redraw furniture by hand.
A frame break divides at a chosen point¶
Decision: FrameBreak divides a page where the author places it, not where the
rows run out.
Because: two frames may be two lists split by what a reader is doing.
Rejected: breaking only when rows fill; it cannot divide a page at a meaningful
point.
Custom for what is not a sequence¶
Decision: Custom(rows, draw) is a part of stated height drawn cell by cell.
Because: a picture, grid or masthead is not a homogeneous sequence.
Rejected: forcing all content through a sequence part; it is the wrong shape for
a placed drawing.
Drawing¶
The drawing operations are free functions¶
Decision: fitted, centred, rule and bar take a canvas and a row, and are
functions rather than methods.
Because: a service can write its own beside them and reach for either without
minding which is which.
Rejected: methods on a canvas or page; a service could not then place its own
operations on equal footing.
A frame is a fixed grid¶
Decision: Frame is a fixed 24 × 40 grid, not a stream of writes.
Because: Commstar wraps from the bottom-right cell back to the top-left instead
of scrolling (Viewdata encoding), so a serialiser one cell
over would overwrite the frame it just drew.
Rejected: a stream of writes; one cell too many corrupts the frame.
A tile is placed, not composed¶
Decision: a Tile of pre-solved cells is placed by Canvas.place and as a page
part, but not through a Composition.
Because: a tile is opaque cells carrying their own attributes, where a
Composition plans a row’s attributes from runs of one style each; threading a
tile through that planner would have it re-plan attributes the solver already
placed, and centring a tile is arithmetic the part does itself.
Rejected: aligning a tile among other runs on one row through the compositor; the
run-of-styles planner and a pre-solved rectangle are different models, and mixing
them buys nothing a placed part does not already give.
One alignment, not a text one and a placement one¶
Decision: everything placed across the frame – a Composition run, a Tile, a
Lines part’s align – takes the same axis-neutral Align
(START/CENTRE/END).
Because: one term, one meaning. A second enum spelled LEFT/RIGHT for text
bought nothing but a name to keep in step with the first.
Rejected: a separate TextAlign to keep a justified mode out of placement types,
which it did briefly (0.4.0, removed at 0.5.0); justification is not shipped, and
were it ever, it edits the spaces between words – a breaking policy set on the
wrapper, not a member of an alignment enum – so it could not reach a placement
regardless.
Colour was built in from the start¶
Decision: Canvas accounts for a colour attribute occupying a character cell, so
no layer above it counts columns.
Because: a colour attribute costs one of the forty columns
(Viewdata encoding), and deferring colour would have forced
the typesetting to be rewritten around it.
Rejected: adding colour later; the cell arithmetic reaches every layer that
places text.
Forms¶
Three suggestions, not nine¶
Decision: TypeAhead offers three matches beneath the field.
Because: nine rows would cost nearly three seconds a keystroke at 1200 baud where
a reader types two characters a second; three costs about one
(Viewdata encoding).
Rejected: a full menu of nine; the wire cannot keep up with the typing.
Digits choose, letters type¶
Decision: a digit always selects a suggestion and a letter always types; an entry
whose text holds a digit is found by the letters around it.
Because: a digit is data or it is a choice, never both, and the session consults
choices first.
Rejected: typing digits into the field; a digit that led nowhere would do nothing
the reader could see.
Middleware and the visits log¶
record_visits takes the key, not the log¶
Decision: record_visits(VISITS) is handed the StateKey the log sits under and
reads request.state per page.
Because: the middleware is built before the service’s lifespan has opened
anything.
Rejected: handing it the log object; the log does not exist when the middleware
is constructed.
The log is a file of its own¶
Decision: SqliteVisits keeps its own file, and trims once a day rather than
once a page.
Because: a service’s own database is often derived and rebuilt, whereas the log
is the only copy of what it holds.
Rejected: a table in the service’s database trimmed on every fetch; a rebuild
would lose the log, and a per-fetch delete is a write nobody asked for.
The public surface¶
The surface is a stated set of submodules¶
Decision: the public surface is a list of submodules with stated names; anything
unlisted is machinery, pinned by test_public_surface.py.
Because: an unlisted module may be renamed, split or deleted without notice, and
what must not drift is pinned by a test rather than a rule someone remembers.
Rejected: one flat namespace; the framework is large enough that a single top
level says nothing about what belongs with what.
Uncalled is not a reason to delete¶
Decision: framework surface goes only when it duplicates another implementation,
never because nothing in the repository calls it.
Because: the surface is justified by being useful to a service — a fourth service
wants read_bitmap and boxed — not by the three that share the repo.
Rejected: deleting uncalled surface code; a since-removed furniture module went
because two implementations of one thing diverge, which is a different reason.
Packaging¶
Click, not argparse, for the command line¶
Decision: the command line is built on click, the framework’s second
dependency, and a service extends it by adding the framework’s render and
serve commands to its own click.Group.
Because: a service’s command line is the framework’s two commands plus its own,
and Click composes groups and commands as values a service adds to, where
argparse subparsers are assembled by mutating a parser through a configure
callback run against each; argparse also expands a help string as a printf
template, so a formatted percent sign in one crashed serve --help (issue #1).
Rejected: staying on the standard library’s argparse; it held the dependency
count at one, but the service-extension seam and the help-string trap cost more
than a second, small, widely-used dependency does.
No mounting¶
Decision: a service is one flat namespace; sub-application mounting was removed. Because: it had no user, and it obliged routing, keywords, contents, state and history to see through the seam for a scalability no service needs. Rejected: web-framework mounting on a number prefix; the prefix cannot be stripped — a drawn page number must be keyable back — which forces a merged-and-disjoint numbering and pervasive accounting.
Lifespan is one context manager¶
Decision: a service opens its resources in one async context manager, writing
them into app.state under StateKey keys.
Because: two halves of one function cannot drift, and the resource is an ordinary
local held across the yield.
Rejected: separate startup and shutdown handlers; they must be kept in step by
hand and must stash what they open where both reach it.
Deployed services live in their own repository¶
Decision: a deployed service lives in its own repository and depends on the
published sextile; the in-tree applications keep the framework honest.
Because: one workspace has one lockfile, and a library must not commit one while
a deployment must — and a service on its own is exercised as a consumer would be.
Rejected: keeping the service in-tree with an ignored lock, or committing the
lock and pinning the library to it.