@skillstate/opencode 2.2.2 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -142
- package/dist/feedback.d.ts +178 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +235 -0
- package/dist/feedback.js.map +1 -0
- package/dist/index.d.ts +58 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -3
- package/dist/index.js.map +1 -1
- package/dist/mode.d.ts +97 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +111 -0
- package/dist/mode.js.map +1 -0
- package/dist/opencode-adapter.d.ts +10 -46
- package/dist/opencode-adapter.d.ts.map +1 -1
- package/dist/opencode-adapter.js +1 -64
- package/dist/opencode-adapter.js.map +1 -1
- package/dist/paper-mode.d.ts +421 -0
- package/dist/paper-mode.d.ts.map +1 -0
- package/dist/paper-mode.js +445 -0
- package/dist/paper-mode.js.map +1 -0
- package/dist/plugin.d.ts +218 -76
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +867 -232
- package/dist/plugin.js.map +1 -1
- package/dist/response-sink.d.ts +208 -0
- package/dist/response-sink.d.ts.map +1 -0
- package/dist/response-sink.js +243 -0
- package/dist/response-sink.js.map +1 -0
- package/dist/runtime.d.ts +203 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +332 -0
- package/dist/runtime.js.map +1 -0
- package/dist/session-registry.d.ts +148 -0
- package/dist/session-registry.d.ts.map +1 -0
- package/dist/session-registry.js +236 -0
- package/dist/session-registry.js.map +1 -0
- package/dist/spec-loader.d.ts +81 -0
- package/dist/spec-loader.d.ts.map +1 -0
- package/dist/spec-loader.js +163 -0
- package/dist/spec-loader.js.map +1 -0
- package/dist/state-store.d.ts +126 -0
- package/dist/state-store.d.ts.map +1 -0
- package/dist/state-store.js +186 -0
- package/dist/state-store.js.map +1 -0
- package/dist/step-boundary.d.ts +91 -0
- package/dist/step-boundary.d.ts.map +1 -0
- package/dist/step-boundary.js +109 -0
- package/dist/step-boundary.js.map +1 -0
- package/dist/system-hint.d.ts +129 -0
- package/dist/system-hint.d.ts.map +1 -0
- package/dist/system-hint.js +166 -0
- package/dist/system-hint.js.map +1 -0
- package/dist/tools.d.ts +148 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +350 -0
- package/dist/tools.js.map +1 -0
- package/package.json +3 -2
- package/dist/plugin-types.d.ts +0 -71
- package/dist/plugin-types.d.ts.map +0 -1
- package/dist/plugin-types.js +0 -8
- package/dist/plugin-types.js.map +0 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The system-prompt fragment the plugin contributes to each model request.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this module is so small and so careful ───────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The v1 integration rewrote the conversation on every turn: it truncated
|
|
7
|
+
* history to the last three messages and appended a synthetic `role: "user"`
|
|
8
|
+
* message whose body was the raw state JSON. The reported symptom was that
|
|
9
|
+
* the agent stopped doing what it was asked and started emitting state
|
|
10
|
+
* JSON instead. The cause was structural, not a model quirk:
|
|
11
|
+
*
|
|
12
|
+
* - The last `role: "user"` message is what the model treats as the current
|
|
13
|
+
* instruction, so the state blob displaced the user's actual request.
|
|
14
|
+
* - Truncating history deleted the task statement, the tool results, and
|
|
15
|
+
* the errors the agent had just been given — it could not reason about
|
|
16
|
+
* work it could no longer see.
|
|
17
|
+
*
|
|
18
|
+
* The fix is a rule, enforced by `tests/opencode/context-integrity.test.ts`:
|
|
19
|
+
* **this plugin never mutates `event.messages`.** It contributes one
|
|
20
|
+
* additive, bounded fragment to `event.system`, states what the tools are
|
|
21
|
+
* for, and explicitly leaves the task alone.
|
|
22
|
+
*
|
|
23
|
+
* The wording below is a product requirement, not prose taste. It must
|
|
24
|
+
* describe the tools without instructing the model to change how it works.
|
|
25
|
+
* Imperative framing ("you must", "always", "respond with a JSON block")
|
|
26
|
+
* is what turns a persistence aid into a prompt override.
|
|
27
|
+
*/
|
|
28
|
+
/** Largest state JSON rendered into the hint before falling back to a key list. */
|
|
29
|
+
export const MAX_INLINE_STATE_CHARS = 4000;
|
|
30
|
+
/** The tool names the hint advertises — kept in sync with `tools.ts`. */
|
|
31
|
+
export const ADVERTISED_TOOLS = [
|
|
32
|
+
'skillstate_read',
|
|
33
|
+
'skillstate_update',
|
|
34
|
+
'skillstate_merge',
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Render a state document for inclusion in the system prompt.
|
|
38
|
+
*
|
|
39
|
+
* Small documents are inlined verbatim (the model sees what is already
|
|
40
|
+
* saved without a tool round-trip). Documents past
|
|
41
|
+
* {@link MAX_INLINE_STATE_CHARS} are summarized as a key list plus a count and
|
|
42
|
+
* a pointer to `skillstate_read` — an unbounded state file must not be able to
|
|
43
|
+
* grow the system prompt without limit.
|
|
44
|
+
*
|
|
45
|
+
* The reported count is in the SAME UNIT as the limit, and it is labelled with
|
|
46
|
+
* that unit's name. The first version reported `bytes` next to a limit expressed
|
|
47
|
+
* in characters, which is this project's whole recurring mistake in one line: a
|
|
48
|
+
* state of 4,003 Cyrillic characters is 3,003 over the limit and 7,993 bytes, so
|
|
49
|
+
* the model reading the summary was handed two numbers in two units and a limit
|
|
50
|
+
* in a third. §4.3 is explicit that sizes here are raw string CHARS.
|
|
51
|
+
*/
|
|
52
|
+
export function renderStateForHint(state) {
|
|
53
|
+
const json = JSON.stringify(state, null, 2);
|
|
54
|
+
if (json.length <= MAX_INLINE_STATE_CHARS)
|
|
55
|
+
return json;
|
|
56
|
+
const keys = Object.keys(state).sort();
|
|
57
|
+
return `${JSON.stringify({
|
|
58
|
+
_truncated: true,
|
|
59
|
+
chars: json.length,
|
|
60
|
+
keys,
|
|
61
|
+
hint: 'Saved state is large — call skillstate_read to load it.',
|
|
62
|
+
}, null, 2)}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* How many MODEL REQUESTS may pass with the state untouched before the drift
|
|
66
|
+
* notice fires.
|
|
67
|
+
*
|
|
68
|
+
* **"Requests", not "turns".** Measured on the weakest model in the
|
|
69
|
+
* catalogue: 20 file reads took 6 requests, 40 reads took 12, 70 reads took
|
|
70
|
+
* 27. The model batches tool calls, so a request is worth several file reads
|
|
71
|
+
* and a threshold described in "turns" is off by a factor of three. The
|
|
72
|
+
* number here is the unit the hook can actually count.
|
|
73
|
+
*
|
|
74
|
+
* 12 is chosen from the corpus rather than taste: the average run in the
|
|
75
|
+
* host's own store showed the model ~29,900 prompt tokens per request, so a
|
|
76
|
+
* dozen silent requests is roughly 350k tokens re-sent for a record that
|
|
77
|
+
* never moved. High enough that an agent reading five files in a row is not
|
|
78
|
+
* nagged.
|
|
79
|
+
*/
|
|
80
|
+
export const DRIFT_NOTICE_AFTER_TURNS = 12;
|
|
81
|
+
/**
|
|
82
|
+
* The line that says what the state IS, which depends on whether the project
|
|
83
|
+
* was initialized.
|
|
84
|
+
*
|
|
85
|
+
* Neither version is an imperative. Both are statements about the setup, and
|
|
86
|
+
* the difference is that one describes a record the user asked for and the
|
|
87
|
+
* other describes an optional convenience. A model told to skip the tools
|
|
88
|
+
* when it judges them unnecessary will eventually judge a long task
|
|
89
|
+
* unnecessary at exactly the wrong moment; a model told the state is the
|
|
90
|
+
* project's record has a fact to work with.
|
|
91
|
+
*/
|
|
92
|
+
function purposeLine(initialized, statePath) {
|
|
93
|
+
return initialized
|
|
94
|
+
? `This project has an execution state at \`${statePath}\`. It is the project's record: what has been established, decided, and left to do, kept across a context reset or compaction. The conversation is not that record, and it is not kept — the state file is.`
|
|
95
|
+
: `Notes for this project are saved at \`${statePath}\` and survive a context reset or compaction.`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The notice shown once when the state has not moved for a long stretch.
|
|
99
|
+
*
|
|
100
|
+
* This is feedback, not an instruction, and the distinction is the whole
|
|
101
|
+
* design. It states a measured fact — this many turns, no write — and
|
|
102
|
+
* nothing about what the model ought to do. That keeps the v1 failure
|
|
103
|
+
* impossible (nothing here can displace the user's task) while still
|
|
104
|
+
* telling a drifting agent that the user initialized this for a reason.
|
|
105
|
+
*
|
|
106
|
+
* It fires once per silence, not every turn: a notice that repeats forever
|
|
107
|
+
* is wallpaper, and after the second copy nobody reads it.
|
|
108
|
+
*/
|
|
109
|
+
export function driftNotice(turnsSinceWrite) {
|
|
110
|
+
return `Note: this state file has not changed across the last ${turnsSinceWrite} steps of work.`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build the system-prompt fragment.
|
|
114
|
+
*
|
|
115
|
+
* Returns `''` for an empty document so an untouched project contributes
|
|
116
|
+
* nothing at all — the plugin is inert until the agent actually saves
|
|
117
|
+
* something, and an inert plugin is indistinguishable from no plugin.
|
|
118
|
+
*/
|
|
119
|
+
export function buildStateHint(options) {
|
|
120
|
+
const { state, statePath } = options;
|
|
121
|
+
const scope = options.scope ?? '';
|
|
122
|
+
if (Object.keys(state).length === 0)
|
|
123
|
+
return '';
|
|
124
|
+
// `skillstate_merge` is only meaningful to a sub-agent, which is the one
|
|
125
|
+
// session that cannot call it — so it is hidden from every other scope.
|
|
126
|
+
const tools = ADVERTISED_TOOLS.filter((name) => name !== 'skillstate_merge' || scope !== '');
|
|
127
|
+
const toolLine = `\nTools: ${tools.map((name) => `\`${name}\``).join(', ')}.`;
|
|
128
|
+
const mergeLine = scope === ''
|
|
129
|
+
? ''
|
|
130
|
+
: '\nThis is a sub-agent session. When you finish, the main session folds your notes back with `skillstate_merge`; write them as if someone else will read them.';
|
|
131
|
+
const initialized = options.initialized === true;
|
|
132
|
+
const turns = options.turnsSinceWrite ?? 0;
|
|
133
|
+
const declared = options.declaredFields ?? [];
|
|
134
|
+
// Only speak when the state and the spec actually DISAGREE. A line naming
|
|
135
|
+
// the declared fields on every turn of every project would cost prompt budget
|
|
136
|
+
// to say something that is true, and the hint has a standing test that it
|
|
137
|
+
// must stay under a tenth of the conversation it rides along with. The case
|
|
138
|
+
// worth spending characters on is the one measured: a model that wrote
|
|
139
|
+
// everything under a namespace it invented while the declared fields sat at
|
|
140
|
+
// their defaults.
|
|
141
|
+
const undeclared = Object.keys(state).filter((k) => !declared.some((d) => d.startsWith(`${k} `)));
|
|
142
|
+
const schemaLine = declared.length === 0 || undeclared.length === 0
|
|
143
|
+
? ''
|
|
144
|
+
: `\nThis project\'s \`skill-spec.json\` declares the state fields ${declared
|
|
145
|
+
.map((f) => `\`${f}\``)
|
|
146
|
+
.join(', ')}. The state currently holds ${undeclared
|
|
147
|
+
.slice(0, 6)
|
|
148
|
+
.map((k) => `\`${k}\``)
|
|
149
|
+
.join(', ')}, which the spec does not declare. Write declared fields at the TOP LEVEL under their own names; a later reader looking for \`${declared[0].split(' ')[0]}\` will not find it one level down.`;
|
|
150
|
+
const lines = [
|
|
151
|
+
'<skillstate-project-notes>',
|
|
152
|
+
purposeLine(initialized, statePath),
|
|
153
|
+
'',
|
|
154
|
+
renderStateForHint(state),
|
|
155
|
+
`${toolLine}${mergeLine}${schemaLine}`,
|
|
156
|
+
initialized
|
|
157
|
+
? 'Record what you establish here as you go — plans made, decisions taken, file paths, values read, and what is left — so it is still here after a reset. The notes are not the task: keep doing what the user asked.'
|
|
158
|
+
: 'Use them only to carry facts across turns — plans already made, decisions already taken, file paths, and what is left to do. The notes are a side channel, not the task: keep doing what the user asked, and skip these tools entirely when the work needs no cross-turn memory.',
|
|
159
|
+
];
|
|
160
|
+
if (initialized && turns >= DRIFT_NOTICE_AFTER_TURNS) {
|
|
161
|
+
lines.push('', driftNotice(turns));
|
|
162
|
+
}
|
|
163
|
+
lines.push('</skillstate-project-notes>');
|
|
164
|
+
return lines.join('\n');
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=system-hint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-hint.js","sourceRoot":"","sources":["../src/system-hint.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native OpenCode tools for skillstate.
|
|
3
|
+
*
|
|
4
|
+
* These are the NATIVE tools: the fast path when the host is opencode v2.
|
|
5
|
+
* They carry a real JSON Schema and structured output, where the MCP server
|
|
6
|
+
* (`@skillstate/mcp`, still shipped and still registered) can only offer a
|
|
7
|
+
* JSON-RPC round-trip and untyped text. Both read the same state file, so
|
|
8
|
+
* the choice between them is about speed and typing on one side and reach
|
|
9
|
+
* on the other -- never about which one is correct.
|
|
10
|
+
*
|
|
11
|
+
* The MCP server was once the ONLY option: an opencode v1 plugin could not
|
|
12
|
+
* contribute first-class tools at all.
|
|
13
|
+
*
|
|
14
|
+
* Three tools, deliberately:
|
|
15
|
+
*
|
|
16
|
+
* - `skillstate_read` - what this session has already saved.
|
|
17
|
+
* - `skillstate_update` - merge a patch (the only write path).
|
|
18
|
+
* - `skillstate_merge` - fold sub-agent notes back into the root session.
|
|
19
|
+
*
|
|
20
|
+
* A fourth "delete everything" tool is intentionally absent: `null` in a
|
|
21
|
+
* patch already deletes a key, and a tool whose only job is to erase state
|
|
22
|
+
* is a foot-gun with no upside.
|
|
23
|
+
*
|
|
24
|
+
* ── Result shape ──────────────────────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* Every tool returns a DISCRIMINATED result: `{ ok: true, ... }` or
|
|
27
|
+
* `{ ok: false, error }`. This is not a stylistic choice. A tool that
|
|
28
|
+
* declares an `output` schema must return a value matching it, so a failure
|
|
29
|
+
* path that returned only text was rejected by the host with "tool did not
|
|
30
|
+
* return its declared output" — a rejected tool call is worse for the agent
|
|
31
|
+
* than a failed one, because it loses the reason. A validation failure is a
|
|
32
|
+
* normal outcome here, and it is modelled as one.
|
|
33
|
+
*
|
|
34
|
+
* SCOPING is automatic. The current session's scope comes from
|
|
35
|
+
* `ToolContext.sessionID` plus the {@link SessionRegistry}, so the model
|
|
36
|
+
* never passes a session id and can never write another agent's file by
|
|
37
|
+
* guessing one.
|
|
38
|
+
*/
|
|
39
|
+
import type { ToolEditor } from '@opencode/plugin/promise/tool';
|
|
40
|
+
import type { StatePatch, StateSchema } from '@skillstate/core';
|
|
41
|
+
import type { SessionRegistry } from './session-registry.js';
|
|
42
|
+
import { stateScopeFor } from './session-registry.js';
|
|
43
|
+
import type { ProjectStateStore, StateChanges } from './state-store.js';
|
|
44
|
+
/**
|
|
45
|
+
* Largest serialized patch a single `skillstate_update` accepts. The state
|
|
46
|
+
* file is a side channel for a human to read; a model that tries to dump a
|
|
47
|
+
* whole file into it should be told, not silently accommodated.
|
|
48
|
+
*/
|
|
49
|
+
export declare const MAX_PATCH_BYTES: number;
|
|
50
|
+
/** A tool call that succeeded. */
|
|
51
|
+
export interface ToolOk<T> {
|
|
52
|
+
readonly ok: true;
|
|
53
|
+
readonly value: T;
|
|
54
|
+
}
|
|
55
|
+
/** A tool call that was refused. `error` is written for the model to read. */
|
|
56
|
+
export interface ToolError {
|
|
57
|
+
readonly ok: false;
|
|
58
|
+
readonly error: string;
|
|
59
|
+
}
|
|
60
|
+
/** Every tool result is one of these two. */
|
|
61
|
+
export type ToolResult<T> = ToolOk<T> | ToolError;
|
|
62
|
+
/** Payload of a successful `skillstate_read`. */
|
|
63
|
+
export interface ReadValue {
|
|
64
|
+
readonly state: Record<string, unknown>;
|
|
65
|
+
readonly path: string;
|
|
66
|
+
readonly scope: string;
|
|
67
|
+
readonly empty: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** Payload of a successful `skillstate_update`. */
|
|
70
|
+
export interface UpdateValue {
|
|
71
|
+
readonly state: Record<string, unknown>;
|
|
72
|
+
readonly changes: StateChanges;
|
|
73
|
+
readonly path: string;
|
|
74
|
+
readonly scope: string;
|
|
75
|
+
}
|
|
76
|
+
/** Payload of a successful `skillstate_merge`. */
|
|
77
|
+
export interface MergeValue {
|
|
78
|
+
readonly state: Record<string, unknown>;
|
|
79
|
+
readonly changes: StateChanges;
|
|
80
|
+
readonly merged: readonly string[];
|
|
81
|
+
readonly skipped: readonly string[];
|
|
82
|
+
}
|
|
83
|
+
/** Everything the tool definitions need, bound to one plugin instance. */
|
|
84
|
+
export interface ToolDeps {
|
|
85
|
+
readonly store: ProjectStateStore;
|
|
86
|
+
readonly sessions: SessionRegistry;
|
|
87
|
+
/** Resolve a session id to its state scope; `''` is the root session. */
|
|
88
|
+
scopeFor: (sessionID: string) => string;
|
|
89
|
+
/**
|
|
90
|
+
* The project's declared schema, when it SHIPPED one.
|
|
91
|
+
*
|
|
92
|
+
* `undefined` is the normal case and the correct one: §6.2 validates
|
|
93
|
+
* `ΔΣ_t` against `P.schema`, so there is nothing to check a patch against
|
|
94
|
+
* for a project that has not declared a schema, and inventing a default here
|
|
95
|
+
* would reject notes that are perfectly reasonable.
|
|
96
|
+
*
|
|
97
|
+
* It is passed because this tool is the same file the paper's Σ lives in, and
|
|
98
|
+
* §9.3 says "a conforming writer must never emit a `state` containing a key
|
|
99
|
+
* absent from the schema". Measured: a notes-mode run called this tool
|
|
100
|
+
* thirty-one times from inside the host's `execute` sandbox, and the state
|
|
101
|
+
* file ended with a `last` key that the schema does not declare — accepted,
|
|
102
|
+
* `ok: true`, and written by an unvalidated second writer into the same
|
|
103
|
+
* document the paper's runtime owns. Only a spec the project SHIPPED is used,
|
|
104
|
+
* for the same reason `declaredFields` is gated on the source: announcing our
|
|
105
|
+
* own fallback schema as the project's would be a false claim.
|
|
106
|
+
*/
|
|
107
|
+
readonly schema?: StateSchema;
|
|
108
|
+
/**
|
|
109
|
+
* Called after a patch is written, with the scope it was written to.
|
|
110
|
+
*
|
|
111
|
+
* The drift counter resets on it, and the counter is what the system prompt
|
|
112
|
+
* says out loud: "this state file has not changed across the last N steps of
|
|
113
|
+
* work". So it has to be reset by *every* write path, not just the one the
|
|
114
|
+
* paper-mode sink owns.
|
|
115
|
+
*
|
|
116
|
+
* Measured: in notes mode there is no sink, `stateWrites` never increments and
|
|
117
|
+
* `turnsSinceWrite` is never reset — so the notice fires at its threshold and
|
|
118
|
+
* reports silence for a run in which the model wrote the state on every step.
|
|
119
|
+
* The drift measurement that "the notice does not work" rested on was reading
|
|
120
|
+
* a counter that could not see the writes it was counting.
|
|
121
|
+
*/
|
|
122
|
+
readonly onWrite?: (scope: string) => void;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Validate and normalize an incoming patch.
|
|
126
|
+
*
|
|
127
|
+
* Returns either the patch or a human-readable reason it was rejected. The
|
|
128
|
+
* checks protect the file; they do not dictate the model's schema. A state
|
|
129
|
+
* note is free-form by design, so the only hard rules are "must be an
|
|
130
|
+
* object", "must be JSON-serializable", and "must fit".
|
|
131
|
+
*/
|
|
132
|
+
export declare function normalizePatch(raw: unknown): {
|
|
133
|
+
ok: true;
|
|
134
|
+
patch: StatePatch;
|
|
135
|
+
} | {
|
|
136
|
+
ok: false;
|
|
137
|
+
reason: string;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Register the skillstate tools on an editor.
|
|
141
|
+
*
|
|
142
|
+
* The callback is synchronous, side-effect-free and replayable: OpenCode
|
|
143
|
+
* re-runs it on every registry rebuild, so it must not do I/O at
|
|
144
|
+
* registration time. All filesystem work happens inside `execute`.
|
|
145
|
+
*/
|
|
146
|
+
export declare function registerTools(editor: ToolEditor, deps: ToolDeps): void;
|
|
147
|
+
export { stateScopeFor };
|
|
148
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAe,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE7E,OAAO,KAAK,EAAc,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAExE;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAY,CAAC;AAEzC,kCAAkC;AAClC,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CACnB;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,6CAA6C;AAC7C,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;AAElD,iDAAiD;AACjD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,mDAAmD;AACnD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,kDAAkD;AAClD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,0EAA0E;AAC1E,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,yEAAyE;IACzE,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AA0JD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,GACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAoCjE;AAwBD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAqJtE;AAOD,OAAO,EAAE,aAAa,EAAE,CAAC"}
|