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
12 changes: 7 additions & 5 deletions apps/demo/src/generate-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1088,14 +1088,16 @@ function applyLineTo(target: SandboxTarget, line: ProtocolLine, context: Surface
if (line.op === 'meta' && line.path === '/ghost-context') {
const value = line.value as
| {
product?: unknown;
targetPath?: unknown;
layers?: unknown;
baseDirectionId?: unknown;
product?: unknown;
source?: unknown;
targetPath?: unknown;
layers?: unknown;
baseDirectionId?: unknown;
styleSource?: unknown;
}
| undefined;
const product = typeof value?.product === 'string' ? value.product : 'Ghost';
const source = typeof value?.source === 'string' ? value.source : 'root';
const targetPath = typeof value?.targetPath === 'string' ? value.targetPath : '.';
const layers = Array.isArray(value?.layers)
? value.layers.filter((layer): layer is string => typeof layer === 'string')
Expand All @@ -1104,7 +1106,7 @@ function applyLineTo(target: SandboxTarget, line: ProtocolLine, context: Surface
const style = typeof value?.styleSource === 'string' ? value.styleSource : 'unknown';
target.onLog(
'op-meta',
`ghost context → ${product}; target=${targetPath}; layers=${layers.join(' › ') || '.'}; base=${base}; style=${style}`,
`ghost context → ${product}; source=${source}; target=${targetPath}; layers=${layers.join(' › ') || '.'}; base=${base}; style=${style}`,
);
return;
}
Expand Down
59 changes: 59 additions & 0 deletions apps/server/src/generate-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,65 @@ test('api generate sends narrowed contract and stream meta shape through package
persistence: 'replayable',
});
assert.deepEqual((policyLines[1] as Extract<ProtocolLine, { op: 'meta' }>).value, surfacePlan);

const ghostResponse = await fetch(`http://127.0.0.1:${appPort}/api/generate`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
prompt: 'build checkout status',
mode: 'static',
ghost: {
source: 'resolved-context',
id: 'checkout',
product: 'Checkout',
prompt: 'You are working inside the Checkout product experience.',
provenance: { layers: ['portable-bundle'] },
},
}),
});
const ghostBody = await ghostResponse.text();
assert.equal(ghostResponse.status, 200, ghostBody);

assert.equal(anthropicRequests.length, 3);
const ghostRequest = anthropicRequests[2] as { system?: Array<{ text?: string }>; stream?: boolean };
const ghostSystemText = ghostRequest.system?.map((block) => block.text ?? '').join('\n') ?? '';
assert.match(ghostSystemText, /Checkout product experience/);

const ghostLines = ghostBody
.trim()
.split(/\n/)
.filter(Boolean)
.map((raw) => JSON.parse(raw) as ProtocolLine);
assert.deepEqual(ghostLines.slice(0, 4).map((line) => `${line.op} ${line.path}`), [
'meta /ghost-context',
'meta /ghost-token-source',
'meta /surface-plan',
'meta /status',
]);
const ghostContext = ghostLines.find((line) => line.path === '/ghost-context') as Extract<ProtocolLine, { op: 'meta' }>;
assert.equal((ghostContext.value as { source?: unknown }).source, 'resolved-context');
assert.equal((ghostContext.value as { product?: unknown }).product, 'Checkout');
const ghostTokenSource = ghostLines.find((line) => line.path === '/ghost-token-source') as Extract<ProtocolLine, { op: 'meta' }>;
assert.equal((ghostTokenSource.value as { kind?: unknown }).kind, 'base-direction');
const ghostReviewPacket = ghostLines.find((line) => line.path === '/ghost-review-packet') as Extract<ProtocolLine, { op: 'meta' }>;
assert.equal((ghostReviewPacket.value as { source?: unknown }).source, 'resolved-context');

const ghostOverrideResponse = await fetch(`http://127.0.0.1:${appPort}/api/generate`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
prompt: 'build checkout status',
ghost: {
source: 'resolved-context',
prompt: 'You are working inside the Checkout product experience.',
},
tokenOverrides: { 'color-accent': 'red' },
}),
});
const ghostOverrideBody = await ghostOverrideResponse.text();
assert.equal(ghostOverrideResponse.status, 400);
assert.match(ghostOverrideBody, /tokenOverrides are not supported with Ghost product memory/);
assert.equal(anthropicRequests.length, 3);
});

