Files
ng-eventually/docs/source-layout-by-fate.md
T
Sylvain Duchesne 2726f4a26f docs: nommer par la fonction, et n'annoncer qu'un point d'entrée
Deux corrections indépendantes dans la doc vivante, les briefs et décisions
datés restant tels qu'écrits.

P1a et P1b ne disaient rien à personne. Six mois plus tard il aurait fallu lire
le code pour savoir de quoi on parle, et le coût de la recherche se repaie à
chaque lecture. Ils deviennent cap-surface — la forme des capacités, livrée le
2026-07-28 — et cap-enforcement — ce qui reste : le chiffrement par document et
les gardes d'écriture aujourd'hui décoratives. 28 occurrences.

Et api-contract.md se contredisait à quatre lignes d'intervalle : il annonçait
deux points d'entrée en tête, et en bas qu'il n'y en a qu'un depuis la fusion du
2026-08-07. Vérifié dans package.json avant d'écrire — exports mappe exactement
{".": "./src/index.ts"} et src/polyfill.ts n'existe pas.

Ce qui identifie un symbole polyfill-era ne change pas : le bloc marqué dans
src/index.ts et le test de vocabulaire, plus aucun chemin d'import.
2026-08-11 19:10:34 +02:00

32 KiB
Raw Permalink Blame History

Source layout by migration fate — analysis and recommendation

Status: ADOPTED. The layout this document recommends is the one in the tree — model/, surface/, emulated-verifier/, shared-wallet/. Read the rest as the reasoning that produced it, not as a proposal. (Two things have moved under it since: the package became packages/polyfill / @ng-eventually/polyfill on 2026-08-10 — it was packages/client / @ng-eventually/client, kept here as a search keyword — and emulated-verifier/public-store.ts was added on 2026-08-06. Paths below point at where the files are now.)

Written 2026-08-04, when nothing had yet been moved, from the source of src/ (25 modules), the two contracts (docs/api-contract.md, docs/internal-contract.md) used as the export-level inventory, and the read-only nextgraph-rs clone (HEAD 213338f6); the upstream facts this analysis leans on were re-verified at the source and are cited with layer numbers per README.md § The three references (1 = engine, 2 = wasm binding, 3 = JS ORM).

The question. Today all 25 modules sit flat in src/, named mechanically. Three different fates coexist undistinguished: modules whose shape the consumer keeps (the surface the real SDK replaces), modules standing in for what the engine/verifier will do natively, and modules that exist only because the emulation runs on one shared wallet. The bet under evaluation: if the folder structure mirrors the target's own structure, divergence gets harder to commit and easier to spot.


1. Is layout-by-future-layer the right axis?

Yes on the axis, no on the literal reading — and with a bounded claim about what it buys.

1a. The bet as literally stated is a category error

"Mirror the target's own structure" cannot mean mirroring nextgraph-rs's tree (engine/, sdk/js/lib-wasm, sdk/js/orm). Every line this library ships lives in the polyfill; the three references are layers we align on, never places we write (README.md § The three references: "REFERENCES, not places we write code"). A folder named engine/ or verifier/ inside src/ would claim residency in a layer we only read — the exact conflation the README warns produces false certainty. The right axis is the target's stack as seen from the polyfill — what each piece of our code stands in for, and therefore what happens to it at migration — not the target's repo tree.

1b. The three fates are real, but they are not three piles of equal nature

The two that evaporate at migration differ in the only way that matters for divergence:

  • Stand-ins for native behaviour (cap possession and filing, the reach boundary, read filtering, inbox processing, branch registers, repo opening) have a model to diverge from — the engine's, level 1. This is where the dangerous failure lives: an emulation that drifts from the model teaches the consumer something to unlearn. Both incidents in README.md § Design principle happened here.
  • Shared-wallet compensation (the account directory, the physical user, the identity relay, the diagnostics) has nothing to align on — NO COUNTERPART at any layer. It cannot diverge from a model; its only law is invisibility from the surface. Its failure mode is leaking, not drifting.

And a fourth group the three-fate framing misses: the target's model vocabulary (the NURI grammar, the type guards, the branded types) — level-1-verified transcription that every layer consumes and that survives migration as knowledge rather than as code to delete.

