Skip to content

💥 Contain document filesystem access behind API.Files (#227) - #403

Merged
taras merged 8 commits into
mainfrom
files-provider-boundary
Aug 9, 2026
Merged

💥 Contain document filesystem access behind API.Files (#227)#403
taras merged 8 commits into
mainfrom
files-provider-boundary

Conversation

@taras

@taras taras commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Why

Issue #227. <File>, <Glob> and <TempDir> reached the host filesystem
directly, through API.Fs and node:path/node:fs. That makes one document
mean two different things depending on where it runs, and it means the
containment those components promise is stated in the components rather than
owned by whoever supplies the filesystem.

This is PR A of a two-PR stack: the neutral Api and the host provider. PR B
adds the transaction-bound DOFS provider.

What changes

Before: a document's file operations resolved against the process's filesystem
in every environment, and the components themselves did the path arithmetic,
the realpath walk, the temporary-plus-rename write, and the traversal.

After: every document file operation goes through API.Files, a contextual Api
of whole semantic operations. The three components hold no host path and import
no host path or filesystem module. xmd run installs a host provider from all
four entrypoints; a run with no provider installed fails rather than reaching
the host.

Every printed message a document can see is unchanged. The whole FL, GB and TD
tiers pass byte-for-byte through the new boundary without an expectation being
edited.

How it works

<File> → core invokeFiles → API.Files → host provider → API.Fs

API.Files has five operations. Four are whole semantic acts — read this path,
replace this path, list what these patterns select, give me a temporary
directory. The fifth, checkFilePath, is deliberately weak: pure path
arithmetic, no filesystem access, and it returns nothing usable.

That weakness is the design. <File>'s write form calls checkFilePath to
decide whether its children may expand — FL18b and FL18c — and the later
writeTextFile repeats the same admission from the same authored path
before owning resolution, target classification, parent creation and the
commit. Nothing is handed between them, so a check that was skipped, replaced
by middleware, or answered by another provider cannot authorize the write. FL18
still holds: a child that swaps the parent for an escaping symlink is caught,
because resolution happens inside the write.

An ordinary filesystem condition comes back as Err carrying a frozen plain
object: a reason from a fixed 20-value vocabulary, plus the phase it came from.
The reason selects a phrase; no message, errno code, resolved path, temporary
name, or symlink target crosses. A write's phase decides what may be said about
the target, and the constructor refuses any other combination.

A provider that is absent, that refuses an operation, or that broke its own
contract throws. packages/core/src/files.ts is the sole wrapper: it
rethrows an existing durability failure by identity, then an existing Files
failure by identity, and only replaces something that is neither — with a fixed
protocol invariant carrying no cause and no host value. errors.ts extends
#394's traversal so precedence is durability → Files → documentation, decided
by kind rather than by aggregate position, with the selected object returned by
identity.

Revision (review round 2)

Rebased onto main through #400, #401, #402 and #404. #400's Workspace
coordination seams are untouched; packages/workflow/tests/workspace-effect.test.ts
passes on this head.

Four blockers resolved:

  1. Parsing and fatal discovery are total. Every value these parsers read is
    a value a provider handed back, and they run from fatalCause — which every
    generic catch in expansion consults. One of them throwing would replace the
    failure being classified with a failure about classifying it. Every read
    now goes through a helper that answers undefined instead, each exported
    parser is wrapped, and causesOf treats an unreadable cause, errors or
    causes as aggregating nothing. A hostile wrapper narrows what discovery
    finds; it never replaces what discovery was finding.
  2. Recognition is strict, because recognition means "travel onward by
    identity."
    A Files fatal must now carry frozen data with no extra fields,
    the fixed diagnostic for its kind, and no cause. A candidate with the right
    tag but a raw platform message or an errno chain fails the contract and
    invokeFiles replaces it with a fresh protocol invariant carrying none of
    it. Durability failures keep 🐛 Restore the durable eval journal boundary #394's exact class-based recognition and
    precedence.
  3. Cleanup that fails while cancellation unwinds is fatal. It has no
    outcome to compose with, so the adapter leaves the scope as a fixed
    teardown invariant rather than manufacturing a write result — carrying
    neither the platform error nor the generated temporary's name. A completed
    write whose cleanup failed still composes as an Err exactly as before.
  4. Loaded-copy evidence is real. packages/core/tests/loaded-copy-files.test.ts
    bundles packages/runtime/files.ts, imports it as its own module, and proves
    recognition in both directions across two copies that share no class. The
    hand-built object in FA27 remains, as parser coverage.

architecture.md now attributes structural-tag recognition specifically to
Files failures and says plainly that durability recognition is unchanged.

Revision (review round 5)

Retargeted from e0c8409 on 35060af. The Result<void> decision is unchanged.

The array brand was the last escape. Array.isArray is itself an operation
on provider-controlled data — it throws on a revoked Proxy — and it ran outside
the total readers, so a raw TypeError left the boundary untagged and the
document carried on. Recognizing the brand now answers instead of throwing, and
a value whose array identity cannot be inspected is malformed success data like
any other.

The guard sits inside the payload contract rather than as a catch-all around
the call site. A wrapper there would have made every guard inside each contract
untestable — and in fact my first attempt added one, which is why the
mutation-check passed when it should not have. Payload contracts are total by
construction instead, and restoring the unguarded Array.isArray now reds
FF14b. The regression was written first and reproduced the escape before the
fix.

FF14b gains a live Proxy.revocable string array: built valid, wrapped in Ok
without inspection, revoked before the provider returns. It asserts the
sanitized protocol invariant, that no later sibling runs, that the selected
failure is not the raw TypeError and carries no cause, and that neither
IsArray nor revoked appears anywhere. The iterator-only success regression
and the copied-array independence checks are retained.

Specification catalog synced with what is implemented: HF12b, and FF11
through FF15. The provider-failure prose now separates an outcome that will
not say
what it is — no readable settlement, or an unreadable selected member,
which is a contract violation — from one that reads fine but reports a failure
this version does not recognize, which selects the generic sentence. It
describes observable behavior only; no internal helper appears as syntax.

Clean-worktree verification

Fresh clone at this head, on a resolved path:

Command Result
deno task lint pass
deno task check (unscoped) pass
deno task check:jsr pass
pnpm exec tsc -p tsconfig.node.json --noEmit pass
the eight named suites 8 files, 158 steps, 0 failed
git diff --check origin/main...HEAD clean
deno task test (full Deno suite) 422 files, 2943 steps, 0 failed

Revision (review round 4)

Retargeted from e669b00, then rebased onto main through #405 and #406.

undefined was standing for three different answers in
packages/core/src/files.ts: a member read fine and held undefined, a member
was absent, and reading a member threw. Presence and readability are now asked
separately, and each operation states which it requires. The two containers the
review named no longer get through: checkFilePath refuses a success whose
value cannot be read, and a non-write failure whose error cannot be read is
a protocol invariant rather than the printable generic. Only a readable
error whose data fails validation gets the generic (FF15). Write keeps its
stricter commit-state validation.

Glob copying is total. length and every element go through the same
reader, and the walk is by index — so the iterator is never consulted and a
for…of regression is caught (FF14b asserts both the hostile traps and that an
iterator-only trap copies cleanly). The array that comes back is the document's
own: mutating the provider's afterwards does not reach the bound value.

The FF14 seam is replaced. Every hostile shape is now a live Proxy around a
real Ok/Err
passed through API.Files — the declared type survives without
a cast, and the traps have never run when the provider hands the value back, so
the normalization boundary is the first thing to touch them. The previous
JSON round-trip invoked the getters inside the test and proved only the
handler-throw path FF7 already covers. Shapes are paired only with the
settlement they actually bite, because a value trap is never consulted by a
failure and pairing them would have demanded a fatal outcome from a legitimate
ordinary path.

All three new guards were mutation-checked: restoring the checkFilePath
conflation, downgrading an unreadable error, and switching the Glob walk back
to for…of each red FF14/FF14b.

One deviation from the stated contract, deliberately

The directive asks that an absent value on checkFilePath be fatal.
Effection spells a payload-free success as its shared Unit{ ok: true },
with no value member at all — which is exactly what the host adapter
returns from checkFilePath. Making absence fatal there would reject the real
provider on every admitted path.

So absence is the ordinary success for that one operation, and what is refused
instead is a value that is present but cannot be read, and one that is present
and is something other than undefined. Both are covered (FF14 "a throwing
value", FF14c's payload-carrying Proxy). Every other operation — read, write,
Glob, TempDir — treats an absent value as fatal, since Ok(x) always carries
one. Absent error is fatal everywhere, since Err always sets it.

Clean-worktree verification

Fresh clone at this head, on a resolved path:

Command Result
deno task lint pass
deno task check (unscoped) pass
deno task check:jsr pass
pnpm exec tsc -p tsconfig.node.json --noEmit pass
the eight named suites 8 files, 158 steps, 0 failed
git diff --check origin/main...HEAD clean
deno task test (full Deno suite) 422 files, 2943 steps, 0 failed

Revision (review round 3)

Retargeted from 6be3844; main had not moved.

  1. The safe-identity contract now covers the whole Error. Recognizing a
    Files fatal hands that exact object onward, so it must match what a
    constructor here produces: the fixed name and diagnostic for its kind,
    frozen data with exactly that kind's fields, no cause, and no other
    enumerable member — string or symbol
    . FF12 now includes a path-bearing
    name, an extra Error-level path, an enumerable symbol payload, and
    Proxies with hostile ownKeys / getOwnPropertyDescriptor. Each is rejected
    by filesFatalFailure, replaced by invokeFiles with a fresh protocol
    invariant, and proved not to survive stringification, spreading, or symbol
    enumeration — while the three real constructors stay recognizable.
  2. The whole Result is validated and rebuilt inside the boundary. A
    Result is only conventionally a Result: the TypeScript signature is a claim
    about the provider, not a guarantee, and a component reading ok, value,
    or error first would be the thing that ran a hostile accessor — outside the
    sanitizer. The core wrappers now inspect the container totally and rebuild
    every outcome from validated parts, so no provider-originated container,
    error object, or payload reaches a component; globFiles returns a copied
    array. A container that will not say how it settled, and a success it cannot
    describe, are protocol invariants that stop later siblings (FF14, across
    read, write, checkFilePath, Glob and TempDir). A malformed non-write
    failure is not fatal — a fresh generic failure is substituted, the existing
    "filesystem operation failed" sentence renders, and the document carries on
    (FF15). Cancellation is still never converted into a Result.
  3. FA24 runs all six orderings of durability, Files and documentation,
    through both AggregateError and InvocationTeardownError; every one
    selects the durability failure by identity. The earlier six-permutation edit
    had silently not applied — the loop had been reflowed by oxfmt — so this is
    the first revision where it is actually true.

Both new contracts were mutation-checked: weakening isFilesFatal reds FF12,
and reading ok without the settlement check reds FF14.

Clean-worktree verification

Run in a fresh clone at this head, which is also where the previously-reported
deno task check caveat goes away:

Command Result
deno task lint pass
deno task check (unscoped) pass
deno task check:jsr pass
pnpm exec tsc -p tsconfig.node.json --noEmit pass
focused Files/fatal-cause/loaded-copy/host-files/component/workflow/contract suites 13 files, 176 steps, 0 failed
deno task test (full Deno suite) 422 files, 2934 steps, 0 failed
git diff --check origin/main...HEAD clean

Review guide

Start with: packages/runtime/files.ts — the contract, and why the data is
structural rather than class-based.

Then review:

  1. packages/core/src/components/File.ts — the ordering and the message table
  2. packages/runtime/host-files.ts — the host provider and where its guarantee
    stops
  3. packages/core/src/files.ts and packages/core/src/errors.ts — the throw
    ordering and the extended fatal traversal
  4. .github/workflows/ci.yml + scripts/files-contract-probe.ts — the
    five-target evidence

Look carefully at:

  • writeTextFile in host-files.ts: the cleanup ensure is registered before
    the temporary is written, and step is what decides the reported phase.
  • refusal() and report() in File.ts: these reproduce the exact current
    sentences from structural data instead of from caught errors.

What must stay true

  • A write's check runs before its children and authorizes nothing
    enforced by checkFilePath returning Result<void>, checked by FL18b, FL18c
    and FF4.
  • Resolution happens after the children — enforced by writeTextFile doing
    its own admission, checked by FL18 and HF10.
  • Nothing from a platform error crosses the boundary — enforced by the
    reason vocabulary, checked by FL23, FL26, GB13, GB14 and HF4.
  • Absence is fatal and never falls back — enforced by the terminal handler,
    checked by FF1, FF2 and HF14 (which also asserts zero API.Fs calls).
  • Durability outranks Files outranks documentation, by identity — enforced
    by fatalCause, checked by FA20–FA28 and FF8/FF9.
  • A separately loaded runtime copy interoperates — enforced by the
    structural tag, checked by LC1/LC2 against a really-bundled second copy, and
    by FA27 for the parser itself.
  • Classification never throws — enforced by total reads in
    packages/runtime/files.ts and causesOf, checked by FF11 against throwing
    accessors and Proxies on data, its fields, key enumeration, cause,
    AggregateError.errors and teardown causes.
  • A tagged-but-unsafe failure is sanitized, not preserved — enforced by
    isFilesFatal's full-contract check, checked by FF12.

How to verify it

  • packages/runtime/tests/host-files.test.ts (HF1–HF16) proves the provider
    contract directly and fails if a phase, a reason, or the absence behavior
    moves. HF10/HF10b assert the documented weakness — a parent or target
    replaced synchronously between observation and use is written or read through
    — so a future change that silently claimed race containment would fail here.
  • packages/core/tests/files-fatal.test.ts (FF1–FF13) fails if a missing
    provider ever becomes a printed error, if a malformed write outcome invents a
    commit state, if a nested durability failure loses its identity, if a hostile
    shape makes classification throw, or if a cleanup failure during cancellation
    stops being fatal.
  • HF12b suspends the write after the temporary is created, halts it, and
    forces removal to fail — deterministically, with no timing race — and fails if
    a halt is ever turned into a write outcome.
  • packages/core/tests/fatal-cause.test.ts FA20–FA28 fail if precedence
    becomes position-dependent or if recognition regresses to instanceof.
  • The existing FL, GB and TD tiers are unchanged expectations; they fail if any
    printed sentence moved.
deno test --allow-all --frozen \
  packages/runtime/tests/host-files.test.ts \
  packages/core/tests/files-fatal.test.ts \
  packages/core/tests/fatal-cause.test.ts \
  packages/core/tests/file-component.test.ts \
  packages/core/tests/glob-component.test.ts \
  packages/core/tests/temp-dir.test.ts \
  scripts/tests/filesystem-contract-workflow.test.ts

Results

Command Result
deno task lint 0 errors, format clean
deno check --frozen packages scripts + .reviews/components/*.ts clean
deno task check:jsr Success Dry run complete
deno task test packages/core/tests packages/runtime/tests packages/cli/tests 219 files, 1793 steps, 0 failed
deno task test --changed=origin/main packages scripts see the revision run below
xmd test packages/core/src --raw exit 0, no failing assertions
deno compile + run scripts/files-contract-probe.ts 13 claims hold on darwin/arm64
tsx --test and bun test on the new suites 17 + 38 + 6 pass

Scope

Included

  • packages/runtime/files.ts — the Api, structural failure data, constructors,
    parsers and recognizers
  • packages/runtime/host-files.ts — the host provider and its test-only phase
    observer
  • packages/core/src/files.ts — the sole invokeFiles wrapper
  • packages/core/src/errors.ts — Files discovery added to 🐛 Restore the durable eval journal boundary #394's traversal
  • the three components, the four CLI entrypoints
  • filesystem-contract CI matrix over the five release targets, held to
    RELEASE_TARGETS and added to green.needs
  • architecture.md and executable-MDX §§1.2, 6.11, 6.13, 6.14 and the
    FL/TD/FA/HF/FF test-plan tiers

Intentionally unchanged

  • The transaction-bound DOFS provider is PR B. Nothing here installs one,
    and no workflow command reaches API.Files yet.
  • API.Fs stays the low-level host surface and the host provider's own
    dependency. Middleware wrapping it still sees a document's access.
  • The engine's control plane — root document, component search, replay guards,
    the eval compiler, the diagnostic journal, the test target — keeps reading
    host paths through API.Fs.
  • No public remove, rename, realpath or stat operation: those are
    private phases of the write, and are tested only as write.commit and
    write.cleanup.
  • No dependency, lockfile, release target, or release artifact change.

New abstractions

  • API.Files exists because the same three components must resolve paths in
    two environments that share no filesystem primitive; consumers are <File>,
    <Glob> and <TempDir> today and the DOFS provider in PR B.

  • The structural failure data exists because instanceof answers false across
    two loaded copies of the runtime package, which is exactly the case where a
    provider failure must not become an unrecognized throw.

  • hostFilesHandler's observer exists so the stable-namespace limitation is
    falsifiable rather than merely stated; production entrypoints omit it.

  • Each new abstraction has multiple concrete uses or a clear justification.

  • No speculative functionality is included.

Risks and limitations

  • Breaking for programmatic core users. A caller driving execute()
    directly must now install a provider (useHostFiles()), or <File>,
    <Glob> and <TempDir> fail the execution. That is the point — a missing
    provider must not silently reach the host — but it is a real API break, and
    the affected in-repo suites were updated to install it the way an entrypoint
    does.
  • The Windows row is new coverage and unproven locally. filesystem-contract
    runs host-files.test.ts on windows-2025, and that suite creates file
    symlinks. Directory links use junctions, which an unprivileged process can
    create; file symlinks depend on the runner being elevated. If that row goes
    red on privileges, the fix is to reshape those cases — the finding is exactly
    what the matrix exists to surface, and I could not verify it from macOS.
  • The host claim is narrower than before, and deliberately so. The spec now
    says containment holds while the host pathname namespace is stable, and HF10
    demonstrates a replacement being written through. Nothing regressed; the
    earlier text implied more than the implementation could deliver.
  • Recovery: reverting this commit restores the previous host-only components
    whole; no persisted state or schema is involved.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

PR #403: 💥 Contain document filesystem access behind API.Files (#227)

35 files, +5546 / -611

Scope

🔴 PR has 6157 lines changed. Split into focused PRs.

🟡 6157 lines changed. PRs under 400 receive more thorough review.

🟡 35 files changed. Are all changes related?

🟡 Changes span 9 directories.

🟡 PR mixes config and source changes.

Structural

🟡 Type declarations with no consumers: FatalFailure.
Symbol Declared at Refs in diff Why flagged
FatalFailure packages/core/src/errors.ts:202 1 referenced ≤1× within the added diff (pre-existing usages not counted)

Oxlint structural signals:

  • no-unnecessary-type-assertion ×2: packages/runtime/apis.ts

Slop

  • packages/core/tests/guarded-journal-probe.ts:217 (removed)
  • packages/core/tests/guarded-journal-probe.ts:271 (removed)
  • packages/core/src/errors.ts:14// that: it is a structural tag and a parser.
  • scripts/files-contract-probe.ts:82// one answering.
  • scripts/files-contract-probe.ts:164// output is the account of what it checked.

Static Analysis

Oxlint: 8 diagnostics across 5 files (5 rules)
Density: 0.001 violations/added-line

no-shadow (2): packages/core/src/components/Glob.ts, packages/core/src/components/File.ts
no-array-sort (2): packages/runtime/host-files.ts, packages/runtime/files.ts
no-unnecessary-type-assertion (2): packages/runtime/apis.ts
no-useless-spread (1): packages/runtime/files.ts
no-base-to-string (1): packages/core/src/components/File.ts

Correctness

No extraneous code patterns detected.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/bun.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

Comment thread packages/cli/src/deno.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

Comment thread packages/cli/src/node.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

// separately loaded copy of `printErrors` bundles, and the runtime's root
// re-exports the host Apis — process, fetch, filesystem — one of which carries a
// native addon no bundler can inline. Recognizing a Files failure needs none of
// that: it is a structural tag and a parser.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// that: it is a structural tag and a parser.

mkdirSync(outside);

// Absence is checked before a provider exists, so the terminal handler is the
// one answering.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// one answering.

check("a temporary directory is removed with its scope", !exists(temporary));

// Diagnostics rather than a result, so they go to stderr: this probe's whole
// output is the account of what it checked.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// output is the account of what it checked.

@taras
taras force-pushed the files-provider-boundary branch from 182dd56 to 6be3844 Compare August 9, 2026 04:02

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 12 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/bun.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

Comment thread packages/cli/src/deno.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

Comment thread packages/cli/src/node.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

Comment thread packages/runtime/files.ts
* is that a workflow run cannot silently touch the caller's filesystem.
*/
export const Files: Api<FilesHandler> = createApi<FilesHandler>("executablemd.runtime.files", {
// deno-lint-ignore require-yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// deno-lint-ignore require-yield

Comment thread packages/runtime/files.ts
*writeTextFile(_input: FileWriteInput): Operation<Result<FileWriteSuccess>> {
throw new FilesProviderUnavailableError();
},
// deno-lint-ignore require-yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// deno-lint-ignore require-yield

Comment thread packages/runtime/files.ts
*globFiles(_input: GlobInput): Operation<Result<string[]>> {
throw new FilesProviderUnavailableError();
},
// deno-lint-ignore require-yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// deno-lint-ignore require-yield

let step: FileWritePhase = "temporary";
// Whether the write reached its own end. Cleanup runs on every exit, and
// the two exits need different answers: one has a Result to compose with
// and the other does not.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// and the other does not.

mkdirSync(outside);

// Absence is checked before a provider exists, so the terminal handler is the
// one answering.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// one answering.

check("a temporary directory is removed with its scope", !exists(temporary));

// Diagnostics rather than a result, so they go to stderr: this probe's whole
// output is the account of what it checked.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// output is the account of what it checked.

@taras
taras force-pushed the files-provider-boundary branch from e669b00 to e0c8409 Compare August 9, 2026 11:08

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 7 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/bun.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

Comment thread packages/cli/src/deno.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

Comment thread packages/cli/src/node.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

if (carried) {
// Present, so it has to be readable *and* be the absent payload. Asking
// only whether the value is `undefined` would let a member that refused to
// be read pass as one that read as nothing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// be read pass as one that read as nothing.

expect(result.ok && result.value).not.toContain("first");

// Rejection is per event: the offending one is absent, everything after it
// still persisted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// still persisted.

expect(persisted.map(label)).toEqual(["yield(import_component)", "yield(exec)", "close(root)"]);

// The bytes on disk are exactly the accepted events — the rejected one
// left no partial record behind.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// left no partial record behind.

@taras
taras force-pushed the files-provider-boundary branch from 1cb3b76 to 6dca5e2 Compare August 9, 2026 11:34

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 17 redundant comments. Inline suggestions to remove them below.

Comment thread packages/cli/src/bun.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

Comment thread packages/cli/src/deno.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

Comment thread packages/cli/src/node.ts
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// host by accident.

// separately loaded copy of `printErrors` bundles, and the runtime's root
// re-exports the host Apis — process, fetch, filesystem — one of which carries a
// native addon no bundler can inline. Recognizing a Files failure needs none of
// that: it is a structural tag and a parser.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// that: it is a structural tag and a parser.

let step: FileWritePhase = "temporary";
// Whether the write reached its own end. Cleanup runs on every exit, and
// the two exits need different answers: one has a Result to compose with
// and the other does not.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// and the other does not.

// beside — and manufacturing one would turn a halt into a write
// result. It leaves the scope as an infrastructure failure instead,
// carrying neither the platform's error nor the generated temporary's
// name, and the engine's fatal discovery finds it there.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// name, and the engine's fatal discovery finds it there.

// The Api compiles patterns as it starts, so an unusable one — an
// unterminated character class — arrives as a `SyntaxError` from `RegExp`
// rather than as an errno. It is the one failure here a document can fix
// by editing what it wrote.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// by editing what it wrote.

mkdirSync(outside);

// Absence is checked before a provider exists, so the terminal handler is the
// one answering.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// one answering.

check("a temporary directory is removed with its scope", !exists(temporary));

// Diagnostics rather than a result, so they go to stderr: this probe's whole
// output is the account of what it checked.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// output is the account of what it checked.

taras added 8 commits August 9, 2026 08:10
`<File>`, `<Glob>` and `<TempDir>` reached the host filesystem directly, so
`xmd run` and a workflow run could not mean the same thing for one document.

Document filesystem access now goes through `API.Files`, a contextual Api of
whole semantic operations with no host default. The four CLI entrypoints
install the host provider explicitly; a run with none installed fails rather
than reaching the host.

The three components make no filesystem call of their own and import no host
path or fs module. What they keep is order: a write's lexical check runs before
its children, and the semantic write that follows repeats admission and owns
every later phase, so the earlier check authorizes nothing.

Ordinary failures cross the boundary as frozen structural data — a reason from
a fixed vocabulary and the phase it came from — and every printed message is
byte-identical to before. A provider that is absent, refuses an operation, or
breaks its own contract throws instead, and core's fatal traversal ranks it
between a durability failure and a documentation failure, by identity and by
structural tag.
`errors.ts` is in the graph a separately loaded copy of `printErrors` bundles,
and importing the runtime's package root pulled the host Apis in with it —
including a native addon no bundler can inline. The Files recognizer needs none
of that, so it comes from the leaf module through a new `./files` subpath.

The five-target job prepares with `deno install` rather than `deno task deps`:
the task caches graphs this job does not need and reaches them by spawning a
child, which does not survive the Windows runner's path handling. Its compile
now carries the repository's isolation flags, and the rule that enforces them
reads every compile in a workflow rather than letting the first invocation's
flags answer for the rest.
`toSorted` needs es2023, which `tsconfig.node.json` does not select. The array
is built from a Set on the line above, so nothing shared is being mutated.
…nup fatal

Structural parsing runs from `fatalCause`, which every generic catch in
expansion consults. A provider is free to hand back a Proxy that refuses to be
inspected, and one of these parsers throwing would replace the failure being
classified with a failure about classifying it. Every read is now total, and so
is the cause traversal: a hostile wrapper narrows what discovery finds instead.

Recognizing a Files fatal is a decision to let that exact object travel onward
by identity, so it now requires the whole public contract — frozen data with no
extra fields, the fixed diagnostic for its kind, and no cause. A candidate
carrying a raw message or an errno chain is replaced by a fresh invariant
rather than preserved. Durability recognition is unchanged: it stays #394's
class-based mechanism, and only the Files boundary is crossed by a second
loaded copy.

A host cleanup that fails while cancellation is unwinding has no outcome to
report beside it. It leaves the scope as a fixed teardown invariant instead of
manufacturing a write result, carrying neither the platform's error nor the
generated temporary's name.

The loaded-copy claim is now proved by a real second copy: the Files module is
bundled, imported as its own module, and its failures are recognized in both
directions.
Recognizing a Files fatal hands that exact object onward, so the contract now
covers the whole Error: the fixed name and diagnostic for its kind, frozen data
with exactly the kind's fields, no cause, and no other enumerable member —
string or symbol. A path riding on `name`, on an extra property, or under a
symbol key fails the contract, and `invokeFiles` replaces the candidate rather
than preserving it.

A `Result` is only conventionally a Result. The TypeScript signature is a claim
about the provider, not a guarantee, so a component that read `ok`, `value`, or
`error` first would be the thing that ran a hostile accessor — outside anything
that sanitizes. The core wrappers now inspect the container totally and rebuild
every outcome from validated parts: no provider-originated container, error
object, or payload reaches `<File>`, `<Glob>` or `<TempDir>`, and a search
result is copied rather than passed along.

A container that will not say how it settled, and a success it cannot describe,
are provider-contract failures. A malformed non-write failure is not: the
vocabulary already has a sentence for it, so a fresh generic failure is
substituted and the document carries on.

FA24 now runs all six orderings of the three fatal kinds, through both wrappers.
`undefined` was standing for three different answers: a member read fine and
held undefined, a member was absent, and reading a member threw. Collapsing
them let two containers through that never described their outcome —
`checkFilePath` accepted a success whose `value` refused to be read, and a
non-write failure whose `error` refused was downgraded to the printable generic.

Presence and readability are now asked separately, and every operation says
which it requires. A search array is copied by index through the same reader,
so length and element traps are covered too and the iterator is never consulted.

The one place the contract bends is `checkFilePath`: Effection spells a
payload-free success as its shared `Unit`, `{ ok: true }` with no `value` member
at all, so absence there is the ordinary success rather than a failure. What is
refused is a `value` that cannot be read, and one that is present but is
something other than undefined.

The seam now passes live Proxies around real `Ok`/`Err` values through
`API.Files`, so the boundary is the first thing to run a hostile trap — the
previous JSON round-trip invoked the getter inside the provider handler and
proved only the already-covered handler-throw path.
`Array.isArray` is itself an operation on provider-controlled data: it throws on
a revoked Proxy. Running it outside the total readers let a raw TypeError leave
the boundary untagged, so a search whose result was revoked before it was
returned surfaced the platform's message and let the document carry on.

The brand check now answers instead of throwing, and a value whose array
identity cannot be inspected is malformed success data like any other. The guard
sits inside the payload contract rather than around the call site, so it stays
the thing under test: restoring the unguarded call reds the regression.

The specification catalog gains the rows for behavior already implemented —
HF12b and FF11 through FF15 — and the provider-failure prose now separates an
outcome that will not say what it is, which is a contract violation, from one
that reads fine and reports a failure this version does not recognize, which is
the generic sentence.
The provider-failure passages named only a read and a search, which left path
admission and TempDir unstated even though they take the same path: readable
data that does not validate selects the generic sentence there too.

The FF14 row described a stricter rule than the boundary implements. It now
matches FF14c and FF15: an unreadable settlement is a contract failure, so is an
unreadable selected member, and an absent success value only where the operation
carries one — path admission succeeds without one. A readable but unrecognized
non-write failure stays printable.

FF11 is renamed to what it asserts. Declining to recognize a hostile shape is
not recognizing it as a valid structural failure.
@taras
taras force-pushed the files-provider-boundary branch from 6dca5e2 to ab552cc Compare August 9, 2026 12:11

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 3 redundant comments. Inline suggestions to remove them below.

// separately loaded copy of `printErrors` bundles, and the runtime's root
// re-exports the host Apis — process, fetch, filesystem — one of which carries a
// native addon no bundler can inline. Recognizing a Files failure needs none of
// that: it is a structural tag and a parser.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// that: it is a structural tag and a parser.

mkdirSync(outside);

// Absence is checked before a provider exists, so the terminal handler is the
// one answering.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// one answering.

check("a temporary directory is removed with its scope", !exists(temporary));

// Diagnostics rather than a result, so they go to stderr: this probe's whole
// output is the account of what it checked.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// output is the account of what it checked.

@taras
taras marked this pull request as ready for review August 9, 2026 12:18
@taras
taras merged commit aa47442 into main Aug 9, 2026
16 checks passed
@taras
taras deleted the files-provider-boundary branch August 9, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant