Skip to content

✅ Cover projected content's lifetime with real modifiers - #326

Merged
taras merged 2 commits into
mainfrom
feat/issue-203-projection-lifetime
Aug 4, 2026
Merged

✅ Cover projected content's lifetime with real modifiers#326
taras merged 2 commits into
mainfrom
feat/issue-203-projection-lifetime

Conversation

@taras

@taras taras commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Why

#203 asks that content projected into a component belong to that component's
resource scope, and lists what has to be covered. The engine already does it —
withInvocation (§4.4), the content scope, the three-stage teardown — and the
specification and website already describe it. Three of the issue's coverage
points had no test:

  • a projected persist eval resource stopping with the invocation,
  • a projected daemon stopping with the invocation, and
  • an executable Markdown test where the ownership is observable without
    JavaScript.

persist and daemon were exercised only in a component's own body (L3, Q5),
and the ordering claims rode entirely on the stub watch modifier in Tier O.
Spec §6.11 promises that a daemon started inside <TempDir> stops before the
directory is removed; nothing checked it.

What changes

No behavior changes. Three tests and their two conformance rows.

Before: the caller's side of the boundary was covered only through the Tier O
stub. A regression that anchored projected persist/daemon resources on the
invocation's eval scope instead of the content scope passed every suite.

After: it fails L8 and the TempDir document.

How it works

L8 (packages/core/tests/eval-persist.test.ts) puts a real persist eval
block on both sides of the boundary. The caller projects one; the component
retains its own after <Content />, which is the order a single scope would
release in reverse. A later sibling reads the timeline:

start:projected, start:own, stop:projected, stop:own

Q14 (packages/core/tests/daemon-integration.test.ts) is Q5 with the daemon
written at the call site and the component body being nothing but <Content />.
It fails closed at both ends: RUNNING is printed only once the process has
been seen alive from inside the component, and the probe after the component
chooses STOPPED only against a pid file that exists — a missing one reports
NOPID. A daemon that never started therefore cannot satisfy the premise.

TempDir.test.md tells the same story in Markdown with no JavaScript. The
daemon traps SIGTERM and records whether its working directory still exists at
the moment it is signalled, writing into a second <TempDir /> that outlives the
first:

<TempDir as="report" /> → <TempDir> daemon traps TERM → invocation ends
  → ALIVE (signalled while the directory existed) + REMOVED (directory gone now)

Review guide

Start with: packages/core/tests/eval-persist.test.ts — L8

Then review:

  1. packages/core/src/components/TempDir.test.md — the reader-facing form of
    the same contract
  2. packages/core/tests/daemon-integration.test.ts — Q14
  3. specs/executable-mdx-spec.md — the L8 and Q14 conformance rows

Look carefully at: the daemon in TempDir.test.md arms its TERM trap
before writing the marker the following block waits on, so the wait cannot
succeed against an unarmed trap.

What must stay true

  • Projected resources stop before the component's own — enforced by stage 1 of
    withInvocation's teardown and by the content scope's parentage, checked by
    L8 and by ALIVE in TempDir.test.md.
  • Nothing a projection starts outlives the invocation — checked by Q14.

How to verify it

Each test was written against a mutation of
Invocation.useContentScope() that returns the invocation's eval scope instead
of creating the content scope — the pre-#203 anchoring:

  • L8 proves boundary ordering and fails if projected resources share the
    component's scope. Under the mutation it produces
    start:projected, start:own, stop:own, stop:projected — the exact inversion.
  • TempDir.test.md proves the same thing without JavaScript and fails under
    the same mutation (REMOVED is absent).
  • Q14 proves a projected daemon does not leak past the invocation, and fails
    if projected processes anchor at the document scope. Removing its daemon block
    makes it report NOPID and fail on RUNNING, so it cannot pass for a daemon
    that never started. It does not discriminate the content-scope mutation,
    and no test of its shape can: under that mutation the daemon anchors on the
    invocation's eval scope, which is still torn down at stage 3 of the same
    invocation, before the probe runs. Only the relative order changes, and a
    lone daemon has nothing to be ordered against. L8 and TempDir.test.md carry
    that direction.
