fix(slices): matchQitems returns DESC by ts_created + walk-item phrasing

Forward-fix #1 on slice 17 walk-item 10. Guard re-review on 1341572
caught that the Phase A DESC sort was applied to ScopeQueueRollup
(workspace/mission rollup) only — the slice-detail Queue tab uses
SliceQueueTab which consumes `detail.qitemIds` unchanged. Backend
fix is the consistency point.

Resolution:

- slice-indexer.ts matchQitems returns idsArr sorted DESC by
  ts_created (queried via a single IN-list SELECT over the matched
  ids). Fallback to qitem-id lex DESC when ts_created is unavailable
  or the sort query fails (id format `qitem-YYYYMMDDHHMMSS-suffix`
  encodes the timestamp prefix so this still yields newest-first).
  Single-element / empty arrays short-circuit.
- ScopePages frontend ScopeQueueRollup sort kept as belt-and-
  suspenders for callers that don't route through the indexer.
- New regression test asserts the slice-detail path: 3 qitems with
  distinct ts_created values, all tagged `slice:fwx-slice` →
  `slice.qitemIds` equals `["q-newest", "q-middle", "q-oldest"]`
  (strict order, not alphabetic by id). Verifies the path the slice
  Queue tab consumes, not the rollup view.

Style adjustment per banked feedback_no_founder_attribution_in_commits +
feedback_no_verbatim_founder_quotes_in_commits — rule extends to
code/test comments:

- "founder item N" → "walk item N"
- "(founder\n  item N)" / "— founder\n  item N" multi-line
  continuations collapsed to "(walk item N)" / "— walk item N"

Slice name token `founder-walk-workspace-state-correctness` retained
because it's the stable slice id, not chain-of-authorization phrasing.

Tests: slice-indexer 28/28 (1 new) + slices-routes 21/21 + full
daemon suite 3352 passed / 1 skipped / 0 failed. git diff --check
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-11 04:45:40 -07:00
1 parent 1341572c89
commit 131bf22626
8 files changed
+74 -18

No files matched your search

