morph::forms has become a domain-specific language: a renderer that is not
morph's own must implement 26 x-* schema keys, 5 document keys (w-steps,
w-title, app-menu, app-screens, app-title) and 13 rule/condition kind
strings, and an application author must reach a C++ declaration surface of 64
public types and 44 public free functions across 10 headers (5,526 lines).
docs/spec/forms/ documents it — 6 files, 4,388 lines — and today it does so
almost completely. The problem is not the prose. It is that nothing can
notice when the prose stops being complete, and this is precisely the
situation morph#233 established a gate for on the other spec that third parties
implement against.
Measured coverage today
Every "x-*" / "w-*" / "app-*" string literal in include/morph/forms/**,
and every public type and free function outside a detail namespace, checked
against the concatenated text of docs/spec/forms/*.md:
| Surface |
Documented |
Wire keys (26 x-* + w-steps, w-title, app-menu, app-screens, app-title) |
31 / 31 |
| Public types, concepts, aliases |
62 / 64 |
| Public free functions |
40 / 44 |
The six gaps are small and worth closing on the way past:
- concepts
HasExplicitSubmit and RuleLiteral are never named in the spec
(the explicitSubmit member is documented; the concept that detects it is
not).
fieldKeyStem, fieldSlotName and withSlot (forms/i18n.hpp) and
groupKindName (forms/layout.hpp). For the i18n three the spec documents
the key format — <actionTypeId>.<wireField>.<slot> — but never names the
functions that build it, so the seam is described and its API is not.
There is also a systematic asymmetry rather than an omission: the rule
constructs AtLeastOneOf, ExactlyOneOf, MutuallyExclusive, ReadonlyWhen,
NotEngaged, Equals, Greater, GreaterOrEqual, Less, LessOrEqual and
Plain appear in the spec only under their JSON-key spellings
(atLeastOneOf, …). A reader emitting JSON is fully served; a reader writing
C++ has to infer the capitalisation of every one of them.
Why the prose being fine today does not settle it
Two gates touch this area and neither can see completeness:
.github/workflows/spec-sync.yml requires only that some file under
docs/spec/forms/ changed when include/morph/forms/** changed. A commit
that adds x-newKey to forms.hpp and a typo fix to choice.md passes.
scripts/check_spec_citations.sh checks 1–3 work outward from
docs/spec/pinned_facts.toml, a hand-maintained list. A key nobody pinned
is a key nobody checks.
Check 4 of that same script is the shape this needs. It exists because
docs/spec/core/wire.md presented itself as exhaustive and was not — the
Envelope table omitted primary and shared, the discriminator table omitted
attach/assign/instances, and four make* factories were missing
(morph#233). Its reasoning applies here verbatim:
Nothing could notice, because morph's own client and server both read
wire.hpp — so neither can observe that the spec disagrees with it. Only a
check that reads both can.
morph's own Qt/QML renderer reads the same headers the schema generator does.
It cannot observe the spec disagreeing with them either. forms.md likewise
presents its key table as the renderer contract ("Renderer contract: the schema
key vocabulary"), and the renderer conformance kit is written against that
claim.
Proposal
Add a check 6 to scripts/check_spec_citations.sh, mirroring check 4:
- every
"x-*" literal in include/morph/forms/** is a row in
docs/spec/forms/*.md;
- every
"w-*" / "app-*" document key likewise;
- every rule/condition
kind string (atLeastOneOf, engaged, equals,
exactlyOneOf, greater, greaterOrEqual, less, lessOrEqual,
mutuallyExclusive, notEngaged, readonlyWhen, requiredWhen,
visibleWhen) is documented as a kind, and its C++ constructor name is named
beside it — which closes the capitalisation asymmetry above by construction.
Inherit check 4's per-category floors, not a lumped total. That check
carries a comment recording why: a lumped count let uint64_t Envelope fields
parse to nothing while the other two categories carried the total over the
line, and the check reported green while ignoring a whole category. A forms
version has the same failure mode across keys / document keys / rule kinds.
Extend scripts/test_check_spec_citations.sh with the matching self-test, as
the other drift-guard checkers have.
Acceptance
- Deleting any documented
x-* row from docs/spec/forms/forms.md fails the
check locally.
- Adding a new
"x-…" literal to a forms header without documenting it fails
the check locally.
- A category that parses to zero fails rather than passing quietly.
- The six gaps listed above are closed and the check passes on
master.
morph::formshas become a domain-specific language: a renderer that is notmorph's own must implement 26
x-*schema keys, 5 document keys (w-steps,w-title,app-menu,app-screens,app-title) and 13 rule/conditionkindstrings, and an application author must reach a C++ declaration surface of 64
public types and 44 public free functions across 10 headers (5,526 lines).
docs/spec/forms/documents it — 6 files, 4,388 lines — and today it does soalmost completely. The problem is not the prose. It is that nothing can
notice when the prose stops being complete, and this is precisely the
situation morph#233 established a gate for on the other spec that third parties
implement against.
Measured coverage today
Every
"x-*"/"w-*"/"app-*"string literal ininclude/morph/forms/**,and every public type and free function outside a
detailnamespace, checkedagainst the concatenated text of
docs/spec/forms/*.md:x-*+w-steps,w-title,app-menu,app-screens,app-title)The six gaps are small and worth closing on the way past:
HasExplicitSubmitandRuleLiteralare never named in the spec(the
explicitSubmitmember is documented; the concept that detects it isnot).
fieldKeyStem,fieldSlotNameandwithSlot(forms/i18n.hpp) andgroupKindName(forms/layout.hpp). For the i18n three the spec documentsthe key format —
<actionTypeId>.<wireField>.<slot>— but never names thefunctions that build it, so the seam is described and its API is not.
There is also a systematic asymmetry rather than an omission: the rule
constructs
AtLeastOneOf,ExactlyOneOf,MutuallyExclusive,ReadonlyWhen,NotEngaged,Equals,Greater,GreaterOrEqual,Less,LessOrEqualandPlainappear in the spec only under their JSON-key spellings(
atLeastOneOf, …). A reader emitting JSON is fully served; a reader writingC++ has to infer the capitalisation of every one of them.
Why the prose being fine today does not settle it
Two gates touch this area and neither can see completeness:
.github/workflows/spec-sync.ymlrequires only that some file underdocs/spec/forms/changed wheninclude/morph/forms/**changed. A committhat adds
x-newKeytoforms.hppand a typo fix tochoice.mdpasses.scripts/check_spec_citations.shchecks 1–3 work outward fromdocs/spec/pinned_facts.toml, a hand-maintained list. A key nobody pinnedis a key nobody checks.
Check 4 of that same script is the shape this needs. It exists because
docs/spec/core/wire.mdpresented itself as exhaustive and was not — theEnvelope table omitted
primaryandshared, the discriminator table omittedattach/assign/instances, and fourmake*factories were missing(morph#233). Its reasoning applies here verbatim:
morph's own Qt/QML renderer reads the same headers the schema generator does.
It cannot observe the spec disagreeing with them either.
forms.mdlikewisepresents its key table as the renderer contract ("Renderer contract: the schema
key vocabulary"), and the renderer conformance kit is written against that
claim.
Proposal
Add a check 6 to
scripts/check_spec_citations.sh, mirroring check 4:"x-*"literal ininclude/morph/forms/**is a row indocs/spec/forms/*.md;"w-*"/"app-*"document key likewise;kindstring (atLeastOneOf,engaged,equals,exactlyOneOf,greater,greaterOrEqual,less,lessOrEqual,mutuallyExclusive,notEngaged,readonlyWhen,requiredWhen,visibleWhen) is documented as a kind, and its C++ constructor name is namedbeside it — which closes the capitalisation asymmetry above by construction.
Inherit check 4's per-category floors, not a lumped total. That check
carries a comment recording why: a lumped count let
uint64_tEnvelope fieldsparse to nothing while the other two categories carried the total over the
line, and the check reported green while ignoring a whole category. A forms
version has the same failure mode across keys / document keys / rule kinds.
Extend
scripts/test_check_spec_citations.shwith the matching self-test, asthe other drift-guard checkers have.
Acceptance
x-*row fromdocs/spec/forms/forms.mdfails thecheck locally.
"x-…"literal to a forms header without documenting it failsthe check locally.
master.