Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/conformance-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"build": "echo 'no build needed'",
"test": "vitest run",
"test:watch": "vitest watch",
"test:fixtures": "vitest run src/canonical-fixtures-conformance.test.ts src/a2a-fixtures-conformance.test.ts src/security-iot-fixtures-conformance.test.ts src/economy-fixtures-conformance.test.ts src/autogen-interop-conformance.test.ts src/agentskill-authz-fixtures-conformance.test.ts src/action-ref-explainability-conformance.test.ts src/payment-governance-fixtures-conformance.test.ts src/agent-commerce-governance-conformance.test.ts src/physical-work-market-conformance.test.ts src/mission-authority-reference-slice-conformance.test.ts src/mission-authority-reference-gateway-conformance.test.ts src/mission-authority-release-lane-evidence-conformance.test.ts src/physical-ai-runtime-safety-fixtures-conformance.test.ts src/post-quantum-crypto-agility-conformance.test.ts src/humanoid-profile-conformance.test.ts src/humanoid-warehouse-pilot-conformance.test.ts src/eu-ai-act-conformity-pack-conformance.test.ts src/humanoid-multivendor-fleet-conformance.test.ts src/open-rmf-handoff-policy-receipts-conformance.test.ts src/moveit-manipulation-policy-receipts-conformance.test.ts src/nav2-navigation-policy-receipts-conformance.test.ts src/px4-offboard-policy-receipts-conformance.test.ts src/lerobot-policy-actuation-receipts-conformance.test.ts src/solar-field-operations-policy-receipts-conformance.test.ts src/industrial-cell-safety-pack-conformance.test.ts src/industrial-humanoid-shipyard-safety-pack-conformance.test.ts src/factory-action-demo-conformance.test.ts src/sint-industrial-pack-conformance.test.ts src/regulated-agent-runtime-conformance.test.ts src/regulated-consent-extensions-conformance.test.ts src/autonomy-supervisor-conformance.test.ts",
"test:fixtures": "vitest run src/canonical-fixtures-conformance.test.ts src/a2a-fixtures-conformance.test.ts src/security-iot-fixtures-conformance.test.ts src/economy-fixtures-conformance.test.ts src/autogen-interop-conformance.test.ts src/agentskill-authz-fixtures-conformance.test.ts src/action-ref-explainability-conformance.test.ts src/payment-governance-fixtures-conformance.test.ts src/agent-commerce-governance-conformance.test.ts src/physical-work-market-conformance.test.ts src/mission-authority-reference-slice-conformance.test.ts src/mission-authority-reference-gateway-conformance.test.ts src/mission-authority-release-lane-evidence-conformance.test.ts src/physical-ai-runtime-safety-fixtures-conformance.test.ts src/kinetic-envelope-conformance.test.ts src/post-quantum-crypto-agility-conformance.test.ts src/humanoid-profile-conformance.test.ts src/humanoid-warehouse-pilot-conformance.test.ts src/eu-ai-act-conformity-pack-conformance.test.ts src/humanoid-multivendor-fleet-conformance.test.ts src/open-rmf-handoff-policy-receipts-conformance.test.ts src/moveit-manipulation-policy-receipts-conformance.test.ts src/nav2-navigation-policy-receipts-conformance.test.ts src/px4-offboard-policy-receipts-conformance.test.ts src/lerobot-policy-actuation-receipts-conformance.test.ts src/solar-field-operations-policy-receipts-conformance.test.ts src/industrial-cell-safety-pack-conformance.test.ts src/industrial-humanoid-shipyard-safety-pack-conformance.test.ts src/factory-action-demo-conformance.test.ts src/sint-industrial-pack-conformance.test.ts src/regulated-agent-runtime-conformance.test.ts src/regulated-consent-extensions-conformance.test.ts src/autonomy-supervisor-conformance.test.ts",
"test:physical-ai-runtime": "vitest run src/physical-ai-runtime-safety-fixtures-conformance.test.ts",
"test:factory-action": "vitest run src/factory-action-demo-conformance.test.ts",
"test:ros2-loop": "vitest run src/ros2-control-loop-latency.test.ts"
Expand Down
207 changes: 207 additions & 0 deletions packages/conformance-tests/src/kinetic-envelope-conformance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
ApprovalTier,
KINETIC_CAPACITY_EXCEEDED,
kineticEnvelopeResultSchema,
kineticReceiptSchema,
type KineticEnvelopeResult,
type KineticTightenedConstraints,
type SupervisionLevel,
} from "@pshkv/core";

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const FIXTURE_PATH = resolve(
ROOT,
"spec/kinetic-envelope/conformance/ros2-reference-v0.1.json",
);

