@you-agent-factory/client 0.0.0 → 0.0.1

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.
Files changed (47) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +78 -17
  3. package/dist/contracts.d.ts +51 -0
  4. package/dist/contracts.js +9 -0
  5. package/dist/event-ordering.d.ts +15 -0
  6. package/dist/event-ordering.js +33 -0
  7. package/dist/generated/factory-event.schema.json +5490 -0
  8. package/dist/generated/factory.schema.json +2606 -0
  9. package/dist/generated/openapi.d.ts +7587 -0
  10. package/dist/generated/openapi.js +1020 -0
  11. package/dist/index.d.ts +8 -0
  12. package/dist/index.js +8 -0
  13. package/dist/recording.d.ts +34 -0
  14. package/dist/recording.js +236 -0
  15. package/dist/replay.d.ts +21 -0
  16. package/dist/replay.js +82 -0
  17. package/dist/schema-validation.d.ts +9 -0
  18. package/dist/schema-validation.js +113 -0
  19. package/dist/visualization-layout-contracts.d.ts +63 -0
  20. package/dist/visualization-layout-contracts.js +1 -0
  21. package/dist/visualization-layout-data.d.ts +9 -0
  22. package/dist/visualization-layout-data.js +105 -0
  23. package/dist/visualization-layout-error.d.ts +5 -0
  24. package/dist/visualization-layout-error.js +10 -0
  25. package/dist/visualization-layout-fields.d.ts +7 -0
  26. package/dist/visualization-layout-fields.js +26 -0
  27. package/dist/visualization-layout-geometry.d.ts +5 -0
  28. package/dist/visualization-layout-geometry.js +74 -0
  29. package/dist/visualization-layout-media.d.ts +11 -0
  30. package/dist/visualization-layout-media.js +110 -0
  31. package/dist/visualization-layout-safety.d.ts +15 -0
  32. package/dist/visualization-layout-safety.js +142 -0
  33. package/dist/visualization-layout-topology.d.ts +11 -0
  34. package/dist/visualization-layout-topology.js +90 -0
  35. package/dist/visualization-layout.d.ts +12 -0
  36. package/dist/visualization-layout.js +299 -0
  37. package/examples/customer-support.factory-recording.v1.json +64 -0
  38. package/examples/customer-support.factory-visualization-layout.v1.json +47 -0
  39. package/package.json +37 -18
  40. package/recordings/factory-recording.json +0 -36
  41. package/src/contracts.ts +0 -13
  42. package/src/generated/factory-recording.schema.json +0 -5521
  43. package/src/generated/openapi.ts +0 -8134
  44. package/src/index.js +0 -5
  45. package/src/index.ts +0 -17
  46. package/src/recording-parser.d.ts +0 -37
  47. package/src/recording-parser.js +0 -230
