Add more untracked runtime extension files

This commit is contained in:
Mikael Hugo 2026-05-08 20:51:18 +02:00
parent fd06629f06
commit a46cbcbe40
10 changed files with 971 additions and 0 deletions

View file

@ -0,0 +1,73 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import {
assessAssertionCoverage,
fulfilledAssertionIdsFromHandoff,
requiredAssertionIdsFromContract,
} from "../uok/assertion-coverage.js";
test("requiredAssertionIdsFromContract_when_droid_and_sf_fields_present_returns_unique_required_ids", () => {
assert.deepEqual(
requiredAssertionIdsFromContract({
assertionIds: ["VAL-001", "VAL-002"],
fulfills: ["VAL-002", "VAL-003"],
validationContract: {
assertions: ["VAL-004"],
fulfills: ["VAL-003"],
},
unitContract: {
assertionIds: ["VAL-005"],
},
}),
["VAL-001", "VAL-002", "VAL-003", "VAL-004", "VAL-005"],
);
});
test("fulfilledAssertionIdsFromHandoff_when_closeout_uses_alias_fields_returns_unique_fulfilled_ids", () => {
assert.deepEqual(
fulfilledAssertionIdsFromHandoff({
fulfilledAssertions: ["VAL-001", "VAL-002"],
assertionsFulfilled: ["VAL-002", "VAL-003"],
fulfills: ["VAL-004"],
}),
["VAL-001", "VAL-002", "VAL-003", "VAL-004"],
);
});
test("assessAssertionCoverage_when_handoff_covers_all_required_assertions_returns_ok", () => {
const result = assessAssertionCoverage(
{
fulfills: ["VAL-LOG-001", "VAL-LOG-002"],
},
{
fulfilledAssertions: ["VAL-LOG-002", "VAL-LOG-001"],
},
);
assert.deepEqual(result, {
ok: true,
required: ["VAL-LOG-001", "VAL-LOG-002"],
fulfilled: ["VAL-LOG-002", "VAL-LOG-001"],
missing: [],
extra: [],
});
});
test("assessAssertionCoverage_when_handoff_omits_required_assertion_reports_missing_and_extra", () => {
const result = assessAssertionCoverage(
{
validationContract: {
assertionIds: ["VAL-A", "VAL-B"],
},
},
{
fulfilledAssertions: ["VAL-A", "VAL-C"],
},
);
assert.equal(result.ok, false);
assert.deepEqual(result.required, ["VAL-A", "VAL-B"]);
assert.deepEqual(result.fulfilled, ["VAL-A", "VAL-C"]);
assert.deepEqual(result.missing, ["VAL-B"]);
assert.deepEqual(result.extra, ["VAL-C"]);
});

View file

@ -0,0 +1,91 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import {
buildModelRouteSnapshot,
redactModelConfigSecrets,
sanitizeModelRouteSnapshot,
} from "../uok/model-route-snapshot.js";
test("sanitizeModelRouteSnapshot_when_route_has_secret_fields_omits_credentials", () => {
assert.deepEqual(
sanitizeModelRouteSnapshot({
provider: "zai",
id: "glm-5.1",
displayName: "Z.AI GLM",
api: "openai-chat",
authMode: "apiKey",
baseUrl: "https://api.example.test/v1",
apiKey: "secret-key",
headers: { Authorization: "Bearer secret" },
noImageSupport: true,
}),
{
provider: "zai",
model: "glm-5.1",
displayName: "Z.AI GLM",
api: "openai-chat",
authMode: "apiKey",
baseUrl: "https://api.example.test/v1",
isCustom: false,
byok: true,
noImageSupport: true,
},
);
});
test("redactModelConfigSecrets_when_config_contains_credentials_replaces_secret_values", () => {
assert.deepEqual(
redactModelConfigSecrets({
provider: "kimi-coding",
apiKey: "key-123",
authHeader: "x-api-key",
headers: { Authorization: "Bearer token" },
baseUrl: "https://api.example.test/v1",
}),
{
provider: "kimi-coding",
apiKey: "[REDACTED]",
authHeader: "[REDACTED]",
headers: "[REDACTED]",
baseUrl: "https://api.example.test/v1",
},
);
});
test("buildModelRouteSnapshot_when_route_and_fallbacks_present_keeps_policy_auto_and_routes_safe", () => {
const snapshot = buildModelRouteSnapshot({
role: "worker",
unitType: "execute-task",
unitId: "M001/S01/T01",
route: {
provider: "kimi-coding",
model: "kimi-k2.6",
authMode: "apiKey",
apiKey: "secret",
},
routingReason: "auto selector",
fallbacksTried: [
{
provider: "ollama-cloud",
model: "deepseek-v4-pro",
authMode: "apiKey",
apiKey: "secret",
},
],
configEvidence: {
apiKey: "secret",
baseUrl: "https://api.example.test/v1",
},
});
assert.equal(snapshot.schemaVersion, 1);
assert.deepEqual(snapshot.requestedPolicy, { mode: "auto", constraints: [] });
assert.equal(snapshot.route.provider, "kimi-coding");
assert.equal(snapshot.route.model, "kimi-k2.6");
assert.equal(snapshot.route.byok, true);
assert.equal(snapshot.route.apiKey, undefined);
assert.equal(snapshot.fallbacksTried[0].provider, "ollama-cloud");
assert.equal(snapshot.fallbacksTried[0].apiKey, undefined);
assert.equal(snapshot.configEvidence.apiKey, "[REDACTED]");
assert.equal(snapshot.configEvidence.baseUrl, "https://api.example.test/v1");
});

