stylegallery 0.1.0 → 0.1.2

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 (230) hide show
  1. package/AGENTS.md +4 -1
  2. package/DOMAINS.md +2 -6
  3. package/GOVERNANCE.md +10 -9
  4. package/README.md +29 -127
  5. package/consumer-reference/agent-native/v2/admission-policy.json +646 -0
  6. package/consumer-reference/agent-native/v2/material-registry.json +1 -0
  7. package/consumer-reference/agent-native/v2/schema/admission-policy.schema.json +50 -0
  8. package/consumer-reference/agent-native/v2/schema/material-manifest.schema.json +17 -0
  9. package/consumer-reference/agent-native/v2/schema/material-record.schema.json +20 -0
  10. package/consumer-reference/contract.md +1 -1
  11. package/consumer-reference/index.md +5 -3
  12. package/layout/index.md +124 -0
  13. package/log.md +57 -0
  14. package/package.json +183 -22
  15. package/scripts/agent-native/v2/experimental-extension-registry.mjs +143 -0
  16. package/scripts/agent-native/v2/extensions/a2a-projection.mjs +264 -0
  17. package/scripts/agent-native/v2/extensions/agui-projection.mjs +191 -0
  18. package/scripts/agent-native/v2/material-admission.mjs +295 -0
  19. package/scripts/agent-native/v2/material-cli-adapter.mjs +134 -0
  20. package/scripts/agent-native/v2/material-context.mjs +251 -0
  21. package/scripts/agent-native/v2/material-identity.mjs +71 -0
  22. package/scripts/agent-native/v2/material-mcp-adapter.mjs +253 -0
  23. package/scripts/agent-native/v2/material-operation-registry.mjs +123 -0
  24. package/scripts/agent-native/v2/material-queries.mjs +398 -0
  25. package/scripts/agent-native/v2/material-registry.mjs +319 -0
  26. package/scripts/sg-material-mcp.mjs +98 -0
  27. package/scripts/sg-material.mjs +27 -0
  28. package/consumer-reference/adapters/style-dictionary.config.mjs +0 -36
  29. package/consumer-reference/baselines/calibration.json +0 -260
  30. package/consumer-reference/baselines/manifest.json +0 -21
  31. package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +0 -282
  32. package/consumer-reference/fixtures/consumer-conformance/manifest.json +0 -102
  33. package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +0 -32
  34. package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +0 -18
  35. package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +0 -142
  36. package/consumer-reference/fixtures/invalid-stable-ended-support.json +0 -19
  37. package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +0 -26
  38. package/consumer-reference/fixtures/promotion/invalid-count-only.json +0 -29
  39. package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +0 -25
  40. package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +0 -25
  41. package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +0 -45
  42. package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +0 -26
  43. package/consumer-reference/fixtures/promotion/manifest.json +0 -15
  44. package/consumer-reference/fixtures/promotion/valid-deferred-example.json +0 -67
  45. package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +0 -67
  46. package/consumer-reference/fixtures/token-portability/invalid-cycle.json +0 -7
  47. package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +0 -6
  48. package/consumer-reference/fixtures/token-portability/invalid-extends.json +0 -4
  49. package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +0 -7
  50. package/consumer-reference/fixtures/token-portability/invalid-resolver.json +0 -9
  51. package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +0 -10
  52. package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +0 -6
  53. package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +0 -6
  54. package/consumer-reference/fixtures/token-portability/valid-border.json +0 -14
  55. package/consumer-reference/fixtures/token-portability/valid-reference.json +0 -32
  56. package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +0 -7
  57. package/consumer-reference/fixtures/valid-deprecated.json +0 -21
  58. package/consumer-reference/fixtures/valid-experimental.json +0 -19
  59. package/consumer-reference/generated/manifest.json +0 -40
  60. package/consumer-reference/generated/tokens.css +0 -12
  61. package/consumer-reference/policies/shared-experimental.json +0 -37
  62. package/consumer-reference/schema/ax-evidence.schema.json +0 -30
  63. package/consumer-reference/schema/baseline-manifest.schema.json +0 -55
  64. package/consumer-reference/schema/calibration-record.schema.json +0 -190
  65. package/consumer-reference/schema/capture-session.schema.json +0 -161
  66. package/consumer-reference/schema/component-state.schema.json +0 -150
  67. package/consumer-reference/schema/consumer-conformance-record.schema.json +0 -365
  68. package/consumer-reference/schema/dom-evidence.schema.json +0 -38
  69. package/consumer-reference/schema/evidence-record.schema.json +0 -81
  70. package/consumer-reference/schema/fixture-manifest.schema.json +0 -32
  71. package/consumer-reference/schema/item.schema.json +0 -188
  72. package/consumer-reference/schema/page-evidence-manifest.schema.json +0 -216
  73. package/consumer-reference/schema/page-evidence-session.schema.json +0 -119
  74. package/consumer-reference/schema/portable-tokens.schema.json +0 -104
  75. package/consumer-reference/schema/promotion-rfc.schema.json +0 -219
  76. package/consumer-reference/schema/runtime-evidence-manifest.schema.json +0 -33
  77. package/consumer-reference/schema/visual-evidence.schema.json +0 -31
  78. package/design-engineering/consumer-migration-readiness.md +0 -124
  79. package/design-engineering/interface-craft.md +0 -107
  80. package/design-engineering/reference-profiles/external-adaptation/index.md +0 -12
  81. package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +0 -125
  82. package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +0 -3504
  83. package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +0 -65
  84. package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +0 -23
  85. package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +0 -19
  86. package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +0 -21
  87. package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +0 -12
  88. package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +0 -195
  89. package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +0 -17
  90. package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +0 -125
  91. package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +0 -3504
  92. package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +0 -65
  93. package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +0 -23
  94. package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +0 -19
  95. package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +0 -21
  96. package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +0 -12
  97. package/design-engineering/reference-profiles/governed-local/terminal/profile.json +0 -43
  98. package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +0 -195
  99. package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +0 -17
  100. package/game-ui/classification.md +0 -82
  101. package/game-ui/reference-record.md +0 -62
  102. package/game-ui/screen-hierarchy.md +0 -107
  103. package/game-ui/unity/architecture.md +0 -166
  104. package/game-ui/unity/cli-loop.md +0 -84
  105. package/game-ui/unity/data/unity-technologies-public-repositories.json +0 -26654
  106. package/game-ui/unity/org-wiki.md +0 -146
  107. package/game-ui/unity/repository-map.md +0 -102
  108. package/game-ui/unity/ui-systems.md +0 -85
  109. package/motion/practice-reference.md +0 -145
  110. package/motion/review-workflow.md +0 -86
  111. package/motion/vocabulary.md +0 -82
  112. package/platform-guides/apple-interaction.md +0 -89
  113. package/scripts/agent-native/events.mjs +0 -113
  114. package/scripts/agent-native/immutable-store.mjs +0 -102
  115. package/scripts/artifact-metadata.mjs +0 -20
  116. package/scripts/audit-evidence-freshness.mjs +0 -308
  117. package/scripts/baseline-contract.mjs +0 -189
  118. package/scripts/baseline-schema-parity.mjs +0 -40
  119. package/scripts/build-reference-artifacts.mjs +0 -127
  120. package/scripts/calibration-raw-contract.mjs +0 -172
  121. package/scripts/capture-session-contract.mjs +0 -187
  122. package/scripts/component-state-artifact-fixture.mjs +0 -44
  123. package/scripts/component-state-contract.mjs +0 -99
  124. package/scripts/component-state-semantics.mjs +0 -128
  125. package/scripts/component-state-workflow-contract.mjs +0 -67
  126. package/scripts/component-state-workflow-negative-cases.mjs +0 -64
  127. package/scripts/consumer-conformance-contract.mjs +0 -198
  128. package/scripts/consumer-evidence-governance-contract.mjs +0 -210
  129. package/scripts/consumer-reference-case-runner.mjs +0 -93
  130. package/scripts/consumer-reference-schema.mjs +0 -155
  131. package/scripts/create-component-state-session.mjs +0 -100
  132. package/scripts/create-page-evidence-session.mjs +0 -139
  133. package/scripts/domain-document-boundaries.mjs +0 -40
  134. package/scripts/domain-validation-checks.mjs +0 -141
  135. package/scripts/evidence-artifact-contract.mjs +0 -191
  136. package/scripts/finalize-component-state-evidence.mjs +0 -165
  137. package/scripts/finalize-page-evidence.mjs +0 -197
  138. package/scripts/generate-consumer-reference-evidence.mjs +0 -154
  139. package/scripts/generate-patterns.mjs +0 -218
  140. package/scripts/governance-matrix-contract.mjs +0 -73
  141. package/scripts/governance-matrix-negative-cases.mjs +0 -114
  142. package/scripts/governance-policy-contract.mjs +0 -49
  143. package/scripts/governance-test-fixture.mjs +0 -321
  144. package/scripts/governance-workflow-negative-cases.mjs +0 -241
  145. package/scripts/governed-profile-registry.mjs +0 -125
  146. package/scripts/json-schema-formats.mjs +0 -31
  147. package/scripts/markdown-structure.mjs +0 -45
  148. package/scripts/page-artifact-metadata.mjs +0 -124
  149. package/scripts/page-evidence-contract.mjs +0 -297
  150. package/scripts/page-evidence-fixture.mjs +0 -271
  151. package/scripts/pattern-data.mjs +0 -325
  152. package/scripts/profile-record-contract.mjs +0 -140
  153. package/scripts/promotion-attestation-contract.mjs +0 -91
  154. package/scripts/promotion-boundary-test-contract.mjs +0 -92
  155. package/scripts/promotion-fixture-inventory.mjs +0 -109
  156. package/scripts/promotion-governance-contract.mjs +0 -28
  157. package/scripts/reference-artifact-path-contract.mjs +0 -28
  158. package/scripts/reference-profile-contract.mjs +0 -114
  159. package/scripts/reference-token-contract.mjs +0 -156
  160. package/scripts/run-consumer-page-evidence-ci.mjs +0 -170
  161. package/scripts/summarize-sentinel-calibration.mjs +0 -98
  162. package/scripts/test-agent-adapter-conformance.mjs +0 -250
  163. package/scripts/test-agent-conformance-receipt.mjs +0 -106
  164. package/scripts/test-agent-events.mjs +0 -81
  165. package/scripts/test-agent-execution-effects.mjs +0 -114
  166. package/scripts/test-agent-execution-fixtures.mjs +0 -140
  167. package/scripts/test-agent-execution-kernel.mjs +0 -39
  168. package/scripts/test-agent-execution-learning.mjs +0 -130
  169. package/scripts/test-agent-execution-lifecycle.mjs +0 -143
  170. package/scripts/test-agent-execution-safety.mjs +0 -158
  171. package/scripts/test-agent-execution-views.mjs +0 -117
  172. package/scripts/test-agent-identity-integrity-cases.mjs +0 -160
  173. package/scripts/test-agent-knowledge-kernel.mjs +0 -183
  174. package/scripts/test-agent-learning.mjs +0 -246
  175. package/scripts/test-agent-mcp.mjs +0 -246
  176. package/scripts/test-agent-native-schemas.mjs +0 -215
  177. package/scripts/test-agent-projections.mjs +0 -249
  178. package/scripts/test-agent-registry-mcp-integrity.mjs +0 -215
  179. package/scripts/test-agent-registry-mutations.mjs +0 -110
  180. package/scripts/test-audit-evidence-freshness.mjs +0 -177
  181. package/scripts/test-component-state-sentinel.mjs +0 -35
  182. package/scripts/test-component-state-source-contract.mjs +0 -83
  183. package/scripts/test-consumer-conformance-sentinel.mjs +0 -165
  184. package/scripts/test-consumer-reference-sentinel.mjs +0 -31
  185. package/scripts/test-generate-consumer-reference-evidence.mjs +0 -124
  186. package/scripts/test-json-cli-output.mjs +0 -151
  187. package/scripts/test-json-schema-formats.mjs +0 -40
  188. package/scripts/test-reference-adapters.mjs +0 -199
  189. package/scripts/test-renderer-purity.mjs +0 -34
  190. package/scripts/test-sg-cli.mjs +0 -129
  191. package/scripts/test-summarize-sentinel-calibration.mjs +0 -226
  192. package/scripts/test-validate-baseline-manifest.mjs +0 -234
  193. package/scripts/test-validate-component-state-artifacts.mjs +0 -243
  194. package/scripts/test-validate-component-state.mjs +0 -106
  195. package/scripts/test-validate-consumer-conformance.mjs +0 -427
  196. package/scripts/test-validate-consumer-reference-cli.mjs +0 -69
  197. package/scripts/test-validate-consumer-reference.mjs +0 -263
  198. package/scripts/test-validate-domains.mjs +0 -357
  199. package/scripts/test-validate-governance.mjs +0 -292
  200. package/scripts/test-validate-ia.mjs +0 -262
  201. package/scripts/test-validate-okf.mjs +0 -73
  202. package/scripts/test-validate-page-evidence.mjs +0 -302
  203. package/scripts/test-validate-patterns.mjs +0 -157
  204. package/scripts/test-validate-promotion-rfc.mjs +0 -250
  205. package/scripts/test-validate-reference-profiles.mjs +0 -170
  206. package/scripts/test-validate-unity-org-wiki.mjs +0 -57
  207. package/scripts/test-validate-unity-source-contracts.mjs +0 -67
  208. package/scripts/test-validate-vocabulary.mjs +0 -174
  209. package/scripts/test-validate-webpage-workflow.mjs +0 -293
  210. package/scripts/validate-baseline-manifest.mjs +0 -44
  211. package/scripts/validate-catalog.mjs +0 -65
  212. package/scripts/validate-component-state.mjs +0 -141
  213. package/scripts/validate-consumer-conformance.mjs +0 -477
  214. package/scripts/validate-consumer-reference.mjs +0 -288
  215. package/scripts/validate-domains.mjs +0 -237
  216. package/scripts/validate-governance.mjs +0 -248
  217. package/scripts/validate-ia.mjs +0 -136
  218. package/scripts/validate-links.mjs +0 -66
  219. package/scripts/validate-okf.mjs +0 -104
  220. package/scripts/validate-page-evidence.mjs +0 -191
  221. package/scripts/validate-patterns.mjs +0 -246
  222. package/scripts/validate-promotion-rfc.mjs +0 -224
  223. package/scripts/validate-reference-artifacts.mjs +0 -125
  224. package/scripts/validate-renderer-purity.mjs +0 -56
  225. package/scripts/validate-unity-org-wiki.mjs +0 -146
  226. package/scripts/validate-unity-source-contracts.mjs +0 -107
  227. package/scripts/validate-vocabulary.mjs +0 -115
  228. package/scripts/validate-webpage-workflow.mjs +0 -254
  229. package/scripts/visual-expectation-contract.mjs +0 -20
  230. package/scripts/workflow-action-contract.mjs +0 -163
