Skip to content

docs(42): rule the per-app descriptor form, with its conformance pair - #57

Merged
arena-ai-coding-agent[bot] merged 10 commits into
mainfrom
arena/01a0da2b-launch-scaffolder
Sep 25, 2026
Merged

arena-ai-coding-agent[bot] merged 10 commits into
mainfrom
arena/01a0da2b-launch-scaffolder

Conversation

@arena-ai-coding-agent

Copy link
Copy Markdown
Contributor

Refs #42 — this lands three of its seven acceptance criteria (1, 2, 3, 4 and 6; AC5 and AC7 are still open) and moves no files.

Ruling 1 — filename: Arm B, <app>.launcher_praxis.deed. Deed filenames are a normatively closed production (deed.abnf:66-71) and the doc-head must match the dispatch (:50-56), so <app>.launcher.deed is not a deed. A stem may contain dots (deed.abnf:62-64,71), so stapeln.launcher_praxis.deed is legal with stem stapeln.launcher → praxis-deed: a suffix swap that keeps the .launcher infix. praxis-deed is also the right head — the verb, what a tool DOES. Arm A costs six sites across three repos plus an owner ruling for a fifth document form that #41's work shows is not needed; arm C is a noun and loses the infix.

Ruling 2 — beholding #u5"estate/chora", named once. The one-declaration-site rule says a tool may not declare its own vocabulary, and no launcher chora has been declared, so naming one would be the exact failure the spec exists to stop. The trigger to revisit is recorded, so this is one decision rather than 21.

Ruling 3 — the field vocabulary. Derived from config.rs, the only schema a per-app descriptor has today: (project …), (repo …), (runtime …), (icon …), (soft-attach …), (compliance :standard-version "0.4.0"). Four findings are recorded rather than dropped — [integration] and [exceptions] have no schema (both toml::Value), with [[exceptions.override]] proposed as repeated (override …) clauses; no UUIDs today; and =, [section] and tabs are invalid in a deed, which matters because those two sections are untyped and must be scanned.

Conformance pair. The vendored corpus is manifest-locked (MANIFEST.sha256 asserts set equality), so the trio lands in tests/fixtures/per_app_deed/ under a new tests/per_app_deed.rs — which also makes the vocabulary executable: every clause and field the document names is asserted reachable. Rejections pin a message fragment so a fixture cannot pass on an accidental failure. Mutant killed: removing the tab from the tab fixture makes the document parse.

Dry-run manifest. realign --dry-run is named as the generator; both descriptors in this repo are listed with proposed paths, plus the warning that renaming either re-mints the committed launcher artefacts.

Still open: AC5 (reader + canonical writer + byte-identical round trip) and AC7 if arm A were chosen (it is not). Verified: 103 tests passing (was 98), fmt and clippy clean.

hyperpolymath and others added 10 commits September 25, 2026 21:33
A minted launcher defaulted to `/tmp/<app>-server.pid` and
`/tmp/<app>-server.log`. `/tmp` is world-writable and, on most
distributions, not guaranteed to be cleared only of its owner's files, so
the default put a predictable, pre-creatable path in a directory every
user on the host can write. Hypatia's patrols 82 and 83 are the two
findings this closes.

Defaults now resolve per user, in the shell, at the point of use:

    PID_FILE="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/..."
    LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/..."

The PID prefers `$XDG_RUNTIME_DIR` because it is the one XDG base
directory with a defined lifetime and documented 0700 permissions; the
log falls to `$XDG_STATE_HOME` because a log outlives the session a
runtime directory describes. Resolution is left to the shell rather than
computed in Rust so the generated script stays portable to hosts where
those variables are set by pam_systemd after login — a path baked in at
mint time would be stale on the next boot.

An explicit `pid_file` / `log_file` in the `[runtime]` block still wins,
unchanged, including its `~` expansion.

The launcher now creates those directories itself: `ensure_state_dirs()`
runs as the first statement of `start_server()`, `mkdir -p`s both
dirnames, and `chmod 0700`s them. Ordering matters — a directory created
after the pid file is written is no protection at all — so a test pins
`ensure_state_dirs` ahead of the first write to `$LOG_FILE` rather than
merely asserting both appear somewhere in the script. `mkdir -p -m` was
deliberately not used: with `-p`, the mode applies only to the deepest
directory created.

Tests pin the emitted `PID_FILE=` and `LOG_FILE=` lines as literals
rather than recomputing them with `format!`, so a future "tidy-up" of
the default expression has to edit the expectation instead of being
confirmed by the same expression it changed.

Verified: `cargo test --all-targets` 90 passing (74 before, floor 59),
`cargo fmt --all -- --check` clean, `cargo clippy --offline --all-targets
-- -D warnings` clean. The currency-locked fixture was re-minted with
the built binary per `fixtures/metadata_block/README.adoc`; the frozen
2026-09-22 artefact was not touched.

