Skip to content

Commit 9990ea6

Browse files
authored
Merge pull request #1227 from Automattic/cook/generic-runtime-profile-compiler-20260619
Add generic runtime profile compiler
2 parents 303efce + ae9dbe1 commit 9990ea6

6 files changed

Lines changed: 271 additions & 9 deletions

File tree

docs/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ unless this index says otherwise.
2929
- [Benchmark contract](./benchmark-contract.md) documents benchmark evidence
3030
shape without making benchmark scoring a core runtime concern.
3131

32+
## Example Consumer Integration Notes
33+
34+
- [Example consumer boundary contracts](./example-consumer-boundary-contracts.md)
35+
documents runtime profile, preview lease, and browser session handoff seams for
36+
host adapters. Named products in that note are examples only; the runtime
37+
contracts remain caller-neutral.
38+
3239
## Audits And Historical Plans
3340

3441
- [Browser runtime dependency audit](./browser-runtime-dependency-audit.md) is a
Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
# Studio/Homeboy Boundary Contracts
1+
# Example Consumer Boundary Contracts
22

3-
WP Codebox exposes small public seams for hosts that need to prepare browser-backed WordPress sandboxes without reading raw task payloads.
3+
WP Codebox exposes small public seams for hosts that need to prepare
4+
browser-backed WordPress sandboxes without reading raw task payloads. The seams
5+
are generic runtime contracts. Named products may appear in integration notes as
6+
example consumers, but they are not runtime concepts and must not define schema
7+
fields, package boundaries, or artifact semantics.
48

59
## Runtime Profile
610

7-
`runtime-core` exports `RUNTIME_PROFILE_SCHEMA`, `RuntimeProfile`, and `runtimeProfile()` from `@automattic/wp-codebox-core` and `wp-codebox-workspace/core/contracts`.
11+
`runtime-core` exports `RUNTIME_PROFILE_SCHEMA`, `RuntimeProfile`, and
12+
`runtimeProfile()` from `@automattic/wp-codebox-core` and
13+
`wp-codebox-workspace/core/contracts`.
814

915
Shape: `wp-codebox/runtime-profile/v1`.
1016

@@ -18,23 +24,37 @@ Shape: `wp-codebox/runtime-profile/v1`.
1824

1925
## Preview Lease
2026

21-
`runtime-core` exports `PREVIEW_LEASE_SCHEMA`, `PreviewLease`, and `previewLease()`.
27+
`runtime-core` exports `PREVIEW_LEASE_SCHEMA`, `PreviewLease`, and
28+
`previewLease()`.
2229

2330
Shape: `wp-codebox/preview-lease/v1`.
2431

2532
- `preview_public_url`: reviewer/public preview URL when leased by a host.
2633
- `site_url`: canonical WordPress site URL.
2734
- `local_url`: local browser/Playground URL.
2835
- `lease`: lease id, status, provider, owner, and timestamps.
29-
- `alignment`: evidence that preview, site, and local URLs point at the same runtime.
36+
- `alignment`: evidence that preview, site, and local URLs point at the same
37+
runtime.
3038

31-
## Browser Product DTOs
39+
## Browser Session DTOs
3240

33-
The WordPress plugin exposes these helpers for Studio Native and Homeboy integrations:
41+
The WordPress plugin exposes these helpers for host integrations that need a
42+
bounded browser session handoff:
3443

3544
- `WP_Codebox_Browser_Task_Builder::product_browser_session_dto( $session )`
3645
- `WP_Codebox_Browser_Task_Builder::browser_preview_boot_config( $session )`
3746
- `wp_codebox_browser_session_product_dto` filter
3847
- `wp_codebox_browser_preview_boot_config` filter
3948

40-
The DTOs include session identity, task label, target, preview boot config, preview lease/alignment data, artifact refs, and readiness signals. They intentionally omit raw `task_payload`, raw blueprint bodies, plugin package data, runtime source bundles, and secret-like fields.
49+
The DTOs include session identity, task label, target, preview boot config,
50+
preview lease/alignment data, artifact refs, and readiness signals. They
51+
intentionally omit raw `task_payload`, raw blueprint bodies, plugin package data,
52+
runtime source bundles, and secret-like fields.
53+
54+
## Example Consumers
55+
56+
Studio Web, Homeboy, Static Site Importer, hosted services, CI jobs, local tools,
57+
and other callers can consume these seams through their own adapters. Those
58+
adapters own product-specific defaults, queue state, deploy behavior, import
59+
semantics, and review UX. WP Codebox owns the generic runtime profile, preview
60+
lease, browser session DTO, and artifact boundaries.

