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).
D-001 Grouped MBR payment convention
Section titled “D-001 Grouped MBR payment convention”- 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 readspre = app.min_balancebefore creations andpostafter (acct_params_get AcctMinBalance), assertspay.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
D-002 M1 status-matrix scope and disputed
Section titled “D-002 M1 status-matrix scope and disputed”- Date: 2026-09-29 · Status: accepted
- Context: §7.3 rows 6-8 need
flag/resolve_dispute(M2);disputedis unreachable on LocalNet in M1. - Decision: M1 LocalNet matrix =
set_statusover all 25 pairs x {author, governance, stranger}; only the 5 author rows succeed;disputedas source or target always fails viaset_status. Disputed-gated negatives (new_version, vote_article, claim_reputation, claim_article) are offline tests seedingstatus = 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; assertsgovernance != ZeroAddress; stores globalsgovernance(bytes) andarticle_seq_next = 1(uint); emitsGovernanceChanged(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.jsonwith 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 whenclaimable == 0. Every successful claim therefore emits >= 1ReputationChanged. - Spec refs: §10.2, §11, §20 · Tests: CAP-06, CLAIM-02
D-005 claim_article status rule
Section titled “D-005 claim_article status rule”- 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_articlecolumn;set_statusfollows §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_dayderive from the singleGlobal.latest_timestampof the call.article.updated_at := tsin 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 whengranted > 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
D-007 Inner asset-config parameter sets
Section titled “D-007 Inner asset-config parameter sets”- 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
D-008 ASA url and metadata_hash
Section titled “D-008 ASA url and metadata_hash”- 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_hashstays empty.
D-009 type / parent_article rule
Section titled “D-009 type / parent_article rule”- 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 inreferences[]. - Spec refs: §6.4, §7.5, §14.2, §20 · Tests: LC-08, SEC-03
D-010 add_field validation
Section titled “D-010 add_field validation”- Date: 2026-09-29 · Status: accepted (amended-by D-047)
- Decision:
field_id != 0; boxt+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 != 0create 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; emitsGovernanceChanged(prev, new, ts). PQ-ness is asserted off-chain, primarily withnot is_ed25519_point(decode_address(addr))(py-algorand-sdk);docker exec ... algokey pq check-addressis 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 asspendable == 0before 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
D-014 publishing-guide.md
Section titled “D-014 publishing-guide.md”- 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
D-015 publish initial values
Section titled “D-015 publish initial values”- 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 assertREP_CAP_DAY <= 65_535;cap_remainingis saturating; new_version fails at version 65 535. - Spec refs: §5, §6.4 · Tests: test_versions (overflow), CAP tests
D-017 ARC-4 naming
Section titled “D-017 ARC-4 naming”- Date: 2026-09-29 · Status: accepted
- Decision:
bytes36=byte[36](arc4.StaticArray[arc4.Byte, Literal[36]]); bools =arc4.Bool; Python parameterarticle_type(selectors use types only). Event structs are constructed explicitly withschema_version = arc4.UInt8(1); no struct field defaults. - Spec refs: §6.4, §11, §12 · Tests: EVT-02 (static)
D-018 Resource references
Section titled “D-018 Resource references”- 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
auandpin vote_article; parent box in amendment publish; ASA in claim_article, new_version, retraction). Layer C asserts the populated list;txn.Accessis not relied on. - Spec refs: §6.2, §21 C · Tests: SIM-01/03
D-019 Epoch-day testing
Section titled “D-019 Epoch-day testing”- Date: 2026-09-29 · Status: accepted
- Decision: rollover in offline tests with patched
latest_timestamp; on LocalNet viaalgod.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
Flaggedgainsnew_status: uint8;resolve_commentfollows the vote_article column;comment()in retracted still creates the commenter’s reputation box; reviews stay allowed indisputed;accept_coauthorshipblocked once final;mentionsis inlinearc4.DynamicArray[arc4.Address](<= 5; Commented with 5 mentions = 278 B);round_flag_weightkeeps accumulating while disputed.- Spec refs: §7.4, §8.3, §9, §11, §22
D-021 new_version edge cases
Section titled “D-021 new_version edge cases”- 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
D-022 Vocabulary
Section titled “D-022 Vocabulary”- 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.authormeans 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 sequencearticle_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 storesasa_id: uint64(appended field, 192 B, MBR 82 900 µAlgo, publish deposit 212 200);Publishedcarriesarticle_idandasa_id; the ASA is still created (ARC-71). All ABI methods takearticle_id= sequence; methods needing the asset readasa_idfrom 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
urlis 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_versionupdatesreservethrough one inner AssetConfig that re-passes manager/freeze/clawback explicitly; no base32 on-chain;metadata_hashempty. 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-downloadfallback) and Docker Desktop (WSL 2 backend, auto-update off, version recorded). GitHub Actions / remote LocalNet throughALGOD_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
D-027 Fee model for pqsig senders
Section titled “D-027 Fee model for pqsig senders”- 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 = 1e6per 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 -> asserttxn-groups[0]["group-usage"] == expected-> send. PQ senders usestatic_fee; never rely oncover_app_call_inner_transaction_feesalone 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 (
_commitin .copier-answers.yml); LocalNetalgorand/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 firstpoetry lock; poetry.lock committed.puyapy --target-avm-versionleft 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
D-029 Repo-owned, digest-pinned LocalNet
Section titled “D-029 Repo-owned, digest-pinned LocalNet”- Date: 2026-09-29 · Status: accepted
- Decision:
algokit localnet start --name ariadneis 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, tokenax64, DevMode true. Operated withdocker compose -f localnet/docker-compose.yml -p ariadne up -d --wait | down -v. Neveralgokit localnet reset. First LocalNet test asserts/v2/statuslast-version ends in268b63433a907455d439995bf916f6b296018f4fand a pqsig txn is accepted. Docker Desktop version recorded here:<fill>. - Spec refs: §3 · Tests: test_00_node, test_00_pq_spike
D-030 ASA names
Section titled “D-030 ASA names”- 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@subroutinebuilders (uint64 viaop.itob, uint16 viaarc4.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 arearc4.Structso 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_nextright before signingpublishand build thea/aubox names from it; on TestNet/MainNet they reference seq and seq+1 (<= 7 refs incl. parent) and retry on failure. Tests usepopulate_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)
D-034 Taxonomy content
Section titled “D-034 Taxonomy content”- 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);0x0304Medical 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-firstfield order, no mode/mtime, hidden files excluded, entries sorted by name, root = the added directory itself (never-w).compute-cid.mjspasses every importer option explicitly (wrapWithDirectory: truewith paths relative to the directory). Hardened Kubo reference command adds--max-file-links=174 --max-directory-links=0 --max-hamt-fanout=256on a fresh repo without an import profile (Kubo 0.43.1, optional cross-check). Repocore.autocrlf = false, root.gitattributes* text=auto eol=lf,spec/fixtures/** -text; a test asserts no\rin the fixtureindex.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 fromalgod.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
D-037 ARC-28 decoding and replay reducer
Section titled “D-037 ARC-28 decoding and replay reducer”- Date: 2026-09-29 · Status: accepted
- Decision: neither algokit-utils 4.2.3 nor the typed clients decode ARC-28 events.
tests/ref/events.pybuilds the prefix map from the ARC-56eventslist (sha512_256(signature)[:4]), decodes withalgosdk.abi.ABITypetuples and skips the ARC-4 return log0x151f7c75.tests/ref/replay.pyrebuilds 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
D-038 Deployment without an indexer
Section titled “D-038 Deployment without an indexer”- 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
createmethod (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 whosetbox exists) and persists appId/genesisHash/governance inspec/networks.json; a later deploy reuses the app when the stored genesisHash equals the node’s andapplication_info(appId)succeeds. Noon_update/on_schema_breakpolicy is involved; the contract is immutable anyway. TestNet/MainNet governance keys come fromalgokey pq generateoutside the repo. - Spec refs: §12.1, §23 · Tests: idempotence step of the end-to-end run, TAX-05/07
D-039 Test layers and the Layer C gate
Section titled “D-039 Test layers and the Layer C gate”- 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, assertunnamed-resources-accessedempty after population,app-budget-consumed <= 600(700 is the consensus hard limit), box refs <= 8, log count <= 32 and bytes <= 1 024,group-usagerecorded; then send. Layer A limits documented intests/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
D-040 DECISIONS.md discipline
Section titled “D-040 DECISIONS.md discipline”- 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.mdcarries<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
Falcon1024AlgorandSignerregistered viaset_signerandstatic_fee; (2) raw py-algorand-sdkAtomicTransactionComposer; (3)docker exec ... algokey pq signfrom pytest. Record: signer class name found inalgosdk.signer, whether unsigned simulate omits the +2e6 usage,static_feevscover_app_call_inner_transaction_feesinterplay,set_timestamp_offsetsemantics, presence ofalgokeyin 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)
D-042 No ARC-3 / ARC-69 metadata in M1
Section titled “D-042 No ARC-3 / ARC-69 metadata in M1”- 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 generateoutside the repo (algod container or WSL); the mnemonic is backed up offline by the owner; only the address entersspec/networks.jsonand 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_reputationas not allowed inretractedandretractedis 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.pygetssimulate_usage(algod, txns, pq_senders)that wraps each unsigned transaction of a PQ sender in a SignedTransaction carryingPQSig(scheme=b"f1")and calls algod simulate with allow-empty-signatures to readgroup-usageBEFORE 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 = 96bytes (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.jsonuses maxLength 96. - Spec refs: §4, §12, §13.2 · Tests: TAX-04, TAX-06, tests/static/test_taxonomy.py
Version pins (M1) — recorded 2026-09-29
Section titled “Version pins (M1) — recorded 2026-09-29”| 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) |
Host facts (2026-09-29)
Section titled “Host facts (2026-09-29)”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.
Open questions (mirror of §22 plus new)
Section titled “Open questions (mirror of §22 plus new)”- Refund of MBR overpayment (M1 keeps it, D-001).
FLAG_THRESHOLDabsolute vs relative (M2).- Whether
disputedshould also blocksubmit_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/5grants 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_namecarrying 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’sfrom_bytes(positional constructor), so structs are plainarc4.Structand are still constructed with keyword arguments; (3)arc4.UIntN.nativeis deprecated in the emulator in favour ofas_uint64(), which the algorand-python 4.0.0 stubs also expose; the contract usesas_uint64()for every UIntN read (Address/Bool/String keep.native); (4)txn.logs(i)returns rawbytes; (5)BoxMap.maybe()on struct values cannot be tuple-unpacked in Puya 5.10.1 (in+[]used instead); (6)Account(...)in the emulator takesstr | algopy.Bytes, never rawbytes. - 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 ispushint 1; return(it never decides anything: update/delete are governed by the approval program). The approval program starts withtxn OnCompletion; !; assertbefore 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-diffstill 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/offsetanswers 404 until an offset is set; afterset_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_timestampinside a call is the timestamp of the previous block. - Decision:
time_travel(seconds)= set offset toseconds, 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_appreusesspec/networks.json[network].appIdonly if the stored genesis hash equals the node’s AND the app’s on-chaingovernanceequals the requested governance address; otherwise it creates a new application. Verified:algokit project deploy localnettwice 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-addressinside 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"")reportsgroup-usage3 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_signerandstatic_feefrom the usage formula; contingencies 2 and 3 were not needed. - caveat: never pass the Falcon signer as the explicit
signer=ofalgorand.send.*parameters. algokit-utils 4.2.3 treats any object withaddressandsignerattributes as an account and unwraps.signer, which onPQAlgorandSigneris the raw signing callable (“‘function’ object has no attribute ‘sign_transactions’”). Registered signers and explicitTransactionWithSignerobjects 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).
- signer class in py-algorand-sdk 2.12.0 is
- 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:
- 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) andsubmitter_extra: uint64(remainders credited to the submitter). Article value = 210 bytes, box MBR 90 100, publish deposit 219 400 microAlgo. vote_article(weightw,N = author_countat 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.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; thenauthor_count += 1and the new co-author’sclaimed_total := share_total(replaces “claimed_total = article.vote_total” of §7.6; same purpose: only votes cast after acceptance ever count).claim_reputation:entitled = share_totalfor a co-author andshare_total + submitter_extrafor 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).- 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 exceedsvote_total(no inflation) and at mostauthor_count - 1weight units are waiting insplit_carry.
- The article box gains three fields (appended after
- Rejected alternative: dividing every single vote (
floor(w / N)each,w mod Nto 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.weightandCoauthorAccepted.author_count). - Rejected alternative: letting the submitter claim the open
split_carryat 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 currentshare_totaland the older carry is settled away from them). Single-author articles behave exactly as in M1 (N = 1givesq = 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
payargument 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). Onlyresolve_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_coauthoraccepts any address except the submitter (an invitation nobody can accept is inert and is paid by the submitter);invited_countandauthor_countoverflow (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
D-056 M2 boxes
Section titled “D-056 M2 boxes”- Date: 2026-09-29 · Status: accepted
- Decision:
Review(reviewer, cid, recommendation, created_at, useful_total) = 85 bytes underr; reviewer index underriholds the raw 8-bytereview_seq;Comment(author, cid, reply_to, root_seq, depth, created_at, vote_total, reply_count, resolved, resolved_at) = 118 bytes underc; review and comment votes reuseVote(24 bytes) undervr/vc;Flag(reason, weight, created_at) = 17 bytes underf. 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 whenraw > 0and MUST exist (the call fails otherwise, §10.5); it is written, andReputationChangedemitted, only whengranted > 0(D-006). The vote box storesweightandgranted;review.useful_total/comment.vote_totalalways grow by the full weight. Event order: the canonical event first, then at most oneReputationChanged. - Observation for the owner (constants are tunable per §5): with
GRANT_COMMENT = 1/5a 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
D-058 Flags and disputes
Section titled “D-058 Flags and disputes”- Date: 2026-09-29 · Status: accepted
- Decision:
flagrequires an existing reputation box of the flagger in the article’s primary field withrep >= MIN_FLAG_REP; weight1 + isqrt(rep); one flag box per (article, dispute_round, flagger).round_flag_weightalways accumulates (D-020); the transition todisputedhappens in the call that makes it reachFLAG_THRESHOLDwhile the status is preprint, under_review or final, storingstatus_before_dispute.Flaggedcarriesround_totalandnew_status(the status after the call), so no separateStatusChangedis emitted (one canonical event per call).resolve_dispute(governance, status = disputed, outcome 1 cleared | 2 retracted): cleared restoresstatus_before_dispute; retracted revokes the ASA exactly like an author retraction (D-033); both then setdispute_round += 1(fails at 65 535),round_flag_weight = 0,status_before_dispute = 0.DisputeResolved.dispute_roundis 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)
D-059 Comments, threads and mentions
Section titled “D-059 Comments, threads and mentions”- Date: 2026-09-29 · Status: accepted
- Decision:
reply_tois 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 hasroot_seq= its own sequence.mentionsholds 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
D-060 updated_at in M2
Section titled “D-060 updated_at in M2”- Date: 2026-09-29 · Status: accepted (refines D-006)
- Decision: the article box is written, and
updated_atstamped, only by calls that change an article field (invite, accept, submit_review, comment, flag, resolve_dispute and the M1 methods).vote_review,vote_commentandresolve_commentchange 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()requiresmentionssorted by the raw 32-byte address, strictly ascending (compared as big-endian unsigned integers, AVMb<). 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.mentionsreflects 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_reputationreached 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)._pushwraps it for review / comment grants (box read only whenraw > 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_appreusesnetworks.json[network].appIdonly if the genesis hash matches, the on-chain approval program equalsbyteCode.approvalof 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.pyandcid.pyis 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.pyfails 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_disputefails whendispute_roundis 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 staydisputedfor ever. Found by the independent Layer A tests. - Decision:
flagfails with “dispute round overflow” whendispute_roundis 65 535. An article can therefore go through at most 65 534 disputes and can never be stuck indisputed. The guard inresolve_disputestays 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 localnetcreated and seeded the application and then failed withOSError: [Errno 22]while openingspec/networks.jsonfor 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_manifestretries 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_COMMENTstays 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:
- 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.
- 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
publishand 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 changespublish. - (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;publishunchanged. - (3) Keep M2 as tagged.
- (1) List declared in
- 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:
publish(pay, cid, primary_field, secondary_field, article_type, parent_article, coauthors: address[]): the submitter declares 0 toMAX_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.publishcreates one co-author box per declared address (accepted = false,invited_at = ts), paid by the submitter, and setsinvited_countto the number declared, which never changes.Publishedcarries the list. Nobody can be added later:invite_coauthorandCoauthorInvitedno longer exist.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 setsaccepted,accepted_atand incrementsauthor_count;claimed_totalstays 0.- Allocation (the owner’s rule of D-054, “equal split, remainder to the submitter”):
N = 1 + invited_countis fixed for ever. Every accepted co-author is entitled tovote_total / N(floor); the submitter to the ceiling ofvote_total / N.claimable = entitled - claimed_total; the rest of section 10.2 is unchanged. - 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 mostN - 1weight units that wait for the next votes. - 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.
- 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.
- The article box returns to 192 bytes (
share_total,split_carry,submitter_extraare removed) andvote_articleis the M1 method again. Publish deposit = 212 200 + 29 300 per declared co-author (microAlgo).CoauthorAccepted.claimed_totalis 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
m2stays as history and the result is taggedm2.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.
publishtouches 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 manyextendcalls as the box references require:ceil(boxes / 8) - 1; a publication with few co-authors needs none. Everyextendcall 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:Publishedwith 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:
extendis 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
Publishedlog 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 = 25is 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;
extendcalls = ceil(boxes / 8) - 1. A publication without theextendcalls 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 --waitreturns a few seconds before algod answers, and a suite started right after it was skipped as a whole once; thealgorandfixture 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_countis 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,.tsimports), so there is no build step;tsc --noEmittype-checks,node --testruns the tests, prettier formats. The REST API is served withnode:httpand 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 bysrc/events.ts, a port of the Python oracletests/ref/events.pyfrom the same ARC-56eventslist (selector = sha512/256 of the signature, ARC-4 tuple payload, return log151f7c75skipped), proven equal to the oracle on exported vectors (test/fixtures/events.json). The decoded arguments are stored by name inpayload_jsonwith addresses as text, byte arrays as hex and integers as numbers (all fit in 53 bits excepttsand totals, which are stored as strings when aboveNumber.MAX_SAFE_INTEGER; in practice never). (amended 2026-09-30: the subscriber’s ownarc28Eventsdecoding 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 achain_eventsrow through(tx_id, log_index)columns where the spec asks for it (versions, votes, flags, disputes, mentions, notifications).rebuilddrops the projection tables and re-applieschain_eventsfrom 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_countand the sum ofReputationClaimed.consumed(D-072); rankingscore = vote_total / (age_days + 2) ** 0.8computed 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 carriesnetworkandappId(section 15). - Content (section 14.3): a
contentrow per distinct article CID; aContentSourcefetches 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_statusisvalid,mismatch,malformed,unavailableoroversized;authors[]beyond the first only produces a notice. When the manifest has no gateway, content staysunavailableand 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(fromPublished.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]ofindex.mdwith the on-chain author.spec/fixtures/sample-article/index.mdcarried a 90-character placeholder (84A+Y5HFKQ) that is not a valid Algorand address; the zero address is 58 characters (52A+Y5HFKQ). The same string appeared in a deadorbranch oftests/localnet/test_20_publish.py. Nothing on-chain depends on the front-matter, so the contract tagsm1/m2.1are unaffected, but the fixture CID changes with its bytes. - Decision:
authors[0]of the fixture is now the zero address;expected-cid.jsonwas regenerated twice withspec/fixtures/compute-cid.mjs(identical runs):bafybeidzsrohfscy7xlt4dh53j3kmlxunjl3r2354pvtfn6pysnjpmduna, bytes0170122079945c72c858fdd73e0cfdda76a62ef46a57b8eb7de3eb32b7cfc49a97b07468, unixfs size 2 020. References updated:tests/ref/test_ref.py(CID-08),spec/publishing-guide.md, the dead branch intest_20_publish.py. The indexer re-hash reproduces the new CID (test/content.test.ts), and the fixture validates asvalidagainst 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
bafybeihtc64snftpqcog5wnwxc3hq5t6jivumtlpvorcsxnemkyxyzpuvyis 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
GETovernode:http, JSON, CORS*(the web app of M4 runs on another origin); every response, including errors, carriesnetworkandappId.GET /articlesdefaults tosort=score,limit=20(max 100) and excludesretractedunlessstatus=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=andstatus=accept the number or the name. Each article carrieswarnings[](disputed,retracted,content:<status>when the content is notvalid) and the content summary (title, abstract, keywords) joined by CID.GET /articles/:idreturns authors (submitter first, then co-authors by address) withentitledandclaimable(D-072 arithmetic,nulluntil 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/:addressderives publications withclaimableandclaim_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/notificationslists newest first with anunreadcount; marking as read needs an authenticated writer and is left to M4 (a signed request or a local-only UI state).GET /fieldscounts non-retracted articles per field.GET /statusreports genesis, watermark, schema version and row counts. - Content: a directory whose canonical re-hash does not reproduce the CID is recorded as
unavailablewith a notice (the bytes obtained are not the content the chain names);oversizedis decided before re-hashing (20 MiB, §14.1);malformed=index.mdmissing or without readable front-matter;mismatch= field, secondary field, type, parent orauthors[0]differ from the chain. Records are re-checked everyARIADNE_CONTENT_RECHECK_SECONDS(default 6 h) and keep the last title/abstract while unavailable. - Runtime:
node:sqlitebinds 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.tscaught up from round 0 and compared every Article and CoAuthor box with the projections, then rebuilt fromchain_eventswith 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 (ingestin three polls of 500 rounds,rebuildof 912 events in 137 ms,contentwith a local directory source,status); the fixture CID published by the suite validates asmismatchthere because the suite’s author is a LocalNet account while the fixture’sauthors[0]is the zero address, which is exactly the check of §14.3. Offline: 41 tests (npm test),tsc --noEmitand 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 generategovernance 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 bynpm 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/123is MainNet and/testnet/a/123is TestNet; bare/redirects client-side to the network remembered inlocalStorage, else to the first enabled network.devOnlyentries (localnet) are served only whenNODE_ENV !== 'production'orARIADNE_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 fromspec/networks.jsonand 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/statusof the indexer, which already refused to start on a mismatch, and through algodversionsin 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.pyandtests/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 readsarticle_seq_nextat 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 withcoverAppCallInnerTransactionFeesand amaxFeeequal to that preview, so the wallet never signs more than what was shown. Every transaction carries a distinct noteariadne/<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.mjsand the indexer (src/lib/unixfs.ts, tested againstexpected-cid.json), shows the CID, writes a CAR and hands it to aPinProvider; the provider’s CID must equal the local one or the flow stops (§14.4). Providers:DevPinProviderposts the CAR to the app’s own/api/dev/pin, which unpacks it intoARIADNE_CONTENT_DIR/<cid>/(the same directory the indexer validates from and the dev gateway serves), only fordevOnlynetworks;PinataProvideris added in the TestNet step (D-079) with the JWT kept inlocalStoragebehind 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, requiresindex.md, validates the front-matter against the form and the size againstMAX_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
devOnlynetworks withipfsGatewayempty, the app servesARIADNE_CONTENT_DIRat/api/dev/content/<cid>[/path](plain files, and?format=carfor 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 themath-inline/math-displayclasses) before rehype-katex; relative URLs are rewritten to the gateway,javascript:anddata: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 buildclean (routes/,/[network],/[network]/a/[id],/[network]/publish,/[network]/u/[address],/[network]/inbox,/[network]/fields, the two dev API routes and the proxy),tsc --noEmitand 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 throughsrc/lib/ariadne.tswith generated Ed25519 signers: publish with 7 declared co-authors (11 boxes, oneextend(), 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
transactionSigneris a new function on every render, souseChainkeeps it behind a stable wrapper (otherwise the chain connection flapped and the sign button was intermittently disabled);NetworkConfigBuilder.addNetworkrefuses the reserved idlocalnet(uselocalnet()); the shared transaction status of the author panel is reset when another panel opens; the generated client is compiled with// @ts-nocheckbecause it does not meetexactOptionalPropertyTypes(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) andalgoIndexer(an Algorand indexer URL used only for catch-up, AlgoNode for TestNet/MainNet). A database without a watermark starts atstartRound - 1. WithalgoIndexer, polls usecatchup-with-indexer(at most 100 000 indexer rounds per poll), otherwisesync-oldeston algod (AlgoNode’s algod is archival). - Content source order:
ARIADNE_CONTENT_DIR(dev) >ARIADNE_KUBO_API(Kubo RPCdag/export, the CAR is block-verified and re-hashed exactly like a gateway CAR) > the manifest’sipfsGateway(trustless CAR). WithARIADNE_KUBO_APIthe indexer pins through Kubo RPCpin/addevery article CID it validated asvalidand every review and comment CID (raw blocks, self-verifying); pin state lives in a new base tablepins(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, multipartfile,network=public,car=true,name) with the author’s own Pinata JWT and comparesdata.cidwith 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 inlocalStorageof 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 (
fetchGatewayin the manifest, defaulthttps://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(laterindexer-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.andipfs.). Images are pinned by digest: node 26.5.1-bookworm-slimsha256:9e6f9357d371591e32ab6f2d8a26d63bdd0d17c29eee3f4f3e7e454d9634bf73, ipfs/kubo v0.43.1sha256:b293923d66e490e70ced64df42ea7a6cf7eac2740e3fb29101df18070fa7be48, caddy 2.11.2-alpinesha256:834468128c7696cec0ceea6172f7d692daf645ae51983ca76e39da54a97c570d. The repository checkout is the source of the manifest (mounted read-only), so enabling a network or filling an app id isgit 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 signis 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.pyfrom 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 gitignoredprojects/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 acceptsGOVERNANCE_PQ_SEED_FILEon TestNet, seeds the fields, and recordsstartRoundandalgoIndexer. 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, deployer5MHQYUCI2SQW4THJSOTFKWHFZCAVPU6TWOTRUHMEH6LFCDAIXZRRLFYL7A. Needed: 0.8974 ALGO and 0.3805 ALGO. A dry run ofalgokit project deploy testnetread.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.ymlwithlocal-stack.env(LocalNet on the host,*.localhostnames on port 8443, Caddy’s internal CA). Both images build from the repository (the web image prunes dev dependencies andnext startruns from it); Kubo appliesdeploy/kubo/init.sh(Gateway.NoFetch = true, RPC on the container network only). projects/web/test/stack/stack.test.ts(run withARIADNE_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’sstartRound) ingests it, fetches the directory through Kubo RPC, validates it asvalidand pins it; the gateway servesindex.mdand 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_EMAILmakes Caddy refuse the configuration, so the variable is now mandatory in the compose file; Node’slookupoption is called with{ all: true }(test helper); the indexer logged every idle poll and every API request: it now logs polls with calls or catch-up, and only 5xx responses (Caddy keeps the access log). - Pins added while wiring wallets after D-079: @txnlab/use-wallet-kmd 5.0.1, -mnemonic 5.0.1, -pera 5.0.1, -defly 5.0.1, -lute 5.0.0; server-only 0.0.1. The root
.gitignore(Python template) ignoreslib/:projects/web/.gitignorere-includessrc/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.pyprepares 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 afterARIADNE_CONTENT_TIMEOUT_SECONDS(30, passed to Kubo astimeout=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.envchange 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_EMAILremoved from the compose file and the Caddyfile). IPFS_STORAGE_MAX15GB for a 40 GB disk;COMPOSE_PROFILESstays empty until the TestNet app exists (an indexer without an app id refuses to start), thentestnet.- 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 testnetcreated app 772965337 (application addressOPQ75K3R6KYS4GFICQXKJASZFVCMBY2CD5V6JFFQGAP5WMQTJRUQ4VB2KE, creation round 67842232, transaction2JRKOVA6ZBNDRFUP26KTOHGIGC7A3UHVBZUOCEHJL6NPB6YO235A), funded it with exactly 100 000 microAlgo and seeded the 42 fields in six groups signed by the Falcon-1024 governanceZAL425O4GBTJWID26DSTEWFU5JZGUJIJQ52GO6WVLMZEVXZTNQN63JBGJIon the public TestNet. The manifest’s testnet entry carriesappId,genesisHash,governance,startRound,indexerApihttps://api.SERVER-IP.sslip.io/testnet,ipfsGatewayhttps://ipfs.SERVER-IP.sslip.ioandenabled: 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/.envholds 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/statusanswersnetwork: testnetand the app id; the web serves/testnetwith the TestNet banner, the network selector offers only TestNet,/testnet/fieldslists 42 registered fields, the console is clean; from the PC,ARIADNE_TEST_NETWORK=testnetruns 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 insrc/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 fieldfile= the CAR built in the browser,Authorization: Bearer <token>; the root CID in the NDJSON answer (Root.Cid./) must equal the local CID andPinErrorMsgmust 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 withAccess-Control-Allow-Origin: *and allows theAuthorizationheader (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: articlein 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 fromspec/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 (caAlgocontains it) are shown. Each network names its NFD API inspec/networks.json(nfdApi: MainNethttps://api.nf.domains, TestNethttps://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>.algoredirects 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.algoanywhere 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@ADDRESSinto the text. Unresolvable names are reported before signing. Comment and review bodies render@ADDRESSas the person (name or short address, linked) and@name.algoas 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-26algorand://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, keyariadne.theme) and is applied before the first paint by abeforeInteractivescript; 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
D-100 Domain: ariadne.press
Section titled “D-100 Domain: ariadne.press”- 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) andhttps://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; withariadne.algo(NFD, for donations, D-098) the names match. - Transition: the former app name
SERVER-IP.sslip.ioredirects 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.presshas no DNS record; with one, it is added toLEGACY_APP_HOSTand 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.tswhere 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.pressnow has its DNS record and redirects toariadne.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 behindariadne.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.jsoncarries 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-sentencenote(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>withContent-Security-Policy: default-src 'none'; sandboxandnosniff, so the reader’s browser never contacts a third party. The NFD reverse lookup now usesview=thumbnailto 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
followmethod emitting an event, no box) are inROADMAP.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)
D-105 USDC donations
Section titled “D-105 USDC donations”- Date: 2026-10-01 · Status: accepted (owner opted the donation account in to USDC)
- Decision:
spec/donations.jsonlists 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=31566704for 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)andfollow(target: address, on: bool)only emitIdentityDeclared,DoiDeclaredandFollowed(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=falseunfollows. - 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_fundingmissed 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-publictoken when credentials are set, anonymous otherwise) and caches the result inorcid_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_identifiersIsIdenticalTo the article page and the version’s CID, communityariadne-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 isverifiedwhen 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 asdeclared; a DataCite test DOI (10.5072) outside the sandbox isinvalid. 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
D-109 Follows on chain (replaces D-104)
Section titled “D-109 Follows on chain (replaces D-104)”- Date: 2026-10-01 · Status: accepted (owner decision)
- Decision:
follow(target, on)events; the indexer buildsfollows(active, since) and serves/users/:a/following,/followersand/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
772965337stays 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
followednotification 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 /healthanswers 503 when no ingestion poll has succeeded forARIADNE_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 psshowsunhealthy). 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.mdwith 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
titleto 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_identityanddeclare_doivalues 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 asbyte[]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-shapedinvalidwithout 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
D-118 Status page (owner request)
Section titled “D-118 Status page (owner request)”- 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.webmanifestfromapp/manifest.ts: name, standalone window, start URL/, which opens the network last used; icons 192, 512 and a maskable 512 generated from the mark byscripts/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/apiand 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
D-120 Search (owner request)
Section titled “D-120 Search (owner request)”- 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.algoin 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_articlesandsearch_posts, kept current by theserveloop (an article is reindexed when anything shown about it changes) and rebuilt byreindex;contentgainsbody_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 inpost_textsonly when it hashes to its CID. Routes/search/articles,/search/posts,/search/people,/search/suggest. The search is a cached view likecontent(§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 undervmwith 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 aListNoterow of chain_events (rebuild replays them in chain order) and projects them tolistsandlist_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.tsasks 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) inorcid_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 inorcid_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-matterreferences; every version is read again once after the migration, schema v8); the indexer derivescitations(citing, cited, how) from it and the chain in theserveloop, 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/:idand/users/:addresscarry 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/:idcarriescitations.thread,/users/:addresscarriesariadne_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 tablecoauthor_checks), recomputed when its inputs change, and each result names its rule version (SEAL_RULE1), 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 fromspec/ARIADNE_SPEC.md,DECISIONS.mdandspec/taxonomy.jsonat every build (the specification’s inlineEDIT D-nnncomments 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 byprojects/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)