Contributing to Foundation
This is the contract between Foundation and the people who work on it: how a method is written, named, documented and tested, and what a change must not break. Read it once before your first change. It is short because the code is the long version: when in doubt, copy the nearest Fnd_* method that does something similar.
Foundation is released under the MIT licence. By contributing you agree that your contribution is too.
What Foundation is
Foundation (Foundation Shell) is a 4D component. It runs inside a host database that supplies its own tables and forms, and it never knows what those are. Everything Foundation does for a host, it does through two kinds of method the host provides:
Fnd_Host_*— a dozen methods the host must supply, because a component cannot do them itself (Fnd_Host_ListToArray,Fnd_Host_ExecuteFormula,Fnd_Host_GetPictureFromLibrary, …).Fnd_Hook_*— about thirty methods the host may supply to customise behaviour (Fnd_Hook_Shell_Setup,Fnd_Hook_Rec_New, …). Foundation checks for them withFnd_Shell_DoesMethodExistand calls them withEXECUTE METHOD.
The demo host, Foundation Product Sales, carries a working copy of every one: https://github.com/open4dshell/Foundation-Product-Sales. HooksAndHosts/ holds an annotated example.
The floor is 4D 20 LTS
Foundation compiles on 4D 20 LTS, 20 R, 21 LTS and 21 R. The LTS line never receives R-release features, so anything that arrived in an R release is out, however old it looks:
| Do not use | Arrived | Use instead |
|---|---|---|
Try(expression), Try…Catch…End try | 20 R4 / 20 R5 | ON ERR CALL("Fnd_Gen_SilentError") around the one statement that can raise; restore the previous handler on the next line; test the result |
Direct typing without Compiler_* entries | 20 R4 | keep the Compiler_Fnd_* typing methods current |
Process info, Call chain, Application info | 20 R10 | Process activity, Get call chain, through Fnd_Shell_CommandWrapper |
... in #DECLARE | 20 R3 | the two-form declaration block (see Variadic methods) |
Never take a version claim from memory. Development/commands.json lists every command with the version it appeared in; Fnd_FCS_VersionCapabilities gates features at runtime.
ON ERR CALL is not Catch: it resumes at the next statement. Guard the single statement that can fail, set the result to Null first, and test it after. Fnd_Gen_SilentError is the component's only no-op method; do not add another.
Modules
Methods are grouped by prefix: Fnd_<Module>_<Subject><Verb>, for example Fnd_Dlg_SetIcon, Fnd_Pref_GetValue, Fnd_Wnd_OpenWindow. Every module has:
Fnd_<Module>_Init— runs once per process, builds the module's state. It is never shared; expose a setter or an_Infokey instead.Fnd_<Module>_Info— answers"version","name"and module-specific questions.Fnd_<Module>_Localizewhere the module has strings, read fromlists.jsonthroughFnd_Loc_GetString.
Fnd_Gen is required by everything; Fnd_Shell is the application shell; Fnd_FCS is build-time tooling for Foundation's own developers. It is never published: no Fnd_FCS_* method is shared, and a host that needs something it does gets an Fnd_XXX_* wrapper in the owning module instead. A Foundation method may call into it (ruled 2026-08-23). The Fnd_FCS folder is kept in step with other projects by DevSync, so Foundation-specific tooling goes in Fnd_FCS_Local; Guide/11-Developer-Tooling.md describes the set and AgentNotes/Reference/Fnd_FCS-Classification.md the split.
Method and variable names cannot exceed 31 characters. Class function names and object properties can.
Spelling. Anything resolved by name at runtime uses US spelling: method names, Storage keys, object properties, list names (Initialized, not Initialised), because Foundation was written by an American in 2003 and names are matched, not read. Comments and local variables are the author's choice.
State
Foundation is thread-safe by default and is removing its last interprocess variables. Add no new <> variables and no new process variables that outlive a method. Instead:
- per-process state goes in
Fnd.<Module>, an object on theFndprocess object made byFnd_Init;Fnd_<Module>_Initbuilds it when it isNull, and "reset" isFnd.<Module>:=Null; - shared state goes in
Storage.Fnd.<Module>, a shared object created underUse (Storage.Fnd)with the test repeated inside the lock; every write is insideUse (Storage.Fnd.<Module>); - a form gets its data with
DIALOG(form; Fnd.<Module>)and bindsForm.x; buttons carry no variable, each object's own method does its work, andOBJECT SET VISIBLE(*; "name"; …)addresses objects by name.
Fnd_Dlg is the worked example of a module converted this way.
Prefer ORDA (ds, entities, query) over classic record commands in new or changed code. Prefer the native 4D command over a hand-written equivalent, and let the method become a wrapper: prove the two agree first, with assertions written against the old implementation.
Writing a method
One method per .4dm file. Helpers get their own file, not a Function block. Every project method begins with a boxed header and a #DECLARE:
//%attributes = {"invisible":true,"shared":true,"folder":"Fnd_Text","lang":"en"}
// ----------------------------------------------------
// Project Method: Fnd_Text_Wrap ($text_t : Text {; $width_i : Integer}) --> $wrapped_t : Text
// Wraps the text into lines of at most the given width, joined by carriage
// returns. The width is a character count, not a pixel width.
// Access: Shared
// Parameters:
// $text_t : Text : The text to wrap
// $width_i : Integer : Characters per line (optional, default 72)
// Returns:
// $wrapped_t : Text : The wrapped text
// Created by Jane Smith on 2026-12-03
// ----------------------------------------------------
#DECLARE($text_t : Text; $width_i : Integer)->$wrapped_t : Text
var $numberOfParameters_i : Integer
Rules the header carries, because a program reads it: Fnd_FCS_WriteDocumentation turns these headers into the documentation under Documentation/Methods/ and the IDE tooltip, and it keys on the literal tokens.
// Project Method: Name (call syntax) --> $result : Type. Optional parameters in{ }. The arrow is-->here and->in#DECLARE.// Access: Sharedor// Access: Private, above// Parameters:.// Parameters:then one line per parameter:// $name_t : Type : Description, with:as the delimiters.// Parameters: Nonewhen there are none.// Returns:then// $result_t : Type : Description, or// Returns: Nothing.// Created by <Name> on YYYY-MM-DD. This line ends the documented header; history lines follow it, one per change:// <Name>, YYYY-MM-DD - what changed.- The header says how to call the method and nothing else. Why the code is shaped as it is goes in the body, beside the line it explains, if it is needed at all. Nine times out of ten it is not.
Then:
#DECLAREwith named parameters. No$1, noC_TEXT($1), and no mixing. A method with no parameters and no return has no#DECLAREat all.varfor every local, with a type suffix:_tText,_iInteger,_rReal,_bBoolean,_dDate,_hTime,_oObject,_cCollection,_picPicture,_ptrPointer,_xBlob,_eentity,_esentity selection,_file,_folder,_sig,_func,_vVariant. Avoid tautologies:$myPicture_pic, not$pic_pic.- Read
Count parametersonce into$numberOfParameters_iwhen it is tested more than once. &&and||for check-then-use guards;&and|evaluate both sides, so(Value type($v)=Is date) & ($v=!00-00-00!)throws for a text.- No operator precedence. 4D evaluates left to right. Parenthesise every right-hand side:
$key:=($base+".count"),If ($n>($max+1)). #honours the@wildcard like=does.- Mixed-case commands are functions; ALL-CAPS commands are procedures that return through an output parameter. Check the signature before first use.
If/Elsewith the shorter block first is the house style; earlyreturnis allowed but not used to rewrite working blocks.- New method → add it to
Project/Sources/folders.jsonand, where a typing entry is needed, to the module'sCompiler_Fnd_*method. Deleted method → remove both, and comment its line inaaaModsrather than deleting it.
Variadic methods
A method with an open-ended parameter list carries both declaration forms between literal markers, and Fnd_FCS_ToggleDeclarationBlocks activates the right one at startup:
// ==== BEGIN COMPILER DECLARATION BLOCK
// ---- 4D 20 R3 and later
#DECLARE($module_t : Text; ... : Text)->$result_t : Text
// ---- 4D 20 R2 and earlier
// C_TEXT($0; $1; ${2})
// var $module_t; $result_t : Text
// $module_t:=$1
// ==== END COMPILER DECLARATION BLOCK
Write the body once with ${$i} and Count parameters, and end with return. Never narrow a variadic signature: hosts rely on the open end.
Signatures are contract
Every method with "shared":true in its attributes is callable by a host, at the signature it has, whatever its header says. Hosts call Foundation by name at runtime, so no static search finds every caller. Therefore:
- a signature may grow (a trailing optional parameter, a widened type) and may never shrink or change meaning;
- a shared method is never made private on the strength of a comment; in March 2026 the surface was narrowed from 697 to 451 shared methods that way and host databases broke in the field;
- removal is staged: mark the method
_DeleteMe:=Truein its body, say so in the header, keep it one release, then remove.
Record every host-visible change in Release/UPGRADING.md as Breaking, Behavioural or Additive.
History
Foundation is more than thirty years of work by many hands, since 1993, and the names in it are kept. Never remove or reword a line naming Dave Batton or Walt Nelson, or any other earlier contributor. Add your own line under // Created by when you change a method, and one line to aaaMods, newest first, inside the current year's Case of. Development churn is squashed to one line per method at each release; the detail lives in the repository history.
Tests
- Unit tests are
Test_Fnd_<Module>, one or two per module, filed in theFnd_Testfolder, with a//MARK:-section per function under test and anASSERTmessage that states the expectation and the actual value. Every test ends withALERT(Current method name+" - all tests done."). - A throwaway probe is
bbb_DELETE_<Name>: it sorts to the top, says what to do with it, and is gitignored. - The test that counts is Foundation as a component in a host, in Foundation Product Sales, interpreted and compiled, on 20 LTS and the latest release. A compile check with 0 errors is the gate; warnings are not.
Forms
Foundation has used one form style since 1993. Three dialogs rewritten in 2026 with all their logic in the form method had to be redone, so:
- Code lives on the objects. Every interactive object has its own file in
ObjectMethods/, even if it is one line calling a project method. No central dispatch on object name in the form method. - The form method is a thin shell:
On Loadsetup, cross-object state (enabling buttons), and a tail call toFnd_Gen_FormMethod, which handlesOn Activate(menu bar),On Close Boxand the close-all and quit machinery.Fnd_SqNo_Editorshows the shape. EnableonActivateandonCloseBoxon the form. An object method that handles one event does not test the event code. - Dialog furniture, right to left along the bottom: Save (default button, answers Enter), Cancel (Cmd-period), then auxiliary buttons. Return and Enter are different keys on the Mac, so every dialog also carries two off-form traps just below the visible boundary, labelled Trap Return (
accept) and Trap Escape (cancel),sizingY: move, not focusable. Entry fields disable the Return trap while they hold the focus.Fnd_Dlg_OSX_Dialogis the reference. The close box behaves as Cancel. - Metrics: a single-line input at the form font is 17 pixels high, not the 21 the editor draws.
- Multi-page editors: shared furniture on page 0, one page per mode.
A host's data-entry forms are a different kind of form, table-bound and inheriting Fnd_IO_InputForm; Guide/06-Input-Forms.md is the guide, with the parent-field and child-listbox patterns and the helpers to copy.
Object names are prefix_name (btn_save, input_email); form names are Fnd_<Module>_<Purpose>. Event names in form.4DForm must match 4D's schema exactly (onAlternateClick), because a wrong name fails silently. Windows open through Fnd_Wnd_* with the menu bar installed and register with Fnd_Menu_Window_Add; Fnd_Art_About is the model. Menus are built at runtime with Fnd_List_AddItem, never in menus.json.
Commits and branches
- One branch per change, named for its ticket or subject in lower case (
fnd-92,dlg-objects), frommain. - Commit the whole tree every time. Nothing is left uncommitted at the end of a change.
- The message says what changed and why in plain sentences; the ticket number goes in it, not in method headers.
- Do not commit
Data/,DerivedData/,Libraries/, local preferences, caches or builds;.gitignorealready knows them. - Run
git config core.hooksPath .githooksonce per clone: the pre-commit hook sorts 4D's own rewrites from real edits and reports, never blocks.
Building
Open Project/Foundation 6.4DProject in the target 4D and run __BuildComponent. The version lives only in Resources/InfoPlist.json; the build derives the target from the running 4D and picks Release/BuildSettings/buildApp-<target>.4DSettings. Release/README.md is the release process and is the source of truth where a script disagrees.
Asking
Open an Issue with the 4D version, the target, and what you saw. A failing Test_Fnd_* section or a ten-line method that reproduces it is worth more than a description.