View file

@ -0,0 +1,68 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import {
buildUokProgressEvent,
UOK_PROGRESS_EVENT_TYPES,
validateUokProgressEvent,
} from "../uok/progress-event.js";
test("buildUokProgressEvent_when_unit_selected_returns_stable_machine_event", () => {
assert.deepEqual(
buildUokProgressEvent({
ts: "2026-05-08T17:00:00.000Z",
eventType: "unit_selected",
unitType: "execute-task",
unitId: "M001/S01/T01",
role: "worker",
sessionId: "session-1",
workerSessionId: "worker-1",
traceId: "trace-1",
data: { source: "dispatch" },
}),
{
schemaVersion: 1,
ts: "2026-05-08T17:00:00.000Z",
eventType: "unit_selected",
unitType: "execute-task",
unitId: "M001/S01/T01",
role: "worker",
sessionId: "session-1",
workerSessionId: "worker-1",
traceId: "trace-1",
data: { source: "dispatch" },
},
);
});
test("validateUokProgressEvent_when_event_type_unknown_reports_allowed_types", () => {
const result = validateUokProgressEvent({
eventType: "invented_event",
data: {},
});
assert.equal(result.ok, false);
assert.match(result.issues[0], /eventType must be one of/);
assert.match(result.issues[0], /unit_selected/);
assert.match(result.issues[0], /unit_handoff_recorded/);
});
test("buildUokProgressEvent_when_payload_data_is_not_object_rejects_event", () => {
assert.throws(
() =>
buildUokProgressEvent({
eventType: "worker_started",
data: "not-object",
}),
/Invalid UOK progress event: data must be an object/,
);
});
test("progressEventTypes_include_droid_value_events_without_concrete_model_config", () => {
assert.ok(UOK_PROGRESS_EVENT_TYPES.includes("model_auto_resolved"));
assert.ok(UOK_PROGRESS_EVENT_TYPES.includes("validation_started"));
assert.ok(UOK_PROGRESS_EVENT_TYPES.includes("unit_handoff_recorded"));
assert.equal(
UOK_PROGRESS_EVENT_TYPES.some((type) => type.includes("custom:")),
false,
);
});

View file

