Render clarify search prompts as guidance only

This commit is contained in:
master
2026-03-08 11:50:34 +02:00
parent e01a499df9
commit abbfe64bd7
10 changed files with 224 additions and 124 deletions

View File

@@ -0,0 +1,77 @@
# Sprint 20260308-011 - FE Search Clarify Guidance Not Query
## Topic & Scope
- Stop rendering clarify prompts as executable search suggestions when they are only asking the operator to narrow the target.
- Fix the release route wording so `/releases/**` no longer suggests dead natural-language queries like "Which environment or release should I narrow this to?".
- Cover the behavior with component and browser tests so dead clarify prompts do not regress back into the shipped search experience.
- Working directory: `src/Web/StellaOps.Web`.
- Expected evidence: component tests, Playwright regression, and updated UI contract docs.
## Dependencies & Concurrency
- Depends on the archived search rollout/correction sprints and the archived live search setup lane.
- Safe parallelism: do not edit unrelated Router, release cutover, or audit-bundle files while executing this search-only fix.
- Cross-module allowance: documentation updates may touch `docs/modules/ui/**` when clarifying the search self-serve contract.
## Documentation Prerequisites
- `src/Web/StellaOps.Web/AGENTS.md`
- `docs/modules/ui/search-self-serve-contract.md`
- `docs/modules/ui/search-zero-learning-primary-entry.md`
## Delivery Tracker
### FE-CLARIFY-001 - Treat clarify prompts as guidance, not runnable search chips
Status: DONE
Dependency: none
Owners: Developer (FE), QA
Task description:
- Update the global search answer panel so clarify prompts are rendered as guidance hints instead of clickable search chips.
- Keep grounded follow-up questions executable; only the clarify branch should stop behaving like a query launcher.
Completion criteria:
- [x] Clarify prompts no longer execute searches when rendered from the answer panel.
- [x] Grounded follow-up questions remain executable.
- [x] Search UX still offers actionable next steps through viable starters and related searches.
### FE-CLARIFY-002 - Fix release-route clarify wording and regression coverage
Status: DONE
Dependency: FE-CLARIFY-001
Owners: Developer (FE), Test Automation
Task description:
- Replace the release clarify wording with guidance phrasing instead of a literal natural-language question.
- Add unit and Playwright coverage proving the clarify UI is guidance-only and release users do not see the old dead-query text as an executable suggestion.
Completion criteria:
- [x] Release clarify copy is guidance-oriented.
- [x] Unit coverage proves clarify prompts are guidance-only.
- [x] Playwright regression covers the clarify-answer rendering path.
### FE-CLARIFY-003 - Document and close
Status: DONE
Dependency: FE-CLARIFY-002
Owners: Documentation, Project Manager
Task description:
- Update the UI search contract docs to state that clarify prompts are explanatory narrowing guidance, not executable searches.
- Archive the sprint after implementation and evidence are recorded.
Completion criteria:
- [x] UI docs reflect the clarify-guidance behavior.
- [x] Execution log captures code/test evidence.
- [x] Sprint is archived only after all tasks are DONE.
## Execution Log
| Date (UTC) | Update | Owner |
| --- | --- | --- |
| 2026-03-08 | Sprint created to stop dead clarify prompts from being surfaced like runnable searches. | Developer |
| 2026-03-08 | Updated global search clarify rendering to guidance-only rows, changed route clarifier copy to narrowing guidance, and kept grounded follow-up questions executable. | Developer |
| 2026-03-08 | Verified focused FE coverage: `ambient-context.service.spec.ts` + `global-search.component.spec.ts` passed `37/37`; Playwright clarify regression pack passed `18/18`. | QA |
## Decisions & Risks
- Decision: fix this at the search contract level rather than patching only `/releases/versions`.
- Risk: clarify prompts currently share the same rendering path as grounded follow-up questions.
- Mitigation: split the clarify-answer rendering path while keeping grounded follow-ups and related searches untouched.
- Decision: clarify prompts are now guidance copy everywhere they are generated, so the UI never presents a dead natural-language query as an executable chip.
- Docs: `docs/modules/ui/search-self-serve-contract.md`, `docs/modules/ui/search-zero-learning-primary-entry.md`.
## Next Checkpoints
- 2026-03-08: clarify answer panel updated and covered by tests.
- 2026-03-08: sprint archived after green regression coverage.

View File

