mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
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:
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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user