chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
# `_parity/` — integration-demo parity tooling
Keeps `examples/integrations/*` demos aligned to a single north-star so
drift doesn't pile up as the canonical demo evolves.
**North-star (v1):** `langgraph-python` — the richest demo (todos state, 5
tools, polished prompt, full frontend canvas). Every other integration demo
should track it.
## What gets tracked
Declared in [`manifest.json`](./manifest.json):
- **verbatim files** — copied byte-for-byte from north-star to each instance
(frontend components, hooks, lib, public assets, shared Docker files,
`docker-compose.test.yml`, `entrypoint.sh`, `postcss.config.mjs`, etc.)
- **package.json keys** — tracked dependency versions and script names
(`@copilotkit/*`, `next`, `react`, shared dev scripts). Per-instance
overrides in manifest (`packageJsonOverrides`) win where the instance
legitimately differs (e.g. `dev:agent` runs `npm install` in JS but
`uv sync` in FastAPI).
- **canonical prompt** — `_parity/canonical/PROMPT.md`. Each agent
**inlines** the prompt as a string literal in its source (matching the
north-star's `main.py` pattern). Verifier greps the canonical prompt's
first non-blank line against instance agent source — drift = error.
- **agent surface** — tool names + state keys expected to appear in each
instance's agent source. Grep-level check — doesn't validate call-site
correctness, that's the aimock fixture tests' job.
## What doesn't get tracked (allowed divergence)
Per-instance `allowedDivergence` list in `manifest.json`:
- `agent/**` — agents are written in different languages/runtimes (Python
create_agent, TS StateGraph, Python StateGraph+FastAPI). Human-authored.
- `src/app/api/copilotkit/**` — north-star uses `LangGraphAgent`, Docker
instances use `HttpAgent`. Different routes.
- `Dockerfile`, `docker/Dockerfile.agent`, `serve.py`, `scripts/**`
language-specific build/run tooling.
Anything outside both `tracked` and `allowedDivergence` is "no-op" — the
verifier neither checks nor touches it.
## Commands
From the repo root:
```bash
# Sync a single instance to north-star (copies verbatim files, rewrites
# package.json keys, writes canonical prompt to agent/PROMPT.md)
pnpm parity:sync --target=langgraph-js
# Dry-run: show what would change without writing
pnpm parity:sync --target=langgraph-js --dry-run
# Sync every non-north-star instance
pnpm parity:sync --all
# Verify — exits non-zero on unexpected drift
pnpm parity:verify
pnpm parity:verify --target=langgraph-js
# CI invocation (same as verify, no color)
pnpm parity:check
```
## Typical workflows
### North-star changed — sync instances
```bash
pnpm parity:sync --all
pnpm parity:verify
# fix any agent-surface drift manually in the relevant agent/src/ files
git add . && git commit
```
### Adding a new instance
1. Create the new demo under `examples/integrations/<name>/` with a
Next.js frontend at the root and an `agent/` dir.
2. Add an entry to `manifest.json` under `instances`:
```json
"new-demo": {
"role": "instance",
"agent": { "language": "python", "runtime": "..." },
"allowedDivergence": ["agent/**", "src/app/api/copilotkit/**",
"Dockerfile", "docker/Dockerfile.agent",
"serve.py", "scripts/**"],
"packageJsonOverrides": { "scripts.dev:agent": "..." }
}
```
3. Run `pnpm parity:sync --target=new-demo`.
4. Hand-port the agent code (tools, state, prompt loading) — manifest
`tracked.agentSurface` tells you what tool names and state keys are
required.
5. Run `pnpm parity:verify --target=new-demo` until green.
### North-star's agent surface changed
Edit `manifest.json` → `tracked.agentSurface.toolNames` / `stateKeys`.
The verifier will then flag every instance that hasn't caught up.
### Canonical prompt changed
1. Edit `_parity/canonical/PROMPT.md`.
2. Update north-star's `agent/main.py` to use the new prompt string
(north-star is where humans read; canonical file is what the
verifier reads).
3. Port the new prompt into every instance's agent source (same
manual-merge rules as any agent change).
4. `pnpm parity:verify` — verifier greps first line of canonical against
each instance's agent source. Passes when all instances inline it.
## CI
`.github/workflows/integrations_parity.yml` runs `pnpm parity:check` on
every PR that touches `examples/integrations/**`. Failures link the
contributor back to this README.
## Design notes
- **Declarative, not prescriptive.** The manifest says _what_ is tracked;
the scripts just walk it. Adding a new tracked file = one line change,
not a code change.
- **Allowed-divergence is explicit, not implicit.** Everything is either
tracked, declared-divergent, or ignored — no silent "probably different"
state.
- **Agent surface is grep-level, on purpose.** A real AST check would be
three times the code and still miss semantic drift. The existing aimock
fixture integration tests (under `fixtures/default.json` per instance)
are the real correctness check; this is the "did someone rip out
`manage_todos`" safety net.
- **North-star is read-only to the scripts.** `sync.ts` refuses to write
into the north-star directory even if asked. Verifier never touches
anything.
@@ -0,0 +1,11 @@
You are a polished, professional demo assistant. Keep responses to 1-2 sentences.
Tool guidance:
- Flights: call search_flights to show flight cards with a pre-built schema.
- Dashboards & rich UI: call generate_a2ui to create dashboard UIs with metrics,
charts, tables, and cards. It handles rendering automatically.
- Charts: call query_data first, then render with the chart component.
- Todos: enable app mode first, then manage todos.
- A2UI actions: when you see a log_a2ui_event result (e.g. "view_details"),
respond with a brief confirmation. The UI already updated on the frontend.
+47
View File
@@ -0,0 +1,47 @@
import { createHash } from "node:crypto";
import { readFileSync, statSync } from "node:fs";
export function sha256(buf: Buffer | string): string {
return createHash("sha256").update(buf).digest("hex");
}
export function fileSha256(path: string): string {
return sha256(readFileSync(path));
}
export function fileExists(path: string): boolean {
try {
statSync(path);
return true;
} catch {
return false;
}
}
export function getByPath(obj: unknown, path: string): unknown {
const parts = path.split(".");
let cur: unknown = obj;
for (const part of parts) {
if (cur == null || typeof cur !== "object") return undefined;
cur = (cur as Record<string, unknown>)[part];
}
return cur;
}
export function setByPath(
obj: Record<string, unknown>,
path: string,
value: unknown,
): void {
const parts = path.split(".");
let cur: Record<string, unknown> = obj;
for (let i = 0; i < parts.length - 1; i++) {
const key = parts[i]!;
const next = cur[key];
if (next == null || typeof next !== "object" || Array.isArray(next)) {
cur[key] = {};
}
cur = cur[key] as Record<string, unknown>;
}
cur[parts[parts.length - 1]!] = value;
}
@@ -0,0 +1,96 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
export type InstanceRole = "north-star" | "instance";
export interface AgentSurface {
toolNames: string[];
stateKeys: string[];
modelFamily: string;
}
export interface TrackedSurface {
verbatimFiles: string[];
packageJsonPaths: string[];
agentSurface: AgentSurface;
}
export interface InstanceAgent {
language: "python" | "typescript";
runtime: string;
}
export interface Instance {
role: InstanceRole;
agent: InstanceAgent;
allowedDivergence: string[];
packageJsonOverrides: Record<string, string>;
}
export interface Manifest {
version: number;
northStar: string;
canonicalPromptFile: string;
tracked: TrackedSurface;
instances: Record<string, Instance>;
}
export interface ParityRoot {
/** Absolute path to the parity dir, e.g. …/examples/integrations/_parity */
parityDir: string;
/** Absolute path to the integrations root, e.g. …/examples/integrations */
integrationsDir: string;
manifest: Manifest;
}
export function loadManifest(parityDir: string): ParityRoot {
const manifestPath = resolve(parityDir, "manifest.json");
const raw = readFileSync(manifestPath, "utf8");
const parsed = JSON.parse(raw) as Manifest;
validate(parsed, manifestPath);
return {
parityDir,
integrationsDir: resolve(parityDir, ".."),
manifest: parsed,
};
}
function validate(m: Manifest, source: string): void {
const err = (msg: string): never => {
throw new Error(`[parity] invalid manifest at ${source}: ${msg}`);
};
if (m.version !== 1) err(`unsupported version ${m.version}`);
if (!m.northStar) err("missing northStar");
if (!m.instances?.[m.northStar])
err(`northStar '${m.northStar}' not in instances`);
if (m.instances[m.northStar].role !== "north-star")
err(`northStar '${m.northStar}' must have role=north-star`);
const nonNorthStar = Object.entries(m.instances).filter(
([n]) => n !== m.northStar,
);
for (const [name, inst] of nonNorthStar) {
if (inst.role !== "instance")
err(`instance '${name}' must have role=instance`);
}
if (!m.tracked?.verbatimFiles?.length) err("tracked.verbatimFiles empty");
if (!m.tracked?.packageJsonPaths?.length)
err("tracked.packageJsonPaths empty");
if (!m.tracked?.agentSurface?.toolNames?.length)
err("tracked.agentSurface.toolNames empty");
}
export function instanceDir(root: ParityRoot, name: string): string {
return resolve(root.integrationsDir, name);
}
export function northStarDir(root: ParityRoot): string {
return instanceDir(root, root.manifest.northStar);
}
export function listInstances(root: ParityRoot): string[] {
return Object.keys(root.manifest.instances).filter(
(n) => n !== root.manifest.northStar,
);
}
@@ -0,0 +1,77 @@
export type Severity = "ok" | "warn" | "error";
export interface DriftItem {
severity: Severity;
instance: string;
kind:
| "verbatim-file"
| "package-json"
| "agent-tool"
| "agent-state"
| "prompt"
| "missing-instance";
subject: string;
detail?: string;
expected?: string;
actual?: string;
}
export interface Report {
instance: string;
items: DriftItem[];
}
export function hasErrors(reports: Report[]): boolean {
return reports.some((r) => r.items.some((i) => i.severity === "error"));
}
const RESET = "\x1b[0m";
const RED = "\x1b[31m";
const YEL = "\x1b[33m";
const GRN = "\x1b[32m";
const BOLD = "\x1b[1m";
const DIM = "\x1b[2m";
function color(enabled: boolean, code: string, s: string): string {
return enabled ? `${code}${s}${RESET}` : s;
}
export function printReports(reports: Report[], useColor = true): void {
for (const r of reports) {
const errors = r.items.filter((i) => i.severity === "error");
const warns = r.items.filter((i) => i.severity === "warn");
const oks = r.items.filter((i) => i.severity === "ok");
const header = color(
useColor,
BOLD,
`\n${r.instance}${oks.length} ok, ${warns.length} warn, ${errors.length} error`,
);
process.stdout.write(header + "\n");
for (const item of [...errors, ...warns]) {
const tag =
item.severity === "error"
? color(useColor, RED, " ✗")
: color(useColor, YEL, " !");
const kindStr = color(useColor, DIM, `[${item.kind}]`);
process.stdout.write(`${tag} ${kindStr} ${item.subject}\n`);
if (item.detail) process.stdout.write(` ${item.detail}\n`);
if (item.expected !== undefined || item.actual !== undefined) {
if (item.expected !== undefined)
process.stdout.write(` expected: ${truncate(item.expected)}\n`);
if (item.actual !== undefined)
process.stdout.write(` actual: ${truncate(item.actual)}\n`);
}
}
if (errors.length === 0 && warns.length === 0) {
process.stdout.write(color(useColor, GRN, " ✓ no drift\n"));
}
}
}
function truncate(s: string, max = 120): string {
if (s.length <= max) return s;
return s.slice(0, max) + "…";
}
+237
View File
@@ -0,0 +1,237 @@
{
"version": 1,
"northStar": "langgraph-python",
"canonicalPromptFile": "_parity/canonical/PROMPT.md",
"tracked": {
"verbatimFiles": [
"postcss.config.mjs",
"tsconfig.json",
"next.config.ts",
"next-env.d.ts",
"public/copilotkit-logo-mark.svg",
"public/copilotkit-logo.svg",
"public/file.svg",
"public/globe.svg",
"public/next.svg",
"public/vercel.svg",
"public/window.svg",
"src/app/favicon.ico",
"src/app/globals.css",
"src/app/layout.tsx",
"src/app/page.tsx",
"src/app/declarative-generative-ui/definitions.ts",
"src/app/declarative-generative-ui/renderers.tsx",
"src/components/example-canvas/index.tsx",
"src/components/example-canvas/todo-card.tsx",
"src/components/example-canvas/todo-column.tsx",
"src/components/example-canvas/todo-list.tsx",
"src/components/example-layout/index.tsx",
"src/components/example-layout/mode-toggle.tsx",
"src/components/generative-ui/charts/bar-chart.tsx",
"src/components/generative-ui/charts/config.ts",
"src/components/generative-ui/charts/pie-chart.tsx",
"src/components/generative-ui/meeting-time-picker.tsx",
"src/components/headless-chat.tsx",
"src/components/tool-rendering.tsx",
"src/components/threads-drawer/index.tsx",
"src/components/threads-drawer/locked-state.tsx",
"src/components/threads-drawer/threads-drawer.module.css",
"src/components/threads-drawer/threads-drawer.tsx",
"src/components/ui/badge.tsx",
"src/components/ui/button.tsx",
"src/components/ui/card.tsx",
"src/components/ui/checkbox.tsx",
"src/components/ui/input.tsx",
"src/components/ui/separator.tsx",
"src/components/ui/spinner.tsx",
"src/hooks/index.ts",
"src/hooks/use-example-suggestions.tsx",
"src/hooks/use-generative-ui-examples.tsx",
"src/hooks/use-theme.tsx",
"src/lib/a2ui-theme.css",
"src/lib/utils.ts",
"Dockerfile",
"docker/Dockerfile.app",
"docker/Dockerfile.agent",
"docker-compose.test.yml",
"docker-route-override.ts",
"entrypoint.sh",
"serve.py",
"showcase.json"
],
"packageJsonPaths": [
"scripts.dev",
"scripts.dev:debug",
"scripts.dev:ui",
"scripts.build",
"scripts.start",
"scripts.postinstall",
"dependencies.@copilotkit/react-core",
"dependencies.@copilotkit/react-ui",
"dependencies.@copilotkit/runtime",
"dependencies.@copilotkit/shared",
"dependencies.@copilotkit/a2ui-renderer",
"dependencies.@radix-ui/react-checkbox",
"dependencies.@radix-ui/react-label",
"dependencies.@radix-ui/react-separator",
"dependencies.class-variance-authority",
"dependencies.clsx",
"dependencies.hono",
"dependencies.lucide-react",
"dependencies.next",
"dependencies.react",
"dependencies.react-dom",
"dependencies.react-rnd",
"dependencies.react18-json-view",
"dependencies.recharts",
"dependencies.tailwind-merge",
"dependencies.zod",
"devDependencies.@tailwindcss/postcss",
"devDependencies.@types/node",
"devDependencies.@types/react",
"devDependencies.@types/react-dom",
"devDependencies.concurrently",
"devDependencies.tailwindcss",
"devDependencies.typescript"
],
"agentSurface": {
"toolNames": ["query_data", "generate_a2ui", "search_flights"],
"stateKeys": ["todos", "messages"],
"modelFamily": "openai"
}
},
"instances": {
"langgraph-python": {
"role": "north-star",
"agent": {
"language": "python",
"runtime": "langgraph-create-agent"
},
"allowedDivergence": [],
"packageJsonOverrides": {}
},
"langgraph-js": {
"role": "instance",
"agent": {
"language": "typescript",
"runtime": "langgraph-stategraph"
},
"allowedDivergence": [
"agent/**",
"src/app/api/copilotkit/**",
"Dockerfile",
"docker/Dockerfile.agent",
"docker-compose.test.yml",
"entrypoint.sh",
"serve.py",
"scripts/**",
"src/components/threads-drawer/**",
"src/app/page.tsx",
"next.config.ts"
],
"packageJsonOverrides": {
"scripts.dev:agent": "./scripts/run-agent.sh || scripts\\run-agent.bat",
"scripts.install:agent": "cd agent && npm install"
}
},
"langgraph-fastapi": {
"role": "instance",
"agent": {
"language": "python",
"runtime": "fastapi-langgraph"
},
"allowedDivergence": [
"agent/**",
"src/app/api/copilotkit/**",
"docker/Dockerfile.agent",
"docker-compose.test.yml",
"scripts/**"
],
"packageJsonOverrides": {
"scripts.dev:agent": "cd agent && uv run main.py",
"scripts.install:agent": "cd agent && uv sync"
}
},
"strands-python": {
"role": "instance",
"agent": {
"language": "python",
"runtime": "strands-ag-ui"
},
"allowedDivergence": [
"agent/**",
"src/app/api/copilotkit/**",
"Dockerfile",
"docker/Dockerfile.agent",
"docker-compose.test.yml",
"entrypoint.sh",
"serve.py",
"scripts/**"
],
"packageJsonOverrides": {
"scripts.dev:agent": "./scripts/run-agent.sh || scripts\\run-agent.bat",
"scripts.install:agent": "./scripts/setup-agent.sh || scripts\\setup-agent.bat"
}
},
"claude-sdk-python": {
"role": "instance",
"agent": {
"language": "python",
"runtime": "claude-agent-sdk"
},
"allowedDivergence": [
"agent/**",
"src/app/api/copilotkit/**",
"Dockerfile",
"docker/Dockerfile.agent",
"docker-compose.test.yml",
"entrypoint.sh",
"serve.py",
"scripts/**",
"docker-route-override.ts",
"src/app/declarative-generative-ui/renderers.tsx",
"src/components/example-canvas/index.tsx",
"src/components/example-canvas/todo-list.tsx",
"src/components/example-layout/mode-toggle.tsx",
"src/components/generative-ui/charts/bar-chart.tsx",
"src/components/headless-chat.tsx",
"src/components/tool-rendering.tsx",
"src/components/ui/spinner.tsx"
],
"packageJsonOverrides": {
"scripts.dev:agent": "./scripts/run-agent.sh || scripts\\run-agent.bat",
"scripts.install:agent": "./scripts/setup-agent.sh || scripts\\setup-agent.bat"
}
},
"claude-sdk-typescript": {
"role": "instance",
"agent": {
"language": "typescript",
"runtime": "claude-agent-sdk"
},
"allowedDivergence": [
"agent/**",
"src/app/api/copilotkit/**",
"Dockerfile",
"docker/Dockerfile.agent",
"docker-compose.test.yml",
"entrypoint.sh",
"serve.py",
"scripts/**",
"docker-route-override.ts",
"src/app/declarative-generative-ui/renderers.tsx",
"src/components/example-canvas/index.tsx",
"src/components/example-canvas/todo-list.tsx",
"src/components/example-layout/mode-toggle.tsx",
"src/components/generative-ui/charts/bar-chart.tsx",
"src/components/headless-chat.tsx",
"src/components/tool-rendering.tsx",
"src/components/ui/spinner.tsx"
],
"packageJsonOverrides": {
"scripts.dev:agent": "./scripts/run-agent.sh || scripts\\run-agent.bat",
"scripts.install:agent": "./scripts/setup-agent.sh || scripts\\setup-agent.bat"
}
}
}
}
+317
View File
@@ -0,0 +1,317 @@
/**
* Integration-demo parity sync.
*
* Copies verbatim files from the north-star integration demo to one or more
* instance demos, and rewrites tracked package.json keys so scripts and
* dependency pins stay aligned. Does NOT touch agent code or the api route —
* those are the manual-merge zones per `_parity/manifest.json`.
*
* Usage (from repo root or anywhere):
* pnpm tsx examples/integrations/_parity/sync.ts --target=langgraph-js
* pnpm tsx examples/integrations/_parity/sync.ts --target=langgraph-js --dry-run
* pnpm tsx examples/integrations/_parity/sync.ts --all
*
* Exit codes:
* 0 — sync applied (or dry-run reported only)
* 1 — unexpected error (missing source file, unwritable target)
* 2 — invalid CLI input
*/
import {
copyFileSync,
mkdirSync,
readFileSync,
writeFileSync,
readdirSync,
statSync,
} from "node:fs";
import { dirname, join, relative } from "node:path";
import { fileURLToPath } from "node:url";
import type { ParityRoot } from "./lib/manifest.js";
import {
loadManifest,
instanceDir,
northStarDir,
listInstances,
} from "./lib/manifest.js";
import { fileExists, getByPath, setByPath } from "./lib/diff.js";
interface CliOpts {
target?: string;
all: boolean;
dryRun: boolean;
}
function parseCli(argv: string[]): CliOpts {
const opts: CliOpts = { all: false, dryRun: false };
for (const arg of argv) {
if (arg === "--all") opts.all = true;
else if (arg === "--dry-run") opts.dryRun = true;
else if (arg.startsWith("--target="))
opts.target = arg.slice("--target=".length);
else if (arg === "--help" || arg === "-h") {
printHelp();
process.exit(0);
} else {
process.stderr.write(`unknown arg: ${arg}\n`);
printHelp();
process.exit(2);
}
}
if (!opts.all && !opts.target) {
process.stderr.write("required: --target=<name> or --all\n");
printHelp();
process.exit(2);
}
return opts;
}
function printHelp(): void {
process.stderr.write(
[
"sync integration demos to north-star.",
"",
" --target=<name> sync a single instance (e.g. langgraph-js)",
" --all sync every non-north-star instance",
" --dry-run print changes without writing",
"",
].join("\n"),
);
}
function resolveParityDir(): string {
return dirname(fileURLToPath(import.meta.url));
}
interface SyncResult {
instance: string;
filesCopied: number;
filesSkipped: number;
pkgKeysRewritten: string[];
}
function syncInstance(
root: ParityRoot,
instance: string,
dryRun: boolean,
): SyncResult {
const manifest = root.manifest;
const from = northStarDir(root);
const to = instanceDir(root, instance);
const inst = manifest.instances[instance]!;
if (!fileExists(to)) {
throw new Error(`target directory missing: ${to}`);
}
let filesCopied = 0;
let filesSkipped = 0;
for (const pattern of manifest.tracked.verbatimFiles) {
const matches = expandPattern(from, pattern);
if (matches.length === 0) {
process.stderr.write(
`[parity] warn: verbatim pattern matched nothing in north-star: ${pattern}\n`,
);
continue;
}
for (const relPath of matches) {
const src = join(from, relPath);
const dst = join(to, relPath);
if (isAllowedDivergence(relPath, inst.allowedDivergence)) {
filesSkipped++;
continue;
}
const changed = copyIfChanged(src, dst, dryRun);
if (changed) filesCopied++;
}
}
// Canonical prompt: not copied as a file. Each agent inlines the prompt
// string in source. sync.ts used to write PROMPT.md per instance, but that
// file was never loaded at runtime — it was cosmetic. Verifier now
// grep-checks the canonical prompt's first line against instance source.
// package.json key rewrite
const pkgSrcPath = join(from, "package.json");
const pkgDstPath = join(to, "package.json");
const pkgSrc = JSON.parse(readFileSync(pkgSrcPath, "utf8")) as Record<
string,
unknown
>;
const pkgDst = JSON.parse(readFileSync(pkgDstPath, "utf8")) as Record<
string,
unknown
>;
const rewritten: string[] = [];
for (const keyPath of manifest.tracked.packageJsonPaths) {
const override = inst.packageJsonOverrides[keyPath];
const value =
override !== undefined ? override : getByPath(pkgSrc, keyPath);
if (value === undefined) continue; // source doesn't declare it — leave instance alone
const existing = getByPath(pkgDst, keyPath);
if (existing !== value) {
setByPath(pkgDst, keyPath, value);
rewritten.push(keyPath);
}
}
// Apply any override keys the source doesn't have (e.g. dev:agent differs
// across instances and is NOT in packageJsonPaths)
for (const [keyPath, override] of Object.entries(inst.packageJsonOverrides)) {
if (manifest.tracked.packageJsonPaths.includes(keyPath)) continue;
const existing = getByPath(pkgDst, keyPath);
if (existing !== override) {
setByPath(pkgDst, keyPath, override);
rewritten.push(keyPath);
}
}
if (rewritten.length > 0 && !dryRun) {
writeFileSync(pkgDstPath, JSON.stringify(pkgDst, null, 2) + "\n");
}
return {
instance,
filesCopied,
filesSkipped,
pkgKeysRewritten: rewritten,
};
}
function copyIfChanged(src: string, dst: string, dryRun: boolean): boolean {
if (!fileExists(src)) {
throw new Error(`missing source: ${src}`);
}
const srcBuf = readFileSync(src);
if (fileExists(dst)) {
const dstBuf = readFileSync(dst);
if (srcBuf.equals(dstBuf)) return false;
}
if (dryRun) return true;
mkdirSync(dirname(dst), { recursive: true });
copyFileSync(src, dst);
return true;
}
/**
* Minimal glob expansion. Supports literal paths, `dir/**` (recursive), and
* `dir/**\/*.ext` (recursive with extension). Intentionally NOT a full glob —
* manifest patterns are curated, not user input.
*/
export function expandPattern(baseDir: string, pattern: string): string[] {
if (!pattern.includes("*")) {
return fileExists(join(baseDir, pattern)) ? [pattern] : [];
}
if (pattern.endsWith("/**")) {
const prefix = pattern.slice(0, -3);
return walk(baseDir, prefix).map((p) => p);
}
const doubleStarExt = pattern.match(/^(.+)\/\*\*\/\*\.([a-zA-Z0-9]+)$/);
if (doubleStarExt) {
const prefix = doubleStarExt[1]!;
const ext = doubleStarExt[2]!;
return walk(baseDir, prefix).filter((p) => p.endsWith(`.${ext}`));
}
throw new Error(`unsupported pattern: ${pattern}`);
}
function walk(baseDir: string, rel: string): string[] {
const full = join(baseDir, rel);
if (!fileExists(full)) return [];
const out: string[] = [];
const stack = [full];
while (stack.length > 0) {
const dir = stack.pop()!;
const entries = readdirSync(dir);
for (const entry of entries) {
const abs = join(dir, entry);
const st = statSync(abs);
if (st.isDirectory()) stack.push(abs);
else out.push(relative(baseDir, abs));
}
}
return out;
}
function isAllowedDivergence(relPath: string, patterns: string[]): boolean {
for (const pattern of patterns) {
if (pattern.endsWith("/**")) {
const prefix = pattern.slice(0, -3);
if (relPath === prefix || relPath.startsWith(prefix + "/")) return true;
} else if (pattern === relPath) {
return true;
}
}
return false;
}
function printSyncResult(r: SyncResult, dryRun: boolean): void {
const verb = dryRun ? "would sync" : "synced";
process.stdout.write(`\n${r.instance}: ${verb}\n`);
process.stdout.write(` files copied: ${r.filesCopied}\n`);
process.stdout.write(
` files skipped: ${r.filesSkipped} (allowed divergence)\n`,
);
process.stdout.write(
` package.json: ${r.pkgKeysRewritten.length} key(s) rewritten\n`,
);
if (r.pkgKeysRewritten.length > 0) {
for (const k of r.pkgKeysRewritten) {
process.stdout.write(` - ${k}\n`);
}
}
}
function main(): void {
const opts = parseCli(process.argv.slice(2));
const parityDir = resolveParityDir();
const root = loadManifest(parityDir);
const targets = opts.all
? listInstances(root)
: opts.target
? [opts.target]
: [];
if (opts.target && !root.manifest.instances[opts.target]) {
process.stderr.write(`unknown instance: ${opts.target}\n`);
process.stderr.write(
`known: ${Object.keys(root.manifest.instances).join(", ")}\n`,
);
process.exit(2);
}
let totalFiles = 0;
let totalKeys = 0;
for (const t of targets) {
if (t === root.manifest.northStar) continue; // paranoia: never write to north-star
const result = syncInstance(root, t, opts.dryRun);
printSyncResult(result, opts.dryRun);
totalFiles += result.filesCopied;
totalKeys += result.pkgKeysRewritten.length;
}
const mode = opts.dryRun ? "(dry-run) " : "";
process.stdout.write(
`\n${mode}total: ${totalFiles} file change(s), ${totalKeys} package.json key(s) across ${targets.length} instance(s)\n`,
);
process.stdout.write(
`\nmanual-merge reminder: agent/ dir and api/copilotkit route are NOT synced.\n` +
`run \`pnpm parity:verify\` to check agent-surface and prompt alignment.\n`,
);
}
// Only run main() when invoked as script, not when imported by tests.
const isMain =
import.meta.url === `file://${process.argv[1]}` ||
fileURLToPath(import.meta.url) === process.argv[1];
if (isMain) {
try {
main();
} catch (e) {
process.stderr.write(`[parity] sync failed: ${(e as Error).message}\n`);
process.exit(1);
}
}
+399
View File
@@ -0,0 +1,399 @@
/**
* Integration-demo parity verifier.
*
* Reads the parity manifest and checks each instance against the north-star:
* - verbatim files byte-equal (modulo allowedDivergence)
* - tracked package.json keys equal (or equal to instance override)
* - canonical prompt present and byte-equal at <instance>/agent/PROMPT.md
* - agent tool names + state keys declared in manifest present in the
* instance's agent source (grep-level, not AST — see `scanAgentSurface`)
*
* Exit codes:
* 0 — no errors
* 1 — one or more errors (drift)
* 2 — invalid CLI input
* 3 — unreadable (missing file, unreadable dir)
*
* Usage:
* pnpm tsx examples/integrations/_parity/verify.ts
* pnpm tsx examples/integrations/_parity/verify.ts --target=langgraph-js
* pnpm tsx examples/integrations/_parity/verify.ts --json
*/
import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type { ParityRoot } from "./lib/manifest.js";
import {
loadManifest,
instanceDir,
northStarDir,
listInstances,
} from "./lib/manifest.js";
import { fileExists, fileSha256, getByPath } from "./lib/diff.js";
import type { DriftItem, Report } from "./lib/report.js";
import { printReports, hasErrors } from "./lib/report.js";
import { expandPattern } from "./sync.js";
interface CliOpts {
target?: string;
json: boolean;
noColor: boolean;
}
function parseCli(argv: string[]): CliOpts {
const opts: CliOpts = { json: false, noColor: false };
for (const arg of argv) {
if (arg === "--json") opts.json = true;
else if (arg === "--no-color") opts.noColor = true;
else if (arg.startsWith("--target="))
opts.target = arg.slice("--target=".length);
else if (arg === "--help" || arg === "-h") {
process.stderr.write(
[
"verify integration-demo parity.",
"",
" --target=<name> verify a single instance",
" --json emit machine-readable report",
" --no-color disable ANSI color",
"",
].join("\n"),
);
process.exit(0);
} else {
process.stderr.write(`unknown arg: ${arg}\n`);
process.exit(2);
}
}
return opts;
}
function resolveParityDir(): string {
return dirname(fileURLToPath(import.meta.url));
}
function verifyInstance(root: ParityRoot, instance: string): Report {
const items: DriftItem[] = [];
const manifest = root.manifest;
const from = northStarDir(root);
const to = instanceDir(root, instance);
const inst = manifest.instances[instance]!;
if (!fileExists(to)) {
items.push({
severity: "error",
instance,
kind: "missing-instance",
subject: instance,
detail: `directory not found: ${to}`,
});
return { instance, items };
}
// Verbatim files
for (const pattern of manifest.tracked.verbatimFiles) {
const matches = expandPattern(from, pattern);
if (matches.length === 0) {
items.push({
severity: "warn",
instance,
kind: "verbatim-file",
subject: pattern,
detail: "pattern matched no files in north-star",
});
continue;
}
for (const relPath of matches) {
if (isAllowedDivergence(relPath, inst.allowedDivergence)) continue;
const src = join(from, relPath);
const dst = join(to, relPath);
if (!fileExists(dst)) {
items.push({
severity: "error",
instance,
kind: "verbatim-file",
subject: relPath,
detail: "missing in instance",
});
continue;
}
const srcSha = fileSha256(src);
const dstSha = fileSha256(dst);
if (srcSha !== dstSha) {
items.push({
severity: "error",
instance,
kind: "verbatim-file",
subject: relPath,
detail: "content differs from north-star",
expected: srcSha,
actual: dstSha,
});
} else {
items.push({
severity: "ok",
instance,
kind: "verbatim-file",
subject: relPath,
});
}
}
}
// package.json tracked keys
const pkgSrc = JSON.parse(
readFileSync(join(from, "package.json"), "utf8"),
) as Record<string, unknown>;
const pkgDst = JSON.parse(
readFileSync(join(to, "package.json"), "utf8"),
) as Record<string, unknown>;
for (const keyPath of manifest.tracked.packageJsonPaths) {
const override = inst.packageJsonOverrides[keyPath];
const expected =
override !== undefined ? override : getByPath(pkgSrc, keyPath);
if (expected === undefined) continue;
const actual = getByPath(pkgDst, keyPath);
if (actual === undefined) {
items.push({
severity: "error",
instance,
kind: "package-json",
subject: keyPath,
detail: "missing in instance package.json",
expected: String(expected),
});
continue;
}
if (actual !== expected) {
items.push({
severity: "error",
instance,
kind: "package-json",
subject: keyPath,
detail: "value differs from expected",
expected: String(expected),
actual: String(actual),
});
} else {
items.push({
severity: "ok",
instance,
kind: "package-json",
subject: keyPath,
});
}
}
// Canonical prompt: grep agent source for the first non-blank line of
// the canonical prompt. Full-text match is too brittle across triple-quote
// indentation; first line is a deterministic, high-signal marker.
const promptSrc = resolve(root.integrationsDir, manifest.canonicalPromptFile);
if (!fileExists(promptSrc)) {
items.push({
severity: "error",
instance,
kind: "prompt",
subject: manifest.canonicalPromptFile,
detail: "canonical prompt missing in repo",
});
} else {
const canonicalFirstLine = readFirstNonBlankLine(promptSrc);
if (canonicalFirstLine === null) {
items.push({
severity: "error",
instance,
kind: "prompt",
subject: manifest.canonicalPromptFile,
detail: "canonical prompt is empty",
});
} else {
const agentTextForPrompt = readAgentText(to);
if (agentTextForPrompt === null) {
items.push({
severity: "warn",
instance,
kind: "prompt",
subject: "agent/",
detail: "agent source not readable — skipping prompt check",
});
} else if (!agentTextForPrompt.includes(canonicalFirstLine)) {
items.push({
severity: "error",
instance,
kind: "prompt",
subject: "canonical prompt",
detail: `first line not found in agent source: "${truncate(canonicalFirstLine, 80)}"`,
});
} else {
items.push({
severity: "ok",
instance,
kind: "prompt",
subject: "canonical prompt",
});
}
}
}
// Agent surface: tool names + state keys grep-check.
// Intentional shallow check: verifier confirms the declared identifiers
// appear somewhere in the agent source. It does NOT validate call-site
// correctness — that's the aimock fixture integration test's job.
const agentText = readAgentText(to);
if (agentText === null) {
items.push({
severity: "warn",
instance,
kind: "agent-tool",
subject: "agent/",
detail: "agent source not readable — skipping surface check",
});
} else {
for (const tool of manifest.tracked.agentSurface.toolNames) {
if (!agentText.includes(tool)) {
items.push({
severity: "error",
instance,
kind: "agent-tool",
subject: tool,
detail: "tool name not found in agent source",
});
} else {
items.push({
severity: "ok",
instance,
kind: "agent-tool",
subject: tool,
});
}
}
for (const key of manifest.tracked.agentSurface.stateKeys) {
if (!agentText.includes(key)) {
items.push({
severity: "warn",
instance,
kind: "agent-state",
subject: key,
detail: "state key not found in agent source",
});
} else {
items.push({
severity: "ok",
instance,
kind: "agent-state",
subject: key,
});
}
}
}
return { instance, items };
}
function readAgentText(instanceRoot: string): string | null {
const agentDir = join(instanceRoot, "agent");
if (!fileExists(agentDir)) return null;
// Recursively read .py, .ts, .js files and concatenate. Cheap enough; the
// agent tree is small (< 1MB).
const parts: string[] = [];
const stack = [agentDir];
while (stack.length > 0) {
const dir = stack.pop()!;
let entries: string[];
try {
entries = require("node:fs").readdirSync(dir);
} catch {
return null;
}
for (const entry of entries) {
if (
entry === "node_modules" ||
entry === ".venv" ||
entry === ".langgraph_api" ||
entry === "__pycache__" ||
entry === "dist" ||
entry === "build" ||
entry === ".next"
)
continue;
const abs = join(dir, entry);
const st = require("node:fs").statSync(abs);
if (st.isDirectory()) {
stack.push(abs);
} else if (/\.(py|ts|tsx|js|mjs)$/.test(entry)) {
try {
parts.push(readFileSync(abs, "utf8"));
} catch {
/* unreadable file — skip */
}
}
}
}
return parts.join("\n");
}
function readFirstNonBlankLine(path: string): string | null {
const text = readFileSync(path, "utf8");
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (trimmed.length > 0) return trimmed;
}
return null;
}
function truncate(s: string, max: number): string {
if (s.length <= max) return s;
return s.slice(0, max) + "…";
}
function isAllowedDivergence(relPath: string, patterns: string[]): boolean {
for (const pattern of patterns) {
if (pattern.endsWith("/**")) {
const prefix = pattern.slice(0, -3);
if (relPath === prefix || relPath.startsWith(prefix + "/")) return true;
} else if (pattern === relPath) {
return true;
}
}
return false;
}
function main(): void {
const opts = parseCli(process.argv.slice(2));
const parityDir = resolveParityDir();
const root = loadManifest(parityDir);
const targets = opts.target ? [opts.target] : listInstances(root);
if (opts.target && !root.manifest.instances[opts.target]) {
process.stderr.write(`unknown instance: ${opts.target}\n`);
process.exit(2);
}
const reports: Report[] = [];
for (const t of targets) {
if (t === root.manifest.northStar) continue;
reports.push(verifyInstance(root, t));
}
if (opts.json) {
process.stdout.write(JSON.stringify(reports, null, 2) + "\n");
} else {
printReports(reports, !opts.noColor);
}
const failed = hasErrors(reports);
process.exit(failed ? 1 : 0);
}
const isMain =
import.meta.url === `file://${process.argv[1]}` ||
fileURLToPath(import.meta.url) === process.argv[1];
if (isMain) {
try {
main();
} catch (e) {
process.stderr.write(`[parity] verify failed: ${(e as Error).message}\n`);
process.exit(3);
}
}