@jini-ai/agentic 0.4.3 → 0.4.5
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/CHANGELOG.md +4 -0
- package/dist/core/a2ui/json-pointer.js +94 -73
- package/dist/core/a2ui/resolve.js +45 -23
- package/dist/core/a2ui/tree.js +8 -5
- package/dist/core/capability.js +21 -12
- package/dist/core/dom/dom-page-driver.js +89 -74
- package/dist/core/gen-ui/encoder.js +31 -31
- package/dist/core/guards.js +13 -6
- package/dist/core/handle.d.ts +13 -0
- package/dist/core/handle.js +19 -0
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -1
- package/dist/core/mcp-ui-apps.js +12 -6
- package/dist/core/page-executor.js +131 -111
- package/dist/core/site-guide/index.d.ts +4 -0
- package/dist/core/site-guide/index.js +4 -0
- package/dist/core/site-guide/site-guide.d.ts +18 -0
- package/dist/core/site-guide/site-guide.js +162 -0
- package/dist/core/site-guide/topic-schema.d.ts +16 -0
- package/dist/core/site-guide/topic-schema.js +68 -0
- package/dist/core/site-guide/types.d.ts +83 -0
- package/dist/core/site-guide/types.js +2 -0
- package/dist/skills/install/archive.js +42 -30
- package/dist/skills/install/github.js +102 -63
- package/dist/skills/install/index.d.ts +1 -0
- package/dist/skills/install/index.js +1 -0
- package/dist/skills/install/install-service.d.ts +20 -1
- package/dist/skills/install/install-service.js +23 -11
- package/dist/skills/install/layout.d.ts +8 -0
- package/dist/skills/install/layout.js +26 -1
- package/dist/skills/install/live-registration.js +1 -0
- package/dist/skills/install/node-filesystem.js +3 -1
- package/dist/skills/install/state.js +18 -7
- package/dist/skills/install/validation.d.ts +5 -1
- package/dist/skills/install/validation.js +85 -52
- package/dist/skills/install/yauzl-archive-reader.d.ts +29 -0
- package/dist/skills/install/yauzl-archive-reader.js +31 -0
- package/package.json +4 -4
- package/dist/a2ui/agent-to-renderer.d.ts +0 -613
- package/dist/a2ui/agent-to-renderer.js +0 -191
- package/dist/a2ui/catalog.d.ts +0 -73
- package/dist/a2ui/catalog.js +0 -304
- package/dist/a2ui/common-types.d.ts +0 -401
- package/dist/a2ui/common-types.js +0 -130
- package/dist/a2ui/index.d.ts +0 -16
- package/dist/a2ui/index.js +0 -16
- package/dist/a2ui/interpreter.d.ts +0 -71
- package/dist/a2ui/interpreter.js +0 -337
- package/dist/a2ui/json-pointer.d.ts +0 -48
- package/dist/a2ui/json-pointer.js +0 -174
- package/dist/a2ui/renderer-to-agent.d.ts +0 -342
- package/dist/a2ui/renderer-to-agent.js +0 -111
- package/dist/a2ui/resolve.d.ts +0 -28
- package/dist/a2ui/resolve.js +0 -144
- package/dist/a2ui/tree.d.ts +0 -66
- package/dist/a2ui/tree.js +0 -74
- package/dist/ag-ui.d.ts +0 -82
- package/dist/ag-ui.js +0 -85
- package/dist/agui/encoder.d.ts +0 -50
- package/dist/agui/encoder.js +0 -97
- package/dist/agui/events.d.ts +0 -59
- package/dist/agui/events.js +0 -15
- package/dist/capability.d.ts +0 -81
- package/dist/capability.js +0 -79
- package/dist/dom/dom-page-driver.d.ts +0 -36
- package/dist/dom/dom-page-driver.js +0 -601
- package/dist/dom/index.d.ts +0 -17
- package/dist/dom/index.js +0 -17
- package/dist/dom/model-context.d.ts +0 -69
- package/dist/dom/model-context.js +0 -44
- package/dist/element-handles.d.ts +0 -120
- package/dist/element-handles.js +0 -39
- package/dist/gen-ui/encoder.d.ts +0 -50
- package/dist/gen-ui/encoder.js +0 -97
- package/dist/gen-ui/events.d.ts +0 -59
- package/dist/gen-ui/events.js +0 -15
- package/dist/gen-ui/index.d.ts +0 -18
- package/dist/gen-ui/index.js +0 -17
- package/dist/guards.d.ts +0 -96
- package/dist/guards.js +0 -186
- package/dist/handle.d.ts +0 -41
- package/dist/handle.js +0 -41
- package/dist/mcp-ui-apps.d.ts +0 -223
- package/dist/mcp-ui-apps.js +0 -252
- package/dist/mcp-ui.d.ts +0 -133
- package/dist/mcp-ui.js +0 -134
- package/dist/page-capabilities.d.ts +0 -14
- package/dist/page-capabilities.js +0 -129
- package/dist/page-driver.d.ts +0 -120
- package/dist/page-driver.js +0 -2
- package/dist/page-executor.d.ts +0 -94
- package/dist/page-executor.js +0 -320
- package/dist/webmcp.d.ts +0 -202
- package/dist/webmcp.js +0 -173
package/CHANGELOG.md
CHANGED
|
@@ -64,42 +64,108 @@ export function parsePointerTokens({ pointer }, _optional = {}) {
|
|
|
64
64
|
return pointer.slice(1).split('/').map(unescapeToken);
|
|
65
65
|
}
|
|
66
66
|
export function joinPointer({ base, tokens }, _optional = {}) {
|
|
67
|
-
const baseTokens = base
|
|
67
|
+
const baseTokens = isRootPointer(base) ? [] : parsePointerTokens({ pointer: base });
|
|
68
68
|
return '/' + [...baseTokens, ...tokens].map(escapeToken).join('/');
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
if (isRootPointer(pointer))
|
|
73
|
-
return { found: true, value: doc };
|
|
74
|
-
let tokens;
|
|
70
|
+
/** The pointer's tokens, or `null` for a malformed pointer — the read and write paths both degrade on that instead of throwing. */
|
|
71
|
+
function tryParsePointerTokens(pointer) {
|
|
75
72
|
try {
|
|
76
|
-
|
|
73
|
+
return parsePointerTokens({ pointer });
|
|
77
74
|
}
|
|
78
75
|
catch {
|
|
79
|
-
return
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** The array index `token` names, or `undefined` unless it is a canonical RFC 6901 index (`0`, or digits with no leading zero — never `-`, signs, exponents or whitespace). */
|
|
80
|
+
function arrayIndexOf(token) {
|
|
81
|
+
return /^(0|[1-9]\d*)$/.test(token) ? Number(token) : undefined;
|
|
82
|
+
}
|
|
83
|
+
function isObjectLike(value) {
|
|
84
|
+
return value !== null && typeof value === 'object';
|
|
85
|
+
}
|
|
86
|
+
function notFound() {
|
|
87
|
+
return { found: false, value: undefined };
|
|
88
|
+
}
|
|
89
|
+
/** One step of a read: the member `token` names in `current`, if `current` itself (never its prototype chain) has it. */
|
|
90
|
+
function readMember(current, token) {
|
|
91
|
+
if (Array.isArray(current)) {
|
|
92
|
+
const index = arrayIndexOf(token);
|
|
93
|
+
return index !== undefined && index < current.length ? { found: true, value: current[index] } : notFound();
|
|
80
94
|
}
|
|
95
|
+
return isObjectLike(current) && hasOwnToken(current, token) ? { found: true, value: current[token] } : notFound();
|
|
96
|
+
}
|
|
97
|
+
/** Reads the value at `pointer` in `doc`. Never throws on a non-resolving path — `found: false` degrades sanely, matching this port's "a bad binding must not crash the renderer" adversarial requirement (see `resolve.ts`). */
|
|
98
|
+
export function getAtPointer({ doc, pointer }, _optional = {}) {
|
|
99
|
+
if (isRootPointer(pointer))
|
|
100
|
+
return { found: true, value: doc };
|
|
101
|
+
const tokens = tryParsePointerTokens(pointer);
|
|
102
|
+
if (tokens === null)
|
|
103
|
+
return notFound();
|
|
81
104
|
let current = doc;
|
|
82
105
|
for (const token of tokens) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (index >= current.length)
|
|
88
|
-
return { found: false, value: undefined };
|
|
89
|
-
current = current[index];
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (current !== null && typeof current === 'object') {
|
|
93
|
-
const record = current;
|
|
94
|
-
if (!hasOwnToken(record, token))
|
|
95
|
-
return { found: false, value: undefined };
|
|
96
|
-
current = record[token];
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
return { found: false, value: undefined };
|
|
106
|
+
const step = readMember(current, token);
|
|
107
|
+
if (!step.found)
|
|
108
|
+
return step;
|
|
109
|
+
current = step.value;
|
|
100
110
|
}
|
|
101
111
|
return { found: true, value: current };
|
|
102
112
|
}
|
|
113
|
+
/** A shallow copy of `current` when it is an object, otherwise a fresh empty object to write into. */
|
|
114
|
+
function copyRecord(current) {
|
|
115
|
+
return isObjectLike(current) ? { ...current } : {};
|
|
116
|
+
}
|
|
117
|
+
/** The final write/delete into an array parent. A non-index token leaves the array untouched (same instance). */
|
|
118
|
+
function writeArrayLeaf(array, token, value) {
|
|
119
|
+
const index = arrayIndexOf(token);
|
|
120
|
+
if (index === undefined)
|
|
121
|
+
return array;
|
|
122
|
+
const next = array.slice();
|
|
123
|
+
// `splice` past the end removes nothing, so an out-of-range delete needs no guard of its own.
|
|
124
|
+
if (value === null) {
|
|
125
|
+
next.splice(index, 1);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
next[index] = value;
|
|
129
|
+
}
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
/** The final write/delete into an object parent (a non-object parent is replaced by a fresh object). */
|
|
133
|
+
function writeRecordLeaf(current, token, value) {
|
|
134
|
+
const record = copyRecord(current);
|
|
135
|
+
if (value === null) {
|
|
136
|
+
delete record[token];
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
defineOwn(record, token, value);
|
|
140
|
+
}
|
|
141
|
+
return record;
|
|
142
|
+
}
|
|
143
|
+
/** Returns a copy of `current` with `leaf` applied at `remaining` (the parent tokens still to walk). */
|
|
144
|
+
function writePath(current, remaining, leaf) {
|
|
145
|
+
if (remaining.length === 0) {
|
|
146
|
+
// `current` here is the parent container; apply the final write/delete.
|
|
147
|
+
return Array.isArray(current) ? writeArrayLeaf(current, leaf.token, leaf.value) : writeRecordLeaf(current, leaf.token, leaf.value);
|
|
148
|
+
}
|
|
149
|
+
const [head, ...rest] = remaining;
|
|
150
|
+
return Array.isArray(current) ? writeArrayBranch(current, head, rest, leaf) : writeRecordBranch(current, head, rest, leaf);
|
|
151
|
+
}
|
|
152
|
+
/** An intermediate array segment. A non-index token (the empty token included) leaves the array untouched. */
|
|
153
|
+
function writeArrayBranch(array, head, rest, leaf) {
|
|
154
|
+
const index = arrayIndexOf(head);
|
|
155
|
+
if (index === undefined)
|
|
156
|
+
return array;
|
|
157
|
+
const next = array.slice();
|
|
158
|
+
next[index] = writePath(index < next.length ? next[index] : {}, rest, leaf);
|
|
159
|
+
return next;
|
|
160
|
+
}
|
|
161
|
+
/** An intermediate object segment. An empty-string token is a documented no-op (see the `"//x"` test): the copy is returned unwritten. */
|
|
162
|
+
function writeRecordBranch(current, head, rest, leaf) {
|
|
163
|
+
const record = copyRecord(current);
|
|
164
|
+
if (!head)
|
|
165
|
+
return record;
|
|
166
|
+
defineOwn(record, head, writePath(hasOwnToken(record, head) ? record[head] : {}, rest, leaf));
|
|
167
|
+
return record;
|
|
168
|
+
}
|
|
103
169
|
/**
|
|
104
170
|
* Writes `value` at `pointer` in `doc`, returning a **new** document (the original is never
|
|
105
171
|
* mutated — every intermediate object on the path is shallow-copied). Missing intermediate
|
|
@@ -121,54 +187,9 @@ export function getAtPointer({ doc, pointer }, _optional = {}) {
|
|
|
121
187
|
export function setAtPointer({ doc, pointer, value }, _optional = {}) {
|
|
122
188
|
if (isRootPointer(pointer))
|
|
123
189
|
return value;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
tokens = parsePointerTokens({ pointer });
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
190
|
+
const tokens = tryParsePointerTokens(pointer);
|
|
191
|
+
if (tokens === null)
|
|
129
192
|
return doc;
|
|
130
|
-
}
|
|
131
|
-
const parentTokens = tokens.slice(0, -1);
|
|
132
|
-
const lastToken = tokens[tokens.length - 1];
|
|
133
|
-
function recurse(current, remaining) {
|
|
134
|
-
if (remaining.length === 0) {
|
|
135
|
-
// `current` here is the parent container; apply the final write/delete.
|
|
136
|
-
if (Array.isArray(current)) {
|
|
137
|
-
if (!/^(0|[1-9]\d*)$/.test(lastToken))
|
|
138
|
-
return current;
|
|
139
|
-
const index = Number(lastToken);
|
|
140
|
-
const next = current.slice();
|
|
141
|
-
if (value === null) {
|
|
142
|
-
if (index < next.length)
|
|
143
|
-
next.splice(index, 1);
|
|
144
|
-
return next;
|
|
145
|
-
}
|
|
146
|
-
next[index] = value;
|
|
147
|
-
return next;
|
|
148
|
-
}
|
|
149
|
-
const record = current !== null && typeof current === 'object' ? { ...current } : {};
|
|
150
|
-
if (value === null) {
|
|
151
|
-
delete record[lastToken];
|
|
152
|
-
return record;
|
|
153
|
-
}
|
|
154
|
-
defineOwn(record, lastToken, value);
|
|
155
|
-
return record;
|
|
156
|
-
}
|
|
157
|
-
const [head, ...rest] = remaining;
|
|
158
|
-
if (Array.isArray(current)) {
|
|
159
|
-
if (!head || !/^(0|[1-9]\d*)$/.test(head))
|
|
160
|
-
return current;
|
|
161
|
-
const index = Number(head);
|
|
162
|
-
const next = current.slice();
|
|
163
|
-
next[index] = recurse(index < next.length ? next[index] : {}, rest);
|
|
164
|
-
return next;
|
|
165
|
-
}
|
|
166
|
-
const record = current !== null && typeof current === 'object' ? { ...current } : {};
|
|
167
|
-
if (!head)
|
|
168
|
-
return record;
|
|
169
|
-
defineOwn(record, head, recurse(hasOwnToken(record, head) ? record[head] : {}, rest));
|
|
170
|
-
return record;
|
|
171
|
-
}
|
|
172
|
-
return recurse(doc, parentTokens);
|
|
193
|
+
return writePath(doc, tokens.slice(0, -1), { token: tokens[tokens.length - 1], value });
|
|
173
194
|
}
|
|
174
195
|
//# sourceMappingURL=json-pointer.js.map
|
|
@@ -56,41 +56,60 @@ function resolveBindingPath(path, ctx) {
|
|
|
56
56
|
}
|
|
57
57
|
return { ok: true, value: result.value };
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
const offsetArg = call.args?.offset;
|
|
65
|
-
const offsetResult = offsetArg === undefined ? { ok: true, value: 0 } : resolveDynamicValue({ value: offsetArg, ctx: ctx }, { itemScope: ctx.itemScope });
|
|
66
|
-
if (!offsetResult.ok)
|
|
67
|
-
return offsetResult;
|
|
68
|
-
const offset = typeof offsetResult.value === 'number' ? offsetResult.value : 0;
|
|
69
|
-
return { ok: true, value: ctx.itemScope.index + offset };
|
|
59
|
+
/** `@index` (the one system function, answered without the catalog): the item scope's index plus an optional, resolved `offset` arg (non-numeric counts as 0). */
|
|
60
|
+
function resolveIndexCall(call, ctx) {
|
|
61
|
+
if (!ctx.itemScope) {
|
|
62
|
+
return { ok: false, reason: 'INDEX_OUTSIDE_LIST_CONTEXT', detail: '@index used outside a list-template item context' };
|
|
70
63
|
}
|
|
64
|
+
const offsetArg = call.args?.offset;
|
|
65
|
+
const offsetResult = offsetArg === undefined ? { ok: true, value: 0 } : resolveValue(offsetArg, ctx);
|
|
66
|
+
if (!offsetResult.ok)
|
|
67
|
+
return offsetResult;
|
|
68
|
+
const offset = typeof offsetResult.value === 'number' ? offsetResult.value : 0;
|
|
69
|
+
return { ok: true, value: ctx.itemScope.index + offset };
|
|
70
|
+
}
|
|
71
|
+
function isCallableFromSide(callableFrom, side) {
|
|
72
|
+
return callableFrom === 'rendererOrAgent' || (callableFrom === 'rendererOnly' && side === 'renderer') || (callableFrom === 'agentOnly' && side === 'agent');
|
|
73
|
+
}
|
|
74
|
+
/** The catalog-side refusals, in the order they apply: not registered, then the `callableFrom` boundary, then no implementation. Returns the implementation when all three pass. */
|
|
75
|
+
function lookUpCallableImpl(call, ctx) {
|
|
71
76
|
if (!isFunctionRegistered({ catalog: ctx.catalog, functionName: call.call })) {
|
|
72
77
|
return { ok: false, reason: 'FUNCTION_NOT_REGISTERED', detail: `function "${call.call}" is not registered in catalog "${ctx.catalog.catalogId}"` };
|
|
73
78
|
}
|
|
74
79
|
const callableFrom = callableFromOf({ catalog: ctx.catalog, functionName: call.call });
|
|
75
|
-
|
|
76
|
-
if (!allowed) {
|
|
80
|
+
if (!isCallableFromSide(callableFrom, ctx.side)) {
|
|
77
81
|
return {
|
|
78
82
|
ok: false,
|
|
79
83
|
reason: 'FUNCTION_NOT_CALLABLE_FROM_SIDE',
|
|
80
84
|
detail: `function "${call.call}" has callableFrom="${callableFrom}", not callable from the "${ctx.side}" side`,
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
87
|
+
const impl = ctx.catalog.functions.get(call.call)?.impl;
|
|
88
|
+
if (!impl) {
|
|
85
89
|
return { ok: false, reason: 'FUNCTION_NOT_IMPLEMENTED', detail: `function "${call.call}" is registered but this renderer has no implementation for it` };
|
|
86
90
|
}
|
|
91
|
+
return { ok: true, impl };
|
|
92
|
+
}
|
|
93
|
+
/** Resolves every arg value in key order, stopping at the first failure. */
|
|
94
|
+
function resolveArgs(args, ctx) {
|
|
87
95
|
const resolvedArgs = {};
|
|
88
|
-
for (const [key, argValue] of Object.entries(
|
|
96
|
+
for (const [key, argValue] of Object.entries(args ?? {})) {
|
|
89
97
|
const argResult = resolveArgValue(argValue, ctx);
|
|
90
98
|
if (!argResult.ok)
|
|
91
99
|
return argResult;
|
|
92
100
|
resolvedArgs[key] = argResult.value;
|
|
93
101
|
}
|
|
102
|
+
return { ok: true, value: resolvedArgs };
|
|
103
|
+
}
|
|
104
|
+
function resolveFunctionCall(call, ctx) {
|
|
105
|
+
if (call.call === '@index')
|
|
106
|
+
return resolveIndexCall(call, ctx);
|
|
107
|
+
const lookup = lookUpCallableImpl(call, ctx);
|
|
108
|
+
if (!lookup.ok)
|
|
109
|
+
return lookup;
|
|
110
|
+
const argsResult = resolveArgs(call.args, ctx);
|
|
111
|
+
if (!argsResult.ok)
|
|
112
|
+
return argsResult;
|
|
94
113
|
// `impl` is host-supplied code handed agent-supplied arguments, and this module validates
|
|
95
114
|
// neither against the other (the catalog's per-function arg schemas are not implemented — see
|
|
96
115
|
// `catalog.ts`'s module doc). An entirely ordinary implementation therefore throws the moment an
|
|
@@ -98,7 +117,7 @@ function resolveFunctionCall(call, ctx) {
|
|
|
98
117
|
// travel out through `applyAgentMessage` into the host's render, where nothing catches it. It is
|
|
99
118
|
// a resolution failure like any other: reported, not propagated.
|
|
100
119
|
try {
|
|
101
|
-
return { ok: true, value:
|
|
120
|
+
return { ok: true, value: lookup.impl(argsResult.value) };
|
|
102
121
|
}
|
|
103
122
|
catch (error) {
|
|
104
123
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -131,15 +150,18 @@ function resolveArgValue(argValue, ctx) {
|
|
|
131
150
|
}
|
|
132
151
|
return { ok: true, value: resolvedItems };
|
|
133
152
|
}
|
|
134
|
-
return
|
|
153
|
+
return resolveValue(argValue, ctx);
|
|
135
154
|
}
|
|
136
|
-
/** Resolves
|
|
137
|
-
|
|
138
|
-
const context = { ...ctx, ...(itemScope === undefined ? {} : { itemScope }) };
|
|
155
|
+
/** Resolves one `Dynamic*` value in an already-built context (the item scope, if any, is in `ctx`). */
|
|
156
|
+
function resolveValue(value, ctx) {
|
|
139
157
|
if (isDataBinding(value))
|
|
140
|
-
return resolveBindingPath(value.path,
|
|
158
|
+
return resolveBindingPath(value.path, ctx);
|
|
141
159
|
if (isFunctionCall(value))
|
|
142
|
-
return resolveFunctionCall(value,
|
|
160
|
+
return resolveFunctionCall(value, ctx);
|
|
143
161
|
return { ok: true, value };
|
|
144
162
|
}
|
|
163
|
+
/** Resolves any `Dynamic*` value (`DynamicString`/`DynamicNumber`/`DynamicBoolean`/`DynamicStringList`/`DynamicValue` all share this one shape: literal | `DataBinding` | `FunctionCall`). */
|
|
164
|
+
export function resolveDynamicValue({ value, ctx }, { itemScope } = {}) {
|
|
165
|
+
return resolveValue(value, { ...ctx, ...(itemScope === undefined ? {} : { itemScope }) });
|
|
166
|
+
}
|
|
145
167
|
//# sourceMappingURL=resolve.js.map
|
package/dist/core/a2ui/tree.js
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
* is orders of magnitude smaller), so this is a fail-safe, not a design budget.
|
|
12
12
|
*/
|
|
13
13
|
export const MAX_RENDER_NODES = 50_000;
|
|
14
|
+
/** Pushes `childIds` onto the walk's stack at `depth`. */
|
|
15
|
+
function pushChildren(stack, childIds, depth) {
|
|
16
|
+
// Pushed in reverse so the first child is popped first, preserving pre-order.
|
|
17
|
+
for (let index = childIds.length - 1; index >= 0; index -= 1) {
|
|
18
|
+
stack.push({ id: childIds[index], depth });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
14
21
|
/**
|
|
15
22
|
* @param components The full component map (id -> instance).
|
|
16
23
|
* @param rootId The id to start walking from (typically `'root'`, but not assumed — a surface
|
|
@@ -63,11 +70,7 @@ export function flattenRenderTree({ components, rootId, getChildIds }, _optional
|
|
|
63
70
|
output.push({ id, depth, status: 'ok' });
|
|
64
71
|
ancestors.add(id);
|
|
65
72
|
stack.push({ leave: id });
|
|
66
|
-
|
|
67
|
-
const childIds = getChildIds({ component });
|
|
68
|
-
for (let index = childIds.length - 1; index >= 0; index -= 1) {
|
|
69
|
-
stack.push({ id: childIds[index], depth: depth + 1 });
|
|
70
|
-
}
|
|
73
|
+
pushChildren(stack, getChildIds({ component }), depth + 1);
|
|
71
74
|
}
|
|
72
75
|
return output;
|
|
73
76
|
}
|
package/dist/core/capability.js
CHANGED
|
@@ -40,19 +40,28 @@ export function findCapabilityInputError({ capability, input }, _optional = {})
|
|
|
40
40
|
return `"${name}" is required`;
|
|
41
41
|
}
|
|
42
42
|
if (additionalProperties === false) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
const unknownError = findUnknownArgumentError({ properties, input });
|
|
44
|
+
if (unknownError !== null)
|
|
45
|
+
return unknownError;
|
|
46
|
+
}
|
|
47
|
+
return findPropertyInputError({ properties, input });
|
|
48
|
+
}
|
|
49
|
+
function findUnknownArgumentError({ properties, input }, _optional = {}) {
|
|
50
|
+
// `key in properties` walks the whole prototype chain, so a key that happens to be inherited
|
|
51
|
+
// from Object.prototype (`__proto__`, `constructor`, `toString`, `valueOf`, …) reads as
|
|
52
|
+
// "known" regardless of whether the schema actually lists it — `'constructor' in {}` is
|
|
53
|
+
// true even on an empty object. `JSON.parse('{"__proto__":...}')` creates a real own data
|
|
54
|
+
// property (not the accessor), so this was a live way for such a key to escape the
|
|
55
|
+
// "unknown argument" refusal entirely. hasOwnProperty ignores the prototype chain, so only
|
|
56
|
+
// keys the schema actually declares count as known.
|
|
57
|
+
const unknown = Object.keys(input)
|
|
58
|
+
.filter((key) => !Object.prototype.hasOwnProperty.call(properties, key));
|
|
59
|
+
if (unknown.length > 0) {
|
|
60
|
+
return `unknown ${unknown.length === 1 ? 'argument' : 'arguments'}: ${unknown.sort().join(', ')}`;
|
|
55
61
|
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function findPropertyInputError({ properties, input }, _optional = {}) {
|
|
56
65
|
for (const [name, schema] of Object.entries(properties)) {
|
|
57
66
|
const value = input[name];
|
|
58
67
|
if (value === undefined)
|
|
@@ -103,25 +103,28 @@ function accessibleLabelsOf(control) {
|
|
|
103
103
|
];
|
|
104
104
|
return [...new Set(candidates.filter((text) => !!text))];
|
|
105
105
|
}
|
|
106
|
+
function editableFieldDescriptorOf({ control }, _optional = {}) {
|
|
107
|
+
// Reported as a field so the same guards run: a contenteditable div named `card-number` is
|
|
108
|
+
// no more fillable than an input would be. `type` is not a real HTML input type, and is not
|
|
109
|
+
// in any denied set, which is correct — the guard should judge it by name and attributes.
|
|
110
|
+
return {
|
|
111
|
+
type: 'contenteditable',
|
|
112
|
+
// Read for the same reason `name` is: neither attribute is standard on a div, but a page
|
|
113
|
+
// that bothers to set one is telling the guard what the field holds, and honouring `name`
|
|
114
|
+
// while ignoring `autocomplete` would refuse `name="card_number"` and accept the
|
|
115
|
+
// `autocomplete="cc-number"` spelling of the same field.
|
|
116
|
+
autocomplete: control.getAttribute('autocomplete')?.toLowerCase() || undefined,
|
|
117
|
+
name: control.getAttribute('name') || undefined,
|
|
118
|
+
id: control.id || undefined,
|
|
119
|
+
accessibleLabels: accessibleLabelsOf(control),
|
|
120
|
+
readOnly: control.getAttribute('aria-readonly') === 'true',
|
|
121
|
+
disabled: control.getAttribute('aria-disabled') === 'true',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
106
124
|
/** The field attributes the guards need, or `null` when the control is not a text-bearing input. */
|
|
107
125
|
function fieldDescriptorOf(control) {
|
|
108
126
|
if (isEditableRegion(control)) {
|
|
109
|
-
|
|
110
|
-
// no more fillable than an input would be. `type` is not a real HTML input type, and is not
|
|
111
|
-
// in any denied set, which is correct — the guard should judge it by name and attributes.
|
|
112
|
-
return {
|
|
113
|
-
type: 'contenteditable',
|
|
114
|
-
// Read for the same reason `name` is: neither attribute is standard on a div, but a page
|
|
115
|
-
// that bothers to set one is telling the guard what the field holds, and honouring `name`
|
|
116
|
-
// while ignoring `autocomplete` would refuse `name="card_number"` and accept the
|
|
117
|
-
// `autocomplete="cc-number"` spelling of the same field.
|
|
118
|
-
autocomplete: control.getAttribute('autocomplete')?.toLowerCase() || undefined,
|
|
119
|
-
name: control.getAttribute('name') || undefined,
|
|
120
|
-
id: control.id || undefined,
|
|
121
|
-
accessibleLabels: accessibleLabelsOf(control),
|
|
122
|
-
readOnly: control.getAttribute('aria-readonly') === 'true',
|
|
123
|
-
disabled: control.getAttribute('aria-disabled') === 'true',
|
|
124
|
-
};
|
|
127
|
+
return editableFieldDescriptorOf({ control });
|
|
125
128
|
}
|
|
126
129
|
if (!(control instanceof HTMLInputElement) && !(control instanceof HTMLTextAreaElement)) {
|
|
127
130
|
return null;
|
|
@@ -139,6 +142,48 @@ function fieldDescriptorOf(control) {
|
|
|
139
142
|
disabled: control.matches(':disabled'),
|
|
140
143
|
};
|
|
141
144
|
}
|
|
145
|
+
function dropdownFieldDescriptorOf({ dropdown }, _optional = {}) {
|
|
146
|
+
if (dropdown === undefined)
|
|
147
|
+
return null;
|
|
148
|
+
return {
|
|
149
|
+
type: 'select',
|
|
150
|
+
name: dropdown.name || undefined,
|
|
151
|
+
id: dropdown.id || undefined,
|
|
152
|
+
// Same reason `fieldDescriptorOf` carries these: a `<select aria-label="Card number"
|
|
153
|
+
// name="f9">` states its sensitivity nowhere else, and this descriptor is built by hand
|
|
154
|
+
// rather than going through that function, so it needs the labels wired up separately.
|
|
155
|
+
accessibleLabels: accessibleLabelsOf(dropdown),
|
|
156
|
+
// Same `:disabled` reasoning as `disabledOf` — a `<select>` inside a disabled `<fieldset>`
|
|
157
|
+
// is not itself flagged by the `.disabled` IDL property.
|
|
158
|
+
disabled: dropdown.matches(':disabled'),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function rawElementStateOf({ element, control, checkable, readable, value, dropdown }, _optional = {}) {
|
|
162
|
+
const text = publicTextOf(element);
|
|
163
|
+
return {
|
|
164
|
+
text,
|
|
165
|
+
// A contenteditable region has no `.value`; its content IS its value, so `text` is the
|
|
166
|
+
// one channel carrying it and the executor has to gate it exactly like a value. Reported
|
|
167
|
+
// as a mechanical fact about the element, not as a judgment about the content — the
|
|
168
|
+
// decision of whether it may be shown stays in `projectElementState`, with every other
|
|
169
|
+
// secrecy rule.
|
|
170
|
+
...(isEditableRegion(control) ? { textIsValue: true } : {}),
|
|
171
|
+
// Reported raw and unconditionally; `projectElementState` applies the read guard. A
|
|
172
|
+
// driver deciding here which values are secret is a driver holding policy.
|
|
173
|
+
...(value !== undefined ? { value } : {}),
|
|
174
|
+
...(checkable ? { checked: control.checked } : {}),
|
|
175
|
+
...(disabledOf(control) !== undefined ? { disabled: disabledOf(control) } : {}),
|
|
176
|
+
...(visibilityOf(element) !== undefined ? { visible: visibilityOf(element) } : {}),
|
|
177
|
+
...(readable !== null ? { field: readable } : {}),
|
|
178
|
+
...(dropdown !== undefined
|
|
179
|
+
? {
|
|
180
|
+
options: Array.from(dropdown.options)
|
|
181
|
+
.map((entry) => entry.text.trim())
|
|
182
|
+
.filter((entry) => entry.length > 0),
|
|
183
|
+
}
|
|
184
|
+
: {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
142
187
|
/**
|
|
143
188
|
* Whether the element is actually rendered, or `undefined` when the platform cannot say.
|
|
144
189
|
*
|
|
@@ -222,6 +267,30 @@ function describe(element, page) {
|
|
|
222
267
|
page,
|
|
223
268
|
};
|
|
224
269
|
}
|
|
270
|
+
function findSelectableOption({ control, handle, option }, _optional = {}) {
|
|
271
|
+
// Visible text first, then value: the text is what a caller saw in `find_elements`, and
|
|
272
|
+
// matching value first would make an option whose value collides with another's label
|
|
273
|
+
// resolve to the wrong entry.
|
|
274
|
+
// A disabled option is not selectable by the user either, for the same reason.
|
|
275
|
+
const options = Array.from(control.options).filter((entry) => !entry.disabled);
|
|
276
|
+
const match = options.find((entry) => entry.text.trim() === option)
|
|
277
|
+
?? options.find((entry) => entry.value === option);
|
|
278
|
+
if (match === undefined) {
|
|
279
|
+
// Every other piece of page-authored text this system hands to a model goes through
|
|
280
|
+
// normalizeAgentLabel first — bounded, control- and bidi-stripped. `available` is exactly
|
|
281
|
+
// that: the page's own <option> texts, read verbatim with no length bound of their own.
|
|
282
|
+
// The caller's `option` is sanitized too, for the same reason page.navigate's caller-
|
|
283
|
+
// supplied `page` now is — an unbounded value reaching a thrown message a model reads is
|
|
284
|
+
// the same shape of problem regardless of which side authored it. `handle` needs no
|
|
285
|
+
// treatment: it is already validated ASCII, length-capped by isValidElementHandle before
|
|
286
|
+
// this driver is ever called.
|
|
287
|
+
const safeOption = normalizeAgentLabel({ raw: option }).text;
|
|
288
|
+
const available = options.map((entry) => normalizeAgentLabel({ raw: entry.text.trim() }).text)
|
|
289
|
+
.filter((text) => text.length > 0);
|
|
290
|
+
throw new Error(`"${safeOption}" is not an option of "${handle}". Available: ${available.length > 0 ? available.join(', ') : '(none)'}`);
|
|
291
|
+
}
|
|
292
|
+
return match;
|
|
293
|
+
}
|
|
225
294
|
/**
|
|
226
295
|
* Builds a driver over `root`.
|
|
227
296
|
*
|
|
@@ -355,45 +424,11 @@ export function createDomPageDriver({ root, pages }, options = {}) {
|
|
|
355
424
|
// avoided by looking. It carries a descriptor too, so the read guard still runs — a
|
|
356
425
|
// `<select name="secret_token">` is no more readable than an input of the same name.
|
|
357
426
|
const dropdown = control instanceof HTMLSelectElement ? control : undefined;
|
|
358
|
-
const readable = field ?? (dropdown
|
|
359
|
-
type: 'select',
|
|
360
|
-
name: dropdown.name || undefined,
|
|
361
|
-
id: dropdown.id || undefined,
|
|
362
|
-
// Same reason `fieldDescriptorOf` carries these: a `<select aria-label="Card number"
|
|
363
|
-
// name="f9">` states its sensitivity nowhere else, and this descriptor is built by hand
|
|
364
|
-
// rather than going through that function, so it needs the labels wired up separately.
|
|
365
|
-
accessibleLabels: accessibleLabelsOf(dropdown),
|
|
366
|
-
// Same `:disabled` reasoning as `disabledOf` — a `<select>` inside a disabled `<fieldset>`
|
|
367
|
-
// is not itself flagged by the `.disabled` IDL property.
|
|
368
|
-
disabled: dropdown.matches(':disabled'),
|
|
369
|
-
});
|
|
427
|
+
const readable = field ?? dropdownFieldDescriptorOf({ dropdown });
|
|
370
428
|
const value = field !== null
|
|
371
429
|
? control.value
|
|
372
430
|
: dropdown?.value;
|
|
373
|
-
|
|
374
|
-
return {
|
|
375
|
-
text,
|
|
376
|
-
// A contenteditable region has no `.value`; its content IS its value, so `text` is the
|
|
377
|
-
// one channel carrying it and the executor has to gate it exactly like a value. Reported
|
|
378
|
-
// as a mechanical fact about the element, not as a judgment about the content — the
|
|
379
|
-
// decision of whether it may be shown stays in `projectElementState`, with every other
|
|
380
|
-
// secrecy rule.
|
|
381
|
-
...(isEditableRegion(control) ? { textIsValue: true } : {}),
|
|
382
|
-
// Reported raw and unconditionally; `projectElementState` applies the read guard. A
|
|
383
|
-
// driver deciding here which values are secret is a driver holding policy.
|
|
384
|
-
...(value !== undefined ? { value } : {}),
|
|
385
|
-
...(checkable ? { checked: control.checked } : {}),
|
|
386
|
-
...(disabledOf(control) !== undefined ? { disabled: disabledOf(control) } : {}),
|
|
387
|
-
...(visibilityOf(element) !== undefined ? { visible: visibilityOf(element) } : {}),
|
|
388
|
-
...(readable !== null ? { field: readable } : {}),
|
|
389
|
-
...(dropdown !== undefined
|
|
390
|
-
? {
|
|
391
|
-
options: Array.from(dropdown.options)
|
|
392
|
-
.map((entry) => entry.text.trim())
|
|
393
|
-
.filter((entry) => entry.length > 0),
|
|
394
|
-
}
|
|
395
|
-
: {}),
|
|
396
|
-
};
|
|
431
|
+
return rawElementStateOf({ element, control, checkable, readable, value, dropdown });
|
|
397
432
|
},
|
|
398
433
|
/**
|
|
399
434
|
* Two animation frames — long enough for a framework to have re-rendered and the browser to
|
|
@@ -489,7 +524,7 @@ export function createDomPageDriver({ root, pages }, options = {}) {
|
|
|
489
524
|
}
|
|
490
525
|
// A disabled control is not one a user could touch, so writing to it is a way around the
|
|
491
526
|
// page's own rule and the resulting selection cannot be submitted. The per-`<option>` filter
|
|
492
|
-
//
|
|
527
|
+
// in `findSelectableOption` reads as if it enforced that, but only ever looked at each option — a
|
|
493
528
|
// `<select disabled>` (and a `<select>` inside a `<fieldset disabled>`, which `:disabled`
|
|
494
529
|
// also covers and `.disabled` does not) was written to happily, and the executor then
|
|
495
530
|
// reported `targetChanged: true` because the value really had changed.
|
|
@@ -501,27 +536,7 @@ export function createDomPageDriver({ root, pages }, options = {}) {
|
|
|
501
536
|
throw new Error(`"${handle}" is disabled, so its selection cannot be changed — `
|
|
502
537
|
+ 'a user cannot change it either, and its value would not be submitted');
|
|
503
538
|
}
|
|
504
|
-
|
|
505
|
-
// matching value first would make an option whose value collides with another's label
|
|
506
|
-
// resolve to the wrong entry.
|
|
507
|
-
// A disabled option is not selectable by the user either, for the same reason.
|
|
508
|
-
const options = Array.from(control.options).filter((entry) => !entry.disabled);
|
|
509
|
-
const match = options.find((entry) => entry.text.trim() === option)
|
|
510
|
-
?? options.find((entry) => entry.value === option);
|
|
511
|
-
if (match === undefined) {
|
|
512
|
-
// Every other piece of page-authored text this system hands to a model goes through
|
|
513
|
-
// normalizeAgentLabel first — bounded, control- and bidi-stripped. `available` is exactly
|
|
514
|
-
// that: the page's own <option> texts, read verbatim with no length bound of their own.
|
|
515
|
-
// The caller's `option` is sanitized too, for the same reason page.navigate's caller-
|
|
516
|
-
// supplied `page` now is — an unbounded value reaching a thrown message a model reads is
|
|
517
|
-
// the same shape of problem regardless of which side authored it. `handle` needs no
|
|
518
|
-
// treatment: it is already validated ASCII, length-capped by isValidElementHandle before
|
|
519
|
-
// this driver is ever called.
|
|
520
|
-
const safeOption = normalizeAgentLabel({ raw: option }).text;
|
|
521
|
-
const available = options.map((entry) => normalizeAgentLabel({ raw: entry.text.trim() }).text)
|
|
522
|
-
.filter((text) => text.length > 0);
|
|
523
|
-
throw new Error(`"${safeOption}" is not an option of "${handle}". Available: ${available.length > 0 ? available.join(', ') : '(none)'}`);
|
|
524
|
-
}
|
|
539
|
+
const match = findSelectableOption({ control, handle, option });
|
|
525
540
|
if (control.multiple) {
|
|
526
541
|
// A multi-select accumulates by default. Assigning `value` would silently clear every
|
|
527
542
|
// prior choice, so a caller building up a selection would end with only its last call's
|