@ -0,0 +1,86 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import {
normalizeToolCommandRegistry,
validateToolCommandRegistry,
} from "../uok/tool-command-registry.js";
test("normalizeToolCommandRegistry_when_commands_and_daemon_services_present_returns_worker_tool_allowlist", () => {
assert.deepEqual(
normalizeToolCommandRegistry({
commands: {
test: " npm run test:unit ",
typecheck: "npm run typecheck:extensions",
},
daemonServices: [" postgres ", "redis", "redis"],
}),
{
schemaVersion: 1,
commands: {
test: "npm run test:unit",
typecheck: "npm run typecheck:extensions",
},
daemonServices: ["postgres", "redis"],
allowedCommands: ["npm run test:unit", "npm run typecheck:extensions"],
allowedCommandNames: ["test", "typecheck"],
allowedDaemonServices: ["postgres", "redis"],
},
);
});
test("normalizeToolCommandRegistry_when_fields_missing_returns_empty_allowlists", () => {
assert.deepEqual(normalizeToolCommandRegistry({}), {
schemaVersion: 1,
commands: {},
daemonServices: [],
allowedCommands: [],
allowedCommandNames: [],
allowedDaemonServices: [],
});
});
test("validateToolCommandRegistry_when_commands_not_object_reports_clear_issue", () => {
const result = validateToolCommandRegistry({
commands: ["npm test"],
daemonServices: [],
});
assert.equal(result.ok, false);
assert.deepEqual(result.issues, ["commands must be an object"]);
});
test("validateToolCommandRegistry_when_command_or_daemon_service_empty_reports_paths", () => {
const result = validateToolCommandRegistry({
commands: {
test: "",
},
daemonServices: ["postgres", ""],
});
assert.equal(result.ok, false);
assert.deepEqual(result.issues, [
"commands.test must be a non-empty string",
"daemonServices[1] must be a non-empty string",
]);
});
test("normalizeToolCommandRegistry_when_droid_services_field_present_treats_it_as_legacy_daemon_services", () => {
assert.deepEqual(
normalizeToolCommandRegistry({
commands: {
build: "npm run build:core",
},
services: ["postgres"],
}),
{
schemaVersion: 1,
commands: {
build: "npm run build:core",
},
daemonServices: ["postgres"],
allowedCommands: ["npm run build:core"],
allowedCommandNames: ["build"],
allowedDaemonServices: ["postgres"],
},
);
});

View file

@ -0,0 +1,105 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import {
normalizeUnitLineage,
recordUnitLineageEvent,
} from "../uok/unit-lineage.js";
test("normalizeUnitLineage_when_worker_ids_repeat_returns_deduplicated_lineage", () => {
assert.deepEqual(
normalizeUnitLineage({
unitType: "execute-task",
unitId: "M001/S01/T01",
workerSessionIds: ["worker-1", "worker-1"],
currentWorkerSessionId: "worker-2",
completedWorkerSessionId: "worker-3",
failedWorkerSessionIds: ["worker-4", "worker-4"],
status: "started",
}),
{
unitType: "execute-task",
unitId: "M001/S01/T01",
status: "started",
workerSessionIds: ["worker-1", "worker-2", "worker-3", "worker-4"],
currentWorkerSessionId: "worker-2",
completedWorkerSessionId: "worker-3",
failedWorkerSessionIds: ["worker-4"],
events: [],
},
);
});
test("recordUnitLineageEvent_when_worker_starts_marks_current_session", () => {
const lineage = recordUnitLineageEvent(
{ unitType: "execute-task", unitId: "M001/S01/T01" },
{
ts: "2026-05-08T17:00:00.000Z",
status: "started",
workerSessionId: "worker-1",
spawnId: "spawn-1",
},
);
assert.equal(lineage.status, "started");
assert.deepEqual(lineage.workerSessionIds, ["worker-1"]);
assert.equal(lineage.currentWorkerSessionId, "worker-1");
assert.equal(lineage.events[0].spawnId, "spawn-1");
});
test("recordUnitLineageEvent_when_worker_completes_moves_current_to_completed", () => {
const started = recordUnitLineageEvent(
{},
{
status: "started",
workerSessionId: "worker-1",
},
);
const completed = recordUnitLineageEvent(started, {
status: "completed",
workerSessionId: "worker-1",
});
assert.equal(completed.status, "completed");
assert.equal(completed.currentWorkerSessionId, null);
assert.equal(completed.completedWorkerSessionId, "worker-1");
assert.deepEqual(completed.failedWorkerSessionIds, []);
});
test("recordUnitLineageEvent_when_worker_fails_tracks_failed_session", () => {
const lineage = recordUnitLineageEvent(
{
workerSessionIds: ["worker-1"],
currentWorkerSessionId: "worker-1",
},
{
status: "failed",
workerSessionId: "worker-1",
note: "provider timeout",
},
);
assert.equal(lineage.status, "failed");
assert.equal(lineage.currentWorkerSessionId, null);
assert.deepEqual(lineage.failedWorkerSessionIds, ["worker-1"]);
assert.equal(lineage.events[0].note, "provider timeout");
});
test("recordUnitLineageEvent_when_unit_blocks_clears_current_worker", () => {
const lineage = recordUnitLineageEvent(
{
unitType: "execute-task",
unitId: "M001/S01/T01",
workerSessionIds: ["worker-1"],
currentWorkerSessionId: "worker-1",
},
{
status: "blocked",
workerSessionId: "worker-1",
note: "artifact verification failed",
},
);
assert.equal(lineage.status, "blocked");
assert.equal(lineage.currentWorkerSessionId, null);
assert.deepEqual(lineage.workerSessionIds, ["worker-1"]);
});

