3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00
Files
adf-codec/AGENTS.md
T

244 lines
13 KiB
Markdown

# Working in this repo
The rules for every collaborator, human or agent, and an index of the decisions a reader would
otherwise relitigate. Using the library: `README.md`. What is still to build: `todo.md`.
## Decisions
In `docs/decisions.md`:
- Each call names its flavour
- Portable markdown is a flavour of the grammar
- The round-trip is the product
- Markdown in is a canonical fixpoint
- Equality is deep
- `!adf:textBreak{}` parts text CommonMark would join
- A list right after one of its kind takes the other marker
- An empty key spells `empty`
- `-0` is spelled `-0`
- A block's marks are named, and `colwidth` is a comma list
- Empty markdown is a document of no blocks
- Unknown nodes ride the carry
- The carry fence names the node type
- A code block is a fence per text node
- Foreign HTML sorts three ways
- Names stay text
- Directives under `!adf:`
- CommonMark is a subset
- Tables
- Links
- An image reads as an image only alone in its paragraph, else as its alt text linked; a link text of hard breaks alone reads as its destination, then the breaks
- An image's title is its caption when the image stands alone in its paragraph, since a title counts as content
- A mark skips a node that cannot hold it, and a directive mark wrapping only such nodes is refused, as is a `[text](url)` link unless its text is hard breaks alone
- A linked image is written `[![alt](url)](href)`, a captioned image as its directive
- A linked image standing inline in portable markdown, its link to another target, is written `[alt](href) ([image](url))`
- Ids stay site-local
- Portable task ids come from position
- A block a task item cannot hold stands after its task list
- A callout title keeps its link targets
- The portable flavour's spellings
- The portable reader refuses what GFM's reading would drop
- The HTML dialect
- `htmlToAdf` also reads the dialect as a browser serializes it
- No runtime dependencies
- Standards ship as data
- fast-check
- Stryker in its own package
- Any ES2022 engine
- ESM only
- One built entrypoint
- Public on npm
- GitHub is canonical
- The formats are API
- The code list
- Which code a cause takes
- `message` and `path`
- Publish on a version bump
- Docs describe the release being built
- Plan items rank by `rank_todo.py`'s goal weights, code health first
- Posts announcing a release wait for its publish, and their drafts wait for the finished release
- A chunk ruled worse merges where a plan item of its release already restores that code's reading, and that item takes the ruling's places
- A chunk ruled worse that restructures the places takes a paired ruling after each restructure, against the same base, and merges once one is not worse
- Schema refusals are made in the first step alone
- The gate runs on Deno and Bun
- The gate installs the tarball
- Firefox reads the build
- The coverage floors
- The size ratchet
- Properties on a fixed seed
- The CommonMark suite checks three ways
- The flavour spec is read as a source
- The node tables answer to Atlassian's schema
- Output past V8's longest string is refused on every engine
- HTML writes a URL as the ADF holds it, whatever its scheme
- Nothing recurses unbounded
- JSON text is read by `parseJsonText`
- No value `JSON.parse` or `structuredClone` builds loops a call
- Nothing spreads an unbounded array
- A retry loop checks its own termination
- After 32 passes, a line carries or drops every em, strike and strong mark
- With the first misread emphasis run, `unspellableRuns` returns only runs that, taken back early, leave the line as the one-run-per-pass reference writes it
- Readers scan by index
- The spelling memo
- Cost fixes are measured, never timed
- Only the hard break holds a raw newline
- Emphasis follows CommonMark's matching
- Where a text backtick or `[` opens a code span, a link or an image, every text backtick or `[` in its line escapes
- Readable spellings take the `try` prefix
- The attribute vocabulary is ADF's
- The source parts by ADF and format
- The comparison table lists every converter that starts and converts
- The real-world corpus holds anonymized documents from sites whose licence it records
## 1. Nothing about any consumer in the library
The shipped library holds no Jira client, no HTTP, no REST shapes, no issue keys and no actual
consumer's name. Design against the README's personas. Tooling and fixtures outside the build may
name public sites, and tooling may fetch from them, as `corpus-gathering/` does.
## 2. Release automation
- The bump commit renames `CHANGELOG.md`'s `## Unreleased` to the version.
- Exact versions: `save-exact=true` in `.npmrc`.
- Renovate watches devDependencies, Docker pins and action tags; automerges everything on green CI.
- Docker images pin the full patch version (`node:24.19.0-alpine3.24`, never `node:24`), as
specific as the publisher tags: `oven/bun:1.4.0-alpine` pins Bun's patch and leaves the base
floating because Bun publishes nothing narrower. Actions pin semver tags.
## 3. Tests first, in Docker
Test for the behaviour wanted first, then implement until green. `node --test`, beside the code.
Node, tsc and npm never run on the host — only via the pinned images (§2). Tests are independent,
containers are torn down after a run. A test reaches only for what Node's, Deno's and Bun's `node:`
shims all carry.
Every leg announces its name and, where a container is in play, the image, before it runs and its
elapsed time after, `publish.sh` alongside `ci.sh`, so a long run reads as progress rather than as
a hang. A leg added later owes the same marker, and a function a leg reaches chains its statements
with `&&`, because the `||` that captures the leg's status suspends `set -e` for everything it
calls. A leg whose output is both streamed and grepped keeps the copy in a `mktemp`
file: `tee /dev/stderr` reopens fd 2, and under `./ci.sh > log 2>&1` the two offsets punch NUL
holes through each other's lines.
`PROPERTY_RUNS=<runs>` raises the property runs and randomizes the seed for local digging. The
generators and run parameters properties share live in `src/conformance/property-harness.ts`,
outside the build and coverage.
Each `- ` bullet in `spec/flavour.md`'s `## Block nodes`, `## Inline nodes` and `## Marks` declares
the nodes named before its first em dash, with the attributes following `Attributes: ` — a
parenthesized value set reading `string`; fenced examples are skipped. Keep prose out of a bullet.
## 4. Code rules
- Two-space indent, English everywhere. Alphabetical order wherever order carries no meaning,
keyed on the name a line introduces: an import sorts on its first binding, type imports ahead of
value imports, so moving or renaming a module reorders nothing.
- No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the
fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states
unrepresentable.
- Failures are values: everything returns `Result<T>`, nothing throws. `try/catch` only wrapped
tightly around a call that genuinely throws, converted to a result on the spot. A reader with no
path to name returns `Read<T>`, and the walk attaches the path where it knows it.
- Reuse before adding; the smallest sufficient diff is the benchmark; no speculative generality —
a second consumer, or it goes.
- Explicit over implicit; descriptive names; no catch-all files (`utils`, `helpers`, `misc`); a file
does not repeat its directory in its name — `adf/document.ts`, never `adf/adf-document.ts`. A name
is the noun `spec/flavour.md` or ADF's schema uses for the thing.
## 5. Prose to a minimum
Applies everywhere: comments, every markdown file in this repo (this one included), PR text.
- Default is no comment. One earns its single line only by naming an invariant, footgun or
external constraint the code cannot show — never restatement, history, absence or arrangement.
A second line belongs in the commit message or a `docs/decisions.md` entry.
- A doc paragraph says what the repo cannot say for itself, or it goes. The fix for a redundant
one is deletion, not trimming. A false claim in any doc is a bug, fixed where found.
- Published text — npm README, error messages, API docs — never references internal systems,
tickets or repos.
## 6. Commits and PRs
One-line commit messages and PR titles; short PR summaries. No AI-attribution markers, ever.
## 7. The working loop
A session works one chunk, starting from the first item in `todo.md` that no item still in the plan
blocks, and stops when that chunk merges, whatever it was asked to finish: a release is a chain of
sessions, so an instruction to work until a release is done names the chain, not the session. An
open PR is a chunk already in flight, and finishing it is the session.
Per chunk:
1. Fresh worktree off updated `origin/main`; implement tests-first (§3).
2. Run the larv-review flow until it passes and CI is green. A reviewer launch states the latest
gate result (commit and outcome); a reviewer does not re-run `ci.sh` or the tests when a
result exists for the commit under review, or when the diff since that result cannot affect
it (docs-only) — re-run only what its own findings or fixes invalidate. A pass at its round
cap whose last round's findings are all fixed takes no further round: the chunk proceeds, and
the PR summary names each fix no round reviewed.
3. Merge the PR (standing authorization, this repo only, granted by the maintainer), report, stop.
A chunk reads these files in part, with `grep -n '^#'` for the headings and a ranged read for
the rest:
- `CHANGELOG.md`: `## Unreleased` only, the one section a chunk edits.
- `MIGRATION.md`: the top `## From` section, the release being built.
- `README.md`: `## Goals`, `## Audience` and the sections the chunk changes or cites.
- `spec/flavour.md`: the sections the chunk changes or cites.
Reserved for the maintainer whatever any rule here says: changing `version` in `package.json` (a
bump on `main` publishes, `docs/decisions.md` §Publish on a version bump — every release is the
maintainer's) and the `NPM_TOKEN` secret.
Before you change a file outside this repo, check whether its directory is a git repo. If it is,
change it through a branch and a PR there, and merge it only with its owner's approval.
### Ask, don't guess
Any choice where what the maintainer would pick is not near-certain gets asked. The confidence bar
is very high — asking too often is the accepted cost, guessing wrong is not.
An ask is a gap in `docs/decisions.md`, and its answer is the entry that closes it, landing there —
never the instance alone; an answer that is a goal lands in the README, one that is a working rule
here. Before asking, name the class the question belongs to and the entries of that class; where
one already decides it, apply it without asking, and where it reads two ways on this input, that
reading is the ask. Never ask "A or B?": state the gap, the earlier entries of its class, the
nearest text, a candidate entry in that file's voice, and the instance it yields. An entry that
keeps collecting instances is wrong: rewrite it.
Which output the audience expects — README goal 6 — is settled by a reader panel rather than
asked: three fresh-context readers, one per README persona the question serves, each given only
`## Audience` and the input, writing what they expect before picking among outputs the goals
allow, rendered, shuffled, with no rationale and nothing saying what is implemented. Three agreeing
settle it; otherwise four more read, five of seven settle it, and less is a missing goal, asked.
The verdict lands in `docs/decisions.md`.
A writer panel settles every new or changed markdown or HTML spelling: a reader panel whose readers
are the people who read and write that format (README `## Audience`). A panel of the developer
personas settles a question about what an app relies on.
### Stated numbers
A stated number — 500 levels, the branch floor — is kept; a chunk that cannot keep it asks, naming
the number it can reach. A number the code needs and no entry states is a gap.
### The continuous loop
A `/loop` session counts as a chain of sessions, each iteration starting by re-reading `AGENTS.md`
and `todo.md` and trusting them over anything remembered from earlier iterations. The loop session
is a thin driver: each chunk's work runs in a fresh-context subagent holding this file as its
charter, and the driver only relays maintainer questions, runs the review flow, merges, and cleans
up. The loop stops when only maintainer-reserved acts remain.
## 8. Scoring run
The comprehension panel's fill-ins:
- Language: TypeScript.
- Kind: a pure-function document converter with hand-written parsers and emitters.
- Domain: Atlassian Document Format, CommonMark and HTML.
- Domain docs: the CommonMark spec, the WHATWG HTML standard, ADF's JSON schema, `spec/flavour.md` and
`spec/html.md`.
- 3am question: a viewer/editor app reports that a document it saved comes back with two text
nodes merged and a mark gone after `losslessMarkdownToAdf(adfToLosslessMarkdown(doc))`.