@evomap/evolver-adapter-public 2.0.2 → 2.0.12

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.
@@ -596,7 +596,8 @@ export class CredentialStore {
596
596
  throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
597
597
  }
598
598
  for (let attempt = 0; attempt < 5; attempt += 1) {
599
- const initialState = securityStateOf(bigFstat(fd));
599
+ const initialMetadata = bigFstat(fd);
600
+ const initialState = securityStateOf(initialMetadata);
600
601
  let output;
601
602
  try {
602
603
  output = this.darwinAclReader(path);
@@ -613,12 +614,33 @@ export class CredentialStore {
613
614
  !sameIdentity(after, identity) || !sameIdentity(openedAfter, identity)) {
614
615
  throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
615
616
  }
616
- const metadataStable = sameSecurityState(initialState, after) &&
617
- sameSecurityState(initialState, openedAfter);
618
- if (metadataStable) {
617
+ if (sameSecurityState(initialState, after) && sameSecurityState(initialState, openedAfter)) {
619
618
  this.securedAncestorStates.set(path, securityStateOf(openedAfter));
620
619
  return;
621
620
  }
621
+ if (sameDarwinAncestorMetadata(initialMetadata, after)
622
+ && sameDarwinAncestorMetadata(initialMetadata, openedAfter)) {
623
+ let confirmedOutput;
624
+ try {
625
+ confirmedOutput = this.darwinAclReader(path);
626
+ }
627
+ catch {
628
+ throw new CredentialStoreError(`ancestor directory ${path} ACL could not be inspected`);
629
+ }
630
+ if (hasUnsafeDarwinAllowAcl(confirmedOutput, rejectAnyAllow)) {
631
+ throw new CredentialStoreError(`ancestor directory ${path} grants access through an extended ACL`);
632
+ }
633
+ const confirmedPath = bigLstat(path);
634
+ const confirmedOpened = bigFstat(fd);
635
+ if (confirmedOutput === output
636
+ && !confirmedPath.isSymbolicLink()
637
+ && confirmedPath.isDirectory()
638
+ && sameDarwinAncestorMetadata(initialMetadata, confirmedPath)
639
+ && sameDarwinAncestorMetadata(initialMetadata, confirmedOpened)) {
640
+ this.securedAncestorStates.set(path, securityStateOf(confirmedOpened));
641
+ return;
642
+ }
643
+ }
622
644
  }
623
645
  throw new CredentialStoreError(`ancestor directory ${path} changed during ACL validation`);
624
646
  }
@@ -1056,6 +1078,13 @@ function sameIdentity(left, right) {
1056
1078
  function sameSecurityState(left, right) {
1057
1079
  return sameIdentity(left, right) && left.ctimeNs === right.ctimeNs;
1058
1080
  }
1081
+ function sameDarwinAncestorMetadata(left, right) {
1082
+ return sameIdentity(left, right)
1083
+ && right.isDirectory()
1084
+ && !right.isSymbolicLink()
1085
+ && left.uid === right.uid
1086
+ && left.mode === right.mode;
1087
+ }
1059
1088
  function samePathSecurityStates(left, right) {
1060
1089
  return left.length === right.length && left.every((state, index) => {
1061
1090
  const candidate = right[index];
@@ -58,6 +58,9 @@ export interface AccountAssetListResult {
58
58
  hasMore: boolean;
59
59
  nextCursor?: string;
60
60
  }
61
+ export declare class MalformedAccountAssetPageError extends Error {
62
+ constructor();
63
+ }
61
64
  /** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
62
65
  export declare function gepEnvelope(messageType: string, payload: unknown, options?: {
63
66
  messageId?: string;
@@ -149,7 +152,13 @@ export declare class PublicHubCapability implements hub.HubCapability {
149
152
  private heartbeatMeta;
150
153
  publish(bundle: hub.AssetRecord[], options?: hub.PublishOptions): Promise<hub.PublishReceipt>;
151
154
  fetch(query: hub.HubQuery): Promise<hub.AssetRecord[]>;
152
- fetchAssetById(assetId: string): Promise<hub.AssetRecord | null>;
155
+ /**
156
+ * Fetch one asset AND say why, when the answer is not an asset. `fetchAssetById` collapses every outcome to
157
+ * `null`, so a caller could not tell "the hub does not have this" from "the hub delivered something the
158
+ * client refuses" — and the CLI reported both as `not_found` on assets that demonstrably exist (#964).
159
+ */
160
+ fetchAssetDeliveryById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetDeliveryOutcome>;
161
+ fetchAssetById(assetId: string, options?: hub.FetchAssetByIdOptions): Promise<hub.AssetRecord | null>;
153
162
  /**
154
163
  * #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
155
164
  * signal/id queries use the Hub's free search-only phase on /a2a/fetch. /a2a/fetch does NOT do semantic,
@@ -1,12 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { bootstrap, hub as hubNs, signals } from '@evomap/evolver-core';
2
+ import { bootstrap, hub as hubNs, signals, wire } from '@evomap/evolver-core';
3
3
  import { AuthError, HubFetch, HubClientError, isHubUnreachableError } from './hubFetch.js';
4
4
  import { isNodeSecret, parseNodeSecretVersion } from './auth/legacyShim.js';
5
5
  import { inboundToAgentEvent, agentEventToOutbound, publishRespToReceipt, searchQueryToFetchWire, searchQueryToSearchOnlyWire, } from './wireMap.js';
6
6
  import { antiAbuseTelemetryMode, buildHeartbeatAntiAbuseTelemetry, } from './antiAbuseTelemetry.js';
7
7
  import { getWorkspaceKeychainMode } from './auth/workspaceKeychain.js';
8
8
  import { agentDirectoryFailure, parsePublicAgentPage, parsePublicAgentProfile, paginatePublicAgentPage, mergePublicAgentPages, publicAgentSearchQuery, publicTaskDiscoveryQuery, PUBLIC_TASK_DISCOVERY_MAX_CANDIDATES, unsupportedPublicAvailability, unsupportedPublicSort, withDirectoryTimeout, } from './agentDirectory.js';
9
- import { assetMatchesId } from './hubReuse.js';
9
+ import { assetMatchesId, stripHubDeliveryMetadataForIntegrity } from './hubReuse.js';
10
10
  export const INBOUND_LIMIT = 100;
11
11
  export const OUTBOUND_MAX_BATCH = 50;
12
12
  export const OUTBOUND_MAX_BODY_BYTES = 4 * 1024 * 1024;
@@ -22,6 +22,12 @@ export const USED_ASSET_IDS_MAX = 50;
22
22
  export const USED_ASSET_ID_MAX_LEN = 200;
23
23
  export const LEARNING_ASSET_IDS_MAX = 50;
24
24
  export const LEARNING_ASSET_ID_MAX_LEN = 128;
25
+ export class MalformedAccountAssetPageError extends Error {
26
+ constructor() {
27
+ super('Hub account asset page is malformed');
28
+ this.name = 'MalformedAccountAssetPageError';
29
+ }
30
+ }
25
31
  /** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
26
32
  export function gepEnvelope(messageType, payload, options = {}) {
27
33
  return {
@@ -280,12 +286,56 @@ export class PublicHubCapability {
280
286
  const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', searchQueryToFetchWire(query)));
281
287
  return assetsFromBody(body);
282
288
  }
283
- async fetchAssetById(assetId) {
289
+ /**
290
+ * Fetch one asset AND say why, when the answer is not an asset. `fetchAssetById` collapses every outcome to
291
+ * `null`, so a caller could not tell "the hub does not have this" from "the hub delivered something the
292
+ * client refuses" — and the CLI reported both as `not_found` on assets that demonstrably exist (#964).
293
+ */
294
+ async fetchAssetDeliveryById(assetId, options) {
284
295
  const id = assetId.trim();
285
296
  if (!id)
286
- return null;
297
+ return { status: 'absent' };
287
298
  const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', { asset_ids: [id] }));
288
- return assetsFromBody(body).find((asset) => fetchResultMatchesId(asset, id)) ?? null;
299
+ const matches = [];
300
+ for (const row of assetCandidatesFromBody(body)) {
301
+ const asset = unwrapFetchDeliveryRow(row);
302
+ if (!fetchDeliveryIdentityConsistent(row, asset))
303
+ return { status: 'rejected', reason: 'identity_mismatch' };
304
+ if (isContentAssetIdRequest(id)) {
305
+ // Identity is what this gate is for: the delivery must bind the REQUESTED content id through its own
306
+ // canonical `asset_id`. Whether the delivered body then hashes to that id, or satisfies the wire schema,
307
+ // is a trust question the caller resolves (quarantine + repair) — not grounds to drop the asset here.
308
+ if (!assetMatchesId(asset, id) && stringField(asset, 'asset_id') !== id) {
309
+ return { status: 'rejected', reason: 'identity_mismatch' };
310
+ }
311
+ if (isRevokedFetchDelivery(row))
312
+ return { status: 'rejected', reason: 'revoked' };
313
+ matches.push(asset);
314
+ continue;
315
+ }
316
+ if (fetchResultMatchesId(asset, id)) {
317
+ if (isRevokedFetchDelivery(row))
318
+ return { status: 'rejected', reason: 'revoked' };
319
+ matches.push(asset);
320
+ }
321
+ }
322
+ if (matches.length === 0)
323
+ return { status: 'absent' };
324
+ const result = unambiguousFetchResult(matches);
325
+ if (!result)
326
+ return { status: 'rejected', reason: 'ambiguous' };
327
+ // Logical-id lookups retain their historical matching semantics. Only a canonical sha256 lookup can opt in
328
+ // to a content-hash drift, and every non-opted-in caller remains strict.
329
+ const verified = assetMatchesId(result, id);
330
+ if (verified || !isContentAssetIdRequest(id))
331
+ return { status: 'delivered', asset: result, verified: true };
332
+ return options?.allowUnverifiedExactIdentity === true
333
+ ? { status: 'delivered', asset: result, verified: false }
334
+ : { status: 'rejected', reason: 'unverified_not_allowed' };
335
+ }
336
+ async fetchAssetById(assetId, options) {
337
+ const outcome = await this.fetchAssetDeliveryById(assetId, options);
338
+ return outcome.status === 'delivered' ? outcome.asset : null;
289
339
  }
290
340
  /**
291
341
  * #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
@@ -368,16 +418,18 @@ export class PublicHubCapability {
368
418
  ...(opts.scope === 'published' && opts.status && opts.status !== 'all' ? { status: opts.status } : {}),
369
419
  };
370
420
  const body = await this.http.call('GET', path, undefined, query);
371
- const payload = asRecord(body['payload']) ?? body;
421
+ const payload = Object.prototype.hasOwnProperty.call(body, 'payload')
422
+ ? asRecord(body['payload'])
423
+ : body;
424
+ if (!payload)
425
+ throw new MalformedAccountAssetPageError();
372
426
  const assets = accountAssetsFromPayload(payload);
373
427
  const count = numberField(payload, 'count');
374
- const nextCursor = stringField(payload, 'next_cursor') ?? stringField(payload, 'nextCursor');
375
- const hasMore = booleanField(payload, 'has_more') ?? booleanField(payload, 'hasMore') ?? Boolean(nextCursor);
428
+ const pagination = accountPaginationFromPayload(payload);
376
429
  return {
377
430
  assets,
378
431
  ...(count !== undefined ? { count } : {}),
379
- hasMore,
380
- ...(nextCursor ? { nextCursor } : {}),
432
+ ...pagination,
381
433
  };
382
434
  }
383
435
  /**
@@ -850,7 +902,7 @@ function dryRunRecipeReceipt(action, recipeId, extra = {}) {
850
902
  },
851
903
  };
852
904
  }
853
- function assetsFromBody(body) {
905
+ function assetCandidatesFromBody(body) {
854
906
  const payload = asRecord(body['payload']);
855
907
  const candidates = [
856
908
  body['asset'],
@@ -861,15 +913,25 @@ function assetsFromBody(body) {
861
913
  ...(Array.isArray(payload?.['results']) ? payload['results'] : []),
862
914
  ];
863
915
  return candidates
864
- .filter((candidate) => Boolean(candidate && typeof candidate === 'object' && !Array.isArray(candidate)))
865
- .map(unwrapFetchDeliveryRow);
916
+ .filter((candidate) => Boolean(candidate && typeof candidate === 'object' && !Array.isArray(candidate)));
917
+ }
918
+ function assetsFromBody(body) {
919
+ return assetCandidatesFromBody(body).map(unwrapFetchDeliveryRow);
866
920
  }
867
921
  // Delivery-row metadata carried over onto the unwrapped GEP record. Ranking fields are consumed by
868
922
  // hubReuse and stripped before canonical storage. `payload_backfill_reason` must also survive this
869
923
  // boundary so integrity consumers can report that the Hub synthesized the payload (#570). Do not
870
924
  // carry `confidence`: it is transport metadata on Gene rows but canonical content on Capsules, so
871
925
  // overloading it can either poison a Gene hash or overwrite Capsule content (#565).
872
- const FETCH_ROW_CARRYOVER_KEYS = ['gdi_score', 'success_rate', 'reuse_count', 'source_node_id', 'payload_backfill_reason'];
926
+ const FETCH_ROW_CARRYOVER_KEYS = [
927
+ 'gdi_score',
928
+ 'success_rate',
929
+ 'reuse_count',
930
+ 'source_node_id',
931
+ 'payload_backfill_reason',
932
+ 'status',
933
+ 'trust_state',
934
+ ];
873
935
  /**
874
936
  * The live hub's /a2a/fetch results are DELIVERY ROWS, not raw GEP records (#565, observed on
875
937
  * evomap.ai 2026-07-22): the record itself nests under `payload`, while the row's own keys are
@@ -893,20 +955,89 @@ function unwrapFetchDeliveryRow(row) {
893
955
  }
894
956
  return { ...inner, ...carryover };
895
957
  }
958
+ function fetchDeliveryIdentityConsistent(row, asset) {
959
+ const outer = row;
960
+ if (typeof outer['type'] === 'string') {
961
+ const transportType = stringField(outer, 'asset_type');
962
+ if (transportType !== undefined && transportType !== outer['type'])
963
+ return false;
964
+ const transportLogicalId = stringField(outer, 'local_id');
965
+ if (transportLogicalId !== undefined && transportLogicalId !== stringField(outer, 'id'))
966
+ return false;
967
+ return true;
968
+ }
969
+ const inner = asRecord(outer['payload']);
970
+ if (!inner)
971
+ return false;
972
+ const outerAssetId = stringField(outer, 'asset_id');
973
+ const innerAssetId = stringField(inner, 'asset_id');
974
+ if (!outerAssetId || !innerAssetId || outerAssetId !== innerAssetId)
975
+ return false;
976
+ const outerType = stringField(outer, 'asset_type');
977
+ const innerType = stringField(inner, 'type');
978
+ if (outerType !== undefined && outerType !== innerType)
979
+ return false;
980
+ const innerLogicalId = stringField(inner, 'id');
981
+ for (const key of ['id', 'local_id']) {
982
+ const outerLogicalId = stringField(outer, key);
983
+ if (outerLogicalId !== undefined && outerLogicalId !== innerLogicalId)
984
+ return false;
985
+ }
986
+ for (const key of ['status', 'trust_state']) {
987
+ const outerMarker = stringField(outer, key);
988
+ const innerMarker = stringField(inner, key);
989
+ const outerRevoked = isRevokedMarker(outerMarker);
990
+ const innerRevoked = isRevokedMarker(innerMarker);
991
+ if (outerMarker !== undefined && innerMarker !== undefined && outerRevoked !== innerRevoked)
992
+ return false;
993
+ }
994
+ return asset === row || stringField(asset, 'asset_id') === innerAssetId;
995
+ }
996
+ function isRevokedFetchDelivery(row) {
997
+ const record = row;
998
+ const nested = asRecord(record['payload']);
999
+ return [record, ...(nested ? [nested] : [])].some((value) => (isRevokedMarker(value['status'])
1000
+ || isRevokedMarker(value['trust_state'])));
1001
+ }
1002
+ function isRevokedMarker(raw) {
1003
+ return typeof raw === 'string' && raw.trim().toLowerCase() === 'revoked';
1004
+ }
896
1005
  function accountAssetsFromPayload(payload) {
897
- const candidates = [
898
- payload['assets'],
899
- payload['results'],
900
- payload['items'],
901
- ];
902
- for (const candidate of candidates) {
903
- if (!Array.isArray(candidate))
904
- continue;
905
- return candidate
906
- .filter((asset) => Boolean(asset && typeof asset === 'object' && !Array.isArray(asset)))
907
- .map(unwrapFetchDeliveryRow);
1006
+ const keys = ['assets', 'results', 'items']
1007
+ .filter((key) => Object.prototype.hasOwnProperty.call(payload, key));
1008
+ if (keys.length !== 1)
1009
+ throw new MalformedAccountAssetPageError();
1010
+ const candidate = payload[keys[0]];
1011
+ if (!Array.isArray(candidate) || candidate.some((asset) => !asset || typeof asset !== 'object' || Array.isArray(asset))) {
1012
+ throw new MalformedAccountAssetPageError();
908
1013
  }
909
- return [];
1014
+ return candidate.map((row) => {
1015
+ const asset = unwrapFetchDeliveryRow(row);
1016
+ return fetchDeliveryIdentityConsistent(row, asset) ? asset : row;
1017
+ });
1018
+ }
1019
+ function accountPaginationFromPayload(payload) {
1020
+ const snakeHasMore = payload['has_more'];
1021
+ const camelHasMore = payload['hasMore'];
1022
+ if ((snakeHasMore !== undefined && typeof snakeHasMore !== 'boolean')
1023
+ || (camelHasMore !== undefined && typeof camelHasMore !== 'boolean')
1024
+ || (snakeHasMore === undefined && camelHasMore === undefined)
1025
+ || (typeof snakeHasMore === 'boolean' && typeof camelHasMore === 'boolean' && snakeHasMore !== camelHasMore)) {
1026
+ throw new MalformedAccountAssetPageError();
1027
+ }
1028
+ const hasMore = typeof snakeHasMore === 'boolean' ? snakeHasMore : camelHasMore;
1029
+ const rawCursors = [payload['next_cursor'], payload['nextCursor']]
1030
+ .filter((value) => value !== undefined && value !== null);
1031
+ if (rawCursors.some((value) => typeof value !== 'string' || !value.trim())) {
1032
+ throw new MalformedAccountAssetPageError();
1033
+ }
1034
+ if (rawCursors.length === 2 && rawCursors[0] !== rawCursors[1]) {
1035
+ throw new MalformedAccountAssetPageError();
1036
+ }
1037
+ const nextCursor = rawCursors[0];
1038
+ if (hasMore !== Boolean(nextCursor))
1039
+ throw new MalformedAccountAssetPageError();
1040
+ return { hasMore, ...(nextCursor ? { nextCursor } : {}) };
910
1041
  }
911
1042
  function learningAssetsFromPayload(payload) {
912
1043
  const candidates = [
@@ -1010,7 +1141,30 @@ function failureReason(error) {
1010
1141
  function fetchResultMatchesId(asset, requestedId) {
1011
1142
  if (assetMatchesId(asset, requestedId))
1012
1143
  return true;
1013
- return !requestedId.startsWith('sha256:') && Boolean(asset && stringField(asset, 'id') === requestedId);
1144
+ if (!asset)
1145
+ return false;
1146
+ if (!requestedId.startsWith('sha256:'))
1147
+ return stringField(asset, 'id') === requestedId;
1148
+ return false;
1149
+ }
1150
+ function unambiguousFetchResult(matches) {
1151
+ if (matches.length === 0)
1152
+ return null;
1153
+ let canonical;
1154
+ try {
1155
+ canonical = wire.canonicalize(stripHubDeliveryMetadataForIntegrity(matches[0]));
1156
+ for (const asset of matches.slice(1)) {
1157
+ if (wire.canonicalize(stripHubDeliveryMetadataForIntegrity(asset)) !== canonical)
1158
+ return null;
1159
+ }
1160
+ }
1161
+ catch {
1162
+ return null;
1163
+ }
1164
+ return matches.find((asset) => stringField(asset, 'payload_backfill_reason') !== undefined) ?? matches[0];
1165
+ }
1166
+ function isContentAssetIdRequest(requestedId) {
1167
+ return /^sha256:[0-9a-f]{64}$/.test(requestedId);
1014
1168
  }
1015
1169
  function stringField(value, key) {
1016
1170
  return typeof value[key] === 'string' && value[key].length > 0 ? value[key] : undefined;
@@ -50,6 +50,7 @@ export declare class ReuseCache {
50
50
  setPayload(assetId: string, payload: hub.AssetRecord): void;
51
51
  clear(): void;
52
52
  }
53
+ export declare function stripHubDeliveryMetadataForIntegrity(rec: hub.AssetRecord): hub.AssetRecord;
53
54
  /**
54
55
  * Map a hub search row (AssetRecord with arbitrary quality fields) → the core's price-free HubMetadata.
55
56
  * Accepts both camelCase and the hub's snake_case (gdi_score / success_rate / reuse_count / ...). Drops any
package/dist/hubReuse.js CHANGED
@@ -36,6 +36,7 @@ const GENE_WIRE_KEYS = new Set([
36
36
  ]);
37
37
  const HUB_DELIVERY_METADATA_KEYS = new Set([
38
38
  'status',
39
+ 'trust_state',
39
40
  'success_streak',
40
41
  'reputation_score',
41
42
  'gdi_score',
@@ -55,8 +56,15 @@ const HUB_DELIVERY_METADATA_KEYS = new Set([
55
56
  'semanticSimilarity',
56
57
  '_search_score',
57
58
  'search_score',
59
+ '_match_score',
60
+ 'match_score',
61
+ '_retrieval_rank',
62
+ 'retrieval_rank',
58
63
  'payload_backfill_reason',
64
+ 'original_asset_id',
59
65
  'asset_type',
66
+ 'local_id',
67
+ 'source',
60
68
  'bundle_id',
61
69
  'callable',
62
70
  'payload_ready',
@@ -246,10 +254,15 @@ function stripHubPayloadMetadata(rec) {
246
254
  }
247
255
  return out;
248
256
  }
249
- function stripHubDeliveryMetadataForIntegrity(rec) {
257
+ // Shared content projection for by-id verification and sync quarantine classification.
258
+ export function stripHubDeliveryMetadataForIntegrity(rec) {
250
259
  const out = { ...rec };
251
- for (const key of HUB_DELIVERY_METADATA_KEYS)
260
+ for (const key of HUB_DELIVERY_METADATA_KEYS) {
261
+ // Hub ranking streak is metadata for Genes, while Capsule.success_streak is canonical content.
262
+ if (key === 'success_streak' && out['type'] === 'Capsule')
263
+ continue;
252
264
  delete out[key];
265
+ }
253
266
  // Hub ranking confidence is metadata for Genes, while Capsule.confidence is canonical content.
254
267
  if (out['type'] === 'Gene')
255
268
  delete out['confidence'];
@@ -9,7 +9,15 @@ export interface HubLearningPacketSinkOptions {
9
9
  /** Optional node identity recorded on the packet (hub nodeId column). */
10
10
  nodeId?: () => string | undefined;
11
11
  }
12
- /** Deterministic content hash over the draft body (hub contentHash column, dedup aid). */
12
+ /**
13
+ * Deterministic content hash over the draft body (hub contentHash column, dedup aid).
14
+ *
15
+ * Bare 64-hex, NOT `sha256:`-prefixed: the hub column is VarChar(64), so a prefixed
16
+ * digest is 71 chars and every upload failed with a Prisma "value too long" 500. The
17
+ * hub schema now rejects over-64 at validation, which would make it a 400 instead —
18
+ * either way the algorithm is fixed at sha256 by this contract, so the prefix carried
19
+ * no information.
20
+ */
13
21
  export declare function learningPacketContentHash(draft: trace.LearningPacketDraft): string;
14
22
  /**
15
23
  * Auth headers for the strict learning-packets routes (requireAuth reads Authorization only).
@@ -19,16 +19,47 @@ function failureCategoryFor(failureKind) {
19
19
  return 'tool_error';
20
20
  return 'other';
21
21
  }
22
- function outcomeStatusFor(status) {
23
- if (status === 'success')
24
- return 'succeeded';
22
+ /**
23
+ * Map the runtime's outcome onto the hub OUTCOME_STATUSES enum, tiered by whether an
24
+ * external verifier actually adjudicated the run.
25
+ *
26
+ * A verified run gets a definite verdict (`succeeded` / `failed`). An unverified one
27
+ * gets `partially_succeeded` -- deliberately NOT `succeeded`, and no longer omitted:
28
+ *
29
+ * - Omitting it (the previous behaviour) threw the run away. The packet reached the
30
+ * hub with no outcome at all, which is indistinguishable from a run nobody looked
31
+ * at, so a consumer could not tell "we don't know" from "not recorded".
32
+ * - Calling it `succeeded` would be worse: the runtime only knows the turn loop
33
+ * ended without crashing, which is not evidence the task was done correctly.
34
+ * Training on that teaches format imitation.
35
+ *
36
+ * `partially_succeeded` says exactly what is true -- it ran to completion and nobody
37
+ * checked the result -- and pairs with `verifier` being absent, so a consumer filters
38
+ * on the verifier rather than having to infer trust from the status. Darwin's training
39
+ * path takes only rows with a real verifier; see docs/rsi-stage1-plan.md.
40
+ * @param status Runtime-side outcome.
41
+ * @param verified True when an external verifier ran (evaluation.placeholder === false).
42
+ * @returns A hub OUTCOME_STATUSES value.
43
+ */
44
+ function outcomeStatusFor(status, verified) {
25
45
  if (status === 'failed')
26
46
  return 'failed';
27
- return undefined;
47
+ if (status === 'success' && verified)
48
+ return 'succeeded';
49
+ // Ran to completion, unadjudicated -- or the runtime itself is unsure.
50
+ return 'partially_succeeded';
28
51
  }
29
- /** Deterministic content hash over the draft body (hub contentHash column, dedup aid). */
52
+ /**
53
+ * Deterministic content hash over the draft body (hub contentHash column, dedup aid).
54
+ *
55
+ * Bare 64-hex, NOT `sha256:`-prefixed: the hub column is VarChar(64), so a prefixed
56
+ * digest is 71 chars and every upload failed with a Prisma "value too long" 500. The
57
+ * hub schema now rejects over-64 at validation, which would make it a 400 instead —
58
+ * either way the algorithm is fixed at sha256 by this contract, so the prefix carried
59
+ * no information.
60
+ */
30
61
  export function learningPacketContentHash(draft) {
31
- return `sha256:${createHash('sha256').update(JSON.stringify(draft)).digest('hex')}`;
62
+ return createHash('sha256').update(JSON.stringify(draft)).digest('hex');
32
63
  }
33
64
  /**
34
65
  * Auth headers for the strict learning-packets routes (requireAuth reads Authorization only).
@@ -47,7 +78,7 @@ export async function learningOpsAuthHeaders(auth, method, path) {
47
78
  export function learningPacketWireBody(draft, nodeId) {
48
79
  const truncated = draft.trajectory.length > HUB_TRACE_EVENTS_MAX;
49
80
  const events = draft.trajectory.slice(0, HUB_TRACE_EVENTS_MAX);
50
- const outcomeStatus = outcomeStatusFor(draft.evaluation.outcomeStatus);
81
+ const outcomeStatus = outcomeStatusFor(draft.evaluation.outcomeStatus, draft.evaluation.placeholder === false);
51
82
  const failureCategory = failureCategoryFor(draft.evaluation.failureCategory);
52
83
  return {
53
84
  schemaVersion: draft.schemaVersion,
@@ -60,7 +91,7 @@ export function learningPacketWireBody(draft, nodeId) {
60
91
  idempotencyKey: `${draft.source.repo}:${draft.source.run}`,
61
92
  contentHash: learningPacketContentHash(draft),
62
93
  ...(nodeId ? { nodeId } : {}),
63
- ...(outcomeStatus ? { outcomeStatus } : {}),
94
+ outcomeStatus,
64
95
  // evaluation fill-in (slice 6): a non-placeholder evaluation carries the runtime's external verifier
65
96
  // ('automated_test' is in the hub VERIFIERS enum); passed/score details ride inside payload.evaluation.
66
97
  ...(draft.evaluation.verifier !== null ? { verifier: draft.evaluation.verifier } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver-adapter-public",
3
- "version": "2.0.2",
3
+ "version": "2.0.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@evomap/atp-sdk": "^0.1.0",
20
- "@evomap/evolver-core": "2.0.2",
20
+ "@evomap/evolver-core": "2.0.12",
21
21
  "undici": "^6.27.0"
22
22
  },
23
23
  "optionalDependencies": {