deno task test
./dist/xmd test packages/core/src --raw

Scope

Included

Intentionally unchanged

  • The engine. withInvocation, projection.ts and expand.ts are untouched.
  • Spec and website prose. §4.4, §6.3.4 and §6.11 already describe this ownership
    model; §6.11 already states the <TempDir> claim the Markdown test now proves.
  • Q5's assertion that a signalled subprocess is reaped stays out of scope, as it
    was in Q7.

Generated or mechanical changes

None.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

The invocation boundary owns everything projected content creates, and
three of #203's coverage points had no test. `persist eval` and `daemon`
were exercised only in a component's *own* body — L3 and Q5 — and the
ordering claims rode entirely on the stub `watch` modifier in Tier O.
The spec's §6.11 promise that a daemon started inside `<TempDir>` stops
before the directory is removed was documented, and unproven.

L8 puts a real `persist` block on both sides of the boundary: the caller
projects one, and the component retains its own *after* projecting, which
is the order a single scope would release in reverse. It asserts
`start:projected, start:own, stop:projected, stop:own`. Anchoring
projected content on the invocation's eval scope instead of the content
scope flips the last two — the test was written against that mutation.

Q14 is Q5 with the daemon written at the call site and the component
doing nothing but `<Content />`: a projected process still stops with the
invocation that hosted it, not with the document.

TempDir.test.md tells the same story in Markdown with no JavaScript. The
daemon traps SIGTERM and records whether its directory still exists at
the moment it is signalled, into a second `<TempDir />` that outlives the
first. `ALIVE` is the ordering; `REMOVED` is the cleanup. It fails on the
same mutation.

No behavior changes. Two conformance rows accompany the new tests.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

PR #326: ✅ Cover projected content's lifetime with real modifiers

4 files, +135 / -0

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

The in-component block printed `ready` unconditionally, and the probe
after it chose `STOPPED` whenever `kill -0` failed — which is also what
a missing pid file produces. Q14 therefore passed when the projected
daemon never started at all: the exact premise it needs in order to
prove that the daemon later stops with the invocation.

Both markers now fail closed. `RUNNING` is printed only after the pid
file is non-empty *and* `kill -0` succeeds, still inside `<Holder>`, so
the process is known to have been alive. The probe distinguishes a
missing pid file as `NOPID` rather than reading it as a stopped process.
The assertion requires `RUNNING` and `STOPPED` and excludes `LEAKED`;
the positive marker is load-bearing because a printed root error does
not stop the run.

Removing the daemon block makes it report `NOPID` and fail on `RUNNING`.
@taras
taras merged commit 511776e into main Aug 4, 2026
10 checks passed
taras added a commit that referenced this pull request Aug 4, 2026
Rebased onto 511776e. #326 closed #203 by proving the two projection
lifetimes with real modifiers — L8 for a projected `persist` resource and
Q14 for a projected daemon — which were the acceptance criteria this
target had the weakest evidence for. Projected content keeping its
caller's bindings while its live effects belong to the component
invocation is now stated as shipped, along with why it matters: a process
a stage starts stops before the workspace it ran in is removed, which is
the ordering `<Worktree>` (#293) is built on.

`architecture.md` is authoritative on the error model, so the `output`
row reads "fails the run; `<PrintErrors>` can print instead" as the
registry states it. The engine does not do that yet — an outer boundary
ends the run instead, whether the failure arose in the region itself or
in content projected into it — so the discrepancy is classified as
missing and filed as #327 rather than allowed to redefine the settled
contract. No stage writes `<PrintErrors>`, so nothing here depends on it.

The distinction the target does keep is the one the registry makes: a
`throw` decision is the mode a boundary cannot replace. Every stage's
parsing sits in documentation, so the final `<Parse>` is a gate for that
reason and not because printing is unavailable.
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