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
31 changes: 29 additions & 2 deletions src/server/infra/dream/operations/synthesize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,31 @@ import {access, mkdir, readdir, readFile, rename, writeFile} from 'node:fs/promi
import {dirname, join, resolve} from 'node:path'

import type {ICipherAgent} from '../../../../agent/core/interfaces/i-cipher-agent.js'
import type {ILogger} from '../../../../agent/core/interfaces/i-logger.js'
import type {IRuntimeSignalStore} from '../../../core/interfaces/storage/i-runtime-signal-store.js'
import type {DreamOperation} from '../dream-log-schema.js'
import type {SynthesisCandidate} from '../dream-response-schemas.js'

import {createDefaultRuntimeSignals} from '../../../core/domain/knowledge/runtime-signals-schema.js'
import {warnSidecarFailure} from '../../../core/domain/knowledge/sidecar-logging.js'
import {isDescendantOf} from '../../../utils/path-utils.js'
import {SynthesizeResponseSchema} from '../dream-response-schemas.js'
import {parseDreamResponse} from '../parse-dream-response.js'

export type SynthesizeDeps = {
agent: ICipherAgent
contextTreeDir: string
/**
* Optional logger. When provided, sidecar seed failures emit a warn
* so the fail-open degradation is observable rather than silent.
*/
logger?: ILogger
/**
* Optional sidecar store for runtime ranking signals. When provided,
* newly created synthesis files are seeded with default signals so
* ranking data lives in the sidecar rather than in markdown frontmatter.
*/
runtimeSignalStore?: IRuntimeSignalStore
searchService: {
search(query: string, options?: {limit?: number; scope?: string}): Promise<{results: Array<{path: string; score: number; title: string}>}>
}
Expand Down Expand Up @@ -92,7 +107,7 @@ export async function synthesize(deps: SynthesizeDeps): Promise<DreamOperation[]
for (const candidate of novel) {
try {
// eslint-disable-next-line no-await-in-loop
const op = await writeSynthesisFile(candidate, contextTreeDir)
const op = await writeSynthesisFile(candidate, contextTreeDir, deps.runtimeSignalStore, deps.logger)
if (op) results.push(op)
} catch {
// Skip failed candidate — don't discard already-written results
Expand Down Expand Up @@ -221,6 +236,8 @@ async function isDuplicateCandidate(
async function writeSynthesisFile(
candidate: SynthesisCandidate,
contextTreeDir: string,
runtimeSignalStore?: IRuntimeSignalStore,
logger?: ILogger,
): Promise<DreamOperation | undefined> {
const slug = slugify(candidate.title)
const relativePath = `${candidate.placement}/${slug}.md`
Expand All @@ -243,7 +260,6 @@ async function writeSynthesisFile(
/* eslint-disable camelcase */
const frontmatter = {
confidence: candidate.confidence,
maturity: 'draft',
sources,
synthesized_at: new Date().toISOString(),
type: 'synthesis',
Expand All @@ -264,6 +280,17 @@ async function writeSynthesisFile(

await atomicWrite(absPath, content)

// Seed the sidecar with default signals so ranking data lives in the
// sidecar rather than in markdown frontmatter. Best-effort — a sidecar
// failure must never prevent the synthesis file from being created.
if (runtimeSignalStore) {
try {
await runtimeSignalStore.set(relativePath, createDefaultRuntimeSignals())
} catch (error) {
warnSidecarFailure(logger, 'synthesize', 'seed', relativePath, error)
}
}

return {
action: 'CREATE',
confidence: candidate.confidence,
Expand Down
1 change: 1 addition & 0 deletions src/server/infra/executor/dream-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export class DreamExecutor {
...(await synthesize({
agent,
contextTreeDir,
runtimeSignalStore: this.deps.runtimeSignalStore,
searchService: this.deps.searchService,
signal,
taskId,
Expand Down
133 changes: 132 additions & 1 deletion test/unit/infra/dream/operations/synthesize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import {join} from 'node:path'
import {restore, type SinonStub, stub} from 'sinon'

import type {ICipherAgent} from '../../../../../src/agent/core/interfaces/i-cipher-agent.js'
import type {IRuntimeSignalStore} from '../../../../../src/server/core/interfaces/storage/i-runtime-signal-store.js'
import type {DreamOperation} from '../../../../../src/server/infra/dream/dream-log-schema.js'

import {synthesize, type SynthesizeDeps} from '../../../../../src/server/infra/dream/operations/synthesize.js'
import {createMockRuntimeSignalStore} from '../../../../helpers/mock-factories.js'

/** Helper: create a markdown file with optional frontmatter */
async function createMdFile(dir: string, relativePath: string, body: string, frontmatter?: Record<string, unknown>): Promise<void> {
Expand Down Expand Up @@ -159,7 +161,7 @@ describe('synthesize', () => {

const content = await readFile(join(ctxDir, 'auth/shared-token-validation.md'), 'utf8')
expect(content).to.include('type: synthesis')
expect(content).to.include('maturity: draft')
expect(content).to.not.include('maturity:')
expect(content).to.include('Shared Token Validation')
expect(content).to.include('Both auth and API share token validation logic.')
})
Expand Down Expand Up @@ -478,4 +480,133 @@ describe('synthesize', () => {
const options = agent.executeOnSession.firstCall.args[2]
expect(options).to.have.property('signal', controller.signal)
})

// ── Runtime-signal sidecar ──────────────────────────────────────────────

describe('runtime-signal sidecar', () => {
let signalStore: IRuntimeSignalStore

beforeEach(() => {
signalStore = createMockRuntimeSignalStore()
})

it('does not write maturity to markdown frontmatter', async () => {
await createMdFile(ctxDir, 'auth/_index.md', '# Auth', {type: 'summary'})
await createMdFile(ctxDir, 'api/_index.md', '# API', {type: 'summary'})

agent.executeOnSession.resolves(llmResponse([{
claim: 'Cross-domain pattern.',
confidence: 0.9,
evidence: [{domain: 'auth', fact: 'A'}, {domain: 'api', fact: 'B'}],
placement: 'auth',
title: 'Sidecar Test',
}]))

await synthesize({...deps, runtimeSignalStore: signalStore})

const content = await readFile(join(ctxDir, 'auth/sidecar-test.md'), 'utf8')
expect(content).to.not.include('maturity:')
expect(content).to.not.include('importance:')
expect(content).to.not.include('recency:')
expect(content).to.not.include('accessCount:')
expect(content).to.not.include('updateCount:')
})

it('seeds sidecar with default signals after writing synthesis file', async () => {
await createMdFile(ctxDir, 'auth/_index.md', '# Auth', {type: 'summary'})
await createMdFile(ctxDir, 'api/_index.md', '# API', {type: 'summary'})

agent.executeOnSession.resolves(llmResponse([{
claim: 'Pattern.',
confidence: 0.85,
evidence: [{domain: 'auth', fact: 'A'}, {domain: 'api', fact: 'B'}],
placement: 'auth',
title: 'Seeded Pattern',
}]))

const setSpy = stub(signalStore, 'set').callThrough()

await synthesize({...deps, runtimeSignalStore: signalStore})

expect(setSpy.calledOnce).to.be.true
expect(setSpy.firstCall.args[0]).to.equal('auth/seeded-pattern.md')
const signals = await signalStore.get('auth/seeded-pattern.md')
Comment thread
RyanNg1403 marked this conversation as resolved.
expect(signals.importance).to.equal(50)
expect(signals.maturity).to.equal('draft')
expect(signals.accessCount).to.equal(0)
expect(signals.updateCount).to.equal(0)
})

it('seeds sidecar for each created file in multi-candidate run', async () => {
await createMdFile(ctxDir, 'auth/_index.md', '# Auth', {type: 'summary'})
await createMdFile(ctxDir, 'api/_index.md', '# API', {type: 'summary'})

agent.executeOnSession.resolves(llmResponse([
{
claim: 'First.',
confidence: 0.9,
evidence: [{domain: 'auth', fact: 'A'}, {domain: 'api', fact: 'B'}],
placement: 'auth',
title: 'Multi One',
},
{
claim: 'Second.',
confidence: 0.8,
evidence: [{domain: 'auth', fact: 'C'}, {domain: 'api', fact: 'D'}],
placement: 'api',
title: 'Multi Two',
},
]))

const setSpy = stub(signalStore, 'set').callThrough()

await synthesize({...deps, runtimeSignalStore: signalStore})

expect(setSpy.calledTwice).to.be.true
expect(setSpy.firstCall.args[0]).to.equal('auth/multi-one.md')
expect(setSpy.secondCall.args[0]).to.equal('api/multi-two.md')
})

it('creates file even when sidecar store.set throws (fail-open)', async () => {
const brokenStore = createMockRuntimeSignalStore()
stub(brokenStore, 'set').rejects(new Error('disk full'))

await createMdFile(ctxDir, 'auth/_index.md', '# Auth', {type: 'summary'})
await createMdFile(ctxDir, 'api/_index.md', '# API', {type: 'summary'})

agent.executeOnSession.resolves(llmResponse([{
claim: 'Fail open.',
confidence: 0.9,
evidence: [{domain: 'auth', fact: 'A'}, {domain: 'api', fact: 'B'}],
placement: 'auth',
title: 'Fail Open Pattern',
}]))

const results = await synthesize({...deps, runtimeSignalStore: brokenStore})
expect(results).to.have.lengthOf(1)

const content = await readFile(join(ctxDir, 'auth/fail-open-pattern.md'), 'utf8')
expect(content).to.include('type: synthesis')
})

it('succeeds even when sidecar store is not provided', async () => {
Comment thread
RyanNg1403 marked this conversation as resolved.
await createMdFile(ctxDir, 'auth/_index.md', '# Auth', {type: 'summary'})
await createMdFile(ctxDir, 'api/_index.md', '# API', {type: 'summary'})

agent.executeOnSession.resolves(llmResponse([{
claim: 'No store.',
confidence: 0.9,
evidence: [{domain: 'auth', fact: 'A'}, {domain: 'api', fact: 'B'}],
placement: 'auth',
title: 'No Store Pattern',
}]))

// No runtimeSignalStore in deps — should still create the file
const results = await synthesize(deps)
expect(results).to.have.lengthOf(1)

const content = await readFile(join(ctxDir, 'auth/no-store-pattern.md'), 'utf8')
expect(content).to.include('type: synthesis')
})
})
})
Loading