Repository navigation
(paths draft) - #467
(paths draft)#467chee wants to merge 81 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Literal doc-shaped segments inside a multi-segment directory key resolve; a pinned root followed by names is reported non-canonical and resolves live; a null folder entry is skipped; / falls back to rootFolderUrl when every binding is malformed; FolderStrategy stays as a deprecated alias. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Benchmarks: Patchwork with this PR against main
This run fails: the PR fails a check main passes.
every benchmark: the median of 3 runs of main and 3 runs of the PR, their range in brackets
|
✅ Patchwork glance readyhttps://patchwork-preview-467.patchwork.inkandswitch.comA peek at |
Records, per browser, whether the service worker can reach a module SharedWorker client, and what it learns about the sender on each reply path (controller postMessage, a transferred port, fetch clientId, and a worker-initiated port handoff), alongside a dedicated worker loaded from the same URL. Runs in CI; the results land in the job summary. Also records in the spec that automerge verifies change hashes on load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only a fetch() from the worker, checked by clientId → Client type and url, works in Chromium, Firefox and WebKit. Index answers go that way; requests can stay broadcast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce worker Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ir account The walk now records the last container it entered, and listFiles lists under it, so a manifest request for a file covers its whole package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A manifest answer settles only the request for the same path, so a page can't get the worker to answer a forged copy and switch the check off. - The worker probes /@/.index before posting, so a fetch that skips the service worker never carries file names to the origin server. - A tab declines paths under accounts it doesn't know yet, instead of answering 404 while it boots. - Listing a package reads file docs without waiting for peers, several at once, on a budget of its own, and a partial manifest isn't kept. - After a manifest times out, requests wait 250ms instead of 10s until the worker answers again. - Immutable legacy responses are cached under the request only, so a navigation no longer replaces the offline app shell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A harness runs the real service worker module in node against a fake global scope (self, clients, caches) with node's own BroadcastChannel and locks, and dispatches fetch and lifecycle events at it. Tabs are fake window clients running the real tab resolver. Covers the legacy navigation fix (an immutable response is kept under its own URL, not over the app shell's / and /index.html), the index channel refusing a page and answering the worker's probe, and activate keeping content stores. The first two fail against the service worker before a2299b3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A container's pinned step carries ~<listHash>, the SHA-256 of a canonical list of the files below it (bytes hash, type, heads read at). The list is content like any other, so a pinned URL is checked against itself: links no longer need heads to pin their files, the in-memory index and the authenticated channel go, and a cold start needs no document. No document ids in URLs or in the store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…list Hashing and the keyhive check move into filesystem; plugins' isKeyhiveDoc goes through it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oviders in walks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…esn't Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pinning, nested subs listed, sub links refused - A lookup inside a document that consumes no name, or more names than there are, is no child, so a buggy provider can't spin the walk. - Whether a document a path ends at is a container is the built-ins' call, and so is what listFiles counts as a file: a path pins and lists the same with or without a package's provider, and installing one doesn't change what an existing pinned URL means. - listFiles lists what's inside the values a package's provider hands out, as far as a walk can reach. - A link with path segments (a sub-document) isn't walked or listed as the whole document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ough Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ecks, worker isolation test - A name a provider spreads over several segments is written as the file list writes it, so each spelling pins to a tail its list covers. - A provider whose list isn't an array has no children. - The router drops a path= naming a different part of doc='s document. - resolve.ts no longer carries a sub branch nothing can reach. - A static test holds that the automerge worker never gets package providers. - Spec sections 2 and 6 say where package providers run and where drops land. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Concurrent package migrations can overwrite /packages bindings and detach a folder, leaving its edits unreachable through /packages or its sidebar link pointing to a different folder.
9 open issues
Make concurrent package-folder migrations converge · New Serialize proto filenames with a prototype-free map · New Watch resolved package targets for plugin updates · New Prevent one unavailable member from blocking package listing · New Wait for provider registration before routing paths · New Prevent duplicate package suffix allocation · New Revalidate document identity after asynchronous lookup · New Validate sub-document segments in path checks · New Preserve paths only for matching sub-documents · New
1 file not reviewed
pnpm-lock.yaml: Generated file
What changed in this PR
This draft adds account-rooted paths to Patchwork, connecting document navigation, package discovery, and verified content delivery through the filesystem and bootloader.
Changes:
- Adds live and pinned paths, namespace mounts, and extensible path providers.
- Introduces
/packagesdiscovery, account migration, package status, and path-aware routing. - Adds hash-verified content caching, automated coverage, benchmarks, and a browser worker-messaging spike.
| File | Description |
|---|---|
vitest.config.ts |
Includes bootloader and plugins tests. |
sites/bench/tests/content-hash.spec.ts |
Benchmarks content-hash verification costs. |
sites/bench/README.md |
Documents the hashing benchmark. |
pnpm-lock.yaml |
Records the filesystem’s base58 dependency. |
packages/e2e/tests/path-routing.spec.ts |
Tests path-only document navigation. |
packages/e2e/tests/content-store.spec.ts |
Tests rejection of forged cached content. |
packages/e2e/spikes/sw-sharedworker/spike.spec.mjs |
Records browser worker-messaging results. |
packages/e2e/spikes/sw-sharedworker/serve.mjs |
Serves the messaging spike. |
packages/e2e/spikes/sw-sharedworker/public/sw.js |
Probes worker communication and sender identity. |
packages/e2e/spikes/sw-sharedworker/public/shared.js |
Implements shared and dedicated worker probes. |
packages/e2e/spikes/sw-sharedworker/public/index.html |
Coordinates browser messaging probes. |
packages/e2e/spikes/sw-sharedworker/playwright.config.mjs |
Configures cross-browser spike execution. |
packages/e2e/spikes/sw-sharedworker/.gitignore |
Excludes generated spike results. |
docs/superpowers/plans/2026-10-28-paths-sidebar.md |
Plans sidebar path and package-status integration. |
docs/superpowers/plans/2026-10-28-paths-phase-4-packages.md |
Plans namespace-based package loading. |
docs/superpowers/plans/2026-10-28-paths-phase-3-routing.md |
Plans path-aware hash routing. |
core/plugins/vitest.config.ts |
Configures plugin unit tests. |
core/plugins/test/path-providers.test.ts |
Tests provider selection and validation. |
core/plugins/src/registry/types.ts |
Adds the path-provider registry type. |
core/plugins/src/path-providers.ts |
Loads datatype-specific path providers. |
core/plugins/src/keyhive.ts |
Reuses filesystem document-protection detection. |
core/plugins/src/index.ts |
Exports path-provider APIs. |
core/plugins/src/account.ts |
Adds namespace typing and deprecates module settings. |
core/plugins/package.json |
Enables the plugin test command. |
core/patchwork/test/router.test.ts |
Tests path routing and provider-backed subdocuments. |
core/patchwork/test/migrate-packages.test.ts |
Tests package migration and account initialization. |
core/patchwork/test/hash-params.test.ts |
Tests preservation of encoded paths. |
core/patchwork/src/types.ts |
Adds paths to document-opening options. |
core/patchwork/src/router.ts |
Resolves, preserves, and checks navigation paths. |
core/patchwork/src/migrate-packages.ts |
Migrates legacy package settings into a namespace folder. |
core/patchwork/src/index.ts |
Wires account registration and package-folder loading. |
core/patchwork/src/hash-params.ts |
Centralizes path-aware hash parsing and serialization. |
core/patchwork/src/createAccount.ts |
Creates namespace mounts and the packages tree link. |
core/filesystem/test/walk.test.ts |
Tests namespace traversal and path pinning. |
core/filesystem/test/resolve.test.ts |
Covers default-branch resolution. |
core/filesystem/test/providers.test.ts |
Tests built-in path providers. |
core/filesystem/test/paths.test.ts |
Tests path grammar and identifier hashing. |
core/filesystem/test/packages-folder.test.ts |
Tests package discovery and stable lookup URLs. |
core/filesystem/test/package-providers.test.ts |
Tests custom providers and subdocuments. |
core/filesystem/test/namespace-fixture.ts |
Supplies shared namespace fixtures. |
core/filesystem/test/files.test.ts |
Tests file reading and listing consistency. |
core/filesystem/test/file-list.test.ts |
Tests hashed lists and nested package files. |
core/filesystem/src/types.ts |
Defines namespace bindings and account shape. |
core/filesystem/src/resolve.ts |
Uses providers for legacy resolution and exposes node reads. |
core/filesystem/src/providers.ts |
Defines provider contracts and built-in implementations. |
core/filesystem/src/paths.ts |
Implements path parsing, formatting, and identifier hashing. |
core/filesystem/src/packages-folder.ts |
Watches package unions and resolves pinned import URLs. |
core/filesystem/src/module-watcher.ts |
Adds folder sources, lookup retries, and package status. |
core/filesystem/src/keyhive.ts |
Centralizes protected-document detection. |
core/filesystem/src/index.ts |
Exports namespace, path, and content APIs. |
core/filesystem/src/file-list.ts |
Builds canonical, hashed file manifests. |
core/filesystem/src/content-hash.ts |
Provides shared byte conversion and SHA-256 hashing. |
core/filesystem/package.json |
Adds base58 encoding support. |
core/elements/src/patchwork-view.ts |
Exposes paths without remounting tools. |
core/elements/src/index.ts |
Adds path attributes to element typings. |
core/elements/src/events.ts |
Carries paths in document-opening events. |
core/bootloader/vitest.config.ts |
Configures bootloader tests. |
core/bootloader/test/setup.ts |
Initializes Wasm modules for tests. |
core/bootloader/test/service-worker.test.ts |
Tests path serving, integrity checks, and cache lifecycle. |
core/bootloader/test/service-worker-harness.ts |
Provides an in-memory service-worker harness. |
core/bootloader/test/content-store.test.ts |
Tests hashing and content-store operations. |
core/bootloader/src/types.ts |
Extends handoff messages for paths and account registration. |
core/bootloader/src/tab-resolver.ts |
Registers accounts and enables tab-side path providers. |
core/bootloader/src/setup.ts |
Replays account registrations after worker restarts. |
core/bootloader/src/service-worker.ts |
Serves verified paths and manages content caches. |
core/bootloader/src/content-store.ts |
Adds per-account, hash-keyed storage and clearing. |
core/bootloader/src/automerge-protocol-handler.ts |
Registers accounts for worker-side path resolution. |
core/bootloader/package.json |
Adds the bootloader test command. |
.github/workflows/spike-sw-sharedworker.yml |
Runs and archives cross-browser messaging probes. |
.changeset/paths-routing.md |
Documents path-routing behavior. |
.changeset/paths-packages.md |
Documents package mounts, migration, and status APIs. |
.changeset/paths-grammar-providers.md |
Documents path grammar and provider API changes. |
.changeset/paths-content-store.md |
Documents verified content serving and caching. |
.changeset/path-provider-plugins.md |
Documents plugin providers and subdocument traversal. |
.changeset/open-document-path.md |
Documents path-aware document-opening APIs. |
Review effort: Balanced
| account.change((doc) => { | ||
| doc.namespace ??= {}; | ||
| doc.namespace["/packages"] = [{ url: folder.url }]; |
| const throughPackages = first?.kind === "name" && first.name === "packages"; | ||
| const isProtected = options.isProtected ?? isKeyhiveDocumentId; | ||
|
|
||
| const files: Record<string, FileEntry> = {}; |
| // A /packages package comes back as its pinned /@/ URL, imported over http. | ||
| if (!isValidAutomergeUrl(latest)) return latest; |
| const handles = ( | ||
| await Promise.all(urls.map((url) => repo.find<unknown>(url).catch(() => undefined))) | ||
| ).filter((handle): handle is DocHandle<unknown> => !!handle); |
| moduleWatcher | ||
| .addPackagesFolder( |
| const taken = new Map<string, number>(); | ||
| const links = described.map(({ url, name, type }) => { | ||
| const count = (taken.get(name) ?? 0) + 1; | ||
| taken.set(name, count); | ||
| return { name: count === 1 ? name : `${name} (${count})`, type, url }; | ||
| }); |
| * A path that doesn't resolve stays: it may name something not synced yet. | ||
| */ | ||
| const checkPath = async (docUrl: AutomergeUrl, path: string) => { | ||
| const handle = await resolvePathParam(path); |
| console.warn(`patchwork: path=${path} doesn't resolve here (yet)`); | ||
| return; | ||
| } | ||
| if (handle.documentId !== parseAutomergeUrl(docUrl).documentId) { |
| if (prevDoc && !sameDoc) params.delete("draft"); | ||
| // Where the doc was opened from survives a tool switch or heads change on | ||
| // the same doc, as `draft` does. | ||
| const keptPath = !path && sameDoc ? params.get("path") ?? undefined : path; |
…ment; path-only hashes retry when providers register - A doc=/path= check that resolves after the hash moved on leaves the new hash alone: dropPath now needs doc= unchanged too. - Opening another part of the same document without a path drops the path; only the same part (a tool or heads change) keeps it. - A path-only hash that doesn't resolve is walked again whenever path providers change, so a cold boot opens a path through a package's datatype once that package registers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… mid-migration wins Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…branch's code changes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed docs read at their heads - A doc named by a doc step is walked with package providers too: only a container is written as one, so a pinned prefix through a package's container rebuilds that container's list, not its parent's. A path that ends at a doc by name still decides container or file by the built-ins. - A value held by a doc below the container (a nested directory in a deep package) is listed with that doc's heads, and read back at them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…so removing one there removes it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e's provider The folder built-in matches anything with a docs array, so a tldraw canvas (which keeps its scripts there) never reached its own provider. A provider declared for such a doc's type now walks it; an untyped or folder-typed folder is still the built-in's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…others Members are found independently. The first listing waits up to 3s for all of them, then lists what it has; a slow member is listed when it arrives, and one that couldn't be found is asked for again 10s later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…no cache The content store is opened lazily: reads and deletes of a cache that doesn't exist find nothing, and only a verified write creates it. Before, every made-up account name in a /@/ link left a cache behind that nothing ever deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t stacks From CodeQL on #467. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ckage doc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lled Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sub-document url (a tldraw shape from the sidebar, say) whose value has no @Patchwork type picked no tool, and patchwork-view said the document was missing its metadata. The tool is now chosen from the document it's part of, and still gets the sub-handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pinned /@/ URL a /packages lookup answers with went straight to importPackageFromHttpUrl on the main thread. It now goes through the importAutomergePackage hook like any package doc's version, so the worker reads its descriptors and load() re-imports the same pinned URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file in a package's nested folder is reached through folder docs linked without heads. After a rename, walking back to it at their current heads found nothing, so its pinned URL failed when the bytes weren't stored. Lists now record those folders' heads as via; the service worker passes them to the resolver, and readPath reads each folder at them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fake tab runs on the service worker's event loop, so its accepted can queue behind its own resolving. Under load that passed the 250ms accept timeout, the tab counted as silent, and the request waited out a 35s broadcast nothing answers: the 30s test timeouts. Four parallel runs of the suite failed 12 of 12 times before; 12 of 12 pass after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


(this is a big draft PR to just work out the feeling, before trying to figure out what the primitives are that this could be built on top of)