@@ -42,7 +42,7 @@
- `clarify` - `clarify`
- `insufficient` - `insufficient`
- `grounded` states must expose visible evidence summary plus citations or top-result references. - `grounded` states must expose visible evidence summary plus citations or top-result references.
- `clarify` states must offer narrowing questions instead of a blank panel. - `clarify` states must offer narrowing guidance instead of a blank panel.
- `insufficient` states must explain the lack of grounding and still provide credible next questions or next searches. - `insufficient` states must explain the lack of grounding and still provide credible next questions or next searches.
## Runtime behavior ## Runtime behavior
@@ -53,7 +53,7 @@
- fallback -> related follow-up question - fallback -> related follow-up question
- Result-state answer panels use: - Result-state answer panels use:
- `commonQuestions[]` as follow-up questions when grounded evidence exists - `commonQuestions[]` as follow-up questions when grounded evidence exists
- `clarifyingQuestions[]` when no grounded answer exists - `clarifyingQuestions[]` as non-executable narrowing guidance when no grounded answer exists
- "Related searches" remains driven by contextual chip logic so pages do not need to define a second parallel action system. - "Related searches" remains driven by contextual chip logic so pages do not need to define a second parallel action system.
## Optional telemetry hooks ## Optional telemetry hooks
@@ -85,7 +85,7 @@
2. Add unit coverage for answer-state rendering in `global-search.component.spec.ts`. 2. Add unit coverage for answer-state rendering in `global-search.component.spec.ts`.
3. Add Playwright coverage for: 3. Add Playwright coverage for:
- grounded answer - grounded answer
- clarify recovery - clarify guidance rendering
- answer-to-AdvisoryAI handoff - answer-to-AdvisoryAI handoff
- priority-route rollout journeys (`findings`, `policy`, `doctor`, `timeline`, `releases`) - priority-route rollout journeys (`findings`, `policy`, `doctor`, `timeline`, `releases`)
4. Keep route and API behavior mocked/deterministic; no live network dependencies. 4. Keep route and API behavior mocked/deterministic; no live network dependencies.

View File

@@ -18,12 +18,14 @@
- The current history contract cannot reliably remove old failed searches after reload because the local store still accepts legacy bare-string entries with no result outcome attached. - The current history contract cannot reliably remove old failed searches after reload because the local store still accepts legacy bare-string entries with no result outcome attached.
- `Did you mean` is still visually tied to the results surface rather than the input correction moment. It needs to live immediately below the search field. - `Did you mean` is still visually tied to the results surface rather than the input correction moment. It needs to live immediately below the search field.
- Suggestions are still too easy to surface without enough corpus proof. Search must treat corpus readiness and suggestion executability as a product requirement, not a test-only concern. - Suggestions are still too easy to surface without enough corpus proof. Search must treat corpus readiness and suggestion executability as a product requirement, not a test-only concern.
- Clarify prompts can still be mistaken for executable searches when they are phrased like literal user queries. Those prompts must behave as narrowing guidance, not dead-end starter searches.
## What still fails after direct operator usage ## What still fails after direct operator usage
- Search and assistant still feel like sibling products in some flows. The operator should always start from search; assistant is a secondary detail view opened from that same surface. - Search and assistant still feel like sibling products in some flows. The operator should always start from search; assistant is a secondary detail view opened from that same surface.
- Any visible "scope" mechanic is wrong for the default path. Current route, visible entities, and recent actions should weight results automatically. If the best answer is outside the current page, show it as overflow only after trying the current page first. - Any visible "scope" mechanic is wrong for the default path. Current route, visible entities, and recent actions should weight results automatically. If the best answer is outside the current page, show it as overflow only after trying the current page first.
- The product still risks teaching internal mechanics through labels, panels, and helper copy. The operator should not need to learn Stella structure, search science, or result modes just to get a useful answer. - The product still risks teaching internal mechanics through labels, panels, and helper copy. The operator should not need to learn Stella structure, search science, or result modes just to get a useful answer.
- Suggestions are not acceptable unless they execute. A surfaced starter chip that leads to zero useful results is a product defect even when the service is healthy. - Suggestions are not acceptable unless they execute. A surfaced starter chip that leads to zero useful results is a product defect even when the service is healthy.
- Clarify text must not look like an executable starter search unless the backend has explicitly grounded it. Guidance such as "add an environment or release" belongs in the answer panel as guidance, not as a clickable query.
- When multiple high-confidence results are close, search should summarize them automatically. The assistant exists to expand and deepen the answer, not to compensate for a weak primary result. - When multiple high-confidence results are close, search should summarize them automatically. The assistant exists to expand and deepen the answer, not to compensate for a weak primary result.
## Product rules ## Product rules

View File

