Add rank handoff readiness gate
This commit is contained in:
@@ -51,7 +51,7 @@ Candidate items may include optional 1–10 `rankerHints` (`value`, `effort`, `c
|
||||
|
||||
Candidate trace note: candidate-level `sourceItemId` / `traceId`, `sourceTitle` / `lensTitle`, and `sourceExcerpt` / `sourceQuote` are preserved in ranked items, `buildOrderDetails`, and `handoff.itemTrace`. Lens-only Build Order text is also split into deterministic `concept-map.lenses.channel#N` source IDs with the original labelled sentence carried as `sourceQuote`, so pasted paid Concept Maps remain traceable even without explicit candidate objects. The decision `brief.quickGlance.sourceTrace` now repeats the winning item's source section/id/title/quote, and both `brief.source.originalPromptExcerpt` and `handoff.source.originalPromptExcerpt` carry a short prompt excerpt so a downstream Scattermind handoff can show why the build order exists without digging through `input.provenance`. Scattermind should use these when a next move came from a specific Concept Map lens sentence, so Ranker can defend not just what wins but where the judgement came from.
|
||||
|
||||
Lane safety note: explicit Scattermind `defer` / `park` hints are hard rails, not mild suggestions. Source `nonGoals` / `avoid` guardrails are also hard enough to keep conflicting candidates out of Do first / Validate next even when their local scoring hints look attractive; the result will mark the lane source as `source-non-goal` so the handoff can explain that the candidate needs guardrail resolution before active work. Handoff `source.requiresSourceTrace` is true only when a real source artifact/title is present; plain idea-only ranking still warns about a missing artifact ID when it carries prompt provenance, but it does not spam source-section/evidence warnings meant for Scattermind artifacts. For low-friction handoff, `/api/rank-feedback` also detects a raw Scattermind/Concept Map JSON object pasted into `idea`, `ideaText`, `optionsText`, or wrapper keys such as `payload`; it expands that object before ranking and reports `input.embeddedPayloadSource` so the public form can accept copy/paste exports without a custom import screen.
|
||||
Lane safety note: explicit Scattermind `defer` / `park` hints are hard rails, not mild suggestions. Source `nonGoals` / `avoid` guardrails are also hard enough to keep conflicting candidates out of Do first / Validate next even when their local scoring hints look attractive; the result will mark the lane source as `source-non-goal` so the handoff can explain that the candidate needs guardrail resolution before active work. Handoff `source.requiresSourceTrace` is true only when a real source artifact/title is present; plain idea-only ranking still warns about a missing artifact ID when it carries prompt provenance, but it does not spam source-section/evidence warnings meant for Scattermind artifacts. Handoff `readiness` now gives downstream bridge consumers a deterministic gate: `ready`, `usable-with-warnings`, `needs-source-context`, or `blocked`, with blockers and next checks for missing evidence, source trace, duplicate IDs, or active source-non-goal conflicts. For low-friction handoff, `/api/rank-feedback` also detects a raw Scattermind/Concept Map JSON object pasted into `idea`, `ideaText`, `optionsText`, or wrapper keys such as `payload`; it expands that object before ranking and reports `input.embeddedPayloadSource` so the public form can accept copy/paste exports without a custom import screen.
|
||||
|
||||
Recommended payload shape:
|
||||
|
||||
|
||||
@@ -99,6 +99,9 @@ try {
|
||||
assert.match(data.brief.summary, /nearest follow-up|strongest signal/i);
|
||||
assert.ok(data.brief.whatWouldChangeRanking.some(item => /evidence fails|re-run the order/i.test(item)));
|
||||
assert.ok(Array.isArray(data.brief.assumptions));
|
||||
assert.equal(data.handoff.readiness.status, 'ready');
|
||||
assert.equal(data.handoff.readiness.sourceComplete, true);
|
||||
assert.ok(data.handoff.readiness.nextChecks.some(item => /Do first item/i.test(item)));
|
||||
|
||||
const messyIdeaOnlyResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||
method: 'POST',
|
||||
@@ -119,6 +122,8 @@ try {
|
||||
assert.equal(messyIdeaOnly.handoff.source.requiresSourceTrace, false);
|
||||
assert.ok(!messyIdeaOnly.handoff.warnings.some(item => /missing source section|missing original prompt/.test(item)));
|
||||
assert.ok(messyIdeaOnly.handoff.warnings.includes('missing source artifact id'));
|
||||
assert.equal(messyIdeaOnly.handoff.readiness.status, 'usable-with-warnings');
|
||||
assert.ok(messyIdeaOnly.handoff.readiness.nextChecks.some(item => /source artifact id if this came from Scattermind/i.test(item)));
|
||||
|
||||
const hintedResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||
method: 'POST',
|
||||
@@ -398,6 +403,8 @@ try {
|
||||
assert.equal(duplicateIds.handoff.itemTrace.find(item => item.id === 'preview-2').idNormalized, true);
|
||||
assert.ok(duplicateIds.handoff.warnings.some(item => /duplicate source id preview normalized to preview-2/.test(item)));
|
||||
assert.ok(Object.values(duplicateIds.buildOrder).flat().includes('preview-2'));
|
||||
assert.equal(duplicateIds.handoff.readiness.status, 'usable-with-warnings');
|
||||
assert.ok(duplicateIds.handoff.readiness.nextChecks.some(item => /duplicate IDs/i.test(item)));
|
||||
|
||||
const structuredContextResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||
method: 'POST',
|
||||
@@ -593,6 +600,9 @@ try {
|
||||
assert.equal(objectBuildOrder.ranked.find(item => item.id === 'workspace-dashboard').lane.id, 'park');
|
||||
assert.equal(objectBuildOrder.ranked.find(item => item.id === 'workspace-dashboard').lane.source, 'hint');
|
||||
assert.ok(objectBuildOrder.handoff.warnings.includes('missing evidence needed for active item feature-1'));
|
||||
assert.equal(objectBuildOrder.handoff.readiness.status, 'needs-source-context');
|
||||
assert.ok(objectBuildOrder.handoff.readiness.blockers.includes('missing evidence needed for active item feature-1'));
|
||||
assert.ok(objectBuildOrder.handoff.readiness.nextChecks.some(item => /evidenceNeeded/i.test(item)));
|
||||
|
||||
const embeddedJsonResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||
method: 'POST',
|
||||
@@ -680,8 +690,10 @@ try {
|
||||
assert.equal(sourceExcerpt.handoff.itemTrace.find(item => item.id === 'copyable-brief').sourceTitle, 'Build Order');
|
||||
assert.match(sourceExcerpt.handoff.itemTrace.find(item => item.id === 'saved-workspace').sourceQuote, /Probably noise/);
|
||||
assert.deepEqual(sourceExcerpt.handoff.warnings, []);
|
||||
assert.equal(sourceExcerpt.handoff.readiness.status, 'ready');
|
||||
assert.equal(sourceExcerpt.handoff.readiness.activeItemCount, 2);
|
||||
|
||||
console.log(JSON.stringify({ ok: true, top: data.ranked[0].id, hintedTop: hinted.ranked[0].id, actionTop: actions.ranked[0].id, nestedConceptTop: nestedConcept.ranked[0].id, nonGoalTop: nonGoal.ranked[0].id, structuredContextTop: structuredContext.ranked[0].id, lensOnlyTop: lensOnly.ranked[0].id, scattermindPaidShapeTop: scattermindPaidShape.ranked[0].id, mergedContextTop: mergedContext.ranked[0].id, embeddedJsonTop: embeddedJson.ranked[0].id, fencedJsonTop: fencedJson.ranked[0].id, sourceExcerptTop: sourceExcerpt.ranked[0].id, duplicateIds: duplicateIds.ranked.map(item => item.id), provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2));
|
||||
console.log(JSON.stringify({ ok: true, top: data.ranked[0].id, hintedTop: hinted.ranked[0].id, actionTop: actions.ranked[0].id, nestedConceptTop: nestedConcept.ranked[0].id, nonGoalTop: nonGoal.ranked[0].id, structuredContextTop: structuredContext.ranked[0].id, lensOnlyTop: lensOnly.ranked[0].id, scattermindPaidShapeTop: scattermindPaidShape.ranked[0].id, mergedContextTop: mergedContext.ranked[0].id, embeddedJsonTop: embeddedJson.ranked[0].id, fencedJsonTop: fencedJson.ranked[0].id, sourceExcerptTop: sourceExcerpt.ranked[0].id, duplicateIds: duplicateIds.ranked.map(item => item.id), readiness: data.handoff.readiness.status, provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2));
|
||||
} finally {
|
||||
server.kill('SIGTERM');
|
||||
}
|
||||
|
||||
@@ -1141,6 +1141,60 @@ function compactBuildItems(items = []) {
|
||||
}));
|
||||
}
|
||||
|
||||
function handoffReadinessFor({ ranked = [], provenance = {}, warnings = [], expectsSourceTrace = false }) {
|
||||
const uniqueWarnings = [...new Set(warnings)];
|
||||
const activeItems = ranked.filter(item => ['do', 'test'].includes(item.lane?.id));
|
||||
const guardrailWarnings = uniqueWarnings.filter(item => /^active item .* conflicts with source non-goals/i.test(item));
|
||||
const sourceWarnings = uniqueWarnings.filter(item => /^missing (source section|original prompt provenance)/i.test(item));
|
||||
const evidenceWarnings = uniqueWarnings.filter(item => /^missing evidence needed for active item/i.test(item));
|
||||
const duplicateWarnings = uniqueWarnings.filter(item => /^duplicate source id/i.test(item));
|
||||
const missingArtifact = uniqueWarnings.includes('missing source artifact id');
|
||||
const blockers = [
|
||||
...guardrailWarnings,
|
||||
...(expectsSourceTrace ? sourceWarnings : []),
|
||||
...(expectsSourceTrace ? evidenceWarnings : []),
|
||||
];
|
||||
const nextChecks = [];
|
||||
|
||||
if (guardrailWarnings.length) nextChecks.push('Resolve source non-goal conflicts before treating any active item as build-ready.');
|
||||
if (expectsSourceTrace && sourceWarnings.length) nextChecks.push('Attach source section / prompt provenance from the Scattermind artifact.');
|
||||
if (expectsSourceTrace && evidenceWarnings.length) nextChecks.push('Add evidenceNeeded to every Do first / Validate next candidate before handoff.');
|
||||
if (missingArtifact) nextChecks.push(expectsSourceTrace ? 'Attach the Scattermind artifact id so the build order can be traced back.' : 'Attach a source artifact id if this came from Scattermind rather than a plain paste.');
|
||||
if (duplicateWarnings.length) nextChecks.push('Review normalized duplicate IDs before downstream tools store references.');
|
||||
if (!nextChecks.length && activeItems.length) nextChecks.push('Use the Do first item as the only active build slice, then rerank after evidence changes.');
|
||||
|
||||
const status = guardrailWarnings.length
|
||||
? 'blocked'
|
||||
: blockers.length
|
||||
? 'needs-source-context'
|
||||
: uniqueWarnings.length
|
||||
? 'usable-with-warnings'
|
||||
: 'ready';
|
||||
const labels = {
|
||||
ready: 'Ready for Ranker handoff',
|
||||
'usable-with-warnings': 'Usable, but provenance is thin',
|
||||
'needs-source-context': 'Needs source context before handoff',
|
||||
blocked: 'Blocked by source guardrails',
|
||||
};
|
||||
const summaries = {
|
||||
ready: `Active order is traceable and evidence-shaped for ${activeItems.length} active item${activeItems.length === 1 ? '' : 's'}.`,
|
||||
'usable-with-warnings': 'Ranking can be read now, but downstream bridge consumers should review the warnings before storing it as a defended handoff.',
|
||||
'needs-source-context': 'A Scattermind-shaped artifact is present, but active items are missing trace or evidence fields needed to defend the order later.',
|
||||
blocked: 'An active item conflicts with source non-goals; do not continue until the guardrail is resolved or the item moves out of the active lanes.',
|
||||
};
|
||||
|
||||
return {
|
||||
status,
|
||||
label: labels[status],
|
||||
summary: summaries[status],
|
||||
blockers,
|
||||
nextChecks: nextChecks.slice(0, 5),
|
||||
activeItemCount: activeItems.length,
|
||||
warningCount: uniqueWarnings.length,
|
||||
sourceComplete: Boolean(!expectsSourceTrace || (provenance?.originalPrompt && activeItems.every(item => item.provenance?.sourceSection))),
|
||||
};
|
||||
}
|
||||
|
||||
function createHandoffContract({ ranked, provenance, decisionContext }) {
|
||||
const warnings = [];
|
||||
const expectsSourceTrace = Boolean(provenance?.artifactId || provenance?.conceptMapId || provenance?.snapshotTitle);
|
||||
@@ -1169,6 +1223,7 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
|
||||
nonGoalConflicts: item.metrics?.nonGoalConflicts || [],
|
||||
};
|
||||
});
|
||||
const uniqueWarnings = [...new Set(warnings)];
|
||||
|
||||
return {
|
||||
schema: 'rank-feedback-result-v1',
|
||||
@@ -1189,7 +1244,8 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
|
||||
assumptions: decisionContext?.assumptions || [],
|
||||
},
|
||||
itemTrace,
|
||||
warnings: [...new Set(warnings)],
|
||||
warnings: uniqueWarnings,
|
||||
readiness: handoffReadinessFor({ ranked, provenance, warnings: uniqueWarnings, expectsSourceTrace }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user