11. Developer tooling: Fnd_FCS and the Constants module
This chapter is for the person working on Foundation itself, not on a host. Nothing in it is called by a host, and nothing in it is part of the published API. It documents the Foundation Construction Set, the Fnd_FCS methods that compile-check, test, document, build and version-adapt the component, together with the two developer folders they live in and the third-party modules Foundation's development depends on.
What Fnd_FCS is, and what it is not
FCS stands for Foundation Construction Set, a name Dave Batton used for the development-only methods in 2005 (Fnd_FCS_ResizeDesignWindows still carries his comment: "this routine is not included in any of the components"). In Foundation 6 the set is larger and the rule has changed in one respect: the methods do ship inside the built component, because Fnd_FCS_BuildComponent excludes nothing. They are invisible and, with one exception noted below, unshared, so a host never sees them. Being unreachable from a host is the property that matters; being absent from the bundle is not.
Three rules govern the set, all ruled on 23 August 2026 and recorded in AgentNotes/Reference/Fnd_FCS-Classification.md (AgentNotes/Reference/Fnd_FCS-Classification.md):
Fnd_FCSis never published. NoFnd_FCS_*method is shared, so none is callable from a host. If a host ever needs something the set does, the owning module gets anFnd_XXX_*wrapper and that is shared.- A Foundation method may call into
Fnd_FCS. Both are internal to the component, soFnd_Pswd_InitreadingFnd_FCS_VersionCapabilitiesis fine. The earlier and vaguer rule, "nothing outside FCS may call into it", is superseded. - Interpreted only. Every method that changes source, reloads the project or runs arbitrary code tests
Is compiled modefirst and does nothing in a compiled build.
The one apparent exception to rule 1 is Fnd_FCS_Info, which is shared because all thirty-three Fnd_<Module>_Info methods are, and Fnd_Gen_ComponentInfo reaches each by building the name. It answers "version", "name", "devSync", "min4DVersion", "dependencies" and "manifest", and nothing else.
Two folders
The methods sit in two Explorer folders, and the split is deliberate.
| Folder | Holds | Why |
|---|---|---|
Fnd_FCS | the twenty-four methods that are general 4D development tooling: the scheduler, the compile check, the test runner and harness, the documentation writers, the command-table tools, version detection | this folder is the unit that DevSync (below) copies between projects, and it pairs folders by name, so everything in it must make sense in a project that is not Foundation |
Fnd_FCS_Local | Fnd_FCS_BuildComponent, __BuildComponent, Fnd_FCS_Constants, Fnd_FCS_SuggestRestart, Fnd_FCS_DeveloperTools, Fnd_FCS_ToggleDeclarationBlocks, Fnd_FCS_ToggleTypingBlock, Fnd_FCS_ListToTextFile, Fnd_FCS_TrnsListToJSON, Compiler_DeleteThese, MethodExport | Foundation-specific: they build this component, write its constants, or are wired to its method names; a name that pairs with nothing is what keeps them from travelling |
Fnd_FCS_Manifest lists the first folder's membership as JSON, and hands it to DevSync_ManifestCheck for verification when that method exists in the project. Inside Foundation it does not, so the manifest comes back marked verified: false, which is the normal answer here. A method's folder is recorded twice, in folders.json and in the folder key of its own //%attributes line, and the two must agree. Promotion into or demotion out of the sync unit is a file move between the two folders, nothing more.
Compiler_Fnd_FCS carries the folder's C_XX typing entries commented out, so the folder arrives complete in a project that still compiles with typing methods. Foundation's live entries are in Compiler_Fnd_MethodParameters; do not uncomment them in Compiler_Fnd_FCS, because from 20 R7 a live C_XX(MethodName; $1) is a compile error, and an edit there is drift against every other copy of the folder.
Startup: Fnd_FCS_DeveloperTools
Foundation's own On Startup and On Exit database methods each make one call, Fnd_FCS_DeveloperTools(Current method name). Interpreted, at startup it:
- starts the compile-check watcher (
Fnd_FCS_SchedStart); - reads what the running 4D can do (
Fnd_FCS_VersionCapabilities) and sets the source to match:Fnd_FCS_ToggleDeclarationBlocksfor the variadic methods,Fnd_FCS_ToggleTypingBlockfor the typing block; - rebuilds the constants file if its stamp is stale (
Fnd_FCS_Constants(True)), restarting 4D once if it did; - records the running 4D in the command table (
Fnd_FCS_CommandTableUpdate), logging to theFnd_FCSlog when anything was written; - deletes the syntax file (
Fnd_FCS_DeleteSyntaxFile).
At exit it stops the watcher and deletes the syntax file again. Compiled, the whole method is a no-op. This is why opening the project in a different 4D rewrites tracked files (see Release/README.md, step 5): the toggles are doing their job, and git restore . afterwards is part of the build procedure.
The compile-check and test scheduler
A watcher process, $Fnd_FCS_SchedStart, polls one request file every five seconds and answers in one result file, both in the project's Resources/:
Resources/schedulerRequest.json you (or a script, or an agent) write this
Resources/schedulerResult.json 4D writes this
Both are gitignored: the pair is rewritten on every tick. The request's action chooses what runs; a request with no action, or an empty file, is a compile check, which is what an empty request has always meant.
{ "action": "compile", "expectedVersion": "2009", "requestedAt": "2026-11-01T09:15:00" }
{ "action": "test", "filter": "Test_Fnd_@" }
{ "action": "test", "method": "Fnd_FCS_WriteDocumentation", "parameters": [{ "silent": true, "inline": true }] }
{ "action": "relaunch" }
{ "action": "restart" }
| Action | Runs | Notes |
|---|---|---|
compile (or none) | Fnd_FCS_CompilerCheck | RELOAD PROJECT, then Compile project with an empty targets collection, which is what makes it a syntax check rather than a build. The result carries syntaxCheckDone, errorCount, warningCount, compilationPath, the error messages with their method and line, and timeInMilliSeconds. Trust errorCount: syntaxCheckDone only says the compiler finished. |
test | Fnd_FCS_TestRunner | runs the methods named by method, methods or filter (wildcards honoured), passing up to five parameters; an empty request runs every Test_Fnd_@ and Fnd_Test_@ method. The result lists each method's passed, failed, failures and error, and an overall success. |
relaunch | Fnd_FCS_SchedRelaunch | stops the watcher and starts it again. The only way an edit to Fnd_FCS_SchedStart itself takes effect, because RELOAD PROJECT refreshes the child processes but not the running loop. |
restart | RESTART 4D | after a short delay. |
Every result begins with the object Fnd_FCS_RequestEcho builds: the 4D version that answered (v4D), when the watcher took the request (requestedAt) and, where the client supplied them, expectedVersion with a versionMatch verdict and requestAgeSeconds. Send expectedVersion on every request. With several 4D versions open on the same project, whichever watcher polls first takes the request, and versionMatch: false is the only way to know it was the wrong one.
A result file is written on every path. Fnd_FCS_CompilerCheckOnErr and Fnd_FCS_TestRunnerOnErr are the error handlers installed around the compile and around each test, and Fnd_FCS_WriteFailedResult writes a payload with errorCount: -1 when the compiler was never reached, so a polling client is never left waiting for a file that will not appear.
Fnd_FCS_CompilerCheck also runs interactively: execute it from the Explorer and the summary goes to the pasteboard (indented JSON, or concise with Option or Alt held) and to an alert. Fnd_FCS_SchedStop asks the watcher to stop; it notices on its next tick.
Four hazards, learned the hard way and written down in AgentNotes/Reference/Scheduler-Port-Guide.md (AgentNotes/Reference/Scheduler-Port-Guide.md):
RELOAD PROJECTis not thread-safe, so nothing here is preemptive, and it does not reload components: editing a component the project hosts needs a restart of both.- A modal dialog in a test hangs the test action silently while compile actions keep answering. Pass a silent flag to anything that might report.
- Editing
Fnd_FCS_CompilerChecktakes two runs to take effect, because its body is loaded before its ownRELOAD PROJECT. - On Windows, write the request to a temporary name in
Resources/and rename it into place; streaming into the watched name lets the watcher read a half-written file and then fail to delete it.
The scheduler is a developer tool, driven by the developer and by AI tooling working alongside them, and it is the channel through which the release procedure's compile check and test run can be made unattended. The port guide lists the sixteen methods another project needs to copy to have it.
Tests
Unit tests are Test_Fnd_<Module>, one or two per module, in the Fnd_Test folder (CONTRIBUTING.md, Tests). They record assertions through Fnd_Test_Assert($ok_b; $message_t) and close with Fnd_Test_End, writing to the Fnd_Test log and tallying in Storage.test, which is what the runner reports. A test runs identically from the Explorer and under the scheduler, because nothing in the harness is interactive.
Fnd_FCS carries a second, portable pair, Fnd_FCS_TestAssert and Fnd_FCS_TestEnd. They keep the same Storage.test contract, so Fnd_FCS_TestRunner reads either, but write their own Fnd_FCS_Test log and reach Fnd_Log only by name, through EXECUTE METHOD, so the folder compiles in a project without it. They exist because DevSync pairs folders by name and a folder whose harness lives in another folder is not a unit. Foundation's own tests keep calling Fnd_Test_Assert; the portable pair earns its place only where the folder is synced.
The documentation writers
Fnd_FCS_WriteDocumentation reads the boxed header of every project method and writes two things from it: the method's comment, set with METHOD SET COMMENTS so the code editor's tooltip shows the call syntax and parameters, and a Markdown page in Documentation/Methods/<method>.md. It keys on the literal header tokens listed in CONTRIBUTING.md (Writing a method): // Project Method:, // Access:, // Parameters:, // Returns:, // Created by. A header that departs from them documents nothing.
It takes an options object or the older positional form:
Fnd_FCS_WriteDocumentation({methodName: "Fnd_Art"; silent: True; inline: True})
Fnd_FCS_WriteDocumentation(""; True; True) // all methods, tooltips, exclude private
methodName is "" for every method, a prefix such as Fnd_Art, or one name. excludePrivate leaves out methods whose header says Access: Private, and an inline pass with it set clears Documentation/Methods/ first, so the folder holds exactly the public set. silent suppresses the final alert and inline runs in the calling process rather than spawning $WriteDocumentation; use both under the scheduler.
Fnd_FCS_WriteClassDocumentation is the companion for classes, which have no METHOD SET COMMENTS equivalent. It writes Documentation/Classes/<class>.md from the block comment at the top of each class file and the comment lines under each Function, the same lines the editor shows as help, so the two cannot disagree. Functions beginning with an underscore are left out. It takes no parameters and alerts about nothing.
The Markdown is tracked in git on purpose (.gitignore says why): only the 4D IDE can regenerate it, output is deterministic, and a diff appears only when a header actually changed. Fnd_FCS_BuildComponent brackets BUILD APPLICATION with two passes, first excluding private methods so the component carries the publishable set, then with every method so the repository's copy is restored. A build stopped between the two leaves the private pages deleted; Fnd_FCS_WriteDocumentation(""; True; False) recovers them. Release/Scripts/package.sh copies the public set out of the built component for the web site, so the site never reads the repository's own Documentation/. Fnd_FCS_ParseParameterLine is the parser Fnd_FCS_WriteDocumentation uses for a // $name : Type : Description line, returning its name, type and description.
Version adaptation
Foundation compiles on 20 LTS, 20 R, 21 LTS and 21 R from one source tree. Four methods make that possible.
Fnd_FCS_VersionCapabilities decodes Application version (two characters of major, one hex digit of R-release, one of revision, so "20A0" is 20 R10) into { version; major; release; key; ellipsis; directTyping; uuidVersion }. ellipsis is True from 20 R3, directTyping from 20 R4, uuidVersion from 20 R10. The setting compilation_path is not a safe signal for any of these, since 20.7 leaves it at "4" while having none of them, which is why the running version decides. Fnd_Pswd_Init, Fnd_Gen_RequiredCompilerMethod and Fnd_Test_VersionCapabilities read it too.
Fnd_FCS_ToggleDeclarationBlocks($ellipsis_b) finds every method carrying the literal markers // ==== BEGIN COMPILER DECLARATION BLOCK and // ==== END COMPILER DECLARATION BLOCK, and activates the #DECLARE with ... on 20 R3 and later or the C_TEXT($0; $1; ${2}) form before it. Seven methods carry the block at the time of writing (Fnd_Gen_ButtonText, Fnd_Loc_FixButtonWidths, Fnd_Loc_FixLabelWidths, Fnd_Loc_GetString, Fnd_Log_AddEntry, Fnd_Msg_PackParameters, Fnd_Shell_OpenTableAdd). It returns { target; changed; alreadyCorrect; unmarked } and writes a method only when its text changed.
Fnd_FCS_ToggleTypingBlock($commentOut_b) wraps or unwraps the block in Compiler_Fnd_MethodParameters between // ==== BEGIN COMPILER TYPING BLOCK and // ==== END COMPILER TYPING BLOCK in /* */. Direct typing rejects C_XX(MethodName; $1) entries outright (788 errors measured on 2026-08-08) while the lower compilation paths require them, so the same source carries the block in one state or the other.
Fnd_FCS_CompilationPath reads compilation_path from settings.4DSettings and puts it in every compile result, because two results at different paths are not comparable.
Fnd_FCS_DeleteSyntaxFile removes Resources/en.lproj/syntaxEN.json. 4D writes it during a full compilation and reads it when the project opens, and a file written by one version misdescribes the parameters to another: one written by 20 makes 21 R reject nearly every call. It is useful only in a built component, where the build recreates it.
The command table
4D renames commands between releases, and a method that depended on a renamed one (Fnd_Gen_ProcessName was lost that way) fails with no warning. Two methods keep a record.
Fnd_FCS_GetCommandNames reads every command name out of the running 4D as { number : name }; pass True to also write Development/<version key>.txt, the listing format the tables were first captured in (21R3.txt, 21r4.txt).
Fnd_FCS_CommandTableUpdate makes sure Development/commands.json knows the running 4D. If the version already has a table, or is already recorded as an alias of one, it does nothing. Otherwise it compares the live table with every one held: identical to one and an alias is recorded under _aliases, costing thirty bytes rather than another thirty thousand (21 R3 is 21 LTS exactly); different and the full table is added under the version key. A new R release therefore costs nothing until 4D actually changes a name, and the change is captured the first time that version opens the project. The file lives in Development/, not Resources/, so it is never built into the component. CONTRIBUTING.md points at it as the source of truth for "when did this command arrive"; never take that from memory.
Constants, and the Constants module
Foundation's constants (Fnd_Wnd_CenterOnScreen, Fnd_Gen_InputForm, Fnd_Log_LevelError, the fo_* listbox keys and so on) are user-created constants: 4D reads any .xlf file at the top level of Resources/, and Foundation's is Resources/Foundation Constants.xlf. The file is generated, never edited by hand.
Fnd_FCS_Constants holds the definitive list, one Constants_Add* call per constant, grouped by theme. It rebuilds the file only when the stamp $lastConstantUpdate_t in its body differs from the constant Fnd_LastConstantUpdate compiled into the running project, which is itself the first constant written. To add, remove or change a constant:
- edit the group in
Fnd_FCS_Constants; - bump
$lastConstantUpdate_tto today's date; - let the next startup rebuild the file, or run the method yourself;
- restart 4D so the new constants are recognised.
At startup Fnd_FCS_DeveloperTools passes True, so the rebuild restarts without asking (a CONFIRM on worker 1 would hang an unattended scheduler). Run by hand it offers the restart through Fnd_FCS_SuggestRestart, called via CALL WORKER so the dialog does not hold up whatever triggered it. It restarts at most once, because after the restart the two stamps agree.
The writing itself is done by the Constants module, eight methods in the Constants folder written by Cannon Smith of Synergy Farm Solutions and included in Foundation with his headers intact:
| Method | Does |
|---|---|
Constants_NewFile("name.xlf") | opens a new XLIFF document in the 4D constants dialect |
Constants_NewGroup("Theme") | starts a group, which 4D shows as a theme |
Constants_AddLong(name; value), Constants_AddReal, Constants_AddString | add a constant to the current group; AddString with one argument uses the name as the value |
Constants_SaveFile | writes the document to Resources/ |
Constants__Readme | the module's own notes and a worked example |
Compiler_Constants | the module's typing declarations |
The files it writes are compatible with the 4DPop Constants Editor. The module creates files; it does not read existing ones. The convention Cannon's readme sets, and Foundation follows, is that a module needing constants gets a <module>_Constants method called from Fnd_FCS_Constants.
Building the component
__BuildComponent is a bare call to Fnd_FCS_BuildComponent, which reads the version from Resources/InfoPlist.json, derives the target (20L, 20R, 21L, 21R) from the running 4D, chooses Release/BuildSettings/buildApp-<target>.4DSettings, rewrites InfoPlist.json and InfoPlist.strings, deletes the scheduler's scratch files so they do not ship, runs the two documentation passes around BUILD APPLICATION, and leaves the result in Builds/Components/Foundation-<target>/. A successful build is logged, not alerted, so it can run unattended; a failed one still stops for a human. Release/README.md (Release/README.md) is the release procedure and the source of truth where a script disagrees.
The rest of the set
| Method | Does |
|---|---|
Fnd_FCS_ResizeDesignWindows | moves and resizes every open Structure, Explorer, Method and Form window to one rectangle; Dave Batton's, from 2005, and the oldest method in the set |
Fnd_FCS_ConfirmListExists(name) | True if a list of that name is in the toolbox |
Fnd_FCS_ListToTextFile({name}) | dumps a toolbox list to Resources/Lists/<name>.txt, prompting for the name if none is passed |
Fnd_FCS_TrnsListToJSON({name}) | gathers a localisation key list and its _EN, _FR, … translations into one JSON object and saves it; a quick tool for inspecting the list triples of chapter 8 |
Compiler_DeleteThese | a holding pen for variables and methods awaiting deletion, so they still compile while the decision is pending; also where _DeleteMe is declared |
MethodExport | an empty placeholder kept for its history; it once backed up method code |
Third-party modules used in Foundation's development
Foundation's development process stands on three pieces of work by Cannon Smith (Synergy Farm Solutions, Hill Spring, Alberta), published for the 4D community and used here with thanks:
- Constants (https://www.synergyfarmsolutions.com, under Code): the eight
Constants_*methods above, shipped inside Foundation and the means by which every Foundation constant is written. Cannon published the module with his UITimer module as code available "as is" to anyone who would like it; his attribution is kept in every method header. His Object module (OBJ_Module) is the model forFnd_Obj_SaveToFile, which the set uses to write JSON files. - DevSync (https://github.com/cannonsmith/DevSync, MIT licence): copies an Explorer folder, with its child folders, methods, classes, forms and their documentation, from the running project to one or more other projects, and updates each target's
folders.json. It is how theFnd_FCSfolder is kept identical across the maintainer's projects, and it is the reason the folder is split as described above. DevSync is not part of Foundation; Foundation only arranges itself so that DevSync can carry the folder. - DevQuickOpen (https://github.com/cannonsmith/DevQuickOpen, MIT licence): Option-Space opens a window that finds methods, classes, forms, database methods, triggers and documentation by partial name, sorted by relevance, in the spirit of Xcode's Open Quickly. Foundation 6 was written with it open; nothing in the repository depends on it.
Neither DevSync nor DevQuickOpen is distributed with Foundation. Install them from Cannon's repositories if you want the same workflow.