@you-agent-factory/client 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export {
2
+ FactoryRecordingValidationError,
3
+ parseFactoryRecording,
4
+ safeParseFactoryRecording,
5
+ } from "./recording-parser.js";
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export type {
2
+ FactoryDefinition,
3
+ FactoryEvent,
4
+ FactoryEventType,
5
+ FactoryRecording,
6
+ } from "./contracts.js";
7
+ export type { components, operations, paths } from "./generated/openapi.js";
8
+ export type {
9
+ FactoryRecordingParseResult,
10
+ FactoryRecordingValidationIssue,
11
+ FactoryRecordingValidationIssueCode,
12
+ } from "./recording-parser.js";
13
+ export {
14
+ FactoryRecordingValidationError,
15
+ parseFactoryRecording,
16
+ safeParseFactoryRecording,
17
+ } from "./recording-parser.js";
@@ -0,0 +1,37 @@
1
+ import type { FactoryRecording } from "./contracts.js";
2
+
3
+ export type FactoryRecordingValidationIssueCode =
4
+ | "INVALID_SHAPE"
5
+ | "UNSUPPORTED_RECORDING_VERSION"
6
+ | "UNSUPPORTED_EVENT_VERSION"
7
+ | "DUPLICATE_EVENT_ID"
8
+ | "MIXED_SESSION_ID"
9
+ | "NON_CANONICAL_ORDER"
10
+ | "MISSING_TOPOLOGY_BOOTSTRAP";
11
+
12
+ export interface FactoryRecordingValidationIssue {
13
+ code: FactoryRecordingValidationIssueCode;
14
+ path: string;
15
+ message: string;
16
+ eventIndex?: number;
17
+ eventId?: string;
18
+ }
19
+
20
+ export class FactoryRecordingValidationError extends Error {
21
+ readonly issues: readonly FactoryRecordingValidationIssue[];
22
+ constructor(issues: readonly FactoryRecordingValidationIssue[]);
23
+ }
24
+
25
+ export type FactoryRecordingParseResult =
26
+ | { success: true; data: FactoryRecording }
27
+ | {
28
+ success: false;
29
+ error: FactoryRecordingValidationError;
30
+ issues: readonly FactoryRecordingValidationIssue[];
31
+ };
32
+
33
+ export function safeParseFactoryRecording(
34
+ input: unknown,
35
+ ): FactoryRecordingParseResult;
36
+
37
+ export function parseFactoryRecording(input: unknown): FactoryRecording;
@@ -0,0 +1,230 @@
1
+ import Ajv2020 from "ajv/dist/2020.js";
2
+ import addFormats from "ajv-formats";
3
+ import recordingSchema from "./generated/factory-recording.schema.json" with {
4
+ type: "json",
5
+ };
6
+
7
+ const TOPOLOGY_EVENT_TYPE = "INITIAL_STRUCTURE_REQUEST";
8
+
9
+ // The standalone schema intentionally retains its OpenAPI discriminator as a
10
+ // raw-data annotation. Mutation-affecting Ajv options stay explicitly disabled.
11
+ const ajv = new Ajv2020({
12
+ allErrors: true,
13
+ coerceTypes: false,
14
+ removeAdditional: false,
15
+ strict: false,
16
+ useDefaults: false,
17
+ });
18
+ addFormats(ajv);
19
+ const validateShape = ajv.compile(recordingSchema);
20
+
21
+ export class FactoryRecordingValidationError extends Error {
22
+ constructor(issues) {
23
+ super(`Invalid Factory Recording: ${issues.length} validation issue(s)`);
24
+ this.name = "FactoryRecordingValidationError";
25
+ this.issues = Object.freeze([...issues]);
26
+ }
27
+ }
28
+
29
+ export function safeParseFactoryRecording(input) {
30
+ if (!validateShape(input)) {
31
+ return failure(shapeIssues(validateShape.errors ?? []));
32
+ }
33
+
34
+ const issues = semanticIssues(input);
35
+ return issues.length === 0 ? { success: true, data: input } : failure(issues);
36
+ }
37
+
38
+ export function parseFactoryRecording(input) {
39
+ const result = safeParseFactoryRecording(input);
40
+ if (!result.success) {
41
+ throw result.error;
42
+ }
43
+ return result.data;
44
+ }
45
+
46
+ function failure(issues) {
47
+ const error = new FactoryRecordingValidationError(issues);
48
+ return { success: false, error, issues: error.issues };
49
+ }
50
+
51
+ function shapeIssues(errors) {
52
+ return errors.map((error) => {
53
+ const path = issuePath(error);
54
+ const eventIndex = eventIndexFromPath(path);
55
+ const versionCode = versionIssueCode(error);
56
+ return {
57
+ code: versionCode ?? "INVALID_SHAPE",
58
+ path,
59
+ message: versionCode
60
+ ? `Unsupported schema version at ${path}.`
61
+ : `${path} ${error.message ?? "does not match the Factory Recording schema"}.`,
62
+ ...(eventIndex === undefined ? {} : { eventIndex }),
63
+ };
64
+ });
65
+ }
66
+
67
+ function issuePath(error) {
68
+ const property =
69
+ error.keyword === "required"
70
+ ? error.params.missingProperty
71
+ : error.keyword === "additionalProperties"
72
+ ? error.params.additionalProperty
73
+ : undefined;
74
+ if (typeof property !== "string") {
75
+ return error.instancePath || "/";
76
+ }
77
+ return `${error.instancePath}/${property.replaceAll("~", "~0").replaceAll("/", "~1")}`;
78
+ }
79
+
80
+ function versionIssueCode(error) {
81
+ if (error.keyword !== "enum") {
82
+ return undefined;
83
+ }
84
+ if (error.instancePath === "/schemaVersion") {
85
+ return "UNSUPPORTED_RECORDING_VERSION";
86
+ }
87
+ return /^\/events\/\d+\/schemaVersion$/.test(error.instancePath)
88
+ ? "UNSUPPORTED_EVENT_VERSION"
89
+ : undefined;
90
+ }
91
+
92
+ function eventIndexFromPath(path) {
93
+ const match = /^\/events\/(\d+)(?:\/|$)/.exec(path);
94
+ return match ? Number(match[1]) : undefined;
95
+ }
96
+
97
+ function semanticIssues(recording) {
98
+ const issues = [];
99
+ const eventIds = new Set();
100
+ let hasTopology = false;
101
+
102
+ for (const [eventIndex, event] of recording.events.entries()) {
103
+ const location = { eventIndex, eventId: event.id };
104
+ if (eventIds.has(event.id)) {
105
+ issues.push({
106
+ code: "DUPLICATE_EVENT_ID",
107
+ path: `/events/${eventIndex}/id`,
108
+ message: `Event id ${JSON.stringify(event.id)} appears more than once.`,
109
+ ...location,
110
+ });
111
+ }
112
+ eventIds.add(event.id);
113
+
114
+ if (event.context.sessionId !== recording.sessionId) {
115
+ issues.push({
116
+ code: "MIXED_SESSION_ID",
117
+ path: `/events/${eventIndex}/context/sessionId`,
118
+ message: `Event sessionId must equal recording sessionId ${JSON.stringify(recording.sessionId)}.`,
119
+ ...location,
120
+ });
121
+ }
122
+
123
+ if (event.type === TOPOLOGY_EVENT_TYPE) {
124
+ hasTopology = true;
125
+ }
126
+
127
+ if (
128
+ eventIndex > 0 &&
129
+ compareEvents(recording.events[eventIndex - 1], event) > 0
130
+ ) {
131
+ issues.push({
132
+ code: "NON_CANONICAL_ORDER",
133
+ path: `/events/${eventIndex}`,
134
+ message:
135
+ "Event is out of canonical tick, sequence, eventTime, and id order.",
136
+ ...location,
137
+ });
138
+ }
139
+ }
140
+
141
+ if (!hasTopology) {
142
+ issues.push({
143
+ code: "MISSING_TOPOLOGY_BOOTSTRAP",
144
+ path: "/events",
145
+ message: `Recording must contain an ${TOPOLOGY_EVENT_TYPE} event.`,
146
+ });
147
+ }
148
+
149
+ return issues;
150
+ }
151
+
152
+ function compareEvents(left, right) {
153
+ return (
154
+ compareNumber(left.context.tick, right.context.tick) ||
155
+ compareNumber(left.context.sequence, right.context.sequence) ||
156
+ compareEventTime(left.context.eventTime, right.context.eventTime) ||
157
+ left.id.localeCompare(right.id)
158
+ );
159
+ }
160
+
161
+ function compareEventTime(left, right) {
162
+ const leftInstant = parseEventTime(left);
163
+ const rightInstant = parseEventTime(right);
164
+ return (
165
+ compareNumber(leftInstant.wholeSecond, rightInstant.wholeSecond) ||
166
+ compareNumber(leftInstant.phase, rightInstant.phase) ||
167
+ compareFraction(leftInstant.fraction, rightInstant.fraction)
168
+ );
169
+ }
170
+
171
+ function parseEventTime(value) {
172
+ const match =
173
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})$/i.exec(
174
+ value,
175
+ );
176
+ // Shape validation, including RFC 3339 format validation, runs before this
177
+ // semantic comparison. Keep this guard explicit if that boundary changes.
178
+ if (!match) {
179
+ throw new TypeError(`Validated eventTime is not RFC 3339: ${value}`);
180
+ }
181
+ const [, year, month, day, hour, minute, second, fraction, zone] = match;
182
+ const offsetSeconds =
183
+ zone.toUpperCase() === "Z"
184
+ ? 0
185
+ : (zone[0] === "+" ? 1 : -1) *
186
+ (Number(zone.slice(1, 3)) * 60 + Number(zone.slice(4, 6))) *
187
+ 60;
188
+ const secondNumber = Number(second);
189
+ return {
190
+ wholeSecond:
191
+ daysFromCivil(Number(year), Number(month), Number(day)) * 86_400 +
192
+ Number(hour) * 3_600 +
193
+ Number(minute) * 60 +
194
+ secondNumber -
195
+ offsetSeconds,
196
+ // A leap second shares the next ordinary second's arithmetic boundary,
197
+ // but occurs immediately before it.
198
+ phase: secondNumber === 60 ? -1 : 0,
199
+ fraction: fraction ?? "",
200
+ };
201
+ }
202
+
203
+ function daysFromCivil(year, month, day) {
204
+ const adjustedYear = year - (month <= 2 ? 1 : 0);
205
+ const era = Math.floor(adjustedYear / 400);
206
+ const yearOfEra = adjustedYear - era * 400;
207
+ const adjustedMonth = month + (month > 2 ? -3 : 9);
208
+ const dayOfYear = Math.floor((153 * adjustedMonth + 2) / 5) + day - 1;
209
+ const dayOfEra =
210
+ yearOfEra * 365 +
211
+ Math.floor(yearOfEra / 4) -
212
+ Math.floor(yearOfEra / 100) +
213
+ dayOfYear;
214
+ return era * 146_097 + dayOfEra;
215
+ }
216
+
217
+ function compareFraction(left, right) {
218
+ const width = Math.max(left.length, right.length);
219
+ const normalizedLeft = left.padEnd(width, "0");
220
+ const normalizedRight = right.padEnd(width, "0");
221
+ return normalizedLeft === normalizedRight
222
+ ? 0
223
+ : normalizedLeft < normalizedRight
224
+ ? -1
225
+ : 1;
226
+ }
227
+
228
+ function compareNumber(left, right) {
229
+ return left === right ? 0 : left < right ? -1 : 1;
230
+ }