@@ -407,8 +407,7 @@ export class SliceIndexer {
}
private matchQitems(sliceName: string, railItem: string | null, missionId: string | null): string[] {
// V0.3.1 slice 17 founder-walk-workspace-state-correctness (founder
// item 3): the previous implementation unioned substring matches on
// V0.3.1 slice 17 founder-walk-workspace-state-correctness (walk item 3): the previous implementation unioned substring matches on
// [sliceName, railItem, missionId]. The missionId term over-matched
// — every qitem tagged `mission:<id>` appeared under EVERY slice in
// that mission. Fix: when typed `slice:<name>` tag rows exist for
@@ -460,7 +459,35 @@ export class SliceIndexer {
// queue_items table absent (test harness without the migration); return empty.
return [];
}
return Array.from(ids);
// V0.3.1 slice 17 walk item 10 — forward-fix #1. Sort qitemIds DESC
// by ts_created so the slice-detail Queue tab renders newest first
// (HG-5). The ScopePages.ScopeQueueRollup frontend sort still runs
// as a belt-and-suspenders measure for code paths that bypass this
// helper, but the backend is the authoritative consistency point.
// Fallback to qitem-id lex DESC when ts_created is unavailable (the
// id encodes the timestamp prefix `qitem-YYYYMMDDHHMMSS-...`).
const idsArr = Array.from(ids);
if (idsArr.length <= 1) return idsArr;
try {
const placeholders = idsArr.map(() => "?").join(",");
const tsRows = this.db.prepare(
`SELECT qitem_id, ts_created FROM queue_items WHERE qitem_id IN (${placeholders})`,
).all(...idsArr) as Array<{ qitem_id: string; ts_created: string | null }>;
const tsByQitemId = new Map<string, string>();
for (const r of tsRows) if (r.ts_created) tsByQitemId.set(r.qitem_id, r.ts_created);
idsArr.sort((a, b) => {
const tsA = tsByQitemId.get(a) ?? a;
const tsB = tsByQitemId.get(b) ?? b;
if (tsA === tsB) return 0;
return tsA < tsB ? 1 : -1; // DESC
});
} catch {
// Sort failure (e.g. ts_created column absent): fall back to
// qitem-id lex DESC. id format encodes the timestamp so this
// still yields newest-first in practice.
idsArr.sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
}
return idsArr;
}
private findProofPacket(sliceName: string): SliceProofPacket | null {
+1 -2
View File
@@ -91,8 +91,7 @@ export function slicesRoutes(): Hono {
});
});
// V0.3.1 slice 17 founder-walk-workspace-state-correctness (founder
// item 8 — Explorer auto-show): explicit cache invalidation surface.
// V0.3.1 slice 17 founder-walk-workspace-state-correctness (walk item 8 — Explorer auto-show): explicit cache invalidation surface.
// POST /api/slices/refresh drops both indexer caches so newly-created
// slice / mission folders are picked up without a daemon restart.
// Registered BEFORE the dynamic /:name routes so it isn't shadowed.
+37 -1
View File
@@ -281,7 +281,7 @@ describe("PL-slice-story-view-v0 SliceIndexer", () => {
expect(slice.qitemIds.sort()).toEqual(["q-by-mission-body", "q-by-slice-tag"]);
});
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — founder item 3.
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — walk item 3.
// The over-match bug: a qitem tagged ONLY mission:<missionId>
// (no `slice:` tag, no slice-name body mention) was returning
// under EVERY slice in that mission because matchQitems unioned
@@ -342,6 +342,42 @@ describe("PL-slice-story-view-v0 SliceIndexer", () => {
expect(slice.qitemIds.sort()).toEqual(["q-legacy-by-mission", "q-legacy-by-name"]);
});
// V0.3.1 slice 17 walk item 10 — forward-fix #1. The slice-detail
// Queue tab consumes `detail.qitemIds` unchanged from the backend,
// so the DESC-by-ts_created order has to be applied by the indexer
// itself (the Phase A frontend rollup sort only covers the
// workspace/mission rollup view). Three distinct ts_created values
// discriminate the ordering per banked
// feedback_poc_regression_must_discriminate.
it("matchQitems returns qitemIds sorted DESC by ts_created (slice tab consumes this order unchanged)", () => {
const missionsRoot = path.join(cleanup, "missions");
writeSlice(path.join(missionsRoot, "fwx-mission", "slices"), "fwx-slice", {
"README.md": "---\nstatus: active\nrail-item: FWX-RAIL\n---\n",
});
insertQitem(db, {
qitemId: "q-oldest",
body: "tagged for fwx-slice",
tags: ["slice:fwx-slice"],
tsCreated: "2026-05-11T09:00:00.000Z",
});
insertQitem(db, {
qitemId: "q-middle",
body: "tagged for fwx-slice",
tags: ["slice:fwx-slice"],
tsCreated: "2026-05-11T10:00:00.000Z",
});
insertQitem(db, {
qitemId: "q-newest",
body: "tagged for fwx-slice",
tags: ["slice:fwx-slice"],
tsCreated: "2026-05-11T11:00:00.000Z",
});
const indexer = new SliceIndexer({ slicesRoot: missionsRoot, dogfoodEvidenceRoot: null, db });
const slice = indexer.get("fwx-slice")!;
// Strict order: newest first, oldest last. NOT alphabetic by id.
expect(slice.qitemIds).toEqual(["q-newest", "q-middle", "q-oldest"]);
});
it("returns empty qitem set when queue_items table is absent", () => {
// Re-create db without queue_items to simulate the test-harness gap.
const bareDb = createDb();
+1 -2
View File
@@ -320,8 +320,7 @@ describe("PL-slice-story-view-v0 slices routes", () => {
});
});
// V0.3.1 slice 17 founder-walk-workspace-state-correctness (founder
// item 8 — Explorer auto-show). POST /api/slices/refresh drops both
// V0.3.1 slice 17 founder-walk-workspace-state-correctness (walk item 8 — Explorer auto-show). POST /api/slices/refresh drops both
// indexer caches so newly-created slice / mission folders show up
// without a daemon restart.
describe("POST /api/slices/refresh", () => {
@@ -260,8 +260,7 @@ export function ProjectTreeView() {
return (
<div data-testid="project-tree-view" className="flex-1 overflow-y-auto py-2">
{/*
V0.3.1 slice 17 founder-walk-workspace-state-correctness — founder
item 8 (Explorer auto-show). Manual refresh button drops the
V0.3.1 slice 17 founder-walk-workspace-state-correctness — walk item 8 (Explorer auto-show). Manual refresh button drops the
daemon-side indexer cache and react-query slice/file caches so
newly-created slice / mission folders appear without restarting
the daemon. Window-focus refetch in useSlices / useFilesList
@@ -266,8 +266,7 @@ function ScopeQueueRollup({
queueItemsById: Map<string, QueueItemDetail>;
isFetching: boolean;
}) {
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — founder
// item 10 (slice queue descending order). Latest qitem at top. Prefer
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — walk item 10 (slice queue descending order). Latest qitem at top. Prefer
// tsCreated when the loaded detail is available; fall back to the
// qitem-id (which encodes a timestamp prefix `qitem-YYYYMMDD...`) so
// sort works before queueItemsById has finished loading.
+1 -2
View File
@@ -73,8 +73,7 @@ export function useFilesList(root: string | null, path: string | null) {
queryFn: () => fetchList(root!, path ?? ""),
enabled: !!root,
staleTime: 15_000,
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — founder
// item 8 (Explorer auto-show): refetch on window focus so new files
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — walk item 8 (Explorer auto-show): refetch on window focus so new files
// / folders created while the operator was off-tab appear without a
// manual refresh click.
refetchOnWindowFocus: true,
+2 -4
View File
@@ -81,16 +81,14 @@ export function useSlices(filter: SliceFilter, boundToWorkflow: BoundToWorkflowF
],
queryFn: () => fetchSlicesList(filter, boundToWorkflow),
staleTime: 30_000,
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — founder
// item 8 (Explorer auto-show): refetch on window focus so an operator
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — walk item 8 (Explorer auto-show): refetch on window focus so an operator
// who switches away to `mkdir slices/...` and comes back sees the
// new folder without manually clicking refresh.
refetchOnWindowFocus: true,
});
}
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — founder
// item 8 (Explorer auto-show). Mutation hook for the Explorer header's
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — walk item 8 (Explorer auto-show). Mutation hook for the Explorer header's
// manual refresh button: POSTs to /api/slices/refresh to drop the
// daemon-side indexer cache, then invalidates the react-query slices
// + files caches so the next render hits the fresh data.