Support Snapshot artifacts in Ranker bridge
This commit is contained in:
@@ -45,9 +45,9 @@ Ranker's continuation job is narrow:
|
|||||||
|
|
||||||
`Snapshot / Concept Map → candidate feature/action set → Rank-ready build order`
|
`Snapshot / Concept Map → candidate feature/action set → Rank-ready build order`
|
||||||
|
|
||||||
`POST /api/rank-feedback` accepts a `prioritix-feature-set-v1`-style payload from Scattermind and returns ranked items plus `buildOrder.doFirst / validateNext / defer / park`. It accepts candidate arrays as `features`, `actions`, `nextMoves`, `candidates`, `experiments`, `validationTests`, or `proofTests` either at the top level or under `featureSet`, and it can consume a nested Concept Map directly, so Scattermind can hand off `conceptMap.nextActions / nextMoves` without renaming them into fake software features. Experiment/proof-test arrays default into Validate next candidates, preserving source provenance while keeping them out of the active build-first lane. Sectioned Concept Maps may also include `validateNext`, `deferred`, and `parkingLot`; Ranker combines those sections into one build-order pass while preserving `sourceSection` and treating deferred/parked sections as lane hints. Ranker also accepts an already-laned Build Order object at top level, under `featureSet`, or as `conceptMap.buildOrder` (`doFirst` / `validateNext` / `deferred` / `parkingLot`, with aliases like `buildNow`, `testManually`, and `probablyNoise`). String items in those lanes are normalized into candidate actions, while object items can use `move`, `questionToAnswer`, or `evidenceQuestion` aliases. Normalized candidate objects may also use `lane` as a lane hint (`do-first`, `validate-next`, `defer`, `park`) without overwriting `sourceSection`, so Scattermind exports that separate recommendation from provenance remain traceable. Empty wrapper arrays are ignored rather than allowed to shadow a real nested Concept Map action set, and non-empty normalized wrappers are merged with Concept Map validation/deferred/parking sections rather than dropping that context. That keeps partially-normalized Scattermind exports rankable without losing the source lane contract. It also returns a `brief` with source, next-48-hour actions, carried-forward assumptions/constraints/non-goals, and `whatWouldChangeRanking` checks, lane-level `buildOrderDetails` with each item's reason, next step, evidence question, success/kill signals, source section, lane source, score, and confidence, plus a `handoff` object (`rank-feedback-result-v1`) with source provenance, item trace rows, explicit next-step/success/kill signals when Scattermind provides them, and contract warnings for missing artifact IDs, source sections, original prompt provenance, or evidence on active items. If Scattermind sends the current paid Concept Map shape as lenses rather than arrays, Ranker can parse `conceptMap.lenses.channel.content` / `buildOrder` labels (`Build first`, `Test manually`, `Defer`, `Probably noise`) into rank-ready candidates and can read guardrails from the risk lens. Keep this contract action-first; do not use it as a reason to add generic dashboard, auth, billing, or workspace layers before the bridge has proof.
|
`POST /api/rank-feedback` accepts a `prioritix-feature-set-v1`-style payload from Scattermind and returns ranked items plus `buildOrder.doFirst / validateNext / defer / park`. It accepts candidate arrays as `features`, `actions`, `nextMoves`, `candidates`, `experiments`, `validationTests`, or `proofTests` either at the top level or under `featureSet`, and it can consume nested Snapshot or Concept Map artifacts directly, so Scattermind can hand off `snapshot.nextActions / nextMoves` or `conceptMap.nextActions / nextMoves` without renaming them into fake software features. Experiment/proof-test arrays default into Validate next candidates, preserving source provenance while keeping them out of the active build-first lane. Sectioned Concept Maps may also include `validateNext`, `deferred`, and `parkingLot`; Ranker combines those sections into one build-order pass while preserving `sourceSection` and treating deferred/parked sections as lane hints. Ranker also accepts an already-laned Build Order object at top level, under `featureSet`, or as `conceptMap.buildOrder` (`doFirst` / `validateNext` / `deferred` / `parkingLot`, with aliases like `buildNow`, `testManually`, and `probablyNoise`). String items in those lanes are normalized into candidate actions, while object items can use `move`, `questionToAnswer`, or `evidenceQuestion` aliases. Normalized candidate objects may also use `lane` as a lane hint (`do-first`, `validate-next`, `defer`, `park`) without overwriting `sourceSection`, so Scattermind exports that separate recommendation from provenance remain traceable. Empty wrapper arrays are ignored rather than allowed to shadow a real nested Concept Map action set, and non-empty normalized wrappers are merged with Concept Map validation/deferred/parking sections rather than dropping that context. That keeps partially-normalized Scattermind exports rankable without losing the source lane contract. It also returns a `brief` with source, next-48-hour actions, carried-forward assumptions/constraints/non-goals, and `whatWouldChangeRanking` checks, lane-level `buildOrderDetails` with each item's reason, next step, evidence question, success/kill signals, source section, lane source, score, and confidence, plus a `handoff` object (`rank-feedback-result-v1`) with source provenance, item trace rows, explicit next-step/success/kill signals when Scattermind provides them, and contract warnings for missing artifact IDs, source sections, original prompt provenance, or evidence on active items. If Scattermind sends the current paid Concept Map shape as lenses rather than arrays, Ranker can parse `conceptMap.lenses.channel.content` / `buildOrder` labels (`Build first`, `Test manually`, `Defer`, `Probably noise`) into rank-ready candidates and can read guardrails from the risk lens. Keep this contract action-first; do not use it as a reason to add generic dashboard, auth, billing, or workspace layers before the bridge has proof.
|
||||||
|
|
||||||
Candidate items may include optional 1–10 `rankerHints` (`value`, `effort`, `confidence`, `urgency`, `revenue`, `novelty`, `risk`). Ranker blends those explicit Scattermind hints with text heuristics; `effort` is inverted into feasibility. Hints improve the defended order, but `recommendedLane: "defer"` or `"park"` remains a safety rail so dashboard-swamp items do not get promoted by flashy wording. For clean bridge handoff, Scattermind should send `sourceSection` and `evidenceNeeded` on each active next move. Scattermind can also send `targetAudience`, `constraints`, `assumptions`, and `nonGoals` / `avoid` at the top level, in `featureSet`, inside `conceptMap.context`, or as a structured top-level `context` object with `summary`, `targetAudience`, `constraints`, `nonGoals` / `avoid`, and `assumptions`; Ranker merges these sources rather than letting a shallow wrapper context shadow deeper Concept Map guardrails. Lens-only Concept Maps may additionally send audience / constraints / assumptions / risk lens content, and Ranker will split sentence-style lens notes into readable decision context instead of leaking `[object Object]`. If Scattermind only has a flat context string, Ranker now extracts simple guardrails such as `Solo builder`, `Manual proof`, `Avoid ...`, `No ...`, and `Do not ...` into `input.decisionContext` / `handoff.decisionContext` so early bridge exports still protect against dashboard/auth/billing drift. Ranker returns that decision context in `input.decisionContext` and `handoff.decisionContext`, and penalizes candidates that conflict with source non-goals (for example saved workspaces/auth/billing before the continuation proof). If Scattermind sends duplicate candidate IDs, Ranker keeps the first ID, suffixes later duplicates (`preview-2`), and reports the normalization in `handoff.warnings` / `handoff.itemTrace` so downstream build-order references remain addressable. Ranker also accepts Scattermind's paid Concept Map object directly when it arrives with top-level `reference_code`, `working_name`, `ideaText`, and string-valued `lenses.channel` / `lenses.risk` fields; the reference code becomes source provenance, the working name becomes the source title, and labelled Build Order text is turned into rank-ready candidates without requiring Scattermind to wrap it first.
|
Candidate items may include optional 1–10 `rankerHints` (`value`, `effort`, `confidence`, `urgency`, `revenue`, `novelty`, `risk`). Ranker blends those explicit Scattermind hints with text heuristics; `effort` is inverted into feasibility. Hints improve the defended order, but `recommendedLane: "defer"` or `"park"` remains a safety rail so dashboard-swamp items do not get promoted by flashy wording. For clean bridge handoff, Scattermind should send `sourceSection` and `evidenceNeeded` on each active next move. Scattermind can also send `targetAudience`, `constraints`, `assumptions`, and `nonGoals` / `avoid` at the top level, in `featureSet`, inside `snapshot.context` or `conceptMap.context`, or as a structured top-level `context` object with `summary`, `targetAudience`, `constraints`, `nonGoals` / `avoid`, and `assumptions`; Ranker merges these sources rather than letting a shallow wrapper context shadow deeper artifact guardrails. Lens-only Concept Maps may additionally send audience / constraints / assumptions / risk lens content, and Ranker will split sentence-style lens notes into readable decision context instead of leaking `[object Object]`. If Scattermind only has a flat context string, Ranker now extracts simple guardrails such as `Solo builder`, `Manual proof`, `Avoid ...`, `No ...`, and `Do not ...` into `input.decisionContext` / `handoff.decisionContext` so early bridge exports still protect against dashboard/auth/billing drift. Ranker returns that decision context in `input.decisionContext` and `handoff.decisionContext`, and penalizes candidates that conflict with source non-goals (for example saved workspaces/auth/billing before the continuation proof). If Scattermind sends duplicate candidate IDs, Ranker keeps the first ID, suffixes later duplicates (`preview-2`), and reports the normalization in `handoff.warnings` / `handoff.itemTrace` so downstream build-order references remain addressable. Ranker also accepts Scattermind's paid Concept Map object directly when it arrives with top-level `reference_code`, `working_name`, `ideaText`, and string-valued `lenses.channel` / `lenses.risk` fields; the reference code becomes source provenance, the working name becomes the source title, and labelled Build Order text is turned into rank-ready candidates without requiring Scattermind to wrap it first.
|
||||||
|
|
||||||
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. 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.
|
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. 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.
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,47 @@ try {
|
|||||||
assert.equal(nestedConcept.ranked.find(item => item.id === 'lesson-library').lane.id, 'park');
|
assert.equal(nestedConcept.ranked.find(item => item.id === 'lesson-library').lane.id, 'park');
|
||||||
assert.deepEqual(nestedConcept.handoff.warnings, []);
|
assert.deepEqual(nestedConcept.handoff.warnings, []);
|
||||||
|
|
||||||
|
const snapshotOnlyResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
sourceName: 'Scattermind',
|
||||||
|
schema: 'scattermind-snapshot-v1',
|
||||||
|
idea: 'Scattermind has only produced a Snapshot so far; Ranker should still defend a tiny continuation order.',
|
||||||
|
mode: 'mvp',
|
||||||
|
snapshot: {
|
||||||
|
id: 'snapshot_only_17',
|
||||||
|
title: 'Tiny audit service snapshot',
|
||||||
|
originalPrompt: 'I want to sell a small audit service but do not know what to build first.',
|
||||||
|
targetAudience: 'Overwhelmed solo service seller',
|
||||||
|
context: {
|
||||||
|
constraints: ['Manual proof before product shell'],
|
||||||
|
nonGoals: ['Avoid accounts and saved workspaces'],
|
||||||
|
assumptions: ['A one-page promise can validate demand'],
|
||||||
|
},
|
||||||
|
nextActions: [
|
||||||
|
{ id: 'promise-proof', action: 'One-page promise proof', why: 'Use the Snapshot to test whether buyers understand the outcome before building app machinery.', evidence: 'Can 3 service sellers say what they would buy and why?', rankerHints: { value: 9, effort: 2, confidence: 8, urgency: 8, risk: 2 } },
|
||||||
|
{ id: 'copyable-brief', action: 'Copyable audit brief', why: 'Turn the defended order into a plain text follow-up.', evidence: 'Does the copied brief make the next move obvious?', suggestedLane: 'validate-next' },
|
||||||
|
],
|
||||||
|
parkingLot: [
|
||||||
|
{ id: 'seller-workspace', action: 'Saved seller workspace', why: 'Accounts, saved projects, dashboard, billing, and collaboration for each service seller.', evidence: 'No proof yet' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(snapshotOnlyResponse.status, 200);
|
||||||
|
const snapshotOnly = await snapshotOnlyResponse.json();
|
||||||
|
assert.equal(snapshotOnly.input.provenance.artifactId, 'snapshot_only_17');
|
||||||
|
assert.equal(snapshotOnly.input.provenance.snapshotTitle, 'Tiny audit service snapshot');
|
||||||
|
assert.equal(snapshotOnly.input.decisionContext.targetAudience, 'Overwhelmed solo service seller');
|
||||||
|
assert.deepEqual(snapshotOnly.input.decisionContext.nonGoals, ['Avoid accounts and saved workspaces']);
|
||||||
|
assert.equal(snapshotOnly.ranked[0].id, 'promise-proof', 'Snapshot nextActions should rank without waiting for a Concept Map wrapper');
|
||||||
|
assert.equal(snapshotOnly.ranked.find(item => item.id === 'promise-proof').provenance.sourceSection, 'snapshot.nextActions');
|
||||||
|
assert.equal(snapshotOnly.ranked.find(item => item.id === 'copyable-brief').lane.id, 'test');
|
||||||
|
assert.equal(snapshotOnly.ranked.find(item => item.id === 'seller-workspace').lane.id, 'park');
|
||||||
|
assert.equal(snapshotOnly.handoff.source.requiresSourceTrace, true);
|
||||||
|
assert.deepEqual(snapshotOnly.handoff.warnings, []);
|
||||||
|
|
||||||
const sectionedConceptResponse = await fetch(`${base}/api/rank-feedback`, {
|
const sectionedConceptResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -601,6 +601,7 @@ function cleanDecisionContext(input = {}) {
|
|||||||
const featureSet = objectFrom(input.featureSet);
|
const featureSet = objectFrom(input.featureSet);
|
||||||
const artifact = objectFrom(input.artifact || featureSet.artifact);
|
const artifact = objectFrom(input.artifact || featureSet.artifact);
|
||||||
const conceptMap = objectFrom(input.conceptMap || featureSet.conceptMap || artifact.conceptMap);
|
const conceptMap = objectFrom(input.conceptMap || featureSet.conceptMap || artifact.conceptMap);
|
||||||
|
const snapshot = objectFrom(input.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot);
|
||||||
const conceptMapLenses = objectFrom(conceptMap.lenses || input.lenses || featureSet.lenses);
|
const conceptMapLenses = objectFrom(conceptMap.lenses || input.lenses || featureSet.lenses);
|
||||||
const riskLens = conceptMapLenses.risk || conceptMapLenses.risks || conceptMapLenses.boundaries || conceptMapLenses.notYet;
|
const riskLens = conceptMapLenses.risk || conceptMapLenses.risks || conceptMapLenses.boundaries || conceptMapLenses.notYet;
|
||||||
const audienceLens = conceptMapLenses.audience || conceptMapLenses.who || conceptMapLenses.customer || conceptMapLenses.users;
|
const audienceLens = conceptMapLenses.audience || conceptMapLenses.who || conceptMapLenses.customer || conceptMapLenses.users;
|
||||||
@@ -612,8 +613,10 @@ function cleanDecisionContext(input = {}) {
|
|||||||
featureSet.decisionContext,
|
featureSet.decisionContext,
|
||||||
artifact.decisionContext,
|
artifact.decisionContext,
|
||||||
conceptMap.decisionContext,
|
conceptMap.decisionContext,
|
||||||
|
snapshot.decisionContext,
|
||||||
structuredContext,
|
structuredContext,
|
||||||
conceptMap.context,
|
conceptMap.context,
|
||||||
|
snapshot.context,
|
||||||
featureSet.context,
|
featureSet.context,
|
||||||
artifact.context,
|
artifact.context,
|
||||||
];
|
];
|
||||||
@@ -622,22 +625,23 @@ function cleanDecisionContext(input = {}) {
|
|||||||
lensContent(riskLens),
|
lensContent(riskLens),
|
||||||
lensContent(constraintsLens),
|
lensContent(constraintsLens),
|
||||||
conceptMap.risk || conceptMap.whatNotToBuildYet || conceptMap.notYet || '',
|
conceptMap.risk || conceptMap.whatNotToBuildYet || conceptMap.notYet || '',
|
||||||
|
snapshot.risk || snapshot.whatNotToBuildYet || snapshot.notYet || '',
|
||||||
].filter(Boolean).join('\n'));
|
].filter(Boolean).join('\n'));
|
||||||
return {
|
return {
|
||||||
targetAudience: cleanText(input.targetAudience || featureSet.targetAudience || firstContextText(contextSources, ['targetAudience', 'audience', 'who', 'whoItHelps', 'customer', 'users']) || conceptMap.targetAudience || lensContent(audienceLens), 180),
|
targetAudience: cleanText(input.targetAudience || featureSet.targetAudience || snapshot.targetAudience || firstContextText(contextSources, ['targetAudience', 'audience', 'who', 'whoItHelps', 'customer', 'users']) || conceptMap.targetAudience || lensContent(audienceLens), 180),
|
||||||
constraints: uniqueList([
|
constraints: uniqueList([
|
||||||
...cleanFlexibleTextList(input.constraints || featureSet.constraints || conceptMap.constraints, 8, 180),
|
...cleanFlexibleTextList(input.constraints || featureSet.constraints || snapshot.constraints || conceptMap.constraints, 8, 180),
|
||||||
...collectContextList(contextSources, ['constraints', 'constraint', 'boundaries', 'scope'], 8),
|
...collectContextList(contextSources, ['constraints', 'constraint', 'boundaries', 'scope'], 8),
|
||||||
...cleanSentenceList(lensContent(constraintsLens), 8, 180),
|
...cleanSentenceList(lensContent(constraintsLens), 8, 180),
|
||||||
...textContextGuardrails.constraints,
|
...textContextGuardrails.constraints,
|
||||||
], 8),
|
], 8),
|
||||||
nonGoals: uniqueList([
|
nonGoals: uniqueList([
|
||||||
...cleanFlexibleTextList(input.nonGoals || input.avoid || featureSet.nonGoals || featureSet.avoid || conceptMap.nonGoals || conceptMap.avoid, 8, 180),
|
...cleanFlexibleTextList(input.nonGoals || input.avoid || featureSet.nonGoals || featureSet.avoid || snapshot.nonGoals || snapshot.avoid || conceptMap.nonGoals || conceptMap.avoid, 8, 180),
|
||||||
...collectContextList(contextSources, ['nonGoals', 'nonGoal', 'avoid', 'notYet', 'doNotBuild'], 8),
|
...collectContextList(contextSources, ['nonGoals', 'nonGoal', 'avoid', 'notYet', 'doNotBuild'], 8),
|
||||||
...textContextGuardrails.nonGoals,
|
...textContextGuardrails.nonGoals,
|
||||||
], 8),
|
], 8),
|
||||||
assumptions: uniqueList([
|
assumptions: uniqueList([
|
||||||
...cleanFlexibleTextList(input.assumptions || featureSet.assumptions || conceptMap.assumptions, 6, 180),
|
...cleanFlexibleTextList(input.assumptions || featureSet.assumptions || snapshot.assumptions || conceptMap.assumptions, 6, 180),
|
||||||
...collectContextList(contextSources, ['assumptions', 'assumption', 'unknowns', 'openQuestions'], 6),
|
...collectContextList(contextSources, ['assumptions', 'assumption', 'unknowns', 'openQuestions'], 6),
|
||||||
...cleanSentenceList(lensContent(assumptionsLens), 6, 180),
|
...cleanSentenceList(lensContent(assumptionsLens), 6, 180),
|
||||||
], 6),
|
], 6),
|
||||||
@@ -807,7 +811,9 @@ function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lense
|
|||||||
|
|
||||||
function optionsFromBody(body = {}) {
|
function optionsFromBody(body = {}) {
|
||||||
const featureSet = objectFrom(body.featureSet);
|
const featureSet = objectFrom(body.featureSet);
|
||||||
const conceptMap = objectFrom(body.conceptMap || featureSet.conceptMap);
|
const artifact = objectFrom(body.artifact || featureSet.artifact);
|
||||||
|
const conceptMap = objectFrom(body.conceptMap || featureSet.conceptMap || artifact.conceptMap);
|
||||||
|
const snapshot = objectFrom(body.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot);
|
||||||
const conceptMapLenses = objectFrom(conceptMap.lenses || body.lenses || featureSet.lenses);
|
const conceptMapLenses = objectFrom(conceptMap.lenses || body.lenses || featureSet.lenses);
|
||||||
const buildOrderLens = objectFrom(conceptMapLenses.channel || conceptMapLenses.buildOrder || conceptMap.buildOrder);
|
const buildOrderLens = objectFrom(conceptMapLenses.channel || conceptMapLenses.buildOrder || conceptMap.buildOrder);
|
||||||
const directCandidateGroup = compactCandidateGroup([
|
const directCandidateGroup = compactCandidateGroup([
|
||||||
@@ -838,11 +844,26 @@ function optionsFromBody(body = {}) {
|
|||||||
{ items: conceptMap.deferred || conceptMap.defer || conceptMap.later, sourceSection: 'concept-map.deferred', defaultLane: 'defer' },
|
{ items: conceptMap.deferred || conceptMap.defer || conceptMap.later, sourceSection: 'concept-map.deferred', defaultLane: 'defer' },
|
||||||
{ items: conceptMap.parkingLot || conceptMap.park || conceptMap.parked, sourceSection: 'concept-map.parkingLot', defaultLane: 'park' },
|
{ items: conceptMap.parkingLot || conceptMap.park || conceptMap.parked, sourceSection: 'concept-map.parkingLot', defaultLane: 'park' },
|
||||||
]);
|
]);
|
||||||
|
const snapshotCandidateGroup = compactCandidateGroup([
|
||||||
|
{ items: snapshot.nextActions, sourceSection: 'snapshot.nextActions' },
|
||||||
|
{ items: snapshot.nextMoves, sourceSection: 'snapshot.nextMoves' },
|
||||||
|
{ items: snapshot.actions, sourceSection: 'snapshot.actions' },
|
||||||
|
{ items: snapshot.features, sourceSection: 'snapshot.features' },
|
||||||
|
{ items: snapshot.candidates, sourceSection: 'snapshot.candidates' },
|
||||||
|
{ items: snapshot.validateNext || snapshot.validate || snapshot.validation, sourceSection: 'snapshot.validateNext', defaultLane: 'validate-next' },
|
||||||
|
{ items: snapshot.experiments, sourceSection: 'snapshot.experiments', defaultLane: 'validate-next' },
|
||||||
|
{ items: snapshot.validationTests, sourceSection: 'snapshot.experiments', defaultLane: 'validate-next' },
|
||||||
|
{ items: snapshot.proofTests, sourceSection: 'snapshot.experiments', defaultLane: 'validate-next' },
|
||||||
|
{ items: snapshot.deferred || snapshot.defer || snapshot.later, sourceSection: 'snapshot.deferred', defaultLane: 'defer' },
|
||||||
|
{ items: snapshot.parkingLot || snapshot.park || snapshot.parked, sourceSection: 'snapshot.parkingLot', defaultLane: 'park' },
|
||||||
|
]);
|
||||||
const groupedCandidates = [
|
const groupedCandidates = [
|
||||||
...directCandidateGroup,
|
...directCandidateGroup,
|
||||||
|
...snapshotCandidateGroup,
|
||||||
...conceptMapCandidateGroup,
|
...conceptMapCandidateGroup,
|
||||||
...buildOrderSectionGroup(body.buildOrder, 'buildOrder'),
|
...buildOrderSectionGroup(body.buildOrder, 'buildOrder'),
|
||||||
...buildOrderSectionGroup(featureSet.buildOrder, 'feature-set.buildOrder'),
|
...buildOrderSectionGroup(featureSet.buildOrder, 'feature-set.buildOrder'),
|
||||||
|
...buildOrderSectionGroup(snapshot.buildOrder, 'snapshot.buildOrder'),
|
||||||
...buildOrderSectionGroup(conceptMap.buildOrder, 'concept-map.buildOrder'),
|
...buildOrderSectionGroup(conceptMap.buildOrder, 'concept-map.buildOrder'),
|
||||||
];
|
];
|
||||||
if (groupedCandidates.length) return normalizeCandidateGroup(groupedCandidates);
|
if (groupedCandidates.length) return normalizeCandidateGroup(groupedCandidates);
|
||||||
|
|||||||
Reference in New Issue
Block a user