Files
Arcrun/registry/scripts/backfill-index.mjs
T
Leo 497f92a268 feat(arcrun): recipe system + resumable workflow + component registry canon
Three new platform capabilities + one component (kbdb_get) to enable
real AI workflow execution through cypher binding YAML.

## Recipe System (容器 + Recipe 模式)
SDD: .agents/specs/recipe-system/

- prompt_recipe schema (Zod): fragments + inputs + assembly + output
- recipe-expander.ts: expand recipe ref → real prompt by fetching KBDB blocks
  + pulling context fields with transforms (pluck_content / extract_field / etc)
- 7 transform whitelist: json_array / to_string / join / markdown_list /
  extract_field / first / pluck_content
- graph-executor hooks: detect node.data.recipe → expand → inject into ctx
- output JSON parser (with markdown fence stripping for Claude-wrapped JSON)
- Stored in RECIPES KV under prompt_recipe:{name}

## Resumable Workflow (webhook callback resume)
SDD: .agents/specs/resumable-workflow/

- WorkflowPaused class + paused-runs.ts (persist/load/consume in EXEC_CONTEXT KV, 24h TTL)
- graph-executor: detect {pending:true, task_id} → persist state → throw WorkflowPaused
- cypher-handlers: catch → return {success:true, paused:true, task_id, run_id}
- POST /workflows/resume route: consume KV state → resumeFromPaused()
- Auto-inject callback_url for claude_api nodes (PUBLIC_BASE_URL or default cypher.arcrun.dev)
- claude_api/main.go: forward callback_url to Mira daemon, default timeout 25s→120s
- Idempotent (consume = load+delete)

## Component Registry Canon
SDD: .agents/specs/component-registry-canon/

- Add POST /components/index-only endpoint (metadata-only, no wasm/sandbox)
- Backfill script (mjs): scan registry/components/*/contract.yaml → submit to KV
- register-component.sh: SSOT for local + CI hook (deploy.yml change in next commit)
- Drop R2 dead storage from submitComponent + types + wrangler
- Schema relaxed: category enum + auth/ai/platform; cold_start 50→500ms; size 2→8MB

## kbdb_get component
- registry/components/kbdb_get/: TinyGo WASM, two modes (block_id / page_name list)
- .component-builds/kbdb_get/: WASI shim worker (kbdb-get.arcrun.dev)

End-to-end validation: AI uses MCP execute_workflow with recipe ref →
cypher-executor expands prompt from KBDB schema/skill blocks + drafts →
claude_api calls Mira daemon → daemon callback fires resume route →
workflow continues. Verified with real 2KB+ Karpathy LLM Wiki draft.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 15:52:19 +08:00

122 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// Backfill component metadata 進 registry index
// SDD: matrix/arcrun/.agents/specs/component-registry-canon/design.md Phase 1
//
// 用法:
// node scripts/backfill-index.mjs --dry-run # 看會送什麼
// node scripts/backfill-index.mjs # 真的灌
//
// 流程:
// 1. 掃 ../components/*/component.contract.yaml
// 2. 解析 YAML(用 zero-dep 簡易 parsercontract 是 well-formed YAML
// 3. 對每個 contract POST registry.arcrun.dev/components/index-only
// 4. 印 success / already_indexed / fail 統計
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const COMPONENTS_DIR = join(__dirname, '..', 'components');
const REGISTRY_URL = process.env.REGISTRY_URL ?? 'https://registry.arcrun.dev';
const DRY_RUN = process.argv.includes('--dry-run');
// YAML 是 well-formed contract.yaml,用 js-yaml 解析最穩
async function parseYaml(text) {
const { load } = await import('js-yaml');
return load(text);
}
function listComponents() {
return readdirSync(COMPONENTS_DIR)
.filter((name) => {
const p = join(COMPONENTS_DIR, name);
return statSync(p).isDirectory();
})
.sort();
}
async function readContract(name) {
const path = join(COMPONENTS_DIR, name, 'component.contract.yaml');
const text = readFileSync(path, 'utf-8');
return parseYaml(text);
}
async function postIndexOnly(contract) {
const res = await fetch(`${REGISTRY_URL}/components/index-only`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contract }),
});
const body = await res.text();
let parsed;
try {
parsed = JSON.parse(body);
} catch {
parsed = { raw: body };
}
return { status: res.status, body: parsed };
}
async function main() {
console.log('=== arcrun Component Registry Backfill ===');
console.log(`Registry: ${REGISTRY_URL}`);
console.log(`Mode: ${DRY_RUN ? 'DRY RUN' : 'LIVE'}`);
console.log();
const names = listComponents();
console.log(`Found ${names.length} components in ${COMPONENTS_DIR}`);
console.log();
const stats = { created: 0, already: 0, fail: 0 };
for (const name of names) {
let contract;
try {
contract = await readContract(name);
} catch (e) {
console.log(` ✗ ${name.padEnd(28)} READ FAIL: ${e.message}`);
stats.fail++;
continue;
}
const cid = contract.canonical_id ?? '(no canonical_id)';
const ver = contract.version ?? '(no version)';
if (DRY_RUN) {
console.log(` → ${name.padEnd(28)} ${cid} ${ver}`);
continue;
}
try {
const { status, body } = await postIndexOnly(contract);
if (status === 200 && body.already_indexed) {
console.log(` = ${name.padEnd(28)} ${cid} ${ver} [already]`);
stats.already++;
} else if (status === 201) {
console.log(` ✓ ${name.padEnd(28)} ${cid} ${ver} [${body.component_hash_id}]`);
stats.created++;
} else {
console.log(` ✗ ${name.padEnd(28)} ${cid} ${ver} HTTP ${status}: ${JSON.stringify(body).slice(0, 200)}`);
stats.fail++;
}
} catch (e) {
console.log(` ✗ ${name.padEnd(28)} POST FAIL: ${e.message}`);
stats.fail++;
}
}
console.log();
console.log('=== Summary ===');
console.log(`Created: ${stats.created}`);
console.log(`Already indexed: ${stats.already}`);
console.log(`Failed: ${stats.fail}`);
process.exit(stats.fail > 0 ? 1 : 0);
}
main().catch((e) => {
console.error('Fatal:', e);
process.exit(1);
});