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.sigil declares boundary handles and environment names
  • src/policies.lib.sigil declares boundary rules and trusted transforms for labelled data
  • config/.lib.sigil exports the selected environment's world plus 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
  • --env is 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 needs config/test.lib.sigil
  • if Sigil is run with --env production, the project needs config/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.sigil and test-local world { ... }
  • 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 FsRoot handles
  • label-aware log, process, and PTY crossings use named LogSink, ProcessHandle, and PtyHandle handles
  • relational database crossings use named SqlHandle handles
  • §websocket.route / §websocket.connections and §httpServer.websocketRoute / §httpServer.websocketConnections use named WebSocketHandle handles
  • raw endpoint usage is rejected
  • in project mode, process.env is only allowed in config/*.lib.sigil
  • standalone files may read process.env directly because there is no separate config module
  • •config. requires --env

Validate-time:

  • the selected environment must be declared in topology
  • config/.lib.sigil must exist
  • the config module must export world
  • world must include every primitive effect entry
  • every declared FsRoot must appear in both fsRoots and fsWatchRoots
  • every declared SqlHandle should appear in sqlHandles
  • every other declared named boundary must appear in the matching world entry 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
  )
}