Built-in project publication v1

September 12, 2026 ยท View on GitHub

Status: implemented bounded publication correction; HOSTED GREEN under the v0.4.0 release baseline. Private-host and standalone creation retain separate admission and authority.

Audience: toolchain contributors, host integrators, and reviewers.

Scope and unchanged interface

The unpublished full toolchain owns semaprax-full new <destination> and the built-in calculator, library, and service templates. Tag archives expose that full CLI as semaprax; the standalone registry compiler does not gain private-host dependencies. See the quickstart for the user workflow and Project Manifest v1 for checked project semantics. The Public Project Scaffold Capsule v3 derives and replays the same template bytes without a destination or write authority; it does not replace this held-parent publication protocol. The standalone compiler's new, owned by standalone project creation v1, writes the same bytes through a bounded create-new route without this protocol's staging or identity re-verification.

The publication protocol does not change the command grammar, lowered Project schema, or source semantics. The existing --name and closed --template calculator|library|service choices are admitted. The authority holds one of two exact fixed-arity source-directory shapes selected before staging: the calculator and service templates share one (their source files have identical names), and the library template uses the other. The exact generated inventories are those of Public Project Scaffold Capsule v3:

  • README.md
  • AGENTS.md
  • semaprax.toml
  • src/app.spx
  • src/core.spx
  • src/tests.spx

or the same three root files plus:

  • src/examples.spx
  • src/lib.spx
  • src/tests.spx

