# Package author conventions
This document codifies the conventions algocline expects from package
authors. It is the single source of truth for the docstring convention,
the `M.meta` / `M.spec` / `M.docs` shape contract, and the lint rules
enforced by `alc_hub_gendoc`.
---
## 0. SSoT / Projection — Source vs generated artifacts
algocline pkg conventions enforce a strict separation between
**Source** (hand-written) and **Projection** (machine-generated):
- **Source (SSoT)**: `init.lua` — specifically `M.meta`, `M.spec`,
and the leading `---` docstring block. This is the only file an
author edits.
- **Projection**: every other artifact about the pkg, regenerated
by `alc_hub_dist` from the Source.
Recognised projections (current set; the authoritative list lives in
`crates/algocline-app/src/service/lua/gendoc/docs/projections.lua`):
| `docs/narrative/{name}.md` | `narrative` | Rendered docstring + Parameters table |
| `llms.txt` / `llms-full.txt` | `llms` | LLM-readable pkg catalog |
| `context7.json` | `context7` | Context7 MCP integration |
| `.devin/wiki.json` | `devin` | Devin AI wiki ingestion |
| `types/alc_pkgs.d.lua` | `luacats` | LuaCATS type stubs for IDE |
**Hand-writing or hand-editing any projection is forbidden.** A
discrepancy between Source and Projection always means either
(a) a stale projection that needs regeneration, or (b) a Source
write disguised as a projection edit. The lint pipeline assumes
Source is canonical.
The legacy `M.docs.narrative` field and standalone `narrative.md`
files were transitional violations of this principle — projections
that authors edited by hand. They are now fully generated.
---
## 1. Publish patterns and lint scope
Pkg authors fall into one of three publish patterns. Each pattern has
a different lint posture; the same lint codes (§5) are evaluated, but
the severity boundary that fails a build differs.
| **Bundled** | Shipped via `algocline-bundled-packages` (the official curated set) | `lint_strict=true` | Required + Recommended fields all populated. Every warning is treated as an error. |
| **Community** | Personal repos, gists, hand-installed packages | `lint_strict=false` | Required fields populated. Recommended fields strongly encouraged but warnings are non-blocking. |
| **Private** | Local-only experiments, never published | Lint not expected to run | No baseline. Authors may run lint at their discretion. |
`lint_strict=true` (Bundled / CI gate) is the only mode that fails
the build on warnings. `lint_strict=false` (default) allows warnings
to surface in tooling output without blocking the build.
After lint passes, `alc_hub_dist` (the **DIST** stage) regenerates
every projection listed in §0.
The Required vs Recommended boundary is defined in §2 (`M.meta`
fields) and §3 (docstring shape). All conventions in this document
apply to all three patterns; only the lint posture differs.
---
## 2. Top-level pkg shape
Every algocline pkg is a Lua module that returns a table `M`. The canonical layout (taken from `cot/init.lua` and trimmed):
```lua
local S = require("alc_shapes")
local T = S.T
local M = {}
---@type AlcMeta
M.meta = {
name = "cot",
version = "0.1.0",
description = "Iterative chain-of-thought — cumulative reasoning steps, then synthesis",
category = "reasoning",
}
---@type AlcSpec
M.spec = {
entries = {
run = {
input = T.shape({
task = T.string:describe("The question or task to reason about"),
depth = T.number:is_optional():describe("Number of reasoning steps (default: 3)"),
}),
result = T.shape({
chain = T.array_of(T.string):describe("Ordered insights, one per reasoning step"),
conclusion = T.string:describe("Synthesized final answer"),
}),
},
},
}
-- M.docs is optional; set schema_version for future compat if needed.
-- Do NOT set M.docs.narrative — that field has been removed.
M.docs = {
schema_version = 1,
}
function M.run(ctx)
-- ...
end
return M
```
Each field below is tagged with one of three status labels. The same labels are used throughout this document.
| **Required** | Must be present. Missing it is a lint error in every publish pattern (§1). |
| **Recommended** | Not required, but strongly encouraged. **Bundled** pkgs are expected to populate it; missing it is a warning by default and an error under `lint_strict=true`. |
| **Optional** | May be omitted. No lint emission. |
### 2.1 `M.meta` — identity
| `name` | **Required** | string | Must match the pkg directory name (lint: `E_NAME_MISMATCH`). |
| `version` | **Required** | string | SemVer. **Top-level `M.VERSION` is legacy** and triggers `W_META_LEGACY_M_VERSION`; use `M.meta.version` only. |
| `description` | **Required** | string | One-line tagline (≤ 80 char). Projected into `llms.txt` entry. |
| `category` | **Required** | string | Grouping key for `llms.txt` and hub search. |
| `alc_shapes_compat` | **Recommended** | string | SemVer range (e.g. `">=0.25.0, <0.26"`) declaring the pkg's compatible `alc_shapes` versions. Validated by `alc_hub_gendoc` against the bundled `alc_shapes` (range mismatch → error; absent → warning). Required for **Bundled** pkgs. |
| `tags` | **Recommended** | table (string array) | Free-form classification tags (e.g. `{ "swarm", "primitive" }`). Indexed by `hub_index.json` and searchable via `alc_hub_search`. No enforced taxonomy; authors choose tags that help discovery. |
(Lint codes: `E_META_MISSING_{NAME,VERSION,DESCRIPTION,CATEGORY}`,
`W_META_LEGACY_M_VERSION` — see §5.)
#### Notes on `alc_shapes_compat`
`alc_shapes_compat` is the pkg's declaration of which `alc_shapes`
versions it has been verified against. The gendoc pipeline extracts
the literal `"..."` value from the `M.meta` table and validates it as
a SemVer range; an unparseable range fails the build and a missing
declaration emits a warning naming the current bundled `alc_shapes`
version. Bundled pkgs should track the SemVer caret of the alc_shapes
they ship against (e.g. `"^0.25"` while alc_shapes is on the 0.25.x
line).
### 2.2 `M.spec` — runtime contract
`M.spec.entries.{entry}.{input, result}` declares each entry's
runtime contract using the `alc_shapes` T DSL. This is the
**SSoT for `## Parameters` projections** (§3.4) and for
`alc.run(<pkg>, ctx)` runtime validation.
| `M.spec.entries.{entry}.input` | **Recommended** | `T.shape(...)` | Each field MUST carry `:describe("...")`. The `:describe` text becomes the `description` column of the `## Parameters` projection. |
| `M.spec.entries.{entry}.result` | **Recommended** | `T.shape(...)` or `T.ref(name)` | Consumed by `alc_hub_dist projections=["luacats"]` for IDE type stubs. |
A pkg without `M.spec` is treated as **opaque**: downstream
generators emit "no declared shape" sections. This is acceptable
for **Community** and **Private** patterns but is rejected by
`lint_strict=true` for **Bundled** pkgs.
> **Note on naming**: the Lua-source location is
> `M.spec.entries.{entry}.input`. The name `input_shape` appears
> in `alc_hub_dist`'s JSON projection output (the field key in the
> generated JSON), but is **not** the source location. Do not
> write `M.meta.input_shape`; that phrasing is stale and the
> linter does not recognise it.
(Lint codes for V1: `E_META_MISSING_INPUT_SHAPE` / `E_PARAM_MISSING_DESCRIBE` — see §5.)
### 2.3 `M.docs` — optional container
`M.docs` is preserved as a container for future schema markers.
The only recognised field is `schema_version`. **`M.docs.narrative`
is removed** — do not set it. The linter will warn on unknown
`M.docs` fields in a future release.
---
## 3. Docstring-driven narrative
`init.lua` is the single source of truth for narrative content. The
gendoc pipeline (`extract.lua split_sections` + `projections.lua
narrative_md`) renders the docstring H2 sections into Markdown on
demand. The output is the `narrative` projection (§0).
**Removed conventions** (do not use):
- `M.docs.narrative = "narrative.md"` — field is gone
- Separate `narrative.md` files — not recognised; will be ignored
### 3.1 How the resource is served
When a client requests `alc://packages/{name}/narrative`:
1. The engine locates the pkg's `init.lua` (variant scope first, then global scope).
2. A fresh mlua VM runs `extract.build_pkg_info` + `projections.narrative_md`.
3. The rendered Markdown is returned as `text/markdown`.
No cache — every request re-renders from the current `init.lua`.
### 3.2 Recommended H2 sections
The H2 sections below map onto the four Diátaxis documentation
classes. Write only the sections relevant to the pkg — a simple
wrapper may need only `## Usage`; a paper implementation typically
needs `## Algorithm` + `## Theoretical foundations` + `## References`.
| `## Usage` | How-to | Minimum working example. Write for almost every pkg. |
| `## When to use` | How-to | When the pkg overlaps with siblings and the choice is non-obvious. |
| `## Algorithm` | Explanation | When the algorithm has 3+ named steps or a non-trivial invariant. |
| `## Theoretical foundations` | Explanation | When correctness follows from a theorem or paper. State the theorem. |
| `## Entry contract` | Reference | When the pkg exposes multiple named entries. |
| `## Caveats` | Reference | Pkg-wide rationale that does not fit a single parameter `:describe()` — known limitations, why certain knobs are hidden, edge cases. |
| `## Empirical validation` | Reference | Bench data, sweep results, coverage observations. |
| `## Comparison with related packages` | Reference | When a sibling pkg does something similar. One bullet per peer. |
| `## References` | Reference | Papers, arXiv IDs, books. Bullet list — see §4.4. |
`## Parameters` is **not** in the list above — it is generated from
`M.spec.entries.{entry}.input` (§2.2 / §4.3) and must not be
hand-written.
### 3.3 Canonical example
The reference docstring below demonstrates the required elements:
```lua
--- conformal_vote — split conformal prediction gate for multi-agent deliberation
---
--- Linear opinion pool + split conformal prediction post-hoc decision layer.
--- Emits a three-way decision (commit / escalate / anomaly) with a finite-sample
--- coverage guarantee `Pr[Y ∈ C(X)] ≥ 1-α` (Theorem 2). Calibration and online
--- rounds share aggregation weights so exchangeability is preserved.
---
--- ## Algorithm
---
--- Given N agents that each emit a verbalized probability distribution
--- π_i(y|x) over a fixed option set, the pkg performs:
---
--- ```math
--- q̂ = sorted[⌈(n+1)(1-α)⌉] (finite-sample quantile, §4.3)
--- C(x) = { y : P_social(y|x) ≥ 1 - q̂ } (prediction set)
--- ```
---
--- ## Theoretical foundations
---
--- Theorem 2 guarantees `Pr[Y ∈ C(X)] ≥ 1-α` in finite samples whenever
--- calibration and online rounds share the same aggregation weights and
--- the data is exchangeable.
---
--- ## Entry contract
---
--- - `calibrate` — pure, direct-args. returns `{ q_hat, tau, alpha, n, weights }`
--- - `aggregate` — pure, direct-args. returns `{ [label] = p_social }`
--- - `predict_set` — pure, direct-args. returns `{ labels, top1, top1_prob, ... }`
--- - `decide` — pure, direct-args. returns `{ action, selected }`
--- - `run` — Strategy, ctx-threading. queries N agents via `alc.llm`
---
--- ## Comparison with related packages
---
--- Category: `validation` (alongside `sprt`, `eval_guard`, `inverse_u`).
---
--- ## References
---
--- Wang, Xie, Wang, Gao, Yang, Li, Qiu, Han, Qiu, Huang, Zhu, Woo (2026).
--- "From Debate to Decision: Conformal Social Choice for Safe Multi-Agent
--- Deliberation". arXiv:2604.07667.
```
---
## 4. Docstring style
### 4.1 1-line summary and abstract
Every docstring opens with a 1-line summary, followed by a 1–3 sentence abstract:
```lua
--- {PkgName} — {verb phrase}
---
--- {abstract: 1-3 sentences explaining the core capability}
---
--- ## {first H2 section}
```
**1-line summary** (line 1):
- One clause, ≤ 80 characters, of the form
`{PkgName}({StyledName}) — {verb phrase}` when the pkg has a stylized
name (typically a paper-cited abbreviation such as `CoT`, `UCB`,
`MCTS`), or `{PkgName} — {verb phrase}` when the pkg directory name
is already the canonical form
- `PkgName` is always the pkg directory name (lowercase, matches
`M.meta.name`). `StyledName` is the conventional reading-aid form
used in the literature; omit it when no such form exists.
- `—` is the em dash (UTF-8 `U+2014`)
- Becomes the H1 title in the rendered narrative and the entry in `llms.txt`
- `M.meta.description` (§2.1) carries the same (or slightly expanded) wording for JSON consumers
Good:
```lua
--- conformal_vote — split conformal prediction gate for multi-agent deliberation
--- cot(CoT) — iterative chain-of-thought reasoning
--- ucb(UCB) — upper confidence bound multi-armed bandit
```
Too vague:
```lua
--- conformal_vote — a useful voting package
```
**Abstract** (lines after the blank `---` separator, until the first `## ` heading):
- 1 to 3 sentences
- Plain prose only — no headings, no lists, no code fences
- Explains the core capability at a glance; rationale and algorithm details belong in the H2 sections (§3.2)
If the pkg is trivial enough that no H2 sections follow, the abstract may stand alone — the rendered narrative will then consist of H1 + abstract only.
### 4.2 Markdown syntax
Within the docstring body, follow these Markdown rules. Concrete examples follow each rule.
**Headings**:
- `#` (H1) — **forbidden**. The generator synthesises H1 from the 1-line summary (§4.1).
- `##` is the highest permitted level
- `###` is allowed as a subsection within a `##` block
- `####` and lower — forbidden
- Each heading must be followed by a blank `---` line before body text:
```lua
--- ## Algorithm
---
--- 1. step
```
**Code fences**:
- Use **explicit** triple backticks (`` ``` ``). 4-space-indent fences are forbidden
- Language hint is recommended (`` ```lua `` for Lua, `` ```math `` for GitHub MathJax-rendered equations)
- One snippet per fence. Multiple snippets must use independent fences
- Inline code uses single backticks (`` ` ``)
```lua
--- ## Usage
---
--- ```lua
--- local pkg = require("pkg")
--- return pkg.run(ctx)
--- ```
```
**Lists**:
- Bullet: `-` (hyphen) only. `*` and `+` are forbidden
- Numbered: `1.` / `2.` / `3.` style
- Letter-numbering (`a.` / `b.`) is renderer-dependent and forbidden
- Indentation: 2 spaces for sub-bullets under `-`, 3 spaces for sub-bullets under numbered lists
- Maximum nesting depth: 2 levels. 3+ levels of nesting are forbidden — renderer-dependent
**Links**:
- GitHub-style markdown links only: `[text](url)`
- Bare URLs are forbidden in body text; they are permitted **only** inside `## References` citations (§4.4)
```lua
--- See [the README](../README.md) for derivation.
```
**Encoding**:
- UTF-8 only
- em dash `—` (`U+2014`) and en dash `–` (`U+2013`) are allowed
- All docstrings are written in English. The rendered narrative and `llms.txt` are public artifacts; non-English docstrings break readability for downstream consumers.
### 4.3 Parameters — generated from `M.spec`
The `## Parameters` section in the rendered narrative is **machine-generated** from `M.spec.entries.{entry}.input` (§2.2). Authors do not write `## Parameters` in the docstring.
The generator emits one row per shape field, using:
- field name as `key`
- shape type as `type`
- `:is_optional()` as `required` (true / false)
- `:describe("...")` text as `description`
**`:describe()` requirements**:
Every shape field MUST carry `:describe("...")`. The describe text is the only source for the `description` column.
```lua
M.spec = {
entries = {
run = {
input = T.shape({
task = T.string:describe("The question or task to reason about"),
depth = T.number:is_optional():describe("Number of reasoning steps (default: 3)"),
}),
},
},
}
```
In **Bundled** mode (`lint_strict=true`), missing `:describe()` is a hard error. In **Community** mode, missing `:describe()` is a warning (lint code `E_PARAM_MISSING_DESCRIBE` — see §5).
**Where to put the rationale** (two-tier rule):
| Per-parameter semantic rationale (e.g. *why* a numeric default was chosen, when truncation breaks invariants) | Inside the parameter's `:describe("...")` text. The text may be long; the projection emits it verbatim into the `## Parameters` table. |
| Pkg-wide rationale (e.g. *why* a token budget knob is hidden, why certain options are intentionally not exposed) | `## Caveats` H2 section (§3.2) |
This separation ensures (a) parameter-specific rationale survives every projection (`## Parameters` table, `context7.json`, `llms-full.txt`, LuaCATS stubs), and (b) pkg-wide design rationale lives where readers look for caveats.
**Hand-written `## Parameters` is forbidden**:
Writing `## Parameters` in the docstring while `M.spec.entries.{entry}.input` is also declared raises lint error `E_PARAMETERS_CONFLICT`. The two are mutually exclusive: `M.spec` is the SSoT.
For pkgs without `M.spec` (opaque pkgs — Community / Private), the generator emits no `## Parameters` section. Authors do not need to write one.
### 4.4 References
The `## References` H2 section uses a flat bullet list. Do not wrap citations in code fences.
- Each citation is a single bullet starting with `-`
- Continuation lines indent by 2 spaces
- Bare URLs are permitted **only** inside this section (e.g. arXiv links, paper DOIs)
```lua
--- ## References
---
--- - Friedman, M. (1937). "The use of ranks to avoid the assumption of
--- normality ...," J. Am. Stat. Assoc. 32(200): 675–701.
--- - Wang, X. et al. (2026). "From Debate to Decision: Conformal Social
--- Choice for Safe Multi-Agent Deliberation". arXiv:2604.07667.
```
Inline citations in body text reference the bullet by surname or arXiv ID — no inline URL.
### 4.5 LuaCATS annotations
LuaCATS annotations (`---@type`, `---@param`, `---@return`, etc.) appear **after** the narrative docstring body. The generator stops narrative extraction at the first `---@` line.
```lua
--- cot(CoT) — iterative chain-of-thought reasoning
---
--- Builds a reasoning chain step by step, then synthesizes the chain
--- into a single coherent conclusion.
---
--- ## Usage
---
--- ```lua
--- local cot = require("cot")
--- return cot.run({ task = "Why is the sky blue?", depth = 3 })
--- ```
local S = require("alc_shapes")
local T = S.T
local M = {}
---@type AlcMeta
M.meta = {
name = "cot",
version = "0.1.0",
description = "Iterative chain-of-thought — cumulative reasoning steps, then synthesis",
category = "reasoning",
}
---@type AlcSpec
M.spec = {
entries = {
run = {
input = T.shape({
task = T.string:describe("The question or task to reason about"),
depth = T.number:is_optional():describe("Number of reasoning steps (default: 3)"),
}),
result = T.shape({
chain = T.array_of(T.string):describe("Ordered insights, one per reasoning step"),
conclusion = T.string:describe("Synthesized final answer"),
}),
},
},
}
---@param ctx AlcCtx
---@return AlcCtx
function M.run(ctx) ... end
return M
```
Place them last so the narrative reads as a single coherent block when stripped of annotations. LuaCATS is consumed by `alc_hub_dist projections=["luacats"]` to generate IDE type stubs (§0).
### 4.6 Disallowed constructs (collected)
The constructs below are forbidden in any docstring. Each is also covered in the relevant subsection above; this table consolidates them with the corresponding lint code (§5).
| `#` (H1) heading | Generator synthesises H1 from the 1-line summary (§4.1) | `E_H1_IN_DOCSTRING` |
| `$...$` / `$$...$$` inline LaTeX | Renders only on GitHub; breaks elsewhere. Use `` ```math `` fence | (no lint, manual review) |
| 4-space-indent code fence | Use explicit `` ``` `` only | (no lint, manual review) |
| HTML tag (`<br>`, `<sub>`, etc.) | Markdown only | (no lint, manual review) |
| Emoji | Breaks llms.txt and CRAN-style consumers | (no lint, manual review) |
| 3+ levels of list nesting | Renderer-dependent | (no lint, manual review) |
| Hand-written `## Parameters` heading | `M.spec.entries.{entry}.input` is the SSoT (§4.3) | `E_PARAMETERS_CONFLICT` |
---
## 5. Lint rules
`alc_hub_gendoc` runs the lint pipeline and emits the codes below. `lint_strict=true` (Bundled / CI gate, §1) treats every `error` severity as a build failure; `lint_strict=false` (default) surfaces them as non-blocking diagnostics.
| `E_META_MISSING_NAME` | error | active | `M.meta.name` missing |
| `E_META_MISSING_VERSION` | error | active | `M.meta.version` missing |
| `E_META_MISSING_DESCRIPTION` | error | active | `M.meta.description` missing |
| `E_META_MISSING_CATEGORY` | error | active | `M.meta.category` missing |
| `E_NAME_MISMATCH` | error | active | `M.meta.name` ≠ pkg directory name |
| `E_H1_IN_DOCSTRING` | error | active | `#` heading present in docstring |
| `E_PARAMETERS_CONFLICT` | error | active | `M.spec.entries.{entry}.input` declared and hand-written `## Parameters` both present |
| `E_RESULT_CONFLICT` | error | active | `M.spec.entries.{entry}.result` declared and hand-written `## Result` both present |
| `E_META_MISSING_INPUT_SHAPE` | warning / error | planned (V1) | `M.spec.entries.{entry}.input` missing. `warning` in default mode, `error` under `lint_strict=true`. |
| `E_PARAM_MISSING_DESCRIBE` | warning / error | planned (V1) | Shape field without `:describe(...)`. `warning` in default mode, `error` under `lint_strict=true`. |
| `W_DESCRIPTION_MULTILINE` | warning | active | `M.meta.description` contains a newline |
| `W_FAKE_LABEL` | warning | active | `Usage:` / `Args:` style label — promote to `## Usage` etc. |
| `W_EMPTY_NARRATIVE` | warning | active | No abstract and no H2 sections |
| `W_META_LEGACY_M_VERSION` | warning | active | `M.VERSION` top-level field detected. Canonical form uses `M.meta.version` only (§2.1). Safe to remove if no external reference. |
**Severity convention** (per `lint.lua`):
- `error` — `lint_strict=true` rejects the pkg; `lint_strict=false` reports non-blocking
- `warning` — never blocks the build, surfaces in tooling diagnostics
- Planned rules with split severity emit `warning` in default mode and **promote to `error`** under `lint_strict=true`
Active rules are implemented in `crates/algocline-app/src/service/lua/gendoc/docs/lint.lua`. Planned rules are not yet implemented.
---
## 6. Migration
### 6.1 Migration from `M.docs.narrative` / `narrative.md`
If an existing pkg uses the old (pre-narrative-decommission) convention:
1. Move narrative content into the `---` docstring block as H2 sections.
2. Remove `M.docs.narrative` from the `M.docs` table (or drop `M.docs` entirely if `schema_version` is also absent).
3. Delete the standalone `narrative.md` file.
4. Run `alc_hub_dist` with `projections=["narrative"]` to regenerate `docs/narrative/{name}.md`.
5. Verify `alc://packages/{name}/narrative` returns the expected Markdown.
### 6.2 Migration to V1 conventions
For pkgs predating the V1 conventions in this document:
1. **Move parameters into `M.spec`**: if the pkg declared `M.meta.input_shape` (a stale phrasing) or had a hand-written `## Parameters` H2, move the shape definition into `M.spec.entries.{entry}.input` (§2.2). Delete the `## Parameters` H2.
2. **Add `:describe()` to every shape field**: each field in `input` and `result` must carry a `:describe("...")` clause. Fields without describe text become empty rows in the projected `## Parameters` table.
3. **Move pkg-wide rationale to `## Caveats`**: prose explaining *why* certain knobs are hidden, why a token budget is fixed, etc., goes into the `## Caveats` H2 (§3.2). Per-parameter rationale stays inside `:describe()` (§4.3).
4. **Run lint**: `alc_hub_gendoc lint_strict=false` first to see warnings, then `lint_strict=true` if the pkg targets the Bundled distribution (§1).
5. **Regenerate projections**: `alc_hub_dist` to refresh `docs/narrative/{name}.md` and downstream artifacts (§0).
## 7. 1-pkg authors: publishing a single package to Hub
If your repository contains exactly one algocline package, follow these steps
to make it discoverable via `alc_hub_search`.
### Layout requirement
Place your package at `<repo>/<pkg_name>/init.lua` (nested), **not** at the
repository root. Example:
```
my-cool-pkg/
├── my_cool_pkg/
│ └── init.lua # M.meta, M.spec, M.run
├── alc.toml
└── hub_index.json # generated by `alc_hub_dist`
```
### Minimal `alc.toml`
```toml
[hub]
# Default values are sufficient for a single-package repo. Optional sections
# [hub.context7] and [hub.devin] customize projection targets — see
# docs/hub-gendoc-config.md.
```
### Publishing
From a Claude Code / rmcp MCP session in your repo root:
```
alc_hub_dist(
source_dir = ".",
output_path = "hub_index.json",
out_dir = "docs",
projections = ["hub", "narrative"],
lint_strict = false
)
```
Then commit and push:
```sh
git add hub_index.json docs/
git commit -m "publish: regenerate hub_index"
git push
```
Consumers can now install via `alc_pkg_install({ url: "github.com/you/my-cool-pkg" })`
and your package will appear in `alc_hub_search` results.
---
## 8. Testing
algocline packages should ship tests under `<pkg>/spec/<file>_spec.lua`. Run
them via `mcp__algocline__alc_pkg_test`.
### Spec file layout
Place spec files at `<pkg_root>/spec/<name>_spec.lua`. Each file is a
self-contained lspec suite. The `lust` global (`describe`, `it`, `expect`,
`spy`, etc.) is pre-loaded automatically — no `require` needed.
```lua
-- <pkg_root>/spec/myfeature_spec.lua
local describe, it, expect = lust.describe, lust.it, lust.expect
describe('myfeature', function()
it('does X correctly', function()
local result = require('mypkg').do_x()
expect(result).to.equal('expected_value')
end)
end)
```
### Running tests
- `alc_pkg_test pkg="mypkg"` — run all `<pkg_root>/spec/*_spec.lua`
- `alc_pkg_test pkg="mypkg" filter="feature"` — run only specs whose stem
contains `"feature"` (e.g. `feature_spec.lua`)
- `alc_pkg_test pkg="mypkg" spec_dir="tests"` — use a custom spec directory
- `alc_pkg_test code_file="<abs_path>"` — run a single file (escape hatch;
use absolute paths in worktree environments)
- `alc_pkg_test code="<inline lua>"` — ad-hoc inline test
### Output shape
```json
{
"passed": 3,
"failed": 0,
"pending": 0,
"total": 3,
"duration_ms": 42,
"spec_files": [
{
"path": "/path/to/myfeature_spec.lua",
"passed": 3,
"failed": 0,
"total": 3,
"duration_ms": 40,
"tests": [
{ "suite": "myfeature", "name": "does X correctly",
"passed": true, "pending": false, "error": null }
]
}
]
}
```
Per-spec-file Lua crashes increment `failed` and continue (execution is not
aborted). Setup failures (package not found, zero spec files) are returned as
a typed error on the MCP wire.
### Migration from `tests/test_<pkg>.lua`
Existing bundled-packages tests use a flat `tests/test_<pkg>.lua` layout and
continue to work via `mcp__lua-debugger__test_launch`. New packages should
adopt the `<pkg>/spec/<file>_spec.lua` layout and use `alc_pkg_test`.
## 9. Env
Bundled / third-party pkgs MUST NOT read OS env / API keys / dotenv files
directly inside `init.lua`. Host-platform integration (the caller orch
entry / Main AI) pre-resolves env into `ctx.env`, and pkg code declares
the variables it actually consumes via `alc.env:use{...}` at the point of
use.
| Forbidden inside a pkg | `os.getenv`, `std.env.get`, hand-rolled dotenv parsing, ad-hoc `API_KEY` / `BASE_URL` lookups |
| Recommended inside a pkg | `local env = alc.env:use{ "KEY1", "KEY2" }` then `env.KEY1` to read |
| Caller-side responsibility | Pass sources at run time: `alc_run(code, ctx = { env = { dotenv = ".env", allow_os = true } })` |
| Reference integration | `coding_orch` Phase 2 migration (agent-profiles issue `1778976345-92995`) replaces `resolve_*_env` helpers with `alc.env:use` |
### Example (Phase 2 reference)
```lua
-- inside the pkg (post-Phase 2)
local env = alc.env:use{ "QWEN_BASE_URL", "QWEN_API_KEY", "QWEN_MODEL" }
local base_url = env.QWEN_BASE_URL -- nil if absent
if base_url == nil then
return error("NEEDS_CONTEXT: QWEN_BASE_URL")
end
```
### Exception: test path resolution
mlua-probe sandbox test path resolution such as `os.getenv("PWD") or "."`
is out of scope for `alc.env` and not subject to this rule — it is a
Lua VM test-runtime constraint, not an application-level env access.
### Why pkg-internal env access is rejected
- Pkgs are portable units; reading env inside an `init.lua` couples the
pkg to a specific host's secret-loading convention.
- `alc.env:use` makes the required vars **declarative** at the point of
use, which lets `coding_orch` / future hosts pre-validate and surface
a single `NEEDS_CONTEXT: <KEY>` error to the caller instead of failing
deep inside pkg code.
- Audit and caching live in the host (`ctx.env`), not scattered across
pkgs. A sweep of `algocline-bundled-packages` v0.24.0 (120 pkgs)
confirms zero pkg-internal env access today; this section codifies the
existing discipline so future pkgs do not regress.
---
## 10. Cards quick reference (`alc_card_find` DSL)
`alc_card_find` filters Cards using a Prisma-style `where` predicate plus
`order_by`/`limit`/`offset`/`pkg`. Use it to surface eval results, lineage
records, or any other Card type produced by strategies.
### Common patterns
```jsonc
// Top 10 cards by pass_rate for a single pkg
{
"pkg": "cot",
"where": { "stats": { "pass_rate": { "gte": 0.8 } } },
"order_by": "-stats.pass_rate",
"limit": 10
}
// AND / OR / NOT
{
"where": {
"_and": [
{ "model": { "id": "claude-sonnet-4-6" } },
{ "_or": [
{ "tags": { "contains": "bench" } },
{ "category": { "in": ["reasoning", "synthesis"] } }
] }
]
}
}
// Existence + dotted path sort
{
"where": { "stats": { "pass_count": { "exists": true } } },
"order_by": ["-created_at"]
}
```
### Predicate cheatsheet
| `{ "field": "value" }` | shorthand for `eq` |
| `{ "field": { "eq": x } }` | equality |
| `{ "field": { "ne": x } }` | inequality |
| `{ "field": { "lt": x } / "lte" / "gt" / "gte" }` | numeric / lexical compare |
| `{ "field": { "in": [a,b] } / "nin": [...] }` | membership |
| `{ "field": { "exists": true } }` | field present (any value) |
| `{ "field": { "contains": "sub" } }` | substring match (strings) |
| `{ "field": { "starts_with": "pre" } }` | prefix match |
| Nested object | section path (e.g. `stats.pass_rate`) |
| `_and` / `_or` / `_not` | logical combinators |
`order_by` accepts a single dotted-path string or an array; `-` prefix is
descending. `pkg` narrows the filesystem scan to one package — pass it
when you know the target. Default sort: `created_at` descending.
### Wiring tips
- Card schemas are pkg-defined. Inspect `mcp__algocline__alc_card_list`
/ `alc_card_samples` first to discover available fields per pkg.
- `where` paths follow Card JSON exactly: nested sections are nested
objects, not flattened dotted strings.
- `limit` defaults are server-side; pass it explicitly for stable paging
with `offset`.
## 11. Pre-publish verification workflow
Before pushing a package to Hub, run the local verification chain so
remote consumers receive a known-good artifact. The chain is built from
existing read-only MCP tools — no new tooling is required.
### Pre-push checklist
Run these MCP calls in order against the package source directory.
Each step is read-only except `alc_hub_dist` (writes `hub_index.json`).
1. **`alc_pkg_test`** — run the package's `spec/*_spec.lua` suite.
```jsonc
{ "pkg": "<pkg_name>" }
```
Verify `failed == 0` in the returned JSON. See §8 Testing for spec
layout.
2. **`alc_hub_dist`** — regenerate the local `hub_index.json` and Hub
docs from the current source tree.
```jsonc
{
"source_dir": "/abs/path/to/source",
"output_path": "/abs/path/to/source/hub_index.json"
}
```
Confirm `reindex.package_count` matches the number of packages you
expect under `source_dir`. A drop to `0` typically means `M.meta.name`
is missing in `init.lua` — see `alc_pkg_doctor`'s `missing_meta`
verdict.
3. **`alc_pkg_doctor`** — inspect every installed package for known
defect patterns.
```jsonc
{} // omit `name` to scan all packages
```
For a release-grade run, **all of these arrays must be empty**:
- `incomplete_pkg` (missing `require()`-ed submodule files)
- `installed_missing` (registered pkg but install dir gone)
- `symlink_dangling` (dead `alc_pkg_link` target)
- `path_missing` (`alc.toml` `path = ...` resolves nowhere)
- `missing_meta` (`init.lua` without `M.meta.name`)
- `missing_hub_index` (collection root with 2+ pkgs but no index)
- `spec_missing` (declared `spec/` but zero `*_spec.lua` files)
The `stale_cache` array is informational — re-run `alc_hub_search`
to refresh. `healthy` always contains the rest.
4. **`alc_hub_search` with `local_indices`** — verify the freshly
regenerated `hub_index.json` is parseable and lists every expected
package, before any remote push.
```jsonc
{
"local_indices": ["/abs/path/to/source/hub_index.json"],
"verbose": "full"
}
```
The returned `results` array includes both remote-fetched packages
and the local-only entries. Inspect `name` to confirm every package
authored in this push is present. Combined with `installed_only:
false` (default), this surfaces the exact set that remote consumers
will see after the push lands.
### Local-only verification
For air-gapped or pre-release inspection, drop step 4's remote fetch
entirely by also passing an offline `filter`:
```jsonc
{
"local_indices": ["/abs/path/to/source/hub_index.json"],
"filter": { "installed": false },
"limit": 0
}
```
`limit: 0` means "no limit" (return all entries — empty-means-all
idiom). Useful when CI does not have network access.
### When the chain passes
If steps 1–4 all return clean results, the source tree is consistent
with what remote consumers will fetch after push. Typical push action
is project-defined (release tag, `git push`, registry publish) and is
outside the MCP tool surface.
### When a step fails
| `alc_pkg_test` `failed > 0` | spec assertion or runtime error | inspect `spec_files[].tests[].error` |
| `alc_hub_dist` `reindex.package_count == 0` | every `init.lua` lacks `M.meta.name` | add `M.meta = { name = "...", version = "..." }` |
| `alc_pkg_doctor` `incomplete_pkg` non-empty | `require("pkg.sub")` references a missing file | add the sub file or remove the `require` |
| `alc_pkg_doctor` `missing_meta` non-empty | `init.lua` parsed but no `M.meta.name` | declare `M.meta` block; see §2.1 |
| `alc_pkg_doctor` `missing_hub_index` non-empty | collection root has 2+ pkg dirs but no index | run step 2 (`alc_hub_dist`) to generate it |
| `alc_pkg_doctor` `spec_missing` non-empty | `spec/` exists but no `*_spec.lua` files | add a `*_spec.lua` file or delete the `spec/` dir |
| `alc_hub_search` does not list a package | the package's `init.lua` was skipped during reindex (no `M.meta.name`) | re-run step 2 after fixing `M.meta` |
The checklist is intentionally tool-only (no shell wrapper). If you need
a single one-shot call site, an `alc_pkg_prepublish_check` combo tool is
tracked as a future convenience; the four-step manual chain remains the
canonical path.
---
## Bundled Hub Sources (Collection-Only Install)
algocline ships with multiple bundled Hub Collection sources, listed in
`src/init.rs::BUNDLED_SOURCES`. These are auto-installed via `alc init` /
`alc update` as a single batch. Each source is a Collection repository
(`<repo>/<pkg-name>/init.lua` layout) and must publish a `hub_index.json`
at the repository root.
**Canonical: collection-unit install.** All bundled sources are designed to
coexist when installed together. Cross-package `require()` chains within a
Collection (e.g., `swarm_frame_algocline` requires `swarm_frame` at the
top level) assume that the entire Collection batch has been installed.
Individual cherry-pick install via `alc_pkg_install` for only a subset of
a Collection may fail at runtime due to unresolved `require()` chains.
algocline does not currently maintain a dependency resolver across packages.
**Discouraged: individual cherry-pick install.** Avoid installing only a
subset of a Collection with `mcp__algocline__alc_pkg_install`. If a
package has cross-package `require()` dependencies within its Collection,
those dependencies will be missing and the package will error at load time.
If you need to remove a specific bundled package (`alc_pkg_remove <name>`),
be aware that other packages in the same Collection may have a runtime
`require()` to it; the registry will not warn you. Re-run `alc update
--force` to restore the full Collection to the pinned bundled version.
The full list of bundled sources and their pinned tags is in
`src/init.rs::BUNDLED_SOURCES`.
**LLM-derived metrics formatting.** Use `alc.fmt` / `alc.log_fmt` instead of raw
`string.format` when interpolating numeric values that may originate from an LLM
(float, NaN/Inf, or string-shaped numbers). The native `string.format("%d", 1.5)`
truncates toward zero and `string.format("%d", 0/0)` raises; `alc.fmt` rounds
half-away-from-zero and substitutes safe string literals.
**Authoring packages that depend on bundled substrate.** When a package
design requires state hand-off, flow control, or frame orchestration, check
whether the bundled substrate already provides the primitive before proposing
a new abstraction. The canonical substrate list is in
`plugins/alc/skills/alc-wake/SKILL.md §Swarm framework` (packages: `flow`,
`swarm_frame`, `swarm_frame_algocline`, `plugin_run_card`, `alc.state`).
During design consultation, `@alc-adviser` cross-references these primitives
and pairs every gap finding with a literal primitive path (e.g.,
`flow.state_save`, `swarm_frame.frame.register`) or the explicit phrase
`no primitive applies`. See `plugins/alc/agents/alc-adviser.md §Substrate Cross-Check`
(step 3b) for the full procedure.