Skip to content

Decision log

Format per entry: ### D-nnn Title / Date / Status (accepted | amended-by D-xxx | superseded-by D-xxx | pending) / Context / Decision / Consequences / Spec refs / Tests. Append-only. Every test that encodes a decision cites its D-id in its docstring; every commit touching contract semantics references a D-id. Reviewed at every checkpoint (CP0..CP5). §2 of the spec is never reopened except by the owner (D-023).

  • Date: 2026-09-29 · Status: accepted
  • Context: §12 says “exact MBR delta” and “overpayment accepted”; §20 says “equal”; ABI signatures lack the payment argument; some methods create boxes only sometimes.
  • Decision: every method that MAY create a box or asset takes a leading pay: gtxn.PaymentTransaction (always present; amount MAY be 0). Contract reads pre = app.min_balance before creations and post after (acct_params_get AcctMinBalance), asserts pay.sender == Txn.sender, pay.receiver == app, close_remainder_to == rekey_to == ZeroAddress, pay.amount >= post - pre. Overpayment kept (no refund in M1, §22).
  • Consequences: M1 methods with pay: publish, vote_article, claim_reputation, add_field. Without: claim_article, new_version, set_status, set_governance. The payment sits at group index i-1 so one payment cannot serve two calls.
  • Spec refs: §12, §13, §17 inv 22, §20 · Tests: COST-01..07, INV22-01/02
  • Date: 2026-09-29 · Status: accepted
  • Context: §7.3 rows 6-8 need flag/resolve_dispute (M2); disputed is unreachable on LocalNet in M1.
  • Decision: M1 LocalNet matrix = set_status over all 25 pairs x {author, governance, stranger}; only the 5 author rows succeed; disputed as source or target always fails via set_status. Disputed-gated negatives (new_version, vote_article, claim_reputation, claim_article) are offline tests seeding status = 4. Rows 6-8 and their LocalNet variants are @pytest.mark.m2 (xfail) and listed in M1_REPORT.md. No test-only entry path on the immutable contract.
  • Spec refs: §7.3, §7.4, §19, §20, §21 · Tests: ST-01, ST-02, ST-04 (m2), VER-04, VOTE-05, CLAIM-04

D-003 Creation, governance initialisation, taxonomy seeding

Section titled “D-003 Creation, governance initialisation, taxonomy seeding”
  • Date: 2026-09-29 · Status: accepted (amended: global schema)
  • Decision: create(governance: address) declared @arc4.abimethod(create="require"), no bare create; asserts governance != ZeroAddress; stores globals governance (bytes) and article_seq_next = 1 (uint); emits GovernanceChanged(1, ZeroAddress, governance, ts). Global schema = 1 uint + 1 bytes (PC-03 said 0 uints; the sequence counter of D-023 needs one). Deploy script seeds /spec/taxonomy.json with groups of <= 8 (pay, add_field) pairs signed by the Falcon-1024 governance account, fees per D-027. No update/delete paths.
  • Spec refs: §4, §11, §12.1, §19, §20 · Tests: test_create_governance, TAX-05/07, SEC-04..08

D-004 claim_reputation with nothing grantable today

Section titled “D-004 claim_reputation with nothing grantable today”
  • Date: 2026-09-29 · Status: accepted
  • Decision: fail when granted_primary == 0 (“cap exhausted today”), in addition to failing when claimable == 0. Every successful claim therefore emits >= 1 ReputationChanged.
  • Spec refs: §10.2, §11, §20 · Tests: CAP-06, CLAIM-02
  • Date: 2026-09-29 · Status: accepted
  • Decision: allowed in preprint, under_review, final, disputed; explicit assert-fail in retracted (otherwise fails opaquely because manager is ZeroAddress). Explicit opt-in assert for a clear error. §7.4 gains a claim_article column; set_status follows §7.3.
  • Spec refs: §7.1, §7.4, §12 · Tests: ASA-04/05/13

D-006 Timestamps, updated_at, reputation-box writes

Section titled “D-006 Timestamps, updated_at, reputation-box writes”
  • Date: 2026-09-29 · Status: accepted
  • Decision: all ts, created_at, updated_at, epoch_day derive from the single Global.latest_timestamp of the call. article.updated_at := ts in every method that writes the article box (publish: equals created_at). A reputation box is written only on creation (rep 0, updated_at = ts, cap_day = epoch_day, cap_today = 0) and when granted > 0. Zero grants never touch a reputation box, so event replay stays exact.
  • Spec refs: §2 Decay, §6.4, §10.2, §11, §20 · Tests: REP-04, EVT-03, VOTE-06
  • Date: 2026-09-29 · Status: accepted (amended by D-024: reserve = CID digest; amended by D-033: Revoked freeze)
  • Context: go-algorand replaces each non-zero address with the transaction’s value; an omitted address becomes ZeroAddress irreversibly; an already-zero address silently stays zero.
  • Decision: every inner AssetConfig passes manager, reserve, freeze and clawback explicitly. Held (claim_article step 4) = manager app, reserve digest(current_cid), freeze Zero, clawback Zero. Version bump (new_version) = manager app, reserve digest(new_cid), freeze (Zero if claimed else app), clawback Zero. Revoked: see D-033. No self-unfreeze is needed (creator holding is never frozen by default_frozen).
  • Spec refs: §7.1, §20 · Tests: ASA-01/02/03, VER-01 (inner fields), claimed x unclaimed variants
  • Date: 2026-09-29 · Status: superseded-by D-024
  • Original: on-chain base32 ipfs:// url with op-up. Superseded: the url is the constant ARC-19 template; no base32 on-chain, no ensure_budget, no extra argument. metadata_hash stays empty.
  • Date: 2026-09-29 · Status: accepted
  • Decision: contract enforces (article_type == 3) <=> (parent_article != 0); parent must exist (article box present), any type, any status; caller passes the parent box reference. notes/dataset/replication use parent = 0 and cite targets in references[].
  • Spec refs: §6.4, §7.5, §14.2, §20 · Tests: LC-08, SEC-03
  • Date: 2026-09-29 · Status: accepted (amended-by D-047)
  • Decision: field_id != 0; box t+id must not exist (append-only, no rename); 1 <= len(name) <= 64; box value = raw UTF-8 name; no structural check on the id; payment per D-001.
  • Spec refs: §4, §20 · Tests: TAX-04, TAX-06

D-011 Reputation boxes created by claim_reputation

Section titled “D-011 Reputation boxes created by claim_reputation”
  • Date: 2026-09-29 · Status: accepted
  • Decision: create the primary box if missing; if secondary_field != 0 create the secondary box if missing, regardless of the granted amounts. MBR delta depends only on box existence (client-computable).
  • Spec refs: §10.2, §10.5, §13.3 · Tests: CLAIM-01/05, COST-03

D-012 set_governance guards and PQ-compliance check

Section titled “D-012 set_governance guards and PQ-compliance check”
  • Date: 2026-09-29 · Status: accepted (amended: SDK check primary)
  • Decision: sender == governance; new != ZeroAddress; new != current; emits GovernanceChanged(prev, new, ts). PQ-ness is asserted off-chain, primarily with not is_ed25519_point(decode_address(addr)) (py-algorand-sdk); docker exec ... algokey pq check-address is an optional extra (binary presence in the algod image unverified). §12.1 wording fixed: check-address exit 0 == compliant.
  • Spec refs: §12, §12.1, §20 · Tests: SEC-07, GOV-01, GOV-04

D-013 Fees, inner counts, invariant-22 sequence

Section titled “D-013 Fees, inner counts, invariant-22 sequence”
  • Date: 2026-09-29 · Status: accepted (amended: new_version 1 inner; app funded with exactly 100 000)
  • Decision: every inner txn fee = 0; outer fee = (1 + n_inner) x min_fee for Ed25519 senders, (3 + n_inner) x min_fee for pqsig senders (D-027). Inner counts: publish 1, claim_article 4, new_version 1 (ARC-19 reserve update), set_status->retracted 1. The application account is funded with exactly 100 000 µAlgo at creation, so invariant 22 is asserted as spendable == 0 before and after every step. Sequence INV22-01: add_field x3 (PQ), publish, claim_article, new_version, set_status x2, vote_article x7, claim_reputation x2 across two epoch days, set_governance PQ->PQ2->PQ, author close-out, second publish + retraction, failing claim_article and claim_reputation. INV22-02: overpayment grows spendable by exactly the surplus.
  • Spec refs: §13, §17 inv 22, §20 · Tests: INV22-01/02, COST-05/06
  • Date: 2026-09-29 · Status: accepted (owner)
  • Decision: the file does not exist; M1 writes a skeleton from §14.5 (canonical params, hardened Kubo command, Path B checklist, CID -> 36 bytes, “TODO M4” markers for Pinata CAR and gateway checks); M4 completes it.
  • Spec refs: §14.4, §14.5, §23
  • Date: 2026-09-29 · Status: accepted
  • Decision: status 1, status_before_dispute 0, version 1, prev_cid 36 x 0x00, review_seq_next = comment_seq_next = 1, dispute_round 1, round_flag_weight 0, vote_total = vote_count = 0, author_count 1, invited_count 0 (submitter not counted; M2 invite_coauthor increments; max 65 535), claimed false, created_at = updated_at = ts, asa_id = created asset; submitter co-author box invited_at = accepted_at = ts, accepted true, claimed_total 0. (TD matrix row M1-LC-01 “invited_count = 1” is not adopted.)
  • Spec refs: §6.4, §7.1, §7.6 · Tests: LC-01, LC-04

D-016 Field widths and saturating arithmetic

Section titled “D-016 Field widths and saturating arithmetic”
  • Date: 2026-09-29 · Status: accepted
  • Decision: keep §6.4 widths; compute in uint64, narrow on store with arc4.UInt16/UInt32 (fails on overflow); static assert REP_CAP_DAY <= 65_535; cap_remaining is saturating; new_version fails at version 65 535.
  • Spec refs: §5, §6.4 · Tests: test_versions (overflow), CAP tests
  • Date: 2026-09-29 · Status: accepted
  • Decision: bytes36 = byte[36] (arc4.StaticArray[arc4.Byte, Literal[36]]); bools = arc4.Bool; Python parameter article_type (selectors use types only). Event structs are constructed explicitly with schema_version = arc4.UInt8(1); no struct field defaults.
  • Spec refs: §6.4, §11, §12 · Tests: EVT-02 (static)
  • Date: 2026-09-29 · Status: accepted
  • Decision: M1_REPORT lists per-method box/asset/account references, including absent boxes that must be referenced (voter’s au and p in vote_article; parent box in amendment publish; ASA in claim_article, new_version, retraction). Layer C asserts the populated list; txn.Access is not relied on.
  • Spec refs: §6.2, §21 C · Tests: SIM-01/03
  • Date: 2026-09-29 · Status: accepted
  • Decision: rollover in offline tests with patched latest_timestamp; on LocalNet via algod.set_timestamp_offset (py-algorand-sdk, dev mode) with a cumulative, session-scoped offset that only ever increases and is never reset; semantics probed in test_00; fresh LocalNet per full run.
  • Spec refs: §10.2, §20 · Tests: CAP-01/02 (A), CAP-03 (B)

D-020 M2 pre-decisions (recorded now, implemented in M2)

Section titled “D-020 M2 pre-decisions (recorded now, implemented in M2)”
  • Date: 2026-09-29 · Status: accepted
  • Flagged gains new_status: uint8; resolve_comment follows the vote_article column; comment() in retracted still creates the commenter’s reputation box; reviews stay allowed in disputed; accept_coauthorship blocked once final; mentions is inline arc4.DynamicArray[arc4.Address] (<= 5; Commented with 5 mentions = 278 B); round_flag_weight keeps accumulating while disputed.
  • Spec refs: §7.4, §8.3, §9, §11, §22
  • Date: 2026-09-29 · Status: accepted
  • Decision: only consecutive CIDs must differ (A->B->A allowed); version overflow fails (D-016).
  • Spec refs: §7.2, §17 inv 7 · Tests: VER-02, test_versions
  • Date: 2026-09-29 · Status: accepted
  • submitter = article.author (editing/status/invite rights); accepted author = submitter or co-author with accepted = true (claim rights, self-action rules); commenter = comment.author; reviewer = review.reviewer. ReputationClaimed.author means the claimant.

D-023 article_id is a contract-assigned sequence (OWNER decision; reopens §2 “Article identity”)

Section titled “D-023 article_id is a contract-assigned sequence (OWNER decision; reopens §2 “Article identity”)”
  • Date: 2026-09-29 · Status: accepted (owner)
  • Context: box names must be known at signing time; the id of an inner-created ASA is the ledger txn counter + 1 and is unpredictable on public networks (AVM-01).
  • Decision: article_id = global sequence article_seq_next (starts at 1), NOT the ASA id. Boxes are keyed by the sequence (a+u64(seq), au+u64(seq)+addr, va+u64(seq)+addr; M2 keys likewise); the article box stores asa_id: uint64 (appended field, 192 B, MBR 82 900 µAlgo, publish deposit 212 200); Published carries article_id and asa_id; the ASA is still created (ARC-71). All ABI methods take article_id = sequence; methods needing the asset read asa_id from the box.
  • Consequences: §2 table text is not edited; the amendment is recorded in the spec copy changelog. §6.2, §6.4, §11, §12, §13.3, §20 amended in the copy.
  • Spec refs: §2, §6.2, §6.4, §11, §12, §20 · Tests: LC-01, ASA-14, KEY-01

D-024 ARC-19 from M1 (OWNER decision; supersedes D-008)

Section titled “D-024 ARC-19 from M1 (OWNER decision; supersedes D-008)”
  • Date: 2026-09-29 · Status: accepted (owner)
  • Context: the ASA url is immutable after creation, so ARC-19 must be chosen at creation.
  • Decision: url = "template-ipfs://{ipfscid:1:dag-pb:reserve:sha2-256}" (51 bytes, constant); reserve = Account(cid.bytes[4:36]) (the 32-byte sha2-256 digest of the current CID as an address); new_version updates reserve through one inner AssetConfig that re-passes manager/freeze/clawback explicitly; no base32 on-chain; metadata_hash empty. ARC-71 allows reserve = ARC-19 metadata. ARC-19 is marked superseded-by ARC-89; the article box remains the authority (revisit ARC-89 in M5).
  • Spec refs: §7.1, §7.2, §22 · Tests: ASA-01/02/03, VER-01 (reserve == digest), tests/ref/cid.py (ARC-19 rebuild)

D-025 LocalNet runs locally (OWNER decision)

Section titled “D-025 LocalNet runs locally (OWNER decision)”
  • Date: 2026-09-29 · Status: accepted (owner)
  • Decision: enable SVM in the Gigabyte BIOS, install WSL 2 (wsl --install --no-distribution, --web-download fallback) and Docker Desktop (WSL 2 backend, auto-update off, version recorded). GitHub Actions / remote LocalNet through ALGOD_SERVER/ALGOD_TOKEN/KMD_* is an optional later backup, never a prerequisite. Offline tests and compilation must work before Docker exists (Phase 0 Track 1, Phases 1-4).
  • Host facts: Windows 10 Pro 22H2 19045, Ryzen 5 2600, 16 GB, ~32 GB free, virtualization disabled in firmware at start, WSL absent, Python 3.14.5 + 3.8.5, Node 26.5.1, git 2.54.
  • Spec refs: §3, §21

D-026 Storage provider: Pinata replaces web3.storage (OWNER decision)

Section titled “D-026 Storage provider: Pinata replaces web3.storage (OWNER decision)”
  • Date: 2026-09-29 · Status: accepted (owner)
  • Decision: web3.storage/Storacha is decommissioned (writes disabled 2026-05-15). Spec copy §3/§14.4 reference Pinata (CAR upload: paid plan, public network, single root, async) plus a second provider to be chosen in M5 (Filebase CAR import or Filecoin Pin, to verify). Affects M4/M5 only.
  • Spec refs: §3, §14.4, §22
  • Date: 2026-09-29 · Status: accepted (amended-by D-045)
  • Context: spec §12.1/§13.2 describe a per-byte surcharge; go-algorand v42 prices a Falcon-1024 signature as a flat +2 000 000 usage; per-byte pricing only applies to oversize note/args/programs.
  • Decision: usage = 1e6 per outer txn (+2e6 if pqsig-signed) + 1e6 per inner txn; fee = ceil(min_fee x usage / 1e6), min_fee read from algod. Flow for every PQ-signed group: compute expected usage -> sign with Falcon -> simulate the SIGNED group -> assert txn-groups[0]["group-usage"] == expected -> send. PQ senders use static_fee; never rely on cover_app_call_inner_transaction_fees alone for them; never mutate the fee after grouping (re-sign instead). Spec copy §12.1/§13.2 corrected.
  • Spec refs: §12.1, §13.2, §20 · Tests: GOV-02/03, SIM-04, tests/ref/pqfee.py

D-028 Version pins and bootstrap smoke test

Section titled “D-028 Version pins and bootstrap smoke test”
  • Date: 2026-09-29 · Status: accepted (amended-by D-046)
  • Pins: AlgoKit CLI 2.10.2 (pipx); Python 3.12.10; puyapy 5.10.1 + algorand-python 4.0.0; algorand-python-testing 1.1.0; algokit-utils 4.2.3; algokit-client-generator 2.2.0; py-algorand-sdk 2.12.0; algorand-falcon 0.1.0; template algokit-python-template 1.8.1 (_commit in .copier-answers.yml); LocalNet algorand/algod:5.0.2-stable@sha256:3a2c9266b61e414e770aa69bd3d3867f8b350177c7af31df251db36ef70405a4; Node 26.5.1 for the M1 CID script, Node 24 LTS for M3/M4; ipfs-unixfs-importer 17.1.1, multiformats 14.0.5, blockstore-core 7.0.1; python-dotenv, hypothesis, Poetry and every dev tool pinned exactly after the first poetry lock; poetry.lock committed. puyapy --target-avm-version left at the compiler default, value read at bootstrap and recorded here: <fill>. Never use algokit-utils 5 betas, CLI 2.11 beta or algorand-python-testing 1.2 betas.
  • Smoke test (2026-09-29): PASSED for the template hello-world (puyapy 5.10.1 + algorand-python 4.0.0 compile, ARC-56 + typed client generated, algorand-python-testing 1.1.0 test green on Python 3.12.14); emulator feature probe in tests/unit/test_smoke_emulator.py. Original wording: template hello-world builds and its offline test runs; tests/unit/test_smoke_emulator.py (BoxMap(Bytes, arc4.Struct), itxn.AssetConfig created_asset, arc4.emit, op.sqrt, AcctParamsGet, gtxn payment arg, patched latest_timestamp). Fallback if it fails: puyapy 5.9.0 + algorand-python 3.5.1 (re-lock, log here); last resort Layer A restricted to pure subroutines.
  • Spec refs: §3, §23.2
  • Date: 2026-09-29 · Status: accepted
  • Decision: algokit localnet start --name ariadne is run once to generate docker-compose.yml, algod_config.json and algod_network_template.json; they are copied to /localnet/, the algod image is pinned by digest, indexer/conduit/postgres services are removed for M1 (algod + kmd only; re-added pinned in M3: indexer 3.10.0 / conduit 1.10.0 by observed digests), ports 4001/4002, token ax64, DevMode true. Operated with docker compose -f localnet/docker-compose.yml -p ariadne up -d --wait | down -v. Never algokit localnet reset. First LocalNet test asserts /v2/status last-version ends in 268b63433a907455d439995bf916f6b296018f4f and a pqsig txn is accepted. Docker Desktop version recorded here: <fill>.
  • Spec refs: §3 · Tests: test_00_node, test_00_pq_spike
  • Date: 2026-09-29 · Status: accepted
  • Decision: asset_name = "Ariadne Article", unit_name = "ARIA" (constants, §7.1 literal). Putting the sequence in the name (AVM-01 option A, "Ariadne #<seq>") stays a §22 open question, to be revisited only if the measured publish budget stays <= 600 ops with an on-chain itoa.
  • Spec refs: §7.1, §22 · Tests: ASA-01

D-031 Explicit Bytes box keys and arc4.Struct values

