From 7deea768e4660861a111341058323252d6269eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Thu=E1=BA=ADn=20Ph=C3=A1t?= Date: Sun, 19 Apr 2026 15:09:35 +0700 Subject: [PATCH 1/2] fix: remove maturity from synthesis file frontmatter, seed sidecar instead synthesize.ts was writing `maturity: 'draft'` into markdown frontmatter of newly created synthesis files. This was missed during the runtime-signals migration (ENG-2160) which moved all five ranking fields to the sidecar. - Remove `maturity` from the synthesis frontmatter object - Add optional `runtimeSignalStore` to `SynthesizeDeps` - Seed sidecar with `createDefaultRuntimeSignals()` after file write - Thread `runtimeSignalStore` from `DreamExecutor.runOperations` to synthesize - Update existing test assertion from `include('maturity: draft')` to `not.include` - Add 4 new sidecar-specific tests (no ranking in markdown, sidecar seeded, multi-candidate seeding, graceful no-store) --- .../infra/dream/operations/synthesize.ts | 24 +++- src/server/infra/executor/dream-executor.ts | 1 + .../infra/dream/operations/synthesize.test.ts | 106 +++++++++++++++++- 3 files changed, 128 insertions(+), 3 deletions(-) diff --git a/src/server/infra/dream/operations/synthesize.ts b/src/server/infra/dream/operations/synthesize.ts index 8b67ab66e..d2e7f9e14 100644 --- a/src/server/infra/dream/operations/synthesize.ts +++ b/src/server/infra/dream/operations/synthesize.ts @@ -17,9 +17,12 @@ 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 {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' @@ -27,6 +30,12 @@ import {parseDreamResponse} from '../parse-dream-response.js' export type SynthesizeDeps = { agent: ICipherAgent contextTreeDir: string + /** + * 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}>}> } @@ -92,7 +101,7 @@ export async function synthesize(deps: SynthesizeDeps): Promise { const slug = slugify(candidate.title) const relativePath = `${candidate.placement}/${slug}.md` @@ -243,7 +253,6 @@ async function writeSynthesisFile( /* eslint-disable camelcase */ const frontmatter = { confidence: candidate.confidence, - maturity: 'draft', sources, synthesized_at: new Date().toISOString(), type: 'synthesis', @@ -264,6 +273,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(undefined, 'synthesize', 'seed', relativePath, error) + } + } + return { action: 'CREATE', confidence: candidate.confidence, diff --git a/src/server/infra/executor/dream-executor.ts b/src/server/infra/executor/dream-executor.ts index d823303a0..80c9438e0 100644 --- a/src/server/infra/executor/dream-executor.ts +++ b/src/server/infra/executor/dream-executor.ts @@ -288,6 +288,7 @@ export class DreamExecutor { ...(await synthesize({ agent, contextTreeDir, + runtimeSignalStore: this.deps.runtimeSignalStore, searchService: this.deps.searchService, signal, taskId, diff --git a/test/unit/infra/dream/operations/synthesize.test.ts b/test/unit/infra/dream/operations/synthesize.test.ts index 6dd7b2b53..c82018f6c 100644 --- a/test/unit/infra/dream/operations/synthesize.test.ts +++ b/test/unit/infra/dream/operations/synthesize.test.ts @@ -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): Promise { @@ -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.') }) @@ -478,4 +480,106 @@ 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', + }])) + + await synthesize({...deps, runtimeSignalStore: signalStore}) + + const signals = await signalStore.get('auth/seeded-pattern.md') + 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', + }, + ])) + + await synthesize({...deps, runtimeSignalStore: signalStore}) + + const sig1 = await signalStore.get('auth/multi-one.md') + const sig2 = await signalStore.get('api/multi-two.md') + expect(sig1.importance).to.equal(50) + expect(sig2.importance).to.equal(50) + }) + + it('succeeds even when sidecar store is not provided', async () => { + 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') + }) + }) }) From e81045d58ae3594e01acc1a95edbdc2a80dfc942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Thu=E1=BA=ADn=20Ph=C3=A1t?= Date: Sun, 19 Apr 2026 15:19:02 +0700 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?test=20reliability,=20fail-open=20test,=20logger=20threading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Spy on signalStore.set with callThrough to prove set() was called (not just asserting defaults that match missing-path fallback) - Add fail-open test: store.set() throws, synthesis file still created - Thread ILogger through SynthesizeDeps so warnSidecarFailure produces observable output instead of silently no-opping --- .../infra/dream/operations/synthesize.ts | 11 ++++-- .../infra/dream/operations/synthesize.test.ts | 35 ++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/server/infra/dream/operations/synthesize.ts b/src/server/infra/dream/operations/synthesize.ts index d2e7f9e14..dca8bce39 100644 --- a/src/server/infra/dream/operations/synthesize.ts +++ b/src/server/infra/dream/operations/synthesize.ts @@ -17,6 +17,7 @@ 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' @@ -30,6 +31,11 @@ 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 @@ -101,7 +107,7 @@ export async function synthesize(deps: SynthesizeDeps): Promise { const slug = slugify(candidate.title) const relativePath = `${candidate.placement}/${slug}.md` @@ -280,7 +287,7 @@ async function writeSynthesisFile( try { await runtimeSignalStore.set(relativePath, createDefaultRuntimeSignals()) } catch (error) { - warnSidecarFailure(undefined, 'synthesize', 'seed', relativePath, error) + warnSidecarFailure(logger, 'synthesize', 'seed', relativePath, error) } } diff --git a/test/unit/infra/dream/operations/synthesize.test.ts b/test/unit/infra/dream/operations/synthesize.test.ts index c82018f6c..ff1f3ad06 100644 --- a/test/unit/infra/dream/operations/synthesize.test.ts +++ b/test/unit/infra/dream/operations/synthesize.test.ts @@ -524,8 +524,12 @@ describe('synthesize', () => { 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') expect(signals.importance).to.equal(50) expect(signals.maturity).to.equal('draft') @@ -554,12 +558,35 @@ describe('synthesize', () => { }, ])) + const setSpy = stub(signalStore, 'set').callThrough() + await synthesize({...deps, runtimeSignalStore: signalStore}) - const sig1 = await signalStore.get('auth/multi-one.md') - const sig2 = await signalStore.get('api/multi-two.md') - expect(sig1.importance).to.equal(50) - expect(sig2.importance).to.equal(50) + 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 () => {