@@ -94,11 +94,11 @@ export const DEFAULT_COMMON_QUESTIONS: readonly SearchQuestionChip[] = withQuest
export const DEFAULT_CLARIFYING_QUESTIONS: readonly SearchQuestionChip[] = withQuestionKind([ export const DEFAULT_CLARIFYING_QUESTIONS: readonly SearchQuestionChip[] = withQuestionKind([
{ {
key: 'ui.search.question.default.clarify_target', key: 'ui.search.question.default.clarify_target',
fallback: 'Which workload, release, or policy should I narrow this to?', fallback: 'Add the workload, release, or policy you want to inspect.',
}, },
{ {
key: 'ui.search.question.default.clarify_scope', key: 'ui.search.question.default.clarify_scope',
fallback: 'What exact blocker or symptom should I narrow this to?', fallback: 'Add the exact blocker or symptom you want to inspect.',
}, },
], 'clarify'); ], 'clarify');
@@ -153,8 +153,8 @@ const FINDINGS_SELF_SERVE: SearchSelfServeDefinition = {
{ key: 'ui.search.question.findings.remediation', fallback: 'What is the safest remediation path?' }, { key: 'ui.search.question.findings.remediation', fallback: 'What is the safest remediation path?' },
], 'page'), ], 'page'),
clarifyingQuestions: withQuestionKind([ clarifyingQuestions: withQuestionKind([
{ key: 'ui.search.question.findings.clarify_target', fallback: 'Which CVE, workload, or package should I narrow this to?' }, { key: 'ui.search.question.findings.clarify_target', fallback: 'Add the CVE, workload, or package you want to inspect.' },
{ key: 'ui.search.question.findings.clarify_scope', fallback: 'What part of this finding matters most right now?' }, { key: 'ui.search.question.findings.clarify_scope', fallback: 'Add the part of the finding that matters most right now.' },
], 'clarify'), ], 'clarify'),
}; };
@@ -165,8 +165,8 @@ const VEX_SELF_SERVE: SearchSelfServeDefinition = {
{ key: 'ui.search.question.vex.conflicting_evidence', fallback: 'What evidence conflicts with this VEX?' }, { key: 'ui.search.question.vex.conflicting_evidence', fallback: 'What evidence conflicts with this VEX?' },
], 'page'), ], 'page'),
clarifyingQuestions: withQuestionKind([ clarifyingQuestions: withQuestionKind([
{ key: 'ui.search.question.vex.clarify_statement', fallback: 'Which statement, component, or product range should I narrow this to?' }, { key: 'ui.search.question.vex.clarify_statement', fallback: 'Add the statement, component, or product range you want to inspect.' },
{ key: 'ui.search.question.vex.clarify_need', fallback: 'Do you want exploitability meaning, coverage, or conflict evidence?' }, { key: 'ui.search.question.vex.clarify_need', fallback: 'Add whether you need exploitability meaning, coverage, or conflict evidence.' },
], 'clarify'), ], 'clarify'),
}; };
@@ -177,8 +177,8 @@ const POLICY_SELF_SERVE: SearchSelfServeDefinition = {
{ key: 'ui.search.question.policy.exception', fallback: 'What is the safest exception path?' }, { key: 'ui.search.question.policy.exception', fallback: 'What is the safest exception path?' },
], 'page'), ], 'page'),
clarifyingQuestions: withQuestionKind([ clarifyingQuestions: withQuestionKind([
{ key: 'ui.search.question.policy.clarify_target', fallback: 'Which rule, environment, or control should I narrow this to?' }, { key: 'ui.search.question.policy.clarify_target', fallback: 'Add the rule, environment, or control you want to inspect.' },
{ key: 'ui.search.question.policy.clarify_need', fallback: 'Do you want recent failures, exceptions, or promotion impact?' }, { key: 'ui.search.question.policy.clarify_need', fallback: 'Add whether you need recent failures, exceptions, or promotion impact.' },
], 'clarify'), ], 'clarify'),
}; };
@@ -189,8 +189,8 @@ const DOCTOR_SELF_SERVE: SearchSelfServeDefinition = {
{ key: 'ui.search.question.doctor.changed', fallback: 'What changed before this health issue?' }, { key: 'ui.search.question.doctor.changed', fallback: 'What changed before this health issue?' },
], 'page'), ], 'page'),
clarifyingQuestions: withQuestionKind([ clarifyingQuestions: withQuestionKind([
{ key: 'ui.search.question.doctor.clarify_check', fallback: 'Which check or symptom should I narrow this to?' }, { key: 'ui.search.question.doctor.clarify_check', fallback: 'Add the check or symptom you want to inspect.' },
{ key: 'ui.search.question.doctor.clarify_need', fallback: 'Do you want diagnosis, remediation, or verification steps?' }, { key: 'ui.search.question.doctor.clarify_need', fallback: 'Add whether you need diagnosis, remediation, or verification steps.' },
], 'clarify'), ], 'clarify'),
}; };
@@ -201,8 +201,8 @@ const GRAPH_SELF_SERVE: SearchSelfServeDefinition = {
{ key: 'ui.search.question.graph.next_hop', fallback: 'What should I inspect next on this path?' }, { key: 'ui.search.question.graph.next_hop', fallback: 'What should I inspect next on this path?' },
], 'page'), ], 'page'),
clarifyingQuestions: withQuestionKind([ clarifyingQuestions: withQuestionKind([
{ key: 'ui.search.question.graph.clarify_node', fallback: 'Which node, package, or edge should I narrow this to?' }, { key: 'ui.search.question.graph.clarify_node', fallback: 'Add the node, package, or edge you want to inspect.' },
{ key: 'ui.search.question.graph.clarify_need', fallback: 'Do you want reachability, impact, or next-step guidance?' }, { key: 'ui.search.question.graph.clarify_need', fallback: 'Add whether you need reachability, impact, or next-step guidance.' },
], 'clarify'), ], 'clarify'),
}; };
@@ -213,8 +213,8 @@ const OPS_MEMORY_SELF_SERVE: SearchSelfServeDefinition = {
{ key: 'ui.search.question.ops_memory.repeat', fallback: 'What repeated failures are related to this?' }, { key: 'ui.search.question.ops_memory.repeat', fallback: 'What repeated failures are related to this?' },
], 'page'), ], 'page'),
clarifyingQuestions: withQuestionKind([ clarifyingQuestions: withQuestionKind([
{ key: 'ui.search.question.ops_memory.clarify_job', fallback: 'Which job, incident, or recurring failure should I narrow this to?' }, { key: 'ui.search.question.ops_memory.clarify_job', fallback: 'Add the job, incident, or recurring failure you want to inspect.' },
{ key: 'ui.search.question.ops_memory.clarify_need', fallback: 'Do you want precedent, likely cause, or recommended recovery?' }, { key: 'ui.search.question.ops_memory.clarify_need', fallback: 'Add whether you need precedent, likely cause, or recommended recovery.' },
], 'clarify'), ], 'clarify'),
}; };
@@ -225,8 +225,8 @@ const TIMELINE_SELF_SERVE: SearchSelfServeDefinition = {
{ key: 'ui.search.question.timeline.next_event', fallback: 'What else happened around this event?' }, { key: 'ui.search.question.timeline.next_event', fallback: 'What else happened around this event?' },
], 'page'), ], 'page'),
clarifyingQuestions: withQuestionKind([ clarifyingQuestions: withQuestionKind([
{ key: 'ui.search.question.timeline.clarify_window', fallback: 'Which deployment, incident, or time window should I narrow this to?' }, { key: 'ui.search.question.timeline.clarify_window', fallback: 'Add the deployment, incident, or time window you want to inspect.' },
{ key: 'ui.search.question.timeline.clarify_need', fallback: 'Do you want causes, impacts, or follow-up events?' }, { key: 'ui.search.question.timeline.clarify_need', fallback: 'Add whether you need causes, impacts, or follow-up events.' },
], 'clarify'), ], 'clarify'),
}; };
@@ -237,8 +237,8 @@ const RELEASES_SELF_SERVE: SearchSelfServeDefinition = {
{ key: 'ui.search.question.releases.next_step', fallback: 'What is the safest next step to ship?' }, { key: 'ui.search.question.releases.next_step', fallback: 'What is the safest next step to ship?' },
], 'page'), ], 'page'),
clarifyingQuestions: withQuestionKind([ clarifyingQuestions: withQuestionKind([
{ key: 'ui.search.question.releases.clarify_target', fallback: 'Which environment or release should I narrow this to?' }, { key: 'ui.search.question.releases.clarify_target', fallback: 'Add the environment or release you want to inspect.' },
{ key: 'ui.search.question.releases.clarify_need', fallback: 'Do you want blockers, approvals, or policy impact?' }, { key: 'ui.search.question.releases.clarify_need', fallback: 'Add whether you need blockers, approvals, or policy impact.' },
], 'clarify'), ], 'clarify'),
}; };