function sse(event: string, data: unknown): string {
Expand Down
169 changes: 165 additions & 4 deletions apps/server/src/ghost-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('Ghost adapter', () => {
const parsed = parseGhostRequest({ rootId: 'checkout' }, roots);
assert.equal(parsed.ok, true);
assert.deepEqual(parsed.ok ? parsed.request : null, {
source: 'root',
rootId: 'checkout',
targetPath: '.',
memoryDir: '.ghost',
Expand All @@ -60,11 +61,15 @@ describe('Ghost adapter', () => {
}, roots);
assert.equal(withBase.ok, true);
assert.deepEqual(withBase.ok ? withBase.request : null, {
source: 'root',
rootId: 'checkout',
targetPath: 'app',
memoryDir: '.ghost',
baseDirectionId: 'ghost',
});
const explicitRoot = parseGhostRequest({ source: 'root', rootId: 'checkout' }, roots);
assert.equal(explicitRoot.ok, true);
assert.equal(explicitRoot.ok ? explicitRoot.request?.source : null, 'root');
assert.deepEqual(parseGhostRequest({ rootId: 'checkout', baseDirectionId: '../ghost' }, roots), {
ok: false,
error: 'ghost.baseDirectionId must be a valid direction id',
Expand All @@ -80,13 +85,30 @@ describe('Ghost adapter', () => {

const ctx = await resolveGhostContext(parsed.request, roots);

assert.equal(ctx.source, 'root');
if (ctx.source !== 'root') assert.fail('expected root context');
assert.equal(ctx.tokenSource.kind, 'ghost-config');
assert.equal(ctx.tokenSource.source, 'tokens.css');
assert.equal(ctx.tokenSource.css, await readDefaultTokensCss());
assert.equal(ctx.stack.merged.fingerprint.summary.product, 'Test Product');
assert.match(ctx.prompt, /Test Product/);
assert.match(ctx.prompt, /quiet/);
assert.match(ctx.prompt, /exacting workflows/);
assert.match(ctx.prompt, /Human-approved test intent/);
});

it('rejects legacy single-file fingerprints', async () => {
const root = await makeLegacyGhostFixture();
const roots = parseGhostRoots(`checkout=${root}`);
const parsed = parseGhostRequest({ rootId: 'checkout' }, roots);
assert.equal(parsed.ok, true);
if (!parsed.ok || !parsed.request) assert.fail('expected valid Ghost request');
const request = parsed.request;

await assert.rejects(
() => resolveGhostContext(request, roots),
/No \.ghost\/fingerprint\.yml found/,
);
});

it('falls back to Summon default tokens when Ghost token CSS is missing or invalid', async () => {
Expand All @@ -98,6 +120,8 @@ describe('Ghost adapter', () => {

const ctx = await resolveGhostContext(parsed.request, roots);

assert.equal(ctx.source, 'root');
if (ctx.source !== 'root') assert.fail('expected root context');
assert.equal(ctx.tokenSource.kind, 'summon-default');
assert.equal(ctx.tokenSource.source, '@anarchitecture/summon/tokens.css');
assert.match(ctx.tokenSource.css, /--color-bg:/);
Expand All @@ -117,6 +141,8 @@ describe('Ghost adapter', () => {
tokensCss: baseTokens,
});

assert.equal(ctx.source, 'root');
if (ctx.source !== 'root') assert.fail('expected root context');
assert.equal(ctx.baseDirectionId, 'ghost');
assert.equal(ctx.tokenSource.kind, 'base-direction');
assert.equal(ctx.tokenSource.source, 'direction:ghost/tokens.css');
Expand All @@ -131,6 +157,8 @@ describe('Ghost adapter', () => {
assert.equal(parsed.ok, true);
if (!parsed.ok || !parsed.request) assert.fail('expected valid Ghost request');
const ctx = await resolveGhostContext(parsed.request, roots);
assert.equal(ctx.source, 'root');
if (ctx.source !== 'root') assert.fail('expected root context');

const packet = buildGhostReviewPacket({
context: ctx,
Expand All @@ -146,6 +174,7 @@ describe('Ghost adapter', () => {
});

assert.equal(packet.schema, 'summon.ghost-generation/v1');
assert.equal(packet.source, 'root');
assert.equal(packet.rootId, 'checkout');
assert.equal(packet.product, 'Test Product');
assert.equal(packet.baseDirectionId, null);
Expand All @@ -162,6 +191,86 @@ describe('Ghost adapter', () => {
assert.equal(packet.tokenSource.kind, 'summon-default');
assert.equal('css' in packet.tokenSource, false);
});

it('resolves caller-provided Ghost context without repo access', async () => {
const roots = parseGhostRoots('');
const parsed = parseGhostRequest({
source: 'resolved-context',
id: 'checkout',
product: 'Checkout',
prompt: 'You are working inside the Checkout product experience.',
provenance: { layers: ['portable'] },
}, roots);
assert.equal(parsed.ok, true);
if (!parsed.ok || !parsed.request) assert.fail('expected valid resolved context request');

const ctx = await resolveGhostSteer(parsed.request, roots);

assert.equal(ctx.source, 'resolved-context');
assert.equal(ctx.prompt, 'You are working inside the Checkout product experience.');
assert.equal(ctx.product, 'Checkout');
assert.equal(ctx.root, null);
assert.equal(ctx.stack, null);
assert.equal(ctx.tokenSource.kind, 'summon-default');
assert.deepEqual(ctx.provenance, { layers: ['portable'] });
});

it('uses valid resolved-context tokens', async () => {
const tokens = await readDefaultTokensCss();
const roots = parseGhostRoots('');
const parsed = parseGhostRequest({
source: 'resolved-context',
prompt: 'Use portable Ghost memory.',
tokensCss: tokens,
tokenSource: 'bundle/tokens.css',
}, roots);
assert.equal(parsed.ok, true);
if (!parsed.ok || !parsed.request) assert.fail('expected valid resolved context request');

const ctx = await resolveGhostSteer(parsed.request, roots);

assert.equal(ctx.source, 'resolved-context');
assert.equal(ctx.tokenSource.kind, 'resolved-context');
assert.equal(ctx.tokenSource.source, 'bundle/tokens.css');
assert.equal(ctx.tokenSource.css, tokens);
});

it('falls back from invalid resolved-context tokens to base direction tokens', async () => {
const baseTokens = await readDefaultTokensCss();
const roots = parseGhostRoots('');
const parsed = parseGhostRequest({
source: 'resolved-context',
prompt: 'Use portable Ghost memory.',
tokensCss: ':root { --color-bg: red; }',
tokenSource: 'bundle/tokens.css',
baseDirectionId: 'ghost',
}, roots);
assert.equal(parsed.ok, true);
if (!parsed.ok || !parsed.request) assert.fail('expected valid resolved context request');

const ctx = await resolveGhostSteer(parsed.request, roots, {
id: 'ghost',
tokensCss: baseTokens,
});

assert.equal(ctx.source, 'resolved-context');
assert.equal(ctx.tokenSource.kind, 'base-direction');
assert.equal(ctx.tokenSource.source, 'direction:ghost/tokens.css');
assert.ok(ctx.tokenSource.warnings.some((warning) => warning.includes('bundle/tokens.css failed token contract')));
});

it('rejects resolved-context requests without prompt', () => {
const roots = parseGhostRoots('');

assert.deepEqual(parseGhostRequest({ source: 'resolved-context' }, roots), {
ok: false,
error: 'ghost.prompt is required for resolved-context',
});
assert.deepEqual(parseGhostRequest({ source: 'resolved-context', prompt: ' ' }, roots), {
ok: false,
error: 'ghost.prompt is required for resolved-context',
});
});
});

async function makeGhostFixture(options: { tokenCss?: string } = {}): Promise<string> {
Expand Down Expand Up @@ -190,15 +299,27 @@ principles:
experience_contracts: []
patterns:
- id: measured-surfaces
kind: visual
status: accepted
kind: visual
pattern: Surfaces are compact, rectangular, and information-first.
implementation_vocabulary:
tokens: [--color-bg, --color-text]
components: []
review_policy:
proposal_policy:
- Agents propose memory changes; humans promote durable truth.
review_policy: {}
`,
);
await writeFile(
join(root, '.ghost', 'checks.yml'),
`schema: ghost.checks/v1
id: test-product
checks: []
`,
);
await writeFile(
join(root, '.ghost', 'intent.md'),
`# Intent

Human-approved test intent keeps generated surfaces grounded.
`,
);
await writeFile(
Expand All @@ -218,6 +339,46 @@ libraries: []
return root;
}

async function makeLegacyGhostFixture(): Promise<string> {
const root = await mkdtemp(join(tmpdir(), 'summon-ghost-adapter-legacy-'));
fixtureRoots.push(root);
await mkdir(join(root, '.ghost'), { recursive: true });
await writeFile(
join(root, '.ghost', 'fingerprint.md'),
`schema: ghost.fingerprint/v1
summary:
product: Test Product
audience: [operators]
goals: [keep work legible]
tone: [quiet, exacting workflows]
topology:
scopes:
- id: app
paths: [.]
surface_types: [dashboard]
surface_types: [dashboard]
situations: []
principles:
- id: calm-density
status: accepted
principle: Preserve quiet density and clear hierarchy.
experience_contracts: []
patterns:
- id: measured-surfaces
kind: visual
status: accepted
pattern: Surfaces are compact, rectangular, and information-first.
implementation_vocabulary:
tokens: [--color-bg, --color-text]
components: []
review_policy:
proposal_policy:
- Agents propose memory changes; humans promote durable truth.
`,
);
return root;
}

async function readDefaultTokensCss(): Promise<string> {
const here = dirname(fileURLToPath(import.meta.url));
return readFile(
Expand Down
Loading
Loading