This document provides project-specific instructions for working with the Alexandria codebase, based on existing Cursor rules and project conventions.
This document provides project-specific instructions for working with the
Alexandria codebase, based on existing Cursor rules and project conventions.
## Developer Context
## Developer Context
You are working with a senior developer who has 20 years of web development experience, 8 years with Svelte, and 4 years developing production Nostr applications. Assume high technical proficiency.
You are working with a senior developer who has 20 years of web development
experience, 8 years with Svelte, and 4 years developing production Nostr
applications. Assume high technical proficiency.
## Project Overview
## Project Overview
Alexandria is a Nostr-based web application for reading, commenting on, and publishing long-form content (books, blogs, etc.) stored on Nostr relays. Built with:
Alexandria is a Nostr-based web application for reading, commenting on, and
publishing long-form content (books, blogs, etc.) stored on Nostr relays. Built
with:
- **Svelte 5** and **SvelteKit 2** (latest versions)
- **Svelte 5** and **SvelteKit 2** (latest versions)
- **TypeScript** (exclusively, no plain JavaScript)
- **TypeScript** (exclusively, no plain JavaScript)
@ -22,19 +27,22 @@ The project follows a Model-View-Controller (MVC) pattern:
- **Model**: Nostr relays (via WebSocket APIs) and browser storage
- **Model**: Nostr relays (via WebSocket APIs) and browser storage
- **View**: Reactive UI with SvelteKit pages and Svelte components
- **View**: Reactive UI with SvelteKit pages and Svelte components
- **Controller**: TypeScript modules with utilities, services, and data preparation
- **Controller**: TypeScript modules with utilities, services, and data
preparation
## Critical Development Guidelines
## Critical Development Guidelines
### Prime Directive
### Prime Directive
**NEVER assume developer intent.** If unsure, ALWAYS ask for clarification before proceeding.
**NEVER assume developer intent.** If unsure, ALWAYS ask for clarification
before proceeding.
### AI Anchor Comments System
### AI Anchor Comments System
Before any work, search for `AI-` anchor comments in relevant directories:
Before any work, search for `AI-` anchor comments in relevant directories:
- `AI-NOTE:`, `AI-TODO:`, `AI-QUESTION:` - Context sharing between AI and developers
- `AI-NOTE:`, `AI-TODO:`, `AI-QUESTION:` - Context sharing between AI and
developers
- `AI-<MM/DD/YYYY>:` - Developer-recorded context (read but don't write)
- `AI-<MM/DD/YYYY>:` - Developer-recorded context (read but don't write)
- **Always update relevant anchor comments when modifying code**
- **Always update relevant anchor comments when modifying code**
- Add new anchors for complex, critical, or confusing code
- Add new anchors for complex, critical, or confusing code
@ -101,7 +109,8 @@ Before any work, search for `AI-` anchor comments in relevant directories:
### Core Classes to Use
### Core Classes to Use
- `WebSocketPool` (`src/lib/data_structures/websocket_pool.ts`) - For WebSocket management
- `WebSocketPool` (`src/lib/data_structures/websocket_pool.ts`) - For WebSocket
management
- `PublicationTree` - For hierarchical publication structure
- `PublicationTree` - For hierarchical publication structure
This technique allows you to create test highlight events (kind 9802) for testing the highlight rendering system in Alexandria. Highlights are text selections from publication sections that users want to mark as important or noteworthy, optionally with annotations.
This technique allows you to create test highlight events (kind 9802) for
testing the highlight rendering system in Alexandria. Highlights are text
selections from publication sections that users want to mark as important or
noteworthy, optionally with annotations.
## When to Use This
## When to Use This
@ -19,75 +22,77 @@ This technique allows you to create test highlight events (kind 9802) for testin
npm install nostr-tools ws
npm install nostr-tools ws
```
```
2. **Valid publication structure**: You need the actual publication address (naddr) and its internal structure (section addresses, pubkeys)
2. **Valid publication structure**: You need the actual publication address
(naddr) and its internal structure (section addresses, pubkeys)
## Step 1: Decode the Publication Address
## Step 1: Decode the Publication Address
If you have an `naddr` (Nostr address), decode it to find the publication structure:
If you have an `naddr` (Nostr address), decode it to find the publication
structure:
**Script**: `check-publication-structure.js`
**Script**: `check-publication-structure.js`
```javascript
```javascript
import { nip19 } from 'nostr-tools';
import { nip19 } from "nostr-tools";
import WebSocket from 'ws';
import WebSocket from "ws";
const naddr = 'naddr1qvzqqqr4t...'; // Your publication naddr
const naddr = "naddr1qvzqqqr4t..."; // Your publication naddr
// Relays to publish to (matching HighlightLayer's relay list)
// Relays to publish to (matching HighlightLayer's relay list)
const relays = [
const relays = [
'wss://relay.damus.io',
"wss://relay.damus.io",
'wss://relay.nostr.band',
"wss://relay.nostr.band",
'wss://nostr.wine',
"wss://nostr.wine",
];
];
// Test highlights to create
// Test highlights to create
const testHighlights = [
const testHighlights = [
{
{
highlightedText: 'Knowledge that tries to stay put inevitably becomes ossified',
highlightedText:
context: 'This is the fundamental paradox... Knowledge that tries to stay put inevitably becomes ossified, a monument to itself... The attempt to hold knowledge still is like trying to photograph a river',
"Knowledge that tries to stay put inevitably becomes ossified",
comment: 'This perfectly captures why traditional academia struggles', // Optional
context:
"This is the fundamental paradox... Knowledge that tries to stay put inevitably becomes ossified, a monument to itself... The attempt to hold knowledge still is like trying to photograph a river",
comment: "This perfectly captures why traditional academia struggles", // Optional
targetAddress: sections[0],
targetAddress: sections[0],
author: testUserKey,
author: testUserKey,
authorPubkey: testUserPubkey,
authorPubkey: testUserPubkey,
},
},
{
{
highlightedText: 'The attempt to hold knowledge still is like trying to photograph a river',
highlightedText:
context: '... a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.',
"The attempt to hold knowledge still is like trying to photograph a river",
comment: null, // No annotation, just highlight
context:
"... a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.",
comment: null, // No annotation, just highlight
targetAddress: sections[0],
targetAddress: sections[0],
author: testUserKey,
author: testUserKey,
authorPubkey: testUserPubkey,
authorPubkey: testUserPubkey,
@ -193,14 +205,14 @@ async function publishEvent(event, relayUrl) {
const ws = new WebSocket(relayUrl);
const ws = new WebSocket(relayUrl);
let published = false;
let published = false;
ws.on('open', () => {
ws.on("open", () => {
console.log(`Connected to ${relayUrl}`);
console.log(`Connected to ${relayUrl}`);
ws.send(JSON.stringify(['EVENT', event]));
ws.send(JSON.stringify(["EVENT", event]));
});
});
ws.on('message', (data) => {
ws.on("message", (data) => {
const message = JSON.parse(data.toString());
const message = JSON.parse(data.toString());
if (message[0] === 'OK'&& message[1] === event.id) {
if (message[0] === "OK"&& message[1] === event.id) {
if (message[2]) {
if (message[2]) {
console.log(`✓ Published ${event.id.substring(0, 8)}`);
console.log(`✓ Published ${event.id.substring(0, 8)}`);
published = true;
published = true;
@ -214,22 +226,22 @@ async function publishEvent(event, relayUrl) {
}
}
});
});
ws.on('error', reject);
ws.on("error", reject);
ws.on('close', () => {
ws.on("close", () => {
if (!published) reject(new Error('Connection closed'));
if (!published) reject(new Error("Connection closed"));
});
});
setTimeout(() => {
setTimeout(() => {
if (!published) {
if (!published) {
ws.close();
ws.close();
reject(new Error('Timeout'));
reject(new Error("Timeout"));
}
}
}, 10000);
}, 10000);
});
});
}
}
async function createAndPublishHighlights() {
async function createAndPublishHighlights() {
console.log('\n=== Creating Test Highlights ===\n');
console.log("\n=== Creating Test Highlights ===\n");
for (const highlight of testHighlights) {
for (const highlight of testHighlights) {
try {
try {
@ -238,23 +250,25 @@ async function createAndPublishHighlights() {
kind: 9802,
kind: 9802,
created_at: Math.floor(Date.now() / 1000),
created_at: Math.floor(Date.now() / 1000),
tags: [
tags: [
['a', highlight.targetAddress, relays[0]],
["a", highlight.targetAddress, relays[0]],
['context', highlight.context],
["context", highlight.context],
['p', publicationPubkey, relays[0], 'author'],
["p", publicationPubkey, relays[0], "author"],
],
],
content: highlight.highlightedText, // The highlighted text
content: highlight.highlightedText, // The highlighted text
- ✅ Validates invalid address format (too few parts)
- ✅ Validates invalid address format (too few parts)
@ -17,6 +21,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function
- ✅ Parses different publication kinds (30040, 30041, 30818, 30023)
- ✅ Parses different publication kinds (30040, 30041, 30818, 30023)
### 2. NIP-22 Event Creation (8 tests)
### 2. NIP-22 Event Creation (8 tests)
- ✅ Creates kind 1111 comment event
- ✅ Creates kind 1111 comment event
- ✅ Includes correct uppercase tags (A, K, P) for root scope
- ✅ Includes correct uppercase tags (A, K, P) for root scope
- ✅ Includes correct lowercase tags (a, k, p) for parent scope
- ✅ Includes correct lowercase tags (a, k, p) for parent scope
@ -27,12 +32,14 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function
- ✅ Handles empty relay list gracefully
- ✅ Handles empty relay list gracefully
### 3. Event Signing and Publishing (4 tests)
### 3. Event Signing and Publishing (4 tests)
- ✅ Signs event with user's signer
- ✅ Signs event with user's signer
- ✅ Publishes to outbox relays
- ✅ Publishes to outbox relays
- ✅ Handles publishing errors gracefully
- ✅ Handles publishing errors gracefully
- ✅ Throws error when publishing fails
- ✅ Throws error when publishing fails
### 4. User Authentication (5 tests)
### 4. User Authentication (5 tests)
- ✅ Requires user to be signed in
- ✅ Requires user to be signed in
- ✅ Shows error when user is not signed in
- ✅ Shows error when user is not signed in
- ✅ Allows commenting when user is signed in
- ✅ Allows commenting when user is signed in
@ -40,6 +47,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function
- ✅ Handles missing user profile gracefully
- ✅ Handles missing user profile gracefully
### 5. User Interactions (7 tests)
### 5. User Interactions (7 tests)
- ✅ Prevents submission of empty comment
- ✅ Prevents submission of empty comment
- ✅ Allows submission of non-empty comment
- ✅ Allows submission of non-empty comment
- ✅ Handles whitespace-only comments as empty
- ✅ Handles whitespace-only comments as empty
@ -49,6 +57,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function
- ✅ Does not error when onCommentPosted is not provided
- ✅ Does not error when onCommentPosted is not provided
### 6. UI State Management (10 tests)
### 6. UI State Management (10 tests)
- ✅ Button is hidden by default
- ✅ Button is hidden by default
- ✅ Button appears on section hover
- ✅ Button appears on section hover
- ✅ Button remains visible when comment UI is shown
- ✅ Button remains visible when comment UI is shown
@ -61,6 +70,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function
- ✅ Enables submit button when comment is valid
- ✅ Enables submit button when comment is valid
### 7. Edge Cases (8 tests)
### 7. Edge Cases (8 tests)
- ✅ Handles invalid address format gracefully
- ✅ Handles invalid address format gracefully
- ✅ Handles network errors during event fetch
- ✅ Handles network errors during event fetch
- ✅ Handles missing relay information
- ✅ Handles missing relay information
@ -71,17 +81,20 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function
- ✅ Handles publish failure when no relays accept event
- ✅ Handles publish failure when no relays accept event
### 8. Cancel Functionality (4 tests)
### 8. Cancel Functionality (4 tests)
- ✅ Clears comment content when canceling
- ✅ Clears comment content when canceling
- ✅ Closes comment UI when canceling
- ✅ Closes comment UI when canceling
- ✅ Clears error state when canceling
- ✅ Clears error state when canceling
- ✅ Clears success state when canceling
- ✅ Clears success state when canceling
### 9. Event Fetching (3 tests)
### 9. Event Fetching (3 tests)
- ✅ Fetches target event to get event ID
- ✅ Fetches target event to get event ID
- ✅ Continues without event ID when fetch fails
- ✅ Continues without event ID when fetch fails
- ✅ Handles null event from fetch
- ✅ Handles null event from fetch
### 10. CSS Classes and Styling (6 tests)
### 10. CSS Classes and Styling (6 tests)
- ✅ Applies visible class when section is hovered
- ✅ Applies visible class when section is hovered
- ✅ Removes visible class when not hovered and UI closed
- ✅ Removes visible class when not hovered and UI closed
- ✅ Button has correct aria-label
- ✅ Button has correct aria-label
@ -90,6 +103,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function
- ✅ Submit button shows normal state when not submitting
- ✅ Submit button shows normal state when not submitting
### 11. NIP-22 Compliance (5 tests)
### 11. NIP-22 Compliance (5 tests)
- ✅ Uses kind 1111 for comment events
- ✅ Uses kind 1111 for comment events
- ✅ Includes all required NIP-22 tags for addressable events
- ✅ Includes all required NIP-22 tags for addressable events
- ✅ A tag includes relay hint and author pubkey
- ✅ A tag includes relay hint and author pubkey
@ -97,6 +111,7 @@ Comprehensive test suite for CommentButton component and NIP-22 comment function
- ✅ Lowercase tags for parent scope match root tags
- ✅ Lowercase tags for parent scope match root tags
### 12. Integration Scenarios (4 tests)
### 12. Integration Scenarios (4 tests)
- ✅ Complete comment flow for signed-in user
- ✅ Complete comment flow for signed-in user
- ✅ Prevents comment flow for signed-out user
- ✅ Prevents comment flow for signed-out user
- ✅ Handles comment with event ID lookup
- ✅ Handles comment with event ID lookup
@ -128,13 +143,18 @@ The tests verify the correct NIP-22 tag structure for addressable events:
```
```
## Files Changed
## Files Changed
- `tests/unit/commentButton.test.ts` - 911 lines (new file)
- `tests/unit/commentButton.test.ts` - 911 lines (new file)
- `package-lock.json` - Updated dependencies
- `package-lock.json` - Updated dependencies
## Current Status
## Current Status
All tests are passing and changes are staged for commit. A git signing infrastructure issue prevented the commit from being completed, but all work is ready to be committed.
All tests are passing and changes are staged for commit. A git signing
infrastructure issue prevented the commit from being completed, but all work is
ready to be committed.
## To Commit and Push
## To Commit and Push
```bash
```bash
cd /home/user/gc-alexandria-comments
cd /home/user/gc-alexandria-comments
git commit -m "Add TDD tests for comment functionality"
git commit -m "Add TDD tests for comment functionality"
The `[[term]]` syntax in content automatically generates w-tags, creating a web of implicit references across your knowledge base, while d-tags remain explicit structural identifiers.
The `[[term]]` syntax in content automatically generates w-tags, creating a web
of implicit references across your knowledge base, while d-tags remain explicit
// Section addresses (from the actual publication structure)
// Section addresses (from the actual publication structure)
constsections=[
constsections=[
@ -25,15 +27,16 @@ const sections = [
// Relays to publish to (matching CommentLayer's relay list)
// Relays to publish to (matching CommentLayer's relay list)
constrelays=[
constrelays=[
'wss://relay.damus.io',
"wss://relay.damus.io",
'wss://relay.nostr.band',
"wss://relay.nostr.band",
'wss://nostr.wine',
"wss://nostr.wine",
];
];
// Test comments to create
// Test comments to create
consttestComments=[
consttestComments=[
{
{
content:'This is a fascinating exploration of how knowledge naturally resists institutional capture. The analogy to flowing water is particularly apt.',
content:
"This is a fascinating exploration of how knowledge naturally resists institutional capture. The analogy to flowing water is particularly apt.",
targetAddress:sections[0],
targetAddress:sections[0],
targetKind:30041,
targetKind:30041,
author:testUserKey,
author:testUserKey,
@ -41,7 +44,8 @@ const testComments = [
isReply:false,
isReply:false,
},
},
{
{
content:'I love this concept! It reminds me of how open source projects naturally organize without top-down control.',
content:
"I love this concept! It reminds me of how open source projects naturally organize without top-down control.",
targetAddress:sections[0],
targetAddress:sections[0],
targetKind:30041,
targetKind:30041,
author:testUser2Key,
author:testUser2Key,
@ -49,7 +53,8 @@ const testComments = [
isReply:false,
isReply:false,
},
},
{
{
content:'The section on institutional capture really resonates with my experience in academia.',
content:
"The section on institutional capture really resonates with my experience in academia.",
targetAddress:sections[1],
targetAddress:sections[1],
targetKind:30041,
targetKind:30041,
author:testUserKey,
author:testUserKey,
@ -57,7 +62,8 @@ const testComments = [
isReply:false,
isReply:false,
},
},
{
{
content:'Excellent point about underground networks of understanding. This is exactly how most practical knowledge develops.',
content:
"Excellent point about underground networks of understanding. This is exactly how most practical knowledge develops.",
targetAddress:sections[2],
targetAddress:sections[2],
targetKind:30041,
targetKind:30041,
author:testUser2Key,
author:testUser2Key,
@ -65,7 +71,8 @@ const testComments = [
isReply:false,
isReply:false,
},
},
{
{
content:'This is a brilliant piece of work! Really captures the tension between institutional knowledge and living understanding.',
content:
"This is a brilliant piece of work! Really captures the tension between institutional knowledge and living understanding.",
targetAddress:rootAddress,
targetAddress:rootAddress,
targetKind:30040,
targetKind:30040,
author:testUserKey,
author:testUserKey,
@ -79,16 +86,18 @@ async function publishEvent(event, relayUrl) {
constws=newWebSocket(relayUrl);
constws=newWebSocket(relayUrl);
letpublished=false;
letpublished=false;
ws.on('open',()=>{
ws.on("open",()=>{
console.log(`Connected to ${relayUrl}`);
console.log(`Connected to ${relayUrl}`);
ws.send(JSON.stringify(['EVENT',event]));
ws.send(JSON.stringify(["EVENT",event]));
});
});
ws.on('message',(data)=>{
ws.on("message",(data)=>{
constmessage=JSON.parse(data.toString());
constmessage=JSON.parse(data.toString());
if(message[0]==='OK'&&message[1]===event.id){
if(message[0]==="OK"&&message[1]===event.id){
if(message[2]){
if(message[2]){
console.log(`✓ Published event ${event.id.substring(0,8)} to ${relayUrl}`);
console.log(
`✓ Published event ${event.id.substring(0,8)} to ${relayUrl}`,
);
published=true;
published=true;
ws.close();
ws.close();
resolve();
resolve();
@ -100,14 +109,14 @@ async function publishEvent(event, relayUrl) {
// Section addresses (from the actual publication structure)
// Section addresses (from the actual publication structure)
constsections=[
constsections=[
@ -25,9 +27,9 @@ const sections = [
// Relays to publish to (matching HighlightLayer's relay list)
// Relays to publish to (matching HighlightLayer's relay list)
constrelays=[
constrelays=[
'wss://relay.damus.io',
"wss://relay.damus.io",
'wss://relay.nostr.band',
"wss://relay.nostr.band",
'wss://nostr.wine',
"wss://nostr.wine",
];
];
// Test highlights to create
// Test highlights to create
@ -35,40 +37,53 @@ const relays = [
// and optionally a user comment/annotation in the ["comment", ...] tag
// and optionally a user comment/annotation in the ["comment", ...] tag
consttestHighlights=[
consttestHighlights=[
{
{
highlightedText:'Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice.',
highlightedText:
context:'This is the fundamental paradox of institutional knowledge: it must be captured to be shared, but the very act of capture begins its transformation into something else. Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.',
"Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice.",
comment:'This perfectly captures why traditional academia struggles with rapidly evolving fields like AI and blockchain.',
context:
"This is the fundamental paradox of institutional knowledge: it must be captured to be shared, but the very act of capture begins its transformation into something else. Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.",
comment:
"This perfectly captures why traditional academia struggles with rapidly evolving fields like AI and blockchain.",
targetAddress:sections[0],
targetAddress:sections[0],
author:testUserKey,
author:testUserKey,
authorPubkey:testUserPubkey,
authorPubkey:testUserPubkey,
},
},
{
{
highlightedText:'The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.',
highlightedText:
context:'Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.',
"The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.",
comment:null,// Highlight without annotation
context:
"Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice. The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow.",
comment:null,// Highlight without annotation
targetAddress:sections[0],
targetAddress:sections[0],
author:testUser2Key,
author:testUser2Key,
authorPubkey:testUser2Pubkey,
authorPubkey:testUser2Pubkey,
},
},
{
{
highlightedText:'Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas.',
highlightedText:
context:'The natural state of knowledge is not purity but promiscuity. Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas. It crosses boundaries not despite them but because of them. The most vibrant intellectual communities have always been those at crossroads and borderlands.',
"Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas.",
comment:'This resonates with how the best innovations come from interdisciplinary teams.',
context:
"The natural state of knowledge is not purity but promiscuity. Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas. It crosses boundaries not despite them but because of them. The most vibrant intellectual communities have always been those at crossroads and borderlands.",
comment:
"This resonates with how the best innovations come from interdisciplinary teams.",
targetAddress:sections[1],
targetAddress:sections[1],
author:testUserKey,
author:testUserKey,
authorPubkey:testUserPubkey,
authorPubkey:testUserPubkey,
},
},
{
{
highlightedText:'The most vibrant intellectual communities have always been those at crossroads and borderlands.',
highlightedText:
context:'Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas. It crosses boundaries not despite them but because of them. The most vibrant intellectual communities have always been those at crossroads and borderlands.',
"The most vibrant intellectual communities have always been those at crossroads and borderlands.",
comment:'Historical examples: Renaissance Florence, Vienna Circle, Bell Labs',
context:
"Understanding is naturally promiscuous—it wants to mix, merge, and mate with other ideas. It crosses boundaries not despite them but because of them. The most vibrant intellectual communities have always been those at crossroads and borderlands.",
comment:
"Historical examples: Renaissance Florence, Vienna Circle, Bell Labs",
targetAddress:sections[1],
targetAddress:sections[1],
author:testUser2Key,
author:testUser2Key,
authorPubkey:testUser2Pubkey,
authorPubkey:testUser2Pubkey,
},
},
{
{
highlightedText:'institutions that try to monopolize understanding inevitably find themselves gatekeeping corpses',
highlightedText:
context:'But institutions that try to monopolize understanding inevitably find themselves gatekeeping corpses—the living knowledge has already escaped and is flourishing in unexpected places. By the time the gatekeepers notice, the game has moved.',
"institutions that try to monopolize understanding inevitably find themselves gatekeeping corpses",
context:
"But institutions that try to monopolize understanding inevitably find themselves gatekeeping corpses—the living knowledge has already escaped and is flourishing in unexpected places. By the time the gatekeepers notice, the game has moved.",
comment:null,
comment:null,
targetAddress:sections[2],
targetAddress:sections[2],
author:testUserKey,
author:testUserKey,
@ -81,16 +96,18 @@ async function publishEvent(event, relayUrl) {
constws=newWebSocket(relayUrl);
constws=newWebSocket(relayUrl);
letpublished=false;
letpublished=false;
ws.on('open',()=>{
ws.on("open",()=>{
console.log(`Connected to ${relayUrl}`);
console.log(`Connected to ${relayUrl}`);
ws.send(JSON.stringify(['EVENT',event]));
ws.send(JSON.stringify(["EVENT",event]));
});
});
ws.on('message',(data)=>{
ws.on("message",(data)=>{
constmessage=JSON.parse(data.toString());
constmessage=JSON.parse(data.toString());
if(message[0]==='OK'&&message[1]===event.id){
if(message[0]==="OK"&&message[1]===event.id){
if(message[2]){
if(message[2]){
console.log(`✓ Published event ${event.id.substring(0,8)} to ${relayUrl}`);
console.log(
`✓ Published event ${event.id.substring(0,8)} to ${relayUrl}`,
);
published=true;
published=true;
ws.close();
ws.close();
resolve();
resolve();
@ -102,14 +119,14 @@ async function publishEvent(event, relayUrl) {
This document outlines the complete restart plan for implementing NKBIP-01 compliant hierarchical AsciiDoc parsing using proper Asciidoctor tree processor extensions.
This document outlines the complete restart plan for implementing NKBIP-01
compliant hierarchical AsciiDoc parsing using proper Asciidoctor tree processor
extensions.
## Current State Analysis
## Current State Analysis
### Problems Identified
### Problems Identified
1. **Dual Architecture Conflict**: Two competing parsing implementations exist:
1. **Dual Architecture Conflict**: Two competing parsing implementations exist:
A special event with kind `5`, meaning "deletion request" is defined as having a list of one or more `e` or `a` tags, each referencing an event the author is requesting to be deleted. Deletion requests SHOULD include a `k` tag for the kind of each event being requested for deletion.
A special event with kind `5`, meaning "deletion request" is defined as having a
list of one or more `e` or `a` tags, each referencing an event the author is
requesting to be deleted. Deletion requests SHOULD include a `k` tag for the
kind of each event being requested for deletion.
The event's `content` field MAY contain a text note describing the reason for the deletion request.
The event's `content` field MAY contain a text note describing the reason for
the deletion request.
For example:
For example:
@ -28,26 +30,48 @@ For example:
}
}
```
```
Relays SHOULD delete or stop publishing any referenced events that have an identical `pubkey` as the deletion request. Clients SHOULD hide or otherwise indicate a deletion request status for referenced events.
Relays SHOULD delete or stop publishing any referenced events that have an
identical `pubkey` as the deletion request. Clients SHOULD hide or otherwise
indicate a deletion request status for referenced events.
Relays SHOULD continue to publish/share the deletion request events indefinitely, as clients may already have the event that's intended to be deleted. Additionally, clients SHOULD broadcast deletion request events to other relays which don't have it.
Relays SHOULD continue to publish/share the deletion request events
indefinitely, as clients may already have the event that's intended to be
deleted. Additionally, clients SHOULD broadcast deletion request events to other
relays which don't have it.
When an `a` tag is used, relays SHOULD delete all versions of the replaceable event up to the `created_at` timestamp of the deletion request event.
When an `a` tag is used, relays SHOULD delete all versions of the replaceable
event up to the `created_at` timestamp of the deletion request event.
## Client Usage
## Client Usage
Clients MAY choose to fully hide any events that are referenced by valid deletion request events. This includes text notes, direct messages, or other yet-to-be defined event kinds. Alternatively, they MAY show the event along with an icon or other indication that the author has "disowned" the event. The `content` field MAY also be used to replace the deleted events' own content, although a user interface should clearly indicate that this is a deletion request reason, not the original content.
Clients MAY choose to fully hide any events that are referenced by valid
deletion request events. This includes text notes, direct messages, or other
yet-to-be defined event kinds. Alternatively, they MAY show the event along with
an icon or other indication that the author has "disowned" the event. The
`content` field MAY also be used to replace the deleted events' own content,
although a user interface should clearly indicate that this is a deletion
request reason, not the original content.
A client MUST validate that each event `pubkey` referenced in the `e` tag of the deletion request is identical to the deletion request `pubkey`, before hiding or deleting any event. Relays can not, in general, perform this validation and should not be treated as authoritative.
A client MUST validate that each event `pubkey` referenced in the `e` tag of the
deletion request is identical to the deletion request `pubkey`, before hiding or
deleting any event. Relays can not, in general, perform this validation and
should not be treated as authoritative.
Clients display the deletion request event itself in any way they choose, e.g., not at all, or with a prominent notice.
Clients display the deletion request event itself in any way they choose, e.g.,
not at all, or with a prominent notice.
Clients MAY choose to inform the user that their request for deletion does not guarantee deletion because it is impossible to delete events from all relays and clients.
Clients MAY choose to inform the user that their request for deletion does not
guarantee deletion because it is impossible to delete events from all relays and
clients.
## Relay Usage
## Relay Usage
Relays MAY validate that a deletion request event only references events that have the same `pubkey` as the deletion request itself, however this is not required since relays may not have knowledge of all referenced events.
Relays MAY validate that a deletion request event only references events that
have the same `pubkey` as the deletion request itself, however this is not
required since relays may not have knowledge of all referenced events.
## Deletion Request of a Deletion Request
## Deletion Request of a Deletion Request
Publishing a deletion request event against a deletion request has no effect. Clients and relays are not obliged to support "unrequest deletion" functionality.
Publishing a deletion request event against a deletion request has no effect.
Clients and relays are not obliged to support "unrequest deletion"
// Use a global regex to catch all occurrences (Asciidoctor might have duplicated them)
standalone: false,
placeholders.forEach((link, placeholder) => {
attributes: {
const className =
showtitle: false,
link.type === "auto"
sectids: false,
? "wiki-link wiki-link-auto"
},
: link.type === "w"
? "wiki-link wiki-link-ref"
: "wiki-link wiki-link-def";
const title =
link.type === "w"
? "Wiki reference (mentions this concept)"
: link.type === "d"
? "Wiki definition (defines this concept)"
: "Wiki link (searches both references and definitions)";
const html = `<aclass="${className}"href="#wiki/${link.type}/${encodeURIComponent(link.term)}"title="${title}"data-wiki-type="${link.type}"data-wiki-term="${link.term}">${link.displayText}</a>`;
// Use global replace to handle all occurrences
const regex = new RegExp(
placeholder.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&",
),
"g",
);
rendered = rendered
.toString()
.replace(regex, html);
});
});
}
// Replace placeholders with actual wiki link HTML
// Use a global regex to catch all occurrences (Asciidoctor might have duplicated them)
placeholders.forEach((link, placeholder) => {
const className =
link.type === 'auto'
? 'wiki-link wiki-link-auto'
: link.type === 'w'
? 'wiki-link wiki-link-ref'
: 'wiki-link wiki-link-def';
const title =
link.type === 'w'
? 'Wiki reference (mentions this concept)'
: link.type === 'd'
? 'Wiki definition (defines this concept)'
: 'Wiki link (searches both references and definitions)';
const html = `<aclass="${className}"href="#wiki/${link.type}/${encodeURIComponent(link.term)}"title="${title}"data-wiki-type="${link.type}"data-wiki-term="${link.term}">${link.displayText}</a>`;
// Use global replace to handle all occurrences
const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
console.log(`[HighlightLayer] Searching in specific section: ${targetAddress}`);
console.log(
`[HighlightLayer] Searching in specific section: ${targetAddress}`,
);
} else {
} else {
console.log(`[HighlightLayer] Section ${targetAddress} not found in DOM, searching globally`);
console.log(
`[HighlightLayer] Section ${targetAddress} not found in DOM, searching globally`,
);
}
}
}
}
console.log(`[HighlightLayer] Searching for text: "${text}" in`, searchRoot);
console.log(
`[HighlightLayer] Searching for text: "${text}" in`,
searchRoot,
);
// Use TreeWalker to find all text nodes
// Use TreeWalker to find all text nodes
const walker = document.createTreeWalker(
const walker = document.createTreeWalker(
searchRoot,
searchRoot,
NodeFilter.SHOW_TEXT,
NodeFilter.SHOW_TEXT,
null
null,
);
);
const textNodes: Node[] = [];
const textNodes: Node[] = [];
@ -338,19 +412,30 @@
}
}
// Search for the highlight text in text nodes
// Search for the highlight text in text nodes
console.log(`[HighlightLayer] Searching through ${textNodes.length} text nodes`);
console.log(
`[HighlightLayer] Searching through ${textNodes.length} text nodes`,
);
for (const textNode of textNodes) {
for (const textNode of textNodes) {
const nodeText = textNode.textContent || "";
const nodeText = textNode.textContent || "";
const index = nodeText.toLowerCase().indexOf(text.toLowerCase());
const index = nodeText.toLowerCase().indexOf(text.toLowerCase());
if (index !== -1) {
if (index !== -1) {
console.log(`[HighlightLayer] Found match in text node:`, nodeText.substring(Math.max(0, index - 20), Math.min(nodeText.length, index + text.length + 20)));
console.log(
`[HighlightLayer] Found match in text node:`,
nodeText.substring(
Math.max(0, index - 20),
Math.min(nodeText.length, index + text.length + 20),
),
);
const parent = textNode.parentNode;
const parent = textNode.parentNode;
if (!parent) continue;
if (!parent) continue;
// Skip if already highlighted
// Skip if already highlighted
if (parent.nodeName === "MARK" || (parent instanceof Element && parent.classList?.contains("highlight"))) {
if (
parent.nodeName === "MARK" ||
(parent instanceof Element && parent.classList?.contains("highlight"))
) {
continue;
continue;
}
}
@ -386,10 +471,14 @@
* Render all highlights on the page
* Render all highlights on the page
*/
*/
function renderHighlights() {
function renderHighlights() {
console.log(`[HighlightLayer] renderHighlights called - visible: ${visible}, containerRef: ${!!containerRef}, highlights: ${highlights.length}`);
console.log(
`[HighlightLayer] renderHighlights called - visible: ${visible}, containerRef: ${!!containerRef}, highlights: ${highlights.length}`,
// Sample highlighted text snippets (things users might actually highlight)
// Sample highlighted text snippets (things users might actually highlight)
consthighlightedTexts=[
consthighlightedTexts=[
'Knowledge that tries to stay put inevitably becomes ossified',
"Knowledge that tries to stay put inevitably becomes ossified",
'The attempt to hold knowledge still is like trying to photograph a river',
"The attempt to hold knowledge still is like trying to photograph a river",
'Understanding emerges not from rigid frameworks but from fluid engagement',
"Understanding emerges not from rigid frameworks but from fluid engagement",
'Traditional institutions struggle with the natural promiscuity of ideas',
"Traditional institutions struggle with the natural promiscuity of ideas",
'Thinking without permission means refusing predetermined categories',
"Thinking without permission means refusing predetermined categories",
'The most valuable insights often come from unexpected juxtapositions',
"The most valuable insights often come from unexpected juxtapositions",
'Anarchistic knowledge rejects the notion of authorized interpreters',
"Anarchistic knowledge rejects the notion of authorized interpreters",
'Every act of reading is an act of creative interpretation',
"Every act of reading is an act of creative interpretation",
'Hierarchy in knowledge systems serves power, not understanding',
"Hierarchy in knowledge systems serves power, not understanding",
'The boundary between creator and consumer is an artificial construction',
"The boundary between creator and consumer is an artificial construction",
];
];
// Context strings (surrounding text to help locate the highlight)
// Context strings (surrounding text to help locate the highlight)
constcontexts=[
constcontexts=[
'This is the fundamental paradox of institutionalized knowledge. Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice.',
"This is the fundamental paradox of institutionalized knowledge. Knowledge that tries to stay put inevitably becomes ossified, a monument to itself rather than a living practice.",
'The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow. What remains is a static representation, not the dynamic reality.',
"The attempt to hold knowledge still is like trying to photograph a river—you capture an image, but you lose the flow. What remains is a static representation, not the dynamic reality.",
'Understanding emerges not from rigid frameworks but from fluid engagement with ideas, people, and contexts. This fluidity is precisely what traditional systems attempt to eliminate.',
"Understanding emerges not from rigid frameworks but from fluid engagement with ideas, people, and contexts. This fluidity is precisely what traditional systems attempt to eliminate.",
'Traditional institutions struggle with the natural promiscuity of ideas—the way concepts naturally migrate, mutate, and merge across boundaries that were meant to contain them.',
"Traditional institutions struggle with the natural promiscuity of ideas—the way concepts naturally migrate, mutate, and merge across boundaries that were meant to contain them.",
'Thinking without permission means refusing predetermined categories and challenging the gatekeepers who claim authority over legitimate thought.',
"Thinking without permission means refusing predetermined categories and challenging the gatekeepers who claim authority over legitimate thought.",
'The most valuable insights often come from unexpected juxtapositions, from bringing together ideas that were never meant to meet.',
"The most valuable insights often come from unexpected juxtapositions, from bringing together ideas that were never meant to meet.",
'Anarchistic knowledge rejects the notion of authorized interpreters, asserting instead that meaning-making is a fundamentally distributed and democratic process.',
"Anarchistic knowledge rejects the notion of authorized interpreters, asserting instead that meaning-making is a fundamentally distributed and democratic process.",
'Every act of reading is an act of creative interpretation, a collaboration between text and reader that produces something new each time.',
"Every act of reading is an act of creative interpretation, a collaboration between text and reader that produces something new each time.",
'Hierarchy in knowledge systems serves power, not understanding. It determines who gets to speak, who must listen, and what counts as legitimate knowledge.',
"Hierarchy in knowledge systems serves power, not understanding. It determines who gets to speak, who must listen, and what counts as legitimate knowledge.",
'The boundary between creator and consumer is an artificial construction, one that digital networks make increasingly untenable and obsolete.',
"The boundary between creator and consumer is an artificial construction, one that digital networks make increasingly untenable and obsolete.",
];
];
// Optional annotations (user comments on their highlights)
// Optional annotations (user comments on their highlights)
constannotations=[
constannotations=[
'This perfectly captures the institutional problem',
"This perfectly captures the institutional problem",
'Key insight - worth revisiting',
"Key insight - worth revisiting",
'Reminds me of Deleuze on rhizomatic structures',
"Reminds me of Deleuze on rhizomatic structures",
'Fundamental critique of academic gatekeeping',
"Fundamental critique of academic gatekeeping",
'The core argument in one sentence',
"The core argument in one sentence",
null,// Some highlights have no annotation
null,// Some highlights have no annotation
'Important for understanding the broader thesis',
"Important for understanding the broader thesis",
null,
null,
'Connects to earlier discussion on page 12',
"Connects to earlier discussion on page 12",
null,
null,
];
];
// Mock pubkeys - MUST be exactly 64 hex characters
// Mock pubkeys - MUST be exactly 64 hex characters
test.test('Subsection content should be cleanly separated',()=>{
test.test("Subsection content should be cleanly separated",()=>{
// "=== Why Investigate the Nature of Knowledge?" subsection
// "=== Why Investigate the Nature of Knowledge?" subsection
constexpectedSubsectionContent=`Understanding the nature of knowledge itself is fundamental, distinct from simply studying how we learn or communicate. Knowledge exests first as representations within individuals, separate from how we interact with it...`;
constexpectedSubsectionContent=
`Understanding the nature of knowledge itself is fundamental, distinct from simply studying how we learn or communicate. Knowledge exests first as representations within individuals, separate from how we interact with it...`;