diff --git a/.github/workflows/launcher-standard-lockstep.yml b/.github/workflows/launcher-standard-lockstep.yml index 21fdc3617..66a5fe999 100644 --- a/.github/workflows/launcher-standard-lockstep.yml +++ b/.github/workflows/launcher-standard-lockstep.yml @@ -4,9 +4,9 @@ name: launcher-standard lock-step # Enforces the sync requirement declared in launcher/README.adoc §Sync -# requirement: the machine-readable form (launcher-standard.a2ml) and the -# prose form (docs/UX-standards/launcher-standard.adoc) MUST be edited in -# the same PR. Drift between the two is a standards-compliance bug. +# requirement: the machine-readable form (launcher-standard_praxis.deed) and +# the prose form (docs/UX-standards/launcher-standard.adoc) MUST be edited +# in the same PR. Drift between the two is a standards-compliance bug. # # How it works: the workflow triggers only when one of the two files in # the lock-step group is touched. Inside the job we then verify that the @@ -16,7 +16,7 @@ name: launcher-standard lock-step on: pull_request: paths: - - 'launcher/launcher-standard.a2ml' + - 'launcher/launcher-standard_praxis.deed' - 'docs/UX-standards/launcher-standard.adoc' concurrency: @@ -48,7 +48,7 @@ jobs: # The lock-step group. Editing any of these MUST be accompanied # by edits to the others in the same PR. declare -a GROUP=( - "launcher/launcher-standard.a2ml" + "launcher/launcher-standard_praxis.deed" "docs/UX-standards/launcher-standard.adoc" ) @@ -78,7 +78,7 @@ jobs: echo "::error::launcher-standard lock-step violation" echo "" echo "Per launcher/README.adoc §Sync requirement, the machine-readable" - echo "form (a2ml) and the prose form (adoc) MUST be edited in the" + echo "form (deed) and the prose form (adoc) MUST be edited in the" echo "same PR. Drift between them is a standards-compliance bug." echo "" echo "Touched in this PR:" @@ -93,7 +93,7 @@ jobs: echo "" echo "Fix: add the matching edit to the file(s) above. If your" echo "change is genuinely one-sided (e.g. a prose-only typo fix" - echo "with no a2ml equivalent), add a no-op whitespace touch to" + echo "with no deed equivalent), add a no-op whitespace touch to" echo "the other file with a commit message explaining why." exit 1 fi diff --git a/1-formats/deed/mappings/launcher-standard-to-praxis-deed.adoc b/1-formats/deed/mappings/launcher-standard-to-praxis-deed.adoc new file mode 100644 index 000000000..f84e6b6a9 --- /dev/null +++ b/1-formats/deed/mappings/launcher-standard-to-praxis-deed.adoc @@ -0,0 +1,366 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 += Mapping spec — `launcher-standard.a2ml` → `praxis-deed` +Campaign: standards#837 · Frame: link:README.adoc[mappings/README] · +Status: **RULED AND TRANSLATED** — owner ruling D73-C (standards#787, +2026-09-22): "convert `launcher-standard` to `.deed`". Unlike the other specs +in this directory, this one is not a proposal: the translation it authorises +has been emitted, validated and committed in the same change. + +This is the campaign's first *executed* family, and it is deliberately a small +one — a single file with a single machine consumer — chosen so the frame is +proved on something whose blast radius can be enumerated rather than on the +222-file estate corpus. + +== 1. Source surface + +* **Canonical instance:** `launcher/launcher-standard.a2ml` in + `hyperpolymath/standards` — 268 lines, 11,863 B, 18 sections, `version = + "0.3.0"`. Exactly one instance estate-wide; this family has no per-repo + fan-out, which is why it is a safe first cut. +* **Prose counterpart:** `docs/UX-standards/launcher-standard.adoc`, bound to + the machine-readable file by `.github/workflows/launcher-standard-lockstep.yml`, + which fails the build if one changes without the other. +* **🪤 Vendored second source (NOT the mapping input):** + `launch-scaffolder/standards/launcher-standard.a2ml` — 5,272 B, `version = + "0.2.0"`, baked into the binary via `include_str!()`. It is **missing the + entire `[resolution]` section**, so the binary ships a standard predating the + fix that removed hard-coded `/var/mnt/eclipse/...` paths. The mapping input is + the standards copy; the vendored copy is a *consumer* obligation (§7), not a + second surface to translate. Translating both would mint two deeds that + disagree. + +== 2. Target deed form + +[source] +---- +launcher-standard_praxis.deed +---- + +`praxis-deed` head, by the `_praxis.deed` dispatch row. + +[NOTE] +==== +**There is no such file as `launcher-standard.deed`.** DEED filenames are a +closed dispatch set of four shapes, and the in-file head must match the +filename. A file named `launcher-standard.deed` matches no row and is rejected +by `deed_lint.py` **at line 1** — verified by mutant, not read off the spec. +"Convert X to `.deed`" therefore never produces `X.deed`. +==== + +=== Why `praxis-deed` and not `repo-deed` + +`repo-deed` records what a repository *is*. This document records what a +conforming launcher *does* — and, decisively, it is **almost entirely +first-match-wins ladders**: six resolution paths, five standard-search paths, +three fallback stages, four browser commands per platform, four GUI dialog +commands, two startup-command paths. + +The grammar's §"Order is not semantic — except `:schema-version`" makes that +decisive rather than stylistic: + +[quote] +____ +Where precedence genuinely matters — the rules inside a praxis deed — it is +carried by an explicit `:priority` INTEGER, never by file position. +____ + +with the rationale: + +[quote] +____ +with first-match-wins, a broad rule inserted above a narrow one silently +shadows it. No error is raised; the narrow rule simply stops firing. +____ + +A `repo-deed` rendering would have put those ladders in bare DEED lists, and a +DEED list carries **no ordering**. The ordering that makes +`desktop-tools-search` correct would have been discarded silently, with the +file still validating. `praxis-deed` + `:priority` is the only form in which +this standard survives translation intact. + +=== Head + +[source] +---- +(praxis-deed + :schema-version "1.0.0" ; REQUIRED, structurally FIRST + :canonical-name "launcher-standard" ; REQUIRED + :beholding-chora #u5"estate/chora" ; REQUIRED — a UUID, never a bare filename + :standard-version "0.4.0" + :standard-date "2026-09-22" + :compliance (...)) +---- + +Three fields are required by the grammar's §"The praxis-deed form"; the +remainder are domain payload. Payload clauses on a head are established +practice, not an invention: the conformance fixture +`tools/fixtures/valid/rsr-template-repo_chora.deed` carries ~12 rich nested +domain clauses and **passes**. The v0.1 WARNING in the grammar spec — that +inventing fields "is precisely the failure this document exists to stop" — +governs inventing required *grammar* fields, not domain payload, and is +therefore not engaged here. + +=== ⚠ Three version numbers meet in this file + +|=== +| number | what it versions | value + +| `:schema-version` | the DEED format itself | `"1.0.0"` — fixed by the grammar +| `:standard-version` | the launcher standard's own contract | `0.3.0` → **`0.4.0`** +| the prose adoc's `:revnumber:` | the human document | tracked separately +|=== + +`:standard-version` is bumped because the resolution ladders now name the +`.deed` file. That is **consumer-visible**: a launcher searching for +`launcher-standard.a2ml` will not find this standard. It is a breaking change +to the search surface, not a re-encoding. + +== 3. Field table + +`→` left: a2ml surface. Right: deed path + type + transform. + +[cols="2,3,4"] +|=== +| a2ml surface | deed path | transform / type + +4+| *Head* + +| `[spec] version` | `:standard-version` | STRING, **bumped `0.3.0` → `0.4.0`** (see above) — the one value that is not carried verbatim +| `[spec] date` | `:standard-date` | STRING (no date type in the grammar) +| `[spec] compliance` | `:compliance (...)` | list of STRINGs — a *set*, unordered, so **no `:priority`** +| — (new) | `:schema-version` | STRING `"1.0.0"`, structurally first +| — (new) | `:canonical-name` | STRING `"launcher-standard"` +| — (new) | `:beholding-chora` | `#u5` literal; body is the NAME input, never hex + +4+| *Ordered ladders — `:priority`-bearing clauses* + +| `[resolution] desktop-tools-search` (6) | `(resolution (desktop-tools-search (path :priority 10 :value "…" :note "…") …))` | array → one `(path …)` clause each, `:priority` 10,20,30… **in source order** +| `[resolution] standard-search` (5) | `(resolution (standard-search (path :priority N :value "…") …))` | as above; ⚠ every `:value` is **rewritten** `…/launcher-standard.a2ml` → `…/launcher-standard_praxis.deed` +| `[fallback-ladder] stages` (3) | `(fallback-ladder (stage :priority N :name gui …) …)` | array of inline tables → `(stage …)` clauses; `name`/`colour`/`on-failure`/`behaviour` values are closed enums → SYMBOLs +| `[browser-launch] linux` (4) | `(browser-launch (linux (command :priority N :value "…") …))` | array of STRINGs → `(command …)` clauses. The platform key becomes a **nested clause**, not a field, because its value is a ladder +| `[browser-launch] macos` / `windows` (1 each) | `(browser-launch (macos (command :priority 10 :value "open")) …)` | same shape at length 1 — uniformity beats brevity; a one-element ladder still grows +| `[error-visibility] gui-dialog-chain` (4) | `(error-visibility (gui-dialog-chain (command :priority N :value "…") …))` | as above +| `[runtime] startup-command-search` (2) | `(runtime (startup-command-search (path :priority N :value "…") …))` | as above +| `[integrity] fallback-paths` (1) | `(integrity (fallback-paths (path :priority 10 :deprecated #t :value "…")))` | ladder shape retained; the legacy eclipse literal carries `:deprecated #t` + +4+| *Unordered sets — bare lists, deliberately NO `:priority`* + +| `[required-modes] runtime` / `integration` / `meta` | `(required-modes :runtime ("--start" …) …)` | lists of STRINGs. A launcher must accept **all** of them; there is no first match, so ordering is not semantic and must not be faked +| `[optional-modes] developer` | `(optional-modes :developer (…))` | list of STRINGs +| `[disinteg] remove` | `(disinteg :remove (desktop-file …))` | closed vocabulary → list of SYMBOLs +| `[disinteg] preserve` | `(disinteg :preserve ("…" …))` | paths stay STRINGs +| `[a2ml-metadata-block] required-fields` | `(metadata-block :required-fields (…))` | list of STRINGs — **and the clause is renamed**, see §4 + +4+| *Scalar clauses, verbatim* + +| `[fallback-ladder]` 6 scalars | `(fallback-ladder :wrapper "…" …)` | STRINGs; `banner-visibility = "loud"` → SYMBOL `loud` (closed enum) +| `[version-output]` 5 keys | `(version-output :first-line-format "…" :exit-code 0 :machine-greppable #t)` | STRING / INTEGER / BOOLEAN — `true` → **`#t`** (`true` is a parse error in DEED) +| `[aliases]` 2 keys | `(aliases (alias :from "--browser" :to "--auto") …)` | ⚠ key→value table → explicit `(alias …)` clauses. **Both halves stay STRINGs**: a DEED symbol cannot begin with `-` +| `[default-mode] mode` | `(default-mode :mode "--auto")` | STRING, same reason +| `[runtime]` 8 keys | `(runtime :background nohup :pid-file-pattern "…" …)` | `background` → SYMBOL; patterns and env-var names STRINGs; timeouts INTEGERs +| `[browser-launch]` 3 scalars | `(browser-launch :env-var-override "…" …)` | STRINGs +| `[error-visibility]` 3 scalars | `(error-visibility :reference-impl "…" :always-also-to-stderr #t …)` | STRING / BOOLEAN +| `[integration.linux]` 8 keys | `(integration (linux :apps-dir "…" :desktop-file-permissions 444 …))` | the dotted TOML section becomes a **nested clause**; `444` stays an INTEGER (it is a mode, and DEED has no octal literal — the source did not write `0444` either) +| `[integration.macos]` 5 / `[integration.windows]` 5 | `(integration (macos …) (windows …))` | as above; `bat-fallback = true` → `#t` +| `[integrity]` 3 scalars | `(integrity :verification-tool "…" :fatal-on-failure #f)` | STRING / BOOLEAN +| `[soft-attach] reference-impl` | `(soft-attach :reference-impl "…")` | STRING +| `[soft-attach] tools` (3) | `(soft-attach (tool :name feedback-o-tron :style event :trigger on-start-failed …) …)` | array of inline tables → `(tool …)` clauses. `name`/`style`/`trigger` are closed enums → SYMBOLs; `event`/`command` are free text → STRINGs. **No `:priority`** — these are independent hooks on the same trigger, not a ladder +|=== + +Fields not listed do not exist in the deed era for this family. The translation +is total over the source: all 18 sections are accounted for. + +== 4. Drop doctrine (what is NOT translated, and why that is right) + +* **`[a2ml-metadata-block]` is renamed to `(metadata-block …)`.** Its *content* + is carried in full; only the format name in the clause name is dropped. A2ML + is a dead format name — the owner's doctrine is "do not push it around, leave + it in place, build on it, or connect anything new to it" — and carrying + `a2ml` forward in a live DEED clause name is connecting something new to it. + ⚠ This is the one **renamed** identifier in the translation; a consumer + grepping for `a2ml-metadata-block` will not find it, by design. +* **Instructional comments are split, not dropped wholesale.** The source + carries two kinds of comment and they are treated differently: +** *Load-bearing caveats* survive as `;;` comments, because deleting them would + lose a warning no field encodes — the `deployed-symlink` DEPRECATED notice, + the "intentionally ugly — visibility beats aesthetics" rationale for `loud`, + the note that `XDG_DATA_HOME` defaults to `$HOME/.local/share`, and the + `[resolution]`-resolved markers on `deployed-name` / `tool-name`. Several are + promoted from comments to real `:note` fields on the `(path …)` clauses, + where they become greppable rather than incidental. +** *Tutorial prose* is dropped: the multi-line explanations of what + `keepopen.sh` is for, the three `hp_soft_attach_*` primitive signatures, the + "Downstream launchers SHOULD source this" advice. These are **guidance, not + data**; they live on in `docs/UX-standards/launcher-standard.adoc` and in + `launcher/soft-attach.sh` itself, which the deed names by path. A deed is a + record, not a manual — the same doctrine the CLADE and STATE specs apply. +* **The `trigger` vocabulary comment is dropped as a comment and preserved as + fact**: the source lists four legal triggers of which only + `on-start-failed` is used. The unused three are not emitted — a deed records + what is, not what is permitted. The vocabulary is the prose document's job. +* **Nothing is dropped for being empty.** This family has no empty-slot fields + of the kind CLADE deliberately preserves, so the question does not arise. + +== 5. Provenance & validation + +* **P-1 (source must parse first):** the `.a2ml` is TOML-shaped and is read with + a TOML reader, never grepped — the estate's own rule, and the reason the + `[aliases]` quoted keys and the inline-table arrays were encoded correctly + rather than line-scraped. +* **P-2 (output must parse as DEED):** the emitted deed is validated by + `1-formats/deed/tools/deed_lint.py`, the estate's own conformance tool — + stdlib-only, and the same one `deed-conformance.yml` runs over every committed + `*.deed`. **Result: `OK`**, with `--self-test` (14-case corpus) and + `--fixtures` green in the same run as positive controls, so a pass is + attributable to the file and not to a broken validator. +* **P-3 (a passing validator proves nothing until a mutant dies).** Seven + controls, six mutants and one negative: ++ +|=== +| mutant | expected | result + +| M1 — file renamed `launcher-standard.deed` | rejected by dispatch | **DIED** rc=1, line 1 +| M2 — one `key = value` line (a2ml habit) | no `=` outside a symbol | **DIED** rc=1, line 25 +| M3 — a tab used as a separator | tabs invalid everywhere (K9-consistent) | **DIED** rc=1, line 19, "HTAB" +| M4 — bare `true` for `#t` | only `#t`/`#f` are booleans | **DIED** rc=1, line 119, "boole…" +| M5 — `:schema-version` removed | required on every head | **DIED** rc=1, line 261 +| M6 — trailing content after the final `)` | full input must be consumed | **DIED** rc=1, line 263, "trail…" +| NEG — untouched copy | passes | **PASSES** (correct) +|=== ++ +M1 is the load-bearing one: it is the *only* direct evidence that +`launcher-standard.deed` is not a legal name, and it was run because the spec +said so — reading the dispatch table is not the same as testing it. +* **P-4 (dispatch check):** the stem is everything before the **final** suffix, + and dispatch is **exact-stem-first** — `estate_chora.deed` is tested before + `*_chora.deed`. This file's stem is `launcher-standard_praxis`, matching the + `_praxis.deed` row, and the in-file head `(praxis-deed` agrees. +* **P-5 (`a2ml_to_deed.py` was NOT used, deliberately):** the estate's + translator **fails closed on untabled fields** by design, and + `launcher-standard` is not in its table. This translation is hand-encoded + against this spec. ⚠ Adding a table entry that made the generic translator + emit this file would be a *second* authority for the same output; if the + translator is ever taught this family, this spec is the input to that work + and the generated output must be diffed byte-for-byte against the committed + deed. +* **P-6 (the lockstep gate survives the rename):** the workflow names the source + file in **two** places — the `on: pull_request: paths:` filter *and* the + in-job `declare -a GROUP=(…)` array. Updating only the filter leaves the gate + asserting a file that no longer exists; updating only the array leaves the + gate never firing. Both are updated in the same change, and the transition PR + itself satisfies the gate because it touches the deed *and* the prose adoc. + +* **P-7 (the source is removed, and stays recoverable):** per the owner's + standing A2ML doctrine — *"Do not push it around, leave it in place, build on + it, or connect anything new to it"* — `launcher/launcher-standard.a2ml` is + **deleted** in the same change, not left beside the deed. Leaving it would + recreate the dual-source defect this spec exists to close, and a `[section]` + file is the `toml-like` surface the owner ruled superseded on 2026-09-08. + The mapping input remains addressable for the life of the repository as git + blob **`adc283013cc50cff78f8c1ad0bf7846fc617d71e`** (11,863 B); recover it + with `git show adc28301`. ⚠ The byte count in §1 was wrong on first writing + (11,886) and was corrected against `git show HEAD: | wc -c`. Two other + counts in §1 were checked in the same pass and were right: 268 lines, 18 + `[section]` headers, and the vendored copy's 5,272 B. + +== 6. Acceptance greps (this family is preserved iff, post-translation) + +[source,bash] +---- +# 1. The deed exists and validates under the estate's own tool. +python3 1-formats/deed/tools/deed_lint.py launcher/launcher-standard_praxis.deed # ⇒ OK + +# 2. The a2ml source is GONE, not moved aside (A2ML doctrine: do not push it around). +test ! -e launcher/launcher-standard.a2ml + +# 3. No LIVE consumer still names the dead file. Three classes of mention are +# legitimate, and are excluded BY NAME rather than by luck: +# a. docs/audits/launcher-standard-review-2026-05-26.{adoc,a2ml} — a dated +# historical record of a 2026-05-26 review, correct as written; +# b. this mapping spec — the authority MUST name its source surface; +# c. launcher-standard_praxis.deed line 7 — a `;;` provenance comment +# recording what the deed was translated from. Wanted, not residue. +# ⚠ The path filter is DELIBERATELY UNANCHORED. GNU grep prefixes each +# result with `./`; ugrep — the estate's interactive default, a shell +# function wrapping ugrep 7.8.4 — does not. So a filter written +# `^\./docs/audits/` is INERT under ugrep and the check then FAILS in the +# correct end state. Verified against both readers: each returns the same +# 4 files, in different path shapes. +grep -rl 'launcher-standard\.a2ml' --exclude-dir=.git . \ + | grep -vE 'docs/audits/|launcher-standard-to-praxis-deed\.adoc|launcher-standard_praxis\.deed' +# ⇒ empty + +# 4. Every ordered ladder carries :priority. ⚠ Exclude `;;` comment lines: +# a bare count returns 32, five of which are the prose explaining the rule. +grep -vE '^\s*;;' launcher/launcher-standard_praxis.deed | grep -c ':priority' # ⇒ 27 +# 27 = 6 desktop-tools + 5 standard-search + 3 stages + 4+1+1 browser +# + 4 gui-dialog + 2 startup-command + 1 integrity-fallback. + +# 5. The unordered sets did NOT acquire a spurious :priority. +sed -n '/(required-modes/,/(optional-modes/p' launcher/launcher-standard_praxis.deed \ + | grep -c ':priority' # ⇒ 0 + +# 6. No tabs anywhere. +grep -nP '\t' launcher/launcher-standard_praxis.deed # ⇒ empty + +# 6b. Bare `true`/`false` used as a BOOLEAN: delegated to the validator (check 1), +# NOT to grep. ⚠ This one resists grepping and the failed attempts are worth +# recording, because both look right: +# grep -E '\b(true|false)\b' → 1 hit +# grep -E ':[a-z-]+ +(true|false)\b' → 1 hit +# Both match the same line, and it is CLEAN: +# :trust-command "gio set {file} metadata::trusted true" +# The word sits inside a STRING carried verbatim from the source, and the +# second pattern is fooled a second time because `::trusted ` inside that +# string re-supplies the `:keyword ` shape. Only a parser knows a value +# position from a string interior. `deed_lint.py` does; mutant M4 proves it +# does. A grep count is not a residue count. + +# 7. The lockstep gate names the new file in BOTH of its two FUNCTIONAL sites +# — the `on: paths:` filter and the in-job GROUP array — plus the header +# comment that explains the pairing. Updating only the filter leaves the +# gate asserting a file that no longer exists; only the array leaves it +# never firing. +W=.github/workflows/launcher-standard-lockstep.yml +grep -c 'launcher-standard_praxis\.deed' "$W" # ⇒ 3 (filter + GROUP + header prose) +grep -c 'a2ml' "$W" # ⇒ 0 (error text says "deed", not "a2ml") +grep -n 'uses:' "$W" # unchanged — so NO actions.lock edit is due +---- + +== 7. Consumer transition (recorded here; executed on its own track) + +The deed is the authority the moment it lands, but two consumers read the old +surface and must follow. They are listed so that "the translation is done" is +never mistaken for "the consumers are done" — `implemented` does not imply +`wired` (AGENTS.md §6). + +. **`launch-scaffolder`** — `crates/launcher-common/src/standard.rs` parses the + standard with `toml::from_str` and bakes a vendored copy via `include_str!()`. + ⚠ **There is no Rust `.deed` parser in the estate.** `deed-ecosystem/rs` is + the unmigrated A2ML markdown crate (`[package] name = "a2ml"`, with + `Block::Heading` / `Block::Paragraph`) and is not a starting point. The work + is a purpose-built recursive-descent parser in `launcher-common`; the + acceptance test is that the old TOML and the new deed parse to an **equal** + `Standard` value, and the vendored copy is made byte-identical to the + committed deed with a drift test asserting it. `[resolution]` joins the + required-section set — the vendored copy's omission of it is the live defect + this closes. +. **`trigger`** — `scripts/trigger-launcher.sh`, `dev-notes/HANDOVER.adoc`, + `docs/IMPLEMENTATION-STATUS.adoc` name the standard. Textual. + +Out of scope, recorded so it is not lost: **10 downstream `*.launcher.a2ml` +files** under `metadatastician/` and `_ESOTERIC_GROUP _SET/`. These are +per-application *instances* of the metadata block, a different family from this +standard, and they need their own spec before anything touches them. + +== 8. What this spec does not claim + +* That the `praxis-deed` head is the only defensible choice. `repo-deed` would + have validated. The argument for `praxis-deed` is §2's — a `repo-deed` + rendering would have had to discard ordering or fake it, and the grammar + names that exact failure. +* That the `:priority` step of 10 is meaningful. It is insertion headroom and + nothing else; only the relative order is semantic. +* That the vendored `launch-scaffolder` copy is now correct. It is not: it is + still v0.2.0, still missing `[resolution]`, and still `.a2ml`. That is §7 + item 1 and it is not done. diff --git a/docs/UX-standards/launcher-standard.adoc b/docs/UX-standards/launcher-standard.adoc index e8956befc..886d80c5e 100644 --- a/docs/UX-standards/launcher-standard.adoc +++ b/docs/UX-standards/launcher-standard.adoc @@ -75,7 +75,7 @@ stop_server() { [source,bash] ---- wait_for_server() { - # All three values are env-overridable per [runtime] in launcher-standard.a2ml. + # All three values are env-overridable per (runtime) in launcher-standard_praxis.deed. local max_wait="${1:-${WAIT_FOR_URL_TIMEOUT_SECONDS:-15}}" local poll_interval="${WAIT_FOR_URL_POLL_INTERVAL:-1}" local per_request_timeout=2 # curl --max-time; caps each probe so a @@ -104,7 +104,7 @@ fi === Browser Launching -Resolution order, per `[browser-launch]` in `launcher-standard.a2ml`: +Resolution order, per `(browser-launch)` in `launcher-standard_praxis.deed`: 1. `$BROWSER` env var (de-facto Unix convention; user/operator override) 2. Platform-specific ladder, dispatched via `uname -s`: @@ -178,8 +178,8 @@ err() { echo "[$APP_NAME] Try: check $LOG_FILE for details" >&2 } -# Source the shared GUI-error helper (reference impl of [error-visibility] -# from launcher-standard.a2ml). Falls back to err() if not available so +# Source the shared GUI-error helper (reference impl of (error-visibility) +# from launcher-standard_praxis.deed). Falls back to err() if not available so # every launcher works even without the shared helper on PATH. if [ -r "$(hp_resolve_desktop_tools gui-error.sh 2>/dev/null)" ]; then # shellcheck disable=SC1090 @@ -197,7 +197,7 @@ fi ---- NOTE: `hp_gui_error` writes to stderr unconditionally per -`[error-visibility].always-also-to-stderr = true`. Set `NO_GUI_ERROR=1` +`(error-visibility :always-also-to-stderr #t)`. Set `NO_GUI_ERROR=1` to suppress the dialog attempt (useful in CI). === Soft-Attach (optional ecosystem integrations) @@ -205,7 +205,7 @@ to suppress the dialog attempt (useful in CI). A "soft-attach" tool is one the launcher calls IF it is installed, and silently skips otherwise. The estate ships three by default (`feedback-o-tron`, `hypatia`, `panic-attack`) — see -`[soft-attach].tools` in `launcher-standard.a2ml` for the live list. +`(soft-attach (tool ...))` in `launcher-standard_praxis.deed` for the live list. Downstream launchers SHOULD source `launcher/soft-attach.sh` rather than re-implementing the if-installed-then-invoke pattern, so behaviour @@ -234,7 +234,7 @@ on_start_failed() { ---- Note that template substitution (`{app-name}`, `{log-file}`, -`{repo-dir}` in the a2ml) is the launcher's responsibility — interpolate +`{repo-dir}` in the deed) is the launcher's responsibility — interpolate before passing the command line to `hp_soft_attach_run`. == Standard Modes @@ -431,7 +431,7 @@ feedback. Type=Application Name=Application Name # The Exec path MUST be the absolute path resolved at install time via -# the [resolution].desktop-tools-search ladder (see §Canonical location). +# the (resolution (desktop-tools-search ...)) ladder (see §Canonical location). # The freedesktop spec does NOT expand environment variables in Exec= # lines, so `--integ` is responsible for picking the host-correct path. # The /var/mnt/eclipse/... value below is one possible resolution — on @@ -487,7 +487,7 @@ The single source of truth is: For desktop files (which need a stable absolute path) a symlink or copy is deployed inside a `.desktop-tools/` directory whose location varies by host. Consumers MUST resolve that location via the search ladder declared -in `launcher/launcher-standard.a2ml` `[resolution].desktop-tools-search`: +in `launcher/launcher-standard_praxis.deed` `(resolution (desktop-tools-search ...))`: 1. `$HP_DESKTOP_TOOLS` — explicit override 2. `$HP_ESTATE_ROOT/.desktop-tools` — estate-root convention @@ -504,7 +504,7 @@ SHOULD use the reference impl rather than re-implementing the ladder. `launch-scaffolder` copies the same script into its baked-in standards so regenerated launchers stay in sync. The legacy single hard-coded path (`/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh`) remains in the -a2ml as `deployed-symlink` for compatibility with pre-resolution +deed as `:deployed-symlink` for compatibility with pre-resolution consumers; new code MUST use the ladder. === Calling convention diff --git a/launcher/README.adoc b/launcher/README.adoc index bafbb6019..c1ac56d15 100644 --- a/launcher/README.adoc +++ b/launcher/README.adoc @@ -7,8 +7,8 @@ specification at `docs/UX-standards/launcher-standard.adoc`. == Files -`launcher-standard.a2ml`:: - The authoritative A2ML form of the launcher standard. All tooling that +`launcher-standard_praxis.deed`:: + The authoritative DEED form of the launcher standard. All tooling that provisions, audits, or validates launchers MUST consume this file rather than re-parsing the prose spec or carrying its own vendored copy. @@ -22,14 +22,16 @@ below). It consumes this standard by reference, in this resolution order: . `--standard ` — explicit CLI argument . `$LAUNCH_SCAFFOLDER_STANDARD` — environment-variable override -. The `[resolution].standard-search` ladder declared in - `launcher-standard.a2ml`, consulted in order, first existing path wins: +. The `(resolution (standard-search ...))` ladder declared in + `launcher-standard_praxis.deed`, consulted in `:priority` order — the + integers carry the order, because file position is not semantic in DEED — + first existing path wins: + -.. `$HP_ESTATE_ROOT/standards/launcher/launcher-standard.a2ml` -.. `$XDG_DATA_HOME/hyperpolymath/standards/launcher/launcher-standard.a2ml` -.. `/var/mnt/eclipse/repos/standards/launcher/launcher-standard.a2ml` -.. `$HOME/developer/repos/standards/launcher/launcher-standard.a2ml` -.. `$HOME/dev/repos/standards/launcher/launcher-standard.a2ml` +.. `$HP_ESTATE_ROOT/standards/launcher/launcher-standard_praxis.deed` +.. `$XDG_DATA_HOME/hyperpolymath/standards/launcher/launcher-standard_praxis.deed` +.. `/var/mnt/eclipse/repos/standards/launcher/launcher-standard_praxis.deed` +.. `$HOME/developer/repos/standards/launcher/launcher-standard_praxis.deed` +.. `$HOME/dev/repos/standards/launcher/launcher-standard_praxis.deed` The ladder replaces the previous single hard-coded `/var/mnt/eclipse/repos/...` literal, which broke the standard on any host diff --git a/launcher/gui-error.sh b/launcher/gui-error.sh index b4ffd8805..d64ca89f7 100755 --- a/launcher/gui-error.sh +++ b/launcher/gui-error.sh @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) # # gui-error.sh — reference implementation of [error-visibility] from -# launcher/launcher-standard.a2ml. +# launcher/launcher-standard_praxis.deed. # # When the launcher runs in a GUI context (no TTY + DISPLAY or # WAYLAND_DISPLAY set), errors written only to stderr disappear: the diff --git a/launcher/launcher-standard.a2ml b/launcher/launcher-standard.a2ml deleted file mode 100644 index adc283013..000000000 --- a/launcher/launcher-standard.a2ml +++ /dev/null @@ -1,268 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# launcher-standard.a2ml — canonical declarative description of a -# hyperpolymath-compliant desktop launcher. -# -# This file is the source of truth for what a compliant launcher looks like. -# It is baked into the launch-scaffolder binary at build time via -# include_str!(), and can be overridden at runtime via --standard or -# $LAUNCH_SCAFFOLDER_STANDARD. -# -# See also: -# - standards/0-canon/rsr/LANGUAGE-POLICY.adoc -# - standards/docs/UX-standards/launcher-standard.adoc (prose version) -# - standards/docs/UX-standards/LM-LA-LIFECYCLE-STANDARD.adoc (install/uninstall) - -[spec] -version = "0.3.0" -date = "2026-05-26" -compliance = [ - "launcher-standard.adoc", - "LM-LA-LIFECYCLE-STANDARD.adoc", - "cross-platform-system-integration-modes", - "fallback-ladder-keepopen", -] - -[resolution] -# Path-resolution ladders. Consumers (launch-scaffolder, downstream launchers, -# the integrity verifier) MUST consult these in order; the first existing path -# wins. This replaces single hard-coded /var/mnt/eclipse/... paths that broke -# the standard on hosts that don't use the eclipse-mount layout. -# -# .desktop files MUST embed the RESOLVED ABSOLUTE PATH at install time — -# the freedesktop spec does not expand environment variables in Exec= lines, -# so the launcher's `--integ` mode is responsible for performing the -# resolution and writing the literal path into the .desktop file. -# -# Reference implementation: launcher/resolve-desktop-tools.sh -desktop-tools-search = [ - "$HP_DESKTOP_TOOLS", # explicit override - "$HP_ESTATE_ROOT/.desktop-tools", # estate-root convention - "$XDG_DATA_HOME/hyperpolymath/.desktop-tools", # XDG default (XDG_DATA_HOME defaults to $HOME/.local/share) - "/var/mnt/eclipse/repos/.desktop-tools", # legacy eclipse-mount layout - "$HOME/developer/repos/.desktop-tools", # alt: $HOME/developer/repos - "$HOME/dev/repos/.desktop-tools", # alt: $HOME/dev/repos -] - -# Same ladder shape for locating the canonical launcher-standard.a2ml itself -# (consumed by launch-scaffolder when neither --standard nor -# $LAUNCH_SCAFFOLDER_STANDARD is set). -standard-search = [ - "$HP_ESTATE_ROOT/standards/launcher/launcher-standard.a2ml", - "$XDG_DATA_HOME/hyperpolymath/standards/launcher/launcher-standard.a2ml", - "/var/mnt/eclipse/repos/standards/launcher/launcher-standard.a2ml", - "$HOME/developer/repos/standards/launcher/launcher-standard.a2ml", - "$HOME/dev/repos/standards/launcher/launcher-standard.a2ml", -] - -[fallback-ladder] -# keepopen.sh wraps every primary desktop-file Exec line. It turns launcher -# failures from invisible flashes into loud, labelled banners, and lands the -# user in an interactive shell at the repo root if everything fails. -# -# See launcher-standard.adoc §Fallback Ladder for the prose version. -wrapper = "keepopen.sh" -canonical-path = "developer-ecosystem/standards/launcher/keepopen.sh" -deployed-name = "keepopen.sh" # resolved via [resolution].desktop-tools-search -deployed-symlink = "/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh" # DEPRECATED: legacy literal, kept for pre-resolution consumers. New code MUST use [resolution]. -calling-convention = "keepopen.sh APP_NAME REPO_DIR \"GUI_CMD\" \"TUI_CMD\" [LOG_FILE]" -stages = [ - { name = "gui", colour = "yellow", on-failure = "show-banner-then-try-tui" }, - { name = "tui", colour = "red", on-failure = "show-banner-then-drop-to-shell" }, - { name = "shell", colour = "green", behaviour = "exec-bash-login-at-repo-dir" }, -] -banner-visibility = "loud" # Intentionally ugly — visibility beats aesthetics. -final-shell = "bash -l at REPO_DIR (never 'press enter to close')" - -[required-modes] -# These are the modes every compliant launcher MUST implement. -# Aliases (see [aliases] below) are not listed here — they are accepted -# inputs, not independent obligations. -runtime = ["--start", "--stop", "--status", "--auto"] -integration = ["--integ", "--disinteg"] -meta = ["--help", "--version"] - -[version-output] -# Required format for the `--version` mode. The first line MUST be -# machine-greppable so field bug reports can quote it verbatim. Optional -# additional lines (build SHA, build date, libc / runtime versions) MAY -# follow on subsequent lines. -# -# Rationale: a launcher with no --version produces unactionable bug -# reports — "it's broken on my machine" with no way to tell which build. -first-line-format = "{app-name} {version} ({build-sha-short}) [{platform}]" -example = "aerie-launcher 0.4.2 (a1b2c3d) [linux-x86_64]" -exit-code = 0 -machine-greppable = true - -[aliases] -# Documented mode aliases. Launchers MUST accept these and route them to -# the canonical mode. Prose: launcher-standard.adoc §Required Modes. -"--browser" = "--auto" -"--web" = "--auto" - -[optional-modes] -# Modes launchers MAY implement for extra functionality. -developer = ["--debug", "--logs", "--tail"] - -[default-mode] -# Mode selected when the launcher is invoked with no arguments. -mode = "--auto" - -[runtime] -# Required runtime behaviour. -background = "nohup" -# PID files live in the user's runtime-state dir — wiped on logout, -# user-scoped (mode 0700 per XDG spec), no symlink-attack target. -# Bash-expansion ladder: -# 1. $XDG_RUNTIME_DIR — Linux per freedesktop XDG Base Directory spec -# 2. $TMPDIR — macOS / BSDs (typically /var/folders/.../T, per-user) -# 3. /tmp — last resort (predictable, world-writable; flagged -# in §Best Practices > Security) -pid-file-pattern = "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/{app-name}-server.pid" -# Logs go to XDG_STATE_HOME (per spec, defaults to $HOME/.local/state). -# Per-user, survives reboot, not world-writable. The {app-name} subdir -# isolates each launcher's logs. -log-file-pattern = "${XDG_STATE_HOME:-$HOME/.local/state}/{app-name}/server.log" -# URL-readiness polling after start. The launcher polls the app's URL -# until either it responds or total-timeout-seconds elapses. -# All three timing values are env-var overridable so operators can tune -# without re-minting the launcher. -wait-for-url-timeout-seconds = 15 -wait-for-url-timeout-env-var = "WAIT_FOR_URL_TIMEOUT_SECONDS" -wait-for-url-poll-interval-seconds = 1 -wait-for-url-poll-interval-env-var = "WAIT_FOR_URL_POLL_INTERVAL" -# Per-request timeout: cap each individual curl probe so a hung server -# does not eat the entire poll budget on a single request. -wait-for-url-per-request-timeout-seconds = 2 -startup-command-search = [ - "{repo-dir}/scripts/run.sh", - "{repo-dir}/dev.sh", -] - -[browser-launch] -# How open_browser() resolves which command to invoke. Implementations -# MUST honour env-var-override first (de-facto $BROWSER convention), -# then fall back to the platform-specific ladder. Platform detected via -# `uname -s`; on Linux, additionally probe /proc/version for "microsoft" -# to detect WSL — under WSL, wslview is preferred so the URL opens in -# the Windows-side default browser (Linux-side browsers either land in -# WSLg or fail). -env-var-override = "BROWSER" -linux = ["wslview", "xdg-open", "firefox", "chromium"] -linux-wsl-detect = "/proc/version contains 'microsoft' (case-insensitive)" -macos = ["open"] -windows = ["start"] # cmd builtin; works under Git Bash / MSYS / Cygwin -fallback = "log 'Open manually: '" - -[error-visibility] -# When the launcher runs in a GUI context (no tty + DISPLAY/WAYLAND_DISPLAY set), -# errors must be visible to the user via a GUI dialog, not only to stderr. -# -# Reference implementation: launcher/gui-error.sh — exposes -# `hp_gui_error "title" "message"`. Downstream launchers SHOULD source -# this rather than re-implement the dialog ladder, so the spec stays -# consistent across the estate. -# -# Env override: NO_GUI_ERROR=1 suppresses the dialog attempt (stderr -# still gets the message). Useful in CI / scripted invocation. -reference-impl = "launcher/gui-error.sh" -gui-dialog-chain = ["kdialog", "zenity", "notify-send", "xmessage"] -always-also-to-stderr = true -suppress-env-var = "NO_GUI_ERROR" - -[integration.linux] -apps-dir = "$HOME/.local/share/applications" -icon-dir = "$HOME/.local/share/icons/hicolor/256x256/apps" -desktop-shortcut-dir = "$HOME/Desktop" -bin-dir = "$HOME/.local/bin" -desktop-file-permissions = 444 -# If no custom icon is available, fall back to this freedesktop named icon. -icon-fallback = "package-x-generic" -refresh-command = "update-desktop-database" -trust-command = "gio set {file} metadata::trusted true" - -[integration.macos] -apps-dir = "$HOME/Applications" -desktop-shortcut-dir = "$HOME/Desktop" -bin-dir = "$HOME/.local/bin" -bundle-pattern = "{app-display}.app" -shortcut-pattern = "{app-display}.command" - -[integration.windows] -start-menu-dir = "$APPDATA/Microsoft/Windows/Start Menu/Programs" -desktop-shortcut-dir = "$HOME/Desktop" -bin-dir = "$HOME/.local/bin" -shortcut-pattern = "{app-display}.lnk" -# Fallback when PowerShell is not reachable under Git Bash / WSL. -bat-fallback = true - -[integrity] -# Per LM-LA-LIFECYCLE §LM/LA-INSTALL: after --integ completes, generate -# integrity hashes via an external tool. The tool is located via -# [resolution].desktop-tools-search + the tool-name below — new consumers -# MUST use that ladder; fallback-paths is kept only for pre-resolution -# consumers and is itself a legacy literal. -verification-tool = "verify-desktop-integrity.sh" -tool-name = "verify-desktop-integrity.sh" # resolved via [resolution].desktop-tools-search -fallback-paths = [ - "/var/mnt/eclipse/repos/.desktop-tools/verify-desktop-integrity.sh", # DEPRECATED -] -fatal-on-failure = false # missing verifier is a log line, not an error - -[disinteg] -# What --disinteg removes and what it deliberately leaves alone. -remove = [ - "desktop-file", - "desktop-shortcut", - "icon", - "launcher-binary", - "start-menu-entry", - "bat-fallback-shortcuts", - "pid-file", -] -preserve = [ - "$HOME/.config/{app-name}/", - "${XDG_STATE_HOME:-$HOME/.local/state}/{app-name}/", - "{repo-dir}", -] - -[soft-attach] -# Optional ecosystem integrations — called if present, silently skipped -# if absent. Each tool entry carries an explicit `trigger` naming the -# hook point at which the launcher should invoke it. -# -# Reference implementation: launcher/soft-attach.sh — exposes three -# primitives: -# hp_soft_attach_present "command" → 0 if on PATH -# hp_soft_attach_run "command line" → run if first token present -# hp_soft_attach_event "tool" "event" [...] → invoke `tool emit event ...` -# Downstream launchers SHOULD source this rather than re-implement. -# -# Trigger values: "on-start-failed", "on-start-succeeded", "on-integ-failed", -# "on-user-request". Additional triggers may be added as the lifecycle grows. -reference-impl = "launcher/soft-attach.sh" -tools = [ - { name = "feedback-o-tron", style = "event", trigger = "on-start-failed", event = "launcher:start_failed" }, - { name = "hypatia", style = "command", trigger = "on-start-failed", command = "hypatia diagnose --app {app-name} --log {log-file}" }, - { name = "panic-attack", style = "command", trigger = "on-start-failed", command = "panic-attack assail {repo-dir}" }, -] - -[a2ml-metadata-block] -# Every generated launcher must carry this metadata block in its header so it -# can be re-parsed by `launch-scaffolder config` and `realign`. -required-fields = [ - "id", - "type", - "version", - "app-name", - "app-display", - "app-url", - "standards-compliance", - "modes", - "platforms", - "lifecycle-phases-covered", - "lifecycle-phases-deferred", -] - diff --git a/launcher/launcher-standard_praxis.deed b/launcher/launcher-standard_praxis.deed new file mode 100644 index 000000000..c751c4ec4 --- /dev/null +++ b/launcher/launcher-standard_praxis.deed @@ -0,0 +1,262 @@ +;; SPDX-FileCopyrightText: © 2026 Jonathan D.A. Jewell (hyperpolymath) +;; SPDX-License-Identifier: MPL-2.0 +;; +;; launcher-standard_praxis.deed — canonical declarative description of a +;; hyperpolymath-compliant desktop launcher. +;; +;; Translated from launcher/launcher-standard.a2ml under the authority of +;; 1-formats/deed/mappings/launcher-standard-to-praxis-deed.adoc (standards#837, +;; owner ruling D73-C). The praxis head is correct because this document states +;; what a tool DOES: it is almost entirely first-match-wins ladders, and DEED +;; carries precedence in an explicit :priority integer, never in file position. +;; +;; Prose counterparts: +;; docs/UX-standards/launcher-standard.adoc +;; docs/UX-standards/LM-LA-LIFECYCLE-STANDARD.adoc +;; 0-canon/rsr/LANGUAGE-POLICY.adoc +(praxis-deed + :schema-version "1.0.0" + :canonical-name "launcher-standard" + :beholding-chora #u5"estate/chora" + + ;; The standard's OWN version, distinct from :schema-version (the DEED + ;; format). Bumped 0.3.0 -> 0.4.0 because the resolution ladders below now + ;; name the .deed file, which is consumer-visible. + :standard-version "0.4.0" + :standard-date "2026-09-22" + :compliance ("launcher-standard.adoc" + "LM-LA-LIFECYCLE-STANDARD.adoc" + "cross-platform-system-integration-modes" + "fallback-ladder-keepopen") + + ;; ---------------------------------------------------------------- resolution + ;; Path-resolution ladders. Consumers (launch-scaffolder, downstream + ;; launchers, the integrity verifier) MUST consult each ladder in ASCENDING + ;; :priority order; the first existing path wins. This replaces single + ;; hard-coded /var/mnt/eclipse/... paths that broke the standard on hosts + ;; that do not use the eclipse-mount layout. + ;; + ;; .desktop files MUST embed the RESOLVED ABSOLUTE PATH at install time -- + ;; the freedesktop spec does not expand environment variables in Exec= + ;; lines, so the launcher's --integ mode performs the resolution and writes + ;; the literal path into the .desktop file. + ;; + ;; Reference implementation: launcher/resolve-desktop-tools.sh + (resolution + (desktop-tools-search + (path :priority 10 :value "$HP_DESKTOP_TOOLS" + :note "explicit override") + (path :priority 20 :value "$HP_ESTATE_ROOT/.desktop-tools" + :note "estate-root convention") + (path :priority 30 :value "$XDG_DATA_HOME/hyperpolymath/.desktop-tools" + :note "XDG default; XDG_DATA_HOME defaults to $HOME/.local/share") + (path :priority 40 :value "/var/mnt/eclipse/repos/.desktop-tools" + :note "legacy eclipse-mount layout") + (path :priority 50 :value "$HOME/developer/repos/.desktop-tools" + :note "alt: $HOME/developer/repos") + (path :priority 60 :value "$HOME/dev/repos/.desktop-tools" + :note "alt: $HOME/dev/repos")) + ;; Same ladder shape for locating this file itself, consumed by + ;; launch-scaffolder when neither --standard nor + ;; $LAUNCH_SCAFFOLDER_STANDARD is set. + (standard-search + (path :priority 10 :value "$HP_ESTATE_ROOT/standards/launcher/launcher-standard_praxis.deed") + (path :priority 20 :value "$XDG_DATA_HOME/hyperpolymath/standards/launcher/launcher-standard_praxis.deed") + (path :priority 30 :value "/var/mnt/eclipse/repos/standards/launcher/launcher-standard_praxis.deed") + (path :priority 40 :value "$HOME/developer/repos/standards/launcher/launcher-standard_praxis.deed") + (path :priority 50 :value "$HOME/dev/repos/standards/launcher/launcher-standard_praxis.deed"))) + + ;; ----------------------------------------------------------- fallback-ladder + ;; keepopen.sh wraps every primary desktop-file Exec line. It turns launcher + ;; failures from invisible flashes into loud, labelled banners, and lands the + ;; user in an interactive shell at the repo root if everything fails. + (fallback-ladder + :wrapper "keepopen.sh" + :canonical-path "developer-ecosystem/standards/launcher/keepopen.sh" + :deployed-name "keepopen.sh" + ;; DEPRECATED legacy literal, kept for pre-resolution consumers. + ;; New code MUST use the (resolution) ladders above. + :deployed-symlink "/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh" + :calling-convention "keepopen.sh APP_NAME REPO_DIR \"GUI_CMD\" \"TUI_CMD\" [LOG_FILE]" + ;; Intentionally ugly -- visibility beats aesthetics. + :banner-visibility loud + :final-shell "bash -l at REPO_DIR (never 'press enter to close')" + (stage :priority 10 :name gui :colour yellow + :on-failure show-banner-then-try-tui) + (stage :priority 20 :name tui :colour red + :on-failure show-banner-then-drop-to-shell) + (stage :priority 30 :name shell :colour green + :behaviour exec-bash-login-at-repo-dir)) + + ;; ------------------------------------------------------------ required-modes + ;; Modes every compliant launcher MUST implement. Aliases are accepted + ;; inputs, not independent obligations, and are listed separately below. + ;; These are sets, not ladders: no :priority, order carries no meaning. + (required-modes + :runtime ("--start" "--stop" "--status" "--auto") + :integration ("--integ" "--disinteg") + :meta ("--help" "--version")) + + (optional-modes :developer ("--debug" "--logs" "--tail")) + (default-mode :mode "--auto") + + ;; Documented mode aliases. Launchers MUST accept these and route them to + ;; the canonical mode. A DEED symbol cannot begin with "-", so both halves + ;; stay strings. + (aliases + (alias :from "--browser" :to "--auto") + (alias :from "--web" :to "--auto")) + + ;; ------------------------------------------------------------ version-output + ;; Required format for --version. The first line MUST be machine-greppable + ;; so field bug reports can quote it verbatim; further lines MAY follow. + ;; Rationale: a launcher with no --version produces unactionable bug + ;; reports -- "it's broken on my machine" with no way to tell which build. + (version-output + :first-line-format "{app-name} {version} ({build-sha-short}) [{platform}]" + :example "aerie-launcher 0.4.2 (a1b2c3d) [linux-x86_64]" + :exit-code 0 + :machine-greppable #t) + + ;; ------------------------------------------------------------------- runtime + (runtime + :background nohup + ;; PID files live in the user's runtime-state dir -- wiped on logout, + ;; user-scoped (mode 0700 per XDG), no symlink-attack target. + :pid-file-pattern "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/{app-name}-server.pid" + ;; Logs go to XDG_STATE_HOME. Per-user, survives reboot, not + ;; world-writable; the {app-name} subdir isolates each launcher's logs. + :log-file-pattern "${XDG_STATE_HOME:-$HOME/.local/state}/{app-name}/server.log" + ;; URL-readiness polling after start. All three timing values are env-var + ;; overridable so operators can tune without re-minting the launcher. + :wait-for-url-timeout-seconds 15 + :wait-for-url-timeout-env-var "WAIT_FOR_URL_TIMEOUT_SECONDS" + :wait-for-url-poll-interval-seconds 1 + :wait-for-url-poll-interval-env-var "WAIT_FOR_URL_POLL_INTERVAL" + ;; Per-request timeout: cap each individual probe so a hung server does + ;; not eat the entire poll budget on a single request. + :wait-for-url-per-request-timeout-seconds 2 + (startup-command-search + (path :priority 10 :value "{repo-dir}/scripts/run.sh") + (path :priority 20 :value "{repo-dir}/dev.sh"))) + + ;; ------------------------------------------------------------ browser-launch + ;; How open_browser() resolves which command to invoke. Implementations + ;; MUST honour the env-var override first (de-facto $BROWSER convention), + ;; then fall back to the platform ladder. Platform is detected via + ;; `uname -s`; on Linux, additionally probe /proc/version for "microsoft" + ;; to detect WSL -- under WSL wslview is preferred so the URL opens in the + ;; Windows-side default browser (Linux-side browsers either land in WSLg + ;; or fail). + (browser-launch + :env-var-override "BROWSER" + :linux-wsl-detect "/proc/version contains 'microsoft' (case-insensitive)" + :fallback "log 'Open manually: '" + (linux + (command :priority 10 :value "wslview") + (command :priority 20 :value "xdg-open") + (command :priority 30 :value "firefox") + (command :priority 40 :value "chromium")) + (macos + (command :priority 10 :value "open")) + (windows + ;; cmd builtin; works under Git Bash / MSYS / Cygwin + (command :priority 10 :value "start"))) + + ;; ----------------------------------------------------------- error-visibility + ;; In a GUI context (no tty + DISPLAY/WAYLAND_DISPLAY set) errors must reach + ;; the user via a GUI dialog, not only stderr. Downstream launchers SHOULD + ;; source the reference implementation rather than re-implement the ladder, + ;; so the spec stays consistent across the estate. + (error-visibility + :reference-impl "launcher/gui-error.sh" + :always-also-to-stderr #t + ;; NO_GUI_ERROR=1 suppresses the dialog attempt; stderr still gets the + ;; message. Useful in CI / scripted invocation. + :suppress-env-var "NO_GUI_ERROR" + (gui-dialog-chain + (command :priority 10 :value "kdialog") + (command :priority 20 :value "zenity") + (command :priority 30 :value "notify-send") + (command :priority 40 :value "xmessage"))) + + ;; --------------------------------------------------------------- integration + (integration + (linux + :apps-dir "$HOME/.local/share/applications" + :icon-dir "$HOME/.local/share/icons/hicolor/256x256/apps" + :desktop-shortcut-dir "$HOME/Desktop" + :bin-dir "$HOME/.local/bin" + :desktop-file-permissions 444 + ;; Freedesktop named icon used when no custom icon is available. + :icon-fallback "package-x-generic" + :refresh-command "update-desktop-database" + :trust-command "gio set {file} metadata::trusted true") + (macos + :apps-dir "$HOME/Applications" + :desktop-shortcut-dir "$HOME/Desktop" + :bin-dir "$HOME/.local/bin" + :bundle-pattern "{app-display}.app" + :shortcut-pattern "{app-display}.command") + (windows + :start-menu-dir "$APPDATA/Microsoft/Windows/Start Menu/Programs" + :desktop-shortcut-dir "$HOME/Desktop" + :bin-dir "$HOME/.local/bin" + :shortcut-pattern "{app-display}.lnk" + ;; Fallback when PowerShell is not reachable under Git Bash / WSL. + :bat-fallback #t)) + + ;; ----------------------------------------------------------------- integrity + ;; Per LM-LA-LIFECYCLE §LM/LA-INSTALL: after --integ completes, generate + ;; integrity hashes via an external tool. The tool is located via + ;; (resolution desktop-tools-search) plus :tool-name -- new consumers MUST + ;; use that ladder. (fallback-paths) is kept only for pre-resolution + ;; consumers and is itself a legacy literal. + (integrity + :verification-tool "verify-desktop-integrity.sh" + :tool-name "verify-desktop-integrity.sh" + ;; A missing verifier is a log line, not an error. + :fatal-on-failure #f + (fallback-paths + (path :priority 10 :deprecated #t + :value "/var/mnt/eclipse/repos/.desktop-tools/verify-desktop-integrity.sh"))) + + ;; ------------------------------------------------------------------ disinteg + ;; What --disinteg removes, and what it deliberately leaves alone. + ;; Both are sets: no ordering, so no :priority. + (disinteg + :remove (desktop-file desktop-shortcut icon launcher-binary + start-menu-entry bat-fallback-shortcuts pid-file) + :preserve ("$HOME/.config/{app-name}/" + "${XDG_STATE_HOME:-$HOME/.local/state}/{app-name}/" + "{repo-dir}")) + + ;; --------------------------------------------------------------- soft-attach + ;; Optional ecosystem integrations -- called if present, silently skipped if + ;; absent. Each tool carries an explicit :trigger naming the hook point at + ;; which the launcher should invoke it. Downstream launchers SHOULD source + ;; the reference implementation rather than re-implement its three + ;; primitives (hp_soft_attach_present / _run / _event). + ;; + ;; Triggers are a closed set today: on-start-failed, on-start-succeeded, + ;; on-integ-failed, on-user-request. More may be added as the lifecycle + ;; grows. These are independent hooks, not a ladder -- every matching tool + ;; fires, so there is no precedence to carry and no :priority. + (soft-attach + :reference-impl "launcher/soft-attach.sh" + (tool :name feedback-o-tron :style event :trigger on-start-failed + :event "launcher:start_failed") + (tool :name hypatia :style command :trigger on-start-failed + :command "hypatia diagnose --app {app-name} --log {log-file}") + (tool :name panic-attack :style command :trigger on-start-failed + :command "panic-attack assail {repo-dir}")) + + ;; ------------------------------------------------------------ metadata-block + ;; Every generated launcher must carry this metadata block in its header so + ;; it can be re-parsed by `launch-scaffolder config` and `realign`. + ;; Renamed from [a2ml-metadata-block]: A2ML is a dead format name and the + ;; deed era does not carry it forward. A set, not a ladder. + (metadata-block + :required-fields ("id" "type" "version" "app-name" "app-display" "app-url" + "standards-compliance" "modes" "platforms" + "lifecycle-phases-covered" "lifecycle-phases-deferred"))) diff --git a/launcher/resolve-desktop-tools.sh b/launcher/resolve-desktop-tools.sh index fde125763..953435895 100755 --- a/launcher/resolve-desktop-tools.sh +++ b/launcher/resolve-desktop-tools.sh @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) # # resolve-desktop-tools.sh — reference implementation of the path-resolution -# ladders declared in launcher/launcher-standard.a2ml §[resolution]. +# ladders declared in launcher/launcher-standard_praxis.deed (resolution). # # Downstream launchers SHOULD `source` this script and call # `hp_resolve_desktop_tools` / `hp_resolve_standard` rather than rolling @@ -16,8 +16,9 @@ # whether that is fatal (e.g. missing keepopen.sh wrapper) or recoverable # (e.g. missing optional verify-desktop-integrity.sh). # -# The ladders mirror [resolution].desktop-tools-search and -# [resolution].standard-search in the a2ml. They MUST stay in sync — see the +# The ladders mirror (resolution (desktop-tools-search …)) and +# (resolution (standard-search …)) in the deed, whose :priority integers +# carry the order — not file position. They MUST stay in sync — see the # CI gate referenced in launcher/README.adoc §Sync requirement. # --------------------------------------------------------------------------- @@ -52,18 +53,18 @@ hp_resolve_desktop_tools() { # --------------------------------------------------------------------------- # hp_resolve_standard # -# Echoes the first existing launcher-standard.a2ml found via the -# [resolution].standard-search ladder. Used by launch-scaffolder and any +# Echoes the first existing launcher-standard_praxis.deed found via the +# (resolution (standard-search …)) ladder. Used by launch-scaffolder and any # other consumer that needs the canonical contract file. # --------------------------------------------------------------------------- hp_resolve_standard() { local -a candidates=( "${LAUNCH_SCAFFOLDER_STANDARD:-}" - "${HP_ESTATE_ROOT:+${HP_ESTATE_ROOT}/standards/launcher/launcher-standard.a2ml}" - "${XDG_DATA_HOME:-${HOME}/.local/share}/hyperpolymath/standards/launcher/launcher-standard.a2ml" - "/var/mnt/eclipse/repos/standards/launcher/launcher-standard.a2ml" - "${HOME}/developer/repos/standards/launcher/launcher-standard.a2ml" - "${HOME}/dev/repos/standards/launcher/launcher-standard.a2ml" + "${HP_ESTATE_ROOT:+${HP_ESTATE_ROOT}/standards/launcher/launcher-standard_praxis.deed}" + "${XDG_DATA_HOME:-${HOME}/.local/share}/hyperpolymath/standards/launcher/launcher-standard_praxis.deed" + "/var/mnt/eclipse/repos/standards/launcher/launcher-standard_praxis.deed" + "${HOME}/developer/repos/standards/launcher/launcher-standard_praxis.deed" + "${HOME}/dev/repos/standards/launcher/launcher-standard_praxis.deed" ) local candidate diff --git a/launcher/soft-attach.sh b/launcher/soft-attach.sh index 165884947..949a253b0 100755 --- a/launcher/soft-attach.sh +++ b/launcher/soft-attach.sh @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) # # soft-attach.sh — reference implementation of [soft-attach] from -# launcher/launcher-standard.a2ml. +# launcher/launcher-standard_praxis.deed. # # Soft-attach = optional ecosystem integrations that the launcher invokes # IF they are installed, and silently skips otherwise. Downstream