mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
fix(workflow): revert SliceWorkflowBinding nullability — drop unused declaration path
Forward-fix #1 on df79077. Guard re-review caught daemon tsc errors:
widening `SliceWorkflowBinding.instanceId` + `createdAt` to nullable
broke SliceDetailProjector at slice-detail-projector.ts:199 + 212
because the downstream projector + WorkflowBindingPayload contract
still expects non-null values.
Per guard option (a) recommendation: keep synthetic declaration
bindings OUT of SliceDetailProjector. The declaration-fallback path
was speculative — no caller actually passed the declaration param.
The missions route (the actual walk-item 7 acceptance surface) calls
`projectSpecGraph(row.spec, null)` DIRECTLY in routes/missions.ts;
it never went through findSliceWorkflowBinding.
Resolution:
- `SliceWorkflowBinding.instanceId` + `createdAt` back to non-null
(preserves SliceDetailProjector + WorkflowBindingPayload contract).
- `findSliceWorkflowBinding` signature reverts to live-only:
`(db, qitemIds) => SliceWorkflowBindingResult`. No declaration
param, no synthetic binding path.
- 4 declaration-fallback tests removed from slice-workflow-binding.test.ts
(the surface they tested no longer exists).
The walk-item-7 acceptance is unaffected — the mission Topology tab
renders the projected spec graph via the missions route's
projectSpecGraph(row.spec, null) call, which doesn't depend on the
synthetic-binding pathway.
If a future use case needs synthetic-binding-in-slice-detail (e.g.,
slice-scope Topology consumes declared workflow_spec), broaden the
WorkflowBindingPayload contract end-to-end (route + UI payload + tests)
in that slice rather than the partial widening this commit reverts.
Tests: 50/50 targeted (slice-indexer 32 + slice-workflow-binding 9 +
missions-routes 9). Daemon tsc clean. Full daemon suite: 3378 passed /
1 skipped / 0 failed on clean run. git diff --check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
df79077f31
commit
b6f3114d4c
2 files changed
+5
-110
No files matched your search
@@ -25,11 +25,8 @@
|
||||
import type Database from "better-sqlite3";
|
||||
|
||||
export interface SliceWorkflowBinding {
|
||||
/** The most recent workflow_instance touching the slice's qitem set.
|
||||
* V0.3.1 slice 13: nullable when binding is synthetic (declaration-
|
||||
* fallback path — slice/mission frontmatter declared workflow_spec
|
||||
* but no live instance is bound). */
|
||||
instanceId: string | null;
|
||||
/** The most recent workflow_instance touching the slice's qitem set. */
|
||||
instanceId: string;
|
||||
workflowName: string;
|
||||
workflowVersion: string;
|
||||
status: string;
|
||||
@@ -37,8 +34,7 @@ export interface SliceWorkflowBinding {
|
||||
/** Frontier qitem_ids parsed from the JSON column. */
|
||||
currentFrontier: string[];
|
||||
hopCount: number;
|
||||
/** ISO timestamp from the live instance row; null for synthetic bindings. */
|
||||
createdAt: string | null;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
@@ -60,29 +56,11 @@ interface InstanceRow {
|
||||
completed_at: string | null;
|
||||
}
|
||||
|
||||
/** V0.3.1 slice 13 walk-item 7 — frontmatter `workflow_spec: <name>@<version>`
|
||||
* declaration. When passed AND no live instance binds the slice's
|
||||
* qitems, findSliceWorkflowBinding returns a synthetic binding so
|
||||
* the Topology projector can render the spec graph for a slice that
|
||||
* has no live qitem activity yet. Live instances always win over
|
||||
* declarations: declared metadata is a fallback, not an override. */
|
||||
export interface WorkflowSpecDeclaration {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export function findSliceWorkflowBinding(
|
||||
db: Database.Database,
|
||||
qitemIds: string[],
|
||||
declaration?: WorkflowSpecDeclaration | null,
|
||||
): SliceWorkflowBindingResult {
|
||||
// V0.3.1 slice 13 — empty-qitemIds path also honors declaration so
|
||||
// mission-level callers (no qitem corpus) can still get a synthetic
|
||||
// binding from their frontmatter declaration.
|
||||
if (qitemIds.length === 0) {
|
||||
if (declaration) return { primary: syntheticBinding(declaration), additionalInstanceIds: [] };
|
||||
return { primary: null, additionalInstanceIds: [] };
|
||||
}
|
||||
if (qitemIds.length === 0) return { primary: null, additionalInstanceIds: [] };
|
||||
|
||||
const instanceIds = new Set<string>();
|
||||
|
||||
@@ -117,11 +95,7 @@ export function findSliceWorkflowBinding(
|
||||
// workflow_instances absent — skip
|
||||
}
|
||||
|
||||
// V0.3.1 slice 13 — declaration-fallback when no live instance binds.
|
||||
if (instanceIds.size === 0) {
|
||||
if (declaration) return { primary: syntheticBinding(declaration), additionalInstanceIds: [] };
|
||||
return { primary: null, additionalInstanceIds: [] };
|
||||
}
|
||||
if (instanceIds.size === 0) return { primary: null, additionalInstanceIds: [] };
|
||||
|
||||
// Resolve full rows; sort by created_at DESC; pick most recent as primary.
|
||||
const idList = Array.from(instanceIds);
|
||||
@@ -147,25 +121,6 @@ export function findSliceWorkflowBinding(
|
||||
return { primary, additionalInstanceIds: additional };
|
||||
}
|
||||
|
||||
/** V0.3.1 slice 13 — synthetic binding for the frontmatter-declared
|
||||
* workflow_spec when no live instance is bound. The projector reads
|
||||
* `currentStepId` (null here) to skip "current step" highlighting;
|
||||
* `instanceId: null` signals to the UI that this is a declared-only
|
||||
* binding (no live instance). */
|
||||
function syntheticBinding(declaration: WorkflowSpecDeclaration): SliceWorkflowBinding {
|
||||
return {
|
||||
instanceId: null,
|
||||
workflowName: declaration.name,
|
||||
workflowVersion: declaration.version,
|
||||
status: "declared",
|
||||
currentStepId: null,
|
||||
currentFrontier: [],
|
||||
hopCount: 0,
|
||||
createdAt: null,
|
||||
completedAt: null,
|
||||
};
|
||||
}
|
||||
|
||||
function rowToBinding(row: InstanceRow): SliceWorkflowBinding {
|
||||
let frontier: string[] = [];
|
||||
try {
|
||||
|
||||
@@ -187,64 +187,4 @@ describe("PL-slice-story-view-v1 findSliceWorkflowBinding", () => {
|
||||
expect(result.primary?.instanceId).toBe("inst-both");
|
||||
expect(result.additionalInstanceIds).toEqual([]);
|
||||
});
|
||||
|
||||
// V0.3.1 slice 13 walk-item 7 — declaration-fallback path. When the
|
||||
// slice/mission frontmatter declares `workflow_spec: <name>@<ver>`
|
||||
// but no live instance touches the slice, the binding falls back to
|
||||
// a synthetic record so the downstream projector can render the
|
||||
// spec graph (currentStepId: null → projectSpecGraph(spec, null)).
|
||||
describe("declaration-fallback binding (slice 13)", () => {
|
||||
it("returns synthetic binding when declaration provided + no live instance", () => {
|
||||
const result = findSliceWorkflowBinding(db, ["q-no-instance"], {
|
||||
name: "openrig-velocity",
|
||||
version: "1.0",
|
||||
});
|
||||
expect(result.primary).not.toBeNull();
|
||||
expect(result.primary?.instanceId).toBeNull();
|
||||
expect(result.primary?.workflowName).toBe("openrig-velocity");
|
||||
expect(result.primary?.workflowVersion).toBe("1.0");
|
||||
expect(result.primary?.currentStepId).toBeNull();
|
||||
expect(result.primary?.currentFrontier).toEqual([]);
|
||||
expect(result.primary?.status).toBe("declared");
|
||||
expect(result.additionalInstanceIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("declaration is IGNORED when a live instance already binds (live signal wins)", () => {
|
||||
insertInstance(db, {
|
||||
instanceId: "inst-live",
|
||||
currentFrontier: ["q-live"],
|
||||
workflowName: "different-spec",
|
||||
workflowVersion: "2.0",
|
||||
});
|
||||
insertTrail(db, {
|
||||
trailId: "t-live",
|
||||
instanceId: "inst-live",
|
||||
stepId: "x",
|
||||
stepRole: "r",
|
||||
priorQitemId: "q-live",
|
||||
});
|
||||
const result = findSliceWorkflowBinding(db, ["q-live"], {
|
||||
name: "openrig-velocity",
|
||||
version: "1.0",
|
||||
});
|
||||
// Live wins; declaration metadata is not surfaced when a real
|
||||
// instance is present.
|
||||
expect(result.primary?.instanceId).toBe("inst-live");
|
||||
expect(result.primary?.workflowName).toBe("different-spec");
|
||||
});
|
||||
|
||||
it("no declaration AND no live instance → null binding (current behavior preserved)", () => {
|
||||
const result = findSliceWorkflowBinding(db, ["q-no-instance"]);
|
||||
expect(result.primary).toBeNull();
|
||||
});
|
||||
|
||||
it("empty qitemIds + declaration provided → still returns synthetic binding (mission case)", () => {
|
||||
const result = findSliceWorkflowBinding(db, [], {
|
||||
name: "openrig-velocity",
|
||||
version: "1.0",
|
||||
});
|
||||
expect(result.primary?.workflowName).toBe("openrig-velocity");
|
||||
expect(result.primary?.instanceId).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user