View file

@ -0,0 +1,103 @@
/**
* assertion-coverage.js - validation assertion coverage helpers.
*
* Purpose: preserve Droid's useful `fulfills: ["VAL-*"]` accounting as
* structured SF data so units can prove which validation claims they satisfy.
*
* Consumer: autonomous unit contract projections, handoff validation, and
* future closeout gates before marking a unit complete.
*/
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 uniqueIds(...values) {
return [...new Set(values.flatMap((value) => stringList(value)))];
}
function objectOrEmpty(value) {
return value && typeof value === "object" && !Array.isArray(value)
? value
: {};
}
/**
* Normalize assertion IDs required by a unit contract.
*
* Purpose: let SF accept Droid-style `fulfills` as well as SF-style
* `assertionIds` while projecting one canonical required assertion list.
*
* Consumer: assertion coverage checks and worker context-pack builders.
*/
export function requiredAssertionIdsFromContract(contract = {}) {
const source = objectOrEmpty(contract);
const validationContract = objectOrEmpty(source.validationContract);
const unitContract = objectOrEmpty(source.unitContract);
return uniqueIds(
source.assertions,
source.assertionIds,
source.fulfills,
source.fulfillmentIds,
validationContract.assertions,
validationContract.assertionIds,
validationContract.fulfills,
unitContract.assertions,
unitContract.assertionIds,
unitContract.fulfills,
);
}
/**
* Normalize assertion IDs fulfilled by a unit handoff.
*
* Purpose: give closeout gates one canonical fulfilled assertion list even
* when agents use Droid-style or SF-style field names.
*
* Consumer: assertion coverage checks and autonomous closeout validation.
*/
export function fulfilledAssertionIdsFromHandoff(handoff = {}) {
const source = objectOrEmpty(handoff);
return uniqueIds(
source.fulfilledAssertions,
source.assertionsFulfilled,
source.fulfills,
source.assertionIds,
);
}
/**
* Compare a unit contract with a worker handoff and report assertion coverage.
*
* Purpose: prevent units from being treated as complete when their structured
* handoff did not cover every validation claim the unit contract required.
*
* Consumer: future autonomous closeout gates and tests that need deterministic
* validation coverage evidence without parsing prose.
*/
export function assessAssertionCoverage(contract = {}, handoff = {}) {
const required = requiredAssertionIdsFromContract(contract);
const fulfilled = fulfilledAssertionIdsFromHandoff(handoff);
const fulfilledSet = new Set(fulfilled);
const missing = required.filter((id) => !fulfilledSet.has(id));
const extra = fulfilled.filter((id) => !required.includes(id));
return {
ok: missing.length === 0,
required,
fulfilled,
missing,
extra,
};
}

View file

