@vendoai/vendo 0.4.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/LICENSE +202 -0
- package/README.md +28 -0
- package/bin/vendo.mjs +4 -0
- package/dist/ai-sdk.d.ts +35 -0
- package/dist/ai-sdk.js +62 -0
- package/dist/auth-presets/auth-js.d.ts +14 -0
- package/dist/auth-presets/auth-js.js +91 -0
- package/dist/auth-presets/auth0.d.ts +23 -0
- package/dist/auth-presets/auth0.js +98 -0
- package/dist/auth-presets/clerk.d.ts +23 -0
- package/dist/auth-presets/clerk.js +65 -0
- package/dist/auth-presets/conformance.d.ts +42 -0
- package/dist/auth-presets/conformance.js +139 -0
- package/dist/auth-presets/identity.d.ts +56 -0
- package/dist/auth-presets/identity.js +152 -0
- package/dist/auth-presets/index.d.ts +14 -0
- package/dist/auth-presets/index.js +12 -0
- package/dist/auth-presets/jwt.d.ts +16 -0
- package/dist/auth-presets/jwt.js +54 -0
- package/dist/auth-presets/shared.d.ts +35 -0
- package/dist/auth-presets/shared.js +1 -0
- package/dist/auth-presets/supabase.d.ts +42 -0
- package/dist/auth-presets/supabase.js +229 -0
- package/dist/byo-approvals.d.ts +37 -0
- package/dist/byo-approvals.js +189 -0
- package/dist/capability-misses.d.ts +38 -0
- package/dist/capability-misses.js +172 -0
- package/dist/catalog.d.ts +19 -0
- package/dist/catalog.js +152 -0
- package/dist/cli/cloud/args.d.ts +3 -0
- package/dist/cli/cloud/args.js +32 -0
- package/dist/cli/cloud/auth.d.ts +17 -0
- package/dist/cli/cloud/auth.js +123 -0
- package/dist/cli/cloud/client.d.ts +28 -0
- package/dist/cli/cloud/client.js +133 -0
- package/dist/cli/cloud/command.d.ts +22 -0
- package/dist/cli/cloud/command.js +73 -0
- package/dist/cli/cloud/deploy.d.ts +26 -0
- package/dist/cli/cloud/deploy.js +187 -0
- package/dist/cli/cloud/device-login.d.ts +32 -0
- package/dist/cli/cloud/device-login.js +259 -0
- package/dist/cli/cloud/index.d.ts +5 -0
- package/dist/cli/cloud/index.js +59 -0
- package/dist/cli/cloud/keys.d.ts +2 -0
- package/dist/cli/cloud/keys.js +31 -0
- package/dist/cli/cloud/members.d.ts +3 -0
- package/dist/cli/cloud/members.js +25 -0
- package/dist/cli/cloud/output.d.ts +4 -0
- package/dist/cli/cloud/output.js +12 -0
- package/dist/cli/cloud/pending-claim.d.ts +29 -0
- package/dist/cli/cloud/pending-claim.js +38 -0
- package/dist/cli/cloud/read.d.ts +3 -0
- package/dist/cli/cloud/read.js +12 -0
- package/dist/cli/cloud/services.d.ts +2 -0
- package/dist/cli/cloud/services.js +9 -0
- package/dist/cli/cloud/session.d.ts +13 -0
- package/dist/cli/cloud/session.js +41 -0
- package/dist/cli/cloud-init.d.ts +60 -0
- package/dist/cli/cloud-init.js +149 -0
- package/dist/cli/dep-versions.d.ts +26 -0
- package/dist/cli/dep-versions.js +77 -0
- package/dist/cli/doctor-codes.d.ts +58 -0
- package/dist/cli/doctor-codes.js +60 -0
- package/dist/cli/doctor-live.d.ts +67 -0
- package/dist/cli/doctor-live.js +169 -0
- package/dist/cli/doctor.d.ts +52 -0
- package/dist/cli/doctor.js +467 -0
- package/dist/cli/eject.d.ts +29 -0
- package/dist/cli/eject.js +141 -0
- package/dist/cli/extract/apply.d.ts +26 -0
- package/dist/cli/extract/apply.js +123 -0
- package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
- package/dist/cli/extract/claude-cli-harness.js +132 -0
- package/dist/cli/extract/claude-harness.d.ts +14 -0
- package/dist/cli/extract/claude-harness.js +127 -0
- package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
- package/dist/cli/extract/codex-cli-harness.js +154 -0
- package/dist/cli/extract/delegate.d.ts +21 -0
- package/dist/cli/extract/delegate.js +77 -0
- package/dist/cli/extract/extraction.d.ts +65 -0
- package/dist/cli/extract/extraction.js +300 -0
- package/dist/cli/extract/gateway-fuel.d.ts +69 -0
- package/dist/cli/extract/gateway-fuel.js +69 -0
- package/dist/cli/extract/harness.d.ts +114 -0
- package/dist/cli/extract/harness.js +84 -0
- package/dist/cli/extract/index.d.ts +9 -0
- package/dist/cli/extract/index.js +9 -0
- package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
- package/dist/cli/extract/npx-engine-harness.js +225 -0
- package/dist/cli/extract/stages.d.ts +149 -0
- package/dist/cli/extract/stages.js +398 -0
- package/dist/cli/framework.d.ts +8 -0
- package/dist/cli/framework.js +36 -0
- package/dist/cli/init-auth.d.ts +67 -0
- package/dist/cli/init-auth.js +142 -0
- package/dist/cli/init-scaffolds.d.ts +28 -0
- package/dist/cli/init-scaffolds.js +235 -0
- package/dist/cli/init.d.ts +115 -0
- package/dist/cli/init.js +1008 -0
- package/dist/cli/mcp/index.d.ts +7 -0
- package/dist/cli/mcp/index.js +59 -0
- package/dist/cli/mcp/registry.d.ts +9 -0
- package/dist/cli/mcp/registry.js +117 -0
- package/dist/cli/mcp/server-json.d.ts +12 -0
- package/dist/cli/mcp/server-json.js +65 -0
- package/dist/cli/mcp/server.schema.json +574 -0
- package/dist/cli/mcp/verify-domain.d.ts +11 -0
- package/dist/cli/mcp/verify-domain.js +48 -0
- package/dist/cli/playground/app/embed-entry.d.ts +20 -0
- package/dist/cli/playground/app/embed-entry.js +46 -0
- package/dist/cli/playground/app/fake-client.d.ts +3 -0
- package/dist/cli/playground/app/fake-client.js +215 -0
- package/dist/cli/playground/app/fixtures.d.ts +47 -0
- package/dist/cli/playground/app/fixtures.js +472 -0
- package/dist/cli/playground/app/main.d.ts +1 -0
- package/dist/cli/playground/app/main.js +108 -0
- package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
- package/dist/cli/playground/app/scenario-mount.js +48 -0
- package/dist/cli/playground/app/scenarios.d.ts +25 -0
- package/dist/cli/playground/app/scenarios.js +152 -0
- package/dist/cli/playground/app/theme-editor.d.ts +13 -0
- package/dist/cli/playground/app/theme-editor.js +149 -0
- package/dist/cli/playground/app/theme-state.d.ts +29 -0
- package/dist/cli/playground/app/theme-state.js +151 -0
- package/dist/cli/playground/bundle.gen.d.ts +2 -0
- package/dist/cli/playground/bundle.gen.js +2 -0
- package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
- package/dist/cli/playground/embed-bundle.gen.js +2 -0
- package/dist/cli/playground.d.ts +33 -0
- package/dist/cli/playground.js +120 -0
- package/dist/cli/pretty.d.ts +48 -0
- package/dist/cli/pretty.js +330 -0
- package/dist/cli/refine.d.ts +43 -0
- package/dist/cli/refine.js +181 -0
- package/dist/cli/semantics.d.ts +31 -0
- package/dist/cli/semantics.js +122 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +184 -0
- package/dist/cli/sync.d.ts +36 -0
- package/dist/cli/sync.js +202 -0
- package/dist/cli/theme/color.d.ts +15 -0
- package/dist/cli/theme/color.js +188 -0
- package/dist/cli/theme/css-vars.d.ts +13 -0
- package/dist/cli/theme/css-vars.js +40 -0
- package/dist/cli/theme/entry-candidates.d.ts +5 -0
- package/dist/cli/theme/entry-candidates.js +22 -0
- package/dist/cli/theme/extract-theme.d.ts +184 -0
- package/dist/cli/theme/extract-theme.js +358 -0
- package/dist/cli/theme/walk.d.ts +2 -0
- package/dist/cli/theme/walk.js +24 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +333 -0
- package/dist/cloud-apps.d.ts +17 -0
- package/dist/cloud-apps.js +46 -0
- package/dist/cloud-console.d.ts +32 -0
- package/dist/cloud-console.js +72 -0
- package/dist/cloud-tools.d.ts +23 -0
- package/dist/cloud-tools.js +196 -0
- package/dist/connections.d.ts +71 -0
- package/dist/connections.js +192 -0
- package/dist/deployment-identity.d.ts +12 -0
- package/dist/deployment-identity.js +69 -0
- package/dist/dev-creds/model.d.ts +65 -0
- package/dist/dev-creds/model.js +159 -0
- package/dist/dev-creds/resolve.d.ts +36 -0
- package/dist/dev-creds/resolve.js +57 -0
- package/dist/hosted-store.d.ts +52 -0
- package/dist/hosted-store.js +322 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -0
- package/dist/mastra.d.ts +46 -0
- package/dist/mastra.js +88 -0
- package/dist/react.d.ts +10 -0
- package/dist/react.js +32 -0
- package/dist/refine.d.ts +244 -0
- package/dist/refine.js +599 -0
- package/dist/remixable.d.ts +18 -0
- package/dist/remixable.js +42 -0
- package/dist/runtime-capture.d.ts +18 -0
- package/dist/runtime-capture.js +111 -0
- package/dist/sandbox-wire.d.ts +101 -0
- package/dist/sandbox-wire.js +101 -0
- package/dist/sandbox.d.ts +38 -0
- package/dist/sandbox.js +389 -0
- package/dist/server.d.ts +216 -0
- package/dist/server.js +1320 -0
- package/dist/sync-impact.d.ts +14 -0
- package/dist/sync-impact.js +75 -0
- package/dist/turn-liveness.d.ts +33 -0
- package/dist/turn-liveness.js +105 -0
- package/dist/wire/approvals.d.ts +10 -0
- package/dist/wire/approvals.js +59 -0
- package/dist/wire/apps.d.ts +5 -0
- package/dist/wire/apps.js +155 -0
- package/dist/wire/automations.d.ts +5 -0
- package/dist/wire/automations.js +57 -0
- package/dist/wire/box.d.ts +3 -0
- package/dist/wire/box.js +268 -0
- package/dist/wire/connections.d.ts +5 -0
- package/dist/wire/connections.js +47 -0
- package/dist/wire/context.d.ts +22 -0
- package/dist/wire/context.js +190 -0
- package/dist/wire/doctor.d.ts +19 -0
- package/dist/wire/doctor.js +116 -0
- package/dist/wire/misc.d.ts +18 -0
- package/dist/wire/misc.js +227 -0
- package/dist/wire/shared.d.ts +158 -0
- package/dist/wire/shared.js +134 -0
- package/dist/wire/threads.d.ts +3 -0
- package/dist/wire/threads.js +68 -0
- package/package.json +151 -0
- package/skills/vendo-setup/SKILL.md +128 -0
package/dist/refine.js
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve, sep } from "node:path";
|
|
3
|
+
import { validateCapabilities, capabilitiesFileSchema, overridesFileSchema, toolsFileSchema, } from "@vendoai/actions";
|
|
4
|
+
import { TOOL_NAME_PATTERN, VENDO_CAPABILITIES_FORMAT, VENDO_OVERRIDES_FORMAT, capabilityMissEventSchema, } from "@vendoai/core";
|
|
5
|
+
import { generateObject } from "ai";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
/**
|
|
8
|
+
* The refine engine (ENG-250, extraction spec §3). One engine, two surfaces:
|
|
9
|
+
* the `vendo refine` CLI command and the end-of-init offer — both call
|
|
10
|
+
* `runRefine`. The engine PROPOSES agent-layer artifacts as reviewable diffs
|
|
11
|
+
* (never silently applied, never touching deterministic `tools.json`):
|
|
12
|
+
*
|
|
13
|
+
* - compound capabilities + capability briefs → `.vendo/capabilities.json`
|
|
14
|
+
* - risk corrections, enable/disable curation, description improvements
|
|
15
|
+
* → `.vendo/overrides.json`
|
|
16
|
+
* - product-brief updates → `.vendo/brief.md`
|
|
17
|
+
*
|
|
18
|
+
* Loop: propose (one BYO-model `generateObject` call over the static
|
|
19
|
+
* extraction output, bounded source context, the miss feed, and the dev
|
|
20
|
+
* interview) → probe each proposed capability against the running dev app
|
|
21
|
+
* (doctor machinery: `/status`, plus read-step reachability — write steps are
|
|
22
|
+
* NEVER executed by the probe) → present diffs → the caller applies approved
|
|
23
|
+
* files.
|
|
24
|
+
*
|
|
25
|
+
* Cloud seam (designed, not built): every input is injectable
|
|
26
|
+
* (`misses`, `interview`, `fetchImpl`) and the run emits a `RefineTranscript`
|
|
27
|
+
* so the identical engine can execute in a hosted sandbox later.
|
|
28
|
+
*/
|
|
29
|
+
const RISK_ORDER = { read: 0, write: 1, destructive: 2 };
|
|
30
|
+
const DEFAULT_SOURCE_BUDGET = 48_000;
|
|
31
|
+
const MAX_FILE_CHARS = 4_000;
|
|
32
|
+
const MAX_TREE_ENTRIES = 400;
|
|
33
|
+
const DEFAULT_MAX_MISSES = 50;
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Proposal schema — what the model returns from the single generateObject call.
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
const proposalStepSchema = z.object({
|
|
38
|
+
id: z.string(),
|
|
39
|
+
tool: z.string(),
|
|
40
|
+
args: z.record(z.string()).optional(),
|
|
41
|
+
if: z.string().optional(),
|
|
42
|
+
forEach: z.string().optional(),
|
|
43
|
+
});
|
|
44
|
+
const proposalsSchema = z.object({
|
|
45
|
+
compounds: z.array(z.object({
|
|
46
|
+
name: z.string(),
|
|
47
|
+
description: z.string(),
|
|
48
|
+
inputSchema: z.record(z.unknown()).optional(),
|
|
49
|
+
steps: z.array(proposalStepSchema).min(1).max(50),
|
|
50
|
+
rationale: z.string().optional(),
|
|
51
|
+
})).optional(),
|
|
52
|
+
briefs: z.array(z.object({
|
|
53
|
+
name: z.string(),
|
|
54
|
+
text: z.string(),
|
|
55
|
+
tools: z.array(z.string()).optional(),
|
|
56
|
+
})).optional(),
|
|
57
|
+
riskCorrections: z.array(z.object({
|
|
58
|
+
tool: z.string(),
|
|
59
|
+
risk: z.enum(["read", "write", "destructive"]),
|
|
60
|
+
reason: z.string().optional(),
|
|
61
|
+
})).optional(),
|
|
62
|
+
curation: z.array(z.object({
|
|
63
|
+
tool: z.string(),
|
|
64
|
+
disabled: z.boolean(),
|
|
65
|
+
reason: z.string().optional(),
|
|
66
|
+
})).optional(),
|
|
67
|
+
descriptions: z.array(z.object({
|
|
68
|
+
tool: z.string(),
|
|
69
|
+
description: z.string(),
|
|
70
|
+
})).optional(),
|
|
71
|
+
briefUpdate: z.string().optional(),
|
|
72
|
+
});
|
|
73
|
+
async function readOptionalFile(path) {
|
|
74
|
+
try {
|
|
75
|
+
return await readFile(path, "utf8");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code === "ENOENT")
|
|
79
|
+
return null;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function loadInputs(root, options) {
|
|
84
|
+
const vendoDir = join(root, ".vendo");
|
|
85
|
+
const toolsRaw = await readOptionalFile(join(vendoDir, "tools.json"));
|
|
86
|
+
if (toolsRaw === null) {
|
|
87
|
+
throw new Error("refine needs .vendo/tools.json — run `vendo init` (or `vendo sync`) first");
|
|
88
|
+
}
|
|
89
|
+
const tools = toolsFileSchema.parse(JSON.parse(toolsRaw)).tools;
|
|
90
|
+
const overridesRaw = await readOptionalFile(join(vendoDir, "overrides.json"));
|
|
91
|
+
const overrides = overridesRaw === null ? null : overridesFileSchema.parse(JSON.parse(overridesRaw));
|
|
92
|
+
const capabilitiesRaw = await readOptionalFile(join(vendoDir, "capabilities.json"));
|
|
93
|
+
const capabilities = capabilitiesRaw === null ? null : capabilitiesFileSchema.parse(JSON.parse(capabilitiesRaw));
|
|
94
|
+
const brief = await readOptionalFile(join(vendoDir, "brief.md"));
|
|
95
|
+
let misses = options.misses;
|
|
96
|
+
if (misses === undefined) {
|
|
97
|
+
misses = [];
|
|
98
|
+
const missesRaw = await readOptionalFile(join(vendoDir, "data", "misses.jsonl"));
|
|
99
|
+
if (missesRaw !== null) {
|
|
100
|
+
for (const line of missesRaw.split("\n")) {
|
|
101
|
+
if (line.trim() === "")
|
|
102
|
+
continue;
|
|
103
|
+
try {
|
|
104
|
+
misses.push(capabilityMissEventSchema.parse(JSON.parse(line)));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// A malformed feed line never sinks the run.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const maxMisses = options.maxMisses ?? DEFAULT_MAX_MISSES;
|
|
113
|
+
// slice(-0) is slice(0) — the WHOLE feed — so a zero cap needs its own leg.
|
|
114
|
+
return { tools, overrides, capabilities, brief, misses: maxMisses <= 0 ? [] : misses.slice(-maxMisses) };
|
|
115
|
+
}
|
|
116
|
+
/** Post-override-merge primitive targets — the same view the registry loads,
|
|
117
|
+
* so write-time validation agrees with load-time quarantine (04 §6). */
|
|
118
|
+
function mergedPrimitives(inputs) {
|
|
119
|
+
const primitives = new Map();
|
|
120
|
+
for (const tool of inputs.tools) {
|
|
121
|
+
const override = inputs.overrides?.tools[tool.name];
|
|
122
|
+
primitives.set(tool.name, {
|
|
123
|
+
risk: override?.risk ?? tool.risk,
|
|
124
|
+
disabled: override?.disabled ?? tool.disabled,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return primitives;
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Source context — bounded, deterministic, prioritized toward API surfaces.
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
const SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
133
|
+
const IGNORED_DIRS = new Set([
|
|
134
|
+
"node_modules", ".git", ".next", ".turbo", ".vendo", "dist", "build", "out", "coverage",
|
|
135
|
+
]);
|
|
136
|
+
function sourcePriority(path) {
|
|
137
|
+
const segments = path.split("/");
|
|
138
|
+
if (segments.some((part) => part === "api" || part === "routes" || part === "server" || part === "actions"))
|
|
139
|
+
return 0;
|
|
140
|
+
if (segments.some((part) => part === "app" || part === "pages"))
|
|
141
|
+
return 1;
|
|
142
|
+
if (segments.some((part) => part === "components" || part === "client"))
|
|
143
|
+
return 2;
|
|
144
|
+
return 3;
|
|
145
|
+
}
|
|
146
|
+
async function collectSourcePaths(root) {
|
|
147
|
+
const found = [];
|
|
148
|
+
const walk = async (dir) => {
|
|
149
|
+
let entries;
|
|
150
|
+
try {
|
|
151
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
157
|
+
if (entry.name.startsWith(".") && entry.name !== ".")
|
|
158
|
+
continue;
|
|
159
|
+
const absolute = join(dir, entry.name);
|
|
160
|
+
if (entry.isDirectory()) {
|
|
161
|
+
if (!IGNORED_DIRS.has(entry.name))
|
|
162
|
+
await walk(absolute);
|
|
163
|
+
}
|
|
164
|
+
else if (SOURCE_EXTENSIONS.some((extension) => entry.name.endsWith(extension))) {
|
|
165
|
+
found.push(absolute.slice(root.length + 1).split(sep).join("/"));
|
|
166
|
+
if (found.length >= MAX_TREE_ENTRIES)
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (found.length >= MAX_TREE_ENTRIES)
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
await walk(root);
|
|
174
|
+
return found;
|
|
175
|
+
}
|
|
176
|
+
async function gatherSource(root, budget) {
|
|
177
|
+
const tree = await collectSourcePaths(root);
|
|
178
|
+
const ordered = [...tree].sort((left, right) => sourcePriority(left) - sourcePriority(right) || left.localeCompare(right));
|
|
179
|
+
const files = [];
|
|
180
|
+
let used = 0;
|
|
181
|
+
for (const path of ordered) {
|
|
182
|
+
if (used >= budget)
|
|
183
|
+
break;
|
|
184
|
+
const raw = await readOptionalFile(join(root, ...path.split("/")));
|
|
185
|
+
if (raw === null)
|
|
186
|
+
continue;
|
|
187
|
+
const content = raw.length > MAX_FILE_CHARS ? `${raw.slice(0, MAX_FILE_CHARS)}\n// … truncated` : raw;
|
|
188
|
+
if (used + content.length > budget)
|
|
189
|
+
continue;
|
|
190
|
+
files.push({ path, content });
|
|
191
|
+
used += content.length;
|
|
192
|
+
}
|
|
193
|
+
return { tree, files };
|
|
194
|
+
}
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Proposal generation — one model call.
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
const SYSTEM_PROMPT = [
|
|
199
|
+
"You are the Vendo refine engine. Vendo turns a host app's API into risk-labeled agent tools;",
|
|
200
|
+
"deterministic extraction already produced the primitive tools you are given. Your job is to close",
|
|
201
|
+
"the gap between what the host UI can do and what single tools can do, by proposing:",
|
|
202
|
+
"",
|
|
203
|
+
"1. compounds — multi-step capabilities the UI supports but no single tool covers. Each compound has",
|
|
204
|
+
" ordered steps; every step references an ENABLED primitive tool name from the provided list (never",
|
|
205
|
+
" another compound, never an unknown name). Step `args` values are JSONata expressions evaluated",
|
|
206
|
+
" against { args, steps, item }: `args.x` reads the compound's own input, `steps.<id>` reads a prior",
|
|
207
|
+
" step's output, and `item` is the current element inside a `forEach` step. `forEach` is a JSONata",
|
|
208
|
+
" expression that must produce an array (max 1000 items); `if` is a JSONata condition that skips the",
|
|
209
|
+
" step when false. Give each compound a JSON Schema `inputSchema` for its own arguments.",
|
|
210
|
+
"2. briefs — short prose playbooks that teach the agent how to combine existing tools.",
|
|
211
|
+
"3. riskCorrections — primitive tools whose extracted risk label (read/write/destructive) is wrong.",
|
|
212
|
+
"4. curation — primitive tools to disable (internal/debug/dangerous surfaces) or re-enable.",
|
|
213
|
+
"5. descriptions — clearer one-line descriptions for badly described primitive tools.",
|
|
214
|
+
"6. briefUpdate — an improved full replacement for the product brief, only when clearly better.",
|
|
215
|
+
"",
|
|
216
|
+
`Tool names must match ${String(TOOL_NAME_PATTERN)} and not collide with existing tools.`,
|
|
217
|
+
"Propose only what the provided source, misses, and interview genuinely support. Fewer, correct",
|
|
218
|
+
"proposals beat many speculative ones. Return empty arrays when nothing is warranted.",
|
|
219
|
+
].join("\n");
|
|
220
|
+
async function propose(model, inputs, source, interview) {
|
|
221
|
+
const prompt = JSON.stringify({
|
|
222
|
+
productBrief: inputs.brief,
|
|
223
|
+
tools: inputs.tools.map((tool) => ({
|
|
224
|
+
name: tool.name,
|
|
225
|
+
description: tool.description,
|
|
226
|
+
risk: inputs.overrides?.tools[tool.name]?.risk ?? tool.risk,
|
|
227
|
+
disabled: inputs.overrides?.tools[tool.name]?.disabled ?? tool.disabled ?? false,
|
|
228
|
+
inputSchema: tool.inputSchema,
|
|
229
|
+
})),
|
|
230
|
+
existingCompounds: (inputs.capabilities?.tools ?? []).map((tool) => tool.name),
|
|
231
|
+
existingBriefs: (inputs.capabilities?.briefs ?? []).map((brief) => brief.name),
|
|
232
|
+
capabilityMisses: inputs.misses.map((miss) => ({ intent: miss.intent, trigger: miss.trigger.kind })),
|
|
233
|
+
interview,
|
|
234
|
+
sourceTree: source.tree,
|
|
235
|
+
sourceFiles: source.files,
|
|
236
|
+
});
|
|
237
|
+
const result = await generateObject({ model, schema: proposalsSchema, system: SYSTEM_PROMPT, prompt });
|
|
238
|
+
return result.object;
|
|
239
|
+
}
|
|
240
|
+
function normalize(proposals, inputs) {
|
|
241
|
+
const dropped = [];
|
|
242
|
+
const primitives = mergedPrimitives(inputs);
|
|
243
|
+
const existingCompoundNames = new Set((inputs.capabilities?.tools ?? []).map((tool) => tool.name));
|
|
244
|
+
const compounds = [];
|
|
245
|
+
const acceptedNames = new Set();
|
|
246
|
+
for (const candidate of proposals.compounds ?? []) {
|
|
247
|
+
const drop = (reason) => { dropped.push({ kind: "compound", target: candidate.name, reason }); };
|
|
248
|
+
if (!TOOL_NAME_PATTERN.test(candidate.name)) {
|
|
249
|
+
drop(`name does not match ${String(TOOL_NAME_PATTERN)}`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (primitives.has(candidate.name)) {
|
|
253
|
+
drop("name collides with an extracted tool");
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (existingCompoundNames.has(candidate.name)) {
|
|
257
|
+
drop("name collides with an existing compound");
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (acceptedNames.has(candidate.name)) {
|
|
261
|
+
drop("duplicate proposal name");
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (new Set(candidate.steps.map((step) => step.id)).size !== candidate.steps.length) {
|
|
265
|
+
drop("step ids must be unique");
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
let maxRisk = "read";
|
|
269
|
+
let stepsValid = true;
|
|
270
|
+
for (const step of candidate.steps) {
|
|
271
|
+
const target = primitives.get(step.tool);
|
|
272
|
+
if (target === undefined) {
|
|
273
|
+
drop(`step ${step.id} references unknown tool ${step.tool}`);
|
|
274
|
+
stepsValid = false;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
if (target.disabled === true) {
|
|
278
|
+
drop(`step ${step.id} references disabled tool ${step.tool}`);
|
|
279
|
+
stepsValid = false;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
if (RISK_ORDER[target.risk] > RISK_ORDER[maxRisk])
|
|
283
|
+
maxRisk = target.risk;
|
|
284
|
+
}
|
|
285
|
+
if (!stepsValid)
|
|
286
|
+
continue;
|
|
287
|
+
const steps = candidate.steps.map((step) => ({
|
|
288
|
+
id: step.id,
|
|
289
|
+
tool: step.tool,
|
|
290
|
+
...(step.args === undefined ? {} : { args: step.args }),
|
|
291
|
+
...(step.if === undefined ? {} : { if: step.if }),
|
|
292
|
+
...(step.forEach === undefined ? {} : { forEach: step.forEach }),
|
|
293
|
+
}));
|
|
294
|
+
acceptedNames.add(candidate.name);
|
|
295
|
+
compounds.push({
|
|
296
|
+
name: candidate.name,
|
|
297
|
+
description: candidate.description,
|
|
298
|
+
inputSchema: candidate.inputSchema ?? { type: "object" },
|
|
299
|
+
// Descriptor risk = max of step risks, computed HERE (validated again below
|
|
300
|
+
// and at load) — the model's opinion of risk is never trusted.
|
|
301
|
+
risk: maxRisk,
|
|
302
|
+
binding: { kind: "compound", steps },
|
|
303
|
+
note: "authored by vendo refine",
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
// Final gate: the SAME semantic validation the registry runs at load (04 §6).
|
|
307
|
+
const candidateFile = { tools: [...(inputs.capabilities?.tools ?? []), ...compounds] };
|
|
308
|
+
const issues = validateCapabilities(candidateFile, primitives);
|
|
309
|
+
const invalid = new Map();
|
|
310
|
+
for (const issue of issues) {
|
|
311
|
+
if (acceptedNames.has(issue.tool))
|
|
312
|
+
invalid.set(issue.tool, issue.message);
|
|
313
|
+
}
|
|
314
|
+
const validCompounds = compounds.filter((compound) => {
|
|
315
|
+
const message = invalid.get(compound.name);
|
|
316
|
+
if (message !== undefined)
|
|
317
|
+
dropped.push({ kind: "compound", target: compound.name, reason: message });
|
|
318
|
+
return message === undefined;
|
|
319
|
+
});
|
|
320
|
+
const existingBriefNames = new Set((inputs.capabilities?.briefs ?? []).map((brief) => brief.name));
|
|
321
|
+
// Only tools that survive to the OUTPUT are legal brief references: primitives
|
|
322
|
+
// plus compounds that passed the final validateCapabilities gate — never a
|
|
323
|
+
// compound this run dropped (Devin/Greptile: no dangling brief references).
|
|
324
|
+
const outputCompoundNames = new Set(validCompounds.map((compound) => compound.name));
|
|
325
|
+
const briefs = [];
|
|
326
|
+
for (const candidate of proposals.briefs ?? []) {
|
|
327
|
+
if (candidate.name.trim() === "" || candidate.text.trim() === "") {
|
|
328
|
+
dropped.push({ kind: "brief", target: candidate.name || "(unnamed)", reason: "empty name or text" });
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (existingBriefNames.has(candidate.name) || briefs.some((brief) => brief.name === candidate.name)) {
|
|
332
|
+
dropped.push({ kind: "brief", target: candidate.name, reason: "brief already exists" });
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const knownTools = candidate.tools?.filter((tool) => primitives.has(tool) || outputCompoundNames.has(tool));
|
|
336
|
+
briefs.push({
|
|
337
|
+
name: candidate.name,
|
|
338
|
+
text: candidate.text,
|
|
339
|
+
...(knownTools === undefined || knownTools.length === 0 ? {} : { tools: knownTools }),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const overridePatches = new Map();
|
|
343
|
+
const overrideWarnings = [];
|
|
344
|
+
const patch = (tool, fields) => {
|
|
345
|
+
overridePatches.set(tool, { ...overridePatches.get(tool), ...fields });
|
|
346
|
+
};
|
|
347
|
+
for (const correction of proposals.riskCorrections ?? []) {
|
|
348
|
+
const current = primitives.get(correction.tool);
|
|
349
|
+
if (current === undefined) {
|
|
350
|
+
dropped.push({ kind: "override", target: correction.tool, reason: "unknown tool" });
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (current.risk === correction.risk) {
|
|
354
|
+
dropped.push({ kind: "override", target: correction.tool, reason: `risk is already ${correction.risk}` });
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (RISK_ORDER[correction.risk] < RISK_ORDER[current.risk]) {
|
|
358
|
+
overrideWarnings.push(`risk DOWNGRADE proposed: ${correction.tool} ${current.risk} → ${correction.risk}`
|
|
359
|
+
+ `${correction.reason === undefined ? "" : ` (${correction.reason})`} — approve only if certain`);
|
|
360
|
+
}
|
|
361
|
+
patch(correction.tool, { risk: correction.risk });
|
|
362
|
+
}
|
|
363
|
+
for (const curation of proposals.curation ?? []) {
|
|
364
|
+
const current = primitives.get(curation.tool);
|
|
365
|
+
if (current === undefined) {
|
|
366
|
+
dropped.push({ kind: "override", target: curation.tool, reason: "unknown tool" });
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if ((current.disabled ?? false) === curation.disabled) {
|
|
370
|
+
dropped.push({ kind: "override", target: curation.tool, reason: `already ${curation.disabled ? "disabled" : "enabled"}` });
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (!curation.disabled) {
|
|
374
|
+
overrideWarnings.push(`re-ENABLE proposed: ${curation.tool}${curation.reason === undefined ? "" : ` (${curation.reason})`}`);
|
|
375
|
+
}
|
|
376
|
+
patch(curation.tool, { disabled: curation.disabled });
|
|
377
|
+
}
|
|
378
|
+
for (const description of proposals.descriptions ?? []) {
|
|
379
|
+
const extracted = inputs.tools.find((tool) => tool.name === description.tool);
|
|
380
|
+
if (extracted === undefined) {
|
|
381
|
+
dropped.push({ kind: "description", target: description.tool, reason: "unknown tool" });
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const currentDescription = inputs.overrides?.tools[description.tool]?.description ?? extracted.description;
|
|
385
|
+
if (description.description.trim() === "" || description.description === currentDescription) {
|
|
386
|
+
dropped.push({ kind: "description", target: description.tool, reason: "empty or unchanged description" });
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
patch(description.tool, { description: description.description });
|
|
390
|
+
}
|
|
391
|
+
let briefUpdate = proposals.briefUpdate;
|
|
392
|
+
if (briefUpdate !== undefined && (briefUpdate.trim() === "" || briefUpdate.trim() === inputs.brief?.trim())) {
|
|
393
|
+
dropped.push({ kind: "brief-update", target: "brief.md", reason: "empty or unchanged brief" });
|
|
394
|
+
briefUpdate = undefined;
|
|
395
|
+
}
|
|
396
|
+
return { compounds: validCompounds, briefs, overridePatches, overrideWarnings, briefUpdate, dropped };
|
|
397
|
+
}
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Probe — doctor machinery against the running dev app. Never mutating: only
|
|
400
|
+
// paramless GET read steps are executed; write steps stay static-only.
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
async function probeStatus(url, fetchImpl) {
|
|
403
|
+
try {
|
|
404
|
+
const response = await fetchImpl(`${url.replace(/\/$/, "")}/status`, { headers: { accept: "application/json" } });
|
|
405
|
+
const body = await response.json();
|
|
406
|
+
if (response.ok && typeof body.posture === "string" && typeof body.version === "string") {
|
|
407
|
+
return { name: "dev-app", ok: true, detail: `/status live (${body.version}, ${body.posture})` };
|
|
408
|
+
}
|
|
409
|
+
return { name: "dev-app", ok: false, detail: `/status returned an invalid composition response (${response.status})` };
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return { name: "dev-app", ok: false, detail: `/status is unreachable at ${url}` };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** A step is live-probeable only when it is READ risk and binds a paramless
|
|
416
|
+
* GET — refine must never mutate the dev app. */
|
|
417
|
+
function liveTarget(binding) {
|
|
418
|
+
if (binding.kind === "route" && binding.method.toUpperCase() === "GET") {
|
|
419
|
+
return { method: "GET", path: binding.path };
|
|
420
|
+
}
|
|
421
|
+
if (binding.kind === "openapi") {
|
|
422
|
+
const method = binding.method?.toUpperCase();
|
|
423
|
+
const path = binding.path;
|
|
424
|
+
if (method === "GET" && typeof path === "string")
|
|
425
|
+
return { method, path };
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
const hasPathParams = (path) => /[{[:]/.test(path);
|
|
430
|
+
async function probeCompound(compound, inputs, statusCheck, origin, fetchImpl) {
|
|
431
|
+
const primitives = mergedPrimitives(inputs);
|
|
432
|
+
const byName = new Map(inputs.tools.map((tool) => [tool.name, tool]));
|
|
433
|
+
const checks = [];
|
|
434
|
+
if (statusCheck !== null)
|
|
435
|
+
checks.push(statusCheck);
|
|
436
|
+
// "verified" requires at least one live STEP check — a live /status alone
|
|
437
|
+
// only proves the composition is up, not that this compound's surface exists.
|
|
438
|
+
let live = false;
|
|
439
|
+
for (const step of compound.binding.steps) {
|
|
440
|
+
const label = `step ${step.id} (${step.tool})`;
|
|
441
|
+
const target = primitives.get(step.tool);
|
|
442
|
+
const extracted = byName.get(step.tool);
|
|
443
|
+
if (target === undefined || extracted === undefined) {
|
|
444
|
+
checks.push({ name: label, ok: false, detail: "not an extracted primitive tool" });
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (target.risk !== "read") {
|
|
448
|
+
checks.push({ name: label, ok: true, detail: `${target.risk} risk — not executed by the probe; validated statically` });
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const request = liveTarget(extracted.binding);
|
|
452
|
+
if (request === null || hasPathParams(request.path) || origin === null || statusCheck?.ok !== true) {
|
|
453
|
+
checks.push({ name: label, ok: true, detail: "read step not live-probeable here; validated statically" });
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const response = await fetchImpl(`${origin}${request.path}`, { headers: { accept: "application/json" } });
|
|
458
|
+
if (response.status === 404 || response.status === 405 || response.status >= 500) {
|
|
459
|
+
checks.push({ name: label, ok: false, detail: `GET ${request.path} → ${response.status}` });
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
const gated = response.status === 401 || response.status === 403 ? " (auth-gated)" : "";
|
|
463
|
+
checks.push({ name: label, ok: true, detail: `GET ${request.path} → ${response.status}${gated}` });
|
|
464
|
+
live = true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
checks.push({ name: label, ok: false, detail: `GET ${request.path} is unreachable` });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// An unreachable dev app degrades to static-only (the diff is still worth
|
|
472
|
+
// presenting); only a STEP-level probe failure marks the compound failed.
|
|
473
|
+
const failed = checks.some((check) => check.name !== "dev-app" && !check.ok);
|
|
474
|
+
return {
|
|
475
|
+
tool: compound.name,
|
|
476
|
+
status: failed ? "failed" : live ? "verified" : "static-only",
|
|
477
|
+
checks,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// Diff building — init's whole-file diff convention.
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
function renderDiff(path, before, after) {
|
|
484
|
+
const oldLines = before === null ? [] : before.trimEnd().split("\n");
|
|
485
|
+
const newLines = after.trimEnd().split("\n");
|
|
486
|
+
return [
|
|
487
|
+
`--- a/${path}`,
|
|
488
|
+
`+++ b/${path}`,
|
|
489
|
+
...oldLines.map((line) => `-${line}`),
|
|
490
|
+
...newLines.map((line) => `+${line}`),
|
|
491
|
+
].join("\n");
|
|
492
|
+
}
|
|
493
|
+
const stringify = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
// The engine.
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
export async function runRefine(options) {
|
|
498
|
+
const root = resolve(options.root);
|
|
499
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
500
|
+
const interview = options.interview ?? [];
|
|
501
|
+
const startedAt = new Date().toISOString();
|
|
502
|
+
const inputs = await loadInputs(root, options);
|
|
503
|
+
const source = await gatherSource(root, options.sourceBudget ?? DEFAULT_SOURCE_BUDGET);
|
|
504
|
+
const proposals = await propose(options.model, inputs, source, interview);
|
|
505
|
+
const normalized = normalize(proposals, inputs);
|
|
506
|
+
// Probe: doctor's /status once, then per-compound verification. Compounds
|
|
507
|
+
// that FAIL their probe are not offered as changes — the loop is
|
|
508
|
+
// propose → probe → present → apply (spec §3).
|
|
509
|
+
const statusCheck = options.url === undefined ? null : await probeStatus(options.url, fetchImpl);
|
|
510
|
+
const origin = options.url === undefined ? null : new URL(options.url).origin;
|
|
511
|
+
const probes = [];
|
|
512
|
+
const verified = [];
|
|
513
|
+
for (const compound of normalized.compounds) {
|
|
514
|
+
const probe = await probeCompound(compound, inputs, statusCheck, origin, fetchImpl);
|
|
515
|
+
probes.push(probe);
|
|
516
|
+
if (probe.status === "failed") {
|
|
517
|
+
normalized.dropped.push({
|
|
518
|
+
kind: "compound",
|
|
519
|
+
target: compound.name,
|
|
520
|
+
reason: `probe failed: ${probe.checks.filter((check) => !check.ok).map((check) => check.detail).join("; ")}`,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
verified.push(compound);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const changes = [];
|
|
528
|
+
const vendoDir = join(root, ".vendo");
|
|
529
|
+
if (verified.length > 0 || normalized.briefs.length > 0) {
|
|
530
|
+
const beforeFile = inputs.capabilities;
|
|
531
|
+
const afterFile = {
|
|
532
|
+
format: VENDO_CAPABILITIES_FORMAT,
|
|
533
|
+
...beforeFile,
|
|
534
|
+
tools: [...(beforeFile?.tools ?? []), ...verified],
|
|
535
|
+
...((beforeFile?.briefs ?? []).length + normalized.briefs.length > 0
|
|
536
|
+
? { briefs: [...(beforeFile?.briefs ?? []), ...normalized.briefs] }
|
|
537
|
+
: {}),
|
|
538
|
+
};
|
|
539
|
+
const before = await readOptionalFile(join(vendoDir, "capabilities.json"));
|
|
540
|
+
const after = stringify(afterFile);
|
|
541
|
+
if (before !== after) {
|
|
542
|
+
changes.push({
|
|
543
|
+
path: ".vendo/capabilities.json",
|
|
544
|
+
before,
|
|
545
|
+
after,
|
|
546
|
+
diff: renderDiff(".vendo/capabilities.json", before, after),
|
|
547
|
+
warnings: [],
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (normalized.overridePatches.size > 0) {
|
|
552
|
+
const beforeFile = inputs.overrides ?? { format: VENDO_OVERRIDES_FORMAT, tools: {} };
|
|
553
|
+
const tools = { ...beforeFile.tools };
|
|
554
|
+
for (const [tool, fields] of normalized.overridePatches) {
|
|
555
|
+
tools[tool] = { ...tools[tool], ...fields };
|
|
556
|
+
}
|
|
557
|
+
const before = await readOptionalFile(join(vendoDir, "overrides.json"));
|
|
558
|
+
const after = stringify({ ...beforeFile, tools });
|
|
559
|
+
if (before !== after) {
|
|
560
|
+
changes.push({
|
|
561
|
+
path: ".vendo/overrides.json",
|
|
562
|
+
before,
|
|
563
|
+
after,
|
|
564
|
+
diff: renderDiff(".vendo/overrides.json", before, after),
|
|
565
|
+
warnings: normalized.overrideWarnings,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (normalized.briefUpdate !== undefined) {
|
|
570
|
+
const before = inputs.brief;
|
|
571
|
+
const after = normalized.briefUpdate.endsWith("\n") ? normalized.briefUpdate : `${normalized.briefUpdate}\n`;
|
|
572
|
+
if (before !== after) {
|
|
573
|
+
changes.push({
|
|
574
|
+
path: ".vendo/brief.md",
|
|
575
|
+
before,
|
|
576
|
+
after,
|
|
577
|
+
diff: renderDiff(".vendo/brief.md", before, after),
|
|
578
|
+
warnings: [],
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const transcript = {
|
|
583
|
+
version: 0,
|
|
584
|
+
startedAt,
|
|
585
|
+
root,
|
|
586
|
+
...(options.url === undefined ? {} : { url: options.url }),
|
|
587
|
+
inputs: {
|
|
588
|
+
tools: inputs.tools.length,
|
|
589
|
+
misses: inputs.misses.length,
|
|
590
|
+
interview,
|
|
591
|
+
sourceFiles: source.files.map((file) => file.path),
|
|
592
|
+
},
|
|
593
|
+
proposals,
|
|
594
|
+
dropped: normalized.dropped,
|
|
595
|
+
probes,
|
|
596
|
+
decisions: [],
|
|
597
|
+
};
|
|
598
|
+
return { changes, probes, dropped: normalized.dropped, transcript };
|
|
599
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RemixableRegistration {
|
|
2
|
+
name: string;
|
|
3
|
+
component: unknown;
|
|
4
|
+
exportable?: boolean;
|
|
5
|
+
remixable?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface RemixableReportOptions {
|
|
8
|
+
/** Vendo wire base. Defaults to the umbrella's standard `/api/vendo`. */
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Marks a host component registration remixable without changing its runtime
|
|
13
|
+
* shape. Passing `import.meta.url` lets a development Vendo server capture the
|
|
14
|
+
* registration module when static sync cannot resolve the component.
|
|
15
|
+
*/
|
|
16
|
+
export declare function remixable<T extends RemixableRegistration>(registration: T, moduleSource: string, options?: RemixableReportOptions): T & {
|
|
17
|
+
remixable: true;
|
|
18
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function developmentBrowser() {
|
|
2
|
+
if (typeof window === "undefined")
|
|
3
|
+
return false;
|
|
4
|
+
// An explicit NODE_ENV (Next/webpack process shim) is authoritative. Vite
|
|
5
|
+
// browsers have no process global — only the process.env.NODE_ENV
|
|
6
|
+
// *expression* is statically replaced, and not in every configuration — so
|
|
7
|
+
// fall back to the bundler-native import.meta.env surface.
|
|
8
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== undefined) {
|
|
9
|
+
return process.env.NODE_ENV === "development";
|
|
10
|
+
}
|
|
11
|
+
const metaEnv = import.meta.env;
|
|
12
|
+
return metaEnv?.DEV === true || metaEnv?.MODE === "development";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Marks a host component registration remixable without changing its runtime
|
|
16
|
+
* shape. Passing `import.meta.url` lets a development Vendo server capture the
|
|
17
|
+
* registration module when static sync cannot resolve the component.
|
|
18
|
+
*/
|
|
19
|
+
export function remixable(registration, moduleSource, options = {}) {
|
|
20
|
+
const marked = { ...registration, remixable: true };
|
|
21
|
+
if (developmentBrowser()) {
|
|
22
|
+
const baseUrl = (options.baseUrl ?? "/api/vendo").replace(/\/$/, "");
|
|
23
|
+
queueMicrotask(() => {
|
|
24
|
+
void globalThis.fetch(`${baseUrl}/dev/remixable-source`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
credentials: "same-origin",
|
|
27
|
+
headers: { "content-type": "application/json" },
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
slot: registration.name,
|
|
30
|
+
source: moduleSource,
|
|
31
|
+
exportable: registration.exportable === true,
|
|
32
|
+
}),
|
|
33
|
+
}).then((response) => {
|
|
34
|
+
if (!response.ok)
|
|
35
|
+
throw new Error(`runtime capture returned ${response.status}`);
|
|
36
|
+
}).catch((error) => {
|
|
37
|
+
console.warn(`Vendo could not runtime-capture remixable slot ${registration.name}:`, error);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return marked;
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RuntimeCaptureRegistration {
|
|
2
|
+
slot: string;
|
|
3
|
+
source: string;
|
|
4
|
+
exportable: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface RuntimeCaptureResult {
|
|
7
|
+
slot: string;
|
|
8
|
+
hash: string;
|
|
9
|
+
status: "captured" | "unchanged" | "preserved";
|
|
10
|
+
}
|
|
11
|
+
export interface RuntimeCaptureHandler {
|
|
12
|
+
capture(registration: RuntimeCaptureRegistration): Promise<RuntimeCaptureResult>;
|
|
13
|
+
}
|
|
14
|
+
/** Node-only capture primitive. Returning null keeps edge/Worker bundles file-system free. */
|
|
15
|
+
export declare function createRuntimeCapture(config: {
|
|
16
|
+
root?: string;
|
|
17
|
+
out?: string;
|
|
18
|
+
}): RuntimeCaptureHandler | null;
|