Topology
Topology Is Runtime Truth
Sigil topology is the canonical, compiler-visible declaration of a project's named runtime boundaries.
Topology is not config.
Topology answers:
- what named boundaries exist outside ordinary local computation
- what those boundaries are called in application code
- which environment names exist
Config answers:
- how one named environment constructs the runtime world for those boundaries
Why Sigil Splits Topology from Config
Without this split, runtime truth gets blurred together:
- architecture and credentials live in one file
- app code falls back to
process.env - tools reconstruct the system from strings
Sigil prefers one explicit model:
src/topology.lib.sigildeclares boundary handles and environment namessrc/policies.lib.sigildeclares boundary rules and trusted transforms for labelled dataconfig/exports the selected environment's.lib.sigil worldplus any
env-selected declarations such as flags
- application code uses typed handles from
•topology - application code may also read selected env declarations through
•config., for example •config.flags
- in projects, only config modules may read
process.env
Outside projects, Sigil uses the same constructors without the split:
- standalone files may declare named topology handles directly
- standalone files may build a local top-level
c world=(...:†runtime.World) - standalone files use ordinary local names instead of
•topology/•config --envis only a project-mode concern
Canonical Project Shape
Topology-aware projects define:
src/topology.lib.sigil
config/local.lib.sigil
config/test.lib.sigil
config/production.lib.sigil
Environment names are flexible, but the file path is canonical:
- if Sigil is run with
--env test, the project needsconfig/test.lib.sigil - if Sigil is run with
--env production, the project needsconfig/production.lib.sigil
Canonical Topology Module
src/topology.lib.sigil declares only boundary handles and environment names:
c auditLog=(§topology.logSink("auditLog"):§topology.LogSink)
c assistantShell=(§topology.ptyHandle("assistantShell"):§topology.PtyHandle)
c appDb=(§topology.sqlHandle("appDb"):§topology.SqlHandle)
c exportsDir=(§topology.fsRoot("exportsDir"):§topology.FsRoot)
c local=(§topology.environment("local"):§topology.Environment)
c mailerApi=(§topology.httpService("mailerApi"):§topology.HttpServiceDependency)
c mailerCli=(§topology.processHandle("mailerCli"):§topology.ProcessHandle)
c prod=(§topology.environment("prod"):§topology.Environment)
c test=(§topology.environment("test"):§topology.Environment)
No URLs. No ports. No usernames. No passwords. No env-var names.
Those belong in config.
Canonical Config Modules
Each declared environment gets one config module exporting world:
e process
c world=(†runtime.world(
†clock.systemClock(),
†fs.real(),
†fsWatch.real(),
[†http.proxy(
mailerApiBaseUrl(),
•topology.mailerApi
)],
†log.capture(),
†process.real(),
†pty.real(),
†random.seeded(1337),
†sql.deny(),
†stream.live(),
†task.real(),
[],
†timer.virtual(),
†websocket.real()
):†runtime.World)
λmailerApiBaseUrl()=>String=mailerApiBaseUrlFromProperty(process.env.hasOwnProperty("sigilHttpTestBaseUrl"))
λmailerApiBaseUrlFromProperty(hasValue:Bool)=>String match hasValue{
true=>(process.env.sigilHttpTestBaseUrl:String)|
false=>"http://127.0.0.1:45110"
}
Production-style config can read env vars, but only there:
e process
c world=(†runtime.world(
†clock.systemClock(),
†fs.real(),
†fsWatch.real(),
[†http.proxy(
mailerApiBaseUrl(),
•topology.mailerApi
)],
†log.stdout(),
†process.real(),
†pty.real(),
†random.real(),
†sql.deny(),
†stream.live(),
†task.real(),
[],
†timer.real(),
†websocket.real()
):†runtime.World)
λmailerApiBaseUrl()=>String=mailerApiBaseUrlFromProperty(process.env.hasOwnProperty("mailerApiUrl"))
λmailerApiBaseUrlFromProperty(hasValue:Bool)=>String match hasValue{
true=>(process.env.mailerApiUrl:String)|
false=>""
}
Config modules may also export selected env declarations for ordinary application code. These are reached through •config. rather than through •topology.
Canonical example:
•config.flags
Application Code Uses Handles, Not Endpoints
Canonical HTTP usage:
λmain()=>!Http String match §httpClient.get(
•topology.mailerApi,
§httpClient.emptyHeaders(),
"/health"
){
Ok(response)=>response.body|
Err(error)=>error.message
}
Canonical TCP usage:
λmain()=>!Tcp String match §tcpClient.send(
•topology.eventStream,
"ping"
){
Ok(response)=>response.message|
Err(error)=>error.message
}
Canonical PTY usage:
c assistantShell=(§topology.ptyHandle("assistantShell"):§topology.PtyHandle)
λmain()=>!Pty Owned[§pty.Session]=§pty.spawnAt(
assistantShell,
{
argv:["codex"],
cols:120,
cwd:Some("."),
env:({↦}:{String↦String}),
rows:40
}
)
Canonical SQL usage:
λmain()=>!Sql String match •sqlRoundtripApp.run(){
Ok(text)=>text|
Err(failure)=>failure.message
}
Canonical fsWatch usage:
c worktree=(§topology.fsRoot("worktree"):§topology.FsRoot)
λmain()=>!FsWatch Owned[§fsWatch.Watch]=§fsWatch.watchAt(
"src",
worktree
)
Canonical WebSocket usage:
c liveUpdates=(§topology.websocketHandle("liveUpdates"):§topology.WebSocketHandle)
λmain()=>!WebSocket Owned[§websocket.Server]=§websocket.listen(
8080,
[§websocket.route(
liveUpdates,
"/sessions"
)]
)
Forbidden patterns:
§httpClient.get("http://127.0.0.1:45110",headers,"/health")
§tcpClient.send("127.0.0.1","ping",45120)
process.env.mailerApiUrl
§file.writeText("raw","/tmp/out.txt")
§process.run(§process.command(["mailer"]))
§pty.spawn({argv:["codex"],cols:120,cwd:Some("."),env:{↦},rows:40})
For labelled boundary handling, projects use the handle-based §file.*At, §fsWatch.watchAt, §log.write, §process.runAt / §process.startAt, §pty.spawnAt / §pty.spawnManagedAt, and websocket route surfaces under §websocket / §httpServer so policy rules can target exact •topology... boundaries.
Example:
λrunExample()=>!Fs!Log!Process Unit={
l _=(§file.makeDirsAt(
"",
•topology.exportsDir
):Unit);
l _=(§file.writeTextAt(
("12345678901":µCpf),
"cpf.txt",
•topology.exportsDir
):Unit);
l _=(§log.write(
•policies.redactSsn(("123456789":µSsn)),
•topology.auditLog
):Unit);
l _=(§process.runAt(
•policies.govBrCommand(("gov-br-token":µGovBrToken)),
•topology.govBrCli
):§process.ProcessResult);
()
}
--env Is Required
Sigil does not guess a default environment for topology-aware or selected-config work.
Use:
sigil validate projects/topology-http --env test
sigil run projects/topology-http/src/getClient.sigil --env test
sigil run projects/featureFlagStorefront/src/main.sigil --env test
sigil test projects/topology-http/tests --env test
If topology is present, or if code reads •config., Sigil rejects the command when --env is missing.
Standalone files with a local c world do not use --env.
What Sigil Enforces
Compile-time:
- in project mode, topology constructors only in
src/topology.lib.sigil - in project mode, world named-boundary entry constructors only in
config/*.lib.sigiland test-localworld { ... } - in standalone mode, the same constructors may appear directly in the file
- topology-aware HTTP/TCP APIs require dependency handles
- label-aware filesystem and fsWatch crossings use named
FsRoothandles - label-aware log, process, and PTY crossings use named
LogSink,ProcessHandle, andPtyHandlehandles - relational database crossings use named
SqlHandlehandles §websocket.route/§websocket.connectionsand§httpServer.websocketRoute/§httpServer.websocketConnectionsuse namedWebSocketHandlehandles- raw endpoint usage is rejected
- in project mode,
process.envis only allowed inconfig/*.lib.sigil - standalone files may read
process.envdirectly because there is no separate config module •config.requires--env
Validate-time:
- the selected environment must be declared in topology
config/must exist.lib.sigil - the config module must export
world worldmust include every primitive effect entry- every declared
FsRootmust appear in bothfsRootsandfsWatchRoots - every declared
SqlHandleshould appear insqlHandles - every other declared named boundary must appear in the matching
worldentry collection - no undeclared boundary handles are allowed in
world
Tests Are Environments
Tests are just another environment:
- same logical dependency identity
- different baseline world
- optional per-test
world { ... }derivation
That keeps one runtime model for:
- app code
- local development
- integration tests
- production
For labelled boundaries, tests should assert the exact named-boundary outcome instead of relying on ambient global state. The canonical helpers are:
※check::file.existsAt(path,•topology.exportsDir)※check::fsWatch.watchingAt(path,•topology.exportsDir)※check::log.containsAt(message,•topology.auditLog)※observe::fsWatch.watchesAt(•topology.exportsDir)※observe::process.commandsAt(•topology.govBrCli)
The canonical example shape is:
λmain()=>Unit=()
test "audit sink receives redacted ssn" =>!Fs!Log!Process world {
c exports=(†fs.sandboxRoot(
".local/labelled-boundaries-tests/audit",
•topology.exportsDir
):†fs.FsRootEntry)
} {
l _=(•app.runExample():Unit);
※check::log.containsAt(
"***-**-6789",
•topology.auditLog
)
}