View File

@@ -78,6 +78,8 @@ type SearchAnswerView = {
}>; }>;
questionLabel: string; questionLabel: string;
questions: SearchQuestionView[]; questions: SearchQuestionView[];
guidanceLabel: string | null;
guidance: SearchQuestionView[];
nextSearches: SearchSuggestionView[]; nextSearches: SearchSuggestionView[];
}; };
type SuccessfulSearchHistoryEntry = { type SuccessfulSearchHistoryEntry = {
@@ -202,6 +204,22 @@ type SuccessfulSearchHistoryEntry = {
</div> </div>
} }
@if (answer.guidance.length > 0) {
<div class="search__answer-guidance">
<div class="search__group-label">{{ answer.guidanceLabel }}</div>
<div class="search__guidance-list">
@for (question of answer.guidance; track question.query) {
<div
class="search__guidance-item"
data-answer-guidance="clarify"
>
{{ question.query }}
</div>
}
</div>
</div>
}
@if (answer.nextSearches.length > 0) { @if (answer.nextSearches.length > 0) {
<div class="search__answer-next"> <div class="search__answer-next">
<div class="search__group-label">{{ t('ui.search.answer.next_searches', 'Related searches') }}</div> <div class="search__group-label">{{ t('ui.search.answer.next_searches', 'Related searches') }}</div>
@@ -681,6 +699,26 @@ type SuccessfulSearchHistoryEntry = {
border-style: dashed; border-style: dashed;
} }
.search__answer-guidance {
margin-top: 0.75rem;
}
.search__guidance-list {
display: grid;
gap: 0.5rem;
padding: 0.25rem 0.75rem 0;
}
.search__guidance-item {
padding: 0.5rem 0.625rem;
border: 1px dashed var(--color-border-secondary);
border-radius: var(--radius-sm);
background: var(--color-surface-primary);
color: var(--color-text-secondary);
font-size: 0.75rem;
line-height: 1.35;
}
.search__answer { .search__answer {
margin: 0.625rem 0.75rem 0.5rem; margin: 0.625rem 0.75rem 0.5rem;
padding: 0.75rem; padding: 0.75rem;
@@ -1174,7 +1212,15 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
questionLabel: backendAnswer.status === 'clarify' questionLabel: backendAnswer.status === 'clarify'
? this.t('ui.search.answer.questions.clarify', 'Try one of these') ? this.t('ui.search.answer.questions.clarify', 'Try one of these')
: this.t('ui.search.answer.questions.follow_up', 'Related questions'), : this.t('ui.search.answer.questions.follow_up', 'Related questions'),
questions: this.buildBackendAnswerQuestions(backendAnswer), questions: backendAnswer.status === 'clarify'
? []
: this.buildBackendAnswerQuestions(backendAnswer),
guidanceLabel: backendAnswer.status === 'clarify'
? this.t('ui.search.answer.guidance.clarify', 'Narrow the search by adding')
: null,
guidance: backendAnswer.status === 'clarify'
? this.buildClarifyGuidance(backendAnswer)
: [],
nextSearches, nextSearches,
}; };
} }
@@ -1191,6 +1237,8 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
citations: this.buildAnswerCitations(response), citations: this.buildAnswerCitations(response),
questionLabel: this.t('ui.search.answer.questions.follow_up', 'Related questions'), questionLabel: this.t('ui.search.answer.questions.follow_up', 'Related questions'),
questions: this.commonQuestions().slice(0, 3), questions: this.commonQuestions().slice(0, 3),
guidanceLabel: null,
guidance: [],
nextSearches, nextSearches,
}; };
} }
@@ -1211,8 +1259,10 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
'No direct grounded answer was found from the current page context.', 'No direct grounded answer was found from the current page context.',
), ),
citations: [], citations: [],
questionLabel: this.t('ui.search.answer.questions.clarify', 'Try one of these'), questionLabel: '',
questions: clarifyingQuestions, questions: [],
guidanceLabel: this.t('ui.search.answer.guidance.clarify', 'Narrow the search by adding'),
guidance: clarifyingQuestions,
nextSearches, nextSearches,
}; };
} }
@@ -1233,6 +1283,8 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
citations: [], citations: [],
questionLabel: this.t('ui.search.answer.questions.retry', 'Try one of these'), questionLabel: this.t('ui.search.answer.questions.retry', 'Try one of these'),
questions: this.commonQuestions().slice(0, 2), questions: this.commonQuestions().slice(0, 2),
guidanceLabel: null,
guidance: [],
nextSearches, nextSearches,
}; };
}); });
@@ -1974,6 +2026,21 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
: this.commonQuestions().slice(0, 3); : this.commonQuestions().slice(0, 3);
} }
private buildClarifyGuidance(answer: NonNullable<UnifiedSearchResponse['contextAnswer']>): SearchQuestionView[] {
const backendGuidance = (answer.questions ?? [])
.filter((question) => question.query.trim().length > 0)
.map((question) => ({
query: question.query,
kind: 'clarify' as const,
}));
if (backendGuidance.length > 0) {
return backendGuidance.slice(0, 3);
}
return this.clarifyingQuestions().slice(0, 3);
}
private answerTitleForStatus(status: SearchAnswerView['status']): string { private answerTitleForStatus(status: SearchAnswerView['status']): string {
switch (status) { switch (status) {
case 'clarify': case 'clarify':

View File

@@ -199,8 +199,8 @@ describe('AmbientContextService', () => {
events.next(new NavigationEnd(1, '/ops/policy', '/ops/policy')); events.next(new NavigationEnd(1, '/ops/policy', '/ops/policy'));
const questions = service.getClarifyingQuestions().map((item) => item.fallback); const questions = service.getClarifyingQuestions().map((item) => item.fallback);
expect(questions).toContain('Which rule, environment, or control should I narrow this to?'); expect(questions).toContain('Add the rule, environment, or control you want to inspect.');
expect(questions).toContain('Do you want recent failures, exceptions, or promotion impact?'); expect(questions).toContain('Add whether you need recent failures, exceptions, or promotion impact.');
}); });
it('returns timeline self-serve questions after navigating to the timeline route', () => { it('returns timeline self-serve questions after navigating to the timeline route', () => {
@@ -215,8 +215,8 @@ describe('AmbientContextService', () => {
expect(commonQuestions).toContain('What changed before this incident?'); expect(commonQuestions).toContain('What changed before this incident?');
expect(commonQuestions).toContain('Which release introduced this risk?'); expect(commonQuestions).toContain('Which release introduced this risk?');
expect(clarifyingQuestions).toContain('Which deployment, incident, or time window should I narrow this to?'); expect(clarifyingQuestions).toContain('Add the deployment, incident, or time window you want to inspect.');
expect(clarifyingQuestions).toContain('Do you want causes, impacts, or follow-up events?'); expect(clarifyingQuestions).toContain('Add whether you need causes, impacts, or follow-up events.');
}); });
it('returns release-control self-serve questions for release routes without forcing a search domain', () => { it('returns release-control self-serve questions for release routes without forcing a search domain', () => {
@@ -233,7 +233,7 @@ describe('AmbientContextService', () => {
expect(panel?.titleFallback).toBe('Release control'); expect(panel?.titleFallback).toBe('Release control');
expect(commonQuestions).toContain('What blocked this promotion?'); expect(commonQuestions).toContain('What blocked this promotion?');
expect(commonQuestions).toContain('Which approvals are missing?'); expect(commonQuestions).toContain('Which approvals are missing?');
expect(clarifyingQuestions).toContain('Which environment or release should I narrow this to?'); expect(clarifyingQuestions).toContain('Add the environment or release you want to inspect.');
expect(clarifyingQuestions).toContain('Do you want blockers, approvals, or policy impact?'); expect(clarifyingQuestions).toContain('Add whether you need blockers, approvals, or policy impact.');
}); });
}); });

View File

@@ -158,12 +158,12 @@ describe('GlobalSearchComponent', () => {
ambientContext.getClarifyingQuestions.and.returnValue([ ambientContext.getClarifyingQuestions.and.returnValue([
{ {
key: 'ui.search.question.findings.clarify_target', key: 'ui.search.question.findings.clarify_target',
fallback: 'Which CVE, workload, or package should I narrow this to?', fallback: 'Add the CVE, workload, or package you want to inspect.',
kind: 'clarify', kind: 'clarify',
}, },
{ {
key: 'ui.search.question.findings.clarify_scope', key: 'ui.search.question.findings.clarify_scope',
fallback: 'What part of this finding matters most right now?', fallback: 'Add the part of the finding that matters most right now.',
kind: 'clarify', kind: 'clarify',
}, },
]); ]);
@@ -807,11 +807,15 @@ describe('GlobalSearchComponent', () => {
const answerQuestions = Array.from( const answerQuestions = Array.from(
fixture.nativeElement.querySelectorAll('[data-answer-question]') as NodeListOf<HTMLButtonElement>, fixture.nativeElement.querySelectorAll('[data-answer-question]') as NodeListOf<HTMLButtonElement>,
).map((node) => node.textContent?.trim()); ).map((node) => node.textContent?.trim());
const guidanceItems = Array.from(
fixture.nativeElement.querySelectorAll('[data-answer-guidance="clarify"]') as NodeListOf<HTMLElement>,
).map((node) => node.textContent?.trim());
expect(answerPanel).not.toBeNull(); expect(answerPanel).not.toBeNull();
expect(answerPanel?.textContent).toContain('Tighten the question'); expect(answerPanel?.textContent).toContain('Tighten the question');
expect(answerQuestions).toContain('Which CVE, workload, or package should I narrow this to?'); expect(answerQuestions).toEqual([]);
expect(answerQuestions).toContain('What part of this finding matters most right now?'); expect(guidanceItems).toContain('Add the CVE, workload, or package you want to inspect.');
expect(guidanceItems).toContain('Add the part of the finding that matters most right now.');
}); });
it('does not hard-filter search requests to the current route scope', async () => { it('does not hard-filter search requests to the current route scope', async () => {

View File

@@ -342,6 +342,26 @@ test.describe('Unified Search - Experience Quality UX', () => {
await expect(page.locator('.search__group').filter({ hasText: 'Recent' })).toHaveCount(0); await expect(page.locator('.search__group').filter({ hasText: 'Recent' })).toHaveCount(0);
}); });
test('renders release clarify guidance as non-executable hints on /releases/versions', async ({ page }) => {
await mockSearchResponses(page, () => emptyResponse('mystery release blocker'));
await page.goto('/releases/versions');
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
await typeInSearch(page, 'mystery release blocker');
await waitForResults(page);
await expect(page.locator('[data-answer-status="clarify"]')).toBeVisible();
await expect(page.locator('[data-answer-guidance="clarify"]').filter({
hasText: /add the environment or release you want to inspect\./i,
})).toHaveCount(1);
await expect(page.locator('[data-answer-guidance="clarify"]').filter({
hasText: /add whether you need blockers, approvals, or policy impact\./i,
})).toHaveCount(1);
await expect(page.getByRole('button', { name: 'Add the environment or release you want to inspect.' })).toHaveCount(0);
await expect(page.locator('app-global-search input[type="text"]')).toHaveValue('mystery release blocker');
});
test('uses backend answer framing and shows overflow as secondary results without manual filters', async ({ page }) => { test('uses backend answer framing and shows overflow as secondary results without manual filters', async ({ page }) => {
await mockSearchResponses(page, (query) => await mockSearchResponses(page, (query) =>
query.includes('critical findings') query.includes('critical findings')

View File

@@ -230,48 +230,6 @@ const doctorGroundedResponse = buildResponse(
}, },
); );
const timelineGroundedResponse = buildResponse(
'Which deployment, incident, or time window should I narrow this to?',
[
timelineCard({
eventId: 'incident-db-spike',
title: 'Database latency spike before promotion',
snippet: 'A deployment preceded the incident and introduced the latency spike that now blocks the rollout.',
}),
],
undefined,
{
contextAnswer: {
status: 'grounded',
code: 'retrieved_scope_weighted_evidence',
summary: 'The timeline shows a deployment immediately before the incident that introduced the blocking latency spike.',
reason: 'Timeline evidence became grounded once the time window was narrowed.',
evidence: 'Grounded in 1 source across Timeline.',
citations: [
{
entityKey: 'timeline:incident-db-spike',
title: 'Database latency spike before promotion',
domain: 'timeline',
},
],
},
coverage: {
currentScopeDomain: 'timeline',
currentScopeWeighted: true,
domains: [
{
domain: 'timeline',
candidateCount: 1,
visibleCardCount: 1,
topScore: 0.9,
isCurrentScope: true,
hasVisibleResults: true,
},
],
},
},
);
const releasesGroundedResponse = buildResponse( const releasesGroundedResponse = buildResponse(
'What blocked this promotion?', 'What blocked this promotion?',
[ [
@@ -437,7 +395,7 @@ test.describe('Unified Search - Priority Route Self-Serve Journeys', () => {
}).first()).toBeVisible(); }).first()).toBeVisible();
}); });
test('recovers a timeline search from clarify to grounded with the route-owned narrowing question', async ({ page }) => { test('shows timeline narrowing guidance instead of a runnable clarify prompt', async ({ page }) => {
await mockPriorityRouteSearch(page); await mockPriorityRouteSearch(page);
await openRoute(page, '/ops/timeline'); await openRoute(page, '/ops/timeline');
@@ -445,14 +403,12 @@ test.describe('Unified Search - Priority Route Self-Serve Journeys', () => {
await waitForResults(page); await waitForResults(page);
await expect(page.locator('[data-answer-status="clarify"]')).toBeVisible(); await expect(page.locator('[data-answer-status="clarify"]')).toBeVisible();
await page.getByRole('button', { name: 'Which deployment, incident, or time window should I narrow this to?' }).click(); await expect(page.locator('[data-answer-guidance="clarify"]')).toContainText([
await waitForResults(page); 'Add the deployment, incident, or time window you want to inspect.',
await waitForEntityCards(page, 1); 'Add whether you need causes, impacts, or follow-up events.',
]);
await expect(page.locator('app-global-search input[type="text"]')).toHaveValue( await expect(page.getByRole('button', { name: 'Add the deployment, incident, or time window you want to inspect.' })).toHaveCount(0);
'Which deployment, incident, or time window should I narrow this to?', await expect(page.locator('app-global-search input[type="text"]')).toHaveValue('spike');
);
await expect(page.locator('[data-answer-status="grounded"]')).toContainText(/deployment immediately before the incident/i);
}); });
test('reruns a policy next-search inside the current policy route context', async ({ page }) => { test('reruns a policy next-search inside the current policy route context', async ({ page }) => {
@@ -557,10 +513,6 @@ function resolvePriorityRouteResponse(currentRoute: string, query: string): unkn
} }
if (currentRoute.includes('/ops/timeline')) { if (currentRoute.includes('/ops/timeline')) {
if (query.includes('which deployment, incident, or time window should i narrow this to')) {
return timelineGroundedResponse;
}
if (query.includes('spike')) { if (query.includes('spike')) {
return emptyResponse('spike'); return emptyResponse('spike');
} }

View File

@@ -46,25 +46,6 @@ const groundedFindingResponse = {
}, },
}; };
const narrowedFindingResponse = buildResponse(
'Which CVE, workload, or package should I narrow this to?',
[
findingCard({
cveId: 'CVE-2023-38545',
title: 'CVE-2023-38545 in edge-router',
snippet: 'A narrowed finding result is now grounded and ready for review.',
severity: 'high',
}),
],
{
summary: 'Narrowing the question exposed a grounded finding answer.',
template: 'finding_overview',
confidence: 'high',
sourceCount: 1,
domainsCovered: ['Findings'],
},
);
test.describe('Unified Search - Self-Serve Answer Panel', () => { test.describe('Unified Search - Self-Serve Answer Panel', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await setupBasicMocks(page); await setupBasicMocks(page);
@@ -103,12 +84,8 @@ test.describe('Unified Search - Self-Serve Answer Panel', () => {
await expect(answerPanel.locator('[data-answer-citation]')).toContainText(['CVE-2024-21626 in api-gateway']); await expect(answerPanel.locator('[data-answer-citation]')).toContainText(['CVE-2024-21626 in api-gateway']);
}); });
test('uses clarify questions to rerun search and recover a grounded answer', async ({ page }) => { test('shows clarify guidance without rendering dead clarify prompts as buttons', async ({ page }) => {
await mockSearchResponses(page, (query) => { await mockSearchResponses(page, (query) => {
if (query.includes('which cve, workload, or package should i narrow this to?')) {
return narrowedFindingResponse;
}
if (query.includes('mystery issue')) { if (query.includes('mystery issue')) {
return emptyResponse('mystery issue'); return emptyResponse('mystery issue');
} }
@@ -122,17 +99,18 @@ test.describe('Unified Search - Self-Serve Answer Panel', () => {
await typeInSearch(page, 'mystery issue'); await typeInSearch(page, 'mystery issue');
await waitForResults(page); await waitForResults(page);
await expect(page.locator('[data-answer-status="clarify"]')).toBeVisible(); await expect(page.locator('[data-answer-status="clarify"]')).toBeVisible();
await expect(
await page.getByRole('button', { name: 'Which CVE, workload, or package should I narrow this to?' }).click(); page.locator('[data-answer-guidance="clarify"]').filter({
await waitForResults(page); hasText: /Add the CVE, workload, or package you want to inspect\./i,
await waitForEntityCards(page, 1); }),
).toHaveCount(1);
await expect(page.locator('app-global-search input[type="text"]')).toHaveValue( await expect(
'Which CVE, workload, or package should I narrow this to?', page.locator('[data-answer-guidance="clarify"]').filter({
); hasText: /Add the part of the finding that matters most right now\./i,
await expect(page.locator('[data-answer-status="grounded"]')).toContainText( }),
'Narrowing the question exposed a grounded finding answer.', ).toHaveCount(1);
); await expect(page.getByRole('button', { name: 'Add the CVE, workload, or package you want to inspect.' })).toHaveCount(0);
await expect(page.locator('app-global-search input[type="text"]')).toHaveValue('mystery issue');
}); });
test('opens AdvisoryAI from the answer panel with grounded answer context', async ({ page }) => { test('opens AdvisoryAI from the answer panel with grounded answer context', async ({ page }) => {