@evomap/evolver-core 2.0.0-beta.19 → 2.0.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/assets/gep/genes.jsonl +5 -5
  2. package/dist/algo/bans.d.ts +10 -1
  3. package/dist/algo/bans.js +56 -6
  4. package/dist/algo/candidateAssembly.d.ts +25 -2
  5. package/dist/algo/candidateAssembly.js +143 -20
  6. package/dist/algo/capabilityCandidates.js +10 -0
  7. package/dist/algo/cycleEngine.d.ts +40 -5
  8. package/dist/algo/cycleEngine.js +277 -37
  9. package/dist/algo/evolutionEvent.d.ts +26 -0
  10. package/dist/algo/evolutionEvent.js +32 -0
  11. package/dist/algo/exploration.d.ts +7 -0
  12. package/dist/algo/exploration.js +16 -3
  13. package/dist/algo/geneHealth.d.ts +34 -1
  14. package/dist/algo/geneHealth.js +42 -0
  15. package/dist/algo/geneIntake.d.ts +46 -1
  16. package/dist/algo/geneIntake.js +121 -10
  17. package/dist/algo/geneSelection.d.ts +115 -6
  18. package/dist/algo/geneSelection.js +459 -32
  19. package/dist/algo/index.d.ts +4 -1
  20. package/dist/algo/index.js +4 -1
  21. package/dist/algo/kautoProjection.d.ts +41 -0
  22. package/dist/algo/kautoProjection.js +95 -0
  23. package/dist/algo/kautoValidator.d.ts +68 -0
  24. package/dist/algo/kautoValidator.js +256 -0
  25. package/dist/algo/orchestrator.d.ts +14 -1
  26. package/dist/algo/orchestrator.js +9 -2
  27. package/dist/algo/solidify.d.ts +11 -2
  28. package/dist/algo/solidify.js +37 -7
  29. package/dist/algo/ucb1.d.ts +53 -0
  30. package/dist/algo/ucb1.js +156 -0
  31. package/dist/assetrepair/hubRejection.d.ts +12 -0
  32. package/dist/assetrepair/hubRejection.js +109 -0
  33. package/dist/assetrepair/index.d.ts +2 -0
  34. package/dist/assetrepair/index.js +2 -0
  35. package/dist/assetrepair/repair.d.ts +33 -0
  36. package/dist/assetrepair/repair.js +155 -0
  37. package/dist/assetstore/assetSidecarRecords.js +5 -1
  38. package/dist/assetstore/assetStoreHealth.d.ts +2 -3
  39. package/dist/assetstore/assetStoreHealth.js +4 -10
  40. package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
  41. package/dist/assetstore/foreignJsonlSource.js +150 -0
  42. package/dist/assetstore/index.d.ts +3 -0
  43. package/dist/assetstore/index.js +3 -0
  44. package/dist/assetstore/learningHistory.js +3 -3
  45. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  46. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  47. package/dist/assetstore/localJsonl.d.ts +8 -0
  48. package/dist/assetstore/localJsonl.js +128 -3
  49. package/dist/assetstore/provenance.d.ts +38 -3
  50. package/dist/assetstore/provenance.js +103 -45
  51. package/dist/assetstore/provider.d.ts +4 -0
  52. package/dist/assetstore/reviewFilter.d.ts +19 -1
  53. package/dist/assetstore/reviewFilter.js +36 -0
  54. package/dist/assetstore/unionReadStore.d.ts +25 -0
  55. package/dist/assetstore/unionReadStore.js +119 -0
  56. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  57. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  58. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  59. package/dist/benchmark/antiGeneRollout.js +4 -3
  60. package/dist/benchmark/index.d.ts +2 -1
  61. package/dist/benchmark/index.js +2 -1
  62. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  63. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  64. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  65. package/dist/bootstrap/envFingerprint.js +5 -0
  66. package/dist/bootstrap/index.d.ts +2 -1
  67. package/dist/bootstrap/index.js +2 -1
  68. package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
  69. package/dist/bootstrap/lifecycleBootstrap.js +433 -0
  70. package/dist/bootstrap/v1EnvCompat.d.ts +3 -0
  71. package/dist/bootstrap/v1EnvCompat.js +48 -1
  72. package/dist/events/eventArchive.d.ts +2 -0
  73. package/dist/events/eventArchive.js +13 -3
  74. package/dist/events/eventSchema.d.ts +7 -7
  75. package/dist/events/eventStore.d.ts +2 -0
  76. package/dist/events/eventStore.js +5 -1
  77. package/dist/events/ingest.d.ts +2 -1
  78. package/dist/events/ingest.js +6 -0
  79. package/dist/exec/autoExec.d.ts +30 -3
  80. package/dist/exec/autoExec.js +294 -29
  81. package/dist/exec/autonomousCycle.d.ts +7 -0
  82. package/dist/exec/autonomousCycle.js +2 -0
  83. package/dist/exec/claudeBridge.d.ts +28 -6
  84. package/dist/exec/claudeBridge.js +411 -258
  85. package/dist/exec/executionBinding.d.ts +414 -0
  86. package/dist/exec/executionBinding.js +588 -0
  87. package/dist/exec/index.d.ts +1 -0
  88. package/dist/exec/index.js +1 -0
  89. package/dist/exec/proofOfWork.d.ts +1 -1
  90. package/dist/exec/proofOfWork.js +2 -2
  91. package/dist/exec/runnerRegistry.d.ts +5 -0
  92. package/dist/exec/runnerRegistry.js +54 -14
  93. package/dist/hub/capability.d.ts +40 -1
  94. package/dist/hub/conversationDistiller.d.ts +19 -0
  95. package/dist/hub/conversationDistiller.js +115 -37
  96. package/dist/hub/fake.d.ts +1 -0
  97. package/dist/hub/fake.js +1 -0
  98. package/dist/hub/index.d.ts +1 -0
  99. package/dist/hub/index.js +1 -0
  100. package/dist/hub/recipeCompose.d.ts +27 -0
  101. package/dist/hub/recipeCompose.js +90 -0
  102. package/dist/hub/sanitize.js +119 -5
  103. package/dist/index.d.ts +2 -0
  104. package/dist/index.js +2 -0
  105. package/dist/mailbox/ipcServer.d.ts +2 -0
  106. package/dist/mailbox/ipcServer.js +20 -0
  107. package/dist/mailbox/store.d.ts +52 -2
  108. package/dist/mailbox/store.js +546 -81
  109. package/dist/modelCompatibility.d.ts +164 -0
  110. package/dist/modelCompatibility.js +309 -0
  111. package/dist/observers/valueDigestObserver.d.ts +9 -0
  112. package/dist/observers/valueDigestObserver.js +35 -2
  113. package/dist/ops/cleanup.js +1 -1
  114. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  115. package/dist/ops/evolutionGraphProjection.js +315 -0
  116. package/dist/ops/index.d.ts +2 -1
  117. package/dist/ops/index.js +2 -1
  118. package/dist/ops/selfUpdate.d.ts +8 -0
  119. package/dist/ops/selfUpdate.js +24 -8
  120. package/dist/ops/valueOutreach.d.ts +3 -1
  121. package/dist/ops/valueOutreach.js +5 -1
  122. package/dist/personality/schema.d.ts +18 -18
  123. package/dist/schema/evolutionGraph.d.ts +784 -0
  124. package/dist/schema/evolutionGraph.js +187 -0
  125. package/dist/schema/index.d.ts +1 -0
  126. package/dist/schema/index.js +1 -0
  127. package/dist/schema/proofOfWork.d.ts +125 -6
  128. package/dist/schema/proofOfWork.js +102 -4
  129. package/dist/shadow/shadowHub.js +1 -0
  130. package/dist/signals/expand.d.ts +15 -1
  131. package/dist/signals/expand.js +148 -0
  132. package/dist/signals/index.d.ts +3 -1
  133. package/dist/signals/index.js +3 -1
  134. package/dist/signals/scopeVocabulary.d.ts +75 -0
  135. package/dist/signals/scopeVocabulary.js +91 -0
  136. package/dist/signals/taskDomain.d.ts +22 -0
  137. package/dist/signals/taskDomain.js +43 -0
  138. package/dist/trace/learningTrace.d.ts +7 -0
  139. package/dist/trace/learningTrace.js +22 -0
  140. package/dist/trace/trajectory.d.ts +8 -0
  141. package/dist/trace/trajectory.js +14 -2
  142. package/dist/util/fileLock.d.ts +92 -5
  143. package/dist/util/fileLock.js +809 -70
  144. package/dist/util/index.d.ts +2 -1
  145. package/dist/util/index.js +1 -1
  146. package/dist/verify/index.d.ts +2 -1
  147. package/dist/verify/index.js +1 -1
  148. package/dist/verify/sandboxRunner.d.ts +2 -0
  149. package/dist/verify/sandboxRunner.js +154 -10
  150. package/dist/verify/sandboxedValidation.d.ts +7 -2
  151. package/dist/verify/sandboxedValidation.js +102 -24
  152. package/dist/verify/validation.d.ts +24 -3
  153. package/dist/verify/validation.js +141 -24
  154. package/dist/wire/geneHints.d.ts +88 -16
  155. package/dist/wire/geneHints.js +124 -15
  156. package/dist/wire/index.d.ts +8 -3
  157. package/dist/wire/index.js +2 -2
  158. package/dist/wire/schemaGate.d.ts +21 -0
  159. package/dist/wire/schemaGate.js +119 -10
  160. package/package.json +5 -4
