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
68 changes: 62 additions & 6 deletions tsworkspace/webapps/studio/src/lib/validation-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,6 @@ describe('validationFixPrompt', () => {
raw: expect.objectContaining({ projectionRole: 'PROJECTION_ROLE_UPSERT' }),
}),
expect.objectContaining({ identity: 'commandSlice:shop/place-order-slice@1' }),
expect.objectContaining({
identity: 'command:shop/place-order@1',
raw: expect.objectContaining({ doc: 'Place the order exactly once.' }),
}),
expect.objectContaining({
identity: 'event:shop/ordered@1',
raw: expect.objectContaining({
Expand All @@ -137,8 +133,9 @@ describe('validationFixPrompt', () => {
);
expect(snapshot(prompt, 'References to fetch')).toEqual({
notLoaded: ['event:payments/payment-confirmed@3'],
loadedButOmitted: [],
loadedButOmitted: ['command:shop/place-order@1'],
});
expect(prompt).not.toContain('Place the order exactly once.');
expect(prompt).not.toContain('unrelated');
});

Expand Down Expand Up @@ -166,10 +163,69 @@ describe('validationFixPrompt', () => {

const prompt = validationFixPrompt(model, finding());

expect(prompt).toContain('commandSlice:shop/unrelated-slice@1');
expect(snapshot(prompt, 'Relevant loaded storyboards')).toEqual([
expect.objectContaining({
orderedSlices: expect.arrayContaining(['slice:shop/unrelated-slice@1']),
}),
]);
expect(prompt).not.toContain('storyboard:shop/unrelated-story@1');
});

it('lists distant storyboard slices as ids to fetch instead of embedding their definitions', () => {
const distant = Array.from({ length: 12 }, (_, index) => id(`distant-${index}`));
const model = buildModel([
...temporalModel().entities.map((entity) => ({ [entity.kind]: entity.raw })),
...distant.map((slice) => ({ commandSlice: { id: slice, doc: `Distant slice ${slice.slug}.` } })),
]);
const storyboard = model.byKey.get('storyboard:shop/checkout@1');
if (!storyboard) throw new Error('Expected checkout storyboard');
storyboard.raw.slices = [...(storyboard.raw.slices as unknown[]), ...distant.map((slice) => ({ id: slice }))];

const prompt = validationFixPrompt(model, finding());

const definitions = snapshot(prompt, 'Loaded subject and related definitions') as { identity: string }[];
expect(definitions.map((definition) => definition.identity)).not.toContain('commandSlice:shop/distant-0@1');
expect(prompt).not.toContain('Distant slice');
const [board] = snapshot(prompt, 'Relevant loaded storyboards') as { orderedSlices: string[] }[];
expect(board?.orderedSlices).toHaveLength(15);
const fetch = snapshot(prompt, 'References to fetch') as { loadedButOmitted: string[] };
expect(fetch.loadedButOmitted).toEqual(
expect.arrayContaining(distant.map((slice) => `slice:shop/${slice.slug}@1`)),
);
});

it('keeps the subject and its direct references but not references of references', () => {
const model = buildModel([
{ event: { id: id('ordered'), doc: 'Direct event.' } },
{
readModel: {
id: id('orders'),
doc: 'Direct read model.',
sourceEvents: [{ id: id('ordered') }, { id: id('far') }],
},
},
{ persona: { id: id('buyer'), doc: 'Direct persona.' } },
{ event: { id: id('far'), doc: 'Two hops away.' } },
{
readModelSlice: {
id: id('project-orders'),
sourceEvents: [{ event: { id: id('ordered') } }],
readModel: { readModel: { id: id('orders') } },
persona: { id: id('buyer') },
},
},
]);
const prompt = validationFixPrompt(model, finding());
const definitions = snapshot(prompt, 'Loaded subject and related definitions') as { identity: string }[];

expect(definitions.map((definition) => definition.identity).sort()).toEqual([
'event:shop/ordered@1',
'persona:shop/buyer@1',
'readModel:shop/orders@1',
'readModelSlice:shop/project-orders@1',
]);
});

it('handles a model-wide finding using the baseline and root membership without dumping every member', () => {
const model = buildModel([
{
Expand Down
20 changes: 11 additions & 9 deletions tsworkspace/webapps/studio/src/lib/validation-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,19 @@ export function validationFixPrompt(
)
) {
include(storyboard);
for (const ref of storyboardSlices(storyboard)) {
remember(ref);
for (const slice of resolve(model, ref)) include(slice);
}
for (const ref of storyboardSlices(storyboard)) remember(ref);
}
}

if (issue.subject || moment) {
for (let depth = 0; depth < 2; depth += 1) {
const refs = [...referenced.values()];
for (const ref of refs) for (const entity of resolve(model, ref)) include(entity);
const hopSources = [issue.subject ? model.byKey.get(identity(issue.subject)) : undefined, moment];
for (const source of hopSources) {
if (!source) continue;
const slicesOfStoryboard = source.kind === 'storyboard';
for (const ref of refsByKey.get(source.key) ?? []) {
if (slicesOfStoryboard && ref.kind === 'slice') continue;
for (const entity of resolve(model, ref)) include(entity);
}
}
} else {
for (const entity of model.byKind.get('eventModel') ?? []) include(entity);
Expand All @@ -162,7 +164,7 @@ export function validationFixPrompt(
'Fix this validation finding in the Atlas project using the `trogon-atlas` CLI or the `trogon-atlas-mcp` MCP tools.',
'Re-read the current model and validation results in the specified branch and scope before editing. The snapshot below may be stale or incomplete.',
'Fetch referenced definitions that are missing or omitted, including their exact pinned versions. Apply a targeted repair that preserves intended behavior, event causality, and existing contracts. Do not merely suppress the rule, downgrade its severity, or delete valid behavior to hide the finding.',
'For temporal findings, inspect the full ordered storyboard, its slice definitions, and the source and emitted events before deciding whether to change ordering, references, or modeling. Do not assume an event missing from this snapshot does not exist.',
'For temporal findings, inspect the full ordered storyboard, fetch the definitions of the slices and events it lists that bear on the finding, and check the source and emitted events before deciding whether to change ordering, references, or modeling. Do not assume an event missing from this snapshot does not exist.',
'Apply the repair, rerun validation for the affected scope, and report the entities changed, the reason for the repair, and the validation results, including any remaining findings. If the model lacks enough information to choose a behavior-preserving repair, explain the specific ambiguity instead of inventing domain behavior.',
'',
'## Scope',
Expand Down Expand Up @@ -207,7 +209,7 @@ export function validationFixPrompt(
'',
'## References to fetch',
json({ notLoaded: missing, loadedButOmitted: omitted }),
'This is a focused snapshot, not the entire model. Follow relevant references in the raw definitions and fetch any additional current definitions needed for the repair. An empty reference list does not prove the model is complete.',
'This is a focused snapshot of the subject and its direct references, not the entire model. Follow relevant references in the raw definitions and fetch any additional current definitions needed for the repair. An empty reference list does not prove the model is complete.',
]
.filter((line) => line !== '')
.join('\n\n');
Expand Down
Loading