@@ -0,0 +1,191 @@
1
+ import { canonicalize, cloneAndFreeze, hashCanonical } from "../../canonical-json.mjs";
2
+
3
+ const TERMINAL = new Set(["RUN_FINISHED", "RUN_ERROR"]);
4
+ const EVENT_TYPES = new Set([
5
+ "RUN_STARTED", "RUN_FINISHED", "RUN_ERROR",
6
+ "TEXT_MESSAGE_START", "TEXT_MESSAGE_CONTENT", "TEXT_MESSAGE_END",
7
+ "TOOL_CALL_START", "TOOL_CALL_ARGS", "TOOL_CALL_ARGUMENTS", "TOOL_CALL_END",
8
+ "STATE_SNAPSHOT", "STATE_DELTA",
9
+ ]);
10
+
11
+ export class AgUiProjectionError extends TypeError {
12
+ constructor(code, message, path = "") {
13
+ super(message);
14
+ this.name = "AgUiProjectionError";
15
+ this.code = code;
16
+ if (path) this.path = path;
17
+ }
18
+ }
19
+
20
+ function pick(value, ...keys) {
21
+ if (!value || typeof value !== "object") return undefined;
22
+ for (const key of keys) if (Object.hasOwn(value, key) && value[key] !== undefined) return value[key];
23
+ return undefined;
24
+ }
25
+
26
+ function text(value, fallback) {
27
+ return typeof value === "string" && value.length > 0 ? value : fallback;
28
+ }
29
+
30
+ function domainId(value, ...keys) {
31
+ return text(pick(value, ...keys, "stable_ref", "stableRef", "id"), "unknown");
32
+ }
33
+
34
+ function bindingIds(binding, task, run) {
35
+ const bindingId = text(pick(binding, "binding_id", "bindingId", "protocol_binding_id", "protocolBindingId"), "sg:binding/agui");
36
+ const taskId = domainId(task, "task_id", "taskId");
37
+ const runId = domainId(run, "run_id", "runId");
38
+ const taskProtocolId = text(
39
+ pick(binding, "agui_task_id", "aguiTaskId", "protocol_task_id", "protocolTaskId", "protocolTask"),
40
+ `agui-task-${hashCanonical({ bindingId, taskId }).slice(7, 23)}`,
41
+ );
42
+ const runProtocolId = text(
43
+ pick(binding, "agui_run_id", "aguiRunId", "protocol_run_id", "protocolRunId", "protocolRun"),
44
+ `agui-run-${hashCanonical({ bindingId, runId }).slice(7, 23)}`,
45
+ );
46
+ const threadId = text(pick(binding, "thread_id", "threadId", "agui_thread_id", "aguiThreadId"), taskProtocolId);
47
+ return { bindingId, domainTaskId: taskId, domainRunId: runId, taskProtocolId, runProtocolId, threadId };
48
+ }
49
+
50
+ function eventType(event) {
51
+ return String(pick(event, "type", "eventType", "event_type", "name") ?? "")
52
+ .trim().toUpperCase().replaceAll("-", "_");
53
+ }
54
+
55
+ function asEvents(input) {
56
+ if (Array.isArray(input)) return input;
57
+ return pick(input, "events", "items") ?? null;
58
+ }
59
+
60
+ function failure(code, message, path) {
61
+ return path ? { code, message, path } : { code, message };
62
+ }
63
+
64
+ /** Validate an AG-UI event sequence before it crosses the protocol boundary. */
65
+ export function validateAgUiTransitions(input) {
66
+ const events = asEvents(input);
67
+ const failures = [];
68
+ if (!Array.isArray(events)) return { ok: false, valid: false, failures: [failure("agui_events_required", "events must be an array", "events")] };
69
+ if (events.length === 0) failures.push(failure("agui_events_empty", "AG-UI event sequence cannot be empty", "events"));
70
+ let started = false;
71
+ let ended = false;
72
+ let openTextId = null;
73
+ let openToolId = null;
74
+ let runId = null;
75
+ let terminalCount = 0;
76
+ for (let index = 0; index < events.length; index += 1) {
77
+ const event = events[index];
78
+ const type = eventType(event);
79
+ const path = `/events/${index}`;
80
+ if (!event || typeof event !== "object" || Array.isArray(event)) {
81
+ failures.push(failure("agui_event_invalid", "event must be an object", path));
82
+ continue;
83
+ }
84
+ if (!EVENT_TYPES.has(type)) failures.push(failure("agui_event_type_invalid", `unsupported AG-UI event type ${type || "<empty>"}`, `${path}/type`));
85
+ const eventRunId = pick(event, "runId", "run_id");
86
+ if (eventRunId !== undefined && runId !== null && eventRunId !== runId) failures.push(failure("agui_run_id_mismatch", "all events must use one protocol run ID", `${path}/runId`));
87
+ if (eventRunId !== undefined) runId = eventRunId;
88
+ if (ended) failures.push(failure("agui_event_after_terminal", "no event may follow a terminal event", path));
89
+ if (type === "RUN_STARTED") {
90
+ if (started) failures.push(failure("agui_run_started_duplicate", "RUN_STARTED may occur only once", path));
91
+ if (index !== 0) failures.push(failure("agui_run_started_not_first", "RUN_STARTED must be the first event", path));
92
+ started = true;
93
+ continue;
94
+ }
95
+ if (!started) failures.push(failure("agui_run_not_started", "event cannot precede RUN_STARTED", path));
96
+ if (TERMINAL.has(type)) {
97
+ terminalCount += 1;
98
+ ended = true;
99
+ if (openTextId !== null) failures.push(failure("agui_text_message_open", "terminal event closed an open text message", path));
100
+ if (openToolId !== null) failures.push(failure("agui_tool_call_open", "terminal event closed an open tool call", path));
101
+ continue;
102
+ }
103
+ if (type === "TEXT_MESSAGE_START") {
104
+ const current = pick(event, "messageId", "message_id");
105
+ if (typeof current !== "string" || current.length === 0) failures.push(failure("agui_message_id_required", "TEXT_MESSAGE_START requires messageId", `${path}/messageId`));
106
+ if (openTextId !== null) failures.push(failure("agui_text_message_nested", "text messages cannot be nested", path));
107
+ openTextId = current ?? null;
108
+ } else if (type === "TEXT_MESSAGE_CONTENT") {
109
+ const current = pick(event, "messageId", "message_id");
110
+ if (openTextId === null || current !== openTextId) failures.push(failure("agui_text_message_not_open", "TEXT_MESSAGE_CONTENT must target an open message", path));
111
+ } else if (type === "TEXT_MESSAGE_END") {
112
+ const current = pick(event, "messageId", "message_id");
113
+ if (openTextId === null || current !== openTextId) failures.push(failure("agui_text_message_not_open", "TEXT_MESSAGE_END must target an open message", path));
114
+ openTextId = null;
115
+ } else if (type === "TOOL_CALL_START") {
116
+ const current = pick(event, "toolCallId", "tool_call_id");
117
+ if (typeof current !== "string" || current.length === 0) failures.push(failure("agui_tool_call_id_required", "TOOL_CALL_START requires toolCallId", `${path}/toolCallId`));
118
+ if (openToolId !== null) failures.push(failure("agui_tool_call_nested", "tool calls cannot be nested", path));
119
+ openToolId = current ?? null;
120
+ } else if (type === "TOOL_CALL_ARGS" || type === "TOOL_CALL_ARGUMENTS") {
121
+ const current = pick(event, "toolCallId", "tool_call_id");
122
+ if (openToolId === null || current !== openToolId) failures.push(failure("agui_tool_call_not_open", "TOOL_CALL_ARGS must target an open tool call", path));
123
+ } else if (type === "TOOL_CALL_END") {
124
+ const current = pick(event, "toolCallId", "tool_call_id");
125
+ if (openToolId === null || current !== openToolId) failures.push(failure("agui_tool_call_not_open", "TOOL_CALL_END must target an open tool call", path));
126
+ openToolId = null;
127
+ }
128
+ }
129
+ if (!started) failures.push(failure("agui_run_started_required", "sequence must contain RUN_STARTED", "events"));
130
+ if (terminalCount !== 1) failures.push(failure("agui_terminal_count_invalid", "sequence must contain exactly one terminal event", "events"));
131
+ if (terminalCount === 1 && !TERMINAL.has(eventType(events.at(-1)))) failures.push(failure("agui_terminal_not_last", "terminal event must be last", `/events/${events.length - 1}`));
132
+ if (openTextId !== null) failures.push(failure("agui_text_message_unclosed", "text message did not receive TEXT_MESSAGE_END", "events"));
133
+ if (openToolId !== null) failures.push(failure("agui_tool_call_unclosed", "tool call did not receive TOOL_CALL_END", "events"));
134
+ return { ok: failures.length === 0, valid: failures.length === 0, failures };
135
+ }
136
+
137
+ function protocolEvent(type, ids, fields = {}) {
138
+ return { type, runId: ids.runProtocolId, ...fields };
139
+ }
140
+
141
+ /** Project a domain Task/Run outcome to one deterministic AG-UI 0.0.57 stream. */
142
+ export function projectAgUiEvents(input = {}) {
143
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new AgUiProjectionError("agui_input_invalid", "projectAgUiEvents expects an object");
144
+ const ids = bindingIds(pick(input, "protocolBinding", "protocol_binding") ?? {}, input.task ?? {}, input.run ?? {});
145
+ const textValue = String(pick(input, "text", "message") ?? "");
146
+ const messageValue = pick(input, "messageId", "message_id") ?? `${ids.runProtocolId}:message`;
147
+ const tool = pick(input, "toolCall", "tool_call") ?? {};
148
+ const toolId = text(pick(tool, "id", "toolCallId", "tool_call_id"), `${ids.runProtocolId}:tool`);
149
+ const toolName = text(pick(tool, "name", "toolName", "tool_name"), "operation");
150
+ const toolArgs = pick(tool, "args", "arguments") ?? {};
151
+ const state = pick(input, "state", "snapshot") ?? {};
152
+ const outcome = String(pick(input, "outcome", "status") ?? pick(input.run, "state", "status") ?? pick(input.task, "state", "status") ?? "COMPLETED").toUpperCase();
153
+ const success = new Set(["COMPLETED", "SUCCEEDED", "SUCCESS", "OK", "PASS", "PASSED"]).has(outcome);
154
+ const correlation = { protocolBindingId: ids.bindingId, domainTaskId: ids.domainTaskId, domainRunId: ids.domainRunId };
155
+ const events = [
156
+ protocolEvent("RUN_STARTED", ids, { threadId: ids.threadId, input: pick(input.task, "intent", "request") ?? {}, metadata: correlation }),
157
+ protocolEvent("TEXT_MESSAGE_START", ids, { messageId: messageValue, role: "assistant" }),
158
+ protocolEvent("TEXT_MESSAGE_CONTENT", ids, { messageId: messageValue, delta: textValue }),
159
+ protocolEvent("TEXT_MESSAGE_END", ids, { messageId: messageValue }),
160
+ protocolEvent("TOOL_CALL_START", ids, { toolCallId: toolId, toolCallName: toolName }),
161
+ protocolEvent("TOOL_CALL_ARGS", ids, { toolCallId: toolId, delta: typeof toolArgs === "string" ? toolArgs : canonicalize(toolArgs) }),
162
+ protocolEvent("TOOL_CALL_END", ids, { toolCallId: toolId }),
163
+ protocolEvent("STATE_SNAPSHOT", ids, { snapshot: state }),
164
+ success
165
+ ? protocolEvent("RUN_FINISHED", ids, { threadId: ids.threadId, result: pick(input, "result", "output") ?? { status: outcome } })
166
+ : protocolEvent("RUN_ERROR", ids, { threadId: ids.threadId, message: text(pick(input, "error", "errorMessage"), outcome) }),
167
+ ];
168
+ const validation = validateAgUiTransitions(events);
169
+ if (!validation.ok) throw new AgUiProjectionError("agui_projection_invalid", "generated event sequence failed transition validation");
170
+ return cloneAndFreeze({ ok: true, events, protocolBinding: { ...correlation, protocol: "ag-ui" }, protocolIds: { threadId: ids.threadId, taskId: ids.taskProtocolId, runId: ids.runProtocolId } });
171
+ }
172
+
173
+ export const projectAGUIEvents = projectAgUiEvents;
174
+ export const projectAguiEvents = projectAgUiEvents;
175
+ export const createAgUiEvents = projectAgUiEvents;
176
+ export const validateAGUITransitions = validateAgUiTransitions;
177
+ export const assertAgUiTransitions = validateAgUiTransitions;
178
+ export const validateEventSequence = validateAgUiTransitions;
179
+
180
+ export const AG_UI_EXPERIMENTAL_EXTENSION = Object.freeze({
181
+ protocol: "ag-ui",
182
+ version: "0.0.57",
183
+ operations: Object.freeze({
184
+ "events.project": projectAgUiEvents,
185
+ "events.validate": validateAgUiTransitions,
186
+ }),
187
+ });
188
+
189
+ export function registerAgUiExtension(registry) {
190
+ return registry.register(AG_UI_EXPERIMENTAL_EXTENSION);
191
+ }
@@ -0,0 +1,295 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import Ajv2020 from "ajv/dist/2020.js";
7
+
8
+ import { canonicalize, deepFreeze } from "../canonical-json.mjs";
9
+
10
+ const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
11
+ const v2Directory = path.resolve(moduleDirectory, "../../../consumer-reference/agent-native/v2");
12
+ const policyPath = path.join(v2Directory, "admission-policy.json");
13
+ const policySchemaPath = path.join(v2Directory, "schema/admission-policy.schema.json");
14
+ const recordSchemaPath = path.join(v2Directory, "schema/material-record.schema.json");
15
+ const manifestSchemaPath = path.join(v2Directory, "schema/material-manifest.schema.json");
16
+ const MANIFEST_REF = "sg:manifest/material-admission-v2";
17
+ const MATERIAL_REF_PATTERN = /^sg:material\/path-sha256-[a-f0-9]{64}$/;
18
+ const MATERIAL_VERSION_PATTERN = /^sg:material\/path-sha256-[a-f0-9]{64}@sha256:[a-f0-9]{64}$/;
19
+ const HASH_PATTERN = /^[a-f0-9]{64}$/;
20
+ const DOMAINS = new Set(["layout", "motion", "design-engineering", "game-ui", "platform-guides", "shared"]);
21
+ const LIFECYCLES = new Set(["draft", "stable", "deprecated", "experimental", "generated"]);
22
+ const RECORD_KEYS = new Set(["schema_version", "record_kind", "stable_ref", "version_id", "repository_path", "media_type", "source_sha256", "byte_length", "lifecycle", "domain"]);
23
+ const MANIFEST_KEYS = new Set(["schema_version", "record_kind", "stable_ref", "version_id", "admission_policy_ref", "admission_policy_version_id", "materials"]);
24
+
25
+ export class MaterialAdmissionError extends TypeError {
26
+ constructor(code, message, sourcePath) {
27
+ super(message);
28
+ this.name = "MaterialAdmissionError";
29
+ this.code = code;
30
+ if (sourcePath !== undefined) this.path = sourcePath;
31
+ }
32
+ }
33
+
34
+ function parseJsonFile(sourcePath) { return JSON.parse(fs.readFileSync(sourcePath, "utf8")); }
35
+ function failure(code, message, sourcePath) { return { code, message, ...(sourcePath === undefined ? {} : { path: sourcePath }) }; }
36
+ function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
37
+ function canonicalVersionId(stableRef, value) {
38
+ const payload = { ...value };
39
+ delete payload.version_id;
40
+ return `${stableRef}@sha256:${sha256(Buffer.from(canonicalize(payload), "utf8"))}`;
41
+ }
42
+ function bytewiseCompare(left, right) { return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8")); }
43
+
44
+ const policySchema = parseJsonFile(policySchemaPath);
45
+ const recordSchema = parseJsonFile(recordSchemaPath);
46
+ const manifestSchema = parseJsonFile(manifestSchemaPath);
47
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
48
+ ajv.addSchema(policySchema);
49
+ ajv.addSchema(recordSchema);
50
+ ajv.addSchema(manifestSchema);
51
+ const validatePolicySchema = ajv.getSchema(policySchema.$id);
52
+ const validateRecordSchema = ajv.getSchema(recordSchema.$id);
53
+ const validateManifestSchema = ajv.getSchema(manifestSchema.$id);
54
+
55
+ export function validateMaterialAdmissionPolicy(policy) {
56
+ const failures = [];
57
+ if (!validatePolicySchema(policy)) failures.push(failure("material_policy_schema_invalid", "admission policy does not satisfy its closed schema"));
58
+ if (failures.length === 0) {
59
+ const paths = policy.allowed_materials.map(({ repository_path }) => repository_path);
60
+ if (new Set(paths).size !== paths.length) failures.push(failure("material_policy_path_duplicate", "admission policy paths must be unique"));
61
+ if (paths.some((entry, index) => index > 0 && bytewiseCompare(paths[index - 1], entry) >= 0)) failures.push(failure("material_policy_order_invalid", "admission policy paths must be bytewise sorted"));
62
+ const exclusions = policy.exclusions.map(({ kind, value }) => `${kind}\0${value}`);
63
+ if (new Set(exclusions).size !== exclusions.length) failures.push(failure("material_policy_exclusion_duplicate", "admission policy exclusions must be unique"));
64
+ if (policy.version_id !== canonicalVersionId(policy.stable_ref, policy)) failures.push(failure("material_policy_version_invalid", "admission policy VersionID does not bind canonical policy content"));
65
+ }
66
+ return deepFreeze({ ok: failures.length === 0, failures });
67
+ }
68
+
69
+ const loadedPolicy = parseJsonFile(policyPath);
70
+ const loadedPolicyResult = validateMaterialAdmissionPolicy(loadedPolicy);
71
+ if (!loadedPolicyResult.ok) throw new MaterialAdmissionError(loadedPolicyResult.failures[0].code, loadedPolicyResult.failures[0].message);
72
+ export const materialAdmissionPolicy = deepFreeze(loadedPolicy);
73
+ const allowedRoutes = new Map(materialAdmissionPolicy.allowed_materials.map((entry) => [entry.repository_path, entry]));
74
+
75
+ function normalizedPathFailure(repositoryPath) {
76
+ if (typeof repositoryPath !== "string" || repositoryPath.length === 0 || repositoryPath.includes("\0")
77
+ || /(?:[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF])/.test(repositoryPath)) {
78
+ return failure("material_path_invalid", "repository_path must be a non-empty valid Unicode string", repositoryPath);
79
+ }
80
+ if (path.posix.isAbsolute(repositoryPath) || path.win32.isAbsolute(repositoryPath)
81
+ || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(repositoryPath)) {
82
+ return failure("material_path_absolute", "repository_path must be repository-relative and not a URI", repositoryPath);
83
+ }
84
+ const components = repositoryPath.split("/");
85
+ if (repositoryPath.includes("\\") || components.some((component) => component === "" || component === ".")) {
86
+ return failure("material_path_not_normalized", "repository_path must use normalized forward-slash components", repositoryPath);
87
+ }
88
+ if (components.includes("..")) return failure("material_path_escape", "repository_path must not contain parent components", repositoryPath);
89
+ if (path.posix.normalize(repositoryPath) !== repositoryPath) return failure("material_path_not_normalized", "repository_path must be normalized", repositoryPath);
90
+ return null;
91
+ }
92
+
93
+ export function materialStableRefForPath(repositoryPath) {
94
+ const pathFailure = normalizedPathFailure(repositoryPath);
95
+ if (pathFailure) throw new MaterialAdmissionError(pathFailure.code, pathFailure.message, repositoryPath);
96
+ return `sg:material/path-sha256-${sha256(Buffer.from(repositoryPath, "utf8"))}`;
97
+ }
98
+
99
+ export function materialVersionIdForSource(stableRef, sourceSha256) {
100
+ if (!MATERIAL_REF_PATTERN.test(stableRef ?? "") || !HASH_PATTERN.test(sourceSha256 ?? "")) {
101
+ throw new MaterialAdmissionError("material_identity_input_invalid", "source VersionID requires a v2 material StableRef and lowercase SHA-256");
102
+ }
103
+ return `${stableRef}@sha256:${sourceSha256}`;
104
+ }
105
+
106
+ function exclusionFor(repositoryPath) {
107
+ const segments = repositoryPath.split("/");
108
+ for (const exclusion of materialAdmissionPolicy.exclusions) {
109
+ if (exclusion.kind === "path_prefix" && repositoryPath.startsWith(exclusion.value)) return exclusion;
110
+ if (exclusion.kind === "path_segment" && segments.includes(exclusion.value)) return exclusion;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ function classifyPath(repositoryPath) {
116
+ const pathFailure = normalizedPathFailure(repositoryPath);
117
+ if (pathFailure) return { failure: pathFailure };
118
+ const exclusion = exclusionFor(repositoryPath);
119
+ if (exclusion) return { failure: failure(exclusion.code, "material path is explicitly excluded", repositoryPath) };
120
+ if (!repositoryPath.endsWith(".md")) return { failure: failure("material_type_not_markdown", "only Markdown material may be admitted", repositoryPath) };
121
+ const route = allowedRoutes.get(repositoryPath);
122
+ if (!route) return { failure: failure("material_path_unapproved", "material path is absent from the sealed allow-set", repositoryPath) };
123
+ return { route };
124
+ }
125
+
126
+ export function createMaterialManifest(materials) {
127
+ if (!Array.isArray(materials)) throw new MaterialAdmissionError("material_manifest_materials_invalid", "materials must be an array");
128
+ const sorted = materials.map((record) => structuredClone(record)).sort((left, right) => bytewiseCompare(left.stable_ref ?? "", right.stable_ref ?? ""));
129
+ const base = {
130
+ schema_version: "2.0", record_kind: "material_manifest", stable_ref: MANIFEST_REF,
131
+ admission_policy_ref: materialAdmissionPolicy.stable_ref,
132
+ admission_policy_version_id: materialAdmissionPolicy.version_id,
133
+ materials: sorted,
134
+ };
135
+ return deepFreeze({ ...base, version_id: canonicalVersionId(MANIFEST_REF, base) });
136
+ }
137
+
138
+ function trackedInventory(repositoryRoot) {
139
+ const result = spawnSync("git", ["-C", repositoryRoot, "ls-files", "-s", "-z", "--"], { encoding: "buffer", maxBuffer: 16 * 1024 * 1024, timeout: 30_000 });
140
+ if (result.status !== 0) {
141
+ const detail = result.error?.message ?? result.stderr.toString("utf8").trim() ?? "git ls-files failed";
142
+ return { failure: failure("material_inventory_failed", detail) };
143
+ }
144
+ const entries = new Map();
145
+ for (const encoded of result.stdout.subarray(0, result.stdout.length - (result.stdout.at(-1) === 0 ? 1 : 0)).toString("utf8").split("\0").filter(Boolean)) {
146
+ const tab = encoded.indexOf("\t");
147
+ const metadata = encoded.slice(0, tab).split(" ");
148
+ const repositoryPath = encoded.slice(tab + 1);
149
+ if (tab < 0 || metadata.length !== 3) return { failure: failure("material_inventory_failed", "Git inventory entry is malformed") };
150
+ entries.set(repositoryPath, { mode: metadata[0], object_id: metadata[1], stage: metadata[2] });
151
+ }
152
+ return { entries };
153
+ }
154
+
155
+ function realpath(fileSystem, target) {
156
+ const implementation = fileSystem.realpathSync.native ?? fileSystem.realpathSync;
157
+ return implementation.call(fileSystem.realpathSync, target);
158
+ }
159
+
160
+ function inspectRepositoryRoot(repositoryRoot, fileSystem) {
161
+ const resolvedRoot = path.resolve(repositoryRoot);
162
+ try {
163
+ const stats = fileSystem.lstatSync(resolvedRoot);
164
+ if (!stats.isDirectory() || stats.isSymbolicLink()) return { failure: failure("material_repository_invalid", "repository root must be a non-symlink directory") };
165
+ return { resolvedRoot, canonicalRoot: realpath(fileSystem, resolvedRoot) };
166
+ } catch (error) {
167
+ return { failure: failure("material_repository_invalid", `repository root is unavailable: ${error.message}`) };
168
+ }
169
+ }
170
+
171
+ function inspectWorktreePath(rootState, repositoryPath, fileSystem) {
172
+ let current = rootState.resolvedRoot;
173
+ let targetStats;
174
+ try {
175
+ for (const component of repositoryPath.split("/")) {
176
+ current = path.join(current, component);
177
+ targetStats = fileSystem.lstatSync(current);
178
+ if (targetStats.isSymbolicLink()) return { failure: failure("material_path_symlink", "material path components must not be symlinks", repositoryPath) };
179
+ }
180
+ if (!targetStats.isFile()) return { failure: failure("material_path_not_regular", "material path must be a regular file", repositoryPath) };
181
+ const canonicalTarget = realpath(fileSystem, current);
182
+ const expectedTarget = path.resolve(rootState.canonicalRoot, ...repositoryPath.split("/"));
183
+ if (canonicalTarget !== expectedTarget || !canonicalTarget.startsWith(`${rootState.canonicalRoot}${path.sep}`)) {
184
+ return { failure: failure("material_path_escape", "material canonical path escapes the repository root", repositoryPath) };
185
+ }
186
+ return { target: current, canonicalTarget, dev: targetStats.dev, ino: targetStats.ino };
187
+ } catch (error) {
188
+ return { failure: failure("material_path_unavailable", `tracked material path is unavailable: ${error.message}`, repositoryPath) };
189
+ }
190
+ }
191
+
192
+ function validateManifestEnvelope(manifest) {
193
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return [failure("material_manifest_invalid", "material manifest must be an object")];
194
+ if (Object.keys(manifest).some((key) => !MANIFEST_KEYS.has(key))) return [failure("material_manifest_schema_invalid", "material manifest contains undeclared properties")];
195
+ if (manifest.schema_version !== "2.0" || manifest.record_kind !== "material_manifest" || manifest.stable_ref !== MANIFEST_REF
196
+ || !Array.isArray(manifest.materials) || manifest.materials.length === 0) return [failure("material_manifest_schema_invalid", "material manifest envelope is malformed")];
197
+ if (manifest.admission_policy_ref !== materialAdmissionPolicy.stable_ref || manifest.admission_policy_version_id !== materialAdmissionPolicy.version_id) {
198
+ return [failure("material_manifest_policy_mismatch", "material manifest does not bind the authoritative admission policy")];
199
+ }
200
+ if (manifest.version_id !== canonicalVersionId(MANIFEST_REF, manifest)) return [failure("material_manifest_version_invalid", "material manifest VersionID does not bind canonical manifest content")];
201
+ return [];
202
+ }
203
+
204
+ function validateRecordShape(record, index) {
205
+ const location = `/materials/${index}`;
206
+ if (!record || typeof record !== "object" || Array.isArray(record)) return failure("material_record_invalid", "material record must be an object", location);
207
+ if (Object.keys(record).some((key) => !RECORD_KEYS.has(key)) || record.schema_version !== "2.0" || record.record_kind !== "material") return failure("material_schema_invalid", "material record is not a closed v2 material", location);
208
+ if (!MATERIAL_REF_PATTERN.test(record.stable_ref ?? "")) return failure("material_stable_ref_invalid", "material StableRef is malformed", `${location}/stable_ref`);
209
+ if (!MATERIAL_VERSION_PATTERN.test(record.version_id ?? "") || !record.version_id.startsWith(`${record.stable_ref}@sha256:`)) return failure("material_version_invalid", "material VersionID is malformed or names another StableRef", `${location}/version_id`);
210
+ if (record.media_type !== materialAdmissionPolicy.media_type) return failure("material_media_type_invalid", "material media type must be text/markdown", `${location}/media_type`);
211
+ if (!HASH_PATTERN.test(record.source_sha256 ?? "")) return failure("material_hash_invalid", "material source_sha256 must be lowercase SHA-256", `${location}/source_sha256`);
212
+ if (!Number.isSafeInteger(record.byte_length) || record.byte_length < 0) return failure("material_length_invalid", "material byte_length must be a non-negative safe integer", `${location}/byte_length`);
213
+ if (!LIFECYCLES.has(record.lifecycle)) return failure("material_lifecycle_invalid", "material lifecycle is unsupported", `${location}/lifecycle`);
214
+ if (!DOMAINS.has(record.domain)) return failure("material_domain_invalid", "material domain is unsupported", `${location}/domain`);
215
+ const classified = classifyPath(record.repository_path);
216
+ if (classified.failure) return classified.failure;
217
+ if (record.stable_ref !== materialStableRefForPath(record.repository_path)) return failure("material_stable_ref_path_mismatch", "material StableRef does not derive from repository_path", `${location}/stable_ref`);
218
+ if (record.domain !== classified.route.domain) return failure("material_domain_route_mismatch", "material domain does not match policy", `${location}/domain`);
219
+ if (record.lifecycle !== classified.route.lifecycle) return failure("material_lifecycle_route_mismatch", "material lifecycle does not match policy", `${location}/lifecycle`);
220
+ if (record.version_id !== materialVersionIdForSource(record.stable_ref, record.source_sha256)) return failure("material_version_invalid", "material VersionID does not bind exact source SHA-256", `${location}/version_id`);
221
+ if (!validateRecordSchema(record)) return failure("material_schema_invalid", "material record does not satisfy the closed schema", location);
222
+ return null;
223
+ }
224
+
225
+ export function validateMaterialManifest({ repositoryRoot, manifest, fileSystem = fs, readFile }) {
226
+ const envelopeFailures = validateManifestEnvelope(manifest);
227
+ if (envelopeFailures.length > 0) return deepFreeze({ ok: false, failures: envelopeFailures, materials: [] });
228
+ const recordFailures = manifest.materials.map(validateRecordShape).filter(Boolean);
229
+ if (recordFailures.length > 0) return deepFreeze({ ok: false, failures: recordFailures, materials: [] });
230
+ if (!validateManifestSchema(manifest)) return deepFreeze({ ok: false, failures: [failure("material_manifest_schema_invalid", "manifest does not satisfy the closed schema")], materials: [] });
231
+
232
+ const refs = manifest.materials.map(({ stable_ref }) => stable_ref);
233
+ const paths = manifest.materials.map(({ repository_path }) => repository_path);
234
+ const duplicateFailures = [];
235
+ if (new Set(refs).size !== refs.length) duplicateFailures.push(failure("material_identity_duplicate", "material StableRefs must be unique"));
236
+ if (new Set(paths).size !== paths.length) duplicateFailures.push(failure("material_path_duplicate", "material paths must be unique"));
237
+ if (duplicateFailures.length > 0) return deepFreeze({ ok: false, failures: duplicateFailures, materials: [] });
238
+ if (refs.some((entry, index) => index > 0 && bytewiseCompare(refs[index - 1], entry) >= 0)) return deepFreeze({ ok: false, failures: [failure("material_manifest_order_invalid", "materials must be bytewise sorted by StableRef")], materials: [] });
239
+
240
+ const inventory = trackedInventory(repositoryRoot);
241
+ if (inventory.failure) return deepFreeze({ ok: false, failures: [inventory.failure], materials: [] });
242
+ for (const record of manifest.materials) {
243
+ const indexEntry = inventory.entries.get(record.repository_path);
244
+ if (!indexEntry) return deepFreeze({ ok: false, failures: [failure("material_path_untracked", "material path is absent from tracked Git inventory", record.repository_path)], materials: [] });
245
+ if (!new Set(["100644", "100755"]).has(indexEntry.mode) || indexEntry.stage !== "0") return deepFreeze({ ok: false, failures: [failure("material_git_mode_invalid", "material Git index entry must be a stage-zero regular blob", record.repository_path)], materials: [] });
246
+ }
247
+
248
+ const rootState = inspectRepositoryRoot(repositoryRoot, fileSystem);
249
+ if (rootState.failure) return deepFreeze({ ok: false, failures: [rootState.failure], materials: [] });
250
+ const inspections = [];
251
+ for (const record of manifest.materials) {
252
+ const inspection = inspectWorktreePath(rootState, record.repository_path, fileSystem);
253
+ if (inspection.failure) return deepFreeze({ ok: false, failures: [inspection.failure], materials: [] });
254
+ inspections.push(inspection);
255
+ }
256
+
257
+ const reader = readFile ?? ((descriptor) => fileSystem.readFileSync(descriptor));
258
+ const admitted = [];
259
+ for (let index = 0; index < manifest.materials.length; index += 1) {
260
+ const record = manifest.materials[index];
261
+ const inspection = inspections[index];
262
+ let descriptor;
263
+ let bytes;
264
+ try {
265
+ descriptor = fileSystem.openSync(inspection.target, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
266
+ const descriptorStats = fileSystem.fstatSync(descriptor);
267
+ const currentStats = fileSystem.lstatSync(inspection.target);
268
+ const currentCanonical = realpath(fileSystem, inspection.target);
269
+ if (!descriptorStats.isFile() || currentStats.isSymbolicLink() || !currentStats.isFile()
270
+ || descriptorStats.dev !== inspection.dev || descriptorStats.ino !== inspection.ino
271
+ || currentStats.dev !== inspection.dev || currentStats.ino !== inspection.ino
272
+ || currentCanonical !== inspection.canonicalTarget) {
273
+ return deepFreeze({ ok: false, failures: [failure("material_path_race", "material path identity changed after preflight", record.repository_path)], materials: [] });
274
+ }
275
+ bytes = reader(descriptor);
276
+ } catch (error) {
277
+ const code = error?.code === "ELOOP" || error?.code === "ENOENT" ? "material_path_race" : "material_read_failed";
278
+ return deepFreeze({ ok: false, failures: [failure(code, `material could not be read safely: ${error.message}`, record.repository_path)], materials: [] });
279
+ } finally {
280
+ if (descriptor !== undefined) fileSystem.closeSync(descriptor);
281
+ }
282
+ if (!(bytes instanceof Uint8Array)) return deepFreeze({ ok: false, failures: [failure("material_read_invalid", "material reader must return bytes", record.repository_path)], materials: [] });
283
+ if (sha256(bytes) !== record.source_sha256) return deepFreeze({ ok: false, failures: [failure("material_source_hash_mismatch", "material bytes do not match source_sha256", record.repository_path)], materials: [] });
284
+ if (bytes.byteLength !== record.byte_length) return deepFreeze({ ok: false, failures: [failure("material_byte_length_mismatch", "material bytes do not match byte_length", record.repository_path)], materials: [] });
285
+ admitted.push({ ...record });
286
+ }
287
+ return deepFreeze({ ok: true, failures: [], materials: admitted });
288
+ }
289
+
290
+ export function resolveMaterialRecord({ manifest, reference }) {
291
+ if (!MATERIAL_REF_PATTERN.test(reference ?? "")) throw new MaterialAdmissionError("material_reference_invalid", "runtime material references must be v2 StableRefs");
292
+ const record = manifest?.materials?.find(({ stable_ref }) => stable_ref === reference);
293
+ if (!record) throw new MaterialAdmissionError("material_reference_not_found", "material StableRef is absent from the manifest");
294
+ return deepFreeze(structuredClone(record));
295
+ }
@@ -0,0 +1,134 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { deepFreeze } from "../canonical-json.mjs";
6
+ import { createMaterialOperationRegistry } from "./material-operation-registry.mjs";
7
+
8
+ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
9
+ const operationForCommand = Object.freeze({
10
+ context: "material-context",
11
+ discover: "material-discover",
12
+ get: "material-get",
13
+ search: "material-search",
14
+ });
15
+ const optionSpecs = Object.freeze({
16
+ "--budget-tokens": { commands: new Set(["context"]), field: "budget_tokens", integer: true },
17
+ "--limit": { commands: new Set(["search"]), field: "limit", integer: true },
18
+ "--limit-bytes": { commands: new Set(["get"]), field: "length", integer: true },
19
+ "--offset": { commands: new Set(["get"]), field: "offset", integer: true },
20
+ "--query": { commands: new Set(["context", "search"]), field: "query" },
21
+ "--reference": { commands: new Set(["get"]), field: "reference" },
22
+ });
23
+ const requiredOptions = Object.freeze({ context: ["--query"], discover: [], get: ["--reference"], search: ["--query"] });
24
+ const inputOrder = Object.freeze({
25
+ context: ["--query", "--budget-tokens"],
26
+ discover: [],
27
+ get: ["--reference", "--offset", "--limit-bytes"],
28
+ search: ["--query", "--limit"],
29
+ });
30
+
31
+ function issue(code, message) { return { code, message }; }
32
+ function failed(operation, failure) { return deepFreeze({ failures: [failure], ok: false, operation }); }
33
+ function parseInteger(value) {
34
+ if (!/^[0-9]+$/.test(value)) return null;
35
+ const parsed = Number(value);
36
+ return Number.isSafeInteger(parsed) ? parsed : null;
37
+ }
38
+
39
+ export function parseMaterialCliArguments(args) {
40
+ if (!Array.isArray(args)) return failed(null, issue("argument_list_invalid", "CLI arguments must be an array"));
41
+ let command;
42
+ let help = false;
43
+ const values = new Map();
44
+ for (let index = 0; index < args.length; index += 1) {
45
+ const argument = args[index];
46
+ if (typeof argument !== "string") return failed(command ?? null, issue("argument_unknown", "unsupported CLI argument"));
47
+ if (argument === "--help") {
48
+ if (help) return failed(command ?? null, issue("argument_duplicate", "an option was supplied more than once"));
49
+ help = true;
50
+ continue;
51
+ }
52
+ if (argument.startsWith("--")) {
53
+ const spec = optionSpecs[argument];
54
+ if (!spec) return failed(command ?? null, issue("argument_unknown", "unknown option"));
55
+ if (values.has(argument)) return failed(command ?? null, issue("argument_duplicate", "an option was supplied more than once"));
56
+ const value = args[index + 1];
57
+ if (value === undefined || typeof value !== "string" || value.startsWith("--")) {
58
+ return failed(command ?? null, issue("argument_value_required", "an option value is required"));
59
+ }
60
+ values.set(argument, value);
61
+ index += 1;
62
+ continue;
63
+ }
64
+ if (command !== undefined) return failed(command, issue("argument_unknown", "positional operands are not supported"));
65
+ command = argument;
66
+ }
67
+ if (help) {
68
+ if (command !== undefined || values.size > 0) return failed(command ?? null, issue("argument_inapplicable", "help cannot be combined with commands or options"));
69
+ return deepFreeze({ command: "help", input: {}, ok: true });
70
+ }
71
+ if (command === undefined) return failed(null, issue("argument_value_required", "a command is required"));
72
+ if (!Object.hasOwn(operationForCommand, command)) return failed(command, issue("command_unknown", "unknown command"));
73
+ for (const [option, value] of values) {
74
+ const spec = optionSpecs[option];
75
+ if (!spec.commands.has(command)) return failed(command, issue("argument_inapplicable", "option is not supported by this command"));
76
+ if (spec.integer && parseInteger(value) === null) return failed(command, issue("argument_integer_invalid", "option value must be an unsigned decimal safe integer"));
77
+ }
78
+ if (requiredOptions[command].some((option) => !values.has(option))) {
79
+ return failed(command, issue("argument_value_required", "a required option is missing"));
80
+ }
81
+ const input = {};
82
+ for (const option of inputOrder[command]) {
83
+ if (!values.has(option)) continue;
84
+ const spec = optionSpecs[option];
85
+ input[spec.field] = spec.integer ? parseInteger(values.get(option)) : values.get(option);
86
+ }
87
+ return deepFreeze({ command, input, ok: true });
88
+ }
89
+
90
+ const helpResult = deepFreeze({
91
+ commands: [
92
+ { name: "discover", options: [] },
93
+ { name: "search", options: ["--query", "--limit"] },
94
+ { name: "get", options: ["--reference", "--offset", "--limit-bytes"] },
95
+ { name: "context", options: ["--query", "--budget-tokens"] },
96
+ ],
97
+ options: ["--query", "--reference", "--offset", "--limit-bytes", "--limit", "--budget-tokens", "--help"],
98
+ });
99
+
100
+ function packagedInventoryRunner(root) {
101
+ const manifestPath = path.join(root, "consumer-reference", "agent-native", "v2", "material-registry.json");
102
+ return () => {
103
+ try {
104
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
105
+ const stdout = Buffer.from(manifest.materials.map((record) => `100644 ${record.source_sha256} 0\t${record.repository_path}\0`).join(""), "utf8");
106
+ return { error: undefined, status: 0, stderr: Buffer.alloc(0), stdout };
107
+ } catch {
108
+ return { error: undefined, status: 1, stderr: Buffer.alloc(0), stdout: Buffer.alloc(0) };
109
+ }
110
+ };
111
+ }
112
+
113
+ function runtimeRegistry() {
114
+ const hasGitMetadata = fs.existsSync(path.join(repositoryRoot, ".git"));
115
+ return createMaterialOperationRegistry({
116
+ repositoryRoot,
117
+ ...(hasGitMetadata ? {} : { gitRunner: packagedInventoryRunner(repositoryRoot) }),
118
+ });
119
+ }
120
+
121
+ export function executeMaterialCli(args, registry = runtimeRegistry()) {
122
+ const parsed = parseMaterialCliArguments(args);
123
+ if (!parsed.ok) return parsed;
124
+ if (parsed.command === "help") return deepFreeze({ ok: true, operation: "help", result: helpResult });
125
+ if (!registry || typeof registry.invoke !== "function") return failed(parsed.command, issue("material_registry_invalid", "v2 material registry is unavailable"));
126
+ return registry.invoke(operationForCommand[parsed.command], parsed.input);
127
+ }
128
+
129
+ export function serializeMaterialCliReport(report) { return `${JSON.stringify(report, null, 2)}\n`; }
130
+
131
+ export function runMaterialCli(args, registry) {
132
+ const report = executeMaterialCli(args, registry);
133
+ return deepFreeze({ exitCode: report.ok ? 0 : 1, output: serializeMaterialCliReport(report), report });
134
+ }