Warn on duplicate Ranker source traces
This commit is contained in:
@@ -177,6 +177,32 @@ try {
|
|||||||
assert.equal(hardRail.brief.quickGlance.topPick, 'Manual source-traced build order preview');
|
assert.equal(hardRail.brief.quickGlance.topPick, 'Manual source-traced build order preview');
|
||||||
assert.equal(hardRail.handoff.readiness.status, 'ready');
|
assert.equal(hardRail.handoff.readiness.status, 'ready');
|
||||||
|
|
||||||
|
const duplicateTraceResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
schema: 'prioritix-feature-set-v1',
|
||||||
|
sourceName: 'Scattermind',
|
||||||
|
artifactId: 'concept_map_duplicate_trace',
|
||||||
|
snapshotTitle: 'Duplicate source trace handoff',
|
||||||
|
originalPrompt: 'Clarify a bridge handoff while preserving source trace identity.',
|
||||||
|
idea: 'Scattermind clarified two different continuation moves from one Concept Map source block.',
|
||||||
|
mode: 'mvp',
|
||||||
|
featureSet: {
|
||||||
|
features: [
|
||||||
|
{ id: 'active-copy-slice', title: 'Active copyable build slice', description: 'Show one defended action with source trace.', evidenceNeeded: 'Can a tired builder act on the copied slice?', sourceSection: 'concept-map.nextActions', sourceItemId: 'next-actions#1', rankerHints: { value: 8, effort: 2, confidence: 7, urgency: 7, risk: 2 } },
|
||||||
|
{ id: 'manual-proof-script', title: 'Manual proof script', description: 'Turn the active slice into one script for a real proof conversation.', evidenceNeeded: 'Does the script produce a yes/no signal?', sourceSection: 'concept-map.nextActions', sourceItemId: 'next-actions#1', rankerHints: { value: 7, effort: 2, confidence: 7, urgency: 6, risk: 2 } },
|
||||||
|
{ id: 'later-dashboard', title: 'Saved result dashboard', description: 'Accounts and saved workspaces for every result.', recommendedLane: 'defer', sourceSection: 'concept-map.deferred', sourceItemId: 'deferred#1' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(duplicateTraceResponse.status, 200);
|
||||||
|
const duplicateTrace = await duplicateTraceResponse.json();
|
||||||
|
assert.ok(duplicateTrace.handoff.warnings.some(item => /duplicate source id next-actions#1 used by active-copy-slice and manual-proof-script/.test(item)), 'duplicate source trace IDs should be surfaced for bridge consumers');
|
||||||
|
assert.equal(duplicateTrace.handoff.readiness.status, 'usable-with-warnings');
|
||||||
|
assert.ok(duplicateTrace.handoff.readiness.nextChecks.some(item => /duplicate IDs/i.test(item)));
|
||||||
|
|
||||||
const softGuardrailResponse = await fetch(`${base}/api/rank-feedback`, {
|
const softGuardrailResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -2049,12 +2049,21 @@ function copyableHandoffText({ ranked = [], provenance = {}, decisionContext = {
|
|||||||
function createHandoffContract({ ranked, provenance, decisionContext }) {
|
function createHandoffContract({ ranked, provenance, decisionContext }) {
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
const expectsSourceTrace = Boolean(provenance?.artifactId || provenance?.conceptMapId || provenance?.snapshotTitle);
|
const expectsSourceTrace = Boolean(provenance?.artifactId || provenance?.conceptMapId || provenance?.snapshotTitle);
|
||||||
|
const seenSourceIds = new Map();
|
||||||
if (!provenance?.artifactId && provenance?.originalPrompt) warnings.push('missing source artifact id');
|
if (!provenance?.artifactId && provenance?.originalPrompt) warnings.push('missing source artifact id');
|
||||||
if (expectsSourceTrace && !provenance?.originalPrompt && !provenance?.sourceSummary) warnings.push('missing source context provenance');
|
if (expectsSourceTrace && !provenance?.originalPrompt && !provenance?.sourceSummary) warnings.push('missing source context provenance');
|
||||||
|
|
||||||
const itemTrace = ranked.map(item => {
|
const itemTrace = ranked.map(item => {
|
||||||
if (expectsSourceTrace && !item.provenance?.sourceSection) warnings.push(`missing source section for ${item.id}`);
|
const sourceId = cleanText(item.provenance?.sourceId || '', 120);
|
||||||
|
const sourceSection = cleanText(item.provenance?.sourceSection || '', 80);
|
||||||
|
const traceKey = sourceId && sourceSection ? `${sourceSection}::${sourceId}` : sourceId;
|
||||||
|
if (expectsSourceTrace && !sourceSection) warnings.push(`missing source section for ${item.id}`);
|
||||||
if (item.provenance?.idNormalized) warnings.push(`duplicate source id ${item.provenance.originalId} normalized to ${item.id}`);
|
if (item.provenance?.idNormalized) warnings.push(`duplicate source id ${item.provenance.originalId} normalized to ${item.id}`);
|
||||||
|
if (expectsSourceTrace && traceKey) {
|
||||||
|
const firstItemId = seenSourceIds.get(traceKey);
|
||||||
|
if (firstItemId && firstItemId !== item.id) warnings.push(`duplicate source id ${sourceId} used by ${firstItemId} and ${item.id}`);
|
||||||
|
else seenSourceIds.set(traceKey, item.id);
|
||||||
|
}
|
||||||
if (expectsSourceTrace && !item.factors?.evidenceNeeded && ['do', 'test'].includes(item.lane?.id)) warnings.push(`missing evidence needed for active item ${item.id}`);
|
if (expectsSourceTrace && !item.factors?.evidenceNeeded && ['do', 'test'].includes(item.lane?.id)) warnings.push(`missing evidence needed for active item ${item.id}`);
|
||||||
if (item.metrics?.nonGoalConflicts?.length && ['do', 'test'].includes(item.lane?.id)) warnings.push(`active item ${item.id} conflicts with source non-goals: ${item.metrics.nonGoalConflicts.join('; ')}`);
|
if (item.metrics?.nonGoalConflicts?.length && ['do', 'test'].includes(item.lane?.id)) warnings.push(`active item ${item.id} conflicts with source non-goals: ${item.metrics.nonGoalConflicts.join('; ')}`);
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user