stylegallery 0.1.0 → 0.1.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.
- package/AGENTS.md +4 -1
- package/DOMAINS.md +2 -6
- package/GOVERNANCE.md +10 -9
- package/README.md +23 -127
- package/consumer-reference/agent-native/v2/admission-policy.json +646 -0
- package/consumer-reference/agent-native/v2/material-registry.json +1 -0
- package/consumer-reference/agent-native/v2/schema/admission-policy.schema.json +50 -0
- package/consumer-reference/agent-native/v2/schema/material-manifest.schema.json +17 -0
- package/consumer-reference/agent-native/v2/schema/material-record.schema.json +20 -0
- package/consumer-reference/contract.md +1 -1
- package/consumer-reference/index.md +5 -3
- package/layout/index.md +124 -0
- package/log.md +51 -0
- package/package.json +181 -22
- package/scripts/agent-native/v2/experimental-extension-registry.mjs +143 -0
- package/scripts/agent-native/v2/extensions/a2a-projection.mjs +264 -0
- package/scripts/agent-native/v2/extensions/agui-projection.mjs +191 -0
- package/scripts/agent-native/v2/material-admission.mjs +295 -0
- package/scripts/agent-native/v2/material-cli-adapter.mjs +134 -0
- package/scripts/agent-native/v2/material-context.mjs +251 -0
- package/scripts/agent-native/v2/material-identity.mjs +71 -0
- package/scripts/agent-native/v2/material-mcp-adapter.mjs +253 -0
- package/scripts/agent-native/v2/material-operation-registry.mjs +123 -0
- package/scripts/agent-native/v2/material-queries.mjs +398 -0
- package/scripts/agent-native/v2/material-registry.mjs +319 -0
- package/scripts/sg-material-mcp.mjs +98 -0
- package/scripts/sg-material.mjs +27 -0
- package/consumer-reference/adapters/style-dictionary.config.mjs +0 -36
- package/consumer-reference/baselines/calibration.json +0 -260
- package/consumer-reference/baselines/manifest.json +0 -21
- package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +0 -282
- package/consumer-reference/fixtures/consumer-conformance/manifest.json +0 -102
- package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +0 -32
- package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +0 -18
- package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +0 -142
- package/consumer-reference/fixtures/invalid-stable-ended-support.json +0 -19
- package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +0 -26
- package/consumer-reference/fixtures/promotion/invalid-count-only.json +0 -29
- package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +0 -25
- package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +0 -25
- package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +0 -45
- package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +0 -26
- package/consumer-reference/fixtures/promotion/manifest.json +0 -15
- package/consumer-reference/fixtures/promotion/valid-deferred-example.json +0 -67
- package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +0 -67
- package/consumer-reference/fixtures/token-portability/invalid-cycle.json +0 -7
- package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +0 -6
- package/consumer-reference/fixtures/token-portability/invalid-extends.json +0 -4
- package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +0 -7
- package/consumer-reference/fixtures/token-portability/invalid-resolver.json +0 -9
- package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +0 -10
- package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +0 -6
- package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +0 -6
- package/consumer-reference/fixtures/token-portability/valid-border.json +0 -14
- package/consumer-reference/fixtures/token-portability/valid-reference.json +0 -32
- package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +0 -7
- package/consumer-reference/fixtures/valid-deprecated.json +0 -21
- package/consumer-reference/fixtures/valid-experimental.json +0 -19
- package/consumer-reference/generated/manifest.json +0 -40
- package/consumer-reference/generated/tokens.css +0 -12
- package/consumer-reference/policies/shared-experimental.json +0 -37
- package/consumer-reference/schema/ax-evidence.schema.json +0 -30
- package/consumer-reference/schema/baseline-manifest.schema.json +0 -55
- package/consumer-reference/schema/calibration-record.schema.json +0 -190
- package/consumer-reference/schema/capture-session.schema.json +0 -161
- package/consumer-reference/schema/component-state.schema.json +0 -150
- package/consumer-reference/schema/consumer-conformance-record.schema.json +0 -365
- package/consumer-reference/schema/dom-evidence.schema.json +0 -38
- package/consumer-reference/schema/evidence-record.schema.json +0 -81
- package/consumer-reference/schema/fixture-manifest.schema.json +0 -32
- package/consumer-reference/schema/item.schema.json +0 -188
- package/consumer-reference/schema/page-evidence-manifest.schema.json +0 -216
- package/consumer-reference/schema/page-evidence-session.schema.json +0 -119
- package/consumer-reference/schema/portable-tokens.schema.json +0 -104
- package/consumer-reference/schema/promotion-rfc.schema.json +0 -219
- package/consumer-reference/schema/runtime-evidence-manifest.schema.json +0 -33
- package/consumer-reference/schema/visual-evidence.schema.json +0 -31
- package/design-engineering/consumer-migration-readiness.md +0 -124
- package/design-engineering/interface-craft.md +0 -107
- package/design-engineering/reference-profiles/external-adaptation/index.md +0 -12
- package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +0 -125
- package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +0 -3504
- package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +0 -65
- package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +0 -23
- package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +0 -19
- package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +0 -21
- package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +0 -12
- package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +0 -195
- package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +0 -17
- package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +0 -125
- package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +0 -3504
- package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +0 -65
- package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +0 -23
- package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +0 -19
- package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +0 -21
- package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +0 -12
- package/design-engineering/reference-profiles/governed-local/terminal/profile.json +0 -43
- package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +0 -195
- package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +0 -17
- package/game-ui/classification.md +0 -82
- package/game-ui/reference-record.md +0 -62
- package/game-ui/screen-hierarchy.md +0 -107
- package/game-ui/unity/architecture.md +0 -166
- package/game-ui/unity/cli-loop.md +0 -84
- package/game-ui/unity/data/unity-technologies-public-repositories.json +0 -26654
- package/game-ui/unity/org-wiki.md +0 -146
- package/game-ui/unity/repository-map.md +0 -102
- package/game-ui/unity/ui-systems.md +0 -85
- package/motion/practice-reference.md +0 -145
- package/motion/review-workflow.md +0 -86
- package/motion/vocabulary.md +0 -82
- package/platform-guides/apple-interaction.md +0 -89
- package/scripts/agent-native/events.mjs +0 -113
- package/scripts/agent-native/immutable-store.mjs +0 -102
- package/scripts/artifact-metadata.mjs +0 -20
- package/scripts/audit-evidence-freshness.mjs +0 -308
- package/scripts/baseline-contract.mjs +0 -189
- package/scripts/baseline-schema-parity.mjs +0 -40
- package/scripts/build-reference-artifacts.mjs +0 -127
- package/scripts/calibration-raw-contract.mjs +0 -172
- package/scripts/capture-session-contract.mjs +0 -187
- package/scripts/component-state-artifact-fixture.mjs +0 -44
- package/scripts/component-state-contract.mjs +0 -99
- package/scripts/component-state-semantics.mjs +0 -128
- package/scripts/component-state-workflow-contract.mjs +0 -67
- package/scripts/component-state-workflow-negative-cases.mjs +0 -64
- package/scripts/consumer-conformance-contract.mjs +0 -198
- package/scripts/consumer-evidence-governance-contract.mjs +0 -210
- package/scripts/consumer-reference-case-runner.mjs +0 -93
- package/scripts/consumer-reference-schema.mjs +0 -155
- package/scripts/create-component-state-session.mjs +0 -100
- package/scripts/create-page-evidence-session.mjs +0 -139
- package/scripts/domain-document-boundaries.mjs +0 -40
- package/scripts/domain-validation-checks.mjs +0 -141
- package/scripts/evidence-artifact-contract.mjs +0 -191
- package/scripts/finalize-component-state-evidence.mjs +0 -165
- package/scripts/finalize-page-evidence.mjs +0 -197
- package/scripts/generate-consumer-reference-evidence.mjs +0 -154
- package/scripts/generate-patterns.mjs +0 -218
- package/scripts/governance-matrix-contract.mjs +0 -73
- package/scripts/governance-matrix-negative-cases.mjs +0 -114
- package/scripts/governance-policy-contract.mjs +0 -49
- package/scripts/governance-test-fixture.mjs +0 -321
- package/scripts/governance-workflow-negative-cases.mjs +0 -241
- package/scripts/governed-profile-registry.mjs +0 -125
- package/scripts/json-schema-formats.mjs +0 -31
- package/scripts/markdown-structure.mjs +0 -45
- package/scripts/page-artifact-metadata.mjs +0 -124
- package/scripts/page-evidence-contract.mjs +0 -297
- package/scripts/page-evidence-fixture.mjs +0 -271
- package/scripts/pattern-data.mjs +0 -325
- package/scripts/profile-record-contract.mjs +0 -140
- package/scripts/promotion-attestation-contract.mjs +0 -91
- package/scripts/promotion-boundary-test-contract.mjs +0 -92
- package/scripts/promotion-fixture-inventory.mjs +0 -109
- package/scripts/promotion-governance-contract.mjs +0 -28
- package/scripts/reference-artifact-path-contract.mjs +0 -28
- package/scripts/reference-profile-contract.mjs +0 -114
- package/scripts/reference-token-contract.mjs +0 -156
- package/scripts/run-consumer-page-evidence-ci.mjs +0 -170
- package/scripts/summarize-sentinel-calibration.mjs +0 -98
- package/scripts/test-agent-adapter-conformance.mjs +0 -250
- package/scripts/test-agent-conformance-receipt.mjs +0 -106
- package/scripts/test-agent-events.mjs +0 -81
- package/scripts/test-agent-execution-effects.mjs +0 -114
- package/scripts/test-agent-execution-fixtures.mjs +0 -140
- package/scripts/test-agent-execution-kernel.mjs +0 -39
- package/scripts/test-agent-execution-learning.mjs +0 -130
- package/scripts/test-agent-execution-lifecycle.mjs +0 -143
- package/scripts/test-agent-execution-safety.mjs +0 -158
- package/scripts/test-agent-execution-views.mjs +0 -117
- package/scripts/test-agent-identity-integrity-cases.mjs +0 -160
- package/scripts/test-agent-knowledge-kernel.mjs +0 -183
- package/scripts/test-agent-learning.mjs +0 -246
- package/scripts/test-agent-mcp.mjs +0 -246
- package/scripts/test-agent-native-schemas.mjs +0 -215
- package/scripts/test-agent-projections.mjs +0 -249
- package/scripts/test-agent-registry-mcp-integrity.mjs +0 -215
- package/scripts/test-agent-registry-mutations.mjs +0 -110
- package/scripts/test-audit-evidence-freshness.mjs +0 -177
- package/scripts/test-component-state-sentinel.mjs +0 -35
- package/scripts/test-component-state-source-contract.mjs +0 -83
- package/scripts/test-consumer-conformance-sentinel.mjs +0 -165
- package/scripts/test-consumer-reference-sentinel.mjs +0 -31
- package/scripts/test-generate-consumer-reference-evidence.mjs +0 -124
- package/scripts/test-json-cli-output.mjs +0 -151
- package/scripts/test-json-schema-formats.mjs +0 -40
- package/scripts/test-reference-adapters.mjs +0 -199
- package/scripts/test-renderer-purity.mjs +0 -34
- package/scripts/test-sg-cli.mjs +0 -129
- package/scripts/test-summarize-sentinel-calibration.mjs +0 -226
- package/scripts/test-validate-baseline-manifest.mjs +0 -234
- package/scripts/test-validate-component-state-artifacts.mjs +0 -243
- package/scripts/test-validate-component-state.mjs +0 -106
- package/scripts/test-validate-consumer-conformance.mjs +0 -427
- package/scripts/test-validate-consumer-reference-cli.mjs +0 -69
- package/scripts/test-validate-consumer-reference.mjs +0 -263
- package/scripts/test-validate-domains.mjs +0 -357
- package/scripts/test-validate-governance.mjs +0 -292
- package/scripts/test-validate-ia.mjs +0 -262
- package/scripts/test-validate-okf.mjs +0 -73
- package/scripts/test-validate-page-evidence.mjs +0 -302
- package/scripts/test-validate-patterns.mjs +0 -157
- package/scripts/test-validate-promotion-rfc.mjs +0 -250
- package/scripts/test-validate-reference-profiles.mjs +0 -170
- package/scripts/test-validate-unity-org-wiki.mjs +0 -57
- package/scripts/test-validate-unity-source-contracts.mjs +0 -67
- package/scripts/test-validate-vocabulary.mjs +0 -174
- package/scripts/test-validate-webpage-workflow.mjs +0 -293
- package/scripts/validate-baseline-manifest.mjs +0 -44
- package/scripts/validate-catalog.mjs +0 -65
- package/scripts/validate-component-state.mjs +0 -141
- package/scripts/validate-consumer-conformance.mjs +0 -477
- package/scripts/validate-consumer-reference.mjs +0 -288
- package/scripts/validate-domains.mjs +0 -237
- package/scripts/validate-governance.mjs +0 -248
- package/scripts/validate-ia.mjs +0 -136
- package/scripts/validate-links.mjs +0 -66
- package/scripts/validate-okf.mjs +0 -104
- package/scripts/validate-page-evidence.mjs +0 -191
- package/scripts/validate-patterns.mjs +0 -246
- package/scripts/validate-promotion-rfc.mjs +0 -224
- package/scripts/validate-reference-artifacts.mjs +0 -125
- package/scripts/validate-renderer-purity.mjs +0 -56
- package/scripts/validate-unity-org-wiki.mjs +0 -146
- package/scripts/validate-unity-source-contracts.mjs +0 -107
- package/scripts/validate-vocabulary.mjs +0 -115
- package/scripts/validate-webpage-workflow.mjs +0 -254
- package/scripts/visual-expectation-contract.mjs +0 -20
- 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
|
+
}
|