@evomap/evolver-core 2.0.0-beta.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/algo/candidateAssembly.d.ts +7 -2
- package/dist/algo/candidateAssembly.js +81 -13
- package/dist/algo/capabilityCandidates.js +10 -0
- package/dist/algo/cycleEngine.d.ts +23 -2
- package/dist/algo/cycleEngine.js +236 -36
- package/dist/algo/exploration.d.ts +7 -0
- package/dist/algo/exploration.js +16 -3
- package/dist/algo/geneHealth.d.ts +2 -2
- package/dist/algo/geneHealth.js +5 -4
- package/dist/algo/geneSelection.d.ts +63 -5
- package/dist/algo/geneSelection.js +330 -30
- package/dist/algo/index.d.ts +2 -0
- package/dist/algo/index.js +2 -0
- package/dist/algo/orchestrator.d.ts +8 -1
- package/dist/algo/orchestrator.js +15 -3
- package/dist/algo/publishEligibility.d.ts +34 -0
- package/dist/algo/publishEligibility.js +52 -0
- package/dist/algo/ucb1.d.ts +53 -0
- package/dist/algo/ucb1.js +156 -0
- package/dist/assetstore/assetSidecarRecords.d.ts +7 -2
- package/dist/assetstore/assetSidecarRecords.js +238 -0
- package/dist/assetstore/assetSidecarRecovery.js +2 -2
- package/dist/assetstore/assetStoreHealth.d.ts +6 -0
- package/dist/assetstore/assetStoreHealth.js +56 -14
- package/dist/assetstore/assetStoreStorage.d.ts +1 -1
- package/dist/assetstore/assetStoreStorage.js +27 -9
- package/dist/assetstore/assetSyncLedger.d.ts +86 -0
- package/dist/assetstore/assetSyncLedger.js +683 -4
- package/dist/assetstore/index.d.ts +1 -0
- package/dist/assetstore/index.js +1 -0
- package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
- package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
- package/dist/assetstore/localJsonl.d.ts +4 -2
- package/dist/assetstore/localJsonl.js +89 -9
- package/dist/assetstore/provenance.d.ts +80 -4
- package/dist/assetstore/provenance.js +313 -5
- package/dist/assetstore/provider.d.ts +58 -1
- package/dist/assetstore/provider.js +97 -6
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +4 -3
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +4 -3
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +2 -1
- package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
- package/dist/benchmark/selectionFlatAbstention.js +481 -0
- package/dist/bootstrap/envFingerprint.d.ts +9 -0
- package/dist/bootstrap/envFingerprint.js +5 -0
- package/dist/bootstrap/index.d.ts +2 -1
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/v1EnvCompat.d.ts +111 -0
- package/dist/bootstrap/v1EnvCompat.js +280 -0
- package/dist/events/eventArchive.d.ts +2 -0
- package/dist/events/eventArchive.js +13 -3
- package/dist/events/eventSchema.d.ts +7 -7
- package/dist/events/eventStore.d.ts +2 -0
- package/dist/events/eventStore.js +5 -1
- package/dist/events/ingest.d.ts +1 -0
- package/dist/events/ingest.js +1 -0
- package/dist/events/paths.d.ts +3 -1
- package/dist/events/paths.js +4 -0
- package/dist/events/public.d.ts +2 -2
- package/dist/events/public.js +2 -2
- package/dist/events/reports.d.ts +2 -0
- package/dist/events/reports.js +4 -0
- package/dist/exec/autoExec.d.ts +43 -2
- package/dist/exec/autoExec.js +76 -9
- package/dist/exec/autonomousCycle.d.ts +22 -4
- package/dist/exec/autonomousCycle.js +64 -13
- package/dist/exec/claudeBridge.d.ts +32 -7
- package/dist/exec/claudeBridge.js +281 -29
- package/dist/exec/prompt.js +5 -1
- package/dist/exec/runnerRegistry.d.ts +68 -26
- package/dist/exec/runnerRegistry.js +307 -72
- package/dist/exec/selfPr.js +1 -7
- package/dist/feedback/envelope.d.ts +61 -0
- package/dist/feedback/envelope.js +168 -0
- package/dist/feedback/index.d.ts +1 -0
- package/dist/feedback/index.js +1 -0
- package/dist/hooks/hooks.js +1 -0
- package/dist/hub/assetCallLog.d.ts +35 -1
- package/dist/hub/assetCallLog.js +124 -1
- package/dist/hub/bindings.d.ts +8 -1
- package/dist/hub/bindings.js +17 -6
- package/dist/hub/capability.d.ts +90 -3
- package/dist/hub/fake.d.ts +2 -2
- package/dist/hub/fake.js +1 -1
- package/dist/hub/questionGenerator.d.ts +5 -1
- package/dist/hub/questionGenerator.js +8 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/mailbox/dispatch.d.ts +1 -1
- package/dist/mailbox/dispatch.js +22 -6
- package/dist/mailbox/envelope.d.ts +7 -1
- package/dist/mailbox/envelope.js +9 -2
- package/dist/mailbox/ipcServer.d.ts +10 -2
- package/dist/mailbox/ipcServer.js +163 -13
- package/dist/mailbox/store.d.ts +89 -3
- package/dist/mailbox/store.js +895 -41
- package/dist/ops/evolutionGraphProjection.d.ts +20 -0
- package/dist/ops/evolutionGraphProjection.js +315 -0
- package/dist/ops/index.d.ts +2 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/selfUpdate.d.ts +8 -0
- package/dist/ops/selfUpdate.js +24 -8
- package/dist/personality/schema.d.ts +16 -16
- package/dist/schema/evolutionGraph.d.ts +784 -0
- package/dist/schema/evolutionGraph.js +187 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/signal.d.ts +3 -3
- package/dist/schema/signal.js +1 -1
- package/dist/signals/curriculum.d.ts +55 -0
- package/dist/signals/curriculum.js +202 -0
- package/dist/signals/cycleHistoryFromEvents.js +17 -8
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +169 -1
- package/dist/signals/extractor.d.ts +2 -2
- package/dist/signals/extractor.js +31 -6
- package/dist/signals/index.d.ts +3 -1
- package/dist/signals/index.js +3 -1
- package/dist/signals/metaSignals.d.ts +4 -0
- package/dist/signals/metaSignals.js +42 -0
- package/dist/signals/signalGate.js +1 -1
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +43 -0
- package/dist/strategy/constraintAblation.js +115 -369
- package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
- package/dist/strategy/constraintAblationPredicates.js +339 -0
- package/dist/trace/index.d.ts +3 -1
- package/dist/trace/index.js +3 -1
- package/dist/trace/learningTrace.d.ts +209 -0
- package/dist/trace/learningTrace.js +276 -0
- package/dist/trace/proxyTurns.d.ts +31 -0
- package/dist/trace/proxyTurns.js +137 -0
- package/dist/verify/sandboxRunner.d.ts +28 -0
- package/dist/verify/sandboxRunner.js +218 -19
- package/dist/verify/sandboxedValidation.d.ts +9 -0
- package/dist/verify/sandboxedValidation.js +113 -13
- package/dist/verify/validation.d.ts +11 -1
- package/dist/verify/validation.js +31 -0
- package/package.json +5 -2
|
@@ -3,11 +3,12 @@ import type { ProvenanceStore } from '../assetstore/provenance.js';
|
|
|
3
3
|
import type { ReviewLedger } from '../assetstore/reviewLedger.js';
|
|
4
4
|
import type { GepCategory } from '../wire/index.js';
|
|
5
5
|
import type { ProblemPattern } from '../schema/problem.js';
|
|
6
|
-
import type { GeneCandidateInput } from './geneSelection.js';
|
|
6
|
+
import type { GeneCandidateInput, SelectionGuardMode } from './geneSelection.js';
|
|
7
7
|
import { CycleEngine, type CycleInput, type CycleResult, type SolidifyPermitGate } from './cycleEngine.js';
|
|
8
8
|
import { type PendingSignalsContext } from '../assetstore/pendingSignals.js';
|
|
9
9
|
import { type ReuseOutcomeSummary, type ReuseOutcomeEvent } from '../ops/reuseOutcomes.js';
|
|
10
10
|
import type { MemoryGraphAdvice } from './memoryGraph.js';
|
|
11
|
+
import type { SelectionPolicy } from './ucb1.js';
|
|
11
12
|
export interface RunCycleOptions {
|
|
12
13
|
cycleId: string;
|
|
13
14
|
problem: ProblemPattern;
|
|
@@ -20,6 +21,12 @@ export interface RunCycleOptions {
|
|
|
20
21
|
summary: string;
|
|
21
22
|
confidence: number;
|
|
22
23
|
selectionFloor?: number;
|
|
24
|
+
/** Emergency rollback for semantic IDF. Also skips pre-admission corpus collection. */
|
|
25
|
+
disableSemanticIdf?: boolean;
|
|
26
|
+
/** Experimental plateau policy. Omit for current engine-health + random drift behavior. */
|
|
27
|
+
selectionPolicy?: SelectionPolicy;
|
|
28
|
+
/** Versioned relevance-guard rollout. Omit for legacy direct-call behavior. */
|
|
29
|
+
selectionGuard?: SelectionGuardMode;
|
|
23
30
|
/** Optional explicit gene chosen by GEP / an external runtime; forwarded to CycleEngine after assembly. */
|
|
24
31
|
forcedGeneId?: string;
|
|
25
32
|
/** The agent runtime's work: run the mutation, return the outcome. */
|
|
@@ -18,6 +18,12 @@ export async function runEvolutionCycle(engine, store, opts) {
|
|
|
18
18
|
console.warn(`[ExplicitSignals] Failed to consume pending signals (non-fatal): ${message}`);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
// Curriculum targets must be present before candidate assembly; injecting them only inside CycleEngine would
|
|
22
|
+
// be too late to recall a gene whose signals_match is curriculum_target:*. The engine repeats this enrichment
|
|
23
|
+
// for direct runCycle callers; the operation is deterministic and set-idempotent.
|
|
24
|
+
const baseSignals = signals;
|
|
25
|
+
const preparedCurriculum = engine.prepareCurriculumSignals(baseSignals);
|
|
26
|
+
const selectionSignals = preparedCurriculum.signals;
|
|
21
27
|
// #268 phase 1 + #274 slice 3: fold cross-runtime reuse counts AND observed recall into the pool (soft re-order).
|
|
22
28
|
// Absent both → no map → default-off. Recall is folded at a lower weight (RECALL_WEIGHT). Assembly sums a gene's
|
|
23
29
|
// ids → sentiment; bounded + clamped in geneSelection; the hard trust/review/ban gates in assembly run first.
|
|
@@ -31,6 +37,7 @@ export async function runEvolutionCycle(engine, store, opts) {
|
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
const asmOpts = {
|
|
40
|
+
includeSemanticCorpus: opts.disableSemanticIdf !== true,
|
|
34
41
|
...(opts.candidateLimit ? { limit: opts.candidateLimit } : {}),
|
|
35
42
|
...(opts.provenance ? { provenance: opts.provenance } : {}),
|
|
36
43
|
...(opts.review ? { review: opts.review } : {}),
|
|
@@ -38,7 +45,7 @@ export async function runEvolutionCycle(engine, store, opts) {
|
|
|
38
45
|
...(opts.hubCandidates && opts.hubCandidates.length > 0 ? { hubCandidates: opts.hubCandidates } : {}),
|
|
39
46
|
...(reuseCounts.size > 0 ? { reuseCounts } : {}),
|
|
40
47
|
};
|
|
41
|
-
const { candidates: assembledCandidates, distilledFallback, antiWarnings } = await assembleSelectionPool(store,
|
|
48
|
+
const { candidates: assembledCandidates, distilledFallback, semanticCorpus, antiWarnings } = await assembleSelectionPool(store, selectionSignals, asmOpts);
|
|
42
49
|
const memoryByGene = new Map((opts.memoryGraphAdvice?.genes ?? []).map((evidence) => [evidence.geneId, evidence]));
|
|
43
50
|
const candidates = assembledCandidates.map((candidate) => {
|
|
44
51
|
const evidence = memoryByGene.get(candidate.geneId) ?? (candidate.assetId ? memoryByGene.get(candidate.assetId) : undefined);
|
|
@@ -53,10 +60,15 @@ export async function runEvolutionCycle(engine, store, opts) {
|
|
|
53
60
|
return engine.runCycle({
|
|
54
61
|
cycleId: opts.cycleId,
|
|
55
62
|
problem: opts.problem,
|
|
56
|
-
signals,
|
|
63
|
+
signals: baseSignals,
|
|
64
|
+
curriculumSignals: preparedCurriculum.curriculumSignals,
|
|
57
65
|
category: opts.category,
|
|
58
66
|
...(opts.strategyName !== undefined ? { strategyName: opts.strategyName } : {}),
|
|
59
67
|
candidates,
|
|
68
|
+
semanticCorpus,
|
|
69
|
+
...(opts.disableSemanticIdf ? { disableSemanticIdf: true } : {}),
|
|
70
|
+
...(opts.selectionPolicy ? { selectionPolicy: opts.selectionPolicy } : {}),
|
|
71
|
+
...(opts.selectionGuard ? { selectionGuard: opts.selectionGuard } : {}),
|
|
60
72
|
target: opts.target,
|
|
61
73
|
expectedEffect: opts.expectedEffect,
|
|
62
74
|
summary: opts.summary,
|
|
@@ -67,7 +79,7 @@ export async function runEvolutionCycle(engine, store, opts) {
|
|
|
67
79
|
...(opts.failureContext !== undefined ? { failureContext: opts.failureContext } : {}),
|
|
68
80
|
...(opts.solidifyPermit ? { solidifyPermit: opts.solidifyPermit } : {}),
|
|
69
81
|
// #97: forward the distilled-gene fallback pool so a no-signal-match cycle reuses a distilled strategy
|
|
70
|
-
// (instead of a blind innovate) when
|
|
82
|
+
// (instead of a blind innovate) when normal selection has no reusable positive choice.
|
|
71
83
|
...(memoryDistilledFallback.length > 0 ? { distilledFallback: memoryDistilledFallback } : {}),
|
|
72
84
|
...(antiWarnings.length > 0 ? { antiWarnings } : {}),
|
|
73
85
|
...(memoryEvidence.length > 0 ? { memoryEvidence } : {}),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type GeneLearningView } from '../assetstore/learningHistory.js';
|
|
2
|
+
import type { AssetStoreProvider } from '../assetstore/provider.js';
|
|
3
|
+
export type PublishEligibilityReason = 'eligible' | 'no_proven_success';
|
|
4
|
+
export interface GenePublishEvidence {
|
|
5
|
+
geneId: string;
|
|
6
|
+
/** Real, value-producing successes (inert zero-work successes excluded upstream). */
|
|
7
|
+
success: number;
|
|
8
|
+
failed: number;
|
|
9
|
+
inert: number;
|
|
10
|
+
total: number;
|
|
11
|
+
eligible: boolean;
|
|
12
|
+
reason: PublishEligibilityReason;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The single publish/upload eligibility predicate: a gene may be published iff
|
|
16
|
+
* it has at least one capsule with a `success` outcome.
|
|
17
|
+
*
|
|
18
|
+
* We count `success + inert`, NOT just `success`. aggregateLearningHistory
|
|
19
|
+
* demotes a `success` capsule that carries no `proof_of_work` to `inert`
|
|
20
|
+
* (#195), but the evox capsule builder (asset_builder.rs) does NOT emit
|
|
21
|
+
* `proof_of_work` — a genuine successful run surfaces there as `inert`. Gating
|
|
22
|
+
* publish on the proof-only `success` count would therefore block genes that
|
|
23
|
+
* actually succeeded in production. "Has a success outcome" is the honest bar
|
|
24
|
+
* for the publish gate; the proof/inert distinction stays where it belongs
|
|
25
|
+
* (success-rate, auto-promote), not here. A gene with only failures — or one
|
|
26
|
+
* never run at all — has `success + inert === 0` and is not publishable.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isGenePublishEligible(view: Pick<GeneLearningView, 'success' | 'inert'>): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Read-only: derive a gene's outcome evidence and whether it clears the publish
|
|
31
|
+
* bar. Returns `no_proven_success` for a gene the store has never seen succeed,
|
|
32
|
+
* so callers can surface a precise, self-serviceable reason.
|
|
33
|
+
*/
|
|
34
|
+
export declare function assessGenePublishEvidence(store: AssetStoreProvider, geneId: string): Promise<GenePublishEvidence>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Proven-success gate for publishing / uploading a gene (#581).
|
|
2
|
+
//
|
|
3
|
+
// A gene's `confidence` is minted from model self-report (solidify), text
|
|
4
|
+
// completeness (conversationDistiller), or is absent (like) — none of which
|
|
5
|
+
// prove the gene ever WORKED. Publishing such a gene to the market pairs it
|
|
6
|
+
// with a capsule and lets the Hub quality gate reject it after a round-trip
|
|
7
|
+
// ("never truly succeeded"). This predicate is the single, outcome-driven bar
|
|
8
|
+
// every publish/upload surface routes through, so an unproven gene is stopped
|
|
9
|
+
// locally with a clear reason instead of leaking to the Hub.
|
|
10
|
+
//
|
|
11
|
+
// Deliberately DISTINCT from `probationWouldPromote` (genePromotion.ts):
|
|
12
|
+
// auto-promotion runs with NO human in the loop, so it demands a strong, clean
|
|
13
|
+
// record (>= minSuccess successes AND zero failures). Publishing is human-
|
|
14
|
+
// initiated, so the bar is only "has this gene truly succeeded at least once"
|
|
15
|
+
// — a gene with many successes and one stray failure is still worth publishing,
|
|
16
|
+
// and blocking it on `failed === 0` would be wrong.
|
|
17
|
+
import { aggregateLearningHistory } from '../assetstore/learningHistory.js';
|
|
18
|
+
/**
|
|
19
|
+
* The single publish/upload eligibility predicate: a gene may be published iff
|
|
20
|
+
* it has at least one capsule with a `success` outcome.
|
|
21
|
+
*
|
|
22
|
+
* We count `success + inert`, NOT just `success`. aggregateLearningHistory
|
|
23
|
+
* demotes a `success` capsule that carries no `proof_of_work` to `inert`
|
|
24
|
+
* (#195), but the evox capsule builder (asset_builder.rs) does NOT emit
|
|
25
|
+
* `proof_of_work` — a genuine successful run surfaces there as `inert`. Gating
|
|
26
|
+
* publish on the proof-only `success` count would therefore block genes that
|
|
27
|
+
* actually succeeded in production. "Has a success outcome" is the honest bar
|
|
28
|
+
* for the publish gate; the proof/inert distinction stays where it belongs
|
|
29
|
+
* (success-rate, auto-promote), not here. A gene with only failures — or one
|
|
30
|
+
* never run at all — has `success + inert === 0` and is not publishable.
|
|
31
|
+
*/
|
|
32
|
+
export function isGenePublishEligible(view) {
|
|
33
|
+
return view.success + (view.inert ?? 0) >= 1;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read-only: derive a gene's outcome evidence and whether it clears the publish
|
|
37
|
+
* bar. Returns `no_proven_success` for a gene the store has never seen succeed,
|
|
38
|
+
* so callers can surface a precise, self-serviceable reason.
|
|
39
|
+
*/
|
|
40
|
+
export async function assessGenePublishEvidence(store, geneId) {
|
|
41
|
+
const view = await aggregateLearningHistory(store, geneId);
|
|
42
|
+
const eligible = isGenePublishEligible(view);
|
|
43
|
+
return {
|
|
44
|
+
geneId,
|
|
45
|
+
success: view.success,
|
|
46
|
+
failed: view.failed,
|
|
47
|
+
inert: view.inert ?? 0,
|
|
48
|
+
total: view.total,
|
|
49
|
+
eligible,
|
|
50
|
+
reason: eligible ? 'eligible' : 'no_proven_success',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { RootEvent } from '../events/eventSchema.js';
|
|
2
|
+
export declare const SELECTION_POLICIES: readonly ["engine-health", "ucb1-shadow", "ucb1"];
|
|
3
|
+
export type SelectionPolicy = (typeof SELECTION_POLICIES)[number];
|
|
4
|
+
export declare const UCB1_SELECTION_POLICY_VERSION = "ucb1-v1";
|
|
5
|
+
export declare const UCB1_REWARD_POLICY_VERSION = "productive-binary-v1";
|
|
6
|
+
export interface Ucb1ArmStats {
|
|
7
|
+
armId: string;
|
|
8
|
+
pulls: number;
|
|
9
|
+
completedPulls: number;
|
|
10
|
+
rewardSum: number;
|
|
11
|
+
meanReward: number;
|
|
12
|
+
}
|
|
13
|
+
export interface Ucb1History {
|
|
14
|
+
arms: ReadonlyMap<string, Ucb1ArmStats>;
|
|
15
|
+
/** Every non-ad-hoc decision counts immediately, including decisions whose terminal event is still pending. */
|
|
16
|
+
totalPulls: number;
|
|
17
|
+
}
|
|
18
|
+
export interface Ucb1Arm {
|
|
19
|
+
armId: string;
|
|
20
|
+
baseScore: number;
|
|
21
|
+
explorationEligible: boolean;
|
|
22
|
+
stats: Ucb1ArmStats;
|
|
23
|
+
}
|
|
24
|
+
export interface Ucb1Choice {
|
|
25
|
+
armId: string;
|
|
26
|
+
pulls: number;
|
|
27
|
+
completedPulls: number;
|
|
28
|
+
totalPulls: number;
|
|
29
|
+
meanReward: number;
|
|
30
|
+
/** Null represents the canonical +Infinity cold-start index without putting Infinity on the event wire. */
|
|
31
|
+
bonus: number | null;
|
|
32
|
+
/** Null represents the canonical +Infinity cold-start index without putting Infinity on the event wire. */
|
|
33
|
+
index: number | null;
|
|
34
|
+
coldStart: boolean;
|
|
35
|
+
}
|
|
36
|
+
export type Ucb1FallbackReason = 'empty_pool' | 'ineligible_arm' | 'missing_history';
|
|
37
|
+
export interface Ucb1Decision {
|
|
38
|
+
choice: Ucb1Choice | null;
|
|
39
|
+
fallbackReason?: Ucb1FallbackReason;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Rebuild UCB1 state from the append-only root event log. Decisions are pulls immediately, so later readers no
|
|
43
|
+
* longer treat an in-flight arm as cold. The read-select-append sequence is not an atomic reservation across
|
|
44
|
+
* workers. A terminal event adds reward exactly once per cycle; duplicate rows collapse to their latest projection.
|
|
45
|
+
*/
|
|
46
|
+
export declare function deriveUcb1History(events: readonly RootEvent[]): Ucb1History;
|
|
47
|
+
/** Combine current asset identity with legacy gene-only history when that bridge is unambiguous. */
|
|
48
|
+
export declare function ucb1StatsForCandidate(history: Ucb1History, geneId: string, assetId?: string, includeLegacyGeneHistory?: boolean): Ucb1ArmStats;
|
|
49
|
+
/**
|
|
50
|
+
* Canonical UCB1: mean reward + sqrt(2 ln(total pulls) / arm pulls). Cold arms have +Infinity and are ordered
|
|
51
|
+
* deterministically by base score then arm identity. The caller supplies only the already-gated exploration window.
|
|
52
|
+
*/
|
|
53
|
+
export declare function chooseUcb1Arm(arms: readonly Ucb1Arm[], historyTotalPulls: number): Ucb1Decision;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export const SELECTION_POLICIES = ['engine-health', 'ucb1-shadow', 'ucb1'];
|
|
2
|
+
export const UCB1_SELECTION_POLICY_VERSION = 'ucb1-v1';
|
|
3
|
+
export const UCB1_REWARD_POLICY_VERSION = 'productive-binary-v1';
|
|
4
|
+
function nonEmptyString(value) {
|
|
5
|
+
if (typeof value !== 'string')
|
|
6
|
+
return undefined;
|
|
7
|
+
const normalized = value.trim();
|
|
8
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
9
|
+
}
|
|
10
|
+
function decisionProjection(event) {
|
|
11
|
+
if (event.type !== 'decision.gene_selected')
|
|
12
|
+
return undefined;
|
|
13
|
+
const payload = event.payload;
|
|
14
|
+
const cycleId = nonEmptyString(payload['cycleId']);
|
|
15
|
+
const geneId = nonEmptyString(payload['selectedGeneId']);
|
|
16
|
+
if (!cycleId)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (!geneId || geneId === 'ad-hoc')
|
|
19
|
+
return { cycleId, decision: null };
|
|
20
|
+
return {
|
|
21
|
+
cycleId,
|
|
22
|
+
decision: { armId: nonEmptyString(payload['selectedAssetId']) ?? geneId },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function terminalReward(event) {
|
|
26
|
+
if (event.type !== 'cycle.failed' && event.type !== 'cycle.solidified')
|
|
27
|
+
return null;
|
|
28
|
+
const payload = event.payload;
|
|
29
|
+
const cycleId = nonEmptyString(payload['cycleId']);
|
|
30
|
+
if (!cycleId)
|
|
31
|
+
return null;
|
|
32
|
+
if (event.type === 'cycle.failed' || payload['producedValue'] === false)
|
|
33
|
+
return { cycleId, reward: 0 };
|
|
34
|
+
// Legacy solidified events predate producedValue. Preserve the existing confidence replay compatibility policy.
|
|
35
|
+
return { cycleId, reward: 1 };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Rebuild UCB1 state from the append-only root event log. Decisions are pulls immediately, so later readers no
|
|
39
|
+
* longer treat an in-flight arm as cold. The read-select-append sequence is not an atomic reservation across
|
|
40
|
+
* workers. A terminal event adds reward exactly once per cycle; duplicate rows collapse to their latest projection.
|
|
41
|
+
*/
|
|
42
|
+
export function deriveUcb1History(events) {
|
|
43
|
+
const cycles = new Map();
|
|
44
|
+
for (const event of events) {
|
|
45
|
+
const decision = decisionProjection(event);
|
|
46
|
+
// A restarted/re-emitted decision for the same cycleId begins a new pending lifecycle. Never let a terminal
|
|
47
|
+
// from the earlier lifecycle leak reward into this newer arm.
|
|
48
|
+
if (decision) {
|
|
49
|
+
if (decision.decision)
|
|
50
|
+
cycles.set(decision.cycleId, { decision: decision.decision });
|
|
51
|
+
else
|
|
52
|
+
cycles.delete(decision.cycleId);
|
|
53
|
+
}
|
|
54
|
+
const terminal = terminalReward(event);
|
|
55
|
+
if (terminal) {
|
|
56
|
+
const cycle = cycles.get(terminal.cycleId);
|
|
57
|
+
if (cycle)
|
|
58
|
+
cycle.reward = terminal.reward;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const mutable = new Map();
|
|
62
|
+
for (const cycle of cycles.values()) {
|
|
63
|
+
const current = mutable.get(cycle.decision.armId) ?? { pulls: 0, completedPulls: 0, rewardSum: 0 };
|
|
64
|
+
current.pulls += 1;
|
|
65
|
+
if (cycle.reward !== undefined) {
|
|
66
|
+
current.completedPulls += 1;
|
|
67
|
+
current.rewardSum += cycle.reward;
|
|
68
|
+
}
|
|
69
|
+
mutable.set(cycle.decision.armId, current);
|
|
70
|
+
}
|
|
71
|
+
const arms = new Map();
|
|
72
|
+
for (const [armId, stats] of mutable) {
|
|
73
|
+
arms.set(armId, {
|
|
74
|
+
armId,
|
|
75
|
+
...stats,
|
|
76
|
+
meanReward: stats.completedPulls > 0 ? stats.rewardSum / stats.completedPulls : 0,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { arms, totalPulls: cycles.size };
|
|
80
|
+
}
|
|
81
|
+
function emptyStats(armId) {
|
|
82
|
+
return { armId, pulls: 0, completedPulls: 0, rewardSum: 0, meanReward: 0 };
|
|
83
|
+
}
|
|
84
|
+
/** Combine current asset identity with legacy gene-only history when that bridge is unambiguous. */
|
|
85
|
+
export function ucb1StatsForCandidate(history, geneId, assetId, includeLegacyGeneHistory = true) {
|
|
86
|
+
const armId = assetId ?? geneId;
|
|
87
|
+
const identities = assetId === undefined
|
|
88
|
+
? [geneId]
|
|
89
|
+
: [...new Set([assetId, ...(includeLegacyGeneHistory ? [geneId] : [])])];
|
|
90
|
+
const matches = identities.map((identity) => history.arms.get(identity)).filter((stats) => stats !== undefined);
|
|
91
|
+
if (matches.length === 0)
|
|
92
|
+
return emptyStats(armId);
|
|
93
|
+
const pulls = matches.reduce((sum, stats) => sum + stats.pulls, 0);
|
|
94
|
+
const completedPulls = matches.reduce((sum, stats) => sum + stats.completedPulls, 0);
|
|
95
|
+
const rewardSum = matches.reduce((sum, stats) => sum + stats.rewardSum, 0);
|
|
96
|
+
return {
|
|
97
|
+
armId,
|
|
98
|
+
pulls,
|
|
99
|
+
completedPulls,
|
|
100
|
+
rewardSum,
|
|
101
|
+
meanReward: completedPulls > 0 ? rewardSum / completedPulls : 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function finiteBaseScore(value) {
|
|
105
|
+
return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
|
|
106
|
+
}
|
|
107
|
+
function finiteCount(value) {
|
|
108
|
+
return Number.isFinite(value) ? Math.min(Number.MAX_SAFE_INTEGER, Math.floor(Math.max(0, value))) : 0;
|
|
109
|
+
}
|
|
110
|
+
function finiteMeanReward(value) {
|
|
111
|
+
return Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
|
|
112
|
+
}
|
|
113
|
+
function saturatingCountSum(total, value) {
|
|
114
|
+
const count = finiteCount(value);
|
|
115
|
+
return total >= Number.MAX_SAFE_INTEGER - count ? Number.MAX_SAFE_INTEGER : total + count;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Canonical UCB1: mean reward + sqrt(2 ln(total pulls) / arm pulls). Cold arms have +Infinity and are ordered
|
|
119
|
+
* deterministically by base score then arm identity. The caller supplies only the already-gated exploration window.
|
|
120
|
+
*/
|
|
121
|
+
export function chooseUcb1Arm(arms, historyTotalPulls) {
|
|
122
|
+
if (arms.length === 0)
|
|
123
|
+
return { choice: null, fallbackReason: 'empty_pool' };
|
|
124
|
+
if (arms.some((arm) => !arm.explorationEligible))
|
|
125
|
+
return { choice: null, fallbackReason: 'ineligible_arm' };
|
|
126
|
+
const totalPulls = Math.max(1, finiteCount(historyTotalPulls), arms.reduce((sum, arm) => saturatingCountSum(sum, arm.stats.pulls), 0));
|
|
127
|
+
const ranked = arms.map((arm) => {
|
|
128
|
+
const pulls = finiteCount(arm.stats.pulls);
|
|
129
|
+
const meanReward = finiteMeanReward(arm.stats.meanReward);
|
|
130
|
+
const coldStart = pulls === 0;
|
|
131
|
+
const bonus = coldStart ? null : Math.sqrt((2 * Math.log(totalPulls)) / pulls);
|
|
132
|
+
const index = coldStart ? null : meanReward + bonus;
|
|
133
|
+
return { arm, pulls, meanReward, coldStart, bonus, index };
|
|
134
|
+
}).sort((left, right) => {
|
|
135
|
+
if (left.coldStart !== right.coldStart)
|
|
136
|
+
return left.coldStart ? -1 : 1;
|
|
137
|
+
if (left.index !== null && right.index !== null && left.index !== right.index)
|
|
138
|
+
return right.index - left.index;
|
|
139
|
+
const leftScore = finiteBaseScore(left.arm.baseScore);
|
|
140
|
+
const rightScore = finiteBaseScore(right.arm.baseScore);
|
|
141
|
+
return rightScore !== leftScore ? rightScore - leftScore : left.arm.armId.localeCompare(right.arm.armId);
|
|
142
|
+
});
|
|
143
|
+
const winner = ranked[0];
|
|
144
|
+
return {
|
|
145
|
+
choice: {
|
|
146
|
+
armId: winner.arm.armId,
|
|
147
|
+
pulls: winner.pulls,
|
|
148
|
+
completedPulls: finiteCount(winner.arm.stats.completedPulls),
|
|
149
|
+
totalPulls,
|
|
150
|
+
meanReward: winner.meanReward,
|
|
151
|
+
bonus: winner.bonus,
|
|
152
|
+
index: winner.index,
|
|
153
|
+
coldStart: winner.coldStart,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssetSyncRecord } from './assetSyncLedger.js';
|
|
1
|
+
import type { AssetSyncInventorySegmentRecord, AssetSyncRecord, AssetSyncRunRecord } from './assetSyncLedger.js';
|
|
2
2
|
import type { ProvenanceRecord } from './provenance.js';
|
|
3
3
|
import type { ReviewRecord } from './reviewLedger.js';
|
|
4
4
|
export type AssetSidecarKind = 'provenance' | 'review' | 'asset-sync';
|
|
@@ -20,4 +20,9 @@ export declare function parseSidecarJsonl<T>(raw: string, parseRecord: (value: u
|
|
|
20
20
|
export declare function assertTrustSidecarHealthy<T>(sidecar: Extract<AssetSidecarKind, 'provenance' | 'review'>, parsed: ParsedSidecarJsonl<T>): void;
|
|
21
21
|
export declare function parseProvenanceRecord(value: unknown): ProvenanceRecord | null;
|
|
22
22
|
export declare function parseReviewRecord(value: unknown): ReviewRecord | null;
|
|
23
|
-
export declare function parseAssetSyncRecord(value: unknown): AssetSyncRecord | null;
|
|
23
|
+
export declare function parseAssetSyncRecord(value: unknown): AssetSyncRecord | null;
|
|
24
|
+
export declare const ASSET_SYNC_INVENTORY_MAX_SEGMENT_BYTES: number;
|
|
25
|
+
export type AssetSyncSidecarRecord = AssetSyncRecord | AssetSyncRunRecord | AssetSyncInventorySegmentRecord;
|
|
26
|
+
export declare function parseAssetSyncRunRecord(value: unknown): AssetSyncRunRecord | null;
|
|
27
|
+
export declare function parseAssetSyncInventorySegmentRecord(value: unknown): AssetSyncInventorySegmentRecord | null;
|
|
28
|
+
export declare function parseAssetSyncSidecarRecord(value: unknown): AssetSyncSidecarRecord | null;
|
|
@@ -62,6 +62,9 @@ export function parseProvenanceRecord(value) {
|
|
|
62
62
|
const decidedBy = stringField(record, 'decidedBy');
|
|
63
63
|
const promotedBy = stringField(record, 'promotedBy');
|
|
64
64
|
const reason = stringField(record, 'reason');
|
|
65
|
+
const frozenContentId = stringField(record, 'frozenContentId');
|
|
66
|
+
if (frozenContentId !== undefined && !/^sha256:[0-9a-f]{64}$/.test(frozenContentId))
|
|
67
|
+
return null;
|
|
65
68
|
return {
|
|
66
69
|
assetId,
|
|
67
70
|
source,
|
|
@@ -71,6 +74,7 @@ export function parseProvenanceRecord(value) {
|
|
|
71
74
|
...(decidedBy ? { decidedBy } : {}),
|
|
72
75
|
...(promotedBy ? { promotedBy } : {}),
|
|
73
76
|
...(reason ? { reason } : {}),
|
|
77
|
+
...(frozenContentId ? { frozenContentId } : {}),
|
|
74
78
|
};
|
|
75
79
|
}
|
|
76
80
|
export function parseReviewRecord(value) {
|
|
@@ -116,6 +120,10 @@ export function parseAssetSyncRecord(value) {
|
|
|
116
120
|
}
|
|
117
121
|
const logicalId = stringField(record, 'logicalId');
|
|
118
122
|
const status = stringField(record, 'status');
|
|
123
|
+
const runKey = optionalStrictString(record, 'runKey');
|
|
124
|
+
const inventoryKey = optionalStrictString(record, 'inventoryKey');
|
|
125
|
+
if (runKey === null || inventoryKey === null)
|
|
126
|
+
return null;
|
|
119
127
|
const forced = record['forced'] === true;
|
|
120
128
|
const collisionWithAssetId = stringField(record, 'collisionWithAssetId');
|
|
121
129
|
return {
|
|
@@ -125,12 +133,223 @@ export function parseAssetSyncRecord(value) {
|
|
|
125
133
|
scope,
|
|
126
134
|
syncedAt,
|
|
127
135
|
remoteAssetId,
|
|
136
|
+
...(runKey ? { runKey } : {}),
|
|
137
|
+
...(inventoryKey ? { inventoryKey } : {}),
|
|
128
138
|
...(logicalId ? { logicalId } : {}),
|
|
129
139
|
...(status ? { status } : {}),
|
|
130
140
|
...(forced ? { forced: true } : {}),
|
|
131
141
|
...(collisionWithAssetId ? { collisionWithAssetId } : {}),
|
|
132
142
|
};
|
|
133
143
|
}
|
|
144
|
+
const MAX_INVENTORY_SEGMENTS = 24;
|
|
145
|
+
const MAX_INVENTORY_ITEMS_PER_SEGMENT = 10_000;
|
|
146
|
+
const MAX_INVENTORY_STRING_LENGTH = 4096;
|
|
147
|
+
export const ASSET_SYNC_INVENTORY_MAX_SEGMENT_BYTES = 2 * 1024 * 1024;
|
|
148
|
+
export function parseAssetSyncRunRecord(value) {
|
|
149
|
+
const record = objectRecord(value);
|
|
150
|
+
if (!record || record['recordType'] !== 'run')
|
|
151
|
+
return null;
|
|
152
|
+
const runId = optionalStrictString(record, 'runId');
|
|
153
|
+
const runKey = optionalStrictString(record, 'runKey');
|
|
154
|
+
const state = optionalStrictString(record, 'state');
|
|
155
|
+
const currentTimestamp = optionalStrictString(record, 'syncedAt');
|
|
156
|
+
const legacyTimestamp = optionalStrictString(record, 'at');
|
|
157
|
+
if (!runId
|
|
158
|
+
|| !runKey
|
|
159
|
+
|| state === null
|
|
160
|
+
|| !isRunState(state)
|
|
161
|
+
|| currentTimestamp === null
|
|
162
|
+
|| legacyTimestamp === null
|
|
163
|
+
|| (currentTimestamp && legacyTimestamp && currentTimestamp !== legacyTimestamp)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const syncedAt = currentTimestamp ?? legacyTimestamp;
|
|
167
|
+
if (!syncedAt || Number.isNaN(Date.parse(syncedAt)))
|
|
168
|
+
return null;
|
|
169
|
+
const remoteAssetId = optionalStrictString(record, 'remoteAssetId');
|
|
170
|
+
const outcomeValue = optionalStrictString(record, 'outcome');
|
|
171
|
+
const reason = optionalStrictString(record, 'reason');
|
|
172
|
+
if (remoteAssetId === null || outcomeValue === null || reason === null)
|
|
173
|
+
return null;
|
|
174
|
+
const outcome = isRunOutcome(outcomeValue) ? outcomeValue : undefined;
|
|
175
|
+
if (outcomeValue !== undefined && !outcome)
|
|
176
|
+
return null;
|
|
177
|
+
let plan;
|
|
178
|
+
const rawPlan = record['plan'];
|
|
179
|
+
if (rawPlan !== undefined) {
|
|
180
|
+
if (!Array.isArray(rawPlan) || rawPlan.length === 0)
|
|
181
|
+
return null;
|
|
182
|
+
const normalized = rawPlan.map((entry) => typeof entry === 'string' ? entry.trim() : '');
|
|
183
|
+
if (normalized.some((entry, index) => !entry || entry !== rawPlan[index]))
|
|
184
|
+
return null;
|
|
185
|
+
if (new Set(normalized).size !== normalized.length)
|
|
186
|
+
return null;
|
|
187
|
+
plan = Object.freeze(normalized);
|
|
188
|
+
}
|
|
189
|
+
if (state === 'started' && (remoteAssetId || outcome || reason))
|
|
190
|
+
return null;
|
|
191
|
+
if (state === 'progress') {
|
|
192
|
+
if (!remoteAssetId || !outcome || plan)
|
|
193
|
+
return null;
|
|
194
|
+
if ((outcome === 'failed') !== Boolean(reason))
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
if (state === 'completed' && (remoteAssetId || outcome || reason || plan))
|
|
198
|
+
return null;
|
|
199
|
+
return immutableRunRecord({
|
|
200
|
+
recordType: 'run',
|
|
201
|
+
runId,
|
|
202
|
+
runKey,
|
|
203
|
+
state,
|
|
204
|
+
...(remoteAssetId ? { remoteAssetId } : {}),
|
|
205
|
+
...(outcome ? { outcome } : {}),
|
|
206
|
+
...(reason ? { reason } : {}),
|
|
207
|
+
...(plan ? { plan } : {}),
|
|
208
|
+
syncedAt,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
export function parseAssetSyncInventorySegmentRecord(value) {
|
|
212
|
+
const record = objectRecord(value);
|
|
213
|
+
if (!record || record['recordType'] !== 'inventory_scan')
|
|
214
|
+
return null;
|
|
215
|
+
try {
|
|
216
|
+
if (Buffer.byteLength(JSON.stringify(record), 'utf8') > ASSET_SYNC_INVENTORY_MAX_SEGMENT_BYTES)
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const scanId = strictBoundedString(record['scanId']);
|
|
223
|
+
const inventoryKey = strictBoundedString(record['inventoryKey']);
|
|
224
|
+
const scope = strictBoundedString(record['scope']);
|
|
225
|
+
const syncedAt = strictBoundedString(record['syncedAt']);
|
|
226
|
+
const index = record['index'];
|
|
227
|
+
const inputCursorFingerprints = parseInventoryCursorFingerprints(record['inputCursorFingerprints']);
|
|
228
|
+
const nextCursorFingerprints = parseInventoryCursorFingerprints(record['nextCursorFingerprints']);
|
|
229
|
+
const anonymousBlocked = record['anonymousBlocked'];
|
|
230
|
+
const cursorHeld = record['cursorHeld'];
|
|
231
|
+
const batchId = record['batchId'];
|
|
232
|
+
const batchIndex = record['batchIndex'];
|
|
233
|
+
const batchSize = record['batchSize'];
|
|
234
|
+
const rawItems = record['items'];
|
|
235
|
+
const hasBatchMetadata = batchId !== undefined || batchIndex !== undefined || batchSize !== undefined;
|
|
236
|
+
if (!scanId
|
|
237
|
+
|| !inventoryKey
|
|
238
|
+
|| (scope !== 'purchased' && scope !== 'published' && scope !== 'all')
|
|
239
|
+
|| !syncedAt
|
|
240
|
+
|| Number.isNaN(Date.parse(syncedAt))
|
|
241
|
+
|| !Number.isInteger(index)
|
|
242
|
+
|| index < 0
|
|
243
|
+
|| index >= MAX_INVENTORY_SEGMENTS
|
|
244
|
+
|| !inputCursorFingerprints
|
|
245
|
+
|| !nextCursorFingerprints
|
|
246
|
+
|| !Number.isInteger(anonymousBlocked)
|
|
247
|
+
|| anonymousBlocked < 0
|
|
248
|
+
|| anonymousBlocked > MAX_INVENTORY_ITEMS_PER_SEGMENT
|
|
249
|
+
|| !Array.isArray(rawItems)
|
|
250
|
+
|| rawItems.length > MAX_INVENTORY_ITEMS_PER_SEGMENT
|
|
251
|
+
|| (cursorHeld !== undefined && cursorHeld !== true)
|
|
252
|
+
|| (hasBatchMetadata && (!strictBoundedString(batchId)
|
|
253
|
+
|| !Number.isInteger(batchIndex)
|
|
254
|
+
|| !Number.isInteger(batchSize)
|
|
255
|
+
|| batchSize < 2
|
|
256
|
+
|| batchSize > MAX_INVENTORY_SEGMENTS
|
|
257
|
+
|| batchIndex < 0
|
|
258
|
+
|| batchIndex >= batchSize
|
|
259
|
+
|| index - batchIndex < 0
|
|
260
|
+
|| index + (batchSize - batchIndex) > MAX_INVENTORY_SEGMENTS))) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
if (scope === 'purchased'
|
|
264
|
+
&& (inputCursorFingerprints.published !== null || nextCursorFingerprints.published !== null))
|
|
265
|
+
return null;
|
|
266
|
+
if (cursorHeld === true
|
|
267
|
+
&& (inputCursorFingerprints.purchased !== nextCursorFingerprints.purchased
|
|
268
|
+
|| inputCursorFingerprints.published !== nextCursorFingerprints.published))
|
|
269
|
+
return null;
|
|
270
|
+
if (scope === 'published'
|
|
271
|
+
&& (inputCursorFingerprints.purchased !== null || nextCursorFingerprints.purchased !== null))
|
|
272
|
+
return null;
|
|
273
|
+
if (index === 0
|
|
274
|
+
&& (inputCursorFingerprints.purchased !== null || inputCursorFingerprints.published !== null))
|
|
275
|
+
return null;
|
|
276
|
+
const items = [];
|
|
277
|
+
const seen = new Set();
|
|
278
|
+
for (const rawItem of rawItems) {
|
|
279
|
+
const item = objectRecord(rawItem);
|
|
280
|
+
if (!item)
|
|
281
|
+
return null;
|
|
282
|
+
const remoteAssetId = strictBoundedString(item['remoteAssetId']);
|
|
283
|
+
const outcome = strictBoundedString(item['outcome']);
|
|
284
|
+
if (!remoteAssetId || !isInventoryOutcome(outcome) || seen.has(remoteAssetId))
|
|
285
|
+
return null;
|
|
286
|
+
seen.add(remoteAssetId);
|
|
287
|
+
items.push(Object.freeze({ remoteAssetId, outcome }));
|
|
288
|
+
}
|
|
289
|
+
if (items.length + anonymousBlocked > MAX_INVENTORY_ITEMS_PER_SEGMENT)
|
|
290
|
+
return null;
|
|
291
|
+
return Object.freeze({
|
|
292
|
+
recordType: 'inventory_scan',
|
|
293
|
+
scanId,
|
|
294
|
+
inventoryKey,
|
|
295
|
+
scope,
|
|
296
|
+
index: index,
|
|
297
|
+
inputCursorFingerprints,
|
|
298
|
+
nextCursorFingerprints,
|
|
299
|
+
items: Object.freeze(items),
|
|
300
|
+
anonymousBlocked: anonymousBlocked,
|
|
301
|
+
...(cursorHeld === true ? { cursorHeld: true } : {}),
|
|
302
|
+
...(hasBatchMetadata ? {
|
|
303
|
+
batchId: batchId,
|
|
304
|
+
batchIndex: batchIndex,
|
|
305
|
+
batchSize: batchSize,
|
|
306
|
+
} : {}),
|
|
307
|
+
syncedAt,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
export function parseAssetSyncSidecarRecord(value) {
|
|
311
|
+
const record = objectRecord(value);
|
|
312
|
+
if (!record)
|
|
313
|
+
return null;
|
|
314
|
+
if (record['recordType'] !== undefined) {
|
|
315
|
+
if (record['recordType'] === 'run')
|
|
316
|
+
return parseAssetSyncRunRecord(record);
|
|
317
|
+
if (record['recordType'] === 'inventory_scan')
|
|
318
|
+
return parseAssetSyncInventorySegmentRecord(record);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return parseAssetSyncRecord(record);
|
|
322
|
+
}
|
|
323
|
+
function parseInventoryCursorFingerprints(value) {
|
|
324
|
+
const record = objectRecord(value);
|
|
325
|
+
if (!record || !Object.hasOwn(record, 'purchased') || !Object.hasOwn(record, 'published'))
|
|
326
|
+
return null;
|
|
327
|
+
const purchased = strictNullableCursorFingerprint(record['purchased']);
|
|
328
|
+
const published = strictNullableCursorFingerprint(record['published']);
|
|
329
|
+
if (purchased === undefined || published === undefined)
|
|
330
|
+
return null;
|
|
331
|
+
return Object.freeze({ purchased, published });
|
|
332
|
+
}
|
|
333
|
+
function strictNullableCursorFingerprint(value) {
|
|
334
|
+
if (value === null)
|
|
335
|
+
return null;
|
|
336
|
+
return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value) ? value : undefined;
|
|
337
|
+
}
|
|
338
|
+
function strictBoundedString(value) {
|
|
339
|
+
return typeof value === 'string'
|
|
340
|
+
&& value.length <= MAX_INVENTORY_STRING_LENGTH
|
|
341
|
+
&& value.trim()
|
|
342
|
+
&& value === value.trim()
|
|
343
|
+
? value
|
|
344
|
+
: undefined;
|
|
345
|
+
}
|
|
346
|
+
function isInventoryOutcome(value) {
|
|
347
|
+
return value === 'imported'
|
|
348
|
+
|| value === 'already_local'
|
|
349
|
+
|| value === 'blocked'
|
|
350
|
+
|| value === 'failed'
|
|
351
|
+
|| value === 'pending';
|
|
352
|
+
}
|
|
134
353
|
function objectRecord(value) {
|
|
135
354
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
136
355
|
? value
|
|
@@ -139,4 +358,23 @@ function objectRecord(value) {
|
|
|
139
358
|
function stringField(value, key) {
|
|
140
359
|
const raw = value[key];
|
|
141
360
|
return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
|
|
361
|
+
}
|
|
362
|
+
function optionalStrictString(value, key) {
|
|
363
|
+
const raw = value[key];
|
|
364
|
+
if (raw === undefined)
|
|
365
|
+
return undefined;
|
|
366
|
+
return typeof raw === 'string' && raw.trim() && raw === raw.trim() ? raw : null;
|
|
367
|
+
}
|
|
368
|
+
function immutableRunRecord(record) {
|
|
369
|
+
const plan = record.plan ? Object.freeze([...record.plan]) : undefined;
|
|
370
|
+
return Object.freeze({ ...record, ...(plan ? { plan } : {}) });
|
|
371
|
+
}
|
|
372
|
+
function isRunState(value) {
|
|
373
|
+
return value === 'started' || value === 'progress' || value === 'completed';
|
|
374
|
+
}
|
|
375
|
+
function isRunOutcome(value) {
|
|
376
|
+
return value === 'imported'
|
|
377
|
+
|| value === 'already_local'
|
|
378
|
+
|| value === 'failed'
|
|
379
|
+
|| value === 'remote_missing';
|
|
142
380
|
}
|