@@ -592,6 +592,10 @@ export function spawnCapture(cmd, args, opts) {
592
592
  });
593
593
  });
594
594
  });
595
+ // Defined input is delivered and closed by stdin.end(opts.input) above. With no input, still close the pipe so
596
+ // EOF-driven CLIs cannot hang.
597
+ if (opts.input === undefined)
598
+ child.stdin?.end();
595
599
  });
596
600
  }
597
601
  /** Map the shared process result into the failure taxonomy used by plain-text runners. */
@@ -774,6 +778,45 @@ function geminiMessage(value) {
774
778
  function geminiWarnings(value) {
775
779
  return Array.isArray(value) ? value.map(geminiMessage).filter(Boolean) : [];
776
780
  }
781
+ /** Accept only native-session-safe ids so Learning Ops never joins on garbage envelope fields. */
782
+ function geminiSessionId(value) {
783
+ if (typeof value !== 'string')
784
+ return undefined;
785
+ const sessionId = value.trim();
786
+ return NATIVE_SESSION_ID_PATTERN.test(sessionId) ? sessionId : undefined;
787
+ }
788
+ function withGeminiSessionId(result, envelope) {
789
+ const sessionId = geminiSessionId(envelope.session_id);
790
+ return sessionId !== undefined ? { ...result, sessionId } : result;
791
+ }
792
+ /**
793
+ * Gemini sometimes prints non-JSON diagnostics before the structured envelope.
794
+ * Prefer pure JSON; otherwise accept the last top-level JSON object in stdout.
795
+ */
796
+ function parseGeminiStdout(stdout) {
797
+ const trimmed = stdout.trim();
798
+ if (!trimmed)
799
+ return null;
800
+ try {
801
+ const parsed = JSON.parse(trimmed);
802
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
803
+ return parsed;
804
+ }
805
+ catch {
806
+ // fall through to last-object recovery
807
+ }
808
+ for (let index = trimmed.lastIndexOf('{'); index >= 0; index = trimmed.lastIndexOf('{', index - 1)) {
809
+ try {
810
+ const parsed = JSON.parse(trimmed.slice(index));
811
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
812
+ return parsed;
813
+ }
814
+ catch {
815
+ // keep scanning earlier braces
816
+ }
817
+ }
818
+ return null;
819
+ }
777
820
  /** Interpret one bounded Gemini subprocess result. Structured output and diagnostics require complete capture. */
778
821
  export function classifyGeminiRunnerResult(result, timeoutMs) {
779
822
  if (result.termination === 'timeout') {
@@ -791,14 +834,8 @@ export function classifyGeminiRunnerResult(result, timeoutMs) {
791
834
  exitCode: result.code,
792
835
  };
793
836
  }
794
- let envelope;
795
- try {
796
- const parsed = JSON.parse(result.stdout);
797
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
798
- throw new Error('JSON object required');
799
- envelope = parsed;
800
- }
801
- catch {
837
+ const envelope = parseGeminiStdout(result.stdout);
838
+ if (!envelope) {
802
839
  const error = result.stderr || (result.code === 0 ? 'gemini returned invalid JSON output' : `gemini exited with code ${String(result.code)}`);
803
840
  return { ok: false, output: result.stdout, error, failureKind: result.code === 0 ? 'invalid_output' : 'non_zero_exit', exitCode: result.code };
804
841
  }
@@ -808,19 +845,19 @@ export function classifyGeminiRunnerResult(result, timeoutMs) {
808
845
  ? GEMINI_TERMINATION_WARNINGS.find(({ pattern }) => warnings.some((warning) => pattern.test(warning)))?.error
809
846
  : undefined;
810
847
  if (terminationError) {
811
- return { ok: false, output: geminiMessage(envelope.response), error: terminationError, failureKind: 'runtime_error', exitCode: result.code };
848
+ return withGeminiSessionId({ ok: false, output: geminiMessage(envelope.response), error: terminationError, failureKind: 'runtime_error', exitCode: result.code }, envelope);
812
849
  }
813
850
  const denial = [structuredError, ...warnings, result.stderr].find((message) => GEMINI_PERMISSION_DENIAL_RE.test(message));
814
851
  if (denial) {
815
- return { ok: false, output: geminiMessage(envelope.response), error: denial, failureKind: 'permission_denied', exitCode: result.code };
852
+ return withGeminiSessionId({ ok: false, output: geminiMessage(envelope.response), error: denial, failureKind: 'permission_denied', exitCode: result.code }, envelope);
816
853
  }
817
854
  if (result.code !== 0) {
818
- return { ok: false, output: geminiMessage(envelope.response), error: structuredError || result.stderr || `gemini exited with code ${String(result.code)}`, failureKind: 'non_zero_exit', exitCode: result.code };
855
+ return withGeminiSessionId({ ok: false, output: geminiMessage(envelope.response), error: structuredError || result.stderr || `gemini exited with code ${String(result.code)}`, failureKind: 'non_zero_exit', exitCode: result.code }, envelope);
819
856
  }
820
857
  if (structuredError) {
821
- return { ok: false, output: geminiMessage(envelope.response), error: structuredError, failureKind: 'runtime_error', exitCode: result.code };
858
+ return withGeminiSessionId({ ok: false, output: geminiMessage(envelope.response), error: structuredError, failureKind: 'runtime_error', exitCode: result.code }, envelope);
822
859
  }
823
- return { ok: true, output: geminiMessage(envelope.response), exitCode: result.code };
860
+ return withGeminiSessionId({ ok: true, output: geminiMessage(envelope.response), exitCode: result.code }, envelope);
824
861
  }
825
862
  /** Build verified Gemini CLI argv. The prompt is appended separately as one argv element with shell:false. */
826
863
  export function geminiRunnerArgs(opts = {}) {
@@ -1014,7 +1051,10 @@ export function makeCursorHeadlessRunner(opts = {}, platform = process.platform)
1014
1051
  };
1015
1052
  }
1016
1053
  const RUNNER_SPECS = {
1017
- claude: { name: 'claude', makeRunner: makeClaudeHeadlessRunner, envAllow: { prefixes: ['ANTHROPIC_', 'CLAUDE_'] } },
1054
+ // AWS_ prefix is allowlisted so the claude CLI can authenticate against Amazon Bedrock (CLAUDE_CODE_USE_BEDROCK=1
1055
+ // + AWS_REGION/AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY). This is a legitimate runner-auth channel,
1056
+ // symmetric to CURSOR_/OPENAI_ for the other runners; no other runner inherits it.
1057
+ claude: { name: 'claude', makeRunner: makeClaudeHeadlessRunner, envAllow: { prefixes: ['ANTHROPIC_', 'CLAUDE_', 'AWS_'] } },
1018
1058
  codex: { name: 'codex', makeRunner: makeCodexHeadlessRunner, envAllow: { prefixes: ['OPENAI_', 'CODEX_'] } },
1019
1059
  // cursor keeps only its OWN auth env (CURSOR_); like every runner it never inherits another's vendor key.
1020
1060
  cursor: { name: 'cursor', makeRunner: makeCursorHeadlessRunner, envAllow: { prefixes: ['CURSOR_'] } },
@@ -238,12 +238,27 @@ export interface RecipeExpressRequest {
238
238
  export interface RecipeExpressionReceipt extends RecipeReceipt {
239
239
  organismId?: string;
240
240
  }
241
+ /** Search/list published recipes. Empty `q` is valid; adapters may fall through to list. */
242
+ export interface RecipeSearchRequest {
243
+ q?: string;
244
+ limit?: number;
245
+ cursor?: string;
246
+ sort?: string;
247
+ }
248
+ export interface RecipeSearchReceipt {
249
+ recipes: unknown[];
250
+ nextCursor?: string;
251
+ hasMore?: boolean;
252
+ raw: unknown;
253
+ }
241
254
  /** Optional Hub recipe/DNA capability. Core stays wire-agnostic; adapters own REST shape. */
242
255
  export interface RecipeCapability {
243
256
  create(request: RecipeCreateRequest): Promise<RecipeReceipt>;
244
257
  publish(recipeId: string, options?: RecipePublishOptions): Promise<RecipeReceipt>;
245
258
  get(recipeId: string): Promise<RecipeFetchReceipt>;
246
259
  express(recipeId: string, request?: RecipeExpressRequest): Promise<RecipeExpressionReceipt>;
260
+ search(request?: RecipeSearchRequest): Promise<RecipeSearchReceipt>;
261
+ list(request?: RecipeSearchRequest): Promise<RecipeSearchReceipt>;
247
262
  }
248
263
  export type HubCapabilityName = 'publish' | 'fetch' | 'search' | 'task' | 'mailbox' | 'auth' | 'audit' | 'air_gap' | 'tenant_isolation' | 'marketplace' | 'economy' | 'questions' | 'recipes' | 'agent_directory' | 'learning_assets';
249
264
  /** hub 能力清单(可选). core 据此动态适配 UI/行为, "按需耦合". */
@@ -300,6 +315,30 @@ export interface AuthProvider {
300
315
  /** 撤销凭证(机器丢失/泄露). */
301
316
  revoke(credentialId: string): Promise<void>;
302
317
  }
318
+ /**
319
+ * By-id fetch is content-addressed by default. Integrity-sensitive callers may
320
+ * explicitly opt in to receiving a schema-valid delivery whose declared
321
+ * asset_id matches the request while its current content hash does not.
322
+ */
323
+ export interface FetchAssetByIdOptions {
324
+ allowUnverifiedExactIdentity?: boolean;
325
+ }
326
+ /** Why a by-id fetch produced no asset. `absent` is the ONLY one that means the hub does not have it. */
327
+ export type AssetDeliveryRejection = 'identity_mismatch' | 'revoked' | 'ambiguous' | 'unverified_not_allowed';
328
+ /**
329
+ * The outcome of a by-id fetch. `verified:false` means the delivery is bound to the requested id but its body
330
+ * hashes to something else — usable only under quarantine, never as canonical content.
331
+ */
332
+ export type AssetDeliveryOutcome = {
333
+ status: 'delivered';
334
+ asset: AssetRecord;
335
+ verified: boolean;
336
+ } | {
337
+ status: 'absent';
338
+ } | {
339
+ status: 'rejected';
340
+ reason: AssetDeliveryRejection;
341
+ };
303
342
  export interface HubCapability {
304
343
  /**
305
344
  * 异步发布. 公版 /a2a/publish 收 **bundle**[Gene,Capsule,(EvolutionEvent)](一个 cycle 的产物一起发, +GDI);
@@ -311,7 +350,7 @@ export interface HubCapability {
311
350
  /** 搜索资产. 与 fetch 同 shape, 差异在 hub 侧召回策略. */
312
351
  search(query: HubQuery): Promise<AssetRecord[]>;
313
352
  /** 可选: 按 content-addressed asset_id 拉单个 full asset. private adapter/proxy 用它保证只 fetch 一个 winner. */
314
- fetchAssetById?(assetId: string): Promise<AssetRecord | null>;
353
+ fetchAssetById?(assetId: string, options?: FetchAssetByIdOptions): Promise<AssetRecord | null>;
315
354
  /** 可选: 报告某个资产被复用后的结果. PHub/enterprise adapter 实现; 不支持时 runtime 应明确降级. */
316
355
  recordReuseResult?(report: ReuseResultReport): Promise<ReuseResultReceipt>;
317
356
  /** 可选: 列出 hub 端 runtime learning assets. Missing means the adapter does not support this MVP surface. */
@@ -43,6 +43,7 @@ interface NormalizedExecution {
43
43
  files: number;
44
44
  lines: number;
45
45
  };
46
+ untrustedStatusClaim: boolean;
46
47
  }
47
48
  interface NormalizedConversation {
48
49
  text: string;
@@ -65,6 +66,22 @@ export interface QualityGate {
65
66
  export interface ConversationDistillOptions {
66
67
  persist?: boolean;
67
68
  store?: AssetStoreProvider;
69
+ /**
70
+ * 仅由受信任的宿主验证器注入。HTTP/MCP 请求体中的 execution 不能替代此证据。
71
+ */
72
+ verifiedExecution?: ConversationDistillVerifiedExecution;
73
+ }
74
+ export interface ConversationDistillVerifiedExecution {
75
+ trace: ReadonlyArray<{
76
+ command: string;
77
+ exit: number;
78
+ summary?: string;
79
+ }>;
80
+ validation?: readonly string[];
81
+ blast_radius?: {
82
+ files?: number;
83
+ lines?: number;
84
+ };
68
85
  }
69
86
  export type ConversationDistillResult = {
70
87
  ok: false;
@@ -80,6 +97,8 @@ export type ConversationDistillResult = {
80
97
  signals: string[];
81
98
  gene: AssetRecord;
82
99
  capsule: AssetRecord;
100
+ /** 仅当质量闸门通过且草稿可以进入发布流程时为 true。 */
101
+ publishable: boolean;
83
102
  };
84
103
  export declare function inferSignals(text: string, providedSignals?: unknown): string[];
85
104
  export declare function evaluateGate(input: ConversationDistillInput, normalized: NormalizedConversation): QualityGate;
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { redactString } from './sanitize.js';
3
- import { SCHEMA_VERSION, computeAssetId } from '../wire/index.js';
2
+ import { intakeGene } from '../algo/geneIntake.js';
3
+ import { redactDeep, redactString } from './sanitize.js';
4
+ import { SCHEMA_VERSION, computeAssetId, stripGeneHints, validateWireDeep } from '../wire/index.js';
4
5
  const DEFAULT_SIGNALS = [
5
6
  'conversation_distillation',
6
7
  'reusable_capability',
@@ -16,6 +17,7 @@ const SIGNAL_RULES = [
16
17
  { signal: 'plugin_integration', re: /\b(plugin|codex|claude|cursor|antigravity|workbuddy|hook|notify)\b|插件|钩子/i },
17
18
  { signal: 'test_verified', re: /\b(test|build|verify|passed|green)\b|测试|验证|通过/i },
18
19
  ];
20
+ const BUNDLE_PERSISTENCE_UNSUPPORTED = 'bundle_persistence_unsupported';
19
21
  function asRecord(v) {
20
22
  return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
21
23
  }
@@ -74,37 +76,63 @@ function normalizePublishBlastRadius(value, artifactCount) {
74
76
  };
75
77
  }
76
78
  function capsuleContent(normalized) {
77
- return redactString(JSON.stringify({
79
+ return redactDeep({
78
80
  platform: normalized.platform,
79
81
  source_thread: normalized.source_thread || null,
80
82
  artifacts: normalized.artifacts,
81
83
  excerpt: normalized.text.slice(0, 1200),
82
- }));
84
+ });
83
85
  }
84
86
  function normalizeExecution(input) {
85
87
  const execution = asRecord(input.execution);
86
88
  const validation = normalizeList(input.validation ?? input.verification ?? execution['validation'], 8, 180);
89
+ const untrustedStatusClaim = execution['status'] !== undefined || execution['ok'] !== undefined;
90
+ let malformedTrace = false;
87
91
  const trace = asArray(execution['trace']).map((item) => {
88
- if (typeof item === 'string')
89
- return { command: trimText(item, 180), exit: 0 };
92
+ if (typeof item === 'string') {
93
+ malformedTrace = true;
94
+ return null;
95
+ }
90
96
  const row = asRecord(item);
91
- if (Object.keys(row).length === 0)
97
+ if (Object.keys(row).length === 0) {
98
+ malformedTrace = true;
99
+ return null;
100
+ }
101
+ const command = trimText(row['command'] ?? row['cmd'] ?? row['name'] ?? '', 180);
102
+ if (!command) {
103
+ malformedTrace = true;
92
104
  return null;
105
+ }
106
+ const rawExit = row['exit'];
107
+ const exit = Number.isInteger(rawExit)
108
+ ? Number(rawExit)
109
+ : (typeof row['ok'] === 'boolean' ? (row['ok'] ? 0 : 1) : undefined);
110
+ if (exit === undefined) {
111
+ malformedTrace = true;
112
+ return null;
113
+ }
93
114
  return {
94
- command: trimText(row['command'] ?? row['cmd'] ?? row['name'] ?? 'validation', 180),
95
- exit: Number.isInteger(row['exit']) ? Number(row['exit']) : (row['ok'] === false ? 1 : 0),
115
+ command,
116
+ exit,
96
117
  summary: trimText(row['summary'] ?? row['output'] ?? '', 240),
97
118
  };
98
119
  }).filter((row) => row !== null);
99
- for (const cmd of validation)
100
- if (!trace.some((t) => t.command === cmd))
101
- trace.push({ command: cmd, exit: 0 });
102
- const ok = execution['status'] === 'success' || execution['ok'] === true || validation.length > 0 || trace.some((t) => t.exit === 0);
120
+ // 声明的验证命令只是计划,并不能证明命令已经执行。
121
+ // 保留它作为质量信号,但不能为未执行的声明伪造 exit-0 轨迹或成功结果。
122
+ // 只有显式结构化的退出码才可作为证据。调用方声明的 status/ok 字段只是元数据,
123
+ // 不能据此把资产晋级到发布流程。
124
+ const normalizedValidation = new Set(validation);
125
+ const covered = [...normalizedValidation].every((command) => trace.some((row) => row.command === command));
126
+ const ok = trace.length > 0
127
+ && !malformedTrace
128
+ && trace.every((t) => t.exit === 0)
129
+ && covered;
103
130
  return {
104
131
  status: ok ? 'success' : 'failed',
105
132
  trace,
106
133
  validation,
107
134
  blast_radius: normalizeBlastRadius(execution['blast_radius'] ?? input.blast_radius),
135
+ untrustedStatusClaim,
108
136
  };
109
137
  }
110
138
  function buildStrategy(input) {
@@ -145,7 +173,9 @@ export function evaluateGate(input, normalized) {
145
173
  reasons.push('explicit_distill_signal');
146
174
  }
147
175
  const rawThreshold = Number(input.min_score ?? input.minScore ?? 5);
148
- const threshold = Number.isFinite(rawThreshold) ? rawThreshold : 5;
176
+ // 调用方可以要求更严格的阈值,但不能把准入策略降为零,
177
+ // 也不能把未经确认的草稿变成可发布资产。
178
+ const threshold = Number.isFinite(rawThreshold) ? Math.max(5, rawThreshold) : 5;
149
179
  if (score < threshold)
150
180
  return { ok: false, score, threshold, reasons, reason: 'insufficient_reusable_signal' };
151
181
  return { ok: true, score, threshold, reasons };
@@ -178,12 +208,17 @@ export function normalizeConversationInput(input) {
178
208
  export async function distillConversation(input, opts = {}) {
179
209
  if (!input || typeof input !== 'object' || Array.isArray(input))
180
210
  return { ok: false, status: 'skipped', reason: 'input_object_required' };
181
- const normalized = normalizeConversationInput(input);
211
+ const effectiveInput = opts.verifiedExecution
212
+ ? {
213
+ ...input,
214
+ execution: opts.verifiedExecution,
215
+ ...(opts.verifiedExecution.validation ? { validation: opts.verifiedExecution.validation } : {}),
216
+ }
217
+ : input;
218
+ const normalized = normalizeConversationInput(effectiveInput);
182
219
  if (!normalized.summary || normalized.summary.length < 20)
183
220
  return { ok: false, status: 'skipped', reason: 'summary_required' };
184
221
  const gate = evaluateGate(input, normalized);
185
- if (!gate.ok)
186
- return { ok: false, status: 'skipped', reason: gate.reason ?? 'insufficient_reusable_signal', quality: gate, signals: normalized.signals };
187
222
  const slug = slugify(input.name ?? input.title ?? normalized.summary);
188
223
  const fingerprint = hashInput({
189
224
  summary: normalized.summary,
@@ -192,9 +227,13 @@ export async function distillConversation(input, opts = {}) {
192
227
  artifacts: normalized.artifacts,
193
228
  });
194
229
  const category = inferCategory(normalized.signals, normalized.text);
195
- const gene = {
196
- type: 'Gene',
197
- schema_version: SCHEMA_VERSION,
230
+ // 只有质量门禁与受信任宿主证据同时通过时,Capsule 才允许广播。
231
+ const publishable = gate.ok && normalized.execution.status === 'success' && opts.verifiedExecution !== undefined;
232
+ const declaredValidation = normalized.execution.validation;
233
+ // GEP Gene schema 要求非空 validation。调用方没有计划时保留旧版 Node 身份检查,
234
+ // 但它只用于形成可审阅草稿,绝不能产生 verifier 坐标或发布资格。
235
+ const validation = declaredValidation.length > 0 ? declaredValidation : ['node --version'];
236
+ const intake = intakeGene({
198
237
  id: `gene_conversation_${slug}_${fingerprint}`,
199
238
  summary: normalized.summary,
200
239
  category,
@@ -204,18 +243,28 @@ export async function distillConversation(input, opts = {}) {
204
243
  'The conversation includes enough evidence to reconstruct when and how to use it.',
205
244
  ],
206
245
  strategy: normalized.strategy,
207
- validation: normalized.execution.validation.length > 0 ? normalized.execution.validation : ['node --version'],
246
+ validation,
208
247
  constraints: { max_files: 20, forbidden_paths: ['.git', 'node_modules', '.env'] },
209
- _source: {
210
- kind: 'conversation_distillation',
211
- platform: normalized.platform,
212
- source_thread: normalized.source_thread || null,
248
+ generation_meta: { source: 'distilled' },
249
+ model_name: normalized.model,
250
+ });
251
+ if (!intake.ok || !intake.gene) {
252
+ return {
253
+ ok: false,
254
+ status: 'skipped',
255
+ reason: 'gene_intake_failed',
213
256
  quality: gate,
214
- distilled_at: new Date().toISOString(),
215
- },
216
- asset_id: '',
217
- };
218
- gene.asset_id = computeAssetId(gene) ?? '';
257
+ signals: normalized.signals,
258
+ };
259
+ }
260
+ const gene = { ...intake.gene };
261
+ // intakeGene 会从任何非空 validation 计划推导 verifier_profile。旧版身份检查并非
262
+ // 行为验证证据,因此必须移除该坐标并重新绑定内容寻址 ID,保持严格 K_auto fail-closed。
263
+ if (declaredValidation.length === 0) {
264
+ delete gene.verifier_profile;
265
+ gene.asset_id = '';
266
+ gene.asset_id = computeAssetId(gene) ?? '';
267
+ }
219
268
  const capsule = {
220
269
  type: 'Capsule',
221
270
  schema_version: SCHEMA_VERSION,
@@ -226,13 +275,13 @@ export async function distillConversation(input, opts = {}) {
226
275
  confidence: Math.min(0.95, 0.5 + gate.score / 20),
227
276
  blast_radius: normalizePublishBlastRadius(normalized.execution.blast_radius, normalized.artifacts.length),
228
277
  outcome: { status: normalized.execution.status, score: normalized.execution.status === 'success' ? 0.82 : 0.35 },
229
- source_type: 'conversation_distillation',
278
+ source_type: 'generated',
230
279
  strategy: normalized.strategy,
231
280
  execution_trace: normalized.execution.trace,
232
- a2a: { eligible_to_broadcast: true },
281
+ a2a: { eligible_to_broadcast: publishable },
233
282
  content: capsuleContent(normalized),
234
- diff: '',
235
- reused_asset_id: '',
283
+ diff: null,
284
+ reused_asset_id: null,
236
285
  env_fingerprint: {
237
286
  platform: normalized.platform,
238
287
  source_thread: normalized.source_thread || null,
@@ -247,14 +296,43 @@ export async function distillConversation(input, opts = {}) {
247
296
  asset_id: '',
248
297
  };
249
298
  capsule.asset_id = computeAssetId(capsule) ?? '';
250
- const persist = (opts.persist ?? input.persist) === true;
299
+ // 质量准入可以保持宽松,但草稿仍必须是有效的 wire 记录。
300
+ // 在持久化或排队前捕获生产者与 schema 的漂移。
301
+ if (!validateWireDeep(stripGeneHints(gene)).ok || !validateWireDeep(capsule).ok) {
302
+ return {
303
+ ok: false,
304
+ status: 'skipped',
305
+ reason: 'wire_schema_invalid',
306
+ quality: gate,
307
+ signals: normalized.signals,
308
+ };
309
+ }
310
+ // 只有受信任宿主验证器提供的成功 execution 才能解锁发布。
311
+ // 本地调用方显式要求 persist 时,可以保留没有成功证据的失败草稿供人工复核,
312
+ // 但请求体伪造的 exit-0 轨迹不能借此写入看似成功的资产。
313
+ const persistRequested = (opts.persist ?? input.persist) === true;
314
+ const failedDraftPersistence = !opts.verifiedExecution
315
+ && normalized.execution.status === 'failed'
316
+ && !normalized.execution.untrustedStatusClaim;
317
+ const persist = persistRequested && (publishable || (gate.ok && failedDraftPersistence));
251
318
  if (persist && opts.store) {
252
- await opts.store.put(gene);
253
- await opts.store.put(capsule);
319
+ // Gene/Capsule 是一个可发布单元。只提供单条写入的 provider 无法恢复
320
+ // 两次写入之间的崩溃窗口;不要用兼容回退制造孤儿 Gene。
321
+ if (typeof opts.store.putBundle !== 'function') {
322
+ return {
323
+ ok: false,
324
+ status: 'skipped',
325
+ reason: BUNDLE_PERSISTENCE_UNSUPPORTED,
326
+ quality: gate,
327
+ signals: normalized.signals,
328
+ };
329
+ }
330
+ await opts.store.putBundle([gene, capsule]);
254
331
  }
255
332
  return {
256
333
  ok: true,
257
334
  status: persist && opts.store ? 'stored' : 'draft',
335
+ publishable,
258
336
  distill_id: fingerprint,
259
337
  quality: gate,
260
338
  signals: normalized.signals,
@@ -24,6 +24,7 @@ export declare class FakeHubCapability implements HubCapability {
24
24
  }>;
25
25
  readonly auth: AuthProvider;
26
26
  agentDirectory?: import('./agentDirectory.js').AgentDirectoryCapability;
27
+ recipes?: import('./capability.js').RecipeCapability;
27
28
  nextPollAfterMs: number | undefined;
28
29
  constructor(opts?: FakeHubOptions);
29
30
  /** 测试注入: 排一条 inbound 事件供 poll 拉. */
package/dist/hub/fake.js CHANGED
@@ -20,6 +20,7 @@ export class FakeHubCapability {
20
20
  completed = [];
21
21
  auth = fakeAuth;
22
22
  agentDirectory;
23
+ recipes;
23
24
  nextPollAfterMs;
24
25
  constructor(opts = {}) {
25
26
  this.opts = opts;
@@ -8,4 +8,5 @@ export * from './assetCallLog.js';
8
8
  export * from './hubReview.js';
9
9
  export * from './reuseDecision.js';
10
10
  export * from './conversationDistiller.js';
11
+ export * from './recipeCompose.js';
11
12
  export * from './questionGenerator.js';
package/dist/hub/index.js CHANGED
@@ -8,4 +8,5 @@ export * from './assetCallLog.js';
8
8
  export * from './hubReview.js';
9
9
  export * from './reuseDecision.js';
10
10
  export * from './conversationDistiller.js';
11
+ export * from './recipeCompose.js';
11
12
  export * from './questionGenerator.js';
@@ -0,0 +1,27 @@
1
+ import type { AssetRecord, HubCapability } from './capability.js';
2
+ export declare const RECIPE_COMPOSE_FLAG = "compose_recipe";
3
+ export interface RecipeComposeInput {
4
+ title?: string;
5
+ description?: string;
6
+ assets?: readonly unknown[];
7
+ source?: unknown;
8
+ [k: string]: unknown;
9
+ }
10
+ export interface RecipeComposeResult {
11
+ attempted: boolean;
12
+ ok: boolean;
13
+ reason?: string;
14
+ recipeId?: string;
15
+ status?: string;
16
+ }
17
+ export declare function recipeComposeRequested(payload: RecipeComposeInput): boolean;
18
+ export declare function recipeAssetsFromPayload(payload: RecipeComposeInput): {
19
+ gene?: AssetRecord;
20
+ capsule?: AssetRecord;
21
+ };
22
+ /**
23
+ * After a Gene/Capsule bundle is accepted by Hub, compose a Recipe as the
24
+ * preferred public artifact. Skill Store publish is not the default outbound.
25
+ * Missing recipe capability or missing Gene is a no-op, never a publish failure.
26
+ */
27
+ export declare function composeRecipeAfterAssetPublish(hub: Pick<HubCapability, 'recipes'>, payload: RecipeComposeInput): Promise<RecipeComposeResult>;
@@ -0,0 +1,90 @@
1
+ export const RECIPE_COMPOSE_FLAG = 'compose_recipe';
2
+ function asRecord(value) {
3
+ return value && typeof value === 'object' && !Array.isArray(value)
4
+ ? value
5
+ : null;
6
+ }
7
+ function assetType(asset) {
8
+ const typed = asset['type'] ?? asset['assetType'];
9
+ return typeof typed === 'string' ? typed : '';
10
+ }
11
+ function assetId(asset) {
12
+ const id = asset['asset_id'] ?? asset['assetId'];
13
+ return typeof id === 'string' ? id : '';
14
+ }
15
+ function assetSummary(asset) {
16
+ const summary = asset['summary'] ?? asset['shortTitle'] ?? asset['nlSummary'];
17
+ return typeof summary === 'string' ? summary : '';
18
+ }
19
+ export function recipeComposeRequested(payload) {
20
+ if (payload[RECIPE_COMPOSE_FLAG] === false)
21
+ return false;
22
+ if (payload['publish_recipe'] === false)
23
+ return false;
24
+ return true;
25
+ }
26
+ function shouldCompose(payload) {
27
+ return recipeComposeRequested(payload);
28
+ }
29
+ export function recipeAssetsFromPayload(payload) {
30
+ const assets = Array.isArray(payload.assets) ? payload.assets : [];
31
+ const records = assets.filter((item) => !!asRecord(item));
32
+ return {
33
+ gene: records.find((asset) => assetType(asset) === 'Gene'),
34
+ capsule: records.find((asset) => assetType(asset) === 'Capsule'),
35
+ };
36
+ }
37
+ /**
38
+ * After a Gene/Capsule bundle is accepted by Hub, compose a Recipe as the
39
+ * preferred public artifact. Skill Store publish is not the default outbound.
40
+ * Missing recipe capability or missing Gene is a no-op, never a publish failure.
41
+ */
42
+ export async function composeRecipeAfterAssetPublish(hub, payload) {
43
+ if (!shouldCompose(payload))
44
+ return { attempted: false, ok: true, reason: 'not_requested' };
45
+ const recipes = hub.recipes;
46
+ if (!recipes)
47
+ return { attempted: false, ok: true, reason: 'recipes_unsupported' };
48
+ const { gene, capsule } = recipeAssetsFromPayload(payload);
49
+ if (!gene)
50
+ return { attempted: false, ok: true, reason: 'gene_missing' };
51
+ const geneId = assetId(gene);
52
+ if (!geneId)
53
+ return { attempted: false, ok: true, reason: 'gene_missing' };
54
+ const title = String(payload.title || assetSummary(gene) || 'Reusable recipe').trim().slice(0, 200);
55
+ if (title.length < 3)
56
+ return { attempted: false, ok: true, reason: 'title_too_short' };
57
+ const description = String(payload.description || assetSummary(gene) || title).trim().slice(0, 2000);
58
+ const steps = [
59
+ { assetId: geneId, assetType: 'Gene', position: 0 },
60
+ ...(capsule && assetId(capsule)
61
+ ? [{ assetId: assetId(capsule), assetType: 'Capsule', position: 1 }]
62
+ : []),
63
+ ];
64
+ try {
65
+ const created = await recipes.create({
66
+ title,
67
+ description,
68
+ steps,
69
+ pricePerExecution: 5,
70
+ currency: 'Credit',
71
+ });
72
+ const recipeId = created.recipeId;
73
+ if (!recipeId)
74
+ return { attempted: true, ok: false, reason: 'recipe_id_missing' };
75
+ const published = await recipes.publish(recipeId);
76
+ return {
77
+ attempted: true,
78
+ ok: true,
79
+ recipeId,
80
+ status: published.status ?? created.status ?? 'published',
81
+ };
82
+ }
83
+ catch (error) {
84
+ return {
85
+ attempted: true,
86
+ ok: false,
87
+ reason: error instanceof Error ? error.message : 'recipe_compose_failed',
88
+ };
89
+ }
90
+ }