branchjam / src /domain.js
abhid1234's picture
Upload folder using huggingface_hub
2fe29a7 verified
Raw
History Blame Contribute Delete
12.5 kB
import { CATALOG_VERSION, SEED_ARTIFACT_ID, getRecipe, constraintsFor } from "./catalog.js";
export const SCHEMA_VERSION = 1;
export const MAX_VOTES_PER_OUTCOME = 999;
export const seedNodeId = (artifactId) => `n:seed:${artifactId}`;
export const roundIdFor = (parentNodeId, recipeId) => `r:${parentNodeId}:${recipeId}`;
export const candidateNodeId = (roundId, slot) => `n:${roundId}:${slot}`;
const clone = (state) => structuredClone(state);
const fail = (message) => { throw new Error(message); };
export function createSession(source) {
const id = seedNodeId(source.seedArtifactId ?? SEED_ARTIFACT_ID);
return {
schemaVersion: SCHEMA_VERSION, catalogVersion: source.version ?? CATALOG_VERSION,
phase: "choose", seedNodeId: id, activeNodeId: id, inspectedNodeId: id,
selectedConstraintId: null, currentRoundId: null, compareSelection: null,
nodesById: {
[id]: { nodeId: id, parentNodeId: null, artifactId: source.seedArtifactId, recipeId: null, outcomeSlot: null, status: "seed", depth: 0 }
},
roundsById: {}
};
}
function assertPhase(state, phases, action) {
if (!phases.includes(state.phase)) fail(`${action} is not legal during ${state.phase}.`);
}
export function selectConstraint(state, source, constraintId) {
assertPhase(state, ["choose"], "selectConstraint");
const parent = state.nodesById[state.activeNodeId];
if (!getRecipe(source, parent.artifactId, constraintId)) fail("Constraint has no recipe for the active artifact.");
const next = clone(state);
next.selectedConstraintId = constraintId;
return next;
}
function insertImmutable(map, id, entity) {
if (!map[id]) { map[id] = entity; return; }
if (JSON.stringify(map[id]) !== JSON.stringify(entity)) fail(`Derived ID collision: ${id}`);
}
export function revealOutcomes(state, source) {
assertPhase(state, ["choose"], "revealOutcomes");
if (!state.selectedConstraintId) fail("Select a constraint before reveal.");
const parent = state.nodesById[state.activeNodeId];
const recipe = getRecipe(source, parent.artifactId, state.selectedConstraintId);
if (!recipe) fail("Selected recipe is unavailable.");
const roundId = roundIdFor(parent.nodeId, recipe.id);
const nodes = recipe.outcomes.map((outcome) => ({
nodeId: candidateNodeId(roundId, outcome.slot), parentNodeId: parent.nodeId,
artifactId: outcome.artifactId, recipeId: recipe.id, outcomeSlot: outcome.slot,
status: "candidate", depth: parent.depth + 1
}));
const round = {
roundId, parentNodeId: parent.nodeId, recipeId: recipe.id,
candidateNodeIds: nodes.map((node) => node.nodeId), votes: { a: 0, b: 0 },
winnerSlot: null, resolution: null
};
const next = clone(state);
insertImmutable(next.roundsById, roundId, round);
nodes.forEach((node) => insertImmutable(next.nodesById, node.nodeId, node));
next.phase = "compare";
next.currentRoundId = roundId;
next.compareSelection = "a";
next.inspectedNodeId = nodes[0].nodeId;
return next;
}
export function selectComparison(state, slot) {
assertPhase(state, ["compare", "vote"], "selectComparison");
if (!["a", "b"].includes(slot)) fail("Comparison slot must be a or b.");
const next = clone(state);
const round = next.roundsById[next.currentRoundId];
next.compareSelection = slot;
next.inspectedNodeId = round.candidateNodeIds[slot === "a" ? 0 : 1];
return next;
}
export function incrementVote(state, slot) {
assertPhase(state, ["compare", "vote"], "incrementVote");
if (!["a", "b"].includes(slot)) fail("Vote slot must be a or b.");
if (state.roundsById[state.currentRoundId].votes[slot] >= MAX_VOTES_PER_OUTCOME) {
fail(`Vote count cannot exceed ${MAX_VOTES_PER_OUTCOME}.`);
}
const next = clone(state);
next.phase = "vote";
next.roundsById[next.currentRoundId].votes[slot] += 1;
return next;
}
export function decrementVote(state, slot) {
assertPhase(state, ["vote"], "decrementVote");
if (!["a", "b"].includes(slot)) fail("Vote slot must be a or b.");
if (state.roundsById[state.currentRoundId].votes[slot] === 0) fail("Vote count cannot be negative.");
const next = clone(state);
next.roundsById[next.currentRoundId].votes[slot] -= 1;
return next;
}
export function resolveVote(state, source) {
assertPhase(state, ["vote"], "resolveVote");
const next = clone(state);
const round = next.roundsById[next.currentRoundId];
if (round.votes.a + round.votes.b === 0) fail("Record at least one vote before resolution.");
const recipe = source.recipes.find((item) => item.id === round.recipeId);
if (!recipe) fail("Round recipe is unavailable.");
const winner = round.votes.a === round.votes.b ? recipe.tieWinnerSlot : (round.votes.a > round.votes.b ? "a" : "b");
round.winnerSlot = winner;
round.resolution = round.votes.a === round.votes.b ? "authored-tie-rule" : "votes";
round.candidateNodeIds.forEach((id, index) => {
next.nodesById[id].status = (index === (winner === "a" ? 0 : 1)) ? "winner" : "not-selected";
});
next.activeNodeId = round.candidateNodeIds[winner === "a" ? 0 : 1];
next.inspectedNodeId = next.activeNodeId;
next.phase = "resolved";
return next;
}
export function continueFromWinner(state, source) {
assertPhase(state, ["resolved"], "continueFromWinner");
const next = clone(state);
next.currentRoundId = null;
next.selectedConstraintId = null;
next.compareSelection = null;
next.phase = constraintsFor(source, next.nodesById[next.activeNodeId].artifactId).length ? "choose" : "complete";
return next;
}
export function inspectNode(state, nodeId) {
if (!state.nodesById[nodeId]) fail("Cannot inspect an unknown node.");
const next = clone(state);
next.inspectedNodeId = nodeId;
return next;
}
export function resetSession(_state, source) {
return createSession(source);
}
export function historyPreorder(state) {
const children = new Map();
for (const node of Object.values(state.nodesById)) {
if (node.parentNodeId) {
const list = children.get(node.parentNodeId) ?? [];
list.push(node);
children.set(node.parentNodeId, list);
}
}
for (const list of children.values()) list.sort((a, b) => (a.outcomeSlot ?? "").localeCompare(b.outcomeSlot ?? ""));
const result = [];
const visit = (id) => {
result.push(state.nodesById[id]);
for (const child of children.get(id) ?? []) visit(child.nodeId);
};
visit(state.seedNodeId);
return result;
}
export function validateSession(state, source) {
try {
if (!state || state.schemaVersion !== SCHEMA_VERSION || state.catalogVersion !== source.version) return false;
if (!["choose", "compare", "vote", "resolved", "complete"].includes(state.phase)) return false;
if (!state.nodesById || typeof state.nodesById !== "object" || Array.isArray(state.nodesById)) return false;
if (!state.roundsById || typeof state.roundsById !== "object" || Array.isArray(state.roundsById)) return false;
const seedId = seedNodeId(source.seedArtifactId);
if (state.seedNodeId !== seedId || !state.nodesById[state.activeNodeId] || !state.nodesById[state.inspectedNodeId]) return false;
const seed = state.nodesById[seedId];
if (!seed || seed.nodeId !== seedId || seed.parentNodeId !== null ||
seed.artifactId !== source.seedArtifactId || seed.recipeId !== null ||
seed.outcomeSlot !== null || seed.status !== "seed" || seed.depth !== 0) return false;
const artifactIds = new Set(source.artifacts.map(({ id }) => id));
const expectedCandidateIds = new Set();
const roundParentIds = new Set();
for (const [mapId, round] of Object.entries(state.roundsById)) {
if (!round || round.roundId !== mapId) return false;
const parent = state.nodesById[round.parentNodeId];
const recipe = source.recipes.find(({ id }) => id === round.recipeId);
if (!parent || !["seed", "winner"].includes(parent.status) || !recipe ||
recipe.parentArtifactId !== parent.artifactId || roundParentIds.has(parent.nodeId)) return false;
roundParentIds.add(parent.nodeId);
const expectedRoundId = roundIdFor(parent.nodeId, recipe.id);
const expectedIds = ["a", "b"].map((slot) => candidateNodeId(expectedRoundId, slot));
if (mapId !== expectedRoundId || !Array.isArray(round.candidateNodeIds) ||
round.candidateNodeIds.length !== 2 ||
round.candidateNodeIds[0] !== expectedIds[0] || round.candidateNodeIds[1] !== expectedIds[1]) return false;
if (!round.votes || !Number.isInteger(round.votes.a) || round.votes.a < 0 ||
round.votes.a > MAX_VOTES_PER_OUTCOME ||
!Number.isInteger(round.votes.b) || round.votes.b < 0 ||
round.votes.b > MAX_VOTES_PER_OUTCOME) return false;
const resolved = round.winnerSlot !== null || round.resolution !== null;
if (resolved) {
if (!["a", "b"].includes(round.winnerSlot) ||
!["votes", "authored-tie-rule"].includes(round.resolution)) return false;
const expectedWinner = round.votes.a === round.votes.b
? recipe.tieWinnerSlot : (round.votes.a > round.votes.b ? "a" : "b");
const expectedResolution = round.votes.a === round.votes.b ? "authored-tie-rule" : "votes";
if (round.winnerSlot !== expectedWinner || round.resolution !== expectedResolution) return false;
} else if (round.winnerSlot !== null || round.resolution !== null) return false;
for (let index = 0; index < expectedIds.length; index += 1) {
const id = expectedIds[index];
const slot = index === 0 ? "a" : "b";
const node = state.nodesById[id];
const outcome = recipe.outcomes[index];
if (!node || node.nodeId !== id || node.parentNodeId !== parent.nodeId ||
node.artifactId !== outcome.artifactId || node.recipeId !== recipe.id ||
node.outcomeSlot !== slot || node.depth !== parent.depth + 1) return false;
const expectedStatus = resolved ? (slot === round.winnerSlot ? "winner" : "not-selected") : "candidate";
if (node.status !== expectedStatus) return false;
expectedCandidateIds.add(id);
}
}
for (const [mapId, node] of Object.entries(state.nodesById)) {
if (!node || node.nodeId !== mapId || !artifactIds.has(node.artifactId) ||
!Number.isInteger(node.depth) || node.depth < 0) return false;
if (mapId !== seedId && !expectedCandidateIds.has(mapId)) return false;
}
const currentPhases = ["compare", "vote", "resolved"];
const currentRound = state.currentRoundId === null ? null : state.roundsById[state.currentRoundId];
if (currentPhases.includes(state.phase) !== Boolean(currentRound)) return false;
for (const [id, round] of Object.entries(state.roundsById)) {
const isCurrentUnresolved = id === state.currentRoundId && ["compare", "vote"].includes(state.phase);
if (isCurrentUnresolved !== (round.winnerSlot === null)) return false;
}
if (state.phase === "choose") {
if (state.currentRoundId !== null || state.compareSelection !== null) return false;
const available = constraintsFor(source, state.nodesById[state.activeNodeId].artifactId);
if (!available.length || (state.selectedConstraintId !== null &&
!available.some(({ id }) => id === state.selectedConstraintId))) return false;
} else if (state.phase === "complete") {
if (state.currentRoundId !== null || state.selectedConstraintId !== null ||
state.compareSelection !== null ||
constraintsFor(source, state.nodesById[state.activeNodeId].artifactId).length) return false;
} else {
const recipe = source.recipes.find(({ id }) => id === currentRound.recipeId);
if (state.selectedConstraintId !== recipe.constraintId || !["a", "b"].includes(state.compareSelection)) return false;
if (state.phase === "compare" && (currentRound.votes.a !== 0 || currentRound.votes.b !== 0)) return false;
if (["vote", "resolved"].includes(state.phase) && currentRound.votes.a + currentRound.votes.b === 0) return false;
if (state.phase === "resolved") {
const winnerId = currentRound.candidateNodeIds[currentRound.winnerSlot === "a" ? 0 : 1];
if (state.activeNodeId !== winnerId) return false;
} else {
if (state.activeNodeId !== currentRound.parentNodeId) return false;
}
}
const active = state.nodesById[state.activeNodeId];
if (active.status !== "seed" && active.status !== "winner") return false;
return true;
} catch {
return false;
}
}