1c. Alternatives, honestly

  • By feature (inbox/, caps/, read/…): optimises "find everything about X", which the api-contract's by-subject sections already do better — and it actively hides the fate axis: inbox.ts would stay one folder while its sender half is target-shaped surface and its reader half is emulation detail a consumer must not code against (docs/api-contract.md § 9). Rejected: it organises along the axis that is already served and flattens the one that is not.
  • By dependency direction (layered, low → high): the import graph refuses it. docs.ts (surface) calls getCaps().mint (emulation) because upstream doc_create itself commits AddRepo (level 1, engine/verifier/src/request_processor.rs:698, re-verified); emulated-verifier/connect.ts (verifier stand-in) calls resolveAccount (shim) because the emulation runs on the shim. These cross-fate imports are target-faithful, not accidents — a layout that forbids them would force artificial inversions or be violated on day one. Rejected: it encodes a property the semantics do not have.
  • Flat with naming conventions only (surface-docs.ts, shim-physical.ts…): carries the same information at the same churn — a rename churns every importer exactly as a move does — with weaker affordances: no per-folder contract note, no one-glance grouping, and no folder-granular entry rule to grep or lint. Prefixes also rot silently in a way a misplaced file in a four-folder tree does not. Rejected as strictly dominated: same cost, less structure.
  • Do nothing — rely on the contracts: the serious alternative. The contracts are finer-grained than any layout (per-claim epistemic labels, not per-module), and they were verified at the source. But they are read after the fact; the incident that motivates this analysis happened in a module whose own header states the right doctrine. A layout is confronted during the edit: a new module must be placed, and placing it forces the "which fate?" question at the moment the docs.ts-style mistake is made. Layout and contracts are complementary instruments — the layout is the cheap always-on prompt, the contracts remain the enforcement.

1d. What the layout actually buys — bounded claims

  1. The placement question fires at creation time. A new module cannot be added without answering "surface, native stand-in, shared-wallet, or model?" — the question whose non-asking is the root of the flat layout's failure.
  2. The entry rule becomes folder-granular and mechanical. Today index.ts's purity is maintained per-symbol (the hand-built surface/placement.ts slice). With folders the rule is "index.ts re-exports only from surface/ and model/" — one grep, lintable in CI, reviewable at a glance.
  3. Cross-fate imports become visible seams. import { … } from "../emulated-verifier/…" inside surface/docs.ts is a reviewable event in a diff; the same call inside a flat sibling import is invisible. The seam does not prevent the docs.ts incident class — it makes it show up in review.
  4. The contracts map onto the tree. docs/api-contract.mdsurface/ + model/; docs/internal-contract.mdemulated-verifier/ + shared-wallet/. Drift between doc and code becomes a folder-membership diff instead of an inventory audit.
  5. It generalises a pattern this repo already proved. surface/placement.ts (a hand-maintained surface slice), shared-wallet/physical.ts (privilege as separate functions, not exemption flags), and the 2026-08-03 entry-header fix are all the same idea implemented piecemeal at module granularity. The layout is the same discipline promoted to the tree.