Section titled “D-031 Explicit Bytes box keys and arc4.Struct values”
  • Date: 2026-09-29 · Status: accepted
  • Decision: all BoxMaps are BoxMap(Bytes, <arc4.Struct>, key_prefix=b"<prefix>") with keys built by @subroutine builders (uint64 via op.itob, uint16 via arc4.UInt16(x).bytes, addresses via .bytes); tuple-typed keys are undocumented in Puya. §6.2 byte layout is normative and unit-tested (11 builders: 3, 9, 42, 17, 42, 17, 42, 50, 50, 43, 35). Field box value = raw UTF-8 bytes. Box values are arc4.Struct so ARC-4 tuple sizes are normative (a 192, au 25, va 24, p 22).
  • Spec refs: §6.2, §6.4, §13.3 · Tests: KEY-01/02/03, tests/static/test_arc56.py

D-032 Publish box references on public networks

Section titled “D-032 Publish box references on public networks”
  • Date: 2026-09-29 · Status: accepted
  • Decision: clients read article_seq_next right before signing publish and build the a/au box names from it; on TestNet/MainNet they reference seq and seq+1 (<= 7 refs incl. parent) and retry on failure. Tests use populate_app_call_resources; LocalNet dev mode is sequential so no collision occurs.
  • Spec refs: §12, §21.C · Tests: SIM-01/03 (footprint recorded)

D-033 Revoked AssetConfig parameter set (amends D-007)

Section titled “D-033 Revoked AssetConfig parameter set (amends D-007)”
  • Date: 2026-09-29 · Status: accepted
  • Decision: on every transition to retracted the inner AssetConfig passes manager = Zero, reserve = digest(current_cid), freeze = Zero unconditionally, clawback = Zero. One code path for claimed and unclaimed articles; the ASA becomes immobile forever (claim_article is forbidden in retracted, D-005). go-algorand silently ignores a new value for an already-zero freeze, which is verified at the spike. Tests assert all four addresses after retraction in claimed x unclaimed variants.
  • Spec refs: §7.1, §20 · Tests: ASA-03 (x6)
  • Date: 2026-09-29 · Status: accepted
  • Decision: /spec/taxonomy.json = 42 OECD FOS subfields (6 areas: 7+11+5+5+9+5), field_id = area << 8 | subfield, source “OECD Frascati Manual 2015 Table 2.2”; 0x0303 = Medical and health sciences / Health sciences with notes “includes sport and fitness sciences” (§4 example reworded in the spec copy); 0x0304 Medical biotechnology with alias Health biotechnology. Area ids are not registered on-chain; 0 reserved. Loader test enforces uniqueness, area byte 1-6, subfield byte >= 1, names 1-64 bytes.
  • Spec refs: §4, §14.2 · Tests: TAX-07, tests/static/test_taxonomy.py

D-035 §14.5 completions and line-ending policy

