Skip to content

forms+investigations: allRequiredEngaged is flat and string-blind, the registrar's namespace is not the cost, and the post-commit tail has no seam (refs #793, refs #791, refs #789) - #795

Merged
Yaraslaut merged 1 commit into
masterfrom
lane/793-791-789-investigations
Sep 24, 2026
Merged

Yaraslaut merged 1 commit into
masterfrom
lane/793-791-789-investigations

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Three investigations, one commit. Each ticket asked a question before it asked
for code, and in each case the cheapest plausible answer — "the framework
already provides this and nobody found it" — was tested first. It held once,
partly held once, and failed once. Full findings are on the three issues; this
is the summary and the one change.

The change — refs #793

forms.hpp's file comment offers allRequiredEngaged(*this) as the model
validate() body thirty lines under a heading that reads "Nested aggregates
(recursive, cycle-safe)". That heading is about schemaJson<A>(). The
readiness helper is flat. The @par Readiness helper block and the function's
own Doxygen now say so, and say that the "non-quantity members are not checked"
exclusion sweeps up std::string — the single commonest thing a rung's
hand-written body tests. Comment-only; docs/spec/forms/forms.md already
stated both facts precisely, and is unchanged.

#793 step 1 — is allRequiredEngaged missing, or just undiscoverable?

Neither. It is discoverable and it is present; it answers a narrower
question than the rungs ask.

Classifying all 103 single-expression validate() bodies across the seven
rungs (script-parsed, and the total matches the issue's own 103):

bucket count reachable by allRequiredEngaged?
return true, action has no empty-capable member 29 it would also return true — no gain
body tests exactly the required empty-capable members 39 exact drop-in
body rests on std::string::empty() (alone or with an id) 26 no — std::string has no hasValue()
other predicates (length bounds, isValidYearMonth, >= 0) 8 no
already calls the helper 1 —

Not a single body was found that checks fewer ids than the action has, so
the 39 swap with zero behaviour change.

It does not recurse. Measured with a probe compiled against the real
header — not read off the source:

EmptyCapableField<std::string> = 0   EmptyCapableField<std::int64_t> = 0
EmptyCapableField<std::vector<int>> = 0

[optional<Sub> engaged, Sub's own required id empty]
    hand-written `!b.has_value() || b->validate()` = 0
    allRequiredEngaged                            = 1
[Sub by value + vector<Sub>, their ids empty]
    allRequiredEngaged                            = 1

"Newer than the rungs" is ruled out: the helper predates rung 0 by four weeks.
"Undocumented" is ruled out: five places name it, including
examples/IMPLEMENTATION.md's own rule, which is what a rung author reads.

#791 — why is the registration macro's namespace anonymous?

The premise is inverted, and the namespace is not the cost. A namespace-scope
const object already has internal linkage in C++; the anonymous namespace
adds nothing to that. Two TUs including a header with a bare
const bool x = f(); link fine, and both run the initialiser — nm reports
b (local BSS) either way.

Removing the anonymous namespace from MORPH_DETAIL_REGISTER_ACTION_LOCAL and
recompiling kanban's board_model.hpp with the rung's own -O3 command:
19.19 → 19.21 CPU-s, best of two each. The registrar symbols stay b.

The cost is that the registrar's initialiser expression is written in the
header, so registerActionOnce<M, A> and its glaze children instantiate in
every including TU whatever the linkage is. Internal linkage must stay — with
external linkage the same header would be a duplicate-symbol link error. So no
edit to that macro can reach the 26.93 CPU-s, and the ticket's own remedy —
move the registrars to one TU — is the only thing that can. That is a
public-contract change; written onto the issue, not attempted here.

#789 — does the architecture already answer the post-commit question?

Direction 2 is already answered and the rung helper is redundant.
ActionDispatcher's runner (registry.hpp:687) and Bridge::executeVia's
localOp (bridge.hpp:2128) both catch (const std::exception&) — every
exception, not only domain errors — append Outcome::Failed, and rethrow. That
is logFailureForCurrentException, in the framework, since before the rung.

The candidate answer "post-commit work is another dispatched action" is
architecturally unavailable.
concurrency_and_lifetimes.md states the
opposite invariant: "A model cannot reach the framework that runs it." There is
no seam for a handler to dispatch anything.

Direction 1 is not answered — and the framework's own site has it.
Reproduced with a sink that throws on the Succeeded entry, as IActionLog's
contract says a failing sink must:

dispatch threw: journal sink unavailable
model.committed = 1, model.balance = 10
journal entries = 1
  outcome=Failed error=journal sink unavailable result=

The mutation is durable, the caller is told it failed, and the journal records
the successful write as Failed. recordActionSuccess sits inside the try
whose catch exists to report failures.

Gates

  • clang-format --dry-run -Werror (22.1.8) — clean
  • clang-tidy-diff.py vs origin/master...HEAD, 1 file, 135 warnings
    suppressed by the line filter (so the changed lines were analysed) — rc 0
  • Doxygen --target doc, WARN_AS_ERROR = FAIL_ON_WARNINGS confirmed in the
    generated Doxyfile — rc 0
  • ctest — 3048/3048 passed, 220.64 s

🤖 Generated with Claude Code

https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW

…blind (refs #793)

`forms.hpp`'s file comment offers
`return morph::forms::allRequiredEngaged(*this);` as the model `validate()`
body, thirty lines under a heading that reads "Nested aggregates (recursive,
cycle-safe)". That heading is about `schemaJson<A>()`. The readiness helper is
flat, and its "non-quantity members are not checked" sentence does not say
which ordinary member types that sweeps up.

Measured on this revision, with a probe compiled against the real header
(clang 22.1.8, glaze v7.4.0):

    EmptyCapableField<std::string>        = 0
    EmptyCapableField<std::int64_t>       = 0
    EmptyCapableField<std::vector<int>>   = 0

    [id set, empty name]  hand-written `id.hasValue() && !name.empty()` = 0
                          allRequiredEngaged                            = 1

    [optional<Sub> engaged, Sub's own required id empty]
                          hand-written `!b.has_value() || b->validate()` = 0
                          allRequiredEngaged                             = 1

    [Sub by value + vector<Sub>, their ids empty]
                          allRequiredEngaged                             = 1

Both facts are already stated precisely in `docs/spec/forms/forms.md`; neither
was stated where an action author meets the helper. This adds them to the
`@par Readiness helper` block and to the function's own Doxygen, and changes
no code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
@Yaraslaut
Yaraslaut force-pushed the lane/793-791-789-investigations branch from 067fe5f to 9b6daea Compare September 24, 2026 00:42
@codecov

codecov Bot commented Sep 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Yaraslaut
Yaraslaut merged commit 96ebf6e into master Sep 24, 2026
39 checks passed
@Yaraslaut
Yaraslaut deleted the lane/793-791-789-investigations branch September 24, 2026 04:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant