Add untracked runtime extension files to git

This commit is contained in:
Mikael Hugo 2026-05-08 19:55:39 +02:00
parent c3b202dd4c
commit 8f02524fd7
8 changed files with 1223 additions and 0 deletions

View file

@ -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"),
);
});

View 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"/,
);
});
});

View file

@ -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);
});

View 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",
]);
});

View 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 };
}

View 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]),
]),
);
}

View 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,
},
}),
);
}

View 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",
};
}