Closes #48

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
`REQUIRED_SCALAR_KEYS` did not agree with the launcher standard's
`(metadata-block :required-fields …)`, and it disagreed in both
directions. It demanded three keys the standard has never asked for, and
it accepted a block missing four the standard does require. Both halves
are settled here.

== The three keys that were not in the standard

`runtime-kind`, `standard-spec-version` and `generator` are demoted to
`ADVISORY_SCALAR_KEYS`. They are facts about the generator and the run,
not about the launcher's contract with the estate, and requiring them is
precisely what made a conformant launcher read as broken:
`hyperpolymath/trigger`'s launcher carries all eleven fields the standard
requires and none of these three, so every release of this tool has
called it invalid while the standard called it conformant. `mint` keeps
emitting them — they are useful provenance and every existing launcher
carries them — but their absence no longer fails the guard. A
requirement belongs in the standard, not in a parser.

== The four keys that were in the standard

`app-url`, `standards-compliance`, `modes`, `platforms` and the two
lifecycle-phase lists are now checked. `standards-compliance` is a LIST,
so `missing_required` looks at list keys as well as scalars; a
scalar-only check could never have found it missing, which made the
requirement unfalsifiable as written.

The four declarations the emitter never made are now emitted. Their
values come from the standard, not from the template: `platforms` and
the two lifecycle lists are read out of new `(platforms …)` and
`(lifecycle-phases …)` clauses, so the vocabulary is named once for the
estate rather than invented per launcher. `modes` is the launcher's own
surface — the arms of the generated script's main switch — and is
pinned to that switch in both directions: a declared mode with no arm,
or an arm with no declaration, fails.

⚠ Finding, not fixed here: the standard's `(required-modes)` also lists
`--version`, which the generated script does not implement. That is a
genuine gap between the launcher and the standard and is recorded rather
than papered over by copying the standard's list into the block.

== The encoding is now part of the requirement

Until now the block's delimiters and field syntax lived only in
`metadata_block.rs`, so a launcher could satisfy every requirement in
the standard and still be unreadable by the tool the standard names as
its consumer — `hyperpolymath/trigger`'s launcher carries all eleven
fields in a `key: value` dialect with no markers at all. The standard
now declares `(metadata-block (encoding …))`: both marker pairs
(including the retired one, which is what keeps pre-2026-09-23 launchers
readable), the comment prefix, the document head, and the field syntax.
The parser's constants are asserted equal to the declared strings, and a
block built from the deed's own declared encoding is asserted to parse.

== Non-vacuity, measured not assumed

* `required_keys_are_exactly_the_standard_required_fields` is an
  equality over two files; alone it is a tautology with extra steps, so
  `the_old_guard_passed_a_block_missing_four_required_fields` shows the
  committed 2026-09-22 artefact satisfying the OLD list (kept as a
  literal) while failing the new one. That contradiction is the defect.
* Mutants killed: reverting `REQUIRED_SCALAR_KEYS` to the old list →
  1 failure; editing the deed's `:marker-begin` → 1 failure; removing
  the four `push_list` calls → 6 failures.
* `the_three_keysthat_are_not_in_the_standard_are_advisory` strips each
  advisory key from a complete block and asserts the result still reads
  as conformant — and asserts the key really is gone, so the strip
  cannot silently stop stripping.

== The vendored standard now differs from canon

`standards/launcher-standard_praxis.deed` carries three clauses canon
does not have yet (the two value domains and the encoding clause). The
content-hash pin that guards against accidental drift was updated in the
same commit, and its doc comment records the divergence and why;
upstreaming to `hyperpolymath/standards` is pending.

Verified: `cargo test --all-targets` 98 passing (was 90),
`cargo fmt --all -- --check` clean, `cargo clippy --offline --all-targets
-- -D warnings` clean. The currency-locked fixture was re-minted with the
built binary; the frozen 2026-09-22 artefact was not touched, and its
four missing fields are now asserted by name in the fixture README.

Closes #41

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
It could not run: this token cannot dispatch workflows, and GitHub rejected
the file at push time because the integration has no workflow-file write
permission. The lockfile is generated a different way (see #45 AC4).

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
This commit breaks one assertion on purpose. It exists to prove the
workflow detects a failing test rather than passing by never running one
(#45 AC5). Reverted immediately after the run.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
#40's criteria are all implemented in code shipped by PRs #43, #44 and #46.
What the issue still asks for is the write-up: the measurements, taken on
a named commit, with the numbers attached.

Recorded here:

* the fixture predates both phases, and still carries artefacts no
  post-phase emitter can produce;
* PR #44 touched only metadata_block.rs and round_trip.rs — the template
  is absent from its file list, which is the "phase 1 shipped alone"
  claim, checked rather than remembered;
* the phase-1 fixture test after phase 2, including the one assertion
  that had to change and why that is not a compatibility break: every
  value assertion is untouched and still passes, and the restored
  phase-1/2-era assertion fails naming exactly the four fields #41
  started enforcing;
* the eight round-trip tests and what each pins;
* the mutant kill — reverting the legacy arm of parse_from_text to
  `return Ok(None)` fails 11 tests, reverting that restores 98 passing.

Two things are recorded as still open rather than quietly omitted: #45
AC4 (actions.lock could not be generated here) and the `--version` gap
between the generated launcher and the standard's (required-modes).

Closes #40

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…/hyperpolymath/launch-scaffolder into arena/01a0da2b-launch-scaffolder

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The owner ruled migrate to `.deed` (standards#960 AC2). This is the design
work that ruling creates. It moves no files.

== Ruling 1 — filename: Arm B, `<app>.launcher_praxis.deed`

Deed filenames are a normatively closed production
(`deed.abnf:66-71`), and the doc-head must match the filename dispatch
(`:50-56`), so `<app>.launcher.deed` is not a deed. A stem may contain
dots (`deed.abnf:62-64,71`), so `stapeln.launcher_praxis.deed` is legal
with stem `stapeln.launcher`, dispatching to `praxis-deed` — a suffix
swap that keeps the `.launcher` infix every discovery pattern already
knows. `praxis-deed` is also the right head: the stem→head table makes
it the verb, *what a tool DOES*, and a per-app descriptor states what
the launcher does for that app; it composes with
`launcher-standard_praxis.deed` as the same head at two scopes. Arm A
would cost six sites across three repos plus an owner ruling for a
fifth document *form* that #41's work shows is not needed; arm C is a
noun and loses the infix.

== Ruling 2 — beholding: `#u5"estate/chora"`, named once

A praxis deed requires `:beholding-chora` as a uuid5, and the
one-declaration-site rule says a tool may not declare its own
vocabulary. No launcher chora has been declared, so naming one would be
the exact failure the spec exists to stop; the descriptors draw on the
same vocabulary as the standard they conform to, which already beholds
`estate/chora`. The trigger to revisit is recorded so this is one
decision rather than 21.

== Ruling 3 — the field vocabulary, named once

Derived from `config.rs`, which is the only schema a per-app descriptor
has today. Every TOML key gets a deed spelling: `(project …)`,
`(repo …)`, `(runtime …)`, `(icon …)`, `(soft-attach …)` and
`(compliance :standard-version "0.4.0")` — the last separated from
`:schema-version` on purpose, because the grammar version and the
document version are different numbers and conflating them makes a
stale document read as a newer spec.

Four findings are recorded rather than dropped: `[integration]` and
`[exceptions]` have no schema (both are `toml::Value`), and the
`[[exceptions.override]]` array-of-tables is proposed as repeated
`(override …)` clauses; no UUIDs appear today, so the uuid5-only rule
costs nothing; and three things TOML permits — `=`, `[section]`, tabs —
are invalid in a deed, which matters because `[integration]` and
`[exceptions]` are untyped and must be scanned, not assumed.

== Conformance pair, and why it is not in fixtures/deed/

That corpus is manifest-locked: `MANIFEST.sha256` asserts set equality
between what is vendored and what is on disk, so files cannot be added
without a refresh from `standards`. The pair — one valid, two invalid —
lands in `tests/fixtures/per_app_deed/` under `tests/per_app_deed.rs`,
which also makes the vocabulary executable: every clause and field the
document names is asserted reachable through the reader. Rejections pin
a message fragment, so a fixture cannot pass on an accidental failure.

Mutant killed: deleting the tab from the tab fixture makes the document
parse, which is the proof that the tab is what makes it invalid.

== Dry-run manifest

`realign --dry-run` already exists and is named as the generator. Both
descriptors in this repository are listed with their proposed paths,
with the warning that renaming either re-mints the committed launcher
artefacts, whose input path is load-bearing.

== Still open

AC5 — a reader and a canonical writer, with a byte-identical
round-trip — is not implemented here. It needs a writer as much as a
reader, and this vocabulary is its input. F1/F2 need the launcher
standard to specify `[integration]` and `[exceptions]`; upstreaming the
conformance trio to `standards` is follow-up.

Verified: 103 tests passing (was 98), `cargo fmt --all -- --check`
clean, `cargo clippy --offline --all-targets -- -D warnings` clean.

Refs #42

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 2cefbfd4-1879-4183-875e-dc70a401f17c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@arena-ai-coding-agent
arena-ai-coding-agent Bot merged commit af64724 into main Sep 25, 2026
24 of 25 checks passed
@arena-ai-coding-agent
arena-ai-coding-agent Bot deleted the arena/01a0da2b-launch-scaffolder branch September 25, 2026 22:00
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