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 (corrected by D-164: Caddy keeps no access log either).
  • 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)

D-133 A verified ORCID iD is required for a review to count toward the seal (owner, seal rule 2)

Section titled “D-133 A verified ORCID iD is required for a review to count toward the seal (owner, seal rule 2)”
  • Date: 2026-10-03 · Status: accepted (amends D-131)
  • Decision: the owner proposed making a linked ORCID iD mandatory for reviews. Three ways were weighed: required to review (the web would hide the form), required for the seal, or not required. The contract cannot require it (the iD is checked outside the chain), and hiding the form would shut out students, people in industry and others without an iD from discussion and from the reputation that useful reviews earn. The owner chose: anyone can still review, but a review counts toward the Peer-reviewed seal only when its author has a verified ORCID iD. This closes a gap of rule 1, where a reviewer without an iD escaped the OpenAlex co-authorship and the same-employer checks. ORCID does not verify identity (anyone can open a record), so it adds traceability and the conflict checks, not protection against invented people. SEAL_RULE is 2; the check runs right after “not an author” and its reason is orcid (“the reviewer has no verified ORCID iD”); the OpenAlex expertise path is unchanged (3 works in the area, found through the verified iD). For an author without an iD, only the ARIADNE co-authorship check applies.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/seal.test.ts, projects/web/test/peerReview.test.ts

D-134 Storage settings on the profile, and one’s own IPFS node (owner request)

