MCPcopy Create free account
hub / github.com/adcontextprotocol/adcp / lintStoryboardIdempotency

Function lintStoryboardIdempotency

scripts/build-compliance.cjs:272–444  ·  view source on GitHub ↗
(sourceDir, schemasDir)

Source from the content-addressed store, hash-verified

270}
271
272function lintStoryboardIdempotency(sourceDir, schemasDir) {
273 const { refs: mutatingRefs, tools: mutatingTools } = loadMutatingSchemaRefs(schemasDir);
274 const violations = [];
275 const stableKeyViolations = [];
276 const duplicateGeneratedKeyViolations = [];
277 const missingSchemaRefs = [];
278 const generatedKeyUses = new Map();
279 const duplicateGeneratedKeyAllowedFiles = new Set([
280 'universal/idempotency.yaml',
281 'universal/webhook-emission.yaml',
282 ]);
283
284 function isGeneratedIdempotencyKey(value, generatedContextNames) {
285 if (typeof value !== 'string') return false;
286 if (value.startsWith('$generate:uuid_v4')) return true;
287 const contextMatch = value.match(/^\$context\.([A-Za-z0-9_]+)$/);
288 return Boolean(contextMatch && generatedContextNames.has(contextMatch[1]));
289 }
290
291 function rememberGeneratedUuidContextOutputs(step, generatedContextNames) {
292 for (const output of step?.context_outputs ?? []) {
293 if (output?.generate === 'uuid_v4' && typeof output.name === 'string') {
294 generatedContextNames.add(output.name);
295 }
296 }
297 }
298
299 function lintFile(p) {
300 const rel = path.relative(sourceDir, p);
301 // Not storyboards: test-kit fixtures and the schema doc itself.
302 if (rel.startsWith('test-kits/')) return;
303 if (rel.endsWith('storyboard-schema.yaml')) return;
304
305 let doc;
306 try { doc = yaml.load(fs.readFileSync(p, 'utf8')); }
307 catch { return; }
308 if (!doc || typeof doc !== 'object' || !Array.isArray(doc.phases)) return;
309 const generatedContextNames = new Set();
310
311 for (const phase of doc.phases) {
312 if (!phase || !Array.isArray(phase.steps)) continue;
313 for (const step of phase.steps) {
314 if (!step || typeof step !== 'object' || !step.task) continue;
315 // Steps without schema_ref are HTTP probes, controller calls, or
316 // synthetic invocations (e.g., comply_test_controller's
317 // simulate_budget scenarios, universal/security.yaml's probe steps)
318 // that don't send a task request schema. They're out of scope for
319 // this lint — UNLESS the task name itself is a known mutating tool,
320 // in which case the missing schema_ref is itself a storyboard bug
321 // (the step would bypass the idempotency_key lint above). Positive
322 // check per red-team I-9 / security.mdx storyboard hygiene.
323 if (step.task && mutatingTools.has(step.task) && !step.schema_ref) {
324 missingSchemaRefs.push({
325 file: rel,
326 step: step.id,
327 msg: `Step uses mutating tool "${step.task}" but has no schema_ref`,
328 });
329 }

Calls 2

loadMutatingSchemaRefsFunction · 0.85
walkFunction · 0.70

Tested by

no test coverage detected