macOS bring-up: the designer starts, survives adding an object, and reads correctly - #75
Open
DitriXNew wants to merge 23 commits into
Open
Conversation
…ckage omitted The official macOS Firebird package ships Firebird.framework/Versions/A/Resources/lib/libfbclient.dylib linked against @rpath/lib/libtommath.dylib while carrying no LC_RPATH of its own. @rpath is resolved against the load commands of the image that INITIATES the load — here backend's wxDynamicLibrary::Load in ibInterfaceFirebird::Init — so with nothing to substitute, dyld gives up: Library not loaded: @rpath/lib/libtommath.dylib Reason: no LC_RPATH's found Init() returns false on the failed Load, and the startup dialog then says the infobase could not be opened — with the framework sitting right there on disk. The symptom is indistinguishable from "Firebird is not installed", which is the one thing it is not. Nothing set an rpath here before: CMake's automatic build rpath covers linked dependencies, and fbclient is not one — it is dlopen'd by name at run time. Two directories, each for its own lookup. Resources/lib is what makes the FIRST Load succeed on the bare name; without it that call fails, wx logs "Failed to load shared library" at error level, and the user is shown an alert about a library the very next line loads successfully through the hard-coded framework path. Resources is what resolves @rpath/lib/libtommath.dylib once fbclient is in. They cost nothing where Firebird is absent — dyld skips an rpath entry that does not exist — so the entries are unconditional under APPLE rather than gated on the opportunistic find_library above. This is the rpath half only. The lock directory the embedded engine then wants is a second, independent macOS obstacle, reported separately and left alone here because its correct default is a project decision. Closes open-enterprise-solutions#72 Refs open-enterprise-solutions#73 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…to its own background Every surface this platform draws is painted from a fixed light palette, in code — #FAF7F0 for the metadata tree, #B8C9D4 for the panels around it, and so on. What is NOT set anywhere near those calls is the text colour, so glyphs come from the system. Under a dark system appearance the system duly returns white, and white on #FAF7F0 is invisible: the metadata tree renders as a column of blank rows. Nothing is broken and nothing is logged — the window is simply unreadable. Declaring the appearance is the honest description of what this application is, not a workaround. Its palette cannot follow the system, so it should not claim to. One call, and every system-derived colour — label text, disabled states, focus rings, native scrollbars — agrees with the palette it is drawn against. It goes in ibWxApp rather than in each exe: designer, enterprise, codeRunner and launcher share the base and the palette both. Before DoOnInit() so it lands ahead of every window — wx wants it at the start of OnInit, and MSW can only apply it before controls exist. The result is deliberately ignored: CannotChange is the correct answer on a platform with no such concept, and it is not a startup failure anywhere. This is legibility, not dark-mode support. Real theming means a second palette and a theme-aware source for all 177 hard-coded colours and 225 system-colour uses, which is tracked separately. Refs open-enterprise-solutions#74 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DitriXNew
marked this pull request as draft
September 4, 2026 18:32
… survives adding a module
On macOS, adding a common module killed the designer outright: no dialog, no journal line, no
dump — the window went away. Under a debugger, EXC_BAD_ACCESS (code=2) at 0x16f603ff8 in
___chkstk_darwin: a write to the main thread's guard page, which is a stack overflow rather
than a bad pointer. Same address three runs out of three.
Twelve frames repeated to the bottom of the stack:
OnSetFocus → ibView::Activate(true) → ibFrontendMainFrame::ActivateView
→ wxWidgetCocoaImpl::SetFocus() → -[NSWindow _realMakeFirstResponder:]
→ resignFirstResponder → DoNotifyFocusEvent → a wx focus event, dispatched inline
→ OnSetFocus …
Activating a view moves the focus, moving the focus tells the window that was holding it, and
on Cocoa that telling is synchronous — _realMakeFirstResponder: calls resignFirstResponder
inside SetFocus and wx dispatches the resulting event in the same stack. Windows delivers that
notification through the message queue, which is the only reason this was never seen there.
Adding a common module is just a reliable way in: CreateItem ends in OpenObjectForm, which
opens the module editor and moves focus.
THE GUARD THAT WAS THERE COULD NOT CLOSE THE LOOP, because it asks a question the call it
guards has not yet answered. `m_metaView != docManager->GetCurrentView()` reads as a stop, but
ibView::Activate records the current view LAST — after mainFrame->ActivateView, which is where
the focus actually moves. On re-entry the current view is still the old one, the condition is
still true, and the handler activates again. The KILL_FOCUS branch below had already stopped
trusting that same state for its own reason; its comment describes the same unreliability from
the other end.
So the state this needs is not which view is current but whether this handler is in the middle
of changing that, and only the handler knows it. One flag, both branches covered — KILL_FOCUS
activates too and would recurse identically from the other side — cleared by scope so that an
exception out of Activate cannot leave the tree permanently deaf to focus. A stuck flag would
be a quieter bug than the crash it replaces, and harder to see.
Verified by repeating the exact steps under the debugger: no crash frames, the process stays
up, the module opens.
The recursing handler lives in designer, an executable, so no gtest target can link it —
oes_frontend_runtime_test links frontend and backend only. What is testable is tracked with
the report.
Closes open-enterprise-solutions#76
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… of them The tree-mutation test spot-checked Catalog and Document. The crash that prompted this arrived through the same door — CreateItem → CreateMetaObject — for a kind neither of them covers, and nothing would have noticed if creating a common module had started returning nullptr instead of recursing. Twenty-two kinds now go through it, and the root hosts all twenty-two. THE LIST IS FILTERED BY THE ROOT RATHER THAN CURATED. There is no enumeration of "what may live here" anywhere in the metadata — only the predicate ResolveChild, which answers 0 for a kind an owner does not host and otherwise the variant it wants. So the candidates are named and the ROOT decides which apply: a kind that stops being top-level drops out by itself, and one that becomes top-level is covered as soon as its CLSID is listed. The names are spelled out beside the CLSIDs because a failure that says "CommonModule" is worth more than one that says a 64-bit number. The second test creates two of every hosted kind, which is the smallest case that exercises GetNewName — the thing that keeps a fresh object from colliding with the one already there. The count is PRINTED, not merely asserted. "5 tests passed" says nothing about how much of the taxonomy was walked, and a silent drop from twenty-two kinds to two would still be a green run; EXPECT_GT(hosted, 0) alone would not catch it. ⚠ WHAT THIS DOES NOT COVER IS THE CRASH ITSELF, and pretending otherwise would be worse than leaving it uncovered. That defect lives in ibConfigurationTree::ibMetaTreeCtrl::OnSetFocus, a focus handler inside `designer` — an add_executable, so no gtest target can link it, and oes_frontend_runtime_test links frontend and backend only. A frontend-level reproduction of the recursion would exercise wx and Cocoa rather than our guard, which sits on the other side of that line. Making the exact site testable means moving the designer's tree into a library; that is a larger change than the fix it would cover. Noted while getting here: BUILD_TESTING=ON alone does not produce a linkable suite — test_queryRenderer references ibDatabaseLayerPostgres::Dialect(), whose translation unit is filtered out unless OES_USE_POSTGRESQL is also ON. Nothing states that, and the failure is a bare undefined symbol at link time. Full suite after the change: 1650 tests, 1644 passed, 6 skipped (Firebird batch insert, no live server). Refs open-enterprise-solutions#76 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…stening TWO STRINGS WHERE THERE SHOULD HAVE BEEN ONE. Every message appeared twice, five pixels apart: the bar painted its field, and a wxStaticText child parked at wxPoint(5, 5) painted the same text over it. DoUpdateStatusText is an override of WHEN the text changes, not of how it is drawn, so overriding it never stopped the field underneath. A fixed point cannot follow the bar's height either, so the ghost was off-centre as well as doubled. The child was not gratuitous. wxStatusBarGeneric::DrawFieldText never calls SetTextForeground, so a field is drawn in the DC's default — black — whatever SetForegroundColour says, and the interior palette's #3F5C77 could not be had any other way. But setting the DC's text colour is the WHOLE of what that child was for, and DrawFieldText is virtual: one line there, and the child goes away along with the ghost, while the base's own centring, clipping and ellipsizing apply as they were meant to. ⚠ THE HOOK IS NOT UNIVERSAL, and pretending otherwise would break the Windows build. wx/statusbr.h picks a different wxStatusBar per platform: generic under macOS and GTK, NATIVE under MSW, different again under wxUniversal and Qt, and DrawFieldText belongs to the generic one. So the override is compiled under OES_STATUSBAR_CUSTOM_INK, and where it is absent the native bar draws its own text in the system colour — which is what a user of that platform expects anyway. Deriving from wxStatusBarGeneric directly is not available: wxFrame::SetStatusBar takes wxStatusBar*, and the generic class is its base rather than its descendant. That was tried first and is why the class still derives from wxStatusBar. AND THE WINDOW NOW SAYS WHETHER AN ASSISTANT CAN REACH IT. Assistant access listens on a port and edits the live configuration with the developer's own rights, and nothing on screen admitted it existed — the state was discoverable only by opening the settings, or by noticing a greyed menu item, which is a consequence rather than a statement. A second field at the right-hand end now carries a lamp and the endpoint. The ENDPOINT and not just the word, because when a client cannot connect the first question is always which address and port it should have used, and the answer is then already on screen. Empty when the server is off rather than the word "off": an indicator that reports an absence in every window of every session is noise, and its absence says the same thing more quietly. Polled once a second rather than subscribed — the server does offer notifiers, but reaching one from the frame means an ibMcpNotifier implementation existing purely to flip a bool, while IsRunning() is a plain read. A lamp is a heartbeat by nature. Nothing is touched while nothing changes, because SetStatusText invalidates the field and writing the same string every second would repaint the bar for the life of the process. Verified on macOS against a running server: one message on the left, the lamp and http://127.0.0.1:3737/ on the right, and the field empties when the server stops. The MSW branch (OES_STATUSBAR_CUSTOM_INK == 0) has not been compiled anywhere — it needs an MSVC run. Closes open-enterprise-solutions#77 Closes open-enterprise-solutions#78 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nearly six thousand of them across 453 files in src/engine — 2797 ⭐, 1357 ⚠, 511 🛑 — and the rule for using them existed nowhere. Not in CLAUDE.md, not here, not in the readme. It has been reconstructed from the files by everyone who has added one since 2026-08-11. That is the failure mode this file otherwise guards against, arriving from an unusual direction: "match the code around you" makes an undocumented convention self-propagating, and a newcomer who copies the shape without the grammar spreads it while wearing it out. The four are not interchangeable — ⭐ says a thing was chosen, 🛑 says it was paid for — and a 🛑 on a hypothetical costs the reader nothing to read and everything to trust. Four rows and a paragraph, in the section that already says the local convention wins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DitriXNew
marked this pull request as ready for review
September 4, 2026 19:36
…setting a style wx will not unset
A Debug build never got as far as showing anything. It stopped on an assertion
inside ibFrontendMainFrame::Create:
wincmn.cpp(1625): assert "(m_backgroundStyle != wxBG_STYLE_TRANSPARENT) ||
(style == wxBG_STYLE_TRANSPARENT)" failed in SetBackgroundStyle():
wxBG_STYLE_TRANSPARENT can't be unset once it is set.
The process stays alive behind that dialog, so from outside it looks like a
designer that started and then answers nothing — a refused MCP connection, an
empty window list, no error anywhere.
wxAuiMDIClientWindow derives from wxAuiNotebook and so from wxBookCtrlBase,
whose HasTransparentBackground() returns true; wxWindowMac::Create reads that
and stamps the window wxBG_STYLE_TRANSPARENT before our code runs. wx then
refuses to let it go, on purpose, because wxGTK cannot honour the change.
The interesting part is that Release behaved no differently in effect. There
wxCHECK_MSG returns false and says nothing, so the call has been a no-op on
every platform since it was written. What actually paints the workspace is the
wxEVT_PAINT / wxEVT_ERASE_BACKGROUND pair bound four lines below, which is why
the dead line was never missed.
So it goes, with a note saying why nothing takes its place — the next person to
look at that #ifdef will otherwise reach for the same call for the same reason.
Refs open-enterprise-solutions#80
Closes open-enterprise-solutions#80
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SIGTERM to the designer produced SIGABRT and a full crash report. The reason was
written a second before the process died:
[session] FATAL: registry-thread backend exception:
Unsuccessful execution caused by an unavailable resource.
The main thread is in ibSessionRegistry::Stop(); the Firebird connection is
being torn down under the registry thread; that thread's next heartbeat —
UPDATE sys_session SET lastActive — throws against the dying handle. ThreadBody
catches it and calls Die(), and Die() chose between its two paths on m_started
alone: not started, soft-fail so the producer can report a startup error;
started, std::terminate.
Shutdown is neither of those, and it arrived through the same door.
The reason the hard path exists is stated where it is taken: the registry was
alive and consistent, so pretending otherwise would let stale state propagate
cluster-wide. That reasoning does not survive the transition to shutdown. There
is nothing left to protect — the process is leaving — and a stale sys_session
row is precisely what the next process's eager sweep DELETEs, which is the same
idempotence the soft path above already leans on and says so.
What it cost was worse than untidy. Every ordinary stop left a crash report
behind, which is how people learn to scroll past them; the exit status was a
signal death, so any supervisor stopping the designer saw a failure; and whether
a given run "crashed" depended on where the heartbeat fell relative to the
connection going away.
Now a stop that is already under way is said out loud and returns. The reason
still reaches the trace file and stderr on both paths, so nothing that was
diagnosable before stops being diagnosable.
Refs open-enterprise-solutions#81
Closes open-enterprise-solutions#81
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lled before the control exists
Inserting a table control in the form editor killed the designer outright —
SIGSEGV at a null dereference, before the control was ever on screen:
ibDataViewCtrl::Refresh datavgen.cpp:5334
wxWindowBase::InheritAttributes() wincmn.cpp:1535
wxWindow::Create(...) window_osx.cpp:407
ibValueModelTableBox::Create(...) tableBox.cpp:558
ibVisualEditorInsertObjectCmd::DoExecute() visualEditorCmdProc.cpp:359
Refresh guards four of its five area windows and not the fifth. m_tableAreaWin
is dereferenced unconditionally in both branches, while every optional area
beside it is tested first.
That would be safe if Refresh could only run on a built control, and it cannot.
wxWindow::Create calls InheritAttributes(), which calls the VIRTUAL SetFont —
ours — which calls Refresh, all before the constructor body has run and while
every area is still null. The guards written for the optional areas turn out to
be exactly what the mandatory one needed, and for a different reason than the
ones they were written for.
It is conditional on the parent, which is why a table works in some places and
kills the process in others: InheritAttributes only calls SetFont when the
parent carries an explicitly set font.
The fix is to return early when m_tableAreaWin is null. It is the mandatory
area, so null there means "not built yet", and there is nothing to repaint
before there is anything to paint on.
SetFont immediately above also calls SetRowHeight(GetDefaultRowHeight()) on the
same half-built object, reached by the same path for the same reason. It
survives today and is left alone here rather than changed on suspicion; the
issue records it so it is looked at deliberately.
Refs open-enterprise-solutions#82
Closes open-enterprise-solutions#82
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Debug configuration needs its own directory, since the Release one is configured and kept. build/ was already ignored; build-debug/ was not, so the whole tree showed up as untracked and every `git add -A` was one keystroke away from committing a few gigabytes of object files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The description promised the opposite, in the strongest terms it had:
It does NOT become the configuration the application runs - that is
config_apply, and the two are separate on purpose. This is the diskette:
cheap, safe, and the thing to do before stopping work.
and the note returned after every save sent the caller to database_diff for
"what it does not yet have".
Both are false. SaveConfiguration goes through SaveDatabase(saveConfigFlag), and
the restructure lives inside that flag's branch, which ApplyConfiguration shares
as its own first step. Building a small configuration through MCP issued ten
CREATE TABLE and six ALTER TABLE during plain saves; database_diff then reported
zero differences, because there were none left.
The cost is not tidiness. config_apply with confirm=false is documented as the
only way to see DDL before it runs — the engine has no rehearsal mode — and by
the time it is asked, a caller who followed this description has already saved
several times and the ledger comes back empty. The sentence taught exactly the
habit that defeats the safeguard, and it teaches it hardest to someone who has
just lost work to a crash and resolved to save often.
So both texts now say what the call does, and config_apply's says that its
rehearsal has to be asked for BEFORE the save, with the reading of an empty
ledger spelled out: nothing outstanding, not nothing to come.
Whether the restructure belongs behind saveConfigFlag at all is a question for
whoever owns the save path, and it is left open in open-enterprise-solutions#85 rather than answered
here. A description that lies costs more than one that is merely incomplete, and
that part could be fixed without deciding anything.
Refs open-enterprise-solutions#85
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The corpus answers well for keywords, global functions and the Query family. It holds nothing for the other creatable types — Array, Structure, Container, Table, TypeDescription — or for any of the platform enumerations, of which AccumulationRecordType and DocumentWriteMode are the ones a posting handler needs first. Those are precisely the names that must be spelled exactly and cannot be inferred from an example, and the miss-message sent the caller to three tools that do not answer for them: metadata_tree and metadata_get hold what the CONFIGURATION has, query_fields holds a source's columns, pattern_read holds how things are usually built. A platform type is none of those, so the redirect was correct about everything except the case in hand — and read as "no such word in this language" rather than "not written down yet". Writing two posting handlers through MCP, this cost a detour into valueMap.cpp, valueTable.cpp and accumulationRegisterEnum.h to recover Insert(key, value), Find(value, column), the Structure constructor and AccumulationRecordType.Receipt — every one of them already declared, with a readable signature, in an AppendFunc or AddEnumeration call. The gap is not closed here; it is named, so a miss on one of these reads as what it is. Filling it is open-enterprise-solutions#83, and the registries make it look closer to wiring than to authoring. Refs open-enterprise-solutions#83 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…apply A document's ListRegisterRecord, passed in `properties`, is neither applied nor refused. The object is created bound to no register, the answer shows the property empty with the intended target listed as a valid choice one line below it, and what finally reports the omission is the REGISTER, later and elsewhere, with "Doesn't have any recorder". That is the one thing this server says it does not do. mcp_call's own description promises that a guess is answered rather than swallowed, and metadata_create proves it can: an unknown property name comes back in `refused`, by name, with a reason. The first reading was that relationships cannot be set through this door, and it was wrong. The choice road is live at that point and behaves correctly for its neighbours — ListOwner with a bad name is refused with the list of candidates, and with a good one it is applied and reads back. Nor is an empty choice list the explanation: the failure reproduces with the register already holding two recorders. Only this property behaves this way, and why is not yet established. So this says what was measured and stops there. A fix built on the reading above would have been a guess wearing a fix's clothes, and the property it touches is the one that decides whether a document posts at all. The description now names it and points at metadata_bind, which is the right verb for a relationship in any case and tells the other end as well; the loop carries the same note for whoever picks up open-enterprise-solutions#84. Refs open-enterprise-solutions#84 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`screen_capture` steered a caller away from the expensive answer toward the
cheap one:
It is NOT the way to check a value - query_run and debug_sandbox say WHY a
number is what it is, which a picture never can.
and `debug_sandbox`'s argument note listed it among the places source arrives.
`grep -rn "query_run" src/` finds those two sentences and nothing else — no
registration, no class, no schema. Either it was planned and not built, or it
was removed and the descriptions were not followed.
The steer itself is right, and that is what makes the dangling name expensive:
it appears exactly where a caller is being told what to reach for INSTEAD of a
screenshot, so the sentence is read carefully and the missing verb is looked
for. `debug_sandbox` is the one that exists and does what the sentence promises,
so it now carries the whole claim alone.
Whether the interface should be able to run a query at all is a larger question
and is left open in open-enterprise-solutions#87: today it can parse one (query_check), describe its
sources (query_sources, query_fields) and execute nothing — and the same report
covers the other half, that no verb here writes a row of business data either.
Refs open-enterprise-solutions#87
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…designer is not a launch of nothing RunApplication built the command line from the bare target name — "enterprise --file=…". A bare name is looked up on PATH and in the working directory, and it is in neither; on macOS it is not even a plain file, because the sibling that was built is enterprise.app and the executable sits three levels inside it. The fork then succeeded and the exec failed, and wxExecute still handed back a pid, so the caller was told the application had started while nothing was running. Resolve the binary next to the running one instead, stepping out of the bundle when this process is inside one, and look inside <name>.app/Contents/MacOS when there is no plain sibling. The bare name stays the fallback, so a layout this does not recognise behaves exactly as before, and the resolved path is quoted because wxExecute splits on spaces. Refs open-enterprise-solutions#86 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…and an accept that timed out stops calling itself a connection Launching the application from the designer with debugging on did not produce a debugged application. Two faults, either of which loses the handshake on its own. The designer swept the ports once and then waited 1.5 seconds. SearchServer starts one thread per port and numberOfConnectionAttempts is 1, so on the loopback every one of them is finished within milliseconds of connect-refused — the rest of that wait is spent on threads that have already given up, and nothing rescans afterwards. The client it had just started was not listening yet: its debug server is created in OnInitialize, after wx is up, the database is open and the user is authenticated, which on a cold start is seconds away. The manifest path in the same file already scans in rounds for exactly this reason; the desktop path never got it. The debuggee, for its part, raised the flag that means a connection was accepted whether or not one was. Accept(true) returns null after the socket's own ten second timeout, and the break condition left the loop on m_waitConnection alone — so CreateServer read "accepted" and asserted on the null socket it then found. The comment at the callsite says the wait blocks bootstrap until the debug client connects; it blocked for ten seconds and continued regardless. So: scan in rounds, stopping on success, so the sweep is still running when the client finally binds. Keep accepting until there is a socket. And bound the wait with a deadline that, when it expires, goes on without a debugger and writes a journal line — an assertion here stopped the client dead on a race the person at the window could do nothing about, and in Release it was compiled out, leaving a client that had accepted --debug and would never stop on a breakpoint. Closes open-enterprise-solutions#88 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…put data into a base at all debug_sandbox was the only verb that executes anything, and its transaction was always rolled back — which is what makes running code inside somebody's live session defensible, and also meant no assistant could create a single record. A fresh configuration could be built and never filled, so nothing built through this interface could be looked at running. So the caller says which of the two it wants. `commit: true` commits instead of rolling back; off by default, because undoing is the reason this is safe. A run that fails is rolled back whatever was asked — `ran` is false when the code threw partway, and keeping that half is a document posted without its movements. The person at the application is told which of the two is happening, before the code runs and after it finishes, because being told "nothing it writes is kept" while a commit is about to happen is the one wrong thing to say here. The flag is written last on the wire, so a runtime that predates it stops after the string and rolls back — an old peer in either direction cannot be talked into keeping writes. Refs open-enterprise-solutions#87 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ist, not the base name of a column the table has not got
Balance(period, New Structure("Warehouse", W)) failed outright on any register
whose dimension is reference-typed, which is the ordinary case: Firebird refused
the statement with "Column unknown FLD1053". A reference is stored as three
fields, and the same dimension is spread into all three everywhere else in that
statement - the projection, the GROUP BY, the ORDER BY. Only the WHERE carried
the bare base name.
ReadFilters took GetPhysicalName() off the leaf's column; ReadKeys, five lines
below, asked ibRegFieldsOf for the whole set. So the keys were always right and
the filter never was, and the three copies of the same loop in the accounting
register had it too. A primitive-typed dimension worked, which is how this
survived: it fails only on the dimensions people declare.
The read spec now carries whole conditions rather than column/value pairs, and
the caller spreads each leaf through ibRegCompositeIR - the spreader that exists
for this, and which the flattener's own comment points at. L2-2 stays
metadata-blind, which is the rule its file header states.
Closes open-enterprise-solutions#89
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…akes the row key as the reference it is while (sel.Next()) never finished. Three items in the catalogue, and the loop counted past a hundred - each fetch returning the same first row, because the keyset cursor was never taken. Worse than the loop: Next() runs on the parked session's thread, so a runtime that entered it answered nothing afterwards, and there is no way to cancel a sandbox run. CaptureAnchor read the row key with GetString(). That column is the queryable's primary key, which for a catalogue or a document is its data-reference, and a reference read as a string gives its presentation rather than its uuid. The guid never parsed, so ApplyAnchor kept answering "first row, no anchor", and every fetch was LIMIT 1 with no keyset clause. The same invalid guid is why Ref read off a selector was empty - GetPropVal builds it from there. ApplyAnchor also raised m_hasAnchor while leaving m_anchorSortValues empty, so even a valid guid would have produced no predicate. The register selector twenty lines below does both halves and is what this now matches. Closes open-enterprise-solutions#90 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ents with a tabular section stops segfaulting Walking a document selection died in CompareAsText, one frame under LoadData's first line - m_objectValue was not an ibValueDataObject at all. ibValueSelectorRecordDataObject derives ibValueSelectorDataObject and ibValueDataObject, so a pointer to it needs an offset to become the second. The constructor wrote (ibValueDataObject*)selectorObject, and objectSelector.h was not included here, so the type was incomplete and the C-style cast fell back to reinterpret_cast and applied none. It compiled without a word, and the first virtual call through the pointer read a foreign vtable. The two constructors beside it pass their argument with no cast, because those headers are included - the cast existed only where it silently could not work. Latent until now: with the selection cursor broken the owner guid was always invalid, and LoadData returned at its first line without touching the pointer. Closes open-enterprise-solutions#91 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ns are not blank Balance() and Turnovers() came back with the right number of rows and nothing in them - every dimension empty, every resource zero - while the movements behind them read correctly through Select(). Any posting handler that checks stock saw zero and refused. ibRegSelectionToTable filled each row through GetColumn(name), which reads one scalar field under the read's own output alias. Neither half fits: a dimension is a reference of three fields, and the resource's alias is the storage spelling the read projects (Quantity_Balance) rather than the name the surface publishes. The comment two paragraphs above the loop warns about exactly that drift. GetValue(col) is the accessor that knows a column's field set, and is what every other reader uses. Closes open-enterprise-solutions#92 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…saying the writes were undone The person at the application was told correctly which mode a run was in; the caller that asked for it was not. Every answer carried "Everything this wrote has been undone", including the answers to runs that had just committed - the one sentence a caller acts on, wrong in the case where acting on it matters. A successful commit now answers under `kept`, and a run that failed under commit says so as the reason it was rolled back. Refs open-enterprise-solutions#87 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…onger the one case the handler skips Clicking a section opened its command panel and nothing put it away again: it stood over the workspace with the section still lit. With one section in the configuration - which is where every configuration starts - there was no way out short of closing the application. OnLeftUp ran its whole body only when the clicked button was NOT the active one, so a click on the open section fell through to nothing. And when another section was clicked it dismissed its OWN popup, which is null; the panel standing open belongs to the button that opened it. The click-outside dismissal a wxPopupTransientWindow is supposed to do does not fire here on macOS, so that was the only road left and it went nowhere. Now the button toggles, a click on another section closes the one that is open, Popup clears the pending-dismiss flag so a window is not reused mid-fade, and a dismissed panel is destroyed - one was built per opening and none were deleted. Closes open-enterprise-solutions#93 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Everything found while bringing the platform up on Apple Silicon (macOS 26.6.2, arm64, Apple Clang 21, CMake + Ninja). Sections 7–9 came out of a Debug build, which is where the last three of these had been hiding. Each commit stands alone and carries its own reasoning; the issues are linked per commit.
1. The Firebird client never loads — #72
The official macOS Firebird package ships
libfbclient.dyliblinked against@rpath/lib/libtommath.dylibwhile carrying noLC_RPATHof its own, so dyld refuses:Reason: no LC_RPATH's found.ibInterfaceFirebird::Init()returns false and the startup dialog reports that the infobase could not be opened — indistinguishable from "Firebird is not installed", with the framework on disk the whole time.Nothing set an rpath before: CMake's automatic build rpath covers linked dependencies, and fbclient is
dlopen'd by name at run time. Two directories are added underAPPLE, each for its own lookup —Resources/libso the bare-nameLoadsucceeds (without it wx logs an error alert about a library the next line loads successfully), andResourcesso@rpath/lib/libtommath.dylibresolves.2. The window is unreadable under a dark appearance — refs #74
Backgrounds are painted from a fixed light palette in code; text colour is set nowhere near them, so glyphs come from the system, and a dark appearance returns white. White on
#FAF7F0: the metadata tree renders as blank rows.ibWxApp::OnInitnow declares the application light throughwxApp::SetAppearance, in the shared base so all four GUI apps get it.Legibility, not dark-mode support. Real theming means a second palette and a theme-aware source for 177 hard-coded colours; that is #74, in progress on a separate branch.
3. Adding a common module killed the designer — #76
EXC_BAD_ACCESS (code=2)at the main thread's guard page — a stack overflow, three runs out of three. Twelve frames repeating:Activating a view moves focus; moving focus tells the window that held it; on Cocoa that telling is synchronous. Windows posts it through the message queue, which is the only reason this was never seen there.
The guard that was there could not close the loop:
m_metaView != docManager->GetCurrentView()asks a question the call it guards has not yet answered —ibView::Activaterecords the current view after the focus moves. A re-entrancy flag, both branches, cleared by scope.4. Every metatype the root hosts is created in a test — refs #76
The tree-mutation test spot-checked two types; the crash arrived through the same door for a third. Twenty-two kinds now go through it, filtered by
ResolveChildrather than by a curated list, so the taxonomy stays in charge. The covered count is printed, not merely asserted.This does not cover the crash itself — the recursing handler lives in
designer, anadd_executable, so no gtest target can link it. Making that site testable means moving the tree into a library.5. The status bar said everything twice — #77, #78
ibDocBottomStatusBarput its text in awxStaticTextchild atwxPoint(5, 5)while the bar painted the same field underneath.DoUpdateStatusTextoverrides when the text changes, not how it is drawn.The child was not gratuitous —
wxStatusBarGeneric::DrawFieldTextnever callsSetTextForeground, so the field is drawn black whateverSetForegroundColoursays. Setting the DC's colour is the whole of what the child was for, andDrawFieldTextis virtual.A second field now carries a lamp and the endpoint when assistant access is running — the address, because when a client cannot connect that is the first question; empty when it is off, because an indicator reporting an absence in every window is noise.
⚠
OES_STATUSBAR_CUSTOM_INK == 0has not been compiled anywhere.wx/statusbr.hpicks a differentwxStatusBarper platform andDrawFieldTextbelongs to the generic one, so the override is conditional — and the MSW branch needs an MSVC run before this is trusted there.6. The comment markers are documented — no issue
⭐ / ⭐⭐ / ⚠ / 🛑 appear nearly six thousand times across 453 files and the rule for them was written down nowhere. Four rows in CONTRIBUTING, in the section that already says the local convention wins.
7. A Debug build never reached its window — #80
It stopped on
wxBG_STYLE_TRANSPARENT can't be unset once it is set, asserted from insideibFrontendMainFrame::Create. The process stays alive behind that dialog, so from outside it is a designer that started and then answers nothing: a refused MCP connection, no window, no error.wxAuiMDIClientWindow→wxAuiNotebook→wxBookCtrlBase, whoseHasTransparentBackground()is true;wxWindowMac::Createreads that and stamps the window transparent before our#ifdef __WXOSX__branch runs. wx then refuses to let it go, deliberately, because wxGTK cannot honour the change.Release behaved no differently in effect — there
wxCHECK_MSGreturns false in silence, so the call had been a no-op on every platform since it was written. The workspace ground comes from the paint/erase pair bound four lines below, which is why a dead line was never missed.8. A deliberate stop was reported as a crash — #81
SIGTERMproducedSIGABRTand a crash report. The main thread is inibSessionRegistry::Stop(), the Firebird connection is going away underneath, and the registry thread's next heartbeat throws against the dying handle — "Unsuccessful execution caused by an unavailable resource".Die()then chose between its two paths onm_startedalone: not started, soft-fail; started,std::terminate.Shutdown is neither, and arrived through the same door. The hard path's stated reason — the registry was consistent, so stale state must not propagate — does not survive the transition: the process is leaving, and a stale
sys_sessionrow is exactly what the next process's eager sweep deletes, which is the idempotence the soft path already leans on.It cost more than tidiness. Every ordinary stop left a crash report behind, the exit status was a signal death for any supervisor, and whether a run "crashed" depended on where the heartbeat fell.
9. Dropping a table onto a form killed the designer — #82
SIGSEGVat a null dereference inibDataViewCtrl::Refresh, before the control was ever on screen. It guards four of its five area windows;m_tableAreaWinis dereferenced unconditionally in both branches.Safe only if
Refreshcould not run on a half-built control — and it can:wxWindow::Create→InheritAttributes()→ the virtualSetFont→ ours →Refresh, all before the constructor body has run. The guards written for the optional areas are what the mandatory one needed, for a different reason than they were written for.Conditional on the parent, which is why a table works in some places and not others:
InheritAttributesonly callsSetFontwhen the parent carries an explicitly set font.SetFontalso callsSetRowHeight(GetDefaultRowHeight())on the same half-built object by the same path. It survives today; left alone here rather than changed on suspicion, and recorded in #82 so it is looked at deliberately.10. Three defects in the MCP surface, found by using it — #83, #84, #85
Building a small stock configuration through the assistant interface — three catalogues, two documents, a balance register, two posting handlers — turned up three faults in the tools themselves. The behaviour is unchanged in all three; what these commits fix is the descriptions, which were wrong in ways that cost the caller real work.
config_saverestructures the database — #85. Its description said the opposite in the strongest terms it had ("it does NOT become the configuration the application runs … the diskette: cheap, safe, and the thing to do before stopping work"), and the note after each save sent the caller todatabase_diff. Both false:SaveConfigurationgoes throughSaveDatabase(saveConfigFlag), and the restructure lives in that branch, whichApplyConfigurationshares as its first step. TenCREATE TABLEand sixALTER TABLEwere issued during plain saves here;config_apply {confirm: false}— the only rehearsal the engine offers — then returned an empty ledger, because there was nothing left to rehearse. The sentence taught precisely the habit that defeats the safeguard, most persuasively to someone who has just lost work to a crash. Both texts now say what the call does, andconfig_apply's says the rehearsal must be asked for before the save. Whether the restructure belongs behindsaveConfigFlagis left open — that is a decision for whoever owns the save path.The syntax helper carries neither the creatable types nor the platform enumerations — #83. It answers well for keywords, global functions and the
Queryfamily;Array,Structure,Container,TypeDescription,AccumulationRecordType,DocumentWriteModereturn nothing. Those are the names that must be spelled exactly and cannot be inferred from an example, and the miss-message redirected to three tools that do not answer for them. Writing the posting handlers meant readingvalueMap.cppandaccumulationRegisterEnum.hto recover signatures already declared, in text, beside their registrations. The miss now names the gap.metadata_createaccepts a document'sListRegisterRecordand applies nothing — #84. No refusal, no effect; the document posts to no register, and the omission surfaces later from the register. That is the one thing the server says it does not do, and it demonstrably can — an unknown property name comes back inrefused, by name. My first reading, that relationships fail through this door, was wrong:ListOwnerrefuses a bad name with the candidate list and applies a good one. Only this property behaves this way and I could not establish why, so the description warns and the loop carries a note — no fix, because a fix would have been a guess about the property that decides whether a document posts at all.The interface can build a configuration and then not put a row in it — #87, half fixed. Of 84 backend verbs and 15 designer ones, none creates, changes or reads business data.
query_checkparses a query,query_sourcesandquery_fieldsdescribe what it may read, and nothing runs one;debug_sandboxexecutes code but did so "INSIDE A TRANSACTION THAT IS ALWAYS ROLLED BACK". So a configuration could be built here end to end and then only be filled by hand in the client, or by adding a data processor to the configuration for the purpose.debug_sandboxnow takescommit: true, which keeps what the code wrote instead of undoing it. Off by default, because undoing is what makes running code in somebody's live session defensible; a run that fails is rolled back whatever was asked, since keeping the half that got through is a document posted without its movements. The person at the application is told which of the two is happening, before the code runs and after it finishes — being told "nothing it writes is kept" while a commit is about to happen is the one wrong thing to say there. The flag is written last on the wire, so a runtime that predates it stops after the string and rolls back. This is the executor growing a second mode, not the read verb — reading business data still has no door of its own, and #87 stays open for it. Two descriptions also pointed at aquery_runthat has never existed — that half is fixed here too, since the steer they carry is right and the dangling name is read precisely where a caller is looking for something to reach for.11. A launch from the designer produced nothing to debug — #86, #88
app_runstarted a process that did not exist. The command line was built from the bare targetname, and a bare name is looked up on
PATHand in the working directory — it is in neither. OnmacOS it is not even a plain file: what was built beside the designer is
enterprise.app, with theexecutable three levels inside it. The fork succeeded, the exec failed, and
wxExecutestill handedback a pid, so the caller was told the application had started while nothing was running. The binary
is now resolved next to the running one, stepping out of the bundle and into the sibling's, with the
bare name kept as the fallback so an unrecognised layout behaves as before.
And once it did start,
--debugstill never attached — #88. Two independent faults, eitherenough on its own. The designer swept the ports once and waited 1.5 s;
SearchServerstarts onethread per port and
numberOfConnectionAttemptsis 1, so on the loopback every one of them isfinished within milliseconds of connect-refused, and the rest of that wait is spent on threads that
have already given up. The client it had just launched was not listening yet — its debug server is
created in
OnInitialize, after wx is up, the database is open and the user is authenticated. Themanifest path in the same file already scans in rounds and says why; the desktop path never got it.
The debuggee, for its part, raised the flag meaning a connection was accepted whether or not one
was:
Accept(true)returns null after the socket's own ten-second timeout, and the break conditionleft the loop on
m_waitConnectionalone.CreateServerread that as its signal and asserted onthe null socket it then found — which is how a Debug client stops on a modal before its window
exists. In Release the assertion is compiled out, so
--debugwas accepted and no breakpoint wouldever fire, with nothing anywhere saying why. Now: scan in rounds stopping on success, keep accepting
until there is a socket, and bound the wait with a deadline that goes on without a debugger and
writes a journal line rather than asserting on a race nobody at the window can act on.
12. Four defects between a base with data in it and a report — #89, #90, #91, #92
None of these was reachable before this branch, and that is the finding rather than an excuse: with
no way to create a record through the assistant interface and
debug_sandboxundoing everything itwrote, the whole read path over live business data had never been walked. Adding
commit: trueandseeding a base — three goods, a warehouse, a counterparty, a receipt, a sale — turned up four faults
in a row, each one hidden behind the one before it.
A balance filtered on a reference dimension sends a column that does not exist — #89. Firebird
refused the statement with
Column unknown FLD1053. A reference is three physical fields, and thesame dimension is spread into all three in the projection, the
GROUP BYand theORDER BYof thatvery statement; only the
WHEREcarried the bare base name.ReadFilterstookGetPhysicalName()off the leaf while
ReadKeys, five lines below, askedibRegFieldsOffor the set — so the keys werealways right and the filter never was, and the three copies of the loop in the accounting register
had it too. A primitive-typed dimension works, which is how it survived: it fails only on the
dimensions people declare. The read spec now carries whole conditions and the caller spreads each
leaf through
ibRegCompositeIR, keeping L2-2 metadata-blind as its own header requires.A catalogue or document selection never ends — #90. Three items, and
while (sel.Next())countedpast a hundred.
CaptureAnchorread the row key withGetString(); that column is the queryable'sprimary key, which for a catalogue is its data-reference, and a reference read as a string gives its
presentation rather than its uuid. The guid never parsed,
ApplyAnchorkept answering "first row",and every fetch was
LIMIT 1with no keyset clause.m_hasAnchorwas also raised withm_anchorSortValuesleft empty, so even a valid guid would have produced no predicate. It takes theprocess with it:
Next()runs on the parked session's thread, so a runtime that entered the loopanswered nothing afterwards and looked dead from the designer — and a sandbox run cannot be
cancelled.
Iterating documents with a tabular section segfaults — #91.
ibValueSelectorRecordDataObjectderives
ibValueDataObjectas its second base, so the pointer needs an offset; the constructorwrote
(ibValueDataObject*)selectorObjectin a file that does not includeobjectSelector.h, so thetype was incomplete, the cast fell back to
reinterpret_cast, and no offset was applied. It compiledsilently and the first virtual call read a foreign vtable. The two constructors beside it pass their
argument with no cast at all — the cast existed only where it could not work. Latent behind #90:
with the cursor broken the owner guid was always invalid and
LoadDatareturned before touching thepointer.
Balances and turnovers come back as blank rows — #92. Right number of rows, every dimension empty
and every resource zero, while the movements behind them read correctly.
ibRegSelectionToTablefilled each row through
GetColumn(name), which reads one scalar field under the read's own outputalias — a dimension is three fields, and the resource's alias is the storage spelling
(
Quantity_Balance) rather than the publishedQuantityBalance. The comment two paragraphs abovethat loop warns about precisely this drift. Any posting handler that checks stock saw zero and
refused.
Verified end to end. Receipt of 10 / 25 / 40, a sale of 3 and 5, balances reading 7 / 20 / 40 over
five movements — through
debug_sandboxin the running client, with the debugger attached and thewrites kept. The
commit: trueanswer also stopped claiming the writes were undone when they werenot (#87): a committed run answers under
kept, and a failed one says the failure is why it wasrolled back.
13. The section panel could not be closed — #93
Clicking a section in the client opens its command panel, and nothing put it away again: it stood
over the workspace with the section still lit. With one section in the configuration — where every
configuration starts — there was no way out short of closing the application.
ibSubSystemButton::OnLeftUpran its body only when the clicked button was not the active one, soa click on the open section fell through to nothing; and a click on a different section dismissed its
own popup, which is null, rather than the one standing open. The click-outside dismissal a
wxPopupTransientWindowis supposed to perform does not fire here on macOS, so that handler was theonly road and it went nowhere. Separately,
OnEventButtonbuilt a new popup window per click andnothing ever deleted one.
The button now toggles, a click on another section closes the open one,
Popupclears thepending-dismiss flag so a window is not reused mid-fade, and a dismissed panel is destroyed.
Verified on screen, and with it the whole arc this branch set out to walk: the section opens and
closes, and
Goods balancecomposed over the seeded base reads Основной склад 67 — tea 20, coffee 7,sugar 40 — matching the receipt of 10 / 25 / 40 less the sale of 3 and 5.
Verification
Release and Debug builds with
-DOES_USE_FIREBIRD=ON -DOES_USE_POSTGRESQL=ONagainst Firebird 5.0.4 arm64. The designer opens a fresh file infobase,sys.fdbis created, the journal recordsmetadata / appliedthensession / opened. Adding a common module no longer crashes, verified under the debugger. The Debug designer now starts and serves its MCP endpoint; a configuration of three catalogues, two documents, a balance register and two posting modules was built through it end to end.⚠ Two of the three Debug fixes are argued, not yet demonstrated. #81 needs a stop to observe and #82 needs the form-editor insertion repeated; both are one-line guards whose reasoning is in the commits, and both should be exercised before this merges. Full suite after every fix here: 1650 tests, 1644 passed, 6 skipped (Firebird batch insert, no live server).
Known, not addressed here
/tmp/firebirdisfirebird:firebird 0770and an ordinary user is not in that group, so the designer needsFIREBIRD_LOCKpointed somewhere writable. Firebird's ownisqlfails identically, so it is not an OES defect, but the fix belongs inibFirebirdBootstrap::Init(). Reported as macOS: embedded Firebird cannot use its lock directory — /tmp/firebird is firebird:firebird 0770, so startup fails for every ordinary user #73 rather than patched, because the correct default interacts with how a shared file base is coordinated.app_runon a file base starts a client that cannot attach — app_run offers a launch that cannot succeed: the designer holds the file base, and the code already knows two processes on one cannot work #86. The launch itself is fixed above; what remains is that the designer holds the base. The designer holds the base; the client exits 1 within a second. The cause turned out to be one Firebird setting, not the platform.ServerModeis left at its shipped default ofSuper, the one mode that opens a database "exclusive by a single server process";SuperClassicandClassicboth permit the other-process attachment. With a privatefirebird.confsettingClassic— nosudo, system install untouched — a designer and a thick client run on the same file base simultaneously, zero exceptions, andsession_listshows both. So the remaining fix is likely to ship a conf rather than to change any of this code; it has to be decided together with macOS: embedded Firebird cannot use its lock directory — /tmp/firebird is firebird:firebird 0770, so startup fails for every ordinary user #73, since multi-process coordination also needs a writable shared lock directory, which/tmp/firebirdis not.BUILD_TESTING=ONalone does not produce a linkable suite:test_queryRendererneedsOES_USE_POSTGRESQL=ONtoo, and nothing states that.🤖 Generated with Claude Code