@evomap/evolver-core 2.0.0-beta.19 → 2.0.0-beta.22
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/assets/gep/genes.jsonl +5 -5
- package/dist/algo/bans.d.ts +10 -1
- package/dist/algo/bans.js +56 -6
- package/dist/algo/candidateAssembly.d.ts +25 -2
- package/dist/algo/candidateAssembly.js +143 -20
- package/dist/algo/capabilityCandidates.js +10 -0
- package/dist/algo/cycleEngine.d.ts +40 -5
- package/dist/algo/cycleEngine.js +277 -37
- package/dist/algo/evolutionEvent.d.ts +26 -0
- package/dist/algo/evolutionEvent.js +32 -0
- package/dist/algo/exploration.d.ts +7 -0
- package/dist/algo/exploration.js +16 -3
- package/dist/algo/geneHealth.d.ts +34 -1
- package/dist/algo/geneHealth.js +42 -0
- package/dist/algo/geneIntake.d.ts +46 -1
- package/dist/algo/geneIntake.js +121 -10
- package/dist/algo/geneSelection.d.ts +115 -6
- package/dist/algo/geneSelection.js +459 -32
- package/dist/algo/index.d.ts +4 -1
- package/dist/algo/index.js +4 -1
- package/dist/algo/kautoProjection.d.ts +41 -0
- package/dist/algo/kautoProjection.js +95 -0
- package/dist/algo/kautoValidator.d.ts +68 -0
- package/dist/algo/kautoValidator.js +256 -0
- package/dist/algo/orchestrator.d.ts +14 -1
- package/dist/algo/orchestrator.js +9 -2
- package/dist/algo/solidify.d.ts +11 -2
- package/dist/algo/solidify.js +37 -7
- package/dist/algo/ucb1.d.ts +53 -0
- package/dist/algo/ucb1.js +156 -0
- package/dist/assetrepair/hubRejection.d.ts +12 -0
- package/dist/assetrepair/hubRejection.js +109 -0
- package/dist/assetrepair/index.d.ts +2 -0
- package/dist/assetrepair/index.js +2 -0
- package/dist/assetrepair/repair.d.ts +33 -0
- package/dist/assetrepair/repair.js +155 -0
- package/dist/assetstore/assetSidecarRecords.js +5 -1
- package/dist/assetstore/assetStoreHealth.d.ts +2 -3
- package/dist/assetstore/assetStoreHealth.js +4 -10
- package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
- package/dist/assetstore/foreignJsonlSource.js +150 -0
- package/dist/assetstore/index.d.ts +3 -0
- package/dist/assetstore/index.js +3 -0
- package/dist/assetstore/learningHistory.js +3 -3
- package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
- package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
- package/dist/assetstore/localJsonl.d.ts +8 -0
- package/dist/assetstore/localJsonl.js +128 -3
- package/dist/assetstore/provenance.d.ts +38 -3
- package/dist/assetstore/provenance.js +103 -45
- package/dist/assetstore/provider.d.ts +4 -0
- package/dist/assetstore/reviewFilter.d.ts +19 -1
- package/dist/assetstore/reviewFilter.js +36 -0
- package/dist/assetstore/unionReadStore.d.ts +25 -0
- package/dist/assetstore/unionReadStore.js +119 -0
- 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/lifecycleBootstrap.d.ts +111 -0
- package/dist/bootstrap/lifecycleBootstrap.js +433 -0
- package/dist/bootstrap/v1EnvCompat.d.ts +3 -0
- package/dist/bootstrap/v1EnvCompat.js +48 -1
- 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 +2 -1
- package/dist/events/ingest.js +6 -0
- package/dist/exec/autoExec.d.ts +30 -3
- package/dist/exec/autoExec.js +294 -29
- package/dist/exec/autonomousCycle.d.ts +7 -0
- package/dist/exec/autonomousCycle.js +2 -0
- package/dist/exec/claudeBridge.d.ts +28 -6
- package/dist/exec/claudeBridge.js +411 -258
- package/dist/exec/executionBinding.d.ts +414 -0
- package/dist/exec/executionBinding.js +588 -0
- package/dist/exec/index.d.ts +1 -0
- package/dist/exec/index.js +1 -0
- package/dist/exec/proofOfWork.d.ts +1 -1
- package/dist/exec/proofOfWork.js +2 -2
- package/dist/exec/runnerRegistry.d.ts +5 -0
- package/dist/exec/runnerRegistry.js +54 -14
- package/dist/hub/capability.d.ts +40 -1
- package/dist/hub/conversationDistiller.d.ts +19 -0
- package/dist/hub/conversationDistiller.js +115 -37
- package/dist/hub/fake.d.ts +1 -0
- package/dist/hub/fake.js +1 -0
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/hub/recipeCompose.d.ts +27 -0
- package/dist/hub/recipeCompose.js +90 -0
- package/dist/hub/sanitize.js +119 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mailbox/ipcServer.d.ts +2 -0
- package/dist/mailbox/ipcServer.js +20 -0
- package/dist/mailbox/store.d.ts +52 -2
- package/dist/mailbox/store.js +546 -81
- package/dist/modelCompatibility.d.ts +164 -0
- package/dist/modelCompatibility.js +309 -0
- package/dist/observers/valueDigestObserver.d.ts +9 -0
- package/dist/observers/valueDigestObserver.js +35 -2
- package/dist/ops/cleanup.js +1 -1
- 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/ops/valueOutreach.d.ts +3 -1
- package/dist/ops/valueOutreach.js +5 -1
- package/dist/personality/schema.d.ts +18 -18
- 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/proofOfWork.d.ts +125 -6
- package/dist/schema/proofOfWork.js +102 -4
- package/dist/shadow/shadowHub.js +1 -0
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +148 -0
- package/dist/signals/index.d.ts +3 -1
- package/dist/signals/index.js +3 -1
- package/dist/signals/scopeVocabulary.d.ts +75 -0
- package/dist/signals/scopeVocabulary.js +91 -0
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +43 -0
- package/dist/trace/learningTrace.d.ts +7 -0
- package/dist/trace/learningTrace.js +22 -0
- package/dist/trace/trajectory.d.ts +8 -0
- package/dist/trace/trajectory.js +14 -2
- package/dist/util/fileLock.d.ts +92 -5
- package/dist/util/fileLock.js +809 -70
- package/dist/util/index.d.ts +2 -1
- package/dist/util/index.js +1 -1
- package/dist/verify/index.d.ts +2 -1
- package/dist/verify/index.js +1 -1
- package/dist/verify/sandboxRunner.d.ts +2 -0
- package/dist/verify/sandboxRunner.js +154 -10
- package/dist/verify/sandboxedValidation.d.ts +7 -2
- package/dist/verify/sandboxedValidation.js +102 -24
- package/dist/verify/validation.d.ts +24 -3
- package/dist/verify/validation.js +141 -24
- package/dist/wire/geneHints.d.ts +88 -16
- package/dist/wire/geneHints.js +124 -15
- package/dist/wire/index.d.ts +8 -3
- package/dist/wire/index.js +2 -2
- package/dist/wire/schemaGate.d.ts +21 -0
- package/dist/wire/schemaGate.js +119 -10
- package/package.json +5 -4
package/dist/algo/solidify.js
CHANGED
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
import { ulid as makeUlid } from 'ulid';
|
|
2
2
|
import { computeAssetId, SCHEMA_VERSION } from '../wire/index.js';
|
|
3
|
-
|
|
3
|
+
import { gitDiffOf, artifactHashOf, externalReceiptOf, toolCallTraceOf, proofOfWorkForWrite } from '../schema/proofOfWork.js';
|
|
4
|
+
/** ProofOfWork 是否表明有实际产出(解锁非 coding agent, 批注#17/#19). #961: 兼容读 helper(snake_case 优先, 回退旧 camelCase 存量). */
|
|
4
5
|
export function proofIndicatesOutput(p) {
|
|
5
6
|
if (!p)
|
|
6
7
|
return false;
|
|
7
8
|
switch (p.kind) {
|
|
8
|
-
case 'git_diff': return (p
|
|
9
|
-
case 'artifact_hash': return Boolean(p
|
|
10
|
-
case 'external_receipt':
|
|
11
|
-
|
|
9
|
+
case 'git_diff': return (gitDiffOf(p)?.files ?? 0) > 0 || (gitDiffOf(p)?.lines ?? 0) > 0;
|
|
10
|
+
case 'artifact_hash': return Boolean(artifactHashOf(p)?.sha256);
|
|
11
|
+
case 'external_receipt': {
|
|
12
|
+
const r = externalReceiptOf(p);
|
|
13
|
+
return Boolean(r?.receipt_id ?? r?.receiptId);
|
|
14
|
+
}
|
|
15
|
+
case 'tool_call_trace': return (toolCallTraceOf(p)?.calls ?? 0) > 0;
|
|
12
16
|
default: return false;
|
|
13
17
|
}
|
|
14
18
|
}
|
|
19
|
+
function nonEmpty(s) {
|
|
20
|
+
return typeof s === 'string' && s.trim().length > 0 ? s.trim() : undefined;
|
|
21
|
+
}
|
|
22
|
+
function failureIdentityTrace(input) {
|
|
23
|
+
if (input.outcome.status !== 'failed' || !input.failureIdentity)
|
|
24
|
+
return undefined;
|
|
25
|
+
const trace = { stage: 'validate' };
|
|
26
|
+
const root = nonEmpty(input.failureIdentity.rootAttemptId);
|
|
27
|
+
const execution = nonEmpty(input.failureIdentity.executionId);
|
|
28
|
+
const failure = nonEmpty(input.failureIdentity.failureId);
|
|
29
|
+
const verifier = nonEmpty(input.failureIdentity.verifierDigest);
|
|
30
|
+
const artifact = nonEmpty(input.failureIdentity.artifactDigest);
|
|
31
|
+
if (root)
|
|
32
|
+
trace['root_attempt_id'] = root;
|
|
33
|
+
if (execution)
|
|
34
|
+
trace['execution_id'] = execution;
|
|
35
|
+
if (failure)
|
|
36
|
+
trace['failure_id'] = failure;
|
|
37
|
+
if (verifier)
|
|
38
|
+
trace['verifier_digest'] = verifier;
|
|
39
|
+
if (artifact)
|
|
40
|
+
trace['artifact_digest'] = artifact;
|
|
41
|
+
return Object.keys(trace).length > 1 ? [trace] : undefined;
|
|
42
|
+
}
|
|
15
43
|
/**
|
|
16
44
|
* 两级 resolution(M4A-8) + ProofOfWork 判价值(M4A-5):
|
|
17
45
|
* - 失败: 分 < 阈 → regressed, 否则 inconclusive.
|
|
@@ -44,7 +72,8 @@ export function solidify(input) {
|
|
|
44
72
|
// (artifact_hash/external_receipt/tool_call_trace) gd is undefined and blast falls back to {0,0} — which here
|
|
45
73
|
// means "blast unknown", NOT "no change". Consumers that read blast=0 as a no-op signal (e.g. cycleFailure's
|
|
46
74
|
// local_gene_no_blast bucket) MUST first confirm the proof kind is git_diff; the cycleEngine guard does this.
|
|
47
|
-
const gd = input.proofOfWork?.kind === 'git_diff' ? input.proofOfWork
|
|
75
|
+
const gd = input.proofOfWork?.kind === 'git_diff' ? gitDiffOf(input.proofOfWork) : undefined;
|
|
76
|
+
const identityTrace = failureIdentityTrace(input);
|
|
48
77
|
const base = {
|
|
49
78
|
type: 'Capsule',
|
|
50
79
|
schema_version: SCHEMA_VERSION,
|
|
@@ -56,7 +85,8 @@ export function solidify(input) {
|
|
|
56
85
|
blast_radius: { files: gd?.files ?? 0, lines: gd?.lines ?? 0 },
|
|
57
86
|
outcome: input.outcome,
|
|
58
87
|
resolution_status: status,
|
|
59
|
-
...(input.proofOfWork ? { proof_of_work: input.proofOfWork } : {}),
|
|
88
|
+
...(input.proofOfWork ? { proof_of_work: proofOfWorkForWrite(input.proofOfWork) } : {}),
|
|
89
|
+
...(identityTrace ? { execution_trace: identityTrace } : {}),
|
|
60
90
|
};
|
|
61
91
|
const asset_id = computeAssetId(base);
|
|
62
92
|
return { capsule: { ...base, asset_id }, resolutionStatus: status, producedValue, reasons };
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RepairIssue } from './repair.js';
|
|
2
|
+
/** Field-level issues reported by the Hub, keyed to the asset index they belong to (`-1` = bundle-level). */
|
|
3
|
+
export interface HubRejectionReport {
|
|
4
|
+
issues: RepairIssue[];
|
|
5
|
+
/** Issues for one asset of the published bundle, by its position in `payload.assets`. */
|
|
6
|
+
byAssetIndex: Map<number, RepairIssue[]>;
|
|
7
|
+
}
|
|
8
|
+
export interface HubRejectionOptions {
|
|
9
|
+
/** The bundle's asset types in `payload.assets` order, so a `<type>_*` rule reason routes to its own asset. */
|
|
10
|
+
assetTypes?: readonly (string | undefined)[];
|
|
11
|
+
}
|
|
12
|
+
export declare function hubRejectionIssues(body: unknown, options?: HubRejectionOptions): HubRejectionReport;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const MAX_ISSUES = 50;
|
|
2
|
+
const MAX_MESSAGE_CHARS = 300;
|
|
3
|
+
const REASON_TOKEN = /^[a-z][a-z0-9_]*$/;
|
|
4
|
+
const REASON_PREFIXES = [
|
|
5
|
+
{ prefix: 'gene', assetType: 'Gene' },
|
|
6
|
+
{ prefix: 'capsule', assetType: 'Capsule' },
|
|
7
|
+
{ prefix: 'event', assetType: 'EvolutionEvent' },
|
|
8
|
+
{ prefix: 'bundle' },
|
|
9
|
+
];
|
|
10
|
+
const REPAIRABLE_REASON_PATHS = new Map([
|
|
11
|
+
['asset_id_verification_failed', 'asset_id'],
|
|
12
|
+
['missing_asset_id', 'asset_id'],
|
|
13
|
+
]);
|
|
14
|
+
export function hubRejectionIssues(body, options = {}) {
|
|
15
|
+
const fieldReport = fieldLevelIssues(body);
|
|
16
|
+
if (fieldReport.issues.length > 0)
|
|
17
|
+
return fieldReport;
|
|
18
|
+
return ruleReasonIssues(body, options.assetTypes ?? []);
|
|
19
|
+
}
|
|
20
|
+
function fieldLevelIssues(body) {
|
|
21
|
+
const report = { issues: [], byAssetIndex: new Map() };
|
|
22
|
+
for (const raw of detailRows(body).slice(0, MAX_ISSUES)) {
|
|
23
|
+
const detail = asRecord(raw);
|
|
24
|
+
if (!detail)
|
|
25
|
+
continue;
|
|
26
|
+
const path = pathSegments(detail['path']);
|
|
27
|
+
const message = text(detail['message']) ?? text(detail['code']);
|
|
28
|
+
if (!message)
|
|
29
|
+
continue;
|
|
30
|
+
const issue = {
|
|
31
|
+
path: assetRelativePath(path),
|
|
32
|
+
keyword: text(detail['code']) ?? 'invalid',
|
|
33
|
+
message: message.slice(0, MAX_MESSAGE_CHARS),
|
|
34
|
+
source: 'hub',
|
|
35
|
+
};
|
|
36
|
+
report.issues.push(issue);
|
|
37
|
+
const index = assetIndex(path);
|
|
38
|
+
const bucket = report.byAssetIndex.get(index);
|
|
39
|
+
if (bucket)
|
|
40
|
+
bucket.push(issue);
|
|
41
|
+
else
|
|
42
|
+
report.byAssetIndex.set(index, [issue]);
|
|
43
|
+
}
|
|
44
|
+
return report;
|
|
45
|
+
}
|
|
46
|
+
function ruleReasonIssues(body, assetTypes) {
|
|
47
|
+
const report = { issues: [], byAssetIndex: new Map() };
|
|
48
|
+
const reason = ruleReason(body);
|
|
49
|
+
if (!reason)
|
|
50
|
+
return report;
|
|
51
|
+
const prefix = REASON_PREFIXES.find((entry) => reason.token.startsWith(`${entry.prefix}_`));
|
|
52
|
+
if (!prefix)
|
|
53
|
+
return report;
|
|
54
|
+
const issue = {
|
|
55
|
+
path: REPAIRABLE_REASON_PATHS.get(reason.token.slice(prefix.prefix.length + 1)) ?? '',
|
|
56
|
+
keyword: reason.token,
|
|
57
|
+
message: reason.message.slice(0, MAX_MESSAGE_CHARS),
|
|
58
|
+
source: 'hub',
|
|
59
|
+
};
|
|
60
|
+
const index = prefix.assetType === undefined ? -1 : assetTypes.indexOf(prefix.assetType);
|
|
61
|
+
report.issues.push(issue);
|
|
62
|
+
report.byAssetIndex.set(index, [issue]);
|
|
63
|
+
return report;
|
|
64
|
+
}
|
|
65
|
+
function ruleReason(body) {
|
|
66
|
+
const root = asRecord(body);
|
|
67
|
+
if (!root)
|
|
68
|
+
return undefined;
|
|
69
|
+
const payload = asRecord(root['payload']);
|
|
70
|
+
const raw = [root['error'], root['reason'], payload?.['error'], payload?.['reason']]
|
|
71
|
+
.map((candidate) => text(candidate))
|
|
72
|
+
.find((candidate) => candidate !== undefined);
|
|
73
|
+
if (!raw)
|
|
74
|
+
return undefined;
|
|
75
|
+
const token = raw.split(":", 1)[0]?.trim() ?? '';
|
|
76
|
+
return REASON_TOKEN.test(token) ? { token, message: raw } : undefined;
|
|
77
|
+
}
|
|
78
|
+
function detailRows(body) {
|
|
79
|
+
const root = asRecord(body);
|
|
80
|
+
if (!root)
|
|
81
|
+
return [];
|
|
82
|
+
const payload = asRecord(root['payload']);
|
|
83
|
+
for (const candidate of [root['details'], root['errors'], payload?.['details'], payload?.['errors']]) {
|
|
84
|
+
if (Array.isArray(candidate))
|
|
85
|
+
return candidate;
|
|
86
|
+
}
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
function pathSegments(value) {
|
|
90
|
+
if (!Array.isArray(value))
|
|
91
|
+
return typeof value === 'string' ? value.split('.') : [];
|
|
92
|
+
return value.filter((segment) => typeof segment === 'string' || typeof segment === 'number');
|
|
93
|
+
}
|
|
94
|
+
/** `['payload','assets',0,'constraints','forbidden_paths']` → `constraints.forbidden_paths`. */
|
|
95
|
+
function assetRelativePath(path) {
|
|
96
|
+
const at = path.findIndex((segment) => typeof segment === 'number');
|
|
97
|
+
const tail = at >= 0 ? path.slice(at + 1) : path.filter((segment) => segment !== 'payload');
|
|
98
|
+
return tail.join('.');
|
|
99
|
+
}
|
|
100
|
+
function assetIndex(path) {
|
|
101
|
+
const found = path.find((segment) => typeof segment === 'number');
|
|
102
|
+
return typeof found === 'number' ? found : -1;
|
|
103
|
+
}
|
|
104
|
+
function text(value) {
|
|
105
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
106
|
+
}
|
|
107
|
+
function asRecord(value) {
|
|
108
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
109
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AssetRecord } from '../assetstore/index.js';
|
|
2
|
+
export type RepairStatus = 'already_valid' | 'repaired' | 'unrepairable';
|
|
3
|
+
export interface RepairIssue {
|
|
4
|
+
path: string;
|
|
5
|
+
keyword: string;
|
|
6
|
+
message: string;
|
|
7
|
+
/** `schema` = found by the local gep-sdk gate; `hub` = reported by the network in its rejection body. */
|
|
8
|
+
source: 'schema' | 'hub';
|
|
9
|
+
}
|
|
10
|
+
export interface RepairChange {
|
|
11
|
+
path: string;
|
|
12
|
+
action: 'added' | 'removed' | 'replaced';
|
|
13
|
+
note: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RepairReport {
|
|
16
|
+
status: RepairStatus;
|
|
17
|
+
/** The accepted-by-schema record. Absent only when blockers remain (`status: 'unrepairable'`). */
|
|
18
|
+
asset?: AssetRecord;
|
|
19
|
+
changes: RepairChange[];
|
|
20
|
+
blockers: RepairIssue[];
|
|
21
|
+
}
|
|
22
|
+
export interface RepairOptions {
|
|
23
|
+
/** Field-level issues the network itself reported — see `hubRejectionIssues`. Stricter than the GEP schema
|
|
24
|
+
* (the Hub adds envelope rules of its own), so they are folded in rather than re-derived locally. */
|
|
25
|
+
hubIssues?: readonly RepairIssue[];
|
|
26
|
+
}
|
|
27
|
+
type JsonRecord = Record<string, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Repair `input` into a record the GEP schema (and, when supplied, the Hub's own rules) accepts.
|
|
30
|
+
* Pure: `input` is never mutated, and nothing is written anywhere — the caller decides what to do with the result.
|
|
31
|
+
*/
|
|
32
|
+
export declare function repairAssetRecord(input: JsonRecord, options?: RepairOptions): RepairReport;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Asset repair — turn a GEP record the network refuses into one it accepts, WITHOUT inventing content.
|
|
2
|
+
//
|
|
3
|
+
// Two producers of refused records exist and both were wasting assets outright:
|
|
4
|
+
// * publish: the Hub rejects a bundle field-by-field (`details[]`), the operator gets a flat error, and the
|
|
5
|
+
// distilled asset is dropped on the floor.
|
|
6
|
+
// * reuse/fetch: the Hub delivers a lossy projection of a published record (dropped `schema_version`, dropped
|
|
7
|
+
// logical `id`, emptied `constraints.forbidden_paths`, extra non-schema keys such as `model_name`), which
|
|
8
|
+
// then fails the local wire gate and is discarded — reported to the operator as "not found".
|
|
9
|
+
//
|
|
10
|
+
// The line this module refuses to cross: a repair may only restore fields whose value is DERIVABLE (recomputed
|
|
11
|
+
// `asset_id`), CONSTANT (`schema_version`), or a strictly-narrowing default (`constraints`). Anything carrying
|
|
12
|
+
// an evidence or meaning claim — a Capsule's `outcome`/`confidence`/`blast_radius`, a Gene's `strategy`, a
|
|
13
|
+
// human summary — is never fabricated: it is reported as a blocker so the operator (or a re-distill) supplies it.
|
|
14
|
+
import { computeAssetId, wireSchemaIssues } from '../wire/index.js';
|
|
15
|
+
import { SCHEMA_VERSION } from '@evomap/gep-sdk';
|
|
16
|
+
const DEFAULT_MAX_FILES = 12;
|
|
17
|
+
const DEFAULT_FORBIDDEN_PATHS = ['.git', 'node_modules'];
|
|
18
|
+
const CONTENT_ASSET_ID = /^sha256:[0-9a-f]{64}$/;
|
|
19
|
+
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
20
|
+
/**
|
|
21
|
+
* Repair `input` into a record the GEP schema (and, when supplied, the Hub's own rules) accepts.
|
|
22
|
+
* Pure: `input` is never mutated, and nothing is written anywhere — the caller decides what to do with the result.
|
|
23
|
+
*/
|
|
24
|
+
export function repairAssetRecord(input, options = {}) {
|
|
25
|
+
const hubIssues = options.hubIssues ?? [];
|
|
26
|
+
const before = wireSchemaIssues(input);
|
|
27
|
+
if (before.length === 0 && hubIssues.length === 0 && assetIdIsCurrent(input)) {
|
|
28
|
+
return { status: 'already_valid', asset: input, changes: [], blockers: [] };
|
|
29
|
+
}
|
|
30
|
+
const changes = [];
|
|
31
|
+
const draft = { ...input };
|
|
32
|
+
restoreSchemaVersion(draft, changes);
|
|
33
|
+
dropUnknownProperties(draft, before, changes);
|
|
34
|
+
restoreGeneConstraints(draft, changes);
|
|
35
|
+
restoreLogicalId(draft, changes);
|
|
36
|
+
restoreAssetId(draft, changes);
|
|
37
|
+
const remaining = wireSchemaIssues(draft);
|
|
38
|
+
const blockers = [...remaining.map(schemaBlocker), ...unresolvedHubIssues(hubIssues, changes)];
|
|
39
|
+
if (blockers.length > 0)
|
|
40
|
+
return { status: 'unrepairable', changes, blockers };
|
|
41
|
+
return { status: 'repaired', asset: draft, changes, blockers: [] };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Hub rules the local schema cannot see (a minimum summary length, a bundle arity) stay blockers — but a hub
|
|
45
|
+
* complaint about a field this pass just mended is answered, and keeping it would report every repairable
|
|
46
|
+
* asset as unrepairable.
|
|
47
|
+
*/
|
|
48
|
+
function unresolvedHubIssues(issues, changes) {
|
|
49
|
+
return issues.filter((issue) => !changes.some((change) => pathsOverlap(change.path, issue.path)));
|
|
50
|
+
}
|
|
51
|
+
function pathsOverlap(repaired, reported) {
|
|
52
|
+
return repaired === reported
|
|
53
|
+
|| reported.startsWith(`${repaired}.`)
|
|
54
|
+
|| repaired.startsWith(`${reported}.`);
|
|
55
|
+
}
|
|
56
|
+
/** True when the record already declares the id its own content hashes to — the invariant `restoreAssetId` holds. */
|
|
57
|
+
function assetIdIsCurrent(record) {
|
|
58
|
+
const declared = record['asset_id'];
|
|
59
|
+
return typeof declared === 'string' && declared === safeComputeAssetId(record);
|
|
60
|
+
}
|
|
61
|
+
function restoreSchemaVersion(draft, changes) {
|
|
62
|
+
const current = draft['schema_version'];
|
|
63
|
+
if (typeof current === 'string' && SEMVER.test(current))
|
|
64
|
+
return;
|
|
65
|
+
draft['schema_version'] = SCHEMA_VERSION;
|
|
66
|
+
changes.push({
|
|
67
|
+
path: 'schema_version',
|
|
68
|
+
action: current === undefined ? 'added' : 'replaced',
|
|
69
|
+
note: `set to the SDK schema version ${SCHEMA_VERSION}`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// `additionalProperties: false` means an unknown key is fatal, so a delivery that adds `model_name` is
|
|
73
|
+
// unusable as-is. Dropping is the only content-preserving move: the key is not part of the GEP contract, so
|
|
74
|
+
// nothing that reads the record by contract can miss it. Every dropped key is named in the report.
|
|
75
|
+
function dropUnknownProperties(draft, issues, changes) {
|
|
76
|
+
for (const issue of issues) {
|
|
77
|
+
if (issue.keyword !== 'additionalProperties' || !issue.property)
|
|
78
|
+
continue;
|
|
79
|
+
if (!(issue.property in draft))
|
|
80
|
+
continue;
|
|
81
|
+
delete draft[issue.property];
|
|
82
|
+
changes.push({ path: issue.path, action: 'removed', note: 'not declared by the GEP schema' });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// A Gene's constraints are a CEILING on what an executing agent may touch. Restoring the default is therefore
|
|
86
|
+
// safe in one direction only: the default must be at least as restrictive as a missing/empty value, which
|
|
87
|
+
// (no ceiling at all) it always is.
|
|
88
|
+
function restoreGeneConstraints(draft, changes) {
|
|
89
|
+
if (draft['type'] !== 'Gene')
|
|
90
|
+
return;
|
|
91
|
+
const current = asRecord(draft['constraints']);
|
|
92
|
+
if (!current) {
|
|
93
|
+
draft['constraints'] = { max_files: DEFAULT_MAX_FILES, forbidden_paths: [...DEFAULT_FORBIDDEN_PATHS] };
|
|
94
|
+
changes.push({ path: 'constraints', action: 'added', note: 'restored the default execution ceiling' });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const repaired = { ...current };
|
|
98
|
+
const maxFiles = repaired['max_files'];
|
|
99
|
+
if (typeof maxFiles !== 'number' || !Number.isInteger(maxFiles) || maxFiles < 1) {
|
|
100
|
+
repaired['max_files'] = DEFAULT_MAX_FILES;
|
|
101
|
+
changes.push({ path: 'constraints.max_files', action: maxFiles === undefined ? 'added' : 'replaced', note: `restored the default ceiling ${DEFAULT_MAX_FILES}` });
|
|
102
|
+
}
|
|
103
|
+
const forbidden = repaired['forbidden_paths'];
|
|
104
|
+
const usable = Array.isArray(forbidden) && forbidden.length > 0 && forbidden.every((entry) => typeof entry === 'string');
|
|
105
|
+
if (!usable) {
|
|
106
|
+
repaired['forbidden_paths'] = [...DEFAULT_FORBIDDEN_PATHS];
|
|
107
|
+
changes.push({ path: 'constraints.forbidden_paths', action: forbidden === undefined ? 'added' : 'replaced', note: 'restored the default forbidden paths' });
|
|
108
|
+
}
|
|
109
|
+
draft['constraints'] = repaired;
|
|
110
|
+
}
|
|
111
|
+
// A logical id names the asset for humans and for Capsule→Gene binding; it carries no claim about the content.
|
|
112
|
+
// A delivery that dropped it cannot be re-bound to its original name (that name is not recoverable from the
|
|
113
|
+
// bytes), so the derived one is marked `repaired_` — visibly synthetic, and stable for the same content.
|
|
114
|
+
function restoreLogicalId(draft, changes) {
|
|
115
|
+
const current = draft['id'];
|
|
116
|
+
if (typeof current === 'string' && current.length > 0)
|
|
117
|
+
return;
|
|
118
|
+
const type = typeof draft['type'] === 'string' ? draft['type'].toLowerCase() : 'asset';
|
|
119
|
+
const digest = safeComputeAssetId(draft)?.slice('sha256:'.length, 'sha256:'.length + 12);
|
|
120
|
+
if (!digest)
|
|
121
|
+
return;
|
|
122
|
+
draft['id'] = `${type}_repaired_${digest}`;
|
|
123
|
+
changes.push({ path: 'id', action: 'added', note: 'derived a stable placeholder id from the content digest' });
|
|
124
|
+
}
|
|
125
|
+
// Content-addressing means `asset_id` is a FUNCTION of the rest of the record, so it is the one field that must
|
|
126
|
+
// be recomputed last — every repair above changes what the content hashes to.
|
|
127
|
+
function restoreAssetId(draft, changes) {
|
|
128
|
+
const declared = draft['asset_id'];
|
|
129
|
+
const computed = safeComputeAssetId(draft);
|
|
130
|
+
if (!computed || !CONTENT_ASSET_ID.test(computed))
|
|
131
|
+
return;
|
|
132
|
+
if (declared === computed)
|
|
133
|
+
return;
|
|
134
|
+
draft['asset_id'] = computed;
|
|
135
|
+
changes.push({
|
|
136
|
+
path: 'asset_id',
|
|
137
|
+
action: typeof declared === 'string' ? 'replaced' : 'added',
|
|
138
|
+
note: 'recomputed the content id over the repaired record',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function safeComputeAssetId(record) {
|
|
142
|
+
try {
|
|
143
|
+
const id = computeAssetId(record);
|
|
144
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function schemaBlocker(issue) {
|
|
151
|
+
return { path: issue.path, keyword: issue.keyword, message: issue.message, source: 'schema' };
|
|
152
|
+
}
|
|
153
|
+
function asRecord(value) {
|
|
154
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
155
|
+
}
|
|
@@ -118,7 +118,7 @@ export function parseAssetSyncRecord(value) {
|
|
|
118
118
|
|| !remoteAssetId) {
|
|
119
119
|
return null;
|
|
120
120
|
}
|
|
121
|
-
const logicalId =
|
|
121
|
+
const logicalId = rawStringField(record, 'logicalId');
|
|
122
122
|
const status = stringField(record, 'status');
|
|
123
123
|
const runKey = optionalStrictString(record, 'runKey');
|
|
124
124
|
const inventoryKey = optionalStrictString(record, 'inventoryKey');
|
|
@@ -359,6 +359,10 @@ function stringField(value, key) {
|
|
|
359
359
|
const raw = value[key];
|
|
360
360
|
return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
|
|
361
361
|
}
|
|
362
|
+
function rawStringField(value, key) {
|
|
363
|
+
const raw = value[key];
|
|
364
|
+
return typeof raw === 'string' && raw.length > 0 ? raw : undefined;
|
|
365
|
+
}
|
|
362
366
|
function optionalStrictString(value, key) {
|
|
363
367
|
const raw = value[key];
|
|
364
368
|
if (raw === undefined)
|
|
@@ -16,9 +16,8 @@ export interface AssetStoreFileHealth {
|
|
|
16
16
|
corruptRows: number;
|
|
17
17
|
hashMismatchRows: number;
|
|
18
18
|
/**
|
|
19
|
-
* Rows that fail content-hash verification but
|
|
20
|
-
*
|
|
21
|
-
* so they are a benign category, counted separately and never degrading the store.
|
|
19
|
+
* Rows that fail content-hash verification but carry an active, content-bound unverified provenance waiver.
|
|
20
|
+
* They remain untrusted and are counted separately instead of being misclassified as store corruption.
|
|
22
21
|
*/
|
|
23
22
|
unverifiedRows: number;
|
|
24
23
|
schemaInvalidRows: number;
|