const PROFILE = {
vMaxMps: 1.5,
fMaxN: 150,
tauMaxNm: 40,
omegaMaxRps: 2.0,
eBase: 1.0,
dSafeM: 1.0,
mAllow: 0.5,
metacognitiveFloor: 0.33,
assistedFloor: 0.15,
} as const;

type DecisionAction = "allow" | "deny" | "escalate";
type MarginBand = "safe" | "approaching" | "over";

interface KineticFixture {
readonly profile: "ros2-reference-v0.1";
readonly note: string;
readonly vectors: readonly KineticVector[];
}

interface KineticVector {
readonly id: string;
readonly note: string;
readonly request: {
readonly resource: string;
readonly action: string;
readonly physicalContext: {
readonly currentVelocityMps?: number;
readonly currentForceNewtons?: number;
readonly currentTorqueNm?: number;
readonly currentAngularVelocityRps?: number;
readonly humanDetected?: boolean;
readonly nearestObstacleMeters?: number;
readonly trajectoryNovel?: boolean;
};
};
readonly expect: {
readonly referenceMargin: number;
readonly marginBand: MarginBand;
readonly supervision: SupervisionLevel;
readonly decision: DecisionAction;
readonly policyCode?: typeof KINETIC_CAPACITY_EXCEEDED;
readonly capacityKnown: boolean;
readonly tightenedAtMost?: KineticTightenedConstraints;
};
}

function loadFixture(): KineticFixture {
return JSON.parse(readFileSync(FIXTURE_PATH, "utf8")) as KineticFixture;
}

function clamp(n: number, min: number, max: number): number {
return Math.min(max, Math.max(min, n));
}

function round3(n: number): number {
return Math.round(n * 1000) / 1000;
}

function supervisionForMargin(margin: number, capacityKnown: boolean): SupervisionLevel {
let supervision: SupervisionLevel;
if (margin <= 0) supervision = "silent_veto";
else if (margin >= PROFILE.mAllow) supervision = "stable";
else if (margin >= PROFILE.metacognitiveFloor) supervision = "metacognitive";
else if (margin >= PROFILE.assistedFloor) supervision = "assisted";
else supervision = "regulated";

if (!capacityKnown && (supervision === "stable" || supervision === "metacognitive")) {
return "assisted";
}
return supervision;
}

function decisionForSupervision(supervision: SupervisionLevel): DecisionAction {
if (supervision === "stable") return "allow";
if (supervision === "silent_veto") return "deny";
return "escalate";
}

function marginBandFor(margin: number): MarginBand {
if (margin <= 0) return "over";
if (margin >= PROFILE.mAllow) return "safe";
return "approaching";
}

