@ggui-ai/protocol 0.1.0-rc.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/LICENSE +201 -0
- package/README.md +46 -0
- package/dist/bridge/invoke-agent.d.ts +65 -0
- package/dist/bridge/invoke-agent.d.ts.map +1 -0
- package/dist/bridge/invoke-agent.js +113 -0
- package/dist/envelope-adapters.d.ts +24 -0
- package/dist/envelope-adapters.d.ts.map +1 -0
- package/dist/envelope-adapters.js +14 -0
- package/dist/envelopes/builders.d.ts +145 -0
- package/dist/envelopes/builders.d.ts.map +1 -0
- package/dist/envelopes/builders.js +113 -0
- package/dist/errors/unknown-permission-name.d.ts +12 -0
- package/dist/errors/unknown-permission-name.d.ts.map +1 -0
- package/dist/errors/unknown-permission-name.js +29 -0
- package/dist/errors/version-mismatch.d.ts +55 -0
- package/dist/errors/version-mismatch.d.ts.map +1 -0
- package/dist/errors/version-mismatch.js +52 -0
- package/dist/gadgets/resolve-contract-gadgets.d.ts +93 -0
- package/dist/gadgets/resolve-contract-gadgets.d.ts.map +1 -0
- package/dist/gadgets/resolve-contract-gadgets.js +119 -0
- package/dist/gadgets/stdlib-gadgets.d.ts +43 -0
- package/dist/gadgets/stdlib-gadgets.d.ts.map +1 -0
- package/dist/gadgets/stdlib-gadgets.js +161 -0
- package/dist/iframe-bridge.d.ts +63 -0
- package/dist/iframe-bridge.d.ts.map +1 -0
- package/dist/iframe-bridge.js +166 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/integrations/mcp-apps.d.ts +1218 -0
- package/dist/integrations/mcp-apps.d.ts.map +1 -0
- package/dist/integrations/mcp-apps.js +427 -0
- package/dist/navigation/index.d.ts +3 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +1 -0
- package/dist/navigation/stack-navigation.d.ts +55 -0
- package/dist/navigation/stack-navigation.d.ts.map +1 -0
- package/dist/navigation/stack-navigation.js +80 -0
- package/dist/recommended-prompts.d.ts +56 -0
- package/dist/recommended-prompts.d.ts.map +1 -0
- package/dist/recommended-prompts.js +55 -0
- package/dist/registry/blueprint-key.d.ts +9 -0
- package/dist/registry/blueprint-key.d.ts.map +1 -0
- package/dist/registry/blueprint-key.js +28 -0
- package/dist/registry/canonicalize-contract.d.ts +35 -0
- package/dist/registry/canonicalize-contract.d.ts.map +1 -0
- package/dist/registry/canonicalize-contract.js +166 -0
- package/dist/registry/summarize-contract.d.ts +46 -0
- package/dist/registry/summarize-contract.d.ts.map +1 -0
- package/dist/registry/summarize-contract.js +63 -0
- package/dist/schema-learning/derive-contract.d.ts +67 -0
- package/dist/schema-learning/derive-contract.d.ts.map +1 -0
- package/dist/schema-learning/derive-contract.js +117 -0
- package/dist/schema-learning/merge.d.ts +32 -0
- package/dist/schema-learning/merge.d.ts.map +1 -0
- package/dist/schema-learning/merge.js +146 -0
- package/dist/schemas/blueprint.d.ts +32 -0
- package/dist/schemas/blueprint.d.ts.map +1 -0
- package/dist/schemas/blueprint.js +92 -0
- package/dist/schemas/data-contract.d.ts +750 -0
- package/dist/schemas/data-contract.d.ts.map +1 -0
- package/dist/schemas/data-contract.js +663 -0
- package/dist/schemas/gadget-name-grammar.d.ts +29 -0
- package/dist/schemas/gadget-name-grammar.d.ts.map +1 -0
- package/dist/schemas/gadget-name-grammar.js +28 -0
- package/dist/schemas/handshake-suggestion.d.ts +46 -0
- package/dist/schemas/handshake-suggestion.d.ts.map +1 -0
- package/dist/schemas/handshake-suggestion.js +107 -0
- package/dist/schemas/invoke.d.ts +337 -0
- package/dist/schemas/invoke.d.ts.map +1 -0
- package/dist/schemas/invoke.js +169 -0
- package/dist/schemas/mcp.d.ts +301 -0
- package/dist/schemas/mcp.d.ts.map +1 -0
- package/dist/schemas/mcp.js +373 -0
- package/dist/schemas/ops-blueprint.d.ts +176 -0
- package/dist/schemas/ops-blueprint.d.ts.map +1 -0
- package/dist/schemas/ops-blueprint.js +259 -0
- package/dist/schemas/sync-check.d.ts +11 -0
- package/dist/schemas/sync-check.d.ts.map +1 -0
- package/dist/schemas/sync-check.js +60 -0
- package/dist/screen-blueprints/define.d.ts +22 -0
- package/dist/screen-blueprints/define.d.ts.map +1 -0
- package/dist/screen-blueprints/define.js +3 -0
- package/dist/screen-blueprints/index.d.ts +4 -0
- package/dist/screen-blueprints/index.d.ts.map +1 -0
- package/dist/screen-blueprints/index.js +3 -0
- package/dist/screen-blueprints/match.d.ts +35 -0
- package/dist/screen-blueprints/match.d.ts.map +1 -0
- package/dist/screen-blueprints/match.js +51 -0
- package/dist/screen-blueprints/types.d.ts +164 -0
- package/dist/screen-blueprints/types.d.ts.map +1 -0
- package/dist/screen-blueprints/types.js +1 -0
- package/dist/stream/stream-parser.d.ts +62 -0
- package/dist/stream/stream-parser.d.ts.map +1 -0
- package/dist/stream/stream-parser.js +199 -0
- package/dist/transport/websocket.d.ts +178 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +1 -0
- package/dist/types/app-config.d.ts +61 -0
- package/dist/types/app-config.d.ts.map +1 -0
- package/dist/types/app-config.js +1 -0
- package/dist/types/auth.d.ts +61 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +1 -0
- package/dist/types/blueprint.d.ts +206 -0
- package/dist/types/blueprint.d.ts.map +1 -0
- package/dist/types/blueprint.js +1 -0
- package/dist/types/canvas-lifecycle.d.ts +105 -0
- package/dist/types/canvas-lifecycle.d.ts.map +1 -0
- package/dist/types/canvas-lifecycle.js +38 -0
- package/dist/types/capabilities.d.ts +40 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +19 -0
- package/dist/types/contract-inference.d.ts +401 -0
- package/dist/types/contract-inference.d.ts.map +1 -0
- package/dist/types/contract-inference.js +44 -0
- package/dist/types/credential.d.ts +41 -0
- package/dist/types/credential.d.ts.map +1 -0
- package/dist/types/credential.js +32 -0
- package/dist/types/data-bindings.d.ts +322 -0
- package/dist/types/data-bindings.d.ts.map +1 -0
- package/dist/types/data-bindings.js +29 -0
- package/dist/types/data-contract.d.ts +1296 -0
- package/dist/types/data-contract.d.ts.map +1 -0
- package/dist/types/data-contract.js +111 -0
- package/dist/types/events.d.ts +182 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +8 -0
- package/dist/types/feedback.d.ts +24 -0
- package/dist/types/feedback.d.ts.map +1 -0
- package/dist/types/feedback.js +7 -0
- package/dist/types/gadget.d.ts +121 -0
- package/dist/types/gadget.d.ts.map +1 -0
- package/dist/types/gadget.js +24 -0
- package/dist/types/handshake-suggestion.d.ts +264 -0
- package/dist/types/handshake-suggestion.d.ts.map +1 -0
- package/dist/types/handshake-suggestion.js +70 -0
- package/dist/types/host-context.d.ts +163 -0
- package/dist/types/host-context.d.ts.map +1 -0
- package/dist/types/host-context.js +142 -0
- package/dist/types/interface-context.d.ts +105 -0
- package/dist/types/interface-context.d.ts.map +1 -0
- package/dist/types/interface-context.js +115 -0
- package/dist/types/invoke.d.ts +28 -0
- package/dist/types/invoke.d.ts.map +1 -0
- package/dist/types/invoke.js +7 -0
- package/dist/types/live-channel.d.ts +613 -0
- package/dist/types/live-channel.d.ts.map +1 -0
- package/dist/types/live-channel.js +1 -0
- package/dist/types/llm.d.ts +61 -0
- package/dist/types/llm.d.ts.map +1 -0
- package/dist/types/llm.js +186 -0
- package/dist/types/mcp-proxy.d.ts +67 -0
- package/dist/types/mcp-proxy.d.ts.map +1 -0
- package/dist/types/mcp-proxy.js +46 -0
- package/dist/types/mcp.d.ts +637 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +30 -0
- package/dist/types/openrouter-models.d.ts +22 -0
- package/dist/types/openrouter-models.d.ts.map +1 -0
- package/dist/types/openrouter-models.js +4843 -0
- package/dist/types/region.d.ts +26 -0
- package/dist/types/region.d.ts.map +1 -0
- package/dist/types/region.js +36 -0
- package/dist/types/session.d.ts +419 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +1 -0
- package/dist/types/thread.d.ts +207 -0
- package/dist/types/thread.d.ts.map +1 -0
- package/dist/types/thread.js +57 -0
- package/dist/types/ui-generator.d.ts +100 -0
- package/dist/types/ui-generator.d.ts.map +1 -0
- package/dist/types/ui-generator.js +53 -0
- package/dist/validation/ajv-runtime.d.ts +140 -0
- package/dist/validation/ajv-runtime.d.ts.map +1 -0
- package/dist/validation/ajv-runtime.js +452 -0
- package/dist/validation/content-hash.d.ts +3 -0
- package/dist/validation/content-hash.d.ts.map +1 -0
- package/dist/validation/content-hash.js +21 -0
- package/dist/validation/contract-validator.d.ts +244 -0
- package/dist/validation/contract-validator.d.ts.map +1 -0
- package/dist/validation/contract-validator.js +711 -0
- package/dist/validation/cross-references.d.ts +105 -0
- package/dist/validation/cross-references.d.ts.map +1 -0
- package/dist/validation/cross-references.js +164 -0
- package/dist/validation/hygiene-rules.d.ts +250 -0
- package/dist/validation/hygiene-rules.d.ts.map +1 -0
- package/dist/validation/hygiene-rules.js +564 -0
- package/dist/validation/lint-contract.d.ts +130 -0
- package/dist/validation/lint-contract.d.ts.map +1 -0
- package/dist/validation/lint-contract.js +225 -0
- package/dist/validation/name-invariants.d.ts +117 -0
- package/dist/validation/name-invariants.d.ts.map +1 -0
- package/dist/validation/name-invariants.js +172 -0
- package/dist/validation/reserved-channels.d.ts +156 -0
- package/dist/validation/reserved-channels.d.ts.map +1 -0
- package/dist/validation/reserved-channels.js +356 -0
- package/dist/validation/resolve-stream-channel.d.ts +78 -0
- package/dist/validation/resolve-stream-channel.d.ts.map +1 -0
- package/dist/validation/resolve-stream-channel.js +64 -0
- package/dist/validation/sanitize-error.d.ts +46 -0
- package/dist/validation/sanitize-error.d.ts.map +1 -0
- package/dist/validation/sanitize-error.js +88 -0
- package/dist/validation/schema-compat-invariants.d.ts +140 -0
- package/dist/validation/schema-compat-invariants.d.ts.map +1 -0
- package/dist/validation/schema-compat-invariants.js +220 -0
- package/dist/validation/schema-meta-validation.d.ts +60 -0
- package/dist/validation/schema-meta-validation.d.ts.map +1 -0
- package/dist/validation/schema-meta-validation.js +131 -0
- package/dist/validation/schema-subset.d.ts +165 -0
- package/dist/validation/schema-subset.d.ts.map +1 -0
- package/dist/validation/schema-subset.js +295 -0
- package/dist/validation/ui-security.d.ts +54 -0
- package/dist/validation/ui-security.d.ts.map +1 -0
- package/dist/validation/ui-security.js +138 -0
- package/dist/validation/zod-to-json-schema.d.ts +63 -0
- package/dist/validation/zod-to-json-schema.d.ts.map +1 -0
- package/dist/validation/zod-to-json-schema.js +126 -0
- package/dist/version.d.ts +1458 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +1459 -0
- package/package.json +113 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authoring-hygiene rules for `DataContract`. Lint rule registry —
|
|
3
|
+
* warnings only, never thrown by {@link validateContract}; surfaced
|
|
4
|
+
* by {@link lintContract} so authoring tools can offer "your contract
|
|
5
|
+
* is technically valid, but here are the polish items" feedback.
|
|
6
|
+
*
|
|
7
|
+
* Ships the most universally applicable subset:
|
|
8
|
+
*
|
|
9
|
+
* - `LINT_ORPHAN_AGENT_TOOL` — `agentCapabilities.tools[X]` is
|
|
10
|
+
* declared but never referenced from any `actionSpec[*].nextStep`
|
|
11
|
+
* or `streamSpec[*].source.tool`. The entry is dead weight — either
|
|
12
|
+
* wire it up or drop it. (Catches the common "agent dropped a
|
|
13
|
+
* reference but left the catalog entry behind" drift.)
|
|
14
|
+
*
|
|
15
|
+
* - `LINT_MISSING_USAGE` — `agentCapabilities.tools[*]` or
|
|
16
|
+
* `clientCapabilities.gadgets[*]` lacks a `usage` field.
|
|
17
|
+
* `usage` is the free-form LLM-targeted prose that bare
|
|
18
|
+
* `description` lacks — when omitted, the agent's reasoning
|
|
19
|
+
* loop loses important context-of-use information.
|
|
20
|
+
*
|
|
21
|
+
* - `LINT_MISSING_EXAMPLE` — `agentCapabilities.tools[*]` lacks an
|
|
22
|
+
* `example`. Examples ground the agent's invocation patterns; a
|
|
23
|
+
* tool without one is harder to use correctly on the first call.
|
|
24
|
+
*
|
|
25
|
+
* - `LINT_GADGET_DUPLICATE_EXPORT` — two `clientCapabilities.gadgets[*]`
|
|
26
|
+
* entries declare the same export name (a `hook` name or a
|
|
27
|
+
* `component` name). The boilerplate generator emits one import
|
|
28
|
+
* per export name; a collision is unresolvable in module scope.
|
|
29
|
+
* Keyed on the export name alone.
|
|
30
|
+
*
|
|
31
|
+
* Gadget lints split by timing into two surfaces:
|
|
32
|
+
*
|
|
33
|
+
* - **Wire-side** ({@link checkHygiene}, input `DataContract`):
|
|
34
|
+
* `checkGadgetHookNames` + `checkDuplicateGadgetHooks`.
|
|
35
|
+
* - **Registry-side** ({@link lintGadgetCatalog}, input
|
|
36
|
+
* `readonly GadgetDescriptor[]`): permission + immutability +
|
|
37
|
+
* duplicate-hook + unscoped-package checks, run at registration
|
|
38
|
+
* time. Some codes are fatal — see {@link FATAL_CATALOG_LINT_CODES}.
|
|
39
|
+
*
|
|
40
|
+
* Pure checks; return violations rather than throwing. The wire-side
|
|
41
|
+
* set is wired into `lintContract` via `phaseHygiene`; consumers that
|
|
42
|
+
* want strict gates layer their own assertions on top.
|
|
43
|
+
*/
|
|
44
|
+
import { STDLIB_GADGETS_PACKAGE, STDLIB_GADGET_HOOKS, } from '../gadgets/stdlib-gadgets.js';
|
|
45
|
+
import { gadgetExportName, listContractGadgets, } from '../gadgets/resolve-contract-gadgets.js';
|
|
46
|
+
import { HOOK_NAME_RE } from '../schemas/gadget-name-grammar.js';
|
|
47
|
+
/**
|
|
48
|
+
* Stable codes for hygiene rules. Each is a `LINT_*` rather than a
|
|
49
|
+
* `CTR_*` — the convention: errors are `CTR_*`, warnings are `LINT_*`.
|
|
50
|
+
*
|
|
51
|
+
* The gadget lints split into two timing buckets:
|
|
52
|
+
*
|
|
53
|
+
* - **Wire-side** (run on a `DataContract` by {@link checkHygiene}):
|
|
54
|
+
* `LINT_GADGET_UNKNOWN_HOOK`, `LINT_GADGET_DUPLICATE_EXPORT`.
|
|
55
|
+
* - **Registry-side** (run on an `App.gadgets` catalog at
|
|
56
|
+
* registration time by {@link lintGadgetCatalog}):
|
|
57
|
+
* `LINT_GADGET_MISSING_PERMISSION`, `LINT_GADGET_UNKNOWN_PERMISSION`,
|
|
58
|
+
* `LINT_GADGET_UNSCOPED_PACKAGE`, `LINT_GADGET_IMMUTABLE_MUTATION`,
|
|
59
|
+
* `LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG`.
|
|
60
|
+
*/
|
|
61
|
+
export const LINT_ORPHAN_AGENT_TOOL = 'LINT_ORPHAN_AGENT_TOOL';
|
|
62
|
+
export const LINT_MISSING_USAGE = 'LINT_MISSING_USAGE';
|
|
63
|
+
export const LINT_MISSING_EXAMPLE = 'LINT_MISSING_EXAMPLE';
|
|
64
|
+
export const LINT_GADGET_UNKNOWN_HOOK = 'LINT_GADGET_UNKNOWN_HOOK';
|
|
65
|
+
export const LINT_GADGET_DUPLICATE_EXPORT = 'LINT_GADGET_DUPLICATE_EXPORT';
|
|
66
|
+
export const LINT_CONTRACT_RETIRED_FIELD = 'LINT_CONTRACT_RETIRED_FIELD';
|
|
67
|
+
// ── Registry-side (lintGadgetCatalog) ──
|
|
68
|
+
export const LINT_GADGET_MISSING_PERMISSION = 'LINT_GADGET_MISSING_PERMISSION';
|
|
69
|
+
export const LINT_GADGET_UNKNOWN_PERMISSION = 'LINT_GADGET_UNKNOWN_PERMISSION';
|
|
70
|
+
export const LINT_GADGET_UNSCOPED_PACKAGE = 'LINT_GADGET_UNSCOPED_PACKAGE';
|
|
71
|
+
export const LINT_GADGET_IMMUTABLE_MUTATION = 'LINT_GADGET_IMMUTABLE_MUTATION';
|
|
72
|
+
export const LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG = 'LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG';
|
|
73
|
+
export const LINT_GADGET_DUPLICATE_PACKAGE = 'LINT_GADGET_DUPLICATE_PACKAGE';
|
|
74
|
+
/**
|
|
75
|
+
* Registry-side lint codes that denote a HARD integrity violation —
|
|
76
|
+
* registration handlers MUST reject the catalog (not just warn) when
|
|
77
|
+
* {@link lintGadgetCatalog} emits one of these. The lint function
|
|
78
|
+
* itself stays pure (returns warnings); severity classification is
|
|
79
|
+
* the caller's, so this set is the single source of truth for "which
|
|
80
|
+
* codes are fatal."
|
|
81
|
+
*
|
|
82
|
+
* - `LINT_GADGET_IMMUTABLE_MUTATION` — two descriptors share a
|
|
83
|
+
* `(package, version)` tuple but disagree on `bundleSri`. The
|
|
84
|
+
* same immutable bundle cannot have two hashes; cached blueprints
|
|
85
|
+
* keyed on that version would silently break.
|
|
86
|
+
* - `LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG` — two descriptors
|
|
87
|
+
* export the same name (a `hook` name or a `component` name). The
|
|
88
|
+
* boilerplate generator emits one
|
|
89
|
+
* `import { <name> } from '<package>'` per export; a name
|
|
90
|
+
* collision in module scope is unresolvable.
|
|
91
|
+
*/
|
|
92
|
+
export const FATAL_CATALOG_LINT_CODES = new Set([
|
|
93
|
+
LINT_GADGET_IMMUTABLE_MUTATION,
|
|
94
|
+
LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG,
|
|
95
|
+
LINT_GADGET_DUPLICATE_PACKAGE,
|
|
96
|
+
]);
|
|
97
|
+
/**
|
|
98
|
+
* Retired top-level `DataContract` field names. The contract schema
|
|
99
|
+
* is `.passthrough()` at the type system level (forward-compat
|
|
100
|
+
* hedge), but these specific names denote fields that have a known
|
|
101
|
+
* replacement in the current protocol. Carrying one of them is a
|
|
102
|
+
* caller bug — silent pass-through would mask the migration.
|
|
103
|
+
*
|
|
104
|
+
* Replacements (kept here so the lint message can teach the fix):
|
|
105
|
+
* - `libraries` → `clientCapabilities.gadgets`
|
|
106
|
+
* - `dispatch` → `agentCapabilities.tools` + `actionSpec[*].nextStep`
|
|
107
|
+
* - `wiredTools` → `agentCapabilities.tools`
|
|
108
|
+
* - `clientTools` → `clientCapabilities.gadgets`
|
|
109
|
+
* - `broadcast` → `streamSpec[ch].source`
|
|
110
|
+
* - `capabilities` → `agentCapabilities` + `clientCapabilities`
|
|
111
|
+
*
|
|
112
|
+
* Push-gate handlers re-use this list to hard-reject; surfacing it
|
|
113
|
+
* here keeps the wire vocabulary single-sourced.
|
|
114
|
+
*/
|
|
115
|
+
export const RETIRED_CONTRACT_FIELDS = {
|
|
116
|
+
libraries: 'clientCapabilities.gadgets',
|
|
117
|
+
dispatch: 'agentCapabilities.tools + actionSpec[*].nextStep',
|
|
118
|
+
wiredTools: 'agentCapabilities.tools',
|
|
119
|
+
clientTools: 'clientCapabilities.gadgets',
|
|
120
|
+
broadcast: 'streamSpec[ch].source',
|
|
121
|
+
capabilities: 'agentCapabilities + clientCapabilities',
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Sourced from {@link STDLIB_GADGET_HOOKS} — the canonical
|
|
125
|
+
* hook-name set the first-party `@ggui-ai/gadgets` package
|
|
126
|
+
* exports. Local alias keeps existing call sites stable while the
|
|
127
|
+
* source of truth lives in `registries/stdlib-gadgets.ts`.
|
|
128
|
+
*/
|
|
129
|
+
const KNOWN_STDLIB_HOOKS = STDLIB_GADGET_HOOKS;
|
|
130
|
+
/**
|
|
131
|
+
* Default package for gadget hooks. Sourced from the
|
|
132
|
+
* stdlib-libraries registry — when an entry's `package` is omitted,
|
|
133
|
+
* the hygiene linter assumes it resolves to the first-party
|
|
134
|
+
* `@ggui-ai/gadgets` package and runs hook-registry +
|
|
135
|
+
* permission checks against the stdlib catalog. Third-party packages
|
|
136
|
+
* skip those checks.
|
|
137
|
+
*/
|
|
138
|
+
const DEFAULT_GADGET_PACKAGE = STDLIB_GADGETS_PACKAGE;
|
|
139
|
+
/**
|
|
140
|
+
* Permission strings the Web Permissions API ratifies, plus the
|
|
141
|
+
* MCP Apps `_meta.ui.permissions` enum members for host
|
|
142
|
+
* passthrough.
|
|
143
|
+
*
|
|
144
|
+
* Exported as a tuple + literal-union type so
|
|
145
|
+
* `strictGadgetDescriptorSchema.permission` can use
|
|
146
|
+
* `z.enum(KNOWN_PERMISSION_NAMES)` for a hard reject at parse time:
|
|
147
|
+
* typos (`'geolocaiton'`) and unsupported values fail at the wire
|
|
148
|
+
* boundary instead of being demoted to a soft warning. Forward-compat
|
|
149
|
+
* additions land via a protocol version bump.
|
|
150
|
+
*/
|
|
151
|
+
export const KNOWN_PERMISSION_NAMES = [
|
|
152
|
+
// Web Permissions API names
|
|
153
|
+
'geolocation',
|
|
154
|
+
'notifications',
|
|
155
|
+
'microphone',
|
|
156
|
+
'camera',
|
|
157
|
+
'persistent-storage',
|
|
158
|
+
'midi',
|
|
159
|
+
'clipboard-read',
|
|
160
|
+
'clipboard-write',
|
|
161
|
+
'speaker-selection',
|
|
162
|
+
'storage-access',
|
|
163
|
+
'background-sync',
|
|
164
|
+
'accelerometer',
|
|
165
|
+
'gyroscope',
|
|
166
|
+
'magnetometer',
|
|
167
|
+
'ambient-light-sensor',
|
|
168
|
+
'screen-wake-lock',
|
|
169
|
+
// MCP Apps `_meta.ui.permissions` mirror (matches the Web Permissions
|
|
170
|
+
// API names today; future spec additions go here).
|
|
171
|
+
];
|
|
172
|
+
const KNOWN_PERMISSION_NAMES_SET = new Set(KNOWN_PERMISSION_NAMES);
|
|
173
|
+
/**
|
|
174
|
+
* Stdlib hooks whose Web Permissions API name is well-known. A
|
|
175
|
+
* registered descriptor for one of these hooks SHOULD declare the
|
|
176
|
+
* matching `permission` so {@link lintGadgetCatalog} can surface the
|
|
177
|
+
* "this UI will prompt for X" context to the agent's reasoning loop.
|
|
178
|
+
* Maps hook name → expected permission.
|
|
179
|
+
*/
|
|
180
|
+
const KNOWN_PERMISSION_HOOKS = {
|
|
181
|
+
useGeolocation: 'geolocation',
|
|
182
|
+
useNotifications: 'notifications',
|
|
183
|
+
useMicrophone: 'microphone',
|
|
184
|
+
useCamera: 'camera',
|
|
185
|
+
useClipboardPaste: 'clipboard-read',
|
|
186
|
+
useClipboardWrite: 'clipboard-write',
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Collect every `agentCapabilities.tools[*]` key that's referenced from
|
|
190
|
+
* `actionSpec[*].nextStep` or `streamSpec[*].source.tool`. Used to
|
|
191
|
+
* detect orphans (declared but unreferenced) and is exported because
|
|
192
|
+
* future rules / tooling may want the same accounting.
|
|
193
|
+
*/
|
|
194
|
+
function collectReferencedAgentTools(contract) {
|
|
195
|
+
const referenced = new Set();
|
|
196
|
+
const actionSpec = contract.actionSpec ?? {};
|
|
197
|
+
for (const entry of Object.values(actionSpec)) {
|
|
198
|
+
if (!entry || typeof entry !== 'object')
|
|
199
|
+
continue;
|
|
200
|
+
const nextStep = entry.nextStep;
|
|
201
|
+
if (typeof nextStep === 'string' && nextStep.length > 0) {
|
|
202
|
+
referenced.add(nextStep);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const streamSpec = contract.streamSpec ?? {};
|
|
206
|
+
for (const entry of Object.values(streamSpec)) {
|
|
207
|
+
if (!entry || typeof entry !== 'object')
|
|
208
|
+
continue;
|
|
209
|
+
const source = entry.source;
|
|
210
|
+
if (!source)
|
|
211
|
+
continue;
|
|
212
|
+
const tool = source.tool;
|
|
213
|
+
if (typeof tool === 'string' && tool.length > 0) {
|
|
214
|
+
referenced.add(tool);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return referenced;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Find agentCapabilities.tools entries that are declared but never
|
|
221
|
+
* referenced from actionSpec or streamSpec. Each orphan is dead
|
|
222
|
+
* weight — either wire it up or drop it from the catalog.
|
|
223
|
+
*/
|
|
224
|
+
export function checkOrphanAgentTools(contract) {
|
|
225
|
+
const tools = contract.agentCapabilities?.tools;
|
|
226
|
+
if (!tools)
|
|
227
|
+
return [];
|
|
228
|
+
const referenced = collectReferencedAgentTools(contract);
|
|
229
|
+
const warnings = [];
|
|
230
|
+
for (const name of Object.keys(tools)) {
|
|
231
|
+
if (referenced.has(name))
|
|
232
|
+
continue;
|
|
233
|
+
warnings.push({
|
|
234
|
+
code: LINT_ORPHAN_AGENT_TOOL,
|
|
235
|
+
path: `agentCapabilities.tools.${name}`,
|
|
236
|
+
message: `agentCapabilities.tools.${name} is declared but never referenced from actionSpec[*].nextStep or streamSpec[*].source.tool. Dead-weight catalog entry — either wire it up or remove it.`,
|
|
237
|
+
fixHint: `Add a reference like 'actionSpec.<action>.nextStep = "${name}"' or 'streamSpec.<channel>.source.tool = "${name}"', or delete the catalog entry.`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return warnings;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Find `agentCapabilities.tools` entries missing the `usage` field.
|
|
244
|
+
* `usage` is the LLM-targeted "when / why / by-whom" prose; without it
|
|
245
|
+
* the agent's reasoning loop loses context-of-use information.
|
|
246
|
+
*
|
|
247
|
+
* Scope is `agentCapabilities.tools` ONLY. `clientCapabilities.gadgets`
|
|
248
|
+
* is intentionally NOT linted here: `GadgetExportUse.usage` is an
|
|
249
|
+
* OPTIONAL intent-OVERRIDE, and the SPEC-documented canonical wire
|
|
250
|
+
* form is the bare identity reference `gadgets[<pkg>][<export>] = {}`.
|
|
251
|
+
* Push-time resolution inherits the registered descriptor's `usage`,
|
|
252
|
+
* and the registry-side `lintGadgetCatalog` (via
|
|
253
|
+
* `strictGadgetExportSchema`) already enforces real teaching text at
|
|
254
|
+
* registration time. Flagging an empty wire-side use object would
|
|
255
|
+
* false-positive the documented happy path.
|
|
256
|
+
*/
|
|
257
|
+
export function checkMissingUsage(contract) {
|
|
258
|
+
const warnings = [];
|
|
259
|
+
const tools = contract.agentCapabilities?.tools;
|
|
260
|
+
if (tools) {
|
|
261
|
+
for (const [name, entry] of Object.entries(tools)) {
|
|
262
|
+
if (!entry || typeof entry !== 'object')
|
|
263
|
+
continue;
|
|
264
|
+
const usage = entry.usage;
|
|
265
|
+
if (typeof usage === 'string' && usage.length > 0)
|
|
266
|
+
continue;
|
|
267
|
+
warnings.push({
|
|
268
|
+
code: LINT_MISSING_USAGE,
|
|
269
|
+
path: `agentCapabilities.tools.${name}.usage`,
|
|
270
|
+
message: `agentCapabilities.tools.${name} has no 'usage' prose. The agent's reasoning loop reads usage as context-of-use; tools without it tend to get invoked at the wrong time.`,
|
|
271
|
+
fixHint: `Add 'usage: "..."' describing when / why / by whom this tool is invoked.`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return warnings;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Find agentCapabilities.tools entries missing the `example` field.
|
|
279
|
+
* Examples ground the agent's invocation patterns; a tool without
|
|
280
|
+
* one is harder to use correctly on the first call.
|
|
281
|
+
*/
|
|
282
|
+
export function checkMissingExample(contract) {
|
|
283
|
+
const tools = contract.agentCapabilities?.tools;
|
|
284
|
+
if (!tools)
|
|
285
|
+
return [];
|
|
286
|
+
const warnings = [];
|
|
287
|
+
for (const [name, entry] of Object.entries(tools)) {
|
|
288
|
+
if (!entry || typeof entry !== 'object')
|
|
289
|
+
continue;
|
|
290
|
+
if (entry.example !== undefined)
|
|
291
|
+
continue;
|
|
292
|
+
warnings.push({
|
|
293
|
+
code: LINT_MISSING_EXAMPLE,
|
|
294
|
+
path: `agentCapabilities.tools.${name}.example`,
|
|
295
|
+
message: `agentCapabilities.tools.${name} has no 'example'. Examples ground the agent's invocation patterns; tools without one are harder to use correctly on the first call.`,
|
|
296
|
+
fixHint: `Add 'example: { input: {...}, output: ... }' with a representative call shape.`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return warnings;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Wire-side gadget hook-name lint. For every
|
|
303
|
+
* `clientCapabilities.gadgets[*]` whose `package` is the first-party
|
|
304
|
+
* stdlib (`@ggui-ai/gadgets`), the `hook` MUST be one the stdlib
|
|
305
|
+
* actually exports — catches typos (`useGeoLocation`) + stale
|
|
306
|
+
* references against a constant catalog.
|
|
307
|
+
*
|
|
308
|
+
* Third-party packages (any `package !== DEFAULT_GADGET_PACKAGE`) are
|
|
309
|
+
* NOT checked here — the lint can't know an operator's own hook
|
|
310
|
+
* names. The registry-side {@link lintGadgetCatalog} + the push-time
|
|
311
|
+
* {@link assertGadgetsRegistered} gate cover third-party resolution.
|
|
312
|
+
*
|
|
313
|
+
* Permission checks live on the registry-side `lintGadgetCatalog`
|
|
314
|
+
* (the wire gadget reference carries no `permission` field). This
|
|
315
|
+
* function is the pure wire-only residue: a constant-catalog
|
|
316
|
+
* hook-name check.
|
|
317
|
+
*/
|
|
318
|
+
export function checkGadgetHookNames(contract) {
|
|
319
|
+
const warnings = [];
|
|
320
|
+
for (const gadget of listContractGadgets(contract)) {
|
|
321
|
+
if (gadget.package !== DEFAULT_GADGET_PACKAGE)
|
|
322
|
+
continue;
|
|
323
|
+
const path = `clientCapabilities.gadgets.${gadget.package}.exports.${gadget.name}`;
|
|
324
|
+
// The first-party stdlib ships hooks only — a component export
|
|
325
|
+
// pinning `@ggui-ai/gadgets` is a mistake. Kind is read off the
|
|
326
|
+
// export-name grammar (`use`-prefixed hook vs PascalCase component).
|
|
327
|
+
if (!HOOK_NAME_RE.test(gadget.name)) {
|
|
328
|
+
warnings.push({
|
|
329
|
+
code: LINT_GADGET_UNKNOWN_HOOK,
|
|
330
|
+
path,
|
|
331
|
+
message: `clientCapabilities.gadgets declares a component '${gadget.name}' from '${gadget.package}', but the first-party stdlib ships hooks only.`,
|
|
332
|
+
fixHint: `Use a hook from the v1 catalog, or reference a third-party gadget package that exports the component.`,
|
|
333
|
+
});
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (!KNOWN_STDLIB_HOOKS.has(gadget.name)) {
|
|
337
|
+
warnings.push({
|
|
338
|
+
code: LINT_GADGET_UNKNOWN_HOOK,
|
|
339
|
+
path,
|
|
340
|
+
message: `clientCapabilities.gadgets references hook '${gadget.name}' from '${gadget.package}', which doesn't ship that hook. Known: ${[
|
|
341
|
+
...KNOWN_STDLIB_HOOKS,
|
|
342
|
+
].join(', ')}.`,
|
|
343
|
+
fixHint: `Pick a hook from the v1 catalog, or reference a third-party hook package the lint doesn't know about.`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return warnings;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Find `clientCapabilities.gadgets` exports that declare the same
|
|
351
|
+
* export NAME from two different packages.
|
|
352
|
+
*
|
|
353
|
+
* The wire is package-keyed, so the same name cannot repeat WITHIN a
|
|
354
|
+
* package (object-key uniqueness). The hazard is cross-package: two
|
|
355
|
+
* packages each exporting `useCheckout`. The boilerplate generator
|
|
356
|
+
* emits one `import { <name> } from '<package>'` per export; two
|
|
357
|
+
* imports of the same name — from different packages — produce an
|
|
358
|
+
* unresolvable identifier collision in the generated module scope.
|
|
359
|
+
*
|
|
360
|
+
* Keys on the export name alone, matching the push-time hard gate
|
|
361
|
+
* `assertNoDuplicateGadgetHooks`. Soft mirror of that gate so
|
|
362
|
+
* authoring tools surface the issue before a push round-trip.
|
|
363
|
+
*/
|
|
364
|
+
export function checkDuplicateGadgetHooks(contract) {
|
|
365
|
+
const seen = new Map(); // export name → first package
|
|
366
|
+
const warnings = [];
|
|
367
|
+
for (const gadget of listContractGadgets(contract)) {
|
|
368
|
+
const prior = seen.get(gadget.name);
|
|
369
|
+
if (prior !== undefined) {
|
|
370
|
+
warnings.push({
|
|
371
|
+
code: LINT_GADGET_DUPLICATE_EXPORT,
|
|
372
|
+
path: `clientCapabilities.gadgets.${gadget.package}.exports.${gadget.name}`,
|
|
373
|
+
message: `clientCapabilities.gadgets declares export '${gadget.name}' from both '${prior}' and '${gadget.package}'. The boilerplate generator emits one import per export name — two packages exporting the same name collide in module scope.`,
|
|
374
|
+
fixHint: `Drop one of the gadgets, or have the operator register the second under an aliased export name.`,
|
|
375
|
+
});
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
seen.set(gadget.name, gadget.package);
|
|
379
|
+
}
|
|
380
|
+
return warnings;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Registry-side catalog lint — runs on an `App.gadgets` descriptor
|
|
384
|
+
* array at registration time (ggui.json load, `ops_register_gadget`,
|
|
385
|
+
* registry install). Pure function; returns warnings. The caller
|
|
386
|
+
* (registration handler) treats any code in
|
|
387
|
+
* {@link FATAL_CATALOG_LINT_CODES} as a hard reject.
|
|
388
|
+
*
|
|
389
|
+
* Checks:
|
|
390
|
+
*
|
|
391
|
+
* - `LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG` (fatal) — two
|
|
392
|
+
* descriptors export the same name (a `hook` name or a
|
|
393
|
+
* `component` name). Each export name is unique per app; the
|
|
394
|
+
* boilerplate's per-export import would collide.
|
|
395
|
+
* - `LINT_GADGET_IMMUTABLE_MUTATION` (fatal) — two descriptors
|
|
396
|
+
* carry the same `(package, version)` tuple but different
|
|
397
|
+
* `bundleSri`. The same immutable bundle cannot have two hashes;
|
|
398
|
+
* a cached blueprint pinned to that version would break.
|
|
399
|
+
* - `LINT_GADGET_MISSING_PERMISSION` — a known-permission stdlib
|
|
400
|
+
* hook (geolocation, camera, …) registered without a
|
|
401
|
+
* `permission` field. The agent's reasoning loop reads it to
|
|
402
|
+
* surface "this UI prompts for X."
|
|
403
|
+
* - `LINT_GADGET_UNKNOWN_PERMISSION` — `permission` set to a value
|
|
404
|
+
* outside the Web Permissions API set. (The strict registry
|
|
405
|
+
* schema enum-checks this too; the lint is defence-in-depth for
|
|
406
|
+
* permissively-parsed catalogs.)
|
|
407
|
+
* - `LINT_GADGET_UNSCOPED_PACKAGE` — `package` lacks an `@scope/`
|
|
408
|
+
* prefix. Soft recommendation: scoped names avoid registry
|
|
409
|
+
* squatting + name collisions.
|
|
410
|
+
*/
|
|
411
|
+
export function lintGadgetCatalog(descriptors) {
|
|
412
|
+
const warnings = [];
|
|
413
|
+
// export name → first occurrence path (catalog-wide uniqueness).
|
|
414
|
+
const seenExportName = new Map();
|
|
415
|
+
// package name → first occurrence path (one descriptor per package).
|
|
416
|
+
const seenPackage = new Map();
|
|
417
|
+
const sriByVersionTuple = new Map();
|
|
418
|
+
descriptors.forEach((descriptor, index) => {
|
|
419
|
+
const path = `gadgets[${index}]`;
|
|
420
|
+
// ── Immutable-bundle mutation (fatal) — package-level ──
|
|
421
|
+
// Two descriptors for the same (package, version) MUST agree on
|
|
422
|
+
// bundleSri — they reference the same immutable artifact.
|
|
423
|
+
if (typeof descriptor.bundleSri === 'string' &&
|
|
424
|
+
descriptor.bundleSri.length > 0) {
|
|
425
|
+
const tupleKey = `${descriptor.package}\t${descriptor.version}`;
|
|
426
|
+
const prior = sriByVersionTuple.get(tupleKey);
|
|
427
|
+
if (prior !== undefined && prior.sri !== descriptor.bundleSri) {
|
|
428
|
+
warnings.push({
|
|
429
|
+
code: LINT_GADGET_IMMUTABLE_MUTATION,
|
|
430
|
+
path: `${path}.bundleSri`,
|
|
431
|
+
message: `gadgets[${index}] (${descriptor.package}@${descriptor.version}) declares bundleSri '${descriptor.bundleSri}' but ${prior.path} declares '${prior.sri}' for the same package+version. A published version is immutable — one bundle, one hash.`,
|
|
432
|
+
fixHint: `Bump the version on whichever descriptor ships the changed bundle, or correct the mismatched SRI.`,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
else if (prior === undefined) {
|
|
436
|
+
sriByVersionTuple.set(tupleKey, { path, sri: descriptor.bundleSri });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// ── Duplicate package (fatal) — package-level ──
|
|
440
|
+
// `(name, package)` ref resolution requires at most ONE descriptor
|
|
441
|
+
// per package in an app's catalog. Two descriptors sharing a
|
|
442
|
+
// package name make `filterDescriptorsToContract` silently pick
|
|
443
|
+
// one — `version` is no longer on the wire to disambiguate.
|
|
444
|
+
const priorPackagePath = seenPackage.get(descriptor.package);
|
|
445
|
+
if (priorPackagePath !== undefined) {
|
|
446
|
+
warnings.push({
|
|
447
|
+
code: LINT_GADGET_DUPLICATE_PACKAGE,
|
|
448
|
+
path: `${path}.package`,
|
|
449
|
+
message: `gadgets[${index}].package is '${descriptor.package}', already registered by ${priorPackagePath}. An app's catalog MUST hold at most one descriptor per package — the wire references a package by name and the server resolves exactly one descriptor (no version to disambiguate).`,
|
|
450
|
+
fixHint: `Register a single descriptor per package; drop or merge the duplicate.`,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
seenPackage.set(descriptor.package, path);
|
|
455
|
+
}
|
|
456
|
+
// ── Unscoped package (soft) — package-level ──
|
|
457
|
+
if (!descriptor.package.startsWith('@')) {
|
|
458
|
+
warnings.push({
|
|
459
|
+
code: LINT_GADGET_UNSCOPED_PACKAGE,
|
|
460
|
+
path: `${path}.package`,
|
|
461
|
+
message: `gadgets[${index}].package is '${descriptor.package}' — an unscoped npm name. Scoped names ('@org/name') avoid registry squatting + cross-publisher collisions.`,
|
|
462
|
+
fixHint: `Publish under an '@scope/' prefix.`,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
// ── Per-export checks ──
|
|
466
|
+
descriptor.exports.forEach((exp, exportIndex) => {
|
|
467
|
+
const exportName = gadgetExportName(exp);
|
|
468
|
+
const exportPath = `${path}.exports[${exportIndex}]`;
|
|
469
|
+
// Duplicate export name (fatal) — catalog-wide. The boilerplate
|
|
470
|
+
// generator emits one import per export name; a collision is
|
|
471
|
+
// unresolvable in module scope.
|
|
472
|
+
const priorPath = seenExportName.get(exportName);
|
|
473
|
+
if (priorPath !== undefined) {
|
|
474
|
+
warnings.push({
|
|
475
|
+
code: LINT_GADGET_DUPLICATE_EXPORT_IN_CATALOG,
|
|
476
|
+
path: exportPath,
|
|
477
|
+
message: `${exportPath} exports '${exportName}', already exported by ${priorPath}. Each gadget export name MUST be unique within an app's catalog — the boilerplate generator emits one import per name.`,
|
|
478
|
+
fixHint: `Drop the duplicate, or publish one gadget under an aliased export name.`,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
seenExportName.set(exportName, exportPath);
|
|
483
|
+
}
|
|
484
|
+
// Missing permission (soft) — a known-permission stdlib hook.
|
|
485
|
+
// `GadgetExport` is a type-exclusive union (`hook?: never` on the
|
|
486
|
+
// component member); discrimination is by VALUE presence, not the
|
|
487
|
+
// `in` operator, since `hook` is now an optional key of both.
|
|
488
|
+
if (exp.hook !== undefined) {
|
|
489
|
+
const expectedPermission = KNOWN_PERMISSION_HOOKS[exp.hook];
|
|
490
|
+
if (expectedPermission !== undefined &&
|
|
491
|
+
(typeof exp.permission !== 'string' || exp.permission.length === 0)) {
|
|
492
|
+
warnings.push({
|
|
493
|
+
code: LINT_GADGET_MISSING_PERMISSION,
|
|
494
|
+
path: `${exportPath}.permission`,
|
|
495
|
+
message: `${exportPath} registers hook '${exp.hook}' without a 'permission' field. The agent's reasoning loop reads permission to surface "this UI will prompt for ${expectedPermission}" context.`,
|
|
496
|
+
fixHint: `Add 'permission: "${expectedPermission}"' (Web Permissions API name).`,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// Unknown permission (soft).
|
|
501
|
+
if (typeof exp.permission === 'string' &&
|
|
502
|
+
exp.permission.length > 0 &&
|
|
503
|
+
!KNOWN_PERMISSION_NAMES_SET.has(exp.permission)) {
|
|
504
|
+
warnings.push({
|
|
505
|
+
code: LINT_GADGET_UNKNOWN_PERMISSION,
|
|
506
|
+
path: `${exportPath}.permission`,
|
|
507
|
+
message: `${exportPath}.permission is '${exp.permission}', which isn't a known Web Permissions API name. Catches typos + flags non-standard permissions for review.`,
|
|
508
|
+
fixHint: `Pick a name from the Web Permissions API spec (e.g., 'geolocation', 'notifications', 'microphone'), or document the custom value if intentional.`,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
return warnings;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Find top-level retired-field carriers on the contract. The schema
|
|
517
|
+
* is `.passthrough()`, so a stray `libraries`/`dispatch`/`wiredTools`/
|
|
518
|
+
* `clientTools`/`broadcast`/`capabilities` slips through silently. The
|
|
519
|
+
* push-gate hard-rejects these (see
|
|
520
|
+
* `mcp-server-handlers/.../assert-contract-no-retired-fields.ts`); this
|
|
521
|
+
* lint surface keeps authoring tools symmetric — show the warning before
|
|
522
|
+
* the push call so the author can fix it without a server round-trip.
|
|
523
|
+
*/
|
|
524
|
+
export function checkRetiredContractFields(contract) {
|
|
525
|
+
// Cast to a generic record so we can probe the keys the passthrough
|
|
526
|
+
// schema lets ride. The contract surface here is post-parse so all
|
|
527
|
+
// typed fields are already covered; we're specifically looking for
|
|
528
|
+
// siblings the type system can't see.
|
|
529
|
+
const raw = contract;
|
|
530
|
+
const warnings = [];
|
|
531
|
+
for (const [retired, replacement] of Object.entries(RETIRED_CONTRACT_FIELDS)) {
|
|
532
|
+
if (raw[retired] === undefined)
|
|
533
|
+
continue;
|
|
534
|
+
warnings.push({
|
|
535
|
+
code: LINT_CONTRACT_RETIRED_FIELD,
|
|
536
|
+
path: retired,
|
|
537
|
+
message: `contract.${retired} is retired. Use ${replacement} instead — the field rides through .passthrough() but the push gate hard-rejects it as a structural error.`,
|
|
538
|
+
fixHint: `Delete contract.${retired}; move its data to ${replacement}.`,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
return warnings;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Run every WIRE-side hygiene rule on a `DataContract`. Aggregates
|
|
545
|
+
* warnings; order is stable (orphans → usage → example → gadget
|
|
546
|
+
* hook-names → duplicate-hook → retired fields) so authoring tools
|
|
547
|
+
* render a predictable checklist.
|
|
548
|
+
*
|
|
549
|
+
* Registry-side gadget lints (`lintGadgetCatalog`) are NOT run here:
|
|
550
|
+
* they need an `App.gadgets` descriptor array, not a contract, and
|
|
551
|
+
* fire at registration time rather than push time.
|
|
552
|
+
* Call {@link lintGadgetCatalog} separately at the registration
|
|
553
|
+
* boundary.
|
|
554
|
+
*/
|
|
555
|
+
export function checkHygiene(contract) {
|
|
556
|
+
return [
|
|
557
|
+
...checkOrphanAgentTools(contract),
|
|
558
|
+
...checkMissingUsage(contract),
|
|
559
|
+
...checkMissingExample(contract),
|
|
560
|
+
...checkGadgetHookNames(contract),
|
|
561
|
+
...checkDuplicateGadgetHooks(contract),
|
|
562
|
+
...checkRetiredContractFields(contract),
|
|
563
|
+
];
|
|
564
|
+
}
|