152 lines
7.8 KiB
Markdown
152 lines
7.8 KiB
Markdown
# Working in this repo
|
|
|
|
Decisions a reader would otherwise relitigate, and the rules for every collaborator, human or
|
|
agent. Using the library: `README.md`. What is still to build: `todo.md`.
|
|
|
|
## 1. Three formats, ADF is the hub
|
|
|
|
ADF, one markdown flavour, one HTML dialect. Six directions exposed, but markdown↔HTML compose
|
|
through ADF: four conversions exist to keep correct — never write a fifth. No fourth format, ever;
|
|
each one doubles the directions.
|
|
|
|
## 2. The round-trip is the product
|
|
|
|
`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must equal `doc` — anything
|
|
less silently destroys content an editor could not represent, in a document it did not author.
|
|
When losslessness and readability conflict, losslessness wins.
|
|
|
|
The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way
|
|
back yields the library's canonical spelling, and that spelling round-trips byte-identically.
|
|
|
|
Round-trip equality is a property tested over a corpus, not a claim made in prose.
|
|
|
|
## 3. Unknown input policy
|
|
|
|
- Unknown ADF node: carried opaquely — raw JSON rides a dedicated syntax in both formats and
|
|
restores to a deep-equal node. The round-trip holds for documents newer than the library.
|
|
- Unmappable foreign HTML element: error result naming the element — never a silent drop.
|
|
- Bare `@name` / `:smile:` in typed text: stays a text node. Only directives produce
|
|
mention/emoji/media nodes; resolving names to ids needs I/O, which is the consumer's job.
|
|
|
|
## 4. The flavour
|
|
|
|
- Directives, one grammar for everything markdown lacks: `:::panel info` … `:::` blocks,
|
|
`:mention[@Mikael]{id=5b10a2}` inline. Prior art: CommonMark's generic-directives proposal.
|
|
- Plain CommonMark is a subset: the flavour adds syntax, never changes CommonMark meaning.
|
|
- Tables: one header row plus plain inline cells → pipe table; anything richer → directive form.
|
|
- Identity-bearing nodes carry their ids in attributes; a document is only portable within its
|
|
site — accepted.
|
|
- The HTML dialect mirrors this: semantic elements, stable `adf-*` classes, `data-*` for what HTML
|
|
cannot express, text always escaped. No stylesheet ships.
|
|
|
|
## 5. Dependencies
|
|
|
|
`dependencies` is empty. A runtime dependency enters only through a decision entry here stating
|
|
why ~20 lines of own code cannot do the job, who maintains it, and what auditing it costs. So the
|
|
CommonMark and HTML parsers are written in this repo. `devDependencies`: few, each earning its
|
|
keep; they never reach a consumer.
|
|
|
|
## 6. The package contract
|
|
|
|
- ESM only — no CommonJS build, no dual-package hazard.
|
|
- One entrypoint: built JavaScript, `.d.ts` beside it. Do not add a TypeScript-source entrypoint —
|
|
Node refuses to type-strip under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`),
|
|
so it cannot serve an npm consumer.
|
|
- Published to public npmjs as `@larvit/atlassian-adf-converter`. Public source: the Gitea repo
|
|
goes public, LICENSE in place, before the first publish.
|
|
- Exact versions: `save-exact=true` in `.npmrc`.
|
|
|
|
## 7. Nothing about any consumer
|
|
|
|
No Jira client, no HTTP, no REST shapes, no issue keys, no actual consumer named anywhere. Design
|
|
against the README's personas.
|
|
|
|
## 8. Semver: the formats are API
|
|
|
|
The emitted markdown and HTML are contracts. After 1.0: previously-emitted output parsing
|
|
differently, or not at all, is MAJOR; new syntax while old output still round-trips is MINOR.
|
|
Pre-1.0, normal 0.x rules.
|
|
|
|
## 9. Release automation
|
|
|
|
- `package.json` version on `main` is the source of truth. CI on `main`: tests green and version
|
|
differs from npm → publish and tag `vX.Y.Z`. No bump, no deploy; the bump is each shipping PR's
|
|
deliberate semver judgment.
|
|
- 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`); actions
|
|
pin semver tags.
|
|
|
|
## 10. 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 (§9). Tests are independent,
|
|
coverage does not decline, containers are torn down after a run.
|
|
|
|
The corpus, all checked in: hand-built fixtures per node and combination; real sanitized ADF from
|
|
live Atlassian APIs; property-generated ADF trees; the CommonMark spec suite against
|
|
`markdownToAdf` and `markdownToHtml`.
|
|
|
|
## 11. Code rules
|
|
|
|
- Two-space indent, strict TypeScript, English everywhere. Alphabetical order wherever order
|
|
carries no meaning.
|
|
- Failures are values: everything returns
|
|
`Result<T>` — `{ ok: true; value } | { ok: false; error: ConvertError }` — nothing throws.
|
|
`try/catch` only wrapped tightly around a call that genuinely throws, converted to a result on
|
|
the spot.
|
|
- 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.
|
|
- Explicit over implicit; descriptive names; no catch-all files (`utils`, `helpers`, `misc`).
|
|
- Reuse before adding; the smallest sufficient diff is the benchmark; no speculative generality —
|
|
a second consumer, or it goes.
|
|
|
|
## 12. 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 decision entry here.
|
|
- Every prose comment in a diff is a review question; the default answer is delete.
|
|
- 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.
|
|
|
|
## 13. Commits and PRs
|
|
|
|
One-line commit messages and PR titles; short PR summaries. No AI-attribution markers, ever.
|
|
|
|
## 14. Non-goals
|
|
|
|
No wiki markup (§1), no network or filesystem I/O, no name→id resolution (§3), no ADF schema
|
|
validation or exported validator, no shipped CSS (§4), no streaming APIs, no performance budget —
|
|
conversions are O(n), real documents are kilobytes. A CLI is a later goal (`todo.md`), not a
|
|
non-goal.
|
|
|
|
## 15. The working loop
|
|
|
|
One unchecked `todo.md` item per session, in the smallest PR-able chunk — split a big milestone
|
|
into sub-items in `todo.md` before starting it. Per chunk:
|
|
|
|
1. Fresh worktree off updated `origin/main`; implement tests-first (§10).
|
|
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 for the commit under review already exists — re-run only what its own findings or
|
|
fixes invalidate.
|
|
3. Merge the PR (standing authorization, this repo only), check the box in `todo.md`, report,
|
|
stop. The next chunk gets a fresh session.
|
|
|
|
Ask, don't guess: any choice where what the maintainer would pick is not near-certain gets asked,
|
|
and the answer lands as a decision in this file. The confidence bar is very high — asking too
|
|
often is the accepted cost, guessing wrong is not.
|
|
|
|
Reserved for the maintainer, never the agent: changing `version` in `package.json` (a bump on
|
|
`main` publishes, §9 — every release including `0.1.0` is the maintainer's), making the repo
|
|
public, and creating the `NPM_TOKEN` secret.
|
|
|
|
A continuous loop session (`/loop`) counts as a chain of sessions: one chunk per iteration, each
|
|
iteration starting by re-reading `AGENTS.md` and `todo.md` and trusting them over anything
|
|
remembered from earlier iterations. The loop stops when only maintainer-reserved acts remain.
|