function computeReferenceEnvelope(vector: KineticVector): KineticEnvelopeResult {
const ctx = vector.request.physicalContext;
const autonomyDemand = Math.max(
(ctx.currentVelocityMps ?? 0) / PROFILE.vMaxMps,
(ctx.currentForceNewtons ?? 0) / PROFILE.fMaxN,
(ctx.currentTorqueNm ?? 0) / PROFILE.tauMaxNm,
Math.abs(ctx.currentAngularVelocityRps ?? 0) / PROFILE.omegaMaxRps,
);
const proximity = clamp((ctx.nearestObstacleMeters ?? 0) / PROFILE.dSafeM, 0, 1);
const human = ctx.humanDetected ? 0.5 : 1;
const novelty = ctx.trajectoryNovel ? 0.6 : 1;
const environmentalCapacity = PROFILE.eBase * proximity * human * novelty;
const capacityKnown = ctx.trajectoryNovel !== true && ctx.nearestObstacleMeters !== undefined;
const margin =
environmentalCapacity > 0 ? 1 - autonomyDemand / environmentalCapacity : Number.NEGATIVE_INFINITY;
const supervision = supervisionForMargin(margin, capacityKnown);
const tightenedConstraints: KineticTightenedConstraints = {};

if (ctx.currentVelocityMps !== undefined && margin > 0) {
tightenedConstraints.maxVelocityMps = round3(PROFILE.vMaxMps * clamp(margin, 0, 1));
}
if (ctx.currentForceNewtons !== undefined && margin > 0) {
tightenedConstraints.maxForceNewtons = round3(PROFILE.fMaxN * clamp(margin, 0, 1));
}
if (ctx.currentTorqueNm !== undefined && margin > 0) {
tightenedConstraints.maxTorqueNm = round3(PROFILE.tauMaxNm * clamp(margin, 0, 1));
}

return {
autonomyDemand: round3(autonomyDemand),
environmentalCapacity: round3(environmentalCapacity),
margin: round3(margin),
capacityKnown,
tightenedConstraints,
supervision,
rationale: [vector.note],
};
}

function expectTightenedAtMost(
actual: KineticTightenedConstraints,
ceilings: KineticTightenedConstraints | undefined,
): void {
if (!ceilings) return;
for (const [key, ceiling] of Object.entries(ceilings)) {
if (typeof ceiling !== "number") continue;
const actualValue = actual[key as keyof KineticTightenedConstraints];
expect(typeof actualValue, key).toBe("number");
expect(actualValue as number, key).toBeLessThanOrEqual(ceiling);
}
}

describe("Kinetic envelope conformance vectors", () => {
const fixture = loadFixture();

it("loads the ROS2 reference fixture with the unknown-capacity clamp case", () => {
expect(fixture.profile).toBe("ros2-reference-v0.1");
expect(fixture.vectors.map((v) => v.id)).toEqual([
"V1-cmd_vel-safe",
"V2-cmd_vel-near-margin",
"V3-human-near",
"V4-force-over-limit",
"V5-obstacle-proximity",
"V6-torque-over-limit",
"V7-novel-workspace-unknown-capacity",
]);
});

for (const vector of fixture.vectors) {
it(`${vector.id}: produces the expected decision surface`, () => {
const result = computeReferenceEnvelope(vector);
const parsed = kineticEnvelopeResultSchema.parse(result);
const decision = decisionForSupervision(parsed.supervision);
const policyCode =
parsed.supervision === "silent_veto" ? KINETIC_CAPACITY_EXCEEDED : undefined;

expect(parsed.margin).toBeCloseTo(vector.expect.referenceMargin, 3);
expect(marginBandFor(parsed.margin)).toBe(vector.expect.marginBand);
expect(parsed.supervision).toBe(vector.expect.supervision);
expect(decision).toBe(vector.expect.decision);
expect(policyCode).toBe(vector.expect.policyCode);
expect(parsed.capacityKnown).toBe(vector.expect.capacityKnown);
expectTightenedAtMost(parsed.tightenedConstraints, vector.expect.tightenedAtMost);

const receipt = kineticReceiptSchema.parse({
requestId: vector.id,
autonomyDemand: parsed.autonomyDemand,
environmentalCapacity: parsed.environmentalCapacity,
margin: parsed.margin,
capacityKnown: parsed.capacityKnown,
supervision: parsed.supervision,
finalTier: decision === "allow" ? ApprovalTier.T1_PREPARE : ApprovalTier.T2_ACT,
deautomated: decision === "escalate",
vetoed: decision === "deny",
policyCode,
rationale: parsed.rationale,
});
expect(receipt.vetoed).toBe(vector.expect.decision === "deny");
});
}
});
1 change: 1 addition & 0 deletions packages/core/src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./capability-token.schema.js";
export * from "./policy.schema.js";
export * from "./engine.schema.js";
export * from "./mission-authority.schema.js";
export * from "./kinetic-envelope.schema.js";
87 changes: 87 additions & 0 deletions packages/core/src/schemas/kinetic-envelope.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* SINT Protocol — Zod validation schemas for kinetic envelopes.
*
* These schemas validate the opt-in physics-aware supervision surface. They do
* not make any autonomy-demand / environmental-capacity formula normative.
*
* @module @sint/core/schemas/kinetic-envelope
*/

