/home/techb158/cosmic.abdallabala.com/src/services
Edit: /home/techb158/cosmic.abdallabala.com/src/services/gateWorkflowService.js (7296B)
const { list, findById, insert, update, audit } = require("../storage/jsonDatabase.js");
const GATE_DECISIONS = ["Approved", "Rejected", "Accepted", "Needs changes"];
const CRITERION_REVIEW_STATUSES = ["Open", "Reviewed", "Evidence requested", "Accepted", "Rejected"];
function safeParseJson(value) {
if (!value || typeof value !== "string") return value || null;
try {
return JSON.parse(value);
} catch (_error) {
return value;
}
}
function sortDescByDate(a, b, field) {
return String(b[field] || b.created_at || "").localeCompare(String(a[field] || a.created_at || ""));
}
function normalizeDecision(value) {
const decision = value || "Needs changes";
if (!GATE_DECISIONS.includes(decision)) {
throw new Error(`decision must be one of: ${GATE_DECISIONS.join(", ")}`);
}
return decision;
}
function normalizeCriterionStatus(value) {
const status = value || "Reviewed";
if (!CRITERION_REVIEW_STATUSES.includes(status)) {
throw new Error(`reviewer_status must be one of: ${CRITERION_REVIEW_STATUSES.join(", ")}`);
}
return status;
}
function toCriterionView(row) {
return {
id: row.id,
gateId: row.gate_id,
name: row.name,
actualValue: row.actual_value,
expectedRule: row.expected_rule,
status: row.status,
blocking: Boolean(row.blocking),
evidence: row.evidence || "",
evidenceTitle: row.evidence_title || "",
evidenceUri: row.evidence_uri || "",
reviewerStatus: row.reviewer_status || "Open",
reviewerNote: row.reviewer_note || "",
createdAt: row.created_at,
updatedAt: row.updated_at
};
}
function toDecisionView(row) {
return {
id: row.id,
gateId: row.gate_id,
decision: row.decision,
reason: row.reason || "",
approvedByUserId: row.approved_by_user_id || null,
reviewerName: row.reviewer_display_name || row.approved_by_user_id || "Governance Reviewer",
approvedAt: row.approved_at || row.created_at,
createdAt: row.created_at,
updatedAt: row.updated_at
};
}
function toGateView(database, gate) {
const criteria = list(database, "gate_criteria", row => row.gate_id === gate.id)
.sort((a, b) => String(a.name || a.id).localeCompare(String(b.name || b.id)))
.map(toCriterionView);
const decisions = list(database, "gate_decisions", row => row.gate_id === gate.id)
.sort((a, b) => sortDescByDate(a, b, "approved_at"))
.map(toDecisionView);
const blockingCriteria = criteria.filter(item => item.blocking || item.status === "Fail").length;
const reviewerNotes = criteria.filter(item => item.reviewerNote).length;
const evidenceLinks = criteria.filter(item => item.evidenceUri).length;
return {
id: gate.id,
projectId: gate.project_id,
status: gate.status,
reviewStatus: gate.review_status || "Pending review",
summary: gate.summary || "",
evaluatedByUserId: gate.evaluated_by_user_id || "system",
evaluatedAt: gate.evaluated_at || gate.created_at,
reviewedBy: gate.reviewed_by_display_name || "",
reviewedAt: gate.reviewed_at || null,
latestDecision: decisions[0] || null,
blockingCriteria,
reviewerNotes,
evidenceLinks,
criteria,
decisions,
createdAt: gate.created_at,
updatedAt: gate.updated_at
};
}
class GateWorkflowService {
constructor(database) {
this.database = database;
}
listGateHistory(projectId) {
const db = this.database.read();
return list(db, "deployment_gates", gate => gate.project_id === projectId)
.sort((a, b) => sortDescByDate(a, b, "evaluated_at"))
.map(gate => toGateView(db, gate));
}
getGate(gateId) {
const db = this.database.read();
const gate = findById(db, "deployment_gates", gateId);
return gate ? toGateView(db, gate) : null;
}
addDecision(gateId, payload, actorUserId = "system") {
if (!payload || typeof payload !== "object") throw new Error("Gate decision payload is required");
const decision = normalizeDecision(payload.decision);
const reason = payload.reason || payload.note || "";
const reviewerName = payload.reviewerName || payload.reviewer_display_name || "Governance Reviewer";
return this.database.transaction(db => {
const gate = findById(db, "deployment_gates", gateId);
if (!gate) return null;
const approvedAt = new Date().toISOString();
const row = insert(db, "gate_decisions", {
gate_id: gateId,
decision,
reason,
approved_by_user_id: actorUserId,
reviewer_display_name: reviewerName,
approved_at: approvedAt
}, "GDEC");
const updatedGate = update(db, "deployment_gates", gateId, {
review_status: decision,
reviewed_by_display_name: reviewerName,
reviewed_at: approvedAt
});
audit(db, {
project_id: gate.project_id,
actor_user_id: actorUserId,
entity_type: "GateDecision",
entity_id: row.id,
action: "create",
after_json: row
});
return toGateView(db, updatedGate);
});
}
updateCriterion(criterionId, payload, actorUserId = "system") {
if (!payload || typeof payload !== "object") throw new Error("Gate criterion payload is required");
const patch = {};
if (payload.reviewerNote !== undefined || payload.reviewer_note !== undefined) {
patch.reviewer_note = payload.reviewerNote || payload.reviewer_note || "";
}
if (payload.reviewerStatus !== undefined || payload.reviewer_status !== undefined) {
patch.reviewer_status = normalizeCriterionStatus(payload.reviewerStatus || payload.reviewer_status);
}
if (payload.evidenceTitle !== undefined || payload.evidence_title !== undefined) {
patch.evidence_title = payload.evidenceTitle || payload.evidence_title || "";
}
if (payload.evidenceUri !== undefined || payload.evidence_uri !== undefined) {
patch.evidence_uri = payload.evidenceUri || payload.evidence_uri || "";
}
return this.database.transaction(db => {
const before = findById(db, "gate_criteria", criterionId);
if (!before) return null;
const criterion = update(db, "gate_criteria", criterionId, patch);
const gate = findById(db, "deployment_gates", criterion.gate_id);
audit(db, {
project_id: gate ? gate.project_id : "unknown",
actor_user_id: actorUserId,
entity_type: "GateCriterion",
entity_id: criterionId,
action: "update",
before_json: before,
after_json: criterion
});
return toCriterionView(criterion);
});
}
listAuditEvents(projectId, limit = 80) {
const db = this.database.read();
return list(db, "audit_events", event => event.project_id === projectId)
.sort((a, b) => sortDescByDate(a, b, "created_at"))
.slice(0, Number(limit) || 80)
.map(event => ({
id: event.id,
projectId: event.project_id,
actorUserId: event.actor_user_id,
entityType: event.entity_type,
entityId: event.entity_id,
action: event.action,
before: safeParseJson(event.before_json),
after: safeParseJson(event.after_json),
createdAt: event.created_at
}));
}
}
module.exports = {
GateWorkflowService,
toGateView,
GATE_DECISIONS,
CRITERION_REVIEW_STATUSES
};