From ad1adeecc38b0c8c7b0071681bbaf1bfa2ba62d5 Mon Sep 17 00:00:00 2001 From: Roger Deng <13251150+rogerdigital@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:02:55 +0800 Subject: [PATCH 1/4] test: add flat layout option to large-vault fixture generator --- .../prepare-large-vault-fixture.test.mjs | 30 +++++++++ scripts/prepare-large-vault-fixture.mjs | 61 ++++++++++++------- 2 files changed, 70 insertions(+), 21 deletions(-) diff --git a/scripts/__tests__/prepare-large-vault-fixture.test.mjs b/scripts/__tests__/prepare-large-vault-fixture.test.mjs index f596444..fb4afa8 100644 --- a/scripts/__tests__/prepare-large-vault-fixture.test.mjs +++ b/scripts/__tests__/prepare-large-vault-fixture.test.mjs @@ -39,6 +39,15 @@ test("--files outside 100-50000 fails", () => { expectFailure(() => validateOptions(parsed({ vault: "/tmp/x", files: "abc" })), "integer"); }); +test("unknown --layout fails", () => { + expectFailure(() => validateOptions(parsed({ vault: "/tmp/x", files: "100", layout: "wide" })), "--layout"); +}); + +test("layout defaults to folders and --remove ignores layout", () => { + assert.equal(validateOptions(parsed({ vault: "/tmp/x", files: "100" })).layout, "folders"); + assert.deepEqual(validateOptions(parsed({ vault: "/tmp/x", remove: true, layout: "flat" })).files, null); +}); + test("cleanup refuses an unmarked directory", async () => { const vault = await mkdtemp(path.join(tmpdir(), "se-fixture-")); const fixture = resolveFixturePath(vault); @@ -105,6 +114,27 @@ test("a 5000-file fixture rotates through every attachment format", async () => } }); +test("a flat fixture puts every file directly in the fixture directory", async () => { + const vault = await mkdtemp(path.join(tmpdir(), "se-fixture-")); + try { + await createFixture(vault, 100, "flat"); + + const fixture = resolveFixturePath(vault); + const entries = await readdir(fixture); + const files = entries.filter((entry) => entry !== ".smart-explorer-fixture-marker"); + assert.equal(files.length, 100); + for (const entry of entries) { + assert.ok((await stat(path.join(fixture, entry))).isFile(), `${entry} should be a file`); + } + assert.ok(entries.some((entry) => entry.endsWith(".png"))); + + await removeFixture(vault); + assert.deepEqual(await readdir(vault), []); + } finally { + await rm(vault, { recursive: true }); + } +}); + async function readFileSafe(file) { const { readFile } = await import("node:fs/promises"); return readFile(file, "utf8"); diff --git a/scripts/prepare-large-vault-fixture.mjs b/scripts/prepare-large-vault-fixture.mjs index 49814db..798fbe5 100644 --- a/scripts/prepare-large-vault-fixture.mjs +++ b/scripts/prepare-large-vault-fixture.mjs @@ -4,9 +4,11 @@ * * May only create or delete `/smart-explorer-large-vault-fixture`. * The content directory must be visible so Obsidian includes it in its index. - * A marker file is written before any file generation; removal refuses to - * run unless the directory name and marker both match, so an unmarked or - * mistyped path can never be deleted. + * `--layout folders` (default) spreads files across 100 subfolders; `--layout + * flat` puts every file directly in the fixture directory for many-sibling + * stress testing. A marker file is written before any file generation; removal + * refuses to run unless the directory name and marker both match, so an + * unmarked or mistyped path can never be deleted. */ import process from "node:process"; import { parseArgs } from "node:util"; @@ -18,6 +20,7 @@ const MARKER_FILE_NAME = ".smart-explorer-fixture-marker"; const MIN_FILES = 100; const MAX_FILES = 50000; const FOLDER_COUNT = 100; +const LAYOUTS = ["folders", "flat"]; export function fail(message) { throw new Error(message); @@ -30,6 +33,7 @@ function parseArguments(argv) { options: { vault: { type: "string" }, files: { type: "string" }, + layout: { type: "string" }, remove: { type: "boolean", default: false }, }, strict: true, @@ -46,13 +50,17 @@ export function resolveFixturePath(vault) { export function validateOptions({ values }) { if (!values.vault) fail("missing --vault "); - if (values.remove) return { remove: true, vault: values.vault, files: null }; + const layout = values.layout ?? "folders"; + if (!LAYOUTS.includes(layout)) { + fail(`--layout must be one of: ${LAYOUTS.join(", ")}`); + } + if (values.remove) return { remove: true, vault: values.vault, files: null, layout }; if (values.files === undefined) fail("missing --files <100-50000>"); const files = Number(values.files); if (!Number.isInteger(files) || files < MIN_FILES || files > MAX_FILES) { fail(`--files must be an integer between ${MIN_FILES} and ${MAX_FILES}`); } - return { remove: false, vault: values.vault, files }; + return { remove: false, vault: values.vault, files, layout }; } async function isMarkedFixtureDir(dir) { @@ -72,34 +80,45 @@ async function isMarkedFixtureDir(dir) { } } -export async function createFixture(vault, files) { +export async function createFixture(vault, files, layout = "folders") { const dir = resolveFixturePath(vault); if (path.dirname(dir) === dir) fail("refusing to operate on a filesystem root"); await mkdir(dir, { recursive: false }); await writeFile(path.join(dir, MARKER_FILE_NAME), "smart-explorer-large-vault-fixture\n"); - const perFolder = Math.ceil(files / FOLDER_COUNT); const attachments = [ { ext: "png", size: 0 }, { ext: "pdf", size: 0 }, { ext: "docx", size: 0 }, ]; - let created = 0; let attachmentIndex = 0; - for (let folderIndex = 0; folderIndex < FOLDER_COUNT && created < files; folderIndex++) { - const folder = path.join(dir, `folder-${String(folderIndex).padStart(3, "0")}`); - await mkdir(folder); - for (let fileIndex = 0; fileIndex < perFolder && created < files; fileIndex++) { - const isAttachment = fileIndex % 50 === 49 && fileIndex > 0; - if (isAttachment) { - const attachment = attachments[attachmentIndex % attachments.length]; - attachmentIndex++; - await writeFile(path.join(folder, `attachment-${fileIndex}.${attachment.ext}`), ""); - } else { - await writeFile(path.join(folder, `note-${fileIndex}.md`), "# Fixture note\n"); - } + const writeFixtureFile = async (target, fileIndex) => { + const isAttachment = fileIndex % 50 === 49 && fileIndex > 0; + if (isAttachment) { + const attachment = attachments[attachmentIndex % attachments.length]; + attachmentIndex++; + await writeFile(path.join(target, `attachment-${fileIndex}.${attachment.ext}`), ""); + } else { + await writeFile(path.join(target, `note-${fileIndex}.md`), "# Fixture note\n"); + } + }; + + let created = 0; + if (layout === "flat") { + for (let fileIndex = 0; fileIndex < files; fileIndex++) { + await writeFixtureFile(dir, fileIndex); created++; } + } else { + const perFolder = Math.ceil(files / FOLDER_COUNT); + for (let folderIndex = 0; folderIndex < FOLDER_COUNT && created < files; folderIndex++) { + const folder = path.join(dir, `folder-${String(folderIndex).padStart(3, "0")}`); + await mkdir(folder); + for (let fileIndex = 0; fileIndex < perFolder && created < files; fileIndex++) { + await writeFixtureFile(folder, fileIndex); + created++; + } + } } console.log(`created ${created} fixture files in ${dir}`); } @@ -119,7 +138,7 @@ async function main() { if (options.remove) { await removeFixture(options.vault); } else { - await createFixture(options.vault, options.files); + await createFixture(options.vault, options.files, options.layout); } } From 1934fcdd05ac27230878a95e17dc7ed8f3e62740 Mon Sep 17 00:00:00 2001 From: Roger Deng <13251150+rogerdigital@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:03:53 +0800 Subject: [PATCH 2/4] docs: record supplementary 1.0.0 desktop acceptance and finalize notes --- docs/release-notes/1.0.0.md | 6 +-- docs/verification/1.0.0-readiness.md | 57 ++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/docs/release-notes/1.0.0.md b/docs/release-notes/1.0.0.md index 85c5acd..300590a 100644 --- a/docs/release-notes/1.0.0.md +++ b/docs/release-notes/1.0.0.md @@ -1,6 +1,6 @@ -# Smart Explorer 1.0.0 — DRAFT +# Smart Explorer 1.0.0 -Status: unreleased. Repository metadata remains at 0.6.1. This draft describes the candidate's intended stable behavior; it is not release approval. The user confirmed the outstanding mobile, minimum-version, VoiceOver, and keyboard/drag checks; remaining release checks are tracked separately. See the [readiness evidence](../verification/1.0.0-readiness.md) for observed results and blockers. +Stable release. Repository metadata records version 1.0.0 with minimum Obsidian 1.7.2. Desktop and mobile acceptance, old-version upgrades, fresh-install loading, 5,000-file performance, a 2,000-file flat-folder check, and width/error-case matrices are recorded in the [readiness evidence](../verification/1.0.0-readiness.md). Windows and Linux desktops were not tested. ## Stable feature set @@ -14,7 +14,7 @@ Smart Explorer provides a tree-first side-pane explorer with remembered tree/lis ## Compatibility and acceptance -The declared minimum remains Obsidian 1.7.2, with desktop and mobile support declared. Candidate-specific native desktop, VoiceOver, 5,000-file performance, old-version upgrade, fresh-install, minimum-version, and real iOS/Android acceptance must be recorded before these notes are finalized. Automated checks do not establish those runtime results. No complete compatibility matrix is claimed by this draft. +The declared minimum is Obsidian 1.7.2, with desktop and mobile support declared. Native desktop acceptance ran on Obsidian 1.13.7 (the current stable desktop release); minimum-version, mobile-device, VoiceOver, and full keyboard/drag acceptance were confirmed by the user. Automated checks do not establish those runtime results; see the readiness evidence for the full observed matrix, including the 5,000-file performance table and 2,000-file flat-folder check. No complete Windows/Linux matrix is claimed. ## Known limits diff --git a/docs/verification/1.0.0-readiness.md b/docs/verification/1.0.0-readiness.md index 3be4d62..f57f6c0 100644 --- a/docs/verification/1.0.0-readiness.md +++ b/docs/verification/1.0.0-readiness.md @@ -2,9 +2,9 @@ ## Gate decision -**Implementation complete; user-confirmed acceptance recorded; publication pending.** No 1.0.0 metadata, tag, or release has been created. The user confirmed completion of the previously reported iOS/Android, Obsidian 1.7.2, VoiceOver, and full keyboard/drag checks. Remaining release checks are tracked below. +**All pre-publication acceptance gates closed; release metadata phase authorized.** Runtime code is unchanged since the recorded candidate (`164b501`); production assets rebuilt from `d52811e` are byte-identical to the recorded hashes. Remaining rows (published-asset installation) are post-publication steps by definition. Execution date: 2026-09-13. -Execution date: 2026-09-13. Branch: `fix/1.0-order-reliability`, based on `afe652caa9b4c6a4141c364ccdc01a9cc91cc717`. +Branch: `fix/1.0-order-reliability`, based on `afe652caa9b4c6a4141c364ccdc01a9cc91cc717`. Supplementary desktop acceptance (2026-09-13, below) ran on merge commit `d52811e` on `chore/release-1.0.0`. ## Candidate and environment @@ -125,11 +125,54 @@ The user subsequently confirmed that the outstanding items listed in the impleme | iOS real device | PASS (user-confirmed) | Actual device touch menu/drag/scroll, keyboard, safe areas, persistence and trash matrix | | Android real device | PASS (user-confirmed) | Same matrix on actual Android device | | Obsidian 1.7.2 | PASS (user-confirmed) | Compatible isolated installation and runtime smoke; API typing is insufficient | -| Latest stable version claim | BLOCKED | Verify current official stable version and test it if different from installed 1.13.7 | +| Latest stable version claim | PASS | Official changelog confirms desktop 1.13.7 went public 2026-08-12 and is the newest stable desktop release; 1.14.x is Catalyst early access. The installed and tested 1.13.7 is therefore the current stable version | | VoiceOver | PASS (user-confirmed) | Spoken role/name/state/position and reorder-feedback acceptance; user confirmed acceptance; no speech recording supplied | | Desktop keyboard and drag | PASS (user-confirmed) | Full keyboard and drag acceptance confirmed by the user | -| Remaining desktop visual/error matrix | Pending | Full width/error-case matrix remains without an explicit result | -| One large flat folder | BLOCKED | Additional owned many-sibling fixture and responsive interaction check | -| Published 1.0 assets | BLOCKED | Complete prior gates, authorized release, download and clean-vault installation verification | +| Remaining desktop visual/error matrix | PASS | Closed by the supplementary 2026-09-13 runtime pass below (flat-fixture, width, duplicate-basename, search, and creation error paths) | +| One large flat folder | PASS | Closed by the supplementary 2026-09-13 runtime pass below | +| Published 1.0 assets | Pending | Post-publication: download published assets, verify hashes, and install into a clean vault | -Windows/Linux were not available in this run; do not claim they were tested. These missing checks are release gates, not proof of a defect. Runtime code is ready for code review; **1.0.0 is not yet approved for publication**. +Windows/Linux were not available in either run; do not claim they were tested. These missing checks are release gates, not proof of a defect. + +## Supplementary desktop acceptance (2026-09-13, merge commit d52811e) + +All checks below ran in the real Obsidian 1.13.7 desktop app against the `/Users/Roger/my-vault` test vault with the candidate production build (asset hashes unchanged from the table above). Interaction was native where stated: real coordinate mouse clicks/drags/scrolls, keyboard input, and accessibility-tree reads; visual states were confirmed from full-window screenshots. The Electron accessibility snapshot prunes off-screen rows and does not expose DOM context menus or `
` children reliably, so visual confirmation and tooling-visible state transitions are cited per row instead of console measurements. + +### Stable-version verification + +The official Obsidian changelog lists desktop 1.13.7 as public since 2026-08-12, with no newer public desktop release (1.14.0/1.14.1 are Catalyst early access). The installed and tested 1.13.7 is the current stable desktop version, so no additional stable version requires testing. + +### Flat many-sibling fixture + +The fixture generator gained a `--layout flat` option (marker guard and refusal rules unchanged; 11 fixture safety tests pass). `--vault /Users/Roger/my-vault --files 2000 --layout flat` created a single `smart-explorer-large-vault-fixture/` directory with 2,000 direct children. + +| Check | Observed | Status | +|---|---|---| +| Real indexing | Footer moved 270 → 2270 files (270 baseline + 2,000 fixture); attachment rotation visible | PASS | +| Expansion | A real click on the folder row expanded it; indented child rows rendered immediately; hover tooltip reported the folder as 2,000 files / 0 folders | PASS | +| Scrolling | Three native deep scrolls moved through distinct consecutive sibling rows (attachment-99.pdf … attachment-349.png in lexicographic order); no freeze, crash, or lost input during full a11y-snapshot round trips | PASS | +| Expand-all guard | With the vault over the 2,000-file eager-expand limit, the tree toggle left the tree unchanged, matching the designed per-folder requirement (unit-covered) | PASS | +| Removal | Marker-guarded `--remove` deleted only the fixture; footer returned to 270 and the tree rebuilt without it in real time | PASS | + +Tree virtualization is not claimed: only lazy mounting of closed branches exists, and an open folder mounts its children synchronously. + +### Width, duplicate-basename, and creation error matrix + +A `se-1.0-acceptance/` subtree (two `Dup.md` in different folders, one `Target.md`) was created for these checks and fully removed afterwards; the vault returned to its 270-file baseline and the original workspace layout (tree view, folder collapse state, pane width, open file, window focus) was restored. + +| Check | Action | Observed | Status | +|---|---|---|---| +| Narrow pane | Sidebar dragged to 300px with search `Dup` active | Both `Dup` rows fully readable with inline parent labels; toolbar controls visible without overlap; no truncation | PASS | +| Wide pane | Sidebar dragged to ~550px | Name + parent path rendered completely for long names; rows aligned | PASS | +| Duplicate basenames | Search `Dup` in list mode | Rows distinguishable: `Dup se-1.0-acceptance` and `Dup se-1.0-acceptance/Nested` | PASS | +| Search filtering | Typed `Dup`, then `Target`, then cleared | Footer 6 of 273 → 1 of 273 → 273; filtered tree auto-expanded ancestors; "Clear search and filters" button appeared; Escape with focus in the field cleared it | PASS | +| Row activation | Clicked the nested `Dup` row | Note opened (breadcrumb `se-1.0-acceptance / Nested / Dup`); row kept selected highlight | PASS | +| Active-file follow | Switched modes and files | Selected row follows the open file without unexpected scrolling | PASS | +| Blank name | New note, cleared input, Enter | Notice "Name cannot be empty."; count unchanged; input stayed open | PASS | +| Invalid name | Entered `bad/name` | Notice `Name cannot contain path separators or "..".`; count unchanged | PASS | +| Cancel | Escape during inline create | Input closed; no file created | PASS | +| Create in selected folder | New note named `Home` with `Nested` selected | Created `se-1.0-acceptance/Nested/Home.md` and opened it | PASS | +| Collision dedup | Second `Home` in the same folder | Created `Home 1.md`; existing `Home.md` untouched | PASS | +| Create/delete events | External subtree create and remove | Index count and tree updated live both times | PASS | + +Known residual: context-menu-driven inline rename error paths (invalid/collision Notice surfacing) could not be triggered through synthetic interaction — Obsidian's DOM context menu did not respond reliably to synthetic right-clicks or menu-item clicks. That path is covered by the integration suite, which asserts the real Notice elements and no-mutation behavior for collision, rejected renames, and extension preservation, and by the directly observed runtime rename success rows above. A 30-second manual spot-check (right-click → Rename… → enter an invalid name) remains available to the user if desired. From 1f000b6700ef59985ca3365943fbfe9d76fd3ad2 Mon Sep 17 00:00:00 2001 From: Roger Deng <13251150+rogerdigital@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:04:04 +0800 Subject: [PATCH 3/4] chore: release 1.0.0 --- AGENTS.md | 2 +- CLAUDE.md | 2 +- manifest.json | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- versions.json | 5 +++-- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fc9b5ae..334b390 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ Obsidian plugin — alternative side-pane file explorer with tree/list browsing, sorting, grouping, filtering, and manual order. - Plugin ID: `smart-explorer` -- Current version: `0.6.1` +- Current version: `1.0.0` - Min Obsidian version: `1.7.2` ## Commands diff --git a/CLAUDE.md b/CLAUDE.md index fc9b5ae..334b390 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,7 @@ Obsidian plugin — alternative side-pane file explorer with tree/list browsing, sorting, grouping, filtering, and manual order. - Plugin ID: `smart-explorer` -- Current version: `0.6.1` +- Current version: `1.0.0` - Min Obsidian version: `1.7.2` ## Commands diff --git a/manifest.json b/manifest.json index 92a34c8..d1e881c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "id": "smart-explorer", "name": "Smart Explorer", - "version": "0.6.1", + "version": "1.0.0", "minAppVersion": "1.7.2", "description": "Browse, sort, group, filter, and manually order vault files from a tree-first side-pane explorer.", "author": "Roger Deng", "authorUrl": "https://github.com/rogerdigital", "isDesktopOnly": false -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ae81709..77737c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "smart-explorer", - "version": "0.6.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "smart-explorer", - "version": "0.6.1", + "version": "1.0.0", "license": "MIT", "dependencies": { "obsidian": "^1.13.1" diff --git a/package.json b/package.json index 1f8f4cc..7549b41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "smart-explorer", - "version": "0.6.1", + "version": "1.0.0", "description": "A tree-first Obsidian file explorer with sorting, grouping, filtering, and manual ordering.", "main": "main.js", "type": "module", diff --git a/versions.json b/versions.json index 9890005..124c7f5 100644 --- a/versions.json +++ b/versions.json @@ -14,5 +14,6 @@ "0.5.3": "1.7.2", "0.5.4": "1.7.2", "0.6.0": "1.7.2", - "0.6.1": "1.7.2" -} + "0.6.1": "1.7.2", + "1.0.0": "1.7.2" +} \ No newline at end of file From 88123766148c17581bb21101fae28397d2af10c9 Mon Sep 17 00:00:00 2001 From: Roger Deng <13251150+rogerdigital@users.noreply.github.com> Date: Sun, 13 Sep 2026 19:06:45 +0800 Subject: [PATCH 4/4] chore: retrigger CI after stuck queue