mirror of
https://github.com/logos-co/logos-test-modules.git
synced 2026-08-30 19:51:16 +00:00
codex/relock-builder-caller-stack
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f03a5d8bf6 |
fix(ext): the two providers agree on echoOptional again, at -> ?tstr
One contract had two providers answering different SHAPES. test_fullapi_ext_rust.lidl declared `echoOptional(v: ?tstr) -> result`; test_fullapi_ext_cpp is header-first and kept deriving `-> ?tstr` from `std::optional<std::string> echoOptional(...)`. Carried as known-ext.json's `ext-optional-return-changed-on-one-provider`, and visible on a second surface as `ext-optional-return-shape-through-the-qt-proxy`. THE WORKAROUND OUTLIVED ITS CAUSE. `-> result` was chosen because `logos-qt-generator --backend consumer` REFUSED `-> ?T` -- null was also how a failed call reported itself on that path, so an empty ?T and a failure were one wire value. That gate is retired: `lidlTypeToQt` maps `?T` to std::optional<T>, so the empty answer is std::nullopt, an inhabitant of its own, and failure travels on logos::CallError, decided by the C ABI return code and never by the value's null-ness. So `-> ?tstr` is emittable today, it is what the C++ provider already derives, and it is what the case table already expects. Reverting the Rust side is what makes the two agree; the alternative (moving C++ to `-> result`) would have meant changing the expectations to match a workaround for a constraint that no longer exists. Changed together, because a proxy that forwards a shape must declare the same one: the Rust LIDL and its impl (`Option<String>` in and out, no success flag standing in for presence), the qtproxy's own interfaces/full_api_ext.lidl, and the qtproxy impl's echoOptional -- which now forwards through sOptStr rather than sResult, and renders with rOpt rather than rResult in the exercise-all and async blocks. Both registry entries are retired. The proxy one had a SECOND, independent half -- a Qt wrapper that could not decode a reply returned a default-constructed LogosResult, and that default is byte-for-byte a provider refusal -- which is fixed in logos-qt-sdk#44 and logos-cpp-sdk#150 and stands whichever shape the contract had chosen. Recorded in the `fixed` entry, because retiring it here without naming that half would credit the shape change with more than it did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
626fbaa032 |
Migrate the last Qt fixtures to interface "universal", retire test_ipc_module, and repoint onto logos-qt-host (#44)
* feat: migrate the two provider-header fixtures to interface "universal"
dummy_module_000000 and test_ipc_new_api_module were the last users of
`logos-cpp-generator --provider-header` (LOGOS_PROVIDER / LOGOS_METHOD).
They were invisible to the "does anything still declare interface: provider?"
check that motivated removing that mode: neither sets the field. Both invoked
the generator BY HAND from a preConfigure hook in this flake, so the only way
to find them was to remove the flag and watch what broke.
Both now derive their contract from a plain impl header:
* the hand-written *_loader.h plugin classes are gone (generated)
* PROVIDER_HEADER / logos_provider_dispatch.cpp drop out of CMakeLists
* test_ipc_new_api_module's 16 methods go from raw
LogosAPIClient::invokeRemoteMethod("mod", "method", ...) — a typo in
either string was a runtime null — to typed modules().<dep>.<method>()
* its `triggeredBasicEvent` becomes a DECLARED logos_events: event, so
consumers get a typed onTriggeredBasicEvent() accessor; it used to be
emitted dynamically by name with no accessor at all
Method surface preserved, checked with lm before/after:
* dummy: identical (noop(), 1 method)
* ipc-new-api: same 16 names and arity. The only difference is int ->
qlonglong on the three integer params, which is the LIDL 64-bit type
contract every universal module already follows (the generated
onMultiArgEvent in test_basic_module_cpp spells it the same way).
checks: ipc-new-api-tests PASS (the live end-to-end one), ipc-tests PASS,
unit-tests PASS.
KNOWN RED: unit-tests-new-api. Its 428-line suite drives the impl through the
legacy mock harness — impl.init(&api) plus a compiled logos_provider_dispatch.cpp
and qt_provider_object.cpp — none of which a universal module has. Porting it
means injecting LogosModules via _logosCoreSetLogosModulesPtr_ and switching
that derivation to --api-style lp. Left as its own change rather than deleted,
because dropping coverage is not a side effect to slip into this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: port unit-tests-new-api to the universal/lp path
The suite drove the impl through the legacy provider harness — impl.init(&api)
plus a compiled logos_provider_dispatch.cpp — neither of which exists once the
module is `interface: "universal"`. Ported rather than deleted: 26 original
tests kept 1:1, 32 passing in total.
* new_api_fixture.h does what the generated plugin glue does in production:
injects the LogosModules umbrella via _logosCoreSetLogosModulesPtr_ and
installs an emit callback. modules() blindly dereferences that pointer, so
the injection is mandatory, not convenience.
* the derivation emits lp-typed wrappers (--api-style lp; it defaulted to qt,
which cannot bind to std-typed call sites) and consumes the deps'
`headers-lp` variant rather than `include` (= headers-qt).
* it also emits the generated `logos_events:` bodies. That is a LINK
requirement: the impl calls triggeredBasicEvent(), whose body is generated.
Only the events file is compiled in, not the 19K C-ABI export wrapper.
Verified the mock still intercepts, which was the whole risk — a non-
intercepting call returns a default-constructed value, so tests can pass
vacuously. It does: lp_invoke delegates to the SAME
LogosAPIClient::invokeRemoteMethod as the legacy path (logos_protocol.cpp:311),
and logos-protocol's own tests/protocol/test_lp_client.cpp already mocks
lp_invoke this way.
Two argument-representation notes, measured rather than assumed:
* args reach MockStore JSON-round-tripped, so a non-negative int is
QVariant(qulonglong), never QVariant(int). wasCalledWith still matches
because Qt6 QVariant equality falls back to numericCompare for numeric
metatypes — but numeric args are asserted via lastArgs() + a typed accessor
anyway, which is the idiom logos-protocol's own lp tests use.
* what WOULD break wasCalledWith is non-numeric drift (QStringList ->
QVariantList, LogosResult -> QVariantMap). No param here has those types.
Six tests added where the port exposed gaps:
* StdLogosResult success/error/map-field — the old suite asserted nothing
about the result-carrying methods, and the Qt LogosResult -> StdLogosResult
change (QVariant value -> nlohmann::json) is exactly what a type swap gets
silently wrong. A LogosResult crosses the wire as its {success,value,error}
OBJECT; my first attempt mocked a bare payload, which decodes to
success == false — and made the "missing key returns empty" test pass for
entirely the wrong reason. Both the positive case and a failed-call case
now pin that distinction.
* triggerBasicEvent asserts the typed emission and its payload; the event was
previously emitted dynamically by name and untested.
Untouched and still red for reasons that predate this branch, confirmed by
building each at the baseline pins: checks `tests`, `thread-safety-tests` and
`qml-modules` (the last dies on `no member named 'add' in 'TestBasicModule'`).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: restore the LEGACY unit-tests block to qt-typed dep headers
My D-a2 change switched basicInclude/extlibInclude to `headers-lp`, but the
replacement had no count and hit BOTH derivations — including the legacy
test_ipc_module one, which still generates its umbrella with the default
`--api-style qt`. The mismatch surfaced as `no matching constructor for
initialization of 'TestBasicModule'` in the generated logos_sdk.h.
Caught by re-running the check and attributing it with a control run rather
than assuming the newer capability_module override was to blame — it was not.
unit-tests PASSes again. Comment added so the two blocks are not "unified"
later without also moving the generator call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(extlib): migrate test_extlib_module to interface "universal"
The last external-library fixture still carrying a hand-written Qt plugin.
The impl is now a plain Qt-free class deriving LogosModuleContext; the
generator derives the LIDL from it and emits the glue, the dispatch and the
C-ABI exports. `test_extlib_module_interface.h` and `_plugin.{h,cpp}` are gone.
The external C library wiring — CMake builds lib/libstrutil.c into a static
`strutil` target and LINK_TARGETS it — is deliberately UNCHANGED. That is what
this fixture exists to exercise, and it is the part a universal migration
could plausibly have broken.
## Method surface, measured before and after with `lm methods --json`
Six methods, all preserved by name and arity. The only signature change is
`int` -> `qlonglong` on countChars/countChar, which is expected and correct:
LIDL numbers are 64-bit.
The two entries that disappear are Qt plumbing, not API — the `eventResponse`
signal and `initLogos(LogosAPI*)`. Neither is part of the module's contract,
and the already-migrated universal modules in this tree (dummy_module_000000,
test_ipc_new_api_module, test_basic_module_cpp) show the same shape.
## Verified
All six checks that reach this module, built by name through the workspace
flake with the local cpp-sdk / module-builder / plugin-qt / capability-module
overrides. Every one PASSes:
ipc-tests 23 passed (legacy test_ipc_module -> extlib, both raw
invokeRemoteMethod and the generated qt
wrapper)
ipc-new-api-tests 23 passed (universal consumer -> extlib over lp)
async-tests 8 passed (incl. asyncCallExtlibReverse)
unit-tests 26 assertions (LEGACY qt-typed consumer compiled against
the now-universal dep's `headers-qt` — the
binding most at risk here, since a universal
module's qt headers are generated from the
derived LIDL rather than copied)
unit-tests-new-api 32 assertions (lp-typed consumer, `headers-lp`)
fullapi-tests 7 passed (control; does not touch this module)
The module's OWN 12 integration assertions live in the `extlib` group of the
`tests` check, which is red at baseline for unrelated reasons. Rather than
skip that coverage, the group was run directly against the check's own
modulesDir and logoscore:
TEST_GROUPS=extlib -> 12 passed, 0 failed, 1 skipped
That covers the strutil round-trips by exact value (reverseString(hello) ->
olleh, uppercaseString(FooBar) -> FOOBAR, countChar(aabaa, a) -> 4,
libVersion() -> 1.0.0), so the C library really is still linked and called.
## NOT verified
* `tests`, `thread-safety-tests` and `qml-modules` were not run. All three are
red at baseline for reasons predating this branch (`qml-modules` dies on
`no member named 'add' in 'TestBasicModule'` — test-qml-backend-module calls
`.add()` on a module that exposes `addInts`). None of them is made better or
worse by this commit; the extlib group inside `tests` was run manually
instead, as described above.
* Linux. Everything here was built and run on aarch64-darwin only.
* countChar's degenerate empty-`ch` path is preserved by construction
(empty -> NUL, which never matches) but is not covered by a test either
before or after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(basic): migrate test_basic_module to interface "universal"
The provider fixture the whole suite is built around: one method per return
type, per parameter type and per argument count (0-5). Its plugin was the last
hand-written Qt loader among the non-QML fixtures. The impl is now a plain
Qt-free class deriving LogosModuleContext; `test_basic_module_interface.h` and
`_plugin.{h,cpp}` are gone, and the generator derives the LIDL from the impl
header and emits the glue, the dispatch, the typed event bodies and the C-ABI
exports.
## Method surface, measured before and after with `lm methods --json`
41 entries before, 39 after. Accounted for exactly:
23 byte-identical
13 `int` -> `qlonglong` only (expected; LIDL numbers are 64-bit)
2 Qt plumbing REMOVED, not API: the `eventResponse` signal and
`initLogos(LogosAPI*)`. Every already-migrated universal module in this
tree has the same shape.
3 REAL signature changes, unavoidable — see below.
The four `returnVariant*` methods KEEP their exact `QVariant` return type.
That is why they are declared `nlohmann::json` and not `LogosMap`/`LogosList`:
the parser maps the bare spelling to LIDL `any` (= QVariant), while the aliases
would have narrowed them to QVariantMap/QVariantList. Likewise
`std::vector<uint8_t>` keeps byteArraySize's `QByteArray` parameter and
`std::vector<std::string>` keeps the three `QStringList` slots.
### The three that could not be preserved
returnJsonArray() QJsonArray -> QVariantList
makeJsonArray(QString,QString) QJsonArray -> QVariantList
urlToString(QUrl) QUrl param -> QString param
`QJsonArray` and `QUrl` have no universal spelling: the C++-to-LIDL map has
`[any]` (-> QVariantList) and `tstr` (-> QString) as the nearest types, and
there is no way to ask for the Qt-specific ones from a Qt-free header. The
VALUES on the wire are unchanged — a JSON array is still a JSON array, and the
URL still crosses as its string form — but the declared types move, and
`urlToString` no longer runs QUrl's normalisation (it never had a caller that
exercised it; see below).
This is a deliberate narrowing of Qt-type coverage and should be reviewed as
such rather than waved through. Note the affected assertions:
returnJsonArray / makeJsonArray only assert "Method call successful", and
urlToString has ALWAYS been skipped ("logoscore cannot pass QUrl params"), so
no test observed the QUrl parameter before this commit either.
## Verified
Six checks, built by name through the workspace flake with the local
cpp-sdk / module-builder / plugin-qt / capability-module overrides. All PASS,
with totals identical to the pre-migration run:
ipc-tests 23 passed (legacy Qt consumer -> this module, raw
invokeRemoteMethod AND generated wrapper)
ipc-new-api-tests 23 passed (universal consumer over lp)
async-tests 8 passed (incl. the generated echoAsync wrapper,
which is generated from this module's
contract)
unit-tests 26 assertions (legacy qt-typed wrappers built from this
module's `headers-qt`, which is now
generated from the derived LIDL instead of
copied from a moc'd plugin — the binding
most at risk)
unit-tests-new-api 32 assertions (lp-typed, `headers-lp`)
fullapi-tests 7 passed (control; does not touch this module)
The module's own assertions live in the `tests` check, which is red at
baseline. Rather than skip them, run_tests.sh was driven directly against the
check's own logoscore and modulesDir, for BOTH the pre-migration and the
post-migration module set:
TEST_GROUPS=basic 41 passed, 1 failed, 10 skipped — both runs
whole suite 174 passed, 2 failed, 25 skipped — both runs
and `diff` of the per-assertion PASS/FAIL/SKIP lines across all 176 is EMPTY.
The one textual difference anywhere in the two logs is the random temp-dir
path quoted inside a failure message that fails in both.
The two failures are pre-existing, reproduced with the identical command
against the pre-migration modules:
* `addInts(3.7, 1.2) [double->int rounding]` — the dispatch rejects a
fractional JSON number for an int param ("expected integer at arg0, got
number"). Byte-identical message before and after; the legacy Qt module
did NOT round it either.
* `getInstancePersistencePath() rooted at temp dir` in
test_context_module_cpp — an expectation that does not match the path
layout the host provisions.
`qml-modules` was built BOTH ways and fails identically — same file, same
line 24, same `no member named 'add' in 'TestBasicModule'`
(test-qml-backend-module calls `.add()`; the contract has `addInts`). Not
caused by this commit and not fixed by it.
## NOT verified
* `tests` and `thread-safety-tests` were never built as derivations. `tests`
is covered above by running its script directly, both ways, which is
strictly more informative; `thread-safety-tests` uses only the dummy module
and cannot see this change.
* Linux. aarch64-darwin only.
* The event PAYLOAD of testEvent / multiArgEvent. The names and arity are
preserved and the emit drivers still return void with the same signatures,
but the `tests` event-system group only exercises the *_cpp twin's typed
events; nothing in the suite subscribes to THIS module's events, before or
after.
* `crashOnDemand` is not exercised here — its consumer is the crash-isolation
test in logos-logoscore-cli, which was not run.
* The three changed signatures are only argued to be value-preserving on the
wire; no test compares the actual serialised bytes of returnJsonArray
before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(basic,extlib): answer in CHARACTERS, and normalise the URL
The two fixtures migrated on this branch were counting, reversing and
reporting BYTES. Every assertion that touched those methods was ASCII, where
bytes and characters agree, so nothing in the suite could see it — that
blind spot is the actual defect, and it is closed here first.
Measured on a live logoscore daemon, both module sets built from this tree
(pre = HEAD, post = this commit; `logoscore call`, aarch64-darwin):
call pre post
stringLength("hello") 5 5
stringLength("héllo") 6 5
stringLength("😀") 4 1
validateInput("héllo").value.length 6 5
urlToString("HTTP://Example.COM/a/../b?x=1") verbatim http://example.com/a/../b?x=1
reverseString("hello") olleh olleh
reverseString("héllo") dispatch_failed: olléh
invalid UTF-8 byte 0xA9
reverseString("a😀b") dispatch_failed: b😀a
invalid UTF-8 byte 0x80
countChar("hello","l") 2 2
countChar("héllo","é") 1 1
countChars("héllo") 6 5
countChar("banana","na") 2 2
countChar("aaa","aa") 3 1
byteArraySize("héllo") 6 6
One character = one Unicode code point, everywhere. Neither predecessor
answered that: the Qt plugins answered QString::length() (UTF-16 units — 2
for the emoji), the universal port answered std::string::size() (UTF-8 bytes
— 4 for it). byteArraySize is the one method that still answers in bytes,
because a bstr IS bytes; it now has an assertion saying so, next to
stringLength's, so the two units are contrasted rather than confused.
## Per method
* stringLength / validateInput.length — count the bytes that are not UTF-8
continuation bytes. One helper, one definition, stated in the header.
* countChar — the needle is a STRING and all of it must match:
countChar("banana","na") == 2. Matches are counted left to right and do not
overlap (countChar("aaa","aa") == 1 — it used to say 3, counting the byte
'a'), and are only accepted where a character starts, so a match can never
be a fragment of a character. An empty needle matches nothing. All of that
is documented in the header and asserted in the suite. "é" answered 1
before too, but only because the FIRST BYTE of "é" (0xC3) happens to occur
exactly once in "héllo"; the Qt module before it took ch.at(0).toLatin1()
(0xE9), a byte that occurs in no UTF-8 string, and answered 0.
* countChars — same character definition as stringLength.
* reverseString — reverses by character and the result is well-formed UTF-8.
Byte reversal was always broken: it splits "é" (C3 A9) into A9 C3.
* urlToString — lowercases the parts that are defined to be case-insensitive
(scheme, host) and leaves the case-sensitive ones exactly as given, so
"/a/../b?x=1" comes back untouched. Userinfo is preserved
("HTTPS://User:Pw@EXAMPLE.com:8080/Path?Q=A#Frag" ->
"https://User:Pw@example.com:8080/Path?Q=A#Frag"), a string with no scheme
is returned unchanged, and an already-lowercase URL is unchanged — which is
what logos-logoscore-cli's test_integration.cpp asserts for it.
## What still goes through libstrutil
This fixture exists to exercise the external C library, so the calls were
kept wherever the C library can carry the work without producing a wrong or
ill-formed answer. Verified in the shipped dylib, not just in the source —
`nm` shows all six strutil symbols defined in the image and `otool -tvV`
shows the call sites:
reverseString strutil_reverse still does the reversal (`bl
_strutil_reverse` inside the impl); the module then
re-reverses each multi-byte run, which byte reversal
necessarily left backwards.
uppercaseString entirely strutil (ASCII-only mapping; bytes >= 0x80 pass
lowercaseString through untouched, so "héllo" -> "HéLLO" — documented,
not fixed: real Unicode casing needs a case table this
fixture has no reason to carry).
countChars strutil_count_chars still supplies the byte length (`bl
_strutil_count_chars`); the module discounts the
continuation bytes.
countChar strutil_count_char still counts a single ASCII needle
(`bl _strutil_count_char`) — an ASCII byte cannot occur
inside a multi-byte sequence, so its byte comparison IS a
character comparison there.
libVersion entirely strutil.
NOT through the C library any more: the multi-byte / multi-character needle
path of countChar, and the repair half of reverseString. strutil compares
and moves single bytes; neither answer can be expressed that way.
## Assertions
13 new rows in the `basic` and `extlib` groups, one per behaviour above plus
the byte/character contrast. Expectations changed (old -> new):
validateInput(hi) "Method call successful" -> '"length":2'
The old expectation asserted only that the call returned. It could not
tell 5 from 6, which is how the drift shipped. Strengthened, not moved.
No other expected value was changed, and no assertion was weakened: the
per-assertion diff of a full-suite run before and after contains only
additions, the three un-skips below, that one rename, and the random temp-dir
path quoted inside a failure that fails identically both ways.
Three skips were false and are now real assertions:
urlToString "logoscore cannot pass QUrl params" — it can, and since the
migration the parameter is a plain string anyway.
joinStrings "logoscore cannot pass QStringList params" — it can, as
`json:[…]`.
byteArraySize "logoscore cannot pass QByteArray params" — it can, as bytes.
## Verified
All 9 checks built BY NAME through the workspace flake with the local
cpp-sdk / module-builder / plugin-qt / capability-module overrides, at HEAD
(pristine copy) and at this tree:
check pre post
async-tests PASS PASS
fullapi-tests PASS PASS
ipc-new-api-tests PASS PASS
ipc-tests PASS PASS
unit-tests PASS PASS
unit-tests-new-api PASS PASS
tests FAIL FAIL 174/2/25 -> 187/2/22 (see below)
qml-modules FAIL FAIL identical error, unrelated
thread-safety-tests FAIL FAIL byte-identical derivation
`tests` is red on both sides on the same two assertions, neither touched
here and both reproduced against the pre-change modules:
* addInts(3.7, 1.2) — the dispatch rejects a fractional JSON number for an
int parameter ("expected integer at arg0, got number"). The expectation
encodes Qt's old lossy coercion; fixing either side belongs to whoever
decides that policy, and is not this commit's to decide.
* getInstancePersistencePath() in test_context_module_cpp — the expectation
does not match the path layout the host provisions.
`qml-modules` fails on test-qml-backend-module calling `.add()` on a module
whose contract has `addInts` — same file, same line, same message on both
sides. `thread-safety-tests` is the SAME derivation hash on both sides (it
only uses the dummy module) and is killed identically.
## NOT verified
* Linux. aarch64-darwin only.
* uppercaseString / lowercaseString on non-ASCII beyond "bytes pass through":
that pass-through holds in the C and UTF-8 locales, and a single-byte
toupper() in a Latin-1 locale could still corrupt a multi-byte character.
Not asserted, because the assertion would be locale-dependent.
* Grapheme clusters. A combining sequence is N code points here, by the
stated definition, and reversing it separates the marks from their base.
* No test compares serialised bytes before/after for the methods this commit
does not change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(extlib): make case mapping locale-independent, and correct a false claim
The header, README and commit message all claimed uppercaseString/lowercaseString
pass bytes >= 0x80 through untouched. An adversarial review measured that this
was FALSE in the locale the module actually runs in:
lowercaseString("HÉLLO") -> dispatch_failed: invalid UTF-8 byte at index 3
480 of 503 non-ASCII code points returned invalid UTF-8 from lowercaseString,
23 of 503 from uppercaseString (every 3- and 4-byte character), and 7 more
came back as a silently DIFFERENT character (µ->U+009C, ε->Μ, Cyrillic е->М).
Cause: strutil used toupper()/tolower(), which are LOCALE-DEPENDENT. In the C
locale the pass-through claim holds; in en_US.UTF-8 they map ~30 of the 128 high
bytes, including the UTF-8 lead bytes 0xC3/0xE4/0xF0 — so they destroy 2-, 3-
and 4-byte characters. The shipping locale is the corrupting one. "héllo" ->
"HéLLO" was a cherry-picked survivor: 0xC3/0xA9 have no UPPERcase mapping, which
is why the mirror-image lowercase call was the one that failed.
That is the same dispatch_failed/invalid-UTF-8 failure the previous commit was
written to eliminate, still live in two of six methods here and documented as
safe.
strutil now compares 'a'..'z' / 'A'..'Z' explicitly, so the pass-through is real
in every locale. The case mapping stays ASCII-only and stays inside the C
library, which is what this fixture exists to exercise. Three assertions added
for the non-ASCII round-trips — the previous reason for not asserting them
(locale-dependence) was backwards: the CALL FAILING is perfectly stable and
assertable.
VERIFIED: ipc-tests PASS with this in place.
NOT VERIFIED BY ME: a live-daemon measurement of the two case methods. My ad-hoc
daemon failed even on pure-ASCII uppercaseString("hello"), so the harness was
wrong and I did not trust any reading from it. The three new assertions live in
run_tests.sh, whose `tests` check is red for two unrelated pre-existing reasons,
so they have not been observed passing either. Someone should run them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(basic-cpp): answer stringLength in characters, mirroring test_basic_module
test_basic_module_cpp is the pure-C++ mirror of test_basic_module, but after the
universal migration the two disagreed on non-ASCII: the Qt-derived one counted
CHARACTERS while this one still counted BYTES (s.size()), so
stringLength("héllo") was 5 there and 6 here, and validateInput's length field
likewise. Same for the code-point definition of "character": one emoji is 1.
The blind spot is the point: this module's group only ever asserted ASCII, where
bytes and characters coincide, so two modules that exist to mirror each other
could drift apart invisibly. Two non-ASCII assertions added — the same pair the
sibling carries, so a future divergence fails on both sides.
The helper is copied rather than shared: these fixtures are deliberately
standalone (this one's whole point is that it compiles with no Qt anywhere), and
a shared header between them would weaken that. The comment on each says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: link the unit tests against logos-qt-host, not logos-qt-sdk
The Qt host runtime the two IPC unit-test binaries link — LogosAPI,
LogosAPIProvider, the provider base classes and the legacy QMetaObject
adapter — now lives in logos-plugin-qt and is exported as
logos-qt-host::logos_qt_host. Point both tests/CMakeLists.txt at that
target and prefix, and give the flake its own logos-plugin-qt input.
logos-qt-sdk is dropped entirely from this repo: the only thing it ever
supplied here was the host runtime. Nothing consumes the qt consumer
emitter or the LpBridge headers from it — the pinned qt-sdk's
include/cpp does not even carry logos_qt_lp_bridge.h.
Two details worth keeping:
* logos-plugin-qt's logos-protocol follows logos-liblogos', so the
logos_qt_host archive and the protocol the test binary links are ONE
build. logos_qt_host is static and its exported target carries
logos-protocol::logos_protocol, so two protocol store paths would put
two protocol archives on a single link line.
* The source layout gains logos_qt_arg_decode.cpp, listed
unconditionally rather than behind an EXISTS guard:
qt_provider_object.cpp routes every incoming argument through
logos::qtArgDecode, and a guard would turn a missing checkout file
into a late undefined symbol instead of a configure error.
find_package(logos-qt-host) stays REQUIRED and the target reference stays
unguarded, so a rename can only produce a hard configure failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): rev-pin the B4 stack, and keep the qt seam headers reachable
Every input this repo reaches through now names an explicit rev, because none
of the four branches involved has landed on its master and a plain
`github:logos-co/<repo>` url lets `nix flake update` silently walk each one
back to a master that cannot satisfy this flake:
logos-module-builder c60d4a9 feat/sdk-codegen-b4-qt-host-repoint
logos-liblogos f2a15ef fix/b4-align-protocol-with-qt-host
logos-logoscore-cli 24ad063 feat/sdk-codegen-b4-qt-host
logos-plugin-qt cc24fa1 feat/b4-qt-host-windows-target
cc24fa1 is the SUPERSET sibling of feat/b4-qt-host-windows-target-8ccb1fc and
the exact rev logos-module-builder pins for BOTH logos-plugin-qt and
logos-plugin-core. With this lock, `logos-qt-host` evaluates to ONE derivation
(1xbf6fy9…-logos-qt-host-0.1.0.drv) whether you reach it through this flake's
logos-plugin-qt or through logos-module-builder's — the unit-test binaries and
the module plugins link the same runtime, not two copies of it.
Consequences of landing this repoint on top of master:
* master moved the SDK inputs (logos-cpp-sdk, logos-protocol) from
logos-liblogos.inputs to logos-module-builder.inputs so the generator and
the headers it emits move together. logos-plugin-qt's logos-protocol
follows the BUILDER's now rather than liblogos's — same invariant, new
address. logos_qt_host is a static archive whose exported target carries
logos-protocol::logos_protocol, so two protocol store paths would put two
protocol archives on one link line. Verified: both resolve to
yq3q02yp…-logos-protocol.drv.
* logos-qt-sdk no longer supplies the host runtime, but it does NOT go away.
It owns the Qt<->lp seam headers (logos_qt_lp_bridge.h, logos_qt_wire.h),
and the builder at c60d4a9 emits the Qt-typed dependency wrapper as a
VENEER over the lp path: the `headers-qt` output the LEGACY unit tests
compile now opens with `#include "logos_qt_lp_bridge.h"`. Dropping the
input outright — which is what "nothing here needs logos-qt-sdk" implied
when this branch was written against liblogos's older pin — made
`.#checks.<sys>.unit-tests` fail with a `file not found` inside generated
code. So logos-qt-sdk is back, headers-only: no archive is linked, no
source is compiled from it, and only the `unit-tests` derivation gets it.
`unit-tests-new-api` consumes `headers-lp`, whose wrapper has no such
include, and is deliberately left alone.
logos-nix moves e637a1f -> 6e0f4a7 as well, and it is not cosmetic: cc24fa1's
flake calls `logos-nix.lib.forAllTargets`, which e637a1f does not export, so
with `logos-plugin-qt.inputs.logos-nix.follows = "logos-nix"` in place every
check failed to EVALUATE ("attribute 'forAllTargets' missing"). 6e0f4a7 is the
rev logos-module-builder, logos-liblogos and logos-plugin-qt itself all lock,
and it pins the same nixpkgs (e9f00bd) e637a1f did — so no Qt moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(qtproxy): a cdylib provider with Qt-typed consumers
Retires the LAST caller of --provider-header, a generator mode D11 removed.
This module invoked it by hand from a preConfigure hook and had NO `interface`
key, which is exactly why module-builder's guard never caught it — the guard
reads a field this module did not set.
It could not simply become `interface: "universal"`: it is the conformance
matrix's THIRD consumer surface (qtproxy-sync / qtproxy-async), the only
subject measuring the QT-TYPED consumer path. known.json records that the
C++/Rust proxies cannot substitute — both are lp-client consumers and share
py's blind spot — and that this surface is where Q1 was found. Making it
universal used to flip its wrappers to lp and delete that coverage.
It is now expressible: `interface: "universal"` + `codegen.consumer_api_style:
"qt"` — a cdylib provider (std logos_module_impl.h C ABI) whose CONSUMER
wrappers are Qt-typed, binding through LpBridge::forOrigin with this module's
own name as origin and no LogosAPI anywhere.
47 methods -> 45. The two removed are useWrapper/currentWrapper, which
existed to switch every forwarded call between the generated wrapper and a
hand-written veneer. Since B5 the Qt wrapper IS the veneer — both run over
lp_* — so that axis measured one thing twice. Retired deliberately, recorded
in known.json with the reason, not dropped. The FullApiVeneer went with it;
it was the only reason this module needed a raw LogosAPI*.
0 events -> 15. Emission moves from a dynamic emitEvent() on the Qt provider
object to a declared logos_events: block serialised through nlohmann into the
cdylib event bridge. That bridge has silently lost uint64 in this tree
before, so it was measured rather than assumed, at the values that would
expose it:
fireUintEvent(18446744073709551615) -> uintEvent:18446744073709551615
fireIntEvent(-9007199254740993) -> intEvent:-9007199254740993
Exact both ways, through the repo's own fire/getLastEvent round-trip idiom.
Verified: builds standalone with NO overrides; the plugin defines
logos_module_accept_token (the property the origin-binding gate keys on, i.e.
this image fills its own TokenManager by the C-ABI route rather than needing
LpBridge::syncTokens); fullapi-tests 7/7; lm methods diff accounts for every
difference; the emitted wrapper contains zero LogosAPI.
check_contract_copies.py had to change too — it parsed this header as kind
`cpp-qt`, and that machinery is now unreachable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(ipc-new-api): cover async consumption from a Qt-free module
test_ipc_module was the only module asserting async inter-module calls, and it
is a Qt consumer — so migrating it off the legacy interface would have deleted
the repo's only async coverage. Its universal successor covered zero async.
This closes that hole, which is the precondition for retiring the Qt-consumer
original rather than a change to it.
Four methods mirroring the originals by name. On the Qt consumer three were RAW
invokeRemoteMethodAsync and one went through a generated wrapper; here all four
go through wrappers, because on the lp surface `<name>Async` IS the generated
wrapper and bottoms out in lp_invoke_async. So these assert something the Qt
ones cannot: async delivery into a module with no Qt in its own TUs.
WHY concurrency: "multi" IS REQUIRED, and is not incidental here. A Qt-affine
client is bound to the Qt main thread, and its completions are marshalled back
to that thread. Under `single`, dispatch runs on the main thread too — so a
method that blocks waiting for its own completion is blocking the exact thread
that has to deliver it, and every call burns its full timeout and returns a
default-constructed value. That is logos_thread_marshal.h's documented hazard
("only pumps events while it happens to be blocked inside a call") reached from
the other side. The Qt consumer never hit it because QEventLoop PUMPS while it
waits; std::future::wait_for does not. Under `multi` the handler runs on a
worker QThread and the main thread stays free to marshal.
Measured, not assumed: under `single` seven of the eight assertions failed with
empty/zero results. Note the eighth, asyncCallBasicAddInts(0, 0), PASSED —
the timeout sentinel is 0, which is also the expected answer. It is kept
deliberately, next to (3, 4), so the pair cannot both go green on a stall.
The promise is held by shared_ptr rather than by reference into the frame: on
the timeout path the frame is gone while the call is still outstanding, and a
late completion would otherwise write through a dangling reference — corrupting
memory precisely when something has already gone wrong. The wait is bounded so
a stall names the method that stalled instead of surfacing as the harness's own
timeout.
Requires the qt-host-generator capture-list fix: this module has a void method
and no result method, the combination that did not compile under multi.
Verified: 31 passed, 0 failed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: retire test_ipc_module, now fully duplicated
It was the last `interface: "legacy"` provider in the repo, and it no longer
covers anything of its own. Its 21-method surface is mirrored 1:1 by
test_ipc_new_api_module, and the Qt-typed consumer style it exercised now has a
dedicated home in test_fullapi_qtproxy (`consumer_api_style: "qt"`).
Removing it is what makes an absent `interface` mean "not a provider" rather
than "a provider whose glue silently went missing" — see the throw added to
logos-module-builder alongside this.
Groups `ipc` and `async` go with it, and so does the `unit` group, whose binary
came from the deleted derivation and would otherwise have gone permanently SKIP.
The `multi` group is repointed at the successor rather than deleted: it tests
sequential -c chaining, not the module, and the successor answers the same three
calls.
The async group is NOT simply gone. Its assertions were ported to the
ipc-new-api group in
|
||
|
|
f8077fab02 |
fix: the record event emitter is typed now — pass the Blob, not its JSON (#43)
* fix: the record event emitter is typed now — pass the Blob, not its JSON rust-sdk#38 made typed event emitters record-aware, so emit_blob_event takes `&Blob` instead of `&serde_json::Value`. This module's call site passed `&v.to_json()` and no longer compiles. Its own comment described the old signature as a workaround — "the emitter takes raw JSON even for a record parameter, so the record is encoded here through its own to_json" — which is exactly what #38 removes. The comment now says what is actually true: the generator calls to_json on the author's behalf, so an event payload cannot drift from the one a record RETURN produces. THE WIRE IS UNCHANGED. `v.to_json()` is the encoder the generator now calls, so the payload is byte-identical — including the tagged bstr field the old comment was protecting. The conformance case `event/Record` (conformance/ext-cases.json) pins that payload and is untouched: {"id": "e", "n": 7, "payload": {"_bytes": "aGk"}} Atomically coupled to the builder bump: this module cannot compile against both the old and the new emitter signature, so the flake.lock bump and the source fix have to land together. !! flake.nix currently pins logos-module-builder to the BRANCH commit of !! logos-module-builder#188 so this is buildable before that merges. Revert to !! `github:logos-co/logos-module-builder` and re-lock to merged master before !! landing. Verified: `nix build .#test_fullapi_ext_rust` succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: re-pin logos-module-builder to merged master Reverts the temporary branch pin now that logos-module-builder#188 has landed as ddddd8c. The lock's narHash is unchanged from the branch commit, so the squashed master commit is byte-identical content — the build this PR was verified against is the build it now gets. Verified: nix build .#test_fullapi_ext_rust resolves to the same store path as before the re-pin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: take the SDK triple from module-builder, not from liblogos The unit tests compiled code the BUILDER's generator emitted against headers reached through a SIBLING's inputs: logosSdkPkg = logos-liblogos.inputs.logos-cpp-sdk.packages.${system}.default; logos-liblogos and logos-module-builder pin logos-cpp-sdk independently, on different schedules, so the two drifted: module-builder cpp-sdk dfd4628 generator emits #include "logos_async_result.h" liblogos cpp-sdk 5f63af6 header does not exist yet The moment logos-cpp-sdk#132 taught the generator to emit that include, the unit tests stopped compiling with generated_code/test_basic_module_api.h:14:10: fatal error: 'logos_async_result.h' file not found Neither repo was wrong on its own — the generator shipped the header it needs, and liblogos was simply pinned elsewhere. The bug is asking a sibling for the SDK that a DIFFERENT repo's generator determines. Now sourced from logos-module-builder, so generator and headers move together by construction. Not caused by the emitter change in this PR, and not new: module-builder 97ffa59 (#186, before any of this work) fails identically, and master only passes because it is pinned five builder releases back — to a cpp-sdk that predates #132, where the generator does not emit the include at all. Verified: `nix build .#checks.<system>.unit-tests` fails on this branch before this commit with the error above, and succeeds after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fbd9d161b3 |
conformance: sweep the prose two landings left behind (#42)
An audit of every factual claim in the conformance corpus against a fresh
green run of both flake checks. 29 stale sites, 9 files, no declaration
changed — the registry layer is correct and stays exactly as it was.
Almost all of it is two landings that were never swept:
* OPT1 + the C++ ext provider (logos-cpp-sdk#125). The ext table is
described throughout as Rust-only, single-provider, with no
differential, an Optional family "red on purpose", and two module
sources that announce "DOES NOT COMPILE TODAY" — while the check
measures 39 cases x 2 providers, 76 pass, differential-provider-pass
37, and compiles both modules to get there.
* the Qt consumer axis + Q1b. Two cases.json paragraphs say "this
matrix has no driver for that surface" and point at known.json M3
under `unmeasurable`; the driver runs three consumer points and M3
moved to `xfail` long enough ago that `unmeasurable` now holds only
the note explaining it moved.
The worst of it is the README, which is the front door: its hand-kept
known-broken table had drifted to eleven rows against a registry of four,
listing M1/M1b, M2, M4, M5/E1, E2, E3 and OPT1 as current defects after
each had moved to `fixed[]`, and omitting M4-residual, the live entry with
the most cells. That table is now the GENERATED one — run_matrix.py --md
already writes it and the flake check drops it at $out/known-broken.md —
pasted verbatim, with the regeneration command next to it. A table that
cannot be hand-edited cannot drift again.
Also corrected there: the documented command line omitted the two
--proxy-consumer flags that are the whole consumer axis, and the exit
status was listed as three statuses when the driver checks six. The three
missing ones — dead-skip, skip-passes, setup-failed — are each a way for a
run to look green while measuring less than it claims, which is the
opposite of a detail.
`expect_error` (cases.json schema comment) documented an expectation key
no driver ever implemented. A case written the way that paragraph
described takes have_want=False and is filed `skip`, which is not in the
failing-status set: present in the table, counted as coverage, asserting
nothing. The paragraph now documents `{"__error__": "<code>"}`, which is
what exists; logos-logoscore-py grows the guard that makes the mistake
impossible rather than merely undocumented.
Verified: both flake checks pass against this tree, and the full-contract
report diff against the pre-sweep run is ONLY the two known.json blocks
the report reproduces verbatim — every count, cell and coordinate
identical. The ext modules were recompiled, which is what proves the
comment edits did not touch code.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
a969c20e96 |
test(conformance): the LIDL optional contract, as cases, before the implementation (#37)
* test(conformance): the LIDL optional contract, as cases, before the implementation
`?T` is declared by every contract and read by no generator, in any language.
This writes down what it must mean, as 17 cells in the full_api_ext table plus
the contract delta on both ext providers, so the implementation has a target
instead of a behaviour to bless.
The contract these cells encode:
* `?T` is TWO-state — a value of T, or empty — never three-state. Forced by
"one LIDL type maps to one type per language": every target has exactly one
empty inhabitant (None / nullopt / invalid QVariant / undefined).
* Omitted and explicit-null are the SAME state on DECODE and DIFFERENT on
ENCODE. Empty is spelled by OMITTING the key in a NAMED slot (a record
field) and by null in a POSITIONAL one (arg, return) — a positional slot has
no key to omit and the arity must never change. The round trip is therefore
CANONICALISING, not identity, and several expectations here are deliberately
not the argument echoed back.
* A present-but-wrong-typed value in an optional slot is STILL AN ERROR.
Optional widens the domain by exactly one inhabitant; it does not disable
type checking. `hostile/Optional/scalar/wrong-type` is the cell that stops
`?T` = `any` from passing, which is what all three backends do today.
* The two spellings the spec binds to one meaning (`? name: T` and `name: ?T`)
must emit identical code. Both are in the contract on purpose: today they do
not even agree at runtime — the field flag is checked against the base type,
the type kind falls to `Ty::Any` and is not checked at all.
Both ext providers are authored AT THE TARGET, which means neither compiles
until the generator work lands, and the ext check is red at build time until
then. That is recorded in known-ext.json (OPT0) rather than hidden: a fixture
written against the current behaviour cannot be the thing the fix is measured
against. Every cell that cannot pass is registered (OPT1: optionality is
unimplemented; OPT2: an empty optional in a positional slot is unspeakable on a
wire where null already means METHOD_FAILED, and the py driver cannot send a
top-level null at all), so the suite is honestly red now and reports xpass —
which fails the run — the day the generators are right.
The registry entries record what was MEASURED about the generators by executing
them: --header-to-lidl silently derives `any` from std::optional, the cdylib
gate rejects `?T` outright, and the emitted record codec ignores the field flag
in both directions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: re-pin logos-module-builder for the optional SDK stack
Carries logos-lidl#7, logos-protocol#37, logos-rust-sdk#31, logos-cpp-sdk#125
and the two module-builder re-pins (#177, #178) that make them reachable.
Both ext providers now BUILD, which is what OPT0 recorded as impossible:
nix build .#test_fullapi_ext_cpp -> exit 0 (was: undefined template
Codec<std::optional<std::string>>)
nix build .#test_fullapi_ext_rust -> exit 0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* conformance: OPT1/OPT2 must declare which consumers they were measured on
run_matrix.py refuses to start otherwise:
known.json xfail entry 'OPT1' has no `consumers`; an xfail must state which
surfaces it was measured on
The entries were written while the ext providers could not build (OPT0), so
the driver had never reached them to reject them. Both are measured on the py
consumer, which is the only driver the ext table has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* conformance: OPT1 is closed — measured, not assumed
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
d4c0d04644 |
feat(conformance): the LIDL type conformance matrix (#28)
* fix(fullapi-ui-qml): normalize event payloads to native JS Qt exposes a list-type event payload (e.g. a [tstr] event) to QML as a non-native JS *sequence* (Array.isArray === false), so the plugin's deepEqual — which uses Array.isArray — rejected received list payloads even when their contents matched (scalars and objects were unaffected). JSON round-trip the received payload to native JS arrays/objects, exactly how method results already arrive (logos.callModule returns a JSON string this plugin JSON.parses). Scalars pass through unchanged. This unblocks the events step of the basecamp-fullapi-ui-qml doctest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(conformance): the LIDL type matrix — case table + xfail registry One question per cell: does a value of LIDL type T, in position P, survive provider R -> consumer K intact? `full_api` had 31 methods and 14 events and the native check made SIX assertions against them. The rest was cardinality — the C++ proxy reports `intList=3 uintList=2`, which passes whatever the elements became — or an aggregate `ALL_OK` asserted over the COMBINED output of both providers, so one provider alone could satisfy it while the other was broken. That is not a weak test suite, it is a structural one: with per-consumer hand-written checks, covering a type costs work in every consumer, so it does not happen. conformance/cases.json is the language-neutral table every driver replays. conformance/known.json is the xfail registry. Both live here, with the providers they describe; the drivers live with the client each one uses (the py driver is in logos-logoscore-py, which already depends on this repo). What the table adds beyond what existed: the whole VALUE axis. Boundary (int64 min/max, uint64 max, 2^53+1, all 256 byte values, embedded NUL, empty string/bytes/list/map), hostile (-1 into a uint, 3.7 into an int, mixed-type array elements) and adversarial (a user map whose only key is `_bytes`; a map carrying `__logos_pending_call__`). Every expectation was MEASURED against both providers, not assumed — several claims that looked obvious were wrong. Four properties make it honest: - a red cell is a COORDINATE (`[uint]/method_arg/test_fullapi_rust/py`), never an aggregate token; - every case runs against BOTH providers and their answers are compared to each other independently of `expect` — that differential needs nobody to know the right answer in advance, and it is what surfaced the `void` divergence; - a known-broken cell is a registry entry, and one that starts passing is an `xpass` that FAILS the run, so a fix cannot land unnoticed; - coverage is computed from the .lidl: a declared (type, position) with no case fails the run. That is the guard against 31-methods-6-assertions recurring. Adding a type now costs one LIDL line, one impl method per provider and N table rows — zero per-consumer cost. Four defects are registered with their measurements (known.json): M1/M1b a uint64 above int64max degrades to a double once nested in a container; exact as a top-level scalar. The C++ provider LOOKS green for [uint] because its typed decode coerces the double back — the Rust provider takes it untyped and reports the loss faithfully. Same defect, one surface masking it. M2 `void`: the C++ provider answers JSON true, the Rust one fails the call outright. M3/M4 the `_bytes` and `__logos_pending_call__` keys are forgeable from user data — one silently reinterprets a map as bytes, the other hijacks the call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(conformance): multi-argument arity + a check that the 5 contract copies agree Two of the six positions had ZERO coverage: an argument in slot k of n, and an event parameter in slot k of n. Every `full_api` method took 0 or 1 argument and every event exactly 1, so nothing anywhere exercised positional dispatch — and both generators emit positional code (`args.at(i)` / `args::as_*(args, i)`). A `bstr` had also never appeared anywhere but first. method echoTriple(i: int, s: tstr, b: bstr) -> tstr method fireTripleEvent(i: int, s: tstr, b: bstr) -> bool event tripleEvent(i: int, s: tstr, b: bstr) The return is a digest — `i=<decimal>|s=<utf8>|b=<lowercase hex>` — so ONE comparison pins all three values AND their order: swap two arguments and it changes. A container return would have confounded "the arguments landed in the right slots" with "the container encoding survived". Measured, since the multi-parameter EVENT path had never run at runtime anywhere and was worth checking rather than assuming: both providers answer `i=-7|s=hé|b=00ff`, and the event arrives as {arg0, arg1, arg2} with the bstr (NUL + high byte) decoded. The driver now compares the ordered argument list, so slot order is part of the assertion. Coverage now distinguishes `method_arg` from `method_arg@k`: a sole argument cannot catch a generator that mixes up positional slots, so they are different cells. A multi-argument case declares the (type, position) PAIRS it covers — `int`@0, `tstr`@1, `bstr`@2 — rather than the cross-product of its type list and position list, which would claim cells it never exercises. Second half: check_contract_copies.py. `full_api` exists in FIVE hand-maintained copies — the C++ provider's impl header (the contract is derived from it), the Rust provider's .lidl, the shared interface .lidl, the C++ consumer's .h, and the Rust proxy's re-export .lidl — and nothing enforced that they agree. Adding a method to one and forgetting another produces no error anywhere: each side compiles against its own copy and the matrix only talks to the providers, so the drift surfaces much later as "the interface does not have that method". Adding this arity surface meant editing all five by hand, which is exactly the moment to add the check. It compares method and event SIGNATURES (name, parameter types in order, return type) in LIDL spelling, mapping C++ declarations through a closed table. An unrecognised C++ spelling FAILS rather than being skipped — silently ignoring a type is how a checker like this becomes decorative. Verified it catches both a missing method and the subtler drift of two parameters silently reordered. Matrix: 79 cases x 2 providers, green. fullapi chain check and the module suite still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(conformance): the full_api_ext contract — records, bytes at depth, typed maps The composite tail of the matrix. These types cannot go in `full_api`: it is implemented by both providers, the C++ one is header-first, and the C++ cdylib backend's typeSupported() gate rejects records and [bstr] BY NAME while its impl-header parser skips `struct` entirely — a header-first C++ provider cannot even declare a record. Adding them there would break test_fullapi_cpp's build rather than test anything. So: a separate contract, Rust-first, with one provider and therefore no differential column. That is a gap, recorded as one, not a design choice. New: test-fullapi-ext-module-rust (a contract-first Rust cdylib), ext-cases.json, known-ext.json, and a `conformance-matrix-ext` check. 20 cases, full contract coverage, green with 9 registered xfails. It found three defects on its first run — all measured, none assumed: E1 A bstr NESTED in a container is UTF-8 mangled. `00 80 ff` comes back as `00 EF BF BD EF BF BD` — every byte >= 0x80 replaced by U+FFFD and re-encoded. Through a [bstr], a {tstr:bstr}, a {tstr:[bstr]}, and a record's bstr field. A top-level bstr is exact on every provider, so the loss is nesting-specific. This is precisely what the canonical {"_bytes"} tag was introduced to prevent (protocol #21/#23), defeated one level down. Also reachable through the EXISTING full_api surface — echoMap/echoList/ echoAny with a tagged value inside — so three cells and registry entry M5 were added to the main table too, where they are red on BOTH providers. The corruption is INBOUND, deducible rather than guessed: echoBlob decodes the field into Vec<u8> and re-encodes it with the same tagged encoder the scalar path uses, and returned SIX bytes for a three-byte input. It faithfully re-tagged an already-replaced value, so neither the module nor its return path is where the bytes are lost. E2 An EMPTY bstr nested in a container arrives as `null` and fails the call: `expected bytes at arg0[1].payload, got null`. Distinct from E1 — dropped rather than corrupted, which is the louder failure mode. A top-level empty bstr is exact. E3 M1 (uint64 above int64max degrading to a double once nested) seen through a record field, with the exact path: `expected integer at arg0.n, got number`. Neither E1 nor E2 is fixed here. Both are wire-representation questions in the same family as M1, and I could not localize the hop by reading — the protocol's own JSON/QVariant bridge handles bytes and unsigned correctly and documents the hazard. Guessing a site and "fixing" it would be worse than recording what was measured. The driver's provider set is now data-driven (`--modules NAME=DIR`), so a table with a different provider set runs through the same driver rather than a second one; the differential simply has nothing to compare when a table has one provider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(conformance): a C++ ext provider — the ext table gets its differential test_fullapi_ext_cpp mirrors test_fullapi_ext_rust method for method, so the composite tail of the matrix (records, bytes at depth, typed maps, nested composites) is now checked on BOTH surfaces instead of one. It is header-first like test_fullapi_cpp: the records are C++ structs in the impl header and the contract is derived from them, which is possible only because the impl-header parser learned `struct` (logos-cpp-sdk 3fd6841). What the second provider immediately bought, in its first run: - {tstr:int}, {tstr:tstr} and [[int]] round-trip EXACTLY on C++. Those types were previously not expressible there at all — the gate rejected them by name — so this is new coverage, not a re-check. - the differential surfaced that the two providers DISAGREE on every bytes-at-depth cell, and in an informative direction: the C++ codec type-checks the byte field and REJECTS the mangled value (`expected bytes at arg0.payload, got string`), while the Rust side takes [bstr] untyped (serde_json::Value) and passes the corruption through. Same defect (E1), loud on one surface and silent on the other. Fixing E1 makes both green; until then the C++ behaviour is the better one. The registry is now built FROM the measurement rather than by hand, with per-case provider precision: 4 of E1's 9 cases fail on only one provider, and registering them against both made the passing provider report `xpass` — the registry manufacturing a failure. The driver honours `per_case_providers`. One correction worth recording. I first wrote this module contract-first (interface: "cdylib" + codegen.impl_class) and it SIGSEGV'd on the first call; a minimal one-method version crashed identically, and so did a build with the completely unmodified pinned generator — which looked like proof that the contract-first C++ cdylib flavour is broken on master, since no module in the workspace uses it. It was not. My CMakeLists came from the Rust template, which has no SOURCES because the crate is a staticlib; without it the C++ impl translation unit is never compiled, the plugin links with the impl's symbols undefined and crashes on first call instead of failing to build. The comment in the CMakeLists now says so. 20 cases x 2 providers, green with 22 registered xfails. The 5 full_api contract copies still agree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(conformance): E1/E2 are fixed by the canonical codec — verified, not merged E1 (a bstr nested in a container UTF-8 mangled), E2 (an empty nested bstr arriving as null) and M1/E3 (a uint64 above int64max degrading to a double once nested) are ALL fixed by logos-protocol `feat/canonical-codec` — the branch that folds the Qt and plain-wire copies into one canonical LIDL<->JSON codec. That branch is 3 commits, a clean fast-forward onto master, and its own 199 tests pass. Measured against it, not predicted: full_api : 11 cells flip to xpass (every M1 and M5 cell) ext : ALL 22 xfails flip to xpass, and the differential goes from 10 pass / 9 xfail to 19/19 — the two ext providers now agree completely So the answer to "fix E1 and E2" is that the fix already exists and was never landed. The entries stay registered here because the workspace pins the PRE-fix protocol, which is what CI runs; when the re-pin lands they report xpass and these entries must be deleted — the registry's own forcing function. How it was found, since the investigation was misleading in an instructive way: every layer I tested in isolation came back CLEAN — the QVariant/json bridge, the plain transport's QVariant/RpcValue conversion, QDataStream marshalling, the whole CLI-to-daemon chain. That was because I was compiling the LOCAL protocol, which already has the fix, while the running daemon used the PINNED build. Probes on both sides of the boundary settled it in one run: core_service saw the tagged value and the module received a mangled string. One consequence to expect at re-pin time, recorded in each hostile case's `why`: the Rust provider becomes STRICT. echoUint(-1), echoInt(3.7), echoBool(1), a mixed-element [tstr], a scalar for [any] and a scalar for {tstr:any} all change from a coerced value to dispatch_failed, because the host stops coercing before the module's LIDL-type validation sees the value. That is the more correct behaviour — the provider now enforces its declared types — but it IS a semantic change, and the pinned expectations will go red until updated. Not re-pinned here: protocol sits deep in the tree and that semantic change should be a deliberate, reviewed call rather than a side effect of this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(conformance): retire what the codec fixed, and register what it did not The re-pin (logos-protocol 362b03f via cpp-sdk 3d322bd, module-builder, logoscore-cli) landed the canonical codec, so the registry has to be settled. Retired, because they now pass: M1 / M1b a uint64 above int64max degrading to a double once nested M5 a bstr nested in a container UTF-8 mangled E1/E2/E3 the same two defects through the ext contract, plus M1 seen through a record field The ext table is now FULLY green — 40/40, no xfails, differential 19/19. Its cases stay exactly where they are: they were red cells, they are now regression guards, which is the whole point of pinning a defect as a case rather than a comment. Registered, because it did NOT pass — M6: echoUint(2^64-1) method -> 18446744073709551615 exact uintEvent(2^64-1) event -> 1.8446744073709552e+19 degraded Same value, same process, one hop later. The canonical codec fixed the method path; an event payload leaves the module by a different route and still loses a value whose type its container does not declare. Worth saying how M6 surfaced: `event/uint/boundary` was one of M1's cases, so deleting M1 wholesale turned it from a registered xfail into a hard failure. The registry caught a fix that covered most of an entry's cases but not all of them — exactly the thing an xfail list is supposed to prevent you from waving through. The six hostile expectations are re-measured, not adjusted to taste: the Rust provider now answers dispatch_failed where it used to coerce (-1 for a uint, 3.7 for an int, 1 for a bool, a mixed-element [tstr], a scalar for [any] or {tstr:any}), because the host stops coercing before the module's LIDL-type validation sees the value. The C++ side stays lenient, so the divergence moved rather than closed — each case's `why` records that. full_api: 156 pass / 8 xfail (M2 void, M3/M4 the forgeable reserved keys, M6). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: re-pin onto the merged protocol/cpp-sdk chain logos-module-builder -> ab0c776 (protocol 362b03f, cpp-sdk 3d322bd) logos-liblogos -> 5c8b9f0 (protocol 362b03f, after #167) logos-logoscore-cli -> f4753dd This makes test_fullapi_ext_cpp build: its records need the impl-header parser and cdylib codec from cpp-sdk 3d322bd, which reach here through module-builder. test_fullapi_ext_rust still does NOT build — its Blob/Wrapper come from the Rust provider codegen in logos-rust-sdk#29, which is still open. module-builder pins rust-sdk a55fdac (pre-records), so that module needs: rust-sdk#29 merged -> module-builder bumps rust-sdk -> re-pin here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: re-pin onto the merged rust-sdk records support logos-module-builder -> 9ac3235 (logos-rust-sdk 2eabc95, via #167) logos-logoscore-cli -> a143727 (64-bit call args, #74) This is the commit that makes test_fullapi_ext_rust build: its Blob/Wrapper structs come from the Rust provider codegen in logos-rust-sdk#29, which reaches here only through module-builder's by-rev rust-sdk pin. All four conformance providers now build against a single merged closure: test_fullapi_cpp, test_fullapi_rust, test_fullapi_ext_cpp, test_fullapi_ext_rust. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |