Add untracked runtime extension files to git
This commit is contained in:
parent
c3b202dd4c
commit
8f02524fd7
8 changed files with 1223 additions and 0 deletions
|
|
@ -0,0 +1,162 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
buildWorkerContextPackProjection,
|
||||
validateWorkerContextPackProjection,
|
||||
} from "../uok/context-pack-projection.js";
|
||||
|
||||
test("buildProjection_when_contract_has_worker_context_returns_droid_sections", () => {
|
||||
const projection = buildWorkerContextPackProjection({
|
||||
milestoneId: "M001",
|
||||
sliceId: "S01",
|
||||
taskId: "T01",
|
||||
unitId: "M001/S01/T01",
|
||||
unitType: "execute-task",
|
||||
purpose: "Protect worker dispatch context.",
|
||||
consumer: "Autonomous worker prompt adapter.",
|
||||
preconditions: ["DB dispatch preview selected this task."],
|
||||
expectedBehavior: ["Worker receives bounded context."],
|
||||
verificationSteps: ["Run focused projection tests."],
|
||||
fulfills: ["REQ-worker-context-pack"],
|
||||
assertionIds: ["ASSERT-db-remains-canonical"],
|
||||
allowedCommands: ["npm run test:unit"],
|
||||
allowedServices: ["git"],
|
||||
workerSkill: {
|
||||
id: "worker-context",
|
||||
name: "Worker Context",
|
||||
instructions: ["Use projected context as read-only prompt input."],
|
||||
},
|
||||
library: {
|
||||
references: ["docs/specs/sf-operating-model.md"],
|
||||
files: ["src/resources/extensions/sf/uok/context-pack-projection.js"],
|
||||
assumptions: ["Runtime wiring happens later."],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(projection.origin, "worker-context-pack-projection");
|
||||
assert.equal(projection.canonicalState.source, "sf-db");
|
||||
assert.equal(projection.canonicalState.projectionOnly, true);
|
||||
assert.deepEqual(
|
||||
Object.keys(projection).filter((key) =>
|
||||
[
|
||||
"services",
|
||||
"validationContract",
|
||||
"unitContract",
|
||||
"workerSkill",
|
||||
"library",
|
||||
].includes(key),
|
||||
),
|
||||
[
|
||||
"services",
|
||||
"validationContract",
|
||||
"unitContract",
|
||||
"workerSkill",
|
||||
"library",
|
||||
],
|
||||
);
|
||||
assert.deepEqual(projection.services.allowedCommands, ["npm run test:unit"]);
|
||||
assert.deepEqual(projection.services.allowedServices, ["git"]);
|
||||
assert.deepEqual(projection.validationContract.preconditions, [
|
||||
"DB dispatch preview selected this task.",
|
||||
]);
|
||||
assert.deepEqual(projection.validationContract.expectedBehavior, [
|
||||
"Worker receives bounded context.",
|
||||
]);
|
||||
assert.deepEqual(projection.validationContract.verificationSteps, [
|
||||
"Run focused projection tests.",
|
||||
]);
|
||||
assert.deepEqual(projection.validationContract.fulfills, [
|
||||
"REQ-worker-context-pack",
|
||||
]);
|
||||
assert.deepEqual(projection.validationContract.assertionIds, [
|
||||
"ASSERT-db-remains-canonical",
|
||||
]);
|
||||
assert.equal(
|
||||
projection.unitContract.purpose,
|
||||
"Protect worker dispatch context.",
|
||||
);
|
||||
assert.equal(projection.workerSkill.id, "worker-context");
|
||||
assert.deepEqual(projection.library.references, [
|
||||
"docs/specs/sf-operating-model.md",
|
||||
]);
|
||||
});
|
||||
|
||||
test("buildProjection_when_contract_uses_nested_sections_preserves_allowed_surfaces", () => {
|
||||
const projection = buildWorkerContextPackProjection({
|
||||
validationContract: {
|
||||
preconditions: "Canonical DB unit exists.",
|
||||
expectedBehavior: "Projection contains prompt-safe shape.",
|
||||
verificationSteps: "Validate the pack before rendering.",
|
||||
assertionIds: ["ASSERT-shape"],
|
||||
},
|
||||
unitContract: {
|
||||
unitId: "M010/S02/T03",
|
||||
unitType: "feature-contract",
|
||||
fulfills: ["REQ-context"],
|
||||
},
|
||||
services: {
|
||||
allowedCommands: ["npm test", "npm test"],
|
||||
allowedServices: ["sqlite", "git"],
|
||||
},
|
||||
workerSkill: {
|
||||
allowedCommands: ["npx vitest run"],
|
||||
allowedServices: ["sqlite"],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(projection.services.allowedCommands, [
|
||||
"npm test",
|
||||
"npx vitest run",
|
||||
]);
|
||||
assert.deepEqual(projection.services.allowedServices, ["sqlite", "git"]);
|
||||
assert.deepEqual(projection.validationContract.preconditions, [
|
||||
"Canonical DB unit exists.",
|
||||
]);
|
||||
assert.deepEqual(projection.validationContract.assertionIds, [
|
||||
"ASSERT-shape",
|
||||
]);
|
||||
assert.deepEqual(projection.validationContract.fulfills, ["REQ-context"]);
|
||||
assert.equal(projection.unitRef.unitId, "M010/S02/T03");
|
||||
assert.equal(projection.unitRef.unitType, "feature-contract");
|
||||
});
|
||||
|
||||
test("buildProjection_when_optional_fields_missing_returns_empty_arrays_not_runtime_reads", () => {
|
||||
const projection = buildWorkerContextPackProjection({
|
||||
unitId: "M001/S01/T99",
|
||||
});
|
||||
|
||||
assert.deepEqual(projection.services.allowedCommands, []);
|
||||
assert.deepEqual(projection.services.allowedServices, []);
|
||||
assert.deepEqual(projection.validationContract.preconditions, []);
|
||||
assert.deepEqual(projection.validationContract.expectedBehavior, []);
|
||||
assert.deepEqual(projection.validationContract.verificationSteps, []);
|
||||
assert.deepEqual(projection.library.files, []);
|
||||
assert.match(projection.canonicalState.note, /projection only/);
|
||||
assert.equal(validateWorkerContextPackProjection(projection).valid, true);
|
||||
});
|
||||
|
||||
test("validateProjection_when_required_section_missing_reports_invalid_projection", () => {
|
||||
const projection = buildWorkerContextPackProjection({
|
||||
preconditions: ["DB unit exists."],
|
||||
});
|
||||
delete projection.validationContract;
|
||||
|
||||
const result = validateWorkerContextPackProjection(projection);
|
||||
|
||||
assert.equal(result.valid, false);
|
||||
assert.ok(result.issues.includes("validationContract section is required"));
|
||||
});
|
||||
|
||||
test("validateProjection_when_array_fields_are_not_arrays_reports_field_issue", () => {
|
||||
const projection = buildWorkerContextPackProjection({
|
||||
allowedCommands: ["npm run test:unit"],
|
||||
});
|
||||
projection.services.allowedCommands = "npm run test:unit";
|
||||
|
||||
const result = validateWorkerContextPackProjection(projection);
|
||||
|
||||
assert.equal(result.valid, false);
|
||||
assert.ok(
|
||||
result.issues.includes("services.allowedCommands must be an array"),
|
||||
);
|
||||
});
|
||||
104
src/resources/extensions/sf/tests/model-role-policy.test.mjs
Normal file
104
src/resources/extensions/sf/tests/model-role-policy.test.mjs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { describe, test } from "vitest";
|
||||
|
||||
import {
|
||||
ModelRolePolicyValidationError,
|
||||
normalizeRolePolicies,
|
||||
normalizeRolePolicy,
|
||||
validateRolePolicy,
|
||||
} from "../uok/model-role-policy.js";
|
||||
|
||||
describe("model role policy", () => {
|
||||
test("normalizeRolePolicy_when_role_has_no_override_returns_auto_policy_with_symbolic_constraints", () => {
|
||||
assert.deepEqual(normalizeRolePolicy("worker"), {
|
||||
role: "worker",
|
||||
mode: "auto",
|
||||
constraints: ["coding"],
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizeRolePolicy_when_constraints_use_alias_casing_returns_canonical_constraint_names", () => {
|
||||
assert.deepEqual(
|
||||
normalizeRolePolicy("validator", {
|
||||
mode: "auto",
|
||||
constraints: ["STRICT_JSON", "review", "strict-json"],
|
||||
}),
|
||||
{
|
||||
role: "validator",
|
||||
mode: "auto",
|
||||
constraints: ["strict-json", "review"],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("validateRolePolicy_when_constraint_unknown_returns_clear_validation_error", () => {
|
||||
const result = validateRolePolicy("reviewer", {
|
||||
mode: "auto",
|
||||
constraints: ["review", "vision-model"],
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.errors[0], /unsupported constraint "vision-model"/);
|
||||
assert.match(result.errors[0], /coding, review, cheap, long-context/);
|
||||
assert.equal(result.policy, null);
|
||||
});
|
||||
|
||||
test("normalizeRolePolicy_when_policy_contains_concrete_model_id_rejects_durable_route_config", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeRolePolicy("orchestrator", {
|
||||
mode: "auto",
|
||||
modelId: "concrete-route",
|
||||
}),
|
||||
(error) => {
|
||||
assert.equal(error instanceof ModelRolePolicyValidationError, true);
|
||||
assert.match(error.message, /durable policy cannot include "modelId"/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizeRolePolicy_when_mode_is_concrete_model_rejects_non_auto_intent", () => {
|
||||
assert.throws(
|
||||
() => normalizeRolePolicy("worker", { model: "concrete-route" }),
|
||||
/must use mode "auto"/,
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizeRolePolicies_when_partial_policy_supplied_projects_all_supported_roles", () => {
|
||||
assert.deepEqual(
|
||||
normalizeRolePolicies({
|
||||
Reviewer: { mode: "auto", constraints: ["cheap", "review"] },
|
||||
}),
|
||||
{
|
||||
orchestrator: {
|
||||
role: "orchestrator",
|
||||
mode: "auto",
|
||||
constraints: ["long-context", "strict-json"],
|
||||
},
|
||||
worker: {
|
||||
role: "worker",
|
||||
mode: "auto",
|
||||
constraints: ["coding"],
|
||||
},
|
||||
validator: {
|
||||
role: "validator",
|
||||
mode: "auto",
|
||||
constraints: ["strict-json", "review"],
|
||||
},
|
||||
reviewer: {
|
||||
role: "reviewer",
|
||||
mode: "auto",
|
||||
constraints: ["cheap", "review"],
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizeRolePolicies_when_role_unknown_rejects_projection", () => {
|
||||
assert.throws(
|
||||
() => normalizeRolePolicies({ planner: { mode: "auto" } }),
|
||||
/unsupported model role "planner"/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, test } from "vitest";
|
||||
import {
|
||||
emitModelAutoResolvedEvent,
|
||||
modelRoleForUnitType,
|
||||
} from "../uok/model-route-evidence.js";
|
||||
|
||||
const tmpRoots = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tmpRoots.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function makeProject() {
|
||||
const root = mkdtempSync(join(tmpdir(), "sf-model-route-evidence-"));
|
||||
tmpRoots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
function readAuditEvents(project) {
|
||||
const path = join(project, ".sf", "audit", "events.jsonl");
|
||||
return readFileSync(path, "utf-8")
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line));
|
||||
}
|
||||
|
||||
test("modelRoleForUnitType_when_validation_unit_returns_validator", () => {
|
||||
assert.equal(modelRoleForUnitType("validation-gate"), "validator");
|
||||
assert.equal(modelRoleForUnitType("run-verification"), "validator");
|
||||
});
|
||||
|
||||
test("modelRoleForUnitType_when_review_unit_returns_reviewer", () => {
|
||||
assert.equal(modelRoleForUnitType("scrutiny-review"), "reviewer");
|
||||
});
|
||||
|
||||
test("modelRoleForUnitType_when_regular_unit_returns_worker", () => {
|
||||
assert.equal(modelRoleForUnitType("execute-task"), "worker");
|
||||
});
|
||||
|
||||
test("emitModelAutoResolvedEvent_records_auto_policy_and_resolved_route", () => {
|
||||
const project = makeProject();
|
||||
|
||||
emitModelAutoResolvedEvent(project, {
|
||||
traceId: "trace-model-1",
|
||||
unitType: "execute-task",
|
||||
unitId: "M001/S01/T01",
|
||||
resolvedModel: {
|
||||
provider: "zai",
|
||||
id: "glm-5.1",
|
||||
api: "openai-chat",
|
||||
apiKey: "secret-key",
|
||||
},
|
||||
authMode: "apiKey",
|
||||
routingReason: "auto selector",
|
||||
routing: {
|
||||
tier: "heavy",
|
||||
modelDowngraded: false,
|
||||
},
|
||||
tokenUsage: 1234,
|
||||
});
|
||||
|
||||
const [event] = readAuditEvents(project);
|
||||
assert.equal(event.schemaVersion, 1);
|
||||
assert.equal(event.category, "model-routing");
|
||||
assert.equal(event.type, "model-auto-resolved");
|
||||
assert.equal(event.traceId, "trace-model-1");
|
||||
assert.equal(event.turnId, "M001/S01/T01");
|
||||
assert.equal(event.payload.role, "worker");
|
||||
assert.deepEqual(event.payload.requestedPolicy, {
|
||||
mode: "auto",
|
||||
constraints: [],
|
||||
});
|
||||
assert.equal(event.payload.resolvedProvider, "zai");
|
||||
assert.equal(event.payload.resolvedModel, "glm-5.1");
|
||||
assert.equal(event.payload.authMode, "apiKey");
|
||||
assert.deepEqual(event.payload.routeSnapshot.requestedPolicy, {
|
||||
mode: "auto",
|
||||
constraints: [],
|
||||
});
|
||||
assert.equal(event.payload.routeSnapshot.route.provider, "zai");
|
||||
assert.equal(event.payload.routeSnapshot.route.model, "glm-5.1");
|
||||
assert.equal(event.payload.routeSnapshot.route.byok, true);
|
||||
assert.equal(event.payload.routeSnapshot.route.apiKey, undefined);
|
||||
assert.equal(event.payload.routingReason, "auto selector");
|
||||
assert.equal(event.payload.tokenUsage, 1234);
|
||||
});
|
||||
107
src/resources/extensions/sf/tests/unit-handoff.test.mjs
Normal file
107
src/resources/extensions/sf/tests/unit-handoff.test.mjs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* unit-handoff.test.mjs - unit closeout schema contracts.
|
||||
*
|
||||
* Purpose: prove SF preserves useful Droid handoff fields as validated
|
||||
* structured data that follow-on autonomous units can trust.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
normalizeUnitHandoff,
|
||||
validateUnitHandoff,
|
||||
} from "../uok/unit-handoff.js";
|
||||
|
||||
test("normalizeUnitHandoff_when_payload_has_droid_closeout_fields_returns_canonical_contract", () => {
|
||||
const handoff = normalizeUnitHandoff({
|
||||
summary: " Added unit handoff schema. ",
|
||||
filesChanged: [" src/resources/extensions/sf/uok/unit-handoff.js "],
|
||||
testsAdded: [" src/resources/extensions/sf/tests/unit-handoff.test.mjs "],
|
||||
commandsRun: [
|
||||
{
|
||||
command: " npm run test:unit -- unit-handoff.test.mjs ",
|
||||
status: "passed",
|
||||
exitCode: 0,
|
||||
cwd: " /repo ",
|
||||
notes: " focused schema test ",
|
||||
durationMs: 124,
|
||||
},
|
||||
],
|
||||
knownFailures: [" none "],
|
||||
leftUndone: [" wire into finalizer later "],
|
||||
fulfilledAssertions: [" malformed commands are rejected "],
|
||||
verificationStatus: "passed",
|
||||
});
|
||||
|
||||
assert.deepEqual(handoff, {
|
||||
summary: "Added unit handoff schema.",
|
||||
filesChanged: ["src/resources/extensions/sf/uok/unit-handoff.js"],
|
||||
testsAdded: ["src/resources/extensions/sf/tests/unit-handoff.test.mjs"],
|
||||
commandsRun: [
|
||||
{
|
||||
command: "npm run test:unit -- unit-handoff.test.mjs",
|
||||
status: "passed",
|
||||
exitCode: 0,
|
||||
cwd: "/repo",
|
||||
notes: "focused schema test",
|
||||
durationMs: 124,
|
||||
},
|
||||
],
|
||||
knownFailures: ["none"],
|
||||
leftUndone: ["wire into finalizer later"],
|
||||
fulfilledAssertions: ["malformed commands are rejected"],
|
||||
verificationStatus: "passed",
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizeUnitHandoff_when_optional_fields_missing_returns_empty_arrays_and_unknown_status", () => {
|
||||
assert.deepEqual(normalizeUnitHandoff({ summary: "No changes needed." }), {
|
||||
summary: "No changes needed.",
|
||||
filesChanged: [],
|
||||
testsAdded: [],
|
||||
commandsRun: [],
|
||||
knownFailures: [],
|
||||
leftUndone: [],
|
||||
fulfilledAssertions: [],
|
||||
verificationStatus: "unknown",
|
||||
});
|
||||
});
|
||||
|
||||
test("validateUnitHandoff_when_array_field_is_not_array_reports_clear_field_error", () => {
|
||||
const result = validateUnitHandoff({
|
||||
summary: "Bad handoff.",
|
||||
filesChanged: "src/file.js",
|
||||
testsAdded: [],
|
||||
commandsRun: [],
|
||||
knownFailures: [],
|
||||
leftUndone: [],
|
||||
fulfilledAssertions: [],
|
||||
verificationStatus: "failed",
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.deepEqual(result.errors, ["filesChanged must be an array"]);
|
||||
});
|
||||
|
||||
test("normalizeUnitHandoff_when_command_entry_is_malformed_rejects_with_command_path", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeUnitHandoff({
|
||||
summary: "Bad command.",
|
||||
commandsRun: [{ status: "passed", exitCode: 0 }],
|
||||
}),
|
||||
/Invalid unit handoff: commandsRun\[0\]\.command must be a non-empty string/,
|
||||
);
|
||||
});
|
||||
|
||||
test("validateUnitHandoff_when_command_entry_has_invalid_status_and_exit_code_reports_both_errors", () => {
|
||||
const result = validateUnitHandoff({
|
||||
commandsRun: [{ command: "npm test", status: "green", exitCode: -1 }],
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.deepEqual(result.errors, [
|
||||
"commandsRun[0].status must be one of passed, failed, skipped, not-run, unknown",
|
||||
"commandsRun[0].exitCode must be a non-negative integer",
|
||||
]);
|
||||
});
|
||||
226
src/resources/extensions/sf/uok/context-pack-projection.js
Normal file
226
src/resources/extensions/sf/uok/context-pack-projection.js
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
/**
|
||||
* context-pack-projection.js - worker context pack projection shapes.
|
||||
*
|
||||
* Purpose: expose a Droid-like worker context pack view without making that
|
||||
* view executable or canonical; SF DB/runtime records remain the source of
|
||||
* truth for dispatch, validation, and lifecycle state.
|
||||
*
|
||||
* Consumer: UOK projection tests and future worker prompt/rendering adapters.
|
||||
*/
|
||||
|
||||
const PROJECTION_SCHEMA_VERSION = 1;
|
||||
const REQUIRED_SECTIONS = [
|
||||
"services",
|
||||
"validationContract",
|
||||
"unitContract",
|
||||
"workerSkill",
|
||||
"library",
|
||||
];
|
||||
|
||||
function stringOrNull(value) {
|
||||
return typeof value === "string" && value.trim().length > 0
|
||||
? value.trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
function stringList(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => stringOrNull(item))
|
||||
.filter((item) => item !== null);
|
||||
}
|
||||
const single = stringOrNull(value);
|
||||
return single === null ? [] : [single];
|
||||
}
|
||||
|
||||
function idList(...values) {
|
||||
return [...new Set(values.flatMap((value) => stringList(value)))];
|
||||
}
|
||||
|
||||
function objectOrEmpty(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value
|
||||
: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Droid-like worker context pack projection from a unit or feature contract.
|
||||
*
|
||||
* Purpose: give workers a stable read model with service, validation, unit,
|
||||
* skill, and library sections while preventing prompt-facing context from
|
||||
* becoming authoritative SF state.
|
||||
*
|
||||
* Consumer: worker context pack tests and future prompt projection adapters.
|
||||
*
|
||||
* @param {Record<string, unknown>} contract
|
||||
* @returns {Record<string, unknown>}
|
||||
*/
|
||||
export function buildWorkerContextPackProjection(contract = {}) {
|
||||
const source = objectOrEmpty(contract);
|
||||
const validation = objectOrEmpty(source.validationContract);
|
||||
const unit = objectOrEmpty(source.unitContract);
|
||||
const workerSkill = objectOrEmpty(source.workerSkill);
|
||||
const library = objectOrEmpty(source.library);
|
||||
const services = objectOrEmpty(source.services);
|
||||
|
||||
const fulfills = idList(
|
||||
source.fulfills,
|
||||
source.fulfillmentIds,
|
||||
unit.fulfills,
|
||||
);
|
||||
const assertions = idList(
|
||||
source.assertions,
|
||||
source.assertionIds,
|
||||
validation.assertions,
|
||||
validation.assertionIds,
|
||||
);
|
||||
const allowedCommands = idList(
|
||||
source.allowedCommands,
|
||||
services.allowedCommands,
|
||||
workerSkill.allowedCommands,
|
||||
);
|
||||
const allowedServices = idList(
|
||||
source.allowedServices,
|
||||
services.allowedServices,
|
||||
workerSkill.allowedServices,
|
||||
);
|
||||
|
||||
return {
|
||||
schemaVersion: PROJECTION_SCHEMA_VERSION,
|
||||
origin: "worker-context-pack-projection",
|
||||
canonicalState: {
|
||||
source: "sf-db",
|
||||
runtimeAuthority: "sf-runtime",
|
||||
projectionOnly: true,
|
||||
note: "This object is a worker-facing projection only; DB/runtime state remains canonical.",
|
||||
},
|
||||
unitRef: {
|
||||
milestoneId: stringOrNull(source.milestoneId),
|
||||
sliceId: stringOrNull(source.sliceId),
|
||||
taskId: stringOrNull(source.taskId),
|
||||
unitId: stringOrNull(source.unitId ?? unit.unitId),
|
||||
unitType: stringOrNull(source.unitType ?? unit.unitType),
|
||||
featureId: stringOrNull(source.featureId),
|
||||
},
|
||||
services: {
|
||||
allowedServices,
|
||||
allowedCommands,
|
||||
disallowedByDefault: true,
|
||||
notes: stringList(services.notes ?? source.serviceNotes),
|
||||
},
|
||||
validationContract: {
|
||||
preconditions: stringList(
|
||||
validation.preconditions ?? source.preconditions,
|
||||
),
|
||||
expectedBehavior: stringList(
|
||||
validation.expectedBehavior ?? source.expectedBehavior,
|
||||
),
|
||||
verificationSteps: stringList(
|
||||
validation.verificationSteps ?? source.verificationSteps,
|
||||
),
|
||||
assertionIds: assertions,
|
||||
fulfills,
|
||||
},
|
||||
unitContract: {
|
||||
purpose: stringOrNull(unit.purpose ?? source.purpose),
|
||||
consumer: stringOrNull(unit.consumer ?? source.consumer),
|
||||
preconditions: stringList(unit.preconditions ?? source.preconditions),
|
||||
expectedBehavior: stringList(
|
||||
unit.expectedBehavior ?? source.expectedBehavior,
|
||||
),
|
||||
verificationSteps: stringList(
|
||||
unit.verificationSteps ?? source.verificationSteps,
|
||||
),
|
||||
fulfills,
|
||||
assertionIds: assertions,
|
||||
},
|
||||
workerSkill: {
|
||||
id: stringOrNull(workerSkill.id ?? source.workerSkillId),
|
||||
name: stringOrNull(workerSkill.name ?? source.workerSkillName),
|
||||
instructions: stringList(
|
||||
workerSkill.instructions ?? source.workerInstructions,
|
||||
),
|
||||
allowedCommands,
|
||||
allowedServices,
|
||||
},
|
||||
library: {
|
||||
references: stringList(library.references ?? source.references),
|
||||
files: stringList(library.files ?? source.files),
|
||||
assumptions: stringList(library.assumptions ?? source.assumptions),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the worker context pack projection shape without reading runtime state.
|
||||
*
|
||||
* Purpose: catch malformed projection objects before a prompt adapter consumes
|
||||
* them while keeping DB/runtime records authoritative for actual execution.
|
||||
*
|
||||
* Consumer: worker context pack tests and future prompt projection adapters.
|
||||
*
|
||||
* @param {unknown} projection
|
||||
* @returns {{ valid: true; issues: [] } | { valid: false; issues: string[] }}
|
||||
*/
|
||||
export function validateWorkerContextPackProjection(projection) {
|
||||
const issues = [];
|
||||
if (
|
||||
!projection ||
|
||||
typeof projection !== "object" ||
|
||||
Array.isArray(projection)
|
||||
) {
|
||||
return { valid: false, issues: ["projection must be an object"] };
|
||||
}
|
||||
const pack = projection;
|
||||
if (pack.schemaVersion !== PROJECTION_SCHEMA_VERSION) {
|
||||
issues.push("schemaVersion must be 1");
|
||||
}
|
||||
if (pack.origin !== "worker-context-pack-projection") {
|
||||
issues.push("origin must be worker-context-pack-projection");
|
||||
}
|
||||
if (pack.canonicalState?.projectionOnly !== true) {
|
||||
issues.push("canonicalState.projectionOnly must be true");
|
||||
}
|
||||
if (pack.canonicalState?.source !== "sf-db") {
|
||||
issues.push("canonicalState.source must be sf-db");
|
||||
}
|
||||
for (const section of REQUIRED_SECTIONS) {
|
||||
if (!pack[section] || typeof pack[section] !== "object") {
|
||||
issues.push(`${section} section is required`);
|
||||
}
|
||||
}
|
||||
for (const [section, fields] of [
|
||||
["services", ["allowedServices", "allowedCommands"]],
|
||||
[
|
||||
"validationContract",
|
||||
[
|
||||
"preconditions",
|
||||
"expectedBehavior",
|
||||
"verificationSteps",
|
||||
"assertionIds",
|
||||
"fulfills",
|
||||
],
|
||||
],
|
||||
[
|
||||
"unitContract",
|
||||
[
|
||||
"preconditions",
|
||||
"expectedBehavior",
|
||||
"verificationSteps",
|
||||
"assertionIds",
|
||||
"fulfills",
|
||||
],
|
||||
],
|
||||
]) {
|
||||
const value = pack[section];
|
||||
if (!value || typeof value !== "object") continue;
|
||||
for (const field of fields) {
|
||||
if (!Array.isArray(value[field])) {
|
||||
issues.push(`${section}.${field} must be an array`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return issues.length === 0
|
||||
? { valid: true, issues: [] }
|
||||
: { valid: false, issues };
|
||||
}
|
||||
235
src/resources/extensions/sf/uok/model-role-policy.js
Normal file
235
src/resources/extensions/sf/uok/model-role-policy.js
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/**
|
||||
* model-role-policy.js -- role intent projection for autonomous model routing.
|
||||
*
|
||||
* Purpose: preserve durable SF model-routing policy as role-level intent and
|
||||
* symbolic constraints so auto model selection remains the only component that
|
||||
* resolves concrete provider/model IDs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* List the autonomous roles that can carry durable model-routing intent.
|
||||
*
|
||||
* Purpose: keep role-policy validation aligned with SF's autonomous dispatch
|
||||
* vocabulary instead of allowing arbitrary role names to become configuration.
|
||||
*
|
||||
* Consumer: preference/import projection code before passing policy intent to
|
||||
* autonomous model selection.
|
||||
*/
|
||||
export const SUPPORTED_MODEL_ROLES = Object.freeze([
|
||||
"orchestrator",
|
||||
"worker",
|
||||
"validator",
|
||||
"reviewer",
|
||||
]);
|
||||
|
||||
/**
|
||||
* List symbolic constraints that role policy can persist.
|
||||
*
|
||||
* Purpose: allow durable policy to express routing needs without persisting
|
||||
* provider names, model names, or custom model IDs.
|
||||
*
|
||||
* Consumer: role-policy validation and autonomous route evidence writers.
|
||||
*/
|
||||
export const SUPPORTED_MODEL_ROLE_CONSTRAINTS = Object.freeze([
|
||||
"coding",
|
||||
"review",
|
||||
"cheap",
|
||||
"long-context",
|
||||
"byok-allowed",
|
||||
"local-only",
|
||||
"strict-json",
|
||||
]);
|
||||
|
||||
const MODEL_ROLES = new Set(SUPPORTED_MODEL_ROLES);
|
||||
const MODEL_ROLE_CONSTRAINTS = new Set(SUPPORTED_MODEL_ROLE_CONSTRAINTS);
|
||||
const CONCRETE_ROUTE_KEYS = new Set([
|
||||
"provider",
|
||||
"providerId",
|
||||
"modelId",
|
||||
"model_id",
|
||||
"customModelId",
|
||||
"custom_model_id",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Provide SF's default symbolic routing constraints per autonomous role.
|
||||
*
|
||||
* Purpose: encode the useful Droid-style role projection while leaving final
|
||||
* provider/model choice to SF's existing auto model selection.
|
||||
*
|
||||
* Consumer: normalizeRolePolicy when a role has no explicit constraints.
|
||||
*/
|
||||
export const DEFAULT_MODEL_ROLE_CONSTRAINTS = Object.freeze({
|
||||
orchestrator: Object.freeze(["long-context", "strict-json"]),
|
||||
worker: Object.freeze(["coding"]),
|
||||
validator: Object.freeze(["strict-json", "review"]),
|
||||
reviewer: Object.freeze(["review"]),
|
||||
});
|
||||
|
||||
/**
|
||||
* Report invalid durable model-role policy input.
|
||||
*
|
||||
* Purpose: give preference and projection callers one clear error type when a
|
||||
* role policy tries to persist unsupported roles, constraints, or route IDs.
|
||||
*
|
||||
* Consumer: normalizeRolePolicy and normalizeRolePolicies callers that surface
|
||||
* validation failures to users or startup diagnostics.
|
||||
*/
|
||||
export class ModelRolePolicyValidationError extends Error {
|
||||
constructor(errors) {
|
||||
super(`Invalid model role policy: ${errors.join("; ")}`);
|
||||
this.name = "ModelRolePolicyValidationError";
|
||||
this.errors = errors;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRoleName(role) {
|
||||
return String(role ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeConstraintName(constraint) {
|
||||
return String(constraint ?? "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll("_", "-");
|
||||
}
|
||||
|
||||
function constraintsFromPolicy(role, policy) {
|
||||
if (Array.isArray(policy)) return policy;
|
||||
if (
|
||||
policy &&
|
||||
typeof policy === "object" &&
|
||||
Array.isArray(policy.constraints)
|
||||
) {
|
||||
return policy.constraints;
|
||||
}
|
||||
return DEFAULT_MODEL_ROLE_CONSTRAINTS[role] ?? [];
|
||||
}
|
||||
|
||||
function modeFromPolicy(policy) {
|
||||
if (policy === undefined || policy === null) return "auto";
|
||||
if (policy === "auto") return "auto";
|
||||
if (Array.isArray(policy)) return "auto";
|
||||
if (policy && typeof policy === "object") {
|
||||
return policy.mode ?? policy.model ?? "auto";
|
||||
}
|
||||
return policy;
|
||||
}
|
||||
|
||||
function validateOne(role, policy) {
|
||||
const normalizedRole = normalizeRoleName(role);
|
||||
const errors = [];
|
||||
if (!MODEL_ROLES.has(normalizedRole)) {
|
||||
errors.push(
|
||||
`unsupported model role "${String(role)}"; expected one of ${SUPPORTED_MODEL_ROLES.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (policy && typeof policy === "object" && !Array.isArray(policy)) {
|
||||
for (const key of Object.keys(policy)) {
|
||||
if (CONCRETE_ROUTE_KEYS.has(key)) {
|
||||
errors.push(
|
||||
`role "${normalizedRole}" must use mode "auto"; durable policy cannot include "${key}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const mode = modeFromPolicy(policy);
|
||||
if (mode !== "auto") {
|
||||
errors.push(
|
||||
`role "${normalizedRole}" must use mode "auto"; received "${String(mode)}"`,
|
||||
);
|
||||
}
|
||||
const seen = new Set();
|
||||
const constraints = [];
|
||||
for (const rawConstraint of constraintsFromPolicy(normalizedRole, policy)) {
|
||||
const constraint = normalizeConstraintName(rawConstraint);
|
||||
if (!MODEL_ROLE_CONSTRAINTS.has(constraint)) {
|
||||
errors.push(
|
||||
`role "${normalizedRole}" has unsupported constraint "${String(rawConstraint)}"; expected one of ${SUPPORTED_MODEL_ROLE_CONSTRAINTS.join(", ")}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!seen.has(constraint)) {
|
||||
seen.add(constraint);
|
||||
constraints.push(constraint);
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: errors.length === 0,
|
||||
errors,
|
||||
policy:
|
||||
errors.length === 0
|
||||
? { role: normalizedRole, mode: "auto", constraints }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate one durable autonomous model role policy.
|
||||
*
|
||||
* Purpose: let callers reject provider-specific routing data before it reaches
|
||||
* SF's durable preference or planning state.
|
||||
*
|
||||
* Consumer: preference projection, Droid-pattern importers, and tests that need
|
||||
* non-throwing diagnostics.
|
||||
*/
|
||||
export function validateRolePolicy(role, policy = undefined) {
|
||||
return validateOne(role, policy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize one durable autonomous model role policy.
|
||||
*
|
||||
* Purpose: produce the canonical role-policy shape consumed by autonomous model
|
||||
* selection while guaranteeing concrete route IDs are not persisted.
|
||||
*
|
||||
* Consumer: startup preference loading and any future projection step that feeds
|
||||
* role intent into UOK model route evidence.
|
||||
*/
|
||||
export function normalizeRolePolicy(role, policy = undefined) {
|
||||
const result = validateOne(role, policy);
|
||||
if (!result.ok) throw new ModelRolePolicyValidationError(result.errors);
|
||||
return result.policy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a role-to-policy map for autonomous model routing.
|
||||
*
|
||||
* Purpose: project partial durable policy into a complete role map where every
|
||||
* supported autonomous role requests SF's existing auto model selection.
|
||||
*
|
||||
* Consumer: SF-native policy import/projection code before dispatch starts an
|
||||
* autonomous run.
|
||||
*/
|
||||
export function normalizeRolePolicies(policies = {}) {
|
||||
if (!policies || typeof policies !== "object" || Array.isArray(policies)) {
|
||||
throw new ModelRolePolicyValidationError([
|
||||
"model role policies must be an object keyed by role",
|
||||
]);
|
||||
}
|
||||
const unknownRoles = Object.keys(policies).filter(
|
||||
(role) => !MODEL_ROLES.has(normalizeRoleName(role)),
|
||||
);
|
||||
if (unknownRoles.length > 0) {
|
||||
throw new ModelRolePolicyValidationError(
|
||||
unknownRoles.map(
|
||||
(role) =>
|
||||
`unsupported model role "${role}"; expected one of ${SUPPORTED_MODEL_ROLES.join(", ")}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
const normalizedPolicies = Object.fromEntries(
|
||||
Object.entries(policies).map(([role, policy]) => [
|
||||
normalizeRoleName(role),
|
||||
policy,
|
||||
]),
|
||||
);
|
||||
return Object.fromEntries(
|
||||
SUPPORTED_MODEL_ROLES.map((role) => [
|
||||
role,
|
||||
normalizeRolePolicy(role, normalizedPolicies[role]),
|
||||
]),
|
||||
);
|
||||
}
|
||||
84
src/resources/extensions/sf/uok/model-route-evidence.js
Normal file
84
src/resources/extensions/sf/uok/model-route-evidence.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
|
||||
import { buildModelRouteSnapshot } from "./model-route-snapshot.js";
|
||||
|
||||
/**
|
||||
* Return the model role implied by an autonomous unit type.
|
||||
*
|
||||
* Purpose: keep SF's durable model policy role-based (`worker`, `validator`,
|
||||
* `reviewer`) while still allowing the dispatcher to journal the concrete
|
||||
* provider/model chosen at runtime.
|
||||
*
|
||||
* Consumer: auto/phases.js after selectAndApplyModel resolves the final unit
|
||||
* model, including hook overrides.
|
||||
*/
|
||||
export function modelRoleForUnitType(unitType) {
|
||||
const normalized = String(unitType ?? "").toLowerCase();
|
||||
if (
|
||||
normalized.includes("validate") ||
|
||||
normalized.includes("verification") ||
|
||||
normalized.includes("gate")
|
||||
) {
|
||||
return "validator";
|
||||
}
|
||||
if (normalized.includes("review") || normalized.includes("scrutiny")) {
|
||||
return "reviewer";
|
||||
}
|
||||
return "worker";
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit runtime evidence for SF's auto-selected model route.
|
||||
*
|
||||
* Purpose: preserve the useful Droid-style role/accounting trail without
|
||||
* promoting provider-specific model IDs into durable SF configuration.
|
||||
*
|
||||
* Consumer: autonomous run-unit dispatch telemetry and UOK audit readers.
|
||||
*/
|
||||
export function emitModelAutoResolvedEvent(basePath, args) {
|
||||
const model = args.resolvedModel;
|
||||
const provider = model?.provider ?? null;
|
||||
const modelId = model?.id ?? null;
|
||||
const role = args.role ?? modelRoleForUnitType(args.unitType);
|
||||
const requestedPolicy = {
|
||||
mode: "auto",
|
||||
constraints: args.constraints ?? [],
|
||||
};
|
||||
const routeSnapshot = buildModelRouteSnapshot({
|
||||
role,
|
||||
unitType: args.unitType,
|
||||
unitId: args.unitId,
|
||||
requestedPolicy,
|
||||
route: {
|
||||
...(model ?? {}),
|
||||
authMode: args.authMode,
|
||||
},
|
||||
routingReason: args.routingReason,
|
||||
fallbacksTried: args.fallbacksTried ?? [],
|
||||
configEvidence: args.configEvidence,
|
||||
});
|
||||
emitUokAuditEvent(
|
||||
basePath,
|
||||
buildAuditEnvelope({
|
||||
traceId: args.traceId,
|
||||
turnId: args.unitId,
|
||||
category: "model-routing",
|
||||
type: "model-auto-resolved",
|
||||
payload: {
|
||||
role,
|
||||
unitType: args.unitType,
|
||||
unitId: args.unitId,
|
||||
requestedPolicy,
|
||||
resolvedProvider: provider,
|
||||
resolvedModel: modelId,
|
||||
authMode: args.authMode,
|
||||
routeSnapshot,
|
||||
routingReason: args.routingReason,
|
||||
fallbacksTried: args.fallbacksTried ?? [],
|
||||
routing: args.routing ?? null,
|
||||
hookOverrideApplied: Boolean(args.hookOverrideApplied),
|
||||
tokenUsage: args.tokenUsage,
|
||||
costEstimate: args.costEstimate,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
212
src/resources/extensions/sf/uok/unit-handoff.js
Normal file
212
src/resources/extensions/sf/uok/unit-handoff.js
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
/**
|
||||
* unit-handoff.js - structured autonomous unit closeout schema.
|
||||
*
|
||||
* Purpose: preserve the useful Droid closeout fields as a small, validated
|
||||
* contract that later units can consume without parsing prose summaries.
|
||||
*
|
||||
* Consumer: SF autonomous unit finalizers and follow-on unit prompts that need
|
||||
* a durable summary of what the previous unit changed, verified, and left open.
|
||||
*/
|
||||
|
||||
const ARRAY_FIELDS = [
|
||||
"filesChanged",
|
||||
"testsAdded",
|
||||
"commandsRun",
|
||||
"knownFailures",
|
||||
"leftUndone",
|
||||
"fulfilledAssertions",
|
||||
];
|
||||
|
||||
const STRING_ARRAY_FIELDS = new Set([
|
||||
"filesChanged",
|
||||
"testsAdded",
|
||||
"knownFailures",
|
||||
"leftUndone",
|
||||
"fulfilledAssertions",
|
||||
]);
|
||||
|
||||
const COMMAND_STATUSES = new Set([
|
||||
"passed",
|
||||
"failed",
|
||||
"skipped",
|
||||
"not-run",
|
||||
"unknown",
|
||||
]);
|
||||
|
||||
const VERIFICATION_STATUSES = new Set([
|
||||
"passed",
|
||||
"failed",
|
||||
"partial",
|
||||
"blocked",
|
||||
"not-run",
|
||||
"unknown",
|
||||
]);
|
||||
|
||||
function isPlainObject(value) {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
(Object.getPrototypeOf(value) === Object.prototype ||
|
||||
Object.getPrototypeOf(value) === null)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeString(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function normalizeStringArray(values) {
|
||||
return values.map((value) => value.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeCommandEntry(entry) {
|
||||
const normalized = {
|
||||
command: entry.command.trim(),
|
||||
status: normalizeString(entry.status) || "unknown",
|
||||
};
|
||||
if ("exitCode" in entry) {
|
||||
normalized.exitCode = entry.exitCode;
|
||||
}
|
||||
if ("cwd" in entry && normalizeString(entry.cwd)) {
|
||||
normalized.cwd = entry.cwd.trim();
|
||||
}
|
||||
if ("notes" in entry && normalizeString(entry.notes)) {
|
||||
normalized.notes = entry.notes.trim();
|
||||
}
|
||||
if ("durationMs" in entry) {
|
||||
normalized.durationMs = entry.durationMs;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function validateCommandEntry(entry, index, errors) {
|
||||
if (!isPlainObject(entry)) {
|
||||
errors.push(`commandsRun[${index}] must be an object`);
|
||||
return;
|
||||
}
|
||||
if (typeof entry.command !== "string" || entry.command.trim() === "") {
|
||||
errors.push(`commandsRun[${index}].command must be a non-empty string`);
|
||||
}
|
||||
if (
|
||||
"status" in entry &&
|
||||
(typeof entry.status !== "string" || !COMMAND_STATUSES.has(entry.status))
|
||||
) {
|
||||
errors.push(
|
||||
`commandsRun[${index}].status must be one of ${Array.from(COMMAND_STATUSES).join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
"exitCode" in entry &&
|
||||
(!Number.isInteger(entry.exitCode) || entry.exitCode < 0)
|
||||
) {
|
||||
errors.push(
|
||||
`commandsRun[${index}].exitCode must be a non-negative integer`,
|
||||
);
|
||||
}
|
||||
if ("cwd" in entry && typeof entry.cwd !== "string") {
|
||||
errors.push(`commandsRun[${index}].cwd must be a string`);
|
||||
}
|
||||
if ("notes" in entry && typeof entry.notes !== "string") {
|
||||
errors.push(`commandsRun[${index}].notes must be a string`);
|
||||
}
|
||||
if (
|
||||
"durationMs" in entry &&
|
||||
(!Number.isFinite(entry.durationMs) || entry.durationMs < 0)
|
||||
) {
|
||||
errors.push(
|
||||
`commandsRun[${index}].durationMs must be a non-negative number`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a candidate unit handoff and return every schema violation.
|
||||
*
|
||||
* Purpose: fail malformed closeout payloads before they become durable unit
|
||||
* state that future agents trust as verified work history.
|
||||
*
|
||||
* Consumer: normalizeUnitHandoff and any caller that wants to show structured
|
||||
* validation errors without throwing.
|
||||
*/
|
||||
export function validateUnitHandoff(candidate) {
|
||||
const errors = [];
|
||||
if (!isPlainObject(candidate)) {
|
||||
return {
|
||||
ok: false,
|
||||
errors: ["handoff must be an object"],
|
||||
};
|
||||
}
|
||||
|
||||
if ("summary" in candidate && typeof candidate.summary !== "string") {
|
||||
errors.push("summary must be a string");
|
||||
}
|
||||
if (
|
||||
"verificationStatus" in candidate &&
|
||||
(typeof candidate.verificationStatus !== "string" ||
|
||||
!VERIFICATION_STATUSES.has(candidate.verificationStatus))
|
||||
) {
|
||||
errors.push(
|
||||
`verificationStatus must be one of ${Array.from(VERIFICATION_STATUSES).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const field of ARRAY_FIELDS) {
|
||||
if (field in candidate && !Array.isArray(candidate[field])) {
|
||||
errors.push(`${field} must be an array`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of STRING_ARRAY_FIELDS) {
|
||||
if (!Array.isArray(candidate[field])) {
|
||||
continue;
|
||||
}
|
||||
candidate[field].forEach((entry, index) => {
|
||||
if (typeof entry !== "string") {
|
||||
errors.push(`${field}[${index}] must be a string`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(candidate.commandsRun)) {
|
||||
candidate.commandsRun.forEach((entry, index) => {
|
||||
validateCommandEntry(entry, index, errors);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ok: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a validated unit handoff into SF's canonical closeout shape.
|
||||
*
|
||||
* Purpose: give autonomous follow-on units a predictable structured payload
|
||||
* instead of letting missing fields, whitespace, or prose-only closeouts drift.
|
||||
*
|
||||
* Consumer: autonomous unit closeout writers before persisting handoff records
|
||||
* and prompt builders before injecting prior-unit context.
|
||||
*/
|
||||
export function normalizeUnitHandoff(candidate = {}) {
|
||||
const validation = validateUnitHandoff(candidate);
|
||||
if (!validation.ok) {
|
||||
throw new TypeError(
|
||||
`Invalid unit handoff: ${validation.errors.join("; ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
summary: normalizeString(candidate.summary),
|
||||
filesChanged: normalizeStringArray(candidate.filesChanged ?? []),
|
||||
testsAdded: normalizeStringArray(candidate.testsAdded ?? []),
|
||||
commandsRun: (candidate.commandsRun ?? []).map(normalizeCommandEntry),
|
||||
knownFailures: normalizeStringArray(candidate.knownFailures ?? []),
|
||||
leftUndone: normalizeStringArray(candidate.leftUndone ?? []),
|
||||
fulfilledAssertions: normalizeStringArray(
|
||||
candidate.fulfilledAssertions ?? [],
|
||||
),
|
||||
verificationStatus:
|
||||
normalizeString(candidate.verificationStatus) || "unknown",
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue