Accept game-specific Scattermind build fields
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. String items in laned Build Order arrays now also receive deterministic section-local source IDs such as `concept-map.buildOrder.validateNext#1` and carry the original string as `sourceQuote`, so simple Scattermind exports stay addressable downstream instead of becoming anonymous `feature-1` rows. Ranker also accepts the current Scattermind storage-row shape with `referenceCode`, `ideaText`, `context`, and string-valued `fullReadingJson` / `full_reading_json`; it expands the saved paid Concept Map before ranking so operators do not have to hand-copy lenses out of Appwrite rows. It also accepts private reading/API envelopes with object-valued `reading` / `fullReading` and `glimpse`, preserving `referenceCode` and `initialPrompt` while treating `reading` as the paid Concept Map. Stringified bridge envelopes in fields such as `rankerInput`, `rankerBridge`, `rankReady`, `bridgePayload`, or `continuationPlan` are expanded the same way, so Appwrite/string-copy handoffs do not have to be manually unwrapped before ranking. If the row only has free Snapshot data (`glimpseJson` / `glimpse_json` / `snapshotJson`), Ranker expands that Snapshot into a minimal continuation order: one manual proof plus the first evidence question, with the Snapshot reference code/title preserved for provenance. The public paste form mirrors this: a prose-wrapped/fenced Appwrite row paste stays intact for the API instead of unwrapping the stringified reading into nonsense fields. The decision `brief.quickGlance.sourceTrace` now repeats the winning item's source section/id/title/quote, and both `brief.source.originalPromptExcerpt` / `handoff.source.originalPromptExcerpt` or, when the original prompt is unavailable, `sourceSummaryExcerpt` carry the source context into the bridge handoff.
|
||||
|
||||
Soft Scattermind labels are accepted at the bridge boundary so Scattermind does not need to use harsh verdict copy in its own product surface. Lens text can say `Continue first`, `Make tangible`, `Try next`, `Evidence next`, `Validate manually`, `Manual proof`, `Hold for later`, or `Set aside`, with either colon or reader-friendly dash separators (`Continue first: …`, `Validate manually — …`, `Evidence next - …`). Build Order objects and direct bridge/envelope sections can use matching camel/snake-case keys such as `continueFirst`, `evidenceNext`, `validateManually`, `manualProof`, `holdForLater`, and `setAside`; whole-payload wrappers may use `rankPayload` / `rank_payload` or `scattermindPayload` / `scattermind_payload`, and already-laned objects may be named `rankReadyBuildOrder` / `rank_ready_build_order` as a more explicit Scattermind handoff alias. Those section values may be arrays, a single candidate object, or a reader-friendly string/newline list; Ranker splits scalar sections into candidate actions so Scattermind does not have to wrap every lane in arrays. Ranker maps those to `doFirst / validateNext / defer / park` while preserving the softer original label in `sourceQuote` or candidate source trace. Ranker also accepts softer continuation envelopes named `rankerBridge`, `continuation`, or `continuationPlan`, candidate arrays named `possibleNextMoves`, `suggestedNextMoves`, `recommendations`, or `opportunities`, laned `buildOrderPreview` / `build_order_preview` objects, `firstWeekBuildOrder` / `first_week_build_order` objects from Scattermind's paid first-week Build Order language, first-48-hour action arrays (`next48Hours`, `next_48_hours`, `first48Hours`, `first_48_hours`, `nextTwoDays`, `next_two_days`), evidence-question fallback arrays (`evidenceQuestions` / `evidence_questions`, `proofQuestions` / `proof_questions`, `proofPlan` / `proof_plan`, `validationQuestions` / `validation_questions`, `validationPlan` / `validation_plan`, `decisionQuestions`, `questionsToAnswer`, `followupQuestions`), and assumption-test arrays (`assumptionTests` / `assumption_tests`, `riskiestAssumptions` / `riskiest_assumptions`, `risksToTest` / `risks_to_test`) that default into Validate next. Direct candidate objects may use reader-friendly prose keys like `text`, `content`, `summary`, `step`, `task`, `instruction`, `why_it_matters`, `evidence_to_collect`, `first_proof_step`, `green_flag`, and `red_flag`; Ranker normalizes those into title, value, evidence, next-step, success, and kill-signal fields so Scattermind does not have to rename paid Concept Map language into software-feature jargon. If a paid Concept Map has no labelled Build Order/action threads but does include `closing_note` / `closingNote` plus decision questions, Ranker treats the closing note as the active 48-hour Do first move and keeps the questions in Validate next. This lets Scattermind pass reader-friendly Concept Map copy without renaming everything into software-feature language.
|
||||
Soft Scattermind labels are accepted at the bridge boundary so Scattermind does not need to use harsh verdict copy in its own product surface. Lens text can say `Continue first`, `Make tangible`, `Try next`, `Evidence next`, `Validate manually`, `Manual proof`, `Hold for later`, or `Set aside`, with either colon or reader-friendly dash separators (`Continue first: …`, `Validate manually — …`, `Evidence next - …`). Build Order objects and direct bridge/envelope sections can use matching camel/snake-case keys such as `continueFirst`, `evidenceNext`, `validateManually`, `manualProof`, `holdForLater`, and `setAside`; whole-payload wrappers may use `rankPayload` / `rank_payload` or `scattermindPayload` / `scattermind_payload`, and already-laned objects may be named `rankReadyBuildOrder` / `rank_ready_build_order` as a more explicit Scattermind handoff alias. Those section values may be arrays, a single candidate object, or a reader-friendly string/newline list; Ranker splits scalar sections into candidate actions so Scattermind does not have to wrap every lane in arrays. Ranker maps those to `doFirst / validateNext / defer / park` while preserving the softer original label in `sourceQuote` or candidate source trace. Route-specific paid Scattermind exports can send game/prototype fields directly: `buildDecisions` / `build_decisions` become Do first candidates, `playtestQuestions` / `playtest_questions` become Validate next candidates, `explicitDeferrals` / `explicit_deferrals` become Defer candidates, and `doNotLetThisBecome` / `do_not_let_this_become` becomes Park candidates plus non-goal guardrails. Ranker also accepts softer continuation envelopes named `rankerBridge`, `continuation`, or `continuationPlan`, candidate arrays named `possibleNextMoves`, `suggestedNextMoves`, `recommendations`, or `opportunities`, laned `buildOrderPreview` / `build_order_preview` objects, `firstWeekBuildOrder` / `first_week_build_order` objects from Scattermind's paid first-week Build Order language, first-48-hour action arrays (`next48Hours`, `next_48_hours`, `first48Hours`, `first_48_hours`, `nextTwoDays`, `next_two_days`), evidence-question fallback arrays (`evidenceQuestions` / `evidence_questions`, `proofQuestions` / `proof_questions`, `proofPlan` / `proof_plan`, `validationQuestions` / `validation_questions`, `validationPlan` / `validation_plan`, `decisionQuestions`, `questionsToAnswer`, `followupQuestions`), and assumption-test arrays (`assumptionTests` / `assumption_tests`, `riskiestAssumptions` / `riskiest_assumptions`, `risksToTest` / `risks_to_test`) that default into Validate next. Direct candidate objects may use reader-friendly prose keys like `text`, `content`, `summary`, `step`, `task`, `instruction`, `why_it_matters`, `evidence_to_collect`, `first_proof_step`, `green_flag`, and `red_flag`; Ranker normalizes those into title, value, evidence, next-step, success, and kill-signal fields so Scattermind does not have to rename paid Concept Map language into software-feature jargon. If a paid Concept Map has no labelled Build Order/action threads but does include `closing_note` / `closingNote` plus decision questions, Ranker treats the closing note as the active 48-hour Do first move and keeps the questions in Validate next. This lets Scattermind pass reader-friendly Concept Map copy without renaming everything into software-feature language.
|
||||
|
||||
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; soft guardrail language such as “this is not a dashboard” or “keep auth/billing/workspaces out until proof” is promoted into non-goals, not merely background context. Ranker now also infers a light `ideaRoute` from the Scattermind source text and carries it in `input.decisionContext` / `handoff.decisionContext`; for game concepts it automatically adds anti-SaaS non-goals so account/dashboard/workspace/subscription candidates cannot win a playable-prototype build order just because they were phrased loudly. 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. Handoff `activeSlice` (`ranker-active-slice-v1`) is the compact machine-readable continuation unit: one active item, its proof/evidence/success/kill signals, source anchor, held-back items, readiness status, and the rule that only this slice is build-ready. For tired first-screen users, `brief.decisionReceipt` repeats the one active move, first proof step, evidence question, held-back items, source anchor, and the handoff rule that only Do first is active; use it as the compact result strip before showing the full lane board. 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. Exact free Snapshot JSON (`working_name`, `restated_idea`, `lenses.shape`, `questions_to_sit_with`, `reference_code`) is rankable too: Ranker derives a manual proof active slice plus evidence questions, carrying the Snapshot reference code/title into provenance so a Snapshot-only handoff does not need a paid Concept Map before it can produce a useful build order. If a Concept Map only carries `questions_to_sit_with` / `questionsToSitWith` / `openQuestions` and no explicit build-order lanes or action threads, Ranker converts those questions into Validate-next evidence actions with source trace instead of pretending they are software features.
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ function parsePastedJsonPayload(value) {
|
||||
|| Array.isArray(parsed.threads_to_hold) || Array.isArray(parsed.threadsToHold) || Array.isArray(parsed.actionThreads) || Array.isArray(parsed.action_threads)
|
||||
|| Array.isArray(parsed.questions_to_sit_with) || Array.isArray(parsed.questionsToSitWith) || Array.isArray(parsed.evidenceQuestions) || Array.isArray(parsed.evidence_questions) || Array.isArray(parsed.proofQuestions) || Array.isArray(parsed.proof_questions) || Array.isArray(parsed.proofPlan) || Array.isArray(parsed.proof_plan) || Array.isArray(parsed.validationQuestions) || Array.isArray(parsed.validation_questions) || Array.isArray(parsed.validationPlan) || Array.isArray(parsed.validation_plan) || Array.isArray(parsed.decisionQuestions) || Array.isArray(parsed.decision_questions) || Array.isArray(parsed.questionsToAnswer) || Array.isArray(parsed.questions_to_answer) || Array.isArray(parsed.followupQuestions) || Array.isArray(parsed.followup_questions) || Array.isArray(parsed.openQuestions) || Array.isArray(parsed.open_questions)
|
||||
|| Array.isArray(parsed.assumptionTests) || Array.isArray(parsed.assumption_tests) || Array.isArray(parsed.riskiestAssumptions) || Array.isArray(parsed.riskiest_assumptions) || Array.isArray(parsed.risksToTest) || Array.isArray(parsed.risks_to_test)
|
||||
|| Array.isArray(parsed.buildDecisions) || Array.isArray(parsed.build_decisions) || Array.isArray(parsed.explicitDeferrals) || Array.isArray(parsed.explicit_deferrals) || Array.isArray(parsed.playtestQuestions) || Array.isArray(parsed.playtest_questions) || Array.isArray(parsed.doNotLetThisBecome) || Array.isArray(parsed.do_not_let_this_become) || typeof parsed.doNotLetThisBecome === 'string' || typeof parsed.do_not_let_this_become === 'string'
|
||||
);
|
||||
return looksLikeBridgePayload ? parsed : null;
|
||||
} catch {
|
||||
|
||||
@@ -2630,6 +2630,43 @@ try {
|
||||
assert.equal(proofGateAlias.handoff.readiness.status, 'ready');
|
||||
assert.deepEqual(proofGateAlias.handoff.warnings, []);
|
||||
|
||||
const gameSpecificExportResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sourceName: 'Scattermind',
|
||||
referenceCode: 'SM-GAME-SPECIFIC-EXPORT-1',
|
||||
working_name: 'Vehicle bullet-heaven prototype',
|
||||
ideaText: 'A game concept about driving through bullet-heaven waves. Scattermind exported game-specific paid fields rather than software feature rows.',
|
||||
ideaType: 'game',
|
||||
context: 'Solo builder. Manual playtest first. No accounts, workspaces, dashboards, subscription tiers, or platform systems before the fun test.',
|
||||
mode: 'validation',
|
||||
conceptMap: {
|
||||
build_decisions: [
|
||||
{ id: 'five-minute-greybox', decision: 'Five-minute greybox driving arena', evidence_needed: 'Can one player feel the steering tension in a five-minute prototype?', source_item_id: 'game-build-1', source_title: 'Game build decisions', ranker_hints: { value: 9, effort: 2, confidence: 7, urgency: 8, risk: 3 } },
|
||||
{ id: 'first-enemy-pattern', build_decision: 'One readable enemy wave pattern', evidence_needed: 'Can players dodge it while still feeling vehicle momentum?', source_item_id: 'game-build-2', source_title: 'Game build decisions' },
|
||||
],
|
||||
playtest_questions: [
|
||||
{ id: 'steering-feel-question', playtest_question: 'Do players fight the controls in a fun way or a frustrating way?', source_item_id: 'game-test-1', source_title: 'Playtest questions' },
|
||||
],
|
||||
explicit_deferrals: ['Meta progression, upgrades, save slots, and content volume until the first control loop feels good.'],
|
||||
do_not_let_this_become: ['Do not let this become an account dashboard, live-service platform, or progression spreadsheet before the movement prototype is fun.'],
|
||||
},
|
||||
}),
|
||||
});
|
||||
assert.equal(gameSpecificExportResponse.status, 200);
|
||||
const gameSpecificExport = await gameSpecificExportResponse.json();
|
||||
assert.equal(gameSpecificExport.input.optionCount, 5, 'game-specific Scattermind fields should become a full build-order pass');
|
||||
assert.equal(gameSpecificExport.ranked[0].id, 'five-minute-greybox');
|
||||
assert.equal(gameSpecificExport.ranked[0].provenance.sourceSection, 'concept-map.buildDecisions');
|
||||
assert.equal(gameSpecificExport.ranked.find(item => item.id === 'steering-feel-question').lane.id, 'test');
|
||||
assert.equal(gameSpecificExport.ranked.find(item => /Meta progression/i.test(item.title)).lane.id, 'defer');
|
||||
assert.equal(gameSpecificExport.ranked.find(item => /account dashboard/i.test(item.title)).lane.id, 'park');
|
||||
assert.ok(gameSpecificExport.input.decisionContext.nonGoals.some(item => /account dashboard/i.test(item)));
|
||||
assert.match(gameSpecificExport.handoff.copyableText, /Five-minute greybox driving arena/);
|
||||
assert.equal(gameSpecificExport.handoff.readiness.status, 'ready');
|
||||
assert.deepEqual(gameSpecificExport.handoff.warnings, []);
|
||||
|
||||
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, embeddedSnapshotTop: embeddedSnapshot.ranked[0].id, sourceExcerptTop: sourceExcerpt.ranked[0].id, snakeCaseBridgeTop: snakeCaseBridge.ranked[0].id, nextStepsAliasTop: nextStepsAlias.ranked[0].id, summaryGuardrailTop: summaryGuardrail.ranked[0].id, bridgeEnvelopeTop: bridgeEnvelope.ranked[0].id, directEnvelopeSectionsTop: directEnvelopeSections.ranked[0].id, softDirectLaneAliasesTop: softDirectLaneAliases.ranked[0].id, threadsFallbackTop: threadsFallback.ranked[0].id, questionsFallbackTop: questionsFallback.ranked[0].id, freeSnapshotTop: freeSnapshot.ranked[0].id, storedScattermindRowTop: storedScattermindRow.ranked[0].id, candidateActionsAliasTop: candidateActionsAlias.ranked[0].id, rankReadyActionsEnvelopeTop: rankReadyActionsEnvelope.ranked[0].id, continuationEnvelopeTop: continuationEnvelope.ranked[0].id, buildOrderPreviewTop: buildOrderPreview.ranked[0].id, scattermindRoadmapLanguageTop: scattermindRoadmapLanguage.ranked[0].id, scattermindRoadmapLensOnlyTop: scattermindRoadmapLensOnly.ranked[0].id, stringifiedRankerInputTop: stringifiedRankerInput.ranked[0].id, scalarBuildOrderSectionsTop: scalarBuildOrderSections.ranked[0].id, snakeReaderFriendlyBuildOrderTop: snakeReaderFriendlyBuildOrder.ranked[0].id, readerFriendlyLensLabelTop: readerFriendlyLensLabel.ranked[0].id, gameRouteGuardrailTop: gameRouteGuardrail.ranked[0].id, rankedBuildOrderAliasTop: rankedBuildOrderAlias.ranked[0].id, proofPlanAliasTop: proofPlanAlias.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');
|
||||
|
||||
@@ -692,6 +692,16 @@ function looksLikeRankPayload(value = {}) {
|
||||
|| Array.isArray(value.riskiest_assumptions)
|
||||
|| Array.isArray(value.risksToTest)
|
||||
|| Array.isArray(value.risks_to_test)
|
||||
|| Array.isArray(value.buildDecisions)
|
||||
|| Array.isArray(value.build_decisions)
|
||||
|| Array.isArray(value.explicitDeferrals)
|
||||
|| Array.isArray(value.explicit_deferrals)
|
||||
|| Array.isArray(value.playtestQuestions)
|
||||
|| Array.isArray(value.playtest_questions)
|
||||
|| Array.isArray(value.doNotLetThisBecome)
|
||||
|| Array.isArray(value.do_not_let_this_become)
|
||||
|| typeof value.doNotLetThisBecome === 'string'
|
||||
|| typeof value.do_not_let_this_become === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1031,6 +1041,11 @@ function cleanDecisionContext(input = {}) {
|
||||
...guardrailTextItems(envelope.threads_to_hold || envelope.threadsToHold || envelope.actionThreads || envelope.action_threads, 8),
|
||||
...guardrailTextItems(featureSet.threads_to_hold || featureSet.threadsToHold || featureSet.actionThreads || featureSet.action_threads, 8),
|
||||
...guardrailTextItems(input.threads_to_hold || input.threadsToHold || input.actionThreads || input.action_threads, 8),
|
||||
...guardrailTextItems(conceptMap.doNotLetThisBecome || conceptMap.do_not_let_this_become, 8),
|
||||
...guardrailTextItems(snapshot.doNotLetThisBecome || snapshot.do_not_let_this_become, 8),
|
||||
...guardrailTextItems(envelope.doNotLetThisBecome || envelope.do_not_let_this_become, 8),
|
||||
...guardrailTextItems(featureSet.doNotLetThisBecome || featureSet.do_not_let_this_become, 8),
|
||||
...guardrailTextItems(input.doNotLetThisBecome || input.do_not_let_this_become, 8),
|
||||
].filter(Boolean).join('\n'));
|
||||
const ideaRoute = cleanText(input.ideaType || input.idea_type || input.category || input.route || envelope.ideaType || envelope.idea_type || featureSet.ideaType || featureSet.idea_type || artifact.ideaType || artifact.idea_type || conceptMap.ideaType || conceptMap.idea_type || snapshot.ideaType || snapshot.idea_type || inferIdeaRouteFromText([
|
||||
input.idea,
|
||||
@@ -1110,11 +1125,11 @@ function nonGoalConflicts(optionText, decisionContext = {}) {
|
||||
function normalizeFeatureOption(item, index, fallbackId = 'feature', defaultSourceSection = '', defaultRecommendedLane = '') {
|
||||
const rawValue = typeof item === 'string' || typeof item === 'number' ? String(item) : '';
|
||||
const raw = rawValue ? { action: rawValue } : objectFrom(item);
|
||||
const title = cleanText(raw.title || raw.name || raw.action || raw.move || raw.step || raw.task || raw.instruction || raw.nextMove || raw.next_move || raw.nextStep || raw.next_step || raw.recommendedNextStep || raw.recommended_next_step || raw.experiment || raw.testName || raw.test_name || raw.hypothesis || raw.label || raw.text || raw.content || raw.summary || '', 140);
|
||||
const title = cleanText(raw.title || raw.name || raw.action || raw.move || raw.decision || raw.buildDecision || raw.build_decision || raw.step || raw.task || raw.instruction || raw.nextMove || raw.next_move || raw.nextStep || raw.next_step || raw.recommendedNextStep || raw.recommended_next_step || raw.experiment || raw.testName || raw.test_name || raw.hypothesis || raw.playtestQuestion || raw.playtest_question || raw.label || raw.text || raw.content || raw.summary || '', 140);
|
||||
const proofGate = objectFrom(raw.proofGate || raw.proof_gate || raw.validationGate || raw.validation_gate || raw.evidenceGate || raw.evidence_gate || raw.decisionGate || raw.decision_gate || raw.gate);
|
||||
const proofSteps = cleanTextList(raw.proofSteps || raw.proof_steps || raw.proof || raw.firstProof || raw.first_proof || raw.validationSteps || raw.validation_steps || raw.steps || raw.method || proofGate.steps || proofGate.proofSteps || proofGate.proof_steps || proofGate.method, 5, 180);
|
||||
const dependencies = cleanTextList(raw.dependencies || raw.blockedBy || raw.blocked_by, 5, 120);
|
||||
const evidenceNeeded = cleanText(raw.evidenceNeeded || raw.evidence_needed || raw.evidenceToCollect || raw.evidence_to_collect || raw.evidence || raw.test || raw.proofQuestion || raw.proof_question || raw.evidenceQuestion || raw.evidence_question || raw.questionToAnswer || raw.question_to_answer || raw.question || raw.learningGoal || raw.learning_goal || proofGate.evidenceNeeded || proofGate.evidence_needed || proofGate.evidence || proofGate.question || proofGate.evidenceQuestion || proofGate.evidence_question || proofGate.learningGoal || proofGate.learning_goal || '', 260);
|
||||
const evidenceNeeded = cleanText(raw.evidenceNeeded || raw.evidence_needed || raw.evidenceToCollect || raw.evidence_to_collect || raw.evidence || raw.test || raw.playtestQuestion || raw.playtest_question || raw.proofQuestion || raw.proof_question || raw.evidenceQuestion || raw.evidence_question || raw.questionToAnswer || raw.question_to_answer || raw.question || raw.learningGoal || raw.learning_goal || proofGate.evidenceNeeded || proofGate.evidence_needed || proofGate.evidence || proofGate.question || proofGate.evidenceQuestion || proofGate.evidence_question || proofGate.learningGoal || proofGate.learning_goal || '', 260);
|
||||
const userValue = cleanText(raw.userValue || raw.user_value || raw.value || raw.outcome || raw.why || raw.whyItMatters || raw.why_it_matters || raw.whyNow || raw.why_now, 260);
|
||||
const risk = cleanText(raw.risk || raw.assumption || raw.unknown || raw.watchOut || raw.watch_out || proofGate.risk || proofGate.assumption || '', 220);
|
||||
const nextStep = cleanText(raw.nextStep || raw.next_step || raw.nextAction || raw.next_action || raw.firstStep || raw.first_step || raw.manualStep || raw.manual_step || raw.actionToTake || raw.action_to_take || raw.firstProofStep || raw.first_proof_step || proofGate.nextStep || proofGate.next_step || proofGate.nextAction || proofGate.next_action || proofGate.firstStep || proofGate.first_step || proofGate.firstProofStep || proofGate.first_proof_step || raw.step || raw.task || raw.instruction || '', 260);
|
||||
@@ -1188,8 +1203,8 @@ function candidateItems(value) {
|
||||
const item = objectFrom(value);
|
||||
if (!Object.keys(item).length) return [];
|
||||
const hasCandidateShape = Boolean(
|
||||
item.id || item.title || item.name || item.action || item.move || item.step || item.task || item.instruction ||
|
||||
item.nextMove || item.next_move || item.nextStep || item.next_step || item.text || item.content || item.summary
|
||||
item.id || item.title || item.name || item.action || item.move || item.decision || item.buildDecision || item.build_decision || item.step || item.task || item.instruction ||
|
||||
item.nextMove || item.next_move || item.nextStep || item.next_step || item.playtestQuestion || item.playtest_question || item.text || item.content || item.summary
|
||||
);
|
||||
return hasCandidateShape ? [item] : [];
|
||||
}
|
||||
@@ -1212,7 +1227,17 @@ function buildOrderSectionGroup(buildOrder = {}, baseSection = 'buildOrder') {
|
||||
|
||||
function normalizeCandidateGroup(group = []) {
|
||||
const options = group.flatMap(entry => {
|
||||
const fallbackId = entry.sourceSection.toLowerCase().includes('action') ? 'action' : 'feature';
|
||||
const fallbackId = entry.sourceSection.toLowerCase().includes('action')
|
||||
? 'action'
|
||||
: entry.sourceSection.toLowerCase().includes('deferral') || entry.sourceSection.toLowerCase().includes('defer')
|
||||
? 'defer'
|
||||
: entry.sourceSection.toLowerCase().includes('donotletthisbecome') || entry.sourceSection.toLowerCase().includes('do-not-let') || entry.sourceSection.toLowerCase().includes('park')
|
||||
? 'park'
|
||||
: entry.sourceSection.toLowerCase().includes('question') || entry.sourceSection.toLowerCase().includes('playtest')
|
||||
? 'question'
|
||||
: entry.sourceSection.toLowerCase().includes('decision')
|
||||
? 'decision'
|
||||
: 'feature';
|
||||
return (entry.items || []).slice(0, 24).map((item, index) => normalizeFeatureOption(item, index, fallbackId, entry.sourceSection, entry.defaultLane));
|
||||
}).filter(item => item.title).slice(0, 24);
|
||||
return normalizeOptionIds(options);
|
||||
@@ -1507,14 +1532,26 @@ function optionsFromBody(body = {}) {
|
||||
{ items: body.validationTests || body.validation_tests || body.validationPlan || body.validation_plan, sourceSection: 'experiments', defaultLane: 'validate-next' },
|
||||
{ items: body.proofTests || body.proof_tests || body.proofPlan || body.proof_plan, sourceSection: 'experiments', defaultLane: 'validate-next' },
|
||||
{ items: body.assumptionTests || body.assumption_tests || body.riskiestAssumptions || body.riskiest_assumptions || body.risksToTest || body.risks_to_test, sourceSection: 'assumptionTests', defaultLane: 'validate-next' },
|
||||
{ items: body.buildDecisions || body.build_decisions, sourceSection: 'buildDecisions', defaultLane: 'do-first' },
|
||||
{ items: body.playtestQuestions || body.playtest_questions, sourceSection: 'playtestQuestions', defaultLane: 'validate-next' },
|
||||
{ items: body.explicitDeferrals || body.explicit_deferrals, sourceSection: 'explicitDeferrals', defaultLane: 'defer' },
|
||||
{ items: body.doNotLetThisBecome || body.do_not_let_this_become, sourceSection: 'doNotLetThisBecome', defaultLane: 'park' },
|
||||
{ items: envelope.experiments, sourceSection: 'ranker-input.experiments', defaultLane: 'validate-next' },
|
||||
{ items: envelope.validationTests || envelope.validation_tests || envelope.validationPlan || envelope.validation_plan, sourceSection: 'ranker-input.experiments', defaultLane: 'validate-next' },
|
||||
{ items: envelope.proofTests || envelope.proof_tests || envelope.proofPlan || envelope.proof_plan, sourceSection: 'ranker-input.experiments', defaultLane: 'validate-next' },
|
||||
{ items: envelope.assumptionTests || envelope.assumption_tests || envelope.riskiestAssumptions || envelope.riskiest_assumptions || envelope.risksToTest || envelope.risks_to_test, sourceSection: 'ranker-input.assumptionTests', defaultLane: 'validate-next' },
|
||||
{ items: envelope.buildDecisions || envelope.build_decisions, sourceSection: 'ranker-input.buildDecisions', defaultLane: 'do-first' },
|
||||
{ items: envelope.playtestQuestions || envelope.playtest_questions, sourceSection: 'ranker-input.playtestQuestions', defaultLane: 'validate-next' },
|
||||
{ items: envelope.explicitDeferrals || envelope.explicit_deferrals, sourceSection: 'ranker-input.explicitDeferrals', defaultLane: 'defer' },
|
||||
{ items: envelope.doNotLetThisBecome || envelope.do_not_let_this_become, sourceSection: 'ranker-input.doNotLetThisBecome', defaultLane: 'park' },
|
||||
{ items: featureSet.experiments, sourceSection: 'feature-set.experiments', defaultLane: 'validate-next' },
|
||||
{ items: featureSet.validationTests || featureSet.validation_tests || featureSet.validationPlan || featureSet.validation_plan, sourceSection: 'feature-set.experiments', defaultLane: 'validate-next' },
|
||||
{ items: featureSet.proofTests || featureSet.proof_tests || featureSet.proofPlan || featureSet.proof_plan, sourceSection: 'feature-set.experiments', defaultLane: 'validate-next' },
|
||||
{ items: featureSet.assumptionTests || featureSet.assumption_tests || featureSet.riskiestAssumptions || featureSet.riskiest_assumptions || featureSet.risksToTest || featureSet.risks_to_test, sourceSection: 'feature-set.assumptionTests', defaultLane: 'validate-next' },
|
||||
{ items: featureSet.buildDecisions || featureSet.build_decisions, sourceSection: 'feature-set.buildDecisions', defaultLane: 'do-first' },
|
||||
{ items: featureSet.playtestQuestions || featureSet.playtest_questions, sourceSection: 'feature-set.playtestQuestions', defaultLane: 'validate-next' },
|
||||
{ items: featureSet.explicitDeferrals || featureSet.explicit_deferrals, sourceSection: 'feature-set.explicitDeferrals', defaultLane: 'defer' },
|
||||
{ items: featureSet.doNotLetThisBecome || featureSet.do_not_let_this_become, sourceSection: 'feature-set.doNotLetThisBecome', defaultLane: 'park' },
|
||||
{ items: body.validateNext || body.validate_next || body.validateManually || body.validate_manually || body.validate || body.validation || body.evidenceNext || body.evidence_next || body.tryNext || body.try_next || body.learnNext || body.learn_next || body.testManually || body.test_manually || body.manualProof || body.manual_proof, sourceSection: 'validateNext', defaultLane: 'validate-next' },
|
||||
{ items: body.deferred || body.defer || body.later || body.afterProof || body.after_proof || body.holdForLater || body.hold_for_later || body.notYet || body.not_yet, sourceSection: 'deferred', defaultLane: 'defer' },
|
||||
{ items: body.parkingLot || body.parking_lot || body.park || body.parked || body.probablyNoise || body.probably_noise || body.setAside || body.set_aside || body.outOfScope || body.out_of_scope, sourceSection: 'parkingLot', defaultLane: 'park' },
|
||||
@@ -1539,6 +1576,10 @@ function optionsFromBody(body = {}) {
|
||||
{ items: conceptMap.validationTests || conceptMap.validation_tests || conceptMap.validationPlan || conceptMap.validation_plan, sourceSection: 'concept-map.experiments', defaultLane: 'validate-next' },
|
||||
{ items: conceptMap.proofTests || conceptMap.proof_tests || conceptMap.proofPlan || conceptMap.proof_plan, sourceSection: 'concept-map.experiments', defaultLane: 'validate-next' },
|
||||
{ items: conceptMap.assumptionTests || conceptMap.assumption_tests || conceptMap.riskiestAssumptions || conceptMap.riskiest_assumptions || conceptMap.risksToTest || conceptMap.risks_to_test, sourceSection: 'concept-map.assumptionTests', defaultLane: 'validate-next' },
|
||||
{ items: conceptMap.buildDecisions || conceptMap.build_decisions, sourceSection: 'concept-map.buildDecisions', defaultLane: 'do-first' },
|
||||
{ items: conceptMap.playtestQuestions || conceptMap.playtest_questions, sourceSection: 'concept-map.playtestQuestions', defaultLane: 'validate-next' },
|
||||
{ items: conceptMap.explicitDeferrals || conceptMap.explicit_deferrals, sourceSection: 'concept-map.explicitDeferrals', defaultLane: 'defer' },
|
||||
{ items: conceptMap.doNotLetThisBecome || conceptMap.do_not_let_this_become, sourceSection: 'concept-map.doNotLetThisBecome', defaultLane: 'park' },
|
||||
{ items: conceptMap.deferred || conceptMap.defer || conceptMap.later || conceptMap.afterProof || conceptMap.after_proof || conceptMap.holdForLater || conceptMap.hold_for_later || conceptMap.notYet || conceptMap.not_yet, sourceSection: 'concept-map.deferred', defaultLane: 'defer' },
|
||||
{ items: conceptMap.parkingLot || conceptMap.parking_lot || conceptMap.park || conceptMap.parked || conceptMap.probablyNoise || conceptMap.probably_noise || conceptMap.setAside || conceptMap.set_aside || conceptMap.outOfScope || conceptMap.out_of_scope, sourceSection: 'concept-map.parkingLot', defaultLane: 'park' },
|
||||
]);
|
||||
@@ -1557,6 +1598,10 @@ function optionsFromBody(body = {}) {
|
||||
{ items: snapshot.validationTests || snapshot.validation_tests || snapshot.validationPlan || snapshot.validation_plan, sourceSection: 'snapshot.experiments', defaultLane: 'validate-next' },
|
||||
{ items: snapshot.proofTests || snapshot.proof_tests || snapshot.proofPlan || snapshot.proof_plan, sourceSection: 'snapshot.experiments', defaultLane: 'validate-next' },
|
||||
{ items: snapshot.assumptionTests || snapshot.assumption_tests || snapshot.riskiestAssumptions || snapshot.riskiest_assumptions || snapshot.risksToTest || snapshot.risks_to_test, sourceSection: 'snapshot.assumptionTests', defaultLane: 'validate-next' },
|
||||
{ items: snapshot.buildDecisions || snapshot.build_decisions, sourceSection: 'snapshot.buildDecisions', defaultLane: 'do-first' },
|
||||
{ items: snapshot.playtestQuestions || snapshot.playtest_questions, sourceSection: 'snapshot.playtestQuestions', defaultLane: 'validate-next' },
|
||||
{ items: snapshot.explicitDeferrals || snapshot.explicit_deferrals, sourceSection: 'snapshot.explicitDeferrals', defaultLane: 'defer' },
|
||||
{ items: snapshot.doNotLetThisBecome || snapshot.do_not_let_this_become, sourceSection: 'snapshot.doNotLetThisBecome', defaultLane: 'park' },
|
||||
{ items: snapshot.deferred || snapshot.defer || snapshot.later || snapshot.afterProof || snapshot.after_proof || snapshot.holdForLater || snapshot.hold_for_later || snapshot.notYet || snapshot.not_yet, sourceSection: 'snapshot.deferred', defaultLane: 'defer' },
|
||||
{ items: snapshot.parkingLot || snapshot.parking_lot || snapshot.park || snapshot.parked || snapshot.probablyNoise || snapshot.probably_noise || snapshot.setAside || snapshot.set_aside || snapshot.outOfScope || snapshot.out_of_scope, sourceSection: 'snapshot.parkingLot', defaultLane: 'park' },
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user