Imported from maelys-dev/maelys-datalog (
AGENTS.md). Install upstream withnpx skills add maelys-dev/maelys-datalog. Copyright stays with the author.
Maelys release socle (maelys-release)
This repository publishes through the shared maelys-release workflows. The
rules below hold for every release-related change; the complete conventions
are in docs/conventions.md of maelys-release. The first rules hold for
every repository on the socle; those after "If this repository" apply only
when it has what they name.
.github/workflows/release.ymland the twoscripts/checkout-dependenc*.share generated bybin/maelys-release adoptof maelys-release fromdependencies/*.pin,dependencies/packagesandpackaging/homebrew/*.rb.in. Never edit them by hand; change the declarations, then runmaelys-release adopt DIR --applyfrom a maelys-release checkout at the wanted tag.maelys-release check DIR(exit 2 on any violation) verifies;maelys-release preflight DIRchecks the tag preconditions before a release.check,preflightandrehearserun as the socle this repository pins;cut,adopt,protectanddependenciesrun as the checkout at hand, andcutsays which. The command follows agent-cli/v2:--format jsoneverywhere,describefor the catalog, and--field NAMEto read one member of the result without ajqexpression.- A release is a signed, annotated tag
vX.Y.Zonmainwhose commit carriesVERSION=X.Y.Zand a datedCHANGELOG.mdentry. Never push a tag beforemake checkpasses on that exact commit, never move or force a tag, never publish from a branch.maelys-release cut DIR X.Y.Z --applydoes exactly that in two stops: it writesVERSION, commits it signed onrelease/vX.Y.Z, opens the pull request and waits for its checks; after the merge,cut DIR X.Y.Z --tag --applysigns the tag on the merge commit those checks ran on. It never merges its own pull request. An adoption does not have to be published: it travels in the next release. - The checks
mainrequires are derived, never typed:maelys-release protect DIRcomputes them and--applywrites them, as a ruleset when a ruleset protects the branch. When this repository still requires the legs under their names before 0.54.0 (check (ubuntu-26.04),check (ubuntu-26.04-arm),check (macos-15)): adopt, merge, thenprotect DIR --apply. The socle keeps reporting those names as aliases, so the adoption loses nothing, andprotect --applyreplaces each alias by its leg in one write: the branch never requires less. It refuses before the adoption is merged. Never narrow a protection to get an adoption through;--without-legsexists for a rename that comes without aliases. adopt DIRwithout--applyprints the plan and, for every socle version between this repository's pin and the checkout, the line that says what that version asks of a product. Read those lines, not the changelog.- A branch is named after the change it carries, with the prefix that
change would take in a commit message (
fix/,docs/,release/…), never after the tool that created it: a name says what changes, not who typed. No list is closed; the commit prefixes this repository already uses are its vocabulary. - A tag whose release or formula failed for a reason outside the code — a
cancelled job, an expired approval, a tap push lost to a race — is replayed
with
gh workflow run release.yml --ref vX.Y.Z -f tag=vX.Y.Z.--refnames the tag: thereleaseenvironment only accepts tagsv*, so a run started from the default branch is refused atpublish. A replay runs the socle that tag pinned; when the socle is at fault, the remedy is a new patch release carrying the corrected pin. A tag is never moved or recreated. - Never commit a secret or a key.
docs/carries what a machine writes and what this repository engages publicly, and no other prose. Where the prose lives is not named from this repository's files, which a public reader may see: do not name a documentation repository here. A private repository that wants the name declares[docs] namedinmaelys-release.conf.- The workflow verifies the tag through the GitHub API, builds on Linux
x86_64, Linux arm64 and macOS arm64 with
scripts/package-release.sh TARGET— or on the targets[targets]names, packaging only on those[package]names when it names any — attests provenance, publishes the GitHub release, renders packaging/homebrew/.rb.in from the tag's own copy, builds bottles when configured and pushes the formula tomaelys-dev/homebrew-tap. - If this repository pins other Maelys repositories: a dependency is
dependencies/<name>.pin(tag on line 1, the commit that tag names on line 2). A repository that declares[dependencies] apartreads$MAELYS_DEPENDENCIES_DIRinstead of a sibling, and the socle it pins from$MAELYS_RELEASE_DIR; every job that builds runssh scripts/checkout-dependencies.sh "$RUNNER_TEMP/dependencies" >>"$GITHUB_ENV"to get both — a variable reaches no other job, each running on another machine. The packages the build needs on the runners are listed independencies/packagesunder[linux]and[macos]; no script installs them..github/workflows/ci.ymlcalls the socle'scheck-product.yml, which reads these declarations itself.maelys-release rehearse DIR TARGETreplays the Linux build job in Docker before a first tag. - If this repository publishes Homebrew formulas: a command is named after
its binary (
maelys-egress), a library after its archive with alibprefix (libmaelys-sys). Dependency pins in a formula are copied from the tag'sdependencies/files, never typed. Tap credentials are the repository secretsHOMEBREW_TAP_TOKENandHOMEBREW_TAP_SIGNING_KEY; without them the tap job renders, lints and reports instead of failing. - If this repository declares runners:
[runners]namesmacos,linux-x86_64andlinux-arm64. A declared runner is honoured wherever only a writer can start the workflow — the release, its channels, the tap — and on the pull-request checks of a private repository only, since on a public one anyone can open a pull request.linux-x86_64is also where the release's write token runs.
Feature work isolation
- Start each new feature on a change-named branch in its own worktree. A branch names the history; a worktree supplies an independent checkout of that branch.
- Inspect existing worktrees first. Do not reuse another task's dirty checkout,
modify
maindirectly, or combine unrelated features on one branch. State the base explicitly when a feature depends on an unmerged change.
Memory-allocation contract
- Prefer caller-owned, aligned storage and explicit bounded capacities for new runtime APIs. Opaque handles do not justify mandatory heap allocation.
- Do not add per-fact, per-term, or grow-on-demand allocations to input paths. Validate a whole batch before publishing facts or text; internal preflight bookkeeping must roll back byte-for-byte on rejection. Capacity exhaustion must fail without partial insertion or heap fallback.
- An allocating convenience constructor must state its allocation count and capacity policy separately from the caller-owned path. Python/CFFI allocations must never be represented as a zero-allocation binding.
- Test the allocation contract, not just functional results: allocator counters, disabled-allocator execution, capacity boundaries, atomic failure and reuse.
- Reference sessions reserve public/native results and provenance at creation. Preserve zero engine allocator calls in append/solve/query/result-release; keep the one-live-result lease and forbid fallback allocation. Explanations prepared in caller-owned storage must not allocate, including Why-false search scratch. Only legacy direct-text convenience calls may allocate a workspace. Keep backend ABI 3 storage/prepare/write callbacks coherent and compare new workspace output with the legacy structured oracle, including truncation. A prepared explanation leases its result; never release/reuse that result until every explanation is released. Custom callbacks/backends do not automatically inherit the reference implementation's guarantee.
- Session explanation workspaces are explicit opt-ins; never reserve one by default or fall back to allocation for an unreserved kind. Keep result-cache keys value-based and generation-scoped. Internal cached explanations are released with the result; externally prepared handles still block release. Test measure-only close, short-output retry, invalidation, borrowed-range exclusivity and allocator failures. Python's configured path reuses storage but its conversions, text buffers and strings still allocate. Backend ABI 3 and existing fingerprints are unchanged; new error statuses require callback validation, CFFI, docs and exhaustive-switch migration notes together.
- Intern repeated input strings rather than reserving worst-case text for every occurrence. Defaults derive from native symbol/registry budgets, not arbitrary MiB multipliers. Preserve canonical result IDs regardless of insertion order.
- Do not use libc qsort on the hot path: it may allocate. Use the bounded in-place sort and maintain the all-engine allocation-guard test.
- Never claim the whole engine is zero-malloc based on an input-buffer test. Session/results, compilation, backend and explanation allocations must be audited separately. Keep the public header and docs/validation.md accurate.
- Hot-path changes require same-compiler/profile A/A noise floors and alternating A/B passes without concurrent builds. Allocation tests do not establish speed. Include sorted/reverse/duplicate/adversarial inputs and canonical IDs. Test collisions, rollback and memory budgets before optimizing.
- Treat release-time writes as a hot-path cost, not just allocations. Clang on Linux can eliminate a memset immediately before free as a dead store; adding a reusable branch can make the same bulk write live on both paths and cause a regression. Free owned results before any reset. Reset only reusable metadata, initialize payload validity before reuse, and test release reset bytes. Never claim secure erasure from ordinary memset; an explicit erasure contract would require a non-elidable primitive and its own measurements.
Manual benchmark evidence
- Keep
bench-compare.ymlworkflow_dispatch-only, with base/head inputs and one sequential GitHub-hosted Ubuntu job. Never add it to PR events or required checks. Do not reuse a release job or token to run a benchmark. - Compile both revisions/profiles once before timing; run two A/A pairs before A B A B. Keep both solver and input probes in the same run, with no concurrent builds, priority/affinity tuning or undisclosed case selection.
- Below 10 microseconds use minima; otherwise retain median and p95 with their own A/A floors. Below-floor differences are indeterminate, not zero or wins. An A/A floor describes repeatability of one binary; it does not bound systematic placement effects between different binaries. Above-floor timing differences are observations, not by themselves algorithmic attribution. Never select an index threshold from inconclusive timings.
- When attribution matters, count executed instructions in the timed region (excluding preparation, clocks and checks) and compare per-function counts, not only totals. Callgrind Ir is a software instruction count, not a hardware retired-instruction counter. Equal counts do not establish equal cycle, cache, branch-prediction or memory costs.
- Distinguish code placement from data layout. Unreachable text padding perturbs instruction addresses; it does not control struct member offsets, array stride or object alignment. For a data-layout hypothesis, preserve sizeof/alignof/offsetof evidence for each revision and, when claiming cache-line alignment, observe the actual base/member addresses in the measured fixture. An offset divisible by a cache-line size does not establish absolute alignment. Compare the baseline, original and revised layout on the same run with checked outputs and instruction evidence when attribution matters. Moving a field may also change code generation, enclosing-object layout and allocation size; restoration of old member offsets alone does not isolate a cache mechanism. A vanished or below-floor gap supports only that case/run, not zero overhead for every program. Keep timing classifications and attribution separate.
- Control code placement by linking the same compiled objects with predeclared amounts of unreachable text padding; verify symbol displacement and preserve binary/harness hashes, disassembly, checked outputs and every variant. Finish all builds before timing. Run two A/A pairs per unperturbed revision before two interleaved rounds of every layout; measure time outside instrumentation and repeat scoped instruction counts in separate processes. A separate diagnostic driver also changes layout: interpret its timings separately from the complete benchmark, which it does not replace.
- The #77 diagnostic measured 9.94% and 22.77% median shifts on one LARGE/2048 solver fixture under 16 bytes of neutral padding with identical instruction counts. These are observations for those binaries, not a universal 10% tolerance or upper bound for other cases. An amplitude band based on a named control may prioritize further diagnostics; it must not erase smaller signals, change their A/A classifications or establish the cause of larger ones. Preserve both views.
- Separate placement sensitivity from unresolved runner variance before requesting dedicated hardware. A dedicated machine can improve repeatability but does not remove systematic binary-layout effects; it is not a prerequisite for scoped instruction counts or neutral-placement controls. If attribution remains unresolved, state that limit rather than discarding passes or changing the interpretation of the statistic.
- Upload raw CSV and the comparison report as run artifacts only. Never commit generated results or add automatic PR comments. Distinguish synthetic tooling tests/local Docker smoke from actual hosted-runner performance measurements.