Section titled “D-134 Storage settings on the profile, and one’s own IPFS node (owner request)”
  • Date: 2026-10-03 · Status: accepted (extends D-085, D-092)
  • Decision: the owner asked for an option on the profile to configure one’s IPFS. Until now the provider key could be set only on the publish page, so someone who wanted to review or comment first had to go there. One’s own profile now has a “Storage (IPFS)” section (shown only to its owner, like the ORCID steps) with the same setting as the publish page: Filebase, Pinata, or a third option, my own IPFS node: the address of a Kubo RPC API (default http://SERVER_IP:5001) and an optional token for nodes behind an authenticating proxy. Filebase’s RPC is Kubo’s, so one provider class serves both (KuboRpcProvider, with FilebaseProvider keeping its own messages). The node must allow the site once (API.HTTPHeaders.Access-Control-Allow-Origin and -Methods), and stay online until ARIADNE’s indexer has made its copy. Saving runs a test, and a Test button runs it again: it pins a fixed 21-byte block (“ARIADNE storage test”) and checks the provider kept exactly that CID, so testing again adds nothing. The setting stays in the browser (not on the chain, not on any server); messages that asked for a key “on the publish page” now point to the profile.
  • Spec refs: §14.4, §16 · Tests: projects/web/test/pinata.test.ts

D-135 Getting started page and an invitation kit (owner request)

Section titled “D-135 Getting started page and an invitation kit (owner request)”
  • Date: 2026-10-03 · Status: accepted
  • Decision: for a TestNet pilot with colleagues, the app gets a Get started page (/<network>/start): five steps (connect a wallet; get TestNet ALGO, at least 0.3 spendable, enough for an article and a first review, with the dispenser; choose where files are stored, with the storage setting in place; link the ORCID iD, required for reviews to count toward the seal; take part: review, publish, follow, notifications), each ticked from what the account has really done (wallet, balance from algod, the browser’s storage setting, the indexer’s profile). The feed shows a welcome card pointing to it until it is hidden (per browser); the footer links it. The documentation gets “Invite colleagues” (/docs/start/invite/): what to ask the people invited to try in one or two weeks, advice (publish first, same field, ORCID early, a mix of institutions), and an invitation to copy in English and Spanish that links the Get started page.
  • Spec refs: §16 · Tests: projects/web/test/onboarding.test.ts; the docs build

D-136 Every author needs a verified ORCID iD for the article to hold the seal (owner, seal rule 3)

Section titled “D-136 Every author needs a verified ORCID iD for the article to hold the seal (owner, seal rule 3)”
  • Date: 2026-10-03 · Status: accepted (amends D-131, D-133)
  • Decision: the owner asked to require a linked ORCID iD to obtain the seal. Read as: an article holds the Peer-reviewed seal only when every author (the submitter and the confirmed co-authors; declared but unconfirmed co-authors are not authors yet) has a verified ORCID iD, besides the reviews of rule 2. It closes the gap left by D-133: with an author’s verified iD, the OpenAlex co-authorship and same-employer checks run against every reviewer. When the reviews are enough but an author lacks a verified iD, the state is awaiting_orcid (“Seal awaits ORCID iDs”), shown in amber, and the Peer review panel names the authors still without one (seal_detail.authors_without_orcid); the seal starts when the last author is verified, if that comes after the reviews; a disputed article whose authors lack an iD stays awaiting_orcid, not suspended. Search’s “Peer-reviewed” filter and the profile counts take only sealed articles. SEAL_RULE is 3.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/seal.test.ts, projects/web/test/peerReview.test.ts

D-137 Before signing a review: will it count toward the seal? (owner, TestNet pilot)

Section titled “D-137 Before signing a review: will it count toward the seal? (owner, TestNet pilot)”
  • Date: 2026-10-03 · Status: accepted (brings forward part of the “seal, second pass” of the roadmap)
  • Decision: with a verified ORCID iD now required (D-133, D-136), a reviewer could easily write a review that does not count without knowing it. The review form shows “Will this review count toward the seal?” from the indexer’s own checks, not a copy: the review checks of src/seal.ts became one pure function (judgeReview) over the data the seal pass loads (loadSealData, cached per database while the signature of the inputs is unchanged), used by the seal pass and by GET /articles/:id/seal-check/:address, which judges a review not written yet for a favourable and an unfavourable recommendation (reciprocity concerns favourable ones only). The text is checked in the browser with the same word count as the indexer (a test pins both to the same samples). Works together on OpenAlex are known only for pairs already looked up, so the form says that check happens after publishing when needed. The form also says when the article itself cannot hold the seal yet (an author without a verified iD). The review is published either way.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/seal.test.ts, projects/web/test/peerReview.test.ts

D-138 Clean-up after the documentation: one guide, spec notes, amendments’ fields, forfeiture shown

Section titled “D-138 Clean-up after the documentation: one guide, spec notes, amendments’ fields, forfeiture shown”
  • Date: 2026-10-03 · Status: accepted
  • Decision: (1) the publishing guide exists once, in the documentation; spec/publishing-guide.md stays where the specification expects it (section 14.4, D-014) as a page of pointers to the documentation and its sources. (2) Notes in the specification where it and the code differ: the size limit applies to datasets too (larger data goes to a repository such as Zenodo, referenced by DOI); the event field names of the code are the reference (article_type, from_status/to_status, claimant). (3) Section 7.5 (“an amendment’s fields SHOULD match the parent’s, enforced by the web app”) is now enforced: the publish form reads the parent article, takes its fields and locks the pickers, and refuses a parent that does not exist. (4) D-044 shown: before a retraction the panel and the confirmation give the reputation not yet claimed that it forfeits, for the submitter and for the co-authors, with the advice to claim first; the profile shows such amounts as “forfeited”. (5) Wording: a publication is at least three transactions (more with many co-authors); the Thread Score line reads “more than about N%” (ties count half).
  • Spec refs: §7.5, §11, §14.1, §14.4 · Tests: projects/web/test/actions.test.ts

D-139 Overlay journals on top of ARIADNE (owner request)

Section titled “D-139 Overlay journals on top of ARIADNE (owner request)”
  • Date: 2026-10-03 · Status: accepted
  • Decision: the owner chose, among “crazy ideas”, overlay journals: curation and prestige without gatekeeping, the answer to whether ARIADNE can replace journals. A journal is a public list (D-122) with an editorial policy and a board, managed by the same notes of 0-ALGO payments to oneself, so the contract is unchanged and every change costs only the network fee. The founder marks a list as a journal with a policy (at most 600 characters, to fit a note) and invites editors (at most 10 per operation, 30 in all); nobody becomes an editor without signing an acceptance (like co-authors, D-072); an invited editor can decline, an editor can resign, the founder can dismiss editors or end the journal (it becomes a plain list). The founder and the accepted editors select articles into the founder’s list; each selection records who made it (added_by) and notifies the article’s confirmed authors; selections stay when their editor leaves. Indexer schema v11 (lists.journal, lists.policy, list_items.added_by, table journal_editors); GET /journals, GET /users/:address/journals; GET /articles/:id names the journals that selected it. Web: a Journals directory (menu and footer), the list page becomes the journal’s page (policy, editorial board, who selected each article), “Journal settings” for the founder, “Your journals” on articles and profiles for editors (select, accept, decline), “Selected by” badges on articles, and the notifications journal_invite and journal_selected (also as browser notifications).
  • Spec refs: §15, §16 · Tests: projects/indexer/test/journals.test.ts, public.test.ts (v10 to v11); projects/web/test/journals.test.ts

D-140 Retraction alerts through citations (owner request)

Section titled “D-140 Retraction alerts through citations (owner request)”
  • Date: 2026-10-03 · Status: accepted
  • Decision: when an article is retracted, the articles that cite it should know. The article page of every ARIADNE article citing a retracted one (citations of D-126, any kind) shows a warning naming it and the date of the retraction (cites_retracted), and the serve loop gives each confirmed author of a citing article that is not retracted a cited_retracted notification, once (derived from the citations, so it is inserted again after a rebuild of the projections; the author of the retracted article is not notified of their own retraction). The notification leads to the citing article; browser notifications can include it.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/journals.test.ts, projects/web/test/journals.test.ts

D-141 Volumes of overlay journals (owner request)

Section titled “D-141 Volumes of overlay journals (owner request)”
  • Date: 2026-10-03 · Status: accepted (amends D-139)
  • Decision: the owner did not want a journal capped at 1 000 articles (the limit of a public list, which protects against lists inflated with spam) and proposed volumes, as in printed journals. A journal now has numbered volumes; one is open and receives the selections. A volume holds up to 1 000 articles and the selection that would overflow it opens the next volume by itself (deterministic: the indexer applies the notes in chain order, so every indexer agrees); the founder can open the next volume earlier, for example every year, with an optional title (at most 80 characters), and rename the open one; an empty volume is never closed; a journal can have up to 1 000 volumes. A closed volume is frozen (no additions or removals, like a printed issue; a retracted article stays and shows its retraction) and keeps its board: the founder and the accepted editors at the moment it closed. An article is selected once per journal, in the volume open at the time. A list that becomes a journal starts with volume 1 holding its articles. Indexer schema v12 (lists.volume, list_items.volume, table journal_volumes); the list route takes volume=<n>; articles name the volume that selected them. Web: the journal page shows the volumes as tabs (the open one by default), each with its dates, size and board; the founder’s settings rename the open volume or open the next; badges read “Selected by Name, vol. N (title)”. Plain lists keep their 1 000-article limit.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/journals.test.ts, public.test.ts (v11 to v12); projects/web/test/journals.test.ts

D-142 Search engines and Google Scholar (owner request, after an outside review)

Section titled “D-142 Search engines and Google Scholar (owner request, after an outside review)”
  • Date: 2026-10-03 · Status: accepted
  • Decision: another AI could not see ariadne.press: the bare address answered with a page that redirected in the browser (“Opening ARIADNE…”, no link), so crawlers never reached the articles; article pages were rendered on the server but all titled “ARIADNE”, with no description, no Google Scholar tags and no link preview; robots.txt and sitemap.xml were HTML pages. Now: / redirects on the server (307) to the network remembered in a cookie (ariadne_network, set with the localStorage key when a person switches network) or the default one (this also removes a latent loop for MainNet, whose home is /mainnet there); every page has its own title (“ · ARIADNE TestNet”); article pages carry their description (the abstract), canonical address, Open Graph and Twitter card (a preview image, public/og-image.png from scripts/og-image.mjs), the Highwire citation_* tags Google Scholar reads (title, authors as “Family, Given” from verified ORCID records, publication and online dates, publisher, DOI when verified, abstract and full-text URL, keywords, and journal title and volume when an overlay journal selected the article) and schema.org ScholarlyArticle JSON-LD (escaped so that user text cannot close the script element); journal, list and profile pages have their name, description and canonical address; search results, the inbox and /me say noindex; robots.txt is real (everything but /api/, with the app’s and the documentation’s sitemaps) and sitemap.xml lists the home, journals, about page, every article and every journal of the networks that may be indexed. Which networks: MainNet only by default (ARIADNE_INDEXABLE_NETWORKS), because TestNet articles are practice; TestNet pages say noindex, follow. The documentation is indexed (Starlight’s own sitemap and tags). Not done: a PDF of each article (Scholar prefers one), to consider for MainNet.
  • Spec refs: §16 · Tests: projects/web/test/scholar.test.ts

D-143 PDF and print from the browser, not from the server (owner decision)

Section titled “D-143 PDF and print from the browser, not from the server (owner decision)”
  • Date: 2026-10-03 · Status: accepted
  • Decision: after D-142 the owner asked whether a PDF of each article was needed and what it would cost. Google Scholar indexes the HTML full text when the page carries the citation_* tags (D-142), so a PDF is not required; readers want to download, print and archive. A server-side renderer (headless Chromium, one render per version cached by CID, a few hundred MB of memory while rendering, no IPFS storage) stays possible later but adds a service to run on a small server; PDFs supplied by authors were rejected as the article’s format: a thousand layouts, no way to check they say what the signed Markdown says, and duplicated storage (a PDF can still be supplementary material in assets/). Chosen: a print stylesheet and a “PDF / Print” button on every article; the reader’s browser makes the PDF (“Save as PDF”). The printed copy shows the article alone in a reading layout (serif text, no app around it, figures, tables, code and formulas kept whole, link addresses written out) headed by a record: version and date, address, CID, DOI and licence when present, the seal’s state and counts, a TestNet warning off MainNet, and the APA reference of that version. No server cost, one uniform format.
  • Spec refs: §16 · Tests: projects/web/test/print.test.ts

D-144 Printing a journal volume, in editing or finished (owner decision)

Section titled “D-144 Printing a journal volume, in editing or finished (owner decision)”
  • Date: 2026-10-03 · Status: accepted
  • Decision: the owner asked that the volumes of overlay journals (D-141) be printable with a cover giving the journal’s and the volume’s data, followed by all their contents, and then that open volumes print too as long as the cover gives their state. A print view per volume (/u/<owner>/lists/<id>/print?volume=N, not indexed), printed or saved as PDF by the reader’s browser as articles are (D-143): a cover (journal name and description, volume number and title, state “In editing” while open or “Finished” once closed, with dates and what the state means for the copy, article count, address, date printed, a TestNet warning off MainNet, the editorial policy, and who edited it: the board snapshot of a finished volume, the founder and accepted editors of one in editing), the contents numbered in selection order, then every article in full from a new page with its printed record (D-143, the network said once on the cover) and its APA reference. The volume’s states are worded “in editing” and “finished” on the journal’s page too. A volume of more than 50 articles prints in parts of 50, so that one page never fetches and renders a thousand bodies. No server rendering or storage.
  • Spec refs: §16 · Tests: projects/web/test/volumes.test.ts

D-145 Quality of life: eight reading, writing and reviewing aids (owner decision)

Section titled “D-145 Quality of life: eight reading, writing and reviewing aids (owner decision)”
  • Date: 2026-10-03 · Status: accepted
  • Decision: from a list of quality-of-life ideas the owner chose eight, all in the web app (no contract or indexer change). (1) Before you sign: a box in step 3 of publishing and before a new version lists what cannot be fixed afterwards: title or abstract still the template’s, empty abstract, no keywords or licence, template example text left in, files linked but missing, empty links or links outside the folder, external images (shown as links), images without description, formulas KaTeX cannot typeset, references without DOI or link, a long text without headings, links to ARIADNE articles that do not exist, and authors without a verified ORCID iD (seal rule 3); warnings only (src/lib/prepublish.ts). (2) What changed between versions: /a//changes?from=&to= compares index.md line by line (Myers) and word by word, and lists the assets added, removed or changed from the directory blocks alone (fetched as raw blocks, checked against their CIDs, decoded without fetching the files); linked from the version number, the Versions list and each review written on an earlier version (“on v1 · what changed since”). (3) Previews of linked ARIADNE articles on hover or keyboard focus (the indexer’s citation rule for what a link to an article is). (4) Copy buttons on code blocks, a click on a formula copies its LaTeX, figures open full size. (5) Quote in review / Quote in comment from a selection in the article, formulas quoted as LaTeX. (6) Reply point by point for authors: every point of a review quoted with a Response line under each. (7) Wallet and transaction errors in plain words (cancelled, pending, expired, duplicate, offline, low funds with the amount missing, the contract’s assert messages from the ARC-56 source map in plain words), the raw error under Technical details. (8) Share with a QR code (made in the browser) on articles, profiles, lists and journals, and the phone’s share sheet where there is one.
  • Spec refs: §16 · Tests: projects/web/test/qol.test.ts

D-146 Writing comments and reviews: preview, formula check, formatting help (owner decision)

Section titled “D-146 Writing comments and reviews: preview, formula check, formatting help (owner decision)”
  • Date: 2026-10-03 · Status: accepted
  • Decision: the owner asked whether comments should have Markdown and LaTeX. They had both since the start (the same rendering as articles, images excepted, raw HTML sanitised), but the box was plain and nothing showed what the text would look like, while a comment or review can never be edited. Chosen: no editor toolbar nor WYSIWYG (researchers know Markdown and LaTeX; more weight and bugs), images and HTML stay out; instead every comment, reply and review box gets a live preview under it (the same rendering as the page; the choice remembered in this browser), a warning for formulas KaTeX cannot typeset (the check of D-145), a warning past the 256 KiB a comment can hold, and a folded formatting help (src/components/MarkdownEditor.tsx).
  • Spec refs: §16 · Tests: projects/web/test/qol.test.ts

D-147 The profile’s figures: twelve equal cards with icons (owner decision)

Section titled “D-147 The profile’s figures: twelve equal cards with icons (owner decision)”
  • Date: 2026-10-03 · Status: accepted
  • Decision: the owner asked for the row of figures on a profile to be symmetric, each with its own icon. The profile always shows the same twelve figures (reputation, fields, publications, reviews, comments, citations of all works on OpenAlex, peer-reviewed articles, reviews that count, cited on ARIADNE, Thread Score, followers, following), a dash with an explanation when one is unknown (no verified ORCID iD, no article that counts yet), in equal cards on a grid of 6, 4, 3 or 2 columns by the width available (a container query): twelve fills every row, so no card is left alone. Each card has a line icon in the accent colour; the Thread Score’s is a spiral, Ariadne’s thread wound.
  • Spec refs: §16 · Tests: none (layout)
Section titled “D-148 Following articles, inbox filters, where the reader left off, related articles (owner decision)”
  • Date: 2026-10-03 · Status: accepted
  • Decision: four more quality-of-life items chosen by the owner. (1) Following an article: recorded like public lists (D-122), a note of a 0-ALGO payment to oneself with the op {"op":"watch","articles":[id],"on":bool}, so no contract change, no deposit, cross-device and public; the indexer (schema v13, projection table watches, at most 500 articles per address, replayed in chain order by rebuild) notifies the followers of a new version (watched_version), a status change by the author, flags or governance (watched_status), a review (watched_review) or a comment (watched_comment), except the actor and whoever is told in their own right (authors, the replied-to, the mentioned); GET /users/:address/watching and watching in the viewer state; Follow on the article page; browser notifications for the four kinds. (2) Inbox filters by kind (reviews, comments and replies, mentions, articles I follow, journals, co-authors and followers, retractions) with unread counts, Unread only and Mark these read, and the list of followed articles. (3) A reading bar and “Continue where you left off” (the section and percentage), kept in this browser only per network and article, for the same version, 90 days, forgotten at the end. (4) Related on ARIADNE after an article: those that cite it, those it cites, others of its field by score (not retracted), each once. The documentation also explains, in What ARIADNE is and How it works, that ARIADNE is three parts: the protocol (the contract and the texts on IPFS, with the note conventions), the indexer and the web app.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/watches.test.ts, projects/web/test/following.test.ts

D-149 Principles and commitments, a draft manifesto (owner decision)

Section titled “D-149 Principles and commitments, a draft manifesto (owner decision)”
  • Date: 2026-10-04 · Status: draft
  • Decision: an outside review argued that removing the editor moves editorial power into the rules, and proposed a document stating which principles ARIADNE protects and what it gives up when they conflict. The owner adopted the idea. The documentation gains /docs/start/principles/ (draft 0.1): the purpose (keep apart making an idea public, judging it and crediting it), eight principles (free reading, publishing without permission, a permanent record with a revisable interpretation, recognition earned by field from others, public rules, no operator needed, figures that describe and do not rank people in the spirit of DORA and the Leiden Manifesto, minimal personal data), seven tensions each with what is chosen, what it costs and how the cost is limited (openness and abuse; permanence, correction and erasure; immutability and fixing mistakes; transparency and privacy; past recognition and present judgement; autonomy and governance; useful figures and their incentives), red lines, and how the page changes. Four points are marked to decide before MainNet (legal requests, delayed-identity reviews, vote weight from recent reputation, jury governance). The review’s precautions go to the roadmap: simulate reputation and attacks, responsible figures in the interface, vulnerability disclosure, governance key loss, a takedown list if adopted (before MainNet); coordinated-voting detection, an “at a glance” box per article, an independence drill (after).
  • Spec refs: §1, §16 · Tests: none (documentation)

D-150 The four open points of the principles, decided; the reputation simulation (owner decision)

Section titled “D-150 The four open points of the principles, decided; the reputation simulation (owner decision)”
  • Date: 2026-10-04 · Status: accepted (the contract parts take effect with the MainNet contract v4)
  • Decision: the owner accepted the proposals for the four points D-149 left open. (1) Lawful requests: we stop serving the content from our indexer, gateway and website and add it to a public list of what we no longer serve (date, reason, not the content); the record is untouched; disagreement is answered with reviews, comments and flags, not takedowns. (2) No delayed-identity reviews: the sender of a review is public on the chain at once, so hiding it in the interface would only pretend to protect, and real protection needs cryptography ARIADNE lacks; a reviewer who wants distance can use another address, whose review does not count toward the seal; revisit with the pilot’s evidence. (3) Vote weight: research/reputation-sim (ten years of one field, eight seeds, five rules, four scenarios) found that weighing by recent reputation changes little (one or two points), that ten fresh addresses voting for each other reach the maximum reputation the daily limit allows (1.15 times the best honest researcher) and can dispute any article within a week for about 5 ALGO, and that damping repeated votes (the n-th vote from a voter for the same author counts 1/n) keeps such a ring at 2% of the best researcher, cuts the founders’ share of influence from 14% to 9% and lifts a contrarian result from the 64th to the 91st percentile, while a cartel of established researchers still gains (1.30x). So reputation stays lifetime, and the MainNet contract will damp repeated votes for articles, reviews and comments, make false flags cost credibility and clear unresolved disputes after a set time. (4) Disputes are settled by a qualified jury: seven jurors drawn verifiably at random among those who pass the seal’s tests and have no conflict with authors or flaggers, signed public answers within 14 days, two thirds of at least five deciding; in a first phase governance executes the verdict and publishes the tally (or decides with public reasons without quorum); later the contract takes the verdict. The principles page becomes version 1.0; the report is published as /docs/reference/reputation-simulation/.
  • Spec refs: §10, §12, §16 · Tests: research/reputation-sim/sim.ts (deterministic seeds)

D-151 The jury for disputes, phase 1, with every case decided (owner decision)

Section titled “D-151 The jury for disputes, phase 1, with every case decided (owner decision)”
  • Date: 2026-10-04 · Status: accepted
  • Decision: the owner asked for the jury of D-150 to be built with every possibility decided. The indexer opens a case when an article becomes disputed and draws the jury from the seed of the Algorand block 8 rounds after the flag that crossed the threshold (unknowable when flagging): eligible people (the seal’s tests: verified ORCID iD, reputation 10 in the primary or secondary field or 3 OpenAlex works in the area, no conflict with any author or flagger; not an author declared or confirmed, not a flagger of the round, not governance, not a juror of an earlier round or attempt for the article; one seat per ORCID iD) ordered by SHA-256(“seed|article|round|attempt|address”); 7 seats, the rest alternates; the whole order published. A field with fewer than 7 widens to its area, then to everyone qualified; fewer than 5 anywhere: no jury, governance decides alone with public reasons. Jurors answer clear or retract with reasons (at most 600 characters) or recuse (the next alternate is seated at once), as list notes {"op":"verdict","article","round","answer","reasons"}; the first answer is final; late answers, answers after a seat passed on, second answers and answers without a seat are kept but not counted. After 7 days silent seats pass to alternates. The outcome: two thirds of at least 5 answers (early when one side holds two thirds of the seats); at the 14-day deadline a quorum without two thirds is “no consensus” and clears; fewer than 5 answers draws a second jury once, and a second failure clears. Governance carries out the outcome within 7 days; the case shows executed, contradicted, before the verdict, or overdue. Schema v14 (base tables jury_cases, jury_seats, jury_answers); notifications jury_summons and jury_verdict; GET /articles/:id/jury, /users/:address/jury; the dispute page /a//dispute with the juror’s form.
  • Spec refs: §9, §15, §16 · Tests: projects/indexer/test/jury.test.ts, projects/web/test/jury.test.ts

D-152 What this service no longer shows: notices and a transparency report (owner decision)

Section titled “D-152 What this service no longer shows: notices and a transparency report (owner decision)”
  • Date: 2026-10-04 · Status: accepted
  • Decision: the owner asked whether a list of withheld content contradicts the project. Answer adopted: the protocol cannot be censored (the chain, other indexers, anyone’s copies), but this service is subject to law, and the honest way to comply is to say so; a silent removal would contradict the principles, a public index of removed content would point to it. So: the list lives in the repository (spec/withheld.json: id, network, kind, article, seq, date, reason in personal-data, unlawful-content, court-order, copyright, and the basis), read by the indexer at start; the API takes out the text, title, abstract, snippets and CIDs of what is withheld and puts a withheld notice in its place (one mask over every response); its CIDs are no longer pinned and the gateway’s Kubo deny list (written by the indexer into a volume Kubo reads) answers them 410 Gone; the website shows the notice in place of the article, review or comment, and a transparency report page per network (footer link) lists the cases by record and reason, never the content or the CID. Only lawful requests; disagreement is answered with reviews, comments and flags.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/withheld.test.ts

D-153 The sandbox: a private test network for invited testers (owner decision)

Section titled “D-153 The sandbox: a private test network for invited testers (owner decision)”
  • Date: 2026-10-05 · Status: accepted
  • Decision: the owner asked for a test mode, for people he chooses, in which articles, votes, lists, reputation and the seal can be tried without anything being on chain. Proposed and accepted instead: a private chain, so that what is tested is the real thing. The sandbox is a fourth network (sandbox in the manifest, between TestNet and LocalNet) served by production but only to invited testers. (1) Chain: an algod in development mode inside the hosting stack (algod-sandbox, the LocalNet image, profile sandbox, no port, tokens generated on the server; the image’s start script prints its tokens, so a wrapper drops those lines from the log). The web app deploys the same contract on it at start (a new governance key, the application funded, the 42 fields), writes the sandbox’s manifest entry into a shared volume, and deploys a new application on every reset; the indexer (indexer-sandbox) waits for the manifest, stops when it names another application (Docker starts it again) and keeps one database per chain and application. (2) Access: invitations by name with codes of their own (revocable) and the owner’s admin code from the server’s environment; the invitation link sets a signed cookie for 180 days; proxy.ts answers 404 to every sandbox page and API request without it, and the manifest leaves the sandbox out for anyone else. (3) The browser reaches algod (reads, simulate, send; never the development-mode or administration routes), the indexer and the content only through gated routes of the web app; content is kept on the server only, never on IPFS. (4) Wallet: test accounts made in the browser (use-wallet’s mnemonic provider, keys kept in the browser), funded from a dispenser exported once from the node’s KMD. (5) Time: algod’s development mode stamps each block with the previous block’s time plus a set offset, so the web app keeps “ahead” (how far the sandbox’s clock runs ahead of the wall clock) and sets the offset every second so that the next block lands on wall clock + ahead, making a block when the chain is idle for two minutes; moving the clock raises “ahead” and makes a block; the indexer and the pages’ relative dates read “ahead”. (6) Identities: the sandbox’s indexer takes any well-formed declared ORCID iD as verified at once (a made-up name, ORCID never asked) and asks OpenAlex nothing. (7) Tools, always through the contract’s own methods, by helper accounts the server holds (fresh where the rules require, never authors, jurors never flaggers): votes for an article; reputation (the tester’s account reviews the field’s host article, signed in the browser, and helpers vote it useful; the clock moves a day when the daily limit holds the grants); the seal (two helpers with test iDs and reputation 10 write favourable reviews of 200 words or more, test iDs declared for the tester’s authors); a dispute with a jury (14 helpers qualify first, because the indexer draws from those qualified when the seed block exists, then others flag to 30, then blocks for the seed), juror answers (all retract, all clear, 4 to 3, only three), governance’s resolution (the sandbox holds the key), more people for a second jury; helpers publishing, reviewing and voting articles; the clock; test ALGO; reset and invitations for admins; a log of what testers did. (8) Interface: a permanent purple SANDBOX band with the clock; a “Sandbox tools” button in the bottom-right corner of every sandbox page with the tools that fit the page (owner’s suggestion); the Test tools page with all of them; a note in the ORCID form. Nothing carries over to TestNet or MainNet.
  • Spec refs: §3.2, §15, §16 · Tests: projects/web/test/sandbox.test.ts, projects/indexer/test/sandbox.test.ts

D-154 Sandbox: problem reports from testers (owner decision)

Section titled “D-154 Sandbox: problem reports from testers (owner decision)”
  • Date: 2026-10-05 · Status: accepted
  • Decision: from the list of sandbox improvements the owner chose problem reports, so that invited testers’ findings reach him with what is needed to reproduce them. “Report a problem” in the floating Sandbox tools panel: the tester describes what they did, expected and saw; the report carries the page, the account in use, the last 15 transactions the page sent through the app’s transaction flow (useTx: ids, round, or the error) and the last 15 errors (window errors, unhandled rejections, console.error), recorded only on sandbox pages, plus browser and screen size; the server adds the application, round, clock and the tester’s tools of the last 15 minutes. “Show what is sent” shows it all before sending. Stored in the sandbox’s database (text at most 4 000 characters, context at most 24 000, 30 reports per tester per hour; the reporter is a hash of the invitation code, never the code). The Test tools page lists a tester’s own reports with their status, and every report for admins, who mark them resolved or open with a note and copy the open ones as Markdown; the banner shows admins the number of open reports.
  • Spec refs: §16 · Tests: projects/web/test/sandbox.test.ts

D-155 Sandbox audit: fixes and the tools that were missing (owner decision)

Section titled “D-155 Sandbox audit: fixes and the tools that were missing (owner decision)”
  • Date: 2026-10-06 · Status: accepted
  • Decision: an agent (Claude) audited the sandbox on a local copy, as admin and as tester with two accounts, and the owner asked for every finding to be fixed and the audit run again. Fixed: (1) after a confirmed transaction the page refreshes and the layout handed the wallet a new network object; a new wallet manager was built and never resumed (use-wallet resumes once), so the next signature failed (“Cannot read properties of null”); the manager is now built from the network’s values, not the object (all networks); (2) “Governance retracts” lacked the fee for its inner transaction (the article token’s revocation); (3) “More people for a second jury” picked helpers who had already been drawn; it now excludes everyone in the article’s jury cases; (4) a page left open across a reset signed for the old application; open pages ask every 20 s (and on focus) which application runs, show “The sandbox was started again by ” and block signing; (5) after the clock moves, ten blocks follow in the background once the indexer has read the new time, so a draw that waits for 8 rounds starts within a minute (it took ~16); (6) the sandbox’s own words: getting started (test accounts, test ALGO, no storage to set up, test iDs), the welcome card, donations, the storage panel, the publish form’s provider, no “ipfs pin add” line; (7) a visit to TestNet dropped the test account: the account last used is connected again, and the invitation link remembers the sandbox as the chosen network; (8) DOIs: in trust mode the indexer takes well-formed DOIs as verified and asks nobody outside (it used to query the real Zenodo), with “Make up a test DOI” in the form; (9) “Give the seal” waits for the authors’ new test iDs to be verified before reporting; (13) the status page no longer calls an idle sandbox degraded; (14) test accounts can be renamed; (15) names: helpers get made-up names (“Helper Ana Ruiz”) and a tester’s test identity shows the account’s name, from a file the web app writes next to the manifest that the indexer reads (identities and DOIs looked at again every minute in trust mode); (16) messages: blocks made, jurors seated now; (17) the seal check offers “Declare a test ORCID iD now”. Added tools: a helper comments, replies to the latest non-helper comment, or mentions the tester; a seat on a jury for the tester’s account (a dispute that qualifies the account first, then helpers ahead of it in the published order recuse until it is seated; or “Put me on the jury”); journals with helpers (a helper founds a journal and invites the tester; helpers invited by the tester accept; a helper editor selects an article; helper addresses to invite). The second audit also found and fixed: the dispute page and the article’s notice showed round n+1 (the contract counts rounds from 1), and the article page fetched juries for every article; a journal invitation in the inbox read “Article 0” and led to the founder’s profile instead of one’s own journals where Accept is; the start page’s subtitle; “already in this journal” for a repeated selection. Not changed: inboxes and journal lists show addresses (or NFD names), not ORCID names, on every network.
  • Spec refs: §9, §15, §16 · Tests: projects/web/test/sandbox.test.ts, projects/web/test/journals.test.ts, projects/indexer/test/sandbox.test.ts

D-156 Accessibility, ergonomics and order pass (owner decision)

Section titled “D-156 Accessibility, ergonomics and order pass (owner decision)”
  • Date: 2026-10-06 · Status: accepted
  • Decision: an agent (Claude) reviewed the web app on the sandbox for accessibility (WCAG 2.2 AA), ergonomics and order, with automated checks at 1280 and 375 px in both themes plus keyboard tests, and the owner asked for all fourteen findings to be fixed. Fixed: (1) every visible label is tied to its field (publish, review, comment, flag, status, CID, storage provider); the review and comment editors have names; (2) links inside running text are underlined (colour alone told them apart); (3) the dark theme’s “low contrast” buttons were a measurement taken during the buttons’ 0.15 s colour transition: measured after it, every text passes, so nothing changed; (4) the sandbox panel: its button comes first in the document, so Tab goes from it into the panel (CSS shows the panel above), Escape and Close return focus to the button, the panel has a heading and its select a name; (5) the wallet and account lists under the header buttons were announced as ARIA menus without the arrow keys a menu promises: they are plain lists of links and buttons now (a disclosure), Escape returns to the button, and a click or Tab outside closes them; (6) headings inside a review or comment start at h3 under “Reviews” or “Comments”, whatever level the author used (## or ###), so the outline never skips; (7) article titles in the feed, citations and lists are headings (h2), with the same look, so people can move from article to article; (8) every target is at least 24 px: addresses, footer links, field tags, the brand on phones, “Test tools”, summaries and filter checkboxes; a profile’s figures make the whole card the link; (9) a profile’s title is the person’s NFD name or verified ORCID name when known, the address below it; (10) the Test tools page has an index and four groups (You and time, An article, Other people, The sandbox), with panels side by side on wide screens; (11) the sandbox panel’s article tools are under sub-headings (Votes and seal, Dispute, Comments, Jurors answer, Governance resolves, The jury); (12) the sandbox band is one line on phones (119 px before, 40 now; the sentence stays for screen readers); (13) the footer keeps room below it for the floating button; (14) “Connecting…” under the article’s actions reads “Getting your account ready…” and is announced politely. Also: the sandbox tools’ failures use the plain words of D-145 (an account without ALGO is told to use “+100 ALGO”), and a test account’s state no longer differs between the server’s page and the browser’s (a hydration warning on the Test tools page). Not changed: the order of the article page (actions, body, versions, peer review, reviews, comments) and the header’s entries were found sound.
  • Spec refs: §16 · Tests: projects/web/test/markdown.test.ts

D-157 Security audit of the web and the server: every finding fixed (owner decision)

Section titled “D-157 Security audit of the web and the server: every finding fixed (owner decision)”
  • Date: 2026-10-06 · Status: accepted
  • Decision: four reviews by agents (Claude): the host over SSH, read only; the web app’s server side; the browser side and the response headers; the indexer and the deployment. About 90 harmless requests went to production, and nothing was exploited. The owner asked for everything to be fixed. Host (the owner ran the commands): SSH accepted passwords (the cloud image’s 50-cloud-init.conf won over 60-cloudimg-settings.conf), with about 13 000 guesses a day; a 10-ariadne.conf now allows keys only and turns X11 forwarding off. The only two password logins were the setup on 2026-10-01, from the IP that later used the key, so there was no sign of intrusion. 2 GB of swap were added and the kernel update applied with a reboot. Indexer: markdownToText (search text, seal word counts, also inside /search/posts) had quadratic regexes: 40 000 “[” took 2.7 s, so one 1 MiB comment could freeze the single-threaded indexer at every restart. It is linear now: input cut to 4x the kept length, classes exclude the delimiter that opens the next attempt, lengths bounded, comments removed with indexOf. The same was fixed in references, front matter, DOI and ORCID URL trimming, with 31 hostile 1 MiB inputs tested. The regex search budget is per client (first X-Forwarded-For address, a /64 for IPv6; 4 s a minute, then 429), within the global 20 s, with 1 s per search; list parameters are capped at 20 values (400 beyond); a search stopped by its timeout no longer leaves its statement open. Withheld content no longer leaks through titles in profiles and notifications, the current CID in content_record, text/keyword/CID searches or /search/suggest, and Kubo garbage-collects after unpinning (retried hourly if it fails). The API answers through a query_only connection and sends nosniff. A Kubo RPC token is supported (ARIADNE_KUBO_API_TOKEN). Web: sandbox and dev content was served from the app’s origin with SVG as image/svg+xml and no policy, so a tester’s SVG opened by the owner could have created an admin invitation and read keys in localStorage. These responses now carry the gateway’s policy (CSP default-src ‘none’ … sandbox), and anything but plain images and text is a download. The reading time on every article page (and the review form’s word count) had the same quadratic regexes on the web server (40 000 “<” took 1.5 s per view); the review form now uses an exact copy of the indexer’s linear function, and the reading time linear patterns. The version comparison skips texts over 20 000 lines and word diffs over 4 000 characters, since an author could have made Myers’ search run for minutes. Other web fixes: the Zenodo reading copy’s link pattern is bounded. Push subscriptions: capped body reads, a per-client limit, keys that must be a real P-256 point and a 16-byte secret, and a first notification the push service must accept before a new subscription is kept, so made-up endpoints cannot fill the table. The sandbox tools: helpers 1 to 10 at a time and 300 in all, the clock at most five years ahead, names.json at most 5 000 entries, helpers keep their names, the account query and the problem report’s page validated, the request body capped. The algod proxy: no “.” or “..” segments, anchored paths, same-site check and capped body on writes. Dev pin: same-site check and capped body. NFD and avatar routes are rate-limited per client. The sandbox gate decodes the path (“%73andbox” was served by the avatar route) and routes that take the network from a JSON body see the sandbox only for testers. Images whose path resolves outside the article’s folder (“%2e%2e”) are not embedded. Formulas cannot paint outside a text’s box (overflow: clip). Provider keys are kept for the tab unless “Remember on this device” is ticked. Problem reports drop anything that looks like a mnemonic or a key. KaTeX is 0.19 (advisory), ws and bn.js updated within their majors (9 moderate WalletConnect v1 advisories remain, no upstream fix). No X-Powered-By. A content security policy with a nonce is sent Report-Only, with violations logged by /api/csp-report, to be enforced once real wallet use is clean. Deployment: HSTS (subdomains included), nosniff and Referrer-Policy on every host; on the app, X-Frame-Options SAMEORIGIN, a base CSP (frame-ancestors, object-src, base-uri, form-action), Permissions-Policy and COOP same-origin-allow-popups. Request bodies are capped (1 MB, 42 MB for dev pin, 16 KB on the API and gateway), and the global notifications feed is closed to the public. deploy/.env was visible inside web and both indexers through a mount of deploy/; the server now mounts nothing of it, and the local rehearsal uses docker-compose.local.yml. Two networks: edge, plus a sandbox network without internet access that only the web app joins (Caddy and the public indexers no longer reach algod or the sandbox indexer). Kubo’s RPC requires a bearer token (API.Authorizations; the healthcheck uses it). Every container has no-new-privileges, a memory ceiling, pids_limit 256 and rotated logs. Capabilities are dropped where possible: web and indexers keep none, Caddy and docs only NET_BIND_SERVICE (the Caddy binary carries it as a file capability and will not start without it in the bounding set, as the rehearsal showed). The sandbox indexer gets no ORCID credentials. .dockerignore and deploy/.gitignore cover any *.env and Notepad’s .txt copies. The whole configuration was rehearsed locally (sandbox profile: services healthy, Kubo 403 without its token, isolation, headers, limits) before it went to the server. Not changed: the sandbox cookie keeps the invitation code (HttpOnly, signed; its HMAC and the Zenodo cookie’s AES keys are derived differently). Pending (ROADMAP): enforcing the CSP, and backups (owner’s choice).
  • Spec refs: §14.4, §15, §16, §18 · Tests: projects/web/test/security.test.ts, projects/web/test/linear.test.ts, projects/web/test/sandbox.test.ts, projects/web/test/markdown.test.ts, projects/indexer/test/linear.test.ts, projects/indexer/test/withheld.test.ts, projects/indexer/test/search.test.ts, projects/indexer/test/api.test.ts, projects/indexer/test/public.test.ts

D-158 User journeys: every finding of three testers fixed (owner decision)

Section titled “D-158 User journeys: every finding of three testers fixed (owner decision)”
  • Date: 2026-10-06 · Status: accepted
  • Decision: three agents (Claude) played an author, a reader who becomes a reviewer, and a juror and journal editor on a local sandbox (production build), each with its own invitation and browser identity, and reported what failed or confused; the owner asked for everything to be fixed. Reading and publishing: the article page shows the abstract, keywords (linked to search) and licence of the front matter; identifiers (ASA, CID) move into “Technical details”; badges show once; a just-published article says it is being read and refreshes itself. Publishing has a preview of the article as readers will see it, a labelled Markdown box, plain wording (exact terms under “Technical details”), field names as YAML comments next to the field codes (both parsers accept them), and links to other articles written as “/sandbox/a/2”, “/testnet/a/2” or “/a/2” are recognised as articles, rendered as app links and counted as citations. Reviews and the seal: the form shows every requirement at once as a checklist, the recommendation starts empty and is required, “Publish the review” replaces “Pin and submit review”, and quoted lines (starting with “>”) no longer count toward the 200 words, in the web and the indexer alike (SEAL_RULE 4, seals recomputed); the seal panel says “being checked” instead of “No review yet” or an IPFS message on the sandbox, refreshes itself while something is pending, and words versions correctly. Test DOIs use the 10.5072 prefix and show as “test DOI” without a doi.org link. Printed articles and volumes name each network (Sandbox, LocalNet), a retraction’s real cause (authors, or governance after a jury) and its order relative to the selection, and the same seal words as the page. Citations: an author without a name is the short address; RIS uses UNPB for preprints. The reading toast needs 3 minutes of reading. Names: one rule everywhere a person appears (NFD name, else the verified identity name from the indexer’s new GET /names, else the short address); mentions are found by name (people search now ignores accents and case) and shown by name; the sandbox no longer invents “Sandbox Researcher NNNN” or prefixes helpers with “Helper”, and a test iD takes the test account’s name. Notifications (indexer schema 15): new kinds coauthor_confirmed, review_useful, reviewed_version, disputed (system, with the flag weight), dispute_resolved (with the outcome), journal_editor_accepted, journal_editor_selected, seal_granted, doi_verified; the jury verdict says its outcome; one notification per person and action (reply > mention > comment on my article > followed article); journal notices carry the journal’s name; links go to the review (#r), comment (#c) or dispute round; unread counts are computed one way in the bell, the inbox and the profile; targets scroll clear of the sticky header and are highlighted; push messages use the same sentences. Disputes: a flag may carry a note of up to 500 characters, sent as a “flag-note” list-note op in the same atomic group as the flag call and kept with the flag; after flagging the page says where the round stands (weight of 30) and what a round is; flagging that needs more reputation says so; the dispute page keeps every round’s flags (reason, note, weight) after resolution, shows each seat’s “answer by” date and state, future times as dates (“in 7 days”), refreshes itself while a case is open, and a cleared article says it was disputed and cleared. Article actions: a claimed reputation stays hidden until the indexer catches up; an invited co-author first sees only “Confirm co-authorship / I am not a co-author” (the review form waits too); the vote panel says the account’s weight; cost lines are plain, with protocol terms under “Technical details”. Journals and lists: retracted articles are not offered for selection; /journals has “Start a journal” (one signature); accepted editors get select and take-out controls on the journal page; “+ New list” under public lists is public; a list being made public stays visible; journal invitations are one confirm step. Following: the feed has a “Following” tab. Shell and sandbox: no “Connect a wallet” flash while the account is being found (useChain().loading; a placeholder of the same size in the header); on the sandbox the words are “Choose a test account”; the account menu lists this browser’s test accounts and announces switches; the sandbox panel closes on page change, shows progress and results at its top, offers only tools that apply and follows the jury’s outcome for governance; the tools accept journal URLs, log plain names and label each account’s buttons; generated articles sit in fitting fields, host articles read as ordinary test articles; on phones the tools button is a small round button at the end of the tab bar, which gains Journals, and the notifications note takes one line. Copy buttons say “Copied” or “Copy failed: select the text”. Verified on the local sandbox after the integration (schema 14 to 15 migration on the testers’ data, a flag with a note through the chain and back, the inbox, the Following tab, a phone view).
  • Spec refs: §9, §11, §13, §15, §16 · Tests: projects/web/test/d158-reading.test.ts, projects/web/test/d158-people.test.ts, projects/web/test/d158-disputes.test.ts, projects/web/test/d158-shell.test.ts, projects/indexer/test/notices.test.ts, projects/indexer/test/public.test.ts

D-159 A “Papyrus” theme (owner decision)

Section titled “D-159 A “Papyrus” theme (owner decision)”
  • Date: 2026-10-06 · Status: accepted
  • Decision: the owner asked for a new theme, pastel brown, legible, like papyrus. The theme switch (D-099) offers System, Light, Dark and Papyrus; Papyrus is remembered in this browser like the others (localStorage ariadne.theme, applied before the first paint). Palette: parchment background #f4ead3, sidebar #eadcbd, panels #f9f2e2, sepia ink #33281c, secondary text #64523c, rust-brown links #8a4517, a softer green for votes, red and amber tuned to the paper; every text pair meets WCAG AA (body 12:1, secondary text above 5.5:1, links above 5.2:1; a test checks the pairs from the CSS) and a scan of four pages found no text below the minimum. Articles, reviews, comments and page titles use a system book face (Iowan Old Style, Palatino, Charter, Georgia) only in this theme; the interface keeps the system font. The system’s dark palette never overrides a chosen Papyrus. The switch shows its four choices two by two in the sidebar and in one row in the footer. Print is unchanged (black on white).
  • Spec refs: §16 · Tests: projects/web/test/people.test.ts

D-160 Performance: every finding of the audit fixed (owner decision)

Section titled “D-160 Performance: every finding of the audit fixed (owner decision)”
  • Date: 2026-10-07 · Status: accepted
  • Decision: agents (Claude) audited performance: the web’s bundles and server rendering, and the indexer on a synthetic database of 20 000 articles, 20 000 reviews, 40 000 comments, 184 000 votes and 50 000 notifications; production was measured from inside the server (the owner’s link was losing packets that day). The owner asked for everything to be fixed. Indexer (schema 16): the HTTP API runs in a worker thread with its own read-only connection, so passes no longer stall it (a 1 ms route went from a p50 of 4.7 s while passes ran to 1.8 ms); passes are incremental (SQL triggers fill work queues and change counters bumped only when a result changes; content rechecked every 30 days, fetched_at untouched when unchanged; word counts stored per text; one cached copy of the seal data; sandbox identities rechecked only when the names file changes), idle cost from about 4.9 s to 43 ms per cycle; new indexes and PRAGMA optimize (at start and daily, ANALYZE once after the migration); search rewritten (dirty queues, two-phase fetch, shared facet scan cached by data generation, facets=0, post search from the matches, cited counts stored, keyword and people tables for suggestions): text search from 10-120 s to 0.07-0.5 s, suggestions from 0.8 s to 25 ms per keystroke; Thread Score computed daily in 7-day age bins (linear; 8.3 s to 0.35 s at 100 000 articles; tested against the exact pairwise reference); article and profile lists cut at defaults (100 reviews and 300 comments; 200 publications, reviews and comments) with counts, has_more, paged routes and keyset cursors; notifications take after= (echoed) for increments; public answers carry Cache-Control max-age and weak ETags (304), with an in-memory response cache; peak memory from 433 MB to 294 MB. Wildcards over every article body alone still hit the 1 s pattern limit (D-157), by design. Web: the contract client loads only when an action is prepared or signed, and the browser copy of the ARC-56 has no TEAL source (scripts/patch-client.mjs writes ariadneClient.ts for the browser and keeps ariadne.ts, with the source, for deploying); review and comment bodies are rendered on the server; editors, CID and pinning code, QR codes and the sandbox panel load on demand; KaTeX’s stylesheet only where formulas render: first-load JS shared by every page from 416K to 304K gzip, the article page from 636K to 352K. The algod genesis check is cached (10 minutes, 30 s on failure, 2 s timeout); the article page fetches in one round; immutable content, directory listings, version diffs, the About page, the donate QR and the sitemap are cached in process; pages wait at most 300 ms for names; Caddy compresses (zstd) instead of Node; the bell loads the newest 50 notifications and then only increments; feed links do not prefetch; the sandbox’s clock loop runs only while testers are active (it made about 180 algod requests a minute day and night); the sandbox panel pings every 60 s and only while visible. Kubo: a DHT client (Routing.Type autoclient) announcing only its pins, with 20-60 connections: it served 171 peers’ DHT queries with a quarter of a CPU and about 20 GB a day for 8 pins of its own. The migration was tried on a copy of the production TestNet database and the whole stack rehearsed locally before the deployment.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/d160.test.ts, projects/web/test/d160-bundle.test.ts, projects/web/test/d160-server.test.ts, projects/web/test/d160-server-inbox.test.ts

D-161 Responsible use of Nodely’s free tier (owner decision)

Section titled “D-161 Responsible use of Nodely’s free tier (owner decision)”
  • Date: 2026-10-07 · Status: accepted
  • Decision: ARIADNE reads TestNet and MainNet through Nodely’s free public API (60 requests a second and 10 at a time per IP, 6 million requests and 5 GB a month, failed requests counted; projects above a million a month are asked to mention Nodely). The owner asked for responsible use. Measured on a copy of the production TestNet database, the indexer made 48 requests a minute (about 69 000 a day, 2.1 million a month for one network): algod’s status and each new block in full, every 2 seconds. Indexer: on a public network (not devOnly) with an Algorand indexer, every poll now goes through that indexer alone (ARIADNE_SYNC=indexer, the default there; algod keeps the block reading of development chains and the sandbox): one health request for the round it has indexed and one search per filter (application calls, list notes) up to that round, never past it (a transaction of a later round would be lost once the watermark moved), never a whole block nor algod’s status (the subscriber gets the tip as currentRound and has no rounds left for algod); polls every 20 s instead of 2 (ARIADNE_POLL_SECONDS still wins). Measured again: about 8.5 requests a minute (about 12 000 a day, 370 000 a month per network), new activity visible within about 20 s. A failed poll waits longer each time (the interval doubled, at most 10 minutes, at least a minute after a 429 or a 403), and every request names ARIADNE in its User-Agent. Web: the status page’s report is shared for a minute and the page refreshes every minute (it was 10 s and 15 s: about 7 probes to Nodely each time); the genesis check of a public network is cached for 6 hours (10 minutes on development chains and the sandbox). Transactions prepared in the browser go from the reader’s own address and do not count against the server’s. The About page credits Nodely. Pera’s public API was considered: it describes assets on MainNet only and cannot replace an algod or an indexer, so it is not used.
  • Spec refs: §15, §16 · Tests: projects/indexer/test/d161.test.ts, projects/web/test/d160-bundle.test.ts, projects/web/test/status.test.ts

D-162 Adversarial audit of the contract and its economics: every off-chain finding fixed (owner decision)

Section titled “D-162 Adversarial audit of the contract and its economics: every off-chain finding fixed (owner decision)”
  • Date: 2026-10-08 · Status: accepted
  • Decision: three agents (Claude) attacked contract v3 and its incentives before MainNet: the contract code on LocalNet (no critical finding: the application account cannot be drained, no box of someone else can be written, a payment cannot serve two calls, the artefacts are reproducible byte for byte), the economics of reputation with an extended copy of the D-150 simulation, and the parity between the contract and the indexer (a differential fuzz of 17 443 random accepted calls found the reducer equal to the contract and to the Python oracle, and rebuild equal to live). They found that any article can be disputed in a day for about 1.6 ALGO (comment resolution mints reputation from nothing; one address with reputation 841 disputes alone), that a Peer-reviewed seal can be faked for about 0.64 ALGO with one ORCID iD behind two reviewers, that the jury’s pool was read after the seed was public, that the D-150 repeat-vote damping keyed on the submitter is bypassed by front submitters, that governance retraction is immediate and that one call could brick governance; the contract findings are fixed by contract v4 (D-163). The owner asked for everything to be fixed. Fixed now, off chain: ingestion (P-1, a regression of D-161): a public endpoint may answer the health request and a search from indexers at different rounds, so every search’s round is recorded and a poll never moves past the lowest (production was compared with a fresh full read: nothing had been lost); at start the indexer compares the deployed approval program with its ARC-56 and refuses another version; raw log bytes are kept and rebuild decodes from them; notification ids survive a rebuild. Jury: the eligible pool is frozen as of the flag that started the dispute (its hash is published), one ticket per ORCID iD, only confirmed authors are excluded, flaggers’ self-asserted employers are ignored, ORCID iDs and reputation must be 90 days old (reputation from at least 3 people outside the authors and flaggers), every deadline uses chain time, no draw for a dispute already resolved, no consensus never clears by itself (a second jury, then governance with public reasons), answers stay hidden on the site until the jury closes. Seal rule 5: one review per ORCID iD, expertise reputation given by at least 3 people other than the authors and earned 90 days before the review, a new conflict when an author supported the reviewer in the 3 years before, “changes requested” only from reviewers passing the same tests. Citations: a CID or DOI belongs to its first (or verified) article and duplicates are shown; Thread Score version 4 counts a citation once per citing submitter, cited article and year, and only from articles with a counted review or a vote of reputation 100. Interface: Google Scholar journal tags only from selections the submitter accepted (new list-note operation “selection”), amendments by non-authors shown as third-party notes, the submitter’s DOI with conflicts shown, inert co-authors never shown as people, claims under the daily cap, reputation by source, notifications from new accounts folded into a daily digest and at most 20 per sender and recipient a day, a pin quota of 200 MB per address per 30 days. The sandbox indexer runs with ARIADNE_AGE_DAYS=0 so its tools need no 90-day clock jumps. Schema 17, rehearsed on a copy of the production TestNet database.
  • Spec refs: §15, §16, §18 · Tests: projects/indexer/test/d161.test.ts, d162-jury.test.ts, d162-seal.test.ts, d162-citations.test.ts, d162-ingest.test.ts, d162-followup.test.ts, projects/web/test/d162-web.test.ts

D-163 Contract v4, the MainNet candidate: bonds for flags, damping of repeated grants, two-step governance (owner decision)

Section titled “D-163 Contract v4, the MainNet candidate: bonds for flags, damping of repeated grants, two-step governance (owner decision)”
  • Date: 2026-10-08 · Status: accepted
  • Decision (the dispute rules below were amended the same day, before any deployment: see the next bullet; the threshold stays at 30, the bond grows, expiry pays nobody): the contract findings of the adversarial audit (D-162) are fixed by a new contract, which replaces the D-150 rules where they differ. Disputes: a flag weighs at most 10 (1 + isqrt(reputation), capped), so a dispute needs at least three people; the flagger’s reputation box in the field must be 30 days old; every flag deposits a bond of 1 ALGO besides its storage, refunded when the dispute ends in a retraction or when the round lapses, paid to the submitter when the dispute is cleared or expires (settle_flag, which anyone may call; the flag’s storage deposit always goes back to the flagger, or with the bond when the flagger closed the account); a round that does not become a dispute within 30 days of its first flag lapses; the threshold doubles after each clearance for 365 days (30, 60, 120, 240, 480; the count of clearances is kept, so a later clearance doubles from it); a dispute expires after 60 days (expire_dispute, by anyone; it counts as a clearance); governance clears at once but retracts only 14 days after the dispute began, with the CID of its published reasons in the event; the submitter may retract a disputed article; votes, claims, confirmations of co-authorship, review and comment votes and comment resolutions continue while disputed (a dispute is a label and a jury case); the bond flows keep invariant 22 restated (spendable balance never below the bonds held, each bond paid out once). This replaces D-150’s “flags count less after a cleared dispute” by a direct cost. Damping (replaces D-150’s per-pair rule, which front submitters bypassed): one counter per (giver, receiver), key “d” + sha256(giver || receiver), counting every grant; the n-th grant is the weight divided by n (a weight-1 voter’s second vote for the same person is worth 0, by design); an article vote is damped by the largest count over all its authors (the voter lists the submitter and every declared co-author, and every author’s counter moves); comment resolution grants 5 // n and only when the resolver has reputation 10 in the field. Governance changes in two steps (propose_governance, accept_governance signed by the new address; the application’s own address is refused); set_governance is gone. Other: amendments only by an accepted author of the parent; DOIs only by the submitter; Followed.on canonical; reputation boxes record the day they were created. Events have schema_version 2 and a gap-free global event_seq; DisputeResolved names who resolved and the reasons CID; Voted carries the raw weight and n; Flagged the bond and the threshold; new FlagSettled and GovernanceProposed. Program 7 252 of 8 192 bytes; the largest single call costs 590 opcodes; measured costs in projects/contracts/V4_REPORT.md (a flag deposits 29 700 microAlgo plus the bond, 7 300 more when it opens the dispute). The audit’s attacks were ported as LocalNet tests and now fail or cost what this design says. TestNet restarts with a v4 application, as at D-110; the independent security review and the MainNet conditions of D-108 still apply.
  • Amended the same day, before any deployment: the economics simulation re-run on the exact v4 code (8 seeds; results in the audit’s economics folder) confirmed that v4 closes the cheap attacks (a first dispute costs about 7.6 ALGO and 30 days of preparation instead of 1.6 ALGO in a day; nobody can dispute alone; the ring with front submitters falls from 1.89 to 0.012 times the best honest reputation; founders’ share of vote weight 10%, weight-to-talent correlation 0.75, the best of the rules compared) but found a new flaw: an author’s own accounts could dispute his article, take the bonds back when it was cleared or expired, and double the threshold each time (16 times for about 0.25 ALGO). So the threshold stays at 30 and the bond grows instead (1 ALGO << clearances, at most 16 ALGO, for 365 days after the last clearance); only a governance clearance counts; at settlement a cleared dispute pays half the bond to the submitter and the application keeps the other half, an expired dispute’s bonds are all kept (there is no withdrawal path: they are burned), a retraction or a lapse refunds them; FlagSettled carries the amount retained, and invariant 22 counts retained amounts. Off chain: the notes of rounds that never became disputes are not shown; for the seal and the jury a granter counts as a person only with a verified ORCID iD or rule-5 expertise, and grants between reciprocal pairs are ignored; Thread Score’s vouching vote must come from a rule-5 expert. Coordinated-voting detection moves before MainNet (a ring of 50 accounts voting each other’s reviews useful still reaches the best honest reputation for about 54 ALGO per account and year). Program 7 307 bytes. A differential fuzz of the amended contract (8 seeds, 56 707 accepted calls, every bond level from 1 to 16 ALGO) found the contract, the Python oracle and the indexer in exact agreement, and the application balance equal to the bonds held plus what was retained. Left as is and noted for the independent review: a declared co-author may flag the article before confirming co-authorship.
  • Spec refs: §5, §6, §7.3, §7.4, §8, §9, §10, §11, §12, §13, §17, §18 · Tests: projects/contracts/tests/unit/test_v4_disputes.py, test_v4_damping.py, test_create_governance.py, tests/localnet/test_96_v4.py, test_98_audit_pocs.py, test_82_invariant22_m2.py
Section titled “D-164 Privacy and legal audit; the legal front end and the off-chain fixes (owner decision)”
  • Date: 2026-10-08 · Status: accepted (the legal texts are drafts for the owner’s lawyer)
  • Decision: three agents (Claude, not lawyers) audited ARIADNE for data protection (GDPR, LOPDGDD, ePrivacy/LSSI cookies), platform law (DSA, LSSI-CE, the TCO Regulation on terrorist content, copyright, defamation, research data) and crypto, consumer, licence, trademark and accessibility law, with primary sources checked as of this date. Main findings: no legal notice, privacy policy, terms, contact or notice channel existed (DSA arts. 11, 12, 14, 16; LSSI art. 10; GDPR arts. 13-14); the published policy of acting only on binding requests would lose the hosting safe harbour (DSA arts. 6, 16(3)); there was no emergency path for terrorist content (one hour, TCO art. 3) or child sexual abuse material, and the transparency page would have listed their record numbers; the indexer copied ORCID e-mails; withholding reached only articles, reviews and comments and kept their texts; texts promised things the service does not do (no e-mails collected, a warning before every signature, an access log). The owner chose to fix everything that needs no contract change now (block A); the on-chain design (ORCID iDs and free-text notes on chain in clear) goes to a contract v5 before MainNet (block B); the owner’s own tasks (an entity, the operator’s identity in the legal notice, the lawyer’s review, tax advice on donations, the trademark, the AGPL question, the impact assessment) are block C. Done: a legal notice and contact page (English and Spanish), a privacy policy, terms of use (English and Spanish) accepted before the first signature, a DSA notice form (anonymous for CSAM) and a rights page where an address is proved by a signed message, an owner’s view with phone alerts, security.txt, a licences page with the notices of the code sent to browsers; the operator’s identity is read from the server’s environment and never stored in the repository. Reviews and comments are published under CC BY 4.0, articles must choose a licence, minimum age 16, Spanish law with consumers’ own protections. Withholding covers every kind of text and person and earlier applications, with grounds (order, notice, terms, data protection, copyright, csam, terrorist, other), statements of reasons and redress, purges stored texts, is reloaded from a file the owner can edit on the server, and the transparency page shows sensitive cases only as monthly counts; Kubo applies the IPFS Bad Bits list; terrorist content is preserved encrypted for six months (TCO art. 6), child sexual abuse material never; an emergency playbook works from a phone. ORCID e-mails are no longer read and stored ones were deleted; ORCID data is kept only while an iD is verified, and a declared iD is shown as unauthenticated (ORCID’s guidelines). A person may object to profiling (a public list note): their Thread Score and reputation figures are hidden and they are excluded from juries. Logs keep no full IP address and no User-Agent; push subscriptions without delivery for a year and sandbox reports after six months are deleted; the wallet modals no longer load Google Fonts; outside services get an identifying User-Agent and their rate limits are respected. Wording follows consumer law (non-refundable storage deposits, no tradable token, gifts to the operator, a commission on NFD sales). Late research found an EU trade mark “Ariadne” registered on 19 June 2026 by Ariadne GmbH for software and SaaS (classes 9, 35, 41, 42): renaming before MainNet is being considered (FILUM screened as the best candidate; a trademark attorney must clear it).
  • Spec refs: §14, §15, §16, §18 · Tests: projects/web/test/legal-.test.ts, d164-web.test.ts, projects/indexer/test/d164-.test.ts

D-165 The project becomes Cabdell, at cabdell.press (owner decision)

Section titled “D-165 The project becomes Cabdell, at cabdell.press (owner decision)”
  • Date: 2026-10-11 · Status: accepted (the name still needs a trademark attorney’s opinion before MainNet)
  • Context: the EU mark “Ariadne” (Ariadne GmbH, classes 9/35/41/42) conflicts with the project’s name (D-164). After screening many candidates the owner chose Cabdell (Valencian and Catalan for a ball of yarn: Ariadne’s ball of thread) and the brand identity “One thread, wound and followed”, and bought cabdell.press (DNS cabdell.press, www., api., ipfs. to the server, 2026-10-11). Full screening (agent, 2026-10-11; summary in the session’s notes): no CABDELL mark anywhere in TMview; the risk is DELL (Dell Inc.): LEXDELL (General Court T-641/14) and TaiDell (OD B 3 118 304) were refused, so registration in class 9 is medium-high, 42 medium, 41 low-medium; CAPDELL (EUTM 006621296, furniture, class 35) sounds the same; the journal “Cabdells” (Oliva, since 1999) and parked cabdell.es / cabdell.cat exist. Mitigation followed in the design: one word, lower case in the logo, the thread running under the whole word, no blue, no circle around the word, never “CabDell” or “CABDELL”.
  • Name in text: Cabdell (capital C only), the site cabdell.press; never all capitals and never split. This replaces D-096’s “ARIADNE in capitals”. The former name is said once where the name is explained (About, the docs’ introduction, the legal notice and the footer: “Cabdell was called ARIADNE until October 2026”, with the non-affiliation note). src/lib/brand.ts holds the name, the palette and the mark’s geometry.
  • Identity (design canvas “Cabdell — brand identity”): the symbol is a spiral of thread drawn as one stroke (five half-turns; three below 40 px) whose loose end, the only orange, leaves to the right and in the main lockup underlines the name. Colours of the Valencian horta: Calç #FAFAF7 (ground), Nit #121A17 (text), Horta #17463A (brand, buttons), Taronja #F07B1F (the thread, lines and fills only), Taronja fosc #B4500C (orange text), Taronja clar #FFA25C (thread on dark); the dark theme on green-black #0F1A16 with a mint accent #8FD1B3; papyrus (D-159) keeps its palette and takes the mark’s colours. Type: Bricolage Grotesque (interface and headings), Newsreader (articles, reviews and comments), DM Mono (addresses, CIDs, figures), served from the site through the @fontsource packages (OFL-1.1, listed in /licenses by the notices script), no font service. The logo’s letters are outlines (scripts/brand_outlines.py, fontTools and HarfBuzz from the served font files, at the display optical size) so the lockup, the icons, the link preview and the docs’ logo look the same everywhere; scripts/pwa-icons.mjs and scripts/og-image.mjs write the favicon, the app icons, the docs’ logo and the 1200 x 630 card.
  • Domain: cabdell.press is the app (APP_HOST), api.cabdell.press and ipfs.cabdell.press the indexer and the gateway; ariadne.press, www.ariadne.press, www.cabdell.press and the sslip.io name redirect permanently (LEGACY_APP_HOST), and api.ariadne.press / ipfs.ariadne.press keep answering next to the new names. The manifest gains legacyAppUrls: ["https://ariadne.press"] for TestNet and MainNet: the indexer accepts profile links and DOI records that point at the old origin (ORCID iDs already verified stay verified), and both the indexer and the web count links to articles at the old origin as links to this site (citations, link previews, the pre-publication checks). New ORCID instructions, Zenodo drafts, citations and the User-Agent use cabdell.press. Browser storage belongs to an origin, so a reader’s theme, pinning keys and wallet session must be set again at cabdell.press; the owner adds https://cabdell.press/api/zenodo/callback to the Zenodo sandbox application.
  • Unchanged until the next contract (v5): the contract’s name and events, the per-article tokens (Ariadne Article, ARIA), the ARIADNE_* settings, package and image names, browser storage keys, the file format of preserved notices (ARIADNE-PRESERVED-1), the donation NFD ariadne.algo and the Zenodo community ariadne-algo. spec/ARIADNE_SPEC.md keeps its name and text with a note that ARIADNE reads as Cabdell; this log keeps its history.
  • Spec refs: §16 · Tests: projects/web/test/branding.test.ts (lockup, name, manifest and former origin), docs.test.ts, legal-pages.test.ts; projects/indexer/test/verify.test.ts (former origin)