diff --git a/scripts/build-graph.mjs b/scripts/build-graph.mjs index 8c2f693..97b9daf 100644 --- a/scripts/build-graph.mjs +++ b/scripts/build-graph.mjs @@ -239,20 +239,39 @@ export function parseGlossary(repoRoot) { const content = readFileSync(glossaryPath, "utf-8"); const terms = []; let currentCategory = ""; + const lines = content.split("\n"); + const termPattern = + /^\*\*(?:\[([^\]]+)\]\(([^)]+)\)|([^*]+))\*\*:\s*(.*)/; - for (const line of content.split("\n")) { + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; const catMatch = line.match(/^### (.+)/); if (catMatch) { currentCategory = catMatch[1].trim(); continue; } - const termMatch = line.match( - /^\*\*(?:\[([^\]]+)\]\(([^)]+)\)|([^*]+))\*\*:\s*(.*)/, - ); + const termMatch = line.match(termPattern); if (termMatch) { + let definition = termMatch[4].trim(); + + // GLOSSARY.md occasionally puts a term's definition on the next line. + // Only consume the immediate continuation line, and never swallow the + // next category or glossary term if the definition is actually missing. + if (!definition) { + const continuation = lines[i + 1]?.trim(); + if ( + continuation && + !continuation.startsWith("### ") && + !termPattern.test(continuation) + ) { + definition = continuation; + i += 1; + } + } + const term = { term: (termMatch[1] ?? termMatch[3]).trim(), - definition: termMatch[4].trim(), + definition, category: currentCategory, }; if (termMatch[2]) term.href = termMatch[2].trim(); diff --git a/src/data/glossary.json b/src/data/glossary.json index b1e2dd9..444b525 100644 --- a/src/data/glossary.json +++ b/src/data/glossary.json @@ -56,7 +56,7 @@ }, { "term": "Data Availability Layer (DA Layer)", - "definition": "", + "definition": "A dedicated network or service that publishes and stores the data required for DA (for example, rollup or application data), separate from the main execution chain.", "category": "Blockchain Architecture" }, { diff --git a/src/lib/render.ts b/src/lib/render.ts index 1d9f99f..c6b10de 100644 --- a/src/lib/render.ts +++ b/src/lib/render.ts @@ -92,8 +92,8 @@ function fileToSlug(filename: string, prefix: string): string { interface ResolvedLink { route: string; // e.g. '/patterns/foo' - exists: boolean; // true if the resolved node ID is in the current graph - nodeId: string; // graph node id, used to look up the real title + isResolved: boolean; // true when the renderer can identify the route shape + nodeId: string; // graph node id when available, used to look up the real title } /** @@ -116,6 +116,20 @@ function resolveMdHref(href: string): ResolvedLink | null { const filename = parts[parts.length - 1]; const dirName = parts.length > 1 ? parts[parts.length - 2] : null; + // RFP pages intentionally sit outside the graph. Bare sibling links inside + // content/rfps therefore have no directory component to disambiguate them + // from use-cases/domains/vendors, all of which use an empty filename prefix. + if (!dirName && /^rfp-[^/]+\.md$/i.test(filename)) { + const routeSlug = toContentSlug(filename.replace(/\.md$/, '')); + return { + route: `/rfps/${routeSlug}/${suffix}`, + // RFPs are not graph nodes, so this records successful route resolution + // rather than claiming filesystem-level existence. + isResolved: true, + nodeId: `rfp/${routeSlug}`, + }; + } + // Collect every structurally plausible candidate, then prefer one that // resolves to a real node. Multiple dirs can share prefix='' (use-cases, // domains, jurisdictions, vendors) so first-match-wins gives wrong answers @@ -133,12 +147,12 @@ function resolveMdHref(href: string): ResolvedLink | null { const routeSlug = toContentSlug(filename.replace(/\.md$/, '')); candidates.push({ route: `${cfg.route}/${routeSlug}/${suffix}`, - exists: nodeIds.has(id), + isResolved: nodeIds.has(id), nodeId: id, }); } if (candidates.length === 0) return null; - return candidates.find(c => c.exists) ?? candidates[0]; + return candidates.find(c => c.isResolved) ?? candidates[0]; } function isAbsoluteOrAnchor(href: string): boolean { @@ -211,11 +225,11 @@ marked.use({ // for the target's real title from graph.json. Authored labels like // `[Noir](../patterns/pattern-noir-private-contracts.md)` are left // untouched because they don't look like filenames. - if (resolved.exists && isFilenameLabel(rawLabel)) { + if (resolved.isResolved && isFilenameLabel(rawLabel)) { const t = nodeTitles.get(resolved.nodeId); if (t) innerHtml = escapeAttr(stripTypePrefix(t)); } - if (!resolved.exists && !warnedHrefs.has(href)) { + if (!resolved.isResolved && !warnedHrefs.has(href)) { warnedHrefs.add(href); console.warn(`[render] unresolved ethsystems/map link: ${href} → ${resolved.route} (node not in graph)`); } diff --git a/src/posts/2026-02-12-private-bonds-with-fhe.md b/src/posts/2026-02-12-private-bonds-with-fhe.md index d81afc7..58d405b 100644 --- a/src/posts/2026-02-12-private-bonds-with-fhe.md +++ b/src/posts/2026-02-12-private-bonds-with-fhe.md @@ -17,6 +17,7 @@ map_refs: - approach-private-bonds - pattern-privacy-l2s - pattern-co-snark + - pattern-shielding - pattern-private-shared-state-fhe - pattern-verifiable-dkg-threshold-decryption - private-bonds @@ -182,7 +183,7 @@ We've now built the same bond (whitelisted participants, private amounts, regula ## Trade-offs -[Custom UTXO](https://github.com/ethsystems/map/blob/master/patterns/pattern-zk-shielded-balances.md) offers the strongest privacy guarantees: even addresses are obscured via nullifiers, and users control their own keys. Railgun and similar systems prove the model works in production. But implementation complexity is significant. Our PoC required building notes, Merkle trees, and nullifier management from scratch. Nullifiers also accumulate forever, creating storage concerns at scale (mitigations like [epoch-based pruning](https://eprint.iacr.org/2025/2031) are being researched but not yet deployed). +[Custom UTXO](https://github.com/ethsystems/map/blob/master/patterns/pattern-shielding.md) offers the strongest privacy guarantees: even addresses are obscured via nullifiers, and users control their own keys. Railgun and similar systems prove the model works in production. But implementation complexity is significant. Our PoC required building notes, Merkle trees, and nullifier management from scratch. Nullifiers also accumulate forever, creating storage concerns at scale (mitigations like [epoch-based pruning](https://eprint.iacr.org/2025/2031) are being researched but not yet deployed). [Privacy L2s](https://github.com/ethsystems/map/blob/master/patterns/pattern-privacy-l2s.md) like Aztec handle the hard parts for you: notes, proofs, encryption. Our contract was just 200 lines. Private composability is native, meaning your bonds could interact with private lending or swaps on the same L2. The catch: neither Aztec nor Miden are live yet (both scheduled for launch later in 2026), so we can't measure real costs. And the learning curve exists: Noir is not Solidity. diff --git a/tests/build-graph.test.mjs b/tests/build-graph.test.mjs index cd4393d..6805370 100644 --- a/tests/build-graph.test.mjs +++ b/tests/build-graph.test.mjs @@ -233,6 +233,93 @@ describe('classifyEdge', () => { // --------------------------------------------------------------------------- describe('parseGlossary', () => { + it('reads a definition from the immediate continuation line', () => { + const repoRoot = mkdtempSync(join(tmpdir(), 'ethsystems-glossary-')); + try { + writeFileSync( + join(repoRoot, 'GLOSSARY.md'), + `### Blockchain Architecture + +**Data Availability Layer (DA Layer)**: +A dedicated network or service that publishes and stores the data required for DA. + +**Sequencer**: Orders L2 transactions. +`, + ); + + const glossary = parseGlossary(repoRoot); + + expect(glossary).toHaveLength(2); + expect(glossary[0]).toEqual({ + term: 'Data Availability Layer (DA Layer)', + definition: + 'A dedicated network or service that publishes and stores the data required for DA.', + category: 'Blockchain Architecture', + }); + expect(glossary[1]).toEqual({ + term: 'Sequencer', + definition: 'Orders L2 transactions.', + category: 'Blockchain Architecture', + }); + } finally { + rmSync(repoRoot, { recursive: true, force: true }); + } + }); + + it('does not consume the next term when a definition is missing', () => { + const repoRoot = mkdtempSync(join(tmpdir(), 'ethsystems-glossary-')); + try { + writeFileSync( + join(repoRoot, 'GLOSSARY.md'), + `### Blockchain Architecture + +**Missing Definition**: +**Sequencer**: Orders L2 transactions. +`, + ); + + const glossary = parseGlossary(repoRoot); + + expect(glossary).toHaveLength(2); + expect(glossary[0].definition).toBe(''); + expect(glossary[1].definition).toBe('Orders L2 transactions.'); + } finally { + rmSync(repoRoot, { recursive: true, force: true }); + } + }); + + it('does not consume the next category when a definition is missing', () => { + const repoRoot = mkdtempSync(join(tmpdir(), 'ethsystems-glossary-')); + try { + writeFileSync( + join(repoRoot, 'GLOSSARY.md'), + `### Blockchain Architecture + +**Missing Definition**: +### Next Category + +**Sequencer**: Orders L2 transactions. +`, + ); + + const glossary = parseGlossary(repoRoot); + + expect(glossary).toHaveLength(2); + expect(glossary[0]).toEqual({ + term: 'Missing Definition', + definition: '', + category: 'Blockchain Architecture', + }); + expect(glossary[1]).toEqual({ + term: 'Sequencer', + definition: 'Orders L2 transactions.', + category: 'Next Category', + }); + } finally { + rmSync(repoRoot, { recursive: true, force: true }); + } + }); + it('preserves linked term hrefs and definition markdown', () => { const repoRoot = mkdtempSync(join(tmpdir(), 'ethsystems-glossary-')); try { diff --git a/tests/render-links.test.ts b/tests/render-links.test.ts index 4bdde2c..9a8802d 100644 --- a/tests/render-links.test.ts +++ b/tests/render-links.test.ts @@ -26,6 +26,27 @@ describe('renderMarkdown link rewriting', () => { expect(href('[MiCA](../jurisdictions/eu-MiCA.md)')).toBe('/jurisdictions/eu-mica/'); }); + it('rewrites bare sibling RFP links to RFP routes', () => { + expect(href('[Benchmark Dashboard](rfp-benchmark-dashboard.md)')).toBe( + '/rfps/rfp-benchmark-dashboard/', + ); + expect(href('[Trust Assurance](rfp-trust-assurance.md)')).toBe( + '/rfps/rfp-trust-assurance/', + ); + expect(href('[Privacy Pools](rfp-privacy-pools.md)')).toBe( + '/rfps/rfp-privacy-pools/', + ); + expect(href('[Compliance Primitives](rfp-compliance-primitives.md)')).toBe( + '/rfps/rfp-compliance-primitives/', + ); + expect(href('[Trust scope](rfp-trust-assurance.md#scope)')).toBe( + '/rfps/rfp-trust-assurance/#scope', + ); + expect(href('[Trust view](rfp-trust-assurance.md?view=compact#scope)')).toBe( + '/rfps/rfp-trust-assurance/?view=compact#scope', + ); + }); + it('leaves absolute and external links untouched', () => { expect(href('[EIP](https://eips.ethereum.org/EIPS/eip-5564)')).toBe( 'https://eips.ethereum.org/EIPS/eip-5564',