@ -0,0 +1,97 @@
/**
* model-route-snapshot.js - secret-safe model route snapshots.
*
* Purpose: keep Droid's useful runtime model reproducibility evidence while
* guaranteeing API keys, tokens, and headers are never persisted in SF state.
*
* Consumer: UOK audit/progress projections and future run-state snapshots that
* need provider/model evidence without copying credentials.
*/
const SECRET_KEYS = new Set([
"apiKey",
"api_key",
"authorization",
"authHeader",
"headers",
"token",
"secret",
"password",
]);
function stringOrNull(value) {
return typeof value === "string" && value.trim().length > 0
? value.trim()
: null;
}
function maybeRedacted(value, key) {
if (SECRET_KEYS.has(key)) return "[REDACTED]";
return value;
}
/**
* Build a secret-safe snapshot of one resolved model route.
*
* Purpose: record what SF actually routed to while keeping durable evidence
* separate from BYOK credentials and provider secrets.
*
* Consumer: model route evidence emitters and run-state projection writers.
*/
export function sanitizeModelRouteSnapshot(route = {}) {
const source = route && typeof route === "object" ? route : {};
return {
provider: stringOrNull(source.provider),
model: stringOrNull(source.id ?? source.model ?? source.modelId),
displayName: stringOrNull(source.displayName),
api: stringOrNull(source.api),
authMode: stringOrNull(source.authMode),
baseUrl: stringOrNull(source.baseUrl),
isCustom: Boolean(source.isCustom ?? source.custom),
byok: source.authMode === "apiKey" || Boolean(source.byok),
noImageSupport: Boolean(source.noImageSupport),
};
}
/**
* Redact a provider/model configuration object while preserving non-secret
* route metadata.
*
* Purpose: make tests and projections fail closed if a caller accidentally
* passes Droid-style custom model config containing raw credentials.
*
* Consumer: snapshot writers before serializing model/provider config evidence.
*/
export function redactModelConfigSecrets(config = {}) {
if (!config || typeof config !== "object" || Array.isArray(config)) return {};
const redacted = {};
for (const [key, value] of Object.entries(config)) {
redacted[key] = maybeRedacted(value, key);
}
return redacted;
}
/**
* Build a secret-safe snapshot for a route plus optional routing evidence.
*
* Purpose: capture enough model evidence to debug auto routing decisions
* without making concrete model IDs durable policy or leaking BYOK secrets.
*
* Consumer: UOK model routing events and future run-state projections.
*/
export function buildModelRouteSnapshot(args = {}) {
const route = sanitizeModelRouteSnapshot(args.route ?? args.model ?? {});
return {
schemaVersion: 1,
role: stringOrNull(args.role),
unitType: stringOrNull(args.unitType),
unitId: stringOrNull(args.unitId),
requestedPolicy: args.requestedPolicy ?? { mode: "auto", constraints: [] },
route,
routingReason: stringOrNull(args.routingReason),
fallbacksTried: Array.isArray(args.fallbacksTried)
? args.fallbacksTried.map(sanitizeModelRouteSnapshot)
: [],
configEvidence: redactModelConfigSecrets(args.configEvidence ?? {}),
};
}

View file

@ -0,0 +1,109 @@
/**
* progress-event.js - typed autonomous progress event helpers.
*
* Purpose: keep Droid's useful machine-readable progress vocabulary while
* preserving SF's existing journal/audit systems as the transport.
*
* Consumer: autonomous loop projections, dashboards, and future machine
* surfaces that need stable progress event names instead of parsing prose.
*/
export const UOK_PROGRESS_EVENT_TYPES = Object.freeze([
"unit_selected",
"model_auto_resolved",
"worker_started",
"validation_started",
"unit_handoff_recorded",
"unit_blocked",
"unit_completed",
]);
const EVENT_TYPES = new Set(UOK_PROGRESS_EVENT_TYPES);
function stringOrNull(value) {
return typeof value === "string" && value.trim().length > 0
? value.trim()
: null;
}
function objectOrEmpty(value) {
return value && typeof value === "object" && !Array.isArray(value)
? value
: {};
}
/**
* Validate a typed UOK progress event.
*
* Purpose: reject unstable ad hoc event names before they become dashboard or
* machine-surface contracts.
*
* Consumer: buildUokProgressEvent and tests that need non-throwing diagnostics.
*/
export function validateUokProgressEvent(event) {
const issues = [];
if (!event || typeof event !== "object" || Array.isArray(event)) {
return { ok: false, issues: ["event must be an object"] };
}
if (!EVENT_TYPES.has(event.eventType)) {
issues.push(
`eventType must be one of ${UOK_PROGRESS_EVENT_TYPES.join(", ")}`,
);
}
if (
"unitId" in event &&
event.unitId !== null &&
typeof event.unitId !== "string"
) {
issues.push("unitId must be a string or null");
}
if (
"unitType" in event &&
event.unitType !== null &&
typeof event.unitType !== "string"
) {
issues.push("unitType must be a string or null");
}
if (
"data" in event &&
(typeof event.data !== "object" ||
event.data === null ||
Array.isArray(event.data))
) {
issues.push("data must be an object");
}
return { ok: issues.length === 0, issues };
}
/**
* Build a typed UOK progress event with canonical timestamp and payload shape.
*
* Purpose: give SF the useful Droid-style progress stream shape without
* coupling event creation to a specific file, DB, or transport.
*
* Consumer: future autonomous journal/audit adapters and focused tests.
*/
export function buildUokProgressEvent(args = {}) {
const event = {
schemaVersion: 1,
ts: args.ts ?? new Date().toISOString(),
eventType: args.eventType,
unitType: stringOrNull(args.unitType),
unitId: stringOrNull(args.unitId),
role: stringOrNull(args.role),
sessionId: stringOrNull(args.sessionId),
workerSessionId: stringOrNull(args.workerSessionId),
traceId: stringOrNull(args.traceId),
data: args.data === undefined ? {} : args.data,
};
const validation = validateUokProgressEvent(event);
if (!validation.ok) {
throw new TypeError(
`Invalid UOK progress event: ${validation.issues.join("; ")}`,
);
}
return {
...event,
data: objectOrEmpty(event.data),
};
}