Section titled “D-035 §14.5 completions and line-ending policy”
  • Date: 2026-09-29 · Status: accepted
  • Decision: canonical UnixFS parameters are completed with: max 174 links per file node, directory HAMT threshold 256 KiB estimated by links-bytes, fanout 256 (8 bits), links-first field order, no mode/mtime, hidden files excluded, entries sorted by name, root = the added directory itself (never -w). compute-cid.mjs passes every importer option explicitly (wrapWithDirectory: true with paths relative to the directory). Hardened Kubo reference command adds --max-file-links=174 --max-directory-links=0 --max-hamt-fanout=256 on a fresh repo without an import profile (Kubo 0.43.1, optional cross-check). Repo core.autocrlf = false, root .gitattributes * text=auto eol=lf, spec/fixtures/** -text; a test asserts no \r in the fixture index.md.
  • Spec refs: §14.5, §23.3 · Tests: CID-08, tests/static/test_fixture_cid.py

D-036 networks.json governance field and deploy rewrite

Section titled “D-036 networks.json governance field and deploy rewrite”
  • Date: 2026-09-29 · Status: accepted
  • Decision: each network entry gains governance (address string). The deploy script rewrites the localnet entry on every deploy (genesisId and genesisHash from algod.versions(), appId, governance) because the LocalNet genesis changes on every reset. MainNet/TestNet entries carry the known genesis hashes, appId 0, enabled false until M4.
  • Spec refs: §3.2, §12.1, §15 · Tests: GOV-04 (static), deploy test
  • Date: 2026-09-29 · Status: accepted
  • Decision: neither algokit-utils 4.2.3 nor the typed clients decode ARC-28 events. tests/ref/events.py builds the prefix map from the ARC-56 events list (sha512_256(signature)[:4]), decodes with algosdk.abi.ABIType tuples and skips the ARC-4 return log 0x151f7c75. tests/ref/replay.py rebuilds article and reputation projections from events and is the projection spec for the M3 indexer (which uses algokit-subscriber). Spec copy §21 reworded accordingly.
  • Spec refs: §11, §15, §20, §21 · Tests: EVT-02/03, REP-04
  • Date: 2026-09-29 · Status: accepted
  • Context: algokit-utils factory.deploy() resolves existing apps through the indexer (creator lookup); M1 LocalNet has no indexer (D-029).
  • Decision: the deploy script calls the typed factory’s explicit create method (factory.send.create.create(args=(governance,)), name per the generated client), funds the app account with exactly 100 000 µAlgo, seeds the taxonomy idempotently (skips fields whose t box exists) and persists appId/genesisHash/governance in spec/networks.json; a later deploy reuses the app when the stored genesisHash equals the node’s and application_info(appId) succeeds. No on_update/on_schema_break policy is involved; the contract is immutable anyway. TestNet/MainNet governance keys come from algokey pq generate outside the repo.
  • Spec refs: §12.1, §23 · Tests: idempotence step of the end-to-end run, TAX-05/07
  • Date: 2026-09-29 · Status: accepted
  • Decision: Layer A = tests/unit (algorand-python-testing, seeded boxes incl. status = disputed, patched latest_timestamp); Layer S = tests/static (ARC-56 JSON: no Update/Delete actions, bareActions.call == [], events == §11 amended, struct sizes == ref/mbr.py; taxonomy; fixture); Layer B = tests/localnet (typed client on the pinned LocalNet); Layer C runs inside every B send: simulate with unnamed resources, assert unnamed-resources-accessed empty after population, app-budget-consumed <= 600 (700 is the consensus hard limit), box refs <= 8, log count <= 32 and bytes <= 1 024, group-usage recorded; then send. Layer A limits documented in tests/README.md: the emulator does not compute MBR, does not apply asset reconfig/transfer/freeze, cannot enforce opt-in; A asserts submitted inner-txn fields (ctx.txn.last_group.get_itxn_group(i)); MBR amounts, ARC-71 authority, opt-in and invariant 22 are Layer B only. CID length 35/37 is tested in A with raw bytes and in B with a hand-built app_args transaction.
  • Spec refs: §21 · Tests: SIM-01..04, CID-06, tests/README.md
  • Date: 2026-09-29 · Status: accepted
  • Decision: this file is append-only; entries use the header format; tests encoding a decision cite the D-id in their docstring; commits touching contract semantics reference a D-id; the file is reviewed at every checkpoint; superseded entries keep their text with a Status pointer. The root ARIADNE_SPEC.md is never edited; spec/ARIADNE_SPEC.md carries <span class="spec-edit">[EDIT D-xxx]</span> tags and a changelog.
  • Spec refs: §23.6

D-041 PQ signing path (filled after the Phase 5 spike)

Section titled “D-041 PQ signing path (filled after the Phase 5 spike)”
  • Date: 2026-09-29 · Status: accepted (outcome in D-053)
  • Candidates: (1) algokit-utils 4.2.3 composer with Falcon1024AlgorandSigner registered via set_signer and static_fee; (2) raw py-algorand-sdk AtomicTransactionComposer; (3) docker exec ... algokey pq sign from pytest. Record: signer class name found in algosdk.signer, whether unsigned simulate omits the +2e6 usage, static_fee vs cover_app_call_inner_transaction_fees interplay, set_timestamp_offset semantics, presence of algokey in the image. An Ed25519 governance account for the bulk suite is not a valid M1 end state (§20).
  • Spec refs: §12.1, §13.2, §20 · Tests: test_00_pq_spike (GOV-01..03, SIM-04)
  • Date: 2026-09-29 · Status: accepted
  • Context: ARC-71 says it extends ARC-3/ARC-69; a bare ASA with an ARC-19 template url is neither, so wallets and explorers show a plain ASA. ARC-3 would force a metadata JSON into the article directory; ARC-69 would need a note on the creation inner txn. ARC-19 and ARC-69 are both marked superseded-by ARC-89.
  • Decision: no ARC-3/ARC-69 metadata in M1. The ASA carries only the ARC-71 parameters and the ARC-19 template url; the article box is the authority (§2). Revisit together with ARC-89 in M5.
  • Spec refs: §2, §7.1, §22

D-043 Governance key custody and generation

Section titled “D-043 Governance key custody and generation”
  • Date: 2026-09-29 · Status: accepted (owner default)
  • Decision: LocalNet uses an ephemeral Falcon-1024 key per test run (deterministic seed sha256(“ariadne-localnet-governance”), never reused elsewhere). TestNet/MainNet governance keys are generated with algokey pq generate outside the repo (algod container or WSL); the mnemonic is backed up offline by the owner; only the address enters spec/networks.json and this file. Before M4 a test proves algokey <-> py-algorand-sdk (mnemonic.to_pq_seed + algorand-falcon) address interoperability. The “operator” of §12.1 is the owner (single signer) until native PQ multisig exists.
  • Spec refs: §12.1, §3.2 · Tests: GOV-01, GOV-04, interop test (M4 gate)

D-044 Retraction forfeits unclaimed article-vote reputation

Section titled “D-044 Retraction forfeits unclaimed article-vote reputation”
  • Date: 2026-09-29 · Status: accepted (documented behaviour, no change)
  • Context: §7.4 marks claim_reputation as not allowed in retracted and retracted is terminal (§7.3), so votes accumulated before retraction can never be materialised.
  • Decision: keep the spec behaviour. Document it here and in the web app (M4): the retract action shows the forfeited claimable amount before signing; the indexer profile shows it as “forfeited” rather than “claimable”.
  • Spec refs: §7.3, §7.4, §10.2, §15, §16

D-045 Fee sizing with a scheme-only placeholder pqsig (amends D-027)

Section titled “D-045 Fee sizing with a scheme-only placeholder pqsig (amends D-027)”
  • Date: 2026-09-29 · Status: accepted (to verify at the Phase 5 spike)
  • Context: ledger/simulation/simulator.go (v5.0.2-stable) documents two placeholder PQ signature types: “Scheme-only placeholder: Scheme set, PublicKey empty, Signature empty, for fee surcharge calculation” and “Full placeholder: Scheme, Salt and PublicKey set”. A completely blank pqsig is proxy-signed with Ed25519 by the simulator and therefore omits the +2 000 000 usage.
  • Decision: tests/ref/pqfee.py gets simulate_usage(algod, txns, pq_senders) that wraps each unsigned transaction of a PQ sender in a SignedTransaction carrying PQSig(scheme=b"f1") and calls algod simulate with allow-empty-signatures to read group-usage BEFORE Falcon signing (algokit-utils 4.2.3 cannot attach a placeholder). static_fee = ceil(min_fee * usage / 1e6), cross-checked against the deterministic formula of D-027. After signing, the signed group is simulated again and the same usage asserted (D-027 flow). Recorded outcome of the spike: .
  • Spec refs: §12.1, §13.2, §20 · Tests: GOV-02, SIM-04, test_00_pq_spike (d)

D-046 Python 3.12 provisioned with uv (amends D-028)

Section titled “D-046 Python 3.12 provisioned with uv (amends D-028)”
  • Date: 2026-09-29 · Status: accepted
  • Context: the host had only Python 3.14.5 and 3.8.5; installing 3.12 from python.org needs an owner action. puyapy/algorand-python require >=3.12,<4 and their CI covers 3.12/3.13 only, so 3.14 was not used.
  • Decision: uv (installed with pipx on Python 3.14) provisions a python-build-standalone CPython 3.12.x under %APPDATA%\uv\python\ without administrator rights. AlgoKit CLI and Poetry are installed with pipx on that interpreter; Poetry uses it for the project venv (poetry env use <path>). Recorded on 2026-09-29: pipx 1.17.7, uv 0.12.20, CPython 3.12.14 (%APPDATA%\uv\python\cpython-3.12.14-windows-x86_64-none\python.exe), AlgoKit 2.10.2, Poetry 2.5.1, pipx bin dir %USERPROFILE%.local\bin. A python.org 3.12.x installer remains an equivalent alternative.
  • Spec refs: §3, §23.2

D-047 Field name length limit is 96 bytes (amends D-010)

Section titled “D-047 Field name length limit is 96 bytes (amends D-010)”
  • Date: 2026-09-29 · Status: accepted
  • Context: D-010 proposed 1 <= len(name) <= 64, but the OECD name “Electrical engineering, electronic engineering, information engineering” (2.2, 0x0202) is 71 bytes. Renaming an OECD field would break the “names follow the 2007/2015 wording” rule of D-034.
  • Decision: MAX_FIELD_NAME = 96 bytes (UTF-8). Field box MBR for the longest name = 2 500 + 400 x (3 + 71) = 32 100 µAlgo; FieldAdded log = 11 + 71 = 82 bytes; both far inside limits. spec/taxonomy.schema.json uses maxLength 96.
  • Spec refs: §4, §12, §13.2 · Tests: TAX-04, TAX-06, tests/static/test_taxonomy.py
Tool / package Pin Notes
AlgoKit CLI 2.10.2 pipx on Python 3.12.14 (D-046); never 2.11.0-beta
Python 3.12.14 (uv-managed) D-046
uv / pipx / Poetry 0.12.20 / 1.17.7 / 2.5.1 tooling only
dev tools (poetry.lock, 2026-09-29) python-dotenv 1.2.3, hypothesis 6.168.3, black 26.5.1, ruff 0.16.9, mypy 2.1.0 (pinned by puyapy), pytest 9.1.1, pytest-cov 7.1.0, pip-audit 2.10.1, pre-commit 4.6.2 exact pins in pyproject.toml
puyapy 5.10.1 requires algorand-python 4.0.x
algorand-python 4.0.0
algorand-python-testing 1.1.0 compatibility with algopy 4.0.0 checked by the bootstrap smoke test (D-028); never 1.2.0-beta
algokit-utils (py) 4.2.3 never 4.2.4-beta / 5.0.0-beta
algokit-client-generator (py) 2.2.0
py-algorand-sdk 2.12.0 Falcon1024AlgorandSigner in algosdk/signer.py
algorand-falcon 0.1.0 win_amd64 wheel
algokit-python-template 1.8.1 _commit in projects/contracts/.copier-answers.yml
puyapy –target-avm-version 11 (compiler default; choices 10-13) read at bootstrap 2026-09-29; unchanged (no AVM 12/13 opcode needed in M1)
LocalNet algod image algorand/algod:5.0.2-stable@sha256:3a2c9266b61e414e770aa69bd3d3867f8b350177c7af31df251db36ef70405a4 D-029; consensus V42 (268b63433a907455d439995bf916f6b296018f4f)
Docker Desktop 4.93.0 (engine 29.8.1, Compose v5.5.1, WSL 2.7.14) auto-update off; recorded 2026-09-29
Node (M1 CID script) 26.5.1 (host) Node 24 LTS for M3/M4
ipfs-unixfs-importer / multiformats / blockstore-core 17.1.1 / 14.0.5 / 7.0.1 spec/fixtures/package.json + package-lock.json
M3 (package.json, 2026-09-30) Node 26.5.1 (engines >= 24) with node:sqlite (SQLite 3.53.3) · algosdk 3.8.0 · @algorandfoundation/algokit-utils 9.2.2 · @algorandfoundation/algokit-subscriber 3.4.0 · multiformats 14.0.5 · ipfs-unixfs-importer 17.1.1 · ipfs-unixfs-exporter 16.2.3 · blockstore-core 7.0.1 · @ipld/car 5.4.7 · @ipld/dag-pb 4.2.0 · typescript 5.9.3 · @types/node 26.6.3 · prettier 3.9.9 D-075; better-sqlite3 dropped
M4 (for reference) algosdk 3.8.0 · @algorandfoundation/algokit-utils 9.2.2 · algokit-subscriber 3.4.0 · algokit-client-generator 6.0.1 · next 16.3.6 · react 19.3.0 · typescript 5.9.3 · tailwindcss 4.3.3 · @txnlab/use-wallet 5.0.1 · better-sqlite3 13.0.3 · react-markdown 10.1.0 · remark-math 6.0.0 · rehype-katex 7.0.1 · rehype-sanitize 6.0.0 · katex 0.16.47 · @ipld/car 5.4.7 · pinata 2.5.6 · kubo 0.43.1 re-verify at the M3 scaffold; web3.storage removed (D-026)

Windows 10 Pro 22H2 (build 19045), AMD Ryzen 5 2600, 16 GB RAM, ~32 GB free on C:. CPU virtualization disabled in firmware at start (BIOS SVM to enable), WSL not installed, no Docker. Python 3.14.5 (py default) and 3.8.5 (python on PATH) pre-existing; Node 26.5.1, npm 11.17.0, git 2.54. The project folder held only ARIADNE_SPEC.md and was not a git repository.

  • Refund of MBR overpayment (M1 keeps it, D-001).
  • FLAG_THRESHOLD absolute vs relative (M2).
  • Whether disputed should also block submit_review (closed for M2 by D-020: allowed).
  • Multi-author allocation rule (§10.2) — closed on 2026-09-29 by the owner: equal split, remainder to the submitter (D-054).
  • Whether a review should stop earning once its reviewer becomes an accepted co-author (D-066, owner).
  • GRANT_COMMENT = 1/5 grants nothing for voters of weight below 5; tune with real activity (D-057, owner).
  • Batch claiming across articles (M5; at most 5 articles per app call because of the 1 024-byte log budget).
  • MainNet articles referencing TestNet ones (no; networks independent).
  • Second pinning provider (M5): Filebase CAR import (unverified) / Filecoin Pin / self-hosted Kubo; Path A default = Pinata paid plan.
  • Timing of the governance migration to native PQ multisig (Algorand roadmap: end of 2026).
  • asset_name carrying the sequence (D-030): only if the measured publish budget stays <= 600 ops with itoa.
  • Follows that work on every device (D-104 keeps them per browser): contract event, contract box or signed list; to decide before the MainNet contract. Decided: contract event, D-109.
  • ARC-19 vs ARC-89 for the current-version pointer (M5).
  • Hosting for indexer / IPFS gateway / web (decide during M2, provision in M3; default one Linux VPS).
  • Domain and EU trademark check (owner task, preamble of the spec).
  • Constants tuning (§5) implies a redeploy of the immutable app: TestNet redeploy cadence to plan in M4.

D-048 Emulator (algorand-python-testing 1.1.0) limits observed at CP2 (amends D-039)

Section titled “D-048 Emulator (algorand-python-testing 1.1.0) limits observed at CP2 (amends D-039)”
  • Date: 2026-09-29 · Status: accepted
  • Observed while making Layer A green (186 tests): (1) the emulator does NOT roll back state when a call fails (the AVM rejects the whole transaction), so negative tests use fresh ids instead of asserting box absence after a failure; (2) arc4.Struct(kw_only=True) breaks the emulator’s from_bytes (positional constructor), so structs are plain arc4.Struct and are still constructed with keyword arguments; (3) arc4.UIntN.native is deprecated in the emulator in favour of as_uint64(), which the algorand-python 4.0.0 stubs also expose; the contract uses as_uint64() for every UIntN read (Address/Bool/String keep .native); (4) txn.logs(i) returns raw bytes; (5) BoxMap.maybe() on struct values cannot be tuple-unpacked in Puya 5.10.1 (in + [] used instead); (6) Account(...) in the emulator takes str | algopy.Bytes, never raw bytes.
  • Consequences: documented in tests/README.md; every item above is either a Layer B assertion or a coding convention.
  • Spec refs: §21 · Tests: tests/unit/, tests/static/

D-049 TEAL static analysis: tealer update/delete detectors are false positives on the Puya router

Section titled “D-049 TEAL static analysis: tealer update/delete detectors are false positives on the Puya router”
  • Date: 2026-09-29 · Status: accepted
  • Context: algokit task analyze (tealer 0.1.2) reports is-updatable, is-deletable and unprotected-* on both programs. The clear program is pushint 1; return (it never decides anything: update/delete are governed by the approval program). The approval program starts with txn OnCompletion; !; assert before any branch, so UpdateApplication (4) and DeleteApplication (5) are rejected on every path; tealer does not recognise that idiom and only looks for explicit comparisons.
  • Decision: the audit-teal task analyses the approval program only and excludes the four update/delete detectors plus the LogicSig-oriented ones (rekey-to, missing-fee-check, can-close-asset, can-close-account). Immutability is proven instead by tests/static/test_teal_router.py (the NoOp gate precedes every branch and is the only OnCompletion handling), tests/static/test_arc56.py (no create/update/delete actions besides create) and Layer B SEC-04/05 (update and delete transactions from deployer, governance and stranger are rejected on LocalNet). ci-teal-diff still guards the artefacts.
  • Spec refs: §12.1, §20 Security, §21 · Tests: test_teal_router, test_arc56, SEC-04/05

D-050 Dev-mode timestamp offset is a per-block delta (amends D-019)

Section titled “D-050 Dev-mode timestamp offset is a per-block delta (amends D-019)”
  • Date: 2026-09-29 · Status: accepted (measured on LocalNet 5.0.2)
  • Context: D-019 assumed a cumulative offset relative to the wall clock. Measured: GET /v2/devmode/blocks/offset answers 404 until an offset is set; after set_timestamp_offset(n) every new block’s timestamp is the PREVIOUS block’s timestamp + n (n = 100 gave +100, +100; n = 1 gave +1, +1). Global.latest_timestamp inside a call is the timestamp of the previous block.
  • Decision: time_travel(seconds) = set offset to seconds, produce one filler block, set offset to 1. From the first time travel on, ledger time advances one second per block: monotone and deterministic. A fresh LocalNet (down -v) is used for every acceptance run.
  • Spec refs: §5, §10.2, §20 · Tests: test_00_pq_spike (g), CAP-03, INV22-01

D-051 Every off-chain-built transaction carries a unique note

Section titled “D-051 Every off-chain-built transaction carries a unique note”
  • Date: 2026-09-29 · Status: accepted
  • Context: algokit-utils caches suggested params for a few seconds, so two otherwise identical transactions built in that window share first/last valid and therefore the transaction id; the second is rejected with “transaction already in ledger”. It happened in the taxonomy seeding (two field names of equal length give two identical payments in one group).
  • Decision: the deploy script notes each payment with the field id; the test harness notes every payment, asset transaction and app call with a session-random counter. The web app and CLI (M4/M5) must do the same for standalone repeated transactions.
  • Spec refs: §12, §13 · Tests: deploy seeding, tests/localnet/conftest.py unique_note

D-052 Deploy reuses an app only when genesis and governance both match

Section titled “D-052 Deploy reuses an app only when genesis and governance both match”
  • Date: 2026-09-29 · Status: accepted (amends D-038)
  • Decision: deploy_app reuses spec/networks.json[network].appId only if the stored genesis hash equals the node’s AND the app’s on-chain governance equals the requested governance address; otherwise it creates a new application. Verified: algokit project deploy localnet twice on a fresh LocalNet -> first run creates, funds with exactly 100 000 microAlgo and seeds 42 fields in 6 Falcon-signed groups; second run reuses the app and adds nothing. The localnet entry of the manifest is machine-specific and is restored before committing.
  • Spec refs: §3.2, §12.1, §23 · Tests: GOV-04, idempotence step of the end-to-end run

D-053 Outcome of the PQ spike (closes D-041, verifies D-045)

Section titled “D-053 Outcome of the PQ spike (closes D-041, verifies D-045)”
  • Date: 2026-09-29 · Status: accepted
  • Measured on LocalNet algod 5.0.2 (consensus 268b63433a907455d439995bf916f6b296018f4f):
    • signer class in py-algorand-sdk 2.12.0 is Falcon1024AlgorandSigner; public key 1 793 bytes; algokey pq check-address inside the algod container confirms the governance address.
    • a pqsig payment at 3 x min fee confirms; at 1 x min fee the node rejects it.
    • simulate with a scheme-only placeholder PQSig(b"f1", 0, b"", b"") reports group-usage 3 000 000, identical to the Falcon-signed transaction (D-045 holds): fees can be sized before signing.
    • chosen path (D-041 candidate 1): typed-client composer with the Falcon signer registered through AccountManager.set_signer and static_fee from the usage formula; contingencies 2 and 3 were not needed.
    • caveat: never pass the Falcon signer as the explicit signer= of algorand.send.* parameters. algokit-utils 4.2.3 treats any object with address and signer attributes as an account and unwraps .signer, which on PQAlgorandSigner is the raw signing callable (“‘function’ object has no attribute ‘sign_transactions’”). Registered signers and explicit TransactionWithSigner objects work.
    • every governance call of the suite (create-time seeding, add_field, set_governance round trips) is pqsig-signed; authors and voters with Falcon accounts publish, claim, vote and claim reputation (COST-08).
  • Spec refs: §12.1, §13.2, §20 Security · Tests: test_00_pq_spike, test_10_governance, test_90_measure

M2 decisions (community layer) — recorded 2026-09-29, before any M2 code

Section titled “M2 decisions (community layer) — recorded 2026-09-29, before any M2 code”

D-054 Multi-author allocation: equal split, remainder to the submitter (OWNER decision; closes the §10.2 M2 gate)

Section titled “D-054 Multi-author allocation: equal split, remainder to the submitter (OWNER decision; closes the §10.2 M2 gate)”
  • Date: 2026-09-29 · Status: accepted (owner chose candidate (b) of §10.2); the rule stands, its mechanics (accumulators, carry, baseline) are superseded by D-072
  • Context: with N accepted authors the text of v2.7 let every author claim the full vote_total (N x inflation). The owner chose “equal split with the submitter taking the remainder”. Two things had to be fixed to make that rule implementable under the pull model: the granularity of the division and what happens when the author set changes.
  • Decision:
    1. The article box gains three fields (appended after asa_id): share_total: uint64 (cumulative equal share credited to EACH accepted author), split_carry: uint16 (vote weight received but not yet divisible, always < author_count) and submitter_extra: uint64 (remainders credited to the submitter). Article value = 210 bytes, box MBR 90 100, publish deposit 219 400 microAlgo.
    2. vote_article (weight w, N = author_count at vote time): pool = split_carry + w; q = pool / N; share_total += q; split_carry = pool - q x N. Constant cost, independent of N; still no reputation box is touched.
    3. accept_coauthorship: the open remainder belongs to the author set that earned it, so it is settled to the submitter before the set changes: submitter_extra += split_carry; split_carry = 0; then author_count += 1 and the new co-author’s claimed_total := share_total (replaces “claimed_total = article.vote_total” of §7.6; same purpose: only votes cast after acceptance ever count).
    4. claim_reputation: entitled = share_total for a co-author and share_total + submitter_extra for the submitter; claimable = entitled - au.claimed_total (MUST be > 0); the rest of §10.2 is unchanged (daily cap, secondary = granted_primary / 2, claimed_total += granted_primary).
    5. Conservation: at any time vote_total = sum over periods (N x share increments) + submitter_extra + split_carry, so the reputation materialisable by all authors together never exceeds vote_total (no inflation) and at most author_count - 1 weight units are waiting in split_carry.
  • Rejected alternative: dividing every single vote (floor(w / N) each, w mod N to the submitter). It is simpler, but most votes of a young network weigh 1 to 3, so with 2-3 authors nearly everything would go to the submitter and co-authors would receive nothing: that is not an equal split. The cumulative division with a carry is still pure uint64 arithmetic and deterministic from the events (Voted.weight and CoauthorAccepted.author_count).
  • Rejected alternative: letting the submitter claim the open split_carry at any time. It makes the split depend on how often the submitter claims (claiming after every weight-1 vote would starve the co-authors). The submitter receives the remainder only when the author set changes; while the set is stable the carry is shared equally by the next votes.
  • Consequences: invariants 8 and 9 of §10.3 hold (claimed_total <= share_total + submitter_extra <= vote_total; a late co-author starts at the current share_total and the older carry is settled away from them). Single-author articles behave exactly as in M1 (N = 1 gives q = w, carry 0). §6.4, §7.6, §8.0, §10.2, §13.3 amended in the spec copy.
  • Spec refs: §6.4, §7.6, §8.0, §10.2, §10.3 inv 8-9, §17 inv 27 · Tests: SPLIT-01..06 (tests/ref/test_ref_m2.py), SPLIT-A-01..04 (Layer A property tests), COAU-01..42, COAU-B-01..07 (tests/localnet/test_55_coauthors.py)

D-055 M2 method surface, payments and inner transactions

Section titled “D-055 M2 method surface, payments and inner transactions”
  • Date: 2026-09-29 · Status: accepted
  • Decision: methods that create a box take the leading pay argument of D-001: invite_coauthor(pay, article_id, coauthor), submit_review(pay, article_id, cid, recommendation) -> uint64, vote_review(pay, article_id, review_seq), comment(pay, article_id, cid, reply_to, mentions) -> uint64, vote_comment(pay, article_id, comment_seq), flag(pay, article_id, reason). Methods that create nothing take no payment: accept_coauthorship(article_id), resolve_comment(article_id, comment_seq), resolve_dispute(article_id, outcome). Only resolve_dispute(retracted) submits an inner transaction (the ARC-71 Revoked AssetConfig of D-033, fee pooled by governance).
  • Status columns (§7.4 with D-020): invite/accept {preprint, under_review}; submit_review and flag {preprint, under_review, final, disputed}; comment every status; vote_review, vote_comment and resolve_comment follow the vote_article column {preprint, under_review, final}; resolve_dispute only in disputed.
  • invite_coauthor accepts any address except the submitter (an invitation nobody can accept is inert and is paid by the submitter); invited_count and author_count overflow (65 535) fail.
  • Spec refs: §7.4, §7.6, §12 · Tests: PERM-01..05 (permission matrix, 13 methods x 5 statuses), COAU-04, COAU-11, REV-08, REV-21, CMT-02, CMT-22, RES-02, tests/static/test_arc56.py
  • Date: 2026-09-29 · Status: accepted
  • Decision: Review (reviewer, cid, recommendation, created_at, useful_total) = 85 bytes under r; reviewer index under ri holds the raw 8-byte review_seq; Comment (author, cid, reply_to, root_seq, depth, created_at, vote_total, reply_count, resolved, resolved_at) = 118 bytes under c; review and comment votes reuse Vote (24 bytes) under vr / vc; Flag (reason, weight, created_at) = 17 bytes under f. Deposits: invite 29 300; review 43 300 + index 22 500 (+ 25 300 for the reviewer’s first reputation box in that field); comment 56 500 (+ 25 300 likewise); review/comment vote 32 100; flag 26 500.
  • Spec refs: §6.2, §6.4, §10.5, §13.3 · Tests: tests/ref/test_ref.py, tests/static/test_arc56.py, Layer B measurements

D-057 Pushed grants (review useful, comment upvote, comment resolved)

Section titled “D-057 Pushed grants (review useful, comment upvote, comment resolved)”
  • Date: 2026-09-29 · Status: accepted
  • Decision: raw = w x 3 / 1 (review; 0 when the voter is an accepted author), w x 1 / 5 (comment) or 5 (resolution); granted = min(raw, cap_remaining(recipient, primary field)). The recipient’s reputation box is read only when raw > 0 and MUST exist (the call fails otherwise, §10.5); it is written, and ReputationChanged emitted, only when granted > 0 (D-006). The vote box stores weight and granted; review.useful_total / comment.vote_total always grow by the full weight. Event order: the canonical event first, then at most one ReputationChanged.
  • Observation for the owner (constants are tunable per §5): with GRANT_COMMENT = 1/5 a comment upvote from a voter of weight below 5 (reputation below 16) grants 0; early on, comments earn reputation mainly through resolution.
  • Accepted authors may comment and may vote on comments; an accepted author’s comment can be upvoted but never resolved (§8.3).
  • Spec refs: §8.2, §8.3, §10.2, §10.5 · Tests: REV-15..38, CMT-20..30, RES-01..12, CMT-41, REV-B-02..05, CMT-B-04..05
  • Date: 2026-09-29 · Status: accepted
  • Decision: flag requires an existing reputation box of the flagger in the article’s primary field with rep >= MIN_FLAG_REP; weight 1 + isqrt(rep); one flag box per (article, dispute_round, flagger). round_flag_weight always accumulates (D-020); the transition to disputed happens in the call that makes it reach FLAG_THRESHOLD while the status is preprint, under_review or final, storing status_before_dispute. Flagged carries round_total and new_status (the status after the call), so no separate StatusChanged is emitted (one canonical event per call). resolve_dispute (governance, status = disputed, outcome 1 cleared | 2 retracted): cleared restores status_before_dispute; retracted revokes the ASA exactly like an author retraction (D-033); both then set dispute_round += 1 (fails at 65 535), round_flag_weight = 0, status_before_dispute = 0. DisputeResolved.dispute_round is the round that was resolved.
  • Spec refs: §7.3 rows 6-8, §9, §11 · Tests: FLAG-01..23, DISP-01..17, PERM-01..05, ST-04 = FLAG-B-01, DISP-B-01..03 (tests/localnet/test_66_flags_disputes.py)
  • Date: 2026-09-29 · Status: accepted
  • Decision: reply_to is 0 or an existing comment of the same article (the key contains the article id, so a comment of another article can never match); depth = parent.depth + 1 <= MAX_COMMENT_DEPTH, root_seq = parent.root_seq, parent.reply_count += 1; a top-level comment has root_seq = its own sequence. mentions holds 0 to 5 addresses, pairwise distinct and different from the sender; nothing else is checked about them. Review and comment CIDs MUST use codec 0x55 (raw).
  • Spec refs: §6.1, §8.3, §17 inv 25-26 · Tests: CMT-01..19, CMT-40, CMT-B-01..03
  • Date: 2026-09-29 · Status: accepted (refines D-006)
  • Decision: the article box is written, and updated_at stamped, only by calls that change an article field (invite, accept, submit_review, comment, flag, resolve_dispute and the M1 methods). vote_review, vote_comment and resolve_comment change only the review / comment / vote / reputation boxes and do not write the article box. The indexer derives “last activity” from events.
  • Spec refs: §6.4 · Tests: REV-15, CMT-20, RES-01, REV-B-02, CMT-B-04

D-061 Mentions are passed in strictly ascending order (amends D-059)

Section titled “D-061 Mentions are passed in strictly ascending order (amends D-059)”
  • Date: 2026-09-29 · Status: accepted (measured on LocalNet)
  • Context: the first implementation compared every pair of mentions. Measured on LocalNet 5.0.2, a reply with 5 mentions cost 653 opcodes (consensus limit 700, Layer C gate 600) before even creating the commenter’s reputation box; pairwise distinctness is quadratic.
  • Decision: comment() requires mentions sorted by the raw 32-byte address, strictly ascending (compared as big-endian unsigned integers, AVM b<). Strict order implies pairwise distinctness, so the MUST of section 8.3 (“distinct, never the sender, at most MAX_MENTIONS”) is enforced in one pass. The order carries no meaning; clients sort before sending (one line in the web app and the CLI). An unsorted or duplicated list fails with “mentions must be distinct and in ascending order”.
  • Consequences: section 8.3 and section 12 annotated in the spec copy; the indexer must not assume that the order of Commented.mentions reflects the order in the comment body.
  • Spec refs: §8.3, §11, §17 inv 26, §21 C · Tests: CMT-mentions matrix (Layer A), test_64_comments (Layer B), SIM-05

D-062 Reputation helpers read each box once (no behaviour change)

Section titled “D-062 Reputation helpers read each box once (no behaviour change)”
  • Date: 2026-09-29 · Status: accepted (measured on LocalNet)
  • Context: with the M2 entitlement branch claim_reputation reached 577 opcodes (545 in M1).
  • Decision: _credit(key, raw, ts) reads the reputation box once, applies the saturating daily cap and writes only when the grant is positive; events are emitted by the caller after every write and after the MBR check, in the normative order (canonical event first). _push wraps it for review / comment grants (box read only when raw > 0, MUST exist).
  • Spec refs: §10.2, §10.5, §21 C · Tests: REP-01 property test, CLAIM-, REV-, CMT-*, Layer C gate

D-063 Deploy reuses an app only when the program matches too (amends D-052)

Section titled “D-063 Deploy reuses an app only when the program matches too (amends D-052)”
  • Date: 2026-09-29 · Status: accepted
  • Context: the application is immutable, so a rebuilt contract can never be installed into an existing app id; D-052 compared only genesis and governance and would have reused an app running an older program.
  • Decision: deploy_app reuses networks.json[network].appId only if the genesis hash matches, the on-chain approval program equals byteCode.approval of the committed ARC-56 artefact, and the on-chain governance equals the requested one. Otherwise it creates a new application. The M2 program needs 2 extra program pages; algokit-utils computes them at creation.
  • Spec refs: §3.2, §12.1 · Tests: GOV-04, idempotence step of the end-to-end run

D-064 Helpers are inlined (@subroutine(inline=True))

Section titled “D-064 Helpers are inlined (@subroutine(inline=True))”
  • Date: 2026-09-29 · Status: accepted (measured on LocalNet 5.0.2)
  • Context: opcode budget is per application call (700); program size is per application (8 192 bytes). Measured with called subroutines: claim_reputation 589, comment (reply, 5 mentions, first reputation box) 505, approval program 4 902 bytes. Measured with every helper inlined: claim_reputation 474, comment 479, vote_review 344, publish 312, approval program 6 066 bytes (3 pages).
  • Decision: every helper of contract.py, keys.py and cid.py is declared @subroutine(inline=True). The budget margin (at least 120 opcodes under the Layer C gate for every method) is worth 1 164 bytes; 2 122 bytes of program space remain. tests/static/test_arc56.py fails the build if the program exceeds 8 192 bytes; the Layer C gate fails any call above 600 opcodes.
  • puyapy optimisation level 2 was measured too: 4 929 vs 4 940 bytes, no meaningful change; the compiler default (level 1) is kept.
  • Spec refs: §21 C, §23.5 · Tests: SIM-05 (tests/localnet/test_92_measure_m2.py), test_program_fits_the_consensus_limit

D-065 The last dispute round takes no flags (amends D-058)

Section titled “D-065 The last dispute round takes no flags (amends D-058)”
  • Date: 2026-09-29 · Status: accepted
  • Context: resolve_dispute fails when dispute_round is 65 535 because the counter cannot be incremented (D-016). Flags were still accepted in that round, so a dispute opened there could never be resolved and the article would stay disputed for ever. Found by the independent Layer A tests.
  • Decision: flag fails with “dispute round overflow” when dispute_round is 65 535. An article can therefore go through at most 65 534 disputes and can never be stuck in disputed. The guard in resolve_dispute stays as defence in depth.
  • Spec refs: §9, §17 inv 15-16 · Tests: DISP-12, DISP-12b, DISP-13

D-066 Readings of section 8 confirmed by the M2 tests (no contract change)

Section titled “D-066 Readings of section 8 confirmed by the M2 tests (no contract change)”
  • Date: 2026-09-29 · Status: accepted; the first item is flagged to the owner as a possible later rule
  • A reviewer who accepts co-authorship AFTER reviewing keeps the review and keeps receiving grants from the useful votes of non-authors (section 8.0: earlier actions “remain on record and keep their effects”); from then on that person’s own votes on reviews of the article grant 0. Known consequence: a submitter can invite a friendly reviewer as co-author without stopping that review from earning. Closing it would need a rule such as “a review stops earning once its reviewer is an accepted author”, which is a protocol change for the owner to decide (not improvised here, section 10.4).
  • A resolved comment can still be voted and replied to (section 8.2 only requires voter != comment.author; section 8.3 allows replies to resolved comments).
  • Review and comment CIDs need not be unique; replying to one’s own comment is an ordinary reply.
  • When a mention list is both out of order and contains the sender, either error may be reported; clients must not rely on which one.
  • Spec refs: §8.0, §8.1, §8.2, §8.3 · Tests: REV-07, REV-27, RES-06, RES-11, CMT-05, CMT-07

D-067 Suggested-params cache of algokit-utils is switched off (amends D-051)

Section titled “D-067 Suggested-params cache of algokit-utils is switched off (amends D-051)”
  • Date: 2026-09-29 · Status: accepted (observed on LocalNet)
  • Context: algokit-utils 4.2.3 (Python) keeps the suggested transaction parameters for time.time() + 3_000, i.e. 3 000 seconds, although the value is documented as milliseconds (“three seconds”). In dev mode every transaction is a block, so after 1 000 blocks in one session every new transaction was built with a validity window that had already closed (“txn dead: round 1006 outside of 2–1002”). M1 never reached 1 000 blocks in a session; the M1 + M2 suite does.
  • Decision: the LocalNet harness and the deploy script call set_suggested_params_cache_timeout(0), so parameters are read from the node for every transaction. Unique notes (D-051) stay: two transactions built without a block in between still share their validity window.
  • Consequences: long-running Python tooling of later milestones must do the same; the web app uses the TypeScript library and must be checked separately in M4.
  • Spec refs: §13.2 · Tests: the full tests/localnet run (more than 1 000 blocks)

D-068 The network manifest is written with retries

Section titled “D-068 The network manifest is written with retries”
  • Date: 2026-09-29 · Status: accepted (observed on this Windows host)
  • Context: during the idempotence check the first algokit project deploy localnet created and seeded the application and then failed with OSError: [Errno 22] while opening spec/networks.json for writing (another process held the file for a moment). The application existed but was not recorded, so the next run created a second one.
  • Decision: write_manifest retries up to 5 times with a growing pause; if every attempt fails it logs the entry that has to be recorded by hand and raises. Verified afterwards on a fresh LocalNet: first run creates app 1003, funds it with exactly 100 000 microAlgo and seeds 42 fields in 6 Falcon-signed groups; second run reuses the app and adds nothing.
  • Consequences: for TestNet / MainNet (M4) an unrecorded deployment costs real funds; the deploy log always prints the application id before the manifest is written.
  • Spec refs: §3.2, §23 · Tests: idempotence step of the end-to-end run

D-069 Notes from the independent M2 test pass (no contract change)

Section titled “D-069 Notes from the independent M2 test pass (no contract change)”
  • Date: 2026-09-29 · Status: accepted
  • D-044 applies to every accepted author: a co-author cannot claim while the article is disputed and loses the unclaimed share when it is retracted, like the submitter.
  • Only the submitter controls the author list (section 7.6), so every acceptance dilutes the future shares of the existing co-authors, and every remainder goes to the submitter (D-054). Recorded for the owner; a co-author consents only to their own acceptance.
  • When two preconditions fail at once the contract reports the first one it checks; clients must not depend on which message they get.
  • Section 10.3 invariant 6 says “in M1” and section 17 invariant 12 says “M1/M2”: reputation never decreases in either milestone (tested in both).
  • Spec refs: §7.4, §7.6, §10.3, §17 · Tests: COAU-20, COAU-39

D-070 Owner delegation after the M2 report (2026-09-29)

Section titled “D-070 Owner delegation after the M2 report (2026-09-29)”
  • Date: 2026-09-29 · Status: accepted (owner: “sobre las otras decisiones, lo que tú creas”)
  • Context: the M2 summary listed the judgement calls taken during M2 and three open points. The owner delegated all of them except the co-author model, on which the owner made a proposal of their own (D-071).
  • Decisions taken under that delegation:
    • Mentions in ascending order (D-061) and no flags in the last dispute round (D-065): confirmed.
    • GRANT_COMMENT stays 1/5, the value of section 5. It is low on purpose (comments are cheap to write); the real grant for a useful comment is the resolution (5). Revisit with real activity; any change is a new deployment.
    • Hosting: M3 is developed and accepted locally (LocalNet + local SQLite), like M1 and M2. One Linux VPS (indexer + IPFS gateway + web) is provisioned when the first public deployment is prepared (M4); nothing has to be bought before that.
  • Not decided here because they depend on D-071: the granularity of the split (D-054), the reviewer who later becomes a co-author (D-066) and the dilution of existing co-authors (D-069).
  • Spec refs: §5, §8.3, §9, §22 · Tests: unchanged

D-071 Co-author model: author list fixed from the start (OWNER proposal, pending)

Section titled “D-071 Co-author model: author list fixed from the start (OWNER proposal, pending)”
  • Date: 2026-09-29 · Status: closed by D-072 (the owner chose option 1: “La 1”)
  • Context: the owner asked whether it would be simpler to establish every co-author when the article is published, instead of inviting and accepting over time until final. This touches the section 2 row “Multi-author” (invitations, signed acceptance, no limit but 65 535), so it can only change by an explicit owner decision, like D-023 and D-024.
  • Assessment: a list that never changes removes the accounting of D-054 (no carry, no baseline per co-author), makes the split independent of when each co-author signs (today a late signer loses the earlier votes, usually the most numerous), and removes the two loose ends of D-066 and D-069. Two constraints remain whatever the model:
    1. Consent (invariant 23): every co-author signs. The signatures cannot be part of the publish group in practice, because a transaction is valid for at most 1 000 rounds (about 45 minutes) and every co-author would have to sign inside that window. Co-authors can be declared at publication and confirm later.
    2. A list that is complete at the instant of publication cannot be unlimited: it has to fit one call (opcode budget, 1 024 bytes of logs). Estimate from the measured cost of the mention loop (about 29 opcodes per address): about 8 co-authors in a single call, about 25 with auxiliary transactions in the group. To be measured if chosen.
  • Options:
    • (1) List declared in publish and closed for ever; each declared co-author confirms by signing when they can; every author receives an equal share of all votes since publication; an address that already voted on or reviewed the article cannot confirm. Capped number of co-authors. Reopens section 2 and changes publish.
    • (2) Invitations as today (no cap, submitter pays the boxes), but the list closes by itself at the first reputation claim or when the article leaves preprint; same equal share of all votes. Section 2 stays as written; publish unchanged.
    • (3) Keep M2 as tagged.
  • Consequences: nothing is deployed on a public network, so this is the cheapest moment to change. Options 1 and 2 reopen the M2 acceptance (contract, oracles, tests, report, new tag); M3 waits for the choice because the indexer reads these events.
  • Spec refs: §2, §7.4, §7.6, §8.0, §10.2, §10.3 inv 9, §17 inv 23, §18 · Tests: to be written with the chosen option

Co-author model v2 — recorded 2026-09-29, before the code change

Section titled “Co-author model v2 — recorded 2026-09-29, before the code change”

D-072 The author list is declared in publish (OWNER decision; closes D-071; reopens §2 “Multi-author”)

Section titled “D-072 The author list is declared in publish (OWNER decision; closes D-071; reopens §2 “Multi-author”)”
  • Date: 2026-09-29 · Status: accepted (owner, option 1 of D-071)
  • Decision:
    1. publish(pay, cid, primary_field, secondary_field, article_type, parent_article, coauthors: address[]): the submitter declares 0 to MAX_COAUTHORS (25) co-authors. The list is strictly ascending by raw 32-byte address (hence distinct, as in D-061) and never contains the submitter. publish creates one co-author box per declared address (accepted = false, invited_at = ts), paid by the submitter, and sets invited_count to the number declared, which never changes. Published carries the list. Nobody can be added later: invite_coauthor and CoauthorInvited no longer exist.
    2. accept_coauthorship(article_id): a declared address confirms by signing, once, while the article is preprint, under_review or final (not in disputed, not in retracted). It MUST NOT have voted on the article nor reviewed it (no article-vote box, no reviewer index). It sets accepted, accepted_at and increments author_count; claimed_total stays 0.
    3. Allocation (the owner’s rule of D-054, “equal split, remainder to the submitter”): N = 1 + invited_count is fixed for ever. Every accepted co-author is entitled to vote_total / N (floor); the submitter to the ceiling of vote_total / N. claimable = entitled - claimed_total; the rest of section 10.2 is unchanged.
    4. Why the ceiling and not “floor plus the whole remainder”: the latter decreases when the total grows (N = 3: a total of 5 gives 3, a total of 6 gives 2), so reputation already claimed could exceed the votes received. The ceiling is the largest share for the submitter that never decreases and never creates reputation: the entitlements of all authors add up to at most vote_total, short by at most N - 1 weight units that wait for the next votes.
    5. The share of a declared co-author who never confirms is never claimed by anyone. Declaring somebody therefore costs every author a part of their share, which discourages padding the list.
    6. Self-action rules (section 8.0) are unchanged and bind ACCEPTED authors only. A declared address that has not confirmed may vote, review and flag like anybody else, so nobody can be silenced by being declared without consent; but after voting or reviewing it can never confirm, so nobody ever receives reputation from their own vote and no reviewer ever becomes an author of the article they reviewed.
    7. The article box returns to 192 bytes (share_total, split_carry, submitter_extra are removed) and vote_article is the M1 method again. Publish deposit = 212 200 + 29 300 per declared co-author (microAlgo). CoauthorAccepted.claimed_total is kept in the event for compatibility with section 11 and is always 0.
  • Consequences:
    • Section 2 row “Multi-author” is amended by the owner: co-authors are declared at publication instead of invited over time, and the limit is 25 co-authors per article instead of 65 535. Signed acceptance, “only accepted co-authors count” and “editing rights stay with the submitter” stand.
    • Section 7.4: the column “invite/accept co-author” becomes “accept co-authorship”, allowed in final too (the list is already fixed, confirming changes nothing for the others).
    • Section 10.3 invariant 9 (“a co-author accepted after votes were cast can never claim those earlier votes”) is replaced by: the author list is fixed at publication, every accepted author is entitled to an equal share of all votes, and an address that voted on or reviewed the article can never become an accepted author.
    • Section 18: “late co-author harvesting earlier votes” is now prevented by the closed list.
    • D-066 first item (reviewer who later becomes a co-author) and D-069 second item (dilution of existing co-authors) no longer apply.
    • The M2 acceptance is reopened: contract, oracles, tests, measurements and report are redone; tag m2 stays as history and the result is tagged m2.1.
  • Spec refs: §2, §6.4, §7.1, §7.4, §7.6, §8.0, §10.2, §10.3, §11, §12, §13.3, §17, §18 · Tests: SPLIT-01..09 (tests/ref/test_ref_m2.py), COAU-01..50, SPLIT-A-01..06 (property), REV-39, REV-40, FLAG-22, FLAG-23, CMT-31, COAU-B-01..07 (tests/localnet/test_55_coauthors.py)

D-073 extend() lends budget and box references; the Layer C gate counts application calls

Section titled “D-073 extend() lends budget and box references; the Layer C gate counts application calls”
  • Date: 2026-09-29 · Status: accepted
  • Context: one application call may touch 8 boxes and spend 700 opcodes. publish touches 3 to 5 boxes plus one per declared co-author. Box references and opcode budget are shared by the application calls of a group.
  • Decision: extend() is an ABI method that changes nothing and emits nothing. A client adds to the publish group as many extend calls as the box references require: ceil(boxes / 8) - 1; a publication with few co-authors needs none. Every extend call carries a distinct note (D-051). The Layer C gate becomes: opcodes <= 600 x application calls of the group and box references <= 8 x application calls; the log limits are unchanged (32 entries, 1 024 bytes per call), and they are what caps the list: Published with 25 co-authors is 914 bytes.
  • Consequences: a publication that needs more than one call’s budget is flagged in the report (section 21 C). Invariant 21 is unaffected: extend is not a state-changing call.
  • Spec refs: §12, §13.2, §21 C · Tests: tests/static/test_arc56.py, COAU-B-*, SIM-05

D-074 Outcome of the co-author model on LocalNet (verifies D-072 and D-073)

Section titled “D-074 Outcome of the co-author model on LocalNet (verifies D-072 and D-073)”
  • Date: 2026-09-29 · Status: accepted (measured on LocalNet algod 5.0.2)
  • Measured, Ed25519 and pqsig alike: publish with 0 / 1 / 2 / 3 / 4 / 9 / 25 co-authors costs at most 370 / 401 / 442 / 483 / 524 / 741 / 1 421 opcodes (about 41 per declared co-author) in 1 / 1 / 1 / 1 / 1 / 2 / 4 application calls, with at most 5 / 5 / 6 / 7 / 8 / 13 / 29 box references; the Published log with 25 co-authors is 914 bytes (926 with the return value, limit 1 024). Deposits equal the min-balance delta to the microAlgo: 212 200 + 29 300 per co-author (944 700 with 25). accept_coauthorship 144 opcodes, 4 box references, no deposit. claim_reputation 479, vote_article 189. Program 5 932 bytes of 8 192.
  • MAX_COAUTHORS = 25 is confirmed: the log budget of one call would allow 28, the round figure leaves room for the return value and is easy to state to users.
  • Rule for clients, verified by COAU-B-06: boxes = 3 + (1 with a secondary field) + (1 for an amendment) + co-authors; extend calls = ceil(boxes / 8) - 1. A publication without the extend calls it needs is rejected.
  • Known residual, left as it is: a declared co-author can mark a review of the article useful before confirming, and that vote grants normally (the rule of section 8.2 binds accepted authors). Blocking it would let a submitter cancel somebody’s review votes just by declaring them without consent, and it would protect nothing: any other address the submitter controls can cast the same vote (section 10.4, not Sybil-resistant). Found by the independent review tests (REV-27).
  • Test harness: docker compose up --wait returns a few seconds before algod answers, and a suite started right after it was skipped as a whole once; the algorand fixture now waits up to 30 seconds for the node before skipping. A later run lost the node in the middle (66 passed, then connection errors; the container was found exited with code 143, i.e. it had received a stop signal); the whole suite was repeated on a fresh LocalNet.
  • Acceptance of the new model: 74 LocalNet tests passed on a fresh LocalNet, deploy idempotence verified again (first run creates the app and seeds 42 fields, second run reuses it); offline figures in M2_REPORT.md.
  • Notes from the independent test pass of the new model (no contract defect found): votes on reviews and on comments, flags, comments and resolutions do not block a confirmation, only an article vote or a review does; CoauthorAccepted.author_count is the number of confirmed authors after that signature and must not be read as N; when two preconditions fail at once the first one checked is reported (D-069). The authors of the offline tests reported mutation checks in scratch directories for the new model: 109 broken copies of the contract (42 reviews, 38 co-authors and split, 29 flags and disputes), all detected by the complete test files; single property tests run alone are a random second layer and missed a few mutants in some runs.
  • Spec refs: §7.6, §10.2, §12, §13.3, §21 C · Tests: tests/localnet/test_55_coauthors.py, test_92_measure_m2.py, tests/unit/test_coauthors.py, test_split_property.py

M3 decisions (indexer) — recorded 2026-09-30, before the code

Section titled “M3 decisions (indexer) — recorded 2026-09-30, before the code”

D-075 Indexer runtime: Node with the built-in SQLite, no native modules, no build step

Section titled “D-075 Indexer runtime: Node with the built-in SQLite, no native modules, no build step”
  • Date: 2026-09-30 · Status: accepted
  • Context: the plan pinned better-sqlite3 13.0.3 and Node 24 LTS. The host runs Node 26.5.1, whose built-in node:sqlite (SQLite 3.53.3) offers the math functions the ranking needs (pow) and JSON functions; better-sqlite3 is a native module that would have to be compiled for this Node line (Visual Studio build tools on this host, another toolchain on the VPS).
  • Decision: the indexer uses node:sqlite (DatabaseSync), synchronous like better-sqlite3 was meant to be. It runs TypeScript directly through Node’s type stripping (erasableSyntaxOnly, .ts imports), so there is no build step; tsc --noEmit type-checks, node --test runs the tests, prettier formats. The REST API is served with node:http and a small router (no web framework). engines.node >= 24; developed and accepted on 26.5.1; the VPS of M4 installs the current LTS.
  • Pins (package.json, exact): algosdk 3.8.0, @algorandfoundation/algokit-utils 9.2.2, @algorandfoundation/algokit-subscriber 3.4.0, multiformats 14.0.5, ipfs-unixfs-importer 17.1.1, ipfs-unixfs-exporter 16.2.3, blockstore-core 7.0.1, @ipld/car 5.4.7, @ipld/dag-pb 4.2.0; dev: typescript 5.9.3, @types/node 26.6.3, prettier 3.9.9. better-sqlite3 is dropped from the M3 pin list.
  • Spec refs: §3, §15 · Tests: projects/indexer/test

D-076 Indexer data model and ingestion rules

Section titled “D-076 Indexer data model and ingestion rules”
  • Date: 2026-09-30 · Status: accepted
  • chain_events(tx_id, log_index, round, intra, app_id, event_name, event_version, payload_json, ts), primary key (tx_id, log_index) (D-037: one application call emits up to three events); ingestion is an INSERT OR IGNORE, so a replay is idempotent. watermark(app_id, last_round) is written in the same transaction as the events of a round.
  • algokit-subscriber only fetches the application calls (filter appId + type: appl, sync-oldest, watermark); the raw logs are decoded by src/events.ts, a port of the Python oracle tests/ref/events.py from the same ARC-56 events list (selector = sha512/256 of the signature, ARC-4 tuple payload, return log 151f7c75 skipped), proven equal to the oracle on exported vectors (test/fixtures/events.json). The decoded arguments are stored by name in payload_json with addresses as text, byte arrays as hex and integers as numbers (all fit in 53 bits except ts and totals, which are stored as strings when above Number.MAX_SAFE_INTEGER; in practice never). (amended 2026-09-30: the subscriber’s own arc28Events decoding was not used, so that the decoder is one code path with the oracle.)
  • Projections are a port of projects/contracts/tests/ref/replay.py, event by event, inside one SQLite transaction per round; every projection row can be traced to a chain_events row through (tx_id, log_index) columns where the spec asks for it (versions, votes, flags, disputes, mentions, notifications). rebuild drops the projection tables and re-applies chain_events from the first row; the result must be byte-identical to incremental ingestion (tested).
  • Derived, never stored on-chain: entitlement and claimable per (article, author) from vote_total, invited_count and the sum of ReputationClaimed.consumed (D-072); ranking score = vote_total / (age_days + 2) ** 0.8 computed at query time.
  • One process per network: it reads spec/networks.json, refuses to start if algod’s genesis hash differs from the manifest entry, and every API response carries network and appId (section 15).
  • Content (section 14.3): a content row per distinct article CID; a ContentSource fetches a directory either through a trustless gateway (CAR, every block hash verified, the directory re-hashed with the canonical importer and compared with the CID) or from a local directory (tests, LocalNet without IPFS); validation_status is valid, mismatch, malformed, unavailable or oversized; authors[] beyond the first only produces a notice. When the manifest has no gateway, content stays unavailable and articles are served with that flag, never hidden.
  • Notifications are derived from events: reply, mention, comment_on_my_article, review_on_my_article, comment_resolved, coauthor_invite (from Published.coauthors); read state is a local column.
  • Spec refs: §14.3, §15, §16.1 · Tests: projects/indexer/test (decoder parity with the Python oracle, reducer scenario, rebuild equality, API, content, LocalNet catch-up compared with the boxes)

D-077 The sample article’s placeholder author was not an address (fixture corrected, CID changed)

Section titled “D-077 The sample article’s placeholder author was not an address (fixture corrected, CID changed)”
  • Date: 2026-09-30 · Status: accepted
  • Context: the indexer’s validation (§14.3) compares authors[0] of index.md with the on-chain author. spec/fixtures/sample-article/index.md carried a 90-character placeholder (84 A + Y5HFKQ) that is not a valid Algorand address; the zero address is 58 characters (52 A + Y5HFKQ). The same string appeared in a dead or branch of tests/localnet/test_20_publish.py. Nothing on-chain depends on the front-matter, so the contract tags m1/m2.1 are unaffected, but the fixture CID changes with its bytes.
  • Decision: authors[0] of the fixture is now the zero address; expected-cid.json was regenerated twice with spec/fixtures/compute-cid.mjs (identical runs): bafybeidzsrohfscy7xlt4dh53j3kmlxunjl3r2354pvtfn6pysnjpmduna, bytes 0170122079945c72c858fdd73e0cfdda76a62ef46a57b8eb7de3eb32b7cfc49a97b07468, unixfs size 2 020. References updated: tests/ref/test_ref.py (CID-08), spec/publishing-guide.md, the dead branch in test_20_publish.py. The indexer re-hash reproduces the new CID (test/content.test.ts), and the fixture validates as valid against on-chain metadata (author = zero address, field 0x0303, secondary 0x0501, type article, parent 0).
  • Consequences: any earlier LocalNet database that published the old fixture CID simply holds a different CID; no migration. The old CID bafybeihtc64snftpqcog5wnwxc3hq5t6jivumtlpvorcsxnemkyxyzpuvy is retired.
  • Spec refs: §14.2, §14.3, §14.5 · Tests: tests/ref/test_ref.py CID-08, tests/static/test_fixture_cid.py, projects/indexer/test/content.test.ts

D-078 Indexer API conventions and M3 acceptance

Section titled “D-078 Indexer API conventions and M3 acceptance”
  • Date: 2026-09-30 · Status: accepted
  • API (§15) judgement calls: read-only GET over node:http, JSON, CORS * (the web app of M4 runs on another origin); every response, including errors, carries network and appId. GET /articles defaults to sort=score, limit=20 (max 100) and excludes retracted unless status= is given; field= matches the primary or the secondary field (an article listed under its secondary field is part of that field’s feed); type= and status= accept the number or the name. Each article carries warnings[] (disputed, retracted, content:<status> when the content is not valid) and the content summary (title, abstract, keywords) joined by CID. GET /articles/:id returns authors (submitter first, then co-authors by address) with entitled and claimable (D-072 arithmetic, null until the co-author confirms), versions, reviews, the comment tree (roots and replies in creation order), the current flag round against the threshold, disputes and the full content record. GET /articles/:id/comments?root= is one thread ordered by (depth, created_at). GET /users/:address derives publications with claimable and claim_allowed (accepted, open status, claimable > 0), reputation by field, reviews, comments and a timeline (union of the user’s own events, newest first, 200 max). GET /users/:address/notifications lists newest first with an unread count; marking as read needs an authenticated writer and is left to M4 (a signed request or a local-only UI state). GET /fields counts non-retracted articles per field. GET /status reports genesis, watermark, schema version and row counts.
  • Content: a directory whose canonical re-hash does not reproduce the CID is recorded as unavailable with a notice (the bytes obtained are not the content the chain names); oversized is decided before re-hashing (20 MiB, §14.1); malformed = index.md missing or without readable front-matter; mismatch = field, secondary field, type, parent or authors[0] differ from the chain. Records are re-checked every ARIADNE_CONTENT_RECHECK_SECONDS (default 6 h) and keep the last title/abstract while unavailable.
  • Runtime: node:sqlite binds JavaScript numbers as REAL, so integer arithmetic in SQL casts bound parameters (CAST(? AS INTEGER) in the ranking); rows come back with a null prototype and are copied to plain objects before leaving a query.
  • Acceptance on LocalNet (2026-09-30): after the contract suite populated the chain (74 LocalNet tests, 4 min), test/localnet/catchup.test.ts caught up from round 0 and compared every Article and CoAuthor box with the projections, then rebuilt from chain_events with an identical fingerprint. Numbers: 738 application calls, 912 events, rounds 1-1131 in 3.2 s; 79 Article boxes and 211 CoAuthor boxes equal field by field; second catch-up applied 0 events. The CLI ran the same chain end to end (ingest in three polls of 500 rounds, rebuild of 912 events in 137 ms, content with a local directory source, status); the fixture CID published by the suite validates as mismatch there because the suite’s author is a LocalNet account while the fixture’s authors[0] is the zero address, which is exactly the check of §14.3. Offline: 41 tests (npm test), tsc --noEmit and prettier clean. Hosting is deferred to M4 (D-070).
  • Spec refs: §14.3, §15 · Tests: projects/indexer/test/api.test.ts, content.test.ts, rebuild.test.ts, localnet/catchup.test.ts

D-079 M4 order and web stack (owner: “dale” to option 1 = web against LocalNet first)

Section titled “D-079 M4 order and web stack (owner: “dale” to option 1 = web against LocalNet first)”
  • Date: 2026-09-30 · Status: accepted
  • Order inside M4: (1) web app developed and accepted against LocalNet with the M3 indexer; (2) indexer + gateway hosting on a VPS; (3) TestNet deployment with a fresh algokey pq generate governance key held by the owner; (4) publishing guide completed. Pinata (Path A on public networks) is wired in step 3, when the owner has the credentials; until then Path A works against the dev pin target below and Path B works everywhere.
  • Stack (package.json, exact, verified on npm 2026-09-30): next 16.3.8 (Node runtime, app router, no static export: dynamic routes), react / react-dom 19.3.0, typescript 5.9.3, algosdk 3.8.0, @algorandfoundation/algokit-utils 9.2.2, @txnlab/use-wallet-react 5.0.1, react-markdown 10.1.0, remark-gfm 4.0.1, remark-math 6.0.0, rehype-sanitize 6.0.0, rehype-katex 7.0.1, katex 0.16.47 (rehype-katex 7 requires ^0.16; katex 0.18 is not usable), multiformats 14.0.5, ipfs-unixfs-importer 17.1.1, ipfs-unixfs-exporter 16.2.3, blockstore-core 7.0.1, @ipld/car 5.4.7; dev: @types/node 26.6.3, @types/react(-dom) 19.3.0, vitest 5.0.3, prettier 3.9.9, @algorandfoundation/algokit-client-generator 6.0.1 (typed TypeScript client generated from the committed ARC-56 into src/contracts/ariadne.ts, regenerated by npm run generate:client). Tailwind is dropped: the design of §16 (monochrome, one accent, no cards) is a few hundred lines of plain CSS and one dependency less.
  • Spec refs: §16, §19 · Tests: projects/web/test

D-080 Network in the URL, wallet checks and the dev network

Section titled “D-080 Network in the URL, wallet checks and the dev network”
  • Date: 2026-09-30 · Status: accepted
  • Routing (§16.1): app/[network]/... serves every page; proxy.ts (Next 16’s middleware) rewrites a path whose first segment is not a known network to /mainnet/..., so /a/123 is MainNet and /testnet/a/123 is TestNet; bare / redirects client-side to the network remembered in localStorage, else to the first enabled network. devOnly entries (localnet) are served only when NODE_ENV !== 'production' or ARIADNE_DEV_NETWORKS=1. A disabled network (MainNet before launch) is a 404 page that says so and links to the enabled ones. The manifest entry of the selected network is loaded on the server from spec/networks.json and handed to the client through a React context; nothing from another network is fetched, cached or shown. Switching networks navigates to the same path under the other prefix; a page that does not exist there falls back to that network’s feed.
  • Wallet: use-wallet is initialised for the selected network only (defaultNetwork, no in-app network switching of the wallet manager). Sign buttons are enabled only when use-wallet’s active network equals the route’s network and algod’s genesis hash equals the manifest’s (checked once per page load through /status of the indexer, which already refused to start on a mismatch, and through algod versions in the client); every transaction is built with the manifest’s genesis hash, so a wrong signature is rejected by the network anyway. Wallets: LocalNet = KMD (unencrypted-default-wallet, token from the manifest) and mnemonic (dev only); TestNet/MainNet = Pera, Defly, Lute. Falcon-1024 (pqsig) wallets are not supported by any wallet today: the fee preview assumes an Ed25519 sender and says so.
  • Spec refs: §16.1 · Tests: projects/web/test/networks.test.ts, manual on LocalNet

D-081 Transactions from the browser: deposits by box existence, resources by simulate, exact fees

Section titled “D-081 Transactions from the browser: deposits by box existence, resources by simulate, exact fees”
  • Date: 2026-09-30 · Status: accepted
  • Deposits (§13, D-001) are computed by a port of tests/ref/mbr.py and tests/ref/keys.py (src/lib/mbr.ts, src/lib/keys.ts, tested against the M2_REPORT table): publish = 212 200 + 29 300 per declared co-author; vote_article 28 900; vote_review / vote_comment 32 100; flag 26 500; claim_reputation 25 300 per missing reputation box (primary, and secondary when the article has one); submit_review 43 300 + 22 500 + 25 300 if the reviewer’s box in the primary field is missing; comment 56 500 + 25 300 likewise. Box existence is read from algod (getApplicationBoxByName, 404 = missing) right before building the group, never from the indexer, so the payment is exact even when the indexer lags. Overpayment is never sent.
  • Box and asset references are populated by algokit-utils’ simulate (populateAppCallResources), which reads article_seq_next at that moment; on public networks a publish that fails because another publication landed in between is retried once with fresh resources (D-032). extend() calls are added as D-073 says: ceil(boxes / 8) - 1, boxes = 3 + secondary + parent + co-authors.
  • Fees: the preview shown before signing is min_fee x (outer transactions + inner transactions) with inner = 1 for publish, 4 for claim_article, 1 for new_version and 1 for a retraction, 0 otherwise (D-013, D-027 for Ed25519 senders); the group is sent with coverAppCallInnerTransactionFees and a maxFee equal to that preview, so the wallet never signs more than what was shown. Every transaction carries a distinct note ariadne/<method>/<ms> (D-051).
  • Every screen shows fee + deposit and the target network before the sign button is enabled (§16 “MUST show the total cost”).
  • Spec refs: §12, §13, §16, §21.C · Tests: projects/web/test/mbr.test.ts, fees.test.ts, localnet/flow.test.ts (preview equals what the chain charged)

D-082 Content in the web app: canonical CID in the browser, pin providers, dev pin target and dev gateway

Section titled “D-082 Content in the web app: canonical CID in the browser, pin providers, dev pin target and dev gateway”
  • Date: 2026-09-30 · Status: accepted
  • The browser builds the UnixFS DAG with the same importer and options as spec/fixtures/compute-cid.mjs and the indexer (src/lib/unixfs.ts, tested against expected-cid.json), shows the CID, writes a CAR and hands it to a PinProvider; the provider’s CID must equal the local one or the flow stops (§14.4). Providers: DevPinProvider posts the CAR to the app’s own /api/dev/pin, which unpacks it into ARIADNE_CONTENT_DIR/<cid>/ (the same directory the indexer validates from and the dev gateway serves), only for devOnly networks; PinataProvider is added in the TestNet step (D-079) with the JWT kept in localStorage behind a “forget” action, never sent anywhere but Pinata (§14.4 “provider credentials”).
  • Path B fetches the CID through the network’s gateway as a CAR (?format=car), verifies every block, re-hashes with the canonical parameters, requires index.md, validates the front-matter against the form and the size against MAX_CONTENT_BYTES, then enables signing.
  • Reviews and comments are one Markdown file with a raw-codec CID (§6.1): the bytes must fit one chunk (≤ 256 KiB, enforced in the composer) so that CID = sha2-256(bytes) with codec 0x55; they are pinned through the same provider (pinRaw) and read back through the gateway as /ipfs/<cid>.
  • Dev gateway: for devOnly networks with ipfsGateway empty, the app serves ARIADNE_CONTENT_DIR at /api/dev/content/<cid>[/path] (plain files, and ?format=car for trustless fetches). This is the only case where assets share the app’s origin; on public networks assets come from the manifest’s gateway origin, as §16 requires. Article Markdown is read on the server (from the directory in dev, from the gateway otherwise) and rendered with react-markdown + remark-gfm + remark-math, sanitised with rehype-sanitize (default schema plus the math-inline / math-display classes) before rehype-katex; relative URLs are rewritten to the gateway, javascript: and data: URLs are dropped.
  • Spec refs: §6.1, §14.1, §14.3, §14.4, §14.5, §16 · Tests: projects/web/test/content.test.ts, lib.test.ts, localnet/flow.test.ts

D-083 Web app accepted against LocalNet (M4 step 1 of D-079)

Section titled “D-083 Web app accepted against LocalNet (M4 step 1 of D-079)”
  • Date: 2026-09-30 · Status: accepted
  • Built and verified: next build clean (routes /, /[network], /[network]/a/[id], /[network]/publish, /[network]/u/[address], /[network]/inbox, /[network]/fields, the two dev API routes and the proxy), tsc --noEmit and prettier clean; vitest 18 tests: 14 offline (key lengths, every deposit of the M2_REPORT table, extend() counts, fee shapes, CID shape, status tables, network routing, canonical CID of the fixture from the browser importer, CAR round trip with a corrupted block rejected, front-matter validation) and 4 on LocalNet through src/lib/ariadne.ts with generated Ed25519 signers: publish with 7 declared co-authors (11 boxes, one extend(), 3 transactions), preview deposit 417 300 + fee 4 000 equal to the balance delta; co-author confirmation; a stranger’s vote; a declared co-author who votes is then refused confirmation; claim_reputation priced at 50 600 (both boxes) and 0 the second time; review (91 100), useful vote, comment with two mentions (81 800), reply, upvote, resolve, flag refused below MIN_FLAG_REP, new version, status change, claim of the frozen ASA with opt-in in the group, retraction, every fee equal to its preview.
  • In the browser (Next dev server, the M3 indexer serving with the dev content directory, KMD wallet of LocalNet): connected, published article #3 from the template (CID computed in the browser, pinned to the dev directory, cost shown 0.2152 ALGO, fees charged 0.003 ALGO as previewed), the article page rendered the Markdown with KaTeX and the figure through the dev gateway, the indexer validated the content as valid, then set the status to under review, posted a comment with math (raw CID pinned, 0.0838 ALGO with the first reputation box), and claimed the article token (opt-in + claim_article, 0.106 ALGO shown); profile and inbox pages rendered for the wallet.
  • Fixes found only by running it: use-wallet’s transactionSigner is a new function on every render, so useChain keeps it behind a stable wrapper (otherwise the chain connection flapped and the sign button was intermittently disabled); NetworkConfigBuilder.addNetwork refuses the reserved id localnet (use localnet()); the shared transaction status of the author panel is reset when another panel opens; the generated client is compiled with // @ts-nocheck because it does not meet exactOptionalPropertyTypes (scripts/patch-client.mjs).
  • Not exercised in the browser (covered at the library level by the flow test): voting, reviewing and flagging from a second wallet, network switching with more than one enabled network, Pera/Defly/Lute (no public network yet). Remaining for M4: indexer + gateway hosting, Pinata provider, TestNet deployment and governance key, publishing guide.
  • Spec refs: §16 · Tests: projects/web/test, manual run recorded here

M4 steps 2-4 (hosting, TestNet) — recorded 2026-10-01, before the code

Section titled “M4 steps 2-4 (hosting, TestNet) — recorded 2026-10-01, before the code”

D-084 Indexer on public networks: start round, indexer catch-up, content through Kubo, second copy

Section titled “D-084 Indexer on public networks: start round, indexer catch-up, content through Kubo, second copy”
  • Date: 2026-10-01 · Status: accepted
  • Context: a fresh database starts at watermark 0; on TestNet that is 67 million rounds of scanning. Section 14.4 asks the indexer to pin every CID it validates as a second copy. The public gateway must not become an open proxy for arbitrary IPFS content.
  • Decision: the manifest entry gains startRound (the round the application was created in, written by the deploy script) and algoIndexer (an Algorand indexer URL used only for catch-up, AlgoNode for TestNet/MainNet). A database without a watermark starts at startRound - 1. With algoIndexer, polls use catchup-with-indexer (at most 100 000 indexer rounds per poll), otherwise sync-oldest on algod (AlgoNode’s algod is archival).
  • Content source order: ARIADNE_CONTENT_DIR (dev) > ARIADNE_KUBO_API (Kubo RPC dag/export, the CAR is block-verified and re-hashed exactly like a gateway CAR) > the manifest’s ipfsGateway (trustless CAR). With ARIADNE_KUBO_API the indexer pins through Kubo RPC pin/add every article CID it validated as valid and every review and comment CID (raw blocks, self-verifying); pin state lives in a new base table pins(cid, kind, article_id, pinned_at, attempts, last_error) that survives rebuilds. This is schema version 2, the first real migration (v1 databases gain the table in place).
  • The public gateway is the same Kubo with Gateway.NoFetch = true: it serves only blocks the node already holds (fetched by the indexer, pinned), never arbitrary content, and answers on its own origin with a sandboxing Content-Security-Policy.
  • Spec refs: §3.2, §14.4, §14.6, §15 · Tests: projects/indexer/test (start round, sync choice, Kubo source and pinner against a mock RPC, v1 -> v2 migration)

D-085 Pinata as the Path A provider on public networks

Section titled “D-085 Pinata as the Path A provider on public networks”
  • Date: 2026-10-01 · Status: accepted
  • Decision: the browser uploads the CAR it built (POST https://uploads.pinata.cloud/v3/files, multipart file, network=public, car=true, name) with the author’s own Pinata JWT and compares data.cid with the CID computed locally; any difference stops the flow (§14.4). Reviews and comments are uploaded the same way as a one-block CAR whose root is the raw CID, so the provider cannot re-encode them. The JWT is the author’s secret: kept only in localStorage of their browser under a per-origin key, shown masked, with a “forget” button, sent nowhere but Pinata; the UI tells the author to create a key restricted to uploads. Pinata’s CAR import needs a paid plan (D-026); without a JWT the flow offers Path B.
  • Path B on public networks fetches through a public trustless gateway (fetchGateway in the manifest, default https://trustless-gateway.link) because our own gateway does not fetch (D-084); every block is verified, so the gateway is not trusted.
  • Spec refs: §14.4 · Tests: projects/web/test/pinata.test.ts (mock upload endpoint: CID equality, mismatch refusal, errors)

D-086 Hosting: one VPS, Docker Compose, Caddy

Section titled “D-086 Hosting: one VPS, Docker Compose, Caddy”
  • Date: 2026-10-01 · Status: accepted (provisioning is the owner’s action)
  • Decision: deploy/ holds a Compose stack for one Linux VPS (Ubuntu 24.04, 2 vCPU, 4 GB, 40 GB is enough for TestNet): indexer-testnet (later indexer-mainnet, one process per network, D-076), web (one Next server for every enabled network), kubo (IPFS node: RPC internal only, gateway with NoFetch), caddy (automatic HTTPS for three hostnames: the app, api. and ipfs.). Images are pinned by digest: node 26.5.1-bookworm-slim sha256:9e6f9357d371591e32ab6f2d8a26d63bdd0d17c29eee3f4f3e7e454d9634bf73, ipfs/kubo v0.43.1 sha256:b293923d66e490e70ced64df42ea7a6cf7eac2740e3fb29101df18070fa7be48, caddy 2.11.2-alpine sha256:834468128c7696cec0ceea6172f7d692daf645ae51983ca76e39da54a97c570d. The repository checkout is the source of the manifest (mounted read-only), so enabling a network or filling an app id is git pull + restart. Server-side calls use internal addresses (ARIADNE_INTERNAL_INDEXER_<NET>, ARIADNE_INTERNAL_GATEWAY, ARIADNE_INTERNAL_ALGOD_<NET>), the browser uses the manifest’s public URLs. Only ports 80/443 (Caddy) and 4001 (IPFS swarm) are exposed.
  • The stack is validated on this machine against LocalNet with deploy/compose.local.yml (hostnames *.localhost, Caddy’s internal CA) before any VPS exists.
  • Spec refs: §3.2, §15, §16 · Tests: deploy/README.md checklist, local stack run recorded in D-088

D-087 TestNet governance key and deployer (amends D-043 for TestNet only)

Section titled “D-087 TestNet governance key and deployer (amends D-043 for TestNet only)”
  • Date: 2026-10-01 · Status: accepted
  • Context: seeding 42 fields needs 42 governance-signed groups; signing them offline with algokey pq sign is 84 manual signatures for a network whose coins have no value.
  • Decision: on TestNet the governance Falcon-1024 key is generated by projects/contracts/scripts/testnet_keys.py from 32 random bytes into %USERPROFILE%\.ariadne\testnet-governance.seed (outside the repository, never printed); the same script creates the Ed25519 deployer and writes its mnemonic to the gitignored projects/contracts/.env.testnet. Only the two addresses are printed, for the owner to fund at the TestNet dispenser (a captcha or GitHub login: the owner’s action). The deploy script accepts GOVERNANCE_PQ_SEED_FILE on TestNet, seeds the fields, and records startRound and algoIndexer. MainNet keeps D-043 unchanged (key generated and held offline by the owner; decided again before MainNet).
  • Keys generated on 2026-10-01 (addresses only; the seed and the mnemonic never left the owner’s PC): TestNet governance (Falcon-1024) ZAL425O4GBTJWID26DSTEWFU5JZGUJIJQ52GO6WVLMZEVXZTNQN63JBGJI, deployer 5MHQYUCI2SQW4THJSOTFKWHFZCAVPU6TWOTRUHMEH6LFCDAIXZRRLFYL7A. Needed: 0.8974 ALGO and 0.3805 ALGO. A dry run of algokit project deploy testnet read .env.testnet, loaded the seed, checked both balances on TestNet and stopped before sending anything.
  • Spec refs: §12.1, §3.2 · Tests: deploy on LocalNet with the same code path; TestNet run recorded when funded

D-088 Hosting stack rehearsed on the development PC (M4 step 2 prepared; the VPS itself is the owner’s action)

Section titled “D-088 Hosting stack rehearsed on the development PC (M4 step 2 prepared; the VPS itself is the owner’s action)”
  • Date: 2026-10-01 · Status: accepted
  • Built and run: deploy/docker-compose.yml with local-stack.env (LocalNet on the host, *.localhost names on port 8443, Caddy’s internal CA). Both images build from the repository (the web image prunes dev dependencies and next start runs from it); Kubo applies deploy/kubo/init.sh (Gateway.NoFetch = true, RPC on the container network only).
  • projects/web/test/stack/stack.test.ts (run with ARIADNE_STACK=1) passed in 21 s: a new Ed25519 author posts a directory CAR to the stack’s dev pin target, which unpacks it and imports it into Kubo; the author publishes on LocalNet; the containerised indexer (starting at the manifest’s startRound) ingests it, fetches the directory through Kubo RPC, validates it as valid and pins it; the gateway serves index.md and the figure with the sandboxing CSP and refuses a CID the node does not hold; a comment’s raw CID is pinned too (2 pins); the app renders the article server-side through the internal indexer and gateway, with KaTeX and asset URLs on the gateway origin; unknown API paths are 404.
  • Fixes found by running it: an empty ACME_EMAIL makes Caddy refuse the configuration, so the variable is now mandatory in the compose file; Node’s lookup option is called with { all: true } (test helper); the indexer logged every idle poll and every API request: it now logs polls with calls or catch-up, and only 5xx responses (Caddy keeps the access log).
  • Pins added while wiring wallets after D-079: @txnlab/use-wallet-kmd 5.0.1, -mnemonic 5.0.1, -pera 5.0.1, -defly 5.0.1, -lute 5.0.0; server-only 0.0.1. The root .gitignore (Python template) ignores lib/: projects/web/.gitignore re-includes src/lib/. Automatic git maintenance is disabled in this clone (maintenance.auto false, gc.auto 0): the geometric repack after each commit hung the shell twice.
  • Remaining for M4, all needing the owner: a server and a domain (deploy/README.md section A), TestNet funding at the dispenser (section D; scripts/testnet_keys.py prepares the keys), a paid Pinata account for Path A on TestNet.
  • Spec refs: §14.4, §15, §16 · Tests: projects/web/test/stack, projects/indexer/test/public.test.ts, projects/web/test/pinata.test.ts

D-089 Content checks are scheduled so that unreachable content never delays a new article

Section titled “D-089 Content checks are scheduled so that unreachable content never delays a new article”
  • Date: 2026-10-01 · Status: accepted
  • Context: the first run of the stack against a LocalNet holding 80 articles whose CIDs exist nowhere (contract test data) left a new article unvalidated: the indexer tried the backlog in article order, each fetch through Kubo waiting up to 120 s. On a public network the same happens with any author whose content is unreachable.
  • Decision: each content cycle checks at most ARIADNE_CONTENT_PER_CYCLE (20) CIDs, ARIADNE_CONTENT_CONCURRENCY (4) at a time; never-checked CIDs come first, newest article first; re-checks follow, least recently checked first; every free worker re-reads the queue, so an article published during a cycle is taken by the next free worker; a Kubo fetch gives up after ARIADNE_CONTENT_TIMEOUT_SECONDS (30, passed to Kubo as timeout= too). Unavailable content is retried at the normal re-check interval (6 h).
  • Pinning (the second copy) runs in its own loop, 4 pins at a time, newest article first, 60 s per pin (Kubo timeout= too), failures retried with the D-084 backoff: the second run showed the content loop blocked behind sequential 5-minute pins of the contract suite’s unreachable review and comment CIDs. With both fixes the stack acceptance test passes in 32 s with 80 unreachable articles and their reviews and comments in the backlog.
  • Spec refs: §14.3, §14.6, §15 · Tests: projects/indexer/test/public.test.ts (order, cap, parallelism), projects/web/test/stack

D-090 First server: OVHcloud VPS-1, sslip.io names, no ACME e-mail

Section titled “D-090 First server: OVHcloud VPS-1, sslip.io names, no ACME e-mail”
  • Date: 2026-10-01 · Status: accepted
  • The owner chose OVHcloud VPS-1 after a price comparison (4.61 EUR/month with VAT against about 29 USD for the Vultr plan first suggested): 2 vCores, 3.7 GiB RAM, 38 GB disk, Ubuntu 26.04 LTS, IPv4 SERVER_IP. Docker comes from Docker’s official apt repository (it supports 26.04, codename resolute).
  • Access: a dedicated ed25519 key created on the owner’s PC (~/.ssh/ariadne_vps); the owner installed its public half and was asked to change the provider-issued password, which had been pasted in chat. The assistant never uses the password. Password login stays enabled; disabling it is the owner’s call.
  • Until a domain exists, the three hostnames are sslip.io names that resolve to the IP (SERVER-IP.sslip.io, api.…, ipfs.…); Let’s Encrypt issues real certificates for them. Moving to a domain is an .env change and a restart.
  • No ACME e-mail: Let’s Encrypt stopped sending expiry e-mails in 2025, so Caddy runs without one and the owner’s address goes to no third party (ACME_EMAIL removed from the compose file and the Caddyfile).
  • IPFS_STORAGE_MAX 15GB for a 40 GB disk; COMPOSE_PROFILES stays empty until the TestNet app exists (an indexer without an app id refuses to start), then testnet.
  • Spec refs: §15, §16 · Tests: deploy/README.md checks on the server, recorded in D-091

D-091 TestNet live on the first server (M4 steps 2 and 3)

Section titled “D-091 TestNet live on the first server (M4 steps 2 and 3)”
  • Date: 2026-10-01 · Status: accepted
  • Contract: algokit project deploy testnet created app 772965337 (application address OPQ75K3R6KYS4GFICQXKJASZFVCMBY2CD5V6JFFQGAP5WMQTJRUQ4VB2KE, creation round 67842232, transaction 2JRKOVA6ZBNDRFUP26KTOHGIGC7A3UHVBZUOCEHJL6NPB6YO235A), funded it with exactly 100 000 microAlgo and seeded the 42 fields in six groups signed by the Falcon-1024 governance ZAL425O4GBTJWID26DSTEWFU5JZGUJIJQ52GO6WVLMZEVXZTNQN63JBGJI on the public TestNet. The manifest’s testnet entry carries appId, genesisHash, governance, startRound, indexerApi https://api.SERVER-IP.sslip.io/testnet, ipfsGateway https://ipfs.SERVER-IP.sslip.io and enabled: true.
  • Server (OVHcloud VPS-1, D-090): system updated and rebooted for libc6, Docker 29.8.2 / Compose 5.5.1 from Docker’s repository, ufw allowing 22, 80, 443 (tcp and udp) and 4001; the repository arrives as a git bundle in /opt/ariadne; deploy/.env holds only hostnames and profile. Let’s Encrypt issued certificates for the three sslip.io names (issuer YE2, valid to 30 Dec 2026).
  • Checks: the TestNet indexer started at round 67842232 and caught up through AlgoNode’s indexer in 3 s (43 events, 42 fields); https://api.SERVER-IP.sslip.io/testnet/status answers network: testnet and the app id; the web serves /testnet with the TestNet banner, the network selector offers only TestNet, /testnet/fields lists 42 registered fields, the console is clean; from the PC, ARIADNE_TEST_NETWORK=testnet runs the catch-up comparison against the public chain and passes.
  • Fix found on the server: the banner read “testnet” because server components imported the label table from a 'use client' module, where a non-component export is only a client reference; the table now lives in src/lib/networks.ts.
  • Not exercised yet on TestNet: a publication from a browser wallet (needs the owner’s Pera/Defly/Lute wallet with TestNet ALGO, and either a paid Pinata key for “Upload” or a CID pinned elsewhere for “I have a CID”).
  • Spec refs: §3.2, §12.1, §15, §16 · Tests: listed above

D-092 Filebase as a second Path A provider (the owner uses Filebase, not Pinata)

Section titled “D-092 Filebase as a second Path A provider (the owner uses Filebase, not Pinata)”
  • Date: 2026-10-01 · Status: accepted
  • Context: the owner has a Filebase account and asked how to hand over its access key and secret. Provider credentials are the author’s secrets (§14.4): they never go to chat, to the assistant or to Ariadne servers.
  • Decision: the Upload tab offers Pinata or Filebase; the author picks one and pastes its key in their own browser, where it is stored (localStorage, one provider at a time) with a “forget” action. Filebase is used through its IPFS RPC API: POST https://rpc.filebase.io/api/v0/dag/import?pin-roots=true, multipart field file = the CAR built in the browser, Authorization: Bearer <token>; the root CID in the NDJSON answer (Root.Cid./) must equal the local CID and PinErrorMsg must be empty. Reviews and comments go up as one-block CARs, as with Pinata. The token is Filebase’s bucket-specific “IPFS RPC API” token (Access Keys page), not the S3 access key and secret, which Ariadne never uses. The endpoint answers CORS preflights with Access-Control-Allow-Origin: * and allows the Authorization header (checked 2026-10-01), so the upload goes straight from the author’s browser to Filebase.
  • Spec refs: §14.4 · Tests: projects/web/test/pinata.test.ts (Filebase mock: request shape, root equality, pin error, 401)

D-093 What an article may contain, pinned by tests

Section titled “D-093 What an article may contain, pinned by tests”
  • Date: 2026-10-01 · Status: accepted
  • The renderer (react-markdown 10 + remark-gfm + remark-math + rehype-sanitize + KaTeX) accepts CommonMark with the GitHub extensions (tables, task lists, strikethrough, footnotes, autolinks) and LaTeX mathematics between $...$ (inline) and $$ lines (display). $$...$$ on one line is inline math, so the templates and the guide now use the block form (the committed fixture keeps its one-line formula: changing it would change its CID). Raw HTML is never rendered: inline tags are dropped and their text kept, HTML blocks are dropped with their content.
  • Images are embedded only from the article’s own directory on the gateway; an external image is shown as a link (“external image not shown”), because it could change, disappear or track readers (the guide already required relative paths; the renderer now enforces it). Links: http(s), mailto, in-page anchors and files of the directory (rewritten to the gateway); every other scheme is dropped.
  • The front-matter parsers of the web and of the indexer no longer cut a value at a # inside quotes or brackets, and no longer split a quoted list item at its commas ("Results #2", ["heart rate, recovery"]).
  • Spec refs: §14.2, §16 · Tests: projects/web/test/markdown.test.ts, content.test.ts; projects/indexer/test/content.test.ts

D-094 The upload template follows the form; the cost box says whether the account can pay

Section titled “D-094 The upload template follows the form; the cost box says whether the account can pay”
  • Date: 2026-10-01 · Status: accepted
  • Context (owner, TestNet): choosing type “notes” still produced type: article in the Upload template, and signing with an unfunded wallet showed algod’s raw “overspend” error.
  • Decision: the template is built from the form (authors with the declared co-authors, field, secondary field, type, parent) with a body suited to each type; the draft lives in the publish form, so going back to the metadata keeps the text, and continuing rewrites only the checked header keys (syncFrontMatter); a “Fill the header from the form” button does the same on demand. The cost box shows the ALGO the connected account can spend (balance minus its minimum balance) and warns when it is not enough, with the TestNet dispenser link on TestNet; an overspend or below-minimum error from any action is reported in plain words.
  • Spec refs: §14.2, §16 · Tests: projects/web/test/content.test.ts (templates for the five types, header sync with block lists, missing header and CRLF), lib.test.ts (low-funds recognition)

D-095 Visual refresh: application shell, neutral tokens, empty states (owner request, incremental)

Section titled “D-095 Visual refresh: application shell, neutral tokens, empty states (owner request, incremental)”
  • Date: 2026-10-01 · Status: accepted
  • Context: the owner asked for a modern, sober, neutral interface that uses the whole viewport, with a clear hierarchy (navigation, network/wallet context, content, filters/actions, empty states), accessible control states and a proper mobile pattern, without touching routes, data or behaviour and without new dependencies.
  • Decision: one stylesheet (app/globals.css) rewritten on design tokens (neutral greys, one blue accent, 6-8 px radii, borders instead of shadows, a system font stack, light and dark), keeping every existing class name. Desktop (>= 1024 px): a full-height left sidebar (brand, main navigation with the current page marked, network and wallet at the bottom) and a content area that uses the width, while text keeps a reading measure of about 72 characters. Tablet: the sidebar becomes a sticky top bar. Phone (< 640 px): a compact top bar with network and wallet, and the five sections as a bottom tab bar (text only, one tap, no hidden menu). The feed gets a page header, a Top/New segmented control, a filter panel that sits beside the list on wide screens and above it on narrow ones, abstracts clamped to three lines, and empty states that say what to do next (publish, read the guide, or clear the filters). Inputs, selects and buttons get visible hover, focus and disabled states; a skip link leads to the content.
  • Kept on purpose: the coloured TestNet banner (§16.1 requires a persistent one off MainNet; it is now a slim strip) and §16’s “no cards or shadows” (lists are rows with dividers inside one bordered container; panels only for forms, filters and empty states).
  • Spec refs: §16, §16.1 · Tests: unchanged behaviour (vitest, next build); visual checks at 1440, 1024, 768 and 375 px recorded in the commit

D-096 ARIADNE wordmark, colour-coded fields chosen by name, an About page (owner proposals)

Section titled “D-096 ARIADNE wordmark, colour-coded fields chosen by name, an About page (owner proposals)”
  • Date: 2026-10-01 · Status: accepted
  • Name: the interface writes the name in capitals, ARIADNE, everywhere a reader sees it (wordmark, titles, page texts, the guide and the About page). The wordmark is the word alone, drawn as thin, widely spaced geometric capitals in an inline SVG that follows the text colour (light and dark), so it looks the same on every system without a web font; the favicon is its first letter. Code, package names and developer documents keep “Ariadne”.
  • Fields: each of the six OECD areas has a colour (a muted palette that stays distinguishable for the common colour-vision deficiencies, light and dark variants), and every field appears as a small tag with a dot of its area’s colour and the field’s name; the code remains only as a tooltip and in the URL. Colour is never the only cue: the name is always written. Every field picker is a list grouped by area with the field names (the feed filter, the publish form’s primary and secondary fields); nobody types a code.
  • About: /<network>/about, rendered from spec/about.md, in two layers: “In short” (what ARIADNE is for, its principles, what the blockchain does here in plain words, costs, TestNet) and “In detail” (what is on chain and on IPFS, the article lifecycle, co-authors, vote weight, every reputation rule with the exact arithmetic and a worked example, flags and disputes, governance, deposits, what runs off chain, known limits). Every number in it comes from the contract constants and the M2 measurements.
  • Found while writing it: the article page said the secondary field receives “the same amount” when claiming; the contract gives half (granted_primary // 2, §10.2); the text is corrected. KaTeX now runs with the limits §18 asks for (trust: false, maxSize, maxExpand, no error thrown).
  • Layout found in the visual check: on phones the wordmark is drawn smaller so brand, network and wallet stay on one 56 px line (at 375 px they wrapped and doubled the sticky header); field tags let long names wrap inside the tag instead of cutting them (“Electrical engineering, electronic engineering, information engineering”).
  • Spec refs: §1, §4, §10, §16, §18 · Tests: projects/web/test/branding.test.ts (sections, area mapping, field tags, field pickers, wordmark, app links only in our own documents, KaTeX untrusted, every number of the About page against the code constants), visual checks at 1440, 1280 and 375 px, light and dark

D-097 NFD names in place of addresses; mentions by @name.algo or @ADDRESS (owner request)

Section titled “D-097 NFD names in place of addresses; mentions by @name.algo or @ADDRESS (owner request)”
  • Date: 2026-10-01 · Status: accepted
  • Names: wherever the interface shows an address (feed, article authors, reviews, comments, inbox, profile, the connected wallet, co-author list of the publish form), it shows the NFD name the owner has verified for it, else the short address; the full address stays in the tooltip and the profile is one click away. Only names that are syntactically valid ([a-z0-9]{1,27}(.[a-z0-9]{1,27})?.algo), unexpired and verified for that address (caAlgo contains it) are shown. Each network names its NFD API in spec/networks.json (nfdApi: MainNet https://api.nf.domains, TestNet https://api.testnet.nf.domains, LocalNet none): names never cross networks.
  • Privacy: the reader’s browser never contacts NFD. The web server resolves names (reverse lookup in batches of 20, forward lookup for typed names) with a bounded in-memory cache (30 minutes, “no name” included; failures are not cached) and serves them to the browser through /api/nfd; server pages resolve their own names before rendering.
  • Profiles: /<network>/u/<name>.algo redirects to the profile of the name’s first verified address; a profile with a name shows it as the title with the full address under it and says the name comes from NFD and was chosen by the owner.
  • Mentions: the comment text is the source. @ADDRESS (checksum valid) and @name.algo anywhere in it are mentioned (deduplicated, at most 5, never the sender); a name counts only through its first verified address, resolved when the comment is written, and that address is what the chain records. The picker (participants by address or name, a pasted address, a typed name) inserts @ADDRESS into the text. Unresolvable names are reported before signing. Comment and review bodies render @ADDRESS as the person (name or short address, linked) and @name.algo as a link to that name’s profile; code, math and links are left alone, and articles are not touched. The indexer now returns each comment’s recorded mentions and the page shows them as “Notified”.
  • Spec refs: §9, §15, §16 · Tests: projects/web/test/people.test.ts, projects/indexer/test/api.test.ts (comments carry their mentions)

D-098 ARIADNE is funded by donations (owner request)

Section titled “D-098 ARIADNE is funded by donations (owner request)”
  • Date: 2026-10-01 · Status: accepted
  • Decision: the About page says how ARIADNE is funded (no fees, no token, no advertising, no investors; deposits are locked storage, not income) and links to /<network>/donate, which explains where the money goes and shows the donation account: one Algorand MainNet address (optionally its NFD) with a QR code (ARC-26 algorand:// URI), copy and “open in wallet” buttons, and a link to its public history on the explorer. Every page has a footer line “ARIADNE is free and runs on donations. Support it”.
  • The account is configured in spec/donations.json (network, address, nfd, optional https links to other ways to give), so that changing it is a reviewed commit and never a server setting; invalid entries are dropped and logged, never shown. Until the owner publishes an address the page says the account is being set up. The owner creates the account in their own wallet (a dedicated one is advised) and only its address enters the repository.
  • Spec refs: §16 · Tests: projects/web/test/people.test.ts (file valid, validation, About text)

D-099 Account menu, theme switch, review replies and author votes in the interface (owner feedback)

Section titled “D-099 Account menu, theme switch, review replies and author votes in the interface (owner feedback)”
  • Date: 2026-10-01 · Status: accepted
  • Account: the connected wallet is a button with the account’s name or short address that opens a menu: My profile, Inbox, Copy address, Disconnect. It renders after hydration only, so the server’s “Connect wallet” never mismatches the account the browser remembers.
  • Theme: System, Light or Dark, in the sidebar on desktop and in the footer below 1024 px; the choice lives in this browser (localStorage, key ariadne.theme) and is applied before the first paint by a beforeInteractive script; the dark palette applies when the system prefers dark unless Light was chosen, and always when Dark was chosen.
  • Reviews: the contract has no replies to reviews; “Reply” on a review opens the new-comment box prefilled with “Re: review #N by @reviewer”, so the reviewer is notified. The interface hid “Useful” on reviews and “Upvote” on comments from the article’s authors, which D-057 allows; both are now offered (an author’s useful vote counts in the review’s total and grants nothing, and the cost panel says so).
  • Spec refs: §8.2, §16 · Tests: projects/web/test/people.test.ts (theme), visual checks on LocalNet with a KMD wallet and on TestNet data
  • Date: 2026-10-01 · Status: accepted (owner registered the domain and its DNS)
  • Decision: the first server answers at https://ariadne.press (app), https://api.ariadne.press (indexer APIs) and https://ipfs.ariadne.press (gateway); three A records to SERVER_IP, no AAAA, no CAA; certificates from Let’s Encrypt through Caddy as before (D-090). “Press” reads as a scholarly publisher; with ariadne.algo (NFD, for donations, D-098) the names match.
  • Transition: the former app name SERVER-IP.sslip.io redirects permanently to the domain with its path (LEGACY_APP_HOST), so links already shared keep working; the former API and gateway names are still served beside the new ones (a host variable may list several names). spec/networks.json (TestNet) points to the new API and gateway. What a browser stores is per site, so on the new domain a reader connects the wallet again and an author pastes the pinning key again.
  • Known: www.ariadne.press has no DNS record; with one, it is added to LEGACY_APP_HOST and redirects too. The EU trademark check for “ARIADNE” (an EU archaeology research infrastructure uses the name) remains the owner’s open item.
  • Spec refs: §3.2, §16 · Tests: Caddyfile adapted and validated with the pinned image (new domain, former names, unset legacy host); live checks after deployment

D-101 Actions shown by what the account can do; vote colour; folding threads (owner request)

Section titled “D-101 Actions shown by what the account can do; vote colour; folding threads (owner request)”
  • Date: 2026-10-01 · Status: accepted
  • Offered actions: the interface shows an action only when the contract would accept it from the connected account, and an action already taken (and not repeatable) stays as an inactive button that says so (“Voted”, “Upvoted”, “Useful”, “Flagged”, “Resolved”, “Claimed”). Without a wallet only counts are shown. Rules, as pure functions in src/lib/rules.ts where they encode the contract: vote on the article (not an accepted author, open status); flag (not an author, status allows it, at least MIN_FLAG_REP in the primary field; flagOffered); useful on a review (not its reviewer; authors may, D-057); upvote a comment (not its author); resolve (an accepted author, on an open comment by a non-author, §8.3; resolveOffered, which also fixes the interface offering it on authors’ comments); claim reputation only when something is claimable; review form hidden once reviewed.
  • State: a new indexer route GET /articles/<id>/viewer/<address> returns what that address did on the article (article vote, review and comment votes, review written, flag in the current round, reputation in the primary field). The page asks it once the wallet is known; an action that just succeeded is kept locally until the indexer has it, and the page refreshes its counts 4 and 12 seconds later. The preview’s on-chain check still catches a vote the indexer has not seen yet.
  • Look: votes, “useful” and upvotes in a green vote colour (--vote, light and dark) with their totals inside the button; one click shows the cost under the row of actions, a second confirms; flags in the danger colour; Reply, fold and secondary actions as light buttons; review recommendations coloured (accept green, minor revision blue, major revision amber, reject red); resolved comments and authors tagged.
  • Threads: every comment with replies folds and unfolds (“Hide replies” / “Show N replies”), and “Collapse all” / “Expand all” act on the whole list; threads deeper than three levels keep “continue thread”.
  • Hydration: wallet-dependent rendering waits for hydration (useHydrated), so the server’s “Connect a wallet” never mismatches the account a browser remembers (article actions, inbox, wallet button).
  • Also: www.ariadne.press now has its DNS record and redirects to ariadne.press (LEGACY_APP_HOST lists it), completing D-100.
  • Spec refs: §8, §16 · Tests: projects/web/test/actions.test.ts, projects/indexer/test/api.test.ts (viewer route); visual checks on LocalNet with a KMD wallet (voter, author and read-only views; a real upvote; folding; dark mode; phone)

D-102 Donation account published: ariadne.algo

Section titled “D-102 Donation account published: ariadne.algo”
  • Date: 2026-10-01 · Status: accepted (owner)
  • Decision: donations go to BJLW77K3PXDYX7IJTZYPPNLXOMNI7CVKKGTQ5MVEBR2JZIJXQBQRWI6UC4, the owner’s account behind ariadne.algo (checked on 2026-10-01: valid checksum; NFD lists it as the verified address and the deposit account, and the reverse lookup of the address returns ariadne.algo). spec/donations.json carries both and a second way to give: NFD segments under ariadne.algo (yourname.ariadne.algo), of which $5 per segment goes to ariadne.algo. Links in the file may carry a one-sentence note (at most 200 characters).
  • The account holds no USDC opt-in today, so the page asks for ALGO; with an opt-in to USDC (ASA 31566704) the page can say “ALGO or USDC”.
  • Spec refs: §16 · Tests: projects/web/test/people.test.ts (the published file, the note limit)

D-103 Presentation: statuses, relative times, notices, profile, citations, contents, avatars (owner request)

Section titled “D-103 Presentation: statuses, relative times, notices, profile, citations, contents, avatars (owner request)”
  • Date: 2026-10-01 · Status: accepted
  • Statuses in colour, the word always written: preprint neutral, under review blue, final green, disputed amber, retracted red (feed, article, profile).
  • Relative times (“3 hours ago”, “yesterday”, the date past a year) with the exact UTC time in the tooltip; the server and the first browser render write the date (no hydration mismatch), the relative form follows.
  • Floating notices: a confirmed transaction is announced in a corner (“Vote confirmed”, round, fees, explorer link), stacked, closed after 7 s, read by screen readers; errors stay next to the button that caused them.
  • Unread badge on Inbox in the menu: unread notifications (read state shared with the inbox page through src/lib/inbox.ts; opening a notification marks it read) plus new activity of followed people (D-104); refreshed every minute while visible and on focus.
  • Comments ordered Oldest, Newest or Top (votes, ties oldest first) for top-level comments; replies keep the conversation’s order; the choice is kept in the browser.
  • Profile: picture, name or address, Follow and Copy address; a summary (total reputation, fields, publications, reviews, comments); reputation per field as bars in the colour of the field’s area, with vote weight and today’s grant.
  • Citations: “Cite” asks for the format (APA 7, MLA 9, Chicago author-date, Harvard, Vancouver, IEEE, BibTeX, RIS), starts on the reader’s default and offers “Use this format by default” (browser storage). Authors are written as ARIADNE knows them, NFD name or address (no real names on chain); the reference names the version read, its date, the publisher (“ARIADNE”, “ARIADNE TestNet”) and, in BibTeX and RIS, the CID. “Copy link” beside it. Formats are a pure function (src/lib/cite.ts).
  • Contents and reading time: from three second- or third-level headings a “Contents” list built from the rendered article (anchors sec-<slug>), beside the text from 1280 px (sticky, the section being read marked) and folded above it on smaller screens; “N min read” at 220 words a minute without code, formulas and link targets.
  • Avatars: the NFD avatar when the owner has one, fetched by this server only from NFD’s image service (https://images.nf.domains/, no redirects, raster only, at most 256 KiB, cached), otherwise a symmetric 5 x 5 pattern generated from the public key; served by /api/avatar/<network>/<address> with Content-Security-Policy: default-src 'none'; sandbox and nosniff, so the reader’s browser never contacts a third party. The NFD reverse lookup now uses view=thumbnail to learn the avatar with the name.
  • Spec refs: §16 · Tests: projects/web/test/presentation.test.ts; visual checks on LocalNet (KMD wallet: follow, notice after a real upvote, badge, profile) and on TestNet data (contents, citation dialog and default, phone)

D-104 Follow people (owner request; per browser for now)

Section titled “D-104 Follow people (owner request; per browser for now)”
  • Date: 2026-10-01 · Status: superseded by D-109 (on-chain follows, contract v3); the browser list is offered for publication once
  • Decision: “Follow” on a profile adds the address to a list kept in the browser for the connected account and network (at most 200). The inbox gets a “Following” tab with the public activity of the people followed (publications as submitter, confirmed co-authorships, reviews, comments), newest first, new items since the last visit marked, and the list itself with Unfollow. The indexer serves that activity: GET /activity?address=... (at most 50 addresses per request, limit, before). Nothing is written on chain and the server keeps no follow lists.
  • Known limit, raised by the owner: a list per browser does not follow the account across devices. The owner asked for it to be fixed even if it needs the contract; options and a recommendation (a follow method emitting an event, no box) are in ROADMAP.md, to decide before the MainNet contract.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/api.test.ts (activity route), projects/web/test/presentation.test.ts (follow list rules)
  • Date: 2026-10-01 · Status: accepted (owner opted the donation account in to USDC)
  • Decision: spec/donations.json lists the accepted currencies (ALGO, USDC); checked on 2026-10-01 that the account holds the opt-in to ASA 31566704 (Circle’s USDC, creator 2UEQTE5Q…). The donation page asks for “ALGO or USDC” and offers one wallet link per currency (algorand://<address>?asset=31566704 for USDC, ARC-26).
  • Spec refs: §16 · Tests: projects/web/test/people.test.ts

Contract v3: ORCID, DOIs, follows — recorded 2026-10-01, before the code

Section titled “Contract v3: ORCID, DOIs, follows — recorded 2026-10-01, before the code”

Owner decisions (2026-10-01): implement ORCID and Zenodo; Zenodo drafts are created by ARIADNE in the author’s account and published by the author; “this wallet is this ORCID iD” and “this article has this DOI” are events of the MainNet contract; follows move on chain with the same mechanism. Plan: ROADMAP.md (done items) and the sections below.

D-106 Contract v3: three event-only methods

Section titled “D-106 Contract v3: three event-only methods”
  • Date: 2026-10-01 · Status: accepted (owner decision; reopens §2 “ORCID link is v2”)
  • Decision: declare_identity(scheme: uint8, value: string), declare_doi(article_id: uint64, version: uint16, doi: string) and follow(target: address, on: bool) only emit IdentityDeclared, DoiDeclared and Followed (schema_version first, ts last, one log per call). No box, no deposit, no payment: the network fee only (0.001 ALGO; 0.003 from a Falcon wallet). Scheme 1 = ORCID; the generic method leaves room for ROR, ISNI and others without a new contract. An empty value withdraws (identity, DOI); on=false unfollows.
  • On-chain rules: scheme >= 1, value <= 64 bytes; a DOI is declared by an accepted author of an existing article, for a version <= the current one (0 = Zenodo’s concept DOI, every version), non-empty DOIs are <= 200 bytes, start with 10. and need a non-retracted article; a DOI can be withdrawn in any state; nobody follows the zero address or themselves. The ORCID check character is not checked on chain (about 300 opcodes, proves nothing about ownership): the web app and the indexer check it.
  • Consequences: program 6 266 bytes (4 pages, 3 of them extra), deployer funding counts 100 000 µAlgo per extra page (the earlier required_funding missed it). The indexer’s reducer mirrors these assertions exactly (a stricter reducer would stall ingestion).
  • Spec refs: §2, §11, §12 · Tests: projects/contracts/tests/unit/test_identity.py, tests/ref/test_ref_identity.py, tests/localnet/test_68_identity.py, projects/indexer/test/projections.test.ts

D-107 ORCID verified by cross-proof; never a source of reputation

Section titled “D-107 ORCID verified by cross-proof; never a source of reputation”
  • Date: 2026-10-01 · Status: accepted
  • Decision: the declaration is signed by the address (the event); the ORCID record lists https://ariadne.press[/testnet]/u/<ADDRESS> under “Websites & social links” with visibility “Everyone”. The indexer reads {orcidApi}/<iD>/person (names and websites in one call; a /read-public token when credentials are set, anonymous otherwise) and caches the result in orcid_checks: verified, unlinked, missing, invalid (check character), pending. Rechecked weekly; a failed check keeps the last result and backs off (1, 2, 4 … minutes, at most a day). The record’s names are shown only while verified. Writing to the ORCID record needs the paid member API, so the researcher adds the link by hand. “Sign in with ORCID” is a later option.
  • No reputation is derived from ORCID: reputation is earned in ARIADNE (§1, §9).
  • These checks are cached results of an outside service, like content (§1.6, §15): the declarations are rebuilt from the chain, the checks are not.
  • Spec refs: §1.6, §2, §15, §16 · Tests: projects/indexer/test/verify.test.ts, spec/fixtures/orcid.json

D-108 DOIs through Zenodo, verified against the record

Section titled “D-108 DOIs through Zenodo, verified against the record”
  • Date: 2026-10-01 · Status: accepted
  • Decision: ARIADNE creates a draft in the author’s Zenodo account (OAuth deposit:write, sandbox on TestNet) with the article’s files from our own gateway and its metadata (creators with verified ORCID iDs, licence, related_identifiers IsIdenticalTo the article page and the version’s CID, community ariadne-algo, DOI reserved); the author reviews and publishes it on Zenodo, then declares the DOI on chain. Published Zenodo records cannot be deleted. A DOI is verified when its record is published and IsIdenticalTo the article page and, for a version, that version’s CID (URLs can be reused across apps; CIDs cannot). Zenodo DOIs are checked with that network’s Zenodo records API; others with DataCite (MainNet) or shown as declared; a DataCite test DOI (10.5072) outside the sandbox is invalid. Later versions are Zenodo “new versions”. Crossref with ARIADNE’s own prefix stays future work.
  • Spec refs: §14, §15, §16 · Tests: projects/indexer/test/verify.test.ts
  • Date: 2026-10-01 · Status: accepted (owner decision)
  • Decision: follow(target, on) events; the indexer builds follows (active, since) and serves /users/:a/following, /followers and /feed. Only the first follow of a pair notifies the person followed (followed), so following and unfollowing never spams. The browser list of D-104 is offered for publication once (only the difference with the chain), then cleared; “seen” marks of the feed stay in the browser. Follows are public and permanent on chain; unfollowing only marks them inactive.
  • Spec refs: §11, §12, §15, §16 · Tests: projects/indexer/test/projections.test.ts, projects/indexer/test/api.test.ts

D-110 TestNet restarts with the v3 application

Section titled “D-110 TestNet restarts with the v3 application”
  • Date: 2026-10-01 · Status: accepted
  • Decision: an application is immutable (D-063), so v3 is a new TestNet application and the candidate for MainNet. The old application 772965337 stays on chain but is no longer indexed: its database is kept, its content stays pinned on our Kubo node, the About page says so, and authors may publish the same CIDs again. The indexer’s database path takes {appId} (/data/testnet-{appId}.sqlite), so a new application starts with a clean database.
  • Spec refs: §3.2, §19 · Tests: projects/indexer/test/verify.test.ts (configuration)

D-111 “Profile” in the main menu; the inbox opens from it (owner request)

Section titled “D-111 “Profile” in the main menu; the inbox opens from it (owner request)”
  • Date: 2026-10-01 · Status: accepted
  • Decision: the main menu (sidebar, top bar, phone tab bar) shows Profile instead of Inbox. It opens the connected account’s profile (/u/<address>), or /me, which asks for a wallet. The unread count (notifications plus new activity of the people followed) moves to Profile. On one’s own profile an Inbox button carries the same count, and the inbox links back to the profile; the account menu keeps both entries. One’s own profile and the inbox count as the Profile section.
  • Spec refs: §16 · Tests: projects/web/test/branding.test.ts (sections)

D-112 Notifications bell, top right (owner request)

Section titled “D-112 Notifications bell, top right (owner request)”
  • Date: 2026-10-01 · Status: accepted (refines D-111)
  • Decision: a bell in the top-right corner with the unread count in red (notifications plus new activity of the people followed). It opens a panel with the latest 8 notifications (unread marked; a click marks it read and opens the comment, the article or the new follower’s profile), “Mark all read”, a line for what is new from the people followed, and “Open the inbox”. On desktops it sits in a bar above the content that also carries the test-network notice; on tablets and phones in the top bar next to the wallet, and only with a wallet connected. The count leaves the “Profile” menu entry (no double badge); the Inbox button of one’s own profile keeps it. The followed notification of D-109 gets its text (“started following you”).
  • Spec refs: §16 · Tests: projects/web/test/presentation.test.ts (notification links and texts)

D-113 Health checks and an outside uptime monitor

Section titled “D-113 Health checks and an outside uptime monitor”
  • Date: 2026-10-01 · Status: accepted
  • Decision: the indexer’s GET /health answers 503 when no ingestion poll has succeeded for ARIADNE_STALL_SECONDS (300 by default): algod unreachable, or a round the reducer refuses (the chain and the reducer disagree, which must never pass silently). The containers of the indexer and the web app have Docker health checks (docker compose ps shows unhealthy). Outside the server, a free uptime service watched by the owner checks three addresses every five minutes and sends an email (or a phone notification) when one fails: the app, the indexer’s /health, and the gateway with an identity CID that needs no fetching (/ipfs/bafkqaaa).
  • Spec refs: §15 · Tests: projects/indexer/test/api.test.ts (health)

D-114 A new version starts from the current one (owner request)

Section titled “D-114 A new version starts from the current one (owner request)”
  • Date: 2026-10-02 · Status: accepted
  • Decision: “New version” loads the files of the current version from the gateway (verified against its CID) into the editor: index.md with the keys the chain checks (authors, fields, type, parent) rewritten from the article, and every other file of the directory. The template is used only when the current version cannot be read, and the page says so.
  • Spec refs: §16 · Tests: visual check; projects/web/test/localnet/flow.test.ts (new_version)

D-115 Replies to a review appear under it (owner request)

Section titled “D-115 Replies to a review appear under it (owner request)”
  • Date: 2026-10-02 · Status: accepted (refines D-099)
  • Decision: a top-level comment whose first line is “Re: review #N …” (what “Reply” on a review writes) is shown, with its whole thread, under review N, without that first line; the comments section counts the rest and says how many replies sit under the reviews. A comment naming a review that does not exist stays among the comments. Nothing changes on chain.
  • Spec refs: §16 · Tests: projects/web/test/feedback.test.ts

D-116 Articles are named by their title in lists (owner request)

Section titled “D-116 Articles are named by their title in lists (owner request)”
  • Date: 2026-10-02 · Status: accepted
  • Decision: profiles (reviews, comments, timeline), the inbox, the notifications bell and the follow feed name an article by its title, cut at a word near 60 characters (48 in the bell) with an ellipsis; “Article N” only while its content has not been read. The indexer adds title to the reviews, comments and timeline rows of a profile and to notifications.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/api.test.ts, projects/web/test/feedback.test.ts

D-117 Review of contract v3: text decoded from raw bytes, Zenodo hardening

Section titled “D-117 Review of contract v3: text decoded from raw bytes, Zenodo hardening”
  • Date: 2026-10-02 · Status: accepted
  • Indexer (critical finding): the contract counts the raw bytes of declare_identity and declare_doi values and accepts invalid UTF-8; algosdk decoded such text with U+FFFD (3 bytes each) and the reducer measured it again, so one transaction could stall ingestion for ever. Strings are now decoded as byte[] and turned into text without rejecting invalid bytes or dropping a leading BOM, and the reducer no longer re-measures what the contract measured (the ASCII “10.” prefix survives decoding). The Python oracle checks the raw bytes (surrogateescape). The verifier marks values that are not DOI-shaped invalid without a lookup, puts the whole DOI in one encoded URL segment for DataCite, checks a Zenodo DOI at the instance of its prefix (10.5281 zenodo.org, 10.5072 the sandbox) whatever the network’s own Zenodo, and asks for a new ORCID token once when one is refused. Article DOI links are built from the trimmed DOI, encoded. Every request to ORCID, Zenodo and DataCite carries a User-Agent naming ARIADNE: zenodo.org answers 403 to the default one of Node.js (found on the server on 2026-10-02).
  • Web: a draft never touches an unpublished new version the author already has on Zenodo (refused with a link to it) and only drafts made by the request are deleted on failure; one draft at a time on the server and one per session every 30 s; the gateway read has a timeout and a byte cap enforced while streaming; the request body is capped; keywords must be text; Zenodo’s 4xx keep their status; sealed values carry their purpose; the token cookie is SameSite=Strict. Each draft also carries a reading copy (README.md) for Zenodo’s file preview: title, authors, abstract and body without the front-matter, links into the canonical directory; the canonical files are unchanged. Following many addresses marks each confirmed group at once (no double fees after a refusal half-way), lists the addresses and drops invalid ones; the follow dialog keeps what it was opened for; the ORCID lookup shows results only for the iD they were found for; the bell gives focus back on Escape and closes when focus leaves it. The DOI form shows the network’s own Zenodo prefix as example and warns about a DOI of the other Zenodo.
  • Spec refs: §11, §15, §16 · Tests: projects/indexer/test/bytes.test.ts, verify.test.ts; projects/contracts/tests/ref/test_ref_identity.py; projects/web/test/zenodo.test.ts, feedback.test.ts, presentation.test.ts
  • Date: 2026-10-02 · Status: accepted
  • Decision: /<network>/status, linked from the footer only (after “Publishing guide”), shows what each part reports now, refreshed every 15 s while the page is visible: the application (live, articles published, governance against the manifest) and the deposits it holds; the Algorand node (round, age of the last block, genesis against the manifest) and the Algorand indexer (rounds behind); the ARIADNE indexer (ingestion, rounds behind the chain, last successful poll, counts), the second copy (pins to retry) and the ORCID/DOI checks; the website (uptime, memory) and the IPFS gateway; the governance account (warning under 1 ALGO) and the donation account on MainNet (ALGO and USDC); ORCID, Zenodo (and whether drafts are configured), DataCite and NFD. The checks run on the server in parallel with 6 s timeouts (GET /api/status?network=), one report per network every 10 s whatever the number of readers, with nothing internal in it (no internal address, no secret, failures as “not reachable”, “answered 503”, “no answer within 6 s”). The headline is “down” only when the node, the application or the indexer is down.
  • Spec refs: §15, §16 · Tests: projects/web/test/status.test.ts

D-119 Installable app (PWA) (owner request)

Section titled “D-119 Installable app (PWA) (owner request)”
  • Date: 2026-10-02 · Status: accepted
  • Decision: a web app manifest (/manifest.webmanifest from app/manifest.ts: name, standalone window, start URL /, which opens the network last used; icons 192, 512 and a maskable 512 generated from the mark by scripts/pwa-icons.mjs; an Apple touch icon), theme colours for light and dark, and a service worker (/sw.js, registered in production builds only). The service worker never serves protocol data from a cache: pages always come from the network, with a small offline page when there is none; only the build’s hashed static files are cached (at most 300); requests to /api and to other origins (the indexer, algod, IPFS, wallets) are not touched. “Install app” appears in the footer when the browser offers installation, and on iPhone and iPad with the Safari steps. The PWA files live at the root with a dotted name, which the network proxy lets through.
  • Spec refs: §16 · Tests: projects/web/test/pwa.test.ts
  • Date: 2026-10-02 · Status: accepted
  • Decision: a Search page (/<network>/search, in the main menu after Feed; on phones Guide and About leave the tab bar for it and stay in the footer; the feed has a search field) with three tabs: articles, reviews and comments, people. The page’s state is its URL (shareable, back and forward work, no script needed); choices apply at once, typed values with Enter. Articles: full text over title, abstract, keywords, body, authors (address, verified ORCID name and iD), field names, DOIs and CID, with stemming, accents and case ignored, phrases, exclusions, OR, prefixes, limits to one part (title: …), and exact lookups by DOI, CID or #id; name.algo in the query or in a people filter becomes that address (NFD). Filters, each option with its count under the other filters: area, field (main field only), type, status (retracted hidden unless chosen), year and dates of publication and of the last version, reviewed or not, the recommendations received, vote weight, comments, DOI (shown ones only, verified), authors with a verified ORCID iD, author (submitter only), reviewer, commenter, ORCID iD, people I follow (connected wallet), language and licence (front-matter), keywords, revised, amendments and the article amended, content checked. Sorts: best match, top, newest, oldest, most voted, most reviewed, most discussed, recently updated. Reviews and comments: by their text, kind, recommendation, author, article, field, dates; a reply to a review says so. People: by verified ORCID name, iD or address, role, verified ORCID, reputation in a field. Results show the matched words highlighted in the title, abstract, body or post (as text, never HTML), authors with their ORCID names, counts and the DOI; removable chips list the filters in use; pages of 20. Suggestions while typing (titles open the article, keywords and fields become filters, people open the profile) and the last 8 searches, kept in this browser only; “/” focuses the field.
  • Indexer (schema v4): SQLite FTS5 tables search_articles and search_posts, kept current by the serve loop (an article is reindexed when anything shown about it changes) and rebuilt by reindex; content gains body_text, language, license (every content record is read again once after the migration); the text of each review and comment is read from IPFS as one raw block (at most 100 000 bytes) and kept in post_texts only when it hashes to its CID. Routes /search/articles, /search/posts, /search/people, /search/suggest. The search is a cached view like content (§15): rebuilt from the chain plus IPFS, never an authority. A DOI whose record does not point back is neither shown nor counted as “with a DOI”.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/search.test.ts, public.test.ts (v3 to v4 migration); projects/web/test/search.test.ts

D-121 Search: filters folded, wildcards and regular expressions (owner request)

Section titled “D-121 Search: filters folded, wildcards and regular expressions (owner request)”
  • Date: 2026-10-02 · Status: accepted (extends D-120)
  • Decision: every group of filters starts folded; a group holding a filter in use stays open, so that the choice can be changed again. The query accepts wildcards inside words (? one letter or digit, * any; wom?n, *ology, neur*sis; a final * stays a prefix of the index, a ? that ends a word is punctuation) and regular expressions between slashes (JavaScript syntax, at most 200 characters, 5 patterns per query), limited to one part like words (title:/^sleep/) or excluded (-/x/). They are tested on the folded texts (accents removed, case ignored) of the articles the index matched, or of all of them, and their matches are marked in the results; reviews and comments and people (name, address, ORCID iD) accept them too. A broken expression matches nothing and says why. The work runs on the indexer’s only thread, where an expression can backtrack for ever: each search’s pattern work runs under vm with a 2 s timeout (400, “make it more specific”) and all searches share 20 s of it per minute (503, “busy”); searches without patterns are not limited. RE2 (re2js, linear time) was measured and rejected: about 300 times slower on long texts.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/search.test.ts

D-122 Lists of articles in the profile (owner request)

Section titled “D-122 Lists of articles in the profile (owner request)”
  • Date: 2026-10-02 · Status: accepted
  • Decision: the owner chose lists that are either private or public, chosen when each list is made. Private lists live in the browser (localStorage per network): free, instant, seen by nobody else, usable without a wallet (shown under “Profile” when none is connected). Public lists are on chain without the contract, which stays as reviewed (no new method, no TestNet restart): each change is a note of a 0-ALGO payment from the owner to themself, ariadne/lists/<appId>: followed by a JSON array of changes (name and describe a list, add or remove articles, delete it); the network fee is the whole cost (0.001 ALGO per note; one note carries up to 50 changes, the wallet signs up to 16 notes at once). The indexer subscribes to these notes next to the application calls, stores each as a ListNote row of chain_events (rebuild replays them in chain order) and projects them to lists and list_items (schema v5); a note that breaks the rules changes nothing and never stops ingestion; limits: 100 lists per address, 1 000 articles per list, names 80 characters, descriptions 280. Only the sender can write their lists, because only they can sign from their address. The web: “Save” in an article’s reader tools opens the lists (private ones change at once; ticks on public ones are collected and saved with one signature, with the cost shown), a new list can be created there; the profile gets tabs, Activity and Lists, where everyone sees the public lists and the owner also manages them (rename, describe, delete) and their private lists (rename, remove articles, delete, make public: one signature, then the private copy goes); each public list has its own page to share (/<network>/u/<address>/lists/<id>), where the owner can rename it, remove articles or delete it. Deleting or removing changes the list; the history stays on chain, and the dialogs say so.
  • Spec refs: §11, §15, §16 · Tests: projects/indexer/test/lists.test.ts, public.test.ts (v4 to v5 migration); projects/web/test/lists.test.ts

D-123 Saving from search, bibliographies of lists (owner request)

Section titled “D-123 Saving from search, bibliographies of lists (owner request)”
  • Date: 2026-10-02 · Status: accepted (extends D-122, D-103)
  • Decision: every article in the search results has a small “Save” button that opens the same lists dialog as on the article page. Every list (the page of a public list, its card in the profile, a private list) has “Bibliography”: all its articles cited in one of the eight formats of D-103 (starting on the reader’s citation default), sorted by author then year (accents and case ignored), by year (newest or oldest first), by title, or in the order of the list (the choice is remembered in the browser); Vancouver and IEEE are numbered in the order shown (“1.” and “[1]”), BibTeX and RIS give one record after the other; it can be copied or saved as a file (.bib, .ris or .txt named after the list). Each article is cited at its current version with its citable DOI (D-108), authors as everywhere (verified ORCID name, else NFD name, else address). The indexer answers GET /cite?ids=… (up to 1 000) or ?owner=&list= (a whole public list) with the data in one request.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/lists.test.ts; projects/web/test/bibliography.test.ts

D-124 Browser notifications and contact from ORCID (owner request)

Section titled “D-124 Browser notifications and contact from ORCID (owner request)”
  • Date: 2026-10-02 · Status: accepted
  • Decision (the owner chose option B for both): browser notifications by Web Push, without email and without outside accounts. In the inbox (linked from the bell) the connected account turns them on for this browser, chooses the kinds (replies, mentions, reviews and comments on their articles, resolved comments, co-author invitations, new followers), sends a test or turns them off; they arrive with the browser closed, on iPhone and iPad once the app is installed on the home screen (iOS 16.4+). The web server keeps each subscription (push endpoint, its two keys, network, address, kinds) in a SQLite file on its own volume; a sender started by instrumentation.ts asks each indexer every 30 s for the notifications created since its cursor (GET /notifications?after=, one request per network) and sends each to the browsers that asked for that address and kind, encrypted end to end with the server’s VAPID keys (web-push), linking to the comment, article or profile; nothing older than a day is sent, gone endpoints (404, 410) are dropped. Notifications are public already, so subscribing needs no signature; to keep the server from posting anywhere else, endpoints must be https URLs of the known push services (Google, Mozilla, Microsoft, Apple); at most 5 addresses per browser. Contact on profiles from ORCID: for a verified ORCID iD the verifier also keeps what the record shows to everyone (websites with their names, emails, countries, and current employments from /employments) in orcid_checks.public_json (schema v6; verified records are checked again at once after the migration), and the profile shows them under the ORCID line, marked as coming from ORCID; nothing is typed into ARIADNE and nothing goes on chain. The link back to the ARIADNE profile is not shown; only http(s) links become links.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/verify.test.ts, api.test.ts, public.test.ts (v5 to v6); projects/web/test/push.test.ts

D-125 Names read from ORCID for citations; citation counts on profiles (owner request)

Section titled “D-125 Names read from ORCID for citations; citation counts on profiles (owner request)”
  • Date: 2026-10-02 · Status: accepted (refines D-103, D-108)
  • Decision: many ORCID records hold the whole name in “given names” with the family name empty (ORCID shows the record as one name, “Lucia Vega Ortiz”). Citations, bibliographies and Zenodo creators therefore read such a name as given names followed by family names (src/lib/names.ts): “Family, Given” with a comma says it outright; a particle starts the family name (de, del, van, von, da…); two words are one given and one family name; three words one given name and two family names (Spanish and Portuguese use); four or more end with two family names, joined by y/e/i when there. A filled family name in ORCID always wins, so anyone this reading gets wrong fixes it on ORCID. The Zenodo draft no longer asks for names of authors whose verified record has any name. Citations on profiles: for a verified ORCID iD the verifier also asks OpenAlex (/authors/orcid:<iD>, free, no key; same service for every network) for the citations, number of works, h-index, i10-index and citations per year of the last five years, kept in orcid_checks.metrics_json (schema v7; verified iDs are checked again at once) and rechecked with the ORCID record; a failed lookup keeps the last counts, an iD OpenAlex does not know shows none. The profile shows Citations and h-index among its numbers, with a line saying they count all the person’s works (not only those on ARIADNE), linking to OpenAlex. Citations between ARIADNE articles are not counted yet.
  • Spec refs: §15, §16 · Tests: projects/web/test/names.test.ts, zenodo.test.ts; projects/indexer/test/verify.test.ts, public.test.ts (v6 to v7)

D-126 Citations between ARIADNE articles, with their lists (owner request)

Section titled “D-126 Citations between ARIADNE articles, with their lists (owner request)”
  • Date: 2026-10-02 · Status: accepted (extends D-125)
  • Decision: an article cites another ARIADNE article when its current version links to the other’s page on the same network (any form of https://ariadne.press[/testnet]/a/), writes a DOI the other declared (verified, declared or not checked yet; never one whose record contradicts it), writes the CID of one of the other’s versions, or amends it (parent on chain). The content check keeps what each version points to (content.refs_json: URLs, DOIs and CIDs from the text, links included, and from the front-matter references; every version is read again once after the migration, schema v8); the indexer derives citations (citing, cited, how) from it and the chain in the serve loop, recomputing only when content, versions, DOIs or their checks change. An article never cites itself; retracted citing articles are not counted. The article page shows “cited by N on ARIADNE · cites M”, linking to /a/<id>/citations (tabs: cited by, references on ARIADNE; each article says how it cites); the profile shows “Cited on ARIADNE” (total citations its articles received), linking to /u/<address>/citations (each citing article, which article it cites, how, and whether it is a self-citation; the h-index on ARIADNE and the self-citations in the summary); the OpenAlex numbers become “Citations (all works)” and “h-index (all works)”. The search can sort by “Most cited on ARIADNE” and shows “cited by N” on results. Routes: /articles/:id/citations?direction=cited_by|cites, /users/:address/citations; /articles/:id and /users/:address carry the counts. The publishing guide says how to cite so that it counts.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/citations.test.ts, public.test.ts (v7 to v8)

D-127 Thread, ARIADNE’s citation measure, instead of an h-index (owner request)

Section titled “D-127 Thread, ARIADNE’s citation measure, instead of an h-index (owner request)”
  • Date: 2026-10-02 · Status: accepted (replaces the h-index of D-125 and D-126)
  • Decision: the owner wants no h-index but a measure of ARIADNE’s own, fair and comparable. Thread (after Ariadne’s thread), 0 to 100, 50 typical: (1) each article counts its external citations: from other ARIADNE articles, not retracted, sharing no accepted author with it; (2) its percentile places it among the articles of the same primary field and publication year (mid-rank: the share it is cited more than, ties counted half); when that group has fewer than 10 articles, the same area and year, then the same year, then every article; (3) a person’s Thread is the mean percentile of their articles, each weighted by their share (1 / accepted authors), with one article at 50 added (a prior: one lucky article cannot make 100, and the prior fades as articles accumulate). Amendments (corrections of one’s own work) and retracted articles take no part. Why: an h-index grows with career length, depends on the field and counts self-citations; percentiles within field and year compare unlike fields and ages, a mean (not a sum) compares short and long careers, fractional weights compare teams of any size, and the prior keeps small samples honest. It is built from established ideas (percentile normalisation, fractional counting, shrinkage) and is ARIADNE’s in its combination and in being reproducible by anyone from the chain and the articles. The indexer keeps each article’s external citations, group and percentile in article_impact (schema v9), recomputed with the citations; /articles/:id carries citations.thread, /users/:address carries ariadne_citations.thread {score, articles}. The web shows “Thread” on profiles (linking to its explanation on the citations page), the article’s Thread percentile on its citations page, and no h-index at all (the OpenAlex h-index is no longer shown either; OpenAlex’s citation count stays as “Citations (all works)”).
  • Spec refs: §15, §16 · Tests: projects/indexer/test/citations.test.ts, public.test.ts (v8 to v9)

D-128 Thread compares articles at the same age, pooling every year (owner question)

Section titled “D-128 Thread compares articles at the same age, pooling every year (owner question)”
  • Date: 2026-10-02 · Status: accepted (refines D-127)
  • Decision: the owner asked whether an old article cited for many years should not stand out even when its year had tremendous articles. Percentiles already limit the effect of a few giants (they take only the top places, unlike a mean), but comparing within the calendar year left the luck of the year, and a first idea of comparing only with older articles would have frozen the first articles of a field at 50. Thread now compares each article with every article of its field (its area, then all, when the field has fewer than 20, up from 10), each pair at the age of the younger of the two: the citations each had received by then (a citation dates from the citing article’s current version). So every year is in the comparison and a strong year weighs little; an old article keeps climbing while it is cited; a recent article is judged only on the time it has lived; the first article of a field can stand out. Percentiles are computed again every day (ages grow). Mid-rank, the share of the group it had more citations than, ties half; the person’s Thread is unchanged (mean by share, one article at 50 added). The cost is quadratic in the size of a field; fine for ARIADNE’s size, to be revisited (binning by age) when fields reach thousands of articles.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/citations.test.ts

D-129 The measure is called “Thread Score” (owner request)

Section titled “D-129 The measure is called “Thread Score” (owner request)”
  • Date: 2026-10-02 · Status: accepted (names D-127, D-128)
  • Decision: everywhere readers see it (profile, citation pages, About) the measure is “Thread Score”; an article’s percentile is shown as its Thread Score. Code and API keep the field name thread.

D-130 Scores shown in lists, votes only on the article page (owner request)

Section titled “D-130 Scores shown in lists, votes only on the article page (owner request)”
  • Date: 2026-10-02 · Status: accepted
  • Decision: the feed, the search results and the other article lists (public lists, citations) show each article’s score as a badge on its left: the weight of its votes (each vote weighs 1 plus the square root of the voter’s reputation in the field, rounded down), with the number of votes in its description. The badge is not a control: voting stays on the article page, where its cost and rules are shown (D-101).
  • Spec refs: §16 · Tests: visual check

D-131 The “Peer-reviewed” seal, computed by the indexer (owner request)

Section titled “D-131 The “Peer-reviewed” seal, computed by the indexer (owner request)”
  • Date: 2026-10-02 · Status: accepted
  • Decision: the owner asked whether ARIADNE should only publish what met a minimum of reviews. Publishing stays free and immediate (the contract is unchanged, and an author can move an article to “final” without any review); instead a seal tells reviewed work apart. The owner approved the proposal (Claude Doc “Propuesta: sello Peer-reviewed”) with its proposed values and one change (co-authorship conflict window 3 years instead of 5). An article holds the seal while at least 2 qualified reviews are favourable (accept, minor revision) and favourable > unfavourable (major revision, reject); 2 or more unfavourable, as many as the favourable or more, show “changes requested”; any qualified review, “in review”; none, “not peer-reviewed yet”. A review qualifies when, in this order: its author is not an author of the article; the reviewer has reputation >= 10 in the primary or secondary field, or a verified ORCID iD with >= 3 works in the article’s area on OpenAlex (a permanent path, so that established researchers count from their first day; OpenAlex fields are mapped to the six OECD areas in src/fieldmap.ts, a field may belong to two); no conflict of interest with any author (the same verified ORCID iD; an ARIADNE article written together within 3 years of the review; works together on OpenAlex from 3 years before the pair’s first review to 3 years after its last, both iDs verified; the same current employer on the public ORCID records); its text was read from IPFS and has >= 200 words; and, for a favourable review, no favourable review from an author of the article to the reviewer in the 2 years before. A verified ORCID iD is not required. The seal is alive (later qualified reviews can remove it), suspended while the article is disputed and absent when it is retracted; it names the version current at the latest counted review, and the article page says when a later version is not reviewed yet. Everything is derived from the chain, IPFS, ORCID and OpenAlex (schema v10: article_seals, review_qualifications, seal_state; base table coauthor_checks), recomputed when its inputs change, and each result names its rule version (SEAL_RULE 1), so anyone can compute it again. OpenAlex costs: one more request per verified iD check (works grouped by field) and one per reviewer-author pair every 30 days. The web shows the seal as a badge in every list, a “Peer review” panel on the article page (each review: counts, or why not), seal counts on profiles, and a “Peer-reviewed” search filter. Status 3 (“final” on chain) is shown as “closed version”, because it says nothing about review.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/seal.test.ts, verify.test.ts, public.test.ts (v9 to v10); projects/web/test/peerReview.test.ts

D-132 Documentation site with Astro Starlight at /docs (owner request)

Section titled “D-132 Documentation site with Astro Starlight at /docs (owner request)”
  • Date: 2026-10-02 · Status: accepted
  • Decision: the owner asked for a GitBook-like place that explains everything in detail and in order, and chose Starlight among the options (GitBook, Starlight, pages inside the app, Docusaurus). The site lives in projects/docs (Astro 7, Starlight 0.42), English first with translations prepared (a locale and a folder), and is built to static files served by its own small Caddy container behind the stack’s Caddy at https://ariadne.press/docs/ (no new DNS name, no outside service: Starlight’s search runs from the static files, Pagefind). Sections: Start here, Guides, Concepts, Reference, Run your own, FAQ. The protocol specification, the decision log and the fields are copied from spec/ARIADNE_SPEC.md, DECISIONS.md and spec/taxonomy.json at every build (the specification’s inline EDIT D-nnn comments become visible notes; its section 23, the brief of the implementation assistant, is left out), so they are written once. Internal links are validated at build time; the numbers the code defines (costs, reputation, flags, the seal) are checked by projects/web/test/docs.test.ts. The app links the site from its menu (“Docs”, desktop) and its footer (“Documentation”). The About page and the publishing guide stay in the app.
  • Spec refs: §16 · Tests: projects/web/test/docs.test.ts; the docs build (link validation)