Verdict on the bet: "easier to spot" — substantially yes (points 24). "Harder to commit" — only mildly: nothing in a folder tree stops a determined or oblivious edit, and the decisive question (is this behaviour the target's?) is answered by reading nextgraph-rs, not by any layout (§ 6). Worth doing, with the expectations of § 1d and the costs of § 5.


Both entry files stay at src/ root, so package.json's exports map is untouched and the consumer application sees no change. (The two doors were merged into one on 2026-08-07 — the exports map is now just ., and the polyfill-era symbols sit in a marked block of index.ts.)

Folder What the name asserts Alignment reference Fate at migration
src/ root (index.ts, polyfill.ts) The two published doors, nothing else. index.ts may re-export only from surface/ and model/; polyfill.ts may re-export by name from anywhere — it is the polyfill-era door and its imports are the list of what dies. (Merged 2026-08-07: one door, index.ts, with the polyfill-era symbols in a marked block. The rule survives as a block boundary instead of a file boundary.) The SDK-shaped half is replaced by the real SDK via the build alias; the polyfill-era block is deleted.
model/ The target's addressing model, transcribed: pure vocabulary (types, NURI grammar, guards). No I/O, no state, no minting. Importable by every layer. Level 1, verified (NuriV0, readcap_nuriengine/repo/src/types.rs:518-521) Survives as knowledge; the guards stay useful against the real SDK (which takes plain strings).
surface/ App-facing, and every symbol has a target counterpart — verified or a documented bet — in docs/api-contract.md. A consumer coding against this folder learns nothing to unlearn. Levels 3/2 where they answer, level-1 shape where they do not (per subject, in the contract) Deleted when the alias flips; the consumer's code is unchanged.
emulated-verifier/ Stand-ins for what the engine/verifier/broker do natively: possession, filing, boundary, non-delivery, inbox processing, branch registers, repo opening. Aligned on the level-1 model; each module names its native counterpart mechanism. This is the folder where divergence from the model is possible, and its main risk. Level 1 (the model is the specification) Deleted — the native side takes over.
shared-wallet/ Exists only because one wallet hosts every identity. NO COUNTERPART at any layer — nothing to align on; the only law is invisibility from surface/ and from the consumer. None (nothing upstream has an image of this) Evaporates entirely, replaced by nothing.

On the name emulated-verifier/: every module in it has its native counterpart running in the verifier (cap state and Store/User-branch replay, Verifier::load_repo_from_read_cap engine/verifier/src/verifier.rs:2237, level 1; non-delivery, resolve_target_for_sparqlRepoNotFound engine/verifier/src/request_processor.rs:264,269, level 1, re-verified; inbox processing, Verifier::inbox verifier.rs:1674-1690, level 1; session repos, self.repos). The emulated- prefix is load-bearing: it says stands in for, never is — the residency confusion § 1a rules out.


3. Module-by-module assignment

The 25 current modules, with the two splits' offspring shown where a module divides (§ 4 gives the criterion and the rulings). "Stays whole" means the file moves as-is.

Current module Destination What the placement asserts / notes
index.ts src/index.ts (unchanged path) The SDK-entry manifest. New rule made checkable: imports only from surface/ and model/.
polyfill.ts src/polyfill.ts (path unchanged) + new shared-wallet/bootstrap.ts Split: the entry keeps the re-exports; the config store (configure, getConfig, registryDeps, the current-user relay, the CapRegistry singleton wiring) moves to shared-wallet/bootstrap.ts — it is the injection machinery with NO COUNTERPART by design (docs/api-contract.md § 1). Side effect: removes the current entry↔internal import cycles (polyfill.ts:16connect.ts:37; polyfill.ts:228inbox.ts:32).
types.ts model/types.ts, minus NgLike / UseShapeLikeshared-wallet/bootstrap.ts Nuri/ReadCap/Scope are level-1-verified vocabulary; PrincipalId stays with a note (target: the wallet user; polyfill: a relayed id). NgLike/UseShapeLike describe the injection, not the target — they belong to the bootstrap. See § 5 for the published-type wrinkle this creates.
nuri.ts model/nuri.ts, minus mintCapemulated-verifier/ The guards and targetOf/parseNuri are the model transcription. mintCap is the emulation's minting point — upstream only the engine mints, at repo creation (level 1, BlockRef::readcap_nuri, engine/repo/src/types.rs:518-521) — and its presence in the model module contradicts the module's own header ("nothing on the surface turns a bare reference into a cap"). Cap-enforcement swaps its constant; migration deletes it.
sparql.ts surface/sparql.ts Published, generic injection-safety utilities with NO COUNTERPART and none expected (docs/api-contract.md § 11) — the one surface family that survives any migration unchanged. Placed with the surface because it is published and documented there; the folder note must carry this exception.
docs.ts surface/docs.ts — stays whole Level-2 passthroughs whose in-body cap filing and reach guard mirror the target's own composition (§ 4 ruling). The mint and the guard become named imports from emulated-verifier/ — the visible seam.
lifecycle.ts surface/lifecycle.ts Pure forwarding to the injected level-2/3 calls (docs/api-contract.md § 2).
surface/ng-proxy.ts surface/ng-proxy.ts — stays whole Builds the published ng; its two overrides compose emulated-verifier/ predicates (the write guard stands in for verify_perm inside Commit::verify, level 1, engine/repo/src/commit.rs:892-899, re-verified — noting verify has no runtime caller today, which says nothing about the target). The login arm is finding F1 of the internal contract: an unprovenanced fabricated member — its fix is deletion, not relocation.
surface/use-shape.ts surface/use-shape.ts Level-3 passthrough + the read-filter view imported from emulated-verifier/ — mixture-by-import, already in the right shape.
surface/watch-shape.ts surface/watch-shape.ts Surface composition over emulated-verifier/ and the placement calls; its "planned useShape upgrade" header claim remains an ASSUMPTION with no provenance (docs/api-contract.md § 5) — a layout cannot fix that (§ 6).
subscribe.ts surface/subscribe.ts, minus subscribePhysicalDocshared-wallet/physical.ts The guarded subscribeDoc/subscribeDocs and docChangeType are surface (level 2, doc_subscribe, sdk/js/lib-wasm/src/lib.rs:1908). The physical door moves to the machinery module (§ 4); the unguarded core is exported under its Unguarded name for that one importer.
surface/read-model.ts surface/read-model.ts — stays whole The anchored-read mechanics are level-1-verified and survive as composition (docs/api-contract.md § 6). Its possession gate and machinery filter mirror native behaviour (§ 4 ruling) and arrive via named emulated-verifier/ imports.
inbox.ts split: surface/inbox.ts (post, postToDocument, shareCap) + emulated-verifier/inbox-processing.ts (read/materialize/readSynced/processInbox/watch, assertOwnInbox, the deposit RDF vocabulary) § 4 ruling. surface/inbox.ts re-exports the processing functions with a header saying exactly what docs/api-contract.md § 9 says — that enumerating deposits is emulation detail — so the published inbox.* namespace is unchanged and the warning sits at the one place the two halves meet.
surface/placement.ts dissolved into surface/placement.ts The hand-built slice becomes a real module: the app-facing placement/addressing calls (createEntityDoc, listMyEntityDocs, resolveScopeGraph, resolveWriteGraph, userInbox, openDocumentInbox, documentInboxAddress), composed from the two lower layers. Its existence today is the strongest in-repo evidence for the whole reorganisation: it does by hand what the folder boundary does structurally.
shared-wallet/account-registry.ts split three ways: surface/placement.ts + emulated-verifier/branch-registers.ts + shared-wallet/account-registry.ts The sharpest case — 1377 lines spanning all three fates. branch-registers.ts takes the durable compartments: the Store-branch cap register (holdOwnCap/readStoreCaps — emulating AddRepo { read_cap }, level 1, engine/repo/src/types.rs:1890-1899), the User-branch Link register (addLink/readLinksAddLink { read_cap }, types.rs:1939-1948), the inbox-cap records (AddInboxCapV0, types.rs:1969-1981) and the Header-branch address records. account-registry.ts takes the pointer→doc-shim indirection, resolveAccount/ensureAccount, reservedAccount, the cache, VirtualUserRecord/RegistrySession — NO COUNTERPART, the shim proper. placement.ts (surface) keeps the app-facing calls listed above plus userStoreDoc/isOwnInbox/myInboxes staying internal on the register side per their contract entries.
emulated-verifier/caps.ts emulated-verifier/caps.ts — stays whole (+ receives mintCap) The in-memory record of what the connected holder holds — the verifier's cap state. The per-holder heldByHolder keying is its one shared-wallet dimension (one wallet, many holders); it stays, isolated behind the injected holder() and noted: at migration there is exactly one holder and the map collapses.
emulated-verifier/reach.ts emulated-verifier/reach.ts The emulated wallet boundary: stands in for "a repo you hold no cap for cannot even be addressed" (level 1, resolve_target_for_sparqlRepoNotFound, request_processor.rs:264,269, re-verified). The two-rules redundancy is a lib choice, documented.
emulated-verifier/read-filter.ts emulated-verifier/read-filter.ts Stands in for cryptographic non-delivery (same level-1 citations); deleted at migration with nothing to migrate to.
emulated-verifier/connect.ts emulated-verifier/connect.ts — stays whole The recipient-verifier moment (Verifier::inboxprocess_inbox, level 1, verifier.rs:1674-1690). Its import of resolveAccount (shared-wallet) is a deliberate cross-fate check — connecting must not provision — and stays visible at the import line.
emulated-verifier/open-repo.ts emulated-verifier/open-repo.ts, minus ensurePhysicalRepoOpenshared-wallet/physical.ts Stands in for the verifier bringing repos into self.repos (native at migration: open by cap at bootstrap). Flag kept from the internal contract: this module is a current-state heal, not a model emulation — its barrier ("TabInfo then first State") is empirical, pinned only by the e2e CONTRACT-3 probe, and its header's mechanism claim is finding F2 (contradicted at the source). The folder cannot fix either (§ 6).
emulated-verifier/machinery.ts emulated-verifier/machinery.ts The subject filter fabricates what is structurally impossible upstream: a content read cannot see Store/User/Header compartments because they are separate branches with no triples (level 1, BranchCrdt::None, engine/repo/src/types.rs:1420, re-verified). It sits beside branch-registers.ts, whose compartments it hides; note that MACHINERY_NS also covers the account-shim's vocabulary.
shared-wallet/physical.ts shared-wallet/physical.ts — grows into the complete privileged door The quintessential shared-wallet module. It absorbs subscribePhysicalDoc and ensurePhysicalRepoOpen, so one module is the machinery's entire unguarded API — completing its own doctrine ("separate functions, never exemptions", physical.ts:19-27) at the tree level. Cost: the two unguarded cores get exported (under Unguarded names) from their mechanism modules; acceptable because neither entry ever re-exports them.
shared-wallet/virtualUsers.ts shared-wallet/virtualUsers.ts Identity persistence for the shared wallet; NO COUNTERPART (docs/api-contract.md § 13); already correctly published via /polyfill only.
shared-wallet/access-log.ts shared-wallet/access-log.ts Diagnoses the shared-wallet isolation leak; the identity it prefixes is the relayed virtual id. Deleted at migration.
shared-wallet/outbox-log.ts shared-wallet/outbox-log.ts Polyfill-era trace probe over the injected SDK's private persistence (level-2 facts verified in the internal contract § 11). Deleted at migration.

Resulting tree: 2 entry files + model/ (2) + surface/ (10) + emulated-verifier/ (8) + shared-wallet/ (6).


4. The mixed modules — mechanical detection, one criterion, and rulings

Detection method

Fate labels exist per export in the two contracts; a module is mixed when its exports (or its internal effects) span fates. Three code signals find the internal effects mechanically, without trusting headers:

  • Signal A — wire call × emulation-state write: the module calls the injected ng (getConfig().ng) and mutates emulation state (getCaps().mint/learn/open, addLink, declareInfrastructure). Grep hits: docs.ts:73 (mint), inbox.ts:385 (learn), store-registry.ts:875,893-895,903,1169,1371 (learn/open), connect.ts:67 (learn).
  • Signal B — guarded/unguarded twin exports: the *Physical* / *Unguarded pairs. Hits: subscribe.ts:104/118, open-repo.ts:167/184.
  • Signal C — act vs stand-in-processing under one namespace: exports of the same module carrying different fate labels in the contracts. Hits: inbox.ts (§ 9: sender acts are target-shaped; deposit enumeration is emulation detail), shared-wallet/account-registry.ts (§ 12: labels range from level-2 VERIFIED to NO COUNTERPART), types.ts (model types vs injection types, § 1 vs § 10), nuri.ts (guards vs mintCap, internal contract § 2), polyfill.ts (entry vs config store).

Full mixed list: docs.ts, surface/read-model.ts, inbox.ts, subscribe.ts, emulated-verifier/open-repo.ts, shared-wallet/account-registry.ts, nuri.ts, types.ts, polyfill.ts, surface/ng-proxy.ts, emulated-verifier/caps.ts, emulated-verifier/connect.ts. (surface/use-shape.ts and surface/watch-shape.ts cross fates only through imports — already the desired end state.)

The criterion

Split when the halves have different fates AND different callers. Keep whole when the mixture reproduces a composition the target itself performs atomically — and then express the emulated half as a named import from the other folder, so the seam is on the import line. Corollary: an unguarded twin of a guarded operation always lives with the machinery that calls it, never beside its guarded sibling.

Rulings

  • docs.ts — KEEP WHOLE. The cap filing inside docCreate mirrors the target's own doc_create, which commits AddRepo to the Store branch and ldp:contains to the Main branch in the same native call (level 1, engine/verifier/src/request_processor.rs:697-710, re-verified). Splitting the mint into a separate caller-visible step would create a two-step creation surface the target does not have — the split itself would be the divergence. Same for the reach guard: the refusal is native (RepoNotFound). The fix is visibility, not surgery: both effects become named imports from emulated-verifier/.
  • surface/read-model.ts — KEEP WHOLE. Its possession gate mirrors native non-delivery, and its machinery-subject drop mirrors the structural invisibility of non-content branches (BranchCrdt::None, types.rs:1420, level 1). Both are the target's own composition of "read a document".
  • inbox.ts — SPLIT. Different fates (api-contract § 9: the acts are target-shaped level-1 inventions; the deposit-list surface "may never have this shape") and different callers (apps post/share/watch; emulated-verifier/connect.ts processes). The deposit RDF vocabulary — pure emulation transport (upstream a deposit is a sealed message, InboxMsgBody, engine/net/src/types.rs:4265, level 1, carrying no target document) — lives once, on the emulated side.
  • subscribe.ts / emulated-verifier/open-repo.ts — SPLIT the physical doors out (criterion's corollary): different caller (machinery only), different fate (the guarded/unguarded pair collapses to one call when the wallet is the boundary). They regroup in shared-wallet/physical.ts.
  • shared-wallet/account-registry.ts — SPLIT three ways (§ 3). It is the module the flat layout hides most: signal C fires on nearly every export group, and the repo already voted for the split by hand-building surface/placement.ts.
  • nuri.ts — SPLIT mintCap out. Different fate (model vocabulary survives; the minting point is deleted when the engine mints) and the module's own stated invariant argues for it.
  • types.ts — SPLIT the injection types out (small; see § 5 for the published-type consequence, which must be decided, not slipped).
  • polyfill.ts — SPLIT entry from config store. Different fates (a published door vs internal state) and it removes real import cycles.
  • surface/ng-proxy.ts — KEEP WHOLE. 59 lines; the overrides are "what the native side takes over" and already compose emulated-verifier/ predicates; the proxy artifact itself is the published surface. F1 (the fabricated login member) is fixed by deletion wherever the file lives.
  • emulated-verifier/caps.ts — KEEP WHOLE. The per-holder keying is shared-wallet-flavoured, but splitting holder-resolution from the possession model would fragment one coherent level-1 model for no boundary gain; the injected holder() already isolates the dimension that collapses at migration.
  • emulated-verifier/connect.ts — KEEP WHOLE. Its shim import is a deliberate cross-fate check (must-not-provision), which is exactly what the seam should show.

5. Cost and risk

What does not change: the published surface. Both entries keep their src/ paths (they were merged into one on 2026-08-07, and package.json's exports map now maps only .); the inbox.* and storeRegistry.* namespaces are re-assembled at the entry with identical contents. A consumer application importing the entry sees nothing — with one deliberate exception below.

Import churn — the inventory:

  • All 23 non-entry src/ modules import each other relatively; every moved file churns its importers' paths (mechanical, type-checked).
  • 19 of 19 unit-test files deep-import ../src/* — 66 static import lines (heaviest: store-registry ×16, polyfill ×14) plus 3 dynamic await import("../src/…") sites (test/isolation-active.test.ts:359, test/reach.test.ts:201-202) that a naive static-import codemod will miss and that fail only at runtime.
  • The e2e harness deep-imports twice (e2e/polyfill-entry.ts:41-42: ../src/store-registry, ../src/accounts); its package-name imports resolve through the exports map and are immune. e2e/tsconfig.json includes "." only — path-agnostic.
  • Decision to make alongside: whether test/ mirrors the new folders (keeps the module↔spec correspondence at more churn) or stays flat.

Silent-breakage candidates — the ones tests may not catch:

  1. Module-evaluation order. polyfill.ts is today both entry and config store and sits inside import cycles (polyfill ↔ connect, polyfill ↔ inbox) that work through ES-module hoisting; the CapRegistry singleton is constructed at module scope (polyfill.ts:97). Re-cutting the graph changes which module evaluates first; a cycle that works today can break — or, worse, change initialization order without breaking. Mitigation: extract shared-wallet/bootstrap.ts as its own first step with the full unit + e2e suite run before any other move (baseline discipline), since the e2e suite is what actually exercises load order against a real broker.
  2. export * from "./types". After the types split, NgLike/UseShapeLike silently vanish from the . entry's type surface — erased types, so nothing in this repo's runtime tests notices; only the consumer's typecheck would. This is a real (if arguably desirable) published-surface change and must be an explicit decision: either re-export them deliberately from /polyfill (where EventuallyConfig, which references them, already lives) with a documented deprecation on ., or accept the narrowing and record it in the api-contract.
  3. The contracts' citations — the largest single cost. docs/api-contract.md and docs/internal-contract.md (plus several briefs) carry hundreds of file:line references into src/; every moved or split module stales them wholesale. These two documents are the library's enforcement instrument — letting their citations rot would undercut the very discipline the reorganisation serves. A citation-refresh pass over both contracts is part of the change, not a follow-up.
  4. git blame archaeology. Moves (and especially the three-way store-registry split) break naive blame; --follow works per-file but split hunks lose lineage. One-time tax; worth staging the splits as move-then-edit commits so content moves stay detectable.

Staging that contains the risk: (1) extract shared-wallet/bootstrap.ts, full suite green; (2) pure moves into the four folders, no content edits, full suite green; (3) the splits (inbox, store-registry, nuri, types), one per commit, each behind its baseline; (4) the citation-refresh pass on both contracts. Each stage leaves the published surface byte-identical (stage-3 exception 2 above being the one flagged decision).


6. What the layout will NOT fix

Being specific, because overclaiming here would recreate the false-certainty problem the layout is meant to reduce:

  • It cannot decide whether a behaviour is the target's. The decisive act remains reading nextgraph-rs. Both README § Design principle incidents would have type-checked and folder-checked: "every document has a native inbox" was a belief error, and the owner-inbox pointer was a model error inside code that belongs exactly where it was. A correctly named folder holds wrong code without complaint.
  • It cannot fix wrong claims inside correctly placed modules. Finding F2 (emulated-verifier/open-repo.ts's header asserts a silent-0-rows mechanism the source contradicts — upstream errors RepoNotFound, request_processor.rs:264,269, level 1) and surface/watch-shape.ts's unprovenanced "planned upgrade" survive any tree untouched. Header claims are policed by source-verification passes, not placement.
  • It is coarser than the epistemic labels. PASSTHROUGH vs LEVEL-1 SHAPE vs ASSUMPTION vary per claim within one module (inbox.post's act is level-1-shaped, its transport is pure emulation, arbitrary payloads are an ASSUMPTION — all in one function's orbit). A folder carries one label; the contracts remain the finer instrument and the layout must not be read as replacing them.
  • It cannot stop in-module shape drift. Deposit growing a target-document field — the exact divergence class of the reverted owner-inbox episode, since upstream a message carries no document (InboxMsgBody, engine/net/src/types.rs:4265, level 1) — is one line in a correctly placed file.
  • It does not police the empirical bets. The sync barrier's push ordering and "a held subscription keeps the repo open" are pinned by the e2e CONTRACT-3 probe alone; no layout substitutes for that tripwire.
  • It does not remove dead or decorative surface (inbox.watch's ignored intervalMs, the decorative write caps) — inventory work, already tracked in the contracts.
  • A wrong placement is worse than no placement. Folders assert; a mis-filed module borrows the folder's authority (a shim-flavoured helper landing in surface/ would look migration-safe). The contracts' per-subject verification remains the check on the layout — never the reverse.

Cross-references: README.md § Design principle and § The three references (the doctrine this layout serializes into the tree); docs/api-contract.md (the would-be surface/+model/ inventory); docs/internal-contract.md (the would-be emulated-verifier/+shared-wallet/ inventory, findings F1F5).