appilot-mcp 0.0.1 → 0.1.0
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/.claude-plugin/plugin.json +43 -0
- package/.codex-plugin/plugin.json +37 -0
- package/.mcp.json +19 -0
- package/README.md +268 -6
- package/dist/appilot-configurator.mcpb +0 -0
- package/dist/client.d.ts +140 -0
- package/dist/client.js +252 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +78 -0
- package/dist/contract/bundleSnapshot.d.ts +12 -0
- package/dist/contract/bundleSnapshot.js +65 -0
- package/dist/contract/healthContract.d.ts +19 -0
- package/dist/contract/healthContract.js +297 -0
- package/dist/contract/index.d.ts +3 -0
- package/dist/contract/index.js +3 -0
- package/dist/contract/types.d.ts +86 -0
- package/dist/contract/types.js +9 -0
- package/dist/index.bundle.js +70059 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +50 -0
- package/dist/manifest.d.ts +93 -0
- package/dist/manifest.js +147 -0
- package/dist/redaction.d.ts +30 -0
- package/dist/redaction.js +33 -0
- package/dist/remote/consent.d.ts +29 -0
- package/dist/remote/consent.js +99 -0
- package/dist/remote/httpServer.d.ts +20 -0
- package/dist/remote/httpServer.js +125 -0
- package/dist/remote/oauth.d.ts +74 -0
- package/dist/remote/oauth.js +288 -0
- package/dist/remote/tokens.d.ts +28 -0
- package/dist/remote/tokens.js +50 -0
- package/dist/scaffold.d.ts +37 -0
- package/dist/scaffold.js +203 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +358 -0
- package/dist/soak.d.ts +32 -0
- package/dist/soak.js +51 -0
- package/dist/verify.d.ts +40 -0
- package/dist/verify.js +149 -0
- package/mcpb/manifest.json +67 -0
- package/package.json +70 -16
- package/skills/app-configurator/SKILL.md +198 -0
- package/skills/app-configurator/agents/openai.yaml +13 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Appilot config health contract.
|
|
3
|
+
*
|
|
4
|
+
* A set of pure lints over a `ConfigSnapshot` that encode what a WELL-FORMED
|
|
5
|
+
* content-model configuration looks like. Every lint traces to a real failure
|
|
6
|
+
* mode observed when apps are hand-authored (see the BeKnow `create-task`
|
|
7
|
+
* audit). The marker + identifier checks REUSE the canonical validators from
|
|
8
|
+
* `appilot-shared` so the contract can never drift from the runtime:
|
|
9
|
+
* - `validateActionPlanMarkers` (the same trust boundary the backend applies)
|
|
10
|
+
* - `IDENTIFIER_KINDS` (the single source of truth for identifier shape)
|
|
11
|
+
*
|
|
12
|
+
* Contract docs: docs/content-model/config-health-contract.md.
|
|
13
|
+
*/
|
|
14
|
+
import { validateActionPlanMarkers } from 'appilot-shared/utils';
|
|
15
|
+
import { IDENTIFIER_KINDS } from 'appilot-shared/validation';
|
|
16
|
+
const VALID_KB_SCOPES = new Set(['app_global', 'domain_global', 'page']);
|
|
17
|
+
/** Extract every `{{kind:id}}` marker (with or without a `[label](...)` wrapper). */
|
|
18
|
+
function extractMarkers(step) {
|
|
19
|
+
const rx = /\{\{([a-z_]+):([^}]+)\}\}/g;
|
|
20
|
+
const out = [];
|
|
21
|
+
let m;
|
|
22
|
+
while ((m = rx.exec(step)) !== null)
|
|
23
|
+
out.push({ kind: m[1], id: m[2].trim() });
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function planMarkers(plan) {
|
|
27
|
+
return plan.sections.flatMap(s => s.steps.flatMap(extractMarkers));
|
|
28
|
+
}
|
|
29
|
+
const CREATE_INTENT = /\b(create|add|new|update|edit|erstell|anleg|neu|hinzu|crear|nuev[ao]|añad|editar|actualiz)/i;
|
|
30
|
+
function localizedValues(text) {
|
|
31
|
+
return Object.values(text).filter((v) => typeof v === 'string');
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Lint 1 — marker trust boundary (reuse appilot-shared).
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
function lintMarkers(snap) {
|
|
37
|
+
const controlIds = new Set(snap.controls.map(c => c.semantic_id));
|
|
38
|
+
const formIds = new Set(snap.forms.map(f => f.semantic_id));
|
|
39
|
+
const zoneIds = new Set(snap.zones.map(z => z.semantic_id));
|
|
40
|
+
const fieldsByForm = new Map(snap.forms.map(f => [f.semantic_id, new Set(f.field_ids)]));
|
|
41
|
+
const findings = [];
|
|
42
|
+
for (const plan of snap.actionPlans) {
|
|
43
|
+
const result = validateActionPlanMarkers(plan.sections, { controlIds, formIds, zoneIds });
|
|
44
|
+
for (const error of result.errors) {
|
|
45
|
+
findings.push({
|
|
46
|
+
severity: 'high',
|
|
47
|
+
category: 'markers',
|
|
48
|
+
entity: `action_plan:${plan.semantic_id}`,
|
|
49
|
+
message: error,
|
|
50
|
+
recommendation: 'Each step needs exactly one executable marker resolving to a known control/form; values never go in the marker.',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// form_values fields must belong to their form.
|
|
54
|
+
for (const [formId, entry] of Object.entries(plan.form_values)) {
|
|
55
|
+
if (!formIds.has(formId)) {
|
|
56
|
+
findings.push({
|
|
57
|
+
severity: 'high', category: 'markers', entity: `action_plan:${plan.semantic_id}`,
|
|
58
|
+
message: `form_values references unknown form "${formId}".`,
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const fields = fieldsByForm.get(formId) ?? new Set();
|
|
63
|
+
for (const f of entry.fields) {
|
|
64
|
+
if (!fields.has(f.control_id)) {
|
|
65
|
+
findings.push({
|
|
66
|
+
severity: 'high', category: 'markers', entity: `action_plan:${plan.semantic_id}`,
|
|
67
|
+
message: `form_values field "${f.control_id}" does not belong to form "${formId}".`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return findings;
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Lint 2 — actionability: a plan must actually enter a value and submit, not
|
|
77
|
+
// just open/focus an element (the core create-task defect).
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
function lintActionability(snap) {
|
|
80
|
+
const findings = [];
|
|
81
|
+
const submitControlIds = new Set(snap.forms.map(f => f.submit_control_id).filter((x) => !!x));
|
|
82
|
+
for (const plan of snap.actionPlans) {
|
|
83
|
+
const markers = planMarkers(plan);
|
|
84
|
+
const hasFormStep = markers.some(m => m.kind === 'form');
|
|
85
|
+
const hasSetValue = markers.some(m => m.kind === 'set_value' || m.kind === 'select');
|
|
86
|
+
const hasFormValues = Object.keys(plan.form_values).length > 0;
|
|
87
|
+
const entersValue = hasFormStep || hasSetValue || hasFormValues;
|
|
88
|
+
const looksLikeCreate = localizedValues({ ...plan.name, ...plan.description }).some(v => CREATE_INTENT.test(v))
|
|
89
|
+
|| CREATE_INTENT.test(plan.semantic_id);
|
|
90
|
+
if (!entersValue) {
|
|
91
|
+
findings.push({
|
|
92
|
+
severity: looksLikeCreate ? 'critical' : 'high',
|
|
93
|
+
category: 'actionability',
|
|
94
|
+
entity: `action_plan:${plan.semantic_id}`,
|
|
95
|
+
message: 'Plan enters no value: it has no {{form:…}} step, no set_value step, and empty form_values, so it only opens/focuses an element and never completes the task.',
|
|
96
|
+
recommendation: 'Add a {{form:…}} step (or a set_value step) to enter the value, then a click step on the submit control to confirm.',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// Submit-without-fill: clicks a form's submit but never fills that form
|
|
100
|
+
// (ports warnOnUnfilledFormSubmit).
|
|
101
|
+
for (const form of snap.forms) {
|
|
102
|
+
if (!form.submit_control_id)
|
|
103
|
+
continue;
|
|
104
|
+
const clicksSubmit = markers.some(m => m.kind === 'click' && m.id === form.submit_control_id);
|
|
105
|
+
if (!clicksSubmit)
|
|
106
|
+
continue;
|
|
107
|
+
const fillsForm = markers.some(m => m.kind === 'form' && m.id === form.semantic_id)
|
|
108
|
+
|| (plan.form_values[form.semantic_id]?.fields?.length ?? 0) > 0;
|
|
109
|
+
if (!fillsForm) {
|
|
110
|
+
findings.push({
|
|
111
|
+
severity: 'high', category: 'actionability', entity: `action_plan:${plan.semantic_id}`,
|
|
112
|
+
message: `Plan clicks the submit control "${form.submit_control_id}" of form "${form.semantic_id}" without a fill step, so it submits an empty form.`,
|
|
113
|
+
recommendation: `Add a {{form:${form.semantic_id}}} step before the submit.`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
void submitControlIds;
|
|
118
|
+
}
|
|
119
|
+
return findings;
|
|
120
|
+
}
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Lint 3 — selector stability: reject auto-generated / positional locators.
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
const UNSTABLE_LOCATOR_PATTERNS = [
|
|
125
|
+
{ rx: /\b[a-z-]*vue-\d+/i, why: 'a framework-generated Vue component id (changes across renders)' },
|
|
126
|
+
{ rx: /\b(?:mui|radix|headlessui|el|ember|ext)-[a-z]*-?\d{2,}/i, why: 'a framework-generated ephemeral id' },
|
|
127
|
+
{ rx: /:nth-(?:child|of-type)\(/i, why: 'a positional selector (breaks when siblings change)' },
|
|
128
|
+
{ rx: /^#?[a-f0-9]{8,}$/i, why: 'a random hash id' },
|
|
129
|
+
];
|
|
130
|
+
function lintSelectorStability(snap) {
|
|
131
|
+
const findings = [];
|
|
132
|
+
for (const c of snap.controls) {
|
|
133
|
+
const match = UNSTABLE_LOCATOR_PATTERNS.find(p => p.rx.test(c.locator));
|
|
134
|
+
if (match) {
|
|
135
|
+
findings.push({
|
|
136
|
+
severity: 'high',
|
|
137
|
+
category: 'selector-stability',
|
|
138
|
+
entity: `control:${c.semantic_id}`,
|
|
139
|
+
message: `Locator "${c.locator}" is ${match.why}.`,
|
|
140
|
+
recommendation: 'Target a stable attribute (data-cy/data-test/aria-label/role/placeholder) and list one or two fallbacks, e.g. use locator_type class_text with a comma-separated selector list.',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// scope=global must not carry a view_path.
|
|
144
|
+
if (c.scope === 'global' && c.view_path) {
|
|
145
|
+
findings.push({
|
|
146
|
+
severity: 'medium', category: 'selector-stability', entity: `control:${c.semantic_id}`,
|
|
147
|
+
message: 'Control has scope "global" but also a view_path; global controls must not be view-scoped.',
|
|
148
|
+
recommendation: 'Clear view_path, or change scope to view.',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return findings;
|
|
153
|
+
}
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Lint 4 — i18n coverage for user-facing translatable fields.
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
function checkLocaleCoverage(text, expected, entity, field, findings) {
|
|
158
|
+
for (const locale of expected) {
|
|
159
|
+
const value = text[locale];
|
|
160
|
+
if (value === undefined || value === null) {
|
|
161
|
+
findings.push({
|
|
162
|
+
severity: 'medium', category: 'i18n-coverage', entity,
|
|
163
|
+
message: `Missing ${locale} translation for ${field}.`,
|
|
164
|
+
recommendation: `Provide the ${field} in ${expected.join('/')}.`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else if (value.trim() === '') {
|
|
168
|
+
findings.push({
|
|
169
|
+
severity: 'medium', category: 'i18n-coverage', entity,
|
|
170
|
+
message: `Empty ${locale} translation for ${field}.`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function lintI18n(snap) {
|
|
176
|
+
const findings = [];
|
|
177
|
+
for (const plan of snap.actionPlans) {
|
|
178
|
+
const entity = `action_plan:${plan.semantic_id}`;
|
|
179
|
+
checkLocaleCoverage(plan.name, snap.expectedLocales, entity, 'name', findings);
|
|
180
|
+
checkLocaleCoverage(plan.description, snap.expectedLocales, entity, 'description', findings);
|
|
181
|
+
if (plan.step_narratives) {
|
|
182
|
+
for (const locale of snap.expectedLocales) {
|
|
183
|
+
const variants = plan.step_narratives[locale];
|
|
184
|
+
if (!variants || variants.length === 0) {
|
|
185
|
+
findings.push({
|
|
186
|
+
severity: 'low', category: 'i18n-coverage', entity,
|
|
187
|
+
message: `Missing ${locale} step narratives.`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const v of snap.views) {
|
|
194
|
+
checkLocaleCoverage(v.name, snap.expectedLocales, `view:${v.path}`, 'name', findings);
|
|
195
|
+
}
|
|
196
|
+
return findings;
|
|
197
|
+
}
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Lint 5 — knowledge: scope validity, language coverage, procedure-in-KB,
|
|
200
|
+
// leftover chatbot hygiene.
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
const CHATBOT_META = /\b(soll ich|möchtest du|dann kann ich|entsprechend anpassen|let me know|i can adjust|feel free to|as an ai|as a language model|espero que|si quieres puedo|puedo ajustar)\b/i;
|
|
203
|
+
const PROCEDURE_HINT = /(^|\n)\s*(\d+[.)]\s+\S.*){3,}|<ol[\s>]|(?:^|\n)\s*(?:schritt|step|paso)\s*\d/im;
|
|
204
|
+
function lintKnowledge(snap) {
|
|
205
|
+
const findings = [];
|
|
206
|
+
for (const kb of snap.knowledge) {
|
|
207
|
+
const entity = `knowledge:${kb.id}`;
|
|
208
|
+
if (!VALID_KB_SCOPES.has(kb.scope)) {
|
|
209
|
+
findings.push({
|
|
210
|
+
severity: 'high', category: 'kb-scope', entity,
|
|
211
|
+
message: `Invalid knowledge scope "${kb.scope}".`,
|
|
212
|
+
recommendation: 'Use one of app_global, domain_global, page.',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const languages = new Set(kb.bodies.map(b => b.language));
|
|
216
|
+
for (const locale of snap.expectedLocales) {
|
|
217
|
+
if (!languages.has(locale)) {
|
|
218
|
+
findings.push({
|
|
219
|
+
severity: 'medium', category: 'kb-scope', entity,
|
|
220
|
+
message: `Knowledge article has no ${locale} language row.`,
|
|
221
|
+
recommendation: `Add a ${locale} translation (retrieval falls back, but coverage should be complete).`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const body of kb.bodies) {
|
|
226
|
+
if (CHATBOT_META.test(body.text)) {
|
|
227
|
+
findings.push({
|
|
228
|
+
severity: 'medium', category: 'kb-hygiene', entity,
|
|
229
|
+
message: `The ${body.language} body contains a chatbot meta-sentence (conversational filler pasted from an assistant).`,
|
|
230
|
+
recommendation: 'Remove offers to "adjust the text" and other conversational filler; KB is reference prose, not a chat reply.',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (PROCEDURE_HINT.test(body.text)) {
|
|
234
|
+
findings.push({
|
|
235
|
+
severity: 'medium', category: 'kb-procedure', entity,
|
|
236
|
+
message: `The ${body.language} body reads like a step-by-step procedure.`,
|
|
237
|
+
recommendation: 'A procedure belongs in a curated Action Plan (adopt_plan), not KB. KB is for meaning (workflows, rules, terminology).',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return findings;
|
|
243
|
+
}
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Lint 6 — identifier hygiene (reuse IDENTIFIER_KINDS).
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
function lintIdentifier(id, entity, findings) {
|
|
248
|
+
if (!IDENTIFIER_KINDS.semantic_id.isValid(id)) {
|
|
249
|
+
findings.push({
|
|
250
|
+
severity: 'high', category: 'identifier', entity,
|
|
251
|
+
message: `Identifier "${id}" is not a valid semantic_id (${IDENTIFIER_KINDS.semantic_id.rule}).`,
|
|
252
|
+
recommendation: `Example: ${IDENTIFIER_KINDS.semantic_id.example}.`,
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Slugified-UI-label heuristic: many segments + long => likely derived from a
|
|
257
|
+
// translated label rather than a purpose-named English identifier.
|
|
258
|
+
const segments = id.split(/[-_]/).filter(Boolean);
|
|
259
|
+
if (segments.length >= 4 && id.length >= 24) {
|
|
260
|
+
findings.push({
|
|
261
|
+
severity: 'low', category: 'identifier', entity,
|
|
262
|
+
message: `Identifier "${id}" looks like a slugified UI label (${segments.length} segments).`,
|
|
263
|
+
recommendation: 'Prefer a short English noun-qualifier, e.g. field-new-task or btn-task-submit; identifiers should survive label/translation changes.',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function lintIdentifiers(snap) {
|
|
268
|
+
const findings = [];
|
|
269
|
+
for (const c of snap.controls)
|
|
270
|
+
lintIdentifier(c.semantic_id, `control:${c.semantic_id}`, findings);
|
|
271
|
+
for (const f of snap.forms)
|
|
272
|
+
lintIdentifier(f.semantic_id, `form:${f.semantic_id}`, findings);
|
|
273
|
+
for (const z of snap.zones)
|
|
274
|
+
lintIdentifier(z.semantic_id, `zone:${z.semantic_id}`, findings);
|
|
275
|
+
for (const p of snap.actionPlans)
|
|
276
|
+
lintIdentifier(p.semantic_id, `action_plan:${p.semantic_id}`, findings);
|
|
277
|
+
return findings;
|
|
278
|
+
}
|
|
279
|
+
const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
280
|
+
/**
|
|
281
|
+
* Run the full config health contract over a snapshot. Findings are returned
|
|
282
|
+
* most-severe first, so the report reads like the manual audit.
|
|
283
|
+
*/
|
|
284
|
+
export function runHealthContract(snap) {
|
|
285
|
+
const findings = [
|
|
286
|
+
...lintMarkers(snap),
|
|
287
|
+
...lintActionability(snap),
|
|
288
|
+
...lintSelectorStability(snap),
|
|
289
|
+
...lintI18n(snap),
|
|
290
|
+
...lintKnowledge(snap),
|
|
291
|
+
...lintIdentifiers(snap),
|
|
292
|
+
].sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
|
|
293
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
294
|
+
for (const f of findings)
|
|
295
|
+
counts[f.severity]++;
|
|
296
|
+
return { ok: counts.critical === 0 && counts.high === 0, counts, findings };
|
|
297
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Appilot config health contract.
|
|
3
|
+
*
|
|
4
|
+
* A `ConfigSnapshot` is a normalized, transport-agnostic view of one app's
|
|
5
|
+
* content-model configuration. The MCP client maps live API responses (or a
|
|
6
|
+
* pasted export) into this shape; the contract lints run purely over it, so the
|
|
7
|
+
* same validator works online, offline (air-gapped), and in unit fixtures.
|
|
8
|
+
*/
|
|
9
|
+
export type Severity = 'critical' | 'high' | 'medium' | 'low';
|
|
10
|
+
export type FindingCategory = 'markers' | 'actionability' | 'selector-stability' | 'i18n-coverage' | 'kb-scope' | 'kb-procedure' | 'kb-hygiene' | 'identifier';
|
|
11
|
+
export interface Finding {
|
|
12
|
+
severity: Severity;
|
|
13
|
+
category: FindingCategory;
|
|
14
|
+
/** Stable entity handle, e.g. `action_plan:create-task` or `control:btn-new`. */
|
|
15
|
+
entity: string;
|
|
16
|
+
/** One-sentence statement of the defect. */
|
|
17
|
+
message: string;
|
|
18
|
+
/** Concrete, actionable fix. */
|
|
19
|
+
recommendation?: string;
|
|
20
|
+
}
|
|
21
|
+
/** A per-locale text map, e.g. `{ de: 'Neuer Ordner', en: 'New folder' }`. */
|
|
22
|
+
export type LocalizedText = Record<string, string | undefined>;
|
|
23
|
+
export interface ControlSnapshot {
|
|
24
|
+
semantic_id: string;
|
|
25
|
+
locator_type: string;
|
|
26
|
+
locator: string;
|
|
27
|
+
view_path?: string | null;
|
|
28
|
+
scope?: string | null;
|
|
29
|
+
}
|
|
30
|
+
export interface FormSnapshot {
|
|
31
|
+
semantic_id: string;
|
|
32
|
+
field_ids: string[];
|
|
33
|
+
submit_control_id?: string | null;
|
|
34
|
+
entry_control_id?: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface ActionPlanSnapshot {
|
|
37
|
+
semantic_id: string;
|
|
38
|
+
sections: Array<{
|
|
39
|
+
view_path?: string;
|
|
40
|
+
steps: string[];
|
|
41
|
+
}>;
|
|
42
|
+
form_values: Record<string, {
|
|
43
|
+
fields: Array<{
|
|
44
|
+
control_id: string;
|
|
45
|
+
value: string;
|
|
46
|
+
}>;
|
|
47
|
+
}>;
|
|
48
|
+
name: LocalizedText;
|
|
49
|
+
description: LocalizedText;
|
|
50
|
+
/** locale -> array of step-narrative variants (each variant an array of step strings). */
|
|
51
|
+
step_narratives?: Record<string, string[][]>;
|
|
52
|
+
is_active?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface ZoneSnapshot {
|
|
55
|
+
semantic_id: string;
|
|
56
|
+
}
|
|
57
|
+
export interface KnowledgeSnapshot {
|
|
58
|
+
id: string | number;
|
|
59
|
+
scope: string;
|
|
60
|
+
/** Body text per language row (knowledge_content is one row per language). */
|
|
61
|
+
bodies: Array<{
|
|
62
|
+
language: string;
|
|
63
|
+
text: string;
|
|
64
|
+
title?: string;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
export interface ViewSnapshot {
|
|
68
|
+
slug?: string;
|
|
69
|
+
path: string;
|
|
70
|
+
name: LocalizedText;
|
|
71
|
+
}
|
|
72
|
+
export interface ConfigSnapshot {
|
|
73
|
+
/** Locales the config is expected to cover, e.g. ['de','en','es']. */
|
|
74
|
+
expectedLocales: string[];
|
|
75
|
+
views: ViewSnapshot[];
|
|
76
|
+
controls: ControlSnapshot[];
|
|
77
|
+
forms: FormSnapshot[];
|
|
78
|
+
actionPlans: ActionPlanSnapshot[];
|
|
79
|
+
zones: ZoneSnapshot[];
|
|
80
|
+
knowledge: KnowledgeSnapshot[];
|
|
81
|
+
}
|
|
82
|
+
export interface HealthReport {
|
|
83
|
+
ok: boolean;
|
|
84
|
+
counts: Record<Severity, number>;
|
|
85
|
+
findings: Finding[];
|
|
86
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Appilot config health contract.
|
|
3
|
+
*
|
|
4
|
+
* A `ConfigSnapshot` is a normalized, transport-agnostic view of one app's
|
|
5
|
+
* content-model configuration. The MCP client maps live API responses (or a
|
|
6
|
+
* pasted export) into this shape; the contract lints run purely over it, so the
|
|
7
|
+
* same validator works online, offline (air-gapped), and in unit fixtures.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|