upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
2026-04-10release: v1.0.2HEADv1.0.2masterDanConwayDev
2026-04-10feat: scan filesystem for orphan git repos with no matching 30617 eventDanConwayDev
Extends cleanup-empty-repos with a second scan direction (filesystem → DB). Bare git repos under the git data path that have no corresponding 30617 announcement event are identified as orphans and cleaned up. Empty orphans are always removed. Non-empty orphans are flagged in the report but only deleted when --purge-orphans is also passed, preventing accidental data loss.
2026-04-10fix: pass --git-dir as global git option in check_repo_emptyDanConwayDev
--git-dir must precede the subcommand; passing it after for-each-ref caused git to ignore it and check the CWD instead, making every repo appear empty.
2026-04-10feat: add cleanup-empty-repos subcommand to remove stale events for empty ↵DanConwayDev
git repos Adds a maintenance subcommand that scans the LMDB database for kind 30617 (repository announcement) events whose bare git repo on disk is empty or missing, then removes both the 30617 and any matching 30618 (state) events. A relay should not serve announcement or state events for a repository with no git data. This was needed to clean up repos leaked by the bug fixed in 2161e3c, and is useful as an ongoing maintenance tool. Usage (dry-run by default, stop relay before --execute): ngit-grasp cleanup-empty-repos [--relay-data-path <path>] [--git-data-path <path>] [--execute] The relay itself is now invoked as an implicit 'serve' subcommand, preserving full backward compatibility with existing deployments and env-var configuration.
2026-04-10fix: purgatory replacement announcements leaked into DB without git dataDanConwayDev
When a replacement 30617 announcement arrived for an entry already in purgatory (e.g. the same event fetched from a second relay during sync, or a user re-submitting a slightly updated announcement), the policy returned Accept instead of AcceptPurgatory. This caused the event to be saved to the database immediately, bypassing the purgatory gate, without the corresponding git data or state events ever arriving. Fix: return AcceptPurgatory when replacing a purgatory entry so the updated event stays in purgatory until git data arrives. The purgatory entry is still updated with the newer event via replace_purgatory_announcement before the return.
2026-04-10fix: accept any d-tag identifier; percent-encode in URLsDanConwayDev
NIP-01 places no restriction on d tag characters and NIP-34 only recommends kebab-case without mandating it. Rejecting identifiers with whitespace or other URL-unsafe characters was therefore overly strict. The correct approach (per NIP-34 PR #2312 and GRASP-01) is to store identifiers verbatim on disk and percent-encode them when constructing URLs. The previous commit already handled the incoming direction (percent-decoding URL paths before filesystem lookup); this commit handles the outgoing direction and removes the validation restriction. Changes: - validate_identifier: drop whitespace rejection; only reject chars that are unsafe as filesystem directory names (/, \, null, . / ..) - git/mod.rs: add percent_encode() alongside percent_decode() - landing.rs: percent-encode identifier in nostr:// clone URL and gitworkshop link (also fixes a pre-existing bug where the clone URL displayed literal '{npub}' / '{identifier}' instead of the values)
2026-04-09fix: reject identifiers with whitespace and URL-decode path componentsDanConwayDev
Two bugs allowed a repository announcement with a space-containing identifier ('kuboslopp by Shakespeare') to enter purgatory and create a bare repo on disk, but then fail to serve git data over HTTP. Bug 1 (serving): parse_git_url and parse_repo_url did not percent-decode the URL path before resolving the filesystem path. A client requesting /npub.../kuboslopp%20by%20Shakespeare.git/info/refs had the identifier extracted as 'kuboslopp%20by%20Shakespeare' (literal %20), which did not match the on-disk directory 'kuboslopp by Shakespeare.git'. Fix: add percent_decode() in src/git/mod.rs and apply it to the repo component in both parse_git_url and parse_repo_url. Bug 2 (validation): validate_announcement did not check that the identifier is safe as a filesystem path component and URL segment. Identifiers containing whitespace, path separators, null bytes, or reserved names (. / ..) should be rejected at acceptance time. Fix: add validate_identifier() in src/nostr/events.rs and call it from validate_announcement before any other policy checks.
2026-03-25chore: remove arbitrary default max connections limitDanConwayDev
When NGIT_MAX_CONNECTIONS is unset the relay imposes no connection cap, deferring to OS fd limits and infrastructure controls. The option remains available for operators who want an explicit ceiling.
2026-02-27release: v1.0.1v1.0.1DanConwayDev
2026-02-26chore: apply cargo fmt and fix clippy warningsDanConwayDev
Fix pre-existing clippy lints: - &PathBuf -> &Path in audit_cleanup.rs - too_many_arguments on process_newly_available_git_data, process_purgatory_announcements, and HttpService::new - clone_on_copy for PublicKey (Copy type) in purgatory cleanup loop
2026-02-26fix: ignore peeled tag entries (^{}) in state event ref parsingDanConwayDev
State events (kind 30618) can include refs/tags/<name>^{} entries which are git's notation for the dereferenced commit behind an annotated tag. These are not real git refs and are never sent as part of a push. extract_refs_from_state and RepositoryState::from_event were treating them as real refs, causing can_satisfy_state to reject valid annotated tag pushes: the would-be state after the push lacked the spurious ^{} entry, so the exact-equality check always failed.
2026-02-26send auth rejection reason to git client via ERR pkt-lineDanConwayDev
Previously push auth failures returned HTTP 403 which git clients display as a generic transport error. Now they return HTTP 200 with an ERR pkt-line containing the rejection reason (e.g. 'authorisation failed: No state events in purgatory'), which git displays directly. Remove GitError::Unauthorized as it is no longer used. GitError variants now represent only transport/infrastructure failures; app-level rejections use ERR pkt-line responses.
2026-02-26release: v1.0.0v1.0.0DanConwayDev
2026-02-25docs: remove comparison doc and update architecture to reflect announcement ↵DanConwayDev
purgatory
2026-02-25drop nostr-db backend support, keep only lmdb and memoryDanConwayDev
2026-02-25show probe help when invoked with no argumentsDanConwayDev
2026-02-25update README to document probe subcommand with checks, flags, and timeout ↵DanConwayDev
behaviour
2026-02-25make read-only the default probe mode; add --create-repo to opt into write pathDanConwayDev
2026-02-25show software and version in nip11_fetch detailDanConwayDev
2026-02-25diagnose overall timeout: name culprit check if it consumed >50% of budget, ↵DanConwayDev
else report cumulative slowness
2026-02-25report partial results on overall timeout: completed checks pass/fail, ↵DanConwayDev
timed-out step marked, remaining skipped
2026-02-25add overall probe timeout of min(20s, watch_interval) to prevent overlapping ↵DanConwayDev
runs
2026-02-25silence all library logs for probe subcommand in both human and JSON modesDanConwayDev
2026-02-25suppress [Run N] header and redirect logs to stderr in JSON mode for clean ↵DanConwayDev
pipe-friendly output
2026-02-25use compact single-line JSON output for machine consumption and --watch log ↵DanConwayDev
piping
2026-02-25rename find_announcement to serves_latest_announcement and drop redundant ↵DanConwayDev
detail from git_fetch_refs
2026-02-25hide read-only mode skips from human and JSON probe outputDanConwayDev
2026-02-25fix git_refs_match_state in read-only mode to fetch state events from relayDanConwayDev
In read-only mode, fetch all served kind:30618 state events for the repo by #d tag. The relay already validates authorization (including recursive maintainer chains), so any served state event is authoritative. Derive expected refs by taking the latest-timestamp state event per ref across all served events, then compare against git info/refs output.
2026-02-25add probe subcommand for end-to-end relay health checksDanConwayDev
Implements grasp-audit probe with full write path (publish events, poll for repo init, push, verify refs match state) and read-only fallback (find existing announcement, fetch refs). Supports --nsec for whitelisted relays, --json output, and --watch for continuous monitoring.
2026-02-24fix grasp-audit test isolation to prevent cross-spec relay state corruptionDanConwayDev
Add Purgatory-prefixed fixture variants (PurgatoryValidRepoSent, PurgatoryOwnerStateDataPushed) that create independent repos never shared with the main fixture chain. Purgatory tests that mutate relay state (replacement announcements, new state events, deletions) now use these isolated fixtures so they cannot corrupt the repo that push-authorization tests depend on. Run purgatory tests before push-auth in the full suite, since push-auth sends new replaceable state events (kind 30618) for the shared repo_id that would displace the original served state event.
2026-02-24wire PurgatoryTests into CLI runner so purgatory tests are actually runDanConwayDev
2026-02-24add background job to clean up grasp-audit test events and git reposDanConwayDev
Spawns a tokio task that runs every 30 minutes and removes all events tagged 'grasp-audit-test-event' older than 2 hours from the LMDB database, along with their associated bare git repositories on disk.
2026-02-24update grasp-audit README port examples and remove stale TAG_MIGRATION docDanConwayDev
2026-02-24remove unused dependencies from ngit-grasp and grasp-auditDanConwayDev
2026-02-24remove dead code: get_authorization_from_db and ↵DanConwayDev
collect_all_authorized_maintainers Both were pub functions with no callers. Clippy doesn't flag dead pub items because the compiler treats them as potentially used by external crates - only private items trigger the dead_code lint.
2026-02-24rename: fetch_repository_data -> ↵DanConwayDev
fetch_repository_data_{excluding,with}_purgatory The old name was ambiguous - it wasn't clear whether purgatory was included or not. The two variants are now explicitly named: - fetch_repository_data_excluding_purgatory: DB only - fetch_repository_data_with_purgatory: DB + purgatory overlay SyncContext trait method also renamed to fetch_repository_data_with_purgatory to match the free function it delegates to.
2026-02-24clarify DB-only count in fetch_repository_data log messageDanConwayDev
2026-02-24Fix purgatory announcement not promoted when OIDs arrive via cross-owner ↵DanConwayDev
state event copy When git data is fetched into owner A's repo and a state event for owner B is released from purgatory (copying OIDs from A's repo to B's repo via process_state_with_git_data), owner B's purgatory announcement was never promoted. process_purgatory_announcements only promotes the announcement for the owner derived from source_repo_path (owner A), so owner B's announcement stayed in purgatory with its 30-minute expiry timer running. 30 minutes later the cleanup task would soft-expire owner B's entry, deleting the bare repository even though the announcement had been effectively satisfied. Fix: after a state event is successfully saved to the database, iterate over all announcements in db_repo_data and promote any purgatory announcement for owners whose repos received OIDs via the copy (i.e. repos other than source_repo_path).
2026-02-24fix migration scripts for non-interactive SSH and bash 5.3DanConwayDev
01-fetch-events.sh: nak buffers output when stdout is not a TTY, causing it to hang silently in non-interactive SSH sessions. Wrap with 'script' to provide a pseudo-TTY, then strip the injected carriage returns and connection banner line from the output. 40-classify-actions.sh: bash 5.3 treats ${#assoc[@]} and array iteration as unbound variable errors under set -u when arrays are empty. Replace ${#assoc[@]} with explicit counters and guard array iterations with set +u/set -u.
2026-02-23feat: announcement purgatoryDanConwayDev
Extends purgatory to hold repository announcements until git data arrives, preventing empty repositories from being served to clients. When an announcement is received, a bare repo is created immediately and the announcement is held in purgatory. It is only promoted and served once a git push confirms real content exists. If no push arrives before expiry, the bare repo is deleted and the announcement is silently discarded. Key behaviours: - Soft expiry: announcements are hidden from clients but kept alive while git pushes are in progress, reviving on successful push - Expiry is extended when a matching state event or git push is observed - NIP-09 deletion events remove announcements from purgatory - Purgatory state (announcements, state events, PR events, expired set) is persisted to disk on graceful shutdown and restored on startup, with elapsed downtime subtracted from expiry deadlines - Purgatory announcements drive StateOnly sync in the sync system so state events are fetched from listed relays before promotion - SyncLevel added to RepoSyncIndex to distinguish purgatory repos (StateOnly) from promoted repos (Full L2+L3 sync)
2026-02-23Merge master into 3ca0-announcements-purgatoryDanConwayDev
2026-02-23persist and restore announcement events across graceful restartsDanConwayDev
Extends purgatory persistence to include announcement purgatory entries. On graceful shutdown, non-soft-expired announcements are serialised to purgatory-state.json alongside state/PR/expired events; on startup they are restored, skipping any entry whose bare repo path no longer exists. Updates purgatory-design.md to reflect that purgatory persists through graceful shutdown and documents the new PurgatoryState disk format. Adds create_announcement_event helper to purgatory_helpers and three new integration tests in purgatory_persistence covering the full save/restore cycle, missing-repo skip, and the combined roundtrip with all entry types.
2026-02-23docs: update purgatory docs to reflect announcements purgatory implementationDanConwayDev
Remove the pre-implementation planning docs (announcements-purgatory-design.md and announcements-purgatory-implementation.md) now that the feature is built. Update the three living docs to reflect what was actually implemented: - purgatory-design.md: expanded to cover all three purgatory stores (announcement, state, PR), including AnnouncementPurgatoryEntry structure, two-phase soft expiry lifecycle, expiry extension triggers, promotion flow, and updated integration points and file structure - grasp-02-proactive-sync.md: added SyncLevel enum (Full/StateOnly) to RepoSyncNeeds, documented the purgatory announcement sync timer as the registration path for purgatory announcements, updated filter building to describe build_sync_level_aware_filters() and StateOnly behaviour - grasp-02-proactive-sync-purgatory-git-data.md: expanded to cover announcement purgatory as a third entry type, added Timeline E showing soft-expiry and revival, replaced the single expiry section with separate hard-expiry (state/PR) and two-phase soft-expiry (announcements) sections with full justification for the 24-hour extended retention window
2026-02-23feat: handle deletion of PR/PR-update events from purgatoryDanConwayDev
Kind 5 deletion events referencing a PR or PR-update event by e-tag now remove the matching purgatory entry, provided the deletion author matches the PR event author. Placeholders (git data arrived before the event) are not removed since they have no author to verify against. PR purgatory is keyed by event ID hex so this is an O(1) lookup, checked before the O(n) announcement and state event scans.
2026-02-23fix: rewrite deletion integration tests to avoid shared-state side effectsDanConwayDev
The previous tests deleted purgatory announcements (kind 30617) and checked for bare-repo absence via git ls-remote, which would corrupt shared-mode test state by destroying repos other tests depend on. New approach tests deletion of purgatory state events (kind 30618) instead: - e-tag test: promotes a repo, creates a unique commit locally, submits a state event pointing to it (enters purgatory), deletes the state event by event ID, then verifies git push of that commit is rejected. - a-tag coordinate test: promotes a repo, generates a fresh maintainer keypair, sends a replacement announcement adding that maintainer, submits a state event signed by the new maintainer (enters purgatory), deletes by coordinate 30618:<new_maintainer_pubkey>:<identifier>, then verifies git push is rejected. Also extends DeletionPolicy to handle kind 30618 state events in purgatory for both e-tag (event ID) and a-tag (coordinate) deletion paths.
2026-02-23feat: remove purgatory announcements on NIP-09 deletion eventsDanConwayDev
Kind 5 deletion events signed by the announcement author now evict the corresponding purgatory entry and delete the bare repository from disk. Both NIP-09 reference styles are supported: - e tag (event ID): matches the purgatory entry whose event ID equals the tag value - a tag (coordinate 30617:<pubkey>:<identifier>): matches by coordinate, only removes entries with created_at <= deletion event created_at per NIP-09 spec Author-only enforcement: coordinate pubkey and e-tag owner must match the deletion event pubkey; third-party deletion attempts are silently ignored. Includes 6 unit tests and 2 integration tests (event ID and coordinate paths).
2026-02-23fix: only soft-expire announcement when bare repo deletion succeedsDanConwayDev
If remove_dir_all fails, leave the entry untouched so the next cleanup cycle retries the deletion automatically. Previously a failed deletion would still set soft_expired=true and extend the expiry, meaning the bare repo would never be retried.
2026-02-23feat: extend purgatory announcement expiry during git push authorizationDanConwayDev
Per design doc decision #4: when git auth finds a matching state event in purgatory that authorizes a push, extend the announcement's expiry. The repo is actively receiving git data so the announcement should not expire prematurely. Also triggers revival of soft-expired announcements.
2026-02-23feat: extend purgatory announcement expiry when state event arrivesDanConwayDev
Per design doc decision #4: state event arrival resets the 30-minute protocol timer for purgatory announcements. This prevents premature expiry during slow sync operations where the repo is actively receiving metadata but git data hasn't arrived yet. Extends expiry for all owners whose announcement authorized the state event, and triggers revival if the announcement was soft-expired.
2026-02-23feat: implement soft expiry and revival for purgatory announcementsDanConwayDev
Two-phase expiry for announcement purgatory entries: - Phase 1 (initial 30min timeout): delete bare repo, set soft_expired=true, extend expiry by 24h so the event is retained for potential revival - Phase 2 (24h extended timeout): fully remove from purgatory Revival: extend_announcement_expiry() now recreates the bare git repo when called on a soft-expired entry (triggered by state event or git auth), clearing soft_expired and resetting the expiry window.
2026-02-23refactor: replace inline purgatory sync registration with timer-only approachDanConwayDev
Remove the redundant inline kind-30617 registration block from the sync event loop and the three is_generic/recompute_new_sync_filters_for_relay calls from confirm_batch error paths. The purgatory announcement sync timer (run_purgatory_announcement_sync) is now the sole registration path. Consolidate NGIT_SYNC_BATCH_WINDOW_MS and NGIT_PURGATORY_SYNC_INTERVAL_MS into a single NGIT_TEST=1 flag that sets both timers to 200ms, replacing two ad-hoc env vars with one reusable test-mode flag.
2026-02-23fix: promote purgatory announcements after git sync copy pathDanConwayDev
When a state event arrives and the required commits already exist in another maintainer's repo on the same relay, process_state_with_git_data copies the OIDs across and aligns refs — but never called process_purgatory_announcements for the target repos. Any announcement waiting in purgatory for that repo stayed there indefinitely. Fix: after process_state_with_git_data, call process_newly_available_git_data for each target repo (those that received copied OIDs) so purgatory announcements are promoted immediately.
2026-02-23fix: re-process hot-cache maintainer announcements after git push promotionDanConwayDev
When an owner announcement is promoted from purgatory via a git push, any maintainer announcements sitting in the rejected_events_index hot cache were never re-processed. The invalidate_and_get call only existed in SyncManager::process_event_static (the nostr sync path); the git push promotion path (http -> handlers -> git::sync) had no access to the rejected_events_index at all. Thread rejected_events_index and write_policy through the git push path: - process_purgatory_announcements: after saving the promoted announcement, parse its maintainers tag and call invalidate_and_get() for each, then re-process any returned hot-cache events via admit_event + save - process_newly_available_git_data: accept optional write_policy and rejected_events_index, pass them through to process_purgatory_announcements - handle_receive_pack: accept Arc<Nip34WritePolicy> and Arc<RejectedEventsIndex>, pass them to process_newly_available_git_data - HttpService / run_server: carry the two new fields, clone into each handle_receive_pack call - main.rs: obtain rejected_events_index from sync_manager before moving it into its task; wrap write_policy in Arc for the HTTP server - RealSyncContext::process_newly_available_git_data: pass None for both new params (purgatory sync path already handles this via SyncManager::process_event_static) Also rewrite the maintainer_reprocessing integration tests to correctly exercise the hot-cache path now that announcements require git data before being released from purgatory: - Start relay_b with relay_a as bootstrap so its SyncManager syncs maintainer announcements via negentropy before the owner git push - Use push_unique_git_data_to_relay (new helper) to give each maintainer a distinct commit hash, preventing git from skipping pack transfer - Make wait_for_event_on_relay poll in a retry loop so transient timing gaps between DB write and query do not cause false negatives
2026-02-23remove recursive relay discovery testDanConwayDev
Recursive discovery relied on announcement events being gossiped across relays regardless of whether they listed the service. Now that announcements enter purgatory until state event and git data arrive, cross-relay discovery cannot be triggered by a synced announcement alone, making the three-relay recursive discovery scenario impossible.
2026-02-23test: update sync tests to set up git data for purgatory flowDanConwayDev
All sync tests now create a local git repo, send announcement + state event to the source relay, and push git data to release both from purgatory before the syncing relay starts bootstrap sync.
2026-02-18test: update run_sync_test to use push_to_relay for purgatory flowDanConwayDev
Previously run_sync_test used a SmartGitServer external to the relay, but never pushed to the source relay itself. With the announcement purgatory feature, announcements stay in purgatory until git data arrives. By using push_to_relay to the source relay, both the announcement and state event are released from purgatory before the syncing relay starts, allowing the announcement to be synced.
2026-02-18fix: use unique commit instead of deterministic Owner variant for ↵DanConwayDev
wrong-commit PR tests PRWrongCommitPushedBeforeEvent and test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected were calling create_deterministic_commit_with_variant(CommitVariant::Owner) on a clone that already had test.txt with 'Initial commit\n' content from OwnerStateDataPushed. Writing identical content staged nothing so git commit failed silently. Now that ValidRepoServed always depends on OwnerStateDataPushed (git data pushed), the clone is never empty - use create_commit (unique file) instead since the wrong commit only needs to differ from PR_TEST_COMMIT_HASH, not be deterministic.
2026-02-18extract OwnerRepoState fixture to make dependency chain explicitDanConwayDev
OwnerStateDataPushed was secretly building and sending the state event internally, with no corresponding fixture in the chain. Add OwnerRepoState as the explicit 'state event sent, sitting in purgatory' step so the dependency chain reads: ValidRepoSent -> OwnerRepoState -> OwnerStateDataPushed -> ValidRepoServed. OwnerStateDataPushed now reads the state event from the OwnerRepoState cache rather than rebuilding it, and only owns the git push + purgatory release.
2026-02-18fix: restructure PR clone tag test to use bootstrap relay instead of ↵DanConwayDev
user-submitted purgatory announcement The test was failing because submitting an announcement directly to syncing_relay (user-submitted, no bootstrap) leaves the announcement in purgatory with no mechanism to trigger relay discovery - there are no existing sync connections whose batch EOSE would fire recompute_new_sync_filters_for_relay. Fix: start syncing_relay with source_grasp as bootstrap. The promoted announcement syncs via L1 generic filter → purgatory (no local git data) → StateOnly subscription → state event → purgatory sync fetches git data → announcement promoted → SelfSubscriber upgrades to Full → connects to mock_relay → PR event synced and promoted. The test's primary purpose (PR event partial OID aggregation from multiple clone URL sources) is fully preserved.
2026-02-18fix: replace repo_sync_index wiring with purgatory announcement sync timerDanConwayDev
Instead of threading repo_sync_index through PolicyContext/builder.rs/main.rs to handle user-submitted purgatory announcements, add a simple background timer (run_purgatory_announcement_sync, every 5s) that scans the purgatory for announcement entries and registers them in repo_sync_index as StateOnly. This is simpler and covers both flows: - Sync-path announcements: inline registration still happens during event processing (sync/mod.rs:1839+), timer provides a safety net - User-submitted announcements: SelfSubscriber never sees them (rejected from DB), timer is the primary registration path The timer calls sync_purgatory_announcements_to_index() which: 1. Snapshots purgatory via new announcements_for_sync() public method 2. Or_inserts StateOnly entries (never downgrades Full entries) 3. Detects newly added relay URLs and calls handle_new_sync_filters to connect and subscribe - fixing the failing test that expected relay discovery from a user-submitted purgatory announcement Removes: repo_sync_index field from PolicyContext, set/get_repo_sync_index methods, set_repo_sync_index on Nip34WritePolicy, wiring in main.rs, and the inline AcceptPurgatory registration block in builder.rs.
2026-02-18refactor: move archive_read_only test to archive_grasp_services and remove ↵DanConwayDev
redundant test
2026-02-18fix: simplify purgatory sync - fix SelfSubscriber sync_level upgrade and ↵DanConwayDev
negentropy fallback Three targeted fixes for purgatory announcement sync: 1. SelfSubscriber sync_level upgrade: After or_insert_with in process_batch, always set entry.sync_level = SyncLevel::Full so that when a promoted announcement is broadcast via notify_event and SelfSubscriber receives it, an existing StateOnly entry gets upgraded to Full and PR event subscriptions are triggered immediately (not delayed up to 24h). 2. Negentropy fallback filter split: In handle_eose, when falling back from negentropy to REQ+EOSE, split batch_repos by SyncLevel and call build_sync_level_aware_filters instead of build_layer2_and_layer3_filters. Prevents StateOnly (purgatory) repos from getting Layer 2 #a/#A/#q filters prematurely, which caused nostr-sdk client deduplication to permanently drop PR events after orphan rejection. 3. Recompute sync filters after announcement batch EOSE: Add recompute_new_sync_filters_for_relay calls at all three batch-completion paths in handle_eose for generic filter (announcement) batches. This triggers state-only subscriptions for any purgatory repos registered during that batch, fixing the 24h delay before state event sync starts. 4. User-submitted purgatory announcements: Add repo_sync_index field to PolicyContext with setter/getter, wire in main.rs after SyncManager creation, and register in AcceptPurgatory handler so user-submitted announcements get StateOnly sync started immediately. 5. Update archive tests: test_archive_without_state_events_does_not_sync_git updated to reflect that StateOnly subscription now proactively fetches state events from source relays. test_archive_read_only_creates_bare_repo un-ignored as it now works end-to-end.
2026-02-18Revert "fix: use sync-level-aware filters in negentropy fallback to prevent ↵DanConwayDev
premature PR event delivery" This reverts commit 806936e7d1aab5dfd0c2ad6b98a115122dc1785c.
2026-02-18Revert "feat: upgrade repo to Full sync and trigger PR event subscription ↵DanConwayDev
after announcement promotion" This reverts commit d76003b629a4a03dba23a8a1c41da6e4ac4c30cf.
2026-02-18test: rewrite PR sync tests to reflect purgatory-first announcement flowDanConwayDev
The tests now correctly reflect the actual purgatory behavior: 1. Announcement goes to purgatory (StateOnly) - not immediately accepted 2. State event goes to purgatory 3. Git push promotes announcement to Full and releases state event 4. PR event is sent AFTER announcement promotion (accepted since repo is Full) 5. PR commit push releases PR event from purgatory This matches the design: announcements require git data validation before being promoted to the database, which means PR events can only be accepted for repos with promoted announcements. Also routes relay stdout to /tmp/relay-{port}.log for easier debugging.
2026-02-18feat: upgrade repo to Full sync and trigger PR event subscription after ↵DanConwayDev
announcement promotion When git data arrives for a purgatory announcement and promotes it to the database, the relay now: 1. Upgrades the announcement's sync level in RepoSyncIndex from StateOnly to Full (git/sync.rs: process_purgatory_announcements) 2. Sends AddFilters actions to SyncManager for all connected relays, using Full sync filters (Layer 2 #a/#A/#q) to subscribe to PR events (purgatory/sync/context.rs: RealSyncContext.process_newly_available_git_data) 3. For user-submitted purgatory announcements, registers the repo in RepoSyncIndex with StateOnly level and sends AddFilters to SyncManager so it discovers and connects to relays listed in the announcement tags (nostr/builder.rs: handle_announcement AcceptPurgatory path) The RealSyncContext now accepts optional repo_sync_index and sync_action_tx parameters. main.rs wires these up from SyncManager. PolicyContext gains repo_sync_index and sync_action_tx fields for the write policy path.
2026-02-18fix: use sync-level-aware filters in negentropy fallback to prevent ↵DanConwayDev
premature PR event delivery StateOnly repos in a pending batch had their repo IDs included in the negentropy REQ+EOSE fallback, which called build_layer2_and_layer3_filters. This generated #a/#A/#q tag filters for repos whose announcements were still in purgatory (not yet promoted to the database). When the remote relay responded with PR events matching those filters, the write policy correctly rejected them as 'orphan' (no accepted repo in DB yet). However, nostr-sdk's client-level deduplication then silently dropped the same event on all subsequent deliveries, making it permanently unavailable even after the announcement was promoted. Fix: split batch_repos into full vs state-only by consulting repo_sync_index at fallback time, then call build_sync_level_aware_filters which only generates #a/#A/#q filters for Full repos. StateOnly repos only get the kind 30618 + #d filter they were originally subscribed with.
2026-02-18fix: update NIP-77 test to use kind 10317 events accepted without promoted repoDanConwayDev
Kind 30617 announcements now go to purgatory (not DB) until git data arrives. Kind 1621 issues referencing purgatory-only repos are rejected. Use kind 10317 (GitUserGraspList) from two keypairs instead - these are unconditionally accepted and stored in DB, making them visible to negentropy sync.
2026-02-18fix: preserve state events when another owner's announcement remains in ↵DanConwayDev
purgatory remove_purgatory_announcement() was unconditionally wiping all state events for an identifier when one owner's announcement was evicted. State events are keyed by identifier alone, so this incorrectly discarded state events belonging to a different owner's repository sharing the same identifier string. Now only removes state events if no other owner's announcement remains in purgatory for that identifier.
2026-02-18fix: only evict purgatory entry when incoming rejected announcement is newerDanConwayDev
An older rejected announcement (e.g. a relay replay of a superseded event) was incorrectly evicting a newer purgatory entry for the same pubkey+identifier. Now only evict when the incoming event's created_at is strictly greater than the stored entry's created_at.
2026-02-18fix: break circular deadlock in sync loop by including purgatory in URL lookupDanConwayDev
The sync loop calls fetch_repository_data() to get clone URLs so it knows where to fetch git data from. Previously this only queried the database, which means an announcement still in purgatory (no git data yet) would return no clone URLs, so the sync loop could never fetch the git data needed to promote the announcement - a circular deadlock. Fix by switching to fetch_repository_data_with_purgatory() which combines database announcements with purgatory announcements. Update the trait method's doc comment to document this behaviour. The mock implementation in tests is unaffected since it returns pre-configured data rather than delegating to either function.
2026-02-18fix: handle announcement replacement when original is still in purgatoryDanConwayDev
Previously, has_active_announcement() only queried the database, so when a newer announcement arrived for the same (pubkey, identifier) while the original was still in purgatory, it was incorrectly routed as a brand-new announcement (AcceptPurgatory) rather than replacing the existing entry. This change splits the logic into two cases: - If the existing entry is in the database: return Accept (replacement) as before - If the existing entry is only in purgatory: replace the purgatory entry via add_announcement() (which overwrites by key) and extend expiries for both the announcement and any waiting state events, then return Accept - If the owner sends a Reject-classified announcement (service removed) but has a purgatory entry: clear the purgatory entry, delete the bare repo, and remove any waiting state events before rejecting Also add an explicit comment to find_accepted_repository() in related.rs clarifying that it intentionally only checks the database. Related events should only be accepted after the repository announcement has been promoted (validated via git data) - this is correct behaviour, not a missing check.
2026-02-18fix: check purgatory in maintainer announcement lookupDanConwayDev
is_maintainer_in_any_announcement only queried the database, missing announcements still in purgatory. A maintainer's announcement (which lists the recursive maintainer) may arrive and enter purgatory before the recursive maintainer's announcement does, causing the maintainer exception check to return false and reject the recursive maintainer's announcement.
2026-02-17docs: clarify why fetch_repository_data excludes purgatoryDanConwayDev
Add comments explaining that PR event processing (both incoming and purgatory) should only use database announcements, not purgatory ones. This is intentional because: - Incoming PR events should only be accepted for validated announcements - Purgatory PR events should only be released when announcement is promoted - This prevents accepting PR events for announcements that fail validation Differs from state event processing which uses fetch_repository_data_with_purgatory because state events check authorization without releasing from purgatory.
2026-02-17fix: include purgatory announcements in state event authorizationDanConwayDev
When processing state events from purgatory, we need to check authorization against announcements that may still be in purgatory (not yet promoted to the database). Previously, process_purgatory_state_events() used fetch_repository_data() which only queries the database. This caused authorization failures when: 1. Git data arrives 2. Announcement is promoted from purgatory to database 3. State events are processed from purgatory 4. But db_repo_data was fetched BEFORE the announcement promotion Now uses fetch_repository_data_with_purgatory() to include both database and purgatory announcements, ensuring authorization works correctly regardless of promotion timing.
2026-02-13feat: add SyncLevel to sync system for purgatory announcement state-only syncDanConwayDev
Purgatory announcements need state events (kind 30618) synced from external relays, but not full L2/L3 events (patches, issues, PRs) which would be rejected anyway. This implements the SyncLevel concept from the design doc (decision #6): - Add SyncLevel enum (Full vs StateOnly) to RepoSyncNeeds - When announcement enters purgatory during sync, register in RepoSyncIndex with SyncLevel::StateOnly - Add build_sync_level_aware_filters() that partitions repos by level: StateOnly repos only get state event filters (kind 30618) - Update derive_relay_targets to track state_only_repos separately - Update compute_actions to handle both repo sets - SelfSubscriber always uses SyncLevel::Full (promoted repos)
2026-02-13fix: revert wrong sync approach for purgatory announcementsDanConwayDev
The partial fix treating ProcessResult::Purgatory as confirmed in pending_sync_index would trigger full L2/L3 sync for purgatory announcements. Per design (decision #6), purgatory announcements should only sync state events via SyncLevel::StateOnly (not yet implemented). Ignore test_archive_read_only_creates_bare_repo until SyncLevel is implemented in Phase 3.
2026-02-13feat: implement announcement purgatory core (breaks archive sync test)DanConwayDev
Route new announcements to purgatory instead of accepting immediately. Announcements are promoted to the database when git data arrives, ensuring we only serve announcements for repos with actual content. Implemented: - AnnouncementPurgatoryEntry type and DashMap store - Route new announcements to purgatory (replacement announcements skip) - Promote announcements on git data arrival (process_purgatory_announcements) - Authorization checks purgatory announcements (fetch_repository_data_with_purgatory) - State policy uses purgatory announcements for maintainer validation - Cleanup task handles announcement expiry - Updated count()/cleanup() to 3-tuples Known broken: - test_archive_read_only_creates_bare_repo fails: sync module does not treat purgatory announcements as confirmed repos, so per-repo sync (state events, PRs) is never triggered for purgatory announcements - Announcement persistence (save/restore) not implemented - SyncLevel (StateOnly vs Full) not implemented - Soft expiry two-phase not implemented - Expiry extension on state event / git auth not wired up
2026-02-13fix: use ValidRepoServed for events that tag repo eventsDanConwayDev
PR events, issues, and comments need a queryable repo announcement to reference. Changed PREvent and PREventGenerated fixtures and related tests to depend on ValidRepoServed instead of ValidRepoSent. This ensures tests will fail correctly when announcement purgatory is implemented - events tagging a repo should require that repo to be served (not in purgatory).
2026-02-13refactor(grasp-audit): clarify PR purgatory test names and intentDanConwayDev
- Remove redundant test_pr_event_remains_in_purgatory_until_git_data - Rename test_pr_event_git_push_accepted -> test_pr_event_in_purgatory_git_push_accepted - Add PASS/FAIL meaning to each test's documentation - Note black-box testing limitation for purgatory detection
2026-02-13test: add PR purgatory tests with PREvent2 fixturesDanConwayDev
Add new fixtures for testing PR purgatory mechanism: - PREvent2Generated: PR event with different commit hash - PREvent2Sent: PR event sent to relay (enters purgatory) - PREvent2GitDataPushed: Git data pushed after event sent - PREvent2Served: Full fixture with event served Add PRTestCommit2 variant for second PR test commit. Update purgatory tests to use new fixtures for proper PR purgatory testing.
2026-02-13fix: add trailing newlines to deterministic commit contentDanConwayDev
The CommitVariant::file_content() methods were returning strings without trailing newlines, but the expected hash constants were calculated with trailing newlines. This caused hash mismatches in tests. Updated all hash constants to match the actual commit hashes produced with trailing newlines in the file content.
2026-02-12chore: fix clippy warningsDanConwayDev
- Derive Default for config structs instead of manual impl - Fix doc comment formatting in ArchiveConfig::matches - Collapse nested if statement in validate_announcement - Allow too_many_arguments for SyncManager::new
2026-02-12feat(grasp-audit): add explicit purgatory testsDanConwayDev
Add PurgatoryTests module with tests for GRASP-01 purgatory behavior: - Announcement purgatory tests (tolerant of unimplemented feature) - State event purgatory tests (already implemented) - PR purgatory tests (tolerant of unimplemented feature) Tests pass regardless of purgatory implementation status, enabling development without breaking the test suite. When features are implemented, tests will verify correct purgatory behavior.
2026-02-12refactor(grasp-audit): split ValidRepo into Sent/Served, add tolerant purgatoryDanConwayDev
- Rename ValidRepo to ValidRepoSent (announcement sent, may be in purgatory) - Add ValidRepoServed (announcement queryable after git data pushed) - Add send_event_and_note_purgatory() for tolerant purgatory detection - Update fixtures to use tolerant method instead of strict assertion - Update event_acceptance_policy tests to use ValidRepoServed This enables tests to pass regardless of purgatory implementation status while still having explicit purgatory tests that verify the behavior.
2026-02-12refactor(grasp-audit): introduce SpecRef enum for type-safe spec referencesDanConwayDev
Replace string-based spec references with typed SpecRef enum for compile-time validation and better IDE support. TestResult::new() now accepts SpecRef enum plus a requirement description string for test-specific context.
2026-02-12fix: update doctest to use valid FixtureKind::RepoState variantDanConwayDev
2026-02-12fix: use consistent git identity for PR test commit hashDanConwayDev
The PR_TEST_COMMIT_HASH constant was incorrect because the discovery test used a different git identity (pr-test@example.com) than the actual create_pr_test_commit function (test@grasp-audit.local from fixtures.rs). This caused the same commit content to produce different hashes due to different author/committer info being embedded in the commit object. Fixed by updating the discovery test to use the same git identity as clone_repo() in fixtures.rs, ensuring consistent commit hashes.
2026-02-12docs: annocunment purgatory clarify soft expiry rationaleDanConwayDev
now we have added announcement purgatory to the protocol spec
2026-02-05docs: complete high-level announcements purgatory designDanConwayDev
- Integrate sync-only-state-events decision (SyncLevel concept) - Add authorization must check purgatory decision - Add soft expiry design (delete repo, retain event for 24h) - Add purgatory lifecycle diagram - Create separate implementation details document - Remove inline questions (now resolved)
2026-02-05add notes to announcment purgatory designDanConwayDev
2026-02-03Handle SIGTERM for graceful shutdown with systemdDanConwayDev
Listen for both SIGINT (Ctrl+C) and SIGTERM (systemd) signals to ensure graceful shutdown cleanup runs when stopping the service via systemd. Previously, only SIGINT was handled, causing purgatory state and rejected events cache to be lost on every systemd restart. Now both signals trigger the cleanup code that saves state files and removes placeholder refs. Fixes issue 0f73
2026-02-03Add hex ID, kind, and pubkey to orphan event rejection logDanConwayDev
2026-02-03Add error logging to all git handler IO operationsDanConwayDev
Previously, some IO errors in git handlers were logged while others were not, leading to inconsistent observability. Additionally, the HTTP layer logged all git errors redundantly, adding no useful context beyond what was already logged at the source. Changes: - Add error logging to all previously unlogged IO operations in handle_upload_pack and handle_receive_pack (stdin writes, stdout/stderr reads, process waits) - Remove redundant error logging at HTTP layer since all errors are now logged at their source with full context - Ensures consistent error-level logging for all git subprocess failures This provides complete observability of git operations while eliminating duplicate log entries that don't add value.
2026-02-03Reduce log noise: change per-ref updates to DEBUG levelDanConwayDev
Only the final summary 'Aligned repository with state' remains at INFO level, showing the total count of refs_created/refs_updated/refs_deleted.
2026-02-03feat: add diagnostic logging for partial state event matchesDanConwayDev
Improves observability when pushes are rejected due to state events that only partially match the pushed refs. Previously, logs only showed 'No state event found' even when state events existed but didn't match. Changes: - Add diagnose_state_mismatch() to explain why state events don't match - Log specific reasons: missing refs, wrong SHAs, or extra refs - Update rejection message to 'No matching state event found' (more accurate) - Add 4 unit tests for diagnostic function Example diagnostic output: WARN State event abc123 from authorized author doesn't match push: refs/heads/main missing (state declares 9cc3d93b) This addresses the issue where a push with only refs/heads/test was rejected because the state event also declared refs/heads/main, but logs didn't explain why the match failed.
2026-02-03fix: accept no-op pushes where old_oid == new_oidDanConwayDev
Fixes race condition where user's push becomes no-op after state event is applied between fetch and push. Now accepts these as successful no-ops, matching Git's 'Everything up-to-date' behavior. - Add early detection in get_state_authorization_for_specific_owner_repo - Return success for all-noop pushes without requiring purgatory event - Document behavior in inline-authorization.md
2026-02-03Merge relay.ngit.dev migration: bug fixes and migration toolingDanConwayDev
This merge includes critical bug fixes and comprehensive migration tooling developed during the relay.ngit.dev migration effort. Bug Fixes: - Fix git protocol error handling to return HTTP 200 with ERR pkt-line - Fix naughty list false positives and DNS failure identification - Fix database query filters in load_existing_events (remove .since()) - Fix OID fetch tracking to distinguish 0 OIDs from successful fetches - Fix purgatory event source tracking for filtered expiry logging - Implement OID retry logic for 'not our ref' errors Migration Tools & Documentation: - Complete 5-phase migration analysis pipeline with orchestration script - Phase 1: Event fetching from source relay - Phase 2: Git sync verification - Phase 3: Categorization and relay comparison - Phase 4: Log extraction (parse failures, purgatory expiry) - Phase 5: Action classification for migration decisions - Comprehensive migration guide with lessons learned - Troubleshooting guide for permission and corruption issues Configuration: - Add NGIT_LOG_LEVEL configuration option - Update git throttle limits to 60/minute - Improve logging throughout for better observability
2026-02-03docs: archive relay.ngit.dev migration materials for referenceDanConwayDev
Move migration guide and scripts to docs/archive/2026-01-relay-ngit-dev-migration/ with clear warnings that these are reference-only materials from a specific migration context, not general-purpose tools. These materials document the relay.ngit.dev migration from ngit-relay to ngit-grasp in January 2026. The scripts were developed iteratively during the migration and are specific to that context. They are preserved for: - Historical reference - Context for production fixes in this branch - Inspiration for future migrations (not direct reuse) The migration uncovered critical bugs now fixed in this branch: - Git protocol error handling - Naughty list false positives - Purgatory event tracking - Sync startup issues - Configuration management
2026-01-28feat(purgatory): track event source for filtered expiry loggingDanConwayDev
Add EventSource enum (Direct/Sync) to purgatory entries to distinguish between user-submitted events and sync-fetched events. This enables: - WARN-level logging for direct submissions that expire (user should know) - DEBUG-level logging for sync-fetched expirations (expected behavior) - Source upgrade from Sync→Direct if user submits after sync - Expiry timer reset on source upgrade (fresh 30-min window for user) The source is included in [PURGATORY_EXPIRED] logs as source=direct or source=sync for easy filtering.