View file

@ -0,0 +1,117 @@
/**
* tool-command-registry.js - worker tool/command registry helpers.
*
* Purpose: preserve Droid's useful command boundary as structured SF data so
* workers receive explicit repo tools/commands instead of free-form prose.
*
* Consumer: worker context pack projections and future dispatch prompts that
* need a tool-command allowlist without treating ad hoc mission files as
* canonical.
*/
function stringOrNull(value) {
return typeof value === "string" && value.trim().length > 0
? value.trim()
: null;
}
function objectOrEmpty(value) {
return value && typeof value === "object" && !Array.isArray(value)
? value
: {};
}
function normalizeStringList(value) {
if (Array.isArray(value)) {
return [
...new Set(value.map(stringOrNull).filter((item) => item !== null)),
];
}
const single = stringOrNull(value);
return single === null ? [] : [single];
}
function normalizeCommandMap(commands) {
const source = objectOrEmpty(commands);
const normalized = {};
for (const [name, command] of Object.entries(source)) {
const normalizedName = stringOrNull(name);
const normalizedCommand = stringOrNull(command);
if (normalizedName && normalizedCommand) {
normalized[normalizedName] = normalizedCommand;
}
}
return normalized;
}
/**
* Normalize an SF worker tool/command registry.
*
* Purpose: project Droid-style command boundaries into a predictable
* tool-first shape while leaving durable source-of-truth ownership to SF
* DB/runtime state.
*
* Consumer: context-pack builders and prompt adapters before rendering worker
* instructions.
*/
export function normalizeToolCommandRegistry(registry = {}) {
const source = objectOrEmpty(registry);
const commands = normalizeCommandMap(source.commands);
const daemonServices = normalizeStringList(
source.daemonServices ?? source.services,
);
return {
schemaVersion: 1,
commands,
daemonServices,
allowedCommands: Object.values(commands),
allowedCommandNames: Object.keys(commands),
allowedDaemonServices: daemonServices,
};
}
/**
* Validate a worker tool/command registry.
*
* Purpose: reject malformed command boundaries before workers treat them as
* executable instructions.
*
* Consumer: context-pack projection validation and tests.
*/
export function validateToolCommandRegistry(registry) {
const issues = [];
if (!registry || typeof registry !== "object" || Array.isArray(registry)) {
return { ok: false, issues: ["registry must be an object"] };
}
if ("commands" in registry) {
if (
!registry.commands ||
typeof registry.commands !== "object" ||
Array.isArray(registry.commands)
) {
issues.push("commands must be an object");
} else {
for (const [name, command] of Object.entries(registry.commands)) {
if (!stringOrNull(name)) issues.push("command name must be non-empty");
if (!stringOrNull(command)) {
issues.push(`commands.${name} must be a non-empty string`);
}
}
}
}
const servicesValue = registry.daemonServices ?? registry.services;
if (servicesValue !== undefined && !Array.isArray(servicesValue)) {
issues.push("daemonServices must be an array");
}
if (Array.isArray(servicesValue)) {
servicesValue.forEach((service, index) => {
if (!stringOrNull(service)) {
issues.push(`daemonServices[${index}] must be a non-empty string`);
}
});
}
return { ok: issues.length === 0, issues };
}
export const normalizeCommandRegistry = normalizeToolCommandRegistry;
export const validateCommandRegistry = validateToolCommandRegistry;

