feat(logs): logs données restructurés — identité en préfixe, trace résolution/barrière, inspection outbox

Chemin données bas-niveau du polyfill rendu lisible pour diagnostiquer en session live :

- Format identité-en-premier : `[<identity>][polyfill] OP shortNuri (label)` ;
  console.error épars (store-registry, inbox) unifiés au même préfixe.
- Trace (derrière le flag debug) : issue de la barrière ensureRepoOpen
  (synced|timed-out + durée) et résultat sémantique de chaque étage de résolution
  (resolvePointer/canonicalDoc/resolveAccount/resolveShimDoc/readScopeIndex).
- outbox-log.ts (nouveau) : inspection read-only de l'outbox hors-ligne au
  démarrage de session ; console.warn si non vide (anomalie, toujours visible),
  sous flag si vide. Le comptage seul est fiable (payloads BARE opaques côté JS).

Pas de changement fonctionnel. bun test 126 pass ; tsc 0 erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
Sylvain Duchesne
2026-07-14 10:37:46 +02:00
parent 3547967d37
commit 70de7afa3c
7 changed files with 207 additions and 33 deletions
+30 -9
View File
@@ -63,6 +63,7 @@ import { docCreate, sparqlUpdate, sparqlQuery } from "./docs";
import { getStoreRegistryDeps } from "./polyfill";
import { ensureRepoOpen } from "./open-repo";
import { escapeLiteral, escapeIri, assertNuri } from "./sparql";
import { accessLogPrefix, logStage, shortNuri } from "./access-log";
import type { Nuri, Scope } from "./types";
// --- sharedWalletShim model ----------------------------------------------
@@ -248,11 +249,20 @@ function bindingValue(row: Record<string, { value: string }>, key: string): stri
*/
function canonicalDoc(rows: Array<Record<string, { value: string }>>, key: string): Nuri {
let chosen = "";
const distinct = new Set<string>();
for (const row of rows) {
const v = bindingValue(row, key);
if (!v) continue;
distinct.add(v);
if (chosen === "" || v < chosen) chosen = v;
}
// Stage trace: which doc got picked, and out of how many DISTINCT candidate
// values — >1 flags residual fork residue (see the module doc above) even
// when resolution still converges correctly on the canonical (smallest) one.
logStage(
"canonicalDoc(" + key + ") → " + (chosen ? shortNuri(chosen) : "none") +
" (" + distinct.size + (distinct.size === 1 ? " candidate)" : " candidates)"),
);
return chosen;
}
@@ -327,15 +337,19 @@ async function resolvePointer(): Promise<Nuri> {
try {
const result = await sparqlQuery(s.sessionId, query, undefined, root, "resolvePointer");
const doc = canonicalDoc(readBindings(result), "shimDoc");
if (doc) return doc;
if (doc) {
logStage("resolvePointer → 1 target: " + shortNuri(doc));
return doc;
}
} catch (error) {
console.error("[storeRegistry] resolvePointer read failed:", error);
console.error(accessLogPrefix() + " resolvePointer failed:", error);
}
if (i < attempts - 1) {
await sleep(step);
step = Math.min(step * 2, maxStepMs);
}
}
logStage("resolvePointer → 0 targets");
return "";
}
@@ -355,7 +369,7 @@ async function writePointer(doc: Nuri): Promise<void> {
try {
await sparqlUpdate(s.sessionId, update, root, "writePointer");
} catch (error) {
console.error("[storeRegistry] writePointer failed:", error);
console.error(accessLogPrefix() + " writePointer failed:", error);
}
}
@@ -394,6 +408,7 @@ async function resolveShimDoc(): Promise<Nuri> {
// so a cold 0 on the doc-shim is authoritative (genuinely absent), not sync-lag.
await ensureRepoOpen(existing);
shimDocNuri = existing;
logStage("resolveShimDoc → " + shortNuri(existing));
return existing;
}
@@ -403,6 +418,7 @@ async function resolveShimDoc(): Promise<Nuri> {
await writePointer(doc);
await ensureRepoOpen(doc);
shimDocNuri = doc;
logStage("resolveShimDoc → " + shortNuri(doc));
return doc;
})();
@@ -456,7 +472,7 @@ export async function loadShim(): Promise<Map<string, AccountRecord>> {
accountCache.set(key, record);
}
} catch (error) {
console.error("[storeRegistry] loadShim failed:", error);
console.error(accessLogPrefix() + " loadShim failed:", error);
}
cache = map;
return map;
@@ -502,16 +518,20 @@ export async function resolveAccount(id: string): Promise<AccountRecord | null>
try {
const result = await sparqlQuery(s.sessionId, query, undefined, doc, "resolveAccount");
const rows = readBindings(result);
if (rows.length === 0) return null;
if (rows.length === 0) {
logStage("resolveAccount(" + key + ") → null");
return null;
}
// DETERMINISTIC: a corrupted shim may return SEVERAL bindings for this one
// account subject (duplicate scope-doc values from past forks). Pick the
// canonical (lexicographically-smallest) doc per scope so writer and reader
// always resolve the SAME docPublic (robustness against PAST fork residue).
const record = recordFromRows(rows, id);
accountCache.set(key, record);
logStage("resolveAccount(" + key + ") → 1 record");
return record;
} catch (error) {
console.error("[storeRegistry] resolveAccount failed:", error);
console.error(accessLogPrefix() + " resolveAccount failed:", error);
return null;
}
}
@@ -542,7 +562,7 @@ async function writeRecord(doc: Nuri, record: AccountRecord): Promise<void> {
try {
await sparqlUpdate(s.sessionId, update, doc, "writeRecord");
} catch (error) {
console.error("[storeRegistry] writeRecord persist failed:", error);
console.error(accessLogPrefix() + " writeRecord persist failed:", error);
}
}
@@ -733,7 +753,7 @@ export async function createEntityDoc(id: string, scope: Scope): Promise<Nuri> {
"createEntityDoc",
);
} catch (error) {
console.error("[storeRegistry] createEntityDoc index append failed:", error);
console.error(accessLogPrefix() + " createEntityDoc index append failed:", error);
}
return entityNuri;
}
@@ -765,8 +785,9 @@ async function readScopeIndex(indexDoc: Nuri): Promise<Nuri[]> {
if (v) out.push(v);
}
} catch (error) {
console.error("[storeRegistry] readScopeIndex read failed:", error);
console.error(accessLogPrefix() + " readScopeIndex failed:", error);
}
logStage("readScopeIndex(" + shortNuri(indexDoc) + ") → " + out.length + " entities");
return out;
}