import { z } from "zod";
import { ApprovalTier, KINETIC_CAPACITY_EXCEEDED } from "../types/index.js";

export const supervisionLevelSchema = z.enum([
"stable",
"metacognitive",
"assisted",
"regulated",
"silent_veto",
]);

export const kineticJointLimitSchema = z.object({
joint: z.string().min(1).max(128),
maxPositionRad: z.number().optional(),
maxVelocityRps: z.number().min(0).optional(),
maxEffortNm: z.number().min(0).optional(),
}).strict();

export const kineticTightenedConstraintsSchema = z.object({
maxVelocityMps: z.number().min(0).optional(),
maxForceNewtons: z.number().min(0).optional(),
maxTorqueNm: z.number().min(0).optional(),
maxJerkMps3: z.number().min(0).optional(),
maxAngularVelocityRps: z.number().min(0).optional(),
jointLimits: z.array(kineticJointLimitSchema).optional(),
proximityMinMeters: z.number().min(0).optional(),
}).strict();

export const kineticEnvelopeResultSchema = z.object({
autonomyDemand: z.number().min(0),
environmentalCapacity: z.number().min(0),
margin: z.number(),
capacityKnown: z.boolean(),
tightenedConstraints: kineticTightenedConstraintsSchema,
supervision: supervisionLevelSchema,
rationale: z.array(z.string().min(1)),
}).strict();

export const kineticReceiptSchema = z.object({
requestId: z.string().min(1),
autonomyDemand: z.number().min(0),
environmentalCapacity: z.number().min(0),
margin: z.number(),
capacityKnown: z.boolean(),
supervision: supervisionLevelSchema,
finalTier: z.nativeEnum(ApprovalTier),
deautomated: z.boolean(),
vetoed: z.boolean(),
policyCode: z.literal(KINETIC_CAPACITY_EXCEEDED).optional(),
rationale: z.array(z.string().min(1)),
}).strict().superRefine((receipt, ctx) => {
if (receipt.vetoed && receipt.policyCode !== KINETIC_CAPACITY_EXCEEDED) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "vetoed kinetic receipts must carry KINETIC_CAPACITY_EXCEEDED",
path: ["policyCode"],
});
}

if (!receipt.vetoed && receipt.policyCode !== undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "policyCode is only valid on vetoed kinetic receipts",
path: ["policyCode"],
});
}

if (receipt.vetoed && receipt.deautomated) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "vetoed kinetic receipts are not deautomated escalations",
path: ["deautomated"],
});
}
});

export type ValidatedKineticEnvelopeResult = z.infer<typeof kineticEnvelopeResultSchema>;
export type ValidatedKineticReceipt = z.infer<typeof kineticReceiptSchema>;
11 changes: 11 additions & 0 deletions packages/core/src/schemas/policy.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ export const physicalContextSchema = z.object({
humanDetected: z.boolean().optional(),
currentForceNewtons: z.number().min(0).optional(),
currentVelocityMps: z.number().min(0).optional(),
currentTorqueNm: z.number().min(0).optional(),
currentJerkMps3: z.number().min(0).optional(),
currentAngularVelocityRps: z.number().min(0).optional(),
nearestObstacleMeters: z.number().min(0).optional(),
trajectoryNovel: z.boolean().optional(),
jointStates: z.array(z.object({
joint: z.string().min(1).max(128),
positionRad: z.number().optional(),
velocityRps: z.number().optional(),
effortNm: z.number().optional(),
}).strict()).optional(),
currentPosition: z.object({
x: z.number(),
y: z.number(),
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./engine.js";
export * from "./compliance.js";
export * from "./protocol.js";
export * from "./mission-authority.js";
export * from "./kinetic-envelope.js";
Loading