View file

@ -0,0 +1,122 @@
/**
* unit-lineage.js - autonomous unit worker/session lineage helpers.
*
* Purpose: preserve Droid's useful worker-session accounting so SF can explain
* which sessions claimed, ran, completed, or failed a unit.
*
* Consumer: autonomous dispatch projections, progress events, and future
* recovery screens that need lineage without parsing logs.
*/
const LINEAGE_STATUSES = new Set([
"selected",
"started",
"completed",
"failed",
"cancelled",
"blocked",
]);
function stringOrNull(value) {
return typeof value === "string" && value.trim().length > 0
? value.trim()
: null;
}
function uniqueStringList(value) {
const values = Array.isArray(value) ? value : [];
return [...new Set(values.map(stringOrNull).filter((item) => item !== null))];
}
/**
* Normalize a unit lineage record.
*
* Purpose: keep worker-session lineage deterministic and deduplicated before
* it is projected into runtime state or machine progress events.
*
* Consumer: recordUnitLineageEvent and future dispatch projection writers.
*/
export function normalizeUnitLineage(record = {}) {
const workerSessionIds = uniqueStringList(record.workerSessionIds);
const currentWorkerSessionId = stringOrNull(record.currentWorkerSessionId);
const completedWorkerSessionId = stringOrNull(
record.completedWorkerSessionId,
);
const failedWorkerSessionIds = uniqueStringList(
record.failedWorkerSessionIds,
);
if (
currentWorkerSessionId &&
!workerSessionIds.includes(currentWorkerSessionId)
) {
workerSessionIds.push(currentWorkerSessionId);
}
if (
completedWorkerSessionId &&
!workerSessionIds.includes(completedWorkerSessionId)
) {
workerSessionIds.push(completedWorkerSessionId);
}
for (const id of failedWorkerSessionIds) {
if (!workerSessionIds.includes(id)) workerSessionIds.push(id);
}
return {
unitType: stringOrNull(record.unitType),
unitId: stringOrNull(record.unitId),
status: LINEAGE_STATUSES.has(record.status) ? record.status : "selected",
workerSessionIds,
currentWorkerSessionId,
completedWorkerSessionId,
failedWorkerSessionIds,
events: Array.isArray(record.events) ? record.events : [],
};
}
/**
* Append one worker/session lineage event to a unit lineage record.
*
* Purpose: model Droid-style worker lifecycle accounting as structured state
* while leaving persistence and transport to SF's existing DB/journal layers.
*
* Consumer: future autonomous dispatch hooks and tests.
*/
export function recordUnitLineageEvent(record = {}, event = {}) {
const current = normalizeUnitLineage(record);
const status = LINEAGE_STATUSES.has(event.status) ? event.status : "selected";
const workerSessionId = stringOrNull(event.workerSessionId);
const next = {
...current,
unitType: stringOrNull(event.unitType) ?? current.unitType,
unitId: stringOrNull(event.unitId) ?? current.unitId,
status,
events: [
...current.events,
{
ts: event.ts ?? new Date().toISOString(),
status,
workerSessionId,
spawnId: stringOrNull(event.spawnId),
note: stringOrNull(event.note),
},
],
};
if (workerSessionId && !next.workerSessionIds.includes(workerSessionId)) {
next.workerSessionIds.push(workerSessionId);
}
if (status === "started") next.currentWorkerSessionId = workerSessionId;
if (status === "completed") {
next.completedWorkerSessionId = workerSessionId;
next.currentWorkerSessionId = null;
}
if (status === "failed" && workerSessionId) {
next.failedWorkerSessionIds = uniqueStringList([
...next.failedWorkerSessionIds,
workerSessionId,
]);
next.currentWorkerSessionId = null;
}
if (status === "blocked" || status === "cancelled") {
next.currentWorkerSessionId = null;
}
return normalizeUnitLineage(next);
}