@evomap/evolver-core 2.0.0-beta.17 → 2.0.0-beta.19
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.js +21 -2
- package/dist/algo/cycleEngine.d.ts +12 -0
- package/dist/algo/cycleEngine.js +36 -4
- package/dist/algo/geneHealth.d.ts +2 -2
- package/dist/algo/geneHealth.js +5 -4
- package/dist/algo/geneSelection.d.ts +1 -1
- package/dist/algo/orchestrator.js +9 -2
- package/dist/assetstore/assetSidecarRecords.js +4 -0
- package/dist/assetstore/assetStoreHealth.js +41 -24
- package/dist/assetstore/assetStoreStorage.d.ts +1 -1
- package/dist/assetstore/assetStoreStorage.js +16 -7
- package/dist/assetstore/localJsonl.d.ts +2 -1
- package/dist/assetstore/localJsonl.js +54 -10
- package/dist/assetstore/provenance.d.ts +24 -0
- package/dist/assetstore/provenance.js +219 -12
- package/dist/assetstore/provider.d.ts +20 -1
- package/dist/assetstore/provider.js +34 -1
- package/dist/bootstrap/index.d.ts +2 -1
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/v1EnvCompat.d.ts +110 -0
- package/dist/bootstrap/v1EnvCompat.js +256 -0
- package/dist/events/public.d.ts +1 -1
- package/dist/events/public.js +1 -1
- package/dist/events/reports.d.ts +2 -0
- package/dist/events/reports.js +4 -0
- package/dist/exec/autoExec.d.ts +18 -1
- package/dist/exec/autoExec.js +24 -9
- package/dist/exec/autonomousCycle.d.ts +19 -4
- package/dist/exec/autonomousCycle.js +63 -13
- package/dist/exec/claudeBridge.d.ts +25 -7
- package/dist/exec/claudeBridge.js +264 -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/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 +11 -1
- package/dist/hub/fake.d.ts +2 -2
- package/dist/hub/fake.js +1 -1
- 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 +38 -2
- package/dist/mailbox/store.js +416 -27
- package/dist/signals/curriculum.d.ts +55 -0
- package/dist/signals/curriculum.js +202 -0
- package/dist/signals/expand.js +17 -6
- package/dist/signals/index.d.ts +2 -1
- package/dist/signals/index.js +2 -1
- 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 +2 -1
- package/dist/trace/index.js +2 -1
- package/dist/trace/proxyTurns.d.ts +31 -0
- package/dist/trace/proxyTurns.js +137 -0
- package/dist/verify/validation.d.ts +11 -1
- package/dist/verify/validation.js +31 -0
- package/package.json +4 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { aggregateLearningHistory } from '../assetstore/learningHistory.js';
|
|
2
2
|
import { reuseSentiment } from '../ops/reuseOutcomes.js';
|
|
3
|
-
import { tagOverlapScore } from '../signals/expand.js';
|
|
3
|
+
import { expandSignals, geneTags, tagOverlapScore, } from '../signals/expand.js';
|
|
4
4
|
import { bannedGenesFromFailures } from './bans.js';
|
|
5
5
|
import { geneGenerationSource } from './geneIntake.js';
|
|
6
6
|
function asStrings(v) {
|
|
@@ -68,6 +68,24 @@ async function computeBans(store, signals, limit) {
|
|
|
68
68
|
}));
|
|
69
69
|
return bannedGenesFromFailures(failures, signals);
|
|
70
70
|
}
|
|
71
|
+
const GENERIC_NAMESPACE_TAGS = new Set(['action', 'area', 'problem', 'risk', 'signal']);
|
|
72
|
+
// Generic namespace tags and inferred action/signal subtypes are too broad to admit a candidate by themselves.
|
|
73
|
+
function hasSelectionAdmissionEvidence(liveSignals, gene) {
|
|
74
|
+
const tags = new Set(geneTags(gene));
|
|
75
|
+
const triggerTags = new Set(expandSignals(gene.signalsMatch ?? []));
|
|
76
|
+
const categoryActionTag = gene.category ? `action:${gene.category.toLowerCase()}` : undefined;
|
|
77
|
+
if (liveSignals.some((signal) => ((triggerTags.has(signal) || signal === categoryActionTag)
|
|
78
|
+
&& !GENERIC_NAMESPACE_TAGS.has(signal)))) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const liveTags = expandSignals(liveSignals);
|
|
82
|
+
if (liveTags.some((tag) => (tags.has(tag)
|
|
83
|
+
&& (tag.startsWith('problem:')
|
|
84
|
+
|| tag.startsWith('area:')
|
|
85
|
+
|| tag.startsWith('risk:')))))
|
|
86
|
+
return true;
|
|
87
|
+
return liveTags.some((tag) => tag.startsWith('signal:') && triggerTags.has(tag));
|
|
88
|
+
}
|
|
71
89
|
/**
|
|
72
90
|
* Assemble the full selection pool from the store for the given signals: the relevant scored candidates AND the
|
|
73
91
|
* distilled-gene fallback pool, in a single pass over the gene list (one store.list + one ban computation). A gene
|
|
@@ -106,7 +124,8 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
|
|
|
106
124
|
const category = typeof g['category'] === 'string' ? String(g['category']) : undefined;
|
|
107
125
|
const summary = typeof g['summary'] === 'string' ? String(g['summary']) : undefined;
|
|
108
126
|
const literal = signalsMatch.some((m) => sigSet.has(m));
|
|
109
|
-
const
|
|
127
|
+
const tagInput = { signalsMatch, geneId, category, summary };
|
|
128
|
+
const relevant = literal || hasSelectionAdmissionEvidence(signals, tagInput);
|
|
110
129
|
if (!relevant) {
|
|
111
130
|
// #97: a trusted, approved, non-banned distilled (or evolved) gene that doesn't match the live signals is not
|
|
112
131
|
// a normal candidate, but it IS eligible as a last-resort fallback (selection uses it only when nothing clears
|
|
@@ -66,6 +66,8 @@ export interface CycleEngineDeps {
|
|
|
66
66
|
rng?: () => number;
|
|
67
67
|
/** Injected environment fingerprint (deterministic tests). Defaults to capturing the real runtime env. */
|
|
68
68
|
envFingerprint?: () => EnvFingerprint;
|
|
69
|
+
/** Latest external capability gaps. Composition owns persistence/wire details; core reads one bounded snapshot. */
|
|
70
|
+
capabilityGaps?: () => readonly string[];
|
|
69
71
|
/** 触发评估(常注入 TriggerEngine.evaluate 的包装; 缺省=直接触发). */
|
|
70
72
|
trigger?: (p: ProblemPattern, now: number) => Promise<TriggerEval> | TriggerEval;
|
|
71
73
|
/** 可进化人格(可选). 注入后, 每轮:
|
|
@@ -82,6 +84,8 @@ export interface CycleInput {
|
|
|
82
84
|
cycleId: string;
|
|
83
85
|
problem: ProblemPattern;
|
|
84
86
|
signals: readonly string[];
|
|
87
|
+
/** Curriculum targets already prepared before candidate assembly. Omit for direct runCycle callers. */
|
|
88
|
+
curriculumSignals?: readonly string[];
|
|
85
89
|
category: GepCategory;
|
|
86
90
|
/** Optional explicit strategy preset name; when set, it wins over history-derived meta-signal auto-detection. */
|
|
87
91
|
strategyName?: string;
|
|
@@ -150,5 +154,13 @@ export interface CycleResult {
|
|
|
150
154
|
export declare class CycleEngine {
|
|
151
155
|
private readonly deps;
|
|
152
156
|
constructor(deps: CycleEngineDeps);
|
|
157
|
+
/**
|
|
158
|
+
* Add V1-compatible curriculum targets from V2's replayable event history. Public so the orchestrator can run
|
|
159
|
+
* it before candidate assembly; runCycle calls it again for direct callers. Set merging makes that idempotent.
|
|
160
|
+
*/
|
|
161
|
+
prepareCurriculumSignals(signals: readonly string[]): {
|
|
162
|
+
signals: string[];
|
|
163
|
+
curriculumSignals: string[];
|
|
164
|
+
};
|
|
153
165
|
runCycle(input: CycleInput): Promise<CycleResult>;
|
|
154
166
|
}
|
package/dist/algo/cycleEngine.js
CHANGED
|
@@ -12,6 +12,7 @@ import { solidify } from './solidify.js';
|
|
|
12
12
|
import { buildEvolutionEvent } from './evolutionEvent.js';
|
|
13
13
|
import { cycleRecordsFromEvents } from '../signals/cycleHistoryFromEvents.js';
|
|
14
14
|
import { computeMetaSignals, deriveCycleHistory } from '../signals/metaSignals.js';
|
|
15
|
+
import { capabilityGapsFromSignals, curriculumOutcomesFromEvents, generateCurriculumSignals, normalizeCapabilityGaps, } from '../signals/curriculum.js';
|
|
15
16
|
import { resolveStrategy } from './strategyPresets.js';
|
|
16
17
|
import { classifyCycleFailure, } from './cycleFailureClassifier.js';
|
|
17
18
|
import { isDistilledGeneId } from './geneIntake.js';
|
|
@@ -164,14 +165,44 @@ export class CycleEngine {
|
|
|
164
165
|
constructor(deps) {
|
|
165
166
|
this.deps = deps;
|
|
166
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Add V1-compatible curriculum targets from V2's replayable event history. Public so the orchestrator can run
|
|
170
|
+
* it before candidate assembly; runCycle calls it again for direct callers. Set merging makes that idempotent.
|
|
171
|
+
*/
|
|
172
|
+
prepareCurriculumSignals(signals) {
|
|
173
|
+
let externalCapabilityGaps = [];
|
|
174
|
+
try {
|
|
175
|
+
externalCapabilityGaps = normalizeCapabilityGaps(this.deps.capabilityGaps?.() ?? []);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// An optional lifecycle-state reader must never suppress local curriculum generation.
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const localCapabilityGaps = capabilityGapsFromSignals(signals);
|
|
182
|
+
const curriculumSignals = generateCurriculumSignals({
|
|
183
|
+
outcomes: curriculumOutcomesFromEvents(this.deps.ingestor.readAll()),
|
|
184
|
+
// Preserve existing explicit local-signal precedence; Hub gaps fill the otherwise-missing V1 source.
|
|
185
|
+
capabilityGaps: normalizeCapabilityGaps([...localCapabilityGaps, ...externalCapabilityGaps]),
|
|
186
|
+
});
|
|
187
|
+
return { signals: mergeSignals(signals, curriculumSignals), curriculumSignals };
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return { signals: [...signals], curriculumSignals: [] };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
167
193
|
async runCycle(input) {
|
|
168
194
|
const { ingestor, store } = this.deps;
|
|
169
195
|
const now = this.deps.now();
|
|
170
196
|
const envKey = envFingerprintKey((this.deps.envFingerprint ?? captureEnvFingerprint)());
|
|
171
197
|
const reasons = [];
|
|
172
|
-
const baseSignals = input.signals;
|
|
198
|
+
const baseSignals = [...input.signals];
|
|
199
|
+
const preparedSignals = input.curriculumSignals === undefined
|
|
200
|
+
? this.prepareCurriculumSignals(input.signals)
|
|
201
|
+
: { signals: mergeSignals(input.signals, input.curriculumSignals), curriculumSignals: [...input.curriculumSignals] };
|
|
202
|
+
const selectionSignals = preparedSignals.signals;
|
|
203
|
+
const curriculumSignals = preparedSignals.curriculumSignals;
|
|
173
204
|
const metaSignals = historyMetaSignals(ingestor, 100);
|
|
174
|
-
const cycleSignals = mergeSignals(
|
|
205
|
+
const cycleSignals = mergeSignals(selectionSignals, metaSignals);
|
|
175
206
|
// PORT v1 #279 issue-reporter: classify cycle.failed payloads when the caller supplies host context. The
|
|
176
207
|
// post-solidify failed path adds V2-local context (current gene + blast radius) so hard-filtered local no-op
|
|
177
208
|
// genes are reachable without re-emitting the old ban_gene soft signal.
|
|
@@ -221,6 +252,7 @@ export class CycleEngine {
|
|
|
221
252
|
cycleId: input.cycleId,
|
|
222
253
|
signals: cycleSignals,
|
|
223
254
|
baseSignals,
|
|
255
|
+
...(curriculumSignals.length > 0 ? { curriculumSignals } : {}),
|
|
224
256
|
...(metaSignals.length > 0 ? { metaSignals, strategy: strategy.name } : {}),
|
|
225
257
|
},
|
|
226
258
|
});
|
|
@@ -250,7 +282,7 @@ export class CycleEngine {
|
|
|
250
282
|
// recentEvents 由事件日志的近期 outcome 派生, 让"失败连击触发变异"在环内真正生效.
|
|
251
283
|
const sel = await applySelectForRun(personalityDeps, {
|
|
252
284
|
driftEnabled: plateau.active,
|
|
253
|
-
signals:
|
|
285
|
+
signals: selectionSignals,
|
|
254
286
|
recentEvents: recentOutcomes.map((status) => ({ outcome: { status } })),
|
|
255
287
|
});
|
|
256
288
|
personalityForRun = sel.state;
|
|
@@ -301,7 +333,7 @@ export class CycleEngine {
|
|
|
301
333
|
const epi = epigeneticPenaltyForIds(candidateIds(c), envKey, geneOutcomes);
|
|
302
334
|
return epi > 0 ? { ...c, epigeneticPenalty: epi } : c;
|
|
303
335
|
});
|
|
304
|
-
const decision = (await this.deps.selection.run({ signals:
|
|
336
|
+
const decision = (await this.deps.selection.run({ signals: selectionSignals, candidates, floor: input.selectionFloor, ...(input.forcedGeneId !== undefined ? { forcedGeneId: input.forcedGeneId } : {}), ...(exploration ? { exploration } : {}), ...(distilledFallback.length > 0 ? { distilledFallback } : {}), ...(input.antiWarnings && input.antiWarnings.length > 0 ? { antiWarnings: input.antiWarnings } : {}), ...(input.memoryEvidence && input.memoryEvidence.length > 0 ? { memoryEvidence: input.memoryEvidence } : {}) }, { now, cycleId: input.cycleId, ...(this.deps.rng ? { rng: this.deps.rng } : {}) }));
|
|
305
337
|
await ingestor.ingest({
|
|
306
338
|
type: 'decision.gene_selected',
|
|
307
339
|
human: { title: `选 gene ${decision.selectedGeneId ?? '(innovate)'}`, why: decision.candidates.map((c) => `${c.geneId}:${c.score.toFixed(3)}`).join(', ') || '无候选→innovate' },
|
|
@@ -12,10 +12,10 @@ export interface GeneHealthWeights {
|
|
|
12
12
|
reuse: number;
|
|
13
13
|
antiPattern: number;
|
|
14
14
|
}
|
|
15
|
-
export declare const HEALTH_WEIGHTS_VERSION = "gh-
|
|
15
|
+
export declare const HEALTH_WEIGHTS_VERSION = "gh-2";
|
|
16
16
|
export declare const DEFAULT_HEALTH_WEIGHTS: GeneHealthWeights;
|
|
17
17
|
/**
|
|
18
|
-
* gene 健康分 = w1
|
|
18
|
+
* gene 健康分 = successRate·(w1 + w2·reuse归一) − w3·antiPattern密度.
|
|
19
19
|
* 输入 = M3-6 聚合视图(不内联 learning_history) + anti_patterns 数 + 复用计数.
|
|
20
20
|
*/
|
|
21
21
|
export declare function geneHealthScore(view: GeneLearningView, opts?: {
|
package/dist/algo/geneHealth.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
export const HEALTH_WEIGHTS_VERSION = 'gh-
|
|
2
|
-
export const DEFAULT_HEALTH_WEIGHTS = { successRate: 0.6, reuse: 0.
|
|
1
|
+
export const HEALTH_WEIGHTS_VERSION = 'gh-2';
|
|
2
|
+
export const DEFAULT_HEALTH_WEIGHTS = { successRate: 0.6, reuse: 0.1, antiPattern: 0.4 };
|
|
3
3
|
/** reuseCount 归一(对数压缩, 复用越多分越高但边际递减). */
|
|
4
4
|
function reuseScore(count) {
|
|
5
5
|
return count <= 0 ? 0 : Math.min(1, Math.log10(count + 1) / 2); // 100 次≈封顶
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* gene 健康分 = w1
|
|
8
|
+
* gene 健康分 = successRate·(w1 + w2·reuse归一) − w3·antiPattern密度.
|
|
9
9
|
* 输入 = M3-6 聚合视图(不内联 learning_history) + anti_patterns 数 + 复用计数.
|
|
10
10
|
*/
|
|
11
11
|
export function geneHealthScore(view, opts = {}, w = DEFAULT_HEALTH_WEIGHTS) {
|
|
12
12
|
const reuseCount = opts.reuseCount ?? view.total;
|
|
13
13
|
const antiPatternPenalty = Math.min(1, (opts.antiPatternCount ?? 0) / 5); // 5+ anti-pattern 封顶惩罚
|
|
14
|
-
const
|
|
14
|
+
const confidenceAdjustedSuccess = view.successRate * (w.successRate + w.reuse * reuseScore(reuseCount));
|
|
15
|
+
const score = confidenceAdjustedSuccess - w.antiPattern * antiPatternPenalty;
|
|
15
16
|
return { geneId: view.geneId, successRate: view.successRate, reuseCount, antiPatternPenalty, score };
|
|
16
17
|
}
|
|
@@ -134,7 +134,7 @@ export declare const MEMORY_GRAPH_WEIGHT = 0.12;
|
|
|
134
134
|
* + CONFIDENCE_WEIGHT × confidence + REUSE_WEIGHT × reuse-sentiment). Bumped whenever a factor is added so golden
|
|
135
135
|
* weight snapshots track the change. Composed from the health-weights version so a change to either layer shows.
|
|
136
136
|
*/
|
|
137
|
-
export declare const SELECTION_WEIGHTS_VERSION = "sel-4(gh-
|
|
137
|
+
export declare const SELECTION_WEIGHTS_VERSION = "sel-4(gh-2,conf=0.15,memory=0.12,reuse=0.1)";
|
|
138
138
|
/** 实现1: engine 健康分主导(health 0.6 + 信号匹配 0.4). */
|
|
139
139
|
export declare const engineHealthSelection: Strategy<SelectionInput, GeneDecision>;
|
|
140
140
|
/** 实现2: 纯信号匹配采样(忽略 health, 对照基线 — 经验主义要可对比). */
|
|
@@ -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.
|
|
@@ -38,7 +44,7 @@ export async function runEvolutionCycle(engine, store, opts) {
|
|
|
38
44
|
...(opts.hubCandidates && opts.hubCandidates.length > 0 ? { hubCandidates: opts.hubCandidates } : {}),
|
|
39
45
|
...(reuseCounts.size > 0 ? { reuseCounts } : {}),
|
|
40
46
|
};
|
|
41
|
-
const { candidates: assembledCandidates, distilledFallback, antiWarnings } = await assembleSelectionPool(store,
|
|
47
|
+
const { candidates: assembledCandidates, distilledFallback, antiWarnings } = await assembleSelectionPool(store, selectionSignals, asmOpts);
|
|
42
48
|
const memoryByGene = new Map((opts.memoryGraphAdvice?.genes ?? []).map((evidence) => [evidence.geneId, evidence]));
|
|
43
49
|
const candidates = assembledCandidates.map((candidate) => {
|
|
44
50
|
const evidence = memoryByGene.get(candidate.geneId) ?? (candidate.assetId ? memoryByGene.get(candidate.assetId) : undefined);
|
|
@@ -53,7 +59,8 @@ export async function runEvolutionCycle(engine, store, opts) {
|
|
|
53
59
|
return engine.runCycle({
|
|
54
60
|
cycleId: opts.cycleId,
|
|
55
61
|
problem: opts.problem,
|
|
56
|
-
signals,
|
|
62
|
+
signals: baseSignals,
|
|
63
|
+
curriculumSignals: preparedCurriculum.curriculumSignals,
|
|
57
64
|
category: opts.category,
|
|
58
65
|
...(opts.strategyName !== undefined ? { strategyName: opts.strategyName } : {}),
|
|
59
66
|
candidates,
|
|
@@ -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) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { lstatSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { acquireLock, releaseLock } from '../util/fileLock.js';
|
|
4
|
-
import { validateWire, verifyAssetId } from '../wire/index.js';
|
|
4
|
+
import { computeAssetId, validateWire, verifyAssetId } from '../wire/index.js';
|
|
5
5
|
import { LOCAL_ASSET_FILES } from './assetStoreLayout.js';
|
|
6
6
|
import { assertOptionalRegularFile, isReliableAssetStoreLockRelease, readUtf8Regular, UnsafeAssetStorePathError, } from './assetStoreStorage.js';
|
|
7
7
|
import { parseAssetSyncSidecarRecord, parseProvenanceRecord, parseReviewRecord, parseSidecarJsonl, } from './assetSidecarRecords.js';
|
|
@@ -107,7 +107,7 @@ function summarize(files, sidecars) {
|
|
|
107
107
|
sidecars: [...sidecars],
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
-
function inspectFile(baseDir, kind, file, maxFileBytes,
|
|
110
|
+
function inspectFile(baseDir, kind, file, maxFileBytes, unverifiedContentIds) {
|
|
111
111
|
try {
|
|
112
112
|
const path = join(baseDir, file);
|
|
113
113
|
const stat = assertOptionalRegularFile(path);
|
|
@@ -116,7 +116,7 @@ function inspectFile(baseDir, kind, file, maxFileBytes, unverifiedIds) {
|
|
|
116
116
|
if (stat.size > maxFileBytes) {
|
|
117
117
|
return { ...emptyFile(kind, file, 'unavailable', 'scan_limit_exceeded'), bytes: stat.size };
|
|
118
118
|
}
|
|
119
|
-
const raw = readUtf8Regular(path);
|
|
119
|
+
const raw = readUtf8Regular(path, maxFileBytes);
|
|
120
120
|
if (raw === null)
|
|
121
121
|
return emptyFile(kind, file, 'unavailable', 'read_unavailable');
|
|
122
122
|
const rows = raw.split('\n').filter((line) => line.trim().length > 0);
|
|
@@ -144,21 +144,22 @@ function inspectFile(baseDir, kind, file, maxFileBytes, unverifiedIds) {
|
|
|
144
144
|
duplicateRows += 1;
|
|
145
145
|
else
|
|
146
146
|
seen.add(assetId);
|
|
147
|
-
|
|
147
|
+
const hashMatches = verifyAssetId(record);
|
|
148
|
+
if (!hashMatches) {
|
|
148
149
|
// A hash mismatch is corruption UNLESS provenance says this id is an unverified hub reuse: the hub
|
|
149
150
|
// rewrote the delivered bytes and reuse froze them untrusted on purpose (#570). That is expected, not
|
|
150
151
|
// store rot, so it lands in the benign unverifiedRows bucket and never degrades the store.
|
|
151
|
-
if (
|
|
152
|
+
if (unverifiedContentIds.get(assetId) === computeAssetId(record))
|
|
152
153
|
unverifiedRows += 1;
|
|
153
154
|
else
|
|
154
155
|
hashMismatchRows += 1;
|
|
155
|
-
continue;
|
|
156
156
|
}
|
|
157
|
-
|
|
157
|
+
const schemaValid = kind === 'AntiGene' || validateWire(record).ok;
|
|
158
|
+
if (!schemaValid) {
|
|
158
159
|
schemaInvalidRows += 1;
|
|
159
|
-
continue;
|
|
160
160
|
}
|
|
161
|
-
|
|
161
|
+
if (hashMatches && schemaValid)
|
|
162
|
+
validRows += 1;
|
|
162
163
|
}
|
|
163
164
|
catch {
|
|
164
165
|
corruptRows += 1;
|
|
@@ -195,20 +196,36 @@ function inspectFile(baseDir, kind, file, maxFileBytes, unverifiedIds) {
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
/**
|
|
198
|
-
* Asset ids
|
|
199
|
+
* Asset ids whose latest provenance decision marks an unverified hub reuse (evolver-v2#570).
|
|
199
200
|
* Read directly from `<baseDir>/provenance.jsonl` — NOT via ProvenanceStore — because inspectLocalAssetStore
|
|
200
201
|
* already holds the shared `.assetstore.lock`, and ProvenanceStore would try to re-acquire it. A missing or
|
|
201
|
-
* unreadable sidecar yields an empty
|
|
202
|
+
* unreadable sidecar yields an empty map: without provenance every mismatch stays classified as corruption.
|
|
202
203
|
*/
|
|
203
|
-
function
|
|
204
|
-
|
|
204
|
+
function readUnverifiedReuseContentIds(baseDir, maxFileBytes) {
|
|
205
|
+
let raw;
|
|
206
|
+
try {
|
|
207
|
+
raw = readUtf8Regular(join(baseDir, 'provenance.jsonl'), maxFileBytes);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return new Map();
|
|
211
|
+
}
|
|
205
212
|
if (raw === null)
|
|
206
|
-
return new
|
|
207
|
-
const ids = new
|
|
213
|
+
return new Map();
|
|
214
|
+
const ids = new Map();
|
|
208
215
|
for (const record of parseSidecarJsonl(raw, parseProvenanceRecord).records) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
216
|
+
const frozenContentId = record.frozenContentId;
|
|
217
|
+
const supportedReason = (record.source === 'hub'
|
|
218
|
+
&& (record.reason === 'unverified_hub_rewrite' || record.reason === 'unverified_hub_synthesized')) || (record.source === 'migrated' && record.reason === 'unverified_gepx_import');
|
|
219
|
+
const qualifies = supportedReason
|
|
220
|
+
&& record.trusted === false
|
|
221
|
+
&& record.decision === undefined
|
|
222
|
+
&& record.decidedBy === undefined
|
|
223
|
+
&& record.promotedBy === undefined
|
|
224
|
+
&& frozenContentId !== undefined;
|
|
225
|
+
if (qualifies)
|
|
226
|
+
ids.set(record.assetId, frozenContentId);
|
|
227
|
+
else
|
|
228
|
+
ids.delete(record.assetId);
|
|
212
229
|
}
|
|
213
230
|
return ids;
|
|
214
231
|
}
|
|
@@ -222,7 +239,7 @@ function inspectSidecar(baseDir, definition, maxFileBytes) {
|
|
|
222
239
|
if (stat.size > maxFileBytes) {
|
|
223
240
|
return { ...emptySidecar(kind, file, 'unavailable', 'scan_limit_exceeded'), bytes: stat.size };
|
|
224
241
|
}
|
|
225
|
-
const raw = readUtf8Regular(path);
|
|
242
|
+
const raw = readUtf8Regular(path, maxFileBytes);
|
|
226
243
|
if (raw === null)
|
|
227
244
|
return emptySidecar(kind, file, 'unavailable', 'read_unavailable');
|
|
228
245
|
const parsed = parseSidecarJsonl(raw, parseRecord);
|
|
@@ -283,14 +300,14 @@ export function inspectLocalAssetStore(baseDir, opts = {}, deps = {}) {
|
|
|
283
300
|
catch {
|
|
284
301
|
return summarize(allFiles('unavailable', 'lock_unavailable'), allSidecars('unavailable', 'lock_unavailable'));
|
|
285
302
|
}
|
|
286
|
-
const maxFileBytes = healthScanLimit(opts.maxFileBytes);
|
|
287
|
-
// Read the unverified-reuse set once, under the lock we already hold, so every asset file classifies its
|
|
288
|
-
// hash mismatches consistently against the same provenance snapshot.
|
|
289
|
-
const unverifiedIds = readUnverifiedReuseIds(baseDir);
|
|
290
303
|
let report;
|
|
291
304
|
try {
|
|
305
|
+
const maxFileBytes = healthScanLimit(opts.maxFileBytes);
|
|
306
|
+
// Read the unverified-reuse set once, under the lock we already hold, so every asset file classifies its
|
|
307
|
+
// hash mismatches consistently against the same bounded provenance snapshot.
|
|
308
|
+
const unverifiedContentIds = readUnverifiedReuseContentIds(baseDir, maxFileBytes);
|
|
292
309
|
report = summarize(Object.entries(LOCAL_ASSET_FILES)
|
|
293
|
-
.map(([kind, file]) => inspectFile(baseDir, kind, file, maxFileBytes,
|
|
310
|
+
.map(([kind, file]) => inspectFile(baseDir, kind, file, maxFileBytes, unverifiedContentIds)), LOCAL_ASSET_SIDECARS.map((definition) => inspectSidecar(baseDir, definition, maxFileBytes)));
|
|
294
311
|
}
|
|
295
312
|
catch {
|
|
296
313
|
report = summarize(allFiles('unavailable', 'read_unavailable'), allSidecars('unavailable', 'read_unavailable'));
|
|
@@ -33,7 +33,7 @@ export declare function withAssetStoreLock<T>(lockPath: string, operation: () =>
|
|
|
33
33
|
export declare function assertOptionalRegularFile(path: string, role?: AssetStorePathRole): Stats | null;
|
|
34
34
|
export declare function regularFileFingerprint(path: string): string;
|
|
35
35
|
export declare function readRegularBuffer(path: string, maxBytes?: number): Buffer | null;
|
|
36
|
-
export declare function readUtf8Regular(path: string): string | null;
|
|
36
|
+
export declare function readUtf8Regular(path: string, maxBytes?: number): string | null;
|
|
37
37
|
export declare function createBufferDurableExclusive(path: string, value: Buffer, opts?: DurableWriteOptions): void;
|
|
38
38
|
export declare function fsyncDirectoryBestEffort(path: string): void;
|
|
39
39
|
export declare function appendUtf8Durable(path: string, value: string, opts?: DurableWriteOptions): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { closeSync, constants, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdirSync, openSync,
|
|
2
|
+
import { closeSync, constants, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdirSync, openSync, readSync, renameSync, unlinkSync, writeSync, } from 'node:fs';
|
|
3
3
|
import { basename, dirname, join } from 'node:path';
|
|
4
4
|
import { acquireLock, LockReleaseError, releaseLock, } from '../util/fileLock.js';
|
|
5
5
|
export class UnsafeAssetStorePathError extends Error {
|
|
@@ -152,17 +152,26 @@ export function readRegularBuffer(path, maxBytes = Number.MAX_SAFE_INTEGER) {
|
|
|
152
152
|
assertOpenedPathMatches(fd, path, 'asset_file');
|
|
153
153
|
if (fstatSync(fd).size > maxBytes)
|
|
154
154
|
throw new AssetStoreReadLimitError();
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
const chunks = [];
|
|
156
|
+
let total = 0;
|
|
157
|
+
while (total <= maxBytes) {
|
|
158
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes - total + 1));
|
|
159
|
+
const bytesRead = readSync(fd, chunk, 0, chunk.byteLength, null);
|
|
160
|
+
if (bytesRead === 0)
|
|
161
|
+
break;
|
|
162
|
+
total += bytesRead;
|
|
163
|
+
if (total > maxBytes)
|
|
164
|
+
throw new AssetStoreReadLimitError();
|
|
165
|
+
chunks.push(bytesRead === chunk.byteLength ? chunk : chunk.subarray(0, bytesRead));
|
|
166
|
+
}
|
|
167
|
+
return Buffer.concat(chunks, total);
|
|
159
168
|
}
|
|
160
169
|
finally {
|
|
161
170
|
closeSync(fd);
|
|
162
171
|
}
|
|
163
172
|
}
|
|
164
|
-
export function readUtf8Regular(path) {
|
|
165
|
-
return readRegularBuffer(path)?.toString('utf8') ?? null;
|
|
173
|
+
export function readUtf8Regular(path, maxBytes = Number.MAX_SAFE_INTEGER) {
|
|
174
|
+
return readRegularBuffer(path, maxBytes)?.toString('utf8') ?? null;
|
|
166
175
|
}
|
|
167
176
|
function writeAll(fd, value) {
|
|
168
177
|
const bytes = typeof value === 'string' ? Buffer.from(value, 'utf8') : value;
|
|
@@ -25,8 +25,9 @@ export declare class LocalJsonlProvider implements AssetStoreProvider {
|
|
|
25
25
|
* 仅 v1→v2 导入用(硬化 A6 存量冻结); 普通写一律走 put(). record 必须自带 asset_id.
|
|
26
26
|
*/
|
|
27
27
|
putFrozen(record: AssetRecord): Promise<PutResult>;
|
|
28
|
+
putFrozenConditional(record: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
|
|
28
29
|
get(assetId: string): Promise<AssetRecord | null>;
|
|
29
|
-
findByLogicalId(id: string, limit?: number): Promise<AssetRecord[]>;
|
|
30
|
+
findByLogicalId(id: string, limit?: number, kind?: AssetKind): Promise<AssetRecord[]>;
|
|
30
31
|
list(kind?: AssetKind, limit?: number): Promise<AssetRecord[]>;
|
|
31
32
|
search(q: SearchQuery): Promise<AssetRecord[]>;
|
|
32
33
|
/**
|
|
@@ -2,7 +2,12 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { acquireLock, releaseLock } from '../util/fileLock.js';
|
|
3
3
|
import { appendUtf8Durable, assertAssetStoreDirectory, assertOptionalRegularFile, ensureAssetStoreDirectory, readUtf8Regular, regularFileFingerprint, replaceUtf8Durable, } from './assetStoreStorage.js';
|
|
4
4
|
import { LOCAL_ASSET_FILES } from './assetStoreLayout.js';
|
|
5
|
-
import { normalizeForPut, } from './provider.js';
|
|
5
|
+
import { FrozenAssetIdCollisionError, frozenAssetRecordsEqual, normalizeForPut, } from './provider.js';
|
|
6
|
+
function resultLogicalId(logicalId) {
|
|
7
|
+
return logicalId !== undefined && logicalId.length > 0 && logicalId === logicalId.trim()
|
|
8
|
+
? logicalId
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
6
11
|
function signalsOf(a) {
|
|
7
12
|
const out = [];
|
|
8
13
|
for (const key of ['signals_match', 'signals', 'trigger', 'trigger_signals']) {
|
|
@@ -109,13 +114,16 @@ export class LocalJsonlProvider {
|
|
|
109
114
|
try {
|
|
110
115
|
// Refresh under the shared lock so another process cannot append between reload and dedupe.
|
|
111
116
|
this.refreshUnderLock();
|
|
112
|
-
|
|
117
|
+
const existing = this.index.get(record.asset_id);
|
|
118
|
+
if (existing) {
|
|
119
|
+
if (!frozenAssetRecordsEqual(existing, record))
|
|
120
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
113
121
|
return {
|
|
114
122
|
asset_id: record.asset_id,
|
|
115
123
|
stored: false,
|
|
116
124
|
verified,
|
|
117
125
|
status: 'already_exists',
|
|
118
|
-
logicalId,
|
|
126
|
+
...(resultLogicalId(logicalId) ? { logicalId } : {}),
|
|
119
127
|
};
|
|
120
128
|
}
|
|
121
129
|
collision = logicalId === undefined
|
|
@@ -129,7 +137,7 @@ export class LocalJsonlProvider {
|
|
|
129
137
|
stored: false,
|
|
130
138
|
verified,
|
|
131
139
|
status: 'logical_collision',
|
|
132
|
-
logicalId,
|
|
140
|
+
...(resultLogicalId(logicalId) ? { logicalId } : {}),
|
|
133
141
|
collisionWithAssetId: collision.asset_id,
|
|
134
142
|
};
|
|
135
143
|
}
|
|
@@ -146,7 +154,7 @@ export class LocalJsonlProvider {
|
|
|
146
154
|
verified,
|
|
147
155
|
status: 'stored',
|
|
148
156
|
...(collision ? {
|
|
149
|
-
logicalId,
|
|
157
|
+
...(resultLogicalId(logicalId) ? { logicalId } : {}),
|
|
150
158
|
collisionWithAssetId: collision.asset_id,
|
|
151
159
|
} : {}),
|
|
152
160
|
};
|
|
@@ -156,15 +164,42 @@ export class LocalJsonlProvider {
|
|
|
156
164
|
* 仅 v1→v2 导入用(硬化 A6 存量冻结); 普通写一律走 put(). record 必须自带 asset_id.
|
|
157
165
|
*/
|
|
158
166
|
async putFrozen(record) {
|
|
167
|
+
return this.putFrozenConditional(record, { allowLogicalCollision: true });
|
|
168
|
+
}
|
|
169
|
+
async putFrozenConditional(record, options = {}) {
|
|
159
170
|
if (!record.asset_id)
|
|
160
171
|
throw new Error('putFrozen 需 record 自带冻结 asset_id');
|
|
161
172
|
const file = join(this.baseDir, LOCAL_ASSET_FILES[record.type]);
|
|
173
|
+
let logicalId;
|
|
174
|
+
let collision;
|
|
162
175
|
assertOptionalRegularFile(this.lockPath, 'lock_file');
|
|
163
176
|
acquireLock(this.lockPath);
|
|
164
177
|
try {
|
|
165
178
|
this.refreshUnderLock();
|
|
166
|
-
|
|
167
|
-
|
|
179
|
+
const existing = this.index.get(record.asset_id);
|
|
180
|
+
if (existing) {
|
|
181
|
+
if (!frozenAssetRecordsEqual(existing, record))
|
|
182
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
183
|
+
return { asset_id: record.asset_id, stored: false, verified: false, status: 'already_exists' };
|
|
184
|
+
}
|
|
185
|
+
logicalId = typeof record['id'] === 'string' && record['id'].length > 0
|
|
186
|
+
? record['id']
|
|
187
|
+
: undefined;
|
|
188
|
+
collision = logicalId === undefined
|
|
189
|
+
? undefined
|
|
190
|
+
: [...this.index.values()].find((candidate) => (candidate.type === record.type
|
|
191
|
+
&& candidate['id'] === logicalId
|
|
192
|
+
&& candidate.asset_id !== record.asset_id));
|
|
193
|
+
if (collision && !options.allowLogicalCollision) {
|
|
194
|
+
return {
|
|
195
|
+
asset_id: record.asset_id,
|
|
196
|
+
stored: false,
|
|
197
|
+
verified: false,
|
|
198
|
+
status: 'logical_collision',
|
|
199
|
+
...(resultLogicalId(logicalId) ? { logicalId } : {}),
|
|
200
|
+
collisionWithAssetId: collision.asset_id,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
168
203
|
appendUtf8Durable(file, `${JSON.stringify(record)}\n`);
|
|
169
204
|
this.index.set(record.asset_id, record);
|
|
170
205
|
this.updateFileStateAfterWrite();
|
|
@@ -172,18 +207,27 @@ export class LocalJsonlProvider {
|
|
|
172
207
|
finally {
|
|
173
208
|
releaseLock(this.lockPath);
|
|
174
209
|
}
|
|
175
|
-
return {
|
|
210
|
+
return {
|
|
211
|
+
asset_id: record.asset_id,
|
|
212
|
+
stored: true,
|
|
213
|
+
verified: false,
|
|
214
|
+
status: 'stored',
|
|
215
|
+
...(collision ? {
|
|
216
|
+
...(resultLogicalId(logicalId) ? { logicalId } : {}),
|
|
217
|
+
collisionWithAssetId: collision.asset_id,
|
|
218
|
+
} : {}),
|
|
219
|
+
};
|
|
176
220
|
}
|
|
177
221
|
async get(assetId) {
|
|
178
222
|
this.ensureFresh();
|
|
179
223
|
return this.index.get(assetId) ?? null;
|
|
180
224
|
}
|
|
181
|
-
async findByLogicalId(id, limit = 2) {
|
|
225
|
+
async findByLogicalId(id, limit = 2, kind) {
|
|
182
226
|
this.ensureFresh();
|
|
183
227
|
const boundedLimit = Number.isFinite(limit) ? Math.max(1, Math.min(1_000, Math.floor(limit))) : 2;
|
|
184
228
|
const out = [];
|
|
185
229
|
for (const record of this.index.values()) {
|
|
186
|
-
if (record['id'] !== id)
|
|
230
|
+
if (record['id'] !== id || (kind !== undefined && record.type !== kind))
|
|
187
231
|
continue;
|
|
188
232
|
out.push(record);
|
|
189
233
|
if (out.length >= boundedLimit)
|
|
@@ -11,11 +11,18 @@ export interface ProvenanceRecord {
|
|
|
11
11
|
/** Legacy promotion actor field kept for existing sidecar readers. */
|
|
12
12
|
promotedBy?: string;
|
|
13
13
|
reason?: string;
|
|
14
|
+
/** Canonical content id of the exact hash-mismatched body accepted by an unverified Hub ingest. */
|
|
15
|
+
frozenContentId?: string;
|
|
14
16
|
}
|
|
15
17
|
export interface ProvenanceTrustChange {
|
|
16
18
|
changed: boolean;
|
|
17
19
|
record: ProvenanceRecord;
|
|
18
20
|
}
|
|
21
|
+
export declare class ProvenanceWritePendingError extends Error {
|
|
22
|
+
readonly assetId: string;
|
|
23
|
+
readonly code = "PROVENANCE_WRITE_PENDING";
|
|
24
|
+
constructor(assetId: string);
|
|
25
|
+
}
|
|
19
26
|
/**
|
|
20
27
|
* Append-only JSONL sidecar (last-write-wins) at <baseDir>/provenance.jsonl. Default for an asset with NO
|
|
21
28
|
* record = trusted: the only local writers (cycleEngine self-produce, v1 migration) are trusted and never
|
|
@@ -36,6 +43,21 @@ export declare class ProvenanceStore {
|
|
|
36
43
|
mark(rec: Omit<ProvenanceRecord, 'at'> & {
|
|
37
44
|
at?: string;
|
|
38
45
|
}): ProvenanceRecord;
|
|
46
|
+
/** Stage a verified Hub write without replacing an in-flight conservative marker. */
|
|
47
|
+
stageUntrustedWriteTracked(assetId: string, source: ProvenanceSource): {
|
|
48
|
+
record: ProvenanceRecord;
|
|
49
|
+
appended: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** Finalize a verified write unless an operator made an explicit decision during I/O. */
|
|
52
|
+
finalizeUntrustedWrite(assetId: string, source: ProvenanceSource): ProvenanceRecord;
|
|
53
|
+
/** Stage an unverified write without overwriting an explicit trust decision or another conservative marker. */
|
|
54
|
+
stageUnverifiedWrite(assetId: string, source: ProvenanceSource, frozenContentId: string): ProvenanceRecord;
|
|
55
|
+
stageUnverifiedWriteTracked(assetId: string, source: ProvenanceSource, frozenContentId: string): {
|
|
56
|
+
record: ProvenanceRecord;
|
|
57
|
+
appended: boolean;
|
|
58
|
+
};
|
|
59
|
+
/** Atomically replace only a pending/no decision with the health-waiver reason after verified persistence. */
|
|
60
|
+
finalizeUnverifiedWrite(assetId: string, source: ProvenanceSource, reason: string, frozenContentId: string): ProvenanceRecord;
|
|
39
61
|
rollbackLast(rec: ProvenanceRecord): void;
|
|
40
62
|
get(assetId: string): ProvenanceRecord | null;
|
|
41
63
|
/** No record → trusted (local default); a record → its trusted flag. */
|
|
@@ -67,6 +89,8 @@ export declare function ingestUntrusted(store: AssetStoreProvider, prov: Provena
|
|
|
67
89
|
* audited promotion. `reason` records WHY verification was waived (e.g. hub rewrite vs synthesized payload).
|
|
68
90
|
*/
|
|
69
91
|
export declare function ingestUnverified(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, reason: string, source?: ProvenanceSource): Promise<PutResult>;
|
|
92
|
+
/** Atomic frozen variant used by reuse so the logical-id check and append share one provider lock. */
|
|
93
|
+
export declare function ingestUnverifiedConditional(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, reason: string, options?: ConditionalPutOptions, source?: ProvenanceSource): Promise<ConditionalPutResult>;
|
|
70
94
|
/**
|
|
71
95
|
* Conditional variant used by Hub sync to reject a logical-id collision without ever allowing a Hub record
|
|
72
96
|
* to become implicitly trusted. Providers that cannot make the condition atomically are rejected here.
|