Summary
Refactor the public Runner render API in SyntaxKit so it neither reads user inputs from disk nor writes rendered outputs to disk. The render methods should take in-memory source and return the rendered Swift source (a String/Data for a single file, a keyed dictionary/collection for a batch). All user-facing filesystem IO — walking the input directory, reading inputs, and mirroring outputs into an output directory — moves into the skit CLI.
Motivation
The SDK boundary is currently inconsistent:
Runner.renderFile(input:) → SingleFileRender is pure on output — it returns stdout: Data + stderr and lets the skit CLI write the file (Sources/skit/Render.swift renderSingle). But it still reads the input path from disk (Runner.swift:140, String(contentsOf: inputURL)).
Runner.renderDirectory(inputDir:outputDir:) → [FileOutcome] is not pure — it walks inputDir (collectInputs), and in Phase 3 writes every successful render into outputDir via writeOutput (Runner+Directory.swift:66-74). FileOutcome only carries input/stderr/result — the rendered content is written and then discarded, never returned.
Making the SDK filesystem-free at its API boundary gives us:
- Testability — render logic exercisable with in-memory strings, no temp dirs (this is exactly what tripped Windows/WASM in #).
- Reusability — callers embedding SyntaxKit (not just the CLI) can render into memory, a buffer, a network response, etc.
- A single, consistent boundary — "pure render in the SDK; all user IO at the skit edge", aligning with the existing SDK/CLI split (Subprocess/ArgumentParser already live only in skit).
Current behavior (references)
Sources/SyntaxKit/Execution/Runner.swift:106 — renderFile(input:); reads input at :140.
Sources/SyntaxKit/Execution/Runner+Directory.swift:42 — renderDirectory(inputDir:outputDir:); input walk at :55, output write at :66-74.
Sources/SyntaxKit/Execution/FileManager+Execution.swift — collectInputs(at:) (input walk), writeOutput(...) (output mirroring).
Sources/SyntaxKit/Execution/RenderTaskResult.swift:50 — writeOutput(...).
Sources/SyntaxKit/Execution/URL+Reroot.swift — rerooted(from:onto:), used only to map an input path under inputDir to its destination under outputDir.
Sources/SyntaxKit/Execution/FileOutcome.swift — input/stderr/result; no rendered content.
Proposed direction
SyntaxKit (pure):
- Single file: render from in-memory source, e.g.
render(source: String, originalPath: String?) async throws(RunError) -> SingleFileRender. originalPath is a label only (for #sourceLocation mapping and stderr path-rewriting — see WrappedSource / processFile), not opened.
- Batch: render a keyed set of in-memory sources, e.g.
render(sources: [String: String]) async -> [String: <render-or-error>] (or an array of outcome values keyed by a caller-supplied identifier). Keep the existing bounded-concurrency task group (processInputs), just operating on in-memory sources keyed by identifier instead of URLs.
FileOutcome (or its replacement) gains the rendered content (the stdout that is currently written and dropped) so the caller can write it.
skit (owns all user IO):
- Walk the input directory and read inputs into memory (move/relocate
collectInputs, the directory walk, and input reads).
- Map each input identifier to its destination and write outputs (move
writeOutput + URL+Reroot rerooting + outputDir mirroring out of the SDK).
Render.renderSingle/renderBatch already own single-file writing and presentation; extend them to own batch writing too.
Implementation notes / nuances
- The internal temp compile artifact stays. Rendering compiles via
swiftc (processFile spills a wrapped .swift into a per-invocation temp dir, then spawns swift). That scratch file is an implementation detail of the compile-based backend, not user IO — it remains inside the SDK. "Filesystem-free" here means no reading user inputs and no writing user outputs.
#sourceLocation / stderr path-rewriting currently key off the absolute input path; preserve that by threading a caller-supplied label (originalPath/identifier) through instead of a real path.
- This is a breaking public API change to
Execution (per project convention these types stay public). Land the SDK signature change and the skit migration together so skit keeps building; consider deprecating the old outputDir-writing entry point for one release if external callers matter.
Tasks
Out of scope
- Changing the compile-based rendering backend or the temp-wrapper mechanism.
- The
swift-subprocess invocation and toolchain verification (already skit-adjacent).
Summary
Refactor the public
Runnerrender API in SyntaxKit so it neither reads user inputs from disk nor writes rendered outputs to disk. The render methods should take in-memory source and return the rendered Swift source (aString/Datafor a single file, a keyed dictionary/collection for a batch). All user-facing filesystem IO — walking the input directory, reading inputs, and mirroring outputs into an output directory — moves into the skit CLI.Motivation
The SDK boundary is currently inconsistent:
Runner.renderFile(input:) → SingleFileRenderis pure on output — it returnsstdout: Data+stderrand lets the skit CLI write the file (Sources/skit/Render.swift renderSingle). But it still reads the input path from disk (Runner.swift:140,String(contentsOf: inputURL)).Runner.renderDirectory(inputDir:outputDir:) → [FileOutcome]is not pure — it walksinputDir(collectInputs), and in Phase 3 writes every successful render intooutputDirviawriteOutput(Runner+Directory.swift:66-74).FileOutcomeonly carriesinput/stderr/result— the rendered content is written and then discarded, never returned.Making the SDK filesystem-free at its API boundary gives us:
Current behavior (references)
Sources/SyntaxKit/Execution/Runner.swift:106—renderFile(input:); reads input at:140.Sources/SyntaxKit/Execution/Runner+Directory.swift:42—renderDirectory(inputDir:outputDir:); input walk at:55, output write at:66-74.Sources/SyntaxKit/Execution/FileManager+Execution.swift—collectInputs(at:)(input walk),writeOutput(...)(output mirroring).Sources/SyntaxKit/Execution/RenderTaskResult.swift:50—writeOutput(...).Sources/SyntaxKit/Execution/URL+Reroot.swift—rerooted(from:onto:), used only to map an input path underinputDirto its destination underoutputDir.Sources/SyntaxKit/Execution/FileOutcome.swift—input/stderr/result; no rendered content.Proposed direction
SyntaxKit (pure):
render(source: String, originalPath: String?) async throws(RunError) -> SingleFileRender.originalPathis a label only (for#sourceLocationmapping and stderr path-rewriting — seeWrappedSource/processFile), not opened.render(sources: [String: String]) async -> [String: <render-or-error>](or an array of outcome values keyed by a caller-supplied identifier). Keep the existing bounded-concurrency task group (processInputs), just operating on in-memory sources keyed by identifier instead ofURLs.FileOutcome(or its replacement) gains the rendered content (thestdoutthat is currently written and dropped) so the caller can write it.skit (owns all user IO):
collectInputs, the directory walk, and input reads).writeOutput+URL+Rerootrerooting +outputDirmirroring out of the SDK).Render.renderSingle/renderBatchalready own single-file writing and presentation; extend them to own batch writing too.Implementation notes / nuances
swiftc(processFilespills a wrapped.swiftinto a per-invocation temp dir, then spawnsswift). That scratch file is an implementation detail of the compile-based backend, not user IO — it remains inside the SDK. "Filesystem-free" here means no reading user inputs and no writing user outputs.#sourceLocation/ stderr path-rewriting currently key off the absolute input path; preserve that by threading a caller-supplied label (originalPath/identifier) through instead of a real path.Execution(per project convention these types staypublic). Land the SDK signature change and the skit migration together so skit keeps building; consider deprecating the oldoutputDir-writing entry point for one release if external callers matter.Tasks
Runner(single + batch) taking in-memory source and an identifier/label.FileOutcome).collectInputs, directory walk, input reads) into skit.writeOutput,URL+Reroot,outputDirrerooting) into skit.Sources/skit/Render.swiftto collect inputs, call the pure SDK, and own all writes.outputDirfrom the public SDK signatures.Out of scope
swift-subprocess invocation and toolchain verification (already skit-adjacent).