packages/runtime-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from "./artifact-export-links.js"
1111
export * from "./runtime-contracts.js"
1212
export * from "./runtime-neutral-contracts.js"
1313
export * from "./runtime-boundary-contracts.js"
14+
export * from "./runtime-profile-compiler.js"
1415
export * from "./runtime-command-result.js"
1516
export * from "./runtime-backend-resolver.js"
1617
export * from "./runtime-defaults.js"
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { runtimeDependencyPlanContract } from "./agent-task-recipe.js"
2+
import { runtimeProfile, type RuntimeProfile, type RuntimeProfileDependency, type RuntimeProfileReadiness } from "./runtime-boundary-contracts.js"
3+
import type { WorkspaceRecipeExtraPlugin } from "./runtime-contracts.js"
4+
import { isPlainObject, stripUndefined } from "./object-utils.js"
5+
6+
export interface RuntimeProfileDependencyPlan {
7+
schema: "wp-codebox/runtime-dependency-plan/v1"
8+
[key: string]: unknown
9+
}
10+
11+
export interface RuntimeProfileExecutionPlan {
12+
schema: "wp-codebox/runtime-profile-execution-plan/v1"
13+
dependency_plan: RuntimeProfileDependencyPlan
14+
extra_plugins?: WorkspaceRecipeExtraPlugin[]
15+
runtime_overlays?: Array<Record<string, unknown>>
16+
runtime_env?: Record<string, string>
17+
readiness?: RuntimeProfileReadiness
18+
}
19+
20+
export interface RuntimeProfilePreflight {
21+
schema: "wp-codebox/runtime-profile-preflight/v1"
22+
readiness: RuntimeProfileReadiness
23+
}
24+
25+
export function compileRuntimeProfile(input: unknown): RuntimeProfileExecutionPlan {
26+
const profile = runtimeProfile(input)
27+
const componentPlugins = [
28+
...dependencyPlugins(profile.components, "component", "plugin"),
29+
...dependencyPlugins(profile.plugins, "plugin", "plugin"),
30+
...dependencyPlugins(profile.mu_plugins, "mu_plugin", "mu-plugin"),
31+
...objectPlugins(profile.extra_plugins),
32+
...objectPlugins(profile.component_contracts),
33+
]
34+
const runtimeOverlays = [
35+
...overlayDependencies(profile.overlays),
36+
...(profile.runtime_overlays ?? []),
37+
]
38+
const readiness = runtimeProfilePreflight(profile).readiness
39+
40+
return stripUndefined({
41+
schema: "wp-codebox/runtime-profile-execution-plan/v1",
42+
dependency_plan: runtimeDependencyPlanContract({
43+
provider_plugins: objectPlugins(profile.provider_plugins),
44+
component_plugins: componentPlugins,
45+
runtime_overlays: runtimeOverlays,
46+
runtime_env: profile.env,
47+
}) as RuntimeProfileDependencyPlan,
48+
extra_plugins: componentPlugins.length > 0 ? componentPlugins : undefined,
49+
runtime_overlays: runtimeOverlays.length > 0 ? runtimeOverlays : undefined,
50+
runtime_env: profile.env,
51+
readiness,
52+
}) as RuntimeProfileExecutionPlan
53+
}
54+
55+
export function runtimeProfilePreflight(input: unknown): RuntimeProfilePreflight {
56+
const profile = isRuntimeProfile(input) ? input : runtimeProfile(input)
57+
return {
58+
schema: "wp-codebox/runtime-profile-preflight/v1",
59+
readiness: profile.readiness ?? inferredReadiness(profile),
60+
}
61+
}
62+
63+
function dependencyPlugins(dependencies: RuntimeProfileDependency[] | undefined, kind: string, loadAs: "plugin" | "mu-plugin"): WorkspaceRecipeExtraPlugin[] {
64+
return (dependencies ?? []).flatMap((dependency) => {
65+
if (!dependency.source) return []
66+
return [stripUndefined({
67+
source: dependency.source,
68+
slug: dependency.slug,
69+
pluginFile: stringField(dependency.metadata, "pluginFile"),
70+
activate: dependency.activate ?? (loadAs === "plugin" ? true : undefined),
71+
loadAs,
72+
metadata: stripUndefined({
73+
runtimeProfileDependency: stripUndefined({
74+
kind,
75+
target: dependency.target,
76+
required: dependency.required,
77+
readiness: dependency.readiness,
78+
provenance: dependency.provenance,
79+
}),
80+
...(dependency.metadata ?? {}),
81+
}),
82+
})]
83+
})
84+
}
85+
86+
function objectPlugins(entries: Array<Record<string, unknown>> | undefined): WorkspaceRecipeExtraPlugin[] {
87+
return (entries ?? []).flatMap((entry) => {
88+
const source = stringField(entry, "source") || stringField(entry, "path")
89+
if (!source) return []
90+
const loadAs: "plugin" | "mu-plugin" = entry.loadAs === "mu-plugin" || entry.load_as === "mu-plugin" ? "mu-plugin" : "plugin"
91+
return [stripUndefined({
92+
source,
93+
slug: stringField(entry, "slug") || stringField(entry, "component") || stringField(entry, "name"),
94+
pluginFile: stringField(entry, "pluginFile") || stringField(entry, "plugin_file"),
95+
activate: typeof entry.activate === "boolean" ? entry.activate : loadAs === "plugin" ? true : undefined,
96+
sha256: stringField(entry, "sha256"),
97+
loadAs,
98+
metadata: isPlainObject(entry.metadata) ? entry.metadata : undefined,
99+
})]
100+
})
101+
}
102+
103+
function overlayDependencies(dependencies: RuntimeProfileDependency[] | undefined): Array<Record<string, unknown>> {
104+
return (dependencies ?? []).map((dependency) => stripUndefined({
105+
kind: dependency.kind,
106+
slug: dependency.slug,
107+
source: dependency.source,
108+
target: dependency.target,
109+
activate: dependency.activate,
110+
required: dependency.required,
111+
readiness: dependency.readiness,
112+
provenance: dependency.provenance,
113+
metadata: dependency.metadata,
114+
}))
115+
}
116+
117+
function inferredReadiness(profile: RuntimeProfile): RuntimeProfileReadiness {
118+
const dependencies = [
119+
...profile.components,
120+
...(profile.plugins ?? []),
121+
...(profile.mu_plugins ?? []),
122+
...(profile.themes ?? []),
123+
...(profile.overlays ?? []),
124+
]
125+
const missing = dependencies
126+
.filter((dependency) => dependency.required !== false && (dependency.readiness === "missing" || !dependency.source))
127+
.map((dependency) => `${dependency.kind}:${dependency.slug}`)
128+
const blocked = dependencies.some((dependency) => dependency.readiness === "blocked")
129+
return {
130+
status: blocked ? "blocked" : missing.length > 0 ? "missing" : "ready",
131+
checks: {
132+
dependencies: missing.length === 0 && !blocked,
133+
},
134+
missing,
135+
}
136+
}
137+
138+
function isRuntimeProfile(input: unknown): input is RuntimeProfile {
139+
return isPlainObject(input) && input.schema === "wp-codebox/runtime-profile/v1" && Array.isArray(input.components)
140+
}
141+
142+
function stringField(record: Record<string, unknown> | undefined, key: string): string | undefined {
143+
if (!record) return undefined
144+
const value = record[key]
145+
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined
146+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import assert from "node:assert/strict"
2+
import { readFile } from "node:fs/promises"
3+
4+
const forbiddenConsumerTerms = [
5+
/\bStudio Web\b/,
6+
/\bStudio Native\b/,
7+
/\bHomeboy\b/,
8+
/\bStatic Site Importer\b/,
9+
]
10+
11+
const genericContractDocs = [
12+
"docs/architecture.md",
13+
"docs/recipe-contract.md",
14+
"docs/sandbox-session-contract.md",
15+
"docs/external-apply-adapter-contract.md",
16+
"docs/agent-fanout-contract.md",
17+
"docs/agent-runtime-contract.md",
18+
"docs/generic-runtime-primitives.md",
19+
"docs/portable-wp-codebox.md",
20+
"docs/benchmark-contract.md",
21+
]
22+
23+
const root = new URL("..", import.meta.url)
24+
const violations: string[] = []
25+
26+
for (const doc of genericContractDocs) {
27+
const source = await readFile(new URL(doc, root), "utf8")
28+
29+
for (const term of forbiddenConsumerTerms) {
30+
if (term.test(source)) {
31+
violations.push(`${doc} contains ${term}`)
32+
}
33+
}
34+
}
35+
36+
assert.deepEqual(
37+
violations,
38+
[],
39+
"Generic boundary docs must not name example consumers as runtime concepts; keep named products in explicit example-consumer notes.",
40+
)
41+
42+
const exampleConsumerDoc = await readFile(new URL("docs/example-consumer-boundary-contracts.md", root), "utf8")
43+
44+
assert.match(exampleConsumerDoc, /^# Example Consumer Boundary Contracts/m)
45+
assert.match(exampleConsumerDoc, /Named products may appear in integration notes as\s+example consumers/)
46+
assert.match(exampleConsumerDoc, /## Example Consumers/)
47+
48+
console.log("docs boundary language ok")

tests/runtime-boundary-contracts.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from "node:assert/strict"
22

3-
import { BROWSER_CONTAINED_SITE_STATUS_SCHEMA, PREVIEW_LEASE_SCHEMA, RUNTIME_PROFILE_SCHEMA, browserContainedSiteStatus, previewLease, previewLeaseStatus, runtimeProfile } from "../packages/runtime-core/src/index.js"
3+
import { BROWSER_CONTAINED_SITE_STATUS_SCHEMA, PREVIEW_LEASE_SCHEMA, RUNTIME_PROFILE_SCHEMA, browserContainedSiteStatus, compileRuntimeProfile, previewLease, previewLeaseStatus, runtimeProfile, runtimeProfilePreflight } from "../packages/runtime-core/src/index.js"
44

55
const profile = runtimeProfile({
66
schema: RUNTIME_PROFILE_SCHEMA,
@@ -20,6 +20,46 @@ assert.equal(profile.components[0].slug, "agents-api")
2020
assert.deepEqual(profile.bootstrap?.steps, ["install", "activate"])
2121
assert.deepEqual(profile.env, { WP_ENVIRONMENT_TYPE: "local" })
2222

23+
const executionPlan = compileRuntimeProfile({
24+
schema: RUNTIME_PROFILE_SCHEMA,
25+
components: [{ kind: "component", slug: "runtime-component", source: "/workspace/runtime-component", required: true }],
26+
plugins: [{ kind: "plugin", slug: "runtime-plugin", source: "/workspace/runtime-plugin", activate: false, metadata: { pluginFile: "runtime-plugin/runtime-plugin.php" } }],
27+
mu_plugins: [{ kind: "mu_plugin", slug: "runtime-loader", source: "/workspace/runtime-loader" }],
28+
overlays: [{ kind: "overlay", slug: "runtime-overlay", source: "/workspace/runtime-overlay", target: "/runtime/overlay" }],
29+
runtime_overlays: [{ kind: "library", library: "generic-runtime", source: "/workspace/overlay", strategy: "copy" }],
30+
env: { WP_ENVIRONMENT_TYPE: "local" },
31+
})
32+
33+
assert.deepEqual(executionPlan.extra_plugins, [
34+
{
35+
source: "/workspace/runtime-component",
36+
slug: "runtime-component",
37+
activate: true,
38+
loadAs: "plugin",
39+
metadata: { runtimeProfileDependency: { kind: "component", required: true } },
40+
},
41+
{
42+
source: "/workspace/runtime-plugin",
43+
slug: "runtime-plugin",
44+
pluginFile: "runtime-plugin/runtime-plugin.php",
45+
activate: false,
46+
loadAs: "plugin",
47+
metadata: { runtimeProfileDependency: { kind: "plugin" }, pluginFile: "runtime-plugin/runtime-plugin.php" },
48+
},
49+
{
50+
source: "/workspace/runtime-loader",
51+
slug: "runtime-loader",
52+
loadAs: "mu-plugin",
53+
metadata: { runtimeProfileDependency: { kind: "mu_plugin" } },
54+
},
55+
])
56+
assert.deepEqual(executionPlan.dependency_plan.component_plugins, executionPlan.extra_plugins)
57+
assert.deepEqual(executionPlan.dependency_plan.runtime_overlays, [
58+
{ kind: "overlay", slug: "runtime-overlay", source: "/workspace/runtime-overlay", target: "/runtime/overlay" },
59+
{ kind: "library", library: "generic-runtime", source: "/workspace/overlay", strategy: "copy" },
60+
])
61+
assert.deepEqual(runtimeProfilePreflight({ schema: RUNTIME_PROFILE_SCHEMA, components: [{ kind: "component", slug: "missing-runtime-component", required: true }] }).readiness.status, "missing")
62+
2363
const lease = previewLease({
2464
schema: PREVIEW_LEASE_SCHEMA,
2565
preview_public_url: "https://preview.example.test",

0 commit comments

Comments
 (0)