-
Notifications
You must be signed in to change notification settings - Fork 2
fix(core): Windows path compatibility + feat: badge.deleteOrphans command #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,8 @@ import { listBadges, readBadge, removeBadge, writeBadge } from './store.js'; | |
| import type { | ||
| BadgeAddRefArgs, | ||
| BadgeDeleteArgs, | ||
| BadgeDeleteOrphansArgs, | ||
| BadgeDeleteOrphansResult, | ||
| BadgeDeleteResult, | ||
| BadgeFile, | ||
| BadgeGetArgs, | ||
|
|
@@ -463,6 +465,68 @@ export const rename: Handler<BadgeRenameArgs, BadgeRenameResult> = async (args, | |
| return { badge: moved, updatedRefs, focusUpdated }; | ||
| }; | ||
|
|
||
| /** | ||
| * Delete all orphan badges in the current workspace in one shot. An orphan is | ||
| * any badge whose `orphan: true` flag was set by the watcher when the backing | ||
| * file was deleted from disk. The badge's prompt and references are discarded; | ||
| * other badges that referred TO the orphan keep their outbound refs (they become | ||
| * dangling refs the user can clean up individually via badge.removeRef). Focus is | ||
| * updated via the same reconcile path as badge.delete so an orphan that was | ||
| * still in focus.md gets removed cleanly. | ||
| * | ||
| * Returns the list of deleted paths so the caller can refresh the canvas. | ||
| */ | ||
| export const deleteOrphans: Handler<BadgeDeleteOrphansArgs, BadgeDeleteOrphansResult> = async ( | ||
| _args, | ||
| ctx, | ||
| ) => { | ||
| const root = await currentWorkspaceRoot(ctx); | ||
| const all = await listBadges(ctx.fs, root); | ||
| const orphans = all.filter((b) => b.orphan === true); | ||
| const deleted: string[] = []; | ||
| for (const badge of orphans) { | ||
| try { | ||
| await removeBadge(ctx.fs, root, badge.file, badge.kind); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If an orphan badge JSON has a stale or mismatched Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged — the theoretical gap is real. In normal operation this can't happen: every The existing defence layers still apply: Fixing this properly — threading the actual walked path out of |
||
| deleted.push(badge.file); | ||
| // Remove from focus.md if it was in the active list. toggleActiveFile | ||
| // removes the file when present, adds it when absent — so guard with | ||
| // focus.get first to avoid accidentally re-adding it. | ||
| try { | ||
| const focusState = await ctx.run<Record<string, never>, { active: readonly string[] }>( | ||
| 'focus.get', | ||
| {}, | ||
| ); | ||
| if (focusState.active.includes(badge.file)) { | ||
| await ctx.run('focus.toggleActiveFile', { file: badge.file }); | ||
| } | ||
| } catch { | ||
| // Best-effort — a PathEscape or missing focus module must never abort | ||
| // the cleanup loop. | ||
| } | ||
| // Clean up inbound index entries: both inbound (other badges → this one) | ||
| // and outbound (this badge → others, which leave stale backlink rows). | ||
| try { | ||
| const inboundRes = await ctx.run<{ file: string }, { entries: { from: string }[] }>( | ||
| 'inbound.get', | ||
| { file: badge.file }, | ||
| ); | ||
| for (const entry of inboundRes.entries) { | ||
| await ctx.run('inbound.removeRef', { from: entry.from, to: badge.file }); | ||
|
Comment on lines
+509
to
+514
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an orphaned badge itself has outbound references, those refs already created inbound-index entries under each target via Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in the follow-up commit (3340ebc). The inbound cleanup block now handles both directions: // Inbound: other badges that pointed AT this orphan
for (const entry of inboundRes.entries) {
await ctx.run('inbound.removeRef', { from: entry.from, to: badge.file });
}
// Outbound: this orphan's own refs leave stale backlink rows in targets
for (const ref of badge.references) {
await ctx.run('inbound.removeRef', { from: badge.file, to: ref.to });
}Both loops run inside the same try/catch so a missing inbound module in lightweight tests still doesn't abort the cleanup. |
||
| } | ||
| for (const ref of badge.references) { | ||
| await ctx.run('inbound.removeRef', { from: badge.file, to: ref.to }); | ||
| } | ||
| } catch { | ||
| // inbound module may not be registered in lightweight tests | ||
| } | ||
| } catch (err) { | ||
| // Skip badges we can't delete (permissions, already gone) — don't abort. | ||
| console.warn(`[bh:badges] deleteOrphans: skipping ${badge.file}:`, err); | ||
| } | ||
| } | ||
| return { deleted }; | ||
| }; | ||
|
|
||
| export function commands(): ReadonlyArray< | ||
| readonly [name: string, handler: Handler<never, unknown>] | ||
| > { | ||
|
|
@@ -471,6 +535,7 @@ export function commands(): ReadonlyArray< | |
| ['badge.set', set as unknown as Handler<never, unknown>], | ||
| ['badge.list', list as unknown as Handler<never, unknown>], | ||
| ['badge.delete', del as unknown as Handler<never, unknown>], | ||
| ['badge.deleteOrphans', deleteOrphans as unknown as Handler<never, unknown>], | ||
| ['badge.addRef', addRef as unknown as Handler<never, unknown>], | ||
| ['badge.removeRef', removeRef as unknown as Handler<never, unknown>], | ||
| ['badge.reconnectRef', reconnectRef as unknown as Handler<never, unknown>], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On POSIX,
\is a valid filename character rather than a separator, but this helper now rewrites it unconditionally for absolute workspace paths as well as generated paths. If a user registers a workspace such as/tmp/project\2026, later calls throughcanonicalize,focusPath,badgePath, etc. will look under/tmp/project/2026instead, which can make materialization miss the real files or create.bhmetadata in the wrong directory; this normalization should be platform-aware or limited to Windows-style paths.Useful? React with 👍 / 👎.