(AGENTS.md was added by scaffold v2; scaffold v3 keeps the three-file root inventory and grows the calculator's held source inventory from two files to three.)

There is no template discovery, arbitrary template input, network access, dependency installation, Git initialization, recursive cleanup API, general workspace mutation, or target process execution in the generator.

Preparation and held authority

The toolchain renders only compiled-in templates, validates their complete fixed inventory, and invokes ordinary Project semantic construction and the bounded in-process test evaluator on those exact owned bytes before staging. It does not reopen an ambient staging pathname as semantic input. Physical held-file comparison and exact directory inventories subsequently bind the staged files to the same checked bytes.

The destination is a fresh child of an existing parent. One absolute requested destination spelling is captured at entry, without resolving symlinks; its parent spelling and the parent's initial identity remain available for final comparison. The existing canonical parent is still used to acquire lower held authority. Final checks must preserve previously admitted relative and parent-relative paths rather than sending .. through a lower API that only accepts normal absolute components.

The lower NewProjectAuthority retains the exact parent and created stage handles, fixed child names, file inventories, and expected absolute parent and published paths. Expected path storage is prepared before namespace creation. Files are written create-new through retained directory authority. Source and root inventories are authenticated before descendant settlement and rename. No byte digest or pathname alone grants deletion or publication authority.

Generated staging names must be distinct from the requested destination. The CLI skips an exact or ASCII-case-equivalent candidate within its existing 32-attempt budget, before acquiring an authority that creates directories. The lower authority independently rejects that pair with Invalid before namespace creation; an already-existing output retains Exists precedence. This prevents .semaprax-new-<pid>-<serial> from becoming the final path while a built-in template is still being staged. It does not reject that CLI destination: the next noncolliding staging candidate can still publish to it.

The generated names are ASCII, and Windows already limits these child names to ASCII. The comparison conservatively excludes ASCII-case aliases on Unix too; it is not a general Unicode normalization or filesystem-name equivalence oracle. Arbitrary Unix names and unusual filesystem alias rules remain a separate authority boundary, not a guarantee supplied by this comparison.

Publication and success binding

Publication remains one same-parent no-replace directory rename. The shared Windows primitive uses the existing extended rename attempts and legacy fallback; their retry schedule and error/handle-close behavior are unchanged. Before the legacy call, the complete shared flags word is explicitly reset to zero. The extended API interprets that storage as flags, but the legacy API interprets its first byte as ReplaceIfExists; reusing the extended POSIX flag would request replacement. This follows Microsoft's FILE_RENAME_INFORMATION definition. It is a correction of requested authority, not evidence of a demonstrated directory-overwrite exploit; Windows independently restricts replacement of existing directories.

Immediately after rename succeeds, the authority latches publication before any hook, reopen, content verification, or path comparison. Published state never regains pre-publication cleanup authority.

The descendant src handle is released before the rename, preserving the Windows publication prerequisite. A failed rename must return its selected error before any src reopen. Reopening is verification-only after successful publication, never a way to reacquire cleanup authority. Previously, an unconditional reopen after failure could adopt an independently created src replacement containing the original tracked files. Their matching file identities did not make the replacement directory owned, yet subsequent cleanup could delete it. Returning before the reopen preserves that ownership history without changing successful publication or the platform rename implementation.

Success requires all of the following observations:

  1. Reopened held files exactly match the checked template bytes and both directory inventories remain exact.
  2. The retained parent still binds its expected absolute parent path.
  3. The retained published stage still binds the expected destination path.
  4. The CLI's captured original parent spelling still resolves to its initial identity, including when canonicalization originally traversed an alias.

These are checked observations, not a filesystem lock, permanent binding, atomic visibility across multiple path queries, hostile same-principal isolation, crash recovery, or power-loss durability. The host must continue to control its selected parent and ancestry. A later caller can change a pathname after any successful observation.

Failure and preservation

Malformed invocation remains exit 2; creation/publication failures remain exit 1. Lower errors retain the existing Exists, StageExists, Changed, and Invalid vocabulary. No new wire schema or diagnostic alias is introduced.

Before publication, cleanup is limited to the exact held stage inventories. An incomplete write, untracked file, changed identity, or foreign inventory can leave inert staging residue. Failure does not promise that every staging directory disappears; cleanup must not infer authority from a nonce prefix.

Once the original src handle has been released for publication, a failed rename leaves the stage inert, including an ordinary output collision. Dropping the authority performs no source or root discard. The original rename error remains selected even if src is now absent or cannot be reopened. A later publication call on that authority rejects before another rename because its original source authority is gone. Earlier preparation failures retain their existing exact-inventory cleanup behavior.

After successful rename, failed content or path binding reports failure and retains the complete published tree, even if its original name has been moved. It must not delete that tree or a foreign replacement at the requested path. There is no post-publication rollback. Callers reconcile retained output and current paths externally.

Required evidence

Preserve existing deterministic templates, Project check/test/Web behavior, CLI grammar, pre-write parent/stage substitution, inventory rejection, and fresh-output tests in crates/semaprax-toolchain/tests/cli_new_project_v1.rs.

The new-project destination and quickstart output-parent rejection fixtures must create a real directory link before exercising rejection. They share one test-only helper: Unix uses a symbolic link; Windows uses a directory junction and verifies its reparse attribute and exact fixture-owned target. Creation failure fails the test instead of silently omitting the hostile case when Windows symbolic-link privilege is unavailable. The rejection assertions are unconditional and retain the link, foreign sentinel bytes and exact target inventory before explicit link cleanup. This is Windows junction evidence, not evidence that privileged Windows symbolic-link creation was exercised. Production path admission and successful template bytes are unchanged.

New regression evidence must cover successful relative and parent-relative inputs; substitution after physical rename of the published directory and its parent; original ancestor-alias displacement; unchanged foreign sentinels and the displaced original inventory after error and drop; and partial/untracked stage residue that is not adopted for cleanup.

Failed-rename evidence must also cover a real output collision after descendant release, both with unchanged src and with a substituted source directory. The Unix substitution case moves the original tracked files into an independently created replacement directory: exact bytes and file identities alone must not authorize deleting that directory. Retain directory identity witnesses and require the original displaced directory, replacement directory, tracked files, stage root and foreign output to survive error and drop. A missing or linked src must not mask the primary collision error or authorize another publication attempt. These cases are bounded namespace-substitution observations, not hostile same-principal isolation or crash recovery.

Staging-name regressions must use invocation-local candidate selection rather than racing or resetting the process-global serial. Cover exact and ASCII-case collisions, the unchanged attempt ceiling, successful publication with exact template bytes, final-path absence during staging/writes and injected failure, and lower-level rejection without creating any child. Existing-output errors and foreign bytes must remain unchanged. The CLI collision/binding cases passed locally on macOS; lower-level and Windows-specific gates remain separate.

Windows-specific tests must force the extended-to-legacy transition and inspect the actual submitted replacement field. They also exercise native legacy success and existing regular-file/directory collision preservation. A physical directory-collision test alone cannot detect the wrong flags field because the OS can reject directory replacement independently.

The full cli_new_project_v1 test binary passed 15 tests locally on macOS arm64 with Rust 1.98. The separate quickstart suite passed nine tests and the version suite passed six. These results do not prove installation, release archives, Windows execution, or hosted gates, and cannot promote WP-06 or any completion-matrix row without its remaining required evidence.

The failed-rename replacement-directory regression was executed against the old ordering on both macOS arm64/Rust 1.98 and Linux arm64/Rust 1.88: dropping the failed authority actually removed the replacement source directory and stage. The same unchanged regression passes after propagating the rename failure before reopening. All nine lower project-publication tests and all 16 calculator-and-library CLI cases pass on both hosts; the complete lower package's 46 unit tests and warnings-denied package Clippy also pass on macOS. Linux used the existing offline, capability-dropped container with read-only source. The CLI's relative-path fixture requires a writable working directory: the initial read-only-checkout run failed setup, then the exact Cargo-reported test executable passed all 15 cases when launched from container /tmp. This is not a source/test relaxation, Windows evidence or a full quality run.

Focused gates, to run on the required hosts before promotion:

cargo test --locked -p semaprax-native-rust-interop-platform-sys --lib platform::publish_tests
cargo test --locked -p semaprax-native-rust-owned-data-package --lib project_publication::tests
cargo test --locked -p semaprax-toolchain --test cli_new_project_v1

The first gate requires Windows; a zero-test result elsewhere is not evidence. Physical post-rename displacement and ancestor-alias fixtures are Unix-only. Common success, publication-latch unwind, residue and relative-path cases still require execution on every supported host. Existing full quality and Project gates remain independently required.