@@ -0,0 +1,105 @@
1
+ const MAX_PLAIN_DATA_DEPTH = 64;
2
+ export function isInputRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ function dataPropertyPath(path, key, isArray) {
6
+ if (typeof key !== "string")
7
+ return path;
8
+ return [
9
+ ...path,
10
+ isArray && /^(?:0|[1-9]\d*)$/u.test(key) ? Number(key) : key,
11
+ ];
12
+ }
13
+ function reportNonPlainData(path, issues) {
14
+ issues.push({
15
+ category: "structure",
16
+ code: "non_plain_data",
17
+ path,
18
+ message: "Expected plain data with standard containers and own data properties.",
19
+ });
20
+ }
21
+ /** Reject behavior-bearing containers before the parser reads any input value. */
22
+ export function validatePlainDataContainers(value, path, issues) {
23
+ if (typeof value !== "object" || value === null)
24
+ return;
25
+ const activeContainers = new WeakSet();
26
+ const frames = [{ value, path, depth: 0, leaving: false }];
27
+ while (frames.length > 0) {
28
+ const frame = frames.pop();
29
+ if (!frame)
30
+ break;
31
+ if (frame.leaving) {
32
+ activeContainers.delete(frame.value);
33
+ continue;
34
+ }
35
+ if (activeContainers.has(frame.value) ||
36
+ frame.depth > MAX_PLAIN_DATA_DEPTH) {
37
+ reportNonPlainData(frame.path, issues);
38
+ continue;
39
+ }
40
+ const isArray = Array.isArray(frame.value);
41
+ let prototype;
42
+ let keys;
43
+ try {
44
+ prototype = Object.getPrototypeOf(frame.value);
45
+ keys = Reflect.ownKeys(frame.value);
46
+ }
47
+ catch {
48
+ reportNonPlainData(frame.path, issues);
49
+ continue;
50
+ }
51
+ if ((isArray && prototype !== Array.prototype) ||
52
+ (!isArray && prototype !== Object.prototype && prototype !== null)) {
53
+ reportNonPlainData(frame.path, issues);
54
+ continue;
55
+ }
56
+ activeContainers.add(frame.value);
57
+ frames.push({ ...frame, leaving: true });
58
+ const childFrames = [];
59
+ for (const key of keys) {
60
+ if (isArray && key === "length")
61
+ continue;
62
+ const propertyPath = dataPropertyPath(frame.path, key, isArray);
63
+ let descriptor;
64
+ try {
65
+ descriptor = Object.getOwnPropertyDescriptor(frame.value, key);
66
+ }
67
+ catch {
68
+ reportNonPlainData(propertyPath, issues);
69
+ continue;
70
+ }
71
+ if (typeof key !== "string" ||
72
+ descriptor === undefined ||
73
+ !Object.hasOwn(descriptor, "value") ||
74
+ !descriptor.enumerable) {
75
+ reportNonPlainData(propertyPath, issues);
76
+ continue;
77
+ }
78
+ if (isArray && !/^(?:0|[1-9]\d*)$/u.test(key)) {
79
+ reportNonPlainData(propertyPath, issues);
80
+ continue;
81
+ }
82
+ if (typeof descriptor.value === "object" && descriptor.value !== null) {
83
+ childFrames.push({
84
+ value: descriptor.value,
85
+ path: propertyPath,
86
+ depth: frame.depth + 1,
87
+ leaving: false,
88
+ });
89
+ }
90
+ }
91
+ frames.push(...childFrames.reverse());
92
+ }
93
+ }
94
+ /** Copy validated values into fresh standard containers without prototype data. */
95
+ export function clonePlainData(value) {
96
+ if (Array.isArray(value))
97
+ return value.map(clonePlainData);
98
+ if (typeof value !== "object" || value === null)
99
+ return value;
100
+ const clone = {};
101
+ for (const [key, child] of Object.entries(value)) {
102
+ clone[key] = clonePlainData(child);
103
+ }
104
+ return clone;
105
+ }
@@ -0,0 +1,5 @@
1
+ import type { FactoryVisualizationLayoutIssue } from "./visualization-layout-contracts.js";
2
+ export declare class FactoryVisualizationLayoutValidationError extends Error {
3
+ readonly issues: readonly FactoryVisualizationLayoutIssue[];
4
+ constructor(issues: readonly FactoryVisualizationLayoutIssue[]);
5
+ }
@@ -0,0 +1,10 @@
1
+ export class FactoryVisualizationLayoutValidationError extends Error {
2
+ issues;
3
+ constructor(issues) {
4
+ super(issues.length === 1
5
+ ? `Factory visualization layout validation failed: ${issues[0]?.message}`
6
+ : `Factory visualization layout validation failed with ${issues.length} issues`);
7
+ this.name = "FactoryVisualizationLayoutValidationError";
8
+ this.issues = issues;
9
+ }
10
+ }
@@ -0,0 +1,7 @@
1
+ export declare const layoutFields: Set<string>;
2
+ export declare const noteFields: Set<string>;
3
+ export declare const imageAnnotationFields: Set<string>;
4
+ export declare const emptyStateFields: Set<string>;
5
+ export declare const textContentFields: Set<string>;
6
+ export declare const imageContentFields: Set<string>;
7
+ export declare const imageSourceFields: Set<string>;
@@ -0,0 +1,26 @@
1
+ export const layoutFields = new Set([
2
+ "schemaVersion",
3
+ "annotations",
4
+ "nodeEmptyStates",
5
+ ]);
6
+ export const noteFields = new Set([
7
+ "id",
8
+ "kind",
9
+ "position",
10
+ "size",
11
+ "title",
12
+ "body",
13
+ "tone",
14
+ ]);
15
+ export const imageAnnotationFields = new Set([
16
+ "id",
17
+ "kind",
18
+ "position",
19
+ "size",
20
+ "altText",
21
+ "source",
22
+ ]);
23
+ export const emptyStateFields = new Set(["nodeId", "content"]);
24
+ export const textContentFields = new Set(["kind", "text"]);
25
+ export const imageContentFields = new Set(["kind", "altText", "source"]);
26
+ export const imageSourceFields = new Set(["kind", "mediaType", "base64"]);
@@ -0,0 +1,5 @@
1
+ import type { FactoryVisualizationLayoutIssue } from "./visualization-layout-contracts.js";
2
+ type InputPath = readonly (string | number)[];
3
+ export declare function validatePosition(input: unknown, path: InputPath, issues: FactoryVisualizationLayoutIssue[]): void;
4
+ export declare function validateSize(input: unknown, path: InputPath, issues: FactoryVisualizationLayoutIssue[]): void;
5
+ export {};
@@ -0,0 +1,74 @@
1
+ import { isInvalidCoordinate, isInvalidDimension, MAX_ANNOTATION_COORDINATE, MAX_ANNOTATION_DIMENSION, MIN_ANNOTATION_COORDINATE, } from "./visualization-layout-safety.js";
2
+ function validateNumericShape(input, fields, path, label, issues) {
3
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
4
+ issues.push({
5
+ category: "structure",
6
+ code: "invalid_type",
7
+ path,
8
+ message: `Expected ${label} to be an object.`,
9
+ });
10
+ return undefined;
11
+ }
12
+ const record = input;
13
+ for (const key of Object.keys(record)) {
14
+ if (!fields.includes(key)) {
15
+ issues.push({
16
+ category: "structure",
17
+ code: "unsupported_field",
18
+ path: [...path, key],
19
+ message: `Unsupported field ${key}.`,
20
+ });
21
+ }
22
+ }
23
+ for (const field of fields) {
24
+ if (!Object.hasOwn(record, field)) {
25
+ issues.push({
26
+ category: "structure",
27
+ code: "missing_required_field",
28
+ path: [...path, field],
29
+ message: `Expected required field ${field}.`,
30
+ });
31
+ }
32
+ else if (typeof record[field] !== "number") {
33
+ issues.push({
34
+ category: "structure",
35
+ code: "invalid_type",
36
+ path: [...path, field],
37
+ message: `Expected ${field} to be a number.`,
38
+ });
39
+ }
40
+ }
41
+ return record;
42
+ }
43
+ export function validatePosition(input, path, issues) {
44
+ const position = validateNumericShape(input, ["x", "y"], path, "position", issues);
45
+ if (!position)
46
+ return;
47
+ for (const coordinate of ["x", "y"]) {
48
+ const value = position[coordinate];
49
+ if (typeof value === "number" && isInvalidCoordinate(value)) {
50
+ issues.push({
51
+ category: "semantic",
52
+ code: "invalid_coordinate",
53
+ path: [...path, coordinate],
54
+ message: `Expected ${coordinate} to be finite and between ${MIN_ANNOTATION_COORDINATE} and ${MAX_ANNOTATION_COORDINATE}.`,
55
+ });
56
+ }
57
+ }
58
+ }
59
+ export function validateSize(input, path, issues) {
60
+ const size = validateNumericShape(input, ["width", "height"], path, "size", issues);
61
+ if (!size)
62
+ return;
63
+ for (const dimension of ["width", "height"]) {
64
+ const value = size[dimension];
65
+ if (typeof value === "number" && isInvalidDimension(value)) {
66
+ issues.push({
67
+ category: "semantic",
68
+ code: "invalid_dimension",
69
+ path: [...path, dimension],
70
+ message: `Expected ${dimension} to be finite, greater than zero, and at most ${MAX_ANNOTATION_DIMENSION}.`,
71
+ });
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,11 @@
1
+ import type { FactoryVisualizationLayoutIssue } from "./visualization-layout-contracts.js";
2
+ type InputPath = readonly (string | number)[];
3
+ export declare const MAX_EMBEDDED_IMAGE_BYTES: number;
4
+ export declare const MAX_LAYOUT_IMAGE_BYTES: number;
5
+ export declare const MAX_IMAGE_ALT_TEXT_LENGTH = 500;
6
+ export interface ImageByteBudget {
7
+ total: number;
8
+ aggregateLimitReported: boolean;
9
+ }
10
+ export declare function validateEmbeddedImageData(base64: string, mediaType: string, path: InputPath, issues: FactoryVisualizationLayoutIssue[], budget: ImageByteBudget): void;
11
+ export {};
@@ -0,0 +1,110 @@
1
+ export const MAX_EMBEDDED_IMAGE_BYTES = 2 * 1024 * 1024;
2
+ export const MAX_LAYOUT_IMAGE_BYTES = 8 * 1024 * 1024;
3
+ export const MAX_IMAGE_ALT_TEXT_LENGTH = 500;
4
+ const canonicalPaddedBase64 = /^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/u;
5
+ const base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
6
+ function decodedByteLength(value) {
7
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
8
+ return (value.length / 4) * 3 - padding;
9
+ }
10
+ function isCanonicalBase64(value) {
11
+ if (!canonicalPaddedBase64.test(value))
12
+ return false;
13
+ if (value.endsWith("==")) {
14
+ return (base64Alphabet.indexOf(value[value.length - 3] ?? "") & 0x0f) === 0;
15
+ }
16
+ if (value.endsWith("=")) {
17
+ return (base64Alphabet.indexOf(value[value.length - 2] ?? "") & 0x03) === 0;
18
+ }
19
+ return true;
20
+ }
21
+ function decodePrefix(value, byteLimit) {
22
+ const decoded = new Uint8Array(Math.min(decodedByteLength(value), byteLimit));
23
+ let decodedIndex = 0;
24
+ for (let index = 0; index < value.length && decodedIndex < byteLimit; index += 4) {
25
+ const first = base64Alphabet.indexOf(value[index] ?? "");
26
+ const second = base64Alphabet.indexOf(value[index + 1] ?? "");
27
+ const thirdCharacter = value[index + 2] ?? "=";
28
+ const fourthCharacter = value[index + 3] ?? "=";
29
+ const third = thirdCharacter === "=" ? 0 : base64Alphabet.indexOf(thirdCharacter);
30
+ const fourth = fourthCharacter === "=" ? 0 : base64Alphabet.indexOf(fourthCharacter);
31
+ const bits = (first << 18) | (second << 12) | (third << 6) | fourth;
32
+ decoded[decodedIndex] = (bits >> 16) & 0xff;
33
+ decodedIndex += 1;
34
+ if (thirdCharacter !== "=" && decodedIndex < decoded.length) {
35
+ decoded[decodedIndex] = (bits >> 8) & 0xff;
36
+ decodedIndex += 1;
37
+ }
38
+ if (fourthCharacter !== "=" && decodedIndex < decoded.length) {
39
+ decoded[decodedIndex] = bits & 0xff;
40
+ decodedIndex += 1;
41
+ }
42
+ }
43
+ return decoded;
44
+ }
45
+ function startsWith(bytes, signature) {
46
+ return (bytes.length >= signature.length &&
47
+ signature.every((byte, index) => bytes[index] === byte));
48
+ }
49
+ function signatureMatches(mediaType, bytes) {
50
+ if (mediaType === "image/png") {
51
+ return startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
52
+ }
53
+ if (mediaType === "image/jpeg") {
54
+ return startsWith(bytes, [0xff, 0xd8, 0xff]);
55
+ }
56
+ return (mediaType === "image/webp" &&
57
+ startsWith(bytes, [0x52, 0x49, 0x46, 0x46]) &&
58
+ bytes.length >= 12 &&
59
+ bytes[8] === 0x57 &&
60
+ bytes[9] === 0x45 &&
61
+ bytes[10] === 0x42 &&
62
+ bytes[11] === 0x50);
63
+ }
64
+ export function validateEmbeddedImageData(base64, mediaType, path, issues, budget) {
65
+ if (!isCanonicalBase64(base64)) {
66
+ issues.push({
67
+ category: "semantic",
68
+ code: "invalid_base64",
69
+ path,
70
+ message: "Expected strict canonical padded base64 without whitespace or a data-URL prefix.",
71
+ });
72
+ return;
73
+ }
74
+ const byteLength = decodedByteLength(base64);
75
+ if (byteLength === 0) {
76
+ issues.push({
77
+ category: "semantic",
78
+ code: "empty_image_payload",
79
+ path,
80
+ message: "Expected an embedded image with a non-empty decoded payload.",
81
+ });
82
+ return;
83
+ }
84
+ if (byteLength > MAX_EMBEDDED_IMAGE_BYTES) {
85
+ issues.push({
86
+ category: "semantic",
87
+ code: "image_too_large",
88
+ path,
89
+ message: `Expected each decoded image to contain at most ${MAX_EMBEDDED_IMAGE_BYTES} bytes.`,
90
+ });
91
+ }
92
+ budget.total += byteLength;
93
+ if (budget.total > MAX_LAYOUT_IMAGE_BYTES && !budget.aggregateLimitReported) {
94
+ budget.aggregateLimitReported = true;
95
+ issues.push({
96
+ category: "semantic",
97
+ code: "aggregate_image_bytes_exceeded",
98
+ path,
99
+ message: `This image occurrence raises the combined decoded image size above ${MAX_LAYOUT_IMAGE_BYTES} bytes.`,
100
+ });
101
+ }
102
+ if (signatureMatches(mediaType, decodePrefix(base64, 12)))
103
+ return;
104
+ issues.push({
105
+ category: "semantic",
106
+ code: "image_media_type_mismatch",
107
+ path,
108
+ message: `Decoded image signature does not match declared media type ${mediaType}.`,
109
+ });
110
+ }
@@ -0,0 +1,15 @@
1
+ import type { FactoryVisualizationLayoutIssue } from "./visualization-layout-contracts.js";
2
+ type InputPath = readonly (string | number)[];
3
+ export declare const MIN_ANNOTATION_COORDINATE = -100000;
4
+ export declare const MAX_ANNOTATION_COORDINATE = 100000;
5
+ export declare const MAX_ANNOTATION_DIMENSION = 10000;
6
+ export declare const MAX_NOTE_TITLE_LENGTH = 160;
7
+ export declare const MAX_NOTE_BODY_LENGTH = 4000;
8
+ export declare const MAX_EMPTY_STATE_TEXT_LENGTH = 500;
9
+ export declare function validatePlainText(value: string, path: InputPath, maxLength: number, label: string, issues: FactoryVisualizationLayoutIssue[], requireNonWhitespace: boolean): void;
10
+ export declare function isUnsafeContentField(field: string): boolean;
11
+ export declare function isInvalidCoordinate(value: number): boolean;
12
+ export declare function isInvalidDimension(value: number): boolean;
13
+ export declare function validateDuplicateAnnotationIds(annotations: readonly unknown[], issues: FactoryVisualizationLayoutIssue[]): void;
14
+ export declare function validateDuplicateNodeIds(emptyStates: readonly unknown[], issues: FactoryVisualizationLayoutIssue[]): void;
15
+ export {};
@@ -0,0 +1,142 @@
1
+ import { marked } from "marked";
2
+ export const MIN_ANNOTATION_COORDINATE = -100_000;
3
+ export const MAX_ANNOTATION_COORDINATE = 100_000;
4
+ export const MAX_ANNOTATION_DIMENSION = 10_000;
5
+ export const MAX_NOTE_TITLE_LENGTH = 160;
6
+ export const MAX_NOTE_BODY_LENGTH = 4_000;
7
+ export const MAX_EMPTY_STATE_TEXT_LENGTH = 500;
8
+ function containsFormattedToken(tokens) {
9
+ for (const token of tokens) {
10
+ if (token.type !== "paragraph" &&
11
+ token.type !== "text" &&
12
+ token.type !== "space") {
13
+ return true;
14
+ }
15
+ if ("tokens" in token &&
16
+ token.tokens &&
17
+ containsFormattedToken(token.tokens)) {
18
+ return true;
19
+ }
20
+ }
21
+ return false;
22
+ }
23
+ function containsMarkdown(value) {
24
+ // Undefined reference labels are plain text to a renderer, but still carry
25
+ // Markdown syntax that could become active when combined with a definition.
26
+ return (containsFormattedToken(marked.lexer(value, { gfm: true })) ||
27
+ /!?\[[^\]\n]*\]\[[^\]\n]*\]/u.test(value));
28
+ }
29
+ function unsafeTextMatch(value) {
30
+ if (/<!--|<!doctype\b|<\/?[a-z][^>]*>/iu.test(value)) {
31
+ return {
32
+ code: "unsafe_html",
33
+ message: "Expected inert plain text without HTML or media markup.",
34
+ };
35
+ }
36
+ if (/(?:\b(?:https?|ftp|file|data|javascript|mailto):|\bwww\.)\S*/iu.test(value)) {
37
+ return {
38
+ code: "unsafe_uri",
39
+ message: "Expected inert plain text without URI-bearing content.",
40
+ };
41
+ }
42
+ if (containsMarkdown(value)) {
43
+ return {
44
+ code: "unsafe_markdown",
45
+ message: "Expected inert plain text without Markdown or links.",
46
+ };
47
+ }
48
+ return undefined;
49
+ }
50
+ export function validatePlainText(value, path, maxLength, label, issues, requireNonWhitespace) {
51
+ if (requireNonWhitespace && value.trim().length === 0) {
52
+ issues.push({
53
+ category: "semantic",
54
+ code: "empty_text",
55
+ path,
56
+ message: `Expected ${label} to contain non-whitespace text.`,
57
+ });
58
+ }
59
+ if ([...value].length > maxLength) {
60
+ issues.push({
61
+ category: "semantic",
62
+ code: "text_too_long",
63
+ path,
64
+ message: `Expected ${label} to contain at most ${maxLength} characters.`,
65
+ });
66
+ }
67
+ const unsafe = unsafeTextMatch(value);
68
+ if (unsafe) {
69
+ issues.push({
70
+ category: "semantic",
71
+ code: unsafe.code,
72
+ path,
73
+ message: unsafe.message,
74
+ });
75
+ }
76
+ }
77
+ export function isUnsafeContentField(field) {
78
+ return /^(?:callback|command|connection|connections|edge|edges|event|handler|href|html|link|markdown|onclick|route|script|src|uri|url)$/iu.test(field);
79
+ }
80
+ export function isInvalidCoordinate(value) {
81
+ return (!Number.isFinite(value) ||
82
+ value < MIN_ANNOTATION_COORDINATE ||
83
+ value > MAX_ANNOTATION_COORDINATE);
84
+ }
85
+ export function isInvalidDimension(value) {
86
+ return (!Number.isFinite(value) || value <= 0 || value > MAX_ANNOTATION_DIMENSION);
87
+ }
88
+ export function validateDuplicateAnnotationIds(annotations, issues) {
89
+ const indexesById = new Map();
90
+ for (const [index, annotation] of annotations.entries()) {
91
+ const record = annotation;
92
+ if (typeof annotation !== "object" ||
93
+ annotation === null ||
94
+ !Object.hasOwn(record, "id") ||
95
+ typeof record.id !== "string") {
96
+ continue;
97
+ }
98
+ const indexes = indexesById.get(record.id) ?? [];
99
+ indexes.push(index);
100
+ indexesById.set(record.id, indexes);
101
+ }
102
+ for (const [id, indexes] of indexesById) {
103
+ if (indexes.length < 2)
104
+ continue;
105
+ for (const index of indexes) {
106
+ issues.push({
107
+ category: "semantic",
108
+ code: "duplicate_annotation_id",
109
+ path: ["annotations", index, "id"],
110
+ message: `Annotation ID ${id} is duplicated at annotation indexes ${indexes.join(", ")}.`,
111
+ });
112
+ }
113
+ }
114
+ }
115
+ export function validateDuplicateNodeIds(emptyStates, issues) {
116
+ const indexesById = new Map();
117
+ for (const [index, emptyState] of emptyStates.entries()) {
118
+ const record = emptyState;
119
+ if (typeof emptyState !== "object" ||
120
+ emptyState === null ||
121
+ !Object.hasOwn(record, "nodeId") ||
122
+ typeof record.nodeId !== "string" ||
123
+ record.nodeId.trim().length === 0) {
124
+ continue;
125
+ }
126
+ const indexes = indexesById.get(record.nodeId) ?? [];
127
+ indexes.push(index);
128
+ indexesById.set(record.nodeId, indexes);
129
+ }
130
+ for (const [nodeId, indexes] of indexesById) {
131
+ if (indexes.length < 2)
132
+ continue;
133
+ for (const index of indexes) {
134
+ issues.push({
135
+ category: "semantic",
136
+ code: "duplicate_node_id",
137
+ path: ["nodeEmptyStates", index, "nodeId"],
138
+ message: `Canonical node ID ${nodeId} is duplicated at empty-state indexes ${indexes.join(", ")}.`,
139
+ });
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,11 @@
1
+ import type { FactoryDefinition } from "./contracts.js";
2
+ import type { FactoryVisualizationLayoutIssue } from "./visualization-layout-contracts.js";
3
+ type InputPath = readonly (string | number)[];
4
+ /**
5
+ * Project the canonical node IDs that the existing Factory graph projection
6
+ * derives from authored entities and references. This is data-only and does
7
+ * not read or update graph-editor state.
8
+ */
9
+ export declare function factoryVisualizationCanonicalNodeIds(factory: Readonly<FactoryDefinition>): ReadonlySet<string>;
10
+ export declare function validateCanonicalNodeId(nodeId: string, path: InputPath, canonicalNodeIds: ReadonlySet<string>, issues: FactoryVisualizationLayoutIssue[]): void;
11
+ export {};
@@ -0,0 +1,90 @@
1
+ function entityIdentifier(explicitId, fallbackName) {
2
+ const normalizedId = explicitId?.trim();
3
+ return normalizedId ? normalizedId : fallbackName;
4
+ }
5
+ function addSubjectNode(nodeIds, kind, explicitId, fallbackName) {
6
+ nodeIds.add(`${kind}:${entityIdentifier(explicitId, fallbackName)}`);
7
+ }
8
+ /**
9
+ * Project the canonical node IDs that the existing Factory graph projection
10
+ * derives from authored entities and references. This is data-only and does
11
+ * not read or update graph-editor state.
12
+ */
13
+ export function factoryVisualizationCanonicalNodeIds(factory) {
14
+ const nodeIds = new Set();
15
+ const resourceIds = new Map();
16
+ const workerIds = new Map();
17
+ const workTypeIds = new Map();
18
+ const stateIds = new Map();
19
+ for (const file of factory.supportingFiles?.bundledFiles ?? []) {
20
+ const targetPath = file.targetPath?.trim();
21
+ if (targetPath)
22
+ addSubjectNode(nodeIds, "doc", targetPath, targetPath);
23
+ }
24
+ for (const resource of factory.resources ?? []) {
25
+ if (resource.id?.trim())
26
+ resourceIds.set(resource.name, resource.id);
27
+ addSubjectNode(nodeIds, "resource", resource.id, resource.name);
28
+ }
29
+ for (const worker of factory.workers ?? []) {
30
+ if (worker.id?.trim())
31
+ workerIds.set(worker.name, worker.id);
32
+ addSubjectNode(nodeIds, "worker", worker.id, worker.name);
33
+ for (const resource of worker.resources ?? []) {
34
+ addSubjectNode(nodeIds, "resource", resourceIds.get(resource.name), resource.name);
35
+ }
36
+ }
37
+ for (const workType of factory.workTypes ?? []) {
38
+ if (workType.id?.trim())
39
+ workTypeIds.set(workType.name, workType.id);
40
+ addSubjectNode(nodeIds, "work-type", workType.id, workType.name);
41
+ const workTypeStateIds = new Map();
42
+ for (const state of workType.states) {
43
+ if (state.id?.trim())
44
+ workTypeStateIds.set(state.name, state.id);
45
+ nodeIds.add(`work-state:${entityIdentifier(workType.id, workType.name)}:${entityIdentifier(state.id, state.name)}`);
46
+ }
47
+ stateIds.set(workType.name, workTypeStateIds);
48
+ }
49
+ const addWorkState = (workTypeName, stateName) => {
50
+ nodeIds.add(`work-state:${entityIdentifier(workTypeIds.get(workTypeName), workTypeName)}:${entityIdentifier(stateIds.get(workTypeName)?.get(stateName), stateName)}`);
51
+ };
52
+ for (const workstation of factory.workstations ?? []) {
53
+ addSubjectNode(nodeIds, "workstation", workstation.id, workstation.name);
54
+ const workerName = workstation.worker.trim();
55
+ if (workerName) {
56
+ addSubjectNode(nodeIds, "worker", workerIds.get(workerName), workerName);
57
+ }
58
+ for (const resource of workstation.resources ?? []) {
59
+ addSubjectNode(nodeIds, "resource", resourceIds.get(resource.name), resource.name);
60
+ }
61
+ for (const route of [
62
+ ...(workstation.inputs ?? []),
63
+ ...(workstation.outputs ?? []),
64
+ ...(workstation.onContinue ?? []),
65
+ ...(workstation.onFailure ?? []),
66
+ ...(workstation.onRejection ?? []),
67
+ ]) {
68
+ addWorkState(route.workType, route.state);
69
+ }
70
+ }
71
+ return nodeIds;
72
+ }
73
+ export function validateCanonicalNodeId(nodeId, path, canonicalNodeIds, issues) {
74
+ if (nodeId.trim().length === 0) {
75
+ issues.push({
76
+ category: "semantic",
77
+ code: "empty_node_id",
78
+ path,
79
+ message: "Expected a non-blank canonical topology node ID.",
80
+ });
81
+ }
82
+ else if (!canonicalNodeIds.has(nodeId)) {
83
+ issues.push({
84
+ category: "semantic",
85
+ code: "unknown_canonical_node_id",
86
+ path,
87
+ message: `Canonical topology node ${nodeId} does not exist in the supplied Factory.`,
88
+ });
89
+ }
90
+ }
@@ -0,0 +1,12 @@
1
+ import type { FactoryDefinition } from "./contracts.js";
2
+ import { type FactoryVisualizationLayoutV1, type SafeParseFactoryVisualizationLayoutResult } from "./visualization-layout-contracts.js";
3
+ /** A prepared topology can supply its canonical node IDs without owning Factory state. */
4
+ export interface FactoryVisualizationLayoutCanonicalNodeContext {
5
+ canonicalNodeIds: ReadonlySet<string>;
6
+ }
7
+ /**
8
+ * Validate presentation metadata beside an immutable canonical Factory.
9
+ * The Factory is accepted as context but is never modified or included in the result.
10
+ */
11
+ export declare function safeParseFactoryVisualizationLayout(input: unknown, factory: Readonly<FactoryDefinition> | FactoryVisualizationLayoutCanonicalNodeContext): SafeParseFactoryVisualizationLayoutResult;
12
+ export declare function parseFactoryVisualizationLayout(input: unknown, factory: Readonly<FactoryDefinition> | FactoryVisualizationLayoutCanonicalNodeContext): FactoryVisualizationLayoutV1;