@evomap/evolver-core 2.0.0-beta.8 → 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/conversationSniffer.js +20 -7
- 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 +91 -11
- 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/hub/sanitize.js +3 -2
- 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.d.ts +64 -0
- package/dist/strategy/constraintAblation.js +2820 -0
- package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
- package/dist/strategy/constraintAblationPredicates.js +339 -0
- package/dist/strategy/index.d.ts +2 -1
- package/dist/strategy/index.js +2 -1
- 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
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type AssetStoreProvider, type AssetRecord, type PutResult } from './provider.js';
|
|
1
|
+
import { type AssetStoreProvider, type AssetRecord, type ConditionalPutOptions, type ConditionalPutResult, type PutResult } from './provider.js';
|
|
2
2
|
export type ProvenanceSource = 'local' | 'migrated' | 'hub';
|
|
3
3
|
export type ProvenanceDecision = 'promoted' | 'revoked';
|
|
4
|
+
export declare const UNVERIFIED_V1_IMPORT_REASON = "unverified_v1_import";
|
|
4
5
|
export interface ProvenanceRecord {
|
|
5
6
|
assetId: string;
|
|
6
7
|
source: ProvenanceSource;
|
|
@@ -11,15 +12,56 @@ export interface ProvenanceRecord {
|
|
|
11
12
|
/** Legacy promotion actor field kept for existing sidecar readers. */
|
|
12
13
|
promotedBy?: string;
|
|
13
14
|
reason?: string;
|
|
15
|
+
/** Canonical content id of the exact hash-mismatched body accepted by a supported unverified ingest. */
|
|
16
|
+
frozenContentId?: string;
|
|
14
17
|
}
|
|
15
18
|
export interface ProvenanceTrustChange {
|
|
16
19
|
changed: boolean;
|
|
17
20
|
record: ProvenanceRecord;
|
|
18
21
|
}
|
|
22
|
+
export type UnverifiedIngestDisposition = {
|
|
23
|
+
readonly status: 'create';
|
|
24
|
+
readonly assetId: string;
|
|
25
|
+
readonly frozenContentId: string;
|
|
26
|
+
readonly provenanceAction: 'stage_and_finalize';
|
|
27
|
+
} | {
|
|
28
|
+
readonly status: 'already_bound';
|
|
29
|
+
readonly assetId: string;
|
|
30
|
+
readonly frozenContentId: string;
|
|
31
|
+
readonly provenanceAction: 'finalize' | 'none';
|
|
32
|
+
} | {
|
|
33
|
+
readonly status: 'preserve_trust';
|
|
34
|
+
readonly assetId: string;
|
|
35
|
+
readonly frozenContentId: string;
|
|
36
|
+
readonly provenanceAction: 'none';
|
|
37
|
+
} | {
|
|
38
|
+
readonly status: 'collision';
|
|
39
|
+
readonly assetId: string;
|
|
40
|
+
readonly frozenContentId: string;
|
|
41
|
+
readonly collision: 'asset' | 'provenance';
|
|
42
|
+
readonly provenanceAction: 'none';
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the exact no-I/O disposition used by {@link ingestUnverified}. Callers may inspect a read-only
|
|
46
|
+
* target snapshot during migration planning, then apply through ingestUnverified without duplicating its
|
|
47
|
+
* trust and collision rules.
|
|
48
|
+
*/
|
|
49
|
+
export declare function planUnverifiedIngest(record: AssetRecord, existing: AssetRecord | null, currentProvenance: ProvenanceRecord | null, reason: string, source?: ProvenanceSource): UnverifiedIngestDisposition;
|
|
50
|
+
/**
|
|
51
|
+
* A frozen hash mismatch is benign only while its latest provenance record is an undecided, content-bound
|
|
52
|
+
* waiver created by a supported ingest path. Keep the storage/health classification centralized; downstream
|
|
53
|
+
* reuse and sync apply their own stricter allowlists before an unverified asset may leave quarantine.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isActiveUnverifiedProvenance(record: ProvenanceRecord | null | undefined, frozenContentId: string | null | undefined): boolean;
|
|
56
|
+
export declare class ProvenanceWritePendingError extends Error {
|
|
57
|
+
readonly assetId: string;
|
|
58
|
+
readonly code = "PROVENANCE_WRITE_PENDING";
|
|
59
|
+
constructor(assetId: string);
|
|
60
|
+
}
|
|
19
61
|
/**
|
|
20
62
|
* Append-only JSONL sidecar (last-write-wins) at <baseDir>/provenance.jsonl. Default for an asset with NO
|
|
21
|
-
* record = trusted:
|
|
22
|
-
* write here
|
|
63
|
+
* record = trusted: verified local writers (cycleEngine self-produce and self-consistent v1 migration rows)
|
|
64
|
+
* do not write here. Every untrusted or hash-mismatched ingest path MUST mark before persistence.
|
|
23
65
|
*/
|
|
24
66
|
export declare class ProvenanceStore {
|
|
25
67
|
private readonly now;
|
|
@@ -36,6 +78,21 @@ export declare class ProvenanceStore {
|
|
|
36
78
|
mark(rec: Omit<ProvenanceRecord, 'at'> & {
|
|
37
79
|
at?: string;
|
|
38
80
|
}): ProvenanceRecord;
|
|
81
|
+
/** Stage a verified Hub write without replacing an in-flight conservative marker. */
|
|
82
|
+
stageUntrustedWriteTracked(assetId: string, source: ProvenanceSource): {
|
|
83
|
+
record: ProvenanceRecord;
|
|
84
|
+
appended: boolean;
|
|
85
|
+
};
|
|
86
|
+
/** Finalize a verified write unless an operator made an explicit decision during I/O. */
|
|
87
|
+
finalizeUntrustedWrite(assetId: string, source: ProvenanceSource): ProvenanceRecord;
|
|
88
|
+
/** Stage an unverified write without overwriting an explicit trust decision or another conservative marker. */
|
|
89
|
+
stageUnverifiedWrite(assetId: string, source: ProvenanceSource, frozenContentId: string): ProvenanceRecord;
|
|
90
|
+
stageUnverifiedWriteTracked(assetId: string, source: ProvenanceSource, frozenContentId: string): {
|
|
91
|
+
record: ProvenanceRecord;
|
|
92
|
+
appended: boolean;
|
|
93
|
+
};
|
|
94
|
+
/** Atomically replace only a pending/no decision with the health-waiver reason after verified persistence. */
|
|
95
|
+
finalizeUnverifiedWrite(assetId: string, source: ProvenanceSource, reason: string, frozenContentId: string): ProvenanceRecord;
|
|
39
96
|
rollbackLast(rec: ProvenanceRecord): void;
|
|
40
97
|
get(assetId: string): ProvenanceRecord | null;
|
|
41
98
|
/** No record → trusted (local default); a record → its trusted flag. */
|
|
@@ -54,4 +111,23 @@ export declare class ProvenanceStore {
|
|
|
54
111
|
* remote-supplied asset_id is never trusted) and mark it untrusted in the sidecar. This is the ONLY path that
|
|
55
112
|
* should bring hub-fetched assets into the local pool — trust-first from the first byte (#30.1).
|
|
56
113
|
*/
|
|
57
|
-
export declare function ingestUntrusted(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, source?: ProvenanceSource): Promise<PutResult>;
|
|
114
|
+
export declare function ingestUntrusted(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, source?: ProvenanceSource): Promise<PutResult>;
|
|
115
|
+
/**
|
|
116
|
+
* Hub → local-pool landing for an asset whose content does NOT hash to its declared asset_id. The hub
|
|
117
|
+
* demonstrably rewrites delivered payloads (injected `validation`, wholesale `payload_backfill_reason`
|
|
118
|
+
* synthesis — evolver-v2#570), which breaks {@link ingestUntrusted}'s normalizeForPut self-consistency check
|
|
119
|
+
* even though the loop's own in-run reuse already consumes hub content without re-verifying it (adapter
|
|
120
|
+
* `hubReuse.ts`). Rather than hard-reject a save the operator explicitly asked for, freeze the asset under its
|
|
121
|
+
* declared (network) asset_id via `putFrozen` and mark it untrusted in the sidecar with an explicit reason.
|
|
122
|
+
* Trust-first still holds (#30.1): selection defaults to trusted-only, so an unverified asset never silently
|
|
123
|
+
* enters the reasoning pool — it lands where the operator put it and stays flagged until an explicit,
|
|
124
|
+
* audited promotion. `reason` records WHY verification was waived (e.g. hub rewrite vs synthesized payload).
|
|
125
|
+
*/
|
|
126
|
+
export declare function ingestUnverified(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, reason: string, source?: ProvenanceSource): Promise<PutResult>;
|
|
127
|
+
/** Atomic frozen variant used by reuse so the logical-id check and append share one provider lock. */
|
|
128
|
+
export declare function ingestUnverifiedConditional(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, reason: string, options?: ConditionalPutOptions, source?: ProvenanceSource): Promise<ConditionalPutResult>;
|
|
129
|
+
/**
|
|
130
|
+
* Conditional variant used by Hub sync to reject a logical-id collision without ever allowing a Hub record
|
|
131
|
+
* to become implicitly trusted. Providers that cannot make the condition atomically are rejected here.
|
|
132
|
+
*/
|
|
133
|
+
export declare function ingestUntrustedConditional(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, options?: ConditionalPutOptions, source?: ProvenanceSource): Promise<ConditionalPutResult>;
|
|
@@ -4,16 +4,118 @@
|
|
|
4
4
|
// must not enter the content hash (#30.2), or it would break content-addressing. Trust-first by construction:
|
|
5
5
|
// selection defaults to trusted-only; an untrusted asset is promoted to trusted only by an explicit, logged act.
|
|
6
6
|
import { join, dirname } from 'node:path';
|
|
7
|
-
import { normalizeForPut } from './provider.js';
|
|
7
|
+
import { assertCapsuleGeneBinding, FrozenAssetIdCollisionError, InvalidFrozenPutResultError, frozenAssetRecordsEqual, normalizeForPut, supportsAtomicConditionalPut, supportsAtomicFrozenConditionalPut, validateConditionalPutResult, validateFrozenPutResult, } from './provider.js';
|
|
8
8
|
import { appendUtf8Durable, assertAssetStoreDirectory, ensureAssetStoreDirectory, readUtf8Regular, regularFileFingerprint, truncateUtf8SuffixDurable, withAssetStoreLock, } from './assetStoreStorage.js';
|
|
9
9
|
import { assertTrustSidecarHealthy, parseProvenanceRecord, parseSidecarJsonl, } from './assetSidecarRecords.js';
|
|
10
|
+
import { computeAssetId } from '../wire/index.js';
|
|
11
|
+
export const UNVERIFIED_V1_IMPORT_REASON = 'unverified_v1_import';
|
|
12
|
+
const UNVERIFIED_WRITE_PENDING_REASON = 'unverified_hub_write_pending';
|
|
13
|
+
function unverifiedStageAction(current, source, frozenContentId) {
|
|
14
|
+
if (!current)
|
|
15
|
+
return 'append';
|
|
16
|
+
const isUndecidedUntrusted = current.trusted === false
|
|
17
|
+
&& current.decision === undefined
|
|
18
|
+
&& current.decidedBy === undefined
|
|
19
|
+
&& current.promotedBy === undefined;
|
|
20
|
+
if (isActiveUnverifiedProvenance(current, frozenContentId))
|
|
21
|
+
return 'append';
|
|
22
|
+
if (isUndecidedUntrusted && current.source === source && current.reason === undefined)
|
|
23
|
+
return 'append';
|
|
24
|
+
if (isUndecidedUntrusted
|
|
25
|
+
&& current.source === source
|
|
26
|
+
&& current.reason === UNVERIFIED_WRITE_PENDING_REASON
|
|
27
|
+
&& current.frozenContentId === frozenContentId)
|
|
28
|
+
return 'reuse_pending';
|
|
29
|
+
return 'blocked';
|
|
30
|
+
}
|
|
31
|
+
function unverifiedFinalizeAction(current, source, reason, frozenContentId) {
|
|
32
|
+
if (!current)
|
|
33
|
+
return 'append';
|
|
34
|
+
if (current.trusted === true
|
|
35
|
+
|| current.decision !== undefined
|
|
36
|
+
|| current.decidedBy !== undefined
|
|
37
|
+
|| current.promotedBy !== undefined)
|
|
38
|
+
return 'preserve';
|
|
39
|
+
const sameWaiver = current.source === source && current.trusted === false && current.reason === reason;
|
|
40
|
+
if (sameWaiver && current.frozenContentId === frozenContentId)
|
|
41
|
+
return 'already_bound';
|
|
42
|
+
if (!sameWaiver && (current.reason !== UNVERIFIED_WRITE_PENDING_REASON
|
|
43
|
+
|| (current.frozenContentId !== undefined && current.frozenContentId !== frozenContentId)))
|
|
44
|
+
return 'preserve';
|
|
45
|
+
return 'append';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the exact no-I/O disposition used by {@link ingestUnverified}. Callers may inspect a read-only
|
|
49
|
+
* target snapshot during migration planning, then apply through ingestUnverified without duplicating its
|
|
50
|
+
* trust and collision rules.
|
|
51
|
+
*/
|
|
52
|
+
export function planUnverifiedIngest(record, existing, currentProvenance, reason, source = 'hub') {
|
|
53
|
+
assertCapsuleGeneBinding(record);
|
|
54
|
+
const frozenContentId = computeAssetId(record);
|
|
55
|
+
if (!frozenContentId)
|
|
56
|
+
throw new Error('failed to compute frozen content id');
|
|
57
|
+
const base = { assetId: record.asset_id, frozenContentId };
|
|
58
|
+
if (currentProvenance && currentProvenance.assetId !== record.asset_id) {
|
|
59
|
+
return { ...base, status: 'collision', collision: 'provenance', provenanceAction: 'none' };
|
|
60
|
+
}
|
|
61
|
+
if (existing) {
|
|
62
|
+
if (!frozenAssetRecordsEqual(existing, record)) {
|
|
63
|
+
return { ...base, status: 'collision', collision: 'asset', provenanceAction: 'none' };
|
|
64
|
+
}
|
|
65
|
+
const finalize = unverifiedFinalizeAction(currentProvenance, source, reason, frozenContentId);
|
|
66
|
+
if (finalize === 'preserve') {
|
|
67
|
+
return { ...base, status: 'preserve_trust', provenanceAction: 'none' };
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
...base,
|
|
71
|
+
status: 'already_bound',
|
|
72
|
+
provenanceAction: finalize === 'append' ? 'finalize' : 'none',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (unverifiedStageAction(currentProvenance, source, frozenContentId) === 'blocked') {
|
|
76
|
+
return { ...base, status: 'collision', collision: 'provenance', provenanceAction: 'none' };
|
|
77
|
+
}
|
|
78
|
+
return { ...base, status: 'create', provenanceAction: 'stage_and_finalize' };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A frozen hash mismatch is benign only while its latest provenance record is an undecided, content-bound
|
|
82
|
+
* waiver created by a supported ingest path. Keep the storage/health classification centralized; downstream
|
|
83
|
+
* reuse and sync apply their own stricter allowlists before an unverified asset may leave quarantine.
|
|
84
|
+
*/
|
|
85
|
+
export function isActiveUnverifiedProvenance(record, frozenContentId) {
|
|
86
|
+
if (!record
|
|
87
|
+
|| !frozenContentId
|
|
88
|
+
|| record.trusted !== false
|
|
89
|
+
|| record.decision !== undefined
|
|
90
|
+
|| record.decidedBy !== undefined
|
|
91
|
+
|| record.promotedBy !== undefined
|
|
92
|
+
|| record.frozenContentId !== frozenContentId)
|
|
93
|
+
return false;
|
|
94
|
+
// V1 input is local legacy data rather than a network-issued identifier. Never let a malformed declared
|
|
95
|
+
// identity become healthy merely because the shallow wire gate does not enforce the schema's asset_id regex.
|
|
96
|
+
if (record.reason === UNVERIFIED_V1_IMPORT_REASON
|
|
97
|
+
&& !/^sha256:[0-9a-f]{64}$/.test(record.assetId))
|
|
98
|
+
return false;
|
|
99
|
+
return (record.source === 'hub'
|
|
100
|
+
&& (record.reason === 'unverified_hub_rewrite' || record.reason === 'unverified_hub_synthesized')) || (record.source === 'migrated'
|
|
101
|
+
&& (record.reason === 'unverified_gepx_import' || record.reason === UNVERIFIED_V1_IMPORT_REASON));
|
|
102
|
+
}
|
|
103
|
+
export class ProvenanceWritePendingError extends Error {
|
|
104
|
+
assetId;
|
|
105
|
+
code = 'PROVENANCE_WRITE_PENDING';
|
|
106
|
+
constructor(assetId) {
|
|
107
|
+
super('asset provenance write is pending');
|
|
108
|
+
this.assetId = assetId;
|
|
109
|
+
this.name = 'ProvenanceWritePendingError';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
10
112
|
function immutableRecord(record) {
|
|
11
113
|
return Object.freeze({ ...record });
|
|
12
114
|
}
|
|
13
115
|
/**
|
|
14
116
|
* Append-only JSONL sidecar (last-write-wins) at <baseDir>/provenance.jsonl. Default for an asset with NO
|
|
15
|
-
* record = trusted:
|
|
16
|
-
* write here
|
|
117
|
+
* record = trusted: verified local writers (cycleEngine self-produce and self-consistent v1 migration rows)
|
|
118
|
+
* do not write here. Every untrusted or hash-mismatched ingest path MUST mark before persistence.
|
|
17
119
|
*/
|
|
18
120
|
export class ProvenanceStore {
|
|
19
121
|
now;
|
|
@@ -69,6 +171,89 @@ export class ProvenanceStore {
|
|
|
69
171
|
return this.appendUnderLock(full);
|
|
70
172
|
});
|
|
71
173
|
}
|
|
174
|
+
/** Stage a verified Hub write without replacing an in-flight conservative marker. */
|
|
175
|
+
stageUntrustedWriteTracked(assetId, source) {
|
|
176
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
177
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
178
|
+
this.refreshUnderLock();
|
|
179
|
+
const current = this.index.get(assetId);
|
|
180
|
+
if (current && (current.trusted === true || current.decision !== undefined)) {
|
|
181
|
+
return { record: current, appended: false };
|
|
182
|
+
}
|
|
183
|
+
if (current?.trusted === false) {
|
|
184
|
+
return { record: current, appended: false };
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
record: this.appendUnderLock({
|
|
188
|
+
assetId,
|
|
189
|
+
source,
|
|
190
|
+
trusted: false,
|
|
191
|
+
at: new Date(this.now()).toISOString(),
|
|
192
|
+
}),
|
|
193
|
+
appended: true,
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/** Finalize a verified write unless an operator made an explicit decision during I/O. */
|
|
198
|
+
finalizeUntrustedWrite(assetId, source) {
|
|
199
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
200
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
201
|
+
this.refreshUnderLock();
|
|
202
|
+
const current = this.index.get(assetId);
|
|
203
|
+
if (current && (current.trusted === true || current.decision !== undefined))
|
|
204
|
+
return current;
|
|
205
|
+
return this.appendUnderLock({
|
|
206
|
+
assetId,
|
|
207
|
+
source,
|
|
208
|
+
trusted: false,
|
|
209
|
+
at: new Date(this.now()).toISOString(),
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Stage an unverified write without overwriting an explicit trust decision or another conservative marker. */
|
|
214
|
+
stageUnverifiedWrite(assetId, source, frozenContentId) {
|
|
215
|
+
return this.stageUnverifiedWriteTracked(assetId, source, frozenContentId).record;
|
|
216
|
+
}
|
|
217
|
+
stageUnverifiedWriteTracked(assetId, source, frozenContentId) {
|
|
218
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
219
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
220
|
+
this.refreshUnderLock();
|
|
221
|
+
const current = this.index.get(assetId);
|
|
222
|
+
const action = unverifiedStageAction(current, source, frozenContentId);
|
|
223
|
+
if (current && action !== 'append')
|
|
224
|
+
return { record: current, appended: false };
|
|
225
|
+
return {
|
|
226
|
+
record: this.appendUnderLock({
|
|
227
|
+
assetId,
|
|
228
|
+
source,
|
|
229
|
+
trusted: false,
|
|
230
|
+
reason: UNVERIFIED_WRITE_PENDING_REASON,
|
|
231
|
+
frozenContentId,
|
|
232
|
+
at: new Date(this.now()).toISOString(),
|
|
233
|
+
}),
|
|
234
|
+
appended: true,
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Atomically replace only a pending/no decision with the health-waiver reason after verified persistence. */
|
|
239
|
+
finalizeUnverifiedWrite(assetId, source, reason, frozenContentId) {
|
|
240
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
241
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
242
|
+
this.refreshUnderLock();
|
|
243
|
+
const current = this.index.get(assetId);
|
|
244
|
+
const action = unverifiedFinalizeAction(current, source, reason, frozenContentId);
|
|
245
|
+
if (current && action !== 'append')
|
|
246
|
+
return current;
|
|
247
|
+
return this.appendUnderLock({
|
|
248
|
+
assetId,
|
|
249
|
+
source,
|
|
250
|
+
trusted: false,
|
|
251
|
+
reason,
|
|
252
|
+
frozenContentId,
|
|
253
|
+
at: new Date(this.now()).toISOString(),
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}
|
|
72
257
|
rollbackLast(rec) {
|
|
73
258
|
const line = `${JSON.stringify(rec)}\n`;
|
|
74
259
|
try {
|
|
@@ -103,14 +288,22 @@ export class ProvenanceStore {
|
|
|
103
288
|
return withAssetStoreLock(this.lockPath, () => {
|
|
104
289
|
this.refreshUnderLock();
|
|
105
290
|
const current = this.index.get(assetId) ?? null;
|
|
106
|
-
if (
|
|
291
|
+
if (trusted
|
|
292
|
+
&& current?.trusted === false
|
|
293
|
+
&& current.decision === undefined
|
|
294
|
+
&& current.reason === UNVERIFIED_WRITE_PENDING_REASON) {
|
|
295
|
+
throw new ProvenanceWritePendingError(assetId);
|
|
296
|
+
}
|
|
297
|
+
const decision = trusted ? 'promoted' : 'revoked';
|
|
298
|
+
if (current?.trusted === trusted && current.decision === decision) {
|
|
107
299
|
return { changed: false, record: current };
|
|
300
|
+
}
|
|
108
301
|
const full = {
|
|
109
302
|
assetId,
|
|
110
303
|
source: current?.source ?? 'local',
|
|
111
304
|
trusted,
|
|
112
305
|
at: new Date(this.now()).toISOString(),
|
|
113
|
-
decision
|
|
306
|
+
decision,
|
|
114
307
|
decidedBy: by,
|
|
115
308
|
...(trusted ? { promotedBy: by } : {}),
|
|
116
309
|
reason,
|
|
@@ -142,4 +335,119 @@ export async function ingestUntrusted(store, prov, record, source = 'hub') {
|
|
|
142
335
|
if (!result.stored)
|
|
143
336
|
prov.rollbackLast(mark);
|
|
144
337
|
return result;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Hub → local-pool landing for an asset whose content does NOT hash to its declared asset_id. The hub
|
|
341
|
+
* demonstrably rewrites delivered payloads (injected `validation`, wholesale `payload_backfill_reason`
|
|
342
|
+
* synthesis — evolver-v2#570), which breaks {@link ingestUntrusted}'s normalizeForPut self-consistency check
|
|
343
|
+
* even though the loop's own in-run reuse already consumes hub content without re-verifying it (adapter
|
|
344
|
+
* `hubReuse.ts`). Rather than hard-reject a save the operator explicitly asked for, freeze the asset under its
|
|
345
|
+
* declared (network) asset_id via `putFrozen` and mark it untrusted in the sidecar with an explicit reason.
|
|
346
|
+
* Trust-first still holds (#30.1): selection defaults to trusted-only, so an unverified asset never silently
|
|
347
|
+
* enters the reasoning pool — it lands where the operator put it and stays flagged until an explicit,
|
|
348
|
+
* audited promotion. `reason` records WHY verification was waived (e.g. hub rewrite vs synthesized payload).
|
|
349
|
+
*/
|
|
350
|
+
export async function ingestUnverified(store, prov, record, reason, source = 'hub') {
|
|
351
|
+
if (typeof store.putFrozen !== 'function') {
|
|
352
|
+
// Only a content-addressed local pool receives hub reuse writes; a provider that cannot freeze a
|
|
353
|
+
// hash-inconsistent record cannot preserve the network id, so fail loudly rather than silently restamp it.
|
|
354
|
+
throw new Error('ingestUnverified requires a store that implements putFrozen');
|
|
355
|
+
}
|
|
356
|
+
// putFrozen bypasses normalizeForPut, so re-assert the M3-4 Capsule↔gene binding here — a hash-mismatched
|
|
357
|
+
// Capsule with an empty gene must still fail closed on the frozen path, exactly as it does on the verified one.
|
|
358
|
+
assertCapsuleGeneBinding(record);
|
|
359
|
+
const frozenContentId = computeAssetId(record);
|
|
360
|
+
if (!frozenContentId)
|
|
361
|
+
throw new Error('failed to compute frozen content id');
|
|
362
|
+
const existing = await store.get(record.asset_id);
|
|
363
|
+
if (existing) {
|
|
364
|
+
if (!frozenAssetRecordsEqual(existing, record))
|
|
365
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
366
|
+
prov.finalizeUnverifiedWrite(record.asset_id, source, reason, frozenContentId);
|
|
367
|
+
return { asset_id: record.asset_id, stored: false, verified: false };
|
|
368
|
+
}
|
|
369
|
+
// Stage a non-waiver record before the body write. If the provider throws or lies about persistence,
|
|
370
|
+
// the asset stays untrusted and health still reports its hash mismatch instead of treating it as #570.
|
|
371
|
+
const staged = prov.stageUnverifiedWrite(record.asset_id, source, frozenContentId);
|
|
372
|
+
// A missing body with an explicit trust decision cannot be replaced without reopening a promotion race.
|
|
373
|
+
if (planUnverifiedIngest(record, null, staged, reason, source).status !== 'create') {
|
|
374
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
375
|
+
}
|
|
376
|
+
const result = validateFrozenPutResult(await store.putFrozen(record), record.asset_id);
|
|
377
|
+
const persisted = await store.get(record.asset_id);
|
|
378
|
+
if (!persisted || !frozenAssetRecordsEqual(persisted, record)) {
|
|
379
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
380
|
+
}
|
|
381
|
+
prov.finalizeUnverifiedWrite(record.asset_id, source, reason, frozenContentId);
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
/** Atomic frozen variant used by reuse so the logical-id check and append share one provider lock. */
|
|
385
|
+
export async function ingestUnverifiedConditional(store, prov, record, reason, options, source = 'hub') {
|
|
386
|
+
if (!supportsAtomicFrozenConditionalPut(store)) {
|
|
387
|
+
throw new Error('asset store does not support conditional frozen writes');
|
|
388
|
+
}
|
|
389
|
+
assertCapsuleGeneBinding(record);
|
|
390
|
+
const frozenContentId = computeAssetId(record);
|
|
391
|
+
if (!frozenContentId)
|
|
392
|
+
throw new Error('failed to compute frozen content id');
|
|
393
|
+
const existing = await store.get(record.asset_id);
|
|
394
|
+
if (existing) {
|
|
395
|
+
if (!frozenAssetRecordsEqual(existing, record))
|
|
396
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
397
|
+
prov.finalizeUnverifiedWrite(record.asset_id, source, reason, frozenContentId);
|
|
398
|
+
return { asset_id: record.asset_id, stored: false, verified: false, status: 'already_exists' };
|
|
399
|
+
}
|
|
400
|
+
const stage = prov.stageUnverifiedWriteTracked(record.asset_id, source, frozenContentId);
|
|
401
|
+
if (planUnverifiedIngest(record, null, stage.record, reason, source).status !== 'create') {
|
|
402
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
403
|
+
}
|
|
404
|
+
const result = validateConditionalPutResult(await store.putFrozenConditional(record, options), record.asset_id, options);
|
|
405
|
+
if (result.verified !== false)
|
|
406
|
+
throw new InvalidFrozenPutResultError();
|
|
407
|
+
if (!result.stored) {
|
|
408
|
+
// The pending row may be shared with a concurrent writer. Keep it until a persisted body is finalized.
|
|
409
|
+
if (result.status === 'already_exists') {
|
|
410
|
+
const persisted = await store.get(record.asset_id);
|
|
411
|
+
if (!persisted || !frozenAssetRecordsEqual(persisted, record)) {
|
|
412
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
413
|
+
}
|
|
414
|
+
prov.finalizeUnverifiedWrite(record.asset_id, source, reason, frozenContentId);
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
const persisted = await store.get(record.asset_id);
|
|
419
|
+
if (!persisted || !frozenAssetRecordsEqual(persisted, record)) {
|
|
420
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
421
|
+
}
|
|
422
|
+
prov.finalizeUnverifiedWrite(record.asset_id, source, reason, frozenContentId);
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Conditional variant used by Hub sync to reject a logical-id collision without ever allowing a Hub record
|
|
427
|
+
* to become implicitly trusted. Providers that cannot make the condition atomically are rejected here.
|
|
428
|
+
*/
|
|
429
|
+
export async function ingestUntrustedConditional(store, prov, record, options, source = 'hub') {
|
|
430
|
+
if (!supportsAtomicConditionalPut(store)) {
|
|
431
|
+
throw new Error('asset store does not support conditional writes');
|
|
432
|
+
}
|
|
433
|
+
const normalized = normalizeForPut(record);
|
|
434
|
+
const stage = prov.stageUntrustedWriteTracked(normalized.record.asset_id, source);
|
|
435
|
+
const result = validateConditionalPutResult(await store.putConditional(record, options), normalized.record.asset_id, options);
|
|
436
|
+
if (result.status === 'logical_collision') {
|
|
437
|
+
if (stage.appended)
|
|
438
|
+
prov.rollbackLast(stage.record);
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
if (!result.stored) {
|
|
442
|
+
const persisted = await store.get(normalized.record.asset_id);
|
|
443
|
+
if (!persisted || !frozenAssetRecordsEqual(persisted, normalized.record)) {
|
|
444
|
+
throw new FrozenAssetIdCollisionError(normalized.record.asset_id);
|
|
445
|
+
}
|
|
446
|
+
if (stage.appended)
|
|
447
|
+
prov.rollbackLast(stage.record);
|
|
448
|
+
}
|
|
449
|
+
if (!stage.appended) {
|
|
450
|
+
prov.finalizeUntrustedWrite(normalized.record.asset_id, source);
|
|
451
|
+
}
|
|
452
|
+
return result;
|
|
145
453
|
}
|
|
@@ -10,12 +10,43 @@ export interface PutResult {
|
|
|
10
10
|
stored: boolean;
|
|
11
11
|
verified: boolean;
|
|
12
12
|
}
|
|
13
|
+
export declare class FrozenAssetIdCollisionError extends Error {
|
|
14
|
+
readonly assetId: string;
|
|
15
|
+
readonly code = "FROZEN_ASSET_ID_COLLISION";
|
|
16
|
+
constructor(assetId: string);
|
|
17
|
+
}
|
|
18
|
+
export declare class InvalidFrozenPutResultError extends Error {
|
|
19
|
+
readonly code = "INVALID_FROZEN_PUT_RESULT";
|
|
20
|
+
constructor();
|
|
21
|
+
}
|
|
22
|
+
export declare function frozenAssetRecordsEqual(left: AssetRecord, right: AssetRecord): boolean;
|
|
23
|
+
export declare function validateFrozenPutResult(value: unknown, expectedAssetId: string): PutResult;
|
|
24
|
+
export type ConditionalPutStatus = 'stored' | 'already_exists' | 'logical_collision';
|
|
25
|
+
export interface ConditionalPutOptions {
|
|
26
|
+
/** Only explicit force-like callers may keep multiple content versions for the same type + logical id. */
|
|
27
|
+
allowLogicalCollision?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ConditionalPutResult extends PutResult {
|
|
30
|
+
status: ConditionalPutStatus;
|
|
31
|
+
logicalId?: string;
|
|
32
|
+
collisionWithAssetId?: string;
|
|
33
|
+
}
|
|
34
|
+
export type InvalidConditionalPutResultReason = 'malformed_result' | 'asset_id_mismatch' | 'inconsistent_status' | 'invalid_collision' | 'collision_bypass';
|
|
35
|
+
export declare class InvalidConditionalPutResultError extends Error {
|
|
36
|
+
readonly reason: InvalidConditionalPutResultReason;
|
|
37
|
+
readonly code = "INVALID_CONDITIONAL_PUT_RESULT";
|
|
38
|
+
constructor(reason: InvalidConditionalPutResultReason);
|
|
39
|
+
}
|
|
40
|
+
/** Validate an injected provider response before callers treat it as an explicit write/no-write decision. */
|
|
41
|
+
export declare function validateConditionalPutResult(value: unknown, expectedAssetId: string, options?: ConditionalPutOptions): ConditionalPutResult;
|
|
13
42
|
export interface SearchQuery {
|
|
14
43
|
kind?: AssetKind;
|
|
15
44
|
signalsAny?: string[];
|
|
16
45
|
category?: string;
|
|
17
46
|
gene?: string;
|
|
18
47
|
text?: string;
|
|
48
|
+
/** Task-domain scope (hub taxonomy slug, e.g. "software_engineering"). Local providers may ignore it; the hub applies it as a recall fence. */
|
|
49
|
+
domain?: string;
|
|
19
50
|
limit?: number;
|
|
20
51
|
}
|
|
21
52
|
/**
|
|
@@ -24,12 +55,32 @@ export interface SearchQuery {
|
|
|
24
55
|
*/
|
|
25
56
|
export interface AssetStoreProvider {
|
|
26
57
|
put(asset: AssetRecord): Promise<PutResult>;
|
|
58
|
+
/** Optional atomic capability; use supportsAtomicConditionalPut() before calling through this interface. */
|
|
59
|
+
putConditional?(asset: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
|
|
27
60
|
get(assetId: string): Promise<AssetRecord | null>;
|
|
28
61
|
/** Optional direct lookup for non-content-addressed logical ids. Callers must handle 0, 1, or multiple matches. */
|
|
29
|
-
findByLogicalId?(id: string, limit?: number): Promise<AssetRecord[]>;
|
|
62
|
+
findByLogicalId?(id: string, limit?: number, kind?: AssetKind): Promise<AssetRecord[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Optional frozen write: store the record under its OWN declared asset_id without recomputing or
|
|
65
|
+
* normalizing it (bypasses {@link normalizeForPut}'s self-consistency check). Only providers backing a
|
|
66
|
+
* content-addressed local pool implement it. Callers that must preserve a hash-inconsistent asset —
|
|
67
|
+
* v1 migration import, and unverified hub reuse of a hub-rewritten payload (see `ingestUnverified`) —
|
|
68
|
+
* feature-detect it rather than assuming it exists.
|
|
69
|
+
*/
|
|
70
|
+
putFrozen?(record: AssetRecord): Promise<PutResult>;
|
|
71
|
+
/** Atomic frozen variant that also enforces the type + logical-id condition under the provider write lock. */
|
|
72
|
+
putFrozenConditional?(record: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
|
|
30
73
|
search(query: SearchQuery): Promise<AssetRecord[]>;
|
|
31
74
|
list(kind?: AssetKind, limit?: number): Promise<AssetRecord[]>;
|
|
32
75
|
}
|
|
76
|
+
export type AtomicConditionalPutProvider = AssetStoreProvider & {
|
|
77
|
+
putConditional(asset: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
|
|
78
|
+
};
|
|
79
|
+
export declare function supportsAtomicConditionalPut(provider: AssetStoreProvider): provider is AtomicConditionalPutProvider;
|
|
80
|
+
export type AtomicFrozenConditionalPutProvider = AssetStoreProvider & {
|
|
81
|
+
putFrozenConditional(record: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
|
|
82
|
+
};
|
|
83
|
+
export declare function supportsAtomicFrozenConditionalPut(provider: AssetStoreProvider): provider is AtomicFrozenConditionalPutProvider;
|
|
33
84
|
export declare class AssetIdMismatchError extends Error {
|
|
34
85
|
readonly claimed: string;
|
|
35
86
|
readonly actual: string;
|
|
@@ -38,6 +89,12 @@ export declare class AssetIdMismatchError extends Error {
|
|
|
38
89
|
export declare class CapsuleGeneBindingError extends Error {
|
|
39
90
|
constructor();
|
|
40
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* M3-4 强绑定校验: Capsule.gene 必须非空(否则一条无来源基因的经验会污染选择池).
|
|
94
|
+
* 抽成独立 helper 以便 normalizeForPut(校验落库路径)与 ingestUnverified(冻结落库路径,
|
|
95
|
+
* 绕过 normalizeForPut)共用同一条不变量,不让降级路径把绑定校验漏掉.
|
|
96
|
+
*/
|
|
97
|
+
export declare function assertCapsuleGeneBinding(asset: AssetRecord): void;
|
|
41
98
|
/**
|
|
42
99
|
* 落库前规范化(共享给各 provider): 计算/校验 asset_id + 强绑定校验.
|
|
43
100
|
* - 缺 asset_